glove/docs/architecture.md
0xkite 09d34002a6
Merge PR #25 (guest channel layer) into PR #24 (wallet stack + Apple repairs)
Combine maintainer/wallet-status-and-apple-repairs (18e75af8) with
maintainer/guest-channel-layer (00d7c473) into a single validated
integration branch for Ghost deployment testing.

Merge resolution decisions:

- Apple container runtime (the known conflict zone) auto-merged
  textually and was verified semantically against both parents:
  the maintainer hard-disable survives (lifecycle_operational()
  returns false unconditionally, apple_container_session_runtime.hpp),
  the sampler/finalizer jthread lifecycle seam and the finalizer
  catch-all well-formed terminal state (a96ab1e) are intact, and
  null_terminated=false strict_json_options is preserved. Velinus's
  observation-intent integration lands adapted on the repaired base:
  prepare_observation_service is only reachable from start(), which
  denies before side effects when the lifecycle is disabled, so the
  observation service never constructs on the disabled Apple path.
  The gated test from PR #25 (every control method refuses before
  side effect; record counts unchanged) survives.
- GLOVE_GUEST_OBSERVATION_* env names kept; observation socket/token
  volumes still mount only when observation_service->active().
- guest_channel layer, host-registered schema admission
  (channel_descriptor/channel_host), SO_PEERCRED/LOCAL_PEERCRED, LRU
  replay cache, bounded retry, fail-closed recovery, and
  channel_identifier_grammar.hpp land as-is; session_registry files
  did not conflict with PR #24's side.
- Wallet status files: PR #25's delta did not touch them, so the
  harness-neutral renames (wallet_status_projection_channel) stand
  unchanged.

Validation (umask 0022): dev/asan/tsan presets build clean and
CTest 80/80 each (control_apple_container_session_runtime
env-gated/skipped as expected); clang-format (llvm@22) applied to
changed sources; git diff --check clean. No push.
2026-08-29 16:23:23 -06:00

20 KiB

Glove architecture

Scope

Glove provides three execution surfaces:

Surface Purpose Current boundary
glove run Contain an agent and mediate MCP tool calls Public CLI
glove exec Contain a direct agent process Public CLI
gloved Validate Sage plans, persist sessions, and deliver receipts Owner-local control service

The public CLI is usable for local containment. The distributed Sage session surface is incomplete and must not advertise remote-launch readiness. A remote_linux_container runtime can be constructed from validated operator configuration, but it is deliberately non-operational and advertises no lifecycle, runtime-adapter, resource-enforcement, or receipt capability.

Process model

operator or Sage controller
          │
          ▼
  Glove control plane ───────► audit and receipt journals
          │
          ├── policy engine
          ├── MCP extensions ─► upstream tool servers
          │
          ▼
  OS sandbox
          └── agent process

The control plane remains outside the sandbox. It owns policy evaluation, upstream transport, audit output, protected filesystem descriptors, and session state. The agent receives only the endpoints and paths required by its selected mode.

MCP upstreams are separate host processes. Glove filters requests to them, but does not sandbox the upstream processes themselves.

Platform isolation

Property Linux macOS
Process visibility PID namespace SBPL process policy
Filesystem mount namespace, pivot_root, read-only binds, private writable mounts deny-default SBPL path rules
Network route-less network namespace; offline socket denial or private-loopback access to the authenticated audited proxy through an inherited Unix descriptor channel deny-default SBPL network rules
Identity user namespace and UID/GID mapping invoking user
IPC and hostname IPC and UTS namespaces SBPL policy
Resource limits private cgroup/quota/watchdog implementation incomplete for the Sage six-limit contract

Linux launches the child through clone3, configures the namespace and mount perimeter, applies seccomp, then releases the child to execute. Writable materializations are quota-backed; retained copies use persistent hard-sized ext4 loop images while ephemeral copies use tmpfs. Read-only inputs are descriptor-pinned. For approved egress, a namespace-local helper passes accepted loopback sockets to a host relay with SCM_RIGHTS. The relay can connect only to the per-run authenticated proxy; it never exposes a host-network socket or DNS service to the agent. The namespace-local helper consumes one PID within the configured session PID limit. macOS constructs a deny-default sandbox profile and applies it before executing the child.

