Skip to content

feat(cloud-sandbox): Phase 20.15 M1 Cloud Sandbox Plane foundation - #2

Open
paopaonyapi-creator wants to merge 8 commits into
fix/gitignore-anchored-export-pathfrom
feat/cloud-sandbox-plane
Open

paopaonyapi-creator wants to merge 8 commits into
fix/gitignore-anchored-export-pathfrom
feat/cloud-sandbox-plane

Conversation

@paopaonyapi-creator

Copy link
Copy Markdown
Owner

Summary

  • Adds Phase 20.15 milestone M1, the foundation of the Cloud Sandbox Plane: a provider-neutral
    CloudEmulatorAdapter SPI, a sidecar SQLite store with a PRAGMA user_version migration, an
    env-var feature-flag layer where every switch defaults off, a brokered DockerControlPort with a
    deny-everything default implementation, a capability/service-fidelity registry, the error
    taxonomy with its retry verdicts, and an in-memory adapter that is the only implementation today.
  • No behavior change for existing users. Nothing is routed yet, no flag is on by default, and the
    four new capabilities added to src/agent-os/policy.ts (cloud.sandbox, cloud.iac.local,
    cloud.staging.apply, cloud.production.apply) are denied by default_deny until someone writes
    a policy row — asserted by tests, not by intent.
  • Two corrections to the phase spec are load-bearing. The ledger is SQLite, not PostgreSQL,
    because this repository has no Postgres client at all; and the Docker port stays null-backed
    because Lambda and RDS are Docker-backed services and the development host has no reachable
    daemon. A service with no daemon reports fidelity UNAVAILABLE rather than passing quietly,
    which is what keeps a local run from being mistaken for production proof.
  • The plan reuses what already exists instead of duplicating it: capability policy and the
    human permit flow (agent-os/policy.ts, agent-os/gateway.ts) gate infrastructure actions,
    generation/cloud/lifecycle-manager.ts shapes the sandbox lifecycle, and the optional-subsystem
    boundary follows the AGENTS.md rule that an inactive subsystem must not be imported at all.
  • Isolation testing surfaced two real defects in the host-protection rules, both fixed here:
    the forbidden bind list was POSIX-only, so a container spec could mount the Windows user profile
    (C:\Users\...) while matching no rule, and bind parsing split C:\Users\me:/workspace on the
    first colon, yielding the source "C". Both are covered by regression tests.
  • Records the phase spec and the vendored upstream spec under docs/, and files the lossy
    debt-ledger round trip this work exposed as upstream #5650.

Verification

Ran on the committed tree (346c224ce), Windows 11 build 26200, Bun 1.4.2:

bun run typecheck        # bun x tsc --noEmit (strict)  -> clean
bun run privacy:scan     # clean
bun test tests/cloud-sandbox-{flags,docker-guard,capabilities,mock-adapter,db-store,core-boundary}.test.ts \
         tests/agent-os-policy.test.ts
                         # 110 pass / 0 fail / 390 expect() calls  [7.93s]
bun test tests/agent-os-{executor,gateway,policy,routes}.test.ts \
         tests/change-control-controller.test.ts tests/repo-hygiene.test.ts
                         # 51 pass / 0 fail / 150 expect() calls   [8.50s]
  • The boundary guard was driven red and then reverted, so it is not vacuous: importing the
    subsystem from src/server/management-api.ts fails both the import-graph walk and the direct-name
    scan. src/server/management-api.ts is byte-identical to dev afterwards.
  • The graph walk initially took 8.8s and hit the default 5s test budget, i.e. the guard would have
    reported itself broken. A per-file specifier cache brings it to 446ms.
  • The debt-ledger mitigation was proven by simulation rather than assumed: replaying recordDebt()
    against a copy of the ledger loses 0 lines and parses 6 entries with no empty fields.

Not run, stated plainly: the full bun run test suite. bun run test:changed is not usable as a
gate for this branch — it resolves its comparison ref to origin/dev, whose merge base with HEAD is
28c69b03a, so it selects tests for 236 unrelated commits and exceeded its own 900s budget.
Coverage for the touched subsystem is therefore the focused sets above. Repository CI
(typecheck + full suite on Linux, Windows and macOS) is the remaining signal, which is why this
opens as a draft.

No UI is added or changed in this milestone, so no screenshot applies.

Checklist

  • Scope stays focused and avoids unrelated cleanup.
  • Docs or release notes were updated when needed.
  • Security-sensitive changes were reviewed for secrets, auth, and unsafe defaults.

