GLM complementary review remediation on the wallet stack:
- The Pi status adapter content string now reflects the structured
connected flag ("Wallet is not connected.") instead of an
unconditional "Wallet status is available." that contradicted
connected:false; bound by a new REQUIRE on the disconnected path.
- The wallet_status_json fuzzer clock now advances its monotonic
component from fuzz input, so fuzzed frames traverse the deeper
revalidation ladder and deadline paths instead of a single fixed-
authority success path.
- docs/architecture.md: documented the one-strike authority teardown
contract (no retry/resync by the bridge) and the ABA generation-
reuse defense living in authority contracts, gated on adversarial
authority tests before any live composition.
Validation (umask 0022): dev CTest 79/79, ASan/UBSan 79/79,
TSan 79/79; wallet_status_json fuzzer 10,000 units fuzz_exit=0;
clang-format clean; git diff --check clean.
315 lines
19 KiB
Markdown
315 lines
19 KiB
Markdown
# 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
|
|
|
|
```text
|
|
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.
|
|
|
|
## 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](session-policy.md).
|