Skip to main content

Local CLI binding v1alpha1

Local management, reads and approved writes; full contract acceptance pending

Rendered from contracts/cli/v1alpha1/semantics.md by connectors-docs; the source owns every rule on this page.

Specified; partial production implementation. This contract owns the local Linux CLI presentation, configuration and host lifecycle choices selected by story:local-cli-binding-semantics. ESS values and fixtures describe data and expected traces. Neither a generated parser nor a recording handler proves keyring durability, process ownership, provider authentication or restart recovery. The existing runtime serves the connectors of the README support table. Setup, configured inventory and passive connection management have production handlers, setup checkpoints-enable (§3) included. The private adapter transport has process/TLS fixtures. Protected GitLab acquisition and restart reuse pass disposable runtime journeys; dedicated provider sandbox acceptance and the other adapters' persistent lifecycle bindings remain open. Approval-key management adds the approvals key-init, key-status, key-rotate, key-recover, key-revoke and key-retire commands. It does not issue approval proofs or enable provider writes. The local approval coordinator adds policy publication, exact subject preparation and protected issuance. Provider write dispatch remains unfinished.

1. Owners and identities​

The CLI parses, selects explicit local configuration, presents safe results and passes admitted requests. Host configuration owns adapter selection and launch; the lifecycle supervisor owns child incarnations; ConnectionAuthorityPort and AcquisitionCoordinatorPort own connection metadata and publication. Linux OS keyring custody owns sensitive versions. Adapter implementations own provider profile/schema interpretation and credential validation. These are replaceable bindings under the existing management, connection, acquisition and custody contracts.

The connector and instance CLI selectors name one configured instance together, and the deprecated adapter selector names a configured entry alias; each resolves to exactly one connectors.declarations.ServiceConfiguration.instance_id (selecting an instance). Neither is an adapter implementation identifier or executable path. connection is the existing owner-qualified connectors.auth_bindings.Connection.connection_ref, and acquisition names its existing acquisition owner/ref. A profile selects the existing immutable AuthProfile under that adapter. Revisions retain the owning record's meaning. No CLI entity, persistence ledger, grant, tenant, fake view or domain command is introduced, except two owner-held records: connectors.cli.LocalRuntimeRecord (stop suppression and the cached bootstrap, §4) and connectors.cli.ConnectionListCursor (the opaque stale-page fence behind connections list cursors, registry_cursors in docs/local-er-metadata.md). Neither grants provider, connection or dispatch authority. Value observations cannot establish authority.

The first local binding admits the current effective Linux UID as the configured owner, verified again at a protected Unix-domain socket using kernel peer credentials. Missing/mismatched owner policy refuses before protected entry or provider work. File ownership alone is insufficient to grant business invocation: configured operation policy and the current shared connection/evidence/grant checks still apply. Local management admits only this owner and configured scope. There is no cloud identity request, implicit tenant selector or saved hosted-login fallback. Writes requiring an unavailable approval/idempotency binding refuse before dispatch; this specification does not silently mark them read-only or select a new write-approval format.

2. Command inventory and process contract​

Every type below is in connectors.cli. The presentation binding is authored separately as ess-cli/1. Process globals config, state-dir and output live in its separate Context; none becomes a command payload field. Paths are resolved before contacting an owner. Connect/reconnect inputs are ephemeral local protected actions: their credential_document: String is acquired through the protected source binding and passed only to trusted completion. Their distinct ConnectionCreateRequest / ConnectionReconnectRequest management projections contain only safe selectors. An ephemeral protected action is never an ordinary management wire payload or a persistent record.

CommandOwnerInput → successful resultMay start local host / selected adapter
setup initlocal configuration writerinputless Context → SetupInitResultno / no
setup checklocal configuration inspectorinputless Context → SetupCheckResultno / no
setup checkpoints-enablelocal metadata store, under the owner lifetime lockSetupCheckpointsEnableInput → SetupCheckpointsEnableResultno / no
setup owner-stopthe running owner of the state directory, through its shutdown request (owner transport)inputless Context → SetupOwnerStopResultno / no
approvals clock-checkconfigured clock inspectorApprovalClockCheckInput → ApprovalClockCheckResultno / no
approvals policy-statuslocal policy inspectorApprovalPolicyStatusInput → ApprovalPolicyResultno / no
approvals policy-setlocal policy coordinatorApprovalPolicySetInput → ApprovalPolicyResultno / no
approvals preparelocal subject coordinatorApprovalPrepareInput → ApprovalPrepareResultno / no
approvals issuelocal issuer coordinatorApprovalIssueInput → ApprovalIssueResultno / no
listthe shipped connector declarations and, when one exists, the configuration's [connectors] table (listing connectors)inputless Context → ConnectorListResultno / no
definitions installthe configuration's definitions.lock and the state directory's read-only definitions cache (installing a definitions release)DefinitionsInstallInput → DefinitionsInstallResultno / no
instances listconfigured inventoryAdapterListInput → AdapterListResultno / no
instances describeconfigured inventory and selected cached metadataAdapterDescribeInput → AdapterDescribeResultno / no
instances statusexisting lifecycle owner observationAdapterStatusInput → AdapterStatusResultno / no
instances stopexisting lifecycle ownerAdapterStopInput → AdapterStopResultno / no
connections listconnection metadata authorityConnectionListInput → ConnectionListResultno / no
connections describeconnection metadata authorityConnectionDescribeInput → ConnectionDescribeResultno / no
connections connectacquisition/configuration coordinatorConnectionConnectInput → ConnectionConnectResultyes / yes, after admission
connections reconnectexact existing connection's coordinatorConnectionReconnectInput → ConnectionReconnectResultyes / yes, after admission
connections revalidateexact existing connection's evidence coordinatorConnectionRevalidateInput → ConnectionRevalidateResultyes / yes, after admission
connections launchexact existing connection's read admission and the owner's custody readConnectionLaunchInput → the consumer's exit status (consumer launch)yes / no
connections tokenexact existing connection's owner, through its access token issue (access token issue)ConnectionTokenInput → ConnectionTokenResult, or the token alone with --revealyes / no
connections statussafe connection/acquisition observationConnectionStatusInput → ConnectionStatusResultno / no
connections revokeconnection metadata authorityConnectionRevokeInput → ConnectionRevokeResultno / no
operations listselected cached descriptor projectionOperationListInput → OperationListResultonly to launch for a description
operations describeselected cached descriptor projectionOperationDescribeInput → OperationDescribeResultonly to launch for a description
invokeadmitted service request on the instance and connection its target selects (invoking an operation)InvokeInput → InvokeResult, or InvokeDescription with --describeyes / yes, after admission; with --describe only to launch for a description

Consumer launch​

connections launch is the one command whose successful outcome is not a result envelope. It starts an operator-pinned consumer from a connectors-local/3 [consumers] entry with the selected connection's protected document on descriptor 3, passes the caller's stdin, stdout and stderr through, and exits with the consumer's exit status. ConnectionLaunchResult is the handler seam's typed value only; it is never written, because stdout belongs to the consumer. A refused launch writes the ordinary failure envelope to stderr and starts no consumer. Its argv is the entry's pinned prefix, then the optional --args JSON array verbatim; the entry's pass_env prefixes select the caller's variables it gets. No adapter starts; the owner alone reads the credential and hands the consumer a sealed memfd. Admission order, delivery, process rules and the same-user boundary are in consumer launch.

approvals clock-check performs the bounded authenticated exchange specified in the clock binding. Its public observation cannot be reused as time evidence; it starts no service and reads no secret or metadata. The observation carries one sources line per configured source. When the quorum forms no answer it refuses unavailable at stage = observation, and its Failure names the combined refusal in clock_refusal and each source's line in clock_sources (refusals): below_quorum with check_configuration if a source is unverified, else retry_explicitly; split and too_imprecise (the combined answer over 8,000 ms; a verified source counts at any width and is never too_imprecise on its own) with choose_clock_source. A local timer, continuity or calendar refusal carries neither field and keeps check_configuration. The refusal never carries the observation's bounds, a source's own interval, the request or the reply. A retired roughtime-clock/1 table refuses invalid_configuration at stage = configuration with check_configuration and configuration_rule approval_clock_format_retired, for clock-check and for every approval issuance, execution or recovery that needs the clock; commands that do not use the clock still load the configuration.

approvals policy-set accepts only --input-file; its path is acquired by the production handler using the bounded document reader. prepare and issue use one of --input-json, --input-file or --input-stdin for nonsecret business input. issue additionally requires --approve-subject and --proof-output. Their authority, publication boundaries and helper limits belong to the local mutation binding. Proof bytes never enter the ordinary successful CLI result.

Inventory and status commands use configured/cached descriptions and safe persisted metadata or query an already-running owner without provider authentication; operations describe and operations list start an owner and the adapter only to launch for a description. Unavailable authoritative metadata returns metadata_unavailable; cached facts are labeled stale and cannot be used to decide fresh connection viability. Missing cached operation metadata returns description_unavailable, never download, installation or authentication, and never a launch except as follows.

Launching for a description​

operations describe, operations list and invoke (a read, a write, or with --describe) start the instance's configured adapter when, and only when, all of these hold: no description is cached for the instance's configured selection; the started executable is the configured one, admitted by its pinned SHA-256 as every launch is; and the instance holds a published connection (the named one, for a command that names one) that is not revoked or stale, whose credential is neither known invalid nor expired, whose upgrade the configured revision has not refused, and that is current under the configured entry, as connections list reports it with stale = false (its configuration revision is the configured one, or both record the same credential-side digest). The launch reads no credential, asks for none and sends nothing to the provider; it only caches the description, and an operator's instances stop is not resumed by it: a stopped instance answers description_unavailable. A revoked or stale connection, one whose credential is known invalid (its identity refused outside an executable-only change), and one whose credential-side digest moved (a changed provider authority, profile authentication or trust roots, for an implementation that states the digest) or that records no digest under another configuration revision are not current, so the command answers description_unavailable as before and starts nothing. An identity refused under an executable-only change records nothing and changes nothing, so it does not stop a launch for a description, which reads no credential; that connection's reads and writes keep refusing as identity_mismatch.

Status distinguishes unavailable owner from a known stopped process. An owner need not be running to commit local revoke if the admitted metadata implementation supports the same serialized authority; otherwise revoke returns metadata_unavailable and starts nothing.

ConnectionListResult carries page-level source, stale, observed_at_ms and optional valid_until_ms, covering every returned summary. Any cached member makes the page source: cached, stale: true; caching never preserves a fresh label, even before a known validity deadline. An all-authoritative page may be fresh only while its owner's observation remains valid for every member. Its deadline is no later than the earliest member deadline. An absent or expired deadline requires stale: true, including for an empty page without a known page deadline. Configuration alone cannot establish connection state. These labels preserve observation provenance; they grant no invocation or publication authority. The same cached-is-stale rule covers adapter descriptions, operation lists/descriptions and cached connection descriptions. instances list reports configured inventory with source: configuration only; it carries no cached lifecycle or readiness claim. Each item is an InstanceSummary: connector, instance, instance_id, startup and restart, never an alias or implementation (story:connectors-do-short-invocation). A legacy [adapters.<alias>] entry of the catalog provider is listed under the connector setup migrate derives for it; one no connector is derived for is left out, and its alias is listed, sorted, in unmigrated, a member present only when there is one. Adapter status and acquisition status read their existing owner's observations or report owner unavailability; they have no cached-result fallback. Connection status can carry a cached ConnectionDescription only with its explicit cached/stale labels.

setup init and setup check explicitly bind input: null and an empty argument list in ess-cli/1; the handler receives {} plus Context. ESS has no empty struct type, so no dummy payload field or fictitious type is introduced for these actions.

connections status selects exactly one connection or acquisition. Connection status uses a safe connection observation; acquisition status uses the acquisition owner and stored repair relation. Both are metadata-only. Repair/revoke select one exact connection and expected public semantic revision; that caller-visible revision is not the private auth publication fence or material generation. The coordinator captures and compares its own current private publication fence when beginning and completing repair and serializes it with revoke/final dispatch. A repeated body selector at any management-envelope boundary must agree with the route selector. No implicit business connection fills an instance-scoped create/list/acquisition-status input.

The public AcquisitionObservation.state is exactly pending, completed or failed, following acquisition §4.3. Internal Pending and Completing both project as pending; Completed projects as completed; Failed and Expired project as failed. Failed observations carry the safe reason from connectors.auth_bindings.AcquisitionFailure: rejected, exchange_unknown or expired. Known expiry specifically carries expired. Reason is present exactly when failed, and the yielded connection reference is present exactly when completed. Pending/completed observations carry no failure reason. These cross-field requirements are runtime obligations; Optional fields and structural schema validation alone do not enforce them. Observation of a consumed or failed acquisition never permits repeating its exchange.

--output json emits exactly one UTF-8 {"ok":true,"result":...} success envelope plus newline to stdout. Failure leaves stdout empty and emits one {"ok":false,"error":{"code":...,"data":...}} envelope plus newline to stderr. A command selected with the deprecated --adapter, or run under a renamed path, adds a deprecations member to either envelope and nothing else (selecting an instance, renamed commands). Application error data is Failure, while parser/source/internal errors have stable codes and empty data as defined by ess-cli/1. Human mode identifies a safe next action through the typed result/error. Progress belongs on stderr; JSON mode suppresses ordinary progress so stderr is also machine-readable. Diagnostics are fixed safe messages, never raw argv, file contents, keyring locators, provider responses, actionable URLs or exception debug dumps. Secret-entry paths are redacted too. Output framing is a codec obligation beyond the ESS value shape. completions SHELL is outside this envelope: it prints the raw completion script to stdout with exit 0 whatever --output selects. The ess-cli/1 generator owns that behaviour; this binding adds nothing to it.

ExitMeaningFailure kind
0successful command, including a truthful negative status observationnone
2malformed arguments, conflicting sources, invalid config or business JSON/schema inputusage
1admitted operation failed, unavailable dependency, stale target, refusal or unknown outcomeoperational
130interrupted command; no implicit retryinterrupted

Parse errors use this same selected output contract, including unknown commands and help/argument collisions. Failures the ess-cli/1 presentation raises before the handler runs carry its fixed codes with empty data, exit 2 and empty stdout, never an application Failure:

