2serverDocumentationGitHub
2server / DocumentationRead Markdown (.md) View source

Soot runtime

Deploy a pinned Linux Soot runtime and an independently reviewed C1 config bundle as a named app. The runtime owns validation, source transactions, activation, deployment acknowledgments and config rollback. 2server stages verified inputs and supervises the process through the native sourceDeployment capability. It does not rebuild Soot, fetch packs, infer a singleton or copy live databases.

The example uses placeholder registry and DNS names. Its receipt describes runtime commit afb1e0d568071d59fe7d4ff7e3cf88e872915b4d, version 0.1.0-ac-config-review, Linux amd64, and OCI manifest sha256:35c9af6ab29b4ffea7374a1233b677afef1a79188b8188eba1d0642451f24f23. That local producer receipt does not establish registry availability. The example AI policy is unconfigured; choose a real provider/model and credentials before source activation, then review the bundle digest with the producer's tooling. No example is an authorized deployment target.

Source and mount contract

sh
2srv init app assistant --template soot -o platform/assistant.yaml
2srv validate -f platform/assistant.yaml
2srv secret set --app assistant --env-file /private/assistant.env --apply
2srv plan -f platform/assistant.yaml --plan-output /private/assistant-plan.json

Set spec.image to a registry repository with an exact @sha256: pin and spec.runtimeReceipt to the matching producer receipt. spec.bundle is optional: omit it for a bare runtime whose Soots arrive as kind: SootSource slices (below), or set it to an already materialized offline C1 directory containing soot.bundle.json for the legacy single-bundle deploy. Locators resolve relative to the App file. tokenEnv names an app-scoped VM secret containing at least 32 URL-safe characters; no secret value enters a document, displayed diff or process argument. Named instances cannot use global or local .env values. Every instance has its own container, credentials and /opt/2server/data/NAME store. Changing state/auth identity requires an explicit operator migration, not a source update.

spec.secretEnv lists further app-scoped VM secrets the runtime reads from its environment, such as the Turnstile secret named by public_web.challenge_secret_env (which admission requires to be listed). Each must also be declared under the document's secrets:. 2server writes them to a private credentials/app.env (0600, replaced atomically) before every initialize, handoff and restart, so secret set followed by restart rotates a value; removing a key from secretEnv deletes the file at the next start. Values are limited to letters, digits and _.:+/=-, and tokenEnv cannot be repeated.

The operator token (tokenEnv) is different: it is written to credentials/operator.env once, at initialize, and restart checks the runtime with the VM secret before restarting, so after secret set it answers Soot unauthorized: … rotate-token --apply. Rotate it in two steps:

sh
2srv secret set --app assistant --env-file /private/new-token.env --apply
2srv app assistant rotate-token --apply   # replaces operator.env, restarts, waits for /readyz

The old token stops working at once; every connected Nest must enter the new one.

The candidate must use listen: 0.0.0.0:7788, a relative inventoried ai_config, the matching token_env, and data_dir: /state (portable data/state is also accepted before C3 normalizes it). Shared AI owns credentials_dir: /credentials. All config, prompt, skill, pack, archive and installed receipt inputs remain bounded and checked. Links, hardlinks, traversal, case collisions, unknown C1 fields, private/compiled content, mismatched digests and unsupported executable closures fail closed. The measured image supplies /bin/sh and /bin/timeout; selected programs must fit that closure. Runtime admission repeats on prepare and commit. Full runtime config semantics are enforced by Soot.

Installed pack receipts must match the local absolute materialization directory; 2server relocates only their installation paths for the fixed VM inbox. Git packs require an already obtained archive at BUNDLE/obtained-archives/ARCHIVE_SHA256.tar; its bytes and installed tree are verified offline, then staged into the instance's private runtime archive cache. Plans never install, fetch or refresh a pack. No source digest is recomputed to silently accept changed operator inputs.

Container pathVM storageLifetime
/configextensions/NAME/configWritable active config; runtime-owned saves
/transactionsextensions/NAME/transactionsPersistent C3 fences, acknowledgments, snapshots and inbox
/state/opt/2server/data/NAMEPersistent conversations, receipts and monitoring
/credentialsextensions/NAME/credentialsPersistent token file, vault and private per-instance home
/releaseextensions/NAME/releases/UUIDRead-only reviewed source, runtime receipt and supervisor definition

Paths under extensions/ are beneath /opt/2server/. Reviewed source is retained under releases/UUID/source, separate from /config; C3 receives a private inbox staging tree. Both trees must match the reviewed file/directory inventory before commit and after a code handoff, including installed receipt bytes. Restart never copies source defaults. Bind mounts forbid Docker from creating missing host paths. The runtime runs as 65532:65532 with a read-only root and no capabilities. Before each start the host adapter hands the four writable mounts to the user the release's supervisor names, without following links or crossing mounts; .2server-owner, operator.env and app.env stay root-owned. The mounted release stays root's; the runtime's group may only traverse it and read receipt.json, which it verifies at startup. Restoring an older release that names no user hands the mounts back to root. Foreign ownership, overlapping state roots and altered mounts block effects.

Two explicit reviewed phases

On a fresh instance, plan inspects absence and creates a private local artifact. Review its server, instance, control revision and source/image/receipt pins; set reviewed_initialization: true in that exact artifact, then apply:

sh
2srv deploy -f platform/assistant.yaml --plan-file /private/assistant-plan.json --apply

Initialization is create-only. It installs an inert management definition with no AI provider because Soot requires one definition even in management mode. It establishes authenticated management and persistent mounts; it does not apply the authored source or publish traffic. A nonempty/partially initialized root is a recovery condition, never permission to reset it. Plan again after initialization and use a new artifact path.

Without --plan-file, deploy -f FILE --apply plans and applies in one step: initialization, a package-only upgrade and an unchanged redeploy are approved as planned; a source replacement asks exactly as for a SootSource below.

The source phase reads GET /v1/settings/deploy/state. Its DeployPlan binds the request, runtime identity, baseline D ID/generation/digest, persisted P and active A revisions, checked fingerprints F, candidate source/package/compatibility, staging UUID/digest and exact difference digest. Review only this safe scope and digest output; mission contents and credential values are not displayed. If there is no D, add plan.reviewed_replacement with kind: initial_source_review and an exact copy of plan.bindings. If P/A/F has drifted since D, use kind: reviewed_replacement with those exact bindings. An operator must explicitly make this decision after review. An agent must not invent it merely because --apply was requested. --apply alone cannot approve source replacement, and refreshing current revisions does not approve edits since the last deployment. Raw changes require runtime source.review first.

Apply holds the normal VM control lock, compares local/control/host/runtime facts again, stages exact bytes, and submits C3 prepare/commit. Any stale binding requires abort/replan/review. The runtime lease independently excludes settings writers and detects changes before writes and activation. Async activation is polled through the same request's sanitized transaction receipt, including brief listener handoffs. No ambiguous POST is replayed with a new ID.

Only acknowledged source and package plus /readyz permit supervisor pointer publication. Shared domain/DNS/TLS reconciliation then publishes the declared Domain. HTTPS /readyz, missing-bearer denial and authenticated active receipt verification must pass. A runtime acknowledgment followed by edge/control failure is reported as an unresolved later phase; inspect retained private recovery and the runtime receipt before retrying. Readiness does not prove provider delivery or a model smoke test.

A public_web listener (for example 0.0.0.0:7790) serves only /public/* and has no /readyz or bearer API, so publish it as a separate kind: Domain targeting CONTAINER:7790 with healthPath: /public/widget/loader.js, not under the App's domains:. Operator verification (source apply and restore) covers only domains routed to :7788; purge still refuses while either routes to the container. The public listener takes the visitor address from X-Forwarded-For only from public_web.trusted_proxies.

Bare runtime and SootSource slices

A bare runtime App (no spec.bundle) installs the runtime and nothing else. Providers, tiers, the vault, default_soot, routes and public-web policy are host settings managed in Nest; no deploy ever replaces them. Each repository then owns one source slice (sources.d/<id>/ on the runtime) and deploys it independently. The recommended repository layout keeps the bundle and its deploy document in one directory:

sh
soot spawn --source lohi2 soot-source            # creates soot-source/soot/source.json
2srv init soot-source lohi2 --runtime soot -o soot-source/deploy.yaml
# edit soot-source/soot/soots/... then
soot bundle --refresh soot-source
2srv deploy -f soot-source/deploy.yaml --apply
yaml
apiVersion: 2server.app/v1
kind: SootSource
metadata: {name: lohi2}        # source id; spec.source overrides it (e.g. 2found)
spec:
  runtime: soot                # the installed Soot App on this server
  bundle: "."                  # this directory

A repository needs neither the runtime App file nor its secrets: spec.runtime names the App already installed on the server. In a checkout with no .2server/connection.yaml of its own, run 2srv connect once (the file is gitignored); later deploys find it.

init soot-source validates the id and, when it is not a valid 2server name (2found), writes metadata.name: source-2found with spec.source: 2found.

The bundle entrypoint is <root>/source.json; its id must equal the document's source id and it may not set listener, state, AI or credential fields. Soots pick a tier, never a provider. Plan reads GET /v1/settings/deploy/state?source=<id>: fingerprint, baseline and drift cover only that slice, so a provider change or another repository's deploy is never drift.

deploy --apply plans, prints the decision and applies in one step:

Runtime state of the sliceDecisionOne-step deploy
Deployed before, no Nest edits sincenoneapplies
Never deployed and empty (source_files: 0)safe (initial_source_review)applies
Never deployed but holding filesreplace (initial_source_review)asks
Edited in Nest since its last deployreplace (reviewed_replacement)asks

A replace decision asks the operator to type the source id on a terminal; a non-interactive run refuses unless the operator passed --replace-edits. An agent must not pass --replace-edits on its own: the edits it discards were made by a person in Nest. The two-step plan --plan-output / deploy --plan-file review stays available and takes the same reviewed_replacement field as above. Apply stages into the runtime inbox and runs C3 prepare/commit with bindings.source; it never hands off the process, publishes a release pointer or changes VM control state. A source deploy rides the package already running: a different package is a runtime App upgrade.

Planning the bare runtime App again reports none when the running package matches, or upgrade when spec.image/runtimeReceipt changed. An upgrade is a reviewed package-only handoff that keeps config, sources, state and vault; if the new package does not come up with the same active configuration, the previous release is restored. A runtime with deployed sources refuses a legacy complete bundle deploy (sources_present), which would delete every slice.

Restart, handoff and guarded restore

sh
2srv app assistant release-status
2srv app assistant restart --apply
2srv app assistant rotate-token --apply
2srv app assistant restore-release --plan-output /private/restore.json
# Review scope and exact request; set reviewed_restore: true in this artifact.
2srv app assistant restore-release --plan-file /private/restore.json --apply

Restart uses the acknowledged installed release and retains saved/active edits; it never resolves local bundle locators. A package replacement retains the C3 lease, disables old supervisor restart, sends TERM and waits for exit/store close before creating the replacement on the same mounts. An uncertain stop or timeout blocks replacement; there is no forced concurrent bbolt opener or live DB copy. Compatible prior runtime receipts/supervisor files are retained for restore. If replacement readiness or staged-byte verification fails before source commit, the adapter joins the candidate and starts the retained supervisor while keeping the C3 lease, then aborts that same lease through the recovered runtime. A blocked close or recovery retains the fence and reports the request for inspection.

Restore binds current P/A/F, exact prior revision/package and soot/api1-store1. Concurrent edits, unavailable snapshots/packages and incompatible stores refuse restoration. The runtime restores config through its journal and acknowledges a new active generation; vault, operator credentials, conversations, accepted-run history, receipts and monitoring are not restored to old values. Different-code restore uses the same stop/join handoff and repeats the same fenced request. The source App remains the desired configuration; inspect active release status after restore before planning another source deployment. Core rollback is not the Soot restore workflow.

Removal (app NAME delete --apply) explicitly stops only the owned process and retains roots, secrets, releases and snapshots. Retire/reroute declared domains first; DNS retirement is separate. To copy the stopped store before removing it, purge afterwards from the source file: 2srv delete -f soot.yaml --purge --apply.

app NAME delete --purge prints the removal plan; with --apply it removes the instance completely. It uses the same guards as retirement: the app-scoped operator token must still be present, no declared Domain may route to the container, and under the instance and edge locks the host refuses a live edge route, then TERM-joins a running owner exactly like retire. Only after that does it docker rm the stopped container (never forced), delete extensions/NAME (config, transactions, vault/credentials, releases and the reviewed sources) and /opt/2server/data/NAME after verifying both ownership markers, remove release and spec images no container uses (in-use images are reported, not forced) and unlink the instance lock. The CLI then drops the app's VM secret namespace, its source deployment records and the manifest entry. A foreign marker, foreign container labels or altered mounts refuse the purge with nothing removed; a retry after a partial failure is safe. Old control revisions and edge releases are counted in the summary, not rewritten; see extension purge for the audit commands. Purge is irreversible: take any data you still need first. Interrupted initialization, prepared leases, source conflicts and missing mounts require inspection, not recursive cleanup or force unlock. No database backup/DR contract is claimed: encrypted control backup excludes Soot databases and volumes. A future data backup requires a runtime-supported snapshot or stopped-store procedure; never copy a live bbolt file.

Verification boundaries

Offline schema/digest checks, mocked adapter races and an opted-in isolated Linux runtime fixture are separate evidence levels. Use DOCKER_TESTS=1 SOOT_RUNTIME_IMAGE=VERIFIED_LOCAL_IMAGE bun test tests/soot-runtime.test.ts after obtaining the exact package. The fixture uses temporary mounts, loopback ports and a local trusted TLS certificate, exercises C3/edit conflicts/restart/ restore and reads retained history through the owner API. It never publishes an image or copies a live DB. It is not public DNS, registry or staging VM evidence. Live acceptance requires a dedicated consuming-project target, provider/registry availability, owned DNS and trusted public HTTPS evidence. Keep that fixture and its credentials outside this product repository.

From a checkout, bun scripts/test-soot-vm.ts --fixture PRIVATE_FILE validates two named sources and inspects a pre-existing dedicated server before planning. The strict private JSON fixture has version: 1, nonproduction: true, server, connectionFile, an independently reviewed providerIdentityDigest, and exactly two distinct instances with name, sourceFile, planOutput, and optional reviewedPlan, restoreOutput and reviewedRestore paths. Paths resolve relative to the private fixture. A mismatched target prints only its inspected identity digest and stops; do not automatically accept that digest. --step selects plan, deploy, status, restart, restore or retire; --instance NAME narrows the scope and remote effects require --apply. This driver never creates a VM, imports/publishes a registry image, sets secrets or fabricates review.