placement-native
v0.1.0-selfupdate.3
Native placement attempts, receipts, materialization, supervision, and cleanup
Run
wippy run bee/placement-nativebee.placement.native
Native placement: one admitted launch becomes one attempt, run as a native
child under a runner process this module owns. Receipts live in an owned
SQLite store opened through bee.persist; attempt homes and retained
session directories live under bee.placement.native:root, the default for
the replaceable target_root requirement.
| Slice | Responsibility |
|---|---|
| bee.placement.native | service: the eight contract operations; runner: the per-attempt process that materializes the home, starts the child, records its identity, pumps output and input, signals and records exit; workdir_preparers: discovery, host authorization, setup and cleanup of workdir preparers; writable_roots_adapter: driver profile writable roots argument formatting; store and migrations: attempts and append-only evidence; capability: the measured cleanup capability of this runtime; identity: leader pid, start ticks and boot id; homes: derived directory keys under the root; protocol: runner, service and recipient messages; resources: linked references |
Order of a start
preparevalidates the request against this host's admitted roots, measures what the runtime can clean, refuses a launch that needs more than that, and records intent. Nothing external exists yet. A host policy selecting another placement is refused before the native capability probe or intent, even when a direct caller omits its placement hint. Calling the native operation does not override host selection. A request forbee.env:machine_homeis admitted only when the pinned host launch policy explicitly setsallow_host_home: true; profile metadata cannot grant that filesystem authority. Window login evidence is checked by existence in the selected provider home. An admitted file projection's source is also checked by metadata so a login about to be copied into a private or retained home does not produce a false warning. Neither check opens login bytes. An absent login adds an advisoryLOGIN_REQUIREDnotice to the prepare reply and does not stop the launch. A retained session home has one holder per owner/session pair: another attempt is refused until the predecessor is exited and its existing cleanup operation has proved the required scope gone and recordedcomplete. Repeating the same admitted idempotency key still returns its original attempt. This is an admission predicate in the existing placement transaction, not a home lock or a separate manager.startspawns the runner under the placement scope and waits for its startup acknowledgment within the admitted start budget.stopbefore the runner claims startup atomically records exit and complete cleanup, releasing the retained session without touching its existing files. A delayed start cannot claim that stopped attempt; repeated stops return its recorded state. If startup wins the claim, normal runner stop and cleanup proof still apply. Thread receipts and gateway revocation retain their own owners; placement completion does not settle either operation.- Both structured and window runners atomically claim
intendedasstartingbefore creating files. A duplicate runner refuses without replacing the recorded runner identity or materializing the attempt home. A launch that names both a retained session and its writable session home selects that session's derived/homebefore materializing provider, gateway, hook or trust configuration; the attempt home still owns attempt evidence and cleanup. Persisted host-generated configuration files in a retained home use atomic publication; login and provider conversation files remain separately owned. Argument-based configuration needs no replacement write. It resolves environment and working directory from the request and the admitted roots, starts the child (in its own process group when the runtime supports it), reads its identity, recordsrunning, and acknowledges.
Environment ownership
Before each configuration publication, materialization rechecks that the attempt
is still starting and that the current process owns its runner identity. The
private filesystem root and the native provider's verified parent handles protect
the file boundary. Configuration cannot overlap the retained login marker, the
projected login file or provider initialization files. Existing immutable
write_protected callers still permit only byte-identical replay.
Publication is per file. A later failure does not roll back earlier files, and
startup refuses if any file fails. If the native error reports that rename
succeeded but directory sync failed, placement records configuration.uncertain
and keeps execution uncertain; a successor cannot take that retained session.
The predecessor checkpoint remains unchanged. This outcome requires inspection;
there is no automatic retry or replay of the user's prompt.
Workdir preparers and writable roots
Placement exposes a generic bee.placement:workdir_preparer extension point discovered
from the registry and authorized by the host through target_workdir_preparers (registry
metadata alone never authorizes). The default preparer list is empty; the host
explicitly selects bindings. Each binding implements read-only plan, idempotent
setup, and idempotent cleanup. Placement persists plan state and the selected
method targets before calling setup, including for preparers without state. The
plan record lives whole in bee_placement_preparer_states (at most 64 KiB; a larger
state refuses the launch before setup) and one workdir_preparer.state evidence row
names the binding. The preparer methods require the runner's setup action or the
service's cleanup action, so a caller without placement's policies is denied.
Write roots handed to preparers are each grant's physical root/subpath; Git
metadata outside a granted subpath needs its own grant.
Setup declares the option names it handles; unhandled requested options refuse the launch.
Changed bindings cannot replace a recorded plan during setup replay.
Cleanup runs on proven attempt ends and the supervisor sweeps outstanding plans.
Failures do not prevent other planned preparers from receiving cleanup. It may contribute extra writable roots inside already
write-granted roots, update the working directory (such as creating a dedicated Git worktree),
and preserve planned ownership state between setup and cleanup. Failures are recorded as placement evidence
(workdir_preparer.failed).
The per-CLI argument rendering stays owned by placement's writable_roots_adapter.
When extra writable roots are contributed and remain inside host-admitted write roots,
placement renders the driver profile's git_writable_roots_adapter: Codex receives
sandbox_workspace_write.writable_roots; Claude Code and Agy receive --add-dir for each path.
Placement resolves physical directories (including symlinks and parent components)
and refuses malformed roots or roots and changed workdirs outside write grants.
Cleanup preserves uncertainty when process absence cannot be proven. It records
retention, checks evidence writes, and surfaces preparer failures even if home
removal also fails.
A read-only workdir, a profile without the matching edit-capable CLI mode, or a launch
without contributed writable roots receives no extra arguments.
Native placement owns HOME, selected from its attempt or retained session home.
For a driver's private provider home, placement also sets only its declared
provider-home variables (CODEX_HOME, CLAUDE_CONFIG_DIR, GROK_HOME, or
OpenCode's XDG roots) below that same private home. An admitted gateway owns its
tool and hook token destinations. prepare refuses literal or referenced
environment values that collide with those names, and refuses a shared
tool/hook destination, before recording intent. Materialization checks those
assignments again for a retained request.
Credential projections cannot overwrite a policy value, another projection, the native home or a gateway destination. Such a conflict ends materialization before the child starts. Error replies and evidence name the destination and projection; they contain no credential bytes. Policies supply nonsecret configuration while the credential broker supplies secrets; precedence is not used to hide conflicts.
Nested configuration paths create each missing parent in order. Every existing
ancestor must belong to the current materialization's created-parent set;
retained byte-identical file replay remains unchanged. This permits Agy's
.agents/mcp_config.json in a retained customization root without adopting
or changing the user's global configuration directories.
Provider login homes
Private attempt homes are created empty. A private provider_home declaration
selects the provider's machine-home source paths, private destinations, runtime
home variables and whether its login can be returned after exit. The credential
broker supplies bounded bytes only for source paths admitted by the host;
placement compares every returned login and setup path with the driver
declaration before creating files. It never scans the source home or copies
unlisted files. Optional absent logins leave the token destination absent.
An admitted launch with a SessionRef and writable home resource uses that
Session’s retained home, including private provider homes. Login, configuration
and conversation state remain there across turns. Launches without a selected
session home use separate attempt homes.
homes.project_attempt_login writes only the broker's primary login file and
its admitted setup initializers into a newly created attempt home. Claude's
format also initializes .claude.json with only hasCompletedOnboarding: true
when login bytes are present; this lets Claude Code recognize the imported
subscription without importing unrelated home state. Setup configuration is
copied as admitted, or created empty only when its host declaration marks it as
a composition base. Bee's generated provider configuration is then composed by
the selected driver.
The CLI may refresh its own login file while it runs. The pinned runtime has
no Lua filesystem operation that opens a regular file while refusing symlinks
in every path component. The runner therefore refuses provider login
write-back with provider login write-back requires runtime no-follow fs after
the child exits. It leaves the worker file unchanged and never truncates or
rewrites it to validate the path. Host-to-private-home projection remains
available because the credential broker reads only the host-declared source
files before the worker starts.
Configuration and state files are never returned. Evidence records status and projection identity only; it never contains login bytes. The runtime feature branch adds the descriptor-relative no-follow read needed to resume write-back; Bee keeps the behavior disabled while its manifest pins the earlier runtime.
Retained homes keep their existing identity marker and seed rules. The first seed records provider, definition ID and revision only after the login file is completely written. A matching resume leaves the login file untouched, so provider-refreshed bytes persist. Changed identity or an interrupted seed refuses reuse. Existing parents are refused unless the current materialization created them; login formats cannot overwrite the retained identity marker.
The native fixture checks the placement root's actual 0700 mode rather than
registry metadata. The helper reads the root's actual numeric
fs.FileInfo.mode and refuses group or other access; it does not infer privacy
from the registry declaration. The pinned fs write contract reports an error
for a short write, so a successful write plus successful close is the
ready-marker precondition. It has no fsync operation: ready-marker ordering
refuses interrupted process writes, but is not a machine-power-loss durability
claim.
An optional source may seed no login bytes. Retained homes preserve either an
absent file or one created by interactive sign-in; later machine credentials
never replace either choice. Required sources refuse a missing login. Changing
the optional policy changes the retained binding and refuses reuse. Empty
provided login bytes remain an error. Default provider windows use the
explicitly authorized host HOME and select no login projection, except Grok,
whose private window projects grok_login into its selected retained session
home. Private batch launches without a retained home selection project their
declared files into attempt homes. Fixture unit tests
cover all six driver declarations and placement paths, and the confined Codex
fixture worker verifies that unrelated machine-home files stay outside its
attempt home. managed-launch-fixture-check uses fixture CLIs only.
Capability
capability.measure starts a probe child with process_group and reads its
group id back. A runtime whose handle carries no pid reports
direct_process; a probe that leads its own group reports process_group.
No runtime here reports contained_tree: a descendant that starts its own
session escapes a process group. The measured value is recorded on every
attempt and returned by capabilities.
Streams
Output leaves the runner as chunks with one sequence per attempt, addressed to the bound recipient under the current attachment generation. The runner keeps unacknowledged chunks up to a spool bound; beyond it the pump blocks and the child blocks on its pipe. Input arrives with a write id and the generation; a repeated write id is acknowledged without a second write. EOF stops are separate messages from chunks, and exit is separate from EOF.
Exit observation
A runtime whose handle offers done() lets the runner learn the exit
independently of the pipes: exit_observation: independent. Without it the
runner can only call wait() after both streams end, because wait()
consumes the handle that signalling and input need: eof_gated. A child
that exits while a descendant holds stdout is then observed late, and a
child that closes its pipes while running would be lost to wait().
Managed harness launches require independent; prepare refuses them on
an eof_gated runtime. Only bounded fixture and simple-command launches
declare required_exit_observation: eof_gated.
Cleanup scope
exited says the direct process ended, observed by the runner or proven
absent by identity. It does not say its group or descendants ended.
cleanup removes the home only when the required scope is proven gone:
direct_process by the observed exit or proven leader absence,
process_group when no member of the recorded group answers a signal
probe, contained_tree never on this runtime.
Overlapping cleanup calls treat a home entry already removed by the other
caller as gone; a path still present after a filesystem error stays uncertain.
A stop accepted during materialization fences the asynchronous credential reply
before login seeding. If no child was created, the same runner commits its exit
and child.not_started evidence together. Cleanup can use that proof only when
all native identity fields are absent. Missing identity without this evidence,
partial identity, a foreign runner or uncertain execution does not authorize
cleanup. Retained session homes remain available for a subsequent admitted attempt.
Resource modes
The host selects the mode in bee.placement.native:placement_resource_mode; a
request cannot. The shipped default is granted, so managed Agent resources
are validated through their authority on use. In host_configured mode
bee.placement.native:placement_admitted_roots
lists the fs.directory roots a launch may name with the widest access the
host allows, prepare checks the caller, the root, the subpath and the
access mode against that list, and a request's grant_ref is correlation
data only. In granted mode every grant_ref is a grant id resolved
through bee.resources.binding:resolve for the owner this placement admitted, with
the attempt as scope; the authority's root and subpath replace the caller's,
the resolved grants are recorded with the attempt, start resolves them
again before spawning the runner (grant.refused evidence and the
authority's code otherwise), and reconcile resolves them for a live
attempt and stops it cooperatively when one no longer holds
(grant.revoked evidence; enforcement is pending until the exit is
proven). capabilities reports the mode, delegated_resource_grants,
revocation_enforcement: stop_on_reconcile and credential_broker: false.
Input uncertainty
The runner deduplicates writes by write id while it lives. A runner lost after bytes were written and before the acknowledgment was recorded leaves those writes uncertain; the recipient must not write them again on its own.
Credential projections
A launch names credential projection ids. prepare checks each binding
through bee.credentials.binding:check for the owner it admitted with the attempt
as scope. Environment projections materialize right before the child starts
at the provider's fixed environment destination. An absent optional environment
projection is validated and omitted; placement never creates an empty variable.
A populated projection must explicitly report present: true, its optional flag
and UTF-8 encoding before placement adds it to the exact child environment. A
file projection requires the selected retained session home, validates its frozen declared login
destination and nonsecret definition identity, then writes its opaque bytes
before immutable driver configuration. One file projection may select a
retained home; matching later resumes preserve provider-refreshed bytes.
Evidence carries only the projection id and outcome, and the stored request
never carries a value. start and reconcile re-check the bindings like
resource grants.
Supervision
sweeper_service (registered as bee.placement.sweeper) reconciles up to
sweep_bound live attempts every scheduling delay through the
owner-independent reconcile_attempt and stop_attempt, so a revoked grant
or projection is stopped within a bound rather than at the next owner call.
capabilities reports revocation_enforcement with the scheduling delay,
the reconcile timeout, the per-attempt stop grace and the sweep bound as
separate figures, because the sweep interval alone is not a stop deadline.
Evidence names what failed: grant.revoked or credential.revoked,
grant.refused or credential.refused. An environment value already
inside a running child cannot be scrubbed; enforcement is the stop. Native
executor error text is never recorded; evidence carries fixed phrases.
Attachment fence
attach records the new generation and, when a runner lives, sends it the
generation and waits for the runner's acknowledgment before returning; a
runner that does not answer within the fence timeout leaves the attempt
uncertain. After the fence the runner accepts input and acknowledgments
from the new recipient only, and answers a refused write with the sender's
own generation.
Uncertainty
Signal evidence is not exit evidence; the runner records exit only from
wait. Without a live runner, stop signals the group only after the
leader is identified alive by pid, start stamp and boot identity; otherwise
the attempt becomes uncertain. Linux reads the start stamp in clock ticks
from /proc and the boot id from the kernel; macOS reads the start second
from the process table and kern.bootsessionuuid. Both read the process
group with ps. A group signal succeeds only when the OS command
exits with status zero. Command refusal leaves stop unproven and records
stop.unproven, rather than claiming signal.group evidence.
Reconciliation treats a starting attempt without an execution identity as
supervised while its runner process is present on a host: the runner answers
status probes only once its child exists. A starting attempt whose runner is
absent becomes uncertain.
reconcile proves absence the same way and
keeps uncertainty where identity is missing. cleanup removes the home
only from exited; a cleanup whose scope is not proven gone records
cleanup.refused with its reason before it answers CONFLICT.
Configured launches must carry the digest of their host-selected provider and gateway inputs. Placement reconstructs those inputs from its pinned policy, activation and binding, refusing omissions or changes before storing intent. Caller-supplied delivery is rejected. For a new intent the activated driver renders bounded argument literals and protected files under an empty callee scope, using the owner-derived home path. The existing intent stores that validated output; identical retries reuse it without re-rendering. Both native execution transports use the same materialization component. Drivers own their formats, including Claude's inline settings and Codex's files; placement owns credential delivery, protected writes and session-home exclusion.
The shared process_backend value lets Docker reuse this materialization,
runner and window lifecycle while supplying its executor, physical mounts,
container identity and cleanup proof. Native stream handles are acquired before
start; Docker stream handles are acquired after start. EOF-based Docker turns
read initial input from a protected file in the admitted private home, so a
container's attached stdin lifetime cannot keep a one-turn provider waiting.
Reading an intent decodes the stored request and its private delivery again, including file digests, paths and argument bounds, before creating a home or starting a child. Malformed stored content is a storage failure. Its retry digest still identifies the original caller request; resolved resource grants in the stored request do not redefine that identity.
Managed terminal supervision
The process-local window owns its PTY and answers the existing placement status probe without consuming the application's terminal completion channel. The periodic sweep therefore rechecks grants without marking a live Agent uncertain merely because a PTY exposes no host execution identity. This reply proves live supervision, not post-crash execution absence or cleanup.
The window registers the same private per-attempt control token as the streamed runner.
Stop notifications require that token and are acted on only after the placement store records stopping
for the exact attempt, owner and runner. An arbitrary process message cannot
stop the window. The listener is installed before publishing running; publication compares the
recorded state with starting, so a concurrent stop cannot be overwritten. The
window retires its listener when finalization commits.
Native PTY acceptance proves live reconciliation, raw-stop denial, admitted stop,
input, resize, finalization and duplicate/foreign-owner refusal. A native Agent
fixture also keeps its MCP binding live across the real 30-second sweep.
Structured turns with a SessionRef and an admitted writable session home retain the driver-selected provider state across attempt cleanup. Ephemeral turns keep their attempt-local private home.
Private provider configuration uses the shared provider projection scanner from
the Docker placement lane. It bounds nesting and resolves {file:...} references
only to driver-declared files materialized by the credential broker. Both
absolute host-home paths and ~/ references become paths under the private
home. Undeclared, absent, traversing or unresolved dependencies refuse before
child creation; refusal evidence contains no configuration values. Native
descriptive text containing a home path remains text. Docker projection retains
its stricter host-command and path checks and declared portable configuration.
After an independently observed child exit, pipe draining has a bounded read budget. Time spent with reads paused at the output spool limit does not consume that budget. After child exit, the retention deadline bounds each continuous wait at the spool limit; acknowledged progress that resumes reads ends that wait. Once both streams end, one retention deadline bounds the remaining unacknowledged output. Drain expiration records forced truncation; retention expiration records output loss rather than consumption.
Native preparation decodes host prepare options through the same driver preferences decoder as the carrier planner before comparing the configuration digest. Empty registry maps and absent options therefore select the same default options.