pi-daddy 0.43.1 → 0.44.0

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 (221) hide show
  1. package/CHANGELOG.md +93 -31
  2. package/README.md +15 -0
  3. package/THIRD_PARTY_LICENSES/musl-COPYRIGHT.txt +193 -0
  4. package/THIRD_PARTY_LICENSES/zig-MIT.txt +21 -0
  5. package/THIRD_PARTY_NOTICES.md +9 -0
  6. package/dist/cli.d.ts +2 -1
  7. package/dist/cli.d.ts.map +1 -1
  8. package/dist/cli.js +10 -5
  9. package/dist/cli.js.map +1 -1
  10. package/dist/executors/activity-session.js +116 -65
  11. package/dist/executors/activity-session.js.map +1 -1
  12. package/dist/executors/captured-execution.d.ts +13 -0
  13. package/dist/executors/captured-execution.d.ts.map +1 -0
  14. package/dist/executors/captured-execution.js +53 -0
  15. package/dist/executors/captured-execution.js.map +1 -0
  16. package/dist/executors/child-final.d.ts +31 -0
  17. package/dist/executors/child-final.d.ts.map +1 -0
  18. package/dist/executors/child-final.js +197 -0
  19. package/dist/executors/child-final.js.map +1 -0
  20. package/dist/executors/executor.d.ts +2 -1
  21. package/dist/executors/executor.d.ts.map +1 -1
  22. package/dist/executors/executor.js +12 -0
  23. package/dist/executors/executor.js.map +1 -1
  24. package/dist/executors/owned-worker.d.ts +18 -0
  25. package/dist/executors/owned-worker.d.ts.map +1 -0
  26. package/dist/executors/owned-worker.js +295 -0
  27. package/dist/executors/owned-worker.js.map +1 -0
  28. package/dist/executors/worker-artifact.d.ts +3 -0
  29. package/dist/executors/worker-artifact.d.ts.map +1 -0
  30. package/dist/executors/worker-artifact.js +3 -0
  31. package/dist/executors/worker-artifact.js.map +1 -0
  32. package/dist/governance/approval-prompt.d.ts +4 -3
  33. package/dist/governance/approval-prompt.d.ts.map +1 -1
  34. package/dist/governance/approval-prompt.js +13 -6
  35. package/dist/governance/approval-prompt.js.map +1 -1
  36. package/dist/governance/captured-worker-record.d.ts +9 -0
  37. package/dist/governance/captured-worker-record.d.ts.map +1 -0
  38. package/dist/governance/captured-worker-record.js +123 -0
  39. package/dist/governance/captured-worker-record.js.map +1 -0
  40. package/dist/governance/lease-record.d.ts +4 -0
  41. package/dist/governance/lease-record.d.ts.map +1 -1
  42. package/dist/governance/lease-record.js +19 -2
  43. package/dist/governance/lease-record.js.map +1 -1
  44. package/dist/governance/ledger-events.d.ts +5 -5
  45. package/dist/governance/ledger-events.d.ts.map +1 -1
  46. package/dist/governance/ledger-v3-validation.js +1 -1
  47. package/dist/governance/ledger-v3-validation.js.map +1 -1
  48. package/dist/governance/workspace-acceptance.d.ts +3 -1
  49. package/dist/governance/workspace-acceptance.d.ts.map +1 -1
  50. package/dist/governance/workspace-acceptance.js +21 -5
  51. package/dist/governance/workspace-acceptance.js.map +1 -1
  52. package/dist/governance/workspace-lease.d.ts.map +1 -1
  53. package/dist/governance/workspace-lease.js +44 -1
  54. package/dist/governance/workspace-lease.js.map +1 -1
  55. package/dist/kernel/authority-text.d.ts +3 -0
  56. package/dist/kernel/authority-text.d.ts.map +1 -0
  57. package/dist/kernel/authority-text.js +22 -0
  58. package/dist/kernel/authority-text.js.map +1 -0
  59. package/dist/kernel/bounded-read-failures.d.ts +9 -0
  60. package/dist/kernel/bounded-read-failures.d.ts.map +1 -0
  61. package/dist/kernel/bounded-read-failures.js +33 -0
  62. package/dist/kernel/bounded-read-failures.js.map +1 -0
  63. package/dist/kernel/bounded-read-sync.d.ts +24 -0
  64. package/dist/kernel/bounded-read-sync.d.ts.map +1 -0
  65. package/dist/kernel/bounded-read-sync.js +75 -0
  66. package/dist/kernel/bounded-read-sync.js.map +1 -0
  67. package/dist/kernel/bounded-read.d.ts +29 -2
  68. package/dist/kernel/bounded-read.d.ts.map +1 -1
  69. package/dist/kernel/bounded-read.js +98 -9
  70. package/dist/kernel/bounded-read.js.map +1 -1
  71. package/dist/kernel/capacity.d.ts +36 -0
  72. package/dist/kernel/capacity.d.ts.map +1 -0
  73. package/dist/kernel/capacity.js +94 -0
  74. package/dist/kernel/capacity.js.map +1 -0
  75. package/dist/kernel/captured-worker-contract.d.ts +38 -0
  76. package/dist/kernel/captured-worker-contract.d.ts.map +1 -0
  77. package/dist/kernel/captured-worker-contract.js +2 -0
  78. package/dist/kernel/captured-worker-contract.js.map +1 -0
  79. package/dist/kernel/catalog.d.ts +12 -3
  80. package/dist/kernel/catalog.d.ts.map +1 -1
  81. package/dist/kernel/catalog.js +28 -8
  82. package/dist/kernel/catalog.js.map +1 -1
  83. package/dist/kernel/chain.d.ts +1 -0
  84. package/dist/kernel/chain.d.ts.map +1 -1
  85. package/dist/kernel/chain.js.map +1 -1
  86. package/dist/kernel/definitions.d.ts +11 -1
  87. package/dist/kernel/definitions.d.ts.map +1 -1
  88. package/dist/kernel/definitions.js +6 -3
  89. package/dist/kernel/definitions.js.map +1 -1
  90. package/dist/kernel/fanout.d.ts +5 -25
  91. package/dist/kernel/fanout.d.ts.map +1 -1
  92. package/dist/kernel/fanout.js +15 -30
  93. package/dist/kernel/fanout.js.map +1 -1
  94. package/dist/kernel/grant-env.d.ts +0 -2
  95. package/dist/kernel/grant-env.d.ts.map +1 -1
  96. package/dist/kernel/grant-env.js +0 -1
  97. package/dist/kernel/grant-env.js.map +1 -1
  98. package/dist/kernel/model-preflight.d.ts +8 -0
  99. package/dist/kernel/model-preflight.d.ts.map +1 -1
  100. package/dist/kernel/model-preflight.js +23 -0
  101. package/dist/kernel/model-preflight.js.map +1 -1
  102. package/dist/kernel/selected-definitions.d.ts +15 -0
  103. package/dist/kernel/selected-definitions.d.ts.map +1 -0
  104. package/dist/kernel/selected-definitions.js +146 -0
  105. package/dist/kernel/selected-definitions.js.map +1 -0
  106. package/dist/kernel/skill-packages.d.ts +4 -2
  107. package/dist/kernel/skill-packages.d.ts.map +1 -1
  108. package/dist/kernel/skill-packages.js +34 -41
  109. package/dist/kernel/skill-packages.js.map +1 -1
  110. package/dist/kernel/skill-resources.d.ts.map +1 -1
  111. package/dist/kernel/skill-resources.js +8 -11
  112. package/dist/kernel/skill-resources.js.map +1 -1
  113. package/dist/kernel/workspace.d.ts +6 -3
  114. package/dist/kernel/workspace.d.ts.map +1 -1
  115. package/dist/kernel/workspace.js +10 -6
  116. package/dist/kernel/workspace.js.map +1 -1
  117. package/dist/products/dashboard-cli.js +1 -1
  118. package/dist/products/dashboard-cli.js.map +1 -1
  119. package/dist/products/dashboard-render.d.ts +1 -2
  120. package/dist/products/dashboard-render.d.ts.map +1 -1
  121. package/dist/products/dashboard-render.js +3 -1
  122. package/dist/products/dashboard-render.js.map +1 -1
  123. package/dist/products/dashboard-session-client.d.ts +1 -2
  124. package/dist/products/dashboard-session-client.d.ts.map +1 -1
  125. package/dist/products/dashboard-session-client.js.map +1 -1
  126. package/dist/products/episode-report.d.ts +12 -8
  127. package/dist/products/episode-report.d.ts.map +1 -1
  128. package/dist/products/episode-report.js +61 -12
  129. package/dist/products/episode-report.js.map +1 -1
  130. package/extensions/approvals.ts +1 -1
  131. package/extensions/chain-plan.ts +9 -3
  132. package/extensions/context-staging.ts +30 -35
  133. package/extensions/dashboard-session-server.ts +1 -2
  134. package/extensions/definition-describe.ts +44 -0
  135. package/extensions/definition-runtime.ts +89 -26
  136. package/extensions/delegate-chain.ts +273 -256
  137. package/extensions/delegation-activation.ts +1 -1
  138. package/extensions/delegation.ts +41 -53
  139. package/extensions/execute-child.ts +167 -134
  140. package/extensions/fanout-outcome.ts +1 -9
  141. package/extensions/grants-command.ts +2 -9
  142. package/extensions/grants.ts +106 -38
  143. package/extensions/init-command.ts +52 -12
  144. package/extensions/reload-environment.ts +21 -2
  145. package/extensions/retired-inputs.ts +7 -0
  146. package/extensions/run-delegation.ts +212 -265
  147. package/extensions/session-capacity.ts +53 -0
  148. package/extensions/session-environment.ts +5 -2
  149. package/extensions/session-model-prompt.ts +33 -32
  150. package/extensions/session-report.ts +28 -11
  151. package/extensions/session.ts +200 -157
  152. package/extensions/workspace-runtime.ts +2 -1
  153. package/native/README.md +25 -0
  154. package/native/linux-x64/worker +0 -0
  155. package/native/linux-x64/worker.sha256 +1 -0
  156. package/native/provenance.json +42 -0
  157. package/native/toolchain.json +18 -0
  158. package/native/worker.c +209 -0
  159. package/package.json +15 -7
  160. package/src/cli.ts +16 -6
  161. package/src/executors/activity-session.ts +120 -66
  162. package/src/executors/captured-execution.ts +61 -0
  163. package/src/executors/child-final.ts +190 -0
  164. package/src/executors/executor.ts +14 -1
  165. package/src/executors/owned-worker.ts +322 -0
  166. package/src/executors/worker-artifact.ts +2 -0
  167. package/src/governance/approval-prompt.ts +18 -8
  168. package/src/governance/captured-worker-record.ts +138 -0
  169. package/src/governance/lease-record.ts +21 -2
  170. package/src/governance/ledger-events.ts +5 -5
  171. package/src/governance/ledger-v3-validation.ts +1 -1
  172. package/src/governance/workspace-acceptance.ts +20 -4
  173. package/src/governance/workspace-lease.ts +59 -1
  174. package/src/kernel/authority-text.ts +17 -0
  175. package/src/kernel/bounded-read-failures.ts +42 -0
  176. package/src/kernel/bounded-read-sync.ts +87 -0
  177. package/src/kernel/bounded-read.ts +128 -11
  178. package/src/kernel/capacity.ts +123 -0
  179. package/src/kernel/captured-worker-contract.ts +29 -0
  180. package/src/kernel/catalog.ts +45 -16
  181. package/src/kernel/chain.ts +1 -0
  182. package/src/kernel/definitions.ts +11 -2
  183. package/src/kernel/fanout.ts +17 -32
  184. package/src/kernel/grant-env.ts +0 -3
  185. package/src/kernel/model-preflight.ts +29 -0
  186. package/src/kernel/selected-definitions.ts +156 -0
  187. package/src/kernel/skill-packages.ts +40 -38
  188. package/src/kernel/skill-resources.ts +8 -9
  189. package/src/kernel/workspace.ts +12 -5
  190. package/src/products/dashboard-cli.ts +1 -1
  191. package/src/products/dashboard-render.ts +6 -2
  192. package/src/products/dashboard-session-client.ts +1 -2
  193. package/src/products/episode-report.ts +80 -17
  194. package/dist/advisors/advisor.d.ts +0 -90
  195. package/dist/advisors/advisor.d.ts.map +0 -1
  196. package/dist/advisors/advisor.js +0 -154
  197. package/dist/advisors/advisor.js.map +0 -1
  198. package/dist/advisors/decider.d.ts +0 -88
  199. package/dist/advisors/decider.d.ts.map +0 -1
  200. package/dist/advisors/decider.js +0 -29
  201. package/dist/advisors/decider.js.map +0 -1
  202. package/dist/advisors/jev.d.ts +0 -41
  203. package/dist/advisors/jev.d.ts.map +0 -1
  204. package/dist/advisors/jev.js +0 -139
  205. package/dist/advisors/jev.js.map +0 -1
  206. package/dist/advisors/settings.d.ts +0 -49
  207. package/dist/advisors/settings.d.ts.map +0 -1
  208. package/dist/advisors/settings.js +0 -99
  209. package/dist/advisors/settings.js.map +0 -1
  210. package/dist/governance/episode-cost-gate.d.ts +0 -31
  211. package/dist/governance/episode-cost-gate.d.ts.map +0 -1
  212. package/dist/governance/episode-cost-gate.js +0 -112
  213. package/dist/governance/episode-cost-gate.js.map +0 -1
  214. package/extensions/advisor-session.ts +0 -67
  215. package/extensions/effort-advice.ts +0 -112
  216. package/extensions/pruning-advice.ts +0 -133
  217. package/src/advisors/advisor.ts +0 -246
  218. package/src/advisors/decider.ts +0 -74
  219. package/src/advisors/jev.ts +0 -163
  220. package/src/advisors/settings.ts +0 -115
  221. package/src/governance/episode-cost-gate.ts +0 -128
@@ -0,0 +1,138 @@
1
+ /** Independent ownership evidence; unreadable/mismatched receipts never establish settlement. */
2
+ import { constants } from "node:fs";
3
+ import { open, realpath, stat, readFile } from "node:fs/promises";
4
+ import { dirname, join } from "node:path";
5
+ import type { CapturedWorkerIdentity, CapturedWorkerReceipt } from "../kernel/captured-worker-contract.ts";
6
+
7
+ const identityKeys = [
8
+ "revision",
9
+ "executionId",
10
+ "nonce",
11
+ "root",
12
+ "rootDevice",
13
+ "rootInode",
14
+ "bootId",
15
+ "pidNamespace",
16
+ "helperPid",
17
+ "helperStartTicks",
18
+ "helperSha256",
19
+ "workerPid",
20
+ "ownershipPath",
21
+ "receiptPath",
22
+ ] as const;
23
+ export function isCapturedWorkerIdentity(value: unknown): value is CapturedWorkerIdentity {
24
+ if (!value || typeof value !== "object") return false;
25
+ const v = value as Record<string, unknown>;
26
+ if (
27
+ v.revision !== 1 ||
28
+ !Number.isSafeInteger(v.helperPid) ||
29
+ Number(v.helperPid) < 1 ||
30
+ !Number.isSafeInteger(v.workerPid) ||
31
+ Number(v.workerPid) < 1
32
+ )
33
+ return false;
34
+ if (
35
+ identityKeys.some(
36
+ (key) => !["revision", "helperPid", "workerPid"].includes(key) && (typeof v[key] !== "string" || !v[key]),
37
+ )
38
+ )
39
+ return false;
40
+ if (!/^[a-f0-9]{64}$/.test(String(v.helperSha256)) || !/^\d+$/.test(String(v.helperStartTicks))) return false;
41
+ return (
42
+ v.ownershipPath === join(dirname(String(v.receiptPath)), "ownership.json") &&
43
+ v.receiptPath === join(dirname(String(v.ownershipPath)), "receipt.json")
44
+ );
45
+ }
46
+ export async function readWorkerRecord(path: string): Promise<unknown> {
47
+ const file = await open(path, constants.O_RDONLY | constants.O_NOFOLLOW | constants.O_NONBLOCK);
48
+ try {
49
+ const info = await file.stat();
50
+ if (!info.isFile() || info.size > 16384) throw new Error("worker record must be a bounded ordinary file");
51
+ const buffer = Buffer.alloc(16385);
52
+ const { bytesRead } = await file.read(buffer, 0, buffer.length, 0);
53
+ if (bytesRead > 16384) throw new Error("worker record grew beyond its bound");
54
+ return JSON.parse(buffer.subarray(0, bytesRead).toString("utf8"));
55
+ } finally {
56
+ await file.close();
57
+ }
58
+ }
59
+ export function sameWorkerIdentity(a: CapturedWorkerIdentity, b: CapturedWorkerIdentity): boolean {
60
+ return identityKeys.every((key) => a[key] === b[key]);
61
+ }
62
+ export async function readCapturedWorkerReceipt(
63
+ identity: CapturedWorkerIdentity,
64
+ ): Promise<CapturedWorkerReceipt | null> {
65
+ try {
66
+ if (!isCapturedWorkerIdentity(identity)) return null;
67
+ const ownership = (await readWorkerRecord(identity.ownershipPath)) as { state?: unknown; identity?: unknown };
68
+ const value = (await readWorkerRecord(identity.receiptPath)) as CapturedWorkerReceipt;
69
+ if (
70
+ ownership.state !== "ready" ||
71
+ !isCapturedWorkerIdentity(ownership.identity) ||
72
+ !sameWorkerIdentity(identity, ownership.identity)
73
+ )
74
+ return null;
75
+ if (
76
+ value.state !== "settled" ||
77
+ value.reapedAll !== true ||
78
+ !isCapturedWorkerIdentity(value.identity) ||
79
+ !sameWorkerIdentity(identity, value.identity)
80
+ )
81
+ return null;
82
+ if (
83
+ value.workerCode !== null &&
84
+ (!Number.isInteger(value.workerCode) || value.workerCode < 0 || value.workerCode > 255)
85
+ )
86
+ return null;
87
+ if (!Number.isInteger(value.workerSignal) || value.workerSignal < 0 || value.workerSignal > 64) return null;
88
+ if (
89
+ !["worker-exit", "owner-loss", "cancelled", "helper-signal", "ownership-write-failed", "start-failed"].includes(
90
+ value.reason,
91
+ )
92
+ )
93
+ return null;
94
+ if (value.workerSignal !== 0 && value.workerCode !== null) return null;
95
+ return value;
96
+ } catch {
97
+ return null;
98
+ }
99
+ }
100
+ /** Before release of the execution gate, validate namespace, workspace and the still-live actual helper. */
101
+ export async function validateLiveWorker(identity: CapturedWorkerIdentity, helperPath: string): Promise<void> {
102
+ if (!isCapturedWorkerIdentity(identity)) throw new Error("malformed captured worker identity");
103
+ const [root, rootInfo, boot, namespace, processStat, executable, packaged] = await Promise.all([
104
+ realpath(identity.root),
105
+ stat(identity.root, { bigint: true }),
106
+ readFile("/proc/sys/kernel/random/boot_id", "utf8"),
107
+ import("node:fs/promises").then((fs) => fs.readlink("/proc/self/ns/pid")),
108
+ readFile(`/proc/${identity.helperPid}/stat`, "utf8"),
109
+ stat(`/proc/${identity.helperPid}/exe`),
110
+ stat(helperPath),
111
+ ]);
112
+ const fields = processStat.slice(processStat.lastIndexOf(")") + 2).split(" ");
113
+ if (
114
+ root !== identity.root ||
115
+ String(rootInfo.dev) !== identity.rootDevice ||
116
+ String(rootInfo.ino) !== identity.rootInode ||
117
+ boot.trim() !== identity.bootId ||
118
+ namespace !== identity.pidNamespace ||
119
+ fields[19] !== identity.helperStartTicks ||
120
+ executable.dev !== packaged.dev ||
121
+ executable.ino !== packaged.ino
122
+ )
123
+ throw new Error("captured worker identity differs from actual live ownership");
124
+ }
125
+
126
+ export async function matchesWorkerWorkspace(identity: CapturedWorkerIdentity, root: string): Promise<boolean> {
127
+ try {
128
+ const info = await stat(root, { bigint: true });
129
+ return (
130
+ identity.root === root &&
131
+ (await realpath(root)) === root &&
132
+ String(info.dev) === identity.rootDevice &&
133
+ String(info.ino) === identity.rootInode
134
+ );
135
+ } catch {
136
+ return false;
137
+ }
138
+ }
@@ -6,10 +6,13 @@
6
6
  * successor's token — all of which are about what the record SAYS rather than about the kernel lock.
7
7
  */
8
8
  import { createHash, randomUUID } from "node:crypto";
9
- import { readFile, rename, writeFile } from "node:fs/promises";
10
- import { join } from "node:path";
9
+ import { open, readFile, rename, writeFile } from "node:fs/promises";
10
+ import { dirname, join } from "node:path";
11
11
  import type { ValidatedWorkspace, WorkspaceAccess } from "../kernel/workspace.ts";
12
12
 
13
+ import type { CapturedWorkerIdentity } from "../kernel/captured-worker-contract.ts";
14
+ import { isCapturedWorkerIdentity } from "./captured-worker-record.ts";
15
+
13
16
  export interface LeaseMetadata {
14
17
  version: 1;
15
18
  state: "active" | "released";
@@ -19,6 +22,7 @@ export interface LeaseMetadata {
19
22
  root: string;
20
23
  pid: number;
21
24
  acquired_at: string;
25
+ captured_worker?: CapturedWorkerIdentity;
22
26
  released_at?: string;
23
27
  release_reason?: string;
24
28
  }
@@ -35,6 +39,8 @@ export interface WorkspaceLease {
35
39
  recovered: boolean | "unknown";
36
40
  /** Attach the governed resource so parent death cleans it before the kernel lock is released. */
37
41
  attachProcess(pid: number): void;
42
+ /** Persist exact gated-worker ownership before its command may execute. */
43
+ attachCapturedWorker(identity: CapturedWorkerIdentity): Promise<void>;
38
44
  attachHerdrTab(tabId: string): void;
39
45
  /** Resolves only when the kernel-lock helper exits before explicit release. */
40
46
  lost: Promise<Error>;
@@ -104,7 +110,19 @@ export function leasePaths(leaseDir: string, root: string) {
104
110
  export async function atomicMetadata(path: string, value: LeaseMetadata): Promise<void> {
105
111
  const temp = `${path}.${process.pid}.${randomUUID()}.tmp`;
106
112
  await writeFile(temp, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", flag: "wx", mode: 0o600 });
113
+ const file = await open(temp, "r+");
114
+ try {
115
+ await file.sync();
116
+ } finally {
117
+ await file.close();
118
+ }
107
119
  await rename(temp, path);
120
+ const directory = await open(dirname(path), "r");
121
+ try {
122
+ await directory.sync();
123
+ } finally {
124
+ await directory.close();
125
+ }
108
126
  }
109
127
 
110
128
  /**
@@ -124,6 +142,7 @@ export async function readMetadata(path: string): Promise<LeaseMetadata | null |
124
142
  if (value?.version !== 1) return "malformed";
125
143
  if (value.state !== "active" && value.state !== "released") return "malformed";
126
144
  if (typeof value.token !== "string" || !value.token) return "malformed";
145
+ if (value.captured_worker !== undefined && !isCapturedWorkerIdentity(value.captured_worker)) return "malformed";
127
146
  return value as LeaseMetadata;
128
147
  } catch {
129
148
  return "malformed";
@@ -150,13 +150,13 @@ export interface ChildLifecycleEvent extends LedgerEventBase {
150
150
  idleTimeoutMs?: number;
151
151
  /** The provider/model selected for the child, confirmed from its persisted assistant message when available. */
152
152
  resolvedModel?: { provider: string; modelId: string } | null;
153
- modelSource?: "explicit" | "session" | "definition" | "global" | "pi";
153
+ modelSource?: "explicit" | "session" | "definition" | "authored" | "global" | "pi";
154
154
  /** The persisted effective level and its selection source, when pi reports one. */
155
155
  thinkingLevel?: {
156
156
  level: string;
157
- source: "explicit" | "session" | "advisor" | "definition" | "global" | "pi";
157
+ source: "explicit" | "session" | "advisor" | "definition" | "authored" | "global" | "pi";
158
158
  } | null;
159
- thinkingSource?: "explicit" | "session" | "advisor" | "definition" | "global" | "pi";
159
+ thinkingSource?: "explicit" | "session" | "advisor" | "definition" | "authored" | "global" | "pi";
160
160
  /** Provider-reported token dimensions; zero is normalised to null rather than invented as reported usage. */
161
161
  tokenDetail?: ChildTokenDetail;
162
162
  /** Aggregate model usage read from the child's pi session file after it stopped. */
@@ -353,9 +353,9 @@ export function buildChildLifecycleEvent(args: {
353
353
  truncated?: boolean;
354
354
  reason?: string;
355
355
  resolvedModel?: { provider: string; modelId: string };
356
- modelSource?: "explicit" | "session" | "definition" | "global" | "pi";
356
+ modelSource?: "explicit" | "session" | "definition" | "authored" | "global" | "pi";
357
357
  effectiveThinkingLevel?: string;
358
- thinkingSource?: "explicit" | "session" | "advisor" | "definition" | "global" | "pi";
358
+ thinkingSource?: "explicit" | "session" | "advisor" | "definition" | "authored" | "global" | "pi";
359
359
  tokenDetail?: ChildTokenDetail;
360
360
  usage?: ChildUsageTotals;
361
361
  compactionCount?: number;
@@ -406,7 +406,7 @@ function validResolvedModel(value: unknown): boolean {
406
406
  );
407
407
  }
408
408
 
409
- const MODEL_SOURCES = ["explicit", "session", "definition", "global", "pi"];
409
+ const MODEL_SOURCES = ["explicit", "session", "definition", "authored", "global", "pi"];
410
410
  const THINKING_SOURCES = [...MODEL_SOURCES, "advisor"];
411
411
  function validThinkingLevel(value: unknown): boolean {
412
412
  return (
@@ -40,8 +40,9 @@
40
40
  *
41
41
  * Keyed by registry path, not by project: accepting an id is a statement about the operator's registry.
42
42
  */
43
- import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
43
+ import { mkdir, rename, writeFile } from "node:fs/promises";
44
44
  import { dirname, join } from "node:path";
45
+ import { readAuthorityText } from "../kernel/authority-text.ts";
45
46
  import { acceptedWorkspacesPath } from "../kernel/project-paths.ts";
46
47
 
47
48
  export interface AcceptedWorkspaces {
@@ -84,12 +85,23 @@ export async function reconcileAcceptedWorkspaces(
84
85
  registryPath: string,
85
86
  registryIds: readonly string[],
86
87
  env: NodeJS.ProcessEnv = process.env,
88
+ /** Trusted physical-read dependency; never supplied by a registry or requester. */
89
+ read: (path: string) => Promise<string> = readAuthorityText,
90
+ assertHealthy?: () => void,
87
91
  ): Promise<AcceptedWorkspaces> {
92
+ assertHealthy?.();
88
93
  const path = acceptedWorkspacesPath(registryPath, env);
89
- const text = await readFile(path, "utf8").catch(() => undefined);
94
+ const text = await read(path).catch((error: unknown) => {
95
+ // Unknown decisions are not first use; preserve physical failures and their recovery capabilities.
96
+ if (!(error instanceof Error) || !("code" in error) || error.code !== "ENOENT") throw error;
97
+ assertHealthy?.();
98
+ return undefined;
99
+ });
100
+ assertHealthy?.();
90
101
  if (text === undefined) {
91
102
  const accepted = [...registryIds].sort();
92
- await write(path, { version: 1, registry: registryPath, accepted });
103
+ await write(path, { version: 1, registry: registryPath, accepted }, assertHealthy);
104
+ assertHealthy?.();
93
105
  return { accepted, firstUse: true, unaccepted: [] };
94
106
  }
95
107
  const stored = parse(text);
@@ -116,9 +128,13 @@ export async function acceptWorkspaces(
116
128
  }
117
129
 
118
130
  /** Atomic, for the reason every other store here is: a half-written record reads as a narrower decision. */
119
- async function write(path: string, body: StoredAcceptance): Promise<void> {
131
+ async function write(path: string, body: StoredAcceptance, assertHealthy?: () => void): Promise<void> {
132
+ assertHealthy?.();
120
133
  await mkdir(dirname(path), { recursive: true });
134
+ assertHealthy?.();
121
135
  const temp = join(dirname(path), `.${Math.random().toString(36).slice(2)}.tmp`);
122
136
  await writeFile(temp, `${JSON.stringify(body, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
137
+ assertHealthy?.();
123
138
  await rename(temp, path);
139
+ assertHealthy?.();
124
140
  }
@@ -1,3 +1,10 @@
1
+ import {
2
+ readCapturedWorkerReceipt,
3
+ matchesWorkerWorkspace,
4
+ readWorkerRecord,
5
+ sameWorkerIdentity,
6
+ isCapturedWorkerIdentity,
7
+ } from "./captured-worker-record.ts";
1
8
  import { createHash, randomUUID } from "node:crypto";
2
9
  import {
3
10
  atomicMetadata,
@@ -84,6 +91,7 @@ export async function acquireWorkspaceLease(input: {
84
91
  ownerId: input.ownerId,
85
92
  recovered: false,
86
93
  attachProcess: () => {},
94
+ attachCapturedWorker: async () => {},
87
95
  attachHerdrTab: () => {},
88
96
  // A read lease took no kernel lock, so there is nothing to keep: `not-held` is the same answer
89
97
  // `release()` gives, and it is the truth rather than a retention that never happened (R-152).
@@ -238,8 +246,25 @@ export async function acquireWorkspaceLease(input: {
238
246
  }
239
247
 
240
248
  const previous = await readMetadata(paths.metadata);
249
+ // A free kernel lock cannot establish settlement of an earlier captured subtree.
250
+ if (
251
+ previous === "malformed" ||
252
+ (previous?.captured_worker &&
253
+ (!(await matchesWorkerWorkspace(previous.captured_worker, input.workspace.root)) ||
254
+ !(await readCapturedWorkerReceipt(previous.captured_worker)))) ||
255
+ (previous?.state === "active" && !previous.captured_worker)
256
+ ) {
257
+ holder.stdin?.end();
258
+ throw new GovernanceRefusal(
259
+ refusal(
260
+ "WORKSPACE_LEASE_STALE",
261
+ "predecessor workspace ownership is unresolved; original matching worker settlement evidence is required",
262
+ { workspace_id: input.workspace.workspaceId, root: input.workspace.root },
263
+ ),
264
+ );
265
+ }
241
266
  // "malformed" is not "no prior owner": an unreadable record cannot prove a clean handover (R-100).
242
- const recovered: boolean | "unknown" = previous === "malformed" ? "unknown" : previous?.state === "active";
267
+ const recovered = previous?.state === "active";
243
268
  const token = randomUUID();
244
269
  const metadata: LeaseMetadata = {
245
270
  version: 1,
@@ -295,6 +320,28 @@ export async function acquireWorkspaceLease(input: {
295
320
  access: "write",
296
321
  ownerId: input.ownerId,
297
322
  recovered,
323
+ async attachCapturedWorker(identity) {
324
+ if (releasing || holder.exitCode !== null || holder.signalCode !== null)
325
+ throw new Error("writer lease lost before captured ownership persistence");
326
+ if (!isCapturedWorkerIdentity(identity) || !(await matchesWorkerWorkspace(identity, input.workspace.root)))
327
+ throw new Error("captured identity does not match leased workspace");
328
+ const current = await readMetadata(paths.metadata);
329
+ if (current === "malformed" || current?.token !== token)
330
+ throw new Error("writer ownership record changed before captured attachment");
331
+ if (metadata.captured_worker && !sameWorkerIdentity(metadata.captured_worker, identity))
332
+ throw new Error("writer lease already owns another captured execution");
333
+ const record = (await readWorkerRecord(identity.ownershipPath)) as { state?: unknown; identity?: unknown };
334
+ if (
335
+ record.state !== "ready" ||
336
+ !isCapturedWorkerIdentity(record.identity) ||
337
+ !sameWorkerIdentity(identity, record.identity)
338
+ )
339
+ throw new Error("captured readiness record mismatch");
340
+ metadata.captured_worker = identity;
341
+ await atomicMetadata(paths.metadata, metadata);
342
+ if (holder.exitCode !== null || holder.signalCode !== null)
343
+ throw new Error("writer lease lost during captured ownership persistence");
344
+ },
298
345
  attachProcess(pid) {
299
346
  attach({ process_pid: pid });
300
347
  },
@@ -383,6 +430,17 @@ export async function acquireWorkspaceLease(input: {
383
430
  async release(reason = "completed"): Promise<LeaseReleaseOutcome> {
384
431
  // Memoized, not a boolean. A second call used to answer "released" whatever the first returned, so a
385
432
  // retry — or a `finally` after an explicit release — read a clean handover that never happened.
433
+ if (metadata.captured_worker) {
434
+ const receipt = await readCapturedWorkerReceipt(metadata.captured_worker);
435
+ if (!receipt) {
436
+ holder.unref();
437
+ unrefStream(holder.stdout);
438
+ unrefStream(holder.stderr);
439
+ return (settled = "retained");
440
+ }
441
+ // Only original matching receipt can reconcile a previously uncertain captured worker.
442
+ if (settled === "retained") settled = undefined;
443
+ }
386
444
  if (settled) return settled;
387
445
  releasing = true;
388
446
  // Already gone: this owner never released anything, and saying so is the caller's business to
@@ -0,0 +1,17 @@
1
+ /** Authority metadata: only ENOENT is absence; damage and unresolved cleanup preserve failure. */
2
+ import { readBoundedFile } from "./bounded-read.ts";
3
+ export async function readAuthorityText(path: string): Promise<string> {
4
+ const read = await readBoundedFile(path, { maxBytes: 1024 * 1024, timeoutMs: 2_000 });
5
+ if (read.ok) return read.text;
6
+ const error = new Error(`Cannot read ${path}: ${read.why} (${read.detail})`);
7
+ if ("code" in read && read.code) Object.assign(error, { code: read.code });
8
+ throw error;
9
+ }
10
+ export async function readOptionalAuthorityText(path: string): Promise<string | undefined> {
11
+ try {
12
+ return await readAuthorityText(path);
13
+ } catch (error) {
14
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
15
+ throw error;
16
+ }
17
+ }
@@ -0,0 +1,42 @@
1
+ import { BoundedReadCleanupError } from "./bounded-read.ts";
2
+
3
+ /** Independent failed readers remain typed, with every original recovery capability intact. */
4
+ export class CollectedBoundedReadCleanupError extends BoundedReadCleanupError {
5
+ readonly errors: readonly BoundedReadCleanupError[];
6
+ constructor(errors: readonly BoundedReadCleanupError[]) {
7
+ const members = Object.freeze([...errors]);
8
+ super(members, async () => {
9
+ const outcomes = await Promise.allSettled(members.map((error) => error.cleanup()));
10
+ const failed = members.filter((_error, index) => outcomes[index].status === "rejected");
11
+ if (failed.length) throw collectBoundedReadFailures(failed);
12
+ });
13
+ this.name = "CollectedBoundedReadCleanupError";
14
+ this.message = `${members.length} bounded reader descriptor cleanups unresolved; retain and explicitly retry`;
15
+ this.errors = members;
16
+ }
17
+ }
18
+
19
+ /** Preserve singleton identity; flatten only our typed collection, never arbitrary caller errors. */
20
+ export function collectBoundedReadFailures(errors: readonly unknown[]): BoundedReadCleanupError | undefined {
21
+ const members = [
22
+ ...new Set(
23
+ errors.flatMap((error) =>
24
+ error instanceof CollectedBoundedReadCleanupError
25
+ ? error.errors
26
+ : error instanceof BoundedReadCleanupError
27
+ ? [error]
28
+ : [],
29
+ ),
30
+ ),
31
+ ];
32
+ const existing = errors.find(
33
+ (error) =>
34
+ error instanceof CollectedBoundedReadCleanupError &&
35
+ error.errors.length === members.length &&
36
+ error.errors.every((member, index) => member === members[index]),
37
+ );
38
+ return (
39
+ (existing as CollectedBoundedReadCleanupError | undefined) ??
40
+ (members.length > 1 ? new CollectedBoundedReadCleanupError(members) : members[0])
41
+ );
42
+ }
@@ -0,0 +1,87 @@
1
+ /** Bounded synchronous reads for the synchronous planner/settings seams. */
2
+ import { constants, openSync, closeSync, fstatSync, readSync, realpathSync } from "node:fs";
3
+ import { isAbsolute, relative } from "node:path";
4
+ import { BoundedReadCleanupError } from "./bounded-read.ts";
5
+
6
+ export interface SyncReadPorts {
7
+ open: typeof openSync;
8
+ close: typeof closeSync;
9
+ stat: typeof fstatSync;
10
+ read: typeof readSync;
11
+ realpath: (path: string) => string;
12
+ }
13
+ const ports: SyncReadPorts = {
14
+ open: openSync,
15
+ close: closeSync,
16
+ stat: fstatSync,
17
+ read: readSync,
18
+ realpath: realpathSync,
19
+ };
20
+ const unresolved = new Set<BoundedReadCleanupError>();
21
+ /** Diagnostic owners only: a numeric fd cannot safely be retried after close failed and may have been reused. */
22
+ export function unresolvedSyncReadCleanups(): readonly BoundedReadCleanupError[] {
23
+ return [...unresolved];
24
+ }
25
+ export function readBoundedTextSync(
26
+ path: string,
27
+ options: {
28
+ maxBytes: number;
29
+ timeoutMs?: number;
30
+ prefix?: boolean;
31
+ confinedRoot?: string;
32
+ now?: () => number;
33
+ },
34
+ io: SyncReadPorts = ports,
35
+ ): { text: string; truncated: boolean; size: number } {
36
+ const now = options.now ?? Date.now;
37
+ const deadline = now() + (options.timeoutMs ?? 2_000);
38
+ const fd = io.open(path, constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0));
39
+ try {
40
+ const stat = io.stat(fd);
41
+ if (!stat.isFile()) throw new Error("not a regular file");
42
+ // Linux /proc resolves the held object, including ancestor substitutions between realpath and open.
43
+ // If this proof is unavailable, named context files are refused rather than trusting the earlier path.
44
+ if (options.confinedRoot) {
45
+ const held = io.realpath(`/proc/self/fd/${fd}`);
46
+ const within = relative(options.confinedRoot, held);
47
+ if (!within || within === ".." || within.startsWith("../") || isAbsolute(within))
48
+ throw new Error("held file is outside this session's working directory");
49
+ }
50
+ if (!options.prefix && stat.size > options.maxBytes) throw new Error("file exceeds byte limit");
51
+ const bytes = Buffer.alloc(options.maxBytes + 1);
52
+ let offset = 0;
53
+ while (offset < bytes.length) {
54
+ if (now() > deadline) throw new Error("bounded read timed out");
55
+ const count = io.read(fd, bytes, offset, Math.min(64 * 1024, bytes.length - offset), offset);
56
+ if (!count) break;
57
+ offset += count;
58
+ }
59
+ const truncated = offset > options.maxBytes;
60
+ if (truncated && !options.prefix) throw new Error("file grew beyond byte limit");
61
+ // stream:true accepts only an incomplete final codepoint at a deliberately truncated boundary.
62
+ // Malformed bytes in the retained prefix still throw; valid replacement characters remain intact.
63
+ let text: string;
64
+ try {
65
+ text = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(
66
+ bytes.subarray(0, Math.min(offset, options.maxBytes)),
67
+ { stream: truncated },
68
+ );
69
+ } catch (cause) {
70
+ throw new Error("invalid UTF-8", { cause });
71
+ }
72
+ return { text, truncated, size: stat.size };
73
+ } finally {
74
+ try {
75
+ io.close(fd);
76
+ } catch (cause) {
77
+ let failure: BoundedReadCleanupError;
78
+ failure = new BoundedReadCleanupError(cause, async () => {
79
+ throw failure;
80
+ });
81
+ failure.message =
82
+ "synchronous reader close failed; descriptor ownership is unresolved; automatic retry is unsafe";
83
+ unresolved.add(failure);
84
+ throw failure;
85
+ }
86
+ }
87
+ }