Skip to content

chore: enforce 7-day supply-chain cooldown on dependencies#39

Open
pofallon wants to merge 5 commits into
mainfrom
chore/supply-chain-cooldown
Open

chore: enforce 7-day supply-chain cooldown on dependencies#39
pofallon wants to merge 5 commits into
mainfrom
chore/supply-chain-cooldown

Conversation

@pofallon

Copy link
Copy Markdown
Contributor

Supply-chain cooldown (7-day minimum release age)

Part of an org-wide rollout hardening get2knowio repos against supply-chain attacks. A newly published dependency version must age 7 days before it can be installed/resolved or proposed by Dependabot — giving time for a malicious release to be detected and yanked. Security updates are never delayed (Dependabot cooldown applies to version updates only).

Config schema verified against current official docs (not training data).

Changes

  • uv (pyproject.toml)exclude-newer = "7 days" (repo is uv-managed via uv.lock).
  • Dependabotcooldown: { default-days: 7 } on the existing pip and github-actions entries, plus a new docker entry (/ and /notifier Dockerfiles).

Notes: the existing Dependabot pip ecosystem value is left as-is (minimal diff). Docker base images have no install-time cooldown — covered by the Dependabot layer only.

pofallon and others added 5 commits May 25, 2026 14:07
Add a comprehensive feature specification for the Credential Broker (docs/remo-fnox-spec.md) describing design, user stories, requirements, components (remo-broker, fnox), lifecycle, and open questions for branch 005-credential-broker. Also update .gitignore to ignore macOS .DS_Store files.
Makes the sibling repos available inside the devcontainer for cross-repo
work (Remo site updates, credential-broker integration).

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ssions (#30)

* feat(shell): project-launch passthrough — -p, --exec, --detach

Adds three composable flags on `remo shell` so you can skip the interactive
project picker, run a command inside the project's devcontainer, and/or
fire-and-forget detached. Motivating use case is launching Claude Code with
/remote-control from one command and walking away to the phone:

    remo shell -p my-app --detach --exec \
      'claude remote-control --name "remo-$REMO_INSTANCE-$REMO_PROJECT"'

The same shape works for any tool — pytest in the devcontainer, a long
build, opencode, etc. `remo shell` and `remo shell <name>` are unchanged.

Client (cli/shell.py, core/ssh.py):
* Three new flags: -p/--project, --exec COMMAND, --detach.
* Validation: --detach requires --exec; --exec requires -p.
* When any of the new flags are active, shell_connect appends
  `project-launch --project X [--detach] [-- args...]` as the SSH remote
  command and forces -t. shlex-quotes both the project name and each
  command arg so spaces / metacharacters round-trip cleanly.
* `--exec` is a single quoted string (parsed locally with shlex.split,
  re-quoted arg-by-arg). Click's `--` separator doesn't disambiguate
  positional arguments from a trailing nargs=-1, so the single-string
  shape sidesteps an ambiguity with the optional `name` positional.

Server (ansible/roles/user_setup):
* New ~/.local/bin/project-launch helper, deployed alongside project-menu
  and devshell. Four cases — with/without .devcontainer × foreground vs.
  detach — wired explicitly. Detached commands run via nohup+setsid and
  capture stdout/stderr to ~/.local/state/remo/<project>.log.
* Both modes export REMO_INSTANCE and REMO_PROJECT so users can build
  deterministic Claude RC session names without remo owning the convention.
* .bashrc DEVCONTAINER AUTO-START block honors REMO_DEVCONTAINER_CMD: if
  set, that command is execed inside the devcontainer instead of the
  default bash/zsh drop-in. This is how project-launch threads --exec
  through the existing zellij+devcontainer chain.

Tests:
* 12 new unit tests (test_shell.py) covering flag parsing, validation
  errors, legacy compatibility, and the shlex-quoted remote command shape
  (project-only, with --exec, with --detach, embedded spaces, special
  chars, empty --exec collapsing back to project-only).
* Full suite: 555 passed, 0 regressions.
* Smoke test (incus job) exercises the no-devcontainer paths against a
  real container: foreground --exec, --detach with log-file verification,
  and both validation errors. Devcontainer paths are exercised organically
  once users put a real project in ~/projects.

Docs:
* README "Jump Straight to a Project" section with the Claude RC headline
  example, plus quick references in the CLI table.

Existing instances need `remo <provider> update` to pick up the new
project-launch helper. `remo shell`'s existing version-mismatch prompt
covers this.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(shell): reject -L combined with --detach

Port forwarding is meaningless when the SSH session exits immediately —
the tunnel dies before the user can use it. Surface that as an exit-2
error mirroring the --detach-without--exec and --exec-without--p checks
rather than silently forwarding to a tunnel that's about to be torn down.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(006-project-launch): avoid \${#var[@]} in Jinja template

Smoke test caught a real bug in project-launch.sh.j2: bash's array-length
syntax \${#CMD[@]} contains the literal '{#' sequence that Jinja2 reads
as the comment-opener, so the parser consumes the rest of the file looking
for '#}' and fails with 'Missing end of comment tag'. The template never
made it to disk on a real host — the Install task itself failed.

Replace \${#CMD[@]} checks with a HAS_CMD boolean flag set when '--' is
parsed (the only path that populates CMD). Same semantics, no '{#' in the
source. Verified the rendered script is still bash-syntactically valid.

Add tests/unit/test_ansible_templates.py — parametrized test that
Jinja2-parses every .j2 in ansible/. Catches this class of bug pre-CI; a
local pytest run on the buggy template surfaces the exact line and message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(006-project-launch): invoke project-launch via ~/.local/bin path

SSH non-interactive remote commands (the form ssh <host> "<cmd>") do not
source .bashrc on the remote, so ~/.local/bin isn't on PATH and bare
"project-launch ..." fails with "command not found" — surfaced by the
smoke test on incus.

Use ~/.local/bin/project-launch as the absolute path in the remote command
string. The remote login shell expands ~ to $HOME, so this works without
hardcoding the user or requiring shell startup files.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(006-project-launch): run --exec command via bash -lc (shell semantics)

The previous design shlex.split the --exec value on the client and ran
"${CMD[@]}" execve-style on the server. That broke user expectations:
variable references (\$REMO_PROJECT) stayed literal, and shell operators
(&&, |, redirects) were passed as plain arguments to whatever the first
word resolved to. Smoke test caught it — `echo \$REMO_PROJECT && pwd`
printed the string `hello-\$REMO_PROJECT-\$REMO_INSTANCE && pwd` instead
of expanding anything.

Protocol change: client forwards --exec as ONE shell-quoted argument
(`project-launch --exec '<cmd-string>'` instead of
`project-launch -- <splitted args>`). project-launch passes that string
to `bash -lc`. Login shell flag (-l) also gets PATH from .bashrc/.profile
inside both the host shell and the devcontainer shell — handy because
that's where ~/.local/bin tools like `claude` live.

DEVCONTAINER bashrc block: when REMO_DEVCONTAINER_CMD is set use bash
-lc; the default-shell drop-in path still uses bare -c since it execs
zsh/bash directly without re-parsing.

Unit tests updated: new builder shape, plus a test asserting shell
operators / vars survive verbatim in the outgoing remote command (they
get interpreted by `bash -lc` on the remote, not by the local builder).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Paul O'Fallon <505519+pofallon@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…spec 007) (#36)

* feat(notifier): add Python notifier service with Telegram transport (spec 007)

Add a self-contained notifier sidecar to remo-cli: a FastAPI daemon that
receives agentsh approval requests, delivers them to a human via a Telegram
bot (long-polling), and returns the decision synchronously — failing secure
(deny) on timeout, shutdown, send failure, or capacity exhaustion. No
persistent state.

- src/remo_cli/notifier/: server, fail-secure resolver, in-memory
  PendingApprovals registry (atomic cap + dup-id + send-after-reserve),
  strict Pydantic config + TOML loader, NotificationTransport ABC, Telegram
  transport, structlog with secret redaction, `remo-notifier serve` + SIGHUP.
- ansible/roles/remo_notifier/ + notifier_deploy.yml: hardened systemd-managed
  docker run, preflight asserts, health-wait; community.docker added;
  configure-flow toggle + group_vars.
- src/remo_cli/cli/notifier.py: remo notifier deploy/status/logs/test/restart
  with fuzzy host picking (additive; no existing command changes).
- notifier/Dockerfile: multi-stage, non-root, read-only (155 MB image).
- [notifier] optional extra so the laptop install is unaffected.
- docs: wire-protocol, config-schema, READMEs.
- tests/notifier/: 66 tests, 94% coverage; ruff + mypy clean.

Implements specs/007-notifier-sidecar (spec, plan, research, data-model,
contracts, tasks). 48/50 tasks done; 2 deferred (live-host acceptance steps).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(notifier): add standing grants — "Always" auto-approval (spec 007 addendum 001)

Add a third Telegram choice — Always — that auto-approves a class of operation
without a round-trip, enforced by the notifier as the agentsh `api`-mode
approver. Delegated-approver model: agentsh asks the notifier; the notifier may
answer from a human's prior standing instruction. Fail-secure preserved —
nothing is auto-allowed except by an active, unexpired, unrevoked, human-created
grant whose deterministic predicate the request satisfies.

- notifier/grants.py: Grant/predicate/scope models, deterministic fail-closed
  matcher, in-memory GrantStore (lock, cap, TTL sweep, pause), tightest-first
  candidate proposer (<=4).
- server.py: intake short-circuit (responder rule:{id}, grant_id, no notify, no
  slot), structural audit log (no secrets), sweep + digest lifespan tasks,
  server-owned digest counter.
- transports/telegram.py: [Approve | Always… | Deny], scope/granularity picker
  (pick:{id}:{index}), grant creation, /rules /revoke /pause /resume, send_digest.
- config.py + role template/defaults: [grants] block (enabled, default_ttl 8h,
  max_grants, allow_global_scope, digest_interval).
- In-memory only (restart clears -> fail-closed re-prompt); single 8h TTL, no
  indefinite grants; narrow default scope; Telegram-only listing.

Design: specs/007-notifier-sidecar/addendum-001-standing-grants.md (+ plan,
research, data-model, grant-schema contract, tasks). 27/27 tasks; SC-G1..G5
verified. Tests: +grants suite, 91% notifier coverage; ruff + mypy clean; full
repo suite green (671). Image rebuilds and serves health with [grants].

Known v1 limitation: the wire Operation has no file read/write/delete verb, so
file grants match by path only (operations field reserved, not enforced).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(notifier): make notifier opt-in in the configure flow (default-off)

The remo_notifier role was included in configure_dev_tools.yml with
`configure_remo_notifier | default(true)`, so every provision ran the role's
Telegram-credential preflight assert — failing container creation in CI (and for
any user) without REMO_NOTIFIER_TELEGRAM_* set. Smoke Tests failed at
"Create container" across incus/aws/hetzner for exactly this reason.

Default the toggle to false: the notifier is opt-in, stood up via the explicit
`remo notifier deploy <host>` (unaffected — it applies notifier_deploy.yml
directly). Updates FR-033 + T031 to document the corrected default-off behavior.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Paul O'Fallon <505519+pofallon@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Hold newly published dependency versions for 7 days before they can be
installed or proposed, so a malicious release has time to be detected and
yanked. Applied at the package-manager and/or Dependabot layer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@pofallon
pofallon force-pushed the main branch 2 times, most recently from 740d05f to e58a154 Compare July 11, 2026 02:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant