Architecture — the rules a twin must obey
This is the concrete, checkable statement of the twins architecture. It exists so we can
review for architecture drift the same way ./conformance.md lets us
review for coverage drift: each rule below is numbered, and tagged [auto] (mechanically
enforced by scripts/architecture.test.ts, run in the gate) or [review] (judgment —
audited each cycle, can't be fully grepped).
If you want to change the architecture, change the rule here first (and its guardrail), then the code — never let the code drift away from the rule silently.
Two kinds of thing
Layers. @volter/world-core owns the shared state kernel: one entry model, checkpoints,
parent-position branches, landing, receipts, and the state system at the head. Its storage can
hold inherited and branch entries in separate files; these are segments of one logical history.
@volter/world-runtime owns process lifetime and World operations; @volter/world-host mounts
Worlds under one origin. @volter/world exposes World and TwinLog and the volter CLI.
Vendor packages under packages/twin/<vendor> implement their vendor's API and state semantics.
@volter/world-tooling provides development-only conformance tooling.
The model owns the user-facing semantics. This document owns the contributor boundaries; the recipe describes how to implement them.
This repo holds a platform and a catalog of packages, and they are different kinds of
thing built by different processes. The platform — the world runtime plus the twins kernel — is
one thing: it owns the protocol a package implements (the descriptor, the fetch adapter, the
store door, the world-store and blob seams, the world clock), the host boundaries (the Bun
shell, workerd), the resolver (covers), the installer (init, up), injection, the registry
(pack-facts) and the gate. Its defining rule is the kernel's: don't break userspace —
installed packages keep working across platform releases, so the protocol is versioned and
changes go through deprecation. A twin pack is a package: one self-contained implementation
of that protocol aimed at ONE external upstream (the real vendor), with its own version, tests,
capabilities, gate, census and page — a Debian package, a Terraform provider, a kernel driver.
Packages reach the platform through one narrow SDK (@volter/world-core) and never through each
other. A world is an install: its config is the application's external package.json and
instance.json its lockfile, naming which packages are present and (intent) at which version.
The gate follows the kind: T1 is platform CI, T0 is package CI, T2 is the archive-wide rebuild
a platform change must pass to prove it broke no package. Support status per package is the
policy/MAINTAINERS.md registry (the kernel's vocabulary); upkeep is archive-wide sweeps whose
per-package results the status column dispatches.
covers resolves detected connections as well as vendor names. The pack descriptor's existing
transport and endpoint facts describe its interface; an HTTP twin cannot satisfy a native
MySQL connection merely because both belong to the same vendor. Database URL schemes and
Prisma datasources supply connection evidence, including the app's endpoint variable. Coverage
requires wiring that variable to a declared World service; unresolved connection types or
bindings remain visible as unknown. Generated infrastructure publishes its declared connection
URLs through the existing external discovery mechanism, so an instance records the bindings
its lifecycle actually provided. Like a package requirements check, this is a static
availability and routing check, not proof that the app's workflows work or that multiple
interfaces share state. Those claims require an executed app scenario.
Generated infrastructure has two private backings behind one declared service: a container
runtime runs the compose definition, and without one volter-world-infra serves the kinds it
can without a container — postgres with PGlite, mongodb with the @volter/twin-mongodb package
over its wire protocol — at the same loopback ports and URLs, each service's bytes under the
World's data directory, so they survive down and a resuming up. A package that serves an
infrastructure kind is an ordinary twin package whose state is in the kernel; the backing runs
its CLI as a process and never imports it. Without a container runtime, a kind the containerless
backing cannot serve is refused by name.
The World manifest format owns saved selection, configuration grouping, and migration. Selection grants neither network access nor credentials.
A vendor's native and HTTP interfaces share one state owner. A native protocol frontend may translate requests into the vendor's existing HTTP state door, retaining a separate vendor session per native connection. The World owns both declared services and their ports; the frontend owns neither a second database nor another lifecycle system. This avoids racing read/derive/write sequences across independent processes over the same kernel tree.
Failure classification uses each test's diagnostic block. Bun's final recap repeats those failures and is not additional evidence. A deprecated connector failure keeps its diagnostic; an independent assertion failure or an unexplained recap entry still blocks the gate.
World manifest format
Format 2 is the emitted format. The runtime also reads legacy unversioned manifests, normalizing both shapes before execution. The config reference owns user-facing fields.
Document ownership and layout
.volter/world.json is the portable, committed intent. No spec wrapper, command-specific
autoInit section, or generic policy bag. Each section has one owner:
| path | owns | previous fields |
|---|---|---|
schemaVersion |
document format; integer 2, independent of package and protocol versions |
absent means legacy format 1 |
metadata |
{ id, description? }; stable World identity |
id, description |
discovery.selection |
usage selectors shared by init and coverage | new; not runtime egress policy |
services |
ordered service declarations; unique ids, startup order unchanged | services |
runtime.isolation |
process, colocated, or worker |
isolation |
runtime.environment |
{ values?, strip? }; declared variables and excluded inherited names |
env, stripEnv |
runtime.network |
{ egress: string[] }; exact HTTPS origins allowed for real outbound connections, otherwise denied |
network |
serving |
{ mode?, name?, share? }; mode app or bare, name <org>/<world> |
bare.name becomes { mode: "bare", name }; share becomes serving.share |
remotes |
name to World URL/path; no credentials | remotes |
scenario |
{ actors?, fixtures? }; opaque metadata, not twin mutations |
actors, fixtures |
provenance.catalog |
unchanged birth stamp { sha, protocol? }, preserved on regeneration |
catalog |
Only schemaVersion, metadata.id and services are required. Empty services are valid.
Omitted runtime isolation means process; serving mode means app. Bare mode requires name;
app mode omits it and retains the existing app-serving identity. The existing share provider
and service-selection object moves unchanged. Environment precedence, $mint, stripping,
remotes, roots and scenario semantics do not change merely because their paths change.
Each path retains its existing resolution base: cwd/colocate paths use the World root;
preloads and process-relative paths use the effective service cwd. JSON nesting changes neither.
Unknown structural fields are errors. Document- and service-level //-prefixed keys are comments;
nested structural sections reject them. User-keyed maps and opaque scenario values retain their
existing contents. Deprecated resources has no format-2 field.
An explicit network policy belongs to the World, never a program pack. The runtime
publishes it as VOLTER_WORLD_NETWORK_POLICY, bound to the instance name, for a
trusted execution boundary to consume. This value contains permission, not
credentials or transport preferences. Twin routing takes precedence; sandbox mode
still refuses untwinned traffic. The injector and scoped proxy apply this policy
to real external destinations in ordinary mode. As with their existing sandbox
refusal, this is cooperative enforcement, not a raw-socket boundary. The preload
refuses native Fetch automatic redirects under an explicit policy, because their
next hop would bypass the interceptor; manual responses remain available for a
client to follow through an authorized new request. A browser
container's trusted host applies the same policy outside its guest realms.
Omission retains the existing host-runtime behavior; an explicit empty list
denies every real external origin. This adds no inbound listener or ingress grant;
the existing declared-service/share lifecycle remains the ingress door.
The scoped proxy's local pass-through is for the World's own host. The proxy has a second
listener for its guests, published as VOLTER_WORLD_GUEST_PROXY, and a World Machine's
forwarder targets it. Every connection there is a guest's, whatever a VM's NAT makes its
source look like. So is any connection to the first listener from anything but the host's
loopback (127.0.0.1, ::1); a machine's forwarder connects from 127.0.0.2 where it
shares the proxy's host. For a guest, every destination that is not a twin, or the
application the proxy routes to, is refused when it is, or resolves to, one of the host's
networks. That means loopback, private, link-local, shared, reserved or multicast IPv4;
unspecified, loopback, unique-local, link-local, multicast, NAT64 or 6to4 IPv6; an IPv4
address mapped into IPv6 in any spelling; or an address of the host's own interfaces. The
guest is then connected to the address that was checked. A machine's host is not the
machine's network: its loopback holds the World's control doors and the twins' own ports.
Minimal generated example:
{
"schemaVersion": 2,
"metadata": { "id": "acme-web" },
"discovery": {
"selection": {
"include": [{ "usage": "application" }],
"exclude": []
}
},
"runtime": {
"isolation": "colocated",
"environment": { "values": { "GITHUB_TOKEN": "twin-fake-github-token" } }
},
"services": [
{
"id": "github",
"type": "twin",
"source": { "package": "@volter/twin-github", "version": "2.0.0" },
"execution": { "colocate": { "export": "createGithubTwinServer" } },
"endpoint": { "port": "auto" },
"bindings": { "injectEnv": "GITHUB_TWIN_URL" }
}
]
}
Selection
selection is { include: Selector[], exclude: Selector[] }. A selector is a nonempty
object with only usage? and vendor?; fields match exactly and conjunctively. Vendor names
are canonical discovery ids, including ids for unmapped services. No globs, expressions,
ordered rules or inheritance. Within each list selectors are alternatives; exclusion wins.
An empty include list selects nothing. Omitted selection uses the example's application-only
default; init materializes it so the decision is saved, not repeatedly inferred.
Usage is one of application, dependencies, build, deployment. It describes a use, not
a permanent vendor category: dependencies means package acquisition; build and deployment mean
the supporting tools. Application calls made during a build still count as application use.
Pack descriptors classify vendor-facing tool packages (for example Wrangler as Cloudflare
deployment); registry destinations are dependency use and SDK/fetch evidence is application use.
Discovery records { vendor, usage, evidence } for each use. Unclassified evidence stays
visible as unresolved; it must not be silently excluded by failing to match a usage.
Apply selectors to uses, then select a vendor if any use survives. Consequently an exclusion
of dependency-tooling npm use cannot erase an application's npm use. { "vendor": "npm-registry" }
deliberately addresses every use of that vendor. Selecting a twin still routes its configured
destinations throughout that execution environment; this is not per-request or per-process
usage routing. Different simultaneous routing requirements need separate execution environments.
Selection governs automatic proposals and coverage obligations, not whether an explicitly
declared service starts. Init preserves saved selection and never silently removes or rewrites
manual services; a conflicting proposal requires an explicit config edit.
Endpoint claims from saved services participate in regeneration conflict checks before any write.
Regeneration preserves document comments and merges infrastructure stubs by id, with saved stubs
winning. An existing seed entry point is operator-owned: preserve its bytes and report the imports
and calls for newly added default seeds rather than rewriting its order. covers reports
selected-but-missing, excluded, unresolved and declared-but-unreachable separately. Every declared
twin is checked for routing even if not detected. Excluded is not covered, and never means
real-service access or inherited credentials are authorized. Ordinary tooling exclusions need
neither questions nor individual acknowledgments. Existing explicit coverage acknowledgments
remain distinct from selection and must not conceal a fixable declared routing failure.
Service layout
Each service requires id and type (twin, process, external). Keep the ordered array:
switching to a name-keyed map would obscure the existing startup-order contract. Service sections
are optional unless the selected type requires them; their leaf semantics are unchanged:
| path | previous fields / contract |
|---|---|
description |
former // rationale; comments may also remain comments |
source |
{ package?, version? }; twin origin/constraint; version also applies to checkout-command twins without a package field, and is not the resolved version |
execution.process |
{ command?, args?, rootArg?, portArg? }; launch a twin or process; package twins may omit command and append args to the package's serve command |
execution.colocate |
previous colocate, including module/export/scenarioPath; twin factory alternative to process launch |
execution.lifecycle |
previous external up, status, down, readyWhen; external services only |
execution.cwd |
previous cwd |
execution.environment.values |
previous service env |
execution.preload, execution.controlPlane |
previous preload, controlPlane; external lifecycle services may be control-plane services too |
endpoint |
previous port, portReason, ready; World-owned listeners only |
bindings |
previous injectEnv, injectEnvTemplates, cliRedirect |
bindings.discover |
previous external.discover; external lifecycle output only |
root |
unchanged vendor root, scope, deploy and refresh policy; twins only |
A twin has either source.package or execution.process.command, not both. source.version
may constrain either form; command twins resolve it from the same entry/module package as today.
A package or
process-backed twin may additionally declare execution.colocate; isolation chooses the
execution mode using existing eligibility rules. A process requires execution.process.command
and forbids source, colocate, lifecycle and root. An external service requires lifecycle up/down;
it forbids source, process, colocate, endpoint, preload and root, and uses discover
rather than injectEnv/templates/cliRedirect for outputs. Cwd and declared environment remain
available to its lifecycle commands. Invalid combinations fail validation before any process
starts. A fixed port still requires its reason; probing and external teardown retain current behavior.
Runtime records and migration
Actual ports, URLs, pids, timestamps, minted credentials and resolved package versions remain in the ignored instance record, not the manifest. No new lockfile. Birth provenance is different from resolution: preserve the catalog birth stamp, and record what actually ran separately. Coverage against a manifest uses its selection and declarations. Coverage against an instance uses its boot-time selection and actual inventory and identifies that source in the report; editing the manifest cannot retroactively change the instance's contract. An absent selection on a legacy instance means legacy coverage behavior, never the new application-only default.
The reader accepts legacy format 1 and format 2 and normalizes both to one internal model. Unknown versions and mixed old/new layouts fail loudly. Ordinary reads never rewrite files. Migration is explicit, backed up and atomic, and reports the field moves. Preserve service order, ids, roots, remotes, handlers, seeds, comments, scenario data, credential custody and birth stamp. Legacy default twin types become explicit. Preserve legacy selection by materializing includes for all four usages; switching to application-only is a separate visible decision. Deprecated resources is reported and retained in the backup, not reinterpreted as enforced limits. Unknown legacy fields or unsupported service combinations block conversion rather than being discarded. Do not rewrite running instances or ownership records; their pinned lifecycle performs teardown.
Round-trip and migration tests must cover package twins, version-pinned checkout-command twins, app processes, external databases, bare serving, sharing, roots/remotes, environment inheritance and scenario metadata, including custom cwd with relative preloads and process paths. Selection tests must cover all usages, mixed uses of one vendor, exclusions, new discoveries, saved-policy regeneration, unknowns and pre-boot versus running coverage. The format number is independent of the platform protocol; old runtimes are not format-2 readers.
The state kernel
A protocol-2 pack reads its tree through twinResources and ownFields, and writes through
applyTwinWrite. The head selects a simulated or real state system. The kernel owns log
ordering, checkpoints, branch positions, landing and receipts; the pack owns the vendor's wire,
resource semantics and executor adaptation. Serve paths do not reconstruct state by replaying
history or maintain a second mutable truth store.
Observations compare against the upstream view: the pinned inherited history, including its
projection layout, followed by this root's observed and landed entries. This root's local writes
are excluded from that comparison and remain overlaid when serving reads. Repeated upstream
state appends nothing even after local edits; an upstream change that matches a local edit still
lands upstream. A complete listing tombstones missing upstream subjects, never local-only work.
Adapters that validate vendor identity use readParentTreeMap for that upstream view alongside
the projected tree, so a local edit cannot hide an identity collision during refresh.
A refresh scope collects both single-resource and batch observations, including empty complete listings. Nothing is appended until its adapter succeeds. The final fold owns the report and applies each service's completion declarations; a failed adapter cannot leave partial observed state behind. Collected roots remain distinct unless the caller supplies the destination root.
A durable branch position identifies an immutable history view plus an offset within it.
Views retain verified prefixes of shared log segments and their projection layout; they never
resolve an ancestor through its mutable current branch pointer. Local forks copy no log payload.
View dependencies retain every referenced storage generation until the owning World is removed. A read's fold is memoized per store and per
process or isolate, keyed by the parent and branch logs' size and version and branch.json; a hit
hands out a clone and takes no coordinator, a branch that only grew by appends extends the memoized
tree with its new entries, and anything else folds in full. Each extension records the subjects it
touched, so a projection kept beside the tree (the derived core's index by type, a pack's own image such
as PlanetScale's SQL tables) follows a write by those subjects (treeChangesSince; a pack reading one type
uses that index, twinResourcesOfType, not a filter over twinResources) and refolds only when
the kernel cannot say which changed; a projection rebuilt from the whole tree after every write makes each
write's cost, and its garbage, grow with the World. A log's reader parses only what was appended since it
last read, and on a store with readRange reads only those bytes, checking that the last entry it parsed
still stands at its offset; a store without it reads the whole file. A checkpoint records the same facts, so a
cold read trusts it without re-hashing the logs; its digests remain the check when the facts differ.
Capture and observation batches share the ancestry coordinator with log appends; no captured
view exposes a partial batch. Checkpoints bind the exact inherited view, not only row counts.
Remote pages name one immutable view; fetch verifies and publishes a complete cache head
atomically, keeping payload rows shared with previous cached views. Push compares view identity
as well as position. A local fork baseline and its tracked remote origin are distinct references.
Captured views retain the origin cursor and its layout layer, so a historical or concurrent fork
cannot borrow a newer cursor from its mutable base. Fetch records a candidate origin; rebase or
push acknowledgment replaces that origin layer while retaining inherited local layers and their
storage ownership. Fetch updates the candidate under the same ancestry lock as selected pointers;
a fetch cannot restore a pointer that a concurrent rebase advanced. An existing local fork must
inherit an origin from its base before fetching one; attaching an unrelated first origin is
refused without changing its history. A historical cut that excludes part of the origin requires pull/rebase before
push. Rebase selects a newer view while existing descendants keep their old view. Push lands entries in the parent; deploy performs
landed entries at a real-system root under its policy. See
log.ts,
head.ts and
state-system.ts.
State persists through one synchronous store seam, WorldStore: whole-file read and write,
line append, list, stat and a lock. The same kernel runs on every backend. FsWorldStore serves
local Worlds. SqlWorldStore serves a hosted World from a Durable Object's synchronous SQLite: a
request touches only the rows it reads, and no load-then-flush boundary caps a World's size.
Versions reported by stat never repeat for a path, including across restarts, because
storage caches validate on them. The active store is scoped per async context
(withWorldStore over AsyncLocalStorage where the runtime provides it), so concurrent requests
for different Worlds in one isolate never see each other's store across an await. The
module-global swap remains only for runtimes without AsyncLocalStorage, and there store-scoped
work must be serialized. A served World's doors, its changesets, roots, sealed credentials and
refresh markers go through the same store, and the doors run against a host that supplies the
World's layout and each twin's wire: a local host forwards to the twin's own port, a hosted World
calls the pack in-process. The key that wraps sealed credentials comes from the host
(setSealingKeySource), the user's key file when none is set. A World's twins never share a module
instance with another World's: local Worlds run in their own processes, and a hosted World runs in
an isolate of its own (the Worker Loader, keyed by World and code version), so a pack's module state
(a delivery registry, a cache) belongs to one World by construction.
A vendor with multiple services remains one package with service areas and shared vendor-specific authentication, errors and client code. Common storage mechanics belong to the kernel; the kernel does not impose one vendor's resource schema or query semantics on another.
Viewing a World
A World is a place a person can step into: look at it through each vendor's own screens, act in
it, move it through time and set one branch beside another. Viewing is the same on every host. The
parts a World answers belong to the served World's doors (WorldDoors) and the console; the parts
that make or remove a World (a branch) are the host's, and every host answers them alike, as
world-host and the hosted World already answer the same admin doors. A host supplies only what it
already supplies, the layout and the wires, plus where a mirror's client comes from: built at serve
time under Bun, prebuilt dist/client/ under Node (the serve seam), the Worker's
static assets on Cloudflare. A viewing capability built into one host's front and absent from the
others is drift.
The view shows and moves the World; it does not play in it. Actors, the application under test and a scripted run (a launch rehearsed across vendors) are the caller's, reaching the World through its doors while a person watches, and their reusable forms are cookbook recipes. Nothing here schedules, generates or drives writes.
- Mirrors are mounted by the World. Every served World answers
/<served>/<vendor>/mirror/for each twin that has a mirror: the pack's client and assets, keyless andGETonly. A page opened there goes to the vendor's site, the twin's own place (/<served>/<vendor>/), which a credential opens: there the shell answers any address the twin refuses or has nothing at (and the site's home), while a resource the twin answers is that resource. The page reads and writes the World's wire through a browser session. A mirror is the vendor's own screen (C1b); acting in it is a write through the vendor's API, logged like any other. The standalonevolter twin <vendor> mirror, which serves a mirror over its own twin without a World, remains for a twin on its own. - The read token can look, and the twin enforces it. A read-token holder opens a browser
session whose scope is read (the session door is answered before the read scope's refusal of
non-
GETdoors). A read-scope request reaches the twin marked read-only for that request, and the twin refuses anything that writes by the same check a read-only twin applies (D3), so the refusal is the vendor's and the pack's, never a vendor switch in the doors (A2). A derived pack already decides it by what an operation does, not the verb it came by (its classretrieve,listorcomputed, or its id in the manifest'sreads;crossCuttinginderived-core.ts), so a Slack Web API read sent asPOSTpasses and a write is refused. The marker isx-volter-read-only: 1, set on every request of the read token at a twin that enforces it (a GET that would write for its caller, as Upstash runs a command from a GET's path or Mixpanel ingests from/track?data=, is refused there too); at any other twin the read token'sGETpasses as it always has. It only restricts, so a twin trusts it without a token. To the pack, a marked request is a request to a read-only twin (the handler seam passesreadOnly), so it refuses in the vendor's own shape and takes no first-use credentials from it. Beneath every pack the kernel holds the line: a marked request runs in a read-only scope (request-scope.ts), and every append, blob write and git ref move refuses in it before anything is written, so a write a pack's own check missed (a GraphQL mutation, a git push) is refused all the same, once, never by asking the pack twice. Read-only refuses the caller's writes, not the vendor's own: a pack's catch-up (moves no API call makes) runs underrunAsVendorMoveand still walks the moves due by the World clock, as a real vendor renews a subscription whoever is looking. A pack opts in by wrapping its fetch inwithRequestScopes(derived packs throughderivedRequestScopes) once nothing it does on a request escapes those seams, and says so in its manifest (requestScopes: ['read']); only such a twin is handed the read token's other requests, and any other getsGETandHEADonly. The rule for a viewer is that observing changes nothing the application can see or do (vendor fidelity is for the application's own tokens): a read-only poll answers the current state without advancing a job or delivering its webhook, a read-only read consumes no one-time result and spends no quota, credits or rate window, and an authorize leg is refused (a viewer mints no code). Opted in: slack, github, stripe, openai, resend, aws; upstashredis, mixpanel, googleoauth, twilio, sendblue, figma and langfuse (GETs that could write, first-use credentials); tiktok, xidentity, paypal and volteridentity (authorize); xai (a deferred result is peeked, not consumed); ahrefs, scrapecreators and youtube (metering); firecrawl, tavily, perplexity, assemblyai, azureformrecognizer, fal, replicate, twelvelabs, elevenlabs, vercel, fly, svix and inngest (poll-driven lifecycles and deliveries). - A World's browser session lives on its own origin. Worlds on one origin would share it: a
page one World's twin serves could ride another World's session, and a same-origin page can even
script into another World's mirror in a frame, so nothing short of separate origins separates them.
A host that gives each World an origin (
browserOriginon the host;<world>--<org>.localhoston world-host andvolter world view,<world>--<org><WORLD_ORIGIN_SUFFIX>hosted) serves that World and the console there and nothing else of its own; the World honours its session cookie only there (host-only,__Host-over https), and answers a session asked for elsewhere with 409 and its origin, which the console follows, carrying its token in the URL's fragment. A host's admin acts on its own origin, never a World's. On every host, with or without origins, a session-authenticated request that is not a read must carrySec-Fetch-Site: same-origin. Path addressing stays for callers that present a token, whose credential is never ambient. - A local World asks for no token. A World this machine serves on loopback is the person's own:
its pages never ask for, show or take a pasted token, and a token is asked for only by a World
served for others (a hosted World, a team host, a non-loopback bind). What stands in for the token
is proof that a request is the World's own page, as Vite checks its dev server's Host: any website
can send requests to 127.0.0.1, so the local host's session door (
localTruston the host) grants a write session to a request that presents no credential only when the World has an origin of its own, the request's Host is a loopback name (a rebound DNS name is not), its Origin is exactly that origin (another site's page cannot claim it) and it saysSec-Fetch-Site: same-origin(one that does not fails closed). With--no-originsevery World's pages share one origin and none is handed a session: the console takes the token in the URL's fragment, as before. The console asks for the session first, on every open, so a restart, a cleared cookie or a rotated token is picked up without the person; the token stays in the World's files for apps and scripts. A session is an opaque id the World keeps, never a token, and rotating the tokens ends every session. The claim is not a secret. Anything that can reach the World's loopback port can make it: this machine's other accounts, a container or a WSL or VM guest that reaches the host, a port forward, a sandboxed agent on this machine. A local World trusts whatever reaches its port, as Vite's dev server does (Jupyter, by contrast, keeps a token by default), so its port is never forwarded or exposed; a World served for others binds a non-loopback--hostand asks for its token. On loopback read-only is a courtesy, not a boundary: any page of the World can open a write session. A browser's session deploys to the real vendors only what the request names (confirm), so no page, link or path deploys by being opened. A launch nonce the CLI opens, exchanged once for the session, would narrow the claim to the browser the CLI opened, at the cost of asking again on a fresh open; the owner's call, not made. On loopback,volter world serveandvolter world vieware the same front;viewalso opens the browser. - The clock is a door. A World's clock lives in its store on every host and the doors answer
clock(show; set and advance with the write token), so a hosted World runs on scripted time as a local one does. A local World hands each twin process the clock's path; a hosted World's isolate, which is that World's alone, sets the same variable for itself. Advancing stays an explicit act, never a drift, and the clock moves only forward: never before its set instant nor, unset, before the World's newest entry. A pack's catch-up stamps each move at its due time, so a clock set back would put new entries before old ones. A World is taken back in time by branching, never by its clock. Advancing is instant and realizes nothing itself; each twin's catch-up walks what fell due on its next request, so the first request after a long jump pays for it. - The past is a branch. A World as of an instant is a branch made there: the
World's history door cuts each twin at the instant, and the host's branches door makes a World
cloned from those cuts, its clock frozen at the instant so no twin's catch-up walks it on, and
viewed like any World. A console's "as of" view is such a branch, short-lived: removed on request
or when its time runs out, and until then kept (a local host mounts it again when it restarts).
Local hosts make them with
LocalBranchesbeside the Worlds they serve (world-host under its directory,volter world viewunder.volter/branches/); a hosted World's supervisor makes its own and removes it on its alarm. The vendor wire answers only the branch's current state; no pack answers an as-of read. An instant is only as fine as the clock: under a frozen clock, entries stamped at one instant are all before it or all after it. - Branches are compared, and changes move by changesets. The doors answer a branch's difference
from its base (
diffWorld). A change moves from one branch to another as it does today: cut into a changeset, pushed and landed. There is no cross-World replay: a hosted World cannot reach a sibling World. - The timeline is the World's logs, merged. Each twin keeps its own log; a door answers the
entries of all of them ordered by each entry's
occurredAt, then by twin and position, paged, which the console renders beside the mirrors. The World keeps no order across twins finer than its clock (under a frozen clock many entries share an instant), and the timeline claims none. It reads the logs and folds nothing. - A cause is followed by the tracing standard. A World links writes across vendors by W3C
Trace Context, the way distributed tracing links services, never by an id of its own. A twin
records an incoming
traceparenton the entries a request causes, and a delivery a twin makes (a webhook) carries thetraceparentof the entry that caused it, so the application's handler continues the same trace. An application instrumented with OpenTelemetry already sends it on its outgoing calls; the vendor's wire is unchanged, since real vendors ignore the header. Without it the World knows only order, and a view says so rather than guessing a cause. - The console is the view, and it has several. It reads nothing but a World's doors (it holds
no data path a
curlcould not call), and each view is another reading of the same doors: the map (every twin and what it holds, counted from its tree by resource type, each linked to its mirror), the timeline, a trace (one cause's entries across vendors, from their sharedtraceparent) and the mirrors side by side under one clock. A view never gets a private door: what it needs is added toWorldDoors, where every host answers it and every other view can use it. The console also shows the clock and the branches. A host that mounts it (world-host at/-/console/) shows every World it serves; a local World that is not hosted serves it for itself by its own command beside the World, never by a step added to the runtime'sup.
The dashboard reviews what a World would do and manages its branches. What a World changed is cut into changesets (its changesets door: list, verify, approve, deploy). The dashboard shows them as a reviewer reads a pull request: what each changes at each vendor, its checks, its approvals and its receipts, with Verify, Approve (signed as the person the session names) and, for a shared World with a real root, Deploy, each refused where the session's scope or the World's rules refuse it. Branches are made, reset and compared there as a database's are: a branch as of now or an instant, reset to its parent, and the compare view between a branch and its parent. Every action is the World's own door; the dashboard adds no rule of its own.
The hosted product
Volter World is one open product that runs on a laptop, on its owner's server and on Volter's cloud, as PostHog and Cal.com are: people, orgs and sign-in are part of the open product, and billing is the one part that is Volter's alone. Four layers, each usable without the one above it:
- The World (
world-core,world-runtime) knows nothing about people. Its doors decide one thing about a request: the grant it carries, which World,readorwrite, and who. - A host (
world-host, andapps/cloudon Cloudflare) serves many Worlds at one URL. It knows Worlds, their keys and the owner org recorded when each was made, never people or membership. Its admin doors (inventory, make, remove, keys, import) are one contract, written once over a storage interface, with a Node adapter (files) and a Cloudflare adapter (Durable Objects); a door or rule on one adapter and absent from the other is drift, as it is for viewing. - The platform (
apps/platform, open, self-hostable) is where people and orgs are: sign-in through an access provider, orgs, members, invitations, tokens for its own API, the hosts it has enrolled, and opening a World for a person. It reaches a World only through the host's doors, never its storage, and holds each host's admin credential in a secret store. It runs with no biller: then nothing is limited and nothing is priced. - Billing (
apps/billing, Volter's) is plans, checkout, usage, quotas and reminders over Polar. The platform asks it what an org may do and passes its doors through; the platform names no plan, price or meter itself.
Three surfaces, one kit.
- The dashboard (
apps/console) is a World's pages: its vendors' screens, overview, branches, changes, activity, keys, the clock. It runs everywhere a World does and is the same page in every mode. It holds nothing about orgs, members, plans or money, and shows nothing that does not work on a laptop: no greyed-out feature, no upgrade prompt, no pricing link. Opened from a platform, its one link out is back to that platform (the issuer of the pass it verified). - The platform's pages (
apps/platform/client) are sign-in, orgs, an org's Worlds, members, settings and tokens, and a Billing page when a biller is attached. - The front door (
apps/www) is the static site: landing, pricing, docs and legal pages. It is the only surface that carries analytics or marketing tags; the platform and the dashboard load no third-party script. - The kit (
@volter/world-console/kit) is the brand's tokens, the header and the shared components. The platform's pages build on it; nothing in it knows an org or a price.
The dashboard's package depends on neither the platform nor billing, so neither can reach its bundle; the platform reaches billing only through the questions it asks.
The platform's state is SQLite. People and orgs (where the platform keeps the directory), the
Worlds each org holds, tokens, sessions, the audit log, webhooks and their deliveries, and the
biller's records are tables in one SQLite database, written through one schema (Drizzle) whose
migrations are plain SQL checked in beside it. The same schema and queries run on three drivers: a
file under the platform's state directory with Bun's bun:sqlite or Node's built-in node:sqlite
(the published platform needs Node 22.13 or later), and on Cloudflare the SQLite of one Durable
Object, which serializes writes and gives transactions. A write that must hold together (the last
admin, a key and its record) is one statement or one transaction, never a read then a write the
platform hopes is still true. A World's own state is not the platform's: it stays behind
WorldStore.
Access providers are optional. A World on this machine needs none: its page is trusted by loopback. A World on a host opens by key. People sign in only where a platform runs, through the provider its operator configures:
- Volter (
volter): Volter Identity (id.volter.ai). Sign-in, and the directory: people, orgs, memberships, roles and invitations are the identity service's, reached through its product door with the platform's own client credential. Volter's cloud runs this provider. - OpenID Connect (
oidc): any OpenID Connect issuer (Google, Microsoft Entra, Okta, Keycloak). Sign-in only; the platform keeps the directory itself, in its state. - GitHub (
github): a GitHub OAuth app, or one on GitHub Enterprise Server. GitHub is not an OpenID Connect issuer: the person is GitHub's own record of them, read once when they sign in, with their primary verified address, and keyed by their GitHub id, which a rename does not change. Sign-in only; the platform keeps the directory itself.
A platform has one provider, which brings its directory (Volter Identity's is the identity service; an OpenID Connect or GitHub provider's is the platform's own). The dashboard never talks to a provider: it trusts the passes of the platforms its host enrolled with, whichever provider signed the person in.
Access is decided in one place. Every door of a World asks one module (world-access) for the
request's grant, and a grant comes three ways, which a host is configured to accept:
- This machine: a loopback World trusts its own page (the local-trust rule under Viewing a World).
- A key: a World's key, named when made and revoked alone, for an app or a script. A World's existing token is its first key; keys are what an app holds, never what a person signs in with.
- A pass: a short-lived grant the platform signs for a person, naming the World, its own
origin (the one place the pass opens), the scope (
writefor a member acting as themselves,readfor a support session or a token that may only read) and the person's subject, checked against the public keys the platform publishes. A host trusts only the platforms it was enrolled with, over https (or http on loopback). A pass opens a World session only at the World's own origin, and only for a World that has one; a World sharing an origin opens by key. The platform never hands a person's browser a World's key; it issues passes. The command's key comes only to a token (volter login), the write key only to one that may write Worlds.
A World may be kept to some of its org. By default every member of an org reaches its Worlds. An org admin can restrict a World to named members (admins always reach it), as a repository or a database is restricted elsewhere. The platform decides it where it already decides membership: it signs no pass, hands no key and lists no World to a member the World is kept from, and taking a member off a World revokes the World keys held for them there, as leaving the org does. A host needs no new rule: it trusts the platform's passes, and the platform no longer signs them.
Every World has an owner from its making. A World is made through the platform, which records it in an org and asks an enrolled host to make it with that owner; the host keeps the owner with the World and answers it in its inventory. A World made directly on a host (self-hosted, or before the platform ran) is claimed into an org once, by the platform's operator, and the host records the owner then; a host that already records another org refuses the claim. Its existing token stays its first key, so its apps notice nothing.
People arrive at the provider. Signing up and signing in are the provider's (a new person is
sent there to make an account and comes back signed in); the platform keeps its own session and
nothing of the credential. volter login signs the command in through the browser: the platform
shows the command a code (the device authorization grant's shape), and the person, signed in as
above, approves it on the platform, which hands the command a token of its own for the platform's
doors: named for the machine, scoped to what the command does, expiring, revoked alone or by
volter logout. A World the command reaches (volter remote add <org>/<world>) is reached with a
key made in that World for it, shown once and revoked alone in the World's settings. The command
holds keys, as gh, the Vercel CLI and neonctl hold theirs; passes are for browsers, since a pass
names a World's own origin, which the command never uses.
The modes are the same code.
| Laptop | Self-hosted | Volter's cloud | |
|---|---|---|---|
| Dashboard | yes | yes | yes |
| Platform | no | optional, with the operator's provider | yes, with Volter |
| Billing | no | no | yes |
A local volter world view is a World with this machine's grant and no host or platform. A
self-hosted host takes its admin token and keys, and passes if its operator enrolls it with a
platform (Volter's, or one they run). Volter's cloud is the Cloudflare host enrolled with Volter's
platform, which runs the Volter provider and the biller. Nothing in the World, the host or the
dashboard branches on which.
The way in is the app's folder, for a person or their agent. The standard setup starts where the
code is, not in the dashboard: volter world init detects the app's vendors, volter login signs the
command in, and volter remote add origin <org>/<world> links the app's World to one on the platform,
making it there from world.json's vendors when it does not exist yet (asked, or --create for a
script or an agent). The dashboard's Create World stays for a person without a terminal. volter open opens a World's dashboard, local or hosted. The CLI installs from npm or a one-line script,
says when a newer version exists, and completes its own commands in the common shells.
Agents are first-class clients of the same verbs. volter mcp is an MCP server over stdio whose
tools are the CLI's verbs (status, up, down, view, log, branch, reset, link, push) called through
the SDK, never a second implementation: locally with the World's own token, on the platform with the
person's token from volter login. volter agents install puts the skill (skills/volter-world,
shipped in the package; it covers running Worlds on this machine and linking them to a platform) and
the MCP server into the coding agents it finds (Claude Code, Cursor, VS Code, Codex), and volter world init offers it. The front door publishes llms.txt, llms-full.txt and each page as
Markdown beside its HTML. Where a person starts (the landing page, the platform's Get started, the
dashboard's Connect) they can copy a prompt for their coding agent that does the setup above.
CI runs in a World, and a pull request gets its own. A GitHub Action in this repo
(actions/setup-world) installs the CLI, brings up the app's World from world.json and runs the
job's command inside it. Given an org token, it makes a preview World for a pull request as a branch
of a shared World on the platform, comments the preview's dashboard link, and removes it when the
pull request closes. A preview is an ordinary branch (its lifetime, keys and owner are a branch's);
the Action with an org token is the integration, as Neon's branch actions are, and no GitHub App is
needed. Other CI systems get a documented template.
A new org sees a World working. Creating an org offers a sample World: the platform makes
<org>/sample with a few common vendors and seeds it through the vendors' own APIs, from a story,
so its screens show an app's work before the person has written any. A World's dashboard can hand
out a read-only link (a read key, revocable, in the link), and follows a theme the person picks as
well as the system's.
Where this stands. Built: the four layers and three surfaces above (the dashboard's bundle
carries no org or billing code; the platform serves its own pages on the kit; apps/billing
attaches through the biller contract; the front door is a static site); the three providers and
their directories; every door's grant decided by world-access; passes, signed by the platform and
spent at a World's session door on world-host and the hosted World; owners recorded on both hosts
from a World's making or its claim; signing up at the provider; volter login, whoami and
logout; named keys; per-World origins on a host reached at its own URL (--world-origins); the
platform published and run under Node. Not yet: the front door is not deployed (no Pages project,
domain or analytics id is set).
Protocol 2: the pack is a plugin
A pack at protocol 2 is exactly seven things, each with a mechanical gate, and nothing else: the
descriptor, the wire, the tree contract, the engine slot, the real-system adapters, behaviour and
evidence. The kernel owns the log, checkpoints, branch positions, landing, receipts and the head;
the pack owns the vendor's wire, its resource semantics and its adapters onto the vendor. A pack
that passes the gates is protocol 2 by construction; one that does not is not, whatever else it
does. Standing is pack.protocol read against PROTOCOL_VERSION (protocolStanding in
packages/world-core/src/packRegistry.ts): the current major is current; the previous major, and
an undeclared protocol (read as the previous major), serve under deprecation; any other major is
refused.
The recipe shows the shape file by file; this section is the law it
implements.
The descriptor
pack: TwinPack, exported from the pack's index.ts; the field docstrings in packRegistry.ts
are the field-level law. Beside identity, transport, archetype, resources,
hosts/hostsNone, endpointEnv/endpointEnvNone, adoption and rateBudget, a protocol 2
descriptor carries:
protocol: '2'.refresh: { every?, webhook?, onDemand?: { atMost } }— how a root is kept current.everyschedules a pull in a served world (the root's ownrefresh.everyoverrides it);webhooksays the vendor pushes to the ingest door;onDemand.atMostis the least time betweenvolter twin <vendor> refreshruns unless forced. Any other key fails the gate.pullPostureremains the scheduling posture the v1 pull tooling guards (assertContinuousPullAllowed);refreshis what the served world reads.stateSystem: { perform, refresh?, ingest? }— the pack's half of the real state system, named on the descriptor, withregisterPack(pack)called on load ofindex.ts, so the head resolvesstateSystemFor(vendor)in the process that serves the twin. A protocol 2 descriptor with nostateSystem.performfails the gate, and the runtime refuses to bind it from anchored exports (adaptersForinworld-runtime/src/root.ts; that fallback is protocol 1's). Anindex.tsdeclaring protocol 2 that never callsregisterPack(fails hygiene (scripts/pack-hygiene.ts).roundTrip— one minimal write on the vendor's wire, or a short sequence whose last write creates something new every time. It is what the branch round-trip sends.references— which fields of which subject types hold another subject's id (referenceField(type, field, to), or{ type, to, key, adopt }), withreferenceTrip: a write that references theroundTripwrite's subject,{{field}}standing for the parent's field.shapeParity: 'held'once the write handler and the refresh adapter store the same shape, andparityOriginwhen the refresh adapter reads its scope from the origin.auth— how the vendor reads its credential when it is not a replaced header (D4).engine: { module }when the pack's state has a second half beside the tree.
Gate: scripts/protocol-2.test.ts (the descriptor's shape), scripts/pack-facts.ts (regeneration;
an undeclared archetype and a refused protocol are errors), scripts/pack-hygiene.ts
(registration).
The wire
One fetch factory, create<Name>TwinFetch({ root, readOnly, … }), returns a
(Request) => Promise<Response>: a handler from the request and the tree to a response.
createTwinFetchFromHandler (packages/world-core/src/twin-fetch.ts) is the one adaptation; a
wire that exceeds it writes its own fetch. Exactly one export matching create*TwinFetch per
index: the catalog's walkers (scripts/vendor-fetch.ts), the branch round-trip and shape parity
find the pack by it. The local server is that closure served through the seam.
The serve path is the import graph from that factory's module: every module it reaches by relative import, transitively. A file-name convention is not the rule. On the serve path:
- Reads go through the kernel's tree readers (
twinResources,readTree), history throughsubjectHistory, writes throughapplyTwinWrite, observations throughobserveResource. No serve module reads or appends a log row (listEvents,listActions,pendingActions,appendEvent,recordObservedDelta,syncPull). A pack's refresh, ingest and connector code reachesobserveResourceandapplyTwinWriteand nothing that reads or writes a row either. - No request-time egress (
scripts/architecture.test.ts); no clock butworldNow()and no randomness in served content (R9,scripts/invariants.ts). An application process a World serves reads the same time: the injector gives it the World clock, frozen or running, asDate—world-clock.cjsis the one home of the clock file's two forms; a World serving an application moves time withvolter-world clock shift, since a frozen instant stops the application's time. So the clock rule also covers what a twin signs, stamps or checks outside a served read — a delivery's signature timestamp, a JWT'siat/exp, a verifier's tolerance window:scripts/architecture.test.tsrefuses the machine's clock in every serve and delivery module of a pack, and names each remaining use (the real-vendor roles, a browser-side helper) with its reason. The one exception to the egress rule is a call that stays inside the World because the vendor's own topology makes it: a queue delivering to the application's endpoint or a proxy forwarding to its target. Each such module is named with its reason in the gate's allowlist, and applies the World's egress rule itself throughworldEgressRefusal(@volter/world-core/network-policy), because the twin host runs without the injector: internal addresses pass, strict egress and the published network policy refuse the rest, exactly as the injector decides for applications. - A served value that must agree with the application's own env (a signing secret the application verifies with)
is read through
worldEnvValue, which answers only the variables the World sets itself (VOLTER_WORLD_ENV_NAMES, written by the runtime); what the caller's shell passes through never becomes World state. Two Worlds with the same configuration serve the same value. - The kernel's
RefusedWriteErrorandVendorWriteErrorreach the wire as the vendor's own error body and status throughanswerVendorErrors(fetch, shape); aHeadErroris a 500. A create's answer is built from the resourceapplyTwinWritereturns: under live use its id is the one the vendor minted, and the local id rides as an alias.
Gate: scripts/protocol-2.test.ts (log rows on the serve path), scripts/architecture.test.ts
(egress), the R9 row of scripts/invariants.ts (determinism).
The tree contract
- Every subject type the pack writes is declared in
resources; every write names type, id and fields. Bookkeeping the vendor never serves lives under_-prefixed types or fields, which shape parity leaves out by definition. - A module-level mutable map, set or
leton the serve path is refused unless its line says// cache:or// counter:with the reason. The tree holds state; a module holds nothing. - A resource's own
id,typeandupdatedAtcome back throughownFields(resource). The kernel keeps the subject's address on the row and the vendor's same-named fields beside it on a non-enumerable property, so a spread,Object.entriesor JSON of the row serves the address and never the vendor's field. - A delete is the tombstone the write carries (
deleted: true). It holds until the subject is written again; a create under the same address resurrects it, and the write need not say so. - A sequence (an issue's open↔closed transitions, a changelog) is
subjectHistory— the entries that touched the subject, in order — never an accumulator a fold keeps. A set (watchers, votes) is one subject per member (<issue>::<account>), never a list one subject overwrites. - An id is minted from the tree: the next in its prefix over the ids state already holds, with
subjectHistorycounting the ids once minted and since deleted; never a module-level counter or a row count. A branch and its base minting the same id from the same tree is the expected outcome of determinism, and a receipt whose vendor id differs rebinds the subject.
Gate: scripts/protocol-2.test.ts (module truth), the branch round-trip (the tree through a
checkpoint and a branch), shape parity (_-prefixed fields aside).
The engine slot
A pack whose state has a second half beside the tree that is not a projection — a git plane, S3
bytes, a SQL engine, a file's content — declares engine: { module }: the one module that owns
every write outside the world store. The tree references engine objects by hash. A file or
database write (writeFileSync, appendFileSync, Bun.write, renameSync, mkdirSync,
new Database(, PGlite() on the serve path outside that module fails the gate. Bytes go through
the blob store (getActiveBlobStore). A
git-hosting pack mounts the kernel's git library (packages/world-core/src/git/: loose objects,
packfiles, refs as world state, smart HTTP as serveSmartHttp) over the blob seam rather than a
git binary; github's plane (github-git-plane.ts, github-git-http.ts) is its consumer.
Gate: scripts/protocol-2.test.ts (writes outside the world store).
The real-system adapters
The three adapters stateSystem names (StateSystemAdapters in
packages/world-core/src/state-system.ts), each over the kernel executor's RemoteExecute seam
(method, path, headers and body in; status, headers and body out) and never over the network
directly. The catalog anchors their names to the fetch factory's <Name>:
perform<Name>Action(execute, entry, ctx) → PushOutcome— perform one entry against the vendor and return the id (and url) the vendor minted.ctx.resolve(type, localId)is the kernel's alias resolver; a pack never reads a ledger to find a vendor id. The head (head.ts) owns the loop: checks, the perform, the landed copy with its receipt; replay is by receipt.- the refresh adapter,
(execute, { root, origin? }), by conventionsync<Name>FromRemote— observes resources throughobserveResourceand returns a count. It never appends: the kernel diffs each observation against the upstream view and folds the batch onto the root's log, andcompletenames only a type the adapter listed in full. A credential the vendor refuses is a thrown refusal, never an empty account folded over observed state. ingest<Name>Event(request, { root, secret })— verifies the vendor's signature with the sealed signing secret and observes what it verified. It fails closed: with a secret present, a missing or invalid signature answers 401 and folds nothing. The ingest door is keyless and internet-facing; the signature is the credential. A looseingest*name is vendor surface, never an adapter.
The credential never enters pack code: the executor applies it by the declared strategy (D4).
Every live call is priced by the pack's rate budget (D8). The connector's sync<Vendor>FromReal
(D7) is the pull over an injected vendor client that the refresh adapter wraps over the kernel
executor.
Gate: scripts/protocol-2.test.ts (adapters named), adaptersFor (a protocol 2 pack naming no
adapters is refused at bind), executor.test.ts and scripts/sigv4-conformance.test.ts
(credential by strategy), scripts/rate-budget-isolation.test.ts (the budget guard).
Behaviour
The scenario adapter (scenarioStatus on the fetch adaptation, the pack's scenario engine over
the kernel's) and the emitter (TwinPack.emitter), as the recipe describes
them. Faults are the kernel's grammar (adding a twin).
Evidence
The capability manifest with failable verify(), the mutation teeth and conformance (the recipe,
§6 and §7), plus two steps the kernel runs against every protocol 2 pack with no pack code:
- The branch round-trip (
scripts/branch-round-trip.test.ts) sendsroundTripat a fresh root; the tree holds what it made; a checkpoint is cut and a read through it equals a read without one; a branch is taken and starts where the base stands; the write goes again on the branch, and the branch's entries are its own, unpushed and absent from the base; the base moves again and the branch's base position is behind the base's log — a claim about positions, never about tree bytes, because a base and a branch minting the same id from the same tree are legitimately identical; a rebase then moves the branch with no conflicts and drops none of its entries. Withreferences,referenceTripis sent, the parent is adopted the way a landing adopts it, the child's reference reads as the adopted id, and a later write naming the parent by its local id is accepted and resolved. - Shape parity (
scripts/shape-parity.test.ts) sends the round trip's writes on one root, runs the refresh adapter over that same wire into a fresh root, and compares every observed type's subjects field by field (_-prefixed fields andupdatedAtaside). Advisory until the descriptor saysshapeParity: 'held'; asserted after. A pack with no refresh adapter has one mapping and nothing to compare.
Shape parity runs in T0's catalog step; the protocol-2 gate and the branch round-trip run in the
catalog-wide scripts/*.test.ts sweep of the full gate. Gates owns the cadence.
Indexes, never a smarter fold
The fold is last-write-wins per field (foldEntries in log.ts), the same for the simulated and
the real state system, so the two never diverge in it. A derived lookup — by type, by a declared
field, a count the wire serves — is an index over the checkpoint or a field stored on the write
that changes it, never logic in the fold.
Alias-aware lookup at the request boundary
After an adoption a caller on a branch may still address a subject by its local id. The serve
path resolves a local id through resolveSubjectId(service, type, id, root) — the alias map the
landed copies carry as aliasOf — once, where the path or the params are parsed, never per
lookup. Adoption is that alias and nothing more: a declared reference is resolved through the map
in the tree, and rewritten through it before the pack's perform sees the entry, so there are no
explicit adopted rows. The round-trip gate sends the referencing write by the old id after
adoption and expects it accepted.
Shape parity
A pack has two shape mappings: what a local write stores and what an observation of the vendor
stores. They are equal when a parent's derived fields (a comment count, a last comment, a thread's
reply summary) are stored on the write that changes them, never derived at read, and when a local
create stores the vendor's full default shape, not a compact one. Observation cursors use vendor
fields distinct from kernel metadata (observedUpdatedAt for GitHub), so ownFields stays the
vendor's. The parity gate refreshes every twin from its own wire, with the credential its round-trip
writes presented. A refresh refuses by throwing, never by returning an empty observation (a returned
result marks the vendor observed); one that cannot be exercised for effect from the twin itself (an
identity pull the twin's wire refuses without a consent-minted token, a pulled id inside the twin's
reserved local namespace) declares the refusal it answers with (refreshRefusal), and the gate
passes it on that refusal alone. Subjects only one side holds are reported apart from field
differences.
The serve seam
A pack serves through the kernel's HTTP seam: serveHttp(options) from @volter/world-core
(packages/world-core/src/serve-http.ts), in Bun.serve's option shape (hostname, port,
fetch, idleTimeout, tls, error), returning { hostname, port, url, stop } asynchronously,
because binding a port is asynchronous on Node; url is a URL whose string form ends in /. It listens on loopback unless the caller names a hostname: a World reaches its twins over loopback, and a front meant to be reached (a served world, a host, reflect) names its own.
Bun-backed under Bun, node:http-backed otherwise, with node:http reached through
process.getBuiltinModule and never a static import, because the module rides into mirror client
bundles. WebSocket upgrade is part of the seam: the portable upgrade: { accepts, open, message, close } callbacks serve on both runtimes, on Node through ws, the one place the kernel loads it
(B1). The request journal decorates the handler through the seam. A pack's serve factory
(create<Name>TwinServer; serveExport on the descriptor when a pack exports several) is
therefore async, its bin says #!/usr/bin/env node, and a Bun.serve( call in a pack that
never uses serveHttp fails hygiene. A pack's other runtime-specific needs go through the
kernel's helpers: fileResponse(path) for a file as a Response, bundleClient(entry) for a
mirror's client bundle (built at serve time under Bun, served prebuilt from dist/client/ under
Node), nodeBuiltin(name) for a Node builtin. Volter's own pages (the console, the UI kit's shell) serve the
Volter brand's tokens and faces with brandTokensResponse(dir, rest), from a gitignored directory
scripts/brand-tokens.ts fills at install, pack and image build; nothing fetches the brand while a
world runs. A raw-TCP twin's Bun.connect/Bun.listen is
named in its README. Its protocol is also exported as create<Name>TwinStream, one connection over
any byte transport (the kernel's TwinStream: greet through a sink, bytes in, settled once they
are answered): its listener hands it a socket's bytes, and a hosted World, which takes no inbound
TCP, a WebSocket's, which the attacher bridges to a loopback listener
(@volter/world-core/stream-bridge). A twin that answers WebSocket upgrades on its vendor API keeps
its relay host-neutral, so a hosted World answers them with a WebSocketPair.
One cause is followed across a World's twins by the tracing standard, W3C Trace Context, never an id
of the twin's own (packages/world-core/src/trace-context.ts). The kernel's request adaptations
(createTwinFetchFromHandler, createDerivedFetch) run the handler inside a valid incoming
traceparent (parsed strictly: a malformed value, version ff or an all-zero id is none); a pack
with its own fetch wraps it in runWithRequestTrace. Every entry appended there records it as
traceparent, authoring metadata like correlationId: kept in the log, excluded from replay
identity. A delivery a write causes carries deliveryTraceHeaders(cause?), a child of the cause
(same trace-id and flags, a new parent-id): the cause is the request being handled, or, for a
delivery made later (a queue's drain), the entry that caused it. No cause, no header. Nothing else
on the vendor wire changes: an answer gains no header and a request without one is served as before.
The publishing pipeline
A pack is published the way the platform packages are: a tsconfig.build.json beside its own
(tests, uitests and generated/ outside the root set), build/prepack/postpack scripts
pointing at scripts/publish/, and dist in files. scripts/publish/build.mjs emits
dist/src (.js and .d.ts, relative .ts specifiers rewritten to .js), carries every non-TS
file under src and every files asset beside it, and bundles each top-level client/*.tsx into
dist/client/<name>.bundle.js; prepare-publish.mjs turns exports and bin from src/*.ts to
dist/src/*.js and workspace: ranges into versions inside npm pack, and restores the live
manifest after. In-repo consumption stays from source. The runtime reaches an installed pack by
its manifest — exports['.'] for the module, bin for the cli (packEntry, packCli in
world-runtime/src/catalog.ts) — and runs it with its own process.execPath, so one resolution
serves the checkout and the published form. The tutorial registry installs every pack built, so a
page red on a built pack is the pack's build being wrong, never the page's.
Every push to main is released: the publish workflow moves each public package changed since
its last version (and every package that pins it through workspace:) to its next patch, then
runs scripts/publish/publish.mjs, which packs every version npm does not have yet (siblings
first, through pack.mjs) and publishes those tarballs, and commits the versions back to main.
Protocol 3: the derived pack
Protocol 3 is the shape every pack moves to. A pack is derived when it has src/manifest.ts and
src/generated/. Four exemplar packs defined it: openai, github, stripe and slack, chosen by
demand and so that together they exercise every capability the platform has (streaming, WebSocket,
GraphQL, an RPC wire, the engine slot, webhooks, UI, real roots). Two packs it was not designed on
tested it: aws, through its Secrets Manager API (a Smithy model, operations named in a header, SigV4),
and smtp (a line protocol over a byte stream, with an RFC for a spec). Both are built to it; what they
forced is in the rules below (other wires). The rest
of this document governs every pack, and where a rule below replaces one above, it says which. A
derived pack runs on protocol 2: everything it adds to the kernel is additive, so protocol 2 packs keep
booting beside it until they move (migrating a pack).
A derived pack hand-writes only how its vendor behaves. Everything the vendor's spec states is generated: a derived core from the spec and the manifest, a hand-written semantics layer over it, the engine slot when the vendor has state beside the tree, and the vendor's UI surfaces. The kernel's log, checkpoints, branches, landing, receipts and head are unchanged.
A pack is done when its report says five things (the report): its one customer life is judged plausible, no check on any step of the life fails, every example the vendor published for an operation it serves is replayed and judged, the life and the examples reach every hand-written line and every declared move and refusal or the pack records why no World can, and no operation of its spec is served by anything but a semantics handler, the derived core, or the vendor's own answer for an operation the twin does not model.
Layers
A request passes the wire (the facade); a write becomes an entry and is decided by the twin's
state system, simulated or real, as the model defines them and
head.ts chooses them: a real head under deploy: auto performs the entry before the app is
answered; any other head lets it wait. Reads are served from the tree in both.
- Wire: derived. Routes, dispatch, request validation, response shapes, status codes and the error envelope, from the spec. It is the same over both state systems.
- Write semantics (actions and state transitions) decide a write's effect. They run whenever
the head does not perform the entry now: with no root, and under a root whose policy is
gatedorhold. A real head underautoruns none: perform resends the entry's request to the vendor, with local ids translated through the alias map and declaredreferences, and the vendor's answer is the effect. The landed copy stores that answer on the subject the entry names (head.ts); what the write did to other subjects (a merge moving a branch) arrives by refresh or ingest, never by local computation. - Read semantics (
computedvalues and vendor query languages such as GitHub's search syntax) derive answers from the tree. They run in both state systems, over simulated state or observed state. A computed value the vendor returns as a field of a resource is stored on that resource by the write or the observation that changes it (shape parity), never derived at read. - Non-resource operations. Stateless compute is performed like any entry under a real head
with
auto, whose model promise is that the app is pointed at the vendor: the entry's subject is the call itself, and the vendor's answer is its effect. Under any other head it is answered through behaviour (a deterministic stub or a scripted scenario, as D4 rules). State beside the tree (git objects, blobs) is answered by the engine slot under every head.
Under a real head with gated or hold, the simulated semantics answer the app, and deploy
performs the entry later with the outcome the model gives deploy: a receipt lands on the entry,
and a refusal or failure stops the deploy. Only auto reverts.
The entry
Entries already carry the vendor operation (operation) and the raw request (input, not
projected) beside the effect (fields, projection). A derived pack keys operation by the
spec's operationId and records the path and query parameters in input, so the entry is enough
to resend the request. Replay folds recorded effects and never recomputes them: a change to a
pack's semantics never changes what an existing log folds to.
State
A subject is stored in the vendor's own response shape, as the spec's schemas define it, which is
the shape parity rule. The state schema is therefore derived, and an observation
needs no mapping. Bookkeeping stays under _-prefixed types and fields, and never reaches the wire:
nothing a pack keeps for itself is answered, under any name.
An operation answers the schema its spec names. Where that is a view of a resource (GitHub answers a
list's users as simple-user, a list's pull requests as pull-request-simple, a search hit as
issue-search-result-item), the answer is the view: the fields the spec gives it, taken from the
resource, never the whole resource. A schema composed over a resource with allOf answers the
resource's fields and its own (ruleset-version-with-state adds state).
The derived core
Generated from spec/ and manifest.ts into src/generated/ by scripts/derive-pack.ts, through
the spec IR (packages/world-tooling/src/spec-ir.ts), committed so that a spec update is a
reviewable diff, never hand-edited. The spec is an OpenAPI document (2 or 3), a Smithy model, or, for
a line protocol, the RFC's own text (other wires). The
surface is data (surface.gen.json), not code, so it adds nothing to a typecheck. The IR reads what a
vendor declares about its own surface where it declares it: Stripe's x-resourceId names its
resources and x-expandableFields its embeds. Contents:
- The operation table, dispatch and the wire. An operation's id is the spec's operationId, or
the
method_pathslugspec-compile.tsmints when the spec has none. An answer that is a union of resources (customerordeleted_customer;card,bank_accountorsource) names the first as the operation's resource and keeps the rest as alternatives. Dispatch (packages/world-core/src/derived.ts) matches the most literal route first (and the headers an operation is named by, where the spec names it in one) and hands an operation to its semantics handler, or to the derived core, or answers the vendor's own refusal for an operation the twin does not model (the pack'sgap). While a pack moves, alegacyfetch takes what neither owns; a derived pack has none (doors). The dispatch reports what owns each operation. - Resource schemas, and candidate state fields: every enum-typed field of a resource schema
with more than one value (a single-value enum such as Stripe's
objectis a discriminator). The manifest declares each candidate a state field or not; an undeclared candidate fails the build, so leaving a state field out is a visible decision, never a silent drop. The cost, a line per candidate, is intended. - Operation classes.
crudis a create, retrieve, list, update or delete of the resource at its path whose answer is that resource.actionis a write whose effect is not storing its body (POST .../merge,/confirm, an RPC method such aschat.postMessage).computedis a read of values derived from other state, including a search.non-resourceis stateless compute, a protocol or a blob. An operation the generator cannot place isaction, nevercrud. - Generic CRUD for
crudoperations. A create sets each state field to the spec's default, or to the initial state the manifest declares. An update applies its non-state fields generically; a change it requests to a state field is applied as the declared transition for that operation, from the current value to the requested one, with that transition's guard and effects. A requested change with no declared transition is unmodeled: it answers the vendor's gap for the operation (D2). - Perform, from the operation table.
- Refresh, from the refresh scope the manifest declares per resource type: the list operation
that enumerates it and, when that list needs a parent, the parent type to enumerate first; or
a get, for a singleton. A type listed in full is
complete, with the meaning the real-system adapters give it. Ingest verifies with the manifest's declared strategy and observes the payload schema the spec publishes, where it publishes one. - The owners of each operation, for the report's breadth (the report).
The core never answers an operation with data that is merely schema-valid. Shape is derived; data and behaviour are not.
Correcting the spec
A vendor's spec lags the vendor. Slack's conversation schema has no updated, and OpenAI's
fine-tuning job has no paused though its own pause example answers it. A pack corrects its spec as
data, in spec/patches.json: RFC 6902 operations applied before the IR, so the vendored file stays
the vendor's bytes and a spec update is still a clean diff. Each patch says why, citing what shows
the vendor differs from its spec: its documentation, the spec's own example, a recording, the SDK's
types. derive-pack.ts refuses a patch with no why, and one whose evidence does not hold (see
Evidence). A patch corrects the spec to the vendor, never to the twin: a
field the twin answers and the vendor does not send is the twin's defect, fixed in the twin.
Evidence that holds
A transition's source and a spec patch's evidence are checked, never trusted: a citation written
from memory named the wrong page for two of three Slack patches, and eight transition sources named
pages that do not exist. A citation is one of:
- a page:
scripts/check-sources.ts <vendor>fetches every page a pack cites and records its answer inspec/sources.json; a patch'ssource: { url, quote }also records whether the page holds the quote (markup and whitespace aside); - the vendored spec's own words:
spec:<operationId or /json/pointer> "quote"(a patch writessource: { spec, quote }), read from the spec before any patch, so a patch never vouches for itself; an RFC is cited by section,spec:4.1.4 "quote". Where a vendor's documentation refuses to be fetched (OpenAI's answers 403), this is the form.
derive-pack.ts reads the record offline and refuses a page that did not answer 200 or was never
checked, a quote the page or the spec does not hold, and a patch that cites nothing. A page that
exists shows a method exists (most of Slack's patches); a rule about a state needs the words that
state it, as a quote. Where the evidence stops: that is not yet enforced for transitions: a transition
citing a page is checked only for the page answering, and one quoting its spec for the quote.
A versioned vendor (Stripe's dated API versions) has one spec per version, and renders one account
of an object per version: a request pinning a version gets that version's shape, one pinning none
the account's. The Stripe exemplar does the same (src/stripe-version.ts): it keeps its objects in
the shape its rules were written against, with the request parameters those rules read, and renders
every answer and webhook payload in the vendored spec's version, or as kept for a request that pins
a version before the first change it models. This is where a derived pack's objects are not stored
in the vendor's response shape: the stored shape is the pack's, and the shape rule holds at the
wire, for the served version. SHAPE does not judge an answer to a request pinned before the spec's
version, which the spec does not describe. Where the evidence stops: this is one exemplar's answer;
that every versioned vendor is served this way, and that one rendered boundary (Stripe's basil) is
enough, is extrapolation.
The semantics layer
Hand-written, under semantics/, grouped by resource family.
Each state field is a state machine declared as data: its values, its transitions (which
operation moves which value to which), each transition's guard with the vendor's refusal for it,
and the effects it writes (closed_at, closed_by). A resource with several state fields
(GitHub's pull request: state, draft, merged) has one machine per field; a guard may read
any field. A resource with no state field has two values, absent and present. Every transition
cites its source, and the citation is checked (see Evidence).
A declared transition is a rule the twin executes, never a description of the vendor. The core, a
handler (ctx.legal) or an engine that is not HTTP-shaped (transitionFor, which smtp's session
asks) consults the machine for every move it makes, and a move the machine does not declare throws.
A move the vendor makes and the twin does not (OpenAI failing a batch whose input does not validate,
or stopping a run at requires_action for its tools' outputs) is not declared: the
manifest names it in a comment beside the machine as the vendor's, so the gap is visible where the
rule would be. An operation the manifest lists as unmodeled declares no transition. Where the
evidence stops: this rule is the orchestrator's, minted while bringing openai to the coverage standard
below, where the machines declared moves no code made and their refusals no request could reach.
A handler is registered under an operationId and receives the validated request and the tree. It returns the write decision and the response through the kernel's write path, or, for a read, the answer. It replaces the core and the declared transitions for its operation; it never runs them and patches the result. Handlers carry what data cannot: actions, computed values, one resource's several views (GitHub's pull request and issue), and effects on other resources. A handler or transition naming an operationId the spec no longer has fails the build.
Doors, screens and the gap
A derived pack's fetch has three parts in front of one another, and nothing behind them. The twin's
own doors (discovery at GET /twin, a store door, upload targets, the /_twin/ doors that stand
in for an act the vendor's API does not have) answer only their own paths. The vendor's screens
answer their hosts and paths. Everything else is the derived dispatch: an operation of the spec
reaches its handler or the core, and one the twin does not model answers the vendor's own refusal
for an unknown request (Stripe's Unrecognized request URL, Slack's unknown_method), which is the
pack's gap. A path the spec does not have answers the same.
So a derived pack has no legacy fetch: the hand-written router a pack moves off is gone when it
has moved, and an endpoint the vendor does not publish is not kept as twin surface. Stripe's router
answered its spec operations with the unknown-URL refusal and served inventions beside them (radar
rules, seeding routes, lists Stripe does not publish); the inventions went with it, and a screen that
read them (the Dashboard mirror's radar rules) lost that section rather than keep an API the vendor
does not have. Where a vendor's page is the only way to an act, the pack builds the page
(screens); until it does, a /_twin/ door stands in, and says which page it stands in for.
Other wires: lanes, headers and line protocols
A vendor with several APIs, each with its own spec, builds each as a lane: a derived unit under
the pack (packages/twin/aws/secretsmanager/, with its own spec/, src/manifest.ts,
src/generated/, journeys/ and fetch) that the pack's router sends that API's traffic to. The tools
take the lane by its path (bun scripts/score-pack.ts aws/secretsmanager). A lane is scored as a pack
is; the vendor's other APIs keep their protocol until each moves.
One API published as several documents is not several APIs. Upstash publishes QStash's and Workflow's
OpenAPI documents with the one server qstash-{region}.upstash.io and one key set, and they share
endpoints (/v2/flowControl, /v2/keys, message retry): that is one pack, whose spec/openapi/ holds
each document as published, read as their union (scripts/spec-documents.ts). A patch addresses a
document as /documents/<name>/.... A path and method, or a component, that two documents both give must
say the same; where they contradict each other neither page is evidence against the other, and derive-pack
refuses the union until a patch rules which holds, citing what shows it (the vendor's own client, a
recording). Where the evidence stops: read for Upstash's two documents (52 paths; 2 contradictions:
FlowControlKey and the message-retry answers); no pack is derived from them yet.
An operation named in a header rather than a path (AWS JSON's X-Amz-Target: secretsmanager.CreateSecret,
every operation a POST /) is routed by that header: the IR carries it as the operation's
headers, and dispatch and SHAPE match on it. A Smithy model is read into the same IR
(fromSmithy): the service's protocol trait names how an operation is addressed, and its output
structures are the resources.
A line protocol (SMTP; POP3, IMAP and FTP have the same form) has no request and response: a
client sends lines on a connection, the server answers each with a coded reply, and some replies
(354, 334) mean the lines that follow belong to the command. Its spec is the RFC, vendored as its
text (spec/rfc5321.txt), and its surface is the RFC's own table of commands and the replies each may
draw (world-tooling/src/spec-ir-lines.ts reads RFC 5321 §4.3.2), corrected by patches as any spec is
(the extensions the server offers, a code the relay it stands in for answers). The session's order of
commands is a declared machine the engine asks for every move, as a handler asks. The pack mounts as a
byte stream (create<Name>TwinStream, what a hosted World hands a socket's or a WebSocket's bytes), and
a journey speaks on it in line steps: connect opens a connection with the options the operator
started the server with, send sends lines, and the answer is the replies they drew. SHAPE for a line
protocol is each reply's code being one the spec gives the command it answers.
A request-reply stream (Redis's RESP; the MySQL and Postgres wire protocols have the same form) is a
byte stream too, but not a line protocol: a client sends typed frames (RESP: an array of bulk strings per
command, or an inline line), pipelines them, and the server answers each with one typed reply, in order.
What a connection is (its protocol version, database, name, transaction, watches, subscriptions, a blocked
command) lives on the connection, never in the tree; what the commands store is the tree's. The pack mounts
as create<Name>TwinStream, as a line protocol does, and its HTTP fetch serves only GET /twin.
Redis is served so, by the redis pack (packages/twin/redis, transport raw-tcp), and its command
semantics are a kernel library, @volter/world-core/redis (packages/world-core/src/redis/: the command
core over the tree and the Lua interpreter), as git's are (the engine slot, above). The placement is forced
by the rules, not chosen for convenience:
- The command core existed in the upstash pack, and a second pack may not import it (A3) nor copy it (A0b). A lane of the upstash pack would be one of Upstash's APIs (its own RESP endpoint answers as Upstash, one database, its refusals), while a World's Redis is stock Redis; and a lane is a derived unit.
- Every vendor that serves Redis serves Redis's semantics: Upstash's compatibility page lists Redis's
commands with Redis's meaning, and a Redis Cloud or ElastiCache twin would too. So the library is shared
meaning that is the same meaning, as the git plane is for every git host, and not the "similar" meaning
A4 forbids. What differs between wires is a dialect (
RedisDialect), which each pack passes and the kernel never names: its refusal for an unknown command, its queue-time check, the commands it adds, whether a failing script keeps its writes, how a Lua number becomes a command argument, its Lua environment (the name of the command API, Redis's runtime libraries:cjson,cmsgpack,bit, Lua patterns) and its tree layout. What a wire answers (its refusals, script rollback, number arguments) has no kernel default; the layout defaults to one subject per key and the queue-time check to each command's arity. - The layout is the pack's resource semantics. The upstash pack keeps one subject per key; the redis pack keeps one per key and one per collection member (hash field, set member, sorted-set member, stream entry), as the tree contract says a set is kept, because a queue's events stream of 10,000 entries rewritten on every XADD is quadratic in the log.
A World's managed redis (volter-world init classifies REDIS_URL as infrastructure) is this pack:
volter-world-infra serves every declared redis service with the redis twin on the port the definition
declares, containerless, its tree under the World's data (world-runtime/src/redis-backing.ts); the
container serves it only when the operator forces VOLTER_WORLD_INFRA_BACKING=docker or the twin is not
installed (the runtime does not depend on it), and a port that already answers is refused. The other kinds keep
their backings (docker, or PGlite for postgres without a container runtime).
Where the evidence stops: the lane is built for one API of one vendor (AWS Secrets Manager) and the line protocol for one protocol (SMTP); that the same forms serve AWS's other APIs, a restJson1 or query protocol, and the other line protocols is extrapolation. The request-reply stream is built for one protocol (RESP) and is not a derived pack: a journey has no RESP step yet, so its evidence is its capability manifest (Redis's command table as the denominator), its conformance and three unmodified clients (ioredis, node-redis over RESP2 and RESP3, and BullMQ processing jobs, a delayed one and a retry), not a report; and the invariant matrix's resource-level R2 replays HTTP writes, which a raw-tcp pack has none of.
A gRPC vendor (Temporal's frontend) is served on the vendor's own wire: HTTP/2 with prior knowledge
on a loopback port, gRPC's length-prefixed messages over it, each RPC answered or refused UNIMPLEMENTED
by name. The pack owns the whole transport (packages/twin/temporal/src/temporal-h2.ts, HPACK tables
derived from the vendored RFC 7541) because Bun's node:http2 server fails nghttp2 clients at the
connection preface; a socket serves under Bun and Node alike. Its spec is the vendor's .proto tree,
vendored, compiled into a committed descriptor the serve path loads (never .proto text), and its
denominator is every method of the services and every command the vendor declares. Such a pack is the
raw-protocol transport class (transport: 'raw-tcp': the SDKs drive HTTP/2 themselves and the injector
never sees it), wired by the app-read address (TEMPORAL_ADDRESS=<host>:<port>, a template in
APP_READ_ENDPOINT_ENV, since the descriptor's endpointEnv.name receives a URL). HTTP/1 on the same
port reaches the pack's fetch — the World's doors and the frontend's HTTP API routed by the protos'
google.api.http annotations — so a World's one port per service carries both. A workflow engine's
state is World state: a run is a tree subject, its history the pack's engine slot (content-addressed
event chunks in the blob store, named in order by the run), and its timers, timeouts and retries run on
the World clock. It is not managed infrastructure: a managed-infrastructure kind is an opaque database a
backing boots, while this is a twin whose state branches with the World and whose time is the World's.
Where the evidence stops: gRPC is built for one vendor (Temporal, proven with its TypeScript SDK 1.15); the spec IR has no protobuf reader, so the pack is protocol 2, not derived.
Pack layout
packages/twin/<vendor>/
spec/ the vendor's spec as vendored; recordings/ where any exist; SOURCE.md (where
each came from, when, its hash)
journeys/ the customer life and its verdict, the record of what no World reaches
(unreachable.json) and the report (score.json, coverage.json)
src/
manifest.ts the descriptor, plus the facts no spec carries: pagination style, id formats,
auth, error envelope, state fields and initial states, refresh scope,
references, roundTrip, engine; and the surfaces outside the spec (UI screens,
engine protocols, query languages), each done or todo
generated/ the derived core
semantics/ state machines as data; handlers by operationId
engine/ the engine slot's module, when there is one
seed/ default data, as calls to the vendor's own operations
screens/ the vendor's screens (see Screens), built from @volter/world-ui's pieces; a hosted
flow's round trip beside it
<vendor>-budget.ts when the vendor rate-limits a live token (D8)
index.ts cli.ts
README.md
spec/ and journeys/ are inputs to the build and the runner and are not published. Everything
the twin serves is under src/, so C2 and the publishing pipeline
apply unchanged. When the vendor publishes no usable spec, the SDK's types or recordings stand
in, and SOURCE.md says which.
Screens
A vendor's screens are twinned for two jobs, and the job decides what the screen must hold to:
- Hosted flow: a screen an application sends its user through. An OAuth consent, Slack's install,
Stripe Checkout, the billing portal, Connect onboarding. The application never reads such a page;
it sends the user to a URL with parameters and receives a redirect back (a
codeandstate, orerror=access_denied), then an API call or a webhook. That round trip is the contract, and the vendor documents it: the entry URL and its parameters, the outcomes and the redirect each one sends, the form fields a submission carries, and the controls a person acts on, named as the vendor names them ("Authorize octocat", "Card number", "Pay"), so a test that finds a control by its role and name finds it. A submission is a transition by an external actor on a declared machine: hosted flows are where moves no API call makes enter the World. - Workspace: a screen people work in. A Slack workspace, a pull request, a dashboard, a board. A person, a tester or an agent must be at home in it; nothing reads it but people.
The vendor's own acts are modelled as the vendor's moves: a check suite GitHub creates when a
commit lands, a payout Stripe pays two days after it is made, an event a webhook delivers. A pack's
/_twin/ doors stand in only for a screen not yet built or for a system outside the vendor's
surface that acts on it (GitHub's Actions runner finishing a run); a door that makes the vendor do
something its own rules would do is a missing move.
What a pack answers can depend on the host serving it: an operation that needs a long-lived
connection (Slack's Socket Mode, apps.connections.open) answers the vendor's refusal where no
socket server runs, as on the in-memory walk, and a journey walked there says so.
A screen neither job reaches is not built: a World's state is inspected elsewhere, never through a
generic resource browser posing as the vendor's UI (C1b's "never a fabricated dashboard"). A widget
the vendor ships into the application's own page (Stripe Elements from js.stripe.com) is not a
screen: it is a client library, twinned from its published types as an SDK surface is.
A screen is authored, never captured. Building a twin never copies a vendor's page: no DOM,
asset or traffic capture is a step of creating or maintaining a pack, because no process reaches
every screen a vendor has, and a pack whose screens depend on captures has a screen count set by
what someone happened to record. Every screen is written from @volter/world-ui's shared pieces
under the vendor's skin, and reads and writes only through the pack's operations; a hosted flow is
the same kind of screen with its round trip declared as its contract.
Demand decides which screens exist, as it decides operations: a hosted flow where an
application's code sends its user to the vendor, a workspace where people do the vendor's job. Each
is declared in manifest.ts (screens: [{ id, kind: 'flow' | 'workspace', host, path, demand }]),
counted done or todo. A screen's control is accepted when driving it through its own click
performs the operation or transition it declares; a flow (an entry and one outcome) when the
redirect, its parameters and the state it leaves match the declared round trip. The life walks a
screen as a browser does, and its lines count toward the pack's coverage.
Where the evidence stops: that application code never reads a hosted page is measured for the six
hosted flows found in the ladder's applications (their code sends a user to a URL and handles the
redirect). Their end-to-end tests were scanned for vendor pages, capped at ten seconds, so partially:
26 applications' tests name a vendor-hosted page and five drive a browser near one. cal.com fills
Stripe Elements by the vendor's input names inside its frame (.StripeElement, then [name="number"],
[name="expiry"], [name="cvc"], [name="postalCode"]) and asserts only Checkout's URL; ToolJet
signs in on Google's page inside cy.origin; dub replaces js.stripe.com and checkout.stripe.com
with its own routes; the rest assert URLs or their own controls. So a vendor widget's field names are
part of its contract, and a hosted page's are not yet shown to be: a selector a scan finds is a
control the screen must carry.
Rules this replaces for a derived pack
- C1: the layout above. The C1 and D7 checks in
scripts/architecture.test.tsandscripts/pack-shape-check.tsmust admit a derived pack by that layout, andscripts/protocol-2.test.tsmust find its fetch factory insrc/generated/. - T0 (gates): for a derived pack, T0 is the core regenerated byte-identically,
the build, hygiene, the branch round-trip, shape parity, and the life replayed with every check
below, ending in the report. T0's manifest step (
scripts/manifest-baseline-one.ts) runs the report for a derived pack in place of its capability manifest: the life walked with no check failing and no legacy fallback.scripts/verify-pack.tsandscripts/scorecard.tsmust do the same in place of the capability, mutation and census steps; they do not yet. - A0: generic CRUD is permitted for operations classed
crud, under the transition rule above. It never stands in for anaction. - D5 and Evidence: the capability manifest, its
verify()predicates and the per-pack conformance harness give way to the report and the life. The branch round-trip and shape parity still run. A derived pack whose protocol 2 harness still exists records the functions only it reads as unreachable, with that reason, until the harness is retired. - D6 and D7: the derived refresh is the pull path. It observes through
observeResourceas D6 requires; there is no connector module.
The report
scripts/score-pack.ts <vendor> walks the pack's customer life through the pack's own fetch (and
stream) on an in-memory World and reports, into journeys/score.json:
- the life: whether a judge's plausible verdict names the life's current content (its sha256);
- correctness: every check on every step held, and the life answered byte for byte the same on a
second fresh World (REPLAYABLE). A step's checks are its status, expectations and captures;
SHAPE (
scripts/shape.ts): the answer carries only fields the spec gives what the operation answers (or the alternative it says it is, deleted forms included), every field the spec requires of it, and state values only those the spec lists, or for a line protocol only reply codes the spec gives the command; and the walk's beats, ECHO and PERSISTS on a step thatcreatesand LANDED on one thatchanges; - coverage: what
scripts/life-coverage.tsmeasures of the hand-written code and the declared state logic (Journeys), against the standard; - breadth: the spec's operations (a line protocol's commands) by what serves each: a handler, the derived core, a legacy fallback, or the gap.
A pack is done when the life is judged plausible, no check fails, the vendor's published examples are replayed and judged, coverage meets the standard, and no operation reaches a legacy fallback. Breadth's gap count is not a failure: it is the part of the vendor the twin does not model, answered as the vendor answers an unknown request, and the pack closes it by modelling, never by answering it otherwise.
Where the evidence stops: the report does not yet check a response's field types, the ISOLATED and
UNKNOWN beats (behavior-journey.ts --vendor runs them on a pack's hand-written journey, score-pack
does not), that a filter the spec maps to a field returns only matching subjects, that lists agree
with gets, that a refused transition changes nothing on its subject, or that the API and the UI show
the same tree. A passing step has held only the checks above.
The denominator is the whole spec, never the operations a twin serves: breadth counts every operation
of the spec, and one the twin does not model is a gap in it, closed by modelling it. Which fields are
state machines is judged case by case: the manifest declares each candidate a state field or lists it
in notState, with its reason beside it, and derive-pack.ts refuses a modelled resource that leaves a
candidate unruled. A union's discriminator (a field each alternative gives one value, Stripe's
object) is never a candidate. A GraphQL schema counts each field of each type.
Recordings and vendor-backed runs are welcome and never required. None is available to the
exemplars: each rule's correctness rests on the source it cites, and perform, refresh and real heads
(Layers) are specified and were exercised by no exemplar. The verified column is
unchanged: the date policy/MAINTAINERS.md declares the pack was last checked against the real
vendor. What a done pack establishes: its surface behaves consistently and in the vendor's shape under
the rules it claims, each with its cited source; not that the claimed rules are the vendor's.
Journeys
A journey is an ordered, stateful story in the form scripts/journey-kit.ts defines: steps, captures
and expectations, as data, with no functions. Logic a check needs belongs to the runner, where every
pack gets it; the runner tells a refusal from a success by the step's declared status, which covers a
vendor that refuses with HTTP 200 (Slack's ok: false). A step is a request (method, path), a
line step on a byte stream (line protocols), a git
step, or a wait that moves the World clock. A git step is what git clone and git push send over
smart HTTP (scripts/git-client.ts, on the kernel's git library): a clone reads a ref's tip and its
paths; a push commits files on a ref's tip, rewrites the tip (amend, which only a force lands) or
deletes the ref, and its answer is the server's report (result: ok or the reason the ref was
refused). A customer of a git host moves code with git, so the life does too.
Each pack has one journey, its life: journeys/customer-life.json, one customer's whole life with the
vendor, from signup to leaving. The life pays for setup once and every later step lands on state it
already built. Time passes in wait steps; the World clock starts at 2026-01-01. A life walks on an
in-memory World (inMemoryWorld, scripts/behavior-journey.ts) in seconds; a slower walk is a defect
of the twin or the kernel. An operation only a vendor page reaches (a token made on a settings page) is
a missing screen: the pack builds the page and the life walks through it, posting what a
browser posts. scripts/behavior-journey.ts replays committed journeys deterministically, with no model
call: judgment happens when a journey is written, never when it is scored.
Writing a life
Plausibility comes before coverage, and every expensive stage is preceded by a cheap one that could reject it. Each stage ends on its test.
Outline. The customer, the people and programs acting for it, the arc from signup to leaving, and the time it spans, in prose with no calls, at
journeys/customer-life.outline.md. It lists every actor with the credential it acts with and that credential's grant, and its schedule keeps within the vendor's documented windows and quotas (how far ahead a send may be scheduled, how often a key may be rolled). Before its judge, the author walks it: per act, who makes it, with which credential, and does the grant allow it; per stated cause, where the story carries it out; per recurring or batch act (a digest, a newsletter), who it reaches at each date against who the story has then. A judge in another session rules on it against what a judge rejects and the rules the lives have taught, reading each item of the list for the outline's beats (a beat is a sitting; a refusal, a toggle and a read-back are judged in prose as in steps); a vendor's grant stated only in general words ("can only send emails") is read by its words, and an act plainly outside them is a ruling. The same judge re-rules as at stage 3. The dispatcher stamps its verdict atjourneys/customer-life.outline.verdict.jsonas a life's is stamped (sha256,judge.agent,authors,rounds). Test: the outline is accepted. Where the evidence stops: the credential list, the walk and the windows are drawn from one pack's blind run (resend), whose first outline drew two rulings for acts its key's grant did not allow and one for a cause never carried out, all three caught by that walk.Life. The steps, written from the accepted outline and the vendor's documentation. The author does not read the coverage report at this stage: a life written toward the report becomes a tour of it (of
score-pack.ts's lines the author reads the walk's checks and flags only). The life names its author session (author). Before the first step the author maps every act of the outline to the twin's door for it (an operation, a screen, a git or line step), one line per act in the unit's progress record (the door, what serves it today: core, handler, legacy or nothing, and whether it holds), and probes the doubtful ones: an act with no door, or one the twin answers unlike the vendor, becomes a twin fix decided before the life is written, or an outline change its judge re-rules. Test:score-pack.tsshows no check failing and no judge flags, and the author has walked every step against what a judge rejects reading what each sends (a push's code, an upload's bytes, a path's origin), not only its name, and every recurring or batch send's recipients against who the story has at its date (resend's first life left out an owner the story still had). Where the evidence stops for the self-walk: a fix handed over without it drew two new rulings of the same two kinds (github). Where the evidence stops for the mapping: drawn from three packs (slack, github, stripe), each of whose accepted outlines needed core acts the twin could not represent (joining a workspace, a pull request from a fork, a subscription's renewal) that lives written toward coverage had never asked for.Judge. A judge in another session, on another model where one is available, rules on the life as a whole and commits its verdict beside it, pinned to the life's sha256. Agents dispatched from one session share its id, so the dispatcher, which alone knows both, stamps the verdict with the judge's agent (
judge.agent) and every agent that wrote the life (authors). A verdict without them, one whose judge is among the authors, or one marked self-judged, is no verdict (score-pack.tsreports it so). A judge rules against a step only for a defect its list names or a vendor contradiction it can point to in the spec; anything weaker it lists as a doubt. The author answers every ruling and every doubt it can settle in one pass, checking each change against the spec before it is judged (a fix that swaps one fact can break another that depended on it), and the same judge re-rules: it confirms each of its rulings is resolved and reads the whole life again. A fresh judge each round samples a different few defects of the same life and does not converge. Test: a plausible verdict. Where the evidence stops: openai's life passed on the sixth round, the first re-ruled by the same judge; aws, smtp, slack, stripe and github, each re-ruled by its own judge throughout, passed in two or three rounds.Validating the judge: a judge from another model family (gpt, through
codex exec) was run once over all six lives the Opus judges had passed, and ruled against steps in every one, several rounds deep; those rulings became the list's newest items, the author's self-walk andscore-pack.tschecks, so the judge the procedure uses (Opus) reads for them. Planted defects measured the result: eleven defects of eleven kinds planted in a copy of a done life (smtp) were each ruled by both families judging blind, andscore-pack.tsalone caught eight; in a second round of twelve subtler defects in stripe's life (a price off by three dollars, a quarter's range running into May, a toggle of one portal feature, a read of a deleted account) the blind Opus judge ruled ten, the gpt judge eleven, and every defect was caught by a judge orscore-pack.ts. Where the evidence stops: two packs, two rounds; a second-family judge is an audit to run when the list is revised, not a stage every life passes. Where two judges disagree on a vendor fact, the dispatcher reads the page either cites and rules by its text, never by majority.Coverage. Only now the author reads
scripts/life-coverage.ts <vendor>. Each gap is closed by a step the accepted story already has a reason for, by a record, or by deleting dead code; the additions go back to a judge (stage 3) before they merge. Test: coverage meets the standard. One life runs a pack's own code only in part (55 to 65 per cent of the hand-written lines in slack, stripe and github); the rest is recorded, so a record's reason carries as much of the standard as a step does. Additions made for coverage are where a life drifts toward a tour: judges read them as "added for coverage" (one member placed in a workspace three ways in one sitting, one method called in both its forms in a row) and listed them as doubts, which the bar lets pass. Where the evidence stops: drawn from those three packs' coverage passes.Done. A plausible verdict against the current list whose judge is not among the authors, no check failing, no judge flags, the vendor's published examples replayed and judged (Published examples), coverage meeting the standard, and no operation reaching a legacy fallback.
What a judge rejects
A judge rules each action follows from the last for a reason a real person or application would have. It rejects:
- a refusal without a cause the story shows, or the same cause drawn twice; a run of refusals;
- a failing repeat of a request that succeeded earlier in the same sitting;
- a state switched away and back in one sitting, or returned with no reason the story shows;
- a stated cause that never happens, and a promise never kept (a container made "to analyse" data that never runs the analysis, a comparison started and never finished);
- a feature tour: one call per option of an endpoint in a row;
- a read-back with no need: a list or get of what the story already holds, unless someone who does not hold it reads it;
- the wrong actor or credential for an act, a date that contradicts the
waitsteps, a name or id used before it is made or after it is gone; - the wrong era: building on an API the vendor had deprecated by the World's date, or calling one past its documented shutdown;
- vendor behaviour the vendor's documentation contradicts;
- content that does not bear out the step's name: the code a push carries does not do what the story says it does, an upload carries no bytes of the file it names, a message the vendor never sends in that mode is said to arrive, an attachment lacks what its format requires;
- a call through a door the vendor does not document (a path the client builds instead of the one the vendor returned, a read the vendor would refuse for want of a grant the story never gives), even where the twin answers it.
A value a person could only have learned from a page, a mail or an answer (a join link, a hosted page's address, a token shown once, an id) is read off it with a capture, never written as the literal the twin's sequence would give; the judges catch this by reading, since a mechanical check that flagged literals an earlier answer had first returned raised sixty-seven flags across the six lives and none was a defect (vendor constants, the World's fixed ids, values a person reads and then types).
A new item on this list reopens every done life: each is re-judged against the whole list by both families before it counts as done again (stripe's lives, last judged before the two items below were added, still built its hosted-page addresses from ids when a blind judge read them).
Where the evidence stops for the last two: drawn from the cross-family judges' rulings on the six lives (code that returned nothing where a fix was claimed, uploads of JSON metadata, an upload path built from a file id, an org app reading a workspace it was never added to).
score-pack.ts flags the first three mechanically (REPEAT, RUN, TOGGLE; a sitting is the steps between
two wait steps), a time of day a step names that its hour on the World clock contradicts, a weekday named
with a date that falls on another, and a wait whose named date its clock does not reach (TIME: a
"morning" at 21:00, a "Wednesday 10 February" in 2026), and a life-wide default credential in a life whose steps act with several (CRED: a
default lends one actor's credential to every step that names none, and a judge found octocat's token
on a server's call). The flags are a floor: they miss most toggles and everything below them, which only
the judge sees. Where the evidence stops: this list is what independent judges rejected in the first
six lives (openai, github, stripe, slack, aws Secrets Manager, smtp) and in one rewrite; it grows by the
same means.
Coverage
What the life must cover is the pack's hand-written code and its declared state logic: semantics,
screens, presenters and doors, and every declared transition and guard's refusal. The generated core
is the generator's to test, once, and needs no journey. scripts/life-coverage.ts <vendor> measures the
lines of the hand-written files the life's walk loads (the real vendor's side, connectors, mirrors and
rate budgets, is out of any World's reach and not counted), each never-run range named by its
declaration. A pack's code is measured with its lanes' lives and examples as well, since a lane reaches
the pack's code through the pack's router (planetscale/api's life makes the branches psdb serves); each
lane's own code and state logic are measured with the lane. A move is reached when the machine allowed it on a step (the kernel's
observeTransitions) or the World's log shows the change; one write may carry a chain of declared moves
the machine walked, and resources stored as one type share their fields. A change to a state field no
declared transition allows is an undeclared move, and counts as missing. A refusal is reached when
the machine gave it, or a step on its operation was answered with it.
The standard is all of it, with no threshold: every hand-written line, every declared transition and
every guard's refusal is reached by a step the accepted story has a reason for, deleted as dead, or
recorded in journeys/unreachable.json (code: a file and its declarations; state: the item as the
report names it; each with its why). A branch no believable customer reaches is never reached by a
contrived step. A record names why the life does not reach the code:
- no World the life runs in reaches it: the host process around the fetch, the twin's own doors, a protocol 2 harness, a World configured otherwise (read-only, scripted, dated before a retirement), a hostile peer, the engine's own bug guard, the vendor-backed half (state observed from a live account), a sealed World's refused delivery;
- a refusal or an option the vendor documents that no customer of this life has cause to draw or use
(a malformed command no mailer of the story sends, a sampling parameter no feature of the shop sets):
its
whynames it and cites the vendor documentation it rests on. Where the evidence stops: this reason has not been ruled on by the owner, whose standard names code no World reaches; a buggy client in some World does reach these.
A record is a claim, checked every time coverage is measured: one naming a file, a declaration or a
state item the pack no longer has, or one the walks now reach, is stale and counts as missing until
life-coverage.ts <vendor> --prune removes it (a record removed wrongly shows again as missing, so
pruning hides nothing). The walks are the life and the vendor's published examples
(Published examples).
A record is by declaration, so a large function is not recorded for one branch: the branch moves into
its own function first. Bun reports lines, not the branches inside a line, and marks a closing brace or
a lone fallback line unrun where the path ran; such a line is rewritten into an expression bun
measures, with the behaviour unchanged, never recorded. A screen's controls are counted by the lines
their handlers run, not by a click. Because bun reads lines, a branch moved out whose call shares a
line with code that runs (catch { return malformedJson(); }) reads as reached while only its function
is recorded: the condition that calls it is unmeasured, and the moved-out function's record is the
only account of it (github joined four such calls).
Published examples
A life shows the twin behaves as a believable customer finds it; the vendor's own examples show it answers what the
vendor says it answers, with no judge reading documentation in between. Every example the vendor publishes for an
operation the twin serves is replayed against it in journeys/vendor-examples.json, a journey in the life's format
whose example steps name their example (<operation>#<title>): the spec's own (Smithy's examples, an OpenAPI
response's, Swagger's, openai's x-oaiMeta, stripe's fixtures, a line protocol's RFC scenarios) and those the
vendor's documentation pages publish, vendored with their URLs in spec/doc-examples.json (each
{title, source, operation, request?, input?, output?, means, status?}; a page's request is checked to route to
its operation, and its ids stand for the World's own, as a spec's do), including a rule a page states, replayed as
the refusal it causes. A field's example value is not an example: a spec that publishes none per operation sends the
author to the vendor's pages (resend's, one request and one answer per operation). An API the pages document and the
spec omits is the spec's lag, patched in with evidence when the pack models it and until then outside the count,
named in SOURCE.md. Setup steps make each example's precondition true or read back its effect,
and nothing else. scripts/vendor-examples.ts checks that each is sent as published, answers the published status,
and holds every key of the published answer with its type (a sample, which is what an object can hold rather than one
request's answer, may answer null), and counts every example: replayed, or recorded with its reason. An example the
twin answers otherwise is the twin's defect, fixed, unless the vendor's example is itself wrong or the server the twin
models answers otherwise where the vendor's text allows it (exampleDiffers, with the reason). An example of an
operation the twin does not serve is counted, not replayed: it comes in when the operation is served. Where the spec and a page
publish different shapes for one answer, the twin answers the spec's, the spec is patched where a page adds a field it
lacks (with the page's words as the evidence), and each remaining difference is an exampleDiffers entry naming both
(resend's pages and spec disagree on a list's empty fields and on two answers' shapes; the evidence is that pack's).
Two of the judge's recurring rulings are the script's checks: a value an earlier step sent or captured, answered back on
an example step, is asserted, or named in the step's undecided with the reason it only coincides (a default the vendor
applies, a value the vendor generates); and a write example whose answer is thin has its effect read by a later step.
Before its judge, the author walks the rest of the bar one item at a time: the values the setup only causes (a count, a
date a send was made, a status a verify leads to), which the script cannot see, are asserted; every setup step makes an
example's precondition true or reads back an example's effect; every exampleDiffers and notReplayed reason is checked
against its page for a twin defect it excuses; and nothing is asserted that the vendor generates or defaults, since a
value pinned to make a check pass states the twin's guess as the vendor's answer. --sweep adds advice (actor fields no
assertion pins, lists no expect closes). Where the evidence stops: resend's first examples file drew sixteen rulings,
thirteen of them the two checked classes; asserting openai's and slack's hits by value alone pinned a reasoning default,
generated counts, cursors and a stale block, which their judges ruled.
An independent judge rules on what the script cannot (padding, a decided value not asserted, a reason or a step name
untrue of its source, an answer that passes the checks and contradicts the vendor's text), in
journeys/vendor-examples.verdict.json, pinned to the file's hash, as a life's verdict is. Coverage counts what the
examples reach beside the life: a line only the vendor's own example reaches is reached for the reason the vendor
gives.
A vendor's examples span its history: one names a model released this autumn, another an API retired last summer. The pack holds the vendor's timeline as data (each model's release and shutdown, each API family's removal, each dated policy, cited to the vendor's changelog and deprecations pages), and the twin answers by it on the World clock: a model before its release or after its shutdown, and an endpoint after its removal, answer as the vendor documents, and a model the pack does not catalogue is unknown. The examples file is then a timeline too: each example is sent on a date when everything it uses is live and its values fall within the vendor's documented windows (a reschedule no further ahead than the vendor allows), and one no single date can hold is recorded with the dates that exclude it. A default the vendor gives (the model a request runs on when it names none) is data in one place, read wherever the request is gated, served and billed.
Where the evidence stops: proven on aws (54 examples: the spec's 24, the schedule page's 15 and its 15 stated rules; the first replay found the twin computing every rotation schedule as thirty days, and its judge a field AWS leaves out answered empty), smtp (RFC 5321's 38 scenario turns; Postfix's own replies) and openai (147; the timeline, from its judge ruling the whole file sent on one date that let removed APIs answer).
Rules the lives have taught the twins
- Every move the twin makes asks the machine, whatever made it: a handler, a read-time presenter, a
screen, another wire (GraphQL, a git push, a runner's report through a door), and a write the twin
stores in another shape than the machine's field (an overlay row, a
closedflag for astate). A move no step can ask for is removed from the machine and named in a comment beside it. An ask that names no target state takes the first transition that allows it, so a guard that lets a state stay is declared before a move sharing its actor and from-states. - Removing a legacy fallback starts by reading what it answers for each operation it owns, then goes
with everything only it wrote or read. An input the vendor's operation does not take goes with it: a
twin's own act has a door under
/_twin/, never a field on the vendor's operation. Where the evidence stops: drawn from github alone (its fallback owned 834 operations and answered Not Found to all). - An answer that was empty hides its shape: a step that fills a list for the first time meets SHAPE on its items, and the defects it turns up are the twin's, fixed where the list is built.
- A twin refuses what the vendor refuses at the edge: a parameter the operation's spec does not have (stripe had no such check, and a life's removed parameter passed until a judge read the changelog), a redirect the app never registered, an org-wide token missing the workspace it must name (slack). A lenient twin hides a life's stale or wrong calls from every mechanical check.
- Every credential resolves to the actor who was given it: a user token to the member who authorized the install, a bot token to that install's bot, revoked with the install (slack issued one fixed token of each, so an uninstalled app's reads still worked). A life carries each step's own credential and no life-wide default, which leaked one person's token into a server's call (github).
- A move the World clock makes (a renewal, a payout, a trial's end, a cancellation at period end) is stamped with the moment it fell due, not the request that caught it up; otherwise a quarter's books count it in the next quarter (stripe).
- A move the World clock makes happens when any request next arrives, never because a
/_twin/door was read: a twin's state cannot depend on being watched, and its doors answer through every router a World mounts (aws's scheduled rotation first started only when its invocations door was read, and its router refused the door's GET). - The customer's own program that the vendor invokes (a rotation function, a webhook receiver) is played by the life every time the vendor would invoke it within the life, and the outline picks the schedule so that stays a story (aws rotated every 90 days, four rotations shown, after a 30-day schedule played twice left a rotation stuck for nine months that no review noticed).
- A twin that serves one API version serves it on every World date, so a life dated before a parameter's release cannot use the vendor's name for that date (stripe's market stall moved a month for it); the account's own version is open on the roadmap.
- A twin takes a payload only in the format the vendor documents: an upload's raw bytes with its content type, a real image or audio file, an upload URL it minted; a body in another shape is refused as the vendor refuses it (github took JSON for a release asset, openai took text for audio, slack took bytes at a path it never minted). A time-limited resource expires on the World clock (openai's batch files after thirty days).
- A twin's generated output (an image, speech, a model's text) is a labelled placeholder, so a life never claims what it shows or says, and never reasons from it; an input a person supplies is real content of its kind (speech that says what the step says, made with the host's text-to-speech), not a valid-but-empty file (openai's first real WAVs were a second of silence, one copied as the other).
- Where the evidence stops: each rule above is drawn from the one pack named beside it, found by an outline's door mapping or its life's judge.
What the exemplars settled
Each question the exemplars were built to answer, as a rule or a rejection with its reason. Where a rule's code does not yet hold, the section names the work it orders.
Moves no API call makes
Every move has a cause the vendor shows, and the twin makes it where that cause arrives:
- Time (a renewal, a payout, a trial's end, a file's expiry, a scheduled rotation): the pack's catch-up, a
semantics function its fetch runs before answering any request on a writable World. It walks every move due by
the World clock, in order, each stamped at its due time. There is no scheduler and no kernel pass; the World
clock is read, never watched. (stripe
advanceBilling/advancePayouts, openaiexpireFiles, awscatchUpRotations.) - The vendor reacting to state (Dependabot reading a manifest, a session completing when its page is paid): the handler of the act that caused it, in the same write, asking the machine for each move.
- A program or third party acting (a CI runner's check run or SARIF upload, a bank transfer arriving): its own call, through the door the vendor gives it (the runner's API, Stripe's test-mode helpers), played by the life as that actor with its own credential.
- A person on a vendor's page: the screen, which asks the machine like any handler.
- A third party outside the vendor (a recipient's mail server refusing mail, a reader clicking a tracked link
in their inbox): the World states the outside fact through a door (resend's
POST /_twin/recipients/:domain, a server that accepts no mail) and the vendor's own act meets it (its delivery bounces); a person outside reaches the vendor's own door as a browser does (the tracked link's host), reading what they hold through the World's door for it (the recipient's inbox,GET /_twin/mail?to=).
Rejected: one door that fires a named move. Each move above has a real cause the vendor exposes; a generic door lets a life skip the cause and a twin model a move no cause produces.
Where the evidence stops: the catch-up is drawn from stripe, openai and aws, the reaction and the third party from github and stripe, the screen from github, slack and stripe, and the third party outside the vendor from resend alone.
Events and webhooks
An event is the vendor's report of a write, and the twin announces it where the write is made: the handler that
makes the write (github's announce, slack's emit), or the manifest's onWrite, which the core calls after every
write it stores (stripe names the event from the write's operation). Its type comes from the vendor's published event
list, never coined; its payload is the object as the operation answers it, or the spec's webhook schema where the spec
publishes one. An announcement is stored where the vendor lists its events (stripe's /v1/events) and delivered to
every endpoint subscribed to its type, signed as the vendor signs; a sealed World refuses the delivery and says so, and
the event stays listed. What the vendor does on reading an event (a push running workflows and Dependabot) happens in
the same announcement (moves no API call makes).
Rejected: events derived from declared transitions. Most of the vendor's events report no transition (a push, a
posted message, a created object, an edit), and one write can report several (a stripe refund is refund.created and
charge.refunded); a transition-keyed derivation would miss the first and merge the second.
Where the evidence stops: drawn from stripe, github and slack; openai's webhooks and aws's events (EventBridge) are not built.
Fields the server assigns
A resource declares what the server assigns as data: its id's prefix (idPrefix), each field set on create
(assigned: the World clock's now, the subject's id, a fixed value, or a template over the stored fields), a number
counted per parent (number, a repository's issue numbers), and the spec's defaults, which the core applies. A
handler assigns by hand only what no rule can say (an AWS ARN's suffix, six characters seeded from the name; a
GitHub node_id encoding type and number), and names the vendor's documentation beside it.
Where the evidence stops: the rules serve every create the generated core makes in all six packs; handlers assign by hand in each, and no pack has been audited for a hand assignment a rule could say.
Embedded and expandable resources
A resource holds another by its id, stored as the id, and declares it in embeds (field → resource); the core answers
the id and, where the vendor has an expand parameter (expandParam: stripe's expand[]), hydrates each path the
caller asks for from the current stored object, at any depth, through that resource's own view. A stored copy of an
embedded object is never kept: it would answer yesterday's object.
Where the evidence stops: drawn from stripe (15 embeds) and openai (5). github and slack embed objects in every answer (a user in an issue, a channel's creator) through their presenters; whether each reads the current object rather than a stored copy has not been audited.
Credentials, scopes, roles and tenants
The manifest declares the credential's wire once (auth: header, scheme, the vendor's answers to a missing and an
invalid one) and how a credential resolves to its actor (identity); every credential resolves to the actor who was
given it (rules the lives have taught). An operation's scopes are the spec's
(its security requirements, which the IR reads onto each operation); where a vendor's spec lists them, the twin
refuses a call whose credential holds none of them with the vendor's own refusal, before any handler runs, and a spec
whose scopes lag the vendor is corrected by a patch, never by a table beside it. Roles and tenants (an organization's
members and teams, a workspace an org-wide app was added to, a project a key belongs to) are the vendor's state, kept
by the pack's handlers; no spec declares them, and none is derived.
A key whose grant no spec declares (resend's full or sending access, optionally one domain) keeps its grant with the
key in the pack, and the pack's fetch refuses a call outside it before dispatch, as slack's scope gate does; a key
the vendor issued and then revoked, deleted or retired with its project is refused as the vendor refuses any key it
does not hold (openai, github, resend), while a key the twin never issued is the World's and is checked only for its
format. A World's first key, which the vendor makes on a dashboard, comes from a door standing in for that page
(github's token door, resend's POST /_twin/api-keys).
Where the evidence stops: scopes from slack alone, the only one of the six whose spec lists them (every operation); github's fine-grained permissions and openai's key permissions are not in their specs and are not modelled; key grants from resend alone.
Behaviour every operation shares
Declared once in the manifest and applied by the core before or around every handler (crossCutting): the
credential's wire and its refusals, the API version header and the answer to a malformed one, the idempotency header
(a repeated key answers the stored first answer, or the vendor's conflict when the parameters differ), a body that
does not parse, a write to a read-only World, the list envelope and its paging (after/before ids, an opaque cursor, or
numbered pages with the vendor's Link header), the error body, and headers every answer carries. A handler never
re-implements one of them.
A rate-limit answer comes only from a limit the twin models (the vendor's documented limit for the account, on the World clock); no derived pack models one, and none answers 429. A header that forces the answer is a twin's act on the vendor's operation and is refused by the rule that such acts have doors of their own.
Conditional requests (an ETag answered, If-None-Match answered 304) are not modelled: a client that sends
If-None-Match gets the full answer, which every client accepts. Where the evidence stops: no life has needed one;
github's REST answers both, and a pack that models it declares it here with the others.
The API version an account answers
A World serves one version of each vendor: its vendored spec's, on every World date, to every account. An account's own version (the one it was created on, answering its unpinned requests) is not modelled: it would need the vendor's history of every parameter and field by version, which no vendor publishes as data, and a twin that guessed it would answer shapes no spec checks. A request pinned to an older version is answered in that version's shape only where the pack renders it from the served one (stripe's versions before basil). A life is dated where the served version holds: nothing it sends was released after its step's date, and a judge rules on a step whose date precedes what it uses.
Rejected: an account version chosen by the World's start date. Where the evidence stops: stripe alone versions its API per account; github and openai version by header or path, slack and aws not at all.
Seeds
A seed is a journey: steps in the customer life's format (journey-kit.ts), played through the vendor's own operations and doors on a fresh World, each actor with its own credential, with captures for the ids a later step or an app needs. There is no second format and no write beneath the vendor's operations: state a seed cannot reach through them is state no customer can have. A seed's steps are checked as any journey's are (status, SHAPE, expectations); it has no judge, because it tells no story.
Where the evidence stops: no derived pack ships a seed yet (seed/ is empty in all six); the rule follows from the
pack layout's "calls to the vendor's own operations" and the lives, whose opening sittings are what a seed would be.
Who is on a screen
A person on a vendor's page is who signed in there: the pack serves the vendor's sign-in page at its own path (built
from @volter/world-ui's SignIn), the person signs in with the password the World gave them (a door under /_twin/),
and the vendor's session cookie names them to every page after. A page asked for by nobody signed in sends its visitor
to the sign-in, which returns them there. A page never takes its person from an API credential, a query parameter or a
default, and a journey carries the cookie as a browser would, captured from the sign-in's answer
(header:set-cookie:<pattern>).
Built in github (src/screens/session.tsx: github.com/login, /session, GitHub's user_session cookie). Not yet so
(the work this rule orders): slack's pages take the person from the request's bearer token, openai's from a fixed
owner. Where the evidence stops: the rule is drawn from the lives' judges, who doubted a token standing in for a browser
in every pack with pages; signing out and a session's expiry are built when a life has cause to reach them.
Streams
A server-sent event stream is derived wire: the manifest declares its framing (sse, streamFor: named events, a
closing sentinel) and a handler answers ctx.sse(events); a journey step reads the frames as its answer (openai's
life streams in eight steps). A byte stream (a line protocol, a WebSocket) is the pack's create<Name>TwinStream,
walked on the in-memory World by a journey's connection steps (on, connect, send), with SHAPE checking each reply
against the spec's (smtp's life holds its whole session this way).
Not yet so (the work this orders): slack's Socket Mode is recorded unreachable because "the in-memory World walks a fetch", which smtp's connection steps contradict; its frames are walked as smtp's lines are once the kit frames a WebSocket, and its record goes. Where the evidence stops: SSE from openai, a byte stream from smtp; no WebSocket has been walked.
One representation of a spec
Every spec is read into one representation, the spec IR (packages/world-tooling/src/spec-ir.ts: operations,
resources, scopes, streams), by its own reader (fromOpenAPI for 3 and 3.1, fromSwagger2, fromSmithy, an RFC's
command-reply table, Protocol Buffers services (spec-ir-proto.ts), a command language's table (spec-ir-commands.ts),
and a vendor's own client (spec-ir-client.ts)), after the pack's patches, and one generator (derive-pack.ts) writes
every derived core from it. A GraphQL schema is read beside it: its root fields are the operations a single endpoint
names in its body, and its types answer through the pack's resolver, not the IR's resources. A vendor that publishes no
spec but ships its own client is read from that client: each request the client makes (its method, URL, query and
body keys, typed by the client's own annotations) is vendored as data, spec/client-ops.json, with the extractor that
made it; the client says nothing of what an answer holds, so the vendor's reference pages give the answers, as examples,
and what the client builds as a raw string, as patches citing the page.
Where the evidence stops: OpenAPI 3 (github, stripe, openai, vercel), Swagger 2 (slack), Smithy (aws), an RFC (smtp), GraphQL SDL (github), Protocol Buffers (planetscale), a command table (upstash). A vendor's client (tinybird's Python client, 128 calls naming all 18 of Dub's Tinybird operations) is read, and no pack is derived from it yet.
The handler
A handler is (ctx) => Promise<Response>, bound to an operationId. Its context carries the request as the core
validated it (params, body, the calling actor, the World clock) and the only doors to state: reads (get, rows,
history), a write through ctx.write (the core records it in the World's log, asks the machine where it moves a
state field, and calls the manifest's onWrite), the machine's own question (legal), the context at the moment a move time makes fell due (at), the
generic core's answer to the same call (core), and the vendor's answers (reply, refuse, notFound, stream). A handler imports the kernel's types, never its functions: whatever else it
needs is on its context. A write is a call, not a returned decision: handlers make several writes and cascades in one
operation (a paid invoice pays its payment), and each still passes the same door.
Rejected: a handler that returns a write decision for the core to apply. Where the evidence stops: the five packs with handlers.
Attestation
A judgment is an attestation: a life's plausibility, a transition's source, a record that code or a move is unreachable, the review of a semantics change. Each is keyed by the hash of the content it depends on and is stale when that content changes:
| Judgment | Depends on | Stale when |
|---|---|---|
| a life is plausible | the life | the life changes |
| a transition's source holds | the transition and its source | the transition changes |
| code or a move is unreachable | the record and what it names | either changes |
| a semantics change is right | the changed transitions and handlers | a later change touches a transition or handler it covered |
A change to a pack's code does not stale plausibility. A spec update is diffed per operation, and only new or changed operations need a judgment. The mechanical checks (replay, spec validation, invariants, the report) rerun in full on every change and call no model.
A change's attestations ride in its changeset (.changeset/) and reach CHANGELOG.md with the
release. Each carries the change's checklist (the core regenerated, with the spec's hash; every
handler and transition bound to an operationId; the report and its change; the life judged;
transitions sourced; checks held; what was not checked), the author's signature and an
independent reviewer's, from different sessions. The tool stamps model id, session and time; the
agent signs only its judgments. An attestation is a record, never a merge gate.
The share of a vendor's operations classed crud, and so the saving, is unmeasured until the
generator classifies a pack; the index then publishes it.
Where the evidence stops: each of the six packs built to Protocol 3 (openai, github, stripe, slack, aws Secrets Manager, smtp) has one life, and all six verdicts are self-judged by the session that wrote the lives, and say so: no judge in another session has ruled on a life yet. No change's attestations have reached a changeset.
Migrating a pack
Every pack moves to Protocol 3, in the generated index's demand order. A move is done in this order,
and the pack keeps serving its consumers throughout. A move made in a git worktree first links its
dependencies with bun scripts/link-worktree-deps.ts <donor checkout>, so the kernel and every pack it
imports are its own source; score-pack.ts prints the kernel it measured through and refuses one outside
the checkout (worktrees that borrowed a donor's whole node_modules scored their twin against the donor's
older kernel).
- Vendor the spec in
spec/as the vendor publishes it, under the namederive-pack.tsreads (openapi.json, gzipped asopenapi.json.gzwhen large;smithy.json;rfcNNNN.txt), withSOURCE.md: the file, its upstream and commit, its version, the sha256 of the vendor's bytes, the corrections, and who read it. Correct it only by patches with evidence;bun scripts/derive-pack.ts <vendor>writessrc/generated/and refuses a citation that does not hold or a candidate left unruled. A spec that declares no resources yields resources named after its response schemas (resend'sGetBroadcastResponseSuccess), and the manifest keys them by those names. - Write
src/manifest.ts: the error envelope, auth, ids and time, the resources and their state machines, each transition cited and each a rule the twin executes (an ask naming no target takes the first transition that allows it, so a guard that lets a state stay comes before a move sharing its actor and from-states: resend's webhook messages counted every refused delivery a success until it did). Check the cited pages withbun scripts/check-sources.ts <vendor>. Declaring a resource hands all its reads and writes to the core, so a moving pack declares the resources whose state it moves and lists inunmodeledthe operations of theirs the legacy fetch still serves. The error template's{status}is the number,{statusText}the same as text;ids.templatecounts ({prefix}_{n}) or mints UUIDs ({uuid});auth.keyFormatis the pattern every key the vendor issues matches. A refusal whose status and message the vendor does not document takes the vendor's documented error for that class, its message stating the rule, and a comment beside it saying where the documentation stops. On a raw row (ctx.row,ctx.rowsRaw)id,type,updatedAtanddeletedare the kernel's; a vendor field of one of those names is read throughctx.own(row). - Serve the pack through
createDerivedFetch: semantics handlers by operationId, the derived core,crossCutting, and, while it moves, the pack's existing fetch aslegacy. The doors and screens sit in front (doors); a door inside the legacy fetch may stay there until step 5. Every operation that moves a declared state gets its handler here. The outline (step 4's first stage) is best accepted before this step: its acts, mapped to doors, name the capabilities the twin lacks (resend's outline needed contacts, segments and topics, which the old router did not serve at all). - Write the customer life by the procedure: an outline a judge accepts, the life
written from it without the coverage report, walked with
bun scripts/score-pack.ts <vendor>until no check fails and no judge flag stands (every defect a check shows is fixed in the twin, never in the life's expectations), then judged. - Remove the legacy fetch: an operation it answered with the vendor's unknown-request refusal becomes the gap, an endpoint the vendor does not publish is deleted, one it truly served moves to the core or a handler, and a door it held moves in front. A behaviour the move removes as an invention is removed from the capability manifest's assertions in the same commit, since that manifest is a moving pack's T0 until this step. It comes before the examples: while the legacy fetch answers an operation, that operation's examples are due, including those of operations it only refuses (resend's old router refused 66 of the 75 it held).
- Replay the vendor's published examples (Published examples), fixing the twin where it answers otherwise, until the script counts every one and their judge rules plausible.
- Bring coverage to the standard (Coverage) from the judged life and the examples: steps the story already has a reason for, records, deletions; the additions are judged again before they merge.
- The pack is done when its report says so (the report), committed with
bun scripts/score-pack.ts <vendor> --write, which writes the pack'sjourneys/score.jsonandcoverage.jsonand regenerates the index. A vendor with several APIs moves one lane at a time.
Every commit of a move holds the pack's typecheck, bun scripts/spec-census.ts, bun scripts/invariants.ts --check <vendor>, the architecture rules, and bun scripts/manifest-baseline-one.ts <vendor>: for a pack still
moving (a life written, operations left on legacy) that is both its life walked with no check failing and its
capability manifest, and for a moved pack the life alone. A step of the pack's gate.ts replay may run at a
later instant, the World clock moved there, so what the vendor does as time passes (a domain verified minutes
after it is asked) happens between steps.
Where the evidence stops: the exemplars were moved in this order only in part (their semantics came before their lives), and aws and smtp were built to it from the start. Steps 4 and 6 held on all six: each life came from a judged outline, was re-ruled by its own judge after every fix, and was judged again after its coverage additions; the order of 1 to 3 is the orchestrator's, from those moves.
A. Layering & boundaries
- A0 [review] The kernel owns vendor-independent execution and state mechanics. Protocol-2
packs use the shared serve/write seams, tree readers and head's state system. Packs own their
vendor's API, resource schema, validation and query semantics. A common request lifecycle must
not force unrelated vendors into a common resource schema or replace vendor behavior with
generic CRUD; a derived pack's generic CRUD covers only operations classed
crud(the derived pack). Rebuilding log folds or confirmation suppression inside a pack is also drift. - A0b [review] Maximize utility reuse — don't reinvent. Before writing a helper, check
@volter/world-coreand@volter/world-tooling. Reuse the shared log, tree readers, checkpoints, branching, observation diffing, blob storage and verification machinery. Vendor-specific pagination, filtering and typed response construction remain the pack's responsibility. Review each new helper for duplicated mechanics and each abstraction for false vendor uniformity. - A1 [auto] Runtime layers: the shared libraries + operator control plane
(
packages/world-core,@volter/world-core), vendor packs (packages/twin/<vendor>), and the optional world-profile orchestrator (packages/world-runtime,@volter/world-runtime). No shared vendor-logic library. The world runtime composes independently runnable twins; vendor packs do not depend on it. There is also a dev-only@volter/world-toolingpackage (packages/world-tooling) holding the conformance framework — it ships no runtime code and a twin runs without it (see E2). - A1b [review] Support/helper packages (optional,
packages/twin/<name>, e.g.browser-assets): reusable CLI tools that support worlds but twin no specific vendor (no vendor API surface, no capability manifest). They are exempt from the vendor-pack shape (C1) and the manifest gate — listed in theNON_VENDORset, hand-mirrored (bash can'timporta TS constant) at all 6 sites:scripts/architecture.test.ts,scripts/capability-manifests.test.ts,scripts/twin-check.sh,scripts/ui-scope.ts,scripts/spec-census.ts,scripts/pull-audit.ts.scripts/architecture.test.ts's "NON_VENDOR mirror-site identity" test (R-T8/TWIN-86) parses every copy back out of source and gates on them being the same 5-element set — treat that test as the enforced authority if this prose and the code ever disagree. They MUST NOT import a vendor pack (A3) and MUST NOT hardcode app-specific config in the kernel (A2). A vendor pack MAY additionally ship*-service-clibins that launch the REAL external service for that vendor's integration/real-media story (e.g.livekit's real media-server / egress / redis) — those still obey B3 (no vendor SDK at runtime) and carry no app-specific config; the launcher is infra, not twin behavior, but it lives with the vendor it serves. - A2 [auto] The kernel is vendor-agnostic:
packages/world-core/srcMUST NOT import any vendor pack or any vendor SDK, and MUST NOT branch on vendor identity or hardcode a vendor's routes/hosts/fields in control-flow (vendor-specific knowledge belongs in the pack'sTwinPackdescriptor, not a kernel switch table — e.g. the dev proxy reads each vendor's browser routing fromTwinPack.browserRouting). Vendor names may appear in human-facing help/error strings, never in logic — the guardrail allows the former and forbids the latter. ⚠️ The[auto]regex is necessary, not sufficient: it matches honest quoted/bare vendor names but can be defeated by obfuscation (e.g.['cl','erk'].join('')). Such evasion is itself a violation — the real rule is "no vendor/app glue in the kernel; put it in the pack descriptor,world-runtime, or a cookbook." The §-review must read kernel diffs for obfuscated evasion, and app-specific launchers (browser proxy wiring, etc.) belong inworld-runtime, not the kernel. - A3 [auto] A vendor pack MUST NOT import another vendor pack. No cross-vendor coupling.
One vendor's single store split across two packs (X's credentials:
xidentityissues OAuth 1.0a tokens,xserves the API they authorize) is not coupling between vendors: the pack that owns the rows writes them and states their shape as a contract at its definition; the other reads them throughprojectOwnerResources(<owner>, root, subject?), never writes them, and never imports the owner's code; an integration test runs both packs on one root, so a renamed row fails. Every (reader, owner) pair is declared inscripts/architecture.test.ts(OWNER_READS), and a call outside it, or one whose owner is not a string literal, fails the check. In a World each twin runs on its own root (<data>/<service id>, the data directory the runtime hands every service asVOLTER_WORLD_DATA), so the owner's store is not under the reader's: the kernel'sownerStoreRootsfinds it on the reader's root when that root holds it, else on the World's service roots that do. A read naming a subject (the token a request presents) takes the one store holding that subject, and is refused (OwnerStoreAmbiguousError, which the reader answers as an invalid credential) only when two hold it; none is an empty read. Only the declared cross-pack read resolves this way; a pack's reads of its own store never leave its root, and the read claims no journal identity. The owner's rows are memoized per store on itstreeStamp, so a per-request lookup refolds only after the owner writes. The roots stay separate rather than one shared store root per World: a root keyed by service id keeps two services of one pack (or a process service's own files under--root) apart, and existing Worlds,branch(which forks each service's root) andcheckoutkeep their layout. - A4 [review] No cross-vendor "query engine" or shared query semantics. Each pack answers its vendor's queries with that vendor's own semantics (REST filters, JQL, GraphQL resolvers). Shared plumbing (pagination cursors, zod helpers) is fine; shared meaning is not — it manufactures false similarity.
- A5 [review] Vendor-specific behavior lives in the pack. If you're tempted to add a vendor's quirk to the kernel, that's drift — generalize it or keep it in the pack.
World resource and diagnostics boundary
Gate startup never signals processes selected by command-line or temporary-directory patterns. Such matches establish neither ownership nor completion; process inspection is read-only. Cleanup belongs to the recorded World or the test's own retained child handles. Gate shard output is retained as it arrives, so an interrupted runner leaves diagnostics. An output-storage failure cannot produce a passing shard verdict. Tutorial cleanup discovers Worlds only inside its own scratch directory without following symlinks, and delegates teardown to the World runtime. A failed teardown retains the scratch directory and reports the failure; fixture removal never discards unresolved ownership records. Tutorial shells join their background jobs and exit before World teardown begins. A failed join retains the scratch directory. Repeated serve signals share one shutdown operation; its deadline reports incomplete cleanup rather than success. Explicit down may reclaim a valid boot marker only when the OS proves its owner absent. Likewise, when a World's instance metadata is gone, down releases its lifecycle record only when the record is this host's and this user's and its owner and every recorded process have exited: nothing remains to tear down. Any live, foreign or unreadable record is retained. Live, uninspectable and malformed boot ownership remains protected; marker reclamation never substitutes for stopping the recorded resources and confirming teardown.
Session TLS leaves are cached by signing CA identity and hostname. Separate Worlds and a
replacement CA at the same path must never reuse a leaf signed by another instance.
Session trust supplements public and caller trust. Variables that replace a client's CA store
receive a bundle of public/default roots, its configured certificates and the session CA;
NODE_EXTRA_CA_CERTS preserves the caller's supplemental certificates. Trust bundles are built
on use, content-addressed under the World's TLS directory, and separate from its signing CA.
HTTP routing through the scoped proxy preserves the configured twin URL's namespace path,
as the Node injector does. VOLTER_TWINS_KEY supplies x-twins-key only on requests routed
to configured twins; vendor Authorization remains unchanged. HTTPS twin endpoints retain
TLS verification even when the original client request uses plain HTTP.
A database twin is reached through the vendor's HTTP driver. An ORM whose client-engine build
requires a driver adapter (Prisma's, the build a browser tab runs and an edge client uses)
gets that adapter from the injector when the application constructs the client without one:
the pack declares prismaAdapter (the adapter package, its export, the URL variable the
endpoint template renders), the injector constructs it from the application's own
dependencies over that URL, and an application that passes an adapter or does not install
the package is left as it was. The World never installs the adapter; the fact is data on the
pack, compiled into pack-facts like the hosts.
Proxy startup records its child before waiting for readiness. Timeout and publication failure
retire that child before fallback; uncertain retirement keeps its PID and lifecycle evidence.
A startup intent is persisted before spawning and retained until PID publication or confirmed
retirement. An unresolved intent blocks replacement, teardown completion and pruning, so a
failed PID write cannot make an unknown child disappear from the lifecycle contract.
PID-file updates share a mutation lock and preserve recorded live or uncertain processes,
including proxies not yet represented by published proxy state. A successful boot rollback
retains stopped metadata and a teardown receipt for inspection and explicit pruning.
Both normal shutdown and failed-boot rollback publish that receipt before releasing lifecycle
ownership; publication failure retains ownership. A zombie awaiting OS reaping is already retired and holds no World resources.
World config stripEnv: ["*"] starts services and attached commands without the caller's
inherited environment. Declared config/service variables and World-generated injection remain.
The policy is retained with the instance and applies to shells and external lifecycle commands
as well; removing the source config cannot widen inheritance. This includes caller NODE_OPTIONS: stripping it must not remove World's own preload or let
an outer preload cross the boundary. Named and prefix filters keep their existing behavior.
This controls environment inheritance; it is not filesystem or OS credential isolation.
Task-owned command lifetime is distinct from persistent service lifetime. up and
run --keep retain explicit teardown. A normal run owns teardown through command
completion and loss of its caller; the ownership channel and instance generation,
not an old PID or elapsed idle time, authorize cleanup. Failed teardown retains its
lifecycle evidence. Other registered attachments remain protected.
CLI env and attach register the command against the same instance. Cancellation
has a bounded grace period and reaches the command's owned process group on POSIX;
the group is private to the command. The synchronous SDK execution API retains its
existing compatibility and reports its untracked coverage. Lifecycle generation records persist independently of the original admitting PID.
Task cleanup is owned by a separate process before admission, linked to its caller by a private
IPC descriptor never inherited by commands. It never restarts services. Cancellation settles
owned startup commands before rollback; external teardown metadata is published before their
startup begins. Boot and teardown retain durable lifecycle ownership until successful cleanup. Each boot tracks its own service groups. Group retirement checks include descendants;
unconfirmed retirement retains ownership and cannot become an ordinary readiness miss.
External-only Worlds use their matching lifecycle generation for runtime status rather than an
inert holder process. Doctor still probes the declared external services. Unreadable instance
metadata refuses teardown before any signals; missing instance metadata with retained lifecycle
ownership also refuses teardown. Neither a missing file nor a missing PID is a cleanup receipt.
External status, teardown and readiness commands are asynchronous with bounded waits (60 seconds
for status/teardown, the declared remaining readiness budget for probes).
The World is the lifecycle and diagnostics boundary. Local startup checks the actual state
destination and required backends; it does not reserve speculative machine-wide memory or
storage. Deprecated config resources values are neither admission requests nor enforced
limits. A declared service that creates subordinate compute MUST enforce explicitly configured
native limits, report unsupported requested limits, and forward causal failures into its log.
Known additional storage requirements may be checked against their actual destination without
promises about subsequent concurrent writes. Lifecycle identities, startup intents, consumer
records and unresolved cleanup block unsafe replacement of the affected World independently
of capacity accounting. Application callers and agents MUST NOT operate infrastructure behind
that boundary separately. Failures identify the World, service, actual operation, available
measurements and retained diagnostics; failed diagnostic writes preserve the cause on stderr.
A process service's World parent, and the co-located host's, is the service recorder. The service
writes its output straight to its log (a pipe through the recorder would lose a crashing service's
last output); the recorder caps the log (64 MiB, copied to <log>.1 and truncated in place) and
writes a time mark about every ten seconds while output flows, the time granularity a line has. It writes a start record and an exit
record (exit code or signal, pid, run time, whether it was asked to stop) into the log and into the
World's logs/events.jsonl, the one list of every service start and end (the proxy daemon records
its own there; volter-world events prints it). Each service is handed VOLTER_WORLD_SERVICE_LOG_DIR
(logs/<service>.d) for the log files it writes itself, which status lists; the World's own stops (down, a boot's rollback, and a SIGKILL after the grace) are recorded
there too, so a stop is attributed to the World or to an outsider. status and doctor report a
stopped service's end from those records with its last output and its log path; a start without
an exit says the parent died with the service. A service that ends during startup fails up at
once with that account, not after its bind or readiness timeout. A fresh up keeps the previous
run's logs as logs/previous (one generation), so a crash's record survives the restart after it.
A service's state is judged by its process group, apart from the World's ownership: status and
list say degraded while any service is down, even when the proxy daemon or a tunnel keeps the
World running, and attach names the down services before it proceeds. doctor re-runs each
service's declared readiness probe (an accepting port is not an answering service), checks the
proxy daemon, whose output is its own log, and names a dead tunnel. The proxy's refusals and
failed twin requests name the twin, its origin and the cause, and are written to that log. A
failed run names each service that ended on its own during it.
A recorded pid names its process only while that process's start time (read in a fixed zone and
locale, or Linux's start tick) matches the one recorded at spawn: a reused pid is never signalled,
never keeps a World running, and down names it as not signalled. Rollback never lets its own
failure replace the boot's error; cleanup failures and attachments that stay uncertain for two
minutes and block a task's teardown are written to the lifecycle log, and the task then stops
waiting on them. An --env-file the runtime creates is owner-only.
The CLI's up boots in an owner process outside the caller's process group, the shape of run's
task owner: the caller's interrupt cancels the boot, which rolls back; losing the caller (a closed
terminal) does not, and the boot's outcome is written to the lifecycle log.
When a service's port first answers, up confirms the answer is the service's: every service waits
a short settle (a program that lost its port to another process ends at once, and fails the boot
with its account), and a twin answers /__volter/world-boot (the kernel seam serves it inside a
World) with its boot's id, so a leftover World's twin on the port fails the boot by name. A
program that neither ends nor answers the path is accepted on the settle.
Resource ownership is explicit metadata, independent of a lifecycle holder PID. Local CLI
attachments register one consumer per command, bound to the running instance, with a host-clock
heartbeat; reports retain unknown legacy, detached, activated-shell and remote consumers as
unknown. A stopped or expired consumer never authorizes automatic World teardown. A command
whose runner died before recording completion stays uncertain until the owner resolves it with
consumers retire, which retires a record only on this host and instance when its runner, its
command and the command's process group are all gone (survivingOwnedGroups), and keeps it,
with the reason, otherwise. down
checks registered consumers before releasing lifecycle ownership, and prune still requires its
successful teardown receipt. Resource inspection reports actual filesystem capacity and lifecycle inventory in a
versioned schema, never subtracting declarations from already reduced free space. Physical
memory is not measured usage or a scheduling guarantee. Legacy combined reservation/lifecycle
records remain readable and inspection never deletes them. An unresolved legacy record blocks
only that World's migration and cleanup; corrupt unrelated records never block new startup.
Managed installations migrate at verified stop boundaries with all lifecycle entrypoints pinned
to the new major runtime. Hosted consumers with legacy resource declarations must review their
capacity contract before adopting it. Neither diagnostics nor disk pressure authorize stopping
another session's World, deleting shared caches, or removing parent history.
Inspection distinguishes instance creation from last observed command use, reports the
observation source and partial coverage, and never updates activity merely by reading it.
Completed attachments retain their latest observation for that instance; a new instance
does not inherit it. These timestamps inform ownership review, never automatic teardown.
Local command attachments set VOLTER_WORLD to the selected World even when the caller
inherited a different marker. Strict egress refusal applies even to an empty World with no
redirected vendors; an empty local World leaves the client functions unchanged.
The injector rewrites at http/https and global fetch; net.connect/tls.connect carry
its backstop for clients that open their own sockets (npm undici's Agent). A socket to a host
a configured twin claims is refused in every mode, naming the pack's endpoint env when it has
one, because it went around the rewrite; a socket to an untwinned host is refused where strict
egress or the network policy refuses it. Loopback, private addresses and configured twin origins
pass. The backstop refuses; it never redirects, since a raw socket carries no request to route.
The redirect proxy answers a host it refuses (sealed, or outside the World's network policy) itself: a CONNECT to port
443 of a DNS name is terminated with the World's TLS and a request on it is answered 502, naming the host, as a page for
a browser (the address bar keeps where the request was sent) and as text otherwise, unless its Host names a host the
World routes, which is forwarded there; any other refused CONNECT, and every one past the first 64 names a proxy has
answered so, is refused on the tunnel line. Each is logged when refused; nothing leaves the World. A name the World
lets through (its own .test names among them) whose tunnel cannot connect on 443 gets the same page, saying why
(a name that does not resolve), within the same 64.
The application's own production hostnames are routed to the application, as DNS routes them to its host in
production: volter-world app-url <world> --host <name> records them beside the application's URL
(app-url.json in the instance; with no URL recorded, the World's own app service wherever a boot puts it), and
after twins and the hosts twins claim, resolveTwin answers a recorded host with the application's origin. The
injector and the redirect proxy route by that answer, reading the record from the instance their World env names,
at most once a second, so a host or URL recorded after the application started applies; the socket backstop refuses
a raw socket to such a host in every mode, as it does a twinned one. A routed request is a real request to where the
application listens (streamed, abortable, the caller's redirect mode), keeps its Host and carries
x-forwarded-host and x-forwarded-proto (https where the caller used TLS), so an application that serves several
hostnames (Dub's app, partners and api) answers each as in production and sets its secure cookies. A recorded
wildcard (*.dub.link) answers every name below its parent and never the parent, as a DNS wildcard record does, for
the names an application creates while it runs (a Dub program's acme.dub.link). A name a vendor's host rule gives its
twin is never the application's, whether or not that twin runs in the World: routing enforces it, and recording
refuses a host or wildcard that names one (reading the descriptors' hosts, suffixes and pattern tails and the hand
table's names). The vendor host table has one dependency-free home (vendor-hosts.cjs), and so does the route with
its request options (@volter/world-core/app-route), both read by the injector and by the colocated twin host, which
runs without the injector: a twin's own outbound request (a QStash delivery, a webhook) asks appDestination, before
worldEgressRefusal since the application is inside the World, and deliverToApp makes it where the application
listens with the same Host, forwarded headers and World CA, never the World key; appFetch is that request in
fetch's shape, for a delivery that reads fetch's answer: it keeps the caller's redirect mode, routing each hop again,
and a hop that leaves the application goes out only where worldEgressRefusal lets it, without the World's headers;
the application's own origin meets the same rule (an app URL recorded outside the World), and a request is bounded as
fetch's is (300 s). A twin's delivery does not
consult the hosts twins claim. A name the World routes (a vendor's host, a claimed host, the application's) also resolves in a
process the injector is installed in, so code that looks a name up before fetching it (an SSRF guard) meets a public
answer: an address in the documentation ranges (203.0.113.7, 2001:db8::7), routed nowhere, a socket to which is
refused with the reason (a guard that refuses every non-unicast range, as ipaddr.js classes these, still refuses
them); every other name is the real resolver's. A lookup or fetch waits a claiming twin's first read 1.5 s at most. The application
is not a twin: it never receives the World key, and a redirect that leaves it drops the credentials and the World's
headers as crossing origins does. A vendor's host, the World's own origin and a twin's are refused when recorded,
and a host a twin claims while it runs takes precedence; nothing on these hosts is twinned or journaled.
Command launchers default the injector's informational routing banner to quiet, inherited by
child processes. An explicit verbosity option overrides the admitted environment; otherwise
an existing quiet setting is preserved. Warnings, errors and routing are unaffected.
B. Dependencies
- B1 [auto] Core runtime dependencies are
zodandwsonly. Thewsimport is confined toserve-http.ts, the Node WebSocket host adapter. Vendor SDKs remain forbidden. - B2 [auto] Each pack declares
@volter/world-coreas apeerDependency(so installed twins share one kernel version), not a harddependency. (devDependenciesmay also pin it for local dev.) - B3 [auto] The vendor SDK (
stripe,@linear/sdk,jira.js,@octokit/rest,@slack/web-api) is adevDependencyonly and is imported only in*.test.ts— never in runtime code. A twin fakes auth locally; it must run without the SDK installed. - B4 [review] A pack's runtime
dependenciesare limited to@volter/world-core(peer), the UI runtime (react/react-dom), and genuinely-needed vendor protocol libs (e.g. linear'sgraphql). Never the vendor SDK.
C. Pack shape (uniform across all vendors)
- C1 [auto] A pack ships (a derived pack has its own layout):
index.ts(public API),cli.ts(binworld-<vendor>withserve/conformance, plusmirrorwhen the pack has one),<vendor>-twin.ts(transport- behavior over kernel state), a connector (
*connector*, pull + push), a conformance harness (*conformance*), and a<vendor>-capabilities.tsmanifest +<vendor>-capabilities.test.ts.
- behavior over kernel state), a connector (
- C1b [review] A mirror UI (
*mirror*/*ui*) is mandated for every vendor except those whose product is the API. The test is NOT "does the vendor have a UI" (nearly all do, including OpenAI's console) — it is "when someone does this vendor's core job, do they open a browser or write code?" Design in a canvas, write in a page, talk in an app, drag tickets on a board → the UI is the product → mirror it. Call the API while the console is incidental key/billing tooling (LLM gateways, TTS, geo/weather/data APIs — OpenAI, Anthropic, ElevenLabs, Mapbox, …) → omit the mirror rather than fabricate a dashboard, and state the reason in the README## Coverage+ the manifest; coverage is API/connector, not UI. Note that a thin or read-only REST API is evidence FOR a mirror — Figma's API is read + comments only precisely because the work happens on the canvas. When a mirror exists it reflects the vendor's real surfaces (data-coupled, proportional to the actual product) and is covered by UI capabilities, folding the same projection and sharing the sameapplyXxxWriteas the API handler (API↔UI parity). The decision procedure + the current per-pack split live in ./adding-a-twin.md ("Does this vendor get a mirror?"). This is a judgment the guardrail can't make generically, so it's enforced in review (§9), not mechanically. - C2 [auto]
package.jsonexports["."]→./src/index.ts;binexposesworld-<vendor>→src/cli.ts.
D. State & behavior invariants (the architectural contract)
D1 [review] State is the kernel's append-only logical log and checkpointed tree, including inherited entries and the branch's own entries. A pack uses the shared write path and tree readers; it MUST NOT keep a parallel mutable side-store as the source of truth.
- Durability: every event/action append is a completed
appendFileSyncwrite, so a process crash after it returns still leaves the record on the log. The remaining window is an OS crash / power loss between the write landing in the page cache and the filesystem flushing it to stable storage (a lost or torn tail record). SettingVOLTER_DURABLE=1closes that window byfsyncSync-ing after each append (the kernel'sappendDurablehelper), at the cost of an fsync per write. It is off by default — the process-crash guarantee is enough for local dev/e2e — and should be turned on where real pulled staging data lives in these files (purpose 2).
- Durability: every event/action append is a completed
D2 [review] A twin presents the vendor's exact API and response shapes. An operation that isn't modeled yet fails like the vendor (404/422/etc.) — never a fabricated success and never invented data. (This is the honesty rule; it's also the conformance contract.)
D3 [review]
readOnlyforbids local writes (the vendor-shaped 405/method-not-allowed), so a pure mirror serves reads and rejects mutation.D4 [review] Simulated execution is local and deterministic. Real vendor calls use the connector or the real head's kernel executor, with an injected client and sealed credential. The executor applies the credential BY STRATEGY (
executor.ts): header replacement by default; a query parameter, or anhmac-sha256/aws-sigv4signature computed per request, when the pack declares that vendor fact inTwinPack.auth. The executor dispatches on the strategy NAME and never on vendor identity (A2); a pack supplies only pure canonicalization (canonical,scope) and never holds the secret; a declared strategy with no secret FAILS CLOSED. Held byexecutor.test.tsand, for SigV4,scripts/sigv4-conformance.test.tsagainst an independent implementation. Pack request handlers never make direct vendor network calls. Reads use held state; refresh observes the vendor explicitly or on its configured schedule. Verification injects fakes. An explicitly selected local-generation capability may hand supported generation misses to a separately declared, World-owned local service. This is an optional extension, not simulated execution or a new World mode: unselected twins remain deterministic, scripted responses and faults take priority, and read-only refusals still apply. The pack validates and makes one scenario decision before handing off, without recording a preliminary stub. The local service owns inference and its generation accounting; twin history does not claim those results as replayable simulated state. The handoff preserves streaming, errors and cancellation, refuses redirects and hosted destinations, and sends no vendor credentials. Runtime retains service lifecycle ownership; neither the kernel nor the pack embeds an inference engine. Capability verification remains deterministic with injected local fakes; real inference is separate integration evidence.D5 [review] Capability
verify()predicates exercise the twin (local round-trip), never the real vendor. A manifest check must run offline and deterministically.D6 [review] A connector's pull path MUST fold observed mutable resources through the kernel's
observeResource(protocol 2: the pack observes, the kernel diffs each resource against the tree and appends to the root's log only what changed). A pull that appends rows itself re-appends unchanged state and breaks the shadow-diff contract downstream consumers depend on.D7 [auto] Every connector MUST export a top-level
sync<Vendor>FromRealfunction that takes the injected vendor client (the real SDK in production, a fake in tests) and options and returns a standard result ({ observed, deltasAppended, ... }). It is the consumer-facing pull entry point, so a caller never needs vendor-specific pull mechanics. Under protocol 2 it observes each resource throughobserveResourceand is the vendor-client half that the descriptor's refresh adapter (stateSystem.refresh, by conventionsync<Vendor>FromRemote) wraps over the kernel executor — see the real-system adapters.scripts/architecture.test.tsasserts each connector exports a function matchingsync<…>FromReal; the vendor segment is free-form (syncDynamoFromReal,syncS3FromReal), so the guardrail matches the shape, not an exact spelling.D8 [review] A connector for a vendor that rate-limits a live token MUST route every real call through one guarded client carrying a persistent, fail-closed budget: a durable rolling-window spend ledger, consulted before the request goes out, that throws instead of calling past a conservative ceiling or while a
Retry-After/429 cooldown is armed. It persists across processes (a fresh process must not get a fresh allowance), charges at check time so a burst is refused rather than raced through, and treats an unreadable ledger as a full window — never as zero spend. Corollaries: raw vendor API calls outside the connector are BANNED (no discipline inside the connector can restrain them, which is the whole point); retries are off by default; and cache/quota logic is proven with an injected fake, offline (D5) — never by calling the live vendor to "check". Rationale: a real ~4.5-day Figma token lockout came from calls made outside an otherwise well-behaved connector. The mechanism is vendor-agnostic and lives in the kernel (packages/world-core/src/rateBudget.ts, exported from@volter/world-core); each pack supplies only its numbers as data (TwinPack.rateBudget/declareRateBudget— the same "vendor knowledge in the descriptor" pattern asbrowserRouting, so A2 still holds). A vendor with no declaration is not unlimited — it falls back toDEFAULT_RATE_BUDGET, and there is deliberately no opt-out.A declaration may be more permissive than that fallback only when the vendor's own documented limits justify it, and then the
reasonmust carry the number. Where a vendor publishes no scalar limit (Figma, OpenAI, Sentry, Slack's tightest tier) thereasonmust say so rather than presenting a guess as a vendor fact — and the ceiling stays at or under the fallback unless a different, stated sizing principle is given. Permissiveness is judged on two axes, not one: sustained calls/hour and the burst a single window admits (an hour-long window is 20× the fallback's burst even while being tighter per hour —githubandlinearboth are, and both say so in prose).This is machine-checked across every declaring pack at once by
scripts/rate-budget-isolation.test.ts, which also holds the roster of guarded vendors. It derives that roster from the filesystem (globbingpackages/twin/*/src/*-budget.tsand importing each), not from the in-process registry — a registry-only check passed green whileslackdeclared a budget nobody had listed. It fails if a pack declares a budget without being listed, if any pack's numbers change shape on the way through the shared mechanism, if any endpoint is priced free, if a window is shorter than the fallback's, or if two packs would share a ledger for the same credential.The burst check is arithmetic against a hand-written vendor figure, never against prose. A pack that out-bursts the fallback must appear in that file's
VENDOR_BURST_ANCHORtable — the vendor's own documented per-minute allowance, in that pack's units, written by a human from the docs — and itsburstCeilingmust fit inside it. The first version of this check accepted the declaration's own longer window as the justification for the burst that window created, and accepted a per-minute phrase appearing anywhere in thereason; both made it unfailable, and three hostile declarations passed. A hostile-replay test now runs the same rule function the gate uses and proves each is refused. Never let a declaration supply its own justification.A guarded factory validates an injected budget by METHOD IDENTITY, not
instanceof(assertBudgetGuardIntact).checkBudgetis an ordinary prototype method, so a one-line subclass — or aProxytrappingget— satisfiedinstanceofand disabled the ceiling entirely. The three functions a factory calls must be the very onesRateBudget.prototypedefines.Wired today (90):
Three workedahrefs,airtable,algolia,anthropic,axiom,bitly,bluesky,calcom,cerebras,cloudflare,cohere,datadog,deepinfra,deepseek,discord,dynadot,elevenlabs,expo,fal,figma,firecrawl,fireworks,fly,gcs,gemini,github,googleads,googlemaps,googleoauth,groq,hubspot,inngest,instagram,intercom,langfuse,linear,linkedin,livekit,mapbox,mistral,mixpanel,moonshot,notion,npm-registry,oa-treasury,openai,openrouter,openweather,paypal,perplexity,pinecone,plain,planetscale,polar,posthog,reddit,replicate,runhuman,scrapecreators,segment,sendblue,sendgrid,sentry,slack,smtp,snowflake,stigg,stream,stripe,supabase,svix,tavily,tiktok,tinybird,togetherai,tremendous,tunnel,twelvelabs,twilio,upstash,upstashvector,vercel,veriff,vital,volteridentity,webrisk,x,xai,xidentity,youtube.examples, one per shape:
twin/figma/src/figma-budget.ts(a pack that builds its own HTTP client),twin/notion/src/notion-budget.ts(a pack that decorates an injected SDK client), andtwin/github/src/github-budget.ts(a vendor whose published scheme is already a weighted point budget, so the declaration reproduces its own numbers rather than proxying them).scripts/rate-budget-isolation.test.tsdrives one pack of each of those three shapes against a shared ledger root, so cross-vendor isolation is proven for every shape, not just the first.One vendor's numbers are shaped by the burst rule rather than only by its own docs:
sendblueneeds a 120s window (the shortest that expresses its documented 30-lookups-per-HOUR limit exactly) and prices a lookup at the whole ceiling, soburstCeilingcannot be tighter than the ceiling without refusing the documented endpoint outright. The ceiling therefore carries the burst bound and is halved to 60 units, i.e. 30 calls drained at once — exactly the fallback's burst — at the cost of a sustained rate half the fallback's. Tighter is the only safe way to resolve that clash.slackis the one partial: the client the pack itself builds (makeSlackReadClient, used by theworld-slack shadowCLI) is guarded, and the unmeteredurl_privatefile downloader refuses a Web-API host so it cannot be used as a way round; but the push/sync entrypoints take an injectedSlackClientLike(a realWebClientin production) whose calls do not pass the guard. Closing that needs theguardNotionClientdecorator shape. The gap is stated in the connector and pinned by a test, so "slack has a budget" cannot be misread as "every Slack call is budgeted".jirais the one connector pack with nothing to wire: it constructs no client at all — the consumer implements the wholeJiraExecutorinterface — so there is no in-pack site where a live call is issued. Guarding it would mean applying the decorator at each connector entrypoint, the Notion shape; until a consumer needs it, the honest state is "no live call site, no budget".
E. Tooling & framework placement
- E1 [auto] Repo tooling (
scripts/) is dev-only and not referenced by any pack's runtimeindex.ts/cli.tsor its publishedexports. - E2 [auto] Conformance/validation tooling is NOT shipped in the runtime kernel. The shared
framework — the capability checker (
checkCapabilities,assertManifestBaseline,CapabilitySpec), the spec-conformance / recorded-diff / UI-conformance harnesses, and the spec derivers — lives in@volter/world-tooling(a dev dependency), never in@volter/world-core. A twin runs without any of it. Per-vendor manifests + conformance harnesses live in each pack but are dev-only: they must NOT be re-exported from the pack's runtimeindex.ts/cli.ts(the surface real apps import). The framework must contain no vendor-specific logic (A2).
How this is enforced
- [auto] rules →
scripts/architecture.test.ts, run byscripts/twin-check.sh. A new pack or a drifting change that breaks a rule fails the gate. - [review] rules → the per-cycle architecture audit (this doc is the rubric). Each audit cycle and any structural PR should re-check the [review] rules, since they can't be fully grepped.
Portable host and immutable payloads
The HTTP host supports TLS and WebSocket upgrades on Bun and Node. Node loads ws only
inside serve-http.ts when an upgrade adapter is configured. Runtime fronts relay text
and binary messages without introducing vendor state. Remote attach composes the session
CA with an existing extra CA bundle, preserving both caller trust and session trust.
The Node HTTP host gives each Fetch request a signal tied to client disconnect and forced server shutdown. A completed upload is not a disconnect. Disconnect interrupts response reads and backpressure waits, cancels the owned response reader, and retires listeners; late handler responses are canceled rather than written to a closed socket. Body failures after headers destroy the transport, never append a successful ending or a second response. Scenario serving accepts an optional caller signal: an already-aborted request makes no decision; abort during a slow or drop fault clears its timer and prevents later realization. A decision already made keeps its once/phase accounting; cancellation does not replay it. These are shared transport and scenario mechanics, independent of vendor or inference service.
Binary remote requests retain their bytes through signing and transmission. Resource
payloads stay in the blob store and follow retained branch ancestry when absent locally;
a corrupt child payload must not be replaced silently with an ancestor's copy. Branches
reference parent state. Local parent pointers register durable child references before publication;
removal and registration share a coordinator outside the removed tree. A multiplexing host
front leaves vendor request journaling to its downstream twins; post-response logging must
not recreate a retired World directory. Fresh boot, reset,
purge, prune, core scrub and host deletion refuse removal of a referenced parent, while ordinary
stop retains history. Parent generations and branch positions are validated on read. Known
legacy World trees are checked before cleanup; an undiscovered legacy fork outside those
trees has corruption detection, not guaranteed preservation. Raw filesystem removal and old
binaries bypass this protocol. No timestamp or absent process is authority to discard a
committed child reference.
Local blob writes may reuse identical bytes through hard links on the same filesystem,
within one OS user's ownership. Every World retains its own ordinary file path; replacing
a blob atomically breaks sharing for that path, and removing a World never removes another
World's bytes. A bounded, disposable per-user lookup index stores paths and digests only,
never payloads or liveness authority. Reuse verifies the linked bytes before publication;
missing, corrupt, inaccessible or unsupported candidates fall back to an ordinary atomic
write. Index loss or failure cannot lose data or fail an otherwise successful blob write.
Deleting the last owning file frees its bytes without a separate archive-cache cleanup.
Shared payload files are immutable outside BlobStore's atomic replacement API: direct
in-place filesystem writes or permission changes affect every hard link. This is storage
deduplication within one OS user, not a security boundary between hostile processes.
Observation cursors use vendor fields distinct from kernel metadata (observedUpdatedAt
for GitHub), preserving the catalog's ownFields convention.
Prune shares shutdown's liveness definition: an exited child awaiting reaping is stopped; an uninspectable process is retained. Only a matching successful teardown receipt permits pruning. Inspection never grants permission to stop a running world.
Published Node CLI entry points identify the executed file through its resolved path when
import.meta.main is unavailable. Importing the CLI as a library must remain inert.