Conditioness-cli/1 code
unknown command or flag, missing or malformed argument, two sources for one fieldcli_parse
an argument value that does not decode to its field type or the command's input shapecli_input
a selected source the presentation cannot read, when the owner recorded no refusal of its owncli_source
an invoke business JSON document that does not decode as exactly one JSON value (malformed, a duplicate object key, trailing document, 128 or more nesting levels counting the outermost value: the decoder's recursion limit, reached before the owner's depth bound)cli_dynamic_input

When the owner's reader refused the selected source, the owner's Failure takes the place of cli_source (an unreadable business document file is invalid_input; a missing safe entry channel is the table row below). A decoded business document that the selected operation schema, its depth bound (64) or its byte bound refuses is the owner's invalid_input Failure (exit 2). approvals prepare and approvals issue hand their document to the owner undecoded, so there an undecodable or trailing document is the owner's invalid_input too, not cli_dynamic_input. Interruption stops waiting and protected capture, restores terminal settings and erases transient capture buffers. It does not claim rollback after a durable publication or possible provider dispatch. A publication whose acknowledgement is lost is outcome_unknown; a later status read may resolve it without replaying begin/exchange or the business request.

The explicit service commands are removed (story:connectors-do-short-invocation): describe --endpoint --token-file [--allow-plaintext], invoke --endpoint --token-file [--allow-plaintext] --operation --input FILE and serve --config are command lines like any other unknown command or option, exit 2, empty stdout and the fixed cli_parse code with empty data on stderr, never an argv echo, with --output before or after the command word, and they contact nothing. describe --help, serve --help, help describe and help serve are parse refusals as well. invoke is the generated command of invoking an operation, and completions bash emits the generated completion function unchanged. The federation library stays a library; no command of this binary serves it, and no compatibility inventory or Legacy* type remains.

Help is the generated grouped tree as connectors::help presents it (apps/connectors/src/help.rs) with what the ess-cli/1 binding cannot declare: each group's summary, a deprecated command path marked Deprecated in its parent's listing and in its own usage line, --adapter marked deprecated, and the help of each invoke argument. It parses exactly as the generated tree does, and help prints no deprecation line on standard error, under an old path or with --adapter too. Root --help or -h, alone or after leading process globals, prints that help's short form, one blank line and then this block, which the generated help does not carry:

Native integration commands (use COMMAND --help for options):
server Serve admitted read-only MCP tools over stdio

Access token issue​

connections token asks the selected connection's owner for a current access token of an oauth2_grant connection, under the issue rules of auth acquisition §4.4. It takes the instance's one connection when --connection is left out, and starts the owner when none runs; the owner must be of the same build, or the command is refused as owner_build_mismatch. It starts no adapter.

Without --reveal the result is ConnectionTokenResult: the connection, the token's expiry as an RFC 3339 instant, its granted scopes, whether this issue refreshed it, and the id of the recorded connectors.oauth2.AccessTokenIssue. Neither output stream carries the token. With --reveal (a bare --reveal reads --reveal true) a successful issue writes the access token and one newline to standard output, nothing to standard error under --output json, and no envelope; a refused issue answers its ordinary failure envelope. Each successful issue records one AccessTokenIssue naming its delivery (metadata_only or revealed); a refusal records none.

Refusalcodenext_action
connection of a profile that is not oauth2_grant, or not in the instance's permissions; a token inside the lead of a confidential client, which this owner cannot refreshforbiddenrequest_permission
Stale connection, or a refresh the provider refused or left uncertainservice_failurerepair_connection
a refresh whose granted scopes no longer cover the profilenot_grantedrepair_connection
a refresh the provider answered as revokedrevokedcreate_connection
token endpoint answered 429unavailable, service code rate_limited, retry_after_secondsretry_explicitly
token endpoint not reached, nothing sent; a token past its expiry with nothing published in its placeunavailableretry_status
the deadline passed before the issue was recorded; a refresh it ran stays publishedtimeoutretry_explicitly
custody cannot be readcustody_unavailableunlock_keyring

3. Explicit local TOML configuration​

Selection order is explicit Context --config, then $XDG_CONFIG_HOME/connectors/config.toml when that absolute, owner-controlled directory is configured, otherwise $HOME/.config/connectors/config.toml. No working-directory search, hosted profile, shell expansion inside TOML or environment credential fallback occurs. Explicit Context --state-dir selects the metadata directory; state otherwise defaults analogously below $XDG_STATE_HOME/connectors or $HOME/[internal source reference]. The configuration path in safe results identifies the selection; it is not a credential locator.

setup init exclusively creates an owner-only file and directories with no symlink traversal, writes/fsyncs a temporary file, publishes without replacing an existing file, and fsyncs its directory before returning created. An existing configuration is never overwritten. When the selected state directory holds no metadata database and the existing configuration loads, setup init creates the owner-only state directory and initialises its metadata database, leaves the configuration byte-for-byte unchanged and returns state_initialized. An existing configuration with an existing metadata database, or one that does not load, returns configuration_exists; an existing database is never opened, migrated or recreated by setup init. The metadata database setup init creates, on created and state_initialized alike, has Entity Runtime durable open checkpoints enabled before setup init returns, so its opens start from a persisted checkpoint instead of verifying the whole history, and connectors 0.32.0 and earlier cannot open it. setup init never enables a database that already exists; only setup checkpoints-enable does (below). The file uses the current connectors-local/5 format, the one instances and connections connect --endpoint need, with an empty legacy [adapters] table (story:catalog-provider-failure-is-named). A connectors-local/2 file's adapter entries each select a private_protocol (private mutation extension); an existing connectors-local/1 file keeps loading unchanged. A connectors-local/3 file is /2 plus an optional [consumers] table of pinned consumer launch executables; [consumers] in a /1 or /2 file refuses the configuration. A connectors-local/4 file is /3 plus an optional [custody] table (connectors.cli.LocalCustodyBinding) that explicitly selects the custody binding: binding (closed: secret_service, macos_keychain, windows_credential_manager, mounted_secret, and one the build supports) and, exactly for mounted_secret, directory, an absolute path with no symbolic link. mounted_secret beside secret_service_socket refuses the configuration, as does [custody] in a /1 to /3 file. Without [custody] custody is the platform binding, unchanged. The mounted directory is an infrastructure binding of the owner configuration, like secret_service_socket, not a per-credential locator (mounted-secret custody). setup init never writes [custody]. It records the local owner policy and OS keyring requirement; it does not write credentials, authenticate, install artifacts or start processes. setup check checks syntax, permissions, configured artifacts and non-interactive keyring availability. A missing/locked keyring produces a failed prerequisite in its typed result; no unlock prompt, write or successful connection is implied. Its installed_provider prerequisite starts the catalog provider installed beside the CLI with --version alone, as a derivation starts it: ready when it prints its version, otherwise failed with next_action = check_configuration and installed_provider_failure naming why (connectors.cli.InstalledProviderFailure: missing, not_executable, killed, exited, deadline, unreadable_output). No instance is derived while it is failed (story:catalog-provider-failure-is-named).

The valid example chooses per-entry startup = "on-demand" | "automatic"; omission means on-demand. restart = "never" is the only first-stage policy. Configuration points at an explicit absolute local executable with SHA-256 digest, expected service instance, adapter identity and protocol version. Launch uses an argv vector, never a shell. The executable must be an owner-admitted regular file and match the pinned digest at launch. No discovered URL/path, PATH lookup, moving branch, download/build or container selection is accepted by this first binding. Discovery is information. Any future remote endpoint or installer is a separately admitted binding.

Duplicate aliases/instance IDs, unknown keys, relative paths, invalid modes, reserved management-name collisions, bad digests and unresolved default selections refuse the entire configuration before host launch. TOML contains no credential bytes, keyring locators or actionable acquisition continuations; a [custody] directory is an infrastructure binding, not a locator. A default adapter must name exactly one entry, and so must a default instance (selecting an instance); connection defaults, if used by a future binding, must be explicitly recorded and cannot be inferred from last usage. This first binding requires an explicit connection for a credentialed operation.

The configuration stage needs an absolute configuration and state path, an owner-only file and owner_uid equal to the effective user, and no other variable: with both --config and --state-dir no environment variable is read. Its invalid_configuration refusal names the rule that refused in configuration_rule (connectors.cli.ConfigurationRule), never a path or OS text: config_path_unresolved (no --config, XDG_CONFIG_HOME or HOME, or an inadmissible path), config_missing (no file at the resolved path), config_not_private (the file or a directory above it is not owner-only), owner_uid_mismatch or, from setup migrate only, config_too_large (the rewritten file would be larger than the 1 MiB a read admits; the refusal carries its size in configuration_bytes and the bound in configuration_limit). invoke --definition alone also names definition_instance_configured (invoking a definition file).

Selecting an instance​

Every command that selects one configured instance takes --connector <connector> --instance <instance>; an instance name is unique within its connector only, so --instance alone selects nothing. --adapter <alias> keeps selecting by a configured entry's alias for a deprecation window whose length the release plan decides. Exactly one of --adapter or --connector with --instance is given; both, or either half of the pair alone, is invalid_input at arguments. With neither, the configuration's default_instance selects, else its default_adapter; with neither configured the command is invalid_input at arguments. A pair that names no configured instance is not_found at admission with next_action check_configuration, as an unknown alias is.

An instance's names are those of the [connectors.<connector>.instances.<instance>] table that names its alias. An [adapters.<alias>] entry that no instance names is selected as connector adapter_id, instance <alias>, until it is migrated. A catalog entry no connector is derived for is no instance: naming it so (--connector catalog --instance <alias>, or the invoke target catalog/<alias>) is invalid_configuration at configuration with configuration_rule legacy_entry_unmigrated and next_action check_configuration. A configuration in which two entries would answer to one connector and instance pair refuses the whole file, as does default_instance that is not "<connector>/<instance>" naming a configured instance, default_instance outside connectors-local/5, or default_instance beside default_adapter.

Both selectors reach the same entry and the same records: a result is the same whichever selected it. Every result type that carries adapter carries connector and instance beside it, all three required. adapter is the instance's legacy alias for the deprecation window. instances list items and every answer of invoke carry no adapter. One shape is exempt: the service descriptor that adapters describe returns (the object carrying configuration_schema) keeps its own adapter, the implementation it describes (such as catalog), and its own instance, the instance id. Neither is a selector there, and the descriptor carries no connector. The surrounding result carries the selector fields.

--adapter says it is deprecated exactly once per command. Under --output human that is one line on standard error, ahead of any failure text the command writes there:

deprecated: --adapter; select with --connector <connector> --instance <instance>

Under --output json nothing is added to standard error. The one envelope the command writes, success or failure, parse refusals included, carries a third member beside ok and result or error:

{"ok":true,"result":{},"deprecations":[{"option":"--adapter","replacement":["--connector","--instance"]}]}