Note for review: src/agent-os/policy.ts is a security-boundary file under MAINTAINERS.md, so
this still needs the explicit security review that file requires. What was checked here: the new
capabilities are additive and fail closed with no policy row, the two out-of-local capabilities are
approval-required, no credential value is ever logged or persisted (only digests and redacted
summaries, via the existing recordWebMcpCall path), and privacy:scan is green.

Deliberately deferred to later milestones, and recorded in docs/Phase-20.15-*.md §12: the real
Floci adapter (M2), the Terraform/OpenTofu runner (M3), any Docker-backed service (M7, needs a
daemon), and all promotion beyond local, which stays flag-off.

@github-actions github-actions Bot added the enhancement New feature or request label Sep 23, 2026
@github-actions

Copy link
Copy Markdown

✅ Deterministic PR hygiene checks passed.

@paopaonyapi-creator
paopaonyapi-creator marked this pull request as ready for review September 23, 2026 05:37
@paopaonyapi-creator

Copy link
Copy Markdown
Owner Author

Don't read this PR's red CI as M1 breakage — the baseline is red independently of these 21 files,
and I have now identified three separate causes behind it.

Proof it is not this branch: the failed-job set of dev at e7be56876 (run 34407531289, failing
since 2026-09-09) diffed against this PR's failed-job set is empty. Whatever is red here was already
red on the base commit, with no cloud-sandbox code in existence.

Causes, filed separately from this feature PR so neither blocks the other:

Consequence for review: bun run test cannot be used as the gate signal on this repository until at
least #5 is fixed, which is why the Verification section of this PR lists the focused sets it actually
ran (110 pass / 0 fail on the seven files covering this subsystem, plus 51 pass across the
policy.ts consumers and repo-hygiene, typecheck and privacy:scan clean) rather than claiming a
full-suite pass.

@paopaonyapi-creator

Copy link
Copy Markdown
Owner Author

Amendment to Verification. The bun run typecheck -> clean line above was true when it was run
and is no longer true at the head of this branch, for a reason that is itself the demonstration
of #3: src/agent-os/video/export/export-package-builder.ts existed on this working tree only as an
ignored, untracked file. While checking out fix/gitignore-anchored-export-path (where it is
committed) and returning here (where it is ignored again), git removed it from the working tree —
correctly, as tracked content of the other commit — and tsc, which globs the filesystem rather
than the index, then reproduced the CI failure locally:

src/agent-os/video/index.ts(23,15): error TS2307: Cannot find module './export/export-package-builder'
src/agent-os/video/mcp-tools.ts(8,43): error TS2307: Cannot find module './export/export-package-builder'
src/server/management/video-routes.ts(12,3): error TS2305: ... has no exported member 'VideoExportPackageBuilder'

Zero of those errors originate in src/agent-os/cloud-sandbox/ (typecheck | grep -c cloud-sandbox
= 0 on both 346c224ce and 448169905), and they disappear once #4 merges. The earlier claim
should have said "no new typecheck errors" rather than "clean" — this branch's typecheck was only
passing because of an untracked file that CI never had.

Also adds the M2 lifecycle slice:

tests/cloud-sandbox-manager.test.ts   21 pass / 0 fail
full subsystem set (9 files)          143 pass / 0 fail
privacy:scan                          clean

One honest loose end: the first combined run of those 9 files reported a single 5s test timeout and
took 17.6s; the two runs after it took 2.64s with 0 fail, and I did not capture which test timed
out. I am not writing it off, just recording that it did not reproduce.

AD PAO added 4 commits September 23, 2026 16:57
…ndation

Lay the provider-neutral contracts, sidecar persistence, policy capabilities and
optional-subsystem guard before any emulator is reachable. Reuse rather than rewrite
where the repo already has it: agent-os/policy.ts and gateway.ts drive approval, and
generation/cloud/lifecycle-manager.ts shapes the sandbox lifecycle.

Two corrections to the source spec are load-bearing. The schema is SQLite, not
PostgreSQL, because this repository has no Postgres; and the Docker port stays
null-backed because Lambda and RDS are Docker-backed and this host has no daemon, so
reporting UNAVAILABLE is honest where faking a pass would violate the fidelity gate.

Isolation testing surfaced two real defects in the host-protection rules: the forbidden
bind list was POSIX-only, so a container could mount the Windows profile, and bind
parsing split `C:\Users\me:/x` on the first colon, yielding a source that matched no
rule. The boundary guard was driven red by importing the subsystem from
management-api.ts and reverted, and the lossy debt-ledger round trip it exposed is
filed upstream as lidge-jun#5650.
…olicy gate

Add SandboxManager: the governed lifecycle the source spec asks for in sections 8, 11,
12, 46 and 60. Every create runs flag check, then policy, then service screening, then
the idempotency fence, and only then an adapter call, so anything that can refuse without
touching a runtime refuses while there is still nothing to tear down. Extending a TTL is
bounded by the same ceiling as creating one, because an uncapped extension is the
easiest way for an agent to pin a sandbox open forever.

