@brainervirus/workit-core 5.0.0 → 6.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.
@@ -1,1204 +0,0 @@
1
- import {
2
- canonicalJson,
3
- failure,
4
- success,
5
- type Id,
6
- type Ref,
7
- type Result,
8
- type Revision,
9
- type Decision,
10
- type Entry,
11
- type TaskRecord,
12
- refSchema,
13
- sha256,
14
- } from "./task-contract";
15
- import { WorkitCore } from "./task-engine";
16
- import { sameDirectoryIdentity, TaskStore } from "./task-store";
17
- import {
18
- chainStepKey,
19
- normalizeChainSteps,
20
- type ActionReservation,
21
- type ChainStep,
22
- } from "./authority";
23
- import * as z from "zod";
24
- import { execFileSync } from "node:child_process";
25
- import { realpathSync } from "node:fs";
26
- import path from "node:path";
27
-
28
- const chainStepSchema = z.union([
29
- z.string().min(1),
30
- z.object({ branch: z.string().min(1) }).strict(),
31
- z.object({ pr: z.literal(true) }).strict(),
32
- ]);
33
-
34
- export type AuthorizedActionInput = {
35
- core: WorkitCore;
36
- taskId: Id;
37
- decisionId: Id;
38
- actionRef: Ref;
39
- expectedRevision: Revision;
40
- expectedWorkspaceRevision: Revision;
41
- binding?: Decision["binding"];
42
- reserveObservation: unknown;
43
- settleObservation: (
44
- outcome: "succeeded" | "not_started" | "unknown",
45
- revisions?: { taskRevision: Revision; workspaceRevision: Revision },
46
- ) => unknown;
47
- refresh?: () => Pick<AuthorizedActionInput, "expectedRevision" | "expectedWorkspaceRevision">;
48
- step?: string;
49
- capability?: string;
50
- available?: boolean;
51
- failureOutcome?: "not_started" | "unknown";
52
- };
53
-
54
- export type ExternalActionResult<T> = Result<T>;
55
- type WithExternalActionLock = <T>(
56
- operation: () => Promise<ExternalActionResult<T>>,
57
- ) => Promise<ExternalActionResult<T>>;
58
-
59
- export const externalActionSchema = z.discriminatedUnion("operation", [
60
- z
61
- .object({
62
- operation: z.literal("git.branch_setup"),
63
- payload: z
64
- .object({
65
- action: z.enum(["setup", "reapply_stash"]).optional(),
66
- sdd_dir: z.string().optional(),
67
- target_branch: z.string().optional(),
68
- stash: z.enum(["yes", "no"]).optional(),
69
- cwd: z.string().min(1).optional(),
70
- })
71
- .strict(),
72
- })
73
- .strict(),
74
- z
75
- .object({
76
- operation: z.literal("git.commit"),
77
- payload: z
78
- .object({
79
- message: z.string().min(1).optional(),
80
- plan_steps: z.array(chainStepSchema).min(1).max(32).optional(),
81
- plan_branch: z.string().min(1).optional(),
82
- cwd: z.string().min(1).optional(),
83
- })
84
- .strict(),
85
- })
86
- .strict(),
87
- z
88
- .object({
89
- operation: z.literal("git.push"),
90
- payload: z
91
- .object({ branch: z.string().min(1).optional(), cwd: z.string().min(1).optional() })
92
- .strict(),
93
- })
94
- .strict(),
95
- z
96
- .object({
97
- operation: z.literal("hosting.pull_request"),
98
- payload: z
99
- .object({
100
- title: z.string().min(1),
101
- body: z.string().optional(),
102
- draft: z.boolean().optional(),
103
- target_branch: z.string().optional(),
104
- babysit: z.boolean().optional(),
105
- cwd: z.string().min(1).optional(),
106
- })
107
- .strict(),
108
- })
109
- .strict(),
110
- z
111
- .object({
112
- operation: z.literal("hosting.merge"),
113
- payload: z
114
- .object({
115
- target_branch: z.string().min(1).optional(),
116
- source_branch: z.string().min(1).optional(),
117
- cwd: z.string().min(1).optional(),
118
- })
119
- .strict(),
120
- })
121
- .strict(),
122
- z
123
- .object({
124
- operation: z.literal("hosting.delete_branch"),
125
- payload: z.object({ branch: z.string().min(1), cwd: z.string().min(1).optional() }).strict(),
126
- })
127
- .strict(),
128
- z
129
- .object({
130
- operation: z.literal("youtrack.update"),
131
- payload: z
132
- .object({
133
- issueId: z.string().min(1),
134
- markdown: z.string().min(1),
135
- minutes: z.number().positive().optional(),
136
- })
137
- .strict(),
138
- })
139
- .strict(),
140
- z
141
- .object({
142
- operation: z.literal("youtrack.time"),
143
- payload: z
144
- .object({
145
- issueId: z.string().min(1),
146
- minutes: z.number().positive(),
147
- text: z.string().optional(),
148
- dateMs: z.number().finite().optional(),
149
- })
150
- .strict(),
151
- })
152
- .strict(),
153
- z
154
- .object({
155
- operation: z.literal("youtrack.meeting"),
156
- payload: z
157
- .object({
158
- issueId: z.string().min(1),
159
- minutes: z.number().positive(),
160
- text: z.string().min(1),
161
- })
162
- .strict(),
163
- })
164
- .strict(),
165
- z
166
- .object({
167
- operation: z.literal("changelog.apply"),
168
- payload: z
169
- .object({
170
- entries: z
171
- .union([
172
- z.record(z.string(), z.array(z.string())),
173
- z.array(z.object({ category: z.string().min(1), text: z.string().min(1) }).strict()),
174
- ])
175
- .optional(),
176
- path: z.string().optional(),
177
- normalize_only: z.boolean().optional(),
178
- })
179
- .strict(),
180
- })
181
- .strict(),
182
- z
183
- .object({
184
- operation: z.literal("context.read"),
185
- payload: z
186
- .object({
187
- kind: z.enum([
188
- "git",
189
- "pr",
190
- "youtrack",
191
- "github_issue",
192
- "gitlab_issue",
193
- "changelog",
194
- "release",
195
- "affected",
196
- ]),
197
- range: z.string().optional(),
198
- issueId: z.string().optional(),
199
- issueUrl: z.string().optional(),
200
- issueRef: z.string().optional(),
201
- mode: z.string().optional(),
202
- specPath: z.string().optional(),
203
- planPath: z.string().optional(),
204
- cwd: z.string().min(1).optional(),
205
- })
206
- .strict(),
207
- })
208
- .strict(),
209
- ]);
210
- export type ExternalActionRequest = z.infer<typeof externalActionSchema>;
211
-
212
- /** The complete external-action contract for hosts that register native tools. */
213
- export const externalActionJsonSchema = (): z.core.JSONSchema.BaseSchema => ({
214
- type: "object",
215
- ...z.toJSONSchema(externalActionSchema, { target: "draft-2020-12" }),
216
- });
217
-
218
- export type ExternalActionOperation = ExternalActionRequest["operation"];
219
-
220
- export const externalActionRequest = (value: unknown): Result<ExternalActionRequest> => {
221
- const parsed = externalActionSchema.safeParse(value);
222
- return parsed.success
223
- ? success(null, null, parsed.data)
224
- : failure("invalid_input", "external action payload is invalid", {
225
- fields: parsed.error.issues.map((issue) => ({
226
- path: issue.path.join("."),
227
- reason: issue.message,
228
- })),
229
- });
230
- };
231
- export type ExternalActionRunner = <T>(
232
- input: Omit<AuthorizedActionInput, "core">,
233
- effect: (step?: string) => T | Result<T> | Promise<T | Result<T>>,
234
- ) => Promise<ExternalActionResult<T>>;
235
- export type ExternalActionEffect = <T>(
236
- operation: string,
237
- effect: (
238
- step?: string,
239
- reservation?: ActionReservation,
240
- ) => T | Result<T> | Promise<T | Result<T>>,
241
- ) => Promise<ExternalActionResult<T>>;
242
- export type ExternalActionBinding = (
243
- operation: string,
244
- ) => Omit<AuthorizedActionInput, "core"> | ExternalActionResult<never>;
245
-
246
- /**
247
- * Host adapters use this exact, canonical string as the native approval
248
- * binding. It deliberately includes the concrete target/payload so an
249
- * approved sentence cannot authorize a different effect by substring.
250
- */
251
- export const externalActionDescriptor = (operation: string, payload: unknown): string =>
252
- canonicalJson({
253
- operation,
254
- payload,
255
- ...(payload &&
256
- typeof payload === "object" &&
257
- !Array.isArray(payload) &&
258
- "resolved" in payload &&
259
- payload.resolved &&
260
- typeof payload.resolved === "object" &&
261
- Array.isArray((payload.resolved as { steps?: unknown }).steps)
262
- ? { steps: (payload.resolved as { steps: string[] }).steps }
263
- : {}),
264
- });
265
-
266
- /**
267
- * Plan reservations never execute a git commit. A git.commit whose resolved
268
- * payload carries a plan_steps array records the listed chain once and lets
269
- * each listed single commit execute later under that authorization. Adapters
270
- * must return plan success without running the commit effect, both for prior
271
- * approvals and standing auto-approval; otherwise the reservation fails as a
272
- * single commit requiring a message. Returns the listed count, else null.
273
- */
274
- export const planReservationLength = (
275
- operation: string,
276
- descriptorPayload: unknown,
277
- ): number | null => {
278
- if (operation !== "git.commit") return null;
279
- const payload = (descriptorPayload ?? {}) as { plan_steps?: unknown };
280
- return Array.isArray(payload.plan_steps) ? payload.plan_steps.length : null;
281
- };
282
-
283
- /** Compact host-facing help for the fixed optional-action surface. */
284
- export const externalActionHelp =
285
- "Fixed actions: Git/hosting operations accept cwd? for an action-time target repository: git.branch_setup {target_branch,action?,sdd_dir?,stash?,cwd?}; git.commit {message?,plan_steps?,plan_branch?,cwd?}; git.push {branch?,cwd?}; hosting.pull_request {title,body?,draft?,target_branch?,babysit?,cwd?} creates only after pre-verifying the approved source SHA and post-verifies the provider PR head before reporting success (the residual non-atomic source-SHA race is accepted); babysit:true opts into PR-ready follow-up and does not authorize merge or release; omission means no babysitting; hosting.merge {target_branch?,source_branch?,cwd?}; hosting.delete_branch {branch,cwd?} deletes only an exact merged PR head. YouTrack: update {issueId,markdown,minutes?}, time {issueId,minutes,text?,dateMs?}, meeting {issueId,minutes,text}. changelog.apply {entries?,path?,normalize_only?}; context.read {kind,range?,cwd?,issueId?,issueUrl?,issueRef?}. context.read is read-only; all other actions require host-native approval (CLI needs TTY; caller-unattested MCP cannot mutate).";
286
-
287
- export const externalActionRef = (
288
- host: "opencode" | "pi" | "workit_cli",
289
- actor: string,
290
- descriptor: string,
291
- ): Ref => ({
292
- kind: "host",
293
- host,
294
- handle: `external:${sha256({ actor, descriptor }).slice(0, 32)}`,
295
- });
296
-
297
- export const externalActionState = (
298
- store: TaskStore,
299
- host: string,
300
- actor: string,
301
- operation: string,
302
- ) => {
303
- const listed = store.listTasks();
304
- const workspace = store.readWorkspace();
305
- if (!listed.ok || !workspace.ok)
306
- return failure("storage_error", "external action state is unavailable");
307
- if (!workspace.data) return failure("not_found", "workspace not found");
308
- const candidates = listed.data.flatMap((task) => {
309
- if (task.status !== "active" || task.workspaceId !== workspace.data!.id) return [];
310
- return task.decisions
311
- .filter(
312
- (entry) =>
313
- entry.data.purpose === "action" &&
314
- entry.data.response === "approved" &&
315
- entry.data.revoked === null &&
316
- entry.data.binding.approvedContent === operation,
317
- )
318
- .map((entry) => ({ task, entry }));
319
- });
320
- if (candidates.length !== 1)
321
- return failure(
322
- "permission_denied",
323
- candidates.length === 0
324
- ? "no approved action is bound to the active task"
325
- : "multiple approved actions match the active task",
326
- );
327
- return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
328
- };
329
-
330
- /** Find a prior decision for the same user-requested operation/payload even when its resolved target drifted. */
331
- export const priorExternalAction = (
332
- store: TaskStore,
333
- host: string,
334
- actor: string,
335
- operation: string,
336
- payload: unknown,
337
- ) => {
338
- const listed = store.listTasks();
339
- const workspace = store.readWorkspace();
340
- if (!listed.ok || !workspace.ok)
341
- return failure("storage_error", "external action state is unavailable");
342
- if (!workspace.data) return failure("not_found", "workspace not found");
343
- const candidates = listed.data.flatMap((task) => {
344
- if (task.status !== "active" || task.workspaceId !== workspace.data!.id) return [];
345
- return task.decisions
346
- .filter((entry) => {
347
- if (
348
- entry.data.purpose !== "action" ||
349
- entry.data.response !== "approved" ||
350
- entry.data.revoked !== null
351
- )
352
- return false;
353
- try {
354
- const descriptor = JSON.parse(entry.data.binding.approvedContent) as {
355
- operation?: unknown;
356
- payload?: Record<string, unknown>;
357
- };
358
- if (
359
- descriptor.operation !== operation ||
360
- !descriptor.payload ||
361
- typeof descriptor.payload !== "object"
362
- )
363
- return false;
364
- const storedPayload = { ...descriptor.payload };
365
- delete storedPayload.resolved;
366
- return canonicalJson(storedPayload) === canonicalJson(payload);
367
- } catch {
368
- return false;
369
- }
370
- })
371
- .map((entry) => ({ task, entry }));
372
- });
373
- if (candidates.length !== 1)
374
- return failure(
375
- "permission_denied",
376
- candidates.length === 0
377
- ? "no prior external action matches this request"
378
- : "multiple prior external actions match this request",
379
- );
380
- return success(workspace.data.revision, null, { ...candidates[0], workspace: workspace.data });
381
- };
382
-
383
- /** Parse a plan-commit authorization descriptor (one approved list per plan). */
384
- export const planCommitDescriptor = (
385
- content: string,
386
- ): { steps: ChainStep[]; branch: string; snapshotHead: string | null; cwd?: string } | null => {
387
- try {
388
- const value = JSON.parse(content) as {
389
- operation?: unknown;
390
- payload?: {
391
- plan_steps?: unknown;
392
- plan_branch?: unknown;
393
- cwd?: unknown;
394
- resolved?: { head?: unknown; branch?: unknown };
395
- };
396
- };
397
- if (value.operation !== "git.commit" || !value.payload) return null;
398
- const steps = normalizeChainSteps(value.payload.plan_steps);
399
- const branch = value.payload.plan_branch;
400
- if (!steps || typeof branch !== "string" || !branch) return null;
401
- const cwd = value.payload.cwd;
402
- if (cwd !== undefined && (typeof cwd !== "string" || !path.isAbsolute(cwd))) return null;
403
- const head = value.payload.resolved?.head;
404
- return {
405
- steps,
406
- branch,
407
- snapshotHead: typeof head === "string" ? head : null,
408
- ...(typeof cwd === "string" ? { cwd } : {}),
409
- };
410
- } catch {
411
- return null;
412
- }
413
- };
414
-
415
- /**
416
- * Chain lease: the recorded head must still be an ancestor of the current
417
- * HEAD — history only moved forward. Rebase/reset underneath an approved
418
- * chain invalidates it instead of executing on rewritten history.
419
- */
420
- export const chainLeaseValid = (
421
- store: TaskStore,
422
- snapshotHead: string | null,
423
- targetRoot = store.root,
424
- ): boolean => {
425
- if (!snapshotHead) return true;
426
- try {
427
- const head = execFileSync("git", ["rev-parse", "HEAD"], {
428
- cwd: targetRoot,
429
- encoding: "utf8",
430
- stdio: ["pipe", "pipe", "pipe"],
431
- }).trim();
432
- if (head === snapshotHead) return true;
433
- execFileSync("git", ["merge-base", "--is-ancestor", snapshotHead, head], {
434
- cwd: targetRoot,
435
- encoding: "utf8",
436
- stdio: ["pipe", "pipe", "pipe"],
437
- });
438
- return true;
439
- } catch {
440
- return false;
441
- }
442
- };
443
-
444
- export type PlanCommitAuthorization = {
445
- task: TaskRecord;
446
- entry: Entry<Decision>;
447
- steps: ChainStep[];
448
- branch: string;
449
- snapshotHead: string | null;
450
- completed: string[];
451
- };
452
-
453
- /**
454
- * Find the single active plan-commit authorization whose next unconsumed step
455
- * is this exact commit message. Consumption order is enforced so each listed
456
- * commit executes once, in plan order; unlisted or replayed messages find no
457
- * authorization and require a fresh exact approval. Typed chain positions
458
- * never match messages (a pathological message falls through to a fresh
459
- * approval instead of consuming the wrong step), and a rewritten history
460
- * breaks the lease.
461
- */
462
- export const approvedPlanCommit = (
463
- store: TaskStore,
464
- host: string,
465
- actor: string,
466
- message: string,
467
- targetRoot = store.root,
468
- ): Result<PlanCommitAuthorization> => {
469
- if (typeof message !== "string" || !message)
470
- return failure("invalid_input", "commit message is required");
471
- const listed = store.listTasks();
472
- const workspace = store.readWorkspace();
473
- if (!listed.ok || !workspace.ok)
474
- return failure("storage_error", "external action state is unavailable");
475
- if (!workspace.data) return failure("not_found", "workspace not found");
476
- const matches = listed.data.flatMap((task) => {
477
- if (task.status !== "active" || task.workspaceId !== workspace.data!.id) return [];
478
- return task.decisions.flatMap((entry) => {
479
- const decision = entry.data;
480
- if (
481
- decision.purpose !== "action" ||
482
- decision.response !== "approved" ||
483
- decision.revoked !== null
484
- )
485
- return [];
486
- if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
487
- return [];
488
- const plan = planCommitDescriptor(decision.binding.approvedContent);
489
- if (!plan) return [];
490
- if (!sameDirectoryIdentity(plan.cwd ?? store.root, targetRoot)) return [];
491
- const completed =
492
- task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ??
493
- [];
494
- const next = plan.steps[completed.length];
495
- if (!next || next.kind !== "commit" || next.message !== message) return [];
496
- if (!chainLeaseValid(store, plan.snapshotHead, targetRoot)) return [];
497
- return [
498
- {
499
- task,
500
- entry,
501
- steps: plan.steps,
502
- branch: plan.branch,
503
- snapshotHead: plan.snapshotHead,
504
- completed: [...completed],
505
- },
506
- ];
507
- });
508
- });
509
- if (matches.length !== 1)
510
- return failure(
511
- "permission_denied",
512
- matches.length === 0
513
- ? "no plan commit authorization matches this message"
514
- : "multiple plan authorizations match this message",
515
- );
516
- return success(workspace.data.revision, null, matches[0]);
517
- };
518
-
519
- export type ChainStepQuery =
520
- | { operation: "git.branch_setup"; target: string; cwd?: string }
521
- | { operation: "hosting.pull_request"; cwd?: string };
522
-
523
- /** Match the next branch or PR step of an active chain authorization. */
524
- export const approvedChainStep = (
525
- store: TaskStore,
526
- host: string,
527
- actor: string,
528
- query: ChainStepQuery,
529
- ): Result<PlanCommitAuthorization> => {
530
- const listed = store.listTasks();
531
- const workspace = store.readWorkspace();
532
- if (!listed.ok || !workspace.ok)
533
- return failure("storage_error", "external action state is unavailable");
534
- if (!workspace.data) return failure("not_found", "workspace not found");
535
- const matches = listed.data.flatMap((task) => {
536
- if (task.status !== "active" || task.workspaceId !== workspace.data!.id) return [];
537
- return task.decisions.flatMap((entry) => {
538
- const decision = entry.data;
539
- if (
540
- decision.purpose !== "action" ||
541
- decision.response !== "approved" ||
542
- decision.revoked !== null
543
- )
544
- return [];
545
- if (entry.provenance.kind !== "host_observed" || entry.provenance.receipts.length === 0)
546
- return [];
547
- const plan = planCommitDescriptor(decision.binding.approvedContent);
548
- if (!plan) return [];
549
- const targetRoot = query.cwd ?? store.root;
550
- if (!sameDirectoryIdentity(plan.cwd ?? store.root, targetRoot)) return [];
551
- const completed =
552
- task.actionProgress?.find((progress) => progress.decisionId === entry.id)?.completedSteps ??
553
- [];
554
- const next = plan.steps[completed.length];
555
- if (!next) return [];
556
- if (query.operation === "git.branch_setup") {
557
- if (next.kind !== "branch" || next.target !== query.target) return [];
558
- } else if (next.kind !== "pr") {
559
- return [];
560
- }
561
- if (!chainLeaseValid(store, plan.snapshotHead, targetRoot)) return [];
562
- return [
563
- {
564
- task,
565
- entry,
566
- steps: plan.steps,
567
- branch: plan.branch,
568
- snapshotHead: plan.snapshotHead,
569
- completed: [...completed],
570
- },
571
- ];
572
- });
573
- });
574
- if (matches.length !== 1)
575
- return failure(
576
- "permission_denied",
577
- matches.length === 0
578
- ? "no chain authorization matches this step"
579
- : "multiple chain authorizations match this step",
580
- );
581
- return success(workspace.data.revision, null, matches[0]);
582
- };
583
-
584
- export const commitMessageFromDescriptor = (operation: string): string | undefined => {
585
- try {
586
- const descriptor = JSON.parse(operation) as {
587
- operation?: unknown;
588
- payload?: { message?: unknown };
589
- };
590
- return descriptor.operation === "git.commit" && typeof descriptor.payload?.message === "string"
591
- ? descriptor.payload.message
592
- : undefined;
593
- } catch {
594
- return undefined;
595
- }
596
- };
597
-
598
- const descriptorTargetRoot = (store: TaskStore, operation: string): string | null => {
599
- try {
600
- const descriptor = JSON.parse(operation) as { payload?: { cwd?: unknown } };
601
- const cwd = descriptor.payload?.cwd;
602
- if (cwd === undefined) return store.root;
603
- return typeof cwd === "string" && path.isAbsolute(cwd) ? realpathSync(cwd) : null;
604
- } catch {
605
- return null;
606
- }
607
- };
608
-
609
- /** Resolve a chain authorization into the exact binding an adapter needs
610
- * to run one branch or PR step under the approved chain. Commit steps stay
611
- * on the plan path; this covers the steps plans could never express. */
612
- export const chainStepBinding = (
613
- store: TaskStore,
614
- host: string,
615
- actor: string,
616
- operation: string,
617
- ): {
618
- taskId: Id;
619
- decisionId: Id;
620
- binding: Decision["binding"];
621
- expectedRevision: Revision;
622
- expectedWorkspaceRevision: Revision;
623
- step: string;
624
- } | null => {
625
- let descriptor: { operation?: unknown; payload?: Record<string, unknown> };
626
- try {
627
- descriptor = JSON.parse(operation) as {
628
- operation?: unknown;
629
- payload?: Record<string, unknown>;
630
- };
631
- } catch {
632
- return null;
633
- }
634
- const targetRoot = descriptorTargetRoot(store, operation);
635
- if (!targetRoot) return null;
636
- const query: ChainStepQuery | null =
637
- descriptor.operation === "git.branch_setup" &&
638
- typeof descriptor.payload?.target_branch === "string"
639
- ? {
640
- operation: "git.branch_setup",
641
- target: descriptor.payload.target_branch,
642
- cwd: targetRoot,
643
- }
644
- : descriptor.operation === "hosting.pull_request"
645
- ? { operation: "hosting.pull_request", cwd: targetRoot }
646
- : null;
647
- if (!query) return null;
648
- const chain = approvedChainStep(store, host, actor, query);
649
- if (!chain.ok) return null;
650
- // PR steps run on the chain branch; the branch step itself creates it.
651
- if (query.operation === "hosting.pull_request") {
652
- let branch: string;
653
- try {
654
- branch = execFileSync("git", ["branch", "--show-current"], {
655
- cwd: targetRoot,
656
- encoding: "utf8",
657
- stdio: ["pipe", "pipe", "pipe"],
658
- }).trim();
659
- } catch {
660
- return null;
661
- }
662
- if (!branch || branch !== chain.data.branch) return null;
663
- }
664
- const workspace = store.readWorkspace();
665
- if (!workspace.ok || !workspace.data) return null;
666
- const next = chain.data.steps[chain.data.completed.length];
667
- if (!next) return null;
668
- return {
669
- taskId: chain.data.task.id,
670
- decisionId: chain.data.entry.id,
671
- binding: chain.data.entry.data.binding,
672
- expectedRevision: chain.data.task.revision,
673
- expectedWorkspaceRevision: workspace.data.revision,
674
- step: chainStepKey(next),
675
- };
676
- };
677
-
678
- /** Resolve a plan-commit authorization into the exact binding an adapter needs
679
- * to run one listed commit under the approved list. */
680
- export const planCommitBinding = (
681
- store: TaskStore,
682
- host: string,
683
- actor: string,
684
- operation: string,
685
- ): {
686
- taskId: Id;
687
- decisionId: Id;
688
- binding: Decision["binding"];
689
- expectedRevision: Revision;
690
- expectedWorkspaceRevision: Revision;
691
- step: string;
692
- } | null => {
693
- const message = commitMessageFromDescriptor(operation);
694
- if (!message) return null;
695
- const targetRoot = descriptorTargetRoot(store, operation);
696
- if (!targetRoot) return null;
697
- const plan = approvedPlanCommit(store, host, actor, message, targetRoot);
698
- if (!plan.ok) return null;
699
- let branch: string;
700
- try {
701
- branch = execFileSync("git", ["branch", "--show-current"], {
702
- cwd: targetRoot,
703
- encoding: "utf8",
704
- stdio: ["pipe", "pipe", "pipe"],
705
- }).trim();
706
- } catch {
707
- return null;
708
- }
709
- if (!branch || branch !== plan.data.branch) return null;
710
- const workspace = store.readWorkspace();
711
- if (!workspace.ok || !workspace.data) return null;
712
- return {
713
- taskId: plan.data.task.id,
714
- decisionId: plan.data.entry.id,
715
- binding: plan.data.entry.data.binding,
716
- expectedRevision: plan.data.task.revision,
717
- expectedWorkspaceRevision: workspace.data.revision,
718
- step: message,
719
- };
720
- };
721
-
722
- export const approvedExternalAction = (
723
- store: TaskStore,
724
- host: string,
725
- actor: string,
726
- operation: string,
727
- ) => {
728
- const selected = externalActionState(store, host, actor, operation);
729
- if (selected.ok) {
730
- if (selected.data.entry.data.consumption !== null)
731
- return failure(
732
- selected.data.entry.data.consumption.state === "uncertain"
733
- ? "external_outcome_unknown"
734
- : "permission_denied",
735
- selected.data.entry.data.consumption.state === "uncertain"
736
- ? "previous external action outcome is unknown"
737
- : "external action was already settled",
738
- );
739
- return selected;
740
- }
741
- // Intent carry for branch setup: an approval binds (target, base) intent,
742
- // so unrelated HEAD or dirt moves between approval and execution re-resolve
743
- // instead of demanding re-approval. Remote, existence, and target drift
744
- // still fail closed through priorResolvedDrift.
745
- const intent = approvedBranchSetupIntent(store, host, actor, operation);
746
- if (intent.ok) {
747
- if (intent.data.entry.data.consumption !== null)
748
- return failure(
749
- intent.data.entry.data.consumption.state === "uncertain"
750
- ? "external_outcome_unknown"
751
- : "permission_denied",
752
- intent.data.entry.data.consumption.state === "uncertain"
753
- ? "previous external action outcome is unknown"
754
- : "external action was already settled",
755
- );
756
- return intent;
757
- }
758
- return selected;
759
- };
760
-
761
- /** Intent identity for branch setup includes the exact remote and local base commits. */
762
- export const branchSetupIntent = (
763
- descriptor: string,
764
- ): {
765
- target: string;
766
- base: string;
767
- localBase: string | null;
768
- remoteBase: string | null;
769
- targetExists: boolean;
770
- cwd?: string;
771
- } | null => {
772
- try {
773
- const value = JSON.parse(descriptor) as {
774
- operation?: unknown;
775
- payload?: {
776
- target_branch?: unknown;
777
- cwd?: unknown;
778
- resolved?: {
779
- base_branch?: unknown;
780
- local_base?: unknown;
781
- remote_base?: unknown;
782
- target_exists?: unknown;
783
- };
784
- };
785
- };
786
- if (value.operation !== "git.branch_setup" || !value.payload) return null;
787
- const { target_branch, resolved } = value.payload;
788
- if (typeof target_branch !== "string" || !target_branch || !resolved) return null;
789
- if (
790
- value.payload.cwd !== undefined &&
791
- (typeof value.payload.cwd !== "string" || !path.isAbsolute(value.payload.cwd))
792
- )
793
- return null;
794
- if (typeof resolved.base_branch !== "string" || typeof resolved.target_exists !== "boolean")
795
- return null;
796
- return {
797
- target: target_branch,
798
- base: resolved.base_branch,
799
- localBase: typeof resolved.local_base === "string" ? resolved.local_base : null,
800
- remoteBase: typeof resolved.remote_base === "string" ? resolved.remote_base : null,
801
- targetExists: resolved.target_exists,
802
- ...(typeof value.payload.cwd === "string" ? { cwd: value.payload.cwd } : {}),
803
- };
804
- } catch {
805
- return null;
806
- }
807
- };
808
-
809
- /** Find the single unconsumed branch-setup approval with the same intent as
810
- * the current descriptor, regardless of head or dirt moves since approval. */
811
- export const approvedBranchSetupIntent = (
812
- store: TaskStore,
813
- host: string,
814
- actor: string,
815
- operation: string,
816
- ) => {
817
- const current = branchSetupIntent(operation);
818
- if (!current)
819
- return failure("permission_denied", "no approved action is bound to the active task");
820
- const listed = store.listTasks();
821
- const workspace = store.readWorkspace();
822
- if (!listed.ok || !workspace.ok)
823
- return failure("storage_error", "external action state is unavailable");
824
- if (!workspace.data) return failure("not_found", "workspace not found");
825
- const candidates = listed.data.flatMap((task) => {
826
- if (task.status !== "active" || task.workspaceId !== workspace.data!.id) return [];
827
- return task.decisions
828
- .filter(
829
- (entry) =>
830
- entry.data.purpose === "action" &&
831
- entry.data.response === "approved" &&
832
- entry.data.revoked === null,
833
- )
834
- .map((entry) => ({ task, entry }));
835
- });
836
- const matches = candidates.filter(({ entry }) => {
837
- const intent = branchSetupIntent(entry.data.binding.approvedContent);
838
- return (
839
- intent !== null &&
840
- (intent.cwd ?? store.root) === (current.cwd ?? store.root) &&
841
- intent.target === current.target &&
842
- intent.base === current.base &&
843
- intent.localBase === current.localBase &&
844
- intent.remoteBase === current.remoteBase &&
845
- intent.targetExists === current.targetExists
846
- );
847
- });
848
- if (matches.length !== 1)
849
- return failure(
850
- "permission_denied",
851
- matches.length === 0
852
- ? "no approved action is bound to the active task"
853
- : "multiple approved actions match the active task",
854
- );
855
- return success(workspace.data.revision, null, { ...matches[0], workspace: workspace.data });
856
- };
857
-
858
- /**
859
- * Re-resolve guard: when a prior approval exists for the same requested
860
- * operation/payload, the intent baseline must still match. For branch setup
861
- * only (target, base, remote base, existence) bind the effect — head and
862
- * dirt moves re-resolve instead of demanding re-approval. Drift names the
863
- * moved element and returns not_started so the agent re-presents the
864
- * current state instead of executing a stale effect.
865
- */
866
- export const priorResolvedDrift = (
867
- store: TaskStore,
868
- host: string,
869
- actor: string,
870
- operation: string,
871
- payload: unknown,
872
- currentResolved: unknown,
873
- ): Result<null> => {
874
- const prior = priorExternalAction(store, host, actor, operation, payload);
875
- if (!prior.ok) return success(null, null, null);
876
- let baseline: unknown;
877
- try {
878
- const parsed = JSON.parse(prior.data.entry.data.binding.approvedContent) as {
879
- payload?: { resolved?: unknown };
880
- };
881
- baseline = parsed.payload?.resolved;
882
- } catch {
883
- return success(null, null, null);
884
- }
885
- if (baseline === undefined || baseline === null) return success(null, null, null);
886
- if (operation === "git.branch_setup") {
887
- const before = branchSetupIntent(prior.data.entry.data.binding.approvedContent);
888
- let after: ReturnType<typeof branchSetupIntent> = null;
889
- try {
890
- after = branchSetupIntent(
891
- JSON.stringify({
892
- operation,
893
- payload: { ...(payload as Record<string, unknown>), resolved: currentResolved },
894
- }),
895
- );
896
- } catch {
897
- after = null;
898
- }
899
- if (before && after) {
900
- const moved =
901
- before.target !== after.target
902
- ? "target_branch"
903
- : before.base !== after.base
904
- ? "base_branch"
905
- : before.remoteBase !== after.remoteBase
906
- ? "remote_base"
907
- : before.localBase !== after.localBase
908
- ? "local_base"
909
- : before.targetExists !== after.targetExists
910
- ? "target_exists"
911
- : null;
912
- if (moved === null) return success(null, null, null);
913
- return failure(
914
- "invalid_input",
915
- `repository state changed since approval (${moved} moved); show the current state and approve the action again`,
916
- { outcome: "not_started", operation },
917
- );
918
- }
919
- }
920
- if (canonicalJson(baseline) === canonicalJson(currentResolved)) return success(null, null, null);
921
- return failure(
922
- "invalid_input",
923
- "repository state changed since approval; show the current state and approve the action again",
924
- { outcome: "not_started", operation },
925
- );
926
- };
927
-
928
- export const createAuthorizedExternalActionRunner =
929
- (core: WorkitCore, bind: ExternalActionBinding, coordinationRoot: string): ExternalActionEffect =>
930
- async <T>(
931
- operation: string,
932
- effect: (
933
- step?: string,
934
- reservation?: ActionReservation,
935
- ) => T | Result<T> | Promise<T | Result<T>>,
936
- ) => {
937
- let steps: string[] = [];
938
- try {
939
- const parsed = JSON.parse(operation) as { steps?: unknown };
940
- if (Array.isArray(parsed.steps) && parsed.steps.every((step) => typeof step === "string"))
941
- steps = parsed.steps;
942
- } catch {
943
- /* non-descriptor callers remain one-shot */
944
- }
945
- const sequence = steps.length ? steps : [undefined];
946
- let latest: ExternalActionResult<T> | undefined;
947
- for (const step of sequence) {
948
- const binding = bind(operation);
949
- if ("ok" in binding) return binding;
950
- const targetRoot = externalActionTargetRoot(operation, coordinationRoot);
951
- if (targetRoot === null)
952
- return failure("capability_unavailable", "external action target could not be locked", {
953
- outcome: "not_started",
954
- });
955
- const withActionLock: WithExternalActionLock | undefined = !sameDirectoryIdentity(
956
- targetRoot,
957
- coordinationRoot,
958
- )
959
- ? (run) => new TaskStore(targetRoot).withExternalActionLock(run, true)
960
- : undefined;
961
- const result = await runAuthorizedExternalAction(
962
- { core, ...binding, ...(step ? { step } : {}) },
963
- (reservation) => effect(step, reservation),
964
- withActionLock,
965
- );
966
- latest = result;
967
- if (!result.ok) return result;
968
- }
969
- return latest ?? failure("recovery_required", "external action did not execute");
970
- };
971
-
972
- const externalActionTargetRoot = (operation: string, coordinationRoot: string): string | null => {
973
- let value: unknown;
974
- try {
975
- value = JSON.parse(operation);
976
- } catch {
977
- return realpathSync(coordinationRoot);
978
- }
979
- if (typeof value !== "object" || value === null) return realpathSync(coordinationRoot);
980
- const descriptor = value as { operation?: unknown; payload?: unknown };
981
- if (
982
- typeof descriptor.operation !== "string" ||
983
- typeof descriptor.payload !== "object" ||
984
- descriptor.payload === null
985
- )
986
- return realpathSync(coordinationRoot);
987
- if (
988
- !descriptor.operation.startsWith("git.") &&
989
- !descriptor.operation.startsWith("hosting.") &&
990
- descriptor.operation !== "changelog.apply"
991
- )
992
- return realpathSync(coordinationRoot);
993
- const payload = descriptor.payload as Record<string, unknown>;
994
- const cwd = typeof payload.cwd === "string" ? payload.cwd : coordinationRoot;
995
- try {
996
- return realpathSync(path.resolve(coordinationRoot, cwd));
997
- } catch {
998
- return null;
999
- }
1000
- };
1001
-
1002
- export type NativeExternalActionObservation = {
1003
- actor: string;
1004
- actionRef: Ref;
1005
- outcome: "reserve" | "succeeded" | "not_started" | "unknown";
1006
- callId?: string;
1007
- taskRevision?: Revision;
1008
- workspaceRevision?: Revision;
1009
- };
1010
-
1011
- export const nativeExternalActionObservation = (
1012
- actor: string,
1013
- actionRef: Ref,
1014
- outcome: NativeExternalActionObservation["outcome"],
1015
- callId = actionRef.kind === "host" ? actionRef.handle : "external-action",
1016
- revisions?: { taskRevision: Revision; workspaceRevision: Revision },
1017
- ): NativeExternalActionObservation =>
1018
- revisions
1019
- ? { actor, actionRef, outcome, callId, ...revisions }
1020
- : { actor, actionRef, outcome, callId };
1021
-
1022
- export const readNativeExternalActionObservation = (
1023
- value: unknown,
1024
- ): NativeExternalActionObservation | null => {
1025
- if (typeof value !== "object" || value === null) return null;
1026
- const candidate = value as Record<string, unknown>;
1027
- if (
1028
- typeof candidate.actor !== "string" ||
1029
- !refSchema.safeParse(candidate.actionRef).success ||
1030
- !["reserve", "succeeded", "not_started", "unknown"].includes(String(candidate.outcome)) ||
1031
- (candidate.callId !== undefined && typeof candidate.callId !== "string") ||
1032
- (candidate.taskRevision !== undefined && typeof candidate.taskRevision !== "string") ||
1033
- (candidate.workspaceRevision !== undefined && typeof candidate.workspaceRevision !== "string")
1034
- )
1035
- return null;
1036
- return {
1037
- actor: candidate.actor,
1038
- actionRef: candidate.actionRef as Ref,
1039
- outcome: candidate.outcome as NativeExternalActionObservation["outcome"],
1040
- ...(candidate.callId === undefined ? {} : { callId: candidate.callId }),
1041
- ...(candidate.taskRevision === undefined ? {} : { taskRevision: candidate.taskRevision }),
1042
- ...(candidate.workspaceRevision === undefined
1043
- ? {}
1044
- : { workspaceRevision: candidate.workspaceRevision }),
1045
- };
1046
- };
1047
-
1048
- export const matchesNativeExternalAction = (
1049
- value: unknown,
1050
- expected: {
1051
- actor: string;
1052
- actionRef: Ref;
1053
- outcome: NativeExternalActionObservation["outcome"];
1054
- taskRevision?: Revision;
1055
- workspaceRevision?: Revision;
1056
- },
1057
- ): boolean => {
1058
- const observation = readNativeExternalActionObservation(value);
1059
- return (
1060
- observation !== null &&
1061
- observation.actor === expected.actor &&
1062
- observation.outcome === expected.outcome &&
1063
- (expected.taskRevision === undefined || observation.taskRevision === expected.taskRevision) &&
1064
- (expected.workspaceRevision === undefined ||
1065
- observation.workspaceRevision === expected.workspaceRevision) &&
1066
- canonicalJson(observation.actionRef) === canonicalJson(expected.actionRef)
1067
- );
1068
- };
1069
-
1070
- /**
1071
- * Run one concrete, already-authorized external effect through the core's
1072
- * one-time reservation and settlement transitions. The adapter supplies the
1073
- * host-observed action evidence; this helper never turns an arbitrary payload
1074
- * into an external effect and never retries an uncertain outcome.
1075
- */
1076
- export async function runAuthorizedExternalAction<T>(
1077
- input: AuthorizedActionInput,
1078
- effect: (reservation: ActionReservation) => T | Result<T> | Promise<T | Result<T>>,
1079
- withActionLock?: WithExternalActionLock,
1080
- ): Promise<ExternalActionResult<T>> {
1081
- if (input.core.isDelegatedCaller())
1082
- return failure("permission_denied", "delegated workers cannot run external actions");
1083
- if (input.available === false)
1084
- return failure(
1085
- "capability_unavailable",
1086
- `${input.capability ?? "external action"} is unavailable`,
1087
- { capability: input.capability ?? "external_action" },
1088
- );
1089
-
1090
- const reserved = input.core.reserveAction({
1091
- taskId: input.taskId,
1092
- decisionId: input.decisionId,
1093
- actionRef: input.actionRef,
1094
- expectedRevision: input.expectedRevision,
1095
- expectedWorkspaceRevision: input.expectedWorkspaceRevision,
1096
- ...(input.binding ? { binding: input.binding } : {}),
1097
- ...(input.step ? { step: input.step } : {}),
1098
- observation: input.reserveObservation,
1099
- });
1100
- if (!reserved.ok) return reserved;
1101
-
1102
- const settle = (
1103
- outcome: "succeeded" | "not_started" | "unknown",
1104
- refreshed = false,
1105
- ): ExternalActionResult<unknown> => {
1106
- try {
1107
- const revisions =
1108
- refreshed && input.refresh
1109
- ? input.refresh()
1110
- : {
1111
- expectedRevision: reserved.data.taskRevision,
1112
- expectedWorkspaceRevision: reserved.data.workspaceRevision,
1113
- };
1114
- const result = input.core.settleAction({
1115
- ...reserved.data,
1116
- taskRevision: revisions.expectedRevision,
1117
- workspaceRevision: revisions.expectedWorkspaceRevision,
1118
- outcome,
1119
- ...(input.step ? { step: input.step } : {}),
1120
- observation: input.settleObservation(outcome, {
1121
- taskRevision: revisions.expectedRevision,
1122
- workspaceRevision: revisions.expectedWorkspaceRevision,
1123
- }),
1124
- });
1125
- return result;
1126
- } catch {
1127
- return failure("external_outcome_unknown", "external action settlement is unavailable", {
1128
- operation: "external_action",
1129
- outcome: "unknown",
1130
- });
1131
- }
1132
- };
1133
- const settleWithRefresh = (
1134
- outcome: "succeeded" | "not_started" | "unknown",
1135
- first: ExternalActionResult<unknown>,
1136
- ): ExternalActionResult<unknown> =>
1137
- !first.ok && first.code === "revision_conflict" && input.refresh
1138
- ? settle(outcome, true)
1139
- : first;
1140
-
1141
- let effectStarted = false;
1142
- const runEffect = async (): Promise<ExternalActionResult<T>> => {
1143
- effectStarted = true;
1144
- let effectValue: T | Result<T>;
1145
- try {
1146
- effectValue = await effect(reserved.data);
1147
- } catch {
1148
- const settled = settleWithRefresh("unknown", settle("unknown"));
1149
- if (!settled.ok) return settled;
1150
- return failure("external_outcome_unknown", "external action outcome is unknown", {
1151
- operation: "external_action",
1152
- outcome: "unknown",
1153
- });
1154
- }
1155
- if (
1156
- typeof effectValue === "object" &&
1157
- effectValue !== null &&
1158
- "ok" in effectValue &&
1159
- typeof (effectValue as { ok?: unknown }).ok === "boolean"
1160
- ) {
1161
- const contract = effectValue;
1162
- if (!contract.ok) {
1163
- const preflight =
1164
- typeof contract.details === "object" &&
1165
- contract.details !== null &&
1166
- (contract.details as Record<string, unknown>).outcome === "not_started";
1167
- const outcome = input.failureOutcome ?? (preflight ? "not_started" : "unknown");
1168
- const settled = settleWithRefresh(outcome, settle(outcome));
1169
- if (!settled.ok) return settled;
1170
- return contract;
1171
- }
1172
- effectValue = contract.data;
1173
- }
1174
- const settled = settleWithRefresh("succeeded", settle("succeeded"));
1175
- if (!settled.ok) return settled;
1176
- return {
1177
- ok: true,
1178
- schemaVersion: 1,
1179
- revision: settled.revision,
1180
- workspaceRevision: settled.workspaceRevision,
1181
- data: effectValue as T,
1182
- };
1183
- };
1184
- let result: ExternalActionResult<T>;
1185
- try {
1186
- result = withActionLock ? await withActionLock(runEffect) : await runEffect();
1187
- } catch {
1188
- if (effectStarted)
1189
- return failure("external_outcome_unknown", "external action outcome is unknown", {
1190
- operation: "external_action",
1191
- outcome: "unknown",
1192
- });
1193
- const settled = settleWithRefresh("not_started", settle("not_started"));
1194
- if (!settled.ok) return settled;
1195
- return failure("writer_conflict", "target checkout was unavailable before the effect", {
1196
- outcome: "not_started",
1197
- });
1198
- }
1199
- if (!effectStarted) {
1200
- const settled = settleWithRefresh("not_started", settle("not_started"));
1201
- if (!settled.ok) return settled;
1202
- }
1203
- return result;
1204
- }