@codemation/core 0.11.1 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/dist/{CostCatalogContract-DZgcUBE4.d.cts → CostCatalogContract-Dxq1BTyi.d.cts} +2 -2
  3. package/dist/{EngineRuntimeRegistration.types-Cggm5GVY.d.cts → EngineRuntimeRegistration.types-CqcTWexS.d.cts} +3 -3
  4. package/dist/{EngineRuntimeRegistration.types-BQbS9_gs.d.ts → EngineRuntimeRegistration.types-Cr75cSfL.d.ts} +2 -2
  5. package/dist/InMemoryRunDataFactory-Csy2evr_.d.cts +205 -0
  6. package/dist/{ItemsInputNormalizer-CwdOhSAK.cjs → ItemsInputNormalizer-57EdA1ad.cjs} +2 -2
  7. package/dist/{ItemsInputNormalizer-CwdOhSAK.cjs.map → ItemsInputNormalizer-57EdA1ad.cjs.map} +1 -1
  8. package/dist/{ItemsInputNormalizer-_Mfcd3YU.d.ts → ItemsInputNormalizer-BWtlwdVI.d.ts} +2 -2
  9. package/dist/{ItemsInputNormalizer-D-MH8MBs.js → ItemsInputNormalizer-BkSvmfAW.js} +2 -2
  10. package/dist/{ItemsInputNormalizer-D-MH8MBs.js.map → ItemsInputNormalizer-BkSvmfAW.js.map} +1 -1
  11. package/dist/{ItemsInputNormalizer-C_dpn76M.d.cts → ItemsInputNormalizer-pLrWwUAP.d.cts} +3 -3
  12. package/dist/{RunIntentService-CEF-sFfI.d.cts → RunIntentService-BitgkKaT.d.cts} +18 -4
  13. package/dist/{RunIntentService-BVur7x9n.d.ts → RunIntentService-DYpqfu6D.d.ts} +18 -4
  14. package/dist/{agentMcpTypes-ZiNbNsEi.d.cts → agentMcpTypes-DGIwk6Ue.d.cts} +201 -4
  15. package/dist/bootstrap/index.cjs +3 -3
  16. package/dist/bootstrap/index.d.cts +63 -7
  17. package/dist/bootstrap/index.d.ts +5 -5
  18. package/dist/bootstrap/index.js +3 -3
  19. package/dist/{bootstrap-BxuTFTLB.cjs → bootstrap-BEu1fJBM.cjs} +175 -4
  20. package/dist/bootstrap-BEu1fJBM.cjs.map +1 -0
  21. package/dist/{bootstrap-D_Yyi0wL.js → bootstrap-CSeInbj1.js} +173 -4
  22. package/dist/bootstrap-CSeInbj1.js.map +1 -0
  23. package/dist/browser.cjs +4 -2
  24. package/dist/browser.d.cts +4 -4
  25. package/dist/browser.d.ts +3 -3
  26. package/dist/browser.js +3 -3
  27. package/dist/contracts.d.cts +5 -5
  28. package/dist/contracts.d.ts +2 -2
  29. package/dist/{di-BlEKdoZS.cjs → di-C-2ep8NZ.cjs} +44 -1
  30. package/dist/di-C-2ep8NZ.cjs.map +1 -0
  31. package/dist/{di-0Wop7z1y.js → di-D9Mv3kF3.js} +33 -2
  32. package/dist/di-D9Mv3kF3.js.map +1 -0
  33. package/dist/{executionPersistenceContracts-BgZMRsTa.d.cts → executionPersistenceContracts-CN9d7AnL.d.cts} +2 -2
  34. package/dist/{index-62Ba9f7D.d.ts → index-CqZeNGAp.d.ts} +343 -101
  35. package/dist/{index-zWGtEhrf.d.ts → index-rllWL4r-.d.ts} +459 -5
  36. package/dist/index.cjs +91 -161
  37. package/dist/index.cjs.map +1 -1
  38. package/dist/index.d.cts +458 -97
  39. package/dist/index.d.ts +5 -5
  40. package/dist/index.js +74 -159
  41. package/dist/index.js.map +1 -1
  42. package/dist/{params-B5SENSzZ.d.cts → params-DRUr0F5v.d.cts} +2 -2
  43. package/dist/{runtime-cxmUkk0l.js → runtime-6-U2Cou5.js} +690 -18
  44. package/dist/runtime-6-U2Cou5.js.map +1 -0
  45. package/dist/{runtime-DBzq5YBi.cjs → runtime-DjYXgOo0.cjs} +749 -17
  46. package/dist/runtime-DjYXgOo0.cjs.map +1 -0
  47. package/dist/testing.cjs +3 -3
  48. package/dist/testing.d.cts +3 -3
  49. package/dist/testing.d.ts +3 -3
  50. package/dist/testing.js +3 -3
  51. package/package.json +1 -1
  52. package/src/authoring/defineHumanApprovalNode.types.ts +379 -0
  53. package/src/authoring/index.ts +6 -0
  54. package/src/binaries/DefaultExecutionBinaryServiceFactory.ts +27 -2
  55. package/src/binaries/DefaultNodeBinaryAttachmentServiceFactory.ts +14 -0
  56. package/src/binaries/boundedReadBinary.types.ts +90 -0
  57. package/src/bootstrap/runtime/EngineRuntimeRegistrar.ts +29 -0
  58. package/src/contracts/CodemationTelemetryAttributeNames.ts +10 -0
  59. package/src/contracts/credentialTypes.ts +10 -0
  60. package/src/contracts/hitlSeamTypes.ts +34 -0
  61. package/src/contracts/humanTaskStoreTypes.ts +48 -0
  62. package/src/contracts/inboxChannelTypes.ts +58 -0
  63. package/src/contracts/index.ts +3 -0
  64. package/src/contracts/runTypes.ts +61 -3
  65. package/src/contracts/runtimeTypes.ts +131 -0
  66. package/src/contracts/workspaceFileTypes.ts +73 -0
  67. package/src/credentials/CredentialMaterialProvider.types.ts +61 -0
  68. package/src/credentials/ManagedCredentialMaterialWriteError.ts +14 -0
  69. package/src/credentials/ManagedMaterialFetchError.ts +16 -0
  70. package/src/execution/ActivationEnqueueService.ts +16 -0
  71. package/src/execution/DefaultExecutionContextFactory.ts +11 -0
  72. package/src/execution/NodeExecutionSnapshotFactory.ts +7 -1
  73. package/src/execution/NodeExecutor.ts +60 -1
  74. package/src/execution/NodeExecutorFactory.ts +12 -2
  75. package/src/execution/NodeSuspensionHandler.ts +220 -0
  76. package/src/execution/PersistedRunStateTerminalBuilder.ts +5 -2
  77. package/src/execution/RunStateSemantics.ts +5 -0
  78. package/src/execution/RunSuspendedError.ts +21 -0
  79. package/src/index.ts +42 -0
  80. package/src/orchestration/Engine.ts +12 -2
  81. package/src/orchestration/EngineWaiters.ts +1 -1
  82. package/src/orchestration/NodeExecutionRequestHandlerService.ts +25 -2
  83. package/src/orchestration/RunContinuationService.ts +226 -2
  84. package/src/orchestration/TestSuiteOrchestrator.ts +5 -4
  85. package/src/runtime/RunIntentService.ts +3 -0
  86. package/src/workflow/dsl/ChainCursorResolver.ts +36 -0
  87. package/dist/InMemoryRunDataFactory-C7YItvHG.d.cts +0 -123
  88. package/dist/bootstrap-BxuTFTLB.cjs.map +0 -1
  89. package/dist/bootstrap-D_Yyi0wL.js.map +0 -1
  90. package/dist/di-0Wop7z1y.js.map +0 -1
  91. package/dist/di-BlEKdoZS.cjs.map +0 -1
  92. package/dist/runtime-DBzq5YBi.cjs.map +0 -1
  93. package/dist/runtime-cxmUkk0l.js.map +0 -1
@@ -19,4 +19,14 @@ export class CodemationTelemetryAttributeNames {
19
19
  static readonly mcpServerId = "mcp.server_id";
20
20
  /** MCP tool name on spans created for callTool invocations. */
21
21
  static readonly mcpToolName = "mcp.tool_name";
22
+ /** Terminal node-execution status (e.g. `"hitl-approved"`, `"hitl-rejected"`) on HITL outcome spans. */
23
+ static readonly nodeExecutionStatus = "codemation.node.execution_status";
24
+ /** Populated on run-halted spans; discriminates the halt reason (e.g. `"hitl-rejected"`). */
25
+ static readonly runHaltReason = "codemation.run.halt_reason";
26
+ /** Human task ID on `hitl.task.*` span events. */
27
+ static readonly hitlTaskId = "codemation.hitl.task_id";
28
+ /** HITL channel name (e.g. `"inbox"`, `"control-plane-inbox"`) on `hitl.task.*` span events. */
29
+ static readonly hitlChannel = "codemation.hitl.channel";
30
+ /** Decision outcome (e.g. `"approved"`, `"rejected"`) on `hitl.task.decided` span events. */
31
+ static readonly hitlDecisionStatus = "codemation.hitl.decision_status";
22
32
  }
@@ -162,6 +162,16 @@ export type CredentialInstanceRecord<TPublicConfig extends CredentialJsonRecord
162
162
  setupStatus: CredentialSetupStatus;
163
163
  createdAt: string;
164
164
  updatedAt: string;
165
+ /**
166
+ * Pointer to where the credential material bytes live. For OSS / standalone
167
+ * rows this is `{source: "local", ref: instanceId}` and the bytes co-locate
168
+ * with the row in the workspace DB. For managed-mode rows this is
169
+ * `{source: "control-plane", ref: <cp_id>}` and the bytes live at CP.
170
+ *
171
+ * The seam is read through `CredentialMaterialProvider`. See
172
+ * `docs/design/credentials-oauth-unification.md` ("Material provider seam").
173
+ */
174
+ material: Readonly<{ source: "local" | "control-plane"; ref: string }>;
165
175
  }>;
166
176
 
167
177
  /**
@@ -0,0 +1,34 @@
1
+ import type { TypeToken } from "../di";
2
+
3
+ /**
4
+ * Seam interfaces for HITL collaborators that are implemented in `@codemation/host`
5
+ * and injected into `NodeSuspensionHandler` at runtime. Core defines the interface only —
6
+ * no HTTP, vendor SDK, or Prisma dependencies here.
7
+ */
8
+
9
+ /** Signs and hashes a HITL resume token. Core only needs the sign and hash operations. */
10
+ export interface HitlResumeTokenSignerSeam {
11
+ sign(args: { taskId: string; expiresAt: Date; schemaHash: string }): string;
12
+ hashToken(token: string): string;
13
+ }
14
+
15
+ /** Schedules a delayed BullMQ job that drives the timeout path. */
16
+ export interface HitlTimeoutJobSchedulerSeam {
17
+ enqueueTimeoutJob(args: { taskId: string; expiresAt: Date }): Promise<void>;
18
+ }
19
+
20
+ export const HitlResumeTokenSignerToken = Symbol.for("codemation.core.HitlResumeTokenSigner") as TypeToken<
21
+ HitlResumeTokenSignerSeam | undefined
22
+ >;
23
+
24
+ export const HitlTimeoutJobSchedulerToken = Symbol.for("codemation.core.HitlTimeoutJobScheduler") as TypeToken<
25
+ HitlTimeoutJobSchedulerSeam | undefined
26
+ >;
27
+
28
+ /**
29
+ * Optional workspace ID injected into NodeSuspensionHandler in managed mode (T7 security fix).
30
+ * Allows the handler to stamp the workspaceId on each HumanTaskRecord so HitlCallbackHandler
31
+ * can assert workspace identity independently of the HMAC middleware.
32
+ * Not registered in non-managed mode; NodeSuspensionHandler defaults to null.
33
+ */
34
+ export const HitlWorkspaceIdToken = Symbol.for("codemation.core.HitlWorkspaceId") as TypeToken<string | undefined>;
@@ -0,0 +1,48 @@
1
+ import type { TypeToken } from "../di";
2
+ import type { HumanTaskActor, HumanTaskSubject } from "./runtimeTypes";
3
+ import type { JsonValue } from "./workflowTypes";
4
+
5
+ export type HumanTaskStatus = "pending" | "decided" | "timed_out" | "auto_accepted" | "cancelled";
6
+
7
+ /** Persisted record for a single HITL task instance. */
8
+ export interface HumanTaskRecord {
9
+ readonly id: string;
10
+ readonly runId: string;
11
+ readonly workflowId: string;
12
+ readonly workspaceId?: string;
13
+ readonly nodeId: string;
14
+ readonly activationId: string;
15
+ readonly itemIndex: number;
16
+ readonly status: HumanTaskStatus;
17
+ readonly channel: string;
18
+ readonly subject: HumanTaskSubject;
19
+ readonly metadata: Record<string, JsonValue>;
20
+ readonly decisionSchemaJson: string;
21
+ readonly decisionSchemaHash: string;
22
+ readonly onTimeout: "halt" | "auto-accept";
23
+ readonly deliveryRef?: JsonValue;
24
+ readonly decision?: JsonValue;
25
+ readonly decidedAt?: Date;
26
+ readonly decidedBy?: HumanTaskActor;
27
+ readonly resumeTokenHash: string;
28
+ readonly expiresAt: Date;
29
+ readonly createdAt: Date;
30
+ }
31
+
32
+ export interface HumanTaskStore {
33
+ create(record: HumanTaskRecord): Promise<void>;
34
+ findById(taskId: string): Promise<HumanTaskRecord | undefined>;
35
+ findByResumeTokenHash(tokenHash: string): Promise<HumanTaskRecord | undefined>;
36
+ findPendingForWorkspace(workspaceId: string): Promise<ReadonlyArray<HumanTaskRecord>>;
37
+ /** Returns all pending tasks regardless of workspace. Used by the local dev inbox (non-managed mode). */
38
+ findAllPending(): Promise<ReadonlyArray<HumanTaskRecord>>;
39
+ markDecided(args: { taskId: string; decision: JsonValue; decidedBy: HumanTaskActor; decidedAt: Date }): Promise<void>;
40
+ markTimedOut(taskId: string): Promise<void>;
41
+ markAutoAccepted(taskId: string): Promise<void>;
42
+ markCancelled(taskId: string): Promise<void>;
43
+ cancelPendingForRun(runId: string): Promise<void>;
44
+ }
45
+
46
+ export const HumanTaskStoreToken = Symbol.for("codemation.core.HumanTaskStore") as TypeToken<
47
+ HumanTaskStore | undefined
48
+ >;
@@ -0,0 +1,58 @@
1
+ import type { TypeToken } from "../di";
2
+ import type { HumanTaskActor, HumanTaskHandle, HumanTaskSubject } from "./runtimeTypes";
3
+ import type { Item } from "./workflowTypes";
4
+
5
+ /**
6
+ * A single inbox delivery channel.
7
+ * Implementations: `LocalInboxChannel`, `ControlPlaneInboxChannel`.
8
+ */
9
+ export interface InboxChannel {
10
+ readonly kind: "local" | "control-plane-inbox";
11
+ deliver(args: InboxDeliverArgs): Promise<InboxDelivery>;
12
+ updateOnDecision?(args: InboxOnDecisionArgs): Promise<void>;
13
+ updateOnTimeout?(args: InboxOnTimeoutArgs): Promise<void>;
14
+ }
15
+
16
+ export type InboxDeliverArgs = Readonly<{
17
+ task: HumanTaskHandle;
18
+ subject: HumanTaskSubject;
19
+ priority: "low" | "normal" | "high";
20
+ item: Item;
21
+ /** Present in managed mode (from `PairingConfig.workspaceId`). */
22
+ workspaceId?: string;
23
+ }>;
24
+
25
+ export type InboxDelivery =
26
+ | { kind: "local"; inboxItemId: string }
27
+ | { kind: "cp"; inboxItemId: string; workspaceId: string };
28
+
29
+ export type InboxOnDecisionArgs = Readonly<{
30
+ delivery: InboxDelivery;
31
+ decision: { approved: boolean; note?: string };
32
+ actor: HumanTaskActor;
33
+ }>;
34
+
35
+ export type InboxOnTimeoutArgs = Readonly<{
36
+ delivery: InboxDelivery;
37
+ policy: "halt" | "auto-accept";
38
+ }>;
39
+
40
+ /**
41
+ * Resolves the correct `InboxChannel` for the current deployment mode
42
+ * (local dev vs. managed/CP). Implemented in `@codemation/host`.
43
+ */
44
+ export interface InboxChannelResolverSeam {
45
+ resolve(): { channel: InboxChannel; workspaceId?: string };
46
+ }
47
+
48
+ export const InboxChannelResolverToken = Symbol.for("codemation.core.InboxChannelResolver") as TypeToken<
49
+ InboxChannelResolverSeam | undefined
50
+ >;
51
+
52
+ export const LocalInboxChannelToken = Symbol.for("codemation.core.LocalInboxChannel") as TypeToken<
53
+ InboxChannel | undefined
54
+ >;
55
+
56
+ export const ControlPlaneInboxChannelToken = Symbol.for("codemation.core.ControlPlaneInboxChannel") as TypeToken<
57
+ InboxChannel | undefined
58
+ >;
@@ -1,5 +1,8 @@
1
1
  export * from "./AgentBindError";
2
2
  export * from "./agentMcpTypes";
3
+ export * from "./hitlSeamTypes";
4
+ export * from "./humanTaskStoreTypes";
5
+ export * from "./inboxChannelTypes";
3
6
  export * from "./NoOpAgentMcpIntegration";
4
7
  export * from "./baseTypes";
5
8
  export * from "./assertionTypes";
@@ -128,7 +128,18 @@ export interface RunQueueEntry {
128
128
  }>;
129
129
  }
130
130
 
131
- export type NodeExecutionStatus = "pending" | "queued" | "running" | "completed" | "failed" | "skipped";
131
+ export type NodeExecutionStatus =
132
+ | "pending"
133
+ | "queued"
134
+ | "running"
135
+ | "completed"
136
+ | "failed"
137
+ | "skipped"
138
+ | "hitl-approved"
139
+ | "hitl-rejected"
140
+ | "hitl-timeout"
141
+ | "hitl-auto-accepted"
142
+ | "hitl-cancelled";
132
143
 
133
144
  export interface NodeExecutionError {
134
145
  message: string;
@@ -245,7 +256,10 @@ export interface ExecutionFrontierPlan {
245
256
  preservedPinnedNodeIds: ReadonlyArray<NodeId>;
246
257
  }
247
258
 
248
- export type RunStatus = "running" | "pending" | "completed" | "failed";
259
+ export type RunStatus = "running" | "pending" | "completed" | "failed" | "suspended" | "halted";
260
+
261
+ /** Reason a run transitioned to {@link RunStatus} `"halted"`. */
262
+ export type RunHaltReason = "hitl-rejected" | "hitl-timeout" | "hitl-cancelled";
249
263
 
250
264
  export interface RunSummary {
251
265
  runId: RunId;
@@ -283,6 +297,37 @@ export interface PersistedRunSchedulingState {
283
297
  queue: RunQueueEntry[];
284
298
  }
285
299
 
300
+ /** One persisted suspension entry per suspended item. */
301
+ export interface PersistedSuspensionEntry {
302
+ /** Opaque task identifier (UUID v4). */
303
+ readonly taskId: string;
304
+ readonly nodeId: NodeId;
305
+ readonly activationId: NodeActivationId;
306
+ readonly itemIndex: number;
307
+ /** SHA-256 hex digest of the decision schema JSON (for schema-drift detection). */
308
+ readonly decisionSchemaHash: string;
309
+ /** Serialized return value from `SuspensionRequest.deliver` (stored on the HumanTask row). */
310
+ readonly deliveryRef: JsonValue;
311
+ /** ISO timestamp when the task expires. */
312
+ readonly timeoutAt: string;
313
+ readonly onTimeout: "halt" | "auto-accept";
314
+ }
315
+
316
+ /**
317
+ * When a node is re-activated after suspension, the engine writes the resume context here
318
+ * so `NodeExecutionRequestHandlerService` can splice `resumeContext` into ctx.
319
+ * Cleared once the re-activation is consumed.
320
+ */
321
+ export interface PendingResumeEntry {
322
+ readonly activationId: NodeActivationId;
323
+ readonly nodeId: NodeId;
324
+ /**
325
+ * Typed as `unknown` here to avoid a circular import between runTypes ↔ runtimeTypes.
326
+ * `NodeExecutionRequestHandlerService` casts this to `ResumeContext` from runtimeTypes.
327
+ */
328
+ readonly resumeContext: unknown;
329
+ }
330
+
286
331
  export interface PersistedRunState {
287
332
  runId: RunId;
288
333
  workflowId: WorkflowId;
@@ -301,12 +346,24 @@ export interface PersistedRunState {
301
346
  /** Successful node completions so far (for activation budget). */
302
347
  engineCounters?: EngineRunCounters;
303
348
  status: RunStatus;
349
+ /** Populated when `status === "halted"` to discriminate why the run was halted. */
350
+ reason?: RunHaltReason;
304
351
  pending?: PendingNodeExecution;
305
352
  queue: RunQueueEntry[];
306
353
  outputsByNode: Record<NodeId, NodeOutputs>;
307
354
  nodeSnapshotsByNodeId: Record<NodeId, NodeExecutionSnapshot>;
308
355
  /** Append-only history of connection invocations (LLM/tool) nested under owning nodes. */
309
356
  connectionInvocations?: ReadonlyArray<ConnectionInvocationRecord>;
357
+ /**
358
+ * One entry per outstanding HITL suspension (per-item).
359
+ * Present and non-empty iff `status === "suspended"`.
360
+ */
361
+ suspension?: ReadonlyArray<PersistedSuspensionEntry>;
362
+ /**
363
+ * Written by `resumeRun()` so `NodeExecutionRequestHandlerService` can splice `resumeContext`
364
+ * into the ctx when re-executing the suspended node. Cleared once consumed.
365
+ */
366
+ pendingResume?: PendingResumeEntry;
310
367
  }
311
368
 
312
369
  export interface WorkflowExecutionRepository {
@@ -349,7 +406,8 @@ export interface WorkflowExecutionPruneRepository {
349
406
  export type RunResult =
350
407
  | { runId: RunId; workflowId: WorkflowId; startedAt: string; status: "completed"; outputs: Items }
351
408
  | { runId: RunId; workflowId: WorkflowId; startedAt: string; status: "pending"; pending: PendingNodeExecution }
352
- | { runId: RunId; workflowId: WorkflowId; startedAt: string; status: "failed"; error: { message: string } };
409
+ | { runId: RunId; workflowId: WorkflowId; startedAt: string; status: "failed"; error: { message: string } }
410
+ | { runId: RunId; workflowId: WorkflowId; startedAt: string; status: "halted"; reason: RunHaltReason };
353
411
 
354
412
  export type WebhookRunResult = Readonly<{
355
413
  runId: RunId;
@@ -19,6 +19,105 @@ import type { WorkflowActivationPolicy } from "./workflowActivationPolicy";
19
19
  import type { TriggerInstanceId, WebhookTriggerMatcher } from "./webhookTypes";
20
20
  import type { ZodType } from "zod";
21
21
 
22
+ // ---------------------------------------------------------------------------
23
+ // HITL primitives
24
+ // ---------------------------------------------------------------------------
25
+
26
+ /** Opaque unique identifier for a single HumanTask instance. */
27
+ export type HumanTaskId = string;
28
+
29
+ /**
30
+ * Duration string — ISO 8601 duration ("PT24H") or shorthand ("24h").
31
+ * Parsed by the timeout job; stored as-is in the suspension record.
32
+ */
33
+ export type Duration = string;
34
+
35
+ /**
36
+ * Minimal handle handed to the `deliver` callback so it can route to the correct
37
+ * inbox channel.
38
+ */
39
+ export interface HumanTaskHandle {
40
+ readonly taskId: HumanTaskId;
41
+ readonly runId: string;
42
+ readonly nodeId: string;
43
+ readonly expiresAt: Date;
44
+ /** TODO: real signed URL; placeholder empty string for now. */
45
+ readonly resumeUrl: string;
46
+ /**
47
+ * Arbitrary JSON metadata copied from `SuspensionRequest.request.metadata` at suspension time.
48
+ * Used by the agent runtime to round-trip the `agentCheckpoint` back to the
49
+ * resumed node via `ctx.resumeContext.task.metadata`.
50
+ */
51
+ readonly metadata?: Readonly<Record<string, import("./workflowTypes").JsonValue>>;
52
+ }
53
+
54
+ /** Human-readable description surface shown to the reviewer. */
55
+ export interface HumanTaskSubject {
56
+ readonly title: string;
57
+ readonly summary: string;
58
+ readonly attributes?: import("./workflowTypes").JsonValue;
59
+ }
60
+
61
+ /** Identity of the person who made a decision on the task. */
62
+ export interface HumanTaskActor {
63
+ readonly actorId: string;
64
+ readonly displayName?: string;
65
+ }
66
+
67
+ /**
68
+ * Resume context injected into `NodeExecutionContext` when the engine re-activates
69
+ * a previously suspended node. `defineHumanApprovalNode` wraps this with parsed
70
+ * `TDecision`; at the engine layer `decision.value` is `unknown`.
71
+ */
72
+ export interface ResumeContext {
73
+ readonly decision:
74
+ | Readonly<{ kind: "decided"; value: unknown; actor: HumanTaskActor; decidedAt: Date }>
75
+ | Readonly<{ kind: "timed_out"; at: Date }>
76
+ | Readonly<{ kind: "auto_accepted"; at: Date }>;
77
+ readonly delivery: import("./workflowTypes").JsonValue;
78
+ readonly task: HumanTaskHandle;
79
+ }
80
+
81
+ /**
82
+ * Thrown by a node's `execute()` to request durable suspension of the current item.
83
+ * The engine catches this, persists the suspension entry, calls `deliver`, and
84
+ * continues to the next item (per-item semantics).
85
+ *
86
+ * @example
87
+ * ```ts
88
+ * throw new SuspensionRequest({
89
+ * decisionSchema: z.object({ approved: z.boolean() }),
90
+ * timeout: "PT24H",
91
+ * onTimeout: "halt",
92
+ * subject: { title: "Approve invoice", summary: "Invoice #1234 needs approval" },
93
+ * deliver: async (handle) => {
94
+ * await notifySlack(handle);
95
+ * return { channel: "slack", ts: "..." };
96
+ * },
97
+ * });
98
+ * ```
99
+ */
100
+ export class SuspensionRequest<
101
+ TDecision = unknown,
102
+ TDelivery extends import("./workflowTypes").JsonValue = import("./workflowTypes").JsonValue,
103
+ > extends Error {
104
+ constructor(
105
+ readonly request: Readonly<{
106
+ decisionSchema: ZodType<TDecision>;
107
+ timeout: Duration;
108
+ onTimeout: "halt" | "auto-accept";
109
+ subject: HumanTaskSubject;
110
+ metadata?: Readonly<Record<string, import("./workflowTypes").JsonValue>>;
111
+ deliver: (handle: HumanTaskHandle) => Promise<TDelivery>;
112
+ }>,
113
+ ) {
114
+ // Extending Error so wrappers like InProcessRetryRunner preserve identity
115
+ // (`instanceof SuspensionRequest`) instead of coercing via String(thrown).
116
+ super(`SuspensionRequest(${request.subject?.title ?? "untitled"})`);
117
+ this.name = "SuspensionRequest";
118
+ }
119
+ }
120
+
22
121
  import type {
23
122
  ActivationIdFactory,
24
123
  BinaryAttachment,
@@ -147,9 +246,28 @@ export interface NodeBinaryAttachmentService extends ExecutionBinaryService {
147
246
  withAttachment<TJson>(item: Item<TJson>, name: string, attachment: BinaryAttachment): Item<TJson>;
148
247
  }
149
248
 
249
+ /** Default maximum bytes read into memory by the bounded helpers (50 MiB). */
250
+ export const BINARY_DEFAULT_MAX_BYTES = 50 * 1024 * 1024;
251
+
150
252
  export interface ExecutionBinaryService {
151
253
  forNode(args: { nodeId: NodeId; activationId: NodeActivationId }): NodeBinaryAttachmentService;
152
254
  openReadStream(attachment: BinaryAttachment): Promise<BinaryStorageReadResult | undefined>;
255
+ /**
256
+ * Reads all bytes from the attachment into a contiguous `Uint8Array`.
257
+ * Checks `attachment.size` against `maxBytes` *before* any allocation; throws a bounded-read
258
+ * error when exceeded (no OOM). Throws if the stream is unavailable or the byte count mismatches.
259
+ */
260
+ getBytes(attachment: BinaryAttachment, maxBytes?: number): Promise<Uint8Array>;
261
+ /**
262
+ * Reads the attachment and decodes the bytes as UTF-8 text.
263
+ * Subject to the same bounded-read safety as `getBytes`.
264
+ */
265
+ getText(attachment: BinaryAttachment, maxBytes?: number): Promise<string>;
266
+ /**
267
+ * Reads the attachment, decodes as UTF-8 text, and parses as JSON.
268
+ * Throws a clear error on invalid JSON. Subject to the same bounded-read safety.
269
+ */
270
+ getJson<T = unknown>(attachment: BinaryAttachment, maxBytes?: number): Promise<T>;
153
271
  }
154
272
 
155
273
  export interface ExecutionContext {
@@ -183,6 +301,14 @@ export interface ExecutionContext {
183
301
  * Collections registered in the codemation config, keyed by collection name.
184
302
  */
185
303
  readonly collections?: CollectionsContext;
304
+ /**
305
+ * Resolve a DI token from the host container.
306
+ * Allows nodes to reach host-side services (e.g. `InboxChannelResolverToken`)
307
+ * without importing host code. Wired by `DefaultExecutionContextFactory`; throws
308
+ * a clear error when no resolver is configured (e.g. in unit tests that don't
309
+ * set up the full container).
310
+ */
311
+ resolve<T>(token: TypeToken<T>): T;
186
312
  }
187
313
 
188
314
  export interface ExecutionContextFactory {
@@ -208,6 +334,11 @@ export interface NodeExecutionContext<TConfig extends NodeConfigBase = NodeConfi
208
334
  config: TConfig;
209
335
  telemetry: NodeExecutionTelemetry;
210
336
  binary: NodeBinaryAttachmentService;
337
+ /**
338
+ * Present when this node activation is a HITL resume.
339
+ * The node checks `ctx.resumeContext !== undefined` and takes the resume branch.
340
+ */
341
+ resumeContext?: ResumeContext;
211
342
  }
212
343
 
213
344
  export interface PollingTriggerHandle {
@@ -0,0 +1,73 @@
1
+ import type { TypeToken } from "../di";
2
+
3
+ /**
4
+ * Metadata returned for a workspace file object.
5
+ * Filename and contentType come from the S3 object's custom metadata
6
+ * (stamped by the control plane at upload time — story 03).
7
+ * The local-fs driver reads them from a companion .meta.json sidecar.
8
+ */
9
+ export interface WorkspaceFileMetadata {
10
+ /** Storage key: `<workspaceId>/files/<fileId>` */
11
+ readonly key: string;
12
+ /** Last path segment of the storage key. */
13
+ readonly fileId: string;
14
+ /** Original filename as stamped by the CP at upload time. Empty string if not yet stamped (pre-story-03). */
15
+ readonly filename: string;
16
+ readonly contentType: string;
17
+ readonly size: number;
18
+ readonly lastModified: Date;
19
+ }
20
+
21
+ /**
22
+ * Read-only, workspace-scoped port for accessing the shared workspace file pool.
23
+ * Implemented in `@codemation/host`; nodes reach it via `ctx.resolve(WorkspaceFileStorageToken)`.
24
+ *
25
+ * Key scheme: `<workspaceId>/files/<fileId>` — but nodes never construct raw keys.
26
+ * Use the workspace-level helpers (`listFiles`, `getFileByName`, `getFileById`) instead.
27
+ *
28
+ * This adapter is SEPARATE from the run-scoped BinaryStorage — do not confuse the two.
29
+ */
30
+ export interface IWorkspaceFileStorage {
31
+ /**
32
+ * Lists all files in this workspace, sorted newest-first by lastModified.
33
+ * Optional case-insensitive substring filter on filename.
34
+ */
35
+ listFiles(filenameFilter?: string): Promise<ReadonlyArray<WorkspaceFileMetadata>>;
36
+
37
+ /**
38
+ * Returns metadata for the newest file with the given filename in this workspace.
39
+ * @throws WorkspaceFileNotFoundError when no file with that name exists.
40
+ */
41
+ getFileByName(filename: string): Promise<WorkspaceFileMetadata>;
42
+
43
+ /**
44
+ * Returns metadata for the file with the given fileId in this workspace.
45
+ * @throws WorkspaceFileNotFoundError when no file with that id exists.
46
+ */
47
+ getFileById(fileId: string): Promise<WorkspaceFileMetadata>;
48
+
49
+ /**
50
+ * Returns a non-buffered stream of the stored object's bytes.
51
+ * Accepts a full storage key or a fileId (prefixes as needed).
52
+ * @throws WorkspaceFileNotFoundError when the key does not exist.
53
+ */
54
+ getStream(key: string): Promise<ReadableStream<Uint8Array>>;
55
+ }
56
+
57
+ /**
58
+ * Error thrown when a requested workspace file key does not exist.
59
+ */
60
+ export class WorkspaceFileNotFoundError extends Error {
61
+ constructor(readonly key: string) {
62
+ super(`Workspace file not found: ${key}`);
63
+ this.name = "WorkspaceFileNotFoundError";
64
+ }
65
+ }
66
+
67
+ /**
68
+ * DI token for the workspace-scoped file storage adapter.
69
+ * Registered by `@codemation/host`; resolved by workspace-file nodes via `ctx.resolve(...)`.
70
+ */
71
+ export const WorkspaceFileStorageToken = Symbol.for("codemation.core.WorkspaceFileStorage") as TypeToken<
72
+ IWorkspaceFileStorage | undefined
73
+ >;
@@ -0,0 +1,61 @@
1
+ import type { OAuthMaterial } from "./OAuthFlowExecutor.types";
2
+
3
+ /**
4
+ * Material provider seam — see `docs/design/credentials-oauth-unification.md`,
5
+ * "Material provider seam" section. Sits beside the workspace's
6
+ * `CredentialStore`; persistence of the row stays at the store, persistence of
7
+ * the bytes goes through this provider so they can live at the control plane
8
+ * in managed mode.
9
+ */
10
+
11
+ /**
12
+ * Pointer to material bytes. For local rows `ref` is the workspace instance id
13
+ * and the bytes co-locate with the row (existing `CredentialOAuth2Material` /
14
+ * `CredentialSecretMaterial` tables). For control-plane rows `ref` is the
15
+ * CP-side credential id; the workspace stores only the pointer.
16
+ */
17
+ export type CredentialMaterialRef = Readonly<{
18
+ source: "local" | "control-plane";
19
+ id: string;
20
+ }>;
21
+
22
+ /**
23
+ * Decrypted material bytes returned by a provider. Shape matches
24
+ * `OAuthMaterial` — every supported credential type today is OAuth-shaped.
25
+ */
26
+ export type MaterialBundle = OAuthMaterial;
27
+
28
+ /**
29
+ * Caller context recorded by the CP material endpoint per fetch (D5 in the
30
+ * `credentials-vault` sprint README). The local provider accepts but ignores
31
+ * it; standalone mode has no audit log.
32
+ */
33
+ export type CallerContext = Readonly<{
34
+ workspaceId: string;
35
+ caller:
36
+ | Readonly<{ kind: "workflow-node"; workflowId: string; nodeId: string }>
37
+ | Readonly<{ kind: "concierge"; chatId: string }>
38
+ | Readonly<{ kind: "research-agent"; chatId: string }>
39
+ | Readonly<{ kind: "manual"; userId: string }>;
40
+ reason?: string;
41
+ }>;
42
+
43
+ export interface CredentialMaterialProvider {
44
+ getMaterial(ref: CredentialMaterialRef, context: CallerContext): Promise<MaterialBundle>;
45
+ setMaterial(ref: CredentialMaterialRef, material: MaterialBundle): Promise<void>;
46
+ }
47
+
48
+ /**
49
+ * Thrown by a provider when asked to operate on a `ref.source` it does not
50
+ * handle (e.g. the local provider being asked to read `control-plane` bytes).
51
+ * Exported so `instanceof`-checks work across the workspace boundary.
52
+ */
53
+ export class IllegalMaterialSourceError extends Error {
54
+ constructor(
55
+ public readonly source: CredentialMaterialRef["source"],
56
+ public readonly providerName: string,
57
+ ) {
58
+ super(`Provider "${providerName}" cannot handle material source "${source}".`);
59
+ this.name = "IllegalMaterialSourceError";
60
+ }
61
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Thrown by managed-mode providers when `setMaterial` is called. Managed
3
+ * credential bytes are owned by the control plane; the workspace must not
4
+ * mutate them. See `docs/design/credentials-oauth-unification.md` and
5
+ * `planning/sprints/credentials-vault/02-controlplane-material-provider.md`.
6
+ */
7
+ export class ManagedCredentialMaterialWriteError extends Error {
8
+ constructor(
9
+ message: string = "managed credentials are owned by the control plane; use the Connected apps page to create or modify them.",
10
+ ) {
11
+ super(message);
12
+ this.name = "ManagedCredentialMaterialWriteError";
13
+ }
14
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Thrown by `ControlPlaneCredentialMaterialProvider` when the control-plane
3
+ * material endpoint returns a non-2xx response or a malformed body. Exposes
4
+ * the HTTP status and the raw error body so call sites can surface actionable
5
+ * detail without parsing strings.
6
+ */
7
+ export class ManagedMaterialFetchError extends Error {
8
+ constructor(
9
+ public readonly status: number,
10
+ public readonly providerErrorBody: string,
11
+ message?: string,
12
+ ) {
13
+ super(message ?? `Control-plane material fetch failed: HTTP ${status} ${providerErrorBody}`);
14
+ this.name = "ManagedMaterialFetchError";
15
+ }
16
+ }
@@ -1,6 +1,8 @@
1
1
  import type {
2
2
  ConnectionInvocationRecord,
3
3
  EngineRunCounters,
4
+ PendingResumeEntry,
5
+ PersistedSuspensionEntry,
4
6
  PreparedNodeActivationDispatch,
5
7
  NodeActivationRequest,
6
8
  NodeActivationScheduler,
@@ -45,6 +47,16 @@ export type ActivationEnqueueRequest = {
45
47
  planner: RunQueuePlanner;
46
48
  engineCounters?: EngineRunCounters;
47
49
  connectionInvocations?: ReadonlyArray<ConnectionInvocationRecord>;
50
+ /**
51
+ * Remaining suspension entries after consuming one for a HITL resume.
52
+ * When provided, saved alongside the new pending state so they survive the enqueue.
53
+ */
54
+ suspension?: ReadonlyArray<PersistedSuspensionEntry>;
55
+ /**
56
+ * Resume context to attach to the re-activated node's execution context.
57
+ * Written here and consumed by `NodeExecutionRequestHandlerService` when building ctx.
58
+ */
59
+ pendingResume?: PendingResumeEntry;
48
60
  };
49
61
 
50
62
  export class ActivationEnqueueService {
@@ -114,6 +126,10 @@ export class ActivationEnqueueService {
114
126
  ...args.previousNodeSnapshotsByNodeId,
115
127
  [args.request.nodeId]: queuedSnapshot,
116
128
  },
129
+ // HITL: preserve suspension entries and resume context when re-activating a
130
+ // suspended node. Omit fields when not provided (avoids polluting normal enqueue).
131
+ ...(args.suspension !== undefined ? { suspension: args.suspension } : {}),
132
+ ...(args.pendingResume !== undefined ? { pendingResume: args.pendingResume } : {}),
117
133
  });
118
134
  await this.dispatchPreparedActivation(preparedDispatch);
119
135
  return {