Two decisions that only look like preferences:

- authorize is injected and DENIES when absent. Defaulting to allow would make a missed
  wiring the one bug that silently disables every gate below it.
- A Docker-backed service is admitted and reported UNAVAILABLE, while a service whose
  capability requires approval is refused. The first keeps the fidelity marker honest per
  section 42; the second stops an agent obtaining a gated service by bundling it with an
  ungated one.

The lifecycle tests caught a real ordering fault, not a test fault: cloud_operations
carries a foreign key to cloud_sandboxes, so writing the audit row before the sandbox row
made every create die on SQLITE_CONSTRAINT_FOREIGNKEY. A provisioning row is now recorded
first, which also leaves a visible sandbox behind if the process dies mid-create.

Also records the upstream distribution reality in docs section 10.1: Floci is a Java
(JAX-RS/Vert.x) CLI plus an image, published on neither npm nor PyPI, and its Docker Hub
tags are only latest and dated nightlies with no stable semver -- so the pinning rule in
section 66 has to be satisfied by digest, and the daemon-presence assumption in section
10 is corrected from "absent here" to "never assume either way".
…ied launch plan

Add FlociAwsAdapter and the launch-plan layer that configures it, against the upstream
configuration reference rather than LocalStack habits. The decisive fact is that FLOCI_PORT
exists, which is what makes one emulator instance per sandbox possible and therefore makes
the isolation guarantee in section 11.2 real instead of aspirational.

Pin by digest, not tag: the published channel is only latest and dated nightlies with no
stable semver, so the section 66 rule can only be satisfied immutably. Record that, the Java
and Vert.x runtime, the absence of npm and PyPI packages, and the per-service image
variables, in docs section 10.2 -- those variables are also the answer to the section 74
question about where the image allowlist lives.

Two refusals that matter more than the features:

- listResources throws RESOURCE_DISCOVERY_FAILED instead of returning an empty list. An
  empty list would tell the observatory, the diff engine and the promotion gate that the
  sandbox holds nothing, and a "no leaks" verdict built on that is worse than no verdict.
  Inventory comes from Terraform state in M3, and upstream documents no resource-listing
  endpoint or core health route.
- The launch plan has no path to process.env and emits no AWS_ variables. Upstream's own
  quickstart is `eval $(floci env)`, which would put fake credentials and a local endpoint
  into every later process; configuring the emulator and configuring a client of it are
  kept as separate surfaces for exactly that reason.

Also corrects three fidelity markers against upstream's own service table -- EventBridge
and API Gateway REST are in-process, so the old PARTIAL would have forced a section 43
production retest the emulator does not need. Step Functions stays PARTIAL because
upstream says nothing about it, and a guessed fidelity is precisely what section 42 forbids.

