Skip to content

Add the amikad sandbox daemon and the no-relay WebSocket SSH path - #316

Merged
dbmikus merged 14 commits into
dylan/amika-cli-skillfrom
dylan/ssh-impl-no-relay
Aug 9, 2026
Merged

Add the amikad sandbox daemon and the no-relay WebSocket SSH path#316
dbmikus merged 14 commits into
dylan/amika-cli-skillfrom
dylan/ssh-impl-no-relay

Conversation

@dbmikus

@dbmikus dbmikus commented Aug 4, 2026

Copy link
Copy Markdown
Contributor

Adds the sandbox-side daemon and the client-side sshv2 path for SSH that does
not traverse the provider's SSH gateway: OpenSSH runs loopback-only inside the
sandbox, and amika reaches it over an authenticated WebSocket bridge served by
amikad on its reserved port (60999).

Everything is off by default. amikad serve refuses to start without
--beta-no-relay, and the paired control-plane stack in amika-mono owns the
default-off NO_RELAY_SSH_ENABLED gate and the
POST /sandboxes/{id}/ssh-sessions API this CLI calls.

Sandbox side: the amikad binary

New go/cmd/amikad + go/internal/amikad. The command tree is built around an
injectable Operations boundary 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 managed sshd_config (loopback
    ListenAddress, AuthenticationMethods publickey, AllowUsers amika, no
    agent/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, never
    private material.
  • amikad authorized-keys set — validates each line with the OpenSSH
    parser, 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 base64url
    32-byte token.
  • amikad serve --beta-no-relay [--bg] — runs the HTTP bridge and
    supervises the loopback sshd child, exiting if either stops. --bg re-execs
    detached and polls until healthy, which is what lifecycle provisioning calls.
    Serves /healthz and /v1/status alongside the bridge route.

The bridge (internal/amikad/norelay) upgrades GET /v1/ssh-sessions to a
binary, compression-disabled WebSocket and splices it to loopback sshd. It
authenticates the Authorization: Bearer token against the on-disk token in
constant 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 goes
through. 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 sshv2

  • amika secret ssh-keygen [--import] [--name] — creates (or imports and
    verifies) a user-owned Ed25519 identity at ~/.ssh/amika_id_ed25519, uploads
    only the public key, and writes the managed SSH config block.
  • amika sandbox sshv2 <name> [-- cmd] — resolves the sandbox, builds the
    <name>.<id>.amika alias, checks the identity file's permissions, pins the
    host key, then execs system ssh against the alias.
  • amika plumbing ssh-stdio-proxy <host> — the hidden ProxyCommand. Per
    dial it mints a fresh session descriptor from the API, validates every field
    (transport, sandbox id, wss URL shape, canonical token, Ed25519 host key),
    dials with the credential in the Authorization header rather than in argv or
    the URL, and copies opaque bytes between OpenSSH stdio and the WebSocket.
  • Host keys are pinned per alias in a dedicated ~/.ssh/amika_known_hosts under
    an advisory lock; a changed key fails closed rather than prompting. The
    rendered Host *.amika block sets StrictHostKeyChecking yes and
    IdentitiesOnly yes, and is added alongside the existing provider-native host
    entries rather than replacing them.

Packaging

  • make build-amikad, install.sh --component amikad with its own default
    version, and an AmikadVersion in buildmetaamikad is versioned and
    released separately from amika.
  • The base image builds amikad from a pinned reviewed commit in a discarded Go
    builder stage, so sandbox images depend on neither an unreleased binary nor a
    retained toolchain. It also installs openssh-server, creates
    /home/amika/.ssh mode 0700, and unlocks the amika account with a discarded
    random 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.
  • Adds github.com/coder/websocket and golang.org/x/crypto.

Validation

  • go test -race ./..., go vet ./...
  • Docker build of the pinned amikad builder stage
  • Real Daytona provider-edge WebSocket-to-OpenSSH vertical slice

Stack

  1. dylan/amika-cli-skill Add amika-cli skill documenting the CLI for agents #315
  2. dylan/ssh-impl-no-relay #THIS ← you are here
  3. dylan/no-ssh-keygen-yet-fix Fix the managed sshd port, add amika scpv2, and scope SSH sessions per control plane #321
  4. dylan/ssh-websocket-code-review Resolve SSH relay review annotations and document the connect-token round trip #322

dbmikus and others added 14 commits August 2, 2026 23:01
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.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment thread install.sh

# 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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment thread install.sh
if importPath == "" {
publicKey, err = ssh.GenerateIdentity(identityPath)
} else {
identityPath, publicKey, err = ssh.ImportIdentity(importPath)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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 👍 / 👎.

@dbmikus dbmikus changed the title Implement no-relay WebSocket SSH daemon and CLI Add the amikad sandbox daemon and the no-relay WebSocket SSH path Aug 9, 2026
@dbmikus
dbmikus merged commit 35551a5 into main Aug 9, 2026
@dbmikus
dbmikus deleted the dylan/ssh-impl-no-relay branch August 9, 2026 17:14
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