Components

Namespace Responsibility
container sandbox creation, mounts, limits, output accounting, terminal receipts
control Unix control protocol, authentication, session registry, receipt delivery
supervisor canonical plan validation, local alias resolution, bundle resolution
mcp bounded JSON-RPC framing and upstream clients
policy tool and argument authorization
kernel extension registration and dispatch
audit structured local activity records
run CLI orchestration for run and exec
host strict XDG configuration, machine setup, diagnostics, and local project enrollment
reflect compile-time extension metadata experiments

Public headers live under include/glove/. Implementations mirror that layout in src/. Tests are separated by concern under tests/.

MCP request flow

For glove run:

  1. Glove starts the configured upstream servers and completes MCP initialization.
  2. The contained agent sends a bounded JSON-RPC request.
  3. The kernel resolves the extension and tool name.
  4. The policy engine validates tool access and configured argument rules.
  5. The extension forwards an allowed request to the selected upstream.
  6. Glove returns the response and appends the audit event.

Malformed frames, unknown tools, policy failures, transport errors, and audit append failures fail closed.

Wallet status bridge construction boundary

Glove contains a construction-only, status-only wallet bridge protocol. It is not registered as a general MCP extension and is not wired into gloved or a managed Pi launch. The host must supply an already-open per-session Unix socket as an inherited descriptor; the bridge never discovers or connects a Sage, Glove, P2P, provider, or wallet endpoint.

The private protocol processor accepts only wallet_status with a 64-KiB frame limit, an absolute receive-inclusive deadline, strict unknown-field rejection, and a host-derived running-session binding. The guest cannot submit session or plan digests. The processor owns four private authority interfaces for the current plan/session/policy, audit/journal and containment readiness, owner-local adapter digest, and stable wallet-server status. Every snapshot is bound to the same nonzero plan generation and absolute deadline; the plan is read again after the other snapshots to detect in-request revision. Missing, ambiguous, rolled-back, revised, or cross-boundary evidence makes the method unavailable. A single failed or over-deadline authority read terminates the status worker for that composition — the bridge never retries or resynchronizes transient authority faults, so composing live authorities requires deliberate retry or replacement policy at composition time. No public capability-discovery method is added by this milestone.

The private processor owns its clock, samples it before request decoding and after authority validation, and revalidates the exact session digests, policy revision, state, expiry, adapter and server digests, owner-local wallet-server alias, chain policy, audit/journal generations, and observation freshness. The authority contracts must reject durable generation reuse or rollback. The bridge keeps no global high-water state and performs no provider, Sage, P2P, wallet, or network call itself. The in-request plan re-read detects revision but does not by itself defend against generation-reuse (ABA) by an authority; that rejection belongs to the authority contracts and must be covered by adversarial authority tests before any live composition is approved.

Responses omit node identifiers, wallet addresses, RPC URLs, peer details, signatures, provider errors, session digests, and host paths. The only available action is status; the mutating-action list is structurally empty. Stale, future, mismatched, missing, failed, stopped, revised, or expired observations return no cached status.

This module is synthetic integration infrastructure, not activation evidence. Before typed Glaze decoding, a bounded recursive structural scan rejects malformed JSON, more than 64 nested containers, and repeated decoded object keys at every level. This includes equivalent escaped keys and keys inside the raw result object. A dedicated 64-KiB fuzz target and adversarial corpus keep that precondition exercised. Only deterministic offline authority fakes exist in this milestone.

A second private, disabled-by-default synthetic owner composes one unnamed Unix stream socketpair with the processor. Construction verifies the creator/owner UID and the creation-time socketpair peer UID (the same synthetic owner, not a future child identity), plus AF_UNIX, SOCK_STREAM, nonblocking mode, and FD_CLOEXEC on both endpoints. One sequential std::jthread exclusively owns the host endpoint. It uses one absolute request deadline for framing, authority processing, and response writing; cancellation, EOF, malformed or partial framing, oversized input, processing failure, backpressure timeout, session expiry, or destruction closes every endpoint still owned by the session without resynchronization. The guest endpoint can be transferred exactly once for in-process socketpair tests and remains CLOEXEC. No production launcher maps it into a child, no environment variable advertises it, and no managed Pi process is started by this milestone.

