import type { HostContext } from './system-prompt.js'; import { type PublicSpawnLimits } from './spawn-rate-limiter.js'; export interface HostResolution extends HostContext { /** Comma-separated list of detected interface names — non-null only when * lanIPv4 resolution failed. Surfaced as the `stderrTail` of a spawn * rejection so the operator sees what was searched. */ lanIPv4Diagnostic: string | null; } export interface ManagerConfig { port: number; persistDir: string; claudeBin: string; /** Absolute node binary for spawned channel-MCP children. On darwin the * launchd server PATH excludes an nvm node, so channel descriptors must name * node explicitly rather than relying on a bare `node` on PATH (Task 1402). * Falls back to bare `node` when NODE_BIN is unset (Linux, where node is on * the base PATH). */ nodeBin: string; spawnCwd: string; urlCaptureTimeoutMs: number; killGraceMs: number; jsonlReadMaxBytes: number; /** Per-account concurrent specialist (`--agent`) PTYs. (N+1)th attempt * evicts the oldest idle specialist; rejects 503 if none are idle. * Env override: `CLAUDE_SESSION_MANAGER_SPECIALIST_CAP`. Default 3. */ specialistCap: number; /** Slots reserved for operator (non-specialist) spawns. Specialist * spawns are rejected with 503 `operator-slots-reserved` once * `currentTotalLive >= totalPtyCap - operatorReserve`. Env: * `CLAUDE_SESSION_MANAGER_OPERATOR_RESERVE`. Default 2. */ operatorReserve: number; /** Hard ceiling on concurrent PTYs the manager will hold open. Taken * as the min of `/proc/sys/kernel/pty/max` (Linux only; absent on * Darwin) and `CLAUDE_SESSION_MANAGER_TOTAL_PTY_CAP` (default 64). */ totalPtyCap: number; /** Reaper TTL: a specialist row with `status: idle` and no JSONL whose * `updatedAt` is older than this is SIGTERMed. Env: * `CLAUDE_SESSION_MANAGER_RECORDER_IDLE_TTL_MS`. Default 120 000. */ recorderIdleTtlMs: number; host: HostResolution; /** per-account directory resolved at boot. Mirrors the * installer's `resolveInstallAccountId` walk: the first sub-directory of * `/data/accounts/` containing `account.json`. Per-spawn the * composer reads `/agents//{IDENTITY.md,SOUL.md}` and * injects them as `` / `` sentinels in the * `--append-system-prompt` block; absence of an account dir refuses * manager start with `account-dir-unresolved` (no silent fallback to * stock Claude Code). */ accountDir: string; /** per-`role=public` spawn rate limit. Loaded from * `brand.json.publicSpawnLimits` if present, else defaults; env vars * `MAXY_PUBLIC_SPAWN_CAP` / `MAXY_PUBLIC_SPAWN_WINDOW_MS` override every * per-channel bucket (operator escape hatch). */ publicSpawnLimits: PublicSpawnLimits; /** `brand.json.productName` ("Maxy", "Real Agent"). Used as the * `claude rc` session-name prefix so the Remote Control device renders * as ` · ` in the composer instead of the OS hostname. */ brandProductName: string; /** Brand slug — `brand.json.serviceName` minus the `.service` suffix * ("maxy-code", "realagent-code"). Matches the `brands/` directory * names, so a per-brand diagnostic line at boot shares one token space * with the cross-brand audit script. */ brandSlug: string; } /** The outcome of the boot account-dir walk. Task 2020 — `resolveAccountDir` * used to collapse every non-resolving path into one `null`, and `loadConfig` * turned that `null` into one message: no `account.json` exists under * `data/accounts`, run the installer. Two of the three failures state the * opposite of the filesystem when that message is printed, and on the * two-house branch the remedy it names would provision a third house. The * three failures are now distinct values, each carrying what the operator * needs to retire the drift. */ export type AccountDirDiagnosis = { kind: 'resolved'; dir: string; } /** No directory under `accountsRoot` holds an `account.json` — including the * cases where the root is absent or unreadable. A genuinely empty registry. */ | { kind: 'no-accounts'; accountsRoot: string; } /** No account carries `role:"house"` and more than one candidate exists, so * the pre-migration single-account tolerance cannot apply. */ | { kind: 'zero-house-ambiguous'; accountsRoot: string; candidateIds: string[]; } /** More than one account carries `role:"house"`. */ | { kind: 'multiple-houses'; accountsRoot: string; houseIds: string[]; }; /** Resolve `/data/accounts/` to the boot account dir under the * managed-service model: the single `role:"house"` account, or the sole * account in the pre-migration window (before `setup-account.sh` stamps the * role). Mirrors the installer's `resolveInstallAccountId` so the runtime reads * the same account the installer wrote. Reports no accounts and the two genuine * drift conditions (zero-house with >1 candidate, multiple houses) as distinct * kinds; the caller surfaces `account-dir-unresolved` naming the kind and * refuses to start, and the boot census (`[account-registry] op=census`) makes * the drift visible in journalctl. * * Ids are directory basenames, which are the account ids — the same identity * `isHouseAccount` compares against. * * `installDir` = parent of `PLATFORM_ROOT` (the systemd unit passes * `PLATFORM_ROOT=/platform`, so `dirname()` is exact). */ export declare function diagnoseAccountDir(platformRoot: string): AccountDirDiagnosis; /** The `string | null` face of `diagnoseAccountDir`, kept so `isHouseAccount` * and `cross-account-census.ts` read house-ness the one way they always have. */ export declare function resolveAccountDir(platformRoot: string): string | null; /** The `account-dir-unresolved` boot-refusal message for a non-resolving * diagnosis. Every branch keeps the `account-dir-unresolved` token so existing * greps still hit, and carries `reason=` so the three failures are * distinguishable from the log line alone. Only `no-accounts` names the * installer: on the two drift branches re-running it adds another account and * deepens the fault. Throws on a resolved diagnosis — there is no failure to * describe, and silently returning prose would let a caller print it. */ export declare function formatAccountDirUnresolved(d: AccountDirDiagnosis): string; /** Registry census — house / client / total counts of the valid accounts under * `accountsRoot`. A dir counts when it holds a parseable `account.json`; * `clients` counts `role:"client"`, and an account with any other or absent * role (the pre-migration unlabelled window) is neither house nor client but * still counted in `total`. Reads roles fresh — house designation is * runtime-mutable via the lifecycle tools. Mirrors `resolveAccountDir`'s inline * scan (the manager has no rootDir-clean import path for the account-enumeration * lib). A missing accounts root reads as an empty registry, not a throw. */ export interface RegistryCensus { houses: number; clients: number; total: number; } export declare function censusAccountsRoot(accountsRoot: string): RegistryCensus; /** The standing census log line body. `houses != 1` is the drift signature the * boot emitter checks. Pure formatter so the emitter and its test share one * wording. */ export declare function formatRegistryCensus(c: RegistryCensus): string; /** True iff `accountId` is the install's house account, by the same resolution * the boot path uses (single role:"house", or the sole pre-migration account). * Returns false on drift (zero-house-with->1-candidate, or multiple houses), * matching `resolveAccountDir`. Task 1440 — house-ness has one definition. */ export declare function isHouseAccount(platformRoot: string, accountId: string): boolean; /** The `HOUSE_ADMIN_SCOPE` value stamped into a spawn, or null when the spawn * must NOT carry cross-account authority. Non-null only for a non-specialist * role:"admin" session on the house account — a specialist/subagent under the * house account must not inherit house authority, and a sub-account admin is * excluded by `isHouse`. Pure: callers pass the filesystem-derived `isHouse` * (via `isHouseAccount`) so this stays testable. Task 1440. */ export declare function houseAdminScopeFor(input: { role: 'admin' | 'public'; accountId: string; isHouse: boolean; isSpecialist: boolean; }): string | null; /** Stamp the resolved house-admin scope onto a spawn's PTY process env + its * key list (mutates both; no-op when scope is null). HOUSE_ADMIN_SCOPE MUST * ride the same process env as ACCOUNT_ID — installed-plugin MCP children * inherit it and read it via `readHouseAdminScope(process.env)`. It is NOT * placed in the per-spawn `.mcp.json` placeholder map, which admin/specialist * spawns no longer write (Task 502). Named so the env surface is unit-testable. * Task 1440. */ export declare function applyHouseAdminScopeEnv(env: NodeJS.ProcessEnv, envKeys: string[], scope: string | null): void; /** The per-spawn `[xacct] op=house-scope-inject …` decision line, emitted by * BOTH spawn paths: pty-spawner's `spawnClaudeSession` (the claude.ai/code PTY * path) and the `/rc-spawn` handler (the channel-admin path). The fleet * observability contract depends on the two lines staying byte-identical — a * grep on one sessionId must show the same columns whichever path spawned it — * so the format lives here once instead of in two hand-copied literals. A null * `sessionId` (an rc-spawn resume that has no id yet) renders `new`; a real id * is sliced to its first 8 chars, so pty-spawner (always a real id) is * unchanged. `sessionRole` is widened to allow `undefined` because rc-spawn's * `rcRole` is `undefined` for a non-admin/non-public request; the original * literal stringified that to `undefined`, and this reproduces it exactly. * Task 1468. */ export declare function formatHouseScopeInjectLine(input: { sessionId: string | null | undefined; sessionRole: string | undefined; acctIsHouse: boolean; inject: boolean; }): string; /** Task 1322 — the account a live/detached session is actually running * under. `SessionRow.cwd` (fs-watcher.ts) is read straight from claude's * own PID file, independent of the manager's own boot-fixed sidecar-read * scope. Task 1315's standing invariant (env.ACCOUNT_DIR === * effectiveAccountDir at spawn) means `cwd`'s basename IS the account id * whenever it names a directory in the registry. Returns null when `cwd` * is absent (e.g. an archived row with no PID-derived cwd) or doesn't * match a registered account. */ export declare function accountIdFromSessionCwd(cwd: string | null, accountsRoot: string): string | null; export declare function loadConfig(env?: NodeJS.ProcessEnv): ManagerConfig; //# sourceMappingURL=config.d.ts.map