@gajae-code/coding-agent 0.17.4 → 0.17.6

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 (86) hide show
  1. package/CHANGELOG.md +82 -0
  2. package/dist/types/config/autorouting-tier-map.d.ts +1031 -3
  3. package/dist/types/config/file-lock.d.ts +4 -2
  4. package/dist/types/config/model-registry.d.ts +3 -1
  5. package/dist/types/config/model-resolver.d.ts +1 -1
  6. package/dist/types/exec/bash-executor.d.ts +2 -0
  7. package/dist/types/extensibility/gjc-plugins/runtime-adapters.d.ts +11 -2
  8. package/dist/types/gjc-runtime/state-writer.d.ts +14 -1
  9. package/dist/types/harness-control-plane/control-endpoint.d.ts +4 -1
  10. package/dist/types/harness-control-plane/finalize.d.ts +14 -3
  11. package/dist/types/main.d.ts +1 -1
  12. package/dist/types/runtime-mcp/loader.d.ts +2 -0
  13. package/dist/types/runtime-mcp/manager.d.ts +13 -6
  14. package/dist/types/sdk/broker/broker.d.ts +4 -2
  15. package/dist/types/sdk/broker/ensure.d.ts +3 -0
  16. package/dist/types/sdk/broker/identity.d.ts +2 -0
  17. package/dist/types/sdk/broker/lifecycle-ledger.d.ts +11 -3
  18. package/dist/types/sdk/broker/managed-task-dag.d.ts +1335 -0
  19. package/dist/types/sdk/broker/managed-task-verification.d.ts +37 -0
  20. package/dist/types/sdk/broker/spawn-authority.d.ts +8 -0
  21. package/dist/types/sdk/broker/startup-failure.d.ts +4 -0
  22. package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +5 -1
  23. package/dist/types/sdk/prompt-deadline-flush.d.ts +2 -0
  24. package/dist/types/sdk/session.d.ts +3 -3
  25. package/dist/types/session/agent-session.d.ts +7 -2
  26. package/dist/types/task/types.d.ts +2 -0
  27. package/dist/types/tools/bash-interactive.d.ts +1 -0
  28. package/dist/types/tools/fetch.d.ts +10 -1
  29. package/dist/types/tools/subagent.d.ts +8 -1
  30. package/package.json +7 -7
  31. package/src/cli/public-command-registry.ts +8 -2
  32. package/src/cli/setup-cli.ts +1 -0
  33. package/src/cli/stats-cli.ts +11 -1
  34. package/src/config/autorouting-tier-map.ts +529 -3
  35. package/src/config/file-lock.ts +59 -14
  36. package/src/config/model-discovery-manager.ts +4 -4
  37. package/src/config/model-profile-activation.ts +83 -23
  38. package/src/config/model-registry.ts +72 -12
  39. package/src/config/model-resolver.ts +6 -1
  40. package/src/coordinator-mcp/question-state.ts +4 -1
  41. package/src/coordinator-mcp/server.ts +14 -6
  42. package/src/exec/bash-executor.ts +3 -0
  43. package/src/extensibility/gjc-plugins/runtime-adapters.ts +46 -8
  44. package/src/gjc-runtime/state-writer.ts +317 -6
  45. package/src/harness-control-plane/control-endpoint.ts +24 -6
  46. package/src/harness-control-plane/finalize.ts +312 -25
  47. package/src/harness-control-plane/owner.ts +323 -89
  48. package/src/internal-urls/docs-index.generated.ts +6 -4
  49. package/src/main.ts +69 -19
  50. package/src/modes/acp/acp-agent.ts +581 -37
  51. package/src/modes/components/gjc-bundle-settings.ts +21 -1
  52. package/src/modes/components/tool-status-header.ts +72 -6
  53. package/src/modes/controllers/runtime-mcp-command-controller.ts +1 -1
  54. package/src/runtime-mcp/loader.ts +6 -3
  55. package/src/runtime-mcp/manager.ts +177 -75
  56. package/src/sdk/broker/broker.ts +1058 -150
  57. package/src/sdk/broker/ensure.ts +65 -8
  58. package/src/sdk/broker/identity.ts +13 -0
  59. package/src/sdk/broker/lifecycle-ledger.ts +16 -4
  60. package/src/sdk/broker/managed-task-dag.ts +1927 -0
  61. package/src/sdk/broker/managed-task-verification.ts +642 -0
  62. package/src/sdk/broker/spawn-authority.ts +140 -7
  63. package/src/sdk/broker/startup-failure.ts +80 -1
  64. package/src/sdk/broker/transport.ts +1 -0
  65. package/src/sdk/bus/chat-daemon-control.ts +2 -2
  66. package/src/sdk/bus/index.ts +22 -6
  67. package/src/sdk/bus/telegram-daemon-contract.ts +5 -1
  68. package/src/sdk/cli/session-cli.ts +18 -0
  69. package/src/sdk/host/session-runtime.test.ts +46 -0
  70. package/src/sdk/host/session-runtime.ts +13 -6
  71. package/src/sdk/lifecycle/service.ts +2 -0
  72. package/src/sdk/prompt-deadline-flush.ts +26 -4
  73. package/src/sdk/session.ts +587 -115
  74. package/src/session/agent-session.ts +60 -14
  75. package/src/session/internal/managed-session-scope.ts +10 -0
  76. package/src/session/internal/managed-session-storage.ts +30 -1
  77. package/src/setup/provider-presets.json +2 -1
  78. package/src/slash-commands/builtin-registry.ts +1 -1
  79. package/src/task/executor.ts +46 -17
  80. package/src/task/types.ts +2 -0
  81. package/src/tools/bash-interactive.ts +2 -0
  82. package/src/tools/bash.ts +20 -0
  83. package/src/tools/fetch.ts +21 -5
  84. package/src/tools/subagent-render.ts +24 -1
  85. package/src/tools/subagent.ts +27 -1
  86. package/src/utils/mupdf.ts +11 -4
