Add the fail-closed recovery regression: reopening a store holding
durable observation intents must fail without the registered schema
table — neither a null channel host nor an empty one (no registered
schemas) can admit the frozen intents, so recovery is refused at the
session_registry_impl.hpp channel guard.
Cut speculative generality with no consumer: the channel_descriptor
on_exchange callback and its exchange_hook plumbing in
enqueue_observation_intent return when the wallet-bridge unification
lands; likewise channel_host::catalog() and channel_catalog_entry had
zero callers and no authorized RPC.
Consolidate the identical 128-byte bounded charset identifier and
64-hex digest grammar (previously duplicated in guest_channel.cpp,
session_registry_impl.hpp, and observation_intent_unix_server.cpp)
into src/control/channel_identifier_grammar.hpp without changing
semantics, and document the cached-replay vs registry-replay response
divergence plus that channel_id is registration metadata only
(registry admission matches by schema_id).
Generalizes the PR #23 guest observation plumbing into a schema-agnostic
specialized-channel layer while keeping all harness policy out of core.
Generalized admission (F2/HIGH):
- New channel_descriptor/channel_host interface (include/glove/control/
guest_channel.hpp): hosts register {channel_id, schema_id,
body_validator, bounds{max_items, max_body_bytes, max_ttl_ms,
max_skew_ms}, on_exchange} at registry construction; core keeps only
structural invariants (identifier charset, digest hex, TTL/skew
arithmetic, capacity, idempotency) and delegates body semantics to
registered validators.
- The closed two-schema check in session_registry internals is removed:
glove_observation_schema, glove_sxxx_proposal_* constants and the
fixed SXXX digest move out of session_registry_impl.hpp. The Sage
schemas are now registered by the harness adapter
(apple_detail::sage_guest_channel_host) and gloved_main wires the host
into the registry; schema strings appear only in harness-adapter and
test-fixture registration examples.
- enqueue_observation_intent binds intents to the session's PARSED
runtime_id; the hardcoded sage_guest_runtime equality checks are
removed from session_registry.cpp, session_registry_recovery.cpp and
valid_record_shape. Replay of a now-unregistered schema fails closed
(open_or_create rejects stores carrying unadmitted intents), and
enqueue without an admission table is rejected. The observation
socket server takes runtime_id from configuration instead of
hardcoding "sage-guest". The observation body hash domain becomes the
harness-neutral "glove.observation-body.v1" (pre-merge delta; no
on-disk compatibility is preserved).
Recovery fix (F1/HIGH):
- same_session_snapshot now neutralizes state, running_at_ms and
policy_revision before comparing frozen intent/disposition snapshots
against the bound session record, so lifecycle progression and policy
revision advances never read as a binding crossing; every other
frozen-field difference still does. Exposed for tests. Regression
tests cover running->stopping and stopping->exited transitions plus a
late post-exit disposition end to end (destroy/reopen), and a
snapshot-identity unit test pins the neutralization (and proves
non-neutralized fields still cross). Note: an empirical probe showed
the previously-claimed failure does not reproduce under ordered
journal replay because intent/disposition records freeze the session
record they were appended from; the neutralization is applied as the
defensive fix the memo requires and is now pinned by tests.
Hardening:
- F3: peer-uid verification (SO_PEERCRED / LOCAL_PEERCRED) on observation
socket accepts, configurable expected uid, rejecting unexpected peers
before any frame is read.
- F4: the replay cache is a bounded LRU (configurable capacity), so the
old fixed 1024 cap can no longer permanently kill the channel; the
durable registry remains the authoritative idempotency layer.
- F5: transient wait/accept errors (EINTR/EAGAIN/ECONNABORTED/EMFILE/
ENFILE/ENOBUFS/ENOMEM) are retried inside serve_one_for with a bounded
backoff ladder; only persistent or fatal errors surface. The Apple
worker loop absorbs persistent failures up to a consecutive-failure
bound and emits an audit event (new audit action "observation") on
repeated failures instead of dying on the first hiccup.
- F6: env/mount names generalized to GLOVE_GUEST_OBSERVATION_SOCKET /
GLOVE_GUEST_OBSERVATION_TOKEN_FILE and the in-container token path to
/home/agent/.glove-observation-token (Apple runtime, docs).
Apple boundary: the soft lifecycle_operational() definition is unchanged;
the lifecycle-disabled lane now additionally asserts start_session/
stop_session deny with method_not_found and leave the registry untouched
(denial before side effects; the observation service only constructs
inside a lifecycle-gated start).
Docs: architecture.md and session-policy.md describe the guest
specialized-channel admission model and the peer-uid/env naming.
Validation (umask 0022):
- cmake --preset dev/asan/tsan + build + ctest: 100% (76/76) in each
configuration; asan/tsan are warnings-as-errors builds.
- git diff --check clean; changed files clang-formatted with Homebrew
llvm@22 against the repo .clang-format.
- No fuzz target exists for the queue/registry (fuzz/ covers plans,
bundles, manifests, MCP codec, jsonpath only), so no fuzz corpus run.
- control_remote_validation_executor is a pre-existing load-sensitive
sleep-race test; it flakes identically on the unmodified pr23-head
baseline under CPU load (verified 1/6 both trees) and always passes
isolated.
Files: 23 changed (+~1500/-~360), including new guest_channel.{hpp,cpp}.
Signed-off-by: 0xkite <me@hack4kites.dev>
Bind digest-addressed Sage bundles and immutable guest identity into the constrained Apple Container runtime.
Co-authored-by: Cursor <cursoragent@cursor.com>
Enable platform-neutral control dispatch and bind Apple Container projection evidence into authenticated terminal receipts.
Co-authored-by: Cursor <cursoragent@cursor.com>
Revalidation walked immutable payloads depth-first while planning hashed a flat lexicographic path sequence. Sort bounded descriptor-safe records before hashing so valid adopted Pi packages retain their canonical identity.