import type { KeychainIO } from './claude-keychain-write.js'; import type { Logger, PtyHandle } from './types.js'; import type { ToolSurface } from './tool-surface.js'; export interface RcDaemonDeps { claudeBin: string; spawnCwd: string; accountDir: string; accountId: string; /** Admin operator's userId, sourced from users.json at boot. Optional — * absent on pre-onboarding installs where users.json does not yet exist. * Stamped as USER_ID in the child env so MCP plugins that key reads by * (accountId, userId) — e.g. scheduling — receive a non-empty value. */ userId?: string; /** brand.json.productName ("Maxy", "Real Agent"). Passed as the * `--remote-control-session-name-prefix` so composer sessions render as * ` · ` instead of the OS hostname default. */ brandProductName: string; claudeConfigDir: string; platformRoot: string; logDir: string; brandedNeo4jUri: string; brandedNeo4jPassword: string; /** MAXY_UI_INTERNAL_PORT — the loopback port the manager's JSONL tail * posts specialist log lines to (`/api/admin/log-ingest`). */ uiPort: string; toolSurface: ToolSurface; logger: Logger; /** Task 573 — node-pty PTY spawn primitive shared with rc-spawn. The * manager holds one master fd per live daemon; node-pty allocates a * real TTY for the child, exit detection runs on the pty's `onExit`, * and the fast-exit output tail comes from a bounded ring buffer * filled by `onData`. The earlier non-PTY scope primitive * (Tasks 552/556) is gone — its activation gate misread the scope's * brief `inactive` window as a child crash, and the retained * typescript file was redundant once node-pty exposes the child's * stdout/stderr directly. `index.ts` wires the same `spawnPtyAdapter` * used for `/rc-spawn`. */ spawnPty: (cmd: string, args: string[], env: NodeJS.ProcessEnv, cwd: string, unitToken: string) => PtyHandle; /** Task 573 — resolves once `claude rc` writes its on-disk PID file * at `/sessions/.json`. Presence of this file * is the success signal for the daemon (rejected = pidfile never * appeared within `pidFileTimeoutMs`). `index.ts` wires * `watcher.waitForPid`. */ waitForPid: (pid: number, timeoutMs: number) => Promise<{ pidFilePath: string | null; }>; /** Timeout for the post-spawn PID-file wait. Matches the rc-spawn * surface's `pidFileTimeoutMs`. */ pidFileTimeoutMs?: number; /** Injectable clock for tests. Defaults to Date.now. */ now?: () => number; /** Injectable timer for tests. Defaults to setTimeout. */ setTimer?: (fn: () => void, ms: number) => unknown; /** Process env source — injectable so tests don't mutate process.env. */ env?: NodeJS.ProcessEnv; /** Neo4j readiness probe. Returns true once Neo4j is accepting TCP * connections on the bolt port parsed from `brandedNeo4jUri`. Defaults to * a `node:net` connect with a 5s timeout. Injectable for tests. A thrown * rejection is treated as not-ready, never an unhandled rejection. */ checkNeo4jReady?: (uri: string) => Promise; /** Interval between readiness probes. Default 2000ms. */ neo4jReadyPollIntervalMs?: number; /** Bounded readiness wait. After this elapses with Neo4j never reachable, the * daemon stops fast-polling and re-arms a slower periodic re-probe (see * neo4jRearmProbeIntervalMs) rather than waiting forever at the fast cadence. * Default 300000ms (5 min) — comfortably exceeds a real Neo4j cold-start * after a host reboot. */ neo4jReadyTimeoutMs?: number; /** Re-arm re-probe interval. After the bounded readiness wait * (neo4jReadyTimeoutMs) elapses with Neo4j never reachable, the daemon does * NOT permanently-fail — it re-arms a periodic re-probe at this slower * cadence until Neo4j returns (then spawns) or stop() is called. Default * 30000ms. Injectable for tests. */ neo4jRearmProbeIntervalMs?: number; } export interface RcDaemon { stop(): void; /** Task 562 — count of currently-live daemon scope handles (0 or 1). * Exposed for the rc-life census + shutdown audit so the shared * observability schema can report a daemon's lifecycle alongside * the script-scope rc-spawn tracker. */ alive(): number; } export declare function resolveRcDaemonCapacity(totalMemBytes: number): number; export declare const RC_DAEMON_CAPACITY: number; /** Parse the bolt host:port from a Neo4j URI (`bolt://`, `neo4j://`, * `bolt+s://`, …). Returns null when no explicit `host:port` is present — * the caller then skips the readiness gate rather than probing a guessed * port, which on a non-default brand could falsely pass against another * brand's Neo4j (brand isolation is absolute). */ export declare function parseBoltHostPort(uri: string): { host: string; port: number; } | null; /** Read-merge-write settings.json so only the `effortLevel` key is owned. Every * other key (model, hooks, permissions, …) is preserved verbatim. Mirrors * `mergeSettingsModel`'s ownership + atomic-write contract — the file is shared * with maxy-spawned sessions and carries the installed Stop hook. */ export declare function mergeSettingsEffortLevel(claudeConfigDir: string, effortLevel: string, logger: Logger): void; /** Read-merge-write settings.json owning ONLY the nested * `permissions.defaultMode` key. Every sibling permissions key (allow, deny, * …) and every top-level key (model, effortLevel, hooks, …) is preserved. * `permissions` is created when absent. This is what the binary and the Task * 798 lifeline (`resolveSettingsPermissionMode`) read. Atomic tmp+rename * mirrors `mergeSettingsModel`. The `siblings=` log count makes a clobber * observable (a drop to 0 with prior siblings is the clobber signature). */ export declare function mergeSettingsPermissionMode(claudeConfigDir: string, mode: string, logger: Logger): void; /** Task 578 — pre-seed RC consent. `claude rc` prompts * `Enable Remote Control? (y/n)` on first run; the daemon spawn is * headless so the prompt hangs and the supervisor restarts the child * until it permanently-fails. Writing `remoteControlAtStartup: true` * into `$CLAUDE_CONFIG_DIR/.claude.json` is the same mechanism * claude itself uses when the operator answers `y`, so the prompt is * never reached on subsequent spawns either. Read-merge-write owns * only this one key; siblings (`teammateMode`, `hasUsedRemoteControl`, * `remoteControlUpsellSeenCount`, claude's own auth blocks) are * preserved. Atomic write mirrors `mergeSettingsModel`. */ export declare function ensureRemoteControlConsent(claudeConfigDir: string, logger: Logger): void; /** Task 578 — substring scan over a `claude rc` PTY output chunk that * returns true once the daemon has reached its post-bind handshake. * Two markers, both emitted by the daemon's own output after RC * registers: the literal "Capacity:" header, and a capacity line of * the form " · · N of M" the composer surface prints * (operator-observed `1 of 32`). The `· N of M` form is preceded by * the `·` separator the composer surface uses; matching on `· N of M` * rejects unrelated prose like "retry 1 of 3" that happens to share * the N-of-M shape. A chunk that contains neither marker is pre-bind * output (boot banner, consent prompt, version line) and the flag * stays false. Pure function so it can be unit-tested without a real * PTY. */ export declare function detectRcHandshake(chunk: string): boolean; /** Read-merge-write settings.json so only the `model` key is owned. Every * other key (hooks, permissions, env, statusLine, …) is preserved verbatim * — the file is shared with maxy-spawned sessions and carries the installed * Stop hook. A missing/unparseable file starts from an empty object. */ export declare function mergeSettingsModel(claudeConfigDir: string, adminModel: string, logger: Logger): void; /** Task 797 — on-demand re-run of the boot-time lever merge. The /chat * composer's settings write updates account.json (UI-owned), then the * manager re-applies the levers into settings.json here so the NEXT * spawned session honours them without a daemon restart. Same functions, * same key ownership, same atomic-write contract as boot. */ export declare function reapplyAdminLevers(_accountDir: string, claudeConfigDir: string, logger: Logger): { model: string | null; effortLevel: string | null; permissionMode: string | null; }; /** Fields the rc-daemon child env and any sibling fire-and-forget rc spawn * (Task 543 — sessions-pane resume + new-session) share. Both inherit the * same env shape (MAXY_SESSION_ROLE, MAXY_UI_INTERNAL_PORT, ACCOUNT_ID) so * the installed admin hooks fire and the MCP children resolve their * account. */ export interface RcChildEnvParts { accountId: string; userId?: string; /** Per-session conversation id (Task 606). Threaded by the `/rc-spawn` * route so session-linked writes (schedule-event PART_OF, memory * createdBySession) resolve `process.env.SESSION_ID`. Absent on the * shared long-lived daemon (it is not a conversation) and on the rare * CLI fresh-spawn with no requested id — both yield SESSION_ID=''. */ sessionId?: string; uiPort: string; platformRoot: string; claudeConfigDir: string; brandedNeo4jUri: string; brandedNeo4jPassword: string; /** Brand log dir (Task 606). Carried so `mcp-spawn-tee` writes its readable * raw sink `mcp--.log` (or `mcp--nosession.log` when * sessionId is absent — see SESSION_ID below); dropped from the rc child env * at the Task 500 migration, which masked the boot signal. `mcp-spawn-tee` * is the sole reader of LOG_DIR for this sink. */ logDir: string; /** Task 626 — public session-end review runs on `/rc-spawn`. When set, the * child stamps `RECORDER_SLICE_TOKEN` so the `database-operator` it * dispatches via Task writes inside the visitor's slice (`memory-write` * stamps sliceToken + scope=user). No read-scope env — the public read path * was retired (Task 654). Absent on the daemon and on channel-admin / * LinkedIn rc spawns. */ sliceToken?: string; /** Task 1764 — when true, stamp `CLAUDE_CODE_REMOTE_ENVIRONMENT_TYPE=remote` * so `SendUserFile` is enabled independent of the REPL bridge. Set for the * operator surfaces (admin `/rc-spawn` resumes, the `claude rc` daemon); * left false/unset for `role='public'` spawns (the public session-end * review), which Task 1724 scoped out. Absent defaults to no stamp, so a * future caller that forgets fails closed to the old bridge-gated behaviour, * never to a public leak. Mirrors pty-spawner's `remoteControl` gate. */ remoteControl?: boolean; /** Task 1466 — the HOUSE_ADMIN_SCOPE the `/rc-spawn` handler computed for this * spawn (via `houseAdminScopeFor`), threaded so the channel-admin claude PTY * and its installed-plugin MCP children carry cross-account authority — the * same var `spawnClaudeSession` injects via `applyHouseAdminScopeEnv`. Non-null * only for a non-specialist role:admin session on the house account. Absent on * the long-lived daemon (`buildDaemonEnv` threads nothing) and on the pure * env-shape unit tests. Set here iff non-null; an empty value is never stamped * (readers treat empty as no scope). */ houseAdminScope?: string | null; env?: NodeJS.ProcessEnv; /** Task 1406 — when set, applyHostCredsEnv emits its op=spawn-hostcreds line * through this logger. Threaded from the rc-daemon start (buildDaemonEnv) and * the /rc-spawn HTTP handler so a darwin child's resolved auth is visible at * every spawn. Absent = no line (e.g. the pure env-shape unit tests). */ logger?: (line: string) => void; } /** Compose the env carried by the rc-daemon child and by Task 543's * sessions-pane spawn route. Exported so the route's HTTP handler can * build a byte-identical env without duplicating the field set. */ export declare function buildRcChildEnv(parts: RcChildEnvParts, platform?: NodeJS.Platform, io?: KeychainIO): NodeJS.ProcessEnv; /** Task 1297 — true when the running kernel's cgroup-v2 root has the * `memory` controller in its list. `controllers` is the raw content of * /sys/fs/cgroup/cgroup.controllers. Pure — the impure read (and its * fallback to false on a missing/unreadable file, e.g. a non-Linux host) * lives at the call site below. Mirrors (negated) * memoryControllerMissing() in * packages/create-maxy-code/src/cgroup-memory-controller.ts; future * divergence between the two should be caught by the test suite. */ export declare function memoryControllerInList(controllers: string): boolean; export declare function startRcDaemon(deps: RcDaemonDeps): RcDaemon; //# sourceMappingURL=rc-daemon.d.ts.map