@crouter/api 0.3.386 → 0.3.388

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/dist/api/__tests__/integration/client.test.js +97 -0
  2. package/dist/api/client.d.ts +7 -0
  3. package/dist/api/client.js +40 -21
  4. package/dist/core/asset-root.d.ts +7 -0
  5. package/dist/core/asset-root.js +18 -0
  6. package/dist/core/canvas/boot-id.d.ts +6 -0
  7. package/dist/core/canvas/boot-id.js +26 -0
  8. package/dist/core/canvas/paths.d.ts +72 -0
  9. package/dist/core/canvas/paths.js +163 -0
  10. package/dist/core/canvas/pid.d.ts +391 -0
  11. package/dist/core/canvas/pid.js +948 -0
  12. package/dist/core/command-plugins/bundle.d.ts +149 -0
  13. package/dist/core/command-plugins/bundle.js +588 -0
  14. package/dist/core/command-plugins/endpoint.d.ts +24 -0
  15. package/dist/core/command-plugins/endpoint.js +51 -0
  16. package/dist/core/config.d.ts +233 -0
  17. package/dist/core/config.js +1120 -0
  18. package/dist/core/env-name.d.ts +6 -0
  19. package/dist/core/env-name.js +9 -0
  20. package/dist/core/errors.d.ts +38 -0
  21. package/dist/core/errors.js +90 -0
  22. package/dist/core/events/emit.d.ts +6 -0
  23. package/dist/core/events/emit.js +42 -0
  24. package/dist/core/events/envelope.d.ts +2 -0
  25. package/dist/core/events/envelope.js +84 -0
  26. package/dist/core/events/errors.d.ts +4 -0
  27. package/dist/core/events/errors.js +69 -0
  28. package/dist/core/events/operation-id.d.ts +4 -0
  29. package/dist/core/events/operation-id.js +24 -0
  30. package/dist/core/events/serialize.d.ts +4 -0
  31. package/dist/core/events/serialize.js +199 -0
  32. package/dist/core/events/source.d.ts +16 -0
  33. package/dist/core/events/source.js +31 -0
  34. package/dist/core/events/types.d.ts +68 -0
  35. package/dist/core/events/types.js +11 -0
  36. package/dist/core/exclusive-lock.d.ts +34 -0
  37. package/dist/core/exclusive-lock.js +197 -0
  38. package/dist/core/fs-utils.d.ts +44 -0
  39. package/dist/core/fs-utils.js +208 -0
  40. package/dist/core/help.d.ts +309 -0
  41. package/dist/core/help.js +406 -0
  42. package/dist/core/human/page-catalog.d.ts +57 -0
  43. package/dist/core/human/page-catalog.js +172 -0
  44. package/dist/core/installed-plugins.d.ts +2 -0
  45. package/dist/core/installed-plugins.js +79 -0
  46. package/dist/core/io.d.ts +122 -0
  47. package/dist/core/io.js +373 -0
  48. package/dist/core/keybindings/attach-control.d.ts +49 -0
  49. package/dist/core/keybindings/attach-control.js +42 -0
  50. package/dist/core/keybindings/catalog.d.ts +18 -0
  51. package/dist/core/keybindings/catalog.js +257 -0
  52. package/dist/core/keybindings/types.d.ts +42 -0
  53. package/dist/core/keybindings/types.js +1 -0
  54. package/dist/core/layout.d.ts +26 -0
  55. package/dist/core/layout.js +94 -0
  56. package/dist/core/locked-file.d.ts +27 -0
  57. package/dist/core/locked-file.js +118 -0
  58. package/dist/core/log.d.ts +9 -0
  59. package/dist/core/log.js +89 -0
  60. package/dist/core/manifest.d.ts +5 -0
  61. package/dist/core/manifest.js +15 -0
  62. package/dist/core/plugin-env.d.ts +8 -0
  63. package/dist/core/plugin-env.js +31 -0
  64. package/dist/core/plugin-extensions.d.ts +29 -0
  65. package/dist/core/plugin-extensions.js +191 -0
  66. package/dist/core/plugin-swap-lock.d.ts +9 -0
  67. package/dist/core/plugin-swap-lock.js +31 -0
  68. package/dist/core/preview-result-path.d.ts +4 -0
  69. package/dist/core/preview-result-path.js +26 -0
  70. package/dist/core/profiles/env-store.d.ts +22 -0
  71. package/dist/core/profiles/env-store.js +163 -0
  72. package/dist/core/profiles/fuzzy-match.d.ts +19 -0
  73. package/dist/core/profiles/fuzzy-match.js +92 -0
  74. package/dist/core/profiles/manifest.d.ts +120 -0
  75. package/dist/core/profiles/manifest.js +529 -0
  76. package/dist/core/rate-limit-scope.d.ts +25 -0
  77. package/dist/core/rate-limit-scope.js +64 -0
  78. package/dist/core/render.d.ts +12 -0
  79. package/dist/core/render.js +138 -0
  80. package/dist/core/resolver.d.ts +14 -0
  81. package/dist/core/resolver.js +111 -0
  82. package/dist/core/runtime/branded-host.d.ts +25 -0
  83. package/dist/core/runtime/branded-host.js +264 -0
  84. package/dist/core/runtime/broker/daemon-ops.d.ts +65 -0
  85. package/dist/core/runtime/broker/daemon-ops.js +177 -0
  86. package/dist/core/runtime/broker/signal-stream.d.ts +30 -0
  87. package/dist/core/runtime/broker/signal-stream.js +149 -0
  88. package/dist/core/scope.d.ts +32 -0
  89. package/dist/core/scope.js +184 -0
  90. package/dist/core/scoped-state/db.d.ts +17 -0
  91. package/dist/core/scoped-state/db.js +247 -0
  92. package/dist/core/scoped-state/migrate.d.ts +8 -0
  93. package/dist/core/scoped-state/migrate.js +187 -0
  94. package/dist/core/scoped-state/paths.d.ts +9 -0
  95. package/dist/core/scoped-state/paths.js +27 -0
  96. package/dist/core/scoped-state/profiles.d.ts +27 -0
  97. package/dist/core/scoped-state/profiles.js +93 -0
  98. package/dist/core/scoped-state/providers.d.ts +24 -0
  99. package/dist/core/scoped-state/providers.js +19 -0
  100. package/dist/core/scoped-state/schema.d.ts +6 -0
  101. package/dist/core/scoped-state/schema.js +43 -0
  102. package/dist/core/scoped-state/settings.d.ts +28 -0
  103. package/dist/core/scoped-state/settings.js +83 -0
  104. package/dist/core/spaces/open-beneath.d.ts +71 -0
  105. package/dist/core/spaces/open-beneath.js +581 -0
  106. package/dist/core/sqlite-statements.d.ts +4 -0
  107. package/dist/core/sqlite-statements.js +17 -0
  108. package/dist/core/subscription-state.d.ts +121 -0
  109. package/dist/core/subscription-state.js +287 -0
  110. package/dist/core/user-settings.d.ts +377 -0
  111. package/dist/core/user-settings.js +458 -0
  112. package/dist/daemon/broker-signals/bus.d.ts +30 -0
  113. package/dist/daemon/broker-signals/bus.js +87 -0
  114. package/dist/daemon/manage.d.ts +176 -0
  115. package/dist/daemon/manage.js +664 -0
  116. package/dist/daemon/pidfile.d.ts +8 -0
  117. package/dist/daemon/pidfile.js +37 -0
  118. package/dist/daemon/startup-policy.d.ts +1 -0
  119. package/dist/daemon/startup-policy.js +1 -0
  120. package/dist/native/linux.d.ts +29 -0
  121. package/dist/native/linux.js +20 -0
  122. package/dist/shared/env.d.ts +116 -0
  123. package/dist/shared/env.js +271 -0
  124. package/dist/shared/inbox-entry-body.d.ts +22 -0
  125. package/dist/shared/inbox-entry-body.js +116 -0
  126. package/dist/shared/working-activity.d.ts +9 -0
  127. package/dist/shared/working-activity.js +27 -0
  128. package/dist/types.d.ts +562 -0
  129. package/dist/types.js +186 -0
  130. package/package.json +1 -1
@@ -0,0 +1,37 @@
1
+ // pidfile.ts — the lean crtrd pidfile + liveness helpers, split out of crtrd.ts
2
+ // so the CLI's daemon front door (`daemon/manage.ts` → `ensureDaemon`) can
3
+ // resolve "is the daemon up?" WITHOUT importing crtrd.ts, whose module graph
4
+ // pulls the entire canvas state store (`core/canvas/index.js` → `openDb`). A
5
+ // `crtr` CLI process asking whether crtrd is running must not, by that question
6
+ // alone, open canvas.db — this module keeps that path fs/process-only.
7
+ //
8
+ // PURITY: Node built-ins + `core/canvas/paths.ts` (pure path builder) + the
9
+ // db-free `core/canvas/pid.ts` liveness probe. Never `openDb` / crtrd.ts / any
10
+ // canvas state accessor. crtrd.ts imports + re-exports these to preserve its
11
+ // historical export surface (tests, `commands/sys/daemon.ts`).
12
+ import { existsSync, readFileSync } from 'node:fs';
13
+ import { join } from 'node:path';
14
+ import { crtrHome } from '../core/canvas/paths.js';
15
+ import { isPidAlive } from '../core/canvas/pid.js';
16
+ import { envPidfileOverride } from '../shared/env.js';
17
+ // Re-exported so `isPidAlive` stays reachable from the daemon layer's public
18
+ // surface (crtrd.ts already re-exports it; callers may reach it either way).
19
+ export { isPidAlive };
20
+ /** Absolute path to crtrd's pidfile. `CRTR_PIDFILE` overrides for tests. */
21
+ export function pidfilePath() {
22
+ return envPidfileOverride() ?? join(crtrHome(), 'crtrd.pid');
23
+ }
24
+ /** Read the pid stored in the pidfile, or null if absent / malformed. */
25
+ export function readPidfile() {
26
+ const p = pidfilePath();
27
+ if (!existsSync(p))
28
+ return null;
29
+ const raw = readFileSync(p, 'utf8').trim();
30
+ const n = Number(raw);
31
+ return Number.isFinite(n) && n > 0 ? n : null;
32
+ }
33
+ /** True when a crtrd process is already running (pidfile exists + pid alive). */
34
+ export function isDaemonRunning() {
35
+ const pid = readPidfile();
36
+ return pid !== null && isPidAlive(pid);
37
+ }
@@ -0,0 +1 @@
1
+ export declare const DAEMON_STARTUP_WINDOW_MS: number;
@@ -0,0 +1 @@
1
+ export const DAEMON_STARTUP_WINDOW_MS = 5 * 60 * 1_000;
@@ -0,0 +1,29 @@
1
+ import type { Socket } from 'node:net';
2
+ export interface LinuxNative {
3
+ peerCred(fd: number): {
4
+ pid: number;
5
+ uid: number;
6
+ gid: number;
7
+ };
8
+ openat2(dirfd: number, path: string, flags: number, resolve: number): number;
9
+ fdPath(fd: number): string;
10
+ fstat(fd: number): {
11
+ uid: number;
12
+ gid: number;
13
+ mode: number;
14
+ nlink: number;
15
+ dev: bigint;
16
+ ino: bigint;
17
+ };
18
+ sendFds(socketFd: number, data: string, fds: number[]): void;
19
+ recvFds(socketFd: number): {
20
+ data: string;
21
+ fds: number[];
22
+ };
23
+ }
24
+ export declare const O_PATH = 2097152;
25
+ export declare const O_CLOEXEC = 524288;
26
+ export declare const RESOLVE_NO_SYMLINKS = 4;
27
+ export declare const RESOLVE_BENEATH = 8;
28
+ export declare function loadLinuxNative(): LinuxNative | undefined;
29
+ export declare function peerUid(socket: Socket): number;
@@ -0,0 +1,20 @@
1
+ import { createRequire } from 'node:module';
2
+ export const O_PATH = 0x200000;
3
+ export const O_CLOEXEC = 0x80000;
4
+ export const RESOLVE_NO_SYMLINKS = 0x04;
5
+ export const RESOLVE_BENEATH = 0x08;
6
+ export function loadLinuxNative() {
7
+ if (process.platform !== 'linux')
8
+ return undefined;
9
+ // One prebuilt addon per architecture (see scripts/ensure-native-linux.mjs).
10
+ return createRequire(import.meta.url)(`./linux/${process.arch}/addon.node`);
11
+ }
12
+ export function peerUid(socket) {
13
+ const fd = socket._handle?.fd;
14
+ if (fd === undefined || fd < 0)
15
+ throw new Error('unix socket peer fd is unavailable');
16
+ const native = loadLinuxNative();
17
+ if (!native)
18
+ throw new Error('Linux peer credentials are unavailable');
19
+ return native.peerCred(fd).uid;
20
+ }
@@ -0,0 +1,116 @@
1
+ /** The current node's id (`CRTR_NODE_ID`), or undefined outside a node. */
2
+ export declare function envNodeId(): string | undefined;
3
+ /** Find the single launch snapshot mounted by the launcher into this process's
4
+ * Linux namespace. Cron bash has no CRTR_NODE_ID, but its creator's mounted
5
+ * launch snapshot supplies config and granted-command projection without
6
+ * claiming the cron is a node. Its broker relay may already have closed when
7
+ * the cron fires; still classify the sandbox so CLI bootstrap never falls back
8
+ * to host stores or person authority. Neither CRTR_NODE_ID, CRTR_ANCHOR, nor
9
+ * the caller-editable CRTR_API_SOCKET selects the launch identity. */
10
+ export declare function sandboxCliNodeId(): string | undefined;
11
+ /** A Linux app command uses its mounted launch snapshot, not host user stores. */
12
+ export declare function isSandboxedNode(): boolean;
13
+ /** The app that owns the current node, supplied in its broker launch identity. */
14
+ export declare function envGrantee(): string | undefined;
15
+ /** Set to '1' by `nodeEnv` for an app-started run's brokers, which admit inbox
16
+ * mail only between agent runs. Terminal nodes also carry `CRTR_RUN_ID`, so
17
+ * that variable is not this signal. */
18
+ export declare const INBOX_BETWEEN_TURNS_ENV = "CRTR_INBOX_BETWEEN_TURNS";
19
+ export declare function envInboxBetweenTurns(): boolean;
20
+ /** The current run's scope allow-list; null means the row inherits everything. */
21
+ export declare function envScopes(): readonly string[] | null;
22
+ /** Whether a launched scope list covers a capability. Null means not narrowed. */
23
+ export declare function scopeAllowed(scopes: readonly string[] | null | undefined, scope: string): boolean;
24
+ export declare function envExecutionId(): string | undefined;
25
+ /** The current node's target profile id (`CRTR_PROFILE_ID`), or undefined. */
26
+ export declare function envProfileId(): string | undefined;
27
+ /** The current node's working directory (`CRTR_NODE_CWD`), or undefined. */
28
+ export declare function envNodeCwd(): string | undefined;
29
+ /** An explicit model-routing override for this turn (`CRTR_MODEL_INTENT`), or
30
+ * undefined. Cleared (not read) via `delete process.env['CRTR_MODEL_INTENT']`
31
+ * in `runtime/broker.ts` — a mutation, not a read, so it stays there. */
32
+ export declare function envModelIntent(): string | undefined;
33
+ /** Whether the launch recipe requires this broker to keep its exact provider/model. */
34
+ export declare function envModelExact(): boolean;
35
+ /** The raw `CRTR_HOME` override, unresolved. Use `crtrHome()`
36
+ * (`core/canvas/paths.ts`) to get the resolved canvas-home path; use this
37
+ * only when a caller needs the override itself — to check whether one is
38
+ * set, or to propagate it verbatim into a child process's env. */
39
+ export declare function envHomeOverride(): string | undefined;
40
+ /** The raw installed-runtime directory override, unresolved. */
41
+ export declare function envRuntimeHomeOverride(): string | undefined;
42
+ /** Dead-provider watchdog timeout (`CRTR_STREAM_WATCHDOG_MS`), 5 minutes by
43
+ * default. See `runtime/stream-watchdog.ts` for what it guards. */
44
+ export declare function envStreamWatchdogMs(): number;
45
+ /** The canvas inbox watcher's poll tick (`CRTR_WATCHER_TICK_MS`). */
46
+ export declare function envWatcherTickMs(): number;
47
+ /** Claim backstop while broker signals are connected (`CRTR_WATCHER_BACKSTOP_MS`). */
48
+ export declare function envWatcherBackstopMs(): number;
49
+ /** The canvas inbox watcher's debounce window (`CRTR_WATCHER_DEBOUNCE_MS`). */
50
+ export declare function envWatcherDebounceMs(): number;
51
+ /** No-new-message window before a recap is shown (`CRTR_RECAP_IDLE_MS`), 60s
52
+ * by default. */
53
+ export declare function envRecapIdleMs(): number;
54
+ /** Explicit recap-model override (`CRTR_RECAP_MODEL`), trimmed; undefined if
55
+ * unset or blank. */
56
+ export declare function envRecapModelOverride(): string | undefined;
57
+ /** The bash safety-valve deadline (`CRTR_BASH_VALVE_MS`), 5 minutes by
58
+ * default; undocumented test-only override so the E2E valve check doesn't
59
+ * sleep 5 minutes. */
60
+ export declare function envBashValveMs(): number;
61
+ /** How long an AUTO-backgrounded bash job may run before the daemon cancels it
62
+ * unless the agent claims it (`CRTR_BASH_AUTO_BG_DEADLINE_MS`), 60s by default;
63
+ * undocumented test-only override so the deadline check doesn't wait a minute. */
64
+ export declare function envBashAutoBgDeadlineMs(): number;
65
+ /** Explicit pidfile path override (`CRTR_PIDFILE`, tests only); undefined if
66
+ * unset or blank. */
67
+ export declare function envPidfileOverride(): string | undefined;
68
+ /** The broker engine module spec (`CRTR_BROKER_ENGINE`, the T11 test seam),
69
+ * defaulting to the real SDK. `runtime/host.ts` layers `inv.env` ahead of
70
+ * this (a launch-time override mirrored into the child's env, since
71
+ * spawn-env.ts's default-deny allowlist strips `CRTR_*` from ambient env). */
72
+ export declare function envBrokerEngine(): string;
73
+ /** Test-only pi-binary substitution (`CRTR_PI_BINARY`) so the integration
74
+ * harness can point a real `crtr node new` at a deterministic fake-pi
75
+ * vehicle. Undefined in production. */
76
+ export declare function envPiBinaryOverride(): string | undefined;
77
+ /** Whether implicit daemon autostart is suppressed for this invocation
78
+ * (`CRTR_NO_DAEMON_AUTOSTART=1`). */
79
+ export declare function envNoDaemonAutostart(): boolean;
80
+ /** The `--tcp` fallback for crtrd's opt-in TCP listener (`CRTRD_TCP`), raw
81
+ * (`host:port` or undefined — absent means unix socket only). */
82
+ export declare function envCrtrdTcp(): string | undefined;
83
+ /** The opt-in bearer token guarding crtrd's TCP listener (`CRTRD_TOKEN`).
84
+ * Unset (or blank) → undefined, and the TCP listener performs NO auth check
85
+ * at all — exactly today's behavior. The unix socket never checks this,
86
+ * set or not. */
87
+ export declare function envCrtrdToken(): string | undefined;
88
+ /** Kill switch for the host-exports writer/pruner (`CRTR_NO_EXPORTS=1`). */
89
+ export declare function envNoExports(): boolean;
90
+ /** Verbose-diagnostics gate for otherwise-silent best-effort catches
91
+ * (`CRTR_DEBUG=1`). Distinct from `CRTR_DEBUG_PID_LIVENESS`
92
+ * (`core/canvas/pid.ts`), a different, unrelated var. */
93
+ export declare function envDebug(): boolean;
94
+ /** Live-broker automatic-revive cap override (`CRTR_MAX_LIVE_BROKERS`); wins
95
+ * over `readConfig('user').brokerThresholds.automaticReviveCap` when set. */
96
+ export declare function envMaxLiveBrokers(): number | undefined;
97
+ /** Live-broker warning threshold override (`CRTR_WARN_LIVE_BROKERS`); wins
98
+ * over `readConfig('user').brokerThresholds.warning` when set. */
99
+ export declare function envWarnLiveBrokers(): number | undefined;
100
+ /** Test-only unattended-park interval override (`CRTR_TEST_UNATTENDED_PARK_MS`),
101
+ * gated by `envNoDaemonAutostart()` at the call site; no production default. */
102
+ export declare function envTestUnattendedParkMs(): number | undefined;
103
+ /** Production unattended-park interval override (`CRTR_UNATTENDED_PARK_MS`), in
104
+ * milliseconds; wins over the user-scope config value
105
+ * (`readConfig('user').lifecycle.unattendedParkMs`, itself defaulting to 15
106
+ * min) whenever set, autostart or not. Unlike `CRTR_TEST_UNATTENDED_PARK_MS`
107
+ * this is not gated on `envNoDaemonAutostart()`: it is the env escape hatch
108
+ * above the configured knob a deployment sets to tune how long an eligible
109
+ * resident idles before its parking turn. */
110
+ export declare function envUnattendedParkMs(): number | undefined;
111
+ /** Test-only park-summary grace-period override
112
+ * (`CRTR_TEST_PARK_SUMMARY_GRACE_MS`); no production default. */
113
+ export declare function envTestParkSummaryGraceMs(): number | undefined;
114
+ /** The bounded lifetime shared by the daemon's pending-park sweep and its
115
+ * in-broker isolated turn. */
116
+ export declare function parkSummaryGraceMs(): number;
@@ -0,0 +1,271 @@
1
+ // The reader-side surface for every environment variable crouter reads back
2
+ // out of `process.env`, outside `core/runtime/spawn-env.ts`'s writer
3
+ // allowlist. That module documents what a broker child is ADMITTED to see;
4
+ // this one documents what crouter itself reads. Two families:
5
+ //
6
+ // - Identity: raw passthrough (`string | undefined`, exactly what
7
+ // `process.env[name]` returns). Every existing call site keeps its own
8
+ // established fallback (`?? ''`, `|| null`, `?? cfg.cwd`, a presence
9
+ // check, ...) — the fallback has always been call-site-owned, not
10
+ // var-owned, so centralizing it here would be a behavior change, not a
11
+ // dedup. `CRTR_HOME` also has a resolved-path accessor, `crtrHome()` in
12
+ // `core/canvas/paths.ts`; that stays the canonical way to get the
13
+ // resolved canvas home; `envHomeOverride()` here is for the handful of
14
+ // sites that want the raw override value itself (to check presence or
15
+ // propagate it verbatim to a child env).
16
+ //
17
+ // - Tuning: parsed, with the default baked into the accessor. Each of
18
+ // these had exactly one parse site before this module existed; the
19
+ // parse logic moved here unchanged so a second site, if one appears,
20
+ // reads the single implementation instead of re-deriving it.
21
+ //
22
+ // `CRTR_DIR_NAME` is NOT here — it is a plain exported string constant
23
+ // (`types.ts`), never read off `process.env`.
24
+ import { lstatSync, readFileSync, readdirSync } from 'node:fs';
25
+ import { join } from 'node:path';
26
+ import { covers } from '@crouter/identity';
27
+ // Identity
28
+ /** The current node's id (`CRTR_NODE_ID`), or undefined outside a node. */
29
+ export function envNodeId() {
30
+ return process.env['CRTR_NODE_ID'];
31
+ }
32
+ /** Find the single launch snapshot mounted by the launcher into this process's
33
+ * Linux namespace. Cron bash has no CRTR_NODE_ID, but its creator's mounted
34
+ * launch snapshot supplies config and granted-command projection without
35
+ * claiming the cron is a node. Its broker relay may already have closed when
36
+ * the cron fires; still classify the sandbox so CLI bootstrap never falls back
37
+ * to host stores or person authority. Neither CRTR_NODE_ID, CRTR_ANCHOR, nor
38
+ * the caller-editable CRTR_API_SOCKET selects the launch identity. */
39
+ export function sandboxCliNodeId() {
40
+ if (process.platform !== 'linux')
41
+ return undefined;
42
+ try {
43
+ const root = '/run/crtr/nodes';
44
+ const mounted = new Set(readFileSync('/proc/self/mountinfo', 'utf8').split('\n').map((line) => line.split(' ')[4]));
45
+ const ids = readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory() &&
46
+ /^[a-zA-Z0-9][a-zA-Z0-9-]{0,127}$/.test(entry.name) &&
47
+ mounted.has(join(root, entry.name)) &&
48
+ lstatSync(join(root, entry.name, 'broker-launch.json'), { throwIfNoEntry: false })?.isFile());
49
+ return ids.length === 1 ? ids[0].name : undefined;
50
+ }
51
+ catch {
52
+ return undefined;
53
+ }
54
+ }
55
+ /** A Linux app command uses its mounted launch snapshot, not host user stores. */
56
+ export function isSandboxedNode() {
57
+ return sandboxCliNodeId() !== undefined;
58
+ }
59
+ /** The app that owns the current node, supplied in its broker launch identity. */
60
+ export function envGrantee() {
61
+ return process.env['CRTR_GRANTEE'];
62
+ }
63
+ /** Set to '1' by `nodeEnv` for an app-started run's brokers, which admit inbox
64
+ * mail only between agent runs. Terminal nodes also carry `CRTR_RUN_ID`, so
65
+ * that variable is not this signal. */
66
+ export const INBOX_BETWEEN_TURNS_ENV = 'CRTR_INBOX_BETWEEN_TURNS';
67
+ export function envInboxBetweenTurns() {
68
+ return process.env[INBOX_BETWEEN_TURNS_ENV] === '1';
69
+ }
70
+ /** The current run's scope allow-list; null means the row inherits everything. */
71
+ export function envScopes() {
72
+ const raw = process.env['CRTR_SCOPES'];
73
+ return raw === undefined ? null : raw === '' ? [] : raw.split(',');
74
+ }
75
+ /** Whether a launched scope list covers a capability. Null means not narrowed. */
76
+ export function scopeAllowed(scopes, scope) {
77
+ if (scopes === null || scopes === undefined)
78
+ return true;
79
+ return scopes.some((held) => covers(held, scope));
80
+ }
81
+ export function envExecutionId() {
82
+ return process.env['CRTR_EXECUTION_ID'];
83
+ }
84
+ /** The current node's target profile id (`CRTR_PROFILE_ID`), or undefined. */
85
+ export function envProfileId() {
86
+ return process.env['CRTR_PROFILE_ID'];
87
+ }
88
+ /** The current node's working directory (`CRTR_NODE_CWD`), or undefined. */
89
+ export function envNodeCwd() {
90
+ return process.env['CRTR_NODE_CWD'];
91
+ }
92
+ /** An explicit model-routing override for this turn (`CRTR_MODEL_INTENT`), or
93
+ * undefined. Cleared (not read) via `delete process.env['CRTR_MODEL_INTENT']`
94
+ * in `runtime/broker.ts` — a mutation, not a read, so it stays there. */
95
+ export function envModelIntent() {
96
+ return process.env['CRTR_MODEL_INTENT'];
97
+ }
98
+ /** Whether the launch recipe requires this broker to keep its exact provider/model. */
99
+ export function envModelExact() {
100
+ return process.env['CRTR_MODEL_EXACT'] === '1';
101
+ }
102
+ /** The raw `CRTR_HOME` override, unresolved. Use `crtrHome()`
103
+ * (`core/canvas/paths.ts`) to get the resolved canvas-home path; use this
104
+ * only when a caller needs the override itself — to check whether one is
105
+ * set, or to propagate it verbatim into a child process's env. */
106
+ export function envHomeOverride() {
107
+ return process.env['CRTR_HOME'];
108
+ }
109
+ /** The raw installed-runtime directory override, unresolved. */
110
+ export function envRuntimeHomeOverride() {
111
+ return process.env['CRTR_RUNTIME_HOME'];
112
+ }
113
+ // Tuning
114
+ /** Generic `Number(raw)` parse with a positive-finite guard, shared by every
115
+ * millisecond tuning var below (each previously reimplemented this locally,
116
+ * identically). */
117
+ function parsePositiveMs(raw, fallback) {
118
+ if (raw === undefined)
119
+ return fallback;
120
+ const n = Number(raw);
121
+ return Number.isFinite(n) && n > 0 ? n : fallback;
122
+ }
123
+ const DEFAULT_STREAM_WATCHDOG_MS = 5 * 60_000; // 5 minutes.
124
+ /** Dead-provider watchdog timeout (`CRTR_STREAM_WATCHDOG_MS`), 5 minutes by
125
+ * default. See `runtime/stream-watchdog.ts` for what it guards. */
126
+ export function envStreamWatchdogMs() {
127
+ return parsePositiveMs(process.env['CRTR_STREAM_WATCHDOG_MS'], DEFAULT_STREAM_WATCHDOG_MS);
128
+ }
129
+ const DEFAULT_WATCHER_TICK_MS = 800;
130
+ /** The canvas inbox watcher's poll tick (`CRTR_WATCHER_TICK_MS`). */
131
+ export function envWatcherTickMs() {
132
+ return parsePositiveMs(process.env['CRTR_WATCHER_TICK_MS'], DEFAULT_WATCHER_TICK_MS);
133
+ }
134
+ const DEFAULT_WATCHER_BACKSTOP_MS = 60_000;
135
+ /** Claim backstop while broker signals are connected (`CRTR_WATCHER_BACKSTOP_MS`). */
136
+ export function envWatcherBackstopMs() {
137
+ return parsePositiveMs(process.env['CRTR_WATCHER_BACKSTOP_MS'], DEFAULT_WATCHER_BACKSTOP_MS);
138
+ }
139
+ const DEFAULT_WATCHER_DEBOUNCE_MS = 1000;
140
+ /** The canvas inbox watcher's debounce window (`CRTR_WATCHER_DEBOUNCE_MS`). */
141
+ export function envWatcherDebounceMs() {
142
+ return parsePositiveMs(process.env['CRTR_WATCHER_DEBOUNCE_MS'], DEFAULT_WATCHER_DEBOUNCE_MS);
143
+ }
144
+ const DEFAULT_RECAP_IDLE_MS = 60_000;
145
+ /** No-new-message window before a recap is shown (`CRTR_RECAP_IDLE_MS`), 60s
146
+ * by default. */
147
+ export function envRecapIdleMs() {
148
+ return parsePositiveMs(process.env['CRTR_RECAP_IDLE_MS'], DEFAULT_RECAP_IDLE_MS);
149
+ }
150
+ /** Explicit recap-model override (`CRTR_RECAP_MODEL`), trimmed; undefined if
151
+ * unset or blank. */
152
+ export function envRecapModelOverride() {
153
+ const raw = process.env['CRTR_RECAP_MODEL'];
154
+ return raw !== undefined && raw.trim() !== '' ? raw.trim() : undefined;
155
+ }
156
+ const DEFAULT_BASH_VALVE_MS = 5 * 60 * 1000;
157
+ /** The bash safety-valve deadline (`CRTR_BASH_VALVE_MS`), 5 minutes by
158
+ * default; undocumented test-only override so the E2E valve check doesn't
159
+ * sleep 5 minutes. */
160
+ export function envBashValveMs() {
161
+ return parsePositiveMs(process.env['CRTR_BASH_VALVE_MS'], DEFAULT_BASH_VALVE_MS);
162
+ }
163
+ const DEFAULT_BASH_AUTO_BG_DEADLINE_MS = 60 * 1000;
164
+ /** How long an AUTO-backgrounded bash job may run before the daemon cancels it
165
+ * unless the agent claims it (`CRTR_BASH_AUTO_BG_DEADLINE_MS`), 60s by default;
166
+ * undocumented test-only override so the deadline check doesn't wait a minute. */
167
+ export function envBashAutoBgDeadlineMs() {
168
+ return parsePositiveMs(process.env['CRTR_BASH_AUTO_BG_DEADLINE_MS'], DEFAULT_BASH_AUTO_BG_DEADLINE_MS);
169
+ }
170
+ /** Explicit pidfile path override (`CRTR_PIDFILE`, tests only); undefined if
171
+ * unset or blank. */
172
+ export function envPidfileOverride() {
173
+ const raw = process.env['CRTR_PIDFILE'];
174
+ return raw !== undefined && raw !== '' ? raw : undefined;
175
+ }
176
+ const DEFAULT_BROKER_ENGINE = '@earendil-works/pi-coding-agent';
177
+ /** The broker engine module spec (`CRTR_BROKER_ENGINE`, the T11 test seam),
178
+ * defaulting to the real SDK. `runtime/host.ts` layers `inv.env` ahead of
179
+ * this (a launch-time override mirrored into the child's env, since
180
+ * spawn-env.ts's default-deny allowlist strips `CRTR_*` from ambient env). */
181
+ export function envBrokerEngine() {
182
+ return process.env['CRTR_BROKER_ENGINE'] ?? DEFAULT_BROKER_ENGINE;
183
+ }
184
+ /** Test-only pi-binary substitution (`CRTR_PI_BINARY`) so the integration
185
+ * harness can point a real `crtr node new` at a deterministic fake-pi
186
+ * vehicle. Undefined in production. */
187
+ export function envPiBinaryOverride() {
188
+ return process.env['CRTR_PI_BINARY'];
189
+ }
190
+ /** Whether implicit daemon autostart is suppressed for this invocation
191
+ * (`CRTR_NO_DAEMON_AUTOSTART=1`). */
192
+ export function envNoDaemonAutostart() {
193
+ return process.env['CRTR_NO_DAEMON_AUTOSTART'] === '1';
194
+ }
195
+ /** The `--tcp` fallback for crtrd's opt-in TCP listener (`CRTRD_TCP`), raw
196
+ * (`host:port` or undefined — absent means unix socket only). */
197
+ export function envCrtrdTcp() {
198
+ return process.env['CRTRD_TCP'];
199
+ }
200
+ /** The opt-in bearer token guarding crtrd's TCP listener (`CRTRD_TOKEN`).
201
+ * Unset (or blank) → undefined, and the TCP listener performs NO auth check
202
+ * at all — exactly today's behavior. The unix socket never checks this,
203
+ * set or not. */
204
+ export function envCrtrdToken() {
205
+ const raw = process.env['CRTRD_TOKEN'];
206
+ return raw !== undefined && raw !== '' ? raw : undefined;
207
+ }
208
+ /** Kill switch for the host-exports writer/pruner (`CRTR_NO_EXPORTS=1`). */
209
+ export function envNoExports() {
210
+ return process.env['CRTR_NO_EXPORTS'] === '1';
211
+ }
212
+ /** Verbose-diagnostics gate for otherwise-silent best-effort catches
213
+ * (`CRTR_DEBUG=1`). Distinct from `CRTR_DEBUG_PID_LIVENESS`
214
+ * (`core/canvas/pid.ts`), a different, unrelated var. */
215
+ export function envDebug() {
216
+ return process.env['CRTR_DEBUG'] === '1';
217
+ }
218
+ // Precedence — broker-supervision.ts's threshold/interval overrides. Each
219
+ // returns the raw parsed value with NO default: the caller supplies the
220
+ // default (user config for the broker thresholds and the unattended-park
221
+ // interval, a hardcoded constant for the test-only park-summary grace), and
222
+ // env wins over it when set. Undefined, unparseable, or non-positive all mean
223
+ // "no override" — the caller's own default applies.
224
+ function parsePositiveInteger(raw) {
225
+ if (raw === undefined || raw === '')
226
+ return undefined;
227
+ const n = Number(raw);
228
+ return Number.isSafeInteger(n) && n >= 1 ? n : undefined;
229
+ }
230
+ /** Live-broker automatic-revive cap override (`CRTR_MAX_LIVE_BROKERS`); wins
231
+ * over `readConfig('user').brokerThresholds.automaticReviveCap` when set. */
232
+ export function envMaxLiveBrokers() {
233
+ return parsePositiveInteger(process.env['CRTR_MAX_LIVE_BROKERS']);
234
+ }
235
+ /** Live-broker warning threshold override (`CRTR_WARN_LIVE_BROKERS`); wins
236
+ * over `readConfig('user').brokerThresholds.warning` when set. */
237
+ export function envWarnLiveBrokers() {
238
+ return parsePositiveInteger(process.env['CRTR_WARN_LIVE_BROKERS']);
239
+ }
240
+ function parsePositiveMsNoDefault(raw) {
241
+ if (raw === undefined)
242
+ return undefined;
243
+ const n = Number(raw);
244
+ return Number.isFinite(n) && n > 0 ? n : undefined;
245
+ }
246
+ /** Test-only unattended-park interval override (`CRTR_TEST_UNATTENDED_PARK_MS`),
247
+ * gated by `envNoDaemonAutostart()` at the call site; no production default. */
248
+ export function envTestUnattendedParkMs() {
249
+ return parsePositiveMsNoDefault(process.env['CRTR_TEST_UNATTENDED_PARK_MS']);
250
+ }
251
+ /** Production unattended-park interval override (`CRTR_UNATTENDED_PARK_MS`), in
252
+ * milliseconds; wins over the user-scope config value
253
+ * (`readConfig('user').lifecycle.unattendedParkMs`, itself defaulting to 15
254
+ * min) whenever set, autostart or not. Unlike `CRTR_TEST_UNATTENDED_PARK_MS`
255
+ * this is not gated on `envNoDaemonAutostart()`: it is the env escape hatch
256
+ * above the configured knob a deployment sets to tune how long an eligible
257
+ * resident idles before its parking turn. */
258
+ export function envUnattendedParkMs() {
259
+ return parsePositiveMsNoDefault(process.env['CRTR_UNATTENDED_PARK_MS']);
260
+ }
261
+ /** Test-only park-summary grace-period override
262
+ * (`CRTR_TEST_PARK_SUMMARY_GRACE_MS`); no production default. */
263
+ export function envTestParkSummaryGraceMs() {
264
+ return parsePositiveMsNoDefault(process.env['CRTR_TEST_PARK_SUMMARY_GRACE_MS']);
265
+ }
266
+ const DEFAULT_PARK_SUMMARY_GRACE_MS = 5 * 60_000;
267
+ /** The bounded lifetime shared by the daemon's pending-park sweep and its
268
+ * in-broker isolated turn. */
269
+ export function parkSummaryGraceMs() {
270
+ return envTestParkSummaryGraceMs() ?? DEFAULT_PARK_SUMMARY_GRACE_MS;
271
+ }
@@ -0,0 +1,22 @@
1
+ export declare const INLINE_REPORT_MAX_CHARS = 1000;
2
+ /** The rendering fields both inbox projections share. */
3
+ export interface InboxEntryFields {
4
+ ts: string;
5
+ from: string | null;
6
+ from_name?: string;
7
+ label: string;
8
+ ref?: string;
9
+ data?: Record<string, unknown>;
10
+ }
11
+ /** A report document's ref (`<node>/reports/<name>`), else undefined. */
12
+ export declare function reportRefNodeId(ref: string | undefined): string | undefined;
13
+ /** Clip a body to the bounded preview stored beside a spilled message body. */
14
+ export declare function clipBody(body: string): {
15
+ text: string;
16
+ clipped: boolean;
17
+ };
18
+ export declare function inlineBody(entry: Pick<InboxEntryFields, 'data' | 'label'>): string;
19
+ /** One entry's card body: an inlined short report, a bounded inline message body, an event
20
+ * push rendered from its metadata, or the label. `report` is the report document's body when
21
+ * the caller could read it. */
22
+ export declare function renderEntryBody(entry: InboxEntryFields, report: string | null): string;
@@ -0,0 +1,116 @@
1
+ // One inbox entry's card body, shared by the daemon (kickoff digest) and broker (live inbox) renderers.
2
+ import { formatInboxRefInstruction } from './generated-context.js';
3
+ const BODY_MAX_LINES = 12;
4
+ const BODY_MAX_CHARS = 1000;
5
+ export const INLINE_REPORT_MAX_CHARS = 1000;
6
+ /** A report document's ref (`<node>/reports/<name>`), else undefined. */
7
+ export function reportRefNodeId(ref) {
8
+ const match = ref === undefined ? null : /^([a-z0-9-]+)\/reports\/[^/]+$/.exec(ref);
9
+ return match?.[1];
10
+ }
11
+ /** Clip a body to the bounded preview stored beside a spilled message body. */
12
+ export function clipBody(body) {
13
+ let text = body;
14
+ let clipped = false;
15
+ const lines = text.split('\n');
16
+ if (lines.length > BODY_MAX_LINES) {
17
+ text = lines.slice(0, BODY_MAX_LINES).join('\n');
18
+ clipped = true;
19
+ }
20
+ if (text.length > BODY_MAX_CHARS) {
21
+ text = text.slice(0, BODY_MAX_CHARS);
22
+ clipped = true;
23
+ }
24
+ return { text: text.trimEnd(), clipped };
25
+ }
26
+ export function inlineBody(entry) {
27
+ const body = typeof entry.data?.['body'] === 'string' ? String(entry.data['body']).trim() : '';
28
+ return body === '' || body === entry.label ? '' : body;
29
+ }
30
+ function pushOf(entry) {
31
+ const push = entry.data?.['push'];
32
+ return typeof push === 'object' && push !== null && typeof push.event === 'string' ? push : null;
33
+ }
34
+ /** The name a reader resolves the object by: `<owner handle>/<name>`, or the owner's id on an older push. */
35
+ function objectRef(o) {
36
+ const owner = o.owner_handle ?? o.owner;
37
+ return owner === null || owner === undefined ? o.name : `${owner}/${o.name}`;
38
+ }
39
+ function objectText(o) {
40
+ if (o.type === 'document')
41
+ return `[[${objectRef(o)}]]`;
42
+ if (o.type === 'job')
43
+ return `job ${o.id} (\`${o.name}\`)`;
44
+ if (o.type === 'node')
45
+ return `node ${o.name} (${o.id})`;
46
+ return `${o.type} ${o.name}`;
47
+ }
48
+ function actorText(entry, push) {
49
+ if (push.actor === null)
50
+ return 'the runtime';
51
+ return entry.from_name === undefined || entry.from_name === push.actor ? push.actor : `${entry.from_name} (${push.actor})`;
52
+ }
53
+ function list(value) {
54
+ return Array.isArray(value) ? value.filter((v) => typeof v === 'string') : [];
55
+ }
56
+ /** One sentence per event: the object, the actor and the pointer — never the act's content. */
57
+ function renderPush(entry, push) {
58
+ const obj = objectText(push.object);
59
+ const actor = actorText(entry, push);
60
+ const a = push.attrs;
61
+ const readHint = push.object.type === 'document'
62
+ ? ` Read it with \`crtr canvas read ${objectRef(push.object)}\`.`
63
+ : '';
64
+ switch (push.event) {
65
+ case 'created':
66
+ return push.object.type === 'node'
67
+ ? `${actor} created ${obj}${typeof a['kind'] === 'string' ? `, kind ${a['kind']}` : ''}.`
68
+ : `${actor} created ${obj}.${readHint}`;
69
+ case 'read': return `${actor} read ${obj}.`;
70
+ case 'watched': return `${actor} started watching ${obj}.`;
71
+ case 'deleted': return `${actor} deleted ${obj}.`;
72
+ case 'edited': {
73
+ const authors = list(a['authors']);
74
+ const rationales = list(a['rationales']);
75
+ const range = typeof a['from_revision'] === 'number' && typeof a['revision'] === 'number' && a['revision'] - a['from_revision'] > 1
76
+ ? `revisions ${a['from_revision'] + 1}–${a['revision']}` : typeof a['revision'] === 'number' ? `revision ${a['revision']}` : 'a new revision';
77
+ const by = authors.length === 0 ? actor : authors.join(', ');
78
+ const why = rationales.length === 0 ? '' : ` Rationale: ${rationales.join('; ')}.`;
79
+ const merge = a['needs_merge'] === true ? ' Two edits were made from the same revision; read it and edit it to merge them.' : '';
80
+ const unwatchHint = ` You get this because you read or wrote it; if it no longer concerns your work, stop with \`crtr canvas unwatch ${objectRef(push.object)}\`.`;
81
+ return `${by} edited ${obj} (${range}).${why}${merge}${readHint}${unwatchHint}`;
82
+ }
83
+ case 'messaged': return `${actor} messaged ${obj}.`;
84
+ case 'sent': return `${actor} sent a message as ${obj}.`;
85
+ case 'configured': return `${actor} changed the settings of ${obj}${typeof a['changed'] === 'string' ? `: ${a['changed']}` : ''}.`;
86
+ case 'exited': return `${obj[0].toUpperCase()}${obj.slice(1)} exited with code ${String(a['exit-code'])}. Log: ${String(a['log-path'])}`;
87
+ case 'killed': {
88
+ const by = push.actor === null ? '' : ` by ${actor}`;
89
+ const why = typeof a['reason'] === 'string' ? ` (${a['reason']})` : '';
90
+ const claim = a['reason'] === 'deadline'
91
+ ? ` It was canceled at its deadline; claim it with \`crtr node bash extend ${push.object.id} --for <duration>\` next time.`
92
+ : '';
93
+ return `${obj[0].toUpperCase()}${obj.slice(1)} was killed${by}${why}.${claim} Log: ${String(a['log-path'])}`;
94
+ }
95
+ default: return entry.label;
96
+ }
97
+ }
98
+ /** One entry's card body: an inlined short report, a bounded inline message body, an event
99
+ * push rendered from its metadata, or the label. `report` is the report document's body when
100
+ * the caller could read it. */
101
+ export function renderEntryBody(entry, report) {
102
+ if (report !== null && report.length < INLINE_REPORT_MAX_CHARS && entry.ref !== undefined) {
103
+ return `${report.trimEnd()}\n\n${formatInboxRefInstruction(entry.ref)}`;
104
+ }
105
+ const body = inlineBody(entry);
106
+ if (body === '') {
107
+ const push = pushOf(entry);
108
+ if (push !== null && entry.ref === undefined)
109
+ return renderPush(entry, push);
110
+ return entry.ref === undefined ? entry.label : `${entry.label}\n\n${formatInboxRefInstruction(entry.ref)}`;
111
+ }
112
+ const { text, clipped } = clipBody(body);
113
+ if (entry.ref !== undefined)
114
+ return `${text}\n\n${formatInboxRefInstruction(entry.ref)}`;
115
+ return clipped ? `${text}\n… (body clipped)` : text;
116
+ }
@@ -0,0 +1,9 @@
1
+ export declare const DEFAULT_WORKING_GERUND = "working";
2
+ export declare const DEFAULT_WORKING_GERUNDS: readonly ["thinking", "planning", "checking", "working"];
3
+ export declare const DEFAULT_WORKING_ACTIVITY = "Working...";
4
+ /** Keep only usable configured labels; malformed or empty lists fall back to working. */
5
+ export declare function normalizeWorkingGerunds(value: unknown): string[];
6
+ /** Format one configured gerund as the activity label shown by every viewer. */
7
+ export declare function formatWorkingActivity(gerund: unknown): string;
8
+ /** Pick one configured gerund when the broker receives a user message. */
9
+ export declare function randomWorkingActivity(value: unknown, random?: () => number): string;
@@ -0,0 +1,27 @@
1
+ export const DEFAULT_WORKING_GERUND = 'working';
2
+ export const DEFAULT_WORKING_GERUNDS = ['thinking', 'planning', 'checking', DEFAULT_WORKING_GERUND];
3
+ export const DEFAULT_WORKING_ACTIVITY = 'Working...';
4
+ /** Keep only usable configured labels; malformed or empty lists fall back to working. */
5
+ export function normalizeWorkingGerunds(value) {
6
+ if (!Array.isArray(value))
7
+ return [DEFAULT_WORKING_GERUND];
8
+ const gerunds = value
9
+ .filter((entry) => typeof entry === 'string')
10
+ .map((entry) => entry.trim())
11
+ .filter((entry) => entry.length > 0);
12
+ return gerunds.length > 0 ? gerunds : [DEFAULT_WORKING_GERUND];
13
+ }
14
+ /** Format one configured gerund as the activity label shown by every viewer. */
15
+ export function formatWorkingActivity(gerund) {
16
+ const normalized = normalizeWorkingGerunds([gerund])[0] ?? DEFAULT_WORKING_GERUND;
17
+ return `${normalized[0].toUpperCase()}${normalized.slice(1)}...`;
18
+ }
19
+ /** Pick one configured gerund when the broker receives a user message. */
20
+ export function randomWorkingActivity(value, random = Math.random) {
21
+ const gerunds = normalizeWorkingGerunds(value);
22
+ const draw = random();
23
+ const index = Number.isFinite(draw)
24
+ ? Math.min(gerunds.length - 1, Math.max(0, Math.floor(draw * gerunds.length)))
25
+ : 0;
26
+ return formatWorkingActivity(gerunds[index]);
27
+ }