Skip to content
This repository was archived by the owner on Sep 19, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@
],
"dependencies": {
"@agentclientprotocol/sdk": "1.4.0",
"@hraness/local-custody": "0.5.1",
"@hraness/local-custody": "0.6.0",
"@hraness/oh": "0.10.8",
"@openai/codex": "0.153.2",
"convex": "1.45.0",
Expand Down
6 changes: 3 additions & 3 deletions scripts/package-policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -131,9 +131,9 @@ export async function assertProductionPackageOnly(

export async function assertReviewedReleaseInventory(packageRoot: string): Promise<void> {
const expected = Object.freeze({
count: 234,
jsonBytes: 11_558,
sha256: "a6ddd4e33d97a857334e3fe682896b8ba1121305a82fc89bd0f98e2a8c914dc8",
count: 235,
jsonBytes: 11_607,
sha256: "f24680adad7934ba885afb0e8143acb5e61dda2f461acc52aaa91afb32c02f9d",
});
const inventory: Array<readonly [string, "directory" | "file", number, number]> = [];
const visit = async (path: string): Promise<void> => {
Expand Down
6 changes: 5 additions & 1 deletion src/claude/account.ts
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,11 @@ async function readAccountMetadataDocument(path: string): Promise<unknown> {
// The personal-home path is user-controlled and can change between scans.
// The stable read opens nonblocking so a FIFO swapped in before stat cannot
// stall daemon admission, then re-proves identity and metadata after the
// bounded read.
// bounded read. A missing document must surface as a raw `ENOENT`
// `ErrnoException` so the projection reads null rather than stale, while a
// permissions failure keeps failing closed; the local-custody Rust engine
// reports both as `CustodyError` domain failures without the errno, so this
// read keeps the direct TypeScript import.
let bytes: Buffer;
try {
bytes = (await readOwnedFileStable(path, ACCOUNT_DOCUMENT_MAX_BYTES)).bytes;
Expand Down
1 change: 1 addition & 0 deletions src/daemon/AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Contents

- The daemon hosts the local command authority, long-running provider processes, and opaque session-memory lifecycle coordination.
- `custody-engine.ts` holds the one process-wide local-custody engine: it prefers the packaged Rust sidecar and falls back per operation. Custody checks whose callers branch on `ENOENT` keep the direct TypeScript imports because the sidecar reports a missing path as a `CustodyError`, not an `ErrnoException`.
- One session has one exact provider binding at a time. A durable switch journal alone may replace that binding; existing effects and interactions retain their original authority. The service selects the captured provider's `SessionRuntimePort` for start, turns, steering, interrupt, projection reads and interactions. Provider facts use one neutral timeline vocabulary.
- The Unix socket transports one bounded authenticated request at a time.
- The explicit live-acceptance daemon composition exposes a structural observer for actual personal-provider children. Its acceptance implementation and bounded status policy live under `scripts/`; the reusable exact-child adapter lives under `src/claude/`.
Expand Down
10 changes: 9 additions & 1 deletion src/daemon/claude-host-tool-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type {
ClaudeHostToolCallbackHandler,
} from "../claude/index.ts";
import { ensurePrivateDirectory, type StatePaths } from "../storage/paths.ts";
import { localCustodyEngine } from "./custody-engine.ts";

const CALLBACK_SOCKET_NAME = "claude-host-tools.sock";
const CALLBACK_REQUEST_MAX_BYTES = 4 * 1_024 * 1_024;
Expand All @@ -25,10 +26,17 @@ export class ClaudeHostToolTransportShutdownTimeoutError extends Error {
export const claudeHostToolCallbackSocketPath = (paths: StatePaths): string =>
join(paths.runtime, CALLBACK_SOCKET_NAME);

// The socket was just bound and chmodded, so a missing path is not a distinct
// outcome here; the custody engine's Rust sidecar owns this re-validation.
const assertPrivateSocket = async (path: string): Promise<void> => {
await assertOwnedPath(path, { kind: "socket", exactMode: 0o600 });
const custody = await localCustodyEngine();
await custody.assertOwnedPath(path, { kind: "socket", exactMode: 0o600 });
};

// A missing endpoint must surface as a raw `ENOENT` `ErrnoException` so the
// stale-socket path is skipped; the custody engine reports a missing path as
// a `CustodyError` domain failure (sidecar code `stat`), so this check keeps
// the direct TypeScript import.
const removeStaleSocket = async (path: string): Promise<void> => {
try {
await assertOwnedPath(path, { kind: "socket" });
Expand Down
23 changes: 23 additions & 0 deletions src/daemon/custody-engine.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import {
loadLocalCustodyRustEngine,
type LocalCustodyRustEngine,
} from "@hraness/local-custody/custody-rust";

let enginePromise: Promise<LocalCustodyRustEngine> | undefined;

/**
* The one process-wide local-custody engine. The loader probes the packaged
* Rust sidecar once; each delegated operation then prefers the sidecar and
* falls back to the TypeScript implementation operation by operation, with a
* bounded `local-custody-rust-fallback` stderr notice when it does.
*
* Only operations whose observable contract survives the engine qualify here:
* the sidecar reports a missing path as a `CustodyError` domain failure with
* a `stat`/`open` code, never an `ENOENT` `ErrnoException`, so custody checks
* whose callers branch on `error.code === "ENOENT"` keep the direct
* `@hraness/local-custody` imports instead of routing through this engine.
*/
export function localCustodyEngine(): Promise<LocalCustodyRustEngine> {
enginePromise ??= loadLocalCustodyRustEngine();
return enginePromise;
}
11 changes: 9 additions & 2 deletions src/daemon/local-transport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { createConnection, createServer, type Server, type Socket } from "node:n
import { basename, dirname } from "node:path";

import { assertOwnedPath, readOwnedFileStable } from "@hraness/local-custody/private-paths";
import { publishPrivateFile } from "@hraness/local-custody/atomic-publish";
import {
commandEnvelopeSchema,
commandResponseSchema,
Expand All @@ -16,6 +15,7 @@ import {
type LocalCommand,
} from "../domain/contracts";
import { ensurePrivateDirectory, type StatePaths } from "../storage/paths";
import { localCustodyEngine } from "./custody-engine";

const maximumRequestBytes = LOCAL_COMMAND_REQUEST_MAX_BYTES;
const maximumResponseBytes = LOCAL_COMMAND_RESPONSE_MAX_BYTES;
Expand Down Expand Up @@ -75,6 +75,12 @@ const boundedTimeoutMs = (value: number | undefined, fallback: number): number =
return candidate;
};

// Every caller of this validator treats a raw `ENOENT` `ErrnoException` as
// "the endpoint is absent": stale-endpoint cleanup skips it and the client
// maps it onto `LocalDaemonUnavailableError`. The custody engine reports a
// missing path as a `CustodyError` domain failure (sidecar code `stat`), not
// `ENOENT`, so these checks keep the direct TypeScript import; the engine
// covers only operations where a missing path is not a distinct outcome.
async function validateOwnedFile(path: string, kind: "file" | "socket", mode?: number): Promise<void> {
try {
await assertOwnedPath(path, { kind, ...(mode === undefined ? {} : { exactMode: mode }) });
Expand All @@ -96,7 +102,8 @@ async function removeStaleEndpoint(paths: StatePaths): Promise<void> {
}

async function publishCapability(paths: StatePaths, capability: string): Promise<void> {
await publishPrivateFile(dirname(paths.capability), basename(paths.capability), `${capability}\n`);
const custody = await localCustodyEngine();
await custody.publishPrivateFile(dirname(paths.capability), basename(paths.capability), `${capability}\n`);
}

const publicFailureCodes = [
Expand Down
2 changes: 1 addition & 1 deletion src/install-preflight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -859,7 +859,7 @@ describe("transactional Oompa installer", () => {
test("strips only dependency maps from the private installer fixture", () => {
expect(sourcePackageManifest.dependencies).toEqual({
"@agentclientprotocol/sdk": "1.4.0",
"@hraness/local-custody": "0.5.1",
"@hraness/local-custody": "0.6.0",
"@hraness/oh": "0.10.8",
"@openai/codex": "0.153.2",
convex: "1.45.0",
Expand Down
Loading