Add the amikad sandbox daemon and the no-relay WebSocket SSH path - #316
Conversation
Add the production amikad state, OpenSSH, token, and bounded WebSocket bridge implementations. Wire strict host pinning, per-dial session creation, the hidden stdio proxy, and the beta sshv2/key commands into the CLI.
Add daemon release metadata and installer support, bake the SSH\ndependencies into the base image, and support health-checked detached\nstartup for lifecycle provisioning.
Generate host keys in memory so temporary paths never enter the scrub\nmanifest, verify imported keypairs match, validate API host keys with the\nOpenSSH parser, and include session duration in bridge logs.
Compile the daemon from an immutable reviewed commit in a discarded\nGo builder stage so sandbox images do not depend on an unreleased binary\nor retain the build toolchain.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4777f5c6b8
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
|
|
||
| # Build from an immutable reviewed commit until the first standalone amikad | ||
| # release exists. The final image receives only the binary, not the toolchain. | ||
| ARG AMIKAD_SOURCE_REF=3fde13c92ff8c3e43a393138459945f7b641cb09 |
There was a problem hiding this comment.
Pin the image build to a durable source revision
When this change is squash-merged and its source branch is later removed, the base image can no longer reliably resolve this revision: 3fde13c... is not an ancestor of the reviewed commit 3440974... or of main, and is currently reachable only through the auxiliary work branch. Since every preset build runs go install ...@${AMIKAD_SOURCE_REF}, garbage collection or removal of that branch will make fresh base-image builds fail; publish/tag the daemon source or pin a revision that remains reachable after merge.
Useful? React with 👍 / 👎.
| if importPath == "" { | ||
| publicKey, err = ssh.GenerateIdentity(identityPath) | ||
| } else { | ||
| identityPath, publicKey, err = ssh.ImportIdentity(importPath) |
There was a problem hiding this comment.
Resolve imported identities to an absolute path
When --import is given a relative path such as id_ed25519.pub, ImportIdentity returns the matching relative private-key path, but the subsequent ConfigureSession call rejects it because RenderSessionConfig requires filepath.IsAbs. The documented import mode therefore fails for an otherwise valid keypair in the current directory; convert the imported path to an absolute path before persisting the session configuration.
Useful? React with 👍 / 👎.
amikad sandbox daemon and the no-relay WebSocket SSH path
Adds the sandbox-side daemon and the client-side
sshv2path for SSH that doesnot traverse the provider's SSH gateway: OpenSSH runs loopback-only inside the
sandbox, and
amikareaches it over an authenticated WebSocket bridge served byamikadon its reserved port (60999).Everything is off by default.
amikad serverefuses to start without--beta-no-relay, and the paired control-plane stack inamika-monoowns thedefault-off
NO_RELAY_SSH_ENABLEDgate and thePOST /sandboxes/{id}/ssh-sessionsAPI this CLI calls.Sandbox side: the
amikadbinaryNew
go/cmd/amikad+go/internal/amikad. The command tree is built around aninjectable
Operationsboundary whose default implementation is fail-closed(
ErrNotImplemented), so an unwired subcommand cannot read input, write files,or open a listener.
amikad setup sshd— writes the managedsshd_config(loopbackListenAddress,AuthenticationMethods publickey,AllowUsers amika, noagent/X11/tunnel forwarding, no PAM) and generates an Ed25519 host key only
when one is absent. Replacing existing user-defined SSH state requires
--force-overwrite.amikad host-key show— prints the canonical public host key, neverprivate material.
amikad authorized-keys set— validates each line with the OpenSSHparser, rejects option-bearing lines and non-allowlisted key types, dedupes,
and atomically replaces the whole set.
amikad connect-token set— accepts only a canonical unpadded base64url32-byte token.
amikad serve --beta-no-relay [--bg]— runs the HTTP bridge andsupervises the loopback sshd child, exiting if either stops.
--bgre-execsdetached and polls until healthy, which is what lifecycle provisioning calls.
Serves
/healthzand/v1/statusalongside the bridge route.The bridge (
internal/amikad/norelay) upgradesGET /v1/ssh-sessionsto abinary, compression-disabled WebSocket and splices it to loopback sshd. It
authenticates the
Authorization: Bearertoken against the on-disk token inconstant time, re-reading the file on every upgrade so rotation applies without
restarting the daemon, and refusing when that file is missing, symlinked,
wrong-sized, or not mode 0600. Capacity is bounded by a slot channel (429 on
overflow), open streams are tracked so shutdown cancels in-flight I/O, and each
session logs open/close as structured JSON with a session id, byte counts,
duration, and close reason — never token or key material.
Sensitive state (
internal/amikad/state) is the writer every secret goesthrough. It registers a path in the scrub manifest before writing the file (a
stale manifest entry is safe; an unregistered secret is not), writes atomically
under a cross-process file lock, refuses non-absolute, unclean, or symlinked
paths, bounds file size, and can assign ownership through the temporary file
descriptor before the rename. Host keys are generated in memory, so no
temporary path ever holds private material outside the manifest.
Client side:
amika sandbox sshv2amika secret ssh-keygen [--import] [--name]— creates (or imports andverifies) a user-owned Ed25519 identity at
~/.ssh/amika_id_ed25519, uploadsonly the public key, and writes the managed SSH config block.
amika sandbox sshv2 <name> [-- cmd]— resolves the sandbox, builds the<name>.<id>.amikaalias, checks the identity file's permissions, pins thehost key, then
execs systemsshagainst the alias.amika plumbing ssh-stdio-proxy <host>— the hiddenProxyCommand. Perdial it mints a fresh session descriptor from the API, validates every field
(transport, sandbox id,
wssURL shape, canonical token, Ed25519 host key),dials with the credential in the
Authorizationheader rather than in argv orthe URL, and copies opaque bytes between OpenSSH stdio and the WebSocket.
~/.ssh/amika_known_hostsunderan advisory lock; a changed key fails closed rather than prompting. The
rendered
Host *.amikablock setsStrictHostKeyChecking yesandIdentitiesOnly yes, and is added alongside the existing provider-native hostentries rather than replacing them.
Packaging
make build-amikad,install.sh --component amikadwith its own defaultversion, and an
AmikadVersioninbuildmeta—amikadis versioned andreleased separately from
amika.amikadfrom a pinned reviewed commit in a discarded Gobuilder stage, so sandbox images depend on neither an unreleased binary nor a
retained toolchain. It also installs
openssh-server, creates/home/amika/.sshmode 0700, and unlocks theamikaaccount with a discardedrandom password hash — OpenSSH refuses every login, including public-key auth,
for a shadow-locked account, and the managed config disables password and
keyboard-interactive auth outright.
github.com/coder/websocketandgolang.org/x/crypto.Validation
go test -race ./...,go vet ./...amikadbuilder stageStack
dylan/amika-cli-skillAdd amika-cli skill documenting the CLI for agents #315dylan/ssh-impl-no-relay#THIS← you are heredylan/no-ssh-keygen-yet-fixFix the managed sshd port, addamika scpv2, and scope SSH sessions per control plane #321dylan/ssh-websocket-code-reviewResolve SSH relay review annotations and document the connect-token round trip #322