@abelxiaoxing/cadence 1.8.4 → 2.0.1

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 (202) hide show
  1. package/README.md +324 -117
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -18
  4. package/agents/diagnosis-worker.md +14 -14
  5. package/agents/implementation-worker.md +16 -17
  6. package/config/workflow-task.example.json +7 -0
  7. package/docs/remote-access.md +60 -0
  8. package/docs/reuse-and-distribution.md +44 -0
  9. package/docs/service-management.md +108 -0
  10. package/licenses/bundled-web.txt +6245 -0
  11. package/licenses/pi-web-ui-MIT.txt +21 -0
  12. package/licenses/pi-web-ui-reuse.md +22 -0
  13. package/package.json +92 -20
  14. package/src/index.ts +21 -1802
  15. package/src/native-build.ts +6 -0
  16. package/src/native-client.ts +103 -0
  17. package/src/native-export.ts +58 -0
  18. package/src/native-history.ts +359 -0
  19. package/src/operator-cli.mjs +453 -1937
  20. package/src/plan-proposal.ts +87 -0
  21. package/src/sdk-worker.mjs +549 -0
  22. package/src/service/contracts/active.ts +82 -0
  23. package/src/service/contracts/budget.ts +148 -0
  24. package/src/service/contracts/build.ts +67 -0
  25. package/src/service/contracts/capture.ts +48 -0
  26. package/src/service/contracts/core.ts +33 -0
  27. package/src/service/contracts/current.ts +261 -0
  28. package/src/service/contracts/domain.ts +68 -0
  29. package/src/service/contracts/history.ts +2245 -0
  30. package/src/service/contracts/models.ts +139 -0
  31. package/src/service/coordination.ts +862 -0
  32. package/src/service/private-files.ts +204 -0
  33. package/src/service-cli.mjs +5592 -0
  34. package/src/service-client.ts +747 -0
  35. package/src/service-contract.ts +245 -0
  36. package/src/service-host.mjs +22382 -0
  37. package/src/service-http-client.ts +425 -0
  38. package/src/service-wire.ts +372 -0
  39. package/src/task-shell.ts +1045 -0
  40. package/src/task-store.ts +479 -0
  41. package/src/task-types.ts +184 -0
  42. package/src/verification-diagnostics.ts +29 -72
  43. package/src/worker-capture.mjs +1197 -0
  44. package/src/worker-public.mjs +1753 -0
  45. package/src/worker-redaction.mjs +608 -0
  46. package/src/worker-tools.mjs +202 -0
  47. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  50. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  56. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  62. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  65. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  68. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  71. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  74. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  77. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  80. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  89. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  92. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  95. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  98. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  101. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  104. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  107. package/web/dist/assets/cadence.css +2 -0
  108. package/web/dist/assets/cadence.js +7 -0
  109. package/web/dist/assets/markdown.js +296 -0
  110. package/web/dist/assets/react.js +9 -0
  111. package/web/dist/assets/rolldown-runtime.js +1 -0
  112. package/web/dist/assets/timeline-client.js +41 -0
  113. package/web/dist/assets/vis-timeline.css +1274 -0
  114. package/web/dist/assets/vis-timeline.js +32 -0
  115. package/web/dist/index.html +18 -0
  116. package/web/dist/manifest.json +505 -0
  117. package/config/plan-draft.example.json +0 -125
  118. package/config/plan-draft.multiple-tasks.example.json +0 -375
  119. package/config/plan-draft.quick.example.json +0 -78
  120. package/prompts/abel-design.md +0 -382
  121. package/prompts/abel-diagnose.md +0 -71
  122. package/prompts/abel-implement.md +0 -284
  123. package/prompts/abel-init.md +0 -52
  124. package/src/activation.ts +0 -44
  125. package/src/activity-contracts.ts +0 -82
  126. package/src/agent-registry.ts +0 -53
  127. package/src/apply-transaction.ts +0 -1437
  128. package/src/artifact-store.ts +0 -213
  129. package/src/build-windows-job.mjs +0 -95
  130. package/src/candidate-artifact.ts +0 -143
  131. package/src/candidate-context.ts +0 -289
  132. package/src/candidate-patch.ts +0 -271
  133. package/src/candidate-workspace.ts +0 -80
  134. package/src/canonical.ts +0 -16
  135. package/src/change-contract-schema.ts +0 -191
  136. package/src/change-contract.ts +0 -324
  137. package/src/change-verification.ts +0 -1617
  138. package/src/contracts.ts +0 -2466
  139. package/src/control-contracts.ts +0 -382
  140. package/src/delivery-compiler.ts +0 -1805
  141. package/src/delivery-revision.ts +0 -42
  142. package/src/design-control.ts +0 -1299
  143. package/src/design-diagnostics.ts +0 -181
  144. package/src/design-journal.ts +0 -1773
  145. package/src/directory-sync.ts +0 -36
  146. package/src/durable-contracts.ts +0 -227
  147. package/src/durable-workflow.ts +0 -822
  148. package/src/evidence-draft.ts +0 -122
  149. package/src/execution-profile.ts +0 -64
  150. package/src/execution-retention.ts +0 -77
  151. package/src/file-snapshot.ts +0 -163
  152. package/src/implement-continuation.ts +0 -559
  153. package/src/implement-graph.ts +0 -690
  154. package/src/implement-plan.ts +0 -72
  155. package/src/install-config.d.mts +0 -6
  156. package/src/isolation-backend.ts +0 -353
  157. package/src/model-source.ts +0 -22
  158. package/src/openspec-cli.ts +0 -494
  159. package/src/operator-cli-entry.mjs +0 -24
  160. package/src/operator-tools.ts +0 -309
  161. package/src/package-candidate-process.ts +0 -206
  162. package/src/package-candidate.ts +0 -24
  163. package/src/package-delivery.ts +0 -486
  164. package/src/package-state.ts +0 -320
  165. package/src/package-verification.ts +0 -635
  166. package/src/package-workflow.ts +0 -305
  167. package/src/packet-runtime.ts +0 -459
  168. package/src/phase-execution.ts +0 -2163
  169. package/src/pi-adapter.ts +0 -19
  170. package/src/plan-draft-summary.ts +0 -527
  171. package/src/plan-draft.ts +0 -502
  172. package/src/remove-path.ts +0 -32
  173. package/src/run-state.ts +0 -420
  174. package/src/run-store.ts +0 -1179
  175. package/src/safe-path.ts +0 -77
  176. package/src/scoped-grep-worker.mjs +0 -23
  177. package/src/scoped-tools.ts +0 -775
  178. package/src/single-task-draft.ts +0 -142
  179. package/src/sqlite-schema.ts +0 -171
  180. package/src/state-root.ts +0 -215
  181. package/src/storage-schema.ts +0 -360
  182. package/src/subagent-activity.ts +0 -1417
  183. package/src/subagent-process.ts +0 -476
  184. package/src/task-ledger.ts +0 -1338
  185. package/src/transport-budget.ts +0 -50
  186. package/src/verification-capability.ts +0 -1191
  187. package/src/verification-environment-io.ts +0 -389
  188. package/src/verification-environment.ts +0 -162
  189. package/src/verification-identity.ts +0 -77
  190. package/src/verification-scan.ts +0 -64
  191. package/src/windows-job-backend.ts +0 -419
  192. package/src/windows-job.c +0 -149
  193. package/src/workflow-engine.ts +0 -28
  194. package/src/workflow-policy.ts +0 -1670
  195. package/src/workflow-recovery-policy.ts +0 -97
  196. package/src/workflow-scheduling.ts +0 -105
  197. package/src/workflow-state-machine.ts +0 -4341
  198. package/src/workflow-status.ts +0 -576
  199. package/src/workspace-io-entry.mjs +0 -76
  200. package/src/workspace-io-worker.mjs +0 -1312
  201. package/src/workspace-io.ts +0 -89
  202. package/src/workspace-store.ts +0 -634