A caller that reads all of standard error as one JSON failure envelope keeps reading its error. Under --output json, output that is no envelope (help, completions, a launched consumer's streams) is unchanged.

Giving an instance its endpoint​

connections connect also takes its instance as one positional argument, connections connect <connector>/<instance>, which selects exactly as --connector <connector> --instance <instance> does. The positional beside --connector, --instance or --adapter, or one that is not exactly two names around one /, each 1 to 128 bytes of [A-Za-z0-9_-], is invalid_input at arguments with field = target (exit 2), before anything is written.

connections connect --endpoint <url> gives the selected instance its endpoint, the one setting a catalog instance answers before it can be derived. No other command takes --endpoint; there it is an ess-cli/1 parse refusal. Before any protected source is acquired and before the provider is started, the endpoint is written into [connectors.<connector>.instances.<instance>] of the configuration file:

  • an instance that is not listed is added with only endpoint, and a [connectors.<connector>] table that is absent is added with enabled = true; a disabled connector table is left as written and the command is invalid_configuration;
  • a listed instance without endpoint gains the key;
  • a listed instance with the same endpoint is not written at all;
  • a listed instance with another endpoint is lifecycle_conflict at configuration with next_action = check_configuration and field = endpoint (exit 1): another host is another instance, which takes another name. The file is unchanged.

Another host is another instance for a connection too. Before the file is written, the provider derives the instance at the new endpoint; when the instance already holds a connection, revoked ones included, admitted under a provider authority other than the one derived, adding or changing its endpoint would strand that connection, and the command is the same lifecycle_conflict naming endpoint, the file unchanged. An endpoint the provider refuses while deriving the instance (one that is not https, or carries userinfo, a query or a fragment) is invalid_input at arguments with field = endpoint (exit 2), nothing written; the refusal never echoes the endpoint. A provider refused the endpoint only when it runs: when the catalog provider installed beside the CLI does not print its version (setup check's installed_provider), the command is unavailable at installed_provider with next_action = check_configuration (exit 1), naming the provider's absolute path in installed_provider_path and why it does not run in installed_provider_failure, nothing written (story:catalog-provider-failure-is-named).

The endpoint must pass the configuration's own endpoint rule (non-empty, at most 2048 bytes, no whitespace or control character); otherwise the command is invalid_input with field = endpoint and nothing is written. So is --endpoint with no <connector>/<instance> named (--adapter or the default instance), and on an instance that carries the pins of a legacy entry, whose endpoint is its pinned selection's. A connector that declares no default endpoint, selected on an instance that is not listed or is listed with neither endpoint nor pins, is invalid_input with field = endpoint when --endpoint is absent, before any credential is asked for.

Only a connectors-local/5 file is written. Any other format is invalid_configuration naming the file's configuration_format, which setup migrate rewrites. The write keeps every other byte of the file (comments, spacing, order, inline tables, a leading UTF-8 byte order mark; in a file whose every line ends in CRLF, the added lines end in CRLF too), checks the new text as every read checks it before anything is written, and replaces the file atomically: a sibling temporary file with the file's own owner-only mode, synced, then renamed over it, while the file still holds the text the edit was made from (else revision_conflict). The file is then read back; a read-back that does not hold the endpoint restores the original text and is invalid_configuration. A file that is not the caller's own, admits another user, or is a symbolic link is refused as every read refuses it, unwritten. The endpoint is never a credential: a token or secret is never written into the configuration.

Invoking a definition file​

invoke --definition <file> <operation>, with the input sources of every invoke, runs an operation of a connectors-definition/1 file that no configuration names (architecture-decision-record:provider-definitions-and-runtimes D12 point 3). The target may be left out; given, it names the definition's connector (invoking an operation). --definitions-root and --credential-file without --definition are invalid_input naming each. --definitions-root <dir> is the tree holding the definition's vendored source and shared auth files; it defaults to the parent of the directory holding the file. --schema and --revision may be left out: each one left out is the one the instance's description states, as invoke --describe reports it.

The file is lowered with the reference lowerer into an owner-only cache under the state directory, definitions/<digest>/, keyed by the digest of every file the lowering reads (the definition, its vendored source, its amendments and the shared auth files its profiles use, each by its path under the definitions root) and of the lowerer's release. Each file is read once; the lowerer reads a private copy of exactly those bytes, so an entry holds the lowering of its key. A run whose inputs are unchanged reuses the entry; a changed one is lowered into a new entry beside it. A definition the lowerer refuses, or one that cannot be read, is invalid_input at arguments and leaves nothing behind; connectors-build definition names the lowerer's refusal. Every variable the definition's base URL names must have a default: the instance's endpoint is the one the lowered declaration states, and its trust roots are the platform's.

The instance is the connector's instance definition (the connector id is the definition's id, else its file stem), with the alias and instance id <connector>.definition, or its moved instance definition-<16 hex> when that instance id cannot hold the file's provider authority or profile (below). Its entry pins the installed catalog provider on the lowered configuration as a derived instance's does, and is held in the owner-only file definitions/instances.toml beside the cache; a later run of a file with the same connector id replaces that entry, so both files are one instance and its connection follows the rules below; a run whose entry another run replaced before it was selected is revision_conflict at publication with retry_explicitly. At most 64 connector ids are held; another is invalid_configuration at configuration. No command forgets an entry: revoking its connection with connections revoke and then removing definitions/ from the state directory while no owner runs forgets every entry and the cache. The connector id, the file stem and the catalog (the stem when the definition names none) are plain names (ASCII letters, digits, - and _); any other is invalid_input at arguments, before anything is written. An entry names the catalog provider started as --local-config of its own cache entry's native.json, compared as paths so every spelling of the state directory names it; an entries file holding any other entry is not read. Every command that selects an instance reads it beside the configuration, so --connector <connector> --instance definition selects it too, but instances list and setup check report the configuration alone. The configuration file is never written. A configuration that already has the connector's instance definition, or an entry under that alias or instance id, is never replaced: the command is invalid_configuration at configuration with configuration_rule definition_instance_configured and next_action check_configuration, and lowers nothing.

Lowering and recording connect nothing: they create no connection, admit no operation and perform no OAuth exchange (D12 point 4). When the instance holds no connection the command asks once, on its controlling terminal, whether to connect; only a y or yes answer starts the profile's acquisition, exactly as connections connect runs it: the token prompt on the terminal, or the file --credential-file names (an OAuth client file becomes the entry by consent; under the read-only mounted_secret binding the owner reads the item itself and the file is refused). Any other answer, or a run without a controlling terminal in the foreground, is not_found at admission with next_action create_connection; nothing is acquired and nothing is sent to the provider. With one connection the operation runs on it with no question, and --connection names one explicitly; an instance that holds more than one is invalid_input at arguments. A change to the bundle, its source or the selection alone keeps the connection (one connection per instance, D12 point 2). A change of the provider authority or of the profile's authentication cannot be followed under the instance id <connector>.definition once a connection of it, revoked ones included, was admitted under the earlier one (docs/local-connection-registry.md, "Configuration upgrades": only a new instance id helps). The definition then resolves to its moved instance, definition-<16 hex>: the first 16 hexadecimal digits of a digest of the connector id, the provider authority and the profile's authentication (its declaration apart from its revision and evidence lifetime, with its id, purpose, subject and minimum scopes). The name is the same on every run of the same definition, a lifetime-only change keeps it, and a file moved back to the earlier authority resolves to definition and its connection again. The moved instance is prepared, cached under its own key and recorded as the connector's entry exactly as above, so its first run is a first invoke: the one question, a fresh connect, and no candidate at the earlier authority. The connector keeps one entry, so the instance id the file left is configured by no entry, and the connections left under it are not used, changed or forgotten; connections revoke of one under an instance of the same connector whose instance id is <connector>.<name>, such as the moved instance, reaches it (the moved entry); an instance of another connector does not. A configuration that already has the moved instance is refused as for definition below. Reusing a stored credential for another authority or profile is not part of this binding.

When the instance holds no connection and another instance of the same connector holds a connection whose credential a new connection of the definition's instance can use, the one question lists those connections and offers to reuse one instead (D12 point 3, story:invoke-definition-reuse-credential). A candidate is a published connection of another instance of the definition's connector id, an instance of [connectors.<connector>] (a legacy [adapters] entry is no instance of a connector), that is Live and not Stale, holds its credential, has current validation evidence and is current under its own configured entry, and is bound to the same adapter, provider authority and profile authentication (equal, or differing only in the evidence lifetime) as the definition's instance. Its profile is a static entry: a profile whose entry an OAuth acquisition refreshes is never copied, since one rotation would end the other's refresh. The definition's lowered configuration must send the credential to its HTTPS API base alone: a profile naming a token endpoint or any other URL, on any origin, is offered nothing, since the binding compares the provider authority and the profile declaration, a token endpoint is part of neither, and the source may send its secret to another place than the definition would. The copy is validated under the platform's trust roots, as every definition is, whatever trust roots the source's own entry pins. Under the read-only mounted_secret binding nothing is offered. A revoked, Stale, expired or moved connection, and a connection of another connector, is never listed. The question lists at most 16 candidates, each by number with its <connector>/<instance>, its provider authority and its validated external identity (kind and subject), never material, and ends [1/c/N] ([1-<n>/c/N] for n candidates). A number reuses that candidate; c or connect connects fresh as above; any other answer, or a run without a controlling terminal in the foreground, is not_found at admission with next_action create_connection, and nothing is read, copied or sent.

On reuse the owner, never the CLI, copies the credential (custody §4.3). It repeats every check above against the current configuration and store, admits the definition's instance as a connect does (refused while it holds a connection or another connect of it is in flight), reads the candidate's current version under a read use of the source, has the definition's adapter validate it with the profile's own identity and scope probes, and publishes a new connection of the definition's instance holding the material as its own new custody version, only when the validation succeeds, the validated identity equals the source's, and the source is, inside the publishing commit, still published, Live, not Stale and under the publication fence and version that were read: a source revoked, made Stale, reconnected or revalidated while its copy was validated publishes nothing (revoked, or lifecycle_conflict). The acquisition is recorded as a connect's, and connections status --acquisition follows it. Otherwise nothing is published, the acquisition is failed, and the command offers to connect fresh with one [y/N] question; declined, the command answers the reuse's own refusal: identity_mismatch at admission with next_action create_connection when the provider answered another identity, otherwise the validation's or the owner's failure. A run that cannot know whether the copy published (outcome_unknown, interrupted) offers nothing. Either way the source connection is unchanged (its revision, state, evidence and identity), and after a copy the two share nothing: revoking or reconnecting either leaves the other as it was.

From the selection on, the command is the configured instance's: the description, admission, approval of writes, audit and the result envelope are those of every invoke of a configured instance, and a result names connector <connector> and instance definition, or the moved instance definition-<16 hex>.

Invoking an operation​

connectors invoke <connector>[/<instance>] <operation> (story:connectors-do-short-invocation) invokes one operation of one configured instance, with only what has no default named. Before the generated runner reads the command line the instance, connection, schema and revision are resolved and stated, and the command is then admitted, approved, audited and dispatched on that instance and connection. It takes no --adapter, --connector, --instance or --endpoint: each is cli_parse.

The target is a connector id, optionally followed by one / and an instance name, each an admitted selector (at most 128 bytes of [A-Za-z0-9._-]); anything else is invalid_input naming target, and the target is never echoed. A [connectors] instance and a recorded definition instance are named by their own names; a legacy [adapters.<alias>] entry by its alias, under the connector setup migrate derives for it, or under its implementation when that is not the catalog; a catalog entry no connector is derived for cannot be named here. A [connectors] instance wins over a legacy entry that derives the same names, as --connector and --instance select it, so each instance has one name. With the instance left out the connector's only instance is selected; default_instance is never consulted. A connector or instance not configured is not_found at admission naming check_configuration.

instance_ambiguous: the target names a connector with several instances and no instance. It is a usage refusal at arguments, exit 2, next_action = none, and its Failure.instances lists every instance of that connector as <connector>/<instance>, sorted, so the caller can name one. Nothing was read beyond the configuration. A target that names its instance is never instance_ambiguous.

Without --connection the instance's one ready connection is selected. When none is ready, its one usable connection is: pending only because its validation evidence lapsed under the instance's current configuration, as an idle connection's does; that evidence is re-checked with the provider before the call, as every read re-checks lapsed evidence (§6). One ready connection beside lapsed ones is the one selected. Revoked connections are not counted, whatever their number. A named --connection is used as named; one the instance does not hold is not_found.

connection_unresolved: no connection is usable, several are ready, or several are usable and none is ready. It is operational at admission, exit 1, and names create_connection when the instance holds no connection, repair_connection when it holds connections and none is usable, and none when --connection must choose. Its Failure.connections lists each connection the instance holds, revoked ones excepted, as {connection, state} with the state connections list reports; the list is empty when none is held.

--schema and --revision left out are the instance's current admitted description's, read through the selected connection's binding as operations describe reports it. Given, each is a pin: a description that differs is stale_description at observation naming refresh_description, and the pin is never replaced by the current value.

The business document comes from --input-json, --input-file or --input-stdin. With none of them standard input is read, unless it is a terminal: then invoke refuses at once with invalid_input naming input, without reading it. A document that does not decode as one JSON value is cli_dynamic_input; one the operation's schema or bounds refuse is invalid_input naming input.

--describe (or --describe true) prints the operation's InvokeDescription: connector, instance, operation, description, effect (read or write), schema, revision and input_schema. A description that leaves an operation's effect unknown is never admitted, so effect is never anything else; one that did would be unsupported. It reads no document, needs no connection and invokes nothing; it may launch for a description as operations describe does, and refuses a differing pin as above.

approval_required on invoke: a write without --approval-file is refused at admission, exit 1, next_action = none, before any provider call, and its Failure.approval_steps names the commands that produce the proof, in order: approvals prepare, approvals clock-check, approvals issue. A read given --approval-file or --idempotency-key is invalid_input naming that argument. A write with its proof is admitted and dispatched under the local mutation binding.

connectors invoke --definition <file> <operation> invokes a connectors-definition/1 file read on the fly (invoking a definition file): the same digest cache under <state>/definitions/<digest>/, the same definition instance and the same connect question. With --definition the target may be left out, and one positional is the operation: the connector and its instance are the file's. A target given beside it names the definition's connector, and its instance if it names one; a target naming another connector or instance is invalid_input naming target. Without --definition one positional is invalid_input naming target. --definitions-root and --credential-file without --definition are invalid_input naming each, and a refusal of the file that names no argument names definition. With --describe the definition is lowered and answered from the description the installed provider reports for it: nothing is recorded, no connection is resolved, asked for or created, --credential-file is not read, and nothing is asked on the terminal.

The result is InvokeResult: connector, instance, operation, revision and the provider's result, with request_id, mutation and source_audit when the invoke produced them. No answer of invoke (result, refusal or help) names a configured entry by its alias. Every invalid_input of invoke names the argument it refused in Failure.field: target, operation, connection, schema, revision, input, approval_file, idempotency_key, definition, definitions_root, credential_file, fields, each, walk, max_pages or out.

Shaping a result​

connectors invoke … --fields <paths> prints only the named members of the provider result, and --each <path> one line per element of an array in it (story:invoke-shapes-its-result). Both are presentation: the paths are parsed before anything is read, are not part of the approval subject, never reach the owner or the provider, and are applied in the CLI process to the full InvokeResult after the generated runner has checked it and the owner, the audit record and the mutation ledger have it. The answer is then InvokeShaped.

A path list is comma-separated paths. , separates the items, so a key holding , cannot be named in --fields, not even by a pointer: /a,b is the two paths /a and b. Each item is trimmed, so a key with leading or trailing whitespace cannot be named either: /a names a. A path is name(.name)*, where [] after a name, or at the start, maps over an array (events[].field_name); a name holds no ., [, ], ,, :, /, whitespace or control character. A key holding ., [, ], :, /, ~, or whitespace between other characters, is written as an RFC 6901 pointer, a path beginning with / whose tokens are member names (~1 is /, ~0 is ~): /page[after]. alias:path prints the value the path reaches under alias, a name. There are no filters and no indexes. Paths read the provider's result; for a catalog result, {status, body, provenance} with an integer status, a path whose first name the result does not have reads body, so ticket.id and body.ticket.id name the same member.

--fields prints each path without an alias nested as written, from its first name, paths that share a prefix in one member; [] keeps the elements the rest of the path reaches, and an empty array stays empty. A path that reaches nothing (a missing member, a member of a scalar, [] over a non-array or over elements none of which has the rest) is listed, as written, in result.missing, so an absent field is never silent; a null member is present. missing is left out when every path matched.

$ connectors --output json invoke zendesk/support ticket.show --fields ticket.id,state:ticket.status <<<'{"ticket_id":101}'
{"ok":true,"result":{"state":"open","ticket":{"id":101}}}
$ connectors --output json invoke zendesk/support ticket.show --fields status <<<'{"ticket_id":101}'
{"ok":true,"result":{"status":200}}

--each <path> takes one path of member names (no [], alias or comma) naming an array and prints one JSON line per element, each shaped by --fields when it is given and whole otherwise, then the envelope as the last line, whose result holds records, the number of lines printed. A field is in missing when no element has it; the each path is, with records = 0, when it names no array. Under --output human the last line is the result alone.

The answer is printed whole, as without either flag, when its result is a catalog result whose status is not 2xx. A refusal is never shaped: the failure envelope stays on standard error unchanged. request_id, mutation and source_audit are printed whenever the answer has them, whatever --fields names, so a write's outcome_unknown cannot be projected away.

Refused before any provider call, as invalid_input at arguments, exit 2, naming fields or each: a malformed path; an empty item; an alias that is not a name; a path without an alias that begins with []; an output name that is missing, records, request_id, mutation or source_audit; an alias that repeats another output name; more than 256 paths or 64 steps in one path; either flag beside --describe, which reads no result. --each on a write is invalid_input naming each, ahead of approval_required; --fields on a write is admitted.

Walking pages​

connectors invoke … --walk --out <file|-> [--max-pages <n>] reads every page of a read whose description declares a walk, writes each record as one NDJSON line and prints a summary (story:invoke-walks-declared-pages). The walk is the one the operation's input_schema carries under x-connectors-walk, as invoke --describe prints it (docs/local-catalog-provider.md): the query parameter that carries the position, the pointer to the next position, the pointer to the records, query values every page sends, first_only parameters only the first page sends, an optional continues condition and the named ends.

The loop runs in the CLI. Each page is one invoke of the same instance, connection, schema and revision with the page's input: the caller's document with the declared query values and, from the second page on, the position the page before named under the declared parameter, without the first_only parameters. Each page is admitted, audited and bounded as a plain invoke is: one audit record and one response bound per page. --walk, --max-pages and --out never reach the owner or the provider. The records of a page are the array at the declared records pointer of its result (for a catalog result, of its body); absent or null is none. --fields shapes each record as it shapes an element of --each. After each page the declared walk decides: while continues holds the walk goes on; otherwise the first end that holds ends it, by its name; otherwise it goes on to the next position.

--out <file> writes the records to a new file, created owner-only (0600) and never over an existing path; --out - writes them to standard output. The summary, InvokeWalked, is printed last on standard output, in the envelope of the output mode:

$ connectors --output json invoke zendesk/support ticket.audits --walk --out audits.ndjson --fields author_id,created_at,events[].field_name <<<'{"ticket_id":101,"page[size]":2}'
{"ok":true,"result":{"complete":true,"end":"has_more_false","out":"audits.ndjson","pages":2,"records":3}}

pages and records count the pages read whose records were written and those records. complete is true exactly when a declared end held, and end names it. missing lists the --fields paths no record had, when there was a record. A walk stops before an end holds, and complete is false:

stoppedwhenresume
max_pages--max-pages pages were read (default 20, at most 10,000) and the last named a next positionthe next page's input
refuseda page's invoke was refused, rate_limited included; error is its failure envelope's errorthe refused page's input
statusa page's catalog result has a status, in status, that is not 2xx; its records are not writtenthat page's input
writea page's records could not be written whole; none of them is counted, and the --out file is cut back to where the page beganthat page's input
continues_without_position, no_position, invalid_position, repeated_position, records_not_a_listthe declared walk cannot go on from the page: no next position, one that is neither a string of at most 16 KiB nor an integer, or one already sentabsent

Records already written stay written. Standard output cannot be taken back, so a reader of --out - may see part of a page that stopped the walk as write. resume is a document: given back as the input of invoke --walk it reads on from that page, and the position it carries counts as sent, so a provider that names it again stops the walk as repeated_position. Positions are compared by the query value they send: "5" and 5 are one position. missing counts only records that were written. A walk that stopped exits 0 when its summary is printed. A refused first page is the failure envelope a plain invoke answers, on standard error with its exit status, and the walk leaves no --out file behind; so is a caller document a plain invoke refuses, such as one naming a key twice, before any page is read.

Refused before any provider call and before the --out file is created, as invalid_input at arguments, exit 2: --walk without --out (naming out); --max-pages that is not a whole number from 1 to 10,000 (naming max_pages); --max-pages or --out without --walk (naming each); --each beside --walk (naming each: records are already one per line); --walk beside --describe, and --walk on a write, ahead of approval_required (naming walk). An --out path that exists or cannot be created is invalid_input naming out, before any call. An operation whose description declares no walk is unsupported at admission naming walk.

Renamed commands​

connections reconnect replaces connections repair, and the instances group replaces adapters: instances list, instances describe, instances status and instances stop. The commands address an instance; the process they observe and stop is still that instance's adapter. Each old path stays an alias of its new one for the deprecation window: the same arguments run the same command against the same instance with the same result. These are the binding's only path aliases.

An old path says it is deprecated exactly once per command, as --adapter does. Under --output human that is one line on standard error, ahead of any other deprecation line and any failure text:

deprecated: connections repair; use connections reconnect

Under --output json the one envelope's deprecations member carries the entry, ahead of an --adapter entry when both apply:

{"ok":true,"result":{},"deprecations":[{"command":"connections repair","replacement":["connections reconnect"]}]}

The machine-facing next_action value repair_connection is unchanged; it names connections reconnect.

One connection per instance​

An instance holds at most one published non-revoked connection (connection semantics §4.2). connections connect is refused while the selected instance holds a published Live or Stale connection, before any credential entry or provider exchange, as lifecycle_conflict at admission. Its next_action is the step the held connection's own reconnect admits:

The instance holdsnext_action
one connection under the configured binding, or one whose binding differs from it only on the operation side (bundle, source or selection, under an equal credential-side digest)repair_connection: connections reconnect replaces its credential and keeps its reference
one connection saved under an older configuration revision that a revalidation can followrevalidate_connection, after which it is the instance's connection
one connection that reconnect refuses as changed (create_connection on a read), or more than onerevoke_connection: connections revoke it, then connect

A connection with another identity (provider authority or profile authentication) keeps check_configuration, as before. The refusal is the same under every custody binding: under the read-only mounted_secret binding the second connect of an instance is this lifecycle_conflict, not custody_read_only. A connect is also refused while another connect of the instance is in flight, as lifecycle_conflict at admission with next_action = retry_status, and the failure data's acquisition names that connect's acquisition, which connections status --acquisition follows until it ends; it is absent when that connect ended before the refusal was read. Two concurrent connects therefore never publish two connections. Revoked connections stay as history and do not count, and neither does a connection a connect allocated and never published: once that connect failed or its acquisition passed its deadline, the next connect is admitted.

A second account is a second instance. connections reconnect keeps the connection's external identity, so a reconnect whose credential the provider answers as another account is identity_mismatch at admission with next_action = revoke_connection, never repair_connection: connect names the reconnect, and reconnecting again with the other account answers the same. Revoking the connection lets the instance connect the other account; keeping it, the other account is a connection of another instance.

An instance that held more than one published non-revoked connection before this rule keeps every one of them, references unchanged, and each stays usable, reconnectable and revocable. A new connect on it is refused with revoke_connection until all but one are revoked.

connections describe, reconnect, revalidate, launch and revoke take --connection optionally. Without it they select the instance's one published non-revoked connection; --expected-revision, where the command takes it, is still required. An instance that holds none refuses as not_found at admission with next_action = create_connection; one that holds more than one refuses as invalid_input at arguments, and --connection chooses. connections status keeps requiring --connection or --acquisition. Under --output json each of these refusals is the one failure envelope on standard error.

Listing connectors​

list answers which connectors this build can serve. It has one row per connector whose declaration the build ships (connectors.declarations.Connector, compiled into the binary from the declarations the generic HTTP implementation releases with it), in connector id order: its id, the auth profile ids it declares in profiles, in declaration order, and in instances the names of the instances the configuration lists under [connectors.<id>.instances], in name order. Connectors served by their own executables ship no declaration in the binary and have no row. No row names an implementation.

It needs no configuration and no state directory: only the configuration path is resolved, and with no file at that path, or no resolvable path, every row's instances is empty and the command succeeds. A file that exists is read and checked as every other command reads it (owner-only, at most 1 MiB, the closed connectors-local envelope, its format, its names and selectors, its pins and aliases), and a file refused there is refused as for every other command, naming its rule. The one difference is that an unpinned instance is not derived: it is checked as a derivation checks it before the catalog provider starts and then listed, so nothing is started, and what only the provider answers (whether it ships the connector, which profiles it declares, and so whether the instance gets an entry that counts toward the 64-entry limit and that a consumer, default_adapter or custody target may name) is not checked: such a name is accepted. list never refuses a file every other command accepts. Legacy [adapters.<alias>] entries are not connector instances and are not listed; instances list lists every configured entry but a legacy catalog entry no connector is derived for. Under --output human the rows are a table with the columns CONNECTOR, PROFILES and INSTANCES, - where a row has none; under --output json they are the result envelope.

Installing a definitions release​

definitions install --source <source> --tag <tag> fetches one connectors-definitions release, checks every file of it against the release's index.json, keeps the files in a read-only cache and records the release in a lock file. The model is connectors.definitions_install (ESS); the index format is the one the connectors-definitions repository publishes beside each tag from 0.3.0 on. Both arguments are required; there is no default source, and nothing updates implicitly.

  • --source is an https repository URL, from which the command reads <source>/releases/download/<tag>/index.json and the asset it names, or a local directory holding <tag>/index.json and that asset beside it. Any other scheme, and a URL with credentials, a query or a fragment, is invalid_input. An HTTPS request follows at most five redirects, each to https, uses no proxy, and reads at most 4 MiB of index.json and 64 MiB of asset.
  • --tag is 1 to 128 bytes of [A-Za-z0-9._-], not . or ..; anything else is invalid_input.

The command reads the configuration as every command does, so setup init comes first. It starts no local host or adapter, reads no credential, and grants nothing: no connection, no operation admission, no OAuth exchange and no write authority. No command of this binding reads the lock yet, and the cache is not <state>/definitions/, the invoked definitions' cache, so an install changes what no instance resolves to.

It checks, in this order, before it writes anything:

  1. index.json's schema is a format version this build reads (urn:beyond10x:connectors-definitions-index:1), else unknown_format_version.
  2. The rest of the index is that format, with no other member: tag equals --tag, commit is 40 or 64 and every digest 64 lower-case hexadecimal digits, the asset's name is one path segment, every file path is relative with no empty, . or .. segment, no segment over 255 bytes and no backslash, no file path is another's directory, the files are sorted by path in byte order with no path twice, no two paths differ only in case (a case-insensitive file system holds them as one), at most 16,384 files and 512 MiB in all, and every feature is 1 to 64 bytes of [a-z0-9_], sorted in byte order with none twice. Else release_invalid.
  3. Every name in features is a feature this build supports (embedded_text, query_fixed, query_folds, rate_limit_headers), else unmet_feature, with feature naming the first unsupported one. These are exactly the members the definition lowerer reads beyond the base format.
  4. The asset's SHA-256 equals the index's asset.sha256, else digest_mismatch with file naming the asset.
  5. The asset is a gzip tar archive whose entries all lie under one top directory. Each regular file under it, that directory removed, is a path the index names, once, with the index's size and SHA-256: another size or digest is digest_mismatch with file naming the path. A file the index does not name, a file it names that the archive lacks, and an entry that is neither a regular file nor a directory are release_invalid, as is an asset that inflates past the files the index names, each padded to a 512-byte block, plus 8 KiB of header room per file and 1 MiB in all.

These are the only things it takes from its own build: the format versions and the features it supports. A release whose tag, commit and connectors this binary was never built with installs without a rebuild.

Only then does it write. The cache entry is <state>/installed-definitions/<index sha256>/, holding index.json (the bytes whose SHA-256 names the entry) and files/<path> for every file the index names. It is written under a temporary name in the same directory and renamed into place whole, its files mode 0444 and its directories 0555. An entry that already exists for that digest is not replaced: it must be a directory, not a link, holding exactly index.json and the files the index names, each with the index's size and SHA-256, its files mode 0444 and its directories 0555; any difference is digest_mismatch naming the file relative to the release, index.json, or the entry itself. The lock is definitions.lock in the configuration file's directory, owner-only, written to a temporary file and renamed over the previous one:

{"format":"connectors-definitions-lock/1","source":"<source>","tag":"<tag>","commit":"<commit>","index_sha256":"<sha256>"}

source is the URL without a trailing /, or the directory's canonical path. A refused command leaves the lock and every cache entry as they were. The result names source, tag, commit, index_sha256, features, the number of files, lock_path, cache_path and cache_entry (created, or existing for an entry that was already there). Several entries may coexist; the lock names one. No command removes an entry.

ConditionFailure.codestagenext_actionExit
a --source or --tag this section refusesinvalid_inputargumentsnone2
a release file the source does not have: no directory, file or HTTP 404not_founddispatchnone1
a source that does not answer, another HTTP status, a redirect it does not follow, or a file over its boundunavailabledispatchretry_explicitly1
an index format version this build does not readunknown_format_versionadmissionupgrade1
a release requiring a feature this build does not supportunmet_featureadmissionupgrade1
an index or asset that is not what the index format statesrelease_invalidadmissionnone1
an asset or file whose SHA-256 or size differs from the indexdigest_mismatchadmissionnone1
the cache or the lock could not be written; nothing was renamed into placeunavailablepublicationretry_explicitly1

Migrating adapter entries​

setup migrate --mode dry-run and setup migrate --mode apply move legacy [adapters.<alias>] entries to connector instances without a new consent, revalidation or approval policy publication. --dry-run and --apply are the same two commands. Each entry whose implementation maps to a shipped connector declaration becomes [connectors.<connector>.instances.<alias>]. The instance name is the alias, so the instance is selected as --connector <connector> --instance <alias>, and --adapter <alias> keeps selecting it for the deprecation window. Its instance_id, configuration_revision and mounted custody item names <alias>/<profile> stay byte for byte, so its connections, approval policy and custody items stay valid. Platform custody items are keyed by connection, not by configuration names. Read and write instance pairs are not merged: merging changes scopes and so the revision.

The instance is written in the short form (form short) when the installed catalog provider derives the entry from it unchanged: the table states alias, the entry's instance_id, and endpoint and profile only where they differ from the declaration's default endpoint and single profile, and the provider, started with --instance-id <instance_id>, derives the entry's instance_id, configuration_revision, credential_revision (stated or not), permissions and startup, with the entry's executable digest when the entry states a credential_revision. The short form selects another executable argument vector, so it is not written for an instance whose approval policy is published, nor for an entry whose alias or executable selection the operation curation <config>.operation-curation.json names, nor for any entry while that file is present but unreadable: their executable_selection would no longer match. Otherwise the instance carries every pin of the entry, its alias included (form pinned), and its executable selection digest stays byte for byte too; pinned_field names why (connectors.cli.MigrateField). For a derived configuration revision other than the entry's it is the first key of the provider's revision digest input in which the two differ (auth for other scopes, api_base for another API base), printed per key by the provider's --print-revision-inputs, or configuration_revision when no key accounts for it or the provider derives nothing; executable for a published approval policy or an operation curation; otherwise the differing credential_revision, permissions, startup or executable.

Only a catalog entry maps to a connector today: the connector whose shipped declaration names the provider of the entry's native configuration as its native_provider (the declaration file's catalog_provider key) (--local-config <file>, admitted as the provider admits it: owner-only, at most 1 MiB), or the shipped connector its --connector argument names. Every other entry stays where it is, and the result names why in reason (connectors.cli.MigrateKeptReason):

reasonThe entry
implementation_unmappedis not a catalog entry: no declaration maps a credential binary, MCP, SQL or Kubernetes implementation to a connector yet
connector_undeclarednames a native provider no shipped declaration carries (OpenRouter ships none)
gateway_prefixstates a request prefix its connector's declaration does not produce: an API gateway in front of the provider's API, such as the Atlassian gateway
native_configuration_unreadablenames no native configuration, or one that is not owner-only, not readable JSON or names no provider
instance_conflictmaps to a connector configured disabled, or one that already has an instance named like the alias
selection_changeswould read back with another value of the field differing_field names

The new file is read back with the reading rules above before anything is written, its 1 MiB bound included, short-form instances derived by the installed provider: a rewritten file larger than that is refused in both modes as invalid_configuration naming config_too_large, and nothing is written. A short-form instance that reads back with another instance_id, configuration_revision, credential_revision, permissions or startup is written pinned instead, naming that field in pinned_field. A pinned entry that reads back with any other value of any of its fields is not migrated, naming the first such field. So is one whose new instance table reads back differently (instance, or the first of description, endpoint and profile) or whose connector's pre-existing table does (enabled, or an existing instance's description, endpoint or profile, which keeps every entry moving to that connector); a format that reads back other than connectors-local/5 keeps every entry. Moving to connectors-local/5 requires private_protocol on every entry, and it enters the executable selection digest, so no entry of a connectors-local/1 file is migrated; each candidate names private_protocol. Nothing else of the file changes, but its comments are not kept.