A future runtime composition must implement the authority interfaces with concrete protected registry, policy, audit/journal, adapter-digest, and status objects; add separately reviewed child descriptor mapping and unintended-FD closure; preserve the same absolute deadline; and prove descriptor containment before capability discovery may expose schema version 1. Live Sage/P2P/wallet connection remains a separate operator gate. Authority callbacks may request session close, but they must not destroy the session owner from its worker thread; live composition requires an independently owned lifetime.

A third private construction-only adapter projects that status into a minimal Pi-shaped tool descriptor and result. It defaults disabled, owns only a single-operation fake status channel, and re-attests through the existing host bridge on every listing and invocation. Disabled, stale, unavailable, mismatched, malformed, cancelled, or expired evidence yields no advertised tool; invocation rechecks and reports only generic absence. A cancellable, deadline-aware transaction gate covers lock acquisition, exchange, strict decoding, and result construction. The accepted tool has an exact empty-object schema, read-only/idempotent/closed-world annotations, and emits only the sanitized v1 status fields with available_actions fixed to ["status"] and an empty mutating_actions array. The adapter is not a kernel extension and is absent from Pi settings, native runtime registration, gloved, production factories, and capability discovery. Its fake-channel tests do not constitute child descriptor mapping or a live contained-Pi integration.

glove exec bypasses the MCP kernel. It is intended for agents that manage their own tool protocol, so its security boundary is the OS sandbox and explicit filesystem/environment exposure.

Sage session flow

gloved uses an owner-only Unix socket and a per-start bootstrap secret. Its public control methods provide capability discovery, canonical plan validation, durable create/status operations, bounded receipt pages, and exact acknowledgement.

glove setup creates owner-only XDG configuration, runtime/state directories, key material, and an optional protected-root policy. glove init then creates a generation-bound exposure through the authenticated local control socket. Project files are never configuration input, and raw paths never cross P2P.

glove daemon installs and controls a fixed per-user service through systemd on Linux or launchd on macOS. The CLI resolves the local gloved executable, binds it to the protected config, writes the service definition atomically, and invokes the service manager without a shell. It never accepts service labels, extra arguments, environment, or lifecycle requests from P2P. The Linux unit enters an unprivileged user namespace and a private mount namespace before executing gloved; that gives the current user namespace only the mount authority needed for quota-backed private session filesystems. Systemd separately delegates the cpu, memory, and pids controllers to the unit. Neither mechanism grants host-root authority.

When Sage configures glove_activation_mode = "user_service", saged first asks the platform user service manager to start the fixed local Glove unit. It waits for gloved.sock and bootstrap-secret before health and capability negotiation. The service label comes only from local Sage configuration and is restricted to a bounded identifier; remote requests cannot select a process, unit, or executable. connect_only preserves externally managed deployments.

When the Linux runtime is configured, the control service executes this lifecycle:

  1. Validate the canonical identifier-only plan against local policy.
  2. Persist the plan and both the controller BLAKE3 digest and Glove SHA-256 content digest.
  3. Reserve a session for preparation; live host writes are never eligible.
  4. Resolve generation-bound exposures and library bundles through pinned descriptors.
  5. Compose mounts, cgroup limits, output accounting, and an immutable launch binding.
  6. Start and recover the child through the executor and reconciler.
  7. Append an authenticated terminal receipt before projecting terminal state.

refinement-eval-v1 is a Glove-owned declarative evaluator on the shipping Linux managed-session backend. The model and harness receive no evidence writer. Before launch, Glove reads the exact fixture projection through its pinned read-only bundle descriptor, strictly decodes a non-executable fixture DSL, and removes that supervisor-only projection from the child mount set. The selected base or candidate skill projection remains an ordinary immutable read-only projection.

The PTY drain worker feeds every raw byte to the evaluator before circular transcript eviction. After clean drain and resource finalization, Glove checks the trusted termination, exit code, wall latency, UTF-8 stream state, and required/forbidden transcript literals, then synthesizes the canonical outcome. PTY JSON is only text and cannot become evidence. An incomplete stream or invalid UTF-8 produces authenticated invalid evidence; resource termination overrides otherwise satisfied transcript assertions.