Verified: 175 pass / 0 fail across the subsystem, policy and hygiene sets; typecheck reports
zero errors from src/agent-os/cloud-sandbox (the three remaining TS errors are issue #3,
removed by PR #4); privacy:scan clean.
@paopaonyapi-creator
paopaonyapi-creator changed the base branch from dev to fix/gitignore-anchored-export-path September 23, 2026 09:58
@paopaonyapi-creator

Copy link
Copy Markdown
Owner Author

Stacked on #4 now — and this supersedes my earlier typecheck amendment.

Base retargeted from dev to fix/gitignore-anchored-export-path per the stacked-PR workflow in
AGENTS.md (enforce-target skips the wrong-base gate for children; this branch must be retargeted
back to dev once #4 lands or closes).

Consequence, and it is the proof the fix actually works: bun run typecheck is now clean on this
branch
, where two commits ago it reported three errors. The stack brings in
src/agent-os/video/export/export-package-builder.ts as tracked content, so tsc resolves the
import that issue #3 described. Nothing in this PR's own commits changed to cause that.

bun run typecheck      -> clean
bun test <1419 cases>  -> 1419 pass / 0 fail / 1811 expect()
privacy:scan           -> clean

Branch was rewritten by the rebase (4 commits replayed, e2ccdee48 → 17f007816), pushed with
--force-with-lease. Pre-rebase tip is kept as the tag
backup/pre-stack-feat-cloud-sandbox-plane (= e2ccdee48) in case any review comment was anchored
to the old SHAs.

…ontainer

Running the image that M2 pins replaced three guesses with observations, and one of the
guesses was wrong in a way worth naming: docs section 10.2 claimed upstream publishes no
core health route. It does -- /_floci/health, which the container's own healthcheck.sh
probes over a raw TCP socket and accepts only HTTP 200. /health is an identical alias and
the trailing-slash form 404s, so the adapter now targets the path the image itself trusts
and requires a parseable document instead of "any HTTP response". A captive proxy that
answers 200-with-html would have satisfied the old rule and been treated as a live
emulator.

The health document registers 121 services and reports every one of them "running",
including lambda, rds and eks, in a container started with no Docker socket mounted. That
is the strongest possible argument for keeping fidelity in the capability registry: if
health had been trusted, an agent would have been told a Lambda sandbox works. So
HealthReport carries registeredServices beside readyServices rather than merging them.

An unsigned GET /?list-type=2 returns a real ListAllMyBucketsResult, because S3 auth
enforcement is off by default. Loopback binding is therefore a control and not a style
preference: published on another interface the emulator is an open object store. Both
findings are now assertions in a live test that skips unless PAO_CLOUD_FLOCI_REAL=1, so
the default suite stays hermetic and Docker-less CI is unaffected.

Verified: typecheck clean, 145 pass and 4 skip in the unit run, 4 pass against the live
container, privacy:scan clean, and the debt ledger still round-trips with zero lines lost.
AD PAO added 3 commits September 23, 2026 17:34
…it on a live daemon

M7 landed early because the Floci adapter could not be verified honestly any other way.
This is not the request-filtering socket proxy of source spec 15.1; the transport is the
operator's own docker CLI, invoked as an argv array with no shell. What 15 actually requires
still holds: the agent has no transport to the daemon, only five verbs through a broker that
validates every spec before it spawns, and there is one run() to audit rather than a call site
per adapter.

The guard this repository already has caught me shipping the exact bug in issue 3: index.ts
imported the new port while that file was still untracked, and the tracked-import test failed
as designed.

Two real defects found before committing, both by reasoning about the daemon rather than
trusting my own assertions:

- --publish was assembled as `127.0.0.1:4570/tcp`, which is not Docker's syntax at all.
  The mapping is now rebuilt as hostPort:containerPort from a numeric container port, and a
  non-numeric port is refused outright.
- mapState understood `docker inspect` vocabulary ("running") but not `docker ps` vocabulary
  ("Up 2 seconds"), so every live container read as unknown and leak detection could not tell
  a running orphan from an exited one.

The live block also stopped assuming somebody had started an emulator: it now provisions its
own through the broker in beforeAll and sweeps by label in afterAll, because the previous
version reported four failures the moment a manually-run container was cleaned up -- a test
reading the state of the machine instead of the behaviour of the code.

Verified with PAO_CLOUD_FLOCI_REAL=1: 7 pass / 0 fail, including a privileged spec refused
before the daemon saw it and destroy leaving no labelled orphan, confirmed independently with
docker ps --filter label=pao.sandbox.id rather than trusted to the test's own assertion.
Default run stays at 1432 pass / 0 fail with the live block skipped, typecheck clean and
privacy:scan clean.
…olicy gate

Adds CleanupController, which is the part of source spec 46 and 47 that can be honest about
its own coverage: it checks containers, registry rows and sandboxes stranded mid-lifecycle, and
says nothing about networks, volumes, host processes and ports, because enumerating those would
mean widening DockerControlPort past the five verbs it brokers -- a broker that lists everything
the daemon knows is a slow path back to the raw socket access section 15 forbids.

Two rules that decide behaviour:

- A leak is reported, never silently repaired, except for orphans whose sandbox row is already
  destroyed or absent. Over-reporting is annoying; a detector that calls a running sandbox's
  container an orphan hands the sweeper permission to destroy live work.
- A container that refuses to die stays in outstandingLeaks rather than being counted as removed,
  and the cycle re-measures after sweeping instead of trusting its own bookkeeping. That is what
  catches a daemon that accepted a stop and never released the container.

destroyWithVerification exists because the adapter cannot see its own emulator's side
containers: Floci starts a labelled worker for a Docker-backed service and its own teardown
leaves it. The test models exactly that and asserts the controller is the only layer that
notices.

The import-graph guard from the previous commit caught this change before it was staged:
index.ts imported cleanup.ts while it was still untracked, the same failure as issue 3.
The version committed in 4534014 read an emulator that a human had started on 4566 behind an
opt-in env flag, and its commit message quoted the 7-pass result of this rewrite -- so the
pushed commit did not contain the file it described. Correcting that here rather than amending,
per the repository's no-amend rule.

Functionally this replaces an environment assumption with setup: beforeAll starts Floci through
the brokered port, afterAll destroys it and sweeps by label so a crashed run cannot leave a
container bound to a port. The previous arrangement produced four failures on a correctly
behaving host the moment the manual container was cleaned up, which is the machine's state
reporting, not the code's.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant