Skip to content

docs: localized AGENTS.md for sbx-mgr, node-mgr and the api crate - #275

Merged
ac-pplx merged 1 commit into
mainfrom
ac/agents-md-lifecycle-crates
Oct 12, 2026
Merged

ac-pplx merged 1 commit into
mainfrom
ac/agents-md-lifecycle-crates

Conversation

@ac-pplx

@ac-pplx ac-pplx commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Agents changing the sandbox-lifecycle crates kept missing crate-local constraints. This adds short AGENTS.md files, each with a CLAUDE.md symlink, for:

  • crates/libs/sbx/mgr/server
  • crates/libs/node/mgr/server
  • crates/libs/api (crate level; it points to src/controllers/substrate/AGENTS.md)

The files state rules for new changes, and name an existing exception only where one or two keep the rule true. Every sentence was checked against code at this head. The files don't name helpers or fixtures that open PRs (#278, #296, #298, #299) add or change, so they stay accurate whichever of those lands first.

The generic guidance appears once, in docs/rust/space-conventions.md, and each file points to it:

  • "Tests run as root on the CI host": the Rust job runs the workspace with ignored tests, as root, with --privileged --network host. Fixtures must fail closed, and host mutation needs an environment opt-in.
  • "Log targets": bare tracing targets follow module_path!(), so a module move breaks exact-value @target filters. Pin those targets with a literal target: "…"; const LOG_TARGET = module_path!() survives a child-module extraction but not a file move.

Rules

sbx-mgr

  • sandboxes.state is written only through src/db writers.
  • A new transition uses or adds a compare-and-set writer in src/db; no generic writer compares against the caller's observed state.
  • A new sandbox child table is registered in delete_sandbox_child_rows.
  • service/adapter.rs delegates mostly to the grpc_* modules.
  • btrfs, dm-crypt, VM launch and runtime cleanup are node-manager RPCs. New RPC fields carry domain identifiers, not host paths; existing path fields (dest_dir, artifact_dir) are validated by node-manager.
  • Tests:
    • Tests must not dial node-manager's fixed socket (connect(), connect_once(), lazy()); give the code under test a test-served node-manager.
    • Cancelling shutdown_token first protects only code paths that check it before dialing.
    • SandboxManagerServiceOptions::default() reads host state.
    • Never call db::open().

node-mgr

  • New RPCs take domain identifiers, and an existing path field is validated against its canonical root.
  • Privileged inputs are pinned.
  • Pinned subvolumes are changed only through the pinned-descriptor helpers (btrfs.rs, clone_cleanup.rs). Where the CLI is unavoidable, it is bound to the pinned descriptor in a private mount namespace (service/artifact_fs/receive.rs).
  • Host tools go through host_command.rs and nft mutations through firewall.rs. Exceptions: the startup supervisor activator in run.rs and the non-Linux net/fallback.rs.
  • Supervisor replay is limited to failures that never executed: a get_or_connect dial error, is_client_connect_failure and is_request_refused_before_transmission. is_client_connect_failure lives in crates/libs/common/grpc/src/retry.rs, so changing it changes every gRPC client.
  • Tests:
    • Never call NetworkPool::open.
    • Unit-test services must not rely on host-reading defaults for runtime paths, memory admission, the lifecycle limiter or the supervisor client; inject test-owned ones.
    • Host mutation needs an environment opt-in naming a disposable target.

api

  • New Kubernetes substrate SQL goes in store/ with DbContext::db_context. Warm-pool slot rows belong to WarmPoolStore.
  • Cold create and warm-pool adoption both consume PerSandboxSetup, so per-sandbox setup goes there.
  • The route manifest is the source of /v1 operations, and the M1 baseline is a compatibility floor, not a snapshot to refresh.
  • ECW handlers live in routes/e2b/.
  • Tests:
    • AppState::from_pool and the from_env constructors read env. Test a parser through its explicit-input form (from_environment, with TestEnv) where one exists; use enter_test_process only for tests that need the process-env constructors.
    • Point clients a test must not use at a closed port.

Shared guidance in docs/rust/space-conventions.md

  • "Tests run as root on the CI host":
    • Tests leave alone host state they do not own. Test-owned temporary directories and child processes are fine.
    • Fixtures fail closed.
    • Host mutation needs an environment opt-in.
    • Env-dependent tests follow "Environment-dependent tests" in docs/rust/unsafe.md: give parsers explicit inputs (TestEnv) first. Only real startup-environment behavior runs in a child process through enter_test_process. Never std::env::set_var.
  • "Log targets": list renamed targets in the PR description. A literal target survives a move; module_path!() constants do not survive moving or renaming their defining module.
  • SQL section: caller SQL does not go through a wrapper that applies sql! for it. The execute helper in crates/libs/db/migrator/src/bootstrap_roles.rs is named as the known exception, not a precedent.

The api post-result lifecycle write rules are left out on purpose.

Size and budget

File Bytes
crates/libs/sbx/mgr/server/AGENTS.md 1,613
crates/libs/node/mgr/server/AGENTS.md 1,825
crates/libs/api/AGENTS.md 1,551

The largest instruction chain is unchanged: root AGENTS.md (5,745) + infra/space/kind/AGENTS.md (6,730) = 12,475 bytes. The deepest new chain, root + crates/libs/api + substrate, is 9,088 bytes. Both are under the 32 KiB budget.

Validation

  • No repository validator covers instruction files.
  • The three CLAUDE.md symlinks are mode 120000 and resolve to their sibling AGENTS.md.
  • The pre-commit Gitleaks hook passed.
  • The space-public-review pass on the staged diff found nothing.
  • This is a documentation-only change, so no code checks were run.

Follow-ups

These test-safety gaps are outside this docs-only PR:

🤖 Generated with Claude Code

@ac-pplx
ac-pplx force-pushed the ac/agents-md-lifecycle-crates branch 2 times, most recently from 07744e5 to e89d061 Compare October 11, 2026 05:41
@ac-pplx
ac-pplx marked this pull request as ready for review October 11, 2026 05:44
@the-gigi-pplx

Copy link
Copy Markdown
Collaborator

Review — 7 findings

Medium

  1. crates/libs/api/AGENTS.md:28 and crates/libs/node/mgr/server/AGENTS.md:33-35 — both "Don't copy …" examples point at code that PR tests: keep three tests off host state they don't own #278 fixes.
    test_state() in src/routes/e2b/tests.rs moves to 127.0.0.1:1 in PR tests: keep three tests off host state they don't own #278, and the loop-device test gains an opt-in there. Whichever of the two lands second leaves these files steering agents away from code that now follows the rule. I'd drop both clauses, because the rules stand without them. Separately, the literal in test_state() is postgresql://postgres:postgres@localhost/space, with no :5432, so grepping for the quoted text finds nothing.
  2. docs/rust/space-conventions.md:383-386 — the "Tests run as root" section misstates the guidance it cites.
    It says to follow docs/rust/unsafe.md "Environment-dependent tests" by pinning values in a child process with enter_test_process. That section puts explicit parser inputs first, and child processes only for real startup-environment behavior. It doesn't mention enter_test_process, and crates/libs/test-env/src/lib.rs:22 says to use TestEnv for in-process configurations. All three new AGENTS.md files route agents here, so they get steered to the heavier mechanism. Suggest: "give parsers explicit inputs (TestEnv); for real startup-environment behavior, use enter_test_process …".
  3. crates/libs/sbx/mgr/server/AGENTS.md:25-26 — "protects only the cleanup-executor paths that check it" names the wrong scope.
    Non-test code checks shutdown_token in reconcile.rs, recovery.rs, cleanup_targets.rs, network_policy.rs, drain.rs, gateway_authority.rs, watchdog.rs and others, not just cleanup_executor.rs. Suggest "protects only code paths that check it before dialing".

Low

  1. crates/libs/sbx/mgr/server/AGENTS.md:14-16 — the citation for delegating work to node-manager is incomplete. src/btrfs_via_nodemgr.rs covers btrfs only. VM launch is in firecracker_via_nodemgr.rs, and dm-crypt is in rootfs_encryption.rs and snapshot_encryption.rs. The cited file also has host-path RPC fields (dest_dir at :71 and :103, artifact_dir at :190), right next to the rule "New RPC fields carry domain identifiers, not host paths". Cite the other modules, or drop the parenthetical.
  2. crates/libs/sbx/mgr/server/AGENTS.md:21-23 — the fixed-socket entry points leave out connect_once() (node/mgr/client/src/lib.rs:381), which also dials SOCKET_PATH and is used at run.rs:785 and :870.
  3. crates/libs/node/mgr/server/AGENTS.md:19-21 — the replay rule attributes both predicates to src/runtime_supervisor_client.rs. is_client_connect_failure is defined in crates/libs/common/grpc/src/retry.rs:143 and only imported there, so changing it affects every gRPC client. The dial error from get_or_connect (runtime_supervisor_client.rs:289-293) is also a replay case the text doesn't name.
  4. docs/rust/space-conventions.md, SQL paragraph — "Don't route a caller's SQL through a wrapper that applies sql!" already has a live counterexample at crates/libs/db/migrator/src/bootstrap_roles.rs:1507-1508. It works as a rule for new code. Naming that wrapper as the known exception would keep the next reader from treating it as precedent.

I checked the other mechanism, file and function claims in the three AGENTS.md files and the conventions section against the source at this head, and they hold. Examples: delete_sandbox_child_rows and the foreign-keys pragma in db.rs, every state UPDATE living in src/db, the host_command exceptions in node-mgr, DbContext lock contention mapping to 503, and the CI flags in run-rust-checks.sh and common-ci.yml. Nothing contradicts the root AGENTS.md. The largest root-plus-localized chain is about 12.5 KB, well under the 32 KiB budget, and the body's figures match.

Verified against e89d0610175dc4780ef030511ffb687653c2d69a. Docs-only, so I ran greps and wc -c and no builds.

@the-gigi-pplx the-gigi-pplx left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Approved. 7 non-blocking findings in the review comment above.

@ac-pplx

ac-pplx commented Oct 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, all seven are addressed in ff7c8be (one commit, docs only):

  1. Dropped both "Don't copy" clauses.
  2. docs/rust/space-conventions.md now says: explicit inputs first (TestEnv passed as the Environment), and enter_test_process only for real startup-environment behavior. The api AGENTS.md follows the same wording.
  3. "protects only code paths that check it before dialing".
  4. Dropped the parenthetical. firecracker_via_nodemgr.rs only reads VM state; launch is LaunchSandbox in start.rs/create.rs. The RPC-field rule now notes that the existing dest_dir/artifact_dir fields are validated by node-manager.
  5. Added connect_once().
  6. The replay rule now names the get_or_connect dial error, and says is_client_connect_failure is shared from common/grpc/src/retry.rs.
  7. Named the bootstrap_roles.rs execute helper as the known exception.

The socket and test-default rules are now worded so they stay accurate whichever of the seam PRs (#296/#299, #298) lands first.

@ac-pplx
ac-pplx enabled auto-merge October 12, 2026 15:26
Agents changing the sandbox-lifecycle crates kept missing crate-local
constraints. Add short AGENTS.md files (with CLAUDE.md symlinks) for
crates/libs/sbx/mgr/server, crates/libs/node/mgr/server and
crates/libs/api. Each states contracts for new changes, and names an
existing exception only where one or two keep the rule true:

- sbx-mgr: sandboxes.state goes through compare-and-set writers in
  src/db. Child tables are registered in the manual delete cascade. New
  node-manager RPC fields carry domain identifiers; existing path fields
  are validated by node-manager. Tests must not dial node-manager's fixed
  socket (connect(), connect_once(), lazy()), and the service's Default
  options read host state.
- node-mgr: new RPCs take domain identifiers and existing path fields
  are validated. Privileged inputs are pinned, pinned subvolumes change
  only through the pinned-descriptor helpers, and host tools and nft
  mutations have named owners. Supervisor replay is limited to
  untransmitted failures (a get_or_connect dial error and the two
  predicates), and is_client_connect_failure is shared by every gRPC
  client. Unit-test services inject test-owned replacements for
  host-reading defaults.
- api: new Kubernetes substrate SQL goes in store/ with db_context error
  mapping. PerSandboxSetup covers cold and pooled setup, and the route
  manifest and M1 baseline hold the public contract.

Generic guidance lives once in docs/rust/space-conventions.md, and each
file points to it:
- "Tests run as root on the CI host": the Rust job is privileged, uses
  host networking and includes ignored tests, so tests leave alone host
  state they do not own. Fixtures fail closed, and host mutation needs an
  environment opt-in. Env-dependent tests give parsers explicit inputs
  (TestEnv), as docs/rust/unsafe.md says; only real startup-environment
  behavior runs in a child process through enter_test_process.
- "Log targets": bare tracing targets follow module_path!(), so a moved
  module breaks exact-value @target filters.
- The SQL section adds that caller SQL must not go through a wrapper
  that applies sql! for it, naming the migrator's bootstrap_roles.rs
  execute helper as the known exception.

The largest instruction chain stays root + infra/space/kind (12,475
bytes), well under Codex's 32 KiB budget.
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.

2 participants