Keep gloved in a systemd-owned supervisor leaf while creating sibling session cgroups under the empty delegated parent. Under mapped-root supervisors, attach blocked children before releasing their existing start gate instead of combining nested UID mapping with CLONE_INTO_CGROUP.
361 lines
22 KiB
Markdown
361 lines
22 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.
|
|
|
|
## Historical synthetic status milestone
|
|
|
|
The former construction-only synthetic status stack and harness-shaped adapter were
|
|
removed because they had no production composition or child descriptor mapping. Any
|
|
future ephemeral query service must be built at a live composition seam and may share
|
|
only the harness-neutral bounded guest-channel transport; payload semantics,
|
|
persistence, and harness projection remain outside that transport.
|
|
|
|
The shared `guest_channel_transport` owns one connected Unix stream descriptor,
|
|
verifies `AF_UNIX`, `SOCK_STREAM`, and the required peer UID, and performs only
|
|
bounded four-byte big-endian framed I/O. Each receive or send uses its caller's
|
|
absolute steady-clock deadline and stop token. The transport is single-thread
|
|
confined and has no handler, exchange, or public close API; its owner stops and
|
|
joins the worker before destruction closes the descriptor, avoiding concurrent
|
|
close and descriptor-reuse races.
|
|
|
|
The observation server carries one absolute accept deadline through every retry
|
|
and stop-aware transient backoff. After accept, receive and response send share
|
|
one absolute I/O deadline. Stop/deadline checks immediately surround registry
|
|
handling, but registry persistence is trusted synchronous local work and cannot
|
|
be forcibly interrupted once started. The hard bound therefore covers accept,
|
|
framed I/O, and cooperative cancellation—not arbitrary handler execution.
|
|
|
|
The Linux local-service proxy remains schema-generic. The compatibility
|
|
transport, selected when an adapter omits `transport_id`, mounts only
|
|
runtime-allowed alias sockets from a private owner-only directory and forwards
|
|
one bounded G2 request/response without parsing the payload. Upstream parents
|
|
are descriptor-pinned and endpoint identities are drift-checked around connect.
|
|
Because portable pathname Unix sockets cannot prevent same-UID ABA replacement,
|
|
the operator and service-UID processes are trusted endpoint authority; the
|
|
checks do not authenticate an application process. Resolved path grants are
|
|
compared by descriptor identity against every endpoint parent ancestor before
|
|
filesystem materialization, preventing a grant from exposing the host socket
|
|
directly.
|
|
|
|
An adapter may instead select `inherited-stream-v1`. Glove creates one
|
|
owner-`0600` connected `AF_UNIX`/`SOCK_STREAM` socketpair for the exact
|
|
adapter-bound alias and keeps the supervisor peer outside the sandbox. Clone
|
|
setup loads seccomp with the committed contiguous child-descriptor range, then
|
|
installs the guest peer at descriptor 3 after internal setup descriptors close.
|
|
Offline `socket`, `connect`, `setsockopt`, and peer inspection remain denied;
|
|
only `getsockname` and `getsockopt(SOL_SOCKET, SO_TYPE)` are permitted on the
|
|
exact committed inherited descriptors so Node/libuv can classify the already
|
|
connected stream. The child receives only the compact one-entry
|
|
`GLOVE_LOCAL_SERVICE_FDS_V1` alias-to-descriptor map; no proxy path, other
|
|
configured local service, or host endpoint metadata is projected. Clone setup
|
|
preserves exactly that descriptor and closes every gap and all undeclared
|
|
descriptors. The persistent stream accepts sequential existing G2 frames,
|
|
rejects empty, oversized, trailing, or queued input at the request boundary and
|
|
immediately before and after response release, uses one absolute deadline per
|
|
exchange, and is poisoned on transport, identity, upstream, framing, timeout,
|
|
or cancellation failure. The managed-launch binding commits the selector,
|
|
alias and child descriptor, both socket identities, and manifest digest.
|
|
User-supplied `GLOVE_LOCAL_SERVICE_*` variables and mixed pathname/inherited
|
|
authority fail closed.
|
|
|
|
Guest-channel semantics enter only through an opaque adapter binding resolved
|
|
in `src/adapters/`. A generic or arbitrarily named `pi` endpoint does not imply
|
|
a Sage schema. The sealed capability requires `inherited-stream-v1` plus the
|
|
exact adapter binding, registry catalog, factory, concrete Linux runtime, and
|
|
adapter/runtime endpoint intersection. Endpoint checks are construction and
|
|
drift evidence, not process-authentication evidence.
|
|
|
|
Proxy audit uses a conservative three-phase sequence: durable non-success
|
|
`delivery_pending` before release, a clean post-audit request boundary before
|
|
send, `delivered` only after successful guest send plus an immediate clean
|
|
post-send boundary, and best-effort `delivery_failed` after failure. Synchronous
|
|
audit persistence checks the exchange deadline before and after the append, but
|
|
a kernel-blocked `fsync(2)` is not cooperatively preemptible. A late append
|
|
cannot release the response. Stream scheduling cannot prove whether a byte that
|
|
arrives after the final post-send peek was written before or after the peer
|
|
observed the response; that byte is never processed concurrently and the next
|
|
request boundary still applies. A protocol that needs causal no-pipelining proof
|
|
must add an explicit versioned credit or acknowledgement.
|
|
|
|
`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. On systemd 254 or newer the generated unit places `gloved` in the fixed
|
|
`glove-host` delegate subgroup. Glove pins that subgroup and its parent cgroup,
|
|
requires owner identity and process exclusivity, enables the three controllers
|
|
only on the empty delegated parent, and creates session cgroups as siblings of
|
|
the supervisor subgroup. This avoids moving the supervisor during service
|
|
startup while preserving a process-free controller node. When the supervisor
|
|
itself has a non-identity UID map, the kernel cannot combine nested UID mapping
|
|
with `CLONE_INTO_CGROUP`; Glove instead creates a child whose first operation is
|
|
a blocking read on a private sync pipe, writes the nested UID/GID maps, attaches
|
|
the blocked child to its sealed session cgroup, and releases it only after the
|
|
durable start gate succeeds. No child setup or agent instruction executes
|
|
before cgroup attachment. 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{schema_id, body_validator, bounds}`). A
|
|
host must register every payload contract, propagate registration failures, and
|
|
freeze the nonempty `channel_host` before registry construction. Schema strings
|
|
never appear in core. Current admission rejects enqueue for every unregistered
|
|
schema. Only an authentic intent already present in durable history may recover
|
|
after its schema retires, and then solely as non-actionable quarantine metadata
|
|
that cannot be enqueued again or launched.
|
|
|
|
The explicit Sage harness adapter under `src/adapters/sage/` registers only the
|
|
bounded, non-signing `sage.glove-observation.v1` schema currently required for
|
|
registry admission. Dormant proposal or mutation schemas are not registered, so
|
|
current enqueue rejects them. Authentic historical records using a subsequently
|
|
retired schema recover only as non-actionable quarantine metadata.
|
|
|
|
Glove commits admitted bodies with the shared length-prefixed digest algorithm
|
|
and preserves the exact schema in the durable queue. Intents bind to the
|
|
session's parsed `runtime_id`, so any managed guest runtime can carry a
|
|
registered payload.
|
|
|
|
Capability discovery does not infer observation ingress from registry storage.
|
|
On Linux, schema version `1` requires an opaque capability produced only by
|
|
sealing the concrete lifecycle runtime against the exact registry, frozen
|
|
catalog, preparer-owned proxy factory, managed runtime intersection, complete
|
|
resource mechanisms, descriptor-pinned endpoint parents, and current rechecked
|
|
socket identities. The opaque capability owns the runtime, which owns its
|
|
preparer and registry; the preparer owns the factory, and the factory owns the
|
|
same registry. No edge points back to the capability or runtime. Generic,
|
|
remote, Apple, mismatched, unsealed, absent-config, and drifted compositions
|
|
report `0`.
|
|
|
|
The optional local-service proxy descriptor-pins each configured owner-`0700`
|
|
parent and records and rechecks the owner-only Unix socket's exact
|
|
`dev/ino/uid/mode/nlink` before and after every connect. An allowed session gets
|
|
one fresh owner-only directory mounted read-only at
|
|
`/run/glove-services/local` and the fixed `GLOVE_LOCAL_SERVICE_DIR` environment.
|
|
Alias listeners forward one bounded G2-framed opaque request and response under
|
|
one absolute deadline with peer-UID checks on both sides. A fixed worker set is
|
|
joined before inode-safe socket and directory cleanup. The launch digest commits
|
|
the sorted runtime-filtered aliases, descriptor-pinned parent identities,
|
|
recorded socket identities, and factory generation; no durable registry or
|
|
receipt schema changes.
|
|
|
|
## 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).
|