The result lists every [adapters.<alias>] entry of the file it read with its disposition (migrated or kept), the connector and instance that select it afterwards, a migrated entry's form and, before and after, its instance_id, configuration_revision, executable_selection and custody_items; a short-form entry's after carries the derived executable's selection. It carries the file in configuration and its format_before and format_after. The format moves to connectors-local/5 only when at least one entry is migrated, or when the file holds no legacy entry at all: then only its format value changes and every other byte, comments included, is kept (story:catalog-provider-failure-is-named). With nothing to migrate the result is unchanged and nothing is written in either mode. A dry run that would migrate is planned and writes nothing. An applied one is applied: it publishes the file it read, byte for byte and owner-only, as config.toml.before-migrate-<UTC timestamp> (YYYYMMDDTHHMMSSZ) beside the configuration without replacing any existing file, names it in backup_path, and then replaces the configuration atomically and owner-only, only while it still holds the bytes the plan was made from; a configuration that changed in between is revision_conflict at publication. Rollback is restoring the backup and running the previous release. Under --output json a refusal is one failure envelope on standard error, like every other command's.

Durable open checkpoints on an existing store​

setup checkpoints-enable --confirm one-way enables Entity Runtime durable open checkpoints on the metadata database in the selected state directory. It is the only way an existing store gets them: no other command, open or upgrade enables them. Enabling installs Eventlog's triggers and continuity tables, and is one-way for older releases: Entity Runtime 0.28.0 and earlier, and so connectors 0.32.0 and earlier, refuse to open the store afterwards. This binding has no command that removes them. Every SetupCheckpointsEnableResult states this: change = one-way and newest_incompatible_release = "0.32.0". The command starts no local host or adapter and runs in this order:

  1. Without --confirm one-way it refuses confirmation_required (kind = usage, exit 2, stage = arguments, next_action = retry_explicitly) before it takes a lock or opens the store, and changes nothing. --confirm admits only the value one-way; any other value is the presentation's cli_input. ess-cli/1 has no valueless switch, so the confirmation carries its value.
  2. It takes the owner lifetime lock of local owner transport without waiting and holds it until it returns, so no owner starts meanwhile. A lock another process holds, a running owner for this state directory, refuses lifecycle_conflict (stage = admission, next_action = stop_owner) with the store unchanged. The command never signals the owner; the user stops it with connectors setup owner-stop, as for owner_build_mismatch.
  3. It opens the store as an admitted mutating open under metadata.lock, within the usual 30-second wait. As every mutating open does, it first migrates a level 1–8 database to level 9 (local metadata). It never creates a database. A missing, unreadable, foreign-owned, unrecognised or integrity-refused store, a metadata.lock not released within the wait, and Eventlog's refusal to enable (a trigger or continuity table the provider did not create) are metadata_unavailable (stage = observation, next_action = retry_status), with the store unchanged.
  4. A store that already carries checkpoints answers disposition = already_enabled, a success with exit 0, and changes nothing. A second confirmed run answers it, and so does a run on a store setup init created.
  5. Otherwise it enables them in one transaction, persists the open checkpoint at the head it verified, so the next open starts from that checkpoint, and answers disposition = enabled. A lost acknowledgement is outcome_unknown (stage = publication) with next_action = retry_explicitly: enabling an enabled store changes nothing, so running the command again answers already_enabled or enables.

The command sees only running owners. A second installed connectors binary or another tool built on Entity Runtime 0.28.0 or earlier that is not running cannot be detected; it fails to open the store once enabled.

Which open verifies what: the provider's tracked open of a store with checkpoints verifies the persisted checkpoint and the suffix after it, and on its own does not detect raw edits of the database file that bypass SQLite while no handle is open (Entity Runtime 0.29.0). Every fresh open the host makes, in a command run directly, a second process, the owner when it starts and recovery alike, is that tracked open followed by a complete read of the store before it answers anything; Entity Runtime 0.29.0 treats a complete read as a complete verification, and no open uses the separate FullVerification policy. So it verifies the whole store, so such an edit is refused as metadata_unavailable by the first command or owner start after it. Only a handle a process already verified and keeps for later opens in that process reads just what was appended since; the owner keeps its handles for its whole run. A command run while an owner of its own build runs reads the store through that owner (its cached request and the owner's admission of invoke, local owner transport) and does not open it; without such an owner it opens the store directly and verifies it completely, except a read on invoke, which starts the owner first and reads through it, its connection listing included, so a cold read verifies the store once, in the owner (local owner transport). A write through any SQLite connection is proven or makes the next read verify completely, as before. Due expiries of transient subjects are recorded in batches of at most 128 before the batch carrying the command's own change; a refused expiry batch leaves earlier ones committed and the command's change unapplied, with its failure unchanged.

Store writer marker and binaries of different versions​

The metadata store records its newest writer in one StoreWriter record (identity store): the release version of the newest binary that wrote it and the digest of the record model that binary persists. The digest is a SHA-256 over the persisted record shapes of the Entity Runtime definitions the store's records are checked against: for each entity its name, identity field, lifecycle states, field schema (names, types, required-ness, bounds and enumerations) and the assertions of its invariants. Invariant names and messages, which the generator chooses, are not part of it, so two releases that persist the same record shapes have the same digest even when the generator that lowered them changed. A release version is a SemVer 2.0.0 version, ordered by SemVer precedence: a pre-release orders below its release, and build metadata is ignored. A binary records the marker in the batch of its first write when the store has none, and raises it in the batch of its first write when its own release version is newer; an equal or older binary leaves it as it is, so the marker is never lowered. A store written before the marker existed has none, is admitted, and gets one on its next write.

