006 credential broker laptop push#35
Open
pofallon wants to merge 9 commits into
Open
Conversation
pofallon
commented
May 31, 2026
Contributor
- docs(spec): add 006 credential broker (laptop-push model)
- docs(spec): lock in Phase 0 decisions — pyrage + TOFU
- docs(spec): pivot 006 from laptop-push to sidecar-devcontainer-push
- Add broker-managed vault sidecar
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>
Supersedes 005, which was implemented in PR #32 and closed without merge once end-to-end testing on a real Proxmox container surfaced the architectural mismatch: the bootstrap-token-on-instance design carries a residual on-disk credential that contradicts the supply-chain threat model the broker was built to defend against. The new design: laptop encrypts a project-scoped secret bundle (age, X25519 + ChaCha20-Poly1305) and pushes to the instance over SSH; broker decrypts in memory via systemd-credentials (TPM2 → host-key → plaintext-mode-0600 fallback ladder); devcontainers fetch via the existing per-project Unix socket protocol with manifest allowlist enforcement. - spec.md: requirements, threat model, removed-from-005 list, architecture diagram, cross-cutting decisions - plan.md: phased implementation (5 phases, ~3 weeks), what stays (~60%) / what goes (~30%) / what's new (~10%), open questions Cross-repo: paired with remo-broker spec 002 (in flight via separate PR) which supersedes that repo's 001-broker-daemon spec. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Q8 (age library): pyrage (Python binding around the Rust age crate).
No user-side age install required; in-process; typed errors; testable.
- Q10 (first-contact trust model): TOFU. Pin silently on first contact
(during remo create, SSH layer is already trusted); warn loudly on
subsequent changes; provide remo {provider} repin <instance> to
acknowledge legitimate rebuilds. Matches SSH host-key UX. Optional
--strict-pin flag deferred until demand.
Q11 (per-project vs single global encrypted blob) remains open as a
Phase 2 implementation decision.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Second pivot in two days. The first 006 redesign (2026-05-30, laptop
pushes age-encrypted blob over SSH) was cleaner than 005 but still
keyed on the laptop being the source of truth, which doesn't match
remo's actual deployment (single-instance, not fleet). Moving the
source-of-truth into a dedicated sidecar devcontainer on the same LXC
eliminates the entire encryption-in-transit + pubkey-trust apparatus
while improving the multi-device-access story.
Architecture: each remo instance gets a dedicated _remo-vault sidecar
devcontainer alongside project devcontainers. The sidecar owns the
OAuth flows (gh / aws / claude login etc.) and holds the encrypted-
at-rest fnox storage. An inotify watcher in the sidecar pushes
plaintext to the broker over a local Unix socket whenever fnox state
changes. Broker becomes purely in-memory (no on-disk blob, no
LoadCredentialEncrypted). Project devcontainers get secrets via env
var injection (default) or tmpfs file materialization (opt-in via
manifest's new fetch_as directive). The manifest at .remo/manifest.toml
is bind-mounted read-only into project devcontainers — a malicious
dep cannot rewrite its own allowlist.
What disappears compared to the first 006 draft:
- remo push-creds CLI command (push is local now, not from laptop)
- remo {provider} repin (no pubkey to pin)
- core/encrypted_blob.py, core/instance_keys.py
- core/broker_admin.py NDJSON-over-SSH transport
- age / pyrage dependency
- /var/lib/remo-broker/secrets.enc on-disk blob
- TOFU pubkey trust model decision
What's new:
- Ansible roles: vault_devcontainer_install, vault_decryption_key_setup
- Sidecar devcontainer image (debian-slim + gh/aws/claude/fnox + helpers)
- remo-vault-watcher (inotify daemon in sidecar)
- Helper scripts: remo-list-creds, remo-test-project, remo-vend-status,
remo-reload
- Devcontainer feature: remo/secrets-feature (project-side entrypoint
helper that reads manifest, fetches from broker, injects env/file)
- Manifest schema extension: fetch_as = "env" | "file" per secret
- Read-only bind-mount of manifest into project devcontainers
Laptop CLI: unchanged. No new commands. All credential management
happens via remo shell -> _remo-vault picker entry.
Estimate down from ~3 weeks to ~2 weeks of focused work.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.