@abelxiaoxing/cadence 1.8.3 → 2.0.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 (208) hide show
  1. package/README.md +324 -111
  2. package/THIRD_PARTY_NOTICES.md +15 -0
  3. package/agents/design-explorer.md +14 -26
  4. package/agents/diagnosis-worker.md +14 -25
  5. package/agents/implementation-worker.md +16 -35
  6. package/config/.env.example +2 -5
  7. package/config/workflow-task.example.json +7 -0
  8. package/docs/remote-access.md +60 -0
  9. package/docs/reuse-and-distribution.md +44 -0
  10. package/docs/service-management.md +108 -0
  11. package/licenses/bundled-web.txt +6245 -0
  12. package/licenses/pi-web-ui-MIT.txt +21 -0
  13. package/licenses/pi-web-ui-reuse.md +22 -0
  14. package/package.json +94 -21
  15. package/src/index.ts +22 -1807
  16. package/src/native-build.ts +6 -0
  17. package/src/native-client.ts +103 -0
  18. package/src/native-export.ts +58 -0
  19. package/src/native-history.ts +359 -0
  20. package/src/operator-cli.mjs +453 -1937
  21. package/src/plan-proposal.ts +87 -0
  22. package/src/sdk-worker.mjs +549 -0
  23. package/src/service/contracts/active.ts +82 -0
  24. package/src/service/contracts/budget.ts +148 -0
  25. package/src/service/contracts/build.ts +67 -0
  26. package/src/service/contracts/capture.ts +48 -0
  27. package/src/service/contracts/core.ts +33 -0
  28. package/src/service/contracts/current.ts +261 -0
  29. package/src/service/contracts/domain.ts +68 -0
  30. package/src/service/contracts/history.ts +2245 -0
  31. package/src/service/contracts/models.ts +139 -0
  32. package/src/service/coordination.ts +862 -0
  33. package/src/service/private-files.ts +204 -0
  34. package/src/service-cli.mjs +5592 -0
  35. package/src/service-client.ts +747 -0
  36. package/src/service-contract.ts +245 -0
  37. package/src/service-host.mjs +22382 -0
  38. package/src/service-http-client.ts +425 -0
  39. package/src/service-wire.ts +372 -0
  40. package/src/task-shell.ts +1045 -0
  41. package/src/task-store.ts +479 -0
  42. package/src/task-types.ts +184 -0
  43. package/src/verification-diagnostics.ts +29 -72
  44. package/src/worker-capture.mjs +1197 -0
  45. package/src/worker-public.mjs +1753 -0
  46. package/src/worker-redaction.mjs +608 -0
  47. package/src/worker-tools.mjs +202 -0
  48. package/web/dist/assets/KaTeX_AMS-Regular.ttf +0 -0
  49. package/web/dist/assets/KaTeX_AMS-Regular.woff +0 -0
  50. package/web/dist/assets/KaTeX_AMS-Regular.woff2 +0 -0
  51. package/web/dist/assets/KaTeX_Caligraphic-Bold.ttf +0 -0
  52. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff +0 -0
  53. package/web/dist/assets/KaTeX_Caligraphic-Bold.woff2 +0 -0
  54. package/web/dist/assets/KaTeX_Caligraphic-Regular.ttf +0 -0
  55. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff +0 -0
  56. package/web/dist/assets/KaTeX_Caligraphic-Regular.woff2 +0 -0
  57. package/web/dist/assets/KaTeX_Fraktur-Bold.ttf +0 -0
  58. package/web/dist/assets/KaTeX_Fraktur-Bold.woff +0 -0
  59. package/web/dist/assets/KaTeX_Fraktur-Bold.woff2 +0 -0
  60. package/web/dist/assets/KaTeX_Fraktur-Regular.ttf +0 -0
  61. package/web/dist/assets/KaTeX_Fraktur-Regular.woff +0 -0
  62. package/web/dist/assets/KaTeX_Fraktur-Regular.woff2 +0 -0
  63. package/web/dist/assets/KaTeX_Main-Bold.ttf +0 -0
  64. package/web/dist/assets/KaTeX_Main-Bold.woff +0 -0
  65. package/web/dist/assets/KaTeX_Main-Bold.woff2 +0 -0
  66. package/web/dist/assets/KaTeX_Main-BoldItalic.ttf +0 -0
  67. package/web/dist/assets/KaTeX_Main-BoldItalic.woff +0 -0
  68. package/web/dist/assets/KaTeX_Main-BoldItalic.woff2 +0 -0
  69. package/web/dist/assets/KaTeX_Main-Italic.ttf +0 -0
  70. package/web/dist/assets/KaTeX_Main-Italic.woff +0 -0
  71. package/web/dist/assets/KaTeX_Main-Italic.woff2 +0 -0
  72. package/web/dist/assets/KaTeX_Main-Regular.ttf +0 -0
  73. package/web/dist/assets/KaTeX_Main-Regular.woff +0 -0
  74. package/web/dist/assets/KaTeX_Main-Regular.woff2 +0 -0
  75. package/web/dist/assets/KaTeX_Math-BoldItalic.ttf +0 -0
  76. package/web/dist/assets/KaTeX_Math-BoldItalic.woff +0 -0
  77. package/web/dist/assets/KaTeX_Math-BoldItalic.woff2 +0 -0
  78. package/web/dist/assets/KaTeX_Math-Italic.ttf +0 -0
  79. package/web/dist/assets/KaTeX_Math-Italic.woff +0 -0
  80. package/web/dist/assets/KaTeX_Math-Italic.woff2 +0 -0
  81. package/web/dist/assets/KaTeX_SansSerif-Bold.ttf +0 -0
  82. package/web/dist/assets/KaTeX_SansSerif-Bold.woff +0 -0
  83. package/web/dist/assets/KaTeX_SansSerif-Bold.woff2 +0 -0
  84. package/web/dist/assets/KaTeX_SansSerif-Italic.ttf +0 -0
  85. package/web/dist/assets/KaTeX_SansSerif-Italic.woff +0 -0
  86. package/web/dist/assets/KaTeX_SansSerif-Italic.woff2 +0 -0
  87. package/web/dist/assets/KaTeX_SansSerif-Regular.ttf +0 -0
  88. package/web/dist/assets/KaTeX_SansSerif-Regular.woff +0 -0
  89. package/web/dist/assets/KaTeX_SansSerif-Regular.woff2 +0 -0
  90. package/web/dist/assets/KaTeX_Script-Regular.ttf +0 -0
  91. package/web/dist/assets/KaTeX_Script-Regular.woff +0 -0
  92. package/web/dist/assets/KaTeX_Script-Regular.woff2 +0 -0
  93. package/web/dist/assets/KaTeX_Size1-Regular.ttf +0 -0
  94. package/web/dist/assets/KaTeX_Size1-Regular.woff +0 -0
  95. package/web/dist/assets/KaTeX_Size1-Regular.woff2 +0 -0
  96. package/web/dist/assets/KaTeX_Size2-Regular.ttf +0 -0
  97. package/web/dist/assets/KaTeX_Size2-Regular.woff +0 -0
  98. package/web/dist/assets/KaTeX_Size2-Regular.woff2 +0 -0
  99. package/web/dist/assets/KaTeX_Size3-Regular.ttf +0 -0
  100. package/web/dist/assets/KaTeX_Size3-Regular.woff +0 -0
  101. package/web/dist/assets/KaTeX_Size3-Regular.woff2 +0 -0
  102. package/web/dist/assets/KaTeX_Size4-Regular.ttf +0 -0
  103. package/web/dist/assets/KaTeX_Size4-Regular.woff +0 -0
  104. package/web/dist/assets/KaTeX_Size4-Regular.woff2 +0 -0
  105. package/web/dist/assets/KaTeX_Typewriter-Regular.ttf +0 -0
  106. package/web/dist/assets/KaTeX_Typewriter-Regular.woff +0 -0
  107. package/web/dist/assets/KaTeX_Typewriter-Regular.woff2 +0 -0
  108. package/web/dist/assets/cadence.css +2 -0
  109. package/web/dist/assets/cadence.js +7 -0
  110. package/web/dist/assets/markdown.js +296 -0
  111. package/web/dist/assets/react.js +9 -0
  112. package/web/dist/assets/rolldown-runtime.js +1 -0
  113. package/web/dist/assets/timeline-client.js +41 -0
  114. package/web/dist/assets/vis-timeline.css +1274 -0
  115. package/web/dist/assets/vis-timeline.js +32 -0
  116. package/web/dist/index.html +18 -0
  117. package/web/dist/manifest.json +505 -0
  118. package/config/plan-draft.example.json +0 -125
  119. package/config/plan-draft.multiple-tasks.example.json +0 -375
  120. package/config/plan-draft.quick.example.json +0 -78
  121. package/config/routes.example.json +0 -39
  122. package/prompts/abel-design.md +0 -382
  123. package/prompts/abel-diagnose.md +0 -71
  124. package/prompts/abel-implement.md +0 -284
  125. package/prompts/abel-init.md +0 -52
  126. package/src/activation.ts +0 -44
  127. package/src/activity-contracts.ts +0 -82
  128. package/src/agent-registry.ts +0 -53
  129. package/src/apply-transaction.ts +0 -1437
  130. package/src/artifact-store.ts +0 -213
  131. package/src/build-windows-job.mjs +0 -95
  132. package/src/candidate-artifact.ts +0 -143
  133. package/src/candidate-patch.ts +0 -271
  134. package/src/candidate-workspace.ts +0 -80
  135. package/src/canonical.ts +0 -16
  136. package/src/change-contract-schema.ts +0 -191
  137. package/src/change-contract.ts +0 -324
  138. package/src/change-verification.ts +0 -1617
  139. package/src/child-budget.ts +0 -58
  140. package/src/child-model.ts +0 -142
  141. package/src/child-session.ts +0 -640
  142. package/src/contracts.ts +0 -2476
  143. package/src/control-contracts.ts +0 -382
  144. package/src/delivery-compiler.ts +0 -1805
  145. package/src/delivery-revision.ts +0 -42
  146. package/src/design-control.ts +0 -1299
  147. package/src/design-diagnostics.ts +0 -181
  148. package/src/design-journal.ts +0 -1773
  149. package/src/directory-sync.ts +0 -36
  150. package/src/durable-contracts.ts +0 -231
  151. package/src/durable-workflow.ts +0 -978
  152. package/src/evidence-draft.ts +0 -122
  153. package/src/execution-profile.ts +0 -64
  154. package/src/execution-retention.ts +0 -77
  155. package/src/file-snapshot.ts +0 -163
  156. package/src/implement-continuation.ts +0 -559
  157. package/src/implement-graph.ts +0 -690
  158. package/src/implement-plan.ts +0 -72
  159. package/src/install-config.d.mts +0 -6
  160. package/src/isolation-backend.ts +0 -353
  161. package/src/model-source.ts +0 -22
  162. package/src/openspec-cli.ts +0 -494
  163. package/src/operator-cli-entry.mjs +0 -24
  164. package/src/operator-tools.ts +0 -309
  165. package/src/package-candidate.ts +0 -227
  166. package/src/package-delivery.ts +0 -486
  167. package/src/package-state.ts +0 -320
  168. package/src/package-verification.ts +0 -635
  169. package/src/package-workflow.ts +0 -342
  170. package/src/packet-runtime.ts +0 -570
  171. package/src/parent-provider.ts +0 -375
  172. package/src/phase-execution.ts +0 -2259
  173. package/src/pi-adapter.ts +0 -19
  174. package/src/plan-draft-summary.ts +0 -527
  175. package/src/plan-draft.ts +0 -502
  176. package/src/remove-path.ts +0 -32
  177. package/src/route-policy.ts +0 -545
  178. package/src/run-state.ts +0 -420
  179. package/src/run-store.ts +0 -1179
  180. package/src/safe-path.ts +0 -77
  181. package/src/scoped-grep-worker.mjs +0 -23
  182. package/src/scoped-tools.ts +0 -775
  183. package/src/single-task-draft.ts +0 -142
  184. package/src/sqlite-schema.ts +0 -171
  185. package/src/state-root.ts +0 -215
  186. package/src/storage-schema.ts +0 -360
  187. package/src/subagent-activity.ts +0 -1417
  188. package/src/submit-tool.ts +0 -950
  189. package/src/task-ledger.ts +0 -1338
  190. package/src/transport-budget.ts +0 -52
  191. package/src/verification-capability.ts +0 -1191
  192. package/src/verification-environment-io.ts +0 -389
  193. package/src/verification-environment.ts +0 -162
  194. package/src/verification-identity.ts +0 -77
  195. package/src/verification-scan.ts +0 -64
  196. package/src/windows-job-backend.ts +0 -419
  197. package/src/windows-job.c +0 -149
  198. package/src/worker-broker.ts +0 -806
  199. package/src/workflow-engine.ts +0 -28
  200. package/src/workflow-policy.ts +0 -1679
  201. package/src/workflow-recovery-policy.ts +0 -97
  202. package/src/workflow-scheduling.ts +0 -105
  203. package/src/workflow-state-machine.ts +0 -4355
  204. package/src/workflow-status.ts +0 -576
  205. package/src/workspace-io-entry.mjs +0 -76
  206. package/src/workspace-io-worker.mjs +0 -1312
  207. package/src/workspace-io.ts +0 -89
  208. 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
+ }