The local-service proxy factory's operational identity rechecked the
runtime root with the exact dev/ino/uid/mode/nlink tuple, but a
directory's st_nlink is 2 plus its subdirectory count on ext4, tmpfs,
and APFS. Every prepare_session mkdirat(2)s a svc-* staging directory
under that root (and inode-safe teardown removes it), so the root's
link count necessarily changes and the factory itself - or any second
factory sharing the same runtime root - failed the next operational
check as false "identity drift". Proven on Ghost at base 4900bda4:
baseline LastTest.log line 666, root nlink 2 -> 3 verified by
instrumentation; the whole downstream local-service test flow was
blocked. The term could never distinguish sanctioned staging from
tampering: only subdirectory creation moves a directory's nlink, files
and symlinks do not, and writes to this owner-only root are trusted
operator authority per session-policy. The production runtime root is
also shared with the gloved instance lock and control socket, so entry
enumeration whitelisting would add no real enforcement claim.
Drop only the root nlink term from operational(): the root's
dev/ino/uid/mode stay exact (replacement and permission drift still
fail closed, covered by the existing chmod test), and the
descriptor-pinned endpoint parents and sockets retain the exact
dev/ino/uid/mode/nlink recheck in endpoint_current(), which is where
the recorded endpoint identity drift claim (architecture.md) lives.
Unknown drift remains fail-closed.
Add a deterministic regression: two factories sharing one runtime root,
and one factory preparing consecutive sessions, must stay operational
across a prepare_session, while chmod on the root still fails closed.
The test fails on pre-fix code (Ghost: operational() false at the first
post-prepare check).
While validating on Ghost (kernel 6.12), the test's downstream
guest-socket ABA section needed two determinism repairs of its own,
both previously unreachable there because the baseline always failed
earlier: resolve the session directory by its exact staged svc-* name
instead of readlink of the detached open_tree mount (6.12+ renders
detached mount roots as "/" in /proc/self/fd), and stage the ABA
replacement listener at a fresh path before swapping it in so the
replacement cannot recycle the just-freed socket inode (the documented
same-UID ABA caveat) and accidentally satisfy the identity check.
The local-service proxy session mount descriptor was a plain
fcntl(F_DUPFD_CLOEXEC) directory descriptor, but bind_session_mount
installs session mounts with move_mount(2), which accepts only a
detached (open_tree-cloned) mount. With local_service_proxy configured,
every session start failed at "attach session mount 'local-services':
Invalid argument" (Workflow #218 Ghost E2E). Clone the verified proxy
session directory with open_tree(OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC)
via the same SYS_open_tree pattern every other mount path uses.
Add a deterministic regression test: inside the existing
control_local_service_proxy test, move_mount(2) the produced descriptor
onto a scratch directory and require success - a plain directory
descriptor fails with EINVAL. Gate the test on a positively-identified
mount-authority probe (EPERM from open_tree returns skip 77) so hosts
without mount authority skip deterministically instead of failing.
MEDIUM 1: session_runtime::start() now returns an explicit
fresh-launch vs replay disposition (session_start_result), and
handle_start_session sets outcome.applied only for a fresh committed
launch. An authenticated idempotent start replay therefore never gains
connection-scoped teardown authority: a broken pipe during a replay can
no longer stop an already-running guest. The control audit event for a
replayed delivery carries the replay disposition
(start_session:replay:<session>).
MEDIUM 2: jsonl_sink truncation now accounts for record delimiters
exactly, so current_bytes_ always tracks the physical file; only
newline-terminated complete records are retained (no blank or partial
lines, bounded read window); the cap is a hard cap (an oversized event
is dropped with a structured operator note; a pre-existing oversized
journal is bounded with a bounded read at construction).
MEDIUM 3: the broken-pipe degrade test now synchronizes through a
mutex-protected condition-variable gate handshake that proves the
dispatch is parked inside the blocked gate before the client resets,
records the start only after the dispatch completes, and the
mark_stopping test destroys every registry lock holder (runtime,
protocol, registry) before the independent registry reopen so the
nonblocking exclusive flock can actually be acquired.
Closes the sol-audit blockers in dd7d3f3:
HIGH - degradation could not stop the guest. create() moved
config.runtime into the protocol and then stored the moved-from config,
so the degrade path always saw an empty runtime pointer and a guest
launched by start_session outlived its vanished controller connection.
The server now shares the live runtime with the protocol (copy on
compose, one owner of teardown authority), and a Linux-gated server test
drives the real create() -> handle_frame -> broken-pipe path and proves
the guest is torn down (fails on the pre-fix ownership bug).
MEDIUM - degradation was not bound to authentication/application. The
degrade path re-decoded the raw frame to extract start_session and its
session id, granting teardown authority with no auth/schema/deadline or
application evidence; a same-UID peer could reset the socket after
submitting a valid-shaped but unauthorized frame to trigger stop() on a
named session. handle_frame now emits structured
receipt_control_outcome metadata (authenticated / applied /
response_success / method / session_id) from the real dispatch, and
degrade_failed_delivery acts only on genuinely authenticated+applied
start_session outcomes; every other outcome records only the
connection-scoped audit event. The raw frame is never re-decoded.
MEDIUM - control-audit.jsonl guarantees. jsonl_sink::record() is now
bounded by an optional max_file_bytes policy with oldest-record
truncation (newest event always attempted); gloved configures the cap
(host::control_audit_max_file_bytes, 4 MiB) with a bound test.
record_control_delivery_audit() surfaces sink write failures as
structured stderr logs instead of discarding them (receipt-chain audit
guarantees stay fatal). The derived control-audit.jsonl path is
registered in all_configured_paths() so it cannot alias the receipt
journal, audit key, session store, or any other configured path, with a
config-distinctness test.
MEDIUM - tests missed the production failure mode. The degrade test now
constructs the real server through create() and drives a real
authenticated+applied start_session over the socket; the broken-pipe
test synchronizes on an observable (runtime recorded the applied start)
before the client resets, removing the reset-vs-write race; a protocol-
level test proves wrong-secret / expired-deadline / rejected / non-start
outcomes never stop the guest; the Linux executor test asserts the
durable mark_stopping ordering survives an independent registry reopen.
LOW - unified the duplicate wire::rpc_error / response envelope in
remote_protocol.cpp with receipt_wire.hpp (ODR hazard).
Validated: clang build + CTest 80/80 local; Ghost g++14.2 build and
CTest in a follow-up artifact. No push.
Two mechanical fixes from the Workflow #218 Ghost E2E evidence:
- gloved exited fatally when a control response could not be written back
to a vanished client (broken pipe after a supervisor timeout), turning a
client disconnect into a daemon crash. Transport-level write failures on
an already-authenticated connection are now connection-scoped: serve_one
reports receipt_audit_serve_outcome::connection_failed, the affected
session is degraded (structured `control` audit event in a new owner-only
control-audit.jsonl journal, idempotent guest teardown for start_session
via the normal stop path with its pending-before-release audit
guarantees), and gloved keeps serving subsequent connections. Listener,
peer-authentication, frame-integrity, and audit-chain failures still fail
closed and remain fatal.
- renamed class-name-shadowing members and TU-local wire types so GCC 14.2
accepts the tree under -Wchanges-meaning (hard error, clang-clean):
receipt_audit_unix_server_config::session_runtime -> runtime,
receipt_audit_protocol::implementation::session_runtime -> runtime,
wire::error -> rpc_error, control_wire::params -> rpc_params,
control_wire::exposure -> exposure_record,
setup_wire::apple_container_runtime -> apple_container_runtime_record.
Member (JSON wire key) names are unchanged.
Regression tests: verify_broken_pipe_delivery_is_connection_scoped (peer
closes with SO_LINGER 0; server must report connection_failed, record the
control audit event, and keep serving) and verify_degrade_failed_delivery
(start_session guests are torn down, failures stay non-fatal and audited).
Introduce the framed guest_channel_transport with shared deadlines and
std::jthread ownership, metadata-only retired-schema quarantine with
fail-closed integrity, stateless frozen-catalog validators with
capability pinned to zero, and the generic local-service proxy with
descriptor-pinned endpoints, alias-only guest sockets, audit-gated
delivery, and a Linux managed runtime capability derived only from a
registry-bound operational factory. Sage vocabulary is confined to
src/adapters/sage.
Delete the Glove-side synthetic wallet bridge, JSON codec, managed
session, and Pi status adapter plus their fuzz corpus; wallet
observation becomes a Sage-owned capability and Glove stays
harness-neutral.
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.
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>
Sage scoping finding (owner directive: Glove must stay general): the
reusable seam was named after a single harness. Rename the virtual
channel interface to wallet_status_projection_channel and the options
struct to wallet_status_projection_options; any guest harness may now
implement the seam. The concrete wallet_pi_status_adapter keeps its
Pi name with the explicit "Pi-shaped projection only" caveat, keeping
the general mechanism honest about which projections are Pi-specific.
No behavior change; mechanical rename. Validation (umask 0022):
dev CTest 79/79, ASan/UBSan 79/79, TSan 79/79; clang-format clean;
git diff --check clean.
GLM complementary review of the Apple repair layers found that the
noexcept finalize() catch-all set finished_ without error_ or
result_, so wait() dereferenced an empty optional (UB), and a
bad_alloc in the pre-join waitpid-error branch (system_error string
construction) could land in the catch-all with the sampler still
running.
Fix: the catch-all now stops and joins the sampler, preserves every
lease, and sets a static "internal error" string when allocation
permits; wait() additionally treats a finished state with neither
error nor result as a generic failure instead of dereferencing.
The catch-all is reachable only under a live Apple Container (fault
injection via allocation failure or authority/monitor failure), so
the deterministic repro is live-gated with the Apple activation
program; this fix is validated by the proven UB path, full-suite
re-run, and no-regression observation.
Validation (umask 0022): dev CTest 79/79, ASan/UBSan 79/79,
TSan 79/79; apple_container_stats, library_bundle, and
session_plan fuzzers 10,000 units each, all fuzz_exit=0;
clang-format clean; git diff --check clean.
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.
Pi-facing wallet status adapter: private, disabled by default,
status-only projection with strict redaction — no wallet secrets,
no mutation actions, no capability advertisement, unregistered on
the MCP surface. Includes the wallet_pi_status_adapter unit suite
and the argument-order repair for the managed-session invocation.
Layer 5 (final) of the reviewed Glove wallet-status stack.
Validation: dev CTest, ASan/UBSan, TSan, and libFuzzer 10,000 units
per target on this exact stacked tree (see workflow184 layer
records).
Wallet status managed session: exact current session/plan/policy
revalidation, owned trusted-clock zero/rollback/deadline/freshness
checks, and bounded 64-KiB SIGPIPE-safe Unix-socket framing over
the inherited channel. Fail-closed on missing readiness or malformed
observation. No wallet secrets or mutation authority.
Layer 4 of the reviewed Glove wallet-status stack; includes the
wallet_status_managed_session unit suite.
Validation: dev CTest, ASan/UBSan, TSan, and libFuzzer 10,000 units
on the final stacked tree (see workflow184 layer records).
Wallet status bridge: strict typed JSON wire, bounded channel, and
no-mutation status surface. Includes the wallet_status_json fuzzer
with a seed corpus (duplicate method keys, escaped duplicates,
malformed surrogates, nested duplicates, out-of-range requests,
unknown fields, and valid request/response shapes) and the
wallet_status_bridge unit suite.
Layer 3 of the reviewed Glove wallet-status stack; status-only,
disabled by default, and no capability advertisement.
Validation: dev CTest, ASan/UBSan, TSan, and libFuzzer 10,000 units
on the final stacked tree (see workflow184 layer records).
Merge PR #23 head 4097a21adb (Velinus's
"Make Apple resource sampling lifecycle safe") into the Layer-2
integration branch, preserving all contributor commits unchanged.
Retained proven fixes from 4097a21:
- Sampler creation moves into start_finalizer() so a sampler
std::system_error is reported synchronously to the caller. In
ca8600a the sampler was constructed inside finalize() after the
caller had treated startup as successful; the failure path
published an error without attach reaping, container deletion, or
durable mark_managed_failed, and set finished_ so stop() became a
no-op while attach/container survived.
Repairs retained beyond the contributor head, each bound to a
demonstrated failure:
- std::jthread + deterministic stop_token handles the partial-start
case where sampler construction succeeds but finalizer launch
fails (regression: tests/control/apple_container_unavailable_test
and the start_sampler_then_finalizer seam used by production).
- Restored hard-disabled Apple boundary: lifecycle_operational()
false, receipt schema 0, disk enforcement unavailable, empty
managed runtime advertisement, and public method denial before
any side effect. The current PR #23 head crossed the reviewed
activation boundary (schema 0 -> 1, filesystem_quota advertised,
lifecycle constructed-state true) without live-validation.
- glaze parsing hardened: strict_json_options now sets
null_terminated=false; non-NUL-terminated string_views fed
parse_apple_container_stats and duplicate_key_guard caused a
heap-buffer-overflow read in glz::skip_ws (ASan crash input
[{"id":"aanumProcesses" reproduced deterministically; seed corpus
fuzz/corpus/apple_container_stats/truncated-object.json added).
- Sage bundle projection admission hardening (Layer-1 2bba117) and
its test coverage carried through the merge.
Validation (umask 0022):
- Dev CTest 76/76; ASan/UBSan 76/76; TSan 76/76.
- libFuzzer 10,000 units each: library_bundle, session_plan,
apple_container_stats (LLVM 22, ASan/UBSan), all fuzz_exit=0.
- clang-format clean on changed TUs; git diff --check clean.
Preserve known non-skill metadata in projection commitments, reject duplicate and oversized bundles, close Codex exec grammar ambiguity, and keep raw-bundle requirements explicit. Keep layer-2 Sage materialization tests out of this layer.
Validated by the exact layer-1 build and 75-test suite; Apple lifecycle remains a separately skipped layer-2 concern.
Signed-off-by: 0xkite <me@hack4kitesdev.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>
Mixed libraries were failing start because any non-skill entry aborted the
projection. The managed sandbox bypass also has to sit after `exec` so
headless Codex can run inside Glove.
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.