# 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](https://oss-oo.io/OSS-OO/laboratory/pulls/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 `.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 `.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.