@@ -0,0 +1,1927 @@
1
+ import { createHash } from "node:crypto";
2
+ import * as fs from "node:fs/promises";
3
+ import * as path from "node:path";
4
+ import { z } from "zod";
5
+ import {
6
+ type GuardedWriteResult,
7
+ readExistingStateForMutation,
8
+ StatePublicationUncertainError,
9
+ StateWriteConflictError,
10
+ withWorkflowStateLock,
11
+ writeGuardedJsonAtomic,
12
+ } from "../../gjc-runtime/state-writer";
13
+ import {
14
+ RECEIPT_SCHEMA_VERSION,
15
+ type ReceiptEnvelope,
16
+ type ValidationEvidence,
17
+ validateReceipt,
18
+ } from "../../harness-control-plane/receipts";
19
+
20
+ const text = z
21
+ .string()
22
+ .min(1)
23
+ .refine(value => value.trim().length > 0, "must not be blank");
24
+ const counter = z.number().int().nonnegative();
25
+ const positiveCounter = z.number().int().positive();
26
+ const digest = z.string().regex(/^[a-f0-9]{64}$/);
27
+ const filesystemIdentity = z.string().regex(/^\d+:\d+$/);
28
+ const mode = z.enum(["read", "write"]);
29
+ export const ManagedPathResourceSchema = z
30
+ .object({
31
+ kind: z.literal("path"),
32
+ path: text,
33
+ mode,
34
+ recursive: z.boolean(),
35
+ namespace: z.boolean(),
36
+ })
37
+ .strict();
38
+ export const ManagedNamedResourceSchema = z
39
+ .object({
40
+ kind: z.enum(["database", "integration"]),
41
+ identity: text,
42
+ mode,
43
+ })
44
+ .strict();
45
+ export const ManagedPortResourceSchema = z
46
+ .object({
47
+ kind: z.literal("port"),
48
+ protocol: z.enum(["tcp", "udp"]),
49
+ address: z.enum(["*", "0.0.0.0", "::", "127.0.0.1", "::1"]),
50
+ port: z.number().int().min(1).max(65535),
51
+ mode,
52
+ })
53
+ .strict();
54
+ export const ManagedResourceSchema = z.union([
55
+ ManagedPathResourceSchema,
56
+ ManagedNamedResourceSchema,
57
+ ManagedPortResourceSchema,
58
+ ]);
59
+ export type ManagedResource = z.infer<typeof ManagedResourceSchema>;
60
+ export const ManagedArtifactSchema = z
61
+ .object({
62
+ path: text,
63
+ role: z.enum(["input", "read-write", "output", "validation-output"]),
64
+ presence: z.enum(["required", "optional", "absent"]),
65
+ })
66
+ .strict();
67
+ export type ManagedArtifact = z.infer<typeof ManagedArtifactSchema>;
68
+ export const ManagedTaskDefinitionSchema = z
69
+ .object({
70
+ id: text,
71
+ task: text,
72
+ workspace: text,
73
+ predecessors: z.array(text),
74
+ criteriaIdentity: digest,
75
+ validations: z.array(z.object({ name: text, command: text }).strict()).min(1),
76
+ resources: z.array(ManagedResourceSchema),
77
+ artifacts: z.array(ManagedArtifactSchema),
78
+ })
79
+ .strict();
80
+ export type ManagedTaskDefinition = z.infer<typeof ManagedTaskDefinitionSchema>;
81
+ export const ManagedDefinitionRecordSchema = z
82
+ .object({
83
+ revision: positiveCounter,
84
+ hash: digest,
85
+ definition: ManagedTaskDefinitionSchema,
86
+ })
87
+ .strict();
88
+ export type ManagedDefinitionRecord = z.infer<typeof ManagedDefinitionRecordSchema>;
89
+ export const ManagedManifestEntrySchema = z
90
+ .object({
91
+ path: text,
92
+ object: filesystemIdentity.nullable(),
93
+ kind: z.enum(["absent", "file", "directory"]),
94
+ hash: digest.nullable(),
95
+ mode: z.number().int().min(0).max(0o777).nullable(),
96
+ })
97
+ .strict();
98
+ export type ManagedManifestEntry = z.infer<typeof ManagedManifestEntrySchema>;
99
+ export type ManagedManifest = ManagedManifestEntry[];
100
+ export const ManagedPredecessorSchema = z
101
+ .object({
102
+ nodeId: text,
103
+ taskRevision: positiveCounter,
104
+ attemptId: text,
105
+ verificationId: text,
106
+ verificationHash: digest,
107
+ produced: z.array(ManagedManifestEntrySchema),
108
+ })
109
+ .strict();
110
+ export type ManagedPredecessor = z.infer<typeof ManagedPredecessorSchema>;
111
+ export const ManagedCanonicalResourceSchema = z
112
+ .object({
113
+ declaration: ManagedResourceSchema,
114
+ path: text.nullable(),
115
+ object: filesystemIdentity.nullable(),
116
+ ancestor: filesystemIdentity.nullable(),
117
+ suffix: z.string().nullable(),
118
+ })
119
+ .strict();
120
+ export type ManagedCanonicalResource = z.infer<typeof ManagedCanonicalResourceSchema>;
121
+ export const ManagedNativeIdentitySchema = z
122
+ .object({
123
+ key: text,
124
+ identity: digest,
125
+ requestHash: digest,
126
+ })
127
+ .strict();
128
+ export type ManagedNativeIdentity = z.infer<typeof ManagedNativeIdentitySchema>;
129
+ export const ManagedVerificationObservationSchema = z
130
+ .object({
131
+ name: text,
132
+ command: text,
133
+ cwd: text,
134
+ exitStatus: z.number().int(),
135
+ pass: z.boolean(),
136
+ })
137
+ .strict();
138
+ export type ManagedVerificationObservation = z.infer<typeof ManagedVerificationObservationSchema>;
139
+ export const ManagedVerificationReceiptSchema = z
140
+ .object({
141
+ id: text,
142
+ hash: digest,
143
+ envelope: z.record(z.string(), z.unknown()),
144
+ })
145
+ .strict();
146
+ export type ManagedVerificationReceipt = z.infer<typeof ManagedVerificationReceiptSchema>;
147
+ export const ManagedVerificationExecutionSchema = z
148
+ .object({
149
+ executionId: text,
150
+ workspace: text,
151
+ commands: z.array(z.object({ name: text, command: text }).strict()).min(1),
152
+ observations: z.array(ManagedVerificationObservationSchema),
153
+ receipts: z.array(ManagedVerificationReceiptSchema),
154
+ })
155
+ .strict();
156
+ export type ManagedVerificationExecution = z.infer<typeof ManagedVerificationExecutionSchema>;
157
+ export const ManagedTaskAttemptSchema = z
158
+ .object({
159
+ id: text,
160
+ nodeId: text,
161
+ taskRevision: positiveCounter,
162
+ graphRevision: positiveCounter,
163
+ definitionHash: digest,
164
+ definition: ManagedTaskDefinitionSchema,
165
+ native: ManagedNativeIdentitySchema,
166
+ inputs: z.array(ManagedManifestEntrySchema),
167
+ predecessors: z.array(ManagedPredecessorSchema),
168
+ resources: z.array(ManagedCanonicalResourceSchema),
169
+ produced: z.array(ManagedManifestEntrySchema).nullable(),
170
+ validationImmutable: z.array(ManagedManifestEntrySchema).nullable(),
171
+ validationOutputs: z.array(ManagedManifestEntrySchema).nullable(),
172
+ worker: z.enum(["reserved", "no-effect", "authorized", "unknown", "closed"]),
173
+ validation: z.enum(["not-started", "running", "unknown", "finished"]),
174
+ fence: z.enum(["current", "canceled", "superseded", "failed"]),
175
+ retired: z.boolean(),
176
+ accepted: z.object({ id: text, hash: digest }).strict().nullable(),
177
+ verificationExecution: ManagedVerificationExecutionSchema.nullable(),
178
+ })
179
+ .strict();
180
+ export type ManagedTaskAttempt = z.infer<typeof ManagedTaskAttemptSchema>;
181
+ export const ManagedTaskGraphSchema = z
182
+ .object({
183
+ id: text,
184
+ owner: text,
185
+ revision: positiveCounter,
186
+ nodes: z.array(ManagedDefinitionRecordSchema).min(1),
187
+ attempts: z.array(ManagedTaskAttemptSchema),
188
+ canceled: z.array(text),
189
+ })
190
+ .strict();
191
+ export type ManagedTaskGraph = z.infer<typeof ManagedTaskGraphSchema>;
192
+ export const ManagedDomainBindingSchema = z
193
+ .object({
194
+ controlRoot: text,
195
+ rootIdentity: filesystemIdentity,
196
+ agentDir: text,
197
+ agentDirIdentity: filesystemIdentity,
198
+ enrollmentId: text,
199
+ worktrees: z.array(z.object({ path: text, identity: filesystemIdentity }).strict()).min(1),
200
+ aliases: z.array(z.object({ path: text, target: text }).strict()),
201
+ })
202
+ .strict();
203
+ export type ManagedDomainBinding = z.infer<typeof ManagedDomainBindingSchema>;
204
+ export const ManagedTaskDomainSchema = z
205
+ .object({
206
+ version: z.literal(1),
207
+ state_revision: counter,
208
+ binding: ManagedDomainBindingSchema,
209
+ graphs: z.array(ManagedTaskGraphSchema),
210
+ })
211
+ .strict();
212
+ export type ManagedTaskDomain = z.infer<typeof ManagedTaskDomainSchema>;
213
+ /**
214
+ * Internal M2 handoff object. It is returned only after a durable M1
215
+ * reservation; M2 must keep it private after verified Broker admission.
216
+ */
217
+ export interface ManagedAttemptRef {
218
+ readonly controlRoot: string;
219
+ readonly enrollmentId: string;
220
+ readonly agentDirIdentity: string;
221
+ readonly graphId: string;
222
+ readonly graphRevision: number;
223
+ readonly nodeId: string;
224
+ readonly taskRevision: number;
225
+ readonly attemptId: string;
226
+ readonly definitionHash: string;
227
+ readonly criteriaIdentity: string;
228
+ readonly inputIdentity: string;
229
+ readonly predecessorIdentity: string;
230
+ readonly resourceIdentity: string;
231
+ readonly native: ManagedNativeIdentity;
232
+ readonly nativeIdentity: string;
233
+ }
234
+
235
+ export function managedIdentity(value: unknown): string {
236
+ function ordered(v: unknown): unknown {
237
+ if (Array.isArray(v)) return v.map(ordered);
238
+ if (v && typeof v === "object")
239
+ return Object.fromEntries(
240
+ Object.entries(v)
241
+ .sort(([a], [b]) => a.localeCompare(b))
242
+ .map(([k, x]) => [k, ordered(x)]),
243
+ );
244
+ return v;
245
+ }
246
+ return createHash("sha256")
247
+ .update(JSON.stringify(ordered(value)))
248
+ .digest("hex");
249
+ }
250
+ function requirePolicy(condition: unknown, reason: string): asserts condition {
251
+ if (!condition) throw new Error(`managed-task: ${reason}`);
252
+ }
253
+ function parseStoredReceiptEnvelope(value: unknown): ReceiptEnvelope<ValidationEvidence> | undefined {
254
+ const parsed = z
255
+ .object({
256
+ receiptId: text,
257
+ schemaVersion: z.literal(RECEIPT_SCHEMA_VERSION),
258
+ sessionId: text,
259
+ family: z.literal("validation"),
260
+ valid: z.literal(true),
261
+ createdAt: text,
262
+ source: z.literal("managed-task-verification"),
263
+ subject: z
264
+ .object({
265
+ workspace: text,
266
+ branch: z.string().nullable(),
267
+ head: z.string().nullable(),
268
+ commit: z.string().nullable(),
269
+ })
270
+ .strict(),
271
+ evidence: z
272
+ .object({
273
+ command: text,
274
+ exactCommand: text,
275
+ cwd: text,
276
+ exitStatus: z.literal(0),
277
+ pass: z.literal(true),
278
+ commitUnderTest: z.null(),
279
+ })
280
+ .strict(),
281
+ artifactHashes: z.record(z.string(), text),
282
+ sha256: digest,
283
+ })
284
+ .strict()
285
+ .safeParse(value);
286
+ if (!parsed.success) return undefined;
287
+ const envelope: ReceiptEnvelope<ValidationEvidence> = parsed.data;
288
+ if (!validateReceipt(envelope).valid) return undefined;
289
+ return envelope;
290
+ }
291
+ function trustedStoredAcceptance(attempt: ManagedTaskAttempt): boolean {
292
+ const accepted = attempt.accepted;
293
+ const execution = attempt.verificationExecution;
294
+ if (!accepted || attempt.validation !== "finished" || !execution) return false;
295
+ const stored = execution.receipts.find(receipt => receipt.id === accepted.id && receipt.hash === accepted.hash);
296
+ if (!stored) return false;
297
+ const envelope = parseStoredReceiptEnvelope(stored.envelope);
298
+ return envelope !== undefined && envelope.receiptId === stored.id && envelope.sha256 === stored.hash;
299
+ }
300
+ export type ManagedEnrollmentRecord = {
301
+ controlRoots: string[];
302
+ establishedRoots: string[];
303
+ publishingRoots: string[];
304
+ nativeIdentities: string[];
305
+ byRoot: Record<string, string[]>;
306
+ };
307
+ function enrollmentIndexDocument(record: ManagedEnrollmentRecord): {
308
+ version: 1;
309
+ controlRoots: string[];
310
+ establishedRoots: string[];
311
+ publishingRoots: string[];
312
+ nativeIdentities: string[];
313
+ byRoot: Record<string, string[]>;
314
+ state_revision: number;
315
+ } {
316
+ const controlRoots = [...record.controlRoots].sort();
317
+ const establishedRoots = [...record.establishedRoots].filter(root => controlRoots.includes(root)).sort();
318
+ const publishingRoots = [...record.publishingRoots]
319
+ .filter(root => controlRoots.includes(root) && !establishedRoots.includes(root))
320
+ .sort();
321
+ const byRoot: Record<string, string[]> = {};
322
+ for (const root of controlRoots) byRoot[root] = [...(record.byRoot[root] ?? [])].sort();
323
+ const nativeIdentities = [
324
+ ...new Set([...record.nativeIdentities, ...controlRoots.flatMap(root => byRoot[root] ?? [])]),
325
+ ].sort();
326
+ return {
327
+ version: 1,
328
+ controlRoots,
329
+ establishedRoots,
330
+ publishingRoots,
331
+ // Global identities may be accepted without a root mapping; cleanup must retain them.
332
+ nativeIdentities,
333
+ byRoot,
334
+ state_revision: 0,
335
+ };
336
+ }
337
+ function parseEnrollmentIndex(value: unknown): ManagedEnrollmentRecord {
338
+ const parsed = z
339
+ .object({
340
+ version: z.literal(1),
341
+ controlRoots: z.array(text),
342
+ establishedRoots: z.array(text),
343
+ publishingRoots: z.array(text),
344
+ nativeIdentities: z.array(digest).default([]),
345
+ byRoot: z.record(text, z.array(digest)).default({}),
346
+ state_revision: counter.optional(),
347
+ })
348
+ .strict()
349
+ .parse(value);
350
+ requireDistinct(parsed.controlRoots, "duplicate enrolled control root");
351
+ requirePolicy(parsed.controlRoots.every(isCanonicalAbsolute), "enrolled control root is not canonical");
352
+ requireDistinct(parsed.establishedRoots, "duplicate established control root");
353
+ requireDistinct(parsed.publishingRoots, "duplicate publishing control root");
354
+ requirePolicy(
355
+ parsed.establishedRoots.every(root => parsed.controlRoots.includes(root)),
356
+ "established control root is not enrolled",
357
+ );
358
+ requirePolicy(
359
+ parsed.publishingRoots.every(
360
+ root => parsed.controlRoots.includes(root) && !parsed.establishedRoots.includes(root),
361
+ ),
362
+ "publishing control root is not pending",
363
+ );
364
+ const byRoot: Record<string, string[]> = {};
365
+ for (const root of parsed.controlRoots) {
366
+ const ids = parsed.byRoot[root] ?? [];
367
+ requireDistinct(ids, "duplicate enrolled native identity");
368
+ byRoot[root] = ids;
369
+ }
370
+ requireDistinct(Object.values(byRoot).flat(), "native identity belongs to multiple control roots");
371
+ const nativeIdentities = [...new Set([...parsed.nativeIdentities, ...Object.values(byRoot).flat()])];
372
+ requireDistinct(nativeIdentities, "duplicate enrolled native identity");
373
+ return {
374
+ controlRoots: parsed.controlRoots,
375
+ establishedRoots: parsed.establishedRoots,
376
+ publishingRoots: parsed.publishingRoots,
377
+ nativeIdentities,
378
+ byRoot,
379
+ };
380
+ }
381
+ async function loadEnrollmentIndexUnderLock(target: string): Promise<ManagedEnrollmentRecord> {
382
+ const read = await readExistingStateForMutation(target);
383
+ if (read.kind === "absent")
384
+ return { controlRoots: [], establishedRoots: [], publishingRoots: [], nativeIdentities: [], byRoot: {} };
385
+ requirePolicy(read.kind === "valid", "corrupt managed enrollment index");
386
+ try {
387
+ return parseEnrollmentIndex(read.value);
388
+ } catch {
389
+ throw new Error("corrupt managed enrollment index");
390
+ }
391
+ }
392
+ export function within(parent: string, child: string): boolean {
393
+ const relative = path.relative(parent, child);
394
+ return relative === "" || (!relative.startsWith(`..${path.sep}`) && relative !== ".." && !path.isAbsolute(relative));
395
+ }
396
+ async function objectIdentity(directory: string): Promise<string> {
397
+ const stat = await fs.lstat(directory, { bigint: true });
398
+ requirePolicy(stat.isDirectory() && !stat.isSymbolicLink(), "binding must be a directory");
399
+ return `${stat.dev}:${stat.ino}`;
400
+ }
401
+
402
+ function isCanonicalAbsolute(value: string): boolean {
403
+ return path.isAbsolute(value) && path.normalize(value) === value;
404
+ }
405
+
406
+ function requireDistinct(values: string[], reason: string): void {
407
+ requirePolicy(new Set(values).size === values.length, reason);
408
+ }
409
+
410
+ function comparePathRecords<T extends { path: string }>(left: T, right: T): number {
411
+ return left.path.localeCompare(right.path);
412
+ }
413
+
414
+ function canonicalNamedIdentity(identity: string): string {
415
+ requirePolicy(
416
+ /^[a-z0-9](?:[a-z0-9._:/-]*[a-z0-9])?$/.test(identity) && !identity.includes("//") && !identity.includes(".."),
417
+ "named resource must have explicit canonical identity",
418
+ );
419
+ return identity;
420
+ }
421
+
422
+ export interface ManagedDomainBindingInput {
423
+ controlRoot: string;
424
+ agentDir: string;
425
+ enrollmentId: string;
426
+ worktrees: string[];
427
+ aliases?: string[];
428
+ }
429
+
430
+ /** Explicit aliases are accepted only while their real target remains the enrolled target. */
431
+ export async function createManagedDomainBinding(input: ManagedDomainBindingInput): Promise<ManagedDomainBinding> {
432
+ text.parse(input.enrollmentId);
433
+ const controlRoot = await fs.realpath(input.controlRoot);
434
+ const agentDir = await fs.realpath(input.agentDir);
435
+ const worktrees = (
436
+ await Promise.all(
437
+ input.worktrees.map(async value => {
438
+ const worktree = await fs.realpath(value);
439
+ return { path: worktree, identity: await objectIdentity(worktree) };
440
+ }),
441
+ )
442
+ ).sort(comparePathRecords);
443
+ requireDistinct(
444
+ worktrees.map(worktree => worktree.path),
445
+ "duplicate worktree",
446
+ );
447
+ const aliases = (
448
+ await Promise.all(
449
+ (input.aliases ?? []).map(async value => ({
450
+ path: path.resolve(value),
451
+ target: await fs.realpath(value),
452
+ })),
453
+ )
454
+ ).sort(comparePathRecords);
455
+ requireDistinct(
456
+ aliases.map(alias => alias.path),
457
+ "duplicate alias",
458
+ );
459
+ requirePolicy(
460
+ aliases.every(a => a.target === controlRoot || worktrees.some(w => w.path === a.target)),
461
+ "alias is not enrolled",
462
+ );
463
+ return ManagedDomainBindingSchema.parse({
464
+ controlRoot,
465
+ rootIdentity: await objectIdentity(controlRoot),
466
+ agentDir,
467
+ agentDirIdentity: await objectIdentity(agentDir),
468
+ enrollmentId: input.enrollmentId,
469
+ worktrees,
470
+ aliases,
471
+ });
472
+ }
473
+
474
+ async function validateBinding(binding: ManagedDomainBinding): Promise<void> {
475
+ requirePolicy(isCanonicalAbsolute(binding.controlRoot), "control root is not canonical");
476
+ requirePolicy(isCanonicalAbsolute(binding.agentDir), "agent directory is not canonical");
477
+ requireDistinct(
478
+ binding.worktrees.map(worktree => worktree.path),
479
+ "duplicate worktree",
480
+ );
481
+ requireDistinct(
482
+ binding.aliases.map(alias => alias.path),
483
+ "duplicate alias",
484
+ );
485
+ requirePolicy(
486
+ binding.worktrees.every(worktree => isCanonicalAbsolute(worktree.path)),
487
+ "worktree is not canonical",
488
+ );
489
+ requirePolicy(
490
+ binding.aliases.every(alias => isCanonicalAbsolute(alias.path) && isCanonicalAbsolute(alias.target)),
491
+ "alias is not canonical",
492
+ );
493
+ requirePolicy(
494
+ (await fs.realpath(binding.controlRoot)) === binding.controlRoot &&
495
+ (await objectIdentity(binding.controlRoot)) === binding.rootIdentity,
496
+ "control root changed",
497
+ );
498
+ requirePolicy(
499
+ (await fs.realpath(binding.agentDir)) === binding.agentDir &&
500
+ (await objectIdentity(binding.agentDir)) === binding.agentDirIdentity,
501
+ "agent directory changed",
502
+ );
503
+ for (const w of binding.worktrees)
504
+ requirePolicy(
505
+ (await fs.realpath(w.path)) === w.path && (await objectIdentity(w.path)) === w.identity,
506
+ "worktree changed",
507
+ );
508
+ for (const alias of binding.aliases) {
509
+ requirePolicy(
510
+ alias.target === binding.controlRoot || binding.worktrees.some(worktree => worktree.path === alias.target),
511
+ "alias is not enrolled",
512
+ );
513
+ requirePolicy((await fs.realpath(alias.path)) === alias.target, "alias changed");
514
+ }
515
+ }
516
+
517
+ async function canonicalPath(
518
+ binding: ManagedDomainBinding,
519
+ supplied: string,
520
+ ): Promise<{ path: string; object: string | null; ancestor: string; suffix: string }> {
521
+ requirePolicy(isCanonicalAbsolute(supplied), "path must be absolute and normalized");
522
+ let candidate = supplied;
523
+ for (const alias of [...binding.aliases].sort((a, b) => b.path.length - a.path.length)) {
524
+ if (within(alias.path, candidate)) {
525
+ requirePolicy((await fs.realpath(alias.path)) === alias.target, "alias changed");
526
+ candidate = path.join(alias.target, path.relative(alias.path, candidate));
527
+ break;
528
+ }
529
+ }
530
+ const root = binding.worktrees
531
+ .filter(w => within(w.path, candidate))
532
+ .sort((a, b) => b.path.length - a.path.length)[0];
533
+ requirePolicy(root, "path is outside enrolled worktrees");
534
+ let current = root.path;
535
+ let stat = await fs.lstat(current, { bigint: true });
536
+ requirePolicy(stat.isDirectory() && !stat.isSymbolicLink(), "worktree changed");
537
+ let ancestor = `${stat.dev}:${stat.ino}`;
538
+ const segments = path.relative(current, candidate).split(path.sep).filter(Boolean);
539
+ for (let i = 0; i < segments.length; i++) {
540
+ current = path.join(current, segments[i]!);
541
+ try {
542
+ stat = await fs.lstat(current, { bigint: true });
543
+ } catch (error) {
544
+ if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
545
+ requirePolicy((await fs.realpath(path.dirname(current))) === path.dirname(current), "path ancestor changed");
546
+ return { path: candidate, object: null, ancestor, suffix: segments.slice(i).join(path.sep) };
547
+ }
548
+ requirePolicy(!stat.isSymbolicLink() && (stat.isFile() || stat.isDirectory()), "unsupported path alias/type");
549
+ if (i < segments.length - 1) requirePolicy(stat.isDirectory(), "non-directory ancestor");
550
+ ancestor = `${stat.dev}:${stat.ino}`;
551
+ }
552
+ requirePolicy((await fs.realpath(candidate)) === candidate, "path changed");
553
+ return { path: candidate, object: ancestor, ancestor, suffix: "" };
554
+ }
555
+
556
+ export async function canonicalizeManagedResources(
557
+ binding: ManagedDomainBinding,
558
+ declarations: ManagedResource[],
559
+ ): Promise<ManagedCanonicalResource[]> {
560
+ await validateBinding(binding);
561
+ return Promise.all(
562
+ declarations.map(async value => {
563
+ const declaration = ManagedResourceSchema.parse(value);
564
+ if (declaration.kind === "path") return { declaration, ...(await canonicalPath(binding, declaration.path)) };
565
+ if (declaration.kind !== "port") canonicalNamedIdentity(declaration.identity);
566
+ return { declaration, path: null, object: null, ancestor: null, suffix: null };
567
+ }),
568
+ );
569
+ }
570
+
571
+ function portAddressesOverlap(
572
+ left: z.infer<typeof ManagedPortResourceSchema>["address"],
573
+ right: z.infer<typeof ManagedPortResourceSchema>["address"],
574
+ ): boolean {
575
+ if (left === right || left === "*" || right === "*") return true;
576
+ const leftV4 = left === "0.0.0.0" || left === "127.0.0.1";
577
+ const rightV4 = right === "0.0.0.0" || right === "127.0.0.1";
578
+ const leftV6 = left === "::" || left === "::1";
579
+ const rightV6 = right === "::" || right === "::1";
580
+ if (left === "0.0.0.0") return rightV4;
581
+ if (right === "0.0.0.0") return leftV4;
582
+ if (left === "::") return rightV6;
583
+ if (right === "::") return leftV6;
584
+ return false;
585
+ }
586
+
587
+ export function managedResourcesOverlap(a: ManagedCanonicalResource, b: ManagedCanonicalResource): boolean {
588
+ const left = ManagedCanonicalResourceSchema.parse(a);
589
+ const right = ManagedCanonicalResourceSchema.parse(b);
590
+ const x = left.declaration,
591
+ y = right.declaration;
592
+ if (x.kind !== y.kind) return false;
593
+ if (x.kind === "path" && y.kind === "path") {
594
+ requirePolicy(
595
+ left.path !== null && right.path !== null && left.ancestor !== null && right.ancestor !== null,
596
+ "invalid canonical path resource",
597
+ );
598
+ return (
599
+ (left.object !== null && left.object === right.object) ||
600
+ left.path === right.path ||
601
+ (left.ancestor === right.ancestor && left.suffix === right.suffix) ||
602
+ (x.recursive && within(left.path, right.path)) ||
603
+ (y.recursive && within(right.path, left.path)) ||
604
+ (x.namespace && within(left.path, right.path)) ||
605
+ (y.namespace && within(right.path, left.path))
606
+ );
607
+ }
608
+ if (x.kind === "port" && y.kind === "port")
609
+ return x.protocol === y.protocol && x.port === y.port && portAddressesOverlap(x.address, y.address);
610
+ return "identity" in x && "identity" in y && x.identity === y.identity;
611
+ }
612
+ export function managedResourcesConflict(a: ManagedCanonicalResource, b: ManagedCanonicalResource): boolean {
613
+ return (a.declaration.mode === "write" || b.declaration.mode === "write") && managedResourcesOverlap(a, b);
614
+ }
615
+ export function parseManagedTaskDefinitions(value: unknown): ManagedTaskDefinition[] {
616
+ const nodes = z.array(ManagedTaskDefinitionSchema).min(1).parse(value);
617
+ const ids = new Set(nodes.map(n => n.id));
618
+ requirePolicy(ids.size === nodes.length, "duplicate node");
619
+ for (const node of nodes)
620
+ requireDistinct(
621
+ node.validations.map(validation => validation.name),
622
+ "duplicate validation",
623
+ );
624
+ const visiting = new Set<string>(),
625
+ visited = new Set<string>();
626
+ function visit(id: string): void {
627
+ requirePolicy(!visiting.has(id), "dependency cycle");
628
+ if (visited.has(id)) return;
629
+ visiting.add(id);
630
+ const node = nodes.find(n => n.id === id)!;
631
+ requirePolicy(new Set(node.predecessors).size === node.predecessors.length, "duplicate predecessor");
632
+ for (const predecessor of node.predecessors) {
633
+ requirePolicy(ids.has(predecessor), "missing predecessor");
634
+ visit(predecessor);
635
+ }
636
+ visiting.delete(id);
637
+ visited.add(id);
638
+ }
639
+ for (const node of nodes) visit(node.id);
640
+ return z.array(ManagedTaskDefinitionSchema).parse(JSON.parse(JSON.stringify(nodes)));
641
+ }
642
+
643
+ function canonicalResourceKey(resource: ManagedCanonicalResource): string {
644
+ const declaration = resource.declaration;
645
+ if (declaration.kind === "path") {
646
+ requirePolicy(
647
+ resource.path !== null && resource.ancestor !== null && resource.suffix !== null,
648
+ "invalid canonical path resource",
649
+ );
650
+ return managedIdentity({
651
+ kind: declaration.kind,
652
+ path: resource.path,
653
+ object: resource.object,
654
+ ancestor: resource.ancestor,
655
+ suffix: resource.suffix,
656
+ });
657
+ }
658
+ if (declaration.kind === "port")
659
+ return `${declaration.kind}:${declaration.protocol}:${declaration.address}:${declaration.port}`;
660
+ return `${declaration.kind}:${declaration.identity}`;
661
+ }
662
+
663
+ export function resourceCoversPath(
664
+ resource: ManagedCanonicalResource,
665
+ targetPath: string,
666
+ needed: "read" | "write",
667
+ ): boolean {
668
+ if (resource.declaration.kind !== "path" || resource.path === null) return false;
669
+ if (needed === "write" && resource.declaration.mode !== "write") return false;
670
+ return resource.path === targetPath || (resource.declaration.recursive && within(resource.path, targetPath));
671
+ }
672
+ function resourceCoversArtifact(
673
+ resource: ManagedCanonicalResource,
674
+ artifact: { path: string; object: string | null; ancestor: string; suffix: string },
675
+ needed: "read" | "write",
676
+ ): boolean {
677
+ return resourceCoversPath(resource, artifact.path, needed);
678
+ }
679
+
680
+ function isInsideWorkspace(workspace: string, artifact: string): boolean {
681
+ return workspace === artifact || within(workspace, artifact);
682
+ }
683
+
684
+ async function validateDefinition(binding: ManagedDomainBinding, node: ManagedTaskDefinition): Promise<void> {
685
+ const workspace = await canonicalPath(binding, node.workspace);
686
+ requirePolicy(workspace.object && (await fs.lstat(workspace.path)).isDirectory(), "workspace missing");
687
+ const resources = await canonicalizeManagedResources(binding, node.resources);
688
+ for (const resource of resources) {
689
+ if (resource.declaration.kind === "path") {
690
+ requirePolicy(
691
+ !resource.declaration.namespace || resource.declaration.mode === "write",
692
+ "namespace requires write resource",
693
+ );
694
+ }
695
+ }
696
+ requireDistinct(resources.map(canonicalResourceKey), "duplicate resource");
697
+ const artifacts = await Promise.all(
698
+ node.artifacts.map(async artifact => ({ artifact, canonical: await canonicalPath(binding, artifact.path) })),
699
+ );
700
+ for (let i = 0; i < artifacts.length; i++) {
701
+ const { artifact, canonical } = artifacts[i]!;
702
+ const needed = artifact.role === "input" ? "read" : "write";
703
+ requirePolicy(isInsideWorkspace(workspace.path, canonical.path), "artifact is outside workspace");
704
+ requirePolicy(
705
+ resources.some(resource => resourceCoversArtifact(resource, canonical, needed)),
706
+ "artifact lacks resource declaration",
707
+ );
708
+ if (needed === "write" && (canonical.object === null || artifact.presence === "absent")) {
709
+ const parent = await canonicalPath(binding, path.dirname(canonical.path));
710
+ requirePolicy(
711
+ resources.some(
712
+ resource =>
713
+ resource.declaration.kind === "path" &&
714
+ resource.declaration.mode === "write" &&
715
+ resource.declaration.namespace &&
716
+ resource.path === parent.path,
717
+ ),
718
+ "output requires parent namespace write",
719
+ );
720
+ }
721
+ for (const other of artifacts.slice(0, i))
722
+ requirePolicy(
723
+ !(canonical.object && canonical.object === other.canonical.object) &&
724
+ !within(canonical.path, other.canonical.path) &&
725
+ !within(other.canonical.path, canonical.path),
726
+ "overlapping artifact roles",
727
+ );
728
+ }
729
+ }
730
+
731
+ function validateManifest(manifest: ManagedManifest): ManagedManifest {
732
+ const parsed = z.array(ManagedManifestEntrySchema).parse(manifest);
733
+ requireDistinct(
734
+ parsed.map(entry => entry.path),
735
+ "duplicate manifest entry",
736
+ );
737
+ for (const entry of parsed) {
738
+ requirePolicy(isCanonicalAbsolute(entry.path), "manifest path is not canonical");
739
+ if (entry.kind === "absent")
740
+ requirePolicy(
741
+ entry.object === null && entry.hash === null && entry.mode === null,
742
+ "invalid absent manifest entry",
743
+ );
744
+ else
745
+ requirePolicy(
746
+ entry.object !== null && entry.hash !== null && entry.mode !== null,
747
+ "incomplete manifest entry",
748
+ );
749
+ }
750
+ return parsed;
751
+ }
752
+
753
+ export async function captureManagedManifest(binding: ManagedDomainBinding, paths: string[]): Promise<ManagedManifest> {
754
+ await validateBinding(binding);
755
+ const requested = [...new Set(paths)].sort();
756
+ const requestedCanonical = await Promise.all(requested.map(value => canonicalPath(binding, value)));
757
+ const roots = requestedCanonical.filter(
758
+ candidate =>
759
+ !requestedCanonical.some(
760
+ other => other !== candidate && other.object !== null && within(other.path, candidate.path),
761
+ ),
762
+ );
763
+ const entries: ManagedManifest = [];
764
+ for (const requestedEntry of roots) {
765
+ const canonical = await canonicalPath(binding, requestedEntry.path);
766
+ if (!canonical.object) {
767
+ entries.push({ path: canonical.path, object: null, kind: "absent", hash: null, mode: null });
768
+ continue;
769
+ }
770
+ const before = await fs.lstat(canonical.path, { bigint: true });
771
+ requirePolicy(
772
+ !before.isSymbolicLink() && (before.isFile() || before.isDirectory()),
773
+ "unsupported manifest path type",
774
+ );
775
+ const directory = before.isDirectory();
776
+ const hash = directory
777
+ ? managedIdentity((await fs.readdir(canonical.path)).sort())
778
+ : createHash("sha256")
779
+ .update(await fs.readFile(canonical.path))
780
+ .digest("hex");
781
+ const after = await fs.lstat(canonical.path, { bigint: true });
782
+ requirePolicy(
783
+ before.dev === after.dev &&
784
+ before.ino === after.ino &&
785
+ before.mtimeNs === after.mtimeNs &&
786
+ before.ctimeNs === after.ctimeNs &&
787
+ before.size === after.size,
788
+ "manifest changed during capture",
789
+ );
790
+ entries.push({
791
+ path: canonical.path,
792
+ object: canonical.object,
793
+ kind: directory ? "directory" : "file",
794
+ hash,
795
+ mode: Number(after.mode & 0o777n),
796
+ });
797
+ if (directory) {
798
+ const children = (await fs.readdir(canonical.path)).sort().map(name => path.join(canonical.path, name));
799
+ entries.push(...(await captureManagedManifest(binding, children)));
800
+ const final = await fs.lstat(canonical.path, { bigint: true });
801
+ requirePolicy(
802
+ final.mtimeNs === after.mtimeNs && final.ctimeNs === after.ctimeNs,
803
+ "directory changed during capture",
804
+ );
805
+ }
806
+ }
807
+ return validateManifest(entries.sort((left, right) => left.path.localeCompare(right.path)));
808
+ }
809
+
810
+ export async function assertManagedManifestCurrent(
811
+ binding: ManagedDomainBinding,
812
+ manifest: ManagedManifest,
813
+ ): Promise<void> {
814
+ const expected = validateManifest(manifest);
815
+ // Directory entries recursively cover their descendants; avoid capturing them twice.
816
+ const roots = expected.filter(
817
+ entry =>
818
+ !expected.some(parent => parent !== entry && parent.kind === "directory" && within(parent.path, entry.path)),
819
+ );
820
+ requirePolicy(
821
+ managedIdentity(
822
+ await captureManagedManifest(
823
+ binding,
824
+ roots.map(entry => entry.path),
825
+ ),
826
+ ) === managedIdentity(expected),
827
+ "content drift",
828
+ );
829
+ }
830
+ export function managedTaskDomainPath(controlRoot: string): string {
831
+ return path.join(controlRoot, ".gjc", "managed-task-domain", "state.json");
832
+ }
833
+ export function managedEnrollmentIndexPath(agentDir: string): string {
834
+ return path.join(agentDir, ".gjc", "managed-task-enrollments", "index.json");
835
+ }
836
+
837
+ export function validateManagedTaskDomain(value: unknown): ManagedTaskDomain {
838
+ const state = ManagedTaskDomainSchema.parse(value);
839
+ requirePolicy(isCanonicalAbsolute(state.binding.controlRoot), "control root is not canonical");
840
+ requirePolicy(isCanonicalAbsolute(state.binding.agentDir), "agent directory is not canonical");
841
+ requireDistinct(
842
+ state.binding.worktrees.map(worktree => worktree.path),
843
+ "duplicate worktree",
844
+ );
845
+ requireDistinct(
846
+ state.binding.aliases.map(alias => alias.path),
847
+ "duplicate alias",
848
+ );
849
+ requirePolicy(new Set(state.graphs.map(g => g.id)).size === state.graphs.length, "duplicate graph");
850
+ const nativeKeys = new Set<string>(),
851
+ nativeIds = new Set<string>();
852
+ const held: ManagedTaskAttempt[] = [];
853
+ for (const graph of state.graphs) {
854
+ parseManagedTaskDefinitions(graph.nodes.map(node => node.definition));
855
+ requirePolicy(new Set(graph.canceled).size === graph.canceled.length, "duplicate canceled node");
856
+ requirePolicy(
857
+ graph.canceled.every(id => graph.nodes.some(n => n.definition.id === id)),
858
+ "unknown canceled node",
859
+ );
860
+ requirePolicy(new Set(graph.attempts.map(a => a.id)).size === graph.attempts.length, "duplicate attempt");
861
+ for (const node of graph.nodes) {
862
+ requirePolicy(node.hash === managedIdentity(node.definition), "definition hash mismatch");
863
+ }
864
+ for (const attempt of graph.attempts) {
865
+ requirePolicy(
866
+ attempt.definitionHash === managedIdentity(attempt.definition) && attempt.nodeId === attempt.definition.id,
867
+ "attempt definition mismatch",
868
+ );
869
+ requirePolicy(attempt.accepted === null || trustedStoredAcceptance(attempt), "untrusted acceptance authority");
870
+ requirePolicy(
871
+ attempt.resources.length === attempt.definition.resources.length &&
872
+ attempt.resources.every(
873
+ (r, i) => managedIdentity(r.declaration) === managedIdentity(attempt.definition.resources[i]),
874
+ ),
875
+ "reservation vector mismatch",
876
+ );
877
+ requireDistinct(attempt.resources.map(canonicalResourceKey), "duplicate attempt resource");
878
+ for (const resource of attempt.resources) {
879
+ if (resource.declaration.kind === "path") {
880
+ requirePolicy(
881
+ resource.path && isCanonicalAbsolute(resource.path) && resource.ancestor && resource.suffix !== null,
882
+ "invalid canonical path identity",
883
+ );
884
+ requirePolicy(
885
+ !resource.declaration.namespace || resource.declaration.mode === "write",
886
+ "namespace requires write resource",
887
+ );
888
+ } else
889
+ requirePolicy(
890
+ resource.path === null &&
891
+ resource.object === null &&
892
+ resource.ancestor === null &&
893
+ resource.suffix === null,
894
+ "invalid named identity fields",
895
+ );
896
+ }
897
+ validateManifest(attempt.inputs);
898
+ requireDistinct(
899
+ attempt.predecessors.map(predecessor => predecessor.nodeId),
900
+ "duplicate predecessor vector",
901
+ );
902
+ requirePolicy(
903
+ attempt.predecessors
904
+ .map(p => p.nodeId)
905
+ .sort()
906
+ .join("\0") === [...attempt.definition.predecessors].sort().join("\0"),
907
+ "predecessor vector mismatch",
908
+ );
909
+ for (const predecessor of attempt.predecessors) validateManifest(predecessor.produced);
910
+ if (attempt.produced) validateManifest(attempt.produced);
911
+ if (attempt.validationImmutable) validateManifest(attempt.validationImmutable);
912
+ if (attempt.validationOutputs) validateManifest(attempt.validationOutputs);
913
+ if (attempt.fence === "current")
914
+ requirePolicy(
915
+ graph.nodes.some(
916
+ n =>
917
+ n.definition.id === attempt.nodeId &&
918
+ n.revision === attempt.taskRevision &&
919
+ n.hash === attempt.definitionHash,
920
+ ),
921
+ "current attempt definition missing",
922
+ );
923
+ if (attempt.validation !== "not-started")
924
+ requirePolicy(attempt.worker === "closed", "validation requires closed worker");
925
+ if (attempt.validation === "not-started")
926
+ requirePolicy(attempt.verificationExecution === null, "verification execution without start");
927
+ if (attempt.validation !== "not-started")
928
+ requirePolicy(attempt.verificationExecution !== null, "missing verification execution");
929
+ if (attempt.validation === "finished") {
930
+ const observations = attempt.verificationExecution?.observations.length ?? 0;
931
+ const commands = attempt.verificationExecution?.commands.length ?? 0;
932
+ const failed = attempt.fence === "failed";
933
+ requirePolicy(
934
+ failed ? observations > 0 && observations <= commands : observations === commands,
935
+ "unfinished verification observations",
936
+ );
937
+ }
938
+ requirePolicy(
939
+ !nativeKeys.has(attempt.native.key) && !nativeIds.has(attempt.native.identity),
940
+ "native identity reused",
941
+ );
942
+ nativeKeys.add(attempt.native.key);
943
+ nativeIds.add(attempt.native.identity);
944
+ if (attempt.retired) requirePolicy(canRetireManagedAttempt(attempt), "unproven retirement");
945
+ if (!attempt.retired) {
946
+ for (const previous of held)
947
+ requirePolicy(
948
+ !attempt.resources.some(a => previous.resources.some(b => managedResourcesConflict(a, b))),
949
+ "conflicting reservations",
950
+ );
951
+ held.push(attempt);
952
+ }
953
+ }
954
+ }
955
+ return state;
956
+ }
957
+
958
+ /**
959
+ * Static schema validation above intentionally does not infer mutable filesystem
960
+ * state. Every durable transaction performs this second binding-aware pass while
961
+ * holding the domain lock, before either user mutation or publication.
962
+ */
963
+ async function validateManagedTaskDomainAuthority(
964
+ state: ManagedTaskDomain,
965
+ binding: ManagedDomainBinding,
966
+ ): Promise<void> {
967
+ requirePolicy(managedIdentity(state.binding) === managedIdentity(binding), "domain binding mismatch");
968
+ await validateBinding(binding);
969
+ for (const graph of state.graphs) {
970
+ for (const node of graph.nodes) await validateDefinition(binding, node.definition);
971
+ for (const attempt of graph.attempts) {
972
+ await validateDefinition(binding, attempt.definition);
973
+ // A produced output may legitimately turn an absent reservation path
974
+ // into a file. Re-canonicalize to reject escapes/aliases, but retain the
975
+ // durable admission vector rather than treating that allowed transition
976
+ // as a reservation rewrite.
977
+ await canonicalizeManagedResources(binding, attempt.definition.resources);
978
+ }
979
+ }
980
+ }
981
+
982
+ export interface ManagedDomainTransactionOptions {
983
+ binding: ManagedDomainBinding;
984
+ expectedRevision?: number;
985
+ /** Only for first enrollment; caller checks its authoritative registration/native journal under this lock. */
986
+ assertNoManagedEvidence?: () => Promise<void>;
987
+ /** Publish a fail-closed marker after definition validation but before the first state write. */
988
+ beforeFirstPublication?: () => Promise<void>;
989
+ /** Runs under the domain lock after the first state is durable, serialized with stale-index reclamation. */
990
+ onFirstPublication?: () => Promise<void>;
991
+ /** Restore pending only when the state writer proves it failed before rename. */
992
+ onFirstPublicationAborted?: () => Promise<void>;
993
+ /** Trusted native/internal observation: reload under the lock and CAS against the live revision. */
994
+ internal?: boolean;
995
+ }
996
+ /** Trusted internal policy boundary, NOT a public wire parser or receipt/PASS submission API. */
997
+ export async function transactManagedTaskDomain<T>(
998
+ options: ManagedDomainTransactionOptions,
999
+ mutation: (state: ManagedTaskDomain) => Promise<T>,
1000
+ ): Promise<{ state: ManagedTaskDomain; result: T }> {
1001
+ const binding = ManagedDomainBindingSchema.parse(options.binding);
1002
+ if (!options.internal) {
1003
+ requirePolicy(options.expectedRevision !== undefined, "expectedRevision is required");
1004
+ counter.parse(options.expectedRevision);
1005
+ } else requirePolicy(options.assertNoManagedEvidence === undefined, "internal mutation cannot enroll");
1006
+ await validateBinding(binding);
1007
+ const target = managedTaskDomainPath(binding.controlRoot);
1008
+ const writer = { cwd: binding.controlRoot, privateDurable: { directory: path.dirname(target) } };
1009
+ return withWorkflowStateLock(
1010
+ target,
1011
+ async () => {
1012
+ await validateBinding(binding);
1013
+ const read = await readExistingStateForMutation(target);
1014
+ requirePolicy(read.kind !== "corrupt", "corrupt authority");
1015
+ const firstPublication = read.kind === "absent";
1016
+ let state: ManagedTaskDomain;
1017
+ if (read.kind === "absent") {
1018
+ requirePolicy(!options.internal, "established authority missing");
1019
+ requirePolicy(options.expectedRevision === 0, "established authority missing");
1020
+ requirePolicy(
1021
+ typeof options.assertNoManagedEvidence === "function",
1022
+ "fresh enrollment requires native evidence check",
1023
+ );
1024
+ await options.assertNoManagedEvidence();
1025
+ state = { version: 1, state_revision: 0, binding, graphs: [] };
1026
+ } else state = validateManagedTaskDomain(read.value);
1027
+ requirePolicy(managedIdentity(state.binding) === managedIdentity(binding), "domain binding mismatch");
1028
+ const expectedRevision = options.internal ? state.state_revision : options.expectedRevision!;
1029
+ if (state.state_revision !== expectedRevision)
1030
+ throw new StateWriteConflictError(target, expectedRevision, state.state_revision);
1031
+ await validateManagedTaskDomainAuthority(state, binding);
1032
+ const previous = structuredClone(state);
1033
+ const result = await mutation(state);
1034
+ for (const graph of state.graphs)
1035
+ for (const attempt of graph.attempts)
1036
+ if (!attempt.retired && canRetireManagedAttempt(attempt)) retireManagedAttempt(attempt);
1037
+ requirePolicy(managedIdentity(state.binding) === managedIdentity(binding), "immutable binding changed");
1038
+ requirePolicy(state.state_revision === previous.state_revision, "revision belongs to source CAS");
1039
+ for (const graph of previous.graphs) {
1040
+ const next = state.graphs.find(g => g.id === graph.id);
1041
+ requirePolicy(
1042
+ next && next.owner === graph.owner && next.revision >= graph.revision,
1043
+ "graph history/owner changed",
1044
+ );
1045
+ for (const attempt of graph.attempts) {
1046
+ const updated = next.attempts.find(a => a.id === attempt.id);
1047
+ requirePolicy(updated, "attempt history removed");
1048
+ const immutable = (a: ManagedTaskAttempt) => [
1049
+ a.id,
1050
+ a.nodeId,
1051
+ a.taskRevision,
1052
+ a.graphRevision,
1053
+ a.definitionHash,
1054
+ a.definition,
1055
+ a.native,
1056
+ a.inputs,
1057
+ a.predecessors,
1058
+ a.resources,
1059
+ ];
1060
+ requirePolicy(
1061
+ managedIdentity(immutable(updated)) === managedIdentity(immutable(attempt)),
1062
+ "immutable attempt changed",
1063
+ );
1064
+ requirePolicy(!attempt.retired || updated.retired, "retired attempt revived");
1065
+ requirePolicy(attempt.fence === "current" || updated.fence !== "current", "fenced attempt revived");
1066
+ }
1067
+ }
1068
+ const previousAccepted = new Map(
1069
+ previous.graphs.flatMap(graph =>
1070
+ graph.attempts.map(attempt => [`${graph.id}\0${attempt.id}`, attempt.accepted] as const),
1071
+ ),
1072
+ );
1073
+ for (const graph of state.graphs)
1074
+ for (const attempt of graph.attempts) {
1075
+ const prior = previousAccepted.get(`${graph.id}\0${attempt.id}`) ?? null;
1076
+ if (managedIdentity(attempt.accepted) === managedIdentity(prior)) continue;
1077
+ requirePolicy(prior === null && attempt.accepted !== null, "acceptance cannot be rewritten");
1078
+ const accepted = attempt.accepted;
1079
+ requirePolicy(
1080
+ attempt.fence === "current" && attempt.validation === "finished" && attempt.worker === "closed",
1081
+ "acceptance requires current finished closed attempt",
1082
+ );
1083
+ requirePolicy(
1084
+ attempt.verificationExecution?.receipts.some(
1085
+ receipt => receipt.id === accepted.id && receipt.hash === accepted.hash,
1086
+ ) === true,
1087
+ "acceptance missing stored receipt",
1088
+ );
1089
+ }
1090
+ validateManagedTaskDomain(state);
1091
+ await validateManagedTaskDomainAuthority(state, binding);
1092
+ if (firstPublication) await options.beforeFirstPublication?.();
1093
+ let written: GuardedWriteResult;
1094
+ try {
1095
+ written = await writeGuardedJsonAtomic(target, state, {
1096
+ ...writer,
1097
+ policy: "source",
1098
+ expectedRevision,
1099
+ lockHeld: true,
1100
+ });
1101
+ } catch (caught) {
1102
+ if (firstPublication && caught instanceof StatePublicationUncertainError) {
1103
+ const published = await readExistingStateForMutation(target);
1104
+ if (published.kind === "valid") {
1105
+ try {
1106
+ const state = validateManagedTaskDomain(published.value);
1107
+ if (managedIdentity(state.binding) === managedIdentity(binding))
1108
+ await options.onFirstPublication?.();
1109
+ } catch {
1110
+ // Keep the publishing marker when the read-back cannot prove this domain.
1111
+ }
1112
+ }
1113
+ // Missing or corrupt after a rename attempt is deliberately retained as publishing.
1114
+ } else if (firstPublication) {
1115
+ await options.onFirstPublicationAborted?.();
1116
+ }
1117
+ throw caught;
1118
+ }
1119
+ requirePolicy(written.written, "source publication skipped");
1120
+ if (firstPublication) await options.onFirstPublication?.();
1121
+ return { state: validateManagedTaskDomain(written.stamped), result };
1122
+ },
1123
+ writer,
1124
+ );
1125
+ }
1126
+ export async function defineManagedTaskGraph(
1127
+ state: ManagedTaskDomain,
1128
+ input: { id: string; owner: string; nodes: unknown },
1129
+ ): Promise<ManagedTaskGraph> {
1130
+ text.parse(input.id);
1131
+ text.parse(input.owner);
1132
+ requirePolicy(!state.graphs.some(g => g.id === input.id), "graph already exists");
1133
+ const nodes = parseManagedTaskDefinitions(input.nodes);
1134
+ for (const node of nodes) await validateDefinition(state.binding, node);
1135
+ const graph: ManagedTaskGraph = {
1136
+ id: input.id,
1137
+ owner: input.owner,
1138
+ revision: 1,
1139
+ nodes: nodes.map(definition => ({ revision: 1, hash: managedIdentity(definition), definition })),
1140
+ attempts: [],
1141
+ canceled: [],
1142
+ };
1143
+ state.graphs.push(graph);
1144
+ return graph;
1145
+ }
1146
+ function predecessorVector(graph: ManagedTaskGraph, node: ManagedTaskDefinition): ManagedPredecessor[] | null {
1147
+ const result: ManagedPredecessor[] = [];
1148
+ for (const id of [...node.predecessors].sort()) {
1149
+ const definition = graph.nodes.find(n => n.definition.id === id)!;
1150
+ const attempt = graph.attempts.find(
1151
+ a =>
1152
+ a.nodeId === id &&
1153
+ a.taskRevision === definition.revision &&
1154
+ a.definitionHash === definition.hash &&
1155
+ a.fence === "current" &&
1156
+ a.accepted,
1157
+ );
1158
+ if (!attempt?.accepted || !attempt.produced) return null;
1159
+ result.push({
1160
+ nodeId: id,
1161
+ taskRevision: attempt.taskRevision,
1162
+ attemptId: attempt.id,
1163
+ verificationId: attempt.accepted.id,
1164
+ verificationHash: attempt.accepted.hash,
1165
+ produced: attempt.produced,
1166
+ });
1167
+ }
1168
+ return result;
1169
+ }
1170
+ export function readyManagedTasks(graph: ManagedTaskGraph): string[] {
1171
+ return graph.nodes
1172
+ .filter(
1173
+ n =>
1174
+ !graph.canceled.includes(n.definition.id) &&
1175
+ predecessorVector(graph, n.definition) !== null &&
1176
+ !graph.attempts.some(
1177
+ a =>
1178
+ a.nodeId === n.definition.id &&
1179
+ (!a.retired || (a.taskRevision === n.revision && a.fence === "current" && a.accepted)),
1180
+ ),
1181
+ )
1182
+ .map(n => n.definition.id)
1183
+ .sort();
1184
+ }
1185
+ function existingExactManagedAttempt(
1186
+ graph: ManagedTaskGraph,
1187
+ input: { owner: string; nodeId: string; attemptId: string; native: ManagedNativeIdentity },
1188
+ ): ManagedTaskAttempt | undefined {
1189
+ return graph.attempts.find(
1190
+ attempt =>
1191
+ !attempt.retired &&
1192
+ attempt.nodeId === input.nodeId &&
1193
+ attempt.id === input.attemptId &&
1194
+ attempt.native.key === input.native.key &&
1195
+ attempt.native.identity === input.native.identity &&
1196
+ attempt.native.requestHash === input.native.requestHash,
1197
+ );
1198
+ }
1199
+ /** Exact same-key retry observes the original reservation; otherwise admits a new one. */
1200
+ export async function observeOrAdmitManagedTask(
1201
+ state: ManagedTaskDomain,
1202
+ input: { graphId: string; owner: string; nodeId: string; attemptId: string; native: ManagedNativeIdentity },
1203
+ ): Promise<ManagedAttemptRef> {
1204
+ const graph = state.graphs.find(g => g.id === input.graphId);
1205
+ requirePolicy(graph && graph.owner === input.owner, "graph owner mismatch");
1206
+ ManagedNativeIdentitySchema.parse(input.native);
1207
+ const existing = existingExactManagedAttempt(graph, input);
1208
+ if (existing) {
1209
+ const node = graph.nodes.find(n => n.definition.id === input.nodeId);
1210
+ requirePolicy(node, "node missing");
1211
+ requirePolicy(!graph.canceled.includes(input.nodeId), "node is canceled");
1212
+ requirePolicy(existing.fence === "current", "managed attempt is fenced");
1213
+ requirePolicy(
1214
+ existing.taskRevision === node.revision && existing.definitionHash === node.hash,
1215
+ "managed native vector changed",
1216
+ );
1217
+ requirePolicy(existing.definition.workspace === node.definition.workspace, "workspace changed");
1218
+ return managedAttemptRefFromState(state, existing, graph.id);
1219
+ }
1220
+ return admitManagedTask(state, input);
1221
+ }
1222
+ /** Caller supplies native deriveIdempotencyIdentity result only after authenticated admission. No native effect here. */
1223
+ export async function admitManagedTask(
1224
+ state: ManagedTaskDomain,
1225
+ input: { graphId: string; owner: string; nodeId: string; attemptId: string; native: ManagedNativeIdentity },
1226
+ ): Promise<ManagedAttemptRef> {
1227
+ const graph = state.graphs.find(g => g.id === input.graphId);
1228
+ requirePolicy(graph && graph.owner === input.owner, "graph owner mismatch");
1229
+ requirePolicy(readyManagedTasks(graph).includes(input.nodeId), "node not ready");
1230
+ text.parse(input.attemptId);
1231
+ ManagedNativeIdentitySchema.parse(input.native);
1232
+ requirePolicy(
1233
+ !state.graphs.some(g =>
1234
+ g.attempts.some(
1235
+ a =>
1236
+ a.native.key === input.native.key ||
1237
+ a.native.identity === input.native.identity ||
1238
+ (g.id === graph.id && a.id === input.attemptId),
1239
+ ),
1240
+ ),
1241
+ "attempt/native identity already reserved",
1242
+ );
1243
+ const node = graph.nodes.find(n => n.definition.id === input.nodeId)!;
1244
+ await validateDefinition(state.binding, node.definition);
1245
+ const predecessors = predecessorVector(graph, node.definition)!;
1246
+ for (const predecessor of predecessors) await assertManagedManifestCurrent(state.binding, predecessor.produced);
1247
+ const resources = await canonicalizeManagedResources(state.binding, node.definition.resources);
1248
+ for (const g of state.graphs)
1249
+ for (const attempt of g.attempts.filter(a => !a.retired)) {
1250
+ const current = await canonicalizeManagedResources(
1251
+ state.binding,
1252
+ attempt.resources.map(r => r.declaration),
1253
+ );
1254
+ requirePolicy(
1255
+ !resources.some(a => [...attempt.resources, ...current].some(b => managedResourcesConflict(a, b))),
1256
+ "resource conflict",
1257
+ );
1258
+ }
1259
+ for (const predecessor of predecessors)
1260
+ for (const entry of predecessor.produced) {
1261
+ const consumed = await canonicalizeManagedResources(state.binding, [
1262
+ { kind: "path", path: entry.path, recursive: entry.kind === "directory", namespace: false, mode: "read" },
1263
+ ]);
1264
+ requirePolicy(
1265
+ !resources.some(r => managedResourcesConflict(r, consumed[0]!)),
1266
+ "successor overwrites predecessor output",
1267
+ );
1268
+ requirePolicy(
1269
+ resources.some(
1270
+ r =>
1271
+ r.declaration.kind === "path" &&
1272
+ r.declaration.mode === "read" &&
1273
+ (r.path === entry.path || (r.declaration.recursive && within(r.path!, entry.path))),
1274
+ ),
1275
+ "consumed predecessor lacks read reservation",
1276
+ );
1277
+ }
1278
+ const inputPaths = [
1279
+ ...new Set([
1280
+ ...node.definition.artifacts.filter(a => a.role !== "validation-output").map(a => a.path),
1281
+ ...predecessors.flatMap(p => p.produced.map(e => e.path)),
1282
+ ]),
1283
+ ];
1284
+ const roots = inputPaths.filter(p => !inputPaths.some(other => other !== p && within(other, p)));
1285
+ const inputs = await captureManagedManifest(state.binding, roots);
1286
+ for (const artifact of node.definition.artifacts) {
1287
+ const entry = inputs.find(
1288
+ e =>
1289
+ e.path ===
1290
+ (resources.find(r => r.declaration.kind === "path" && r.declaration.path === artifact.path)?.path ??
1291
+ artifact.path),
1292
+ );
1293
+ if (artifact.role === "input")
1294
+ requirePolicy(
1295
+ entry &&
1296
+ (artifact.presence !== "required" || entry.kind !== "absent") &&
1297
+ (artifact.presence !== "absent" || entry.kind === "absent"),
1298
+ "input presence contract failed",
1299
+ );
1300
+ }
1301
+ graph.attempts.push({
1302
+ id: input.attemptId,
1303
+ nodeId: input.nodeId,
1304
+ taskRevision: node.revision,
1305
+ graphRevision: graph.revision,
1306
+ definitionHash: node.hash,
1307
+ definition: structuredClone(node.definition),
1308
+ native: structuredClone(input.native),
1309
+ inputs,
1310
+ predecessors,
1311
+ resources,
1312
+ produced: null,
1313
+ validationImmutable: null,
1314
+ validationOutputs: null,
1315
+ worker: "reserved",
1316
+ validation: "not-started",
1317
+ fence: "current",
1318
+ retired: false,
1319
+ accepted: null,
1320
+ verificationExecution: null,
1321
+ });
1322
+ return {
1323
+ controlRoot: state.binding.controlRoot,
1324
+ enrollmentId: state.binding.enrollmentId,
1325
+ agentDirIdentity: state.binding.agentDirIdentity,
1326
+ graphId: graph.id,
1327
+ graphRevision: graph.revision,
1328
+ nodeId: input.nodeId,
1329
+ taskRevision: node.revision,
1330
+ attemptId: input.attemptId,
1331
+ definitionHash: node.hash,
1332
+ criteriaIdentity: node.definition.criteriaIdentity,
1333
+ inputIdentity: managedIdentity(inputs),
1334
+ predecessorIdentity: managedIdentity(predecessors),
1335
+ resourceIdentity: managedIdentity(resources),
1336
+ native: structuredClone(input.native),
1337
+ nativeIdentity: input.native.identity,
1338
+ };
1339
+ }
1340
+ export function canRetireManagedAttempt(attempt: ManagedTaskAttempt): boolean {
1341
+ return (
1342
+ (attempt.worker === "no-effect" || attempt.worker === "closed") &&
1343
+ (attempt.validation === "not-started" || attempt.validation === "finished") &&
1344
+ (attempt.fence !== "current" || attempt.accepted !== null)
1345
+ );
1346
+ }
1347
+ export function retireManagedAttempt(attempt: ManagedTaskAttempt): void {
1348
+ requirePolicy(canRetireManagedAttempt(attempt), "effect lifetime still held");
1349
+ attempt.retired = true;
1350
+ }
1351
+ /** Invalidation never releases reservations or rewrites old immutable attempts. */
1352
+ export function cancelManagedTasks(graph: ManagedTaskGraph, ids: string[]): string[] {
1353
+ requirePolicy(
1354
+ ids.every(id => graph.nodes.some(n => n.definition.id === id)),
1355
+ "unknown cancellation node",
1356
+ );
1357
+ const affected = downstreamClosure(
1358
+ ids,
1359
+ graph.nodes.map(n => n.definition),
1360
+ );
1361
+ graph.canceled = [...new Set([...graph.canceled, ...affected])].sort();
1362
+ for (const attempt of graph.attempts)
1363
+ if (affected.has(attempt.nodeId) && attempt.fence === "current") attempt.fence = "canceled";
1364
+ return [...affected].sort();
1365
+ }
1366
+ function definitionVector(definition: ManagedTaskDefinition): unknown {
1367
+ return {
1368
+ task: definition.task,
1369
+ workspace: definition.workspace,
1370
+ criteriaIdentity: definition.criteriaIdentity,
1371
+ predecessors: [...definition.predecessors].sort(),
1372
+ resources: definition.resources,
1373
+ artifacts: definition.artifacts,
1374
+ validations: definition.validations,
1375
+ };
1376
+ }
1377
+ function downstreamClosure(ids: string[], definitions: ManagedTaskDefinition[]): Set<string> {
1378
+ const affected = new Set(ids);
1379
+ let changed = true;
1380
+ while (changed) {
1381
+ changed = false;
1382
+ for (const node of definitions)
1383
+ if (!affected.has(node.id) && node.predecessors.some(p => affected.has(p))) {
1384
+ affected.add(node.id);
1385
+ changed = true;
1386
+ }
1387
+ }
1388
+ return affected;
1389
+ }
1390
+ export async function reviseManagedTaskGraph(
1391
+ state: ManagedTaskDomain,
1392
+ graph: ManagedTaskGraph,
1393
+ value: unknown,
1394
+ ): Promise<string[]> {
1395
+ const definitions = parseManagedTaskDefinitions(value);
1396
+ for (const definition of definitions) await validateDefinition(state.binding, definition);
1397
+ const old = graph.nodes;
1398
+ const seeds = new Set<string>();
1399
+ for (const previous of old) {
1400
+ const next = definitions.find(definition => definition.id === previous.definition.id);
1401
+ if (!next || managedIdentity(definitionVector(next)) !== managedIdentity(definitionVector(previous.definition)))
1402
+ seeds.add(previous.definition.id);
1403
+ }
1404
+ for (const definition of definitions) {
1405
+ const previous = old.find(node => node.definition.id === definition.id);
1406
+ if (
1407
+ !previous ||
1408
+ managedIdentity(definitionVector(definition)) !== managedIdentity(definitionVector(previous.definition))
1409
+ )
1410
+ seeds.add(definition.id);
1411
+ }
1412
+ const affected = downstreamClosure([...seeds], [...old.map(node => node.definition), ...definitions]);
1413
+ graph.nodes = definitions.map(definition => {
1414
+ const previous = old.find(node => node.definition.id === definition.id);
1415
+ const historic = Math.max(
1416
+ 0,
1417
+ ...graph.attempts.filter(attempt => attempt.nodeId === definition.id).map(attempt => attempt.taskRevision),
1418
+ );
1419
+ return {
1420
+ definition,
1421
+ hash: managedIdentity(definition),
1422
+ revision: previous ? previous.revision + (affected.has(definition.id) ? 1 : 0) : historic + 1,
1423
+ };
1424
+ });
1425
+ graph.canceled = graph.canceled.filter(
1426
+ id => definitions.some(definition => definition.id === id) && !affected.has(id),
1427
+ );
1428
+ graph.revision++;
1429
+ for (const attempt of graph.attempts)
1430
+ if (affected.has(attempt.nodeId) && attempt.fence === "current") attempt.fence = "superseded";
1431
+ return [...affected].sort();
1432
+ }
1433
+
1434
+ export function reducedWorkroomManagedDefinitions(
1435
+ workspace: string,
1436
+ criteriaIdentity: string,
1437
+ ): ManagedTaskDefinition[] {
1438
+ const node = (id: string, predecessors: string[]): ManagedTaskDefinition => ({
1439
+ id,
1440
+ task: `Workroom ${id}`,
1441
+ workspace,
1442
+ predecessors,
1443
+ criteriaIdentity,
1444
+ validations: [{ name: "check", command: "true" }],
1445
+ resources: [],
1446
+ artifacts: [],
1447
+ });
1448
+ return [
1449
+ node("S5", []),
1450
+ node("UI1a", ["S5"]),
1451
+ node("S6", ["S5"]),
1452
+ node("S7", ["S6"]),
1453
+ node("S8", ["UI1a", "S7"]),
1454
+ node("UI1b", ["S8"]),
1455
+ node("UI2", ["UI1b"]),
1456
+ node("S10a", ["S8"]),
1457
+ node("S10b", ["S10a"]),
1458
+ node("S9a", ["S8"]),
1459
+ node("S9b", ["S9a"]),
1460
+ node("S11", []),
1461
+ ];
1462
+ }
1463
+ export type ManagedNativeEffectFence = "launch" | "seed";
1464
+
1465
+ export function managedNativeVector(attempt: ManagedTaskAttempt): string {
1466
+ return managedIdentity({
1467
+ task: attempt.definition.task,
1468
+ workspace: attempt.definition.workspace,
1469
+ criteriaIdentity: attempt.definition.criteriaIdentity,
1470
+ resources: attempt.definition.resources,
1471
+ definitionHash: attempt.definitionHash,
1472
+ native: attempt.native,
1473
+ });
1474
+ }
1475
+
1476
+ export function lookupManagedAttemptByNativeIdentity(
1477
+ state: ManagedTaskDomain,
1478
+ nativeIdentity: string,
1479
+ ): ManagedTaskAttempt | undefined {
1480
+ const matches = state.graphs.flatMap(graph =>
1481
+ graph.attempts.filter(attempt => attempt.native.identity === nativeIdentity),
1482
+ );
1483
+ return matches.length === 1 ? matches[0] : undefined;
1484
+ }
1485
+
1486
+ /** Trusted read of one attempt under the domain lock. Does not mint ManagedAttemptRef. */
1487
+ export async function inspectManagedAttemptByNativeIdentity(
1488
+ binding: ManagedDomainBinding,
1489
+ nativeIdentity: string,
1490
+ ): Promise<ManagedTaskAttempt | undefined> {
1491
+ const enrolled = ManagedDomainBindingSchema.parse(binding);
1492
+ digest.parse(nativeIdentity);
1493
+ await validateBinding(enrolled);
1494
+ const target = managedTaskDomainPath(enrolled.controlRoot);
1495
+ return withWorkflowStateLock(
1496
+ target,
1497
+ async () => {
1498
+ const read = await readExistingStateForMutation(target);
1499
+ requirePolicy(read.kind !== "corrupt", "corrupt authority");
1500
+ if (read.kind === "absent") return undefined;
1501
+ const state = validateManagedTaskDomain(read.value);
1502
+ requirePolicy(managedIdentity(state.binding) === managedIdentity(enrolled), "domain binding mismatch");
1503
+ await validateManagedTaskDomainAuthority(state, enrolled);
1504
+ return lookupManagedAttemptByNativeIdentity(state, nativeIdentity);
1505
+ },
1506
+ { cwd: enrolled.controlRoot, privateDurable: { directory: path.dirname(target) } },
1507
+ );
1508
+ }
1509
+
1510
+ /**
1511
+ * Recheck current fence and immutable native vector, then run the native
1512
+ * transition before releasing the domain lock. Callers must not enter this
1513
+ * from a nested native serialization callback.
1514
+ */
1515
+ export async function withManagedNativeEffectAuthorized<T>(
1516
+ binding: ManagedDomainBinding,
1517
+ nativeIdentity: string,
1518
+ expectedVector: string,
1519
+ fence: ManagedNativeEffectFence,
1520
+ effect: () => Promise<T>,
1521
+ ): Promise<T> {
1522
+ digest.parse(expectedVector);
1523
+ requirePolicy(fence === "launch" || fence === "seed", "unknown native effect fence");
1524
+ const enrolled = ManagedDomainBindingSchema.parse(binding);
1525
+ await validateBinding(enrolled);
1526
+ const target = managedTaskDomainPath(enrolled.controlRoot);
1527
+ return withWorkflowStateLock(
1528
+ target,
1529
+ async () => {
1530
+ const read = await readExistingStateForMutation(target);
1531
+ requirePolicy(read.kind !== "corrupt", "corrupt authority");
1532
+ requirePolicy(read.kind === "valid", "established authority missing");
1533
+ const state = validateManagedTaskDomain(read.value);
1534
+ requirePolicy(managedIdentity(state.binding) === managedIdentity(enrolled), "domain binding mismatch");
1535
+ await validateManagedTaskDomainAuthority(state, enrolled);
1536
+ const attempt = lookupManagedAttemptByNativeIdentity(state, nativeIdentity);
1537
+ requirePolicy(attempt, "managed native identity is not reserved");
1538
+ requirePolicy(attempt.fence === "current" && !attempt.retired, "managed attempt is fenced");
1539
+ requirePolicy(managedNativeVector(attempt) === expectedVector, "managed native vector changed");
1540
+ if (fence === "seed") requirePolicy(attempt.worker !== "no-effect", "canceled attempt cannot seed");
1541
+ if (fence === "launch") {
1542
+ requirePolicy(
1543
+ attempt.worker === "reserved" ||
1544
+ attempt.worker === "authorized" ||
1545
+ (attempt.worker === "no-effect" && attempt.validation === "not-started" && attempt.accepted === null),
1546
+ "illegal worker observation",
1547
+ );
1548
+ if (attempt.worker !== "authorized") {
1549
+ attempt.worker = "authorized";
1550
+ const written = await writeGuardedJsonAtomic(target, state, {
1551
+ cwd: enrolled.controlRoot,
1552
+ privateDurable: { directory: path.dirname(target) },
1553
+ policy: "source",
1554
+ expectedRevision: state.state_revision,
1555
+ lockHeld: true,
1556
+ });
1557
+ requirePolicy(written.written, "source publication skipped");
1558
+ }
1559
+ }
1560
+ return effect();
1561
+ },
1562
+ { cwd: enrolled.controlRoot, privateDurable: { directory: path.dirname(target) } },
1563
+ );
1564
+ }
1565
+ export async function assertManagedNativeEffectAuthorized(
1566
+ binding: ManagedDomainBinding,
1567
+ nativeIdentity: string,
1568
+ expectedVector: string,
1569
+ fence: ManagedNativeEffectFence,
1570
+ ): Promise<void> {
1571
+ await withManagedNativeEffectAuthorized(binding, nativeIdentity, expectedVector, fence, async () => undefined);
1572
+ }
1573
+
1574
+ export function managedAttemptRefMatches(
1575
+ ref: ManagedAttemptRef,
1576
+ attempt: ManagedTaskAttempt,
1577
+ binding: ManagedDomainBinding,
1578
+ ): boolean {
1579
+ return (
1580
+ ref.controlRoot === binding.controlRoot &&
1581
+ ref.enrollmentId === binding.enrollmentId &&
1582
+ ref.agentDirIdentity === binding.agentDirIdentity &&
1583
+ ref.graphId.length > 0 &&
1584
+ ref.nodeId === attempt.nodeId &&
1585
+ ref.attemptId === attempt.id &&
1586
+ ref.taskRevision === attempt.taskRevision &&
1587
+ ref.graphRevision === attempt.graphRevision &&
1588
+ ref.definitionHash === attempt.definitionHash &&
1589
+ ref.criteriaIdentity === attempt.definition.criteriaIdentity &&
1590
+ ref.inputIdentity === managedIdentity(attempt.inputs) &&
1591
+ ref.predecessorIdentity === managedIdentity(attempt.predecessors) &&
1592
+ ref.resourceIdentity === managedIdentity(attempt.resources) &&
1593
+ ref.nativeIdentity === attempt.native.identity &&
1594
+ managedIdentity(ref.native) === managedIdentity(attempt.native)
1595
+ );
1596
+ }
1597
+ export type ManagedNativeObservation = "no-effect" | "authorized" | "unknown" | "closed";
1598
+
1599
+ export function managedAttemptRefFromState(
1600
+ state: ManagedTaskDomain,
1601
+ attempt: ManagedTaskAttempt,
1602
+ graphId: string,
1603
+ ): ManagedAttemptRef {
1604
+ requirePolicy(
1605
+ state.graphs.some(graph => graph.id === graphId && graph.attempts.some(item => item.id === attempt.id)),
1606
+ "graph missing",
1607
+ );
1608
+ return {
1609
+ controlRoot: state.binding.controlRoot,
1610
+ enrollmentId: state.binding.enrollmentId,
1611
+ agentDirIdentity: state.binding.agentDirIdentity,
1612
+ graphId,
1613
+ graphRevision: attempt.graphRevision,
1614
+ nodeId: attempt.nodeId,
1615
+ taskRevision: attempt.taskRevision,
1616
+ attemptId: attempt.id,
1617
+ definitionHash: attempt.definitionHash,
1618
+ criteriaIdentity: attempt.definition.criteriaIdentity,
1619
+ inputIdentity: managedIdentity(attempt.inputs),
1620
+ predecessorIdentity: managedIdentity(attempt.predecessors),
1621
+ resourceIdentity: managedIdentity(attempt.resources),
1622
+ native: structuredClone(attempt.native),
1623
+ nativeIdentity: attempt.native.identity,
1624
+ };
1625
+ }
1626
+
1627
+ export async function loadManagedEnrollmentIndex(agentDir: string): Promise<string[]> {
1628
+ return (await loadManagedEnrollmentRecord(agentDir)).controlRoots;
1629
+ }
1630
+
1631
+ export async function loadManagedEnrollmentRecord(agentDir: string): Promise<ManagedEnrollmentRecord> {
1632
+ const agent = await fs.realpath(agentDir);
1633
+ const target = managedEnrollmentIndexPath(agent);
1634
+ // Managed enrollment can only be published on Linux (private durable publication). Elsewhere
1635
+ // an absent index is the only reachable state; do not let the Linux-only lock turn it into a
1636
+ // startup failure for brokers that never used task.dag. Only a proven ENOENT beneath real
1637
+ // directories is absent: any symlink, non-directory ancestor, or non-file leaf fails closed below.
1638
+ if (process.platform !== "linux" && (await isAbsentBeneathRealDirectories(agent, target)))
1639
+ return { controlRoots: [], establishedRoots: [], publishingRoots: [], nativeIdentities: [], byRoot: {} };
1640
+ try {
1641
+ return await withWorkflowStateLock(target, () => loadEnrollmentIndexUnderLock(target), {
1642
+ cwd: agent,
1643
+ privateDurable: { directory: path.dirname(target) },
1644
+ });
1645
+ } catch (error) {
1646
+ if ((error as NodeJS.ErrnoException).code === "ENOENT")
1647
+ return { controlRoots: [], establishedRoots: [], publishingRoots: [], nativeIdentities: [], byRoot: {} };
1648
+ if (error instanceof Error && error.message === "corrupt managed enrollment index") throw error;
1649
+ throw new Error("corrupt managed enrollment index");
1650
+ }
1651
+ }
1652
+
1653
+ /**
1654
+ * True only when some component of `target` below `root` is missing and every component before it
1655
+ * is a real directory (not a symlink). lstat never follows the component it inspects, so walking
1656
+ * one component at a time keeps a symlinked ancestor from masking a present or corrupt namespace.
1657
+ */
1658
+ async function isAbsentBeneathRealDirectories(root: string, target: string): Promise<boolean> {
1659
+ let current = root;
1660
+ for (const part of path.relative(root, target).split(path.sep)) {
1661
+ current = path.join(current, part);
1662
+ try {
1663
+ const stat = await fs.lstat(current);
1664
+ if (current === target || !stat.isDirectory()) return false;
1665
+ } catch (error) {
1666
+ return (error as NodeJS.ErrnoException).code === "ENOENT";
1667
+ }
1668
+ }
1669
+ return false;
1670
+ }
1671
+
1672
+ export async function recordManagedEnrollment(
1673
+ agentDir: string,
1674
+ controlRoot: string,
1675
+ nativeIdentity?: string,
1676
+ ): Promise<void> {
1677
+ requirePolicy(isCanonicalAbsolute(controlRoot), "control root is not canonical");
1678
+ if (nativeIdentity !== undefined) digest.parse(nativeIdentity);
1679
+ const agent = await fs.realpath(agentDir);
1680
+ const target = managedEnrollmentIndexPath(agent);
1681
+ const writer = { cwd: agent, privateDurable: { directory: path.dirname(target) } };
1682
+ await withWorkflowStateLock(
1683
+ target,
1684
+ async () => {
1685
+ const record = await loadEnrollmentIndexUnderLock(target);
1686
+ const roots = new Set(record.controlRoots);
1687
+ const byRoot = { ...record.byRoot };
1688
+ const rootNatives = new Set(byRoot[controlRoot] ?? []);
1689
+ const already = roots.has(controlRoot) && (nativeIdentity === undefined || rootNatives.has(nativeIdentity));
1690
+ if (already) return;
1691
+ if (nativeIdentity !== undefined && record.nativeIdentities.includes(nativeIdentity))
1692
+ throw new Error("managed native identity is already enrolled under another control root");
1693
+ roots.add(controlRoot);
1694
+ if (nativeIdentity) rootNatives.add(nativeIdentity);
1695
+ byRoot[controlRoot] = [...rootNatives];
1696
+ const expectedRevision = await persistedEnrollmentRevision(target);
1697
+ const written = await writeGuardedJsonAtomic(
1698
+ target,
1699
+ enrollmentIndexDocument({
1700
+ controlRoots: [...roots],
1701
+ establishedRoots: record.establishedRoots,
1702
+ publishingRoots: record.publishingRoots,
1703
+ nativeIdentities: record.nativeIdentities,
1704
+ byRoot,
1705
+ }),
1706
+ { ...writer, policy: "source", expectedRevision, lockHeld: true },
1707
+ );
1708
+ requirePolicy(written.written, "source publication skipped");
1709
+ },
1710
+ writer,
1711
+ );
1712
+ }
1713
+
1714
+ async function updateManagedEnrollmentPublication(
1715
+ agentDir: string,
1716
+ controlRoot: string,
1717
+ publication: "pending" | "publishing" | "established",
1718
+ ): Promise<void> {
1719
+ requirePolicy(isCanonicalAbsolute(controlRoot), "control root is not canonical");
1720
+ const agent = await fs.realpath(agentDir);
1721
+ const target = managedEnrollmentIndexPath(agent);
1722
+ const writer = { cwd: agent, privateDurable: { directory: path.dirname(target) } };
1723
+ await withWorkflowStateLock(
1724
+ target,
1725
+ async () => {
1726
+ const record = await loadEnrollmentIndexUnderLock(target);
1727
+ const establishedRoots = new Set(record.establishedRoots);
1728
+ const publishingRoots = new Set(record.publishingRoots);
1729
+ if (publication === "established") {
1730
+ establishedRoots.add(controlRoot);
1731
+ publishingRoots.delete(controlRoot);
1732
+ } else if (publication === "publishing") {
1733
+ if (establishedRoots.has(controlRoot)) return;
1734
+ publishingRoots.add(controlRoot);
1735
+ } else {
1736
+ if (establishedRoots.has(controlRoot)) return;
1737
+ publishingRoots.delete(controlRoot);
1738
+ }
1739
+ const controlRoots = [...new Set([...record.controlRoots, controlRoot])];
1740
+ const byRoot = { ...record.byRoot, [controlRoot]: record.byRoot[controlRoot] ?? [] };
1741
+ if (
1742
+ controlRoots.length === record.controlRoots.length &&
1743
+ establishedRoots.size === record.establishedRoots.length &&
1744
+ publishingRoots.size === record.publishingRoots.length
1745
+ )
1746
+ return;
1747
+ const expectedRevision = await persistedEnrollmentRevision(target);
1748
+ const written = await writeGuardedJsonAtomic(
1749
+ target,
1750
+ enrollmentIndexDocument({
1751
+ controlRoots,
1752
+ establishedRoots: [...establishedRoots],
1753
+ publishingRoots: [...publishingRoots],
1754
+ nativeIdentities: record.nativeIdentities,
1755
+ byRoot,
1756
+ }),
1757
+ { ...writer, policy: "source", expectedRevision, lockHeld: true },
1758
+ );
1759
+ requirePolicy(written.written, "source publication skipped");
1760
+ },
1761
+ writer,
1762
+ );
1763
+ }
1764
+
1765
+ /** Mark a first domain publication in progress before its state rename. */
1766
+ export async function markManagedEnrollmentPublishing(agentDir: string, controlRoot: string): Promise<void> {
1767
+ await updateManagedEnrollmentPublication(agentDir, controlRoot, "publishing");
1768
+ }
1769
+
1770
+ /** A valid domain snapshot is authoritative even if the marker update was interrupted. */
1771
+ export async function markManagedEnrollmentEstablished(agentDir: string, controlRoot: string): Promise<void> {
1772
+ await updateManagedEnrollmentPublication(agentDir, controlRoot, "established");
1773
+ }
1774
+
1775
+ /** Only a proven pre-rename state-write failure may return publication to pending. */
1776
+ export async function markManagedEnrollmentPending(agentDir: string, controlRoot: string): Promise<void> {
1777
+ await updateManagedEnrollmentPublication(agentDir, controlRoot, "pending");
1778
+ }
1779
+
1780
+ /**
1781
+ * Remove a root that was pre-published for a first enrollment but never got a
1782
+ * domain state. Native identities make the root non-recoverable, so callers
1783
+ * must leave those entries indexed and report them as failed instead.
1784
+ */
1785
+ async function removeEmptyManagedEnrollment(agentDir: string, controlRoot: string): Promise<boolean> {
1786
+ requirePolicy(isCanonicalAbsolute(controlRoot), "control root is not canonical");
1787
+ const agent = await fs.realpath(agentDir);
1788
+ const domainTarget = managedTaskDomainPath(controlRoot);
1789
+ return withWorkflowStateLock(
1790
+ domainTarget,
1791
+ async () => {
1792
+ const domain = await readExistingStateForMutation(domainTarget);
1793
+ if (domain.kind !== "absent") return false;
1794
+ const target = managedEnrollmentIndexPath(agent);
1795
+ const writer = { cwd: agent, privateDurable: { directory: path.dirname(target) } };
1796
+ return withWorkflowStateLock(
1797
+ target,
1798
+ async () => {
1799
+ const record = await loadEnrollmentIndexUnderLock(target);
1800
+ if (!record.controlRoots.includes(controlRoot)) return true;
1801
+ if (
1802
+ record.establishedRoots.includes(controlRoot) ||
1803
+ record.publishingRoots.includes(controlRoot) ||
1804
+ (record.byRoot[controlRoot] ?? []).length > 0
1805
+ )
1806
+ return false;
1807
+ const controlRoots = record.controlRoots.filter(root => root !== controlRoot);
1808
+ const byRoot = { ...record.byRoot };
1809
+ delete byRoot[controlRoot];
1810
+ const expectedRevision = await persistedEnrollmentRevision(target);
1811
+ const written = await writeGuardedJsonAtomic(
1812
+ target,
1813
+ enrollmentIndexDocument({
1814
+ controlRoots,
1815
+ establishedRoots: record.establishedRoots,
1816
+ publishingRoots: record.publishingRoots,
1817
+ nativeIdentities: record.nativeIdentities,
1818
+ byRoot,
1819
+ }),
1820
+ { ...writer, policy: "source", expectedRevision, lockHeld: true },
1821
+ );
1822
+ requirePolicy(written.written, "source publication skipped");
1823
+ return true;
1824
+ },
1825
+ writer,
1826
+ );
1827
+ },
1828
+ { cwd: controlRoot, privateDurable: { directory: path.dirname(domainTarget) } },
1829
+ );
1830
+ }
1831
+
1832
+ async function persistedEnrollmentRevision(target: string): Promise<number> {
1833
+ const read = await readExistingStateForMutation(target);
1834
+ if (read.kind === "absent") return 0;
1835
+ requirePolicy(read.kind === "valid", "corrupt managed enrollment index");
1836
+ const revision = (read.value as { state_revision?: unknown }).state_revision;
1837
+ return typeof revision === "number" && Number.isSafeInteger(revision) && revision >= 0 ? revision : 0;
1838
+ }
1839
+
1840
+ export async function currentManagedRevision(binding: ManagedDomainBinding): Promise<number> {
1841
+ const enrolled = ManagedDomainBindingSchema.parse(binding);
1842
+ await validateBinding(enrolled);
1843
+ const target = managedTaskDomainPath(enrolled.controlRoot);
1844
+ return withWorkflowStateLock(
1845
+ target,
1846
+ async () => {
1847
+ const read = await readExistingStateForMutation(target);
1848
+ requirePolicy(read.kind !== "corrupt", "corrupt authority");
1849
+ requirePolicy(read.kind === "valid", "established authority missing");
1850
+ return validateManagedTaskDomain(read.value).state_revision;
1851
+ },
1852
+ { cwd: enrolled.controlRoot, privateDurable: { directory: path.dirname(target) } },
1853
+ );
1854
+ }
1855
+
1856
+ export async function recordManagedNativeObservation(
1857
+ binding: ManagedDomainBinding,
1858
+ nativeIdentity: string,
1859
+ worker: ManagedNativeObservation,
1860
+ authorize?: { fence: ManagedNativeEffectFence; expectedVector: string },
1861
+ ): Promise<void> {
1862
+ await transactManagedTaskDomain({ binding, internal: true }, async state => {
1863
+ const attempt = lookupManagedAttemptByNativeIdentity(state, nativeIdentity);
1864
+ requirePolicy(attempt, "managed native identity is not reserved");
1865
+ if (authorize) {
1866
+ requirePolicy(attempt.fence === "current" && !attempt.retired, "managed attempt is fenced");
1867
+ requirePolicy(managedNativeVector(attempt) === authorize.expectedVector, "managed native vector changed");
1868
+ if (authorize.fence === "seed") requirePolicy(attempt.worker !== "no-effect", "canceled attempt cannot seed");
1869
+ }
1870
+ if (worker === "authorized")
1871
+ requirePolicy(attempt.worker === "reserved" || attempt.worker === "authorized", "illegal worker observation");
1872
+ if (worker === "no-effect")
1873
+ requirePolicy(
1874
+ attempt.worker === "reserved" || attempt.worker === "no-effect",
1875
+ "illegal no-effect observation",
1876
+ );
1877
+ if (worker === "unknown") requirePolicy(attempt.worker !== "closed", "illegal unknown observation");
1878
+ if (worker === "closed") requirePolicy(attempt.worker !== "no-effect", "illegal close observation");
1879
+ attempt.worker = worker;
1880
+ });
1881
+ }
1882
+
1883
+ export type RestoredManagedAttemptRefs = {
1884
+ refs: ManagedAttemptRef[];
1885
+ failedRoots: string[];
1886
+ };
1887
+
1888
+ export async function restoreManagedAttemptRefs(agentDir: string): Promise<RestoredManagedAttemptRefs> {
1889
+ const identity = await objectIdentity(await fs.realpath(agentDir));
1890
+ const refs: ManagedAttemptRef[] = [];
1891
+ const failedRoots: string[] = [];
1892
+ const enrollment = await loadManagedEnrollmentRecord(agentDir);
1893
+ for (const controlRoot of enrollment.controlRoots) {
1894
+ try {
1895
+ const read = await readExistingStateForMutation(managedTaskDomainPath(controlRoot));
1896
+ if (read.kind !== "valid") {
1897
+ if (read.kind === "absent" && (enrollment.byRoot[controlRoot] ?? []).length === 0) {
1898
+ if (await removeEmptyManagedEnrollment(agentDir, controlRoot)) continue;
1899
+ }
1900
+ failedRoots.push(controlRoot);
1901
+ continue;
1902
+ }
1903
+ const state = validateManagedTaskDomain(read.value);
1904
+ if (state.binding.agentDirIdentity !== identity || state.binding.controlRoot !== controlRoot) {
1905
+ failedRoots.push(controlRoot);
1906
+ continue;
1907
+ }
1908
+ await markManagedEnrollmentEstablished(agentDir, controlRoot);
1909
+ for (const graph of state.graphs)
1910
+ for (const attempt of graph.attempts)
1911
+ if (!attempt.retired) refs.push(managedAttemptRefFromState(state, attempt, graph.id));
1912
+ } catch {
1913
+ failedRoots.push(controlRoot);
1914
+ }
1915
+ }
1916
+ return { refs, failedRoots };
1917
+ }
1918
+
1919
+ export async function loadManagedDomainBinding(controlRoot: string): Promise<ManagedDomainBinding | undefined> {
1920
+ try {
1921
+ const read = await readExistingStateForMutation(managedTaskDomainPath(controlRoot));
1922
+ if (read.kind !== "valid") return undefined;
1923
+ return validateManagedTaskDomain(read.value).binding;
1924
+ } catch {
1925
+ return undefined;
1926
+ }
1927
+ }