The distinct refinement envelope shares the receipt journal's global sequence and HMAC head but uses its own receipt and envelope domains. Recovery and audit paging preserve the receipt kind, and the session registry refuses a resource V1 envelope for a refinement plan. Capability discovery reports schema version 1 only when the Linux runtime and protected library projection store were both constructed.

The local protocol exposes attach, input, resize, signal, detach, stop, and cleanup only when the constructed runtime reports lifecycle_operational(). Pointer presence is not capability evidence. The remote runtime currently reports false, schema version 0, an empty managed-runtime set, and unavailable resource mechanisms; every lifecycle dispatch returns method_not_found without starting SSH. Sage wires only operational lifecycle and receipt reconciliation. Exposure create/revoke remains owner-local; peers may receive only the redacted catalog. Retained-change inspection and independently authorized apply remain separate launch gates.

Construction-only remote backend

When remote_backend is present, gloved validates it as mutually exclusive with local materialization and Apple Container configuration. It requires the container identity as an untagged canonical name@sha256:<64 lowercase hex> reference whose suffix exactly equals container_image_digest, retains both values in the runtime composition, and rejects tags or mismatches. It writes an isolated OpenSSH config and pinned known_hosts beneath the already verified owner-only runtime directory, then constructs the non-operational runtime. This step performs no DNS lookup, network probe, authentication, SSH process launch, or Docker operation.

The generated SSH configuration ignores global known-hosts files, accepts only ssh-ed25519 host keys, disables host-key learning, and disables connection sharing with GlobalKnownHostsFile none, HostKeyAlgorithms ssh-ed25519, UpdateHostKeys no, ControlMaster no, ControlPath none, and ControlPersist no. The forced remote executable accepts exactly --stdio, sets umask 077, and reads its expected executable and image SHA-256 digests only from the administrator-owned /etc/glove/remote-executor.identity source. It measures its opened executable and fails closed on missing, empty, malformed, writable, or mismatched identity. Before each frame read, it captures the monotonic receive start and channel deadline. Request-TTL conversion subtracts all frame-receive time and rejects a frame that consumed its TTL; response writes use only the resulting request deadline. Only remote_health is available and it always reports not_operational; lifecycle methods return method_not_found. This is construction evidence, not remote-launch readiness.

Library projection

Sage plans refer to bundle digests and destination aliases, never arbitrary host paths. Local policy maps each alias to a protected sandbox target. Glove opens a digest-named bundle beneath an owner-only root without following links, verifies its type, ownership, link count, size, identity, and SHA-256 digest, then mounts the descriptor as a read-only sandbox file.

The launch binding and terminal receipt commit the projection identifier, destination, target, and digest. Bundle expansion into harness-native prompt directories is not implemented; prompt_ref remains rejected.

Guest specialized-channel admission

The durable per-session queue is schema-generic: Glove core enforces only structural invariants (identifier charset, digest hex, TTL/skew arithmetic, capacity, idempotency) and delegates body semantics to a host-registered admission table (channel_descriptor{channel_id, schema_id, body_validator, bounds} registered through a channel_host at registry construction). Schema strings never appear in core; a host registers the payload contracts its guests use, and replay of a durable intent whose schema is no longer registered fails closed.

The Sage guest adapter registers two closed logical schemas. The observation schema carries bounded non-signing observations. The sage.glove-sxxx-self-delegation-proposal.v1 schema carries only a proposal identifier, the fixed sxxx-self-delegation kind, one constant value digest, and item_count = 1. It carries no wallet, profile, chain, token, calldata, fee, nonce, Remote Procedure Call endpoint, credential, or operator proof.

Both registered schemas use the same channel token and authoritative session context, but their body contracts are distinct. Glove commits either body with the same length-prefixed digest algorithm and preserves its exact schema in the durable queue. Sage decides whether to build a final host intent. Glove never signs or broadcasts a transaction. Intents bind to the session's parsed runtime_id, so any managed guest runtime can carry a registered payload.

Persistence

Session and receipt journals are append-only and bounded. Recovery rejects corrupt, reordered, duplicated, truncated, or externally resized records. Authenticated terminal receipts form an HMAC chain anchored by Sage. The general JSONL activity log and session-state journal are not protected against a same-user host process that can rewrite them.

Detailed policy and file invariants are defined in session-policy.md.