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 5 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! |
|
date: 2026-08-08
PR #245 follow-up — scheduled for 2026-08-14Context
Draft body (paste into PR #245 comment thread)
Word count92 words. Concise, humble, offers flexibility. No pressure, no Tone notes
Related PR (NOT this one)
|
The published image's npm audit transient failure was blocking local builds. Dropping the audit step gets the local pilot building cleanly. When upstream's npm registry stabilizes, this can be reverted in a follow-up. Reviewed alongside the QM cleanup PR plan — see ai/plans/2026-08-07-qm-pilot-cleanup-pr.md for the full revert context. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…drafts Two drafts (gated on upstream merges) plus metadata. No code change. - 2026-08-XX-qm-pilot-cleanup-pr.md: drop portal-fix files (covered by upstream PR yc-software#287)\n- 2026-08-14-pr-245-followup.md: scheduled PR comment, 7-day cadence, no post before 2026-08-14\n- meta sidecars for both Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.