Every command that opens the metadata store, setup check included, compares the marker with its own release version and model digest after it opens the store and before it reads a record. When the digests differ and the marker's version is newer than the binary's, the command refuses metadata_schema_newer (kind = operational, exit 1, stage = admission, next_action = upgrade), and the Failure names store_version (the marker's version) and binary_version (its own); it reads and writes nothing. The same digest under a newer version, a release that changed no persisted record, is admitted. A marker whose version this binary cannot order (not a SemVer version, or a pre-release longer than 40 bytes) under a different digest is refused the same way with store_version = "unrecorded"; under the same digest it is admitted and kept, since it is never known to be lower. A store whose SQL level (user_version) is above the highest this binary knows was migrated by a newer binary and is refused the same way with store_version = "unrecorded", before any record is replayed: its marker is not read through a schema this binary does not know. A store that fails integrity verification is not a newer store and stays metadata_unavailable. A command an owner of its own build answers reports the owner's refusal the same way. Independently of the marker, a write the Entity Runtime refuses because a record carries a field this binary's model does not declare is metadata_schema_newer too, with store_version = "unrecorded" unless a newer marker names the writer; it is never metadata_unavailable or retry_status, because running the same binary again cannot change the record. None of these refusals changes the store.

So once a binary has written a state directory, an older binary may not use it: binaries of different versions share one state directory only while the oldest of them is at least the newest that wrote it. Releases before the marker cannot read it and answer such a store as they always did, with metadata_unavailable (next_action = retry_status), which retrying does not change. A consumer pinned to a version keeps its own state directory with --state-dir, initialises it with setup init, and connects there; connections, cursors, approvals and cached descriptions recorded in another state directory are not visible in it.

4. Lifecycle and bounded startup​

The executable owner binding is specified in local owner transport. Each configured adapter has an optional permissions table with profiles and operations lists of exact reviewed identifiers (at most 64 and 256 respectively). Omission means an empty allowlist, never all discovered capabilities. These select local-owner permission; profile/grant evidence, operation effects and native target allowlists remain additional checks. The first binding admits reads only.

Optional top-level secret_service_socket selects an absolute, owner-controlled local Unix socket instead of /run/user/<uid>/bus. It is an explicit infrastructure binding, not a credential locator or business input. Parent directories, socket ownership and kernel peer UID must be admitted; TCP and environment bus addresses are never alternatives. The exact native service, encrypted storage and durable acknowledgement qualification remain mandatory on either transport. This permits dedicated local sessions and disposable acceptance infrastructure without weakening credential custody or selecting another collection after a failure.

automatic means start when this local host starts. Pure list, configuration inspection and status never start that host or its adapters. After local policy and selected action admission, connect/repair/revalidate/invoke may start the host; its automatic set and the selected on-demand entry become launch candidates. Provider dispatch remains fenced on actual readiness and current connection admission. Preflight admission is not a reusable grant across startup.

The host coalesces simultaneous startup by configured instance identity and exact configuration revision. One process incarnation owns that launch; other callers wait for the same result within their own original deadline. They do not spawn duplicates, reset budgets or inherit another caller's authority. At most one configured incarnation is live; changing artifact/configuration while one is active refuses lifecycle_conflict until exact stop/drain completes.

Readiness requires descriptor/bootstrap agreement with the configured instance, adapter identity, protocol and expected configuration revision. A mismatch is readiness_mismatch, never ready. The supervisor terminates only the child it just launched, records a safe failure and isolates it. Failed automatic entry A does not prevent independent ready entry B from serving. A request for A receives A's failure; it cannot silently choose B. Failure of the host itself fails all waiting requests without a second competing host launch.

Stop requires configured alias, current host incarnation and current child incarnation, plus the expected configuration revision. The host compares all coordinates atomically against its owned process handle; a stale target returns incarnation_mismatch without signaling anything. Linux pidfd or equivalent non-reusable kernel process ownership is required; a PID/path from caller input or persisted cache alone is not sufficient. Graceful drain is bounded, then the owner may terminate only that exact owned child and its registered owned children. A remote, foreign or already-replaced process cannot be killed by this command.

An explicit stop sets a durable suppression marker for the configured instance in the existing configuration owner's state; neither a later automatic sweep, crash nor host restart clears it. An admitted later connect/repair/revalidate/invoke directed explicitly at that entry is the selected resume action, clears suppression under current authority and requests one fresh launch. An unrelated command never resumes it. restart = "never" also forbids background crash-restart loops; observed unexpected termination is failed, and a later explicit admitted request may attempt one new launch. Config changes do not silently clear stop suppression. The suppression marker is the suppressed field of the configuration owner's local_runtime_instances row, modeled as connectors.cli.LocalRuntimeRecord (ess/domains/cli.yaml, mapped in docs/local-er-metadata.md); the resume and restart rules above remain owner obligations.

Bounds for this first binding are explicit, selected limits, not measured runtime performance: 64 configured entries; four concurrent launches; one live launch per instance; 10 seconds for readiness and 5 seconds for graceful stop; 1 MiB TOML; 100 default/500 maximum list items per page; 128 UTF-8 bytes per selector; 4 KiB per path; 64 KiB protected credential document; 1 MiB business input; 8 MiB result; JSON depth 64; 30-second default/120-second maximum business deadline. Provider and descriptor limits may only lower these bounds. List cursors are opaque, admitted, bound to the selection/revision and expire after 300 seconds; stale cursors refuse. No automatic paging or unbounded retry occurs. Entry capture expires after 300 seconds. OAuth continuations/registration and browser callback ingress remain separately specified; they are not secretly implemented by this terminal profile.

Why a start failed​

A start that fails keeps its failure code and stage (unavailable or readiness_mismatch at readiness, invalid_configuration); instances status of the failed instance adds why (story:program-start-failure-reason). start_failure is one of admission (the executable's path was refused), digest (its content is not the pinned executable), copy (the owner's private copy of it could not be made or launched), spawn (the process, its channel or its standard-error pipe could not be created), exited_before_handshake (with start_exit_code or start_signal), handshake_deadline or handshake_mismatch. On Linux and macOS the program's standard error goes to a pipe the owner holds until the handshake has ended, and start_log names the owner-only (0600) file under the state directory, start-logs/<instance id>.log, that holds at most 64 KiB of it, replaced on each start. The owner then closes its end and the runtime points the program's standard error at /dev/null: nothing the program writes once it can hold a credential is kept. Windows starts a program with no standard handles and keeps no start log.

5. Protected entry and durable publication​

The Linux OS keyring binding selects the user's admitted Secret Service collection through authenticated local session transport. It must satisfy custody write_new/read/delete with immutable version identity, scoped reads and definite durable acknowledgement. A transient session cache or a successful method return without that durability guarantee is insufficient. Collection lock/absence/write failure returns custody_unavailable; plaintext TOML, environment variables or ordinary local files are never fallback custody. The one exception is not a fallback: the read-only mounted_secret binding, used only when the owner configuration explicitly selects it (custody §5.2), reads files a platform mounted read-only and writes nothing; an unusable platform binding still answers custody_unavailable. Keyring locations remain private inside the binding. Secret Service's actual durability/ownership behavior remains a required backend conformance check before this binding is advertised.

Exactly one protected entry source is selected for connect/repair: hidden terminal, protected file or stdin, each explicitly selected by its own source flag. Under the read-only mounted_secret binding connect and repair take no source: the owner reads the item <adapter alias>/<profile> itself, and a source flag is invalid_input at arguments. Under a platform binding a missing source is protected_entry_unavailable with next_action = select_protected_source, except on an oauth2_grant profile of a public client, whose grant runs in the CLI inside the capture window and whose token response is the entry (contracts/auth/acquisition/v1alpha1/semantics.md, the oauth2_grant row); a confidential client's source is the document {"client_secret": "…"}, from which its grant runs the same way. The consent address goes to the controlling terminal, else to one standard error line connectors: consent-url <address>. --device (--device true; a bare --device reads as --device true) runs the device grant instead, RFC 8628 at the declaration's device_authorization_url unless the composition registers a device dialect for the profile id, and shows connectors: device-code <user_code> <verification_uri> the same way; on any other profile, or under the read-only binding, --device is invalid_input (such a profile given no source under a platform binding is protected_entry_unavailable first). A grant that fails, times out or is cancelled stores nothing; a missing required scope is not_granted. Hidden terminal requires a trusted controlling TTY; unavailable or conflicting sources refuse as a usage/source error and explain safe source selection. No password/token argv flag, environment source, generic business input, or automatic read from a different descriptor is accepted. These inputs select a reviewed static-entry profile; unsupported managed flows refuse before acquiring a usable flow. static_config uses its existing configuration activation path, never an invented auth.begin exchange.

Terminal capture uses the controlling terminal, verifies the local owner's session, disables echo before reading, restores settings on success/failure/interruption, and bounds capture before allocation. Prompts go to that protected terminal, never JSON stdout/stderr. File capture opens an absolute regular file without following symlinks in any component, verifies owner UID, single link and no group/ other permission bits on the opened descriptor, and enforces safe parent traversal. The source is read once from that descriptor, bounded before parsing; it is not reopened after verification. Read-only 0400 and read/write 0600 are permitted.

Protected stdin accepts either such an already-open protected regular file or an inherited anonymous pipe held within the admitted local UID/session boundary. Sockets, named FIFOs, terminals on the stdin mode, unknown ownership, unexpected descriptor types and broad permissions refuse. A pipe's writer must be supplied deliberately by that trusted launcher/session; the CLI does not claim fstat alone authenticates arbitrary producer behavior. Same-UID debugger/ptrace compromise is outside this process-isolation promise. Named file/stdin credentials use one UTF-8 JSON document with exactly the reviewed profile's fields; duplicate keys, trailing documents, invalid UTF-8, depth/size excess and unknown fields refuse. A final JSON whitespace newline is allowed; secret string contents are never trimmed. Terminal prompts construct the same protected profile document privately. The CLI input model carries bytes only in the explicitly ephemeral local protected-action field; its safe management projection omits that field. Protected bytes bypass ordinary recording evidence, public serialization, tracing, telemetry, history and error reports. Fictional sentinel capture in a bounded parser test is test evidence only.

Business file/stdin JSON is a different channel. If both channels claim stdin in one invocation, source admission fails before either is consumed. File paths, parse snippets and secret sentinel values must be absent from every ordinary success/failure/progress/help output, including early parser failures. Clear transient buffers promptly; do not promise impossible erasure of external keyring, OS or caller buffers.

Success requires, in order: current management admission and exact target; private profile validation and baseline external identity evidence; a definite durable keyring write of an immutable candidate; current identity/revision/non-revocation checks; atomic durable metadata publication linking that exact stored version; then a safe successful connection reference. An unknown keyring write cannot become acknowledged custody. A crash after custody and before publication leaves an unpublished candidate for guarded cleanup/reconciliation, never a usable connection. An unknown metadata commit yields outcome_unknown; status resolves the existing acquisition/connection under the original owner without recapturing or repeating a potentially consumed exchange. The shared acquisition/refresh fencing and retention rules remain authoritative.

Once published, a still-valid generation is reusable after both CLI and local owner restart without re-entry, hosted login or an unnecessary provider exchange. The restarted owner loads authoritative metadata and reads the exact retained custody version, then checks current expiry/revocation/evidence at invocation. Missing material or a locked collection is an explicit failure, never another generation or environment credential. Persisted metadata without valid custody is not a successful connection.

connections reconnect binds the same connection identity, immutable profile/owner/target and expected revision. Same-principal/same-target credential renewal may publish a new private generation and advance its private publication fence while preserving both connection reference and public semantic revision. Only separately admitted effect-relevant configuration/management changes advance that semantic revision under the shared fixed-binding rules; material renewal or evidence recollection alone does not. Different principal, account or provider target returns identity_mismatch with next_action = revoke_connection: another account is another connection, which the instance holds only once its own is revoked (one connection per instance). A failed or uncertain candidate repair does not replace or globally invalidate a still-valid active generation. Expired/revoked old generations do not become valid merely because repair failed.

Local revoke is terminal. It serializes with publication and final dispatch, definitely commits the cutoff before reporting success and survives CLI/owner restart, configuration reload and static reactivation. Pending repairs cannot publish across it. The returned provider outcome is not_requested; this first binding exposes no provider-revoke flag. Keyring deletion is separately guarded retirement, not a prerequisite for local cutoff and not implied by it. Repeated revoke observes the terminal record without repeating provider work. Previously dispatched effects are not undone. Lost acknowledgement remains unknown until an authoritative read; no observation of missing sockets permits resurrection.

6. Dynamic operation input and safe failure mapping​

InvokeInput.input is a String carrying one bounded UTF-8 JSON document. It is the exception needed for runtime-selected provider schemas, not a universal input/result property bag. A file/stdin presentation binding reads text once; the provider JSON decoder then rejects malformed/duplicate/trailing input and validates the decoded JSON value against the exact selected operation schema. Carrier validation and provider-schema validation establish separate facts. When serializing the service request, input is that decoded JSON value, never the quoted carrier string or a double-encoded document. Answers are not carriers: InvokeResult.result is the provider result as a JSON value, validated against the selected operation's output schema, and OperationDescription answers input_schema and output_schema as JSON schema objects in operations describe, in the instances describe descriptor and in invoke --describe. A caller decodes the answer once; none of these fields is a string holding JSON. The schema digest (schema), not the schema document, remains the identity an invocation selects. ServiceDescriptor.configuration_schema remains a JSON text carrier. Structured command metadata uses specific types, not a catch-all string.

Invocation selects adapter, operation, exact schema identity (schema), description revision (revision) and explicit connection where required. Refreshed readiness/descriptor information that does not match the selected revision returns stale_description before provider dispatch; it never silently substitutes a new schema or retries the request. Operation absence, malformed field/type, missing permission and unreachable service remain separate errors. An operation id the adapter does not expose answers not_found before any grant check, so absence is never reported as forbidden. Bound input before parsing, validate current authority immediately before dispatch, preserve shared mutation uncertainty and never replay a possible write after interruption, timeout or lost response.

ConditionFailure.codeExit
decoded business document the operation schema, depth or byte bound refuses; approvals prepare or approvals issue business document that does not decode or has a trailing document; unreadable business document file; argument value the handler refuses; on an approval command, field names the argument (below)invalid_input2
TOML syntax, unsupported startup/restart, duplicate identity, bad permissionsinvalid_configuration2
configuration already exists (setup init: with a metadata database, or one that does not load)configuration_exists1
no safe entry channel (in place of the presentation's cli_source)protected_entry_unavailable2
setup checkpoints-enable without --confirm one-way; nothing changed (stage = arguments, next_action = retry_explicitly)confirmation_required2
keyring unavailable or durable storage not acknowledgedcustody_unavailable1
a read-only custody binding refused a write it would need (a refresh-rotating profile, an approval-signing key; stage = custody, next_action = check_configuration)custody_read_only1
the read-only binding's item no longer holds the bytes its version pinned (stage = custody, next_action = revalidate_connection)custody_unavailable1
metadata authority unavailablemetadata_unavailable1
store written by a newer binary whose record model differs (its marker names a newer or an unorderable version), a store whose SQL level is above this binary's highest, or a store record carrying a field this binary's model does not declare; nothing read or written (stage = admission, next_action = upgrade, store_version and binary_version named)metadata_schema_newer1
a non-guarded metadata write the store changed under (a concurrent commit); nothing was written (stage = publication, next_action = retry_explicitly). A guarded write keeps the guarded-write rule below: its mutation record, next_action = retry_statusrevision_conflict1
metadata publication acknowledgement uncertainoutcome_unknown1
changed principal/account/target during repair (next_action = revoke_connection, one connection per instance)identity_mismatch1
stale revision, occupied changed configuration; setup checkpoints-enable while an owner runs for the state directory (stage = admission, next_action = stop_owner: run connectors setup owner-stop); connections connect on an instance that holds its connection, or while another connect of it is in flight (one connection per instance)lifecycle_conflict1
stale/foreign stop targetincarnation_mismatch1
wrong readiness identity/version/revisionreadiness_mismatch1
running owner is a different executable build (next_action = stop_owner: run connectors setup owner-stop)owner_build_mismatch1
no selected cached descriptiondescription_unavailable1
schema/descriptor changedstale_description1
absent selected operation/connectionnot_found1
invoke target naming a connector with several instances and no instance (stage = arguments, next_action = none, instances named; invoking an operation)instance_ambiguous2
invoke without --connection on an instance with no usable connection, several ready, or several usable and none ready (stage = admission, connections named; invoking an operation)connection_unresolved1
invoke write without --approval-file (stage = admission, next_action = none, approval_steps named)approval_required1
stale, expired or foreign list cursor (stage = observation, next_action = retry_explicitly)stale_cursor1
connect whose acquisition failed before publication (stage = dispatch, next_action = retry_explicitly)the acquisition owner's code1
known revoked connectionrevoked1
current permission missingforbidden (or shared not_granted)1
unavailable service / budget expiredunavailable / timeout1
setup owner-stop: state directory, owner socket or lock file of another user (stage = stop, next_action = check_configuration)forbidden1
setup owner-stop: the lifetime lock is held and no owner answers within the bound; nothing is signalled (stage = stop, next_action = retry_explicitly)unavailable1
setup owner-stop: the owner accepted the request and still holds the lifetime lock at the bound (stage = stop, next_action = retry_explicitly)timeout1
definitions install: an index format version this build does not read; nothing written (stage = admission, next_action = upgrade, installing a definitions release)unknown_format_version1
definitions install: a release requiring a feature this build does not support, named in feature; nothing written (stage = admission, next_action = upgrade)unmet_feature1
definitions install: a release asset or file whose SHA-256 or size differs from index.json, named in file; nothing written (stage = admission, next_action = none)digest_mismatch1
definitions install: an index or asset that is not what the index format states; nothing written (stage = admission, next_action = none)release_invalid1
user interruptioninterrupted130

A configuration whose adapter entry contradicts its format — a connectors-local/1 entry that carries private_protocol, or a connectors-local/2 or /3 entry that lacks it — is invalid_configuration with next_action = check_configuration, and its Failure additionally carries configuration_format (the file's format value) and instance_id (the entry's instance_id). They are named only after every entry has passed every other check, so the refusal never depends on alias order: the first mismatched entry in alias order is named, and a file with any other defect names nothing. configuration_format is the closed enum ConfigurationFormat; instance_id is an admitted selector (at most 128 bytes of [A-Za-z0-9._-]). Every CLI command except setup init refuses such a file this way, before acquiring any protected source or dispatching. The selector shape and the rule that both fields appear only on invalid_configuration are host obligations: ESS 0.40 declares no invariant on a CLI type. No parser, OS or database text is ever carried.

The refused argument​

An invalid_input that approvals policy-set, approvals prepare or approvals issue decides names the argument it refused in Failure.field, as the CLI input model names it: connection, operation, schema, revision or input (the business document, from whichever of --input-json, --input-file or --input-stdin) for the target, approve_subject and proof_output for issue, input_file and expected_revision for policy-set, and adapter for the instance selection (--connector with no --instance or the reverse, --adapter beside either, or no selector and no default instance; an unknown instance stays not_found). issue refuses proof_output unless it is an absolute path of plain names, no component is a symbolic link (each is opened without following one, so a link to a private directory is refused too), its directory exists, is the caller's own and admits no other user, every ancestor is writable only by its owner (the caller or root), and the directory holds no entry of that name; an existing file is never replaced. A path relative to the working directory is refused, not resolved. Every invalid_input of invoke names its argument too (invoking an operation).

connections connect names target for a refused <connector>/<instance> and endpoint for a refused --endpoint, on invalid_input, and endpoint on the lifecycle_conflict of an instance that holds another endpoint (§3, "Giving an instance its endpoint").

connections revalidate, connections revoke and connections reconnect name expected_revision when --expected-revision is not the connection's current revision. Given the selected instance's configuration_revision instead, the command is invalid_input at arguments (exit 2) and also carries revision_expected = connection, revision_given = configuration and read_with = "connections describe", the command that reads the connection's revision; nothing is admitted, captured or dispatched. Any other stale value keeps its lifecycle_conflict with next_action = retry_status (exit 1) and carries field, revision_expected and read_with, never revision_given. revision_expected and revision_given are the closed enum RevisionKind (connection, configuration). connections reconnect answers both before it acquires its protected source, so no credential is asked for.

field never carries the caller's value and appears only on the refusals named here and on invalid_input of invoke; no other command carries it, and no other refusal of invoke, an approved write's included; a refusal the presentation raises before the handler runs carries its ess-cli/1 code and no field. As for instance_id, the presence rules of field, revision_expected, revision_given and read_with are host obligations: ESS declares no invariant on a CLI type.

Other admitted provider/service failures preserve their existing shared error code inside Failure.service_code; code = service_failure identifies that route. Provider strings never bypass safe message projection. Failure has a safe stage and next-action enum, plus optional opaque correlation refs under current result access; it never contains raw provider evidence or custody references.

Failure.service_reason is the one provider text a failure may carry, and only in this admitted form. It appears on invoke of a read, at stage = dispatch, when the provider refused the dispatched request: beside service_code, or on the provider's own forbidden. It also appears on a validation (connections connect, reconnect, repair, revalidate) refused with code = not_granted because every alternative binding of the instance was refused, naming each binding's answer. It is absent otherwise, and absent unless every rule below holds:

  • Source. The provider adapter read it from the refusal's response body, and only from the first of the top-level message, error_description and error members of a JSON object body that holds a string. A header, a nested member (error.message), an array, any other member and a body that is not a JSON object are never read. Today only the catalog provider supplies one.
  • Shape. Whitespace runs are folded to one space and the ends trimmed. The reason is withheld when it is empty, or when its first 4096 bytes hold a control character (Unicode Cc), any format character (Unicode Cf, as of Unicode 16.0: U+00AD, U+0600–U+0605, U+061C, U+06DD, U+070F, U+0890–U+0891, U+08E2, U+180E, U+200B–U+200F, U+202A–U+202E, U+2060–U+2064, U+2066–U+206F, U+FEFF, U+FFF9–U+FFFB, U+110BD, U+110CD, U+13430–U+1343F, U+1BCA0–U+1BCA3, U+1D173–U+1D17A and the TAG block U+E0000–U+E007F), or another character that renders as nothing and can carry hidden data (U+034F, U+115F–U+1160, U+3164, U+FFA0, and the variation selectors U+FE00–U+FE0F and U+E0100–U+E01EF).
  • No personal data or secret shape. The reason is withheld whole, never partly masked, when its first 4096 bytes hold an @ (or U+FF20, U+FE6B), so no email address is ever carried; or, without regard to ASCII case, a header- or credential-like marker (authorization, bearer, basic , password, passwd, secret, cookie, api_key, apikey, api-key, private_key, private key, -----begin, token=, token:, access_token, refresh_token, id_token, client_assertion, signature=, sig=, session=); or a token-like run: a maximal run of ASCII letters, digits and -_+/=.~ that is at least 32 bytes long, or at least 20 bytes long and holds both a letter and a digit; or a token-like word: a space-separated word of at least 16 bytes that holds a digit, or whose ASCII letters change case at least once per four letters. UUIDs, long numbers and request identifiers are withheld by these rules too.
  • Not the request's credential. The adapter child withholds a reason that holds any piece of the credential value it derived and sent (an HTTP basic header value, an OAuth access token), and the host admits the reason again after the child returns it, withholding it when it holds any piece of any string value of the protected document the call carried (the whole document, when it is not JSON or holds no string). A piece is eight consecutive bytes compared without regard to ASCII case, or eight consecutive ASCII letters and digits compared lowercased with every other character removed from both sides, so an echo with changed separators matches; a value shorter than eight counts whole. The reason is also read with its %XX escapes decoded.
  • Bound. It is at most 256 bytes of UTF-8. A longer reason is cut on a character boundary and then back to its last whole space-separated word; one whose first word does not fit is withheld.

The reason is text for a person reading why the provider refused, such as the missing token scope behind an unauthorized. It never decides code, stage or next_action, and a client must not parse it. A guarded write's failure, any other connection probe's answer and every other route carry none.

Failure.retry_after_seconds is the delay a provider named before a rate-limited read may be sent again, so the caller can wait. It appears on invoke of a read, at stage = dispatch, only beside service_code = rate_limited, and only when the provider's 429 carried a Retry-After the adapter could read: delta-seconds, or an HTTP-date counted from when the answer arrived and rounded up to a whole second (a date already past is 0). It is a whole number of seconds from 0 to 4294967295; a value outside that range, or in any other form, is not read: the adapter does not read it, and the host drops one an adapter reports, keeping the failure. Two Retry-After field lines on one answer that disagree name no delay; identical repeats count as one. Its absence beside rate_limited states that the provider named no delay the adapter could read. Before refusing, the catalog provider sends such a read once more after the named delay, never more than once, and only when the wait, then a second request as long as the first one took, then a 500 ms margin, all end before the invocation's deadline. The second request is bounded by the deadline less that margin; if it does not finish, the first answer's refusal and delay stand. A failure carrying retry_after_seconds therefore names a delay that did not leave such time, a delay whose second request did not finish, or the delay the second 429 named. A guarded write is never sent again and carries none. The field never decides code, stage or next_action.

The stage names who refused, because the same code can come from either side. A refusal made before any provider request — by the host's admission (an unknown adapter alias included), or by an adapter from its own configured scope (a namespace, resource kind or operation the adapter entry does not allow) — reports stage = admission (forbidden with next_action = request_permission, not_found with check_configuration). invoke, approvals prepare, approvals issue and approvals policy-set read their business document before the adapter alias is admitted, so an unreadable or undecodable document is refused first (the document refusals above) and an unknown alias is not_found only once the document has been read. Every other command answers an unknown alias with not_found at admission. Only the provider's own answer to a dispatched call reports stage = dispatch: an upstream forbidden answer (HTTP 403) keeps code = forbidden with next_action = request_permission, and an upstream not-found answer (HTTP 404; the catalog provider also 410) is code = service_failure, service_code = not_found with next_action = none. Neither sends the operator to connectors configuration. A stored credential the provider refuses on invoke of a read on an existing connection — an HTTP 401, or an OAuth token endpoint's invalid_grant or invalid_client for a refresh profile — is code = service_failure, service_code = unauthorized at stage = dispatch with next_action = repair_connection: a retry with the same protected entry cannot succeed, and connections reconnect replaces it. A credential refused while a connection is made (connections connect, connections reconnect) or revalidated is not this case and keeps next_action = retry_explicitly. A guarded write refused for its stored credential, at its preflight or its commit, is not this case either: it reports its mutation record with next_action = retry_status until story:guarded-write-credential-refusal-says-repair extends this rule to writes. A connection probe's answer (the catalog provider's identity and scope probes, the Kubernetes token review) is classified separately and not by this rule: a 403 reads as forbidden at admission, and a 404 is service_failure with service_code = upstream_protocol at dispatch and next_action = retry_explicitly.

A read on invoke whose connection's validation evidence expired while its credential is intact (the connection reports pending) recollects that evidence once before it is admitted, as connections revalidate does: the owner, on the instance's serialized worker, captures the retained custody version, runs the profile's identity probe on exactly those bytes, publishes the new baseline under the unchanged generation and revision, and then admits the read against it. Evidence still current that would lapse before the read could use it counts as lapsed: a read whose evidence has less than half its own lifetime left (from its collection to its valid_until, which the provider's identity probe may set below the profile's evidence_lifetime_ms, never the profile's bound), and less than the read's own remaining deadline, recollects it first, because a read's use and its provider deadline never outlast the evidence it was admitted on. Evidence fresh from a recollection therefore admits its read without another one. Evidence that still lapses between the read's capture and its dispatch is refused as expired evidence (below), never as the read's own timeout. The caller revalidates nothing: a connection connected or revalidated once keeps serving reads for as long as its credential stays valid, across any number of evidence lifetimes. The recollection asks for no credential, writes no custody version and never follows changed material: a mounted item whose bytes changed still refuses the read with code = custody_unavailable and next_action = revalidate_connection, and only connections revalidate re-pins it. A credential the provider refuses in the recollection is cut off as an explicit revalidation cuts it off, and the read answers as for a stored credential the provider refuses on a read: code = service_failure, service_code = unauthorized with next_action = repair_connection; the business read is not dispatched. A recollection run only because the evidence would lapse early, while it is still current, keeps the read admitted on that evidence: when it fails with anything but such a credential refusal (the provider or custody briefly unavailable, a timeout), the read is served on the evidence it was admitted on, for as long as that evidence stays current, because a transient failure preserves the still-valid baseline. Any other failure of the recollection of expired evidence is the read's own answer, before dispatch. Only a read still refused for expired evidence after its one recollection answers code = not_granted at stage = admission with next_action = revalidate_connection. A consumer launch (connections launch) recollects nothing and keeps its own refusal (consumer-launch.md). A connection whose credential the provider refused or is known invalid (stale), one whose credential expired (reauthorization_required), and a credential below the operation's scopes, keep code = not_granted with next_action = repair_connection and recollect nothing.

A connection whose binding and configured entry both record a credential-side digest (credential_revision, the implementation's digest of what decides where the credential is sent and how it authenticates; never the bundle, its source or the selection) is bound to it and to the provider executable, not to the configuration revision. A change that leaves the digest equal is not a stale configuration: when only the bundle, source or selection moved the connection reports ready with stale = false, and a read, a reconnect, an approval naming it and a consumer launch admit it with no credential source and no question; its next revalidation records the new revision. When only the provider executable changed, it also reports ready with stale = false; a read validates its stored credential once under the new executable and is served when the recorded identity answers, and a revalidation follows it the same way; another identity is refused as identity_mismatch and changes nothing. Every other change, and every change for a connection whose binding records no credential-side digest (saved before it was recorded, or under an entry that states none), follows the configuration-revision rule below. A changed configuration's description is cached by the first read command that launches for it; a write after an executable-only change validates the connection once, as a read does.

A connection saved under another configuration revision of its instance also reports pending (stale = true), and no status change is pending for it: a read on invoke, an approval naming it and a consumer launch refuse it as code = lifecycle_conflict at stage = admission, never with next_action = retry_status. The next action is revalidate_connection only when a revalidation can follow the configuration upgrade: its provider authority and profile declaration are unchanged, its credential is neither known invalid nor expired, and the new provider has not already refused it under the configured revision. The connection's identity is the instance, adapter, provider authority and the profile's authentication, which is the profile declaration apart from its evidence lifetime. A configuration that changes only evidence_lifetime_ms leaves the identity unchanged, but the connection stays bound to the declaration it was admitted under, so the next action is create_connection: connections connect under the same instance id names the old connection's revocation while the instance holds it (revoke_connection, one connection per instance), then admits the new connection. When the provider authority or the profile's authentication changed, the next action is check_configuration: no command under the same instance id can help, since connections connect there is refused for the same reason (lifecycle_conflict at stage = admission, next_action = check_configuration), and only a new instance id admits a new connection. The connections left under the earlier instance id stay revocable through the entry, under the same adapter, that moved: connections revoke reaches a connection whose recorded instance id no entry configures any longer, never one another entry still configures. Otherwise it is create_connection, a saved connection whose profile id the configured adapter no longer declares included (revalidate and repair refuse it as a read does, never as unsupported); connect under a declared profile admits the new connection. A connections reconnect of a connection saved under another binding is classified the same way, never as identity_mismatch: reconnect refuses a changed binding. A connections revalidate, connections connect, connections reconnect, read or write whose adapter process still runs an earlier adapter entry stops that process and launches the configured one before it captures anything, so no command answers retry_status for it until instances stop. Only a credential validation captured by the earlier process is still refused as lifecycle_conflict; its connections connect starts again with a launch that replaces the process. A revalidation the new provider refuses (the credential invalid, below scope or another identity) answers create_connection and records the refusal against the configured revision (Connection.refused_configuration_revision), so the next read names create_connection too; the refusal invalidates nothing under the connection's own revision, revalidation stays admitted, and a later upgrade that succeeds or a repair that replaces the credential removes it. An approval reads the wall clock for this classification only, as the read path does, without advancing the registry clock. connections connect under the configured revision is admitted once the instance holds no published non-revoked connection; an instance from before that rule may still hold older ones saved under their own revisions. The instance's own recorded configuration revision moves to the configured one only when a connection is published under it: a revalidation that upgrades one, or a new connection whose publication succeeds while the instance still records the revision it had when that connect began. A connect that fails or is abandoned moves nothing, and a connect begun under an older configuration never moves the instance back. Until it moves, the instance's approval keys (approvals key-status and the other key commands) and its approval policy commands refuse as code = lifecycle_conflict with next_action = create_connection.

A provider's timeout or capacity answer is the provider's too: a request the provider transport sent whose deadline then passed (a connection probe's included, and one that passes while the provider's answer is still being read after its status and headers arrived), a database statement timeout, and a capacity refusal the adapter marks as the upstream's answer (the SQL adapter's database connection limit) report timeout / capacity at stage = dispatch with next_action = retry_explicitly. A connection that never opened sent nothing and is not one of them. The host's own deadline and limits, and an adapter's own connect or work deadline and result bounds, keep stage = admission (retry_explicitly). An invoke read whose deadline passes after the owner dispatched it (its read use recorded dispatched) is the provider's timeout at stage = dispatch; one whose deadline passes before is the host's at stage = admission, and the owner dispatches nothing for it afterwards. The owner answers by the deadline; an invoke the CLI sent whose answer it never receives may have been dispatched and reports outcome_unknown. An HTTP 429 stays service_failure with service_code = rate_limited, and with retry_after_seconds when the provider named a delay. unsupported is always admission: every source of it is raised before dispatch, with one exception: a device grant whose authorization server names a poll interval longer than the sign-in window is refused as unsupported after the device authorization request and before any poll, because only that response names the interval. Nothing is stored and no token was requested.

Revalidation retries. connections revalidate sends its validation (the adapter's identity probe, a read) again when the attempt failed transiently, inside the same dispatched revalidation and within its deadline. A transient failure is the adapter answering unavailable (a provider connection refused, reset or closed, such as a pooled keep-alive connection the provider closed while it was idle; a name that does not resolve; the catalog provider's identity or scope probe also on an HTTP 429 or 5xx), a provider timeout, rate limit, internal error or marked capacity answer. The owner sends at most three attempts, waiting 200 ms before the second and 1 s before the third, and only while the adapter process still runs, the owner is not stopping, and the wait, then an attempt as long as the last one took, then a 500 ms margin, all end before the deadline. A credential the provider refuses (HTTP 401), a permission refusal (403), a credential below the profile's scopes, an identity mismatch and a protocol violation are never sent again. When every attempt failed, the failure keeps its code, stage and next action, and additionally carries cause, the class of the last attempt (connectors.cli.FailureCause), and attempts, the number of validations the owner sent, when it sent any. cause is adapter_exited when the adapter process was no longer running after the failed attempt, and owner_stopping when the owner began to stop during the revalidation. No other command and no other failure carries either field; a revalidation whose reply never arrived stays outcome_unknown and carries neither.

A guarded write's failure carries its mutation record, and its stage follows who refused and whether the write was attempted. A refusal the host made before anything was sent (mutation.classification = not_attempted — the host's approval refusals and its own deadline included) reports stage = admission. The provider's answer, and a host failure once the write was sent or may have been (refused, applied, unknown), report stage = dispatch. The provider's own refusal of the write names its next action as a read's does: an upstream 403 (the catalog provider also 405 and 415) is forbidden with next_action = request_permission, an upstream 404 or 410 is service_failure, service_code = not_found with next_action = none, and a provider timeout or capacity answer to the write's preflight is retry_explicitly. A write whose selection declares a conflict status, answered with it (the target changed since it was read, nothing applied), is refused as service_failure, service_code = conflict and is never sent again. A preflight declaring mismatch: conflict whose check differs refuses the write unsent with the same service_failure, service_code = conflict. A write classified applied or unknown is never offered retry_explicitly: it may have taken effect. Every other write failure — the host's approval refusals included — reports next_action = retry_status. A write whose effect is unknown after its request was sent, including by a timeout, stays outcome_unknown. The settled attempt stores which side refused, so a replay of the same idempotency key reports the same stage and next action as the original reply; an attempt stored before the origin was recorded replays as the host's (retry_status).

7. Verification and remaining obligations​

Scenarios enumerate C01–C05, startup races and failure boundaries. Value fixtures carry explicit valid/invalid expectations against generated schemas. Their structural validation proves required fields, types, enums and closed objects, not cross-field bounds or any process trace. Cached fixture expectations retain the two specific cached operation regressions. The fixture gate must additionally inspect every object in every structurally valid example, require stale: true whenever source: cached, and check the public acquisition state/reason/connection combinations and connection-list page freshness labels. Selections must be nonempty, fixture ids unique, and each named expectation must match exactly one fixture. These are consistency checks on authored examples, not evidence that a runtime enforces the same rules. TOML fixtures and trace expectations are reviewed specification inputs; they are not a pretend running backend. The dependent CLI surface story owns generated parser/process/source fixtures and the integrated gate.

Runtime acceptance must exercise real Linux ownership/no-symlink descriptor checks, echo restoration and output redaction before parsing failures; keyring lock/write/ durability and crash recovery; metadata publication/revoke/dispatch atomicity; same-target repair and revision races; child launch coalescing and pidfd stop; durable stop suppression; actual readiness/timeout/isolation; still-valid credential reuse across both process restarts; and durable open checkpoints: a store setup init creates carries them, an existing store setup checkpoints-enable enabled then opens from its checkpoint, and an unconfirmed run is refused with the store unchanged. New ESS and deterministic output must be revalidated using the selected exact current-source ESS toolchain. An older release binary is provisional authoring evidence only.

macOS platform guarantees​

The host builds on seven operating-system guarantees, each fixed by the platform the binary was built for. connectors.cli.PlatformGuarantee names them, connectors.cli.PlatformGuaranteeStatement states each row below as an invariant, and connectors.cli.PlatformGuaranteeWeakerForm names every weaker form. Linux holds all seven, by the mechanisms the rest of this binding states. On macOS one holds and six hold only in a named weaker form:

GuaranteeLinuxmacOSmacOS mechanism
sealed_executableholdsweaker: owner_private_copyno memfd: the image is copied into a mode 0500 file in a fresh mode 0700 directory, and the digest and Mach-O header are checked on that copy before exec by its path. Other users cannot change it; a process of the same user can rewrite it between the check and exec. A copy that is refused or never launched is removed with its directory; a launched copy is removed by its launch once the exec it serves no longer needs it.
parent_death_killholdsweaker: adapter_channel_closeno PR_SET_PDEATHSIG: an adapter exits when its inherited channel on descriptor 3 reaches end of file. A consumer has no channel and outlives a launcher that dies.
exact_process_signalholdsweaker: child_reap_lockno pidfd: kqueue EVFILT_PROC with NOTE_EXIT observes exit, and kill signals the child's process id only while holding the lock that reaps it, so the id cannot be reused between the check and the signal.
own_image_build_measureholdsweaker: launch_path_readno /proc/self/exe: the owner build digest is read from the executable's path. A file replaced there is measured as a new build and refused as owner_build_mismatch.
atomic_no_replace_renameholdsholdsrenamex_np with RENAME_EXCL, on a filesystem that reports VOL_CAP_INT_RENAME_EXCL (APFS). A state directory on any other filesystem is refused.
atomic_close_on_execholdsweaker: set_after_receiveno MSG_CMSG_CLOEXEC: FD_CLOEXEC is set on each received descriptor right after recvmsg returns. An exec by another thread of the same process in between inherits it.
peer_credential_snapshotholdsweaker: separate_pid_queryno SO_PEERCRED: LOCAL_PEERCRED gives the connect-time uid and gid, and LOCAL_PEERPID the pid in a separate query. The uid check is unchanged. The owner and the supervisor use the pid only for its sign; an adapter also requires it to equal its parent's process id, the owner that made the socket pair, as on Linux. A peer that has already closed has no pid to query: the owner and the supervisor admit it by its connect-time uid, as Linux does, and an adapter refuses it.

Protected stdin (§5) keeps its rule on macOS with other mechanisms, and no guarantee row changes. A protected regular file is checked as on Linux, its parent directory found through F_GETPATH. macOS names no pipe:[…]: an inherited anonymous pipe is a FIFO owned by the admitted UID for which F_GETPATH returns no path, and a FIFO with a path is a named FIFO and refuses. The producer check reads the parent's owner through proc_pidinfo in place of /proc/<parent>: the parent must not be pid 1, must share this process's session, and must be owned by the admitted UID.

Exact stop (§4) requires "Linux pidfd or equivalent non-reusable kernel process ownership"; on macOS the equivalent is child_reap_lock, which covers the owner's own children only, and those are the only processes stop signals. The runtime acceptance above applies on macOS with each Linux mechanism replaced by its row here: stop is exercised through the reap lock, and the owner-only descriptor and socket-peer checks through LOCAL_PEERCRED.

Windows has its own rows, in the next section. Every Linux rule above stands as written. These macOS rows are specified; until the host and CLI suites run on a macOS runner, none of them is observed.

Windows platform guarantees​

The same seven guarantees apply on Windows, stated by the same invariants of connectors.cli.PlatformGuaranteeStatement. Linux and macOS keep their rows above. On Windows four hold and three hold only in a named weaker form:

GuaranteeLinuxmacOSWindowsWindows mechanism
sealed_executableholdsweaker: owner_private_copyweaker: deny_write_copyno memfd: the image is copied into a fresh owner-only directory inside the state directory's owner-only run directory, never the system's temporary directory, and the copy is opened for reading without FILE_SHARE_WRITE or FILE_SHARE_DELETE before its digest and PE header are checked on that handle. The handle, and one on its directory without delete sharing, are held until the child is created, so from that open no process can write, rename or delete the copy or rename its directory; after creation the mapped image keeps it unwritable, and the child's current directory, which is the copy's directory, keeps that directory in place. A copy is removed with its directory on every refusal; one left by a process that died is removed when the next owner starts, unless a running process still holds it. A writable mapping that a process of the same user made of the copy before that open is not excluded.
parent_death_killholdsweaker: adapter_channel_closeholdsno parent-death signal: every child is created inside a job object with JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, through PROC_THREAD_ATTRIBUTE_JOB_LIST, so it is never outside the job, and its launcher holds the job's only handle. When the launcher dies the handle closes and the job ends every process in it, consumers included. An adapter's job permits no breakaway. A consumer's job permits breakaway on request (JOB_OBJECT_LIMIT_BREAKAWAY_OK, never JOB_OBJECT_LIMIT_SILENT_BREAKAWAY_OK), so a process the consumer creates with CREATE_BREAKAWAY_FROM_JOB, such as an owner, leaves it; the consumer itself stays in it.
exact_process_signalholdsweaker: child_reap_lockholdsno pidfd: the launcher holds the child's process handle, which names exactly that process and keeps its id from reuse while open. Exit is observed by waiting on the handle, and stop terminates through it (TerminateProcess); Windows has no signal to send first.
own_image_build_measureholdsweaker: launch_path_readweaker: launch_path_readno /proc/self/exe: the owner build digest is read from the path GetModuleFileNameW reports. A running image cannot be written in place, but it can be renamed and another file put at its path; that file is measured as a new build and refused as owner_build_mismatch.
atomic_no_replace_renameholdsholdsholdsMoveFileExW with MOVEFILE_WRITE_THROUGH and without MOVEFILE_REPLACE_EXISTING refuses an existing destination in the same call, on NTFS. A state directory on any other filesystem is refused.
atomic_close_on_execholdsweaker: set_after_receiveholdsno descriptor passing: a handle reaches the other process through DuplicateHandle with inheritance off, so it is not inheritable from the moment it exists there. A child is created with PROC_THREAD_ATTRIBUTE_HANDLE_LIST, and only the handles listed there reach it.
peer_credential_snapshotholdsweaker: separate_pid_queryweaker: client_token_queryno Unix socket: the owner listens on a named pipe whose access control list grants the current user's SID alone, so the kernel admits no other user at connect. GetNamedPipeClientProcessId gives the client's process id as recorded at connect; the client's SID is then read from that process's token, a separate query, and must be the current user's. If the client exited and its id was reused before the query, the token read is another process's. The token's integrity level must also equal the reader's own, and the token must not be restricted: an elevated and a non-elevated process of one user, or a restricted one, do not admit each other, on either end, and refuse with lifecycle_conflict. The owner opens an admitted client's process once from that id; the checks and every handle duplicated into the client go through that one process handle.

Owner-only files keep their rule (§5) with another mechanism, and no guarantee row changes. Ownership and permission are checked on the opened handle: the owner SID is the current user's, the discretionary access control list exists, and every entry that allows access names the current user, LocalSystem or the built-in Administrators group, which correspond to what root may read on Linux whatever the mode bits say. Any entry allowing another SID refuses, as group or other mode bits do. A file is opened with FILE_FLAG_OPEN_REPARSE_POINT and refused when it, or any directory on its path, is a reparse point (a symbolic link or junction); it must have one link. A sidecar of the metadata database that SQLite's last close is deleting keeps its name until every handle to it closes, and opening it answers access denied meanwhile; one deleted after it was opened shows no link on its handle. Such a sidecar is probed again, within the lifecycle lock's wait, until its name is gone, and is then absent; one still there at the end of the wait refuses, and a present sidecar that is not being deleted keeps the full rule. Files and directories the host creates get a protected list naming only the current user. Protected stdin admits a protected regular file; a pipe on stdin is refused on Windows until a producer check is specified for it.

Where a rule above fsyncs a directory (setup init, the metadata database's creation and each migration), Windows takes no directory step: a directory is held open for listing only, and Windows flushes no directory through such a handle. A file the host publishes is made durable by its own flush and by MoveFileExW with MOVEFILE_WRITE_THROUGH, which returns only once the rename is written; the metadata database's files are flushed by SQLite, which on Windows syncs files and never their directory.

Hidden terminal entry opens CONIN$ and CONOUT$, clears ENABLE_ECHO_INPUT and restores the saved console mode on success, failure and interruption. A process with no console has no trusted terminal and refuses as on Linux.

The clock reading of the clock binding uses QueryUnbiasedInterruptTimePrecise, which stops while the machine sleeps, in place of the suspend-exclusive clock, and QueryInterruptTimePrecise, which counts sleep, in place of the suspend-inclusive one. The non-precise forms move only at each timer interrupt, coarser than the 1 ms the binding requires.

Exact stop (§4) is held through the process handle on Windows, which is the "equivalent non-reusable kernel process ownership" §4 requires. The runtime acceptance above applies on Windows with each Linux mechanism replaced by its row here and in the owner, adapter and consumer Windows sections. These Windows rows are specified; until the host and CLI suites run on a Windows runner, none of them is observed.

Bound connections​

An instance may describe its operations as more than one binding, each its own descriptor and operations, of which a validation selects the one its credential can read (private adapter binding; the catalog provider's bindings). The connection is then bound: ConnectionDescription.binding names the binding its current evidence selected, and is absent for every instance without bindings. A connection whose every binding was refused is not made; the validation fails with code = not_granted and a service_reason naming each answer.

Each validation selects again. When connections revalidate (or repair) selects another binding than the evidence it replaces, the result says so in ConnectionRevalidateResult.rebound, {"from": <id>, "to": <id>}, absent otherwise; a read recollecting evidence may rebind as well, and that read refuses as stale_description unsent, so the next read runs under the new binding.

invoke admits and dispatches a read under the binding of the evidence it captured: an id that binding does not serve refuses as not_found before any request, though another binding of the instance serves it. operations list and operations describe answer the first binding by default, as for every instance; with --connection CONNECTION, a connection of the selected instance, they answer the binding that connection's evidence selected, and its revision is the one invoke checks. A connection the selected instance does not hold refuses as not_found, as connections describe refuses it, and one never validated answers the first binding.

Operations by family​

operations list --family CONTRACT answers only the listed operations whose descriptor contract is exactly CONTRACT, each with its id, contract and native profile. It is how a consumer finds a shared family on a saved connection without knowledge of the provider: a connection is selected by the --adapter alias it was made under (§1), and the operations it can be invoked with are that adapter's cached description, so the consumer lists them with the same alias, then describes and invokes the family's fixed operation ids. For datasource.feed/v1alpha1 these are feed.containers and feed.items under one profile. An adapter added or upgraded after the consumer was built is found the same way once its description is cached.

The filter narrows the same selection operations list already answers: an operation the configuration does not grant stays hidden, and the cursor is bound to the family as well as the adapter, revision and granted operations, so a cursor issued under one filter refuses as stale_cursor under another. A family no listed operation binds answers an empty page, a truthful negative observation, not not_found. The value is matched exactly; one that is empty, longer than 128 bytes or holds a character other than ASCII letters, digits, -, _, . and / refuses as invalid_input before the cached description is read.