The control surface
Concept (informative) · For: operators and client authors who want to know how the manager and other daemons are driven · Normative: SPEC §13
Cotal once had a privileged control rail: a fixed set of named service tiers
(self / manager / admin / delivery) on their own ctl.* subjects, with the manager
as a special case the broker recognised by name. That rail is gone. Everything that serves
structured commands now, the manager, the delivery daemon, a wrapped MCP server, a
third-party service, is an ordinary endpoint: a daemon that registers a service
identity, publishes its contracts, and answers describe. manager is an endpoint name
like any other; no subject, envelope, or grant in this surface knows it specially. The
manager is a service on the mesh, not an authority over it: it holds only the capability
rows its callers grant it, and serves over a scoped credential.
The ep rails
Section titled “The ep rails”One kind, ep, carries every request under a mode token that says where the request
routes, never which verb it is (the verb rides the envelope): one (queue-group
anycast, one and only one class member), all (scatter, every instance), and inst (one instance by its
stable address). Replies come back on a reply rail keyed to the serving instance and its
epoch. Around these sit the sibling planes the composites use: per-goal events, timers,
sessions, and the journal that holds durable facts. Every request carries the caller as
three forge-locked tokens, owner, actor, and lifecycle uid, plus an unguessable
nonce, so the broker polices who is calling in the subject grammar itself. See
SPEC §13.2 for the grammar and §13.5 for
the verbs (call, cast, watch, claim, scatter).
Lifecycle identity
Section titled “Lifecycle identity”A principal owner.actor is a reusable routing alias: a despawn frees the actor name and a
later spawn may legitimately reuse it, so the alias alone is never authority. Two further
coordinates make an identity durable: a lifecycle uid, an unguessable, never-reused id
for one managed lifecycle under a principal, and a process epoch, the fenced ownership
epoch of the process currently animating it, advanced on every restart or takeover. At most
one live epoch owns an identity, and a superseded epoch must stop serving. Durables and
credentials key on the lifecycle uid, not the reusable name, which is what lets a
supervised restart recover the same lifecycle instead of minting a new one. See
SPEC §13.1 and identity & auth.
Service discovery
Section titled “Service discovery”No client has compile-time knowledge of any endpoint’s commands. cotal describe <endpoint> resolves a registered endpoint’s command set off the wire: the reserved
describe command answers the registered contract digests, the schemas are fetched from the
space’s content-addressed contract store, recompiled, and verified against those digests.
Each command prints with its capability class and targeting shape. cotal invoke <endpoint> <command> --args '<json>' then calls one command by name, validating the arguments as they
will be sent (JSON drops a key whose value is undefined) against the fetched input schema before
publish. A refusal at that check means nothing was sent, and it is marked not-executed. An
endpoint that refuses a request before running the command states not-executed in the reply’s
error.outcome too, for example on a wrong envelope version, sender, contract digest or target
mode, or a body that does not parse. A refusal the command itself raises carries only the
outcome it states. A refusal that does not serialize comes back as internal, and one too large
to publish as resource-exhausted, and both keep that outcome. A
signed-in user invokes the same surface through their bearer, and the broker enforces each
command’s existing capability grant. A manager alias supplied through --name resolves through
its name-keyed inspect command, so an authorized targeted call does not need the manager-wide
ps enumeration grant. Every built-in manager command uses this
same trust chain, so there is nothing the built-ins can reach that a described contract cannot. The registered
auth endpoint is describable the same way manager is: cotal describe auth lists
retire-lifecycle and its exact-mode target shape.
See SPEC §13.7 and cli.md.
The manager’s resolve-cwd command is in the manager.spawn capability class. It accepts an
absolute path on that manager’s host and returns its canonical directory plus the host name. It
refuses a relative, missing or non-directory path with failed-precondition; it creates nothing.
spawn applies the same check at admission, before any credentials or durables are minted.
spawn refuses an empty or whitespace-only value in any of its optional string fields, such as
role, agent or identity, with bad-request naming the field. To take the persona file’s value,
omit the field.
Inspecting a managed name
Section titled “Inspecting a managed name”Manager inspect keeps its successful response as the live managed-agent row. A live hit does
not read durable lifecycle state, so a temporary records-store failure cannot break inspection of
an agent the manager currently holds.
On a live miss, a static manager point-reads its durable slot row. A name with no slot, or a slot
whose phase is retired, remains not-found. A nonterminal slot returns
failed-precondition with error.details[].kind = ai.cotal.manager.static-slot-observation. The detail carries the slot’s slotPhase,
owner, actor, slotLifecycleUid, cleanupComplete when recorded, and slotRevision. It
then carries the separate lifecycle head’s headState, headOp when present,
headLifecycleUid, and headRevision. Head fields are absent when provisioning has not written
the lifecycle head yet.
The error message carries the same diagnostic summary so string-only operator paths do not hide
the structured detail.
A slot row records the manager instance that owns it. In a space with more than one manager, the
class queue can hand inspect to an instance that does not host the name. When the row names a
different instance and is not retired, the miss returns failed-precondition with the same
detail plus ownerInstanceId, which names the only manager that can act on it. The message names
both instances, so a caller that reads only the string can tell it from not-found. A sibling’s
retired row remains not-found. A named cotal_despawn resolves its target through this read
and cannot address an instance, so it asks again until the owning instance answers, up to 16
times.
The slot is read before the head. These records do not form one atomic snapshot, so the detail
also carries readOrder: ["slot", "head"] and consistency: "ordered-not-atomic". A head can
advance between the reads. The issuance gate is not projected because the retirement operation
needed for this diagnosis is already recorded on the head, and reading a third record would add
another non-atomic edge without changing the per-name result.
If either durable read fails or exceeds its bound, the miss returns unavailable with
ai.cotal.manager.static-slot-read-failed rather than claiming the name is absent. That detail
names the inspected name, the failed record (slot, head, or slot-or-head when the layer
cannot distinguish them), and operation: "read". User-auth managers do not own mgrslot rows,
so their inspect misses remain live-map reads.
For Linux custodied seats, retirement requires the runtime’s process-exit evidence before freeing the alias or deleting its credentials and delivery state. Socket loss alone is not proof of exit. The runtime retains the record captured at launch or adoption so a clean custodian exit can unlink its file without losing the recorded boot and process identities. If the file is missing, reaping uses that retained record and the existing kernel identity checks. An unknown reference without either record refuses cleanup. Reused process ids are never signalled on the strength of the old record.
Listing the durable slots
Section titled “Listing the durable slots”Manager slots (manager.read, untargeted) lists the durable static slot rows this manager
owns. Only static managers hold these rows: a user-mode or open manager answers
failed-precondition, and a manager whose durable store is not standing answers unavailable.
Each row carries the same readOrder and consistency fields inspect uses, because the list
is read the same way: torn across rows as well as within each row’s slot/head pair. A retired
row is never listed. live reflects the manager’s live roster at render time, not the durable
row. A slot row is never deleted, so a row whose latest operation is a DEL or PURGE marker is
corruption: the list answers unavailable naming that row, as inspect does for its name.
Spawn is a goal
Section titled “Spawn is a goal”Long-running commands are actions (SPEC §13.6): the caller
submits with a client-generated goalId and a request fingerprint, the endpoint records a
durable accept or reject decision, progress rides per-goal events, and the work ends in one
terminal outcome (succeeded, failed, cancelled, expired, or uncertain). Spawn is
the reference case. Rather than block the caller for up to 30 seconds while an agent comes
up, the manager accepts the goal and returns the allocated identity at once:
{ "name": "reviewer_2", "owner": "u_...", "actor": "reviewer_2", "uid": "...", "goalId": "...", "fingerprint": "...", "readinessDeadlineMs": 30000, "executor": { "lifecycleUid": "...", "epoch": 3 }}The uid is the lifecycle the agent runs at. On a participant manager whose host enrolls its
agents, the host picks that uid, so the manager accepts the goal only after the host has answered.
A host refusal there refuses the spawn, and no goal is bound.
The name is the one actually allocated: a persona-derived collision is auto-numbered
(reviewer, then reviewer_2), while a hard-pinned --name that collides with a live
agent is refused at accept, before anything is minted. Auto-numbering never hands out a numbered
name it has already issued in that manager process, even after the agent holding it is gone, so
a collision takes the next number. Only numbering consults that history: a hard-pinned --name,
or a persona whose own name is a numbered string, takes that name whenever it is free, and
numbering does not skip a string such a spawn held before. The triple plus goalId let the
caller follow progress (connector handoff, process launched, presence join) and reconcile
later against the exact instance that accepted. Presence within the manager’s default
30-second readiness window, or a connector’s declared bounded window, settles the goal
succeeded; an early process exit is failed; the window passing with neither is uncertain,
a bounded, durable outcome that a later ps or status read settles against the live roster.
uncertain is a real terminal outcome, not an absence and not a silent hang. It carries the
diagnosis of whoever owned the deadline: for a launch that
names the agent and says to inspect it rather than re-issue, since re-issuing after a launch
that in fact succeeded mints a duplicate. A follower keeps the acceptance as the data of any
terminal other than succeeded, so cotal_spawn returns an uncertain launch as a pending result
instead of an error: it names the allocated agent, its id, and its manager, and tells the calling
agent to watch the roster. A committer that supplies no diagnosis falls back to
“the success signal did not arrive within the readiness deadline”. The agent’s own eventual
state is then observable on its presence record.
The acceptance carries that exact readinessDeadlineMs. A synchronous follower treats its own
request deadline as a floor and waits through the accepted readiness budget plus delivery margin,
so a connector-specific slow boot cannot be reported as a caller timeout while the manager is
still legitimately waiting for its terminal.
A spawn that is refused because a lifecycle barrier already holds the actor (a frozen
issuance gate, a retiring alias, a retired uid) is not a wait-timeout. The manager already
knows the blocked op (registration / retirement / activation / takeover), the opId
holding it, and the remedy when one exists (retry, cotal reconcile-gate). The detail
carries headState (active / retiring / retired) only when the refusing site read the
lifecycle head, and gateState (frozen / retired) only when it read the issuance gate. A
gate frozen by a takeover or a registration says nothing about the head, so that refusal
carries gateState=frozen and no headState. Those facts ride error.details[] as
kind = ai.cotal.ep.lifecycle-blocked and are also appended to the error string, so a
caller that only prints error.message still sees them. The CLI and the connector tools hand a
refusal on in one shape, so cotal spawn -f keeps the same code, details, rendered facts and
acceptance data as cotal spawn --detach and cotal_spawn. A connector that collapses the
refusal to “startup failed (unknown)” or a SPEC 13.6 wait-timeout is hiding a knowable
state, not reporting a missing one.
Instance routing
Section titled “Instance routing”A space can run more than one manager. Each manager persists a stable logical instance id
across restarts and advances its process epoch when it comes back, so callers address a
specific manager without caring which process currently serves it. A start serves only at the
epoch its own registration committed, never at the epoch of a later start of the same instance.
On a static or open mesh,
an untargeted spawn rides class anycast (any manager may accept, and the acceptance records which one did).
cotal spawn <persona> --detach --on <instance> and cotal_spawn(instance: "<instance>")
pin one instance by its exact id. A foreground CLI spawn has no manager to pin and refuses the
flag. An MCP pin that does not resolve is refused without falling back to class anycast. There are no ordinal
aliases and no short forms: wherever a display names an instance you can address, it prints
the whole id, because both surfaces take nothing else.
On a user-auth mesh, manager commands obtain a short-lived manager-caller view from the
exchange. It authorizes one concrete manager instance using the caller’s current actor grant and
the host’s registered service records. Discovery and invocation both use that instance’s inst
route. This view grants no registry scan, class queue, or additional command capability. An absent,
ambiguous or unauthorized selection refuses before the command is sent.
Managed launches carry COTAL_MANAGER_INSTANCE so their tools address the manager that launched
them. Existing unbound sessions can use the exchange’s unique authorized selection without
replacing their actor or conversation. The connector uses a separate control connection; the
standing message connection and its credential source are unchanged. Accepted spawn goals are
followed on that renewing connection, using its existing caller-scoped progress grant, so a long
readiness budget does not depend on the short-lived control credential. The follower confirms its
progress subscription with the broker before submitting on the separate connection. A caller still
checks the resolved instance and epoch, and never retries an ambiguous mutation outcome.
The manager’s goal-result command accepts {goalId} and returns {goalId, result?}. It reads
only the authenticated caller’s owner, actor and lifecycle through the manager’s separate trusted
goal-writer connection. The caller receives an attributed reply, never a raw JetStream reader
grant. Each read is admitted by the connection’s broker-enforced command grant. A live user-auth
connection remains bounded by its bearer expiry after revocation; a renewed connection is checked
against fresh authority. There is no separate per-read ledger check. An absent result means no
terminal is recorded; it does not prove the goal is running or permit another submission. The
existing trusted goal-writer’s leader-served EPF read is space-wide at the broker; the handler
confines it to this endpoint and caller triple.
The manager’s reserved cancel command accepts {goalId, mode?} and returns {goalId, state}.
It is served for a turn the manager relays. The goal is the authenticated caller’s own, so a caller
withdraws only a turn it submitted. The turn ends cancelled, its seat is not shown it again, and a
later yield of it is answered with that terminal. A goal that already ended is refused
failed-precondition with its cached outcome attached, and a goal this manager does not relay is
refused without being changed. So is a second cancel that arrives while a first is still ending the
turn; a first that fails leaves the turn pending unless something ended it meanwhile. A workflow
run sends it for the turn, ask attempt or escalation of a branch it cancelled.
A followed mutation requires a manager whose attributed describe includes goal-result. Update
the manager, issuer and client together before using that recovery path. Reloading an issuer alone
cannot change an already-running participant manager. Recovery re-resolves the accepting instance’s
epoch, preserves the caller lifecycle and validates the result against the accepted goal and any
acceptance fingerprint. Stopping the caller ends its observation, not the already accepted goal.
A followed call resolves the endpoint within its deadline before the submission starts, so a
refused or unanswered describe surfaces as its own error. A describe or command publish that the
broker refuses reports not-executed.
Cancellation before submission reports not-executed. Once submission starts, cancellation or a
lost reply reports an unknown outcome unless an attributed refusal proves otherwise. A received
refusal remains a refusal even when stop races it. Local failures do not invent responder identities.
The follower owns its subscription, timers and read cancellation signal. Reconciliation begins
before the wait deadline, and late read completions cannot settle an expired observation. Its read
callback receives the accepting caller triple, remaining budget and abort signal; borrowed bearer
commands and control connections use that signal. An in-flight dial that finishes after cancellation
closes without publishing. A local reply-subscription failure prevents publication and is observed
by the same request promise, including when the transport is closing or draining. Request
cancellation does not revoke or resubmit the accepted operation.
“Only one manager per space” is not the current invariant. A split topology that keeps the
broker host manager-free is still a topology choice: cotal up on that host starts a
manager you then stop with cotal down manager after ✓ manager up in
.cotal/manager.<spaceKey>.log (detach stdout listing manager is pidfile liveness, not a
teardown boundary), and cotal supervise --server runs the manager elsewhere (Run a mesh). Extra live managers
are addressable, not an error.
The reserved describe bootstrap is the one request the resolver may repeat while waiting: it is
read-only, it is re-published under the same request binding, and every attempt stays inside the
original deadline. This covers the startup window where Core NATS discards the first request before
the manager has subscribed. If the connection closes while the resolver waits, the describe fails
cleanly instead of throwing from the retry timer. The resolved command is never repeated by this
readiness behavior.
The describe and the contract store reads after it share the resolver’s one deadline, so a slow
store read fails the resolve with deadline-exceeded when that deadline expires. Cancelling a
resolve settles a store read in flight instead of waiting for it to return.
The resolve and the invoke are separate trips through the same anycast queue, so in a
multi-manager space an unpinned call can land on an instance the caller did not resolve. Every
call carries the incarnation it resolved against, and a manager that is not that incarnation
refuses before running the command, so the failure an operator sees says the command did
not run, and re-issuing it cannot duplicate the effect. That is the difference that matters for
a mutation: the older behaviour detected the mismatch on the reply, after the manager had
already acted, and could only tell you to go and check. --on still matters for reaching a
specific manager (ps, stop, attach, spawn --detach), but it is no longer what stands
between a split and a duplicated spawn. Against a manager older than this fence the refusal is
still after the fact, and its message says so. The re-issue is automatic only when the refusal
states not-executed in its outcome field; a refusal that omits the field, or states
unknown, is surfaced to the caller instead of repaired, because neither proves the command did
not run. The CLI’s manager commands, cotal invoke, the cotal run verbs and the manager row of
cotal status re-describe and re-issue an unpinned call after each such refusal, up to 16 times,
so a split reaches the operator only when every attempt split. A hosted run’s own manager calls
use the same bound. A pinned call is never re-issued. An agent’s own manager
tools, such as cotal_spawn and cotal_despawn, re-describe and re-issue with the same bound,
including the goal-result read that follows a spawn to its outcome.
An unpinned targeted call, such as cotal_despawn or a hosted run’s turn relay, can also reach a
manager that does not host its target, because each manager resolves targets against the agents
it runs. That manager refuses with expired and not-executed and says it holds no mapping for
the target, and the same re-issue repairs it within the same bound. An agent that no manager hosts
still ends in that refusal once the re-issues run out. A pinned call gets the refusal of the
instance it named.
A manager whose boot inventory marked every declared connector unavailable does not subscribe
spawn or launch on the class one rail. Those commands stay on scatter and on this
instance’s inst rail, so a sibling that can launch them can take an unpinned spawn, and a
caller that pins this instance with --on still gets a named harness refusal. describe
still lists the commands: the instance rail serves them, and describe itself stays on the
class rail (SPEC 13.7). An unpinned spawn can therefore bind-fence: describe may land on
the skip member while spawn lands on a sibling, the command was not run, and the caller
re-issues or pins --on. status reports classSpawn: false when that skip is in effect.
A manager that can launch some connectors keeps the class rail. If the queue hands it a
harness its inventory marked unavailable, the refusal names --on because the standing serve
credential cannot read sibling inventories. Pin the capable instance (the whole id, as ps
prints it).
ps and
status become a scatter across every registered instance: the caller freezes the
expected set from the service registry, invokes each under a shared deadline, and merges the
results with per-instance attribution. A non-answering instance is labelled as registered
with no answer within the deadline, never silently omitted. See SPEC §13.5 (scatter) and cli.md.
The expected set comes from the registry, which records registration rather than liveness. An instance that crashes never deregisters, so it stays in the set and the gather has nothing left to wait for but an answer that cannot come. It pays the whole deadline, on every scatter, indefinitely. A scatter can therefore be given a per-instance liveness probe: when the broker itself reports that an instance holds no subscription on its own instance rail, the gather stops waiting for it. Only that affirmative report counts. A lapsed presence entry, a probe that timed out, and a probe that failed are all absence of evidence, and treating any of them as death would turn a slow correct answer into a fast wrong one, so they leave the full deadline standing. Nothing about the outcome changes either way: an instance that did not answer is still unreachable, still surfaced, and the scatter is still not complete.
The probe is supplied by the caller, not invented by the scatter. Asking about an instance is
a publish on that instance’s rail, and a credential that holds no row for it is refused by the
broker asynchronously, while the publish itself returns normally. The probe verb watches for that
refusal and raises it as permission-denied naming the rail, so it is never mistaken for a quiet
instance, and it never burns the probe budget waiting out a refusal. Only the layer that
minted the credential knows which ids it may ask about, so that layer asks about those and no
others. cotal ps freezes the class on its first connection, re-mints an instrument pinned only
to the frozen ids, and scatters on a second; a refusal the broker raises anyway is printed and
the instance’s row says the probe was refused, which is a fact about the credential, not about
the instance.
This does not help against an instance that is connected but not answering. A hung manager holds its subscriptions, so it is indistinguishable from a slow one, and it still costs the full deadline. That is the correct result, not a gap in the probe.
Deregistration
Section titled “Deregistration”A probe makes a dead registration cheap to skip; it does not remove it. Removal is the
registration’s own exit, and there are two explicit routes to it
(SPEC §13.5: a deleted svc spec is the deregistration).
A manager that stops cleanly removes its own registration, so an ordinary shutdown leaves no stale row. The delete is pinned to the registration revision that process wrote. When a successor has registered the same instance since then, the stop logs that and leaves the successor’s registration alone. It refuses that delete while this instance holds the endpoint governance slot at the live issuance-gate generation (a registration still completing its reopen). A leftover slot whose generation is behind that live generation is not in-flight and does not block the stop. A manager that cannot renew or read its lease keeps serving, stays registered, and retries. If another process holds the same instance key, that process has taken the instance over, so this one logs the conflict and exits without deregistering, leaving the successor’s registration alone.
A restart that died mid-registration is a different residue: the issuance gate stays frozen under
that op. The successor completes the dead registration on boot when the freeze-holder is
affirmatively gone under a complete CONNZ sweep (the same composition as
cotal reconcile-gate). A committed spec write is finished under that
same freeze; only a definite no-commit abort-reopens and then runs the normal takeover.
It does not invent a TTL and it does not start a new freeze over a still-held one.
That residue has a second half, and it is the endpoint governance slot rather than the gate. Every registration takes the endpoint-wide slot before it publishes its spec and holds it until its own gate reopens, which is what serializes registration for the endpoint. An instance that stopped between those two points leaves the slot held with no registration behind it, so the endpoint refuses new registrations while nothing is actually in flight. The slot is stamped with the generation of the gate its holder had frozen when it took it, and a slot is promoted only at that same generation. So once the holder’s gate has reopened past the stamp, the slot can never be promoted by anyone, and the next registration for that endpoint replaces it. That reclaim is part of an ordinary start and needs no operator step.
A slot whose holder’s gate is still at the stamped generation is a registration that is genuinely in
flight, and it keeps refusing. The two states read differently only in the holder’s gate coordinate,
so reopening that gate is what separates them: the holder’s own restart heals it on boot, and
cotal reconcile-gate is the operator’s route when the boot path cannot
run. The registration path is the slot’s only writer, and neither repair command writes it.
A registration that cannot read the holder’s gate at all refuses, because an unreadable gate does
not distinguish the two states either. Each of these refusals carries
kind = ai.cotal.ep.foreign-slot-held in error.details[] with the holder’s instance id and the
condition that refused: in-flight for a holder gate still at the stamp, or no-seam,
unreadable, garbled or behind when the registration could not read that gate or read it below
the stamp. A remote manager asks its host to reconcile the holder only on in-flight, the one
condition a gate repair can clear.
For the instance that cannot cooperate, an operator names it:
cotal deregister-instance --instance <id> (cli.md). It removes the
record only on the same evidence cotal ps acts on: the broker reporting nothing subscribed on
that instance’s own rail. It refuses if the instance answers a describe, refuses if the probe could
not run at all, and refuses if the instance is merely quiet, because a hung process still holds its
subscriptions and is therefore not affirmed gone. It also refuses while that instance holds the
endpoint governance slot at the live issuance-gate generation (a registration still completing);
a leftover slot behind that generation is not in-flight and does not block. Nothing sweeps the
registry on an age threshold or on silence.
An instance that is deregistered while it is merely wedged re-registers over the tombstone on its
next start, which is what makes the operator’s decision a recoverable one.
Attach sessions
Section titled “Attach sessions”cotal attach no longer returns a ws://127.0.0.1 URL. It creates a one-use, holder-bound
session offer: the manager mints a token bound to the caller, the target lifecycle, its own
instance id and epoch, and an expiry, and replies with a session id and expiry only, no URL
and no secret in the reply. The CLI redeems the offer over the mesh (a second redeem is
refused). On a registered open mesh that redeem is a bare connection, the same path other
control commands already use; on a static-auth mesh it is still a session-caller credential
minted from the resolved root’s seed. On a user-auth mesh the CLI holds no seed: it exchanges its
login and the grant for a session-caller view bearer, and the callout mints the same caller rails
with the grant’s expiry. Terminal bytes then stream on core-NATS session subjects
scoped to the two parties. Backpressure is a bounded in-flight window with an explicit drop notice, never
silent loss; a late attach still repaints the full screen from a replayed terminal
snapshot. Close, expiry, target despawn, and a manager restart are distinct, surfaced end
states: a restarted manager’s successor refuses the old epoch’s sessions and the client
shows “manager restarted; re-attach”.
Seat input
Section titled “Seat input”attach is a stream, so it is the wrong shape for a program that wants to send one line: it
holds a session open and expects a terminal at the caller’s end. The input command is the
other half. One authorized call writes text into a running seat’s terminal as if it had been
typed there, and answers with the seat and the number of bytes delivered.
It exists for harness commands. A line beginning with / (/compact, /clear, /model)
is neither chat nor an event: the agent’s own harness handles it, and the keyboard is the only
way in. An external control surface that can already read a seat’s turns and talk to it still
cannot drive it without this.
The op is targeted, rides the manager.lifecycle capability, and declares authz modes owner
and any, the row shape attach and despawn already carry, checked by the same authorization.
Enter is appended unless the caller suppresses it, and nothing is echoed back, since the resulting
turns already have somewhere to go.
Who may call it is narrower than either of those, and the reasoning is worth stating because
the natural assumption is wrong. despawn and attach are granted to anything holding spawn;
input is granted only to operator credentials. The tempting argument for treating them alike is
that an attach session’s write already reaches the same terminal, so input adds nothing. It
does not reach it: an attach yields a signed session offer, and redeeming one needs a per-session
credential minted from the space signing seed, which no agent holds. So input would be new
authority, and the own-owner rule that bounds despawn covers every seat under an owner rather
than only the ones a caller launched. Killing a peer is denial; typing into a peer is control of
it. The write therefore sits with the credential that is already the administrative authority for
the domain.
Only a runtime that owns the child’s input stream can serve it. The pty runtime does; the
external terminal runtimes attach to a process they do not own, and there the command refuses
and names the runtime rather than dropping the keystroke. A seat that is not running refuses for
its own reason, and the two are distinguishable, so a caller can tell “this will never work”
from “not right now”. See cli.md.
Grants
Section titled “Grants”There is no broad control credential. A caller holds one capability row per command it is allowed to send, and minting maps each named capability to the request subjects it needs and no others. The manager serves over a scoped serve credential that can answer and reply but cannot, for instance, write another endpoint’s records or forge a goal terminal; the goal-fact writer and the session writer are separate, narrowly scoped credentials the broker fences by subject. Authorization is checked at the serving boundary, and for actions it linearises at acceptance: a spawn refused there mints no reservation and leaves no process. See SPEC §13.9 and identity & auth.
A carried resume transcript never rides the rails. The operator-only transcript-receive command
answers whether to upload and hands back a one-time claim for spawn, and the bytes travel through
the target instance’s own transfer bucket under two one-shot credentials: a writer the operator
mints for that one transcript, and a reader the target instance mints for its own bucket, or that
the host issues a remote manager through its transferReader authority operation.
See also
Section titled “See also”- Architecture, where the manager and the wire fit in the whole system.
- CLI, for
describe,invoke,spawn,ps,status,attach, andinput. - SPEC §13, the normative contract.