feat(cli): add --local-only flag for docker-target sandbox publish - #245
feat(cli): add --local-only flag for docker-target sandbox publish#245RollingStonie wants to merge 3 commits into
Conversation
When --target docker, the operator can now run `qm sandbox publish --local-only` which loads the layer into the local Docker daemon via `docker buildx --load` instead of pushing to Fly's Sprites registry. The image digest is resolved from the daemon and recorded into qm.config.jsonc sandbox.image — satisfying the config.ts:198-201 sandboxPinPending gate without requiring FLY_SANDBOX_API_TOKEN. Restores spec-true local-Docker deployments per ADR-2026-08-05-01 (Phase 4 yc-software#1 §1, Phase 4 yc-software#4) which call for 'no third-party services'. The flag is rejected at the CLI layer for --target fly and --target aws since both backends still require a registry push. Scaffolded by MiniMax-M3 on behalf of Kenneth's A008 QM pilot install. Verified with the unit suite (all green) and a live run against ~/qm-pilot-deploy which recorded sha256:ba726b4ccd9f… into the config and produced a usable local image. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
qm 0.1.4 config validator previously required sandbox.backend ∈ {sprites, aws}.
This adds "local" as a valid value for docker target, where the agent-computer
layer image (already loaded via --local-only publish) is used to spawn
agents via the same Docker host as the qm stack.
- cli/src/config.ts: extend SandboxBackend type to include "local"; update
error message + target docker requirement
- cli/src/backends/registry.ts: make docker.requiresSandboxApp a function
that returns false for backend === "local" (previously hardcoded true)
- cli/src/commands/check.ts: dispatch the new function form in
bootChecks
Out of scope for this PR: the actual local-spawn runtime inside the
ghcr.io/yc-software/qm/core image. Tested with beltish.com pilot:
core/admin/auth start cleanly with backend=local, portal fails on
private-network allowlist for 'auth' service name (image-level issue).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Follow-up commit |
…rkUrl
The portal's OIDC endpoint validation rejects OIDC URLs that point at the
built-in auth broker when the broker host is a docker-compose service
name (e.g. `http://auth:8080/token`) because the existing
isPrivateNetworkUrl check only matches IP literals and known TLD
suffixes (.internal/.flycast/.local), not single-label hostnames.
This is the fix that lets qm up complete with the built-in auth broker
under docker target — the OIDC endpoints derived from the docker
service name (`auth:8080`) are now recognized as private-network and
the portal boots.
The new check matches single-label hostnames of the form
/^[a-z][a-z0-9_-]{0,62}$/ — public DNS hostnames always have at
least one dot (TLD), so a single-label hostname on a docker network
is safe to trust. The .localhost / .internal / .local / .flycast TLD
suffixes are still treated as private (their names take precedence).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Follow-up commit |
|
Hi @yc-software/maintainers — friendly ping on this PR. It's been open since 2026-08-06 with the three commits ( We've been running this on a local fork at https://github.com/RollingStonie/qm-qm-pilot (qm-pilot namespace) to unblock an internal pilot. It works for us with the patches, but the cleanest path is upstream merge + a portal image rebuild so other operators don't have to carry the local fork. Specifically what's needed from yc-software side:
If there's a review timeline or a maintainer I should be pinging directly, happy to coordinate. Thanks! |
Upstream PR Scaffold —
qm sandbox publish --local-onlyRepository:
yc-software/qm(upstream)Branch:
feat/sandbox-local-only-mode(locally created at/Users/kenneth/AG_Mission/qm)Base:
main@866764e(CONTRIBUTING: clarify feature PRs vs bug issues)Diff:
0001-feat-sandbox-local-only-mode.patch(alongside this file)Authoring session: 2026-08-06, MiniMax-M3 (Claude) — review by Kenneth required before push
PR Title
PR Description
Problem
qm sandbox publishrequires a Fly Sprites registry token (FLY_SANDBOX_API_TOKEN) and an authenticated Fly organization even when the deployment target is--target docker. This violates the contract of the docker-only deployment model (see ADR-2026-08-05-01 and Phase 4 spec #1 §1 / Phase 4 spec #4): "local Docker on Claire, no third-party services."The CLI exposes no
--local-only/--no-publish/--skip-registryflag —cli/src/commands/sandbox.ts:328-350callsauthenticateFlyRegistry()unconditionally (gated only by!opts.dryRun), and the buildx call at line 471 hard-codes--push. So even a docker target ends up requiring a Fly token.This is the QM pilot's actual install blocker, not just an inconvenience —
qm doctorcannot advance past the sandbox-image-pinning gate without first running a publish, and publish cannot run without Fly credentials.Proposed Change
A single new flag:
qm sandbox publish --local-only. When set:authenticateFlyRegistry()— noFLY_SANDBOX_API_TOKENrequired, nofly auth dockersubprocess.docker buildx build --loadinstead of--push— the image is loaded into the local Docker daemon with the sameregistry.fly.io/<app>tag the deployment contract expects.docker image inspect --format '{{json .RepoDigests}}' <ref>returns the sha256 the daemon stored; that digest is recorded intoqm.config.jsoncassandbox.imageexactly as the Fly-push path does.--local-onlywith--target flyor--target awsis rejected at the CLI layer because both backends require a registry push. Keeps the surface area honest.The result is a real
sandbox.image = "registry.fly.io/<app>@sha256:<sha>"pin satisfyingconfig.ts:198-201'ssandboxPinPendinggate, produced without any third-party service contact.Files Changed
cli/src/commands/sandbox.tslocalOnlytoSandboxBuildOpts; gateauthenticateFlyRegistry; swap--pushfor--loadwhen local-only; resolve digest from local daemon; adddigestFromLocalImagehelper; preserve original arg order in the non-local-only path so existing test assertions don't driftcli/src/cli.ts--local-onlyto sandboxrejectUnknownFlagsallowlist; validatetarget === "docker"when set; pipelocalOnlythrough the common opts object; update help textcli/test/sandbox-local-only.test.ts--load, fake fly never invoked, no token in.env, asserts the local digest is recorded intosandbox.imageVerification (in this scaffold session)
npx tsc -p tsconfig.json --noEmit→ cleannode --test test/*.test.ts(unit suite) → pass (exit 0, all tests green)cli/test/sandbox-local-only.test.ts→ pass in isolation/Users/kenneth/qm-pilot-deploy:qm check→✓ check passed — config, sandbox layer, and plugins are valid.qm doctorno longer reports the sandbox-image gate as a blocker (downstream gates now stop at the 11 missing A008/Infisical secrets, which are out of scope here).Spec Compatibility
This restores compatibility with the spec-true local-Docker deployment model. No spec amendment is required. The ADR-2026-08-05-01 ("local Docker on Claire, no third-party services") and Phase 4 #1 §1 / Phase 4 #4 stand as written.
Why Not a Fork?
The kenneth-fleet rule (
kenneth-fleetskill, "Current platform decision" §) is explicit: "Maintain a private organization deployment repo/layer with anupstreamremote, keep Kenneth-specific skills/config/connectors outside core, and carry the smallest possible patch queue. Fork core only when an upstream contribution cannot satisfy a proven requirement."This change is small, behavior-preserving for non-local-only callers (all existing tests pass unchanged), and clearly belongs upstream. The temporary patch queue (see "Temporary patch queue" below) is a stopgap, not the destination.
Alternatives Considered
--no-publishinstead of--local-only: rejected — "no-publish" implies the contract is still satisfied (the pin gets recorded anyway, just without building), which would be misleading.--local-onlydescribes what actually happens.--registry none/--registry local): deferred to a follow-up — the docker backend only needs the load+inspect path for now; adding a registry abstraction is a bigger surface than this PR wants.cli/src/backends/docker.tsonly: rejected —cli/src/commands/sandbox.tsowns the auth+push flow today, so the docker backend isn't the right file to edit. The CLI layer + sandbox command module are.Temporary Patch Queue (kenneth-fleet rule)
Until this lands upstream, the A008 operator install at
~/qm-pilot-deploycarries a private patch queue againstnode_modules/@yc-software/qm/dist/src/{cli,commands/sandbox}.js. The patch is generated bynpm run buildfrom this branch and copied into the installed copy. When upstream releases a 0.1.5+ with this change, the patch queue is dropped.Tracking:
/Users/kenneth/AG_Mission/qmbranchfeat/sandbox-local-only-mode~/qm-pilot-deploy/node_modules/@yc-software/qm/dist/src/{cli,commands/sandbox}.js0001-feat-sandbox-local-only-mode.patch(alongside)Review Checklist for Kenneth
0001-feat-sandbox-local-only-mode.patch--local-only) reads well in the help text--local-onlyrequires--target docker) is what you want — vs allowing it as a no-op on fly/aws with a warningdigestFromLocalImage) and its error messages match the house stylesandbox-publish.test.tsdigestFromLocalImageextraction in tests)Out-of-Scope Reminder
This PR does NOT touch:
AUTH_CLIENT_SECRET,CORE_SIGNING_SECRET, etc.) — those are issued by the A008 backing service, not by qm.OPENROUTER_API_KEY,AUTH_ALLOWED_EMAILS,AUTH_EMAIL_FROM,SMTP_*) — provisioned separately by the operator.qm doctorafter this PR lands — that's the next gate, not this one.Session-Learned Note (durable memory candidate)
Need help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.