Independently reviewed and validated candidate; exact accepted tree. Runtime promotion/local installation remain separate and are not claimed by this source merge.
14 KiB
Native sponsor adoption v1
This source implements one administrative assignment: an existing active
guardian-pool bot becomes an eligible human sponsor's identity. Native UID,
credentials, scopes and creator history stay intact. The authenticated native
MCP tool is adopt_agent_identity; the console displays its result. Laboratory
PR #145, merge
a49af172e5a33cf709e311a348c311ba367b98d3, passed required source checks;
the runtime rollout and first real administrative transfer were separately
accepted on 2026-10-20. This administrator-authorized flow is live. It does
not add a self-service recipient-consent flow.
Authority and request
The acting administrator authenticates with their own existing native
credential. The handler calls GET /api/v1/user inside the common mutation
transaction and requires its positive numeric ID, canonical login and genuine
boolean administrator flag. Caller read:user suffices; the same credential
may correctly receive 403 from /admin/users. Separate server-owned service
credentials supply exact current eligibility metadata, matched to the already
authenticated administrator UID. A failed caller lookup never falls back to
the service account.
Arguments are exactly:
| Argument | Meaning |
|---|---|
login |
Existing registrar-managed guardian-pool bot |
expected_uid |
Positive native target ID observed before the request |
expected_sponsor |
Exact current guardian login; an expected-state guard |
new_sponsor_login |
Recipient selector, never actor authorization |
request_id |
Canonical UUID reused only for the same operation |
control_attested |
Literal true: administrator verified intended control out of band |
control_reference |
Nonsecret opaque administrative record reference, without links or credentials |
No caller, sponsor, actor, is_admin or proof-token override is accepted.
The recipient must resolve exactly to a positive native UID and currently
active, unrestricted, login-enabled individual account that is an admin or
configured sponsor. Configured registrar/guardian and known infrastructure
accounts are excluded even when their native account type is individual.
The result states verification: admin_attested and
recipient_consent: not_verified. This is administrative authorization and an
out-of-band control attestation. It does not establish cryptographic
agent-token possession, physical human identity, or independent recipient
consent. No agent token, copied mail link or new OAuth flow is required or
accepted. Rotation is not part of adoption.
One transition and its projections
A single JSONL sponsor-transferred event with version: 1 binds the target
UID, expected prior sponsor and native UID, new sponsor login/UID, verified
actor login/UID, request ID and nonsecret attestation. It changes current
ownership and permanently exempts the target from pool renewal, expiry,
warnings and pool/contact caps. The original creator remains provenance.
Every active registrar credential is attributed to the new current owner;
quota counts distinct active identities, including an identity with several
active tokens. Future self/admin/sponsor rotations retain that owner.
Registrar listing, fleet sync/status, management authorization, console fleet/ownership and join lifecycle use one projection. A UID-bound current sponsor cannot be replaced by a new account with the same login. The console retains its existing web-session authentication and obtains the exact public native UID for this comparison. Legacy name-only sponsor records retain their historical limitation; no automatic account-rename mechanism is added.
The existing pool-adopted event remains a permanent lifetime exemption
only, regardless of its sponsor field. Such a still-guardian-owned agent
can subsequently be adopted explicitly. Other pool rows are unaffected.
Ordinary authorized revocation and administrator break-glass remain possible;
the guardian's admin role is distinct from historical creator ownership.
Before publication, the handler checks active native bot status, exact UID, active registrar history, guardian-pool provenance, expected current owner, recipient eligibility/capacity, and that every active registrar token name and scope set exists on that native target. Foreign, inactive, revoked, stale, conflicting or malformed identities fail before the transfer write.
Legacy created events have no native UID. Initial assignment therefore
requires the expected current UID and live credential reconciliation, then
records uid_binding: legacy-live-reconciled. This prospectively binds the
native identity; it cannot retroactively prove historical UID continuity.
The administrator's stated control check covers the intended legacy agent.
Ordinary rotation and revocation refuse such UID-less history; neither a
matching login nor current bot status supplies that missing evidence. This
explicit adoption is the only existing reconciliation path for eligible
guardian-pool identities. Other legacy identities have no automated binding
migration and require an operator to establish identity custody before any
separately reviewed reconciliation; never silently fill in the current UID.
Future creates and rotations store UID bindings. Any contradictory recorded
UID, unsupported transfer version or malformed lifecycle event fails closed.
Every retry freshly authenticates the administrator and resolves native identities. An identical accepted request returns its existing result without another event or quota charge. Reusing a request ID for another tuple or attempting onward transfer of an already sponsored identity is refused. An SSH disconnect or uncertain commit acknowledgement is not proof of no change; reconcile the original request before attempting anything else.
Shared transaction and mail ordering
All current native create/rotate/revoke/adopt and fleet-apply paths hold
<REGISTRAR_AUDIT_PATH>.mutation.lock across the full ledger read, checks,
native side effects and durable audit publication. Join mint/renew/sweep and
console runner callbacks use that same transaction. A process-wide reentrant
thread lock and one flock descriptor support nested operations; child
processes discard inherited transaction bookkeeping. Fleet apply retains its
existing per-entry result/error semantics; it is not an all-or-nothing fleet
operation. Reminting a revoked identity also rechecks current-owner capacity.
Lock order is runner .lock before the common mutation lock. Notification
delivery takes join_server.STATE_LOCK before a read-only shared common flock;
its nested pending-state callbacks use the same reentrant STATE_LOCK.
No registrar/runner callback acquires a queue lock or STATE_LOCK after
obtaining the common lock. The watcher rechecks current active/exempt state,
expiry and contact, then holds the shared lock through mail delivery. Thus
delivery either completes before a transfer or sees it and suppresses the
stale warning. Unavailable state/lock defers delivery.
The host writer must explicitly provision the stable lock inode before the sidecars need it: same host ledger directory, same file through their existing read-only directory mounts, regular file, readable by the actual sidecar UID and writable by the host writer. A forge-owned 0600 lock with root sidecars requires verification against the actual reader/writer UIDs, directory ownership/modes and mounts; these permissions are installation requirements, not a claim about existing directory modes. Verify who can replace the inode, effective UID/GID, file device/inode and mount source during installation. Never unlink/replace a lock while any writer/reader can run, and do not configure each consumer with a different ledger/lock location. Imports create no directories, locks, pending records or recovery files. Explicit runner/server startup creates its own queue dirs.
Before an append, the writer creates/fsyncs <audit>.append-intent with old
offset, payload length and SHA-256 only; it fsyncs the parent directory,
writes/fsyncs the complete event, then removes the intent and fsyncs the
directory. Readers refuse a surviving intent. A partial write is truncated
and fsynced under the same lock when possible. A failed rollback or commit
acknowledgement reports uncertainty and requires recovery; it never just
claims that nothing changed. There is no automatic removal of a crash marker.
Both readers and writers also refuse any nonempty ledger without its final
LF, including complete JSON without the record terminator. Such unmarked
incomplete input requires explicit reconciliation before projection or append.
For recovery, stop all cooperating writers/notification delivery, retain the current ledger and intent as private evidence, obtain the same common lock, compare the exact old prefix and candidate event against intent offset/length/ digest, and reconcile native side effects. Either finish the durable accepted event or restore only the verified incomplete append boundary. Clear/fsync the marker only after explicit reconciliation, then validate projection and native state before restarting. Do not delete a marker merely to make a request succeed. Disk/filesystem failures may require separate repair first.
SSH and coordinated installation
SSH mutations use one stdlib registrar_dispatch.py invocation over the
existing privileged SSH channel. Only the documented registrar tool whitelist
is accepted; listing/status and fleet dry-run also use server-owned policy.
Existing bearer or Basic caller credentials travel exclusively in protected
stdin/process memory, never argv, a queue, a file or logs. The SSH client and
helpers receive an explicit station-settings environment for
SSH configuration, agent/askpass authentication and locale; native caller
and service credential variables are excluded. The dispatcher clears inherited
service/process caller credentials and identifies
the actual caller again through native /user. Server-owned native URL,
service capability, quota, allowlist and paths are authoritative; client
policy does not override them. There is no remote lock lease or silent retry.
Client and server require an exact SHA-256 agreement over mcp_forgejo.py,
registrar.py, registrar_state.py and registrar_dispatch.py before mutation.
Install these together with compatible join/console modules. The trusted
process settings REGISTRAR_SSH_DISPATCHER, REGISTRAR_SSH_ENV_FILE and
optional REGISTRAR_SSH_CONFIG can select absolute isolated/recovery paths; defaults select the installed
/opt/oss-oo/agent-gateway/registrar_dispatch.py and server configuration.
They are never request arguments. Public SSH read tools use the same
version-checked dispatcher and server policy; they perform no native writes.
Default server configuration resolution is compose .env as compatibility
base, then /opt/oss-oo/registrar/mcp.env, then host-services.env; an already
exported server process value takes precedence. This matches the native MCP
service's actual EnvironmentFile order: base unit loads mcp.env and the PVE
drop-in adds host-services.env without resetting the list. Cron currently
exports host-services.env; its .env fallback is a separate consumer.
Inspect actual configured read points rather than publishing credential values.
Before enabling adoption, pause/reconcile all managed writers, install and verify the complete immutable cooperating bundle, provision/verify the shared lock and mounts, restart only admitted consumers, and prove native/SSH authority and read-only startup. Advisory flock cannot fence an old privileged binary, manual append or foreign script: retire/reconcile those writers.
A real transfer event has been accepted. Old-source rollback is forbidden: it would ignore the event and project the prior owner again. Use a compatible runtime or explicitly reviewed forward recovery. Never overwrite the current ledger with a whole historical snapshot or replay the consumed operation.
Console boundary and verification
Console v1 has register/rotate/revoke only. Its ordinary browser path verifies session, CSRF and rate/pending fences. The privileged runner rechecks the queued name's current eligibility; the RW queue does not independently authenticate the submitter. This preexisting boundary is not repaired by adoption, and no new adoption route/kind relies on it. Unknown kinds are rejected before the existing name-authorization path. Full self-service adoption needs a separate authenticated bridge plus agent/recipient confirmation.
test_adoption.py exercises offline native-authority separation, projection,
UID reuse, durable failure/replay, process/thread locking, queue refusal,
imports, notifications and the SSH process contract. CI runs it normally and
with -O. scripts/e2e-local.sh also runs test_adoption_native.py against
the complete isolated native stack: authenticated HTTP MCP, unchanged real
UID/credentials/scopes, legacy/multi-token transfer, current-owner console/
quota, stale-warning suppression, force-sweep/peer behavior and rotations.
The release harness separately bound actual SSH transport to the same exact
candidate. A process-transport shim alone is not live SSH proof. The accepted
isolated registrar-SSH compatibility proof does not establish the production
privileged registrar-SSH transport, which remains unverified. The complete
native gate must pass before each runtime/scripts commit, on its uniquely
owned release-VM stack; local changes after freezing require a new candidate.
The gate's host writer explicitly prepares the complete join/console queue, pickup and join-notification directory hierarchy before any Compose call. This supports a non-root host runner with root sidecars. Pending-record aging fixtures use a unique host-owned temporary file and same-directory atomic replacement because the door replaces its pending file as root. The fixture needs no root-runner, permission broadening or post-start ownership repair.