CLI authentication
Static minting requires local signing authority. Account sign-in and actor grants are separate, and diagnostic reports state which local claims they checked.
auth mint
cotal auth mint <name> [--profile agent|observer|admin] [--out <private-file>] [--json|--plain]
cotal auth mint <name> --provision [--role <role>] [--space <space>] [--server <url>]
cotal auth mint <name> [--expires-in <seconds> | --expires-at <unix-seconds> | --no-expiry]
cotal auth mint <name> --identity <private-creds> --out <private-file>
cotal auth mint <name> --replace
cotal auth mint <name> --endpoint <class> --out <private-file> [--channels <a,b>] [--expires-in <seconds>]The fixed-profile forms use the existing native static credential profiles. No generic host
profile is exposed.
A space registered as open refuses UNSUPPORTED, since it authenticates no one and issues no
credentials. Per-user-auth spaces refuse too; a cached login does not replace signing authority. The
resolved root supplies the persona ACL, signer and default storage together. A root that cannot be
resolved refuses with its next step, such as cotal up. Conflicting current-folder and
selected-root accounts refuse before issuance.
A label takes letters, digits and _, since - separates the parts of a principal's name. --role
takes letters, digits, _ and -. A label or role with any other character, or an empty one,
refuses USAGE_INVALID without the command's help. The refusal names the character, the value and
the characters allowed, and its Next names the value with each such character replaced by _.
agent is the default profile. Its channel ACLs and role come from the selected root's
<name>.md persona file, when present. It does not fall back to default.md for a different name.
Explicit --allow-subscribe, --allow-publish and --role override the file's grants. A persona
file of that name that cannot be read as a persona refuses the mint SCHEMA_INVALID, exit 1, even
when the flags name every grant. The mint refusal does not print file values or parser text.
It names the file and cotal persona show <name> to inspect it. A role from the file that is not one word
of letters, digits, _ and - refuses the same way, naming cotal persona edit <name>. With
--identity naming an agent credential this space issued, the new credential keeps that
credential's read channels, post channels and role in place of the persona's, and each flag still
replaces its own grant. The receipt lists the grants as May read, May post and Role, marking those
carried from --identity; JSON has them under result.grants, with fromIdentity naming the
carried ones. An empty ACL flag means no explicit entries. The native read-ACL resolver still enforces boot membership.
Without a matching persona, --identity or grant flags, the agent credential has no channel grants
or role. Its validity does not establish that it can perform a particular task.
Observer and admin have fixed native scope and reject agent ACL, role and provisioning flags
rather than implying those inputs narrowed anything.
Private credential output
The default credential is created through its canonical secret-store kind. An existing label is
not overwritten. Use another label or an explicit private output. --replace replaces the label's
standing credential when it has expired, has no expiry claim or has a key that
auth revoke-credential revoked. It mints under a new identity unless --identity names one, which
a revoked key never is, and removes a no-expiry record beside it unless --no-expiry writes a new
one. The receipt names what it replaced and its public key under result.replaced. A replacement
does not revoke the credential it replaced, so a copy of one with no expiry claim stays valid. The
receipt then warns REPLACED_CREDENTIAL_VALID, naming that key and the
auth revoke-credential route that stops it. A standing credential that is still
valid refuses CONFLICT, naming its expiry, and so does one that is not a native credential.
--profile still defaults to agent, so a replacement of an observer or admin credential needs
--profile observer or --profile admin; a replacement with another profile than the one it
replaced warns REPLACED_PROFILE_CHANGED. A replacement holds only the grants its persona file,
--identity or grant flags name, not those of the agent credential it replaced. When both are
agent-profile credentials, the old one names this space's account as issuer, and their grants
differ, the receipt warns REPLACED_GRANTS_CHANGED, naming both and the
--allow-subscribe, --allow-publish and
--role flags that name the replaced ones. With no standing credential under the label, --replace
mints a new one and warns NOTHING_REPLACED. Exported files stay unchanged; the warning names
doctor auth --space <space> to inspect their state and the guidance for each file, and the
space's root to run it in, since doctor auth reads the root it runs in.
--replace takes no --out. An exported file stays outside
managed .cotal and workstation state. Its path, label, profile and public key, without the
credential, are recorded under credentials/ in the Cotal home so doctor auth lists the file. A pre-existing explicit output must be an owned private
native credential matching the nkey deliberately supplied through --identity; unrelated files
and identities refuse. New outputs use create-only publication. The output's directories follow
the space create path rule, which also accepts a root-owned sticky directory
such as /tmp, and the file is owner-only. Unsupported platforms refuse rather than weakening it.
A credential is never written through a symbolic link: a link among the output's folders refuses
PERMISSION_DENIED, naming the link, the folder it points to and the same file named without the
link. A path through a file refuses USAGE_INVALID, and a folder this account cannot create
refuses PERMISSION_DENIED, naming the folder. None of them writes anything.
--identity reuses the native nkey. The out-of-band agent mint reports its newly allocated
lifecycle UID, with a warning that it did not reuse the old inbox/delivery footprint. Use that
reported UID for the resulting client; do not infer it from the label or assume the old UID was
preserved. No private nkey or JWT appears in a receipt. A key revoked with auth revoke-credential
refuses CONFLICT, because a credential signed now would postdate the revocation and the broker
would accept the key again; mint a fresh key without --identity.
Provisioning
Without --provision, fixed-profile minting writes credentials only. --provision uses one native provisioner
connection to pre-create that lifecycle's bind-only DM/delivery and role footprint. It performs
the authenticated preflight against the same recorded broker and TLS requirement, without
pruning a registration. An acknowledgement proves neither a joined agent nor task completion.
A partial or unknown result retains the allocated public identity and lifecycle for diagnosis
and does not retry the mint.
A fixed-profile credential expires 24 hours after it is minted unless --expires-in or
--expires-at sets another time, in native seconds. --no-expiry issues one with no expiry claim and
records that request beside the file, in <file>.no-expiry.json, so doctor auth reports it as a
warning rather than a finding. The receipt reports the resulting JWT expiry. The three flags are
mutually exclusive, and an --expires-at that is not in the future refuses before anything is written. So does an expiry
after the last date Cotal can represent, in the year 275760. JSON is
one finite document; plain output uses the same public receipt. Signing material is exported by
auth signer, not a signer flag on mint. The old top-level mint spelling
is refused with the literal namespace replacement.
A minted credential stays valid until it expires, and one minted with --no-expiry until it is
revoked or the space identity rotates. auth revoke-credential stops one
credential and keeps the store. Rotating the space identity stops every credential the space issued:
cotal down, then cotal reset space <space> (or cotal clean all --force from the root when
down left no registration), then cotal up. That reset also removes the space's store, with its
workflow runs and their journals, channel history and stored messages, so the space starts again
with no history. cotal reset space <space> --dry-run lists what the store holds first. It also stops every
earlier full backup of the space from restoring into this root: only its registry restores, and
chat and DM history start empty (see Backups).
Endpoint caller credentials
--endpoint is a separate closed mode, not an ACL override on an observer or admin profile. It
requires a private --out in an existing protected physical parent outside managed state. Only
--channels, --expires-in, space/server and presentation flags combine with it. It has no default
lifetime: without --expires-in the caller carries no expiry claim. An empty or
omitted channel list grants no explicit read/post channels. Supplied channels deliberately enter
both ACLs, as in the native standalone Linear caller form. No persona ACL or role is inferred.
The same static-root/account checks run first. The existing native control instrument then resolves a live endpoint and digest-verifies its visible command surface. This issuer accepts only nonempty, bounded untargeted synchronous ephemeral surfaces. A targeted, journaled or action command refuses the whole issuance before footprint work; unsupported rows are not silently dropped. The result records the observed responder and contract digests, not a promise that every future incarnation has the same implementation. The caller is scoped to the class's observed command names, not that single answering instance.
The native agent provisioner creates the new lifecycle's bind-only footprint and additional endpoint request rows. No spawn, run or admin capability is added. The ordinary native agent baseline remains; this is not a new hand-written permission profile or a managed-seat launch adapter. User-auth/open spaces still refuse. The selected broker's required TLS is retained for discovery and provisioning.
Issuance records
An immutable owner-private <out>.issuance.json intent precedes the first footprint operation.
It contains public identity/lifecycle, requested scope and contract coordinates, never credentials.
Its presence is not a grant or successful submission receipt. An existing output or retained intent
blocks another issuance, even if no output was published. Partial/unknown provisioning retains that
original intent and does not replay the native call. Success reports native footprint acknowledgement
and the exact privately published file; provisioning is not presence, task completion or cleanup.
Temporary provisioner close is reported separately and can leave a partial result.
Rich/plain receipts show the actual caller/lifecycle, described endpoint owner and responder, explicit channel scope and native expiry. Command tables show at most 40 rows with an omission count; JSON retains every command and contract digest. The local issuance request is not a broker request acknowledgement.
No credential bytes or environment-export recipe are printed. Endpoint describe/invoke consume this paired file under the same registered static root:
cotal ep describe ep:<class> --space <space> --creds /absolute/private/caller.creds
cotal ep invoke ep:<class> <command> --space <space> --creds /absolute/private/caller.creds --args '<json>'Keep the matching <private-file>.issuance.json beside the credential. The consumer checks both
protected snapshots and validates the selected root's full static trust chain. The native JWT
signature, matching user seed, current validity window, account and current delegated signing key
are checked before connecting. A retained older signing key is not guessed from a different root.
The caller coordinates and recorded commands must reproduce the signed publish/subscribe/response
permissions and principal tags through the native builder. The unsigned local intent selects that
comparison; it grants nothing and proves no submission, lifecycle currency or completion. Original
responder/contract metadata remains an issuance observation, not an instance or future-code pin.
The connection retains registered TLS and uses only that file. Its broker admission is separate
from local checks. Invocation accepts only an included untargeted synchronous ephemeral method
from the fresh digest-verified surface. A changed targeted, journaled or action shape refuses before
publication. Instance pins, scatter, admin, agent targeting and goal-follow flags are unsupported
for this caller mode. Calls do not repair a binding, switch authority or resubmit an unknown effect.
--server, when supplied, must repeat the registered broker. Target/reference selection is
read-only, including when a paired file is selected. Receipts retain the public caller binding;
describe shows methods outside the file scope without granting them. Issuance next steps keep
explicit file selection instead of switching back to the normal operator instrument.
Arbitrary raw files, issued-generation files and different native profiles are not interpreted as this standalone caller. User-auth issuance and managed static launch are not supported.
Account login
cotal login --idp <auth base URL> [--client-id <id>] [--plain]
cotal logout --idp <auth base URL> [--local]Signs you in to a per-user-auth mesh's IdP (device code flow) and caches the session; run it
once per machine. --plain presents the approval prompt and signed-in result without color.
Device approval is still interactive and has no native noninteractive or JSON result in this
client; --no-input refuses before starting a provider request. The IdP URL must use https://.
Plain http:// is accepted only on a loopback IP literal such as 127.0.0.1 or ::1, for an IdP on this machine. localhost is refused because it
is a name: a hosts entry would choose the IdP. It prints your IdP subject, the id the operator
grants against. When the trusted
/token response advertises a same-origin space catalog, login validates and records that account's
spaces immediately, lists them under Your spaces, and names cotal use <name> to select one. A
space registered from elsewhere that has not granted your account refuses with those discovered
spaces as its remedy, because only that space's operator can grant you. On the root that holds the
space's grant ledger, the refusal names the operator's cotal actor grant command instead. After a
login, every command on that mesh works under your identity: each connect takes a fresh IdP
proof, exchanges it locally for a short-lived bearer, and is authorized against the actor
ledger at connect time. A provider that cannot be reached, or that stops answering, refuses with
DEPENDENCY_UNAVAILABLE (exit 2) and caches no session. logout revokes the IdP session, clears
its cache, and removes only that
account's discovered registry entries. See
identity & auth.
actor
# an upsert of the WHOLE row: name all three ACL flags, or pass --full for the wide defaults below
cotal actor grant <actor> --sub <IdP subject> --scope a,b --allow-subscribe a,b --allow-publish a,b [--role <r>] [--label <l>] [--json|--plain]
cotal actor grant <actor> --sub <IdP subject> --full [--scope a,b] [--allow-subscribe a,b] [--allow-publish a,b] [--role <r>] [--label <l>] [--json|--plain]
cotal actor revoke <actor> (--sub <IdP subject> | --owner <u_…>) [--json|--plain]
cotal actor label <actor> (--sub <IdP subject> | --owner <u_…>) <label> [--json|--plain]
cotal actor list [--sub <IdP subject>] [--json|--plain]| Flag | Default | Meaning |
|---|---|---|
--space <s> | the resolved mesh | Space whose ledger to manage |
--sub <subject> | none | The IdP subject (shown by cotal login) the actor belongs to. With list, only that subject's rows are listed |
--owner <u_…> | none | The derived owner token (alternative to --sub) |
--full | off | Fill each ACL flag left off with its wide default; without it, grant refuses unless all three are named |
--scope <a,b> | spawn,role:default with --full | Capability scope ('' = none; spawn = may run agents; run = may start workflow runs; role:<r> = may delegate role r; admin = cross-agent control; supervise = eligible for the closed remote manager-service view when the host enables it). Any other name is refused |
--allow-subscribe <a,b> | > (all channels) with --full | Channel read ACL; the user's envelope, their agents can never read beyond it |
--allow-publish <a,b> | > (all channels) with --full | Channel post ACL; also the envelope for their agents' posting |
--role <r> | none | Role (scopes the task-queue consumer) |
--label <l> | none | Display label for actor list (never the IdP subject) |
--parent <owner.actor> | none | Audit link to the spawning principal. An operator grant is the authority, so the link does not narrow the row |
--json, -j, --plain | off | Print one JSON document or line-oriented text, as auth grants, auth grant and auth revoke do |
The actor ledger is the single authorization source of a user-auth space: no row, no access.
The ledger stores no IdP subject. actor list --sub <subject> and auth grants --sub <subject>
derive the subject's owner as grant --sub does, with this root's owner secret and IdP pin, and list
that owner's rows, interactive and managed-agent alike. The report is titled
Rows for <subject> (owner <u_…>), and JSON adds subject and owner to its result and names the
owner in its target. A subject with no rows lists none. An empty --sub refuses USAGE_INVALID.
actor label <actor> <label> sets only an interactive row's label. The label is outside the
authorization digest, so the row keeps its lifecycle, grant time, capabilities and channels, and a
bearer issued under it keeps working. The receipt names the earlier label, and JSON carries it as
previousLabel. An actor with no row refuses TARGET_NOT_FOUND, and a managed agent's row refuses
UNSUPPORTED. Neither writes anything.
cotal help actor lists grant, revoke, label and list, and shell completion offers them.
actor list, grant and revoke resolve their space and root as auth grants, auth grant and
auth revoke do, and refuse the same way. With no space to resolve they refuse with a next step,
such as cotal up, rather than assume one, and a root that does not hold the space's user-auth
ledger refuses list rather than report no rows. On a static or open space the refusal says that
actor grants apply only to a space started with cotal up --user-auth, and grant and revoke
refuse that way before asking for --sub, --owner or the ACL flags.
Otherwise a usage error refuses with exit 1 before any authority is read, and one refusal names every
missing input: --sub or --owner, and for grant the ACL flags left off without --full. An
input that is not valid refuses the same way, naming each field and why: the actor, --owner,
--role, --parent, a channel pattern, or a --scope name other than admin, run, spawn,
supervise and role:<r>. Nothing is written. A channel needs no creation, so a grant may name one
that has no message or registry entry yet.
grant --full is the full envelope (all channels; scope spawn,role:default, so it may spawn and may delegate the default role). A
grant that leaves off --scope, --allow-subscribe or --allow-publish without --full is
refused and writes nothing. A re-grant replaces the whole row, not the one field you name, so to add a capability spell
every field out: the new scope plus the row's current read set, post set, role and label
(cotal actor list and cotal auth grants show what a row holds: its capabilities, read and post
sets, role, label and parent). Under --full, a field left off does not stay as it
was: it reverts to the wide default in the table above. A re-grant retires the current interactive lifecycle through the running auth
service before it rotates the row, so copied bearers cannot cross an authorization update. The
receipt names the earlier grant as none, the same fields (titled Actor grant stored unchanged) or
replaced, and says when its lifecycle was retired; JSON carries previous and
previousLifecycleRetired. If that
retirement cannot be confirmed, the row is left unchanged and the command fails with the recovery
action. revoke uses the same retirement before deleting the row, which lets a later grant create a
real successor instead of colliding with a live predecessor. supervise is separate from spawn and admin: it only makes a signed-in
person eligible for the host-provided closed remote manager-service view; it does not grant
management of another owner or a general host profile. revoke denies the next exchange and
the next connect with no restart, and evicts the principal's live connections. It asks nothing and
takes no --yes, because a later grant gives the actor access again under a new lifecycle. It reports
verified gone only when the delivery daemon's eviction result names the principal and shows a
complete scan with no connection left; any other result prints live-connection eviction INCOMPLETE
with its counts, or with the reason the result was not accepted. Managed-agent rows
(written by the spawn path) live in a disjoint row space this command never touches. See
identity & auth.
doctor
cotal doctor auth [--space <space>] [--fix] [--json|--plain]This inspects the current root's local credential claims and recorded renewal state. It does not
prove current broker authorization, daemon adoption, connected agents or task progress. The shared
report keeps full credential paths and public issue/expiry metadata; JSON retains all diagnostic
rows without JWTs, seeds, fingerprints or raw parser/renewal error bodies. On a root that holds several
spaces, --space names one; it defaults to the sole space. Without --space, a root with several
spaces refuses with TARGET_AMBIGUOUS (exit 2) and names them. In a folder whose root holds no space,
the report is partial (exit 4) and its next step is to run it in a space's root, which cotal space
lists. Without --space, in the root of a space cotal space create prepared and no cotal up
started, the step is to start it with cotal up, then run the same command again. In the root of a
space registered as open, the report says that the space issues no
credentials, so there are none to check, and the result is a no-op (exit 0). With --space naming
a space whose signing trust this root does not hold, it refuses AUTH_REQUIRED (exit 2) and reads
no other root. Its next step names the folder that space is registered at, where the same command
runs, or else a space's root, which cotal space lists. For a space cotal space create prepared
and no cotal up started, the step is to start it with cotal up first, then run the same command
again. From another folder, that cotal up names the space's root with --root, quoted for a shell.
Diagnosis limits
Plain diagnosis starts no repair or connection. A required legacy credential or user-auth directory
migration refuses instead of silently moving files or reading canonical absence as an empty scope.
A credential file that other accounts can open is a finding that names its mode, with chmod 600
on the file as its repair. That includes the space's own store credentials, and the other files are
still checked. Each file is also parsed the way --creds checks one, so a file the NATS
client cannot parse is a finding even when its claims are legible, such as one with Windows (CRLF)
line endings.
Once the root holds the space's trust, a missing membership-observer or connection-evictor
credential is a finding too, with cotal down then cotal up --rotate-sys as its repair, because
nothing else mints that pair again. A missing delivery or membership-rw credential is only noted,
since cotal up mints it.
A credential with no expiry claim is a finding whatever its kind, since a copy stays valid until it
is revoked or its signer rotates. --fix re-signs only the static daemon credentials, so an unbounded agent credential
stays a finding until its file is gone. The exception is one auth mint --no-expiry wrote, whose
record beside it still names the file's bytes: someone asked for it, so it is a
CREDENTIAL_UNBOUNDED_BY_REQUEST warning instead, and the run ends ok, exit 0. Both get guidance that
names the ways out. cotal auth revoke-credential <file> --yes between cotal down and cotal up
stops that credential and keeps the store. Replacing
a label's file with cotal auth mint <label> --replace once nothing uses it bounds it from then on,
and so does removing any other file and its record and minting a replacement without --no-expiry;
a copy made before stays valid until it is revoked. Rotating the space identity (cotal down,
cotal reset space <space>, then cotal up) retires every credential the space issued and removes
the file, and the reset removes the space's store with its history. An expired label's file is a finding too, and
its guidance names that same --replace, next to the managed renewal that re-signs an agent a
manager runs. An agent file whose key was revoked is a finding in state revoked, with
result.items[].revokedAt, however far off its expiry is. A revoked key is not signed again, so
for a label's file the guidance names --replace, and for any other agent file it names stopping
the agent that uses it. Each --replace this guidance names, for an unbounded, expired or revoked
file, carries the file's profile as --profile observer or --profile admin, and an agent file's
read and post channels and its role as --allow-subscribe, --allow-publish and --role, since
the replacement is a new key, which holds only the profile and grants its mint names. For an
observer or admin file it names no managed renewal, as no manager runs one.
Each file auth mint --out wrote for the space from this root and that is still there is listed
under Exported credential files, and in result.exported, with its state and expiry. These rows
are not findings. An expired one is an EXPORTED_CREDENTIAL_EXPIRED warning. One near expiry or
expired names cotal auth mint <label> --identity <file> --out <file>, which signs the same key
again with the same grants. One whose key was revoked, or that an earlier space identity issued,
names a mint with a new key instead, because the broker refuses that key and auth mint --identity
refuses a revoked one. For an agent file, that mint names the file's read and post channels and its
role with --allow-subscribe, --allow-publish and --role, since a new key holds only the grants
its mint names.
A run with no findings says so in its heading only when nothing else is printed. While an exported
file needs attention, or a credential is unbounded by request, the heading starts No findings; and
names each, such as 2 exported credential files need attention. The exit code stays 0.
Missing local signing trust is not inferred to mean an open or healthy mesh. An unreadable renewal
record prevents a complete healthy verdict. Locally ordered older incarnation files are labeled as
file observations, never proof of a live successor or permission to delete or respawn an agent.
Supported repair
--fix explicitly permits native local material migration and the supported static daemon re-sign
phase. Its path-move and re-sign acknowledgements are retained separately. It holds the native
renewal lease when using the broker and refuses another holder. Offline repair is labeled as
uncoordinated; an authenticated preflight failure is not called an offline target. User-auth roots
cannot receive a static-signer repair fallback. No system account is rotated and no agent is
respawned or silently given a new identity.
A delivery or membership-rw credential the NATS client cannot parse holds no key to re-sign, and
the manager cannot renew it either. When no broker answers, --fix mints it again under a new
identity, as cotal up mints a missing one, and its result row has reminted: true. While its
broker answers it stays a finding, and its guidance and row name cotal down first. A refusal
because the manager holds the renewal lease names the same step. Every result row that is not a
plain re-sign has a reason. When cotal up starts with such a membership-rw credential, the
delivery daemon runs without it, and up warns DAEMON_CREDENTIAL_UNPARSABLE, naming the same step.
A local re-sign does not erase a recorded broker refusal or claim that a daemon adopted it.
A recorded refusal names the space's manager, the renewal owner, as the next step. A pre-per-space
.cotal/renewal.json names no space, so it is reported as a leftover and never read as the space's
verdict.
Remaining findings, incomplete audit reads and partial/unknown repair outcomes keep classified
finite receipts. Inspect the original renewal owner and lifecycle rather than repeating a mutation
or creating replacement agents merely to obtain a different report.
Account screens
Bare login chooses a known provider from cached account metadata or registered spaces. When
none is known, an interactive terminal asks for the provider auth URL. --idp selects it directly.
The device-approval link and code are written only to stderr. Session tokens are never printed.
The sign-in receipt separates account identity from each space's grants. This client's device
flow requires approval and refuses --no-input before requesting a device session.
Bare logout chooses among cached providers. A single cached provider is unambiguous; scripts
with multiple providers must select one with --idp. logout --json and --plain report the
same result. No cached session is a local no-op, not proof that all provider sessions are gone.
Revocation must return before local cleanup. If revocation is unconfirmed the login is retained
for retry, and the refusal names --local; a later cleanup failure is partial and keeps the
confirmed server outcome. logout --local clears this machine's cached login and that account's
discovered registry entries without contacting the provider, for a provider that is down or does
not confirm sign-out. The provider session stays valid until it expires or is revoked at the
provider, and the receipt says so.
endpoints --json and endpoints --plain show the authorized live presence snapshot, including
agents and infrastructure. This legacy live read is labeled as presence, not a saved-reference
inventory or proof that a service registration is active. Native contract resolution remains a
separate read. Each --json item carries the presence values: reportedStatus, condition,
activity, and the ISO 8601 times statusSince, activitySince and activeAt, each null when the
record has none. The report works out the ages it prints from those times.
Authority
Bare auth reports only safe cached provider metadata. It does not verify a session online,
turn a sign-in into a grant, or print tokens. Its next steps name the --space you gave or the
space a bare command resolves; with no space they point to cotal space. auth grants reads the
selected root's authoritative user-auth actor ledger; a root without its owner secret and pinned IdP
refuses rather than reporting an empty authoritative list. A static or open space has no such ledger
on any machine, so there auth grants, auth grant and auth revoke refuse with UNSUPPORTED and
say that actor grants apply only to a space started with cotal up --user-auth. auth grant and
auth revoke refuse there before they ask for a subject or the ACL flags. A space that cannot be
resolved refuses with its next step, such as cotal up or cotal space.
auth grant <actor> and auth revoke <actor> use the existing local operator authority path.
Choose --sub or --owner, never both. Grant requires explicit scope/read/post lists unless
--full deliberately selects the native wide defaults. These usage errors refuse with exit 1 before
the space's authority is read, and one refusal names every missing input. Re-grant/revoke
retain predecessor lifecycle retirement and concurrent-row checks before replacing or deleting
authority. A retirement that did not run, because the auth service is not running, refused the
connection or answered that the retirement did not run, is refused (exit 2) with the actor row
unchanged and the recovery named, usually cotal up. Any other failed retirement request may have
started the retirement, so it is unknown (exit 3) and is not retried. Grant
rows in reports contain scope and ACL metadata, not token hashes or secret material.
Revocation receipts separate committed deny-new from the delivery daemon's live-eviction notice. They do not claim all related processes stopped. An interrupted mutation is unknown and is not retried. Neither grant management nor actor reads install connector packages. Grant management does not issue credentials, export signers, retire lifecycles or act on capability handles.
Internal commands
cotal __complete <words…> is the internal entry the shell-completion stubs call to emit candidates
for the current command line; you run it directly only to debug a completion, with
COTAL_COMPLETE_DEBUG=1. cotal agent-bearer is machine-facing
plumbing on user-auth meshes: spawned agents exec it to print a fresh short-lived bearer from their
spawn-time secret; you never run it directly either, so cotal help leaves it out. Its local arm uses --dir to discover the
capability-gated loopback service. A remotely enrolled, already-granted agent instead receives
--exchange-url <https://base> in its launch argv: that arm sends {owner, actor, actorToken} to the
pinned public exchange with no local capability, follows no redirects, and refuses every non-HTTPS
URL because the actor token is the credential in the request body. Because a seat execs it on every
bearer refresh, it skips the connector-seed boot gate entirely: it reads one 0600 token file,
exchanges it and prints the bearer without consulting or writing the operator-global seed store, so
a newer store generation cannot refuse a live seat's refresh. For the same reason the seat's command
names the CLI entry file the cotal bin link resolved to at spawn, not the link: an install that
later replaces the link, such as the get.cotal.ai launcher over an npm i -g link, leaves running
seats able to refresh. The command runs before the rest of the CLI loads and reads none of its
preferences, extensions or config folder, so a refresh does not pay the CLI's start-up time. A command line it
cannot parse goes on to the full CLI, which refuses it with the command's help. --manager-call asks for the
instance-bound manager-caller view; --manager-instance <id> selects an explicit live candidate,
and an owner with more than one live candidate is refused with their ids until it names one.
That mode still prints only the raw token and does not update --health-file. A spawn runs it once as the agent auth
preflight. When it fails there without printing a sentence of its own, the refusal names the cause: the 30 second
timeout, the signal that killed it, or its exit code. (cotal start is a removed tombstone: it
errors and points you to cotal spawn --detach.)
cotal app-identity is plumbing for a Claude app conversation, and you never run it directly
either. The Claude connector's local server runs cotal app-identity provision --name <name> when
a conversation connects. It picks the mesh as cotal spawn does (--space selects one) and prints
one JSON line. A new identity opens only after a person approves it at the mesh's authority, so
provision needs a signed-in mesh whose authority advertises connection approval v1, and the
conversation's native --session. It keeps the request and its secret in a private record under
~/.cotal/app-identities. Until the person decides, it answers
{"ok":false,"pending":true,"requestId":"<id>","approval":{"id":"<id>","url":"<url>","expiresAt":"<time>"},"proposed":<selection>},
with no credential, secret or path. proposed is null for a request without a selection.
Provisioning again sends the same request: it answers the same approval while pending, and the
launch env and the reference that retire takes once approved. The grant is what the person
approved, which can differ from the request. A denial or an expiry drops the record, and the next
provision makes a new request. --selection <json> proposes channels, a role and capabilities for
that conversation. A mesh with static credentials, an open mesh, and a signed-in mesh whose
authority this machine holds have no approval page, so provision opens no identity on them.
The connector passes --require-approval. Every provision needs approval with or without it, and
an earlier CLI that opened identities without approval refuses the unknown flag before doing
anything. A saved request that was never issued needs the person's decision even when an earlier
version saved it.
cotal app-identity retire --dir <dir> still removes an identity an earlier version opened on a
static mesh: its broker footprint, then its files and directory. When the broker is not answering it
keeps them. Each provision also retires those directories whose owning process has exited. For a
signed-in identity, retire keeps the record until the mesh confirms retirement, and names the
mesh's phase while it is pending. A request still pending is dropped.
cotal app-identity reap retires the selected mesh's saved conversations whose server process no
longer exists. The connector runs it after each signed-in connection, so a connection never waits
for that cleanup. Like agent-bearer, it skips the connector-seed boot gate.
auth revoke-credential
cotal auth revoke-credential <public-key | creds-file> [--space <space>] [--yes] [--json|--plain]It stops one credential that auth mint wrote and keeps the space's store and every other
credential. Name the credential by its user public key, which the mint receipt reports as its
identity, or by its creds file, which must be private to you and issued by this space's account. A
word that is not a user public key and names no file refuses TARGET_NOT_FOUND and says both. A
file from another account refuses CONFLICT. A file the space's system account issued, such as the
stack's connection-evictor and membership-observer credentials, refuses UNSUPPORTED. Its Next
is cotal down --yes, then cotal up --rotate-sys, which issues a new system account and retires
the old one's credentials, and after which earlier full backups no longer restore (see up). The command records the key in the space's account,
re-signed by the space's operator, and the broker refuses that key from its next start.
The broker loads a revocation only when it starts, so the space must be stopped: run
cotal down --yes, then this command, then cotal up. On a running space it refuses
LIFECYCLE_BLOCKED and names that route, and it refuses the same way while the root records a
maintenance attempt. A connection held with the credential ends at the down and is refused after
the up. It asks before it revokes, or takes --yes; --json without --yes refuses. A key
already revoked reports the time it was first revoked and changes nothing. --json reports
publicKey, revokedAt in Unix seconds, alreadyRevoked and takesEffect.
auth mint --identity refuses a revoked key. A credential the stack renews itself, such as its
daemons' or a managed agent's, is signed again for the same key at renewal, and the broker accepts
that newer credential, so stop or despawn that agent instead. A per-user-auth space refuses
UNSUPPORTED and names auth revoke <actor>, and an open space issues no credentials to revoke.
Credential profiles
See the current command contract for this behavior.
Paired caller validation
See the current command contract for this behavior.
Whole-row grants
See the current command contract for this behavior.
Lifecycle retirement
See the current command contract for this behavior.