@@ -0,0 +1,862 @@
1
+ import { randomBytes, randomUUID } from "node:crypto";
2
+ import { opendirSync, realpathSync } from "node:fs";
3
+ import { hostname } from "node:os";
4
+ import { dirname, isAbsolute, join, relative, sep } from "node:path";
5
+ import {
6
+ type CheckoutLock,
7
+ resolveWorkspace,
8
+ TaskStore,
9
+ } from "../task-store.ts";
10
+ import {
11
+ ContractError,
12
+ closed,
13
+ decodeHash,
14
+ decodeId,
15
+ integer,
16
+ list,
17
+ literal,
18
+ oneOf,
19
+ text,
20
+ } from "./contracts/core.ts";
21
+ import {
22
+ encoded,
23
+ type FileSnapshot,
24
+ hash,
25
+ json,
26
+ PrivateFiles,
27
+ REGISTRY_BYTES,
28
+ REGISTRY_ENTRIES,
29
+ refused,
30
+ } from "./private-files.ts";
31
+
32
+ export interface OwnerConfig {
33
+ schema: "cadence-service-owner-config/1";
34
+ /** Requested loopback policy; :0 persists unchanged across explicit starts. */
35
+ endpoint: string;
36
+ /** Identity of secret-free selected service policy, NOT provider/auth bytes. */
37
+ policyIdentity: string;
38
+ }
39
+ export type OwnerPhase = "idle" | "stopping" | "quarantined" | "stopped";
40
+ export interface Registration {
41
+ schema: "cadence-service-registration/1";
42
+ root: string;
43
+ stateHome: string;
44
+ configIdentity: string;
45
+ endpoint: string;
46
+ epoch: string;
47
+ pid: number;
48
+ host: string;
49
+ ownerToken: string;
50
+ checkoutToken: string;
51
+ phase: OwnerPhase;
52
+ }
53
+ type Reservation = Omit<Registration, "schema" | "phase" | "checkoutToken">;
54
+ interface Registry {
55
+ schema: "cadence-service-registry/1";
56
+ entries: Reservation[];
57
+ }
58
+ const registrationKeys = [
59
+ "schema",
60
+ "root",
61
+ "stateHome",
62
+ "configIdentity",
63
+ "endpoint",
64
+ "epoch",
65
+ "pid",
66
+ "host",
67
+ "ownerToken",
68
+ "checkoutToken",
69
+ "phase",
70
+ ];
71
+ const reservationKeys = registrationKeys.filter(
72
+ (k) => !["schema", "phase", "checkoutToken"].includes(k),
73
+ );
74
+ function pathValue(value: unknown): string {
75
+ const path = text(value, 4096, true);
76
+ if (!isAbsolute(path)) refused();
77
+ return path;
78
+ }
79
+ function endpoint(value: unknown, allowZero = false): string {
80
+ const s = text(value, 128, true);
81
+ const match = /^http:\/\/(127\.0\.0\.1|\[::1\]):(0|[1-9][0-9]{0,4})$/.exec(s);
82
+ if (!match || Number(match[2]) > 65535 || (!allowZero && match[2] === "0"))
83
+ refused();
84
+ return s;
85
+ }
86
+ function endpointMatches(requested: string, bound: string): boolean {
87
+ return requested.endsWith(":0")
88
+ ? requested.slice(0, -1) === bound.slice(0, bound.lastIndexOf(":") + 1)
89
+ : requested === bound;
90
+ }
91
+ export function decodeOwnerConfig(value: unknown): OwnerConfig {
92
+ const o = closed(value, ["schema", "endpoint", "policyIdentity"]);
93
+ return {
94
+ schema: literal(o.schema, "cadence-service-owner-config/1"),
95
+ endpoint: endpoint(o.endpoint, true),
96
+ policyIdentity: decodeHash(o.policyIdentity),
97
+ };
98
+ }
99
+ function decodeReservation(value: unknown): Reservation {
100
+ const o = closed(value, reservationKeys);
101
+ return {
102
+ root: pathValue(o.root),
103
+ stateHome: pathValue(o.stateHome),
104
+ configIdentity: decodeHash(o.configIdentity),
105
+ endpoint: endpoint(o.endpoint),
106
+ epoch: decodeId(o.epoch),
107
+ pid: integer(o.pid, 1),
108
+ host: text(o.host, 255, true),
109
+ ownerToken: decodeId(o.ownerToken),
110
+ };
111
+ }
112
+ function reservation(r: Registration): Reservation {
113
+ const {
114
+ schema: _schema,
115
+ phase: _phase,
116
+ checkoutToken: _checkout,
117
+ ...row
118
+ } = r;
119
+ return row;
120
+ }
121
+ function decodeRegistration(value: unknown): Registration {
122
+ const o = closed(value, registrationKeys);
123
+ const row = Object.fromEntries(reservationKeys.map((k) => [k, o[k]]));
124
+ return {
125
+ schema: literal(o.schema, "cadence-service-registration/1"),
126
+ ...decodeReservation(row),
127
+ checkoutToken: decodeId(o.checkoutToken),
128
+ phase: oneOf(o.phase, ["idle", "stopping", "quarantined", "stopped"]),
129
+ };
130
+ }
131
+ /** Segment-aware, symmetric, independent of Git classification. */
132
+ export function rootsOverlap(a: string, b: string): boolean {
133
+ const within = (x: string, y: string) => {
134
+ const r = relative(x, y);
135
+ return (
136
+ r === "" || (r !== ".." && !r.startsWith(`..${sep}`) && !isAbsolute(r))
137
+ );
138
+ };
139
+ return within(a, b) || within(b, a);
140
+ }
141
+ function names(path: string, allowed: readonly string[]): void {
142
+ const dir = opendirSync(path);
143
+ try {
144
+ let count = 0;
145
+ for (let entry = dir.readSync(); entry; entry = dir.readSync()) {
146
+ if (++count > allowed.length || !allowed.includes(entry.name)) refused();
147
+ }
148
+ } finally {
149
+ dir.closeSync();
150
+ }
151
+ }
152
+ interface RegistryView {
153
+ format: FileSnapshot;
154
+ snapshot: FileSnapshot;
155
+ value: Registry;
156
+ }
157
+ interface Selected {
158
+ config: FileSnapshot;
159
+ hostConfig?: FileSnapshot;
160
+ manifest: FileSnapshot;
161
+ registration?: FileSnapshot;
162
+ owner?: FileSnapshot;
163
+ capability?: FileSnapshot;
164
+ checkout?: FileSnapshot;
165
+ value?: Registration;
166
+ }
167
+ export function recoveryOwnerStatus(
168
+ pid: number,
169
+ host: string,
170
+ ): "live" | "absent" | "ambiguous" {
171
+ if (host !== hostname()) return "ambiguous";
172
+ try {
173
+ process.kill(pid, 0);
174
+ return "live";
175
+ } catch (error) {
176
+ return error instanceof Error && "code" in error && error.code === "ESRCH"
177
+ ? "absent"
178
+ : "ambiguous";
179
+ }
180
+ }
181
+ function recoveryDigest(registry: RegistryView, selected: Selected): string {
182
+ return hash(
183
+ encoded([
184
+ registry.format.identity,
185
+ registry.format.stamp,
186
+ registry.snapshot.identity,
187
+ registry.snapshot.stamp,
188
+ selected.capability?.stamp ?? null, // incarnation only; never a secret-derived capability digest
189
+ ...[
190
+ selected.manifest,
191
+ selected.config,
192
+ selected.hostConfig,
193
+ selected.registration,
194
+ selected.owner,
195
+ selected.checkout,
196
+ ].map((value) => (value ? [value.identity, value.stamp] : null)),
197
+ ]),
198
+ );
199
+ }
200
+ export type CoordinationStatus =
201
+ | { status: "absent" | "unavailable" }
202
+ | {
203
+ status: "stopped" | "unavailable" | "stopping" | "quarantined";
204
+ epoch: string | null;
205
+ };
206
+
207
+ /** Explicit filesystem ownership only. No listener, worker, auth resolution,
208
+ * task creation, stale reclaim or accepting execution route lives here.
209
+ */
210
+ export class ServiceCoordination {
211
+ readonly store: TaskStore;
212
+ readonly files: PrivateFiles;
213
+ readonly directory: string;
214
+ readonly registryDirectory: string;
215
+ readonly stateHome: string;
216
+ constructor(workspace: string, stateHome?: string) {
217
+ this.store = new TaskStore(resolveWorkspace(workspace).root, stateHome);
218
+ const namespace = dirname(this.store.privateRoot);
219
+ this.files = new PrivateFiles(namespace);
220
+ this.stateHome = dirname(namespace);
221
+ this.directory = join(this.store.privateRoot, "service-v1");
222
+ this.registryDirectory = join(namespace, "service-registry-v1");
223
+ }
224
+ path(
225
+ name:
226
+ | "format.json"
227
+ | "config.json"
228
+ | "registration.json"
229
+ | "owner.lock"
230
+ | "local-capability.json"
231
+ | "host-config.json"
232
+ | "access-config.json"
233
+ | "access.json",
234
+ ): string {
235
+ return join(this.directory, name);
236
+ }
237
+ private registryPath(name: string): string {
238
+ return join(this.registryDirectory, name);
239
+ }
240
+ private registry(): RegistryView | undefined {
241
+ if (!this.files.directory(this.registryDirectory)) return undefined;
242
+ names(this.registryDirectory, [
243
+ "format.json",
244
+ "roots.json",
245
+ "registry.lock",
246
+ ]);
247
+ const format = this.files.read(this.registryPath("format.json"));
248
+ const snapshot = this.files.read(
249
+ this.registryPath("roots.json"),
250
+ REGISTRY_BYTES,
251
+ );
252
+ if (!format || !snapshot) refused();
253
+ const f = closed(json(format), ["schema", "stateHome"]);
254
+ literal(f.schema, "cadence-service-registry-format/1");
255
+ literal(f.stateHome, this.stateHome);
256
+ const o = closed(json(snapshot, REGISTRY_BYTES), ["schema", "entries"]);
257
+ const entries = list(o.entries, REGISTRY_ENTRIES, decodeReservation);
258
+ for (const [i, row] of entries.entries()) {
259
+ if (
260
+ row.stateHome !== this.stateHome ||
261
+ realpathSync(row.root) !== row.root
262
+ )
263
+ refused();
264
+ // An endpoint cannot be shared by two supported owners, even for disjoint roots.
265
+ if (
266
+ entries
267
+ .slice(0, i)
268
+ .some(
269
+ (other) =>
270
+ rootsOverlap(row.root, other.root) ||
271
+ row.endpoint === other.endpoint,
272
+ )
273
+ )
274
+ refused();
275
+ }
276
+ return {
277
+ format,
278
+ snapshot,
279
+ value: {
280
+ schema: literal(o.schema, "cadence-service-registry/1"),
281
+ entries,
282
+ },
283
+ };
284
+ }
285
+ assertNamespace(): void {
286
+ if (!this.files.directory(this.directory)) refused();
287
+ names(this.directory, [
288
+ "format.json",
289
+ "config.json",
290
+ "registration.json",
291
+ "owner.lock",
292
+ "local-capability.json",
293
+ "access-config.json",
294
+ "access.json",
295
+ "host-config.json",
296
+ "history-v2",
297
+ ]);
298
+ this.files.directory(join(this.directory, "history-v2"));
299
+ }
300
+ private selected(): Selected | undefined {
301
+ if (!this.files.directory(this.directory)) return undefined;
302
+ this.assertNamespace();
303
+ const manifest = this.files.read(this.path("format.json"));
304
+ const config = this.files.read(this.path("config.json"));
305
+ if (!manifest || !config) refused();
306
+ const f = closed(json(manifest), [
307
+ "schema",
308
+ "root",
309
+ "stateHome",
310
+ "configIdentity",
311
+ ]);
312
+ literal(f.schema, "cadence-service-format/1");
313
+ literal(f.root, this.store.root);
314
+ literal(f.stateHome, this.stateHome);
315
+ literal(f.configIdentity, config.identity);
316
+ const c = decodeOwnerConfig(json(config));
317
+ const hostConfig = this.files.read(this.path("host-config.json"));
318
+ if (hostConfig && hostConfig.identity !== c.policyIdentity) refused();
319
+ const registration = this.files.read(this.path("registration.json"));
320
+ const owner = this.files.read(this.path("owner.lock"));
321
+ const capability = this.files.read(this.path("local-capability.json"));
322
+ const checkout = this.files.read(this.store.lockPath, 8192);
323
+ const value = registration
324
+ ? decodeRegistration(json(registration))
325
+ : undefined;
326
+ if (
327
+ value &&
328
+ (value.root !== this.store.root ||
329
+ value.stateHome !== this.stateHome ||
330
+ value.configIdentity !== config.identity ||
331
+ !endpointMatches(c.endpoint, value.endpoint))
332
+ )
333
+ refused();
334
+ if (value && value.phase !== "stopped") {
335
+ if (!owner || !checkout || !capability) refused();
336
+ const lock = closed(json(owner), [
337
+ "schema",
338
+ "epoch",
339
+ "token",
340
+ "pid",
341
+ "host",
342
+ ]);
343
+ literal(lock.schema, "cadence-service-owner-lock/1");
344
+ literal(lock.epoch, value.epoch);
345
+ literal(lock.token, value.ownerToken);
346
+ literal(lock.pid, value.pid);
347
+ literal(lock.host, value.host);
348
+ const legacy = this.store.inspectLock();
349
+ if (
350
+ !legacy ||
351
+ legacy.token !== value.checkoutToken ||
352
+ legacy.pid !== value.pid ||
353
+ legacy.host !== value.host
354
+ )
355
+ refused();
356
+ const cap = closed(json(capability), ["schema", "epoch", "capability"]);
357
+ literal(cap.schema, "cadence-service-local-capability/1");
358
+ literal(cap.epoch, value.epoch);
359
+ if (!/^[A-Za-z0-9_-]{43}$/.test(text(cap.capability, 43))) refused();
360
+ } else if (owner || capability || (value && checkout)) refused();
361
+ return {
362
+ config,
363
+ hostConfig,
364
+ manifest,
365
+ registration,
366
+ owner,
367
+ capability,
368
+ checkout,
369
+ value,
370
+ };
371
+ }
372
+ /** A PID alone cannot establish alive/authenticated readiness. Transport will
373
+ * challenge the epoch separately; stale file status remains unavailable here. */
374
+ inspect(): CoordinationStatus {
375
+ try {
376
+ if (this.files.read(this.registryPath("registry.lock")))
377
+ return { status: "unavailable" };
378
+ const registry = this.registry();
379
+ const selected = this.selected();
380
+ const row = registry?.value.entries.find(
381
+ (r) => r.root === this.store.root,
382
+ );
383
+ if (!selected)
384
+ return row ? { status: "unavailable" } : { status: "absent" };
385
+ if (!selected.value)
386
+ return row
387
+ ? { status: "unavailable" }
388
+ : { status: "stopped", epoch: null };
389
+ const r = selected.value;
390
+ if (r.phase === "stopped")
391
+ return { status: row ? "unavailable" : "stopped", epoch: r.epoch };
392
+ if (!row || encoded(row) !== encoded(reservation(r)))
393
+ return { status: "unavailable", epoch: r.epoch };
394
+ return {
395
+ status: r.phase === "idle" ? "unavailable" : r.phase,
396
+ epoch: r.epoch,
397
+ };
398
+ } catch {
399
+ return { status: "unavailable" };
400
+ }
401
+ }
402
+ /** Explicit initialization is not discovery. Partial initialization is never
403
+ * adopted/reset. Caller must retain the failure and request manual diagnosis. */
404
+ initialize(input: OwnerConfig): void {
405
+ const config = decodeOwnerConfig(input);
406
+ const existing = this.selected();
407
+ if (existing) {
408
+ if (existing.config.identity !== hash(encoded(config)))
409
+ throw new ContractError("conflict");
410
+ return;
411
+ }
412
+ this.files.directory(this.directory, true);
413
+ const saved = this.files.create(this.path("config.json"), encoded(config));
414
+ this.files.create(
415
+ this.path("format.json"),
416
+ encoded({
417
+ schema: "cadence-service-format/1",
418
+ root: this.store.root,
419
+ stateHome: this.stateHome,
420
+ configIdentity: saved.identity,
421
+ }),
422
+ );
423
+ }
424
+ private withRegistry<T>(
425
+ operation: (
426
+ view: RegistryView,
427
+ mutate: () => void,
428
+ assertHeld: () => void,
429
+ ) => T,
430
+ ): T {
431
+ this.files.directory(this.registryDirectory, true);
432
+ names(this.registryDirectory, [
433
+ "format.json",
434
+ "roots.json",
435
+ "registry.lock",
436
+ ]);
437
+ const lockPath = this.registryPath("registry.lock");
438
+ const lock = this.files.create(
439
+ lockPath,
440
+ encoded({
441
+ schema: "cadence-service-registry-lock/1",
442
+ token: randomUUID(),
443
+ pid: process.pid,
444
+ host: hostname(),
445
+ }),
446
+ );
447
+ let dirty = false;
448
+ try {
449
+ let format = this.files.read(this.registryPath("format.json"));
450
+ const roots = this.files.read(
451
+ this.registryPath("roots.json"),
452
+ REGISTRY_BYTES,
453
+ );
454
+ if (!format && !roots) {
455
+ dirty = true;
456
+ format = this.files.create(
457
+ this.registryPath("format.json"),
458
+ encoded({
459
+ schema: "cadence-service-registry-format/1",
460
+ stateHome: this.stateHome,
461
+ }),
462
+ );
463
+ this.files.create(
464
+ this.registryPath("roots.json"),
465
+ encoded({ schema: "cadence-service-registry/1", entries: [] }),
466
+ REGISTRY_BYTES,
467
+ );
468
+ dirty = false;
469
+ }
470
+ // Corrupt/partial existing registry is uncertainty, not an inert conflict.
471
+ dirty = true;
472
+ const view = this.registry();
473
+ if (!view) refused();
474
+ dirty = false;
475
+ const result = operation(
476
+ view,
477
+ () => {
478
+ dirty = true;
479
+ },
480
+ () => {
481
+ this.files.assert(lockPath, lock);
482
+ this.files.assert(this.registryPath("format.json"), format);
483
+ names(this.registryDirectory, [
484
+ "format.json",
485
+ "roots.json",
486
+ "registry.lock",
487
+ ]);
488
+ },
489
+ );
490
+ this.files.remove(lockPath, lock);
491
+ return result;
492
+ } catch (error) {
493
+ // Before any write, a conflict is a known inert refusal. Persistence or
494
+ // release uncertainty retains this exact short lock, never age-expires it.
495
+ if (!dirty) this.files.remove(lockPath, lock);
496
+ throw error;
497
+ }
498
+ }
499
+ acquire(
500
+ input: OwnerConfig,
501
+ resolvedEndpoint: string = input.endpoint,
502
+ initializeNew?: () => void,
503
+ ): OwnerLease {
504
+ const config = decodeOwnerConfig(input);
505
+ const bound = endpoint(resolvedEndpoint);
506
+ if (!endpointMatches(config.endpoint, bound))
507
+ throw new ContractError("conflict");
508
+ // Do not initialize a conflicting root as a side effect of a failed start.
509
+ return this.withRegistry((registry, mutate) => {
510
+ if (
511
+ registry.value.entries.some(
512
+ (r) => rootsOverlap(r.root, this.store.root) || r.endpoint === bound,
513
+ )
514
+ )
515
+ throw new ContractError("conflict");
516
+ if (registry.value.entries.length >= REGISTRY_ENTRIES)
517
+ throw new ContractError("capacity");
518
+ const old = this.selected();
519
+ if (old?.value && old.value.phase !== "stopped")
520
+ throw new ContractError("quarantine");
521
+ if (this.store.inspectLock()) throw new ContractError("conflict");
522
+ if (old && old.config.identity !== hash(encoded(config)))
523
+ throw new ContractError("conflict");
524
+ // Reserve BEFORE root initialization/legacy acquisition. Any later failure
525
+ // keeps the reservation and short lock as an explicit partial fence.
526
+ const row: Reservation = {
527
+ root: this.store.root,
528
+ stateHome: this.stateHome,
529
+ configIdentity: hash(encoded(config)),
530
+ endpoint: bound,
531
+ epoch: randomUUID(),
532
+ pid: process.pid,
533
+ host: hostname(),
534
+ ownerToken: randomUUID(),
535
+ };
536
+ mutate();
537
+ this.files.replace(
538
+ this.registryPath("roots.json"),
539
+ registry.snapshot,
540
+ encoded({
541
+ ...registry.value,
542
+ entries: [...registry.value.entries, row],
543
+ }),
544
+ REGISTRY_BYTES,
545
+ );
546
+ this.initialize(config);
547
+ // Called only for genuinely absent selected metadata, under the registry
548
+ // reservation and before registration. Failures preserve the partial fence.
549
+ if (!old) initializeNew?.();
550
+ const checkout = this.store.acquire();
551
+ const checkoutFile = this.files.read(this.store.lockPath, 8192);
552
+ if (!checkoutFile) refused();
553
+ const owner = this.files.create(
554
+ this.path("owner.lock"),
555
+ encoded({
556
+ schema: "cadence-service-owner-lock/1",
557
+ epoch: row.epoch,
558
+ token: row.ownerToken,
559
+ pid: row.pid,
560
+ host: row.host,
561
+ }),
562
+ );
563
+ const capability = this.files.create(
564
+ this.path("local-capability.json"),
565
+ encoded({
566
+ schema: "cadence-service-local-capability/1",
567
+ epoch: row.epoch,
568
+ capability: randomBytes(32).toString("base64url"),
569
+ }),
570
+ );
571
+ const value: Registration = {
572
+ schema: "cadence-service-registration/1",
573
+ ...row,
574
+ checkoutToken: checkout.token,
575
+ phase: "idle",
576
+ };
577
+ const registration = old?.registration
578
+ ? this.files.replace(
579
+ this.path("registration.json"),
580
+ old.registration,
581
+ encoded(value),
582
+ )
583
+ : this.files.create(this.path("registration.json"), encoded(value));
584
+ const selected = this.selected();
585
+ if (!selected) refused();
586
+ return new OwnerLease(this, checkout, {
587
+ ...selected,
588
+ owner,
589
+ capability,
590
+ checkout: checkoutFile,
591
+ registration,
592
+ value,
593
+ });
594
+ });
595
+ }
596
+ configuration(): OwnerConfig | undefined {
597
+ const selected = this.selected();
598
+ return selected ? decodeOwnerConfig(json(selected.config)) : undefined;
599
+ }
600
+ /** Private direct-native client material; never a public status projection. */
601
+ localAccess(): { endpoint: string; epoch: string; capability: string } {
602
+ if (this.files.read(this.registryPath("registry.lock"))) refused();
603
+ const selected = this.selected(),
604
+ registry = this.registry();
605
+ if (
606
+ !selected?.value ||
607
+ !selected.capability ||
608
+ !registry ||
609
+ selected.value.phase === "stopped" ||
610
+ !registry.value.entries.some(
611
+ (row) =>
612
+ selected.value &&
613
+ encoded(row) === encoded(reservation(selected.value)),
614
+ )
615
+ )
616
+ throw new ContractError("unavailable");
617
+ const cap = closed(json(selected.capability), [
618
+ "schema",
619
+ "epoch",
620
+ "capability",
621
+ ]);
622
+ return {
623
+ endpoint: selected.value.endpoint,
624
+ epoch: selected.value.epoch,
625
+ capability: text(cap.capability, 43, true),
626
+ };
627
+ }
628
+ /** Internal release adapter. Never called by status/restart/recovery. */
629
+ release(lease: OwnerLease): void {
630
+ this.withRegistry((view, mutate, assertHeld) => {
631
+ lease.assert();
632
+ const r = lease.registration;
633
+ const rows = view.value.entries.filter((row) => row.epoch === r.epoch);
634
+ if (rows.length !== 1 || encoded(rows[0]) !== encoded(reservation(r)))
635
+ refused();
636
+ mutate();
637
+ lease.phase("stopped");
638
+ const releasedRegistry = this.files.replace(
639
+ this.registryPath("roots.json"),
640
+ view.snapshot,
641
+ encoded({
642
+ ...view.value,
643
+ entries: view.value.entries.filter((row) => row.epoch !== r.epoch),
644
+ }),
645
+ REGISTRY_BYTES,
646
+ );
647
+ lease.removeOwnedFiles(() => {
648
+ assertHeld();
649
+ this.files.assert(
650
+ this.registryPath("roots.json"),
651
+ releasedRegistry,
652
+ REGISTRY_BYTES,
653
+ );
654
+ });
655
+ });
656
+ }
657
+ /** Capture exact private resources BEFORE the append/confirmation awaits. The
658
+ * closure is internal local management authority, never a wire capability.
659
+ * A durable bridge must be verified by the caller before EVERY release effect. */
660
+ prepareRecoveryRelease(
661
+ expected: string,
662
+ assertAuthority: () => void,
663
+ ): (assertDurable: () => void) => void {
664
+ assertAuthority();
665
+ if (this.recoverySnapshot().digest !== expected)
666
+ throw new ContractError("conflict");
667
+ const selected = this.selected();
668
+ if (
669
+ !selected?.value ||
670
+ !selected.registration ||
671
+ !selected.owner ||
672
+ !selected.checkout ||
673
+ !selected.capability
674
+ )
675
+ throw new ContractError("quarantine");
676
+ const r = selected.value;
677
+ const absent = () => {
678
+ assertAuthority();
679
+ if (recoveryOwnerStatus(r.pid, r.host) !== "absent")
680
+ throw new ContractError("quarantine");
681
+ };
682
+ absent();
683
+ let used = false;
684
+ return (assertDurable) => {
685
+ if (used) throw new ContractError("consumed-grant");
686
+ used = true;
687
+ this.withRegistry((view, mutate, assertHeld) => {
688
+ let registry = view.snapshot;
689
+ let registration = selected.registration;
690
+ let capability = selected.capability;
691
+ let owner = selected.owner;
692
+ let checkout = selected.checkout;
693
+ if (!registration || !capability || !owner || !checkout)
694
+ throw new ContractError("conflict");
695
+ if (recoveryDigest(view, selected) !== expected)
696
+ throw new ContractError("conflict");
697
+ const assert = () => {
698
+ absent();
699
+ assertHeld();
700
+ this.assertNamespace();
701
+ this.files.assert(
702
+ this.registryPath("roots.json"),
703
+ registry,
704
+ REGISTRY_BYTES,
705
+ );
706
+ for (const [name, snapshot] of [
707
+ ["format.json", selected.manifest],
708
+ ["config.json", selected.config],
709
+ ["host-config.json", selected.hostConfig],
710
+ ["registration.json", registration],
711
+ ["local-capability.json", capability],
712
+ ["owner.lock", owner],
713
+ ] as const)
714
+ this.files.assert(this.path(name), snapshot);
715
+ this.files.assert(this.store.lockPath, checkout, 8192);
716
+ assertDurable();
717
+ };
718
+ assert();
719
+ const matching = view.value.entries.filter(
720
+ (row) => row.epoch === r.epoch,
721
+ );
722
+ if (
723
+ matching.length !== 1 ||
724
+ encoded(matching[0]) !== encoded(reservation(r))
725
+ )
726
+ throw new ContractError("conflict");
727
+ mutate(); // every subsequent partial failure retains the registry fence
728
+ registration = this.files.replace(
729
+ this.path("registration.json"),
730
+ registration,
731
+ encoded({ ...r, phase: "stopped" }),
732
+ );
733
+ assert();
734
+ registry = this.files.replace(
735
+ this.registryPath("roots.json"),
736
+ registry,
737
+ encoded({
738
+ ...view.value,
739
+ entries: view.value.entries.filter((row) => row.epoch !== r.epoch),
740
+ }),
741
+ REGISTRY_BYTES,
742
+ );
743
+ assert();
744
+ this.files.remove(this.path("local-capability.json"), capability);
745
+ capability = undefined;
746
+ assert();
747
+ this.files.remove(this.path("owner.lock"), owner);
748
+ owner = undefined;
749
+ assert();
750
+ // Only the exact v1 lock tied to THIS registration, never a legacy lock.
751
+ this.files.remove(this.store.lockPath, checkout, 8192);
752
+ checkout = undefined;
753
+ assert();
754
+ });
755
+ };
756
+ }
757
+ /** Private recovery identity only. Does not read/scan task records or logs.
758
+ * W3 independently supplies the affected-history digest; capability bytes and
759
+ * their hashes are deliberately absent from this recovery projection. */
760
+ recoverySnapshot(): { registration: Registration; digest: string } {
761
+ if (this.files.read(this.registryPath("registry.lock")))
762
+ throw new ContractError("quarantine");
763
+ const registry = this.registry();
764
+ const selected = this.selected();
765
+ if (
766
+ !registry ||
767
+ !selected?.value ||
768
+ !selected.registration ||
769
+ !selected.owner ||
770
+ !selected.checkout
771
+ )
772
+ refused();
773
+ const value = selected.value;
774
+ const matching = registry.value.entries.filter(
775
+ (row) => row.epoch === value.epoch,
776
+ );
777
+ if (
778
+ matching.length !== 1 ||
779
+ encoded(matching[0]) !== encoded(reservation(value))
780
+ )
781
+ refused();
782
+ return {
783
+ registration: { ...selected.value },
784
+ digest: recoveryDigest(registry, selected),
785
+ };
786
+ }
787
+ }
788
+
789
+ /** Held only by the original owner. Raw-byte AND inode/time identities supplement
790
+ * TaskStore's semantic lock assertion, including whitespace-only interference. */
791
+ export class OwnerLease {
792
+ private released = false;
793
+ constructor(
794
+ readonly coordination: ServiceCoordination,
795
+ readonly checkout: CheckoutLock,
796
+ private selected: Selected & {
797
+ value: Registration;
798
+ registration: FileSnapshot;
799
+ },
800
+ ) {}
801
+ get registration(): Readonly<Registration> {
802
+ return { ...this.selected.value };
803
+ }
804
+ assert(): void {
805
+ if (
806
+ this.released ||
807
+ this.selected.value.pid !== process.pid ||
808
+ this.selected.value.host !== hostname()
809
+ )
810
+ throw new ContractError("conflict");
811
+ const c = this.coordination;
812
+ c.assertNamespace();
813
+ for (const [name, snapshot] of [
814
+ ["config.json", this.selected.config],
815
+ ["host-config.json", this.selected.hostConfig],
816
+ ["format.json", this.selected.manifest],
817
+ ["registration.json", this.selected.registration],
818
+ ["owner.lock", this.selected.owner],
819
+ ["local-capability.json", this.selected.capability],
820
+ ] as const)
821
+ c.files.assert(c.path(name), snapshot);
822
+ c.files.assert(c.store.lockPath, this.selected.checkout, 8192);
823
+ c.store.assertOwner(this.checkout);
824
+ }
825
+ phase(phase: OwnerPhase): void {
826
+ this.assert();
827
+ const value = { ...this.selected.value, phase };
828
+ const registration = this.coordination.files.replace(
829
+ this.coordination.path("registration.json"),
830
+ this.selected.registration,
831
+ encoded(value),
832
+ );
833
+ this.selected = { ...this.selected, value, registration };
834
+ }
835
+ release(): void {
836
+ this.assert();
837
+ this.coordination.release(this);
838
+ this.released = true;
839
+ }
840
+ /** Coordination's final ordered release; callers must use owner.stop(). */
841
+ removeOwnedFiles(assertRegistry: () => void): void {
842
+ this.assert();
843
+ assertRegistry();
844
+ const c = this.coordination;
845
+ if (!this.selected.capability || !this.selected.owner) refused();
846
+ const owner = this.selected.owner;
847
+ c.files.remove(c.path("local-capability.json"), this.selected.capability);
848
+ this.selected = { ...this.selected, capability: undefined };
849
+ // Only successful removals change the expected state. A failure or change
850
+ // at a later boundary must retain every remaining fence, not finish release.
851
+ this.assert();
852
+ assertRegistry();
853
+ c.files.remove(c.path("owner.lock"), owner);
854
+ this.selected = { ...this.selected, owner: undefined };
855
+ this.assert();
856
+ assertRegistry();
857
+ // Last per-checkout removal: legacy v1 writers stay blocked through all
858
+ // preceding durable state publication. Multi-file release is not atomic.
859
+ c.files.assert(c.store.lockPath, this.selected.checkout, 8192);
860
+ c.store.release(this.checkout);
861
+ }
862
+ }