Commit Graph
5 Commits
Author SHA1 Message Date
mroberts ace4e81f97 Pass a custom template and mixin kits through to sbx
Sandboxes ignore the host ~/.claude by design: the agent runs as a separate
user with HOME elsewhere, so even a read-only mount is not picked up. Skills
can be shared with sbx skills import, but plugins carry commands, hooks and
MCP servers that only a custom image can deliver.

Adds --template, --stock-template and a repeatable --kit, persisted per
repository so run and refresh reuse them. AI_SBX_TEMPLATE supplies the default
image, so one custom template can be declared once in the user's mise config
and apply to every repository, with --template overriding it per repository
and --stock-template opting out.

save_config now packs two arrays into one argument list separated by a count,
so it ships with a round-trip test covering empty arrays, values containing
spaces, and the boundary between kits and AWS profiles.
2026-07-30 16:29:25 -05:00
mroberts 890d10a315 Replace GitHub App tokens with a pre-filled token form
The App approach does not survive contact with a hundred developers and
hundreds of repositories. Minting installation tokens requires the App private
key on every developer's machine, and a key that widely distributed is a key
that grants org-wide minting to everyone holding it.

Device flow looked like the way out, since it needs no private key, but
testing showed it does not scope. A token requested with repository_id for one
repository reached a second repository in the same installation: a
permission-gated endpoint returned 200 where an installation token scoped to
one repository returned 403 for the same public repository. GitHub accepts
repository_id and silently ignores it. Per-repo scoping therefore requires
either the private key or the client secret, and neither can live on a
developer's machine.

Fine-grained PATs do scope per repository and share no secret, and GitHub
supports pre-filling the creation form via URL parameters, which removes the
toil that made them unattractive. Setup now builds that URL from the origin
remote and opens it, leaving the operator to select the repository and paste
the result.

Three permissions - checks, vulnerability_alerts and secret_scanning_alerts -
are absent from GitHub's pre-fill parameters, so they are printed as a
checklist instead of sent as parameters that would be silently dropped and
look granted. There is no parameter for repository selection either.

Tokens are no longer re-minted per launch, since a PAT outlives a session; the
new token subcommand replaces one on expiry or revocation.
2026-07-30 15:28:44 -05:00
mroberts d96f32d773 Resolve the repository from the invoking directory
A task included from the global mise config runs with the config root as its
working directory - $HOME - rather than the directory the user invoked it from.
Deriving the repository from the current directory therefore failed everywhere
except a project-level include, which defeats the point of installing the task
once and using it in every repository.

mise passes the real directory as MISE_ORIGINAL_CWD, so enter it before
resolving the repository, falling back to the current directory when the task
is run directly rather than through mise.
2026-07-30 14:35:40 -05:00
mroberts b03fcd6dd7 Mint GitHub App installation tokens instead of per-repo PATs
GitHub exposes no API to create a fine-grained PAT and no way to prefill the
creation form, so every repository meant hand-clicking a permission set and
remembering to rotate it. Installation tokens are API-mintable, so configuring
one GitHub App removes the per-repository work entirely.

A new 'app' subcommand records the App ID and private key path once. Setup then
resolves the installation for the repository, and run and refresh mint a fresh
token scoped to that single repository before every launch. Tokens expire in an
hour on their own, which retires manual rotation.

sbx secret set is invoked with --force because without it a second write prompts
for confirmation, reads the prompt from the stdin already consumed by the token,
cancels, and still exits 0 - leaving the previous, expired token in place.

The permission set is validated against GitHub's app-permissions schema. Notably
workflows has no read level, and write is required to push any commit touching
.github/workflows, which is a separate permission from actions.

Also corrects several sbx invocations that did not match the installed CLI:
--no-share-skills and --clone are not create flags, isolation is --branch; run
takes a sandbox name rather than --name; exec takes no -- separator; ls --quiet
replaces parsing tabular output; and the sandbox home is queried rather than
assumed to be /home/agent.

Adds a JWT test that verifies signatures against a generated public key and
confirms tampered input fails to verify.
2026-07-30 14:25:37 -05:00
mroberts d43abe693e Add repository-scoped AI sandbox mise task
Provides a shareable mise task, ai:sbx, that runs an AI coding agent in a
Docker Sandbox scoped to a single GitHub repository and a set of read-only
AWS roles.

The repository is derived from origin rather than configured, so the sandbox
identity cannot drift from the checkout in use. GitHub access is a
repository-scoped fine-grained PAT held in the sbx secret store and injected
by its host-side proxy, so the token is never exposed to the agent. The host
~/.aws directory and SSO token cache are never mounted; instead the host
exports short-lived credentials for approved read-only profiles and only
those land in the sandbox.

Host profiles are commonly suffixed to mark the grant (api-portal-readonly)
while Terraform references the account name (api-portal), so a trailing
-readonly is stripped when the profile is written into the sandbox. Two host
profiles that collapse to the same sandbox name are rejected during setup,
before any credentials are exported, since a silent overwrite would hand
Terraform the wrong identity under a plausible-looking name.

All state lives under ~/.config/ai-sbx; repositories supply nothing and need
no mise.toml.
2026-07-30 13:52:57 -05:00