@inflexa-ai/harness 0.37.0 → 0.39.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 (101) hide show
  1. package/README.md +1 -2
  2. package/dist/auth/local-run-authorizer.js +5 -4
  3. package/dist/auth/types.d.ts +28 -9
  4. package/dist/auth/types.js +5 -3
  5. package/dist/billing/cached-resolver.d.ts +11 -7
  6. package/dist/billing/cached-resolver.js +17 -11
  7. package/dist/billing/noop-run-charge.js +3 -2
  8. package/dist/billing/noop-usage-recorder.js +2 -1
  9. package/dist/billing/run-charge.d.ts +18 -5
  10. package/dist/billing/run-charge.js +5 -1
  11. package/dist/billing/usage-recorder.d.ts +11 -9
  12. package/dist/billing/usage-recorder.js +2 -3
  13. package/dist/contracts/chat-events.d.ts +4 -1
  14. package/dist/contracts/chat-parts.d.ts +1 -1
  15. package/dist/contracts/schemas/chat-parts.d.ts +4 -4
  16. package/dist/execution/artifact-registration.d.ts +6 -1
  17. package/dist/execution/artifact-registration.js +12 -4
  18. package/dist/execution/artifact-registry.d.ts +10 -3
  19. package/dist/execution/noop-artifact-registry.js +3 -4
  20. package/dist/execution/post-step-pipeline.d.ts +14 -3
  21. package/dist/execution/post-step-pipeline.js +21 -8
  22. package/dist/execution/run-authorizer.d.ts +5 -3
  23. package/dist/execution/run-canceler.js +4 -20
  24. package/dist/index.d.ts +14 -8
  25. package/dist/index.js +12 -6
  26. package/dist/lib/hooks.d.ts +49 -0
  27. package/dist/lib/hooks.js +39 -0
  28. package/dist/lib/result.d.ts +22 -11
  29. package/dist/lib/result.js +24 -14
  30. package/dist/loop/run-agent.d.ts +2 -2
  31. package/dist/loop/run-agent.js +5 -4
  32. package/dist/provenance/seam.d.ts +1 -1
  33. package/dist/providers/ai-sdk.d.ts +43 -5
  34. package/dist/providers/ai-sdk.js +55 -52
  35. package/dist/providers/anthropic.d.ts +2 -3
  36. package/dist/providers/anthropic.js +2 -2
  37. package/dist/providers/embedding.d.ts +13 -7
  38. package/dist/providers/embedding.js +26 -6
  39. package/dist/providers/errors.d.ts +67 -37
  40. package/dist/providers/errors.js +57 -58
  41. package/dist/providers/request-headers.d.ts +18 -0
  42. package/dist/providers/request-headers.js +22 -0
  43. package/dist/report-model/production-resolver.d.ts +5 -2
  44. package/dist/report-model/production-resolver.js +12 -2
  45. package/dist/runtime/assemble.d.ts +3 -2
  46. package/dist/runtime/dbos.d.ts +4 -3
  47. package/dist/runtime/dbos.js +18 -3
  48. package/dist/runtime/result-serialization.d.ts +29 -0
  49. package/dist/runtime/result-serialization.js +45 -0
  50. package/dist/sandbox/client.d.ts +11 -3
  51. package/dist/sandbox/create-sandbox.d.ts +5 -2
  52. package/dist/sandbox/create-sandbox.js +51 -25
  53. package/dist/sandbox/docker-client.d.ts +4 -3
  54. package/dist/sandbox/docker-client.js +23 -31
  55. package/dist/sandbox/k8s-client.d.ts +4 -13
  56. package/dist/sandbox/k8s-client.js +28 -61
  57. package/dist/sandbox/labels.d.ts +34 -0
  58. package/dist/sandbox/labels.js +47 -0
  59. package/dist/sandbox/mount-plan.d.ts +3 -1
  60. package/dist/sandbox/mount-plan.js +9 -0
  61. package/dist/sandbox/sandbox-error.d.ts +36 -13
  62. package/dist/sandbox/sandbox-error.js +32 -17
  63. package/dist/sandbox/types.d.ts +18 -10
  64. package/dist/schemas/validate-plan.js +13 -0
  65. package/dist/schemas/workflow-state.d.ts +4 -4
  66. package/dist/state/analyses.d.ts +4 -4
  67. package/dist/state/analyses.js +4 -4
  68. package/dist/state/schema.d.ts +5 -5
  69. package/dist/tasks/data-profile.d.ts +5 -8
  70. package/dist/tasks/data-profile.js +88 -43
  71. package/dist/tasks/derive-table-exec.d.ts +10 -6
  72. package/dist/tasks/derive-table-exec.js +17 -12
  73. package/dist/tasks/extract-values.d.ts +16 -10
  74. package/dist/tasks/extract-values.js +34 -18
  75. package/dist/tools/execute-analysis.js +29 -24
  76. package/dist/tools/report-session/derive-table.d.ts +9 -1
  77. package/dist/tools/report-session/derive-table.js +25 -22
  78. package/dist/tools/report-session/examine-page.d.ts +6 -3
  79. package/dist/tools/report-session/examine-page.js +16 -8
  80. package/dist/tools/report-session/index.d.ts +1 -1
  81. package/dist/tools/report-session/preview-report.js +11 -9
  82. package/dist/tools/report-session/session-page-publisher.d.ts +17 -21
  83. package/dist/tools/report-session/session-page-publisher.js +5 -7
  84. package/dist/tools/research/inspect-run.d.ts +1 -1
  85. package/dist/tools/sandbox/link-packages.d.ts +8 -0
  86. package/dist/tools/sandbox/link-packages.js +19 -9
  87. package/dist/workflows/execute-analysis.d.ts +25 -16
  88. package/dist/workflows/execute-analysis.js +223 -216
  89. package/dist/workflows/metrics.d.ts +4 -3
  90. package/dist/workflows/metrics.js +4 -3
  91. package/dist/workflows/sandbox-step.d.ts +20 -29
  92. package/dist/workflows/sandbox-step.js +71 -127
  93. package/dist/workflows/suspension.d.ts +32 -0
  94. package/dist/workflows/suspension.js +38 -0
  95. package/package.json +1 -1
  96. package/dist/billing/noop-resolver.d.ts +0 -6
  97. package/dist/billing/noop-resolver.js +0 -7
  98. package/dist/billing/resolver.d.ts +0 -64
  99. package/dist/billing/resolver.js +0 -24
  100. package/dist/loop/budget-exceeded.d.ts +0 -38
  101. package/dist/loop/budget-exceeded.js +0 -88
@@ -18,8 +18,10 @@
18
18
  * callback unroutable and it lands as a `400`. The pull backstop still settles such an exec, but it pays
19
19
  * the quiet interval first.
20
20
  */
21
+ import { type Result } from "neverthrow";
21
22
  import type { Logger } from "../lib/logger.js";
22
23
  import type { SandboxClient } from "../sandbox/client.js";
24
+ import { type Suspension } from "../workflows/suspension.js";
23
25
  import type { ExecResult } from "../sandbox/types.js";
24
26
  import { type DeriveTableExecInput } from "../tools/report-session/derive-table.js";
25
27
  /** The construction-time deps of the body. The registration closes over them, thus the trigger holds none. */
@@ -35,15 +37,17 @@ export interface DeriveTableExecDeps {
35
37
  /**
36
38
  * The body, exported so a test drives it without a registered workflow.
37
39
  *
38
- * A fault of any seam call throws. The tool reads a rejection as one short detail, thus the caller needs no
39
- * result type here.
40
+ * A fault of any seam call throws. The tool reads a rejection as one short detail. A chat turn awaits this
41
+ * workflow, thus a suspension is the `err` of the result, and the workflow does not cancel
42
+ * (workflow-suspension spec). The body does not mark the analysis: the tool reports the suspension to the
43
+ * conversation agent.
40
44
  */
41
- export declare function runDeriveTableExecBody(input: DeriveTableExecInput, deps: DeriveTableExecDeps): Promise<ExecResult>;
45
+ export declare function runDeriveTableExecBody(input: DeriveTableExecInput, deps: DeriveTableExecDeps): Promise<Result<ExecResult, Suspension>>;
42
46
  /**
43
47
  * Register the derivation workflow with DBOS. It gives back the registered callable, thus a trigger
44
48
  * dispatches with `DBOS.startWorkflow`.
45
49
  */
46
- export declare function registerDeriveTableExecWorkflow(deps: DeriveTableExecDeps): (input: DeriveTableExecInput) => Promise<ExecResult>;
50
+ export declare function registerDeriveTableExecWorkflow(deps: DeriveTableExecDeps): (input: DeriveTableExecInput) => Promise<Result<ExecResult, Suspension>>;
47
51
  /**
48
52
  * The workflow id of one derivation.
49
53
  *
@@ -55,6 +59,6 @@ export declare function deriveTableExecWorkflowId(executionId: string): string;
55
59
  * The async edge. It starts the registered workflow and it awaits the terminal result.
56
60
  *
57
61
  * The tool authorizes the derivation before this call and it revokes after, thus this edge holds no
58
- * lifecycle of its own. A workflow fault rejects the returned promise.
62
+ * lifecycle of its own. A workflow fault rejects the returned promise, and a suspension is the `err`.
59
63
  */
60
- export declare function triggerDeriveTableExec(workflow: (input: DeriveTableExecInput) => Promise<ExecResult>, input: DeriveTableExecInput): Promise<ExecResult>;
64
+ export declare function triggerDeriveTableExec(workflow: (input: DeriveTableExecInput) => Promise<Result<ExecResult, Suspension>>, input: DeriveTableExecInput): Promise<Result<ExecResult, Suspension>>;
@@ -19,16 +19,22 @@
19
19
  * the quiet interval first.
20
20
  */
21
21
  import { DBOS } from "@dbos-inc/dbos-sdk";
22
+ import { err, ok } from "neverthrow";
23
+ import { forStep } from "../auth/types.js";
22
24
  import { createNoopLogger } from "../lib/console-logger.js";
23
25
  import { mintSandboxIdentity } from "../sandbox/identity.js";
26
+ import { keepSuspendingRefusal } from "../sandbox/sandbox-error.js";
27
+ import { suspensionOfSpawnRefusal } from "../workflows/suspension.js";
24
28
  import { buildDerivationExec, DERIVATION_DEADLINE_MS, DERIVATION_RESOURCES, DERIVE_RUN_LITERAL, DERIVE_STEP_LITERAL, } from "../tools/report-session/derive-table.js";
25
29
  /** The awaitExec callback. A derivation reports no live activity, thus the callback drops each event. */
26
30
  const noopEmit = () => { };
27
31
  /**
28
32
  * The body, exported so a test drives it without a registered workflow.
29
33
  *
30
- * A fault of any seam call throws. The tool reads a rejection as one short detail, thus the caller needs no
31
- * result type here.
34
+ * A fault of any seam call throws. The tool reads a rejection as one short detail. A chat turn awaits this
35
+ * workflow, thus a suspension is the `err` of the result, and the workflow does not cancel
36
+ * (workflow-suspension spec). The body does not mark the analysis: the tool reports the suspension to the
37
+ * conversation agent.
32
38
  */
33
39
  export async function runDeriveTableExecBody(input, deps) {
34
40
  const logger = (deps.logger ?? createNoopLogger()).named("derive-table-exec").with({ analysisId: input.analysisId });
@@ -36,14 +42,13 @@ export async function runDeriveTableExecBody(input, deps) {
36
42
  // One workflow submits one exec, thus the function id is a constant. The three segments are what a
37
43
  // callback host parses to find the workflow that awaits this exec.
38
44
  const execId = `${workflowId}:${DERIVE_STEP_LITERAL}:fn-0`;
39
- const sandbox = await deps.sandboxClient.createSandbox({
40
- runId: DERIVE_RUN_LITERAL,
41
- stepId: DERIVE_STEP_LITERAL,
42
- analysisId: input.analysisId,
43
- childWorkflowId: workflowId,
44
- resources: DERIVATION_RESOURCES,
45
- writableTail: input.writableTail,
46
- }, mintSandboxIdentity(DERIVE_RUN_LITERAL));
45
+ const spawned = keepSuspendingRefusal(await deps.sandboxClient.createSandbox(forStep(input.runSession, DERIVE_STEP_LITERAL), { childWorkflowId: workflowId, resources: DERIVATION_RESOURCES, writableTail: input.writableTail }, mintSandboxIdentity(DERIVE_RUN_LITERAL)));
46
+ if (spawned.isErr()) {
47
+ const suspension = suspensionOfSpawnRefusal(spawned.error);
48
+ logger.warn("derivation suspended", { reason: suspension.reason });
49
+ return err(suspension);
50
+ }
51
+ const sandbox = spawned.value;
47
52
  try {
48
53
  // A checkpointed clock, not `Date.now()`: the await gates on this absolute deadline, and a
49
54
  // wall-clock deadline that grew on replay would shift which loop iteration crosses it.
@@ -55,7 +60,7 @@ export async function runDeriveTableExecBody(input, deps) {
55
60
  inputs: input.inputs,
56
61
  output: input.output,
57
62
  }));
58
- return await deps.sandboxClient.awaitExec(sandbox, execId, noopEmit, deadline);
63
+ return ok(await deps.sandboxClient.awaitExec(sandbox, execId, noopEmit, deadline));
59
64
  }
60
65
  finally {
61
66
  try {
@@ -86,7 +91,7 @@ export function deriveTableExecWorkflowId(executionId) {
86
91
  * The async edge. It starts the registered workflow and it awaits the terminal result.
87
92
  *
88
93
  * The tool authorizes the derivation before this call and it revokes after, thus this edge holds no
89
- * lifecycle of its own. A workflow fault rejects the returned promise.
94
+ * lifecycle of its own. A workflow fault rejects the returned promise, and a suspension is the `err`.
90
95
  */
91
96
  export async function triggerDeriveTableExec(workflow, input) {
92
97
  const handle = (await DBOS.startWorkflow(workflow, {
@@ -11,11 +11,13 @@
11
11
  * authorization on every terminal path. The container is ephemeral, and it goes away with the work, thus no
12
12
  * standing machine exists.
13
13
  */
14
- import type { AuthContext, RunSession } from "../auth/types.js";
14
+ import { type Result } from "neverthrow";
15
+ import { type AuthContext, type RunSession } from "../auth/types.js";
15
16
  import type { RunAuthorizer } from "../execution/run-authorizer.js";
16
17
  import type { Logger } from "../lib/logger.js";
17
18
  import type { ExtractionArm, ExtractionArtifact, ExtractionRequest } from "../report-model/production-resolver.js";
18
19
  import type { SandboxClient } from "../sandbox/client.js";
20
+ import { type Suspension } from "../workflows/suspension.js";
19
21
  import type { ExecResult, SubmitExecBody } from "../sandbox/types.js";
20
22
  import { type ExtractValuesResult } from "./extract-values-script.js";
21
23
  export type { ExtractValuesResult } from "./extract-values-script.js";
@@ -71,13 +73,16 @@ export declare function parseExtractionOutput(result: ExecResult): ExtractValues
71
73
  * The body, extracted so a test drives it without a registered workflow. It creates the ephemeral
72
74
  * container, submits one exec, awaits the result durably, and gives the value map back. It revokes the run
73
75
  * authorization on every terminal path, and it tears the container down on both paths.
76
+ *
77
+ * A chat turn awaits this workflow, thus a suspension is the `err` of the result, and the workflow does not
78
+ * cancel (workflow-suspension spec). The body does not mark the analysis: the caller reports the suspension.
74
79
  */
75
- export declare function runExtractValuesBody(input: ExtractValuesWorkflowInput, deps: ExtractValuesDeps): Promise<ExtractValuesResult>;
80
+ export declare function runExtractValuesBody(input: ExtractValuesWorkflowInput, deps: ExtractValuesDeps): Promise<Result<ExtractValuesResult, Suspension>>;
76
81
  /**
77
82
  * Register the extraction workflow with DBOS. It gives back the registered callable, thus a trigger
78
83
  * dispatches with `DBOS.startWorkflow`.
79
84
  */
80
- export declare function registerExtractValuesWorkflow(deps: ExtractValuesDeps): (input: ExtractValuesWorkflowInput) => Promise<ExtractValuesResult>;
85
+ export declare function registerExtractValuesWorkflow(deps: ExtractValuesDeps): (input: ExtractValuesWorkflowInput) => Promise<Result<ExtractValuesResult, Suspension>>;
81
86
  /** The per-attempt workflow id. A fresh nonce keeps each pass under a distinct id. */
82
87
  export declare function extractValuesWorkflowId(analysisId: string, nonce: string): string;
83
88
  /**
@@ -86,7 +91,7 @@ export declare function extractValuesWorkflowId(analysisId: string, nonce: strin
86
91
  */
87
92
  export interface ExtractValuesTriggerDeps {
88
93
  readonly runAuthorizer: RunAuthorizer;
89
- readonly workflow: (input: ExtractValuesWorkflowInput) => Promise<ExtractValuesResult>;
94
+ readonly workflow: (input: ExtractValuesWorkflowInput) => Promise<Result<ExtractValuesResult, Suspension>>;
90
95
  }
91
96
  /** The identity and the requests for one extraction pass. */
92
97
  export interface ExtractValuesTriggerParams {
@@ -96,12 +101,13 @@ export interface ExtractValuesTriggerParams {
96
101
  }
97
102
  /**
98
103
  * The async edge. It authorizes through the `RunAuthorizer`, starts the workflow, and awaits the outcome.
99
- * The body owns the revoke, thus the trigger holds no lifecycle. A workflow fault rejects the returned
100
- * promise.
104
+ * The body owns the revoke, thus the trigger holds no lifecycle. A workflow fault and a refused
105
+ * authorization reject the returned promise. A suspension is the `err` of the result: of the workflow, or of
106
+ * an authorization that the host refused with the suspend flag.
101
107
  */
102
- export declare function triggerExtractValues(deps: ExtractValuesTriggerDeps, params: ExtractValuesTriggerParams): Promise<ExtractValuesResult>;
103
- /** Runs one extraction pass for the bound analysis, and gives the raw value map back. */
104
- export type ExtractionRunner = (requests: readonly ExtractionRequest[]) => Promise<ExtractValuesResult>;
108
+ export declare function triggerExtractValues(deps: ExtractValuesTriggerDeps, params: ExtractValuesTriggerParams): Promise<Result<ExtractValuesResult, Suspension>>;
109
+ /** Runs one extraction pass for the bound analysis, and gives the raw value map back, or the suspension. */
110
+ export type ExtractionRunner = (requests: readonly ExtractionRequest[]) => Promise<Result<ExtractValuesResult, Suspension>>;
105
111
  /**
106
112
  * Bind the trigger to one analysis and one auth context. The result runs one pass with every request in it.
107
113
  */
@@ -119,6 +125,6 @@ export declare function extractionArtifactsFromResult(result: ExtractValuesResul
119
125
  /**
120
126
  * Make the extraction arm over a runner. One `extract` call makes one run with every request in it, and it
121
127
  * gives the value map. A run fault rejects the promise, and the resolver turns the rejection into a failed
122
- * reference.
128
+ * reference. A suspension is the `err`, and the resolver reports its reason on each reference.
123
129
  */
124
130
  export declare function createExtractionArm(run: ExtractionRunner): ExtractionArm;
@@ -12,10 +12,15 @@
12
12
  * standing machine exists.
13
13
  */
14
14
  import { DBOS } from "@dbos-inc/dbos-sdk";
15
+ import { err, ok } from "neverthrow";
15
16
  import { randomUUID } from "node:crypto";
17
+ import { forStep } from "../auth/types.js";
16
18
  import { createNoopLogger } from "../lib/console-logger.js";
19
+ import { deliverNotice, passGate } from "../lib/hooks.js";
17
20
  import { generateExecutionId } from "../sandbox/execution-id.js";
18
21
  import { mintSandboxIdentity } from "../sandbox/identity.js";
22
+ import { keepSuspendingRefusal } from "../sandbox/sandbox-error.js";
23
+ import { suspensionOfRefusal, suspensionOfSpawnRefusal } from "../workflows/suspension.js";
19
24
  import { EXTRACTION_INPUT_ENV, EXTRACTION_SCRIPT, ExtractValuesResultSchema } from "./extract-values-script.js";
20
25
  /** The synthetic run id and step id of the extraction pass. Both are constants shared by every analysis. */
21
26
  const EXTRACT_VALUES_RUN_LITERAL = "extract-values";
@@ -85,24 +90,28 @@ export function parseExtractionOutput(result) {
85
90
  * The body, extracted so a test drives it without a registered workflow. It creates the ephemeral
86
91
  * container, submits one exec, awaits the result durably, and gives the value map back. It revokes the run
87
92
  * authorization on every terminal path, and it tears the container down on both paths.
93
+ *
94
+ * A chat turn awaits this workflow, thus a suspension is the `err` of the result, and the workflow does not
95
+ * cancel (workflow-suspension spec). The body does not mark the analysis: the caller reports the suspension.
88
96
  */
89
97
  export async function runExtractValuesBody(input, deps) {
90
98
  const logger = (deps.logger ?? createNoopLogger()).named("extract-values").with({ analysisId: input.analysisId });
91
99
  const { analysisId, runSession, requests, ownsMandate = true } = input; // oss-core-managed-ok
92
100
  const authorization = { runSession, ownsMandate }; // oss-core-managed-ok
101
+ const revoke = (reason) => deliverNotice(logger, "RunAuthorizer.revoke", deps.runAuthorizer.revoke(authorization, reason));
93
102
  try {
94
103
  const executionId = generateExecutionId(EXTRACT_VALUES_AGENT_ID);
95
104
  const workflowId = DBOS.workflowID ?? `${EXTRACT_VALUES_RUN_LITERAL}:${executionId}`;
96
105
  // The container mounts the analysis tree read-only. The extraction pass only reads, thus it needs
97
106
  // no writable step mount.
98
- const sandbox = await deps.sandboxClient.createSandbox({
99
- runId: EXTRACT_VALUES_RUN_LITERAL,
100
- stepId: EXTRACT_VALUES_STEP_LITERAL,
101
- analysisId,
102
- childWorkflowId: workflowId,
103
- resources: EXTRACTION_RESOURCES,
104
- readOnly: true,
105
- }, mintSandboxIdentity(EXTRACT_VALUES_RUN_LITERAL));
107
+ const spawned = keepSuspendingRefusal(await deps.sandboxClient.createSandbox(forStep(runSession, EXTRACT_VALUES_STEP_LITERAL), { childWorkflowId: workflowId, resources: EXTRACTION_RESOURCES, readOnly: true }, mintSandboxIdentity(EXTRACT_VALUES_RUN_LITERAL)));
108
+ if (spawned.isErr()) {
109
+ const suspension = suspensionOfSpawnRefusal(spawned.error);
110
+ logger.warn("extraction suspended", { reason: suspension.reason });
111
+ await revoke("extract-values-suspended");
112
+ return err(suspension);
113
+ }
114
+ const sandbox = spawned.value;
106
115
  try {
107
116
  // A checkpointed clock, not `Date.now()`: the await gates on this absolute deadline, and a
108
117
  // wall-clock deadline that grew on replay would shift which loop iteration crosses it.
@@ -110,8 +119,8 @@ export async function runExtractValuesBody(input, deps) {
110
119
  await deps.sandboxClient.submitExec(sandbox, buildExtractionExec(analysisId, requests, executionId));
111
120
  const result = await deps.sandboxClient.awaitExec(sandbox, executionId, noopEmit, deadlineAbs);
112
121
  const map = parseExtractionOutput(result);
113
- await deps.runAuthorizer.revoke(authorization, "extract-values-completed");
114
- return map;
122
+ await revoke("extract-values-completed");
123
+ return ok(map);
115
124
  }
116
125
  finally {
117
126
  try {
@@ -124,7 +133,7 @@ export async function runExtractValuesBody(input, deps) {
124
133
  }
125
134
  catch (err) {
126
135
  logger.error("extraction failed", logger.errorFields(err));
127
- await deps.runAuthorizer.revoke(authorization, "extract-values-failed");
136
+ await revoke("extract-values-failed");
128
137
  throw err;
129
138
  }
130
139
  }
@@ -141,18 +150,25 @@ export function extractValuesWorkflowId(analysisId, nonce) {
141
150
  }
142
151
  /**
143
152
  * The async edge. It authorizes through the `RunAuthorizer`, starts the workflow, and awaits the outcome.
144
- * The body owns the revoke, thus the trigger holds no lifecycle. A workflow fault rejects the returned
145
- * promise.
153
+ * The body owns the revoke, thus the trigger holds no lifecycle. A workflow fault and a refused
154
+ * authorization reject the returned promise. A suspension is the `err` of the result: of the workflow, or of
155
+ * an authorization that the host refused with the suspend flag.
146
156
  */
147
157
  export async function triggerExtractValues(deps, params) {
148
158
  const { auth, analysisId, requests } = params;
149
- const { runSession, ownsMandate } = await deps.runAuthorizer.authorize({
150
- // oss-core-managed-ok
159
+ const authorized = await passGate("RunAuthorizer.authorize", deps.runAuthorizer.authorize({
151
160
  auth,
152
161
  scope: { kind: "analysis", analysisId },
153
162
  provenance: { agentId: EXTRACT_VALUES_AGENT_ID, callPath: [EXTRACT_VALUES_AGENT_ID] },
154
163
  frame: { runId: EXTRACT_VALUES_RUN_LITERAL, stepId: EXTRACT_VALUES_STEP_LITERAL },
155
- });
164
+ }));
165
+ if (authorized.isErr()) {
166
+ const suspension = suspensionOfRefusal(authorized.error);
167
+ if (suspension !== undefined)
168
+ return err(suspension);
169
+ throw new Error(`the extraction was not authorized: ${authorized.error.reason}`);
170
+ }
171
+ const { runSession, ownsMandate } = authorized.value; // oss-core-managed-ok
156
172
  const handle = (await DBOS.startWorkflow(deps.workflow, {
157
173
  workflowID: extractValuesWorkflowId(analysisId, randomUUID()),
158
174
  })({ analysisId, runSession, requests: [...requests], ownsMandate })); // oss-core-managed-ok
@@ -182,12 +198,12 @@ export function extractionArtifactsFromResult(result) {
182
198
  /**
183
199
  * Make the extraction arm over a runner. One `extract` call makes one run with every request in it, and it
184
200
  * gives the value map. A run fault rejects the promise, and the resolver turns the rejection into a failed
185
- * reference.
201
+ * reference. A suspension is the `err`, and the resolver reports its reason on each reference.
186
202
  */
187
203
  export function createExtractionArm(run) {
188
204
  return {
189
205
  async extract(requests) {
190
- return extractionArtifactsFromResult(await run(requests));
206
+ return (await run(requests)).map(extractionArtifactsFromResult);
191
207
  },
192
208
  };
193
209
  }
@@ -3,21 +3,23 @@
3
3
  * hoc analyses. Both modes launch the ordinary executeAnalysis workflow.
4
4
  */
5
5
  import { randomUUID } from "node:crypto";
6
- import { ok } from "neverthrow";
6
+ import { err, ok } from "neverthrow";
7
7
  import { z } from "zod";
8
8
  import { SANDBOX_AGENT_META } from "../agents/sandbox/index.js";
9
9
  import { DEFAULT_SANDBOX_MAX_STEPS } from "../config/resource-limits.js";
10
10
  import { createNoopLogger } from "../lib/console-logger.js";
11
+ import { deliverNotice, passGate } from "../lib/hooks.js";
11
12
  import { unwrapOrThrow } from "../lib/result.js";
12
13
  import { buildRunCardData } from "../memory/card-builders.js";
13
14
  import { formatQuery, parseQuery } from "../sandbox/package-identity.js";
14
15
  import { AnalysisPlanSchema } from "../schemas/workflow-state.js";
15
16
  import { validatePlan } from "../schemas/validate-plan.js";
16
- import { RunDedupCollisionError, insertRun, loadPlan, queryActiveRun, reserveRunById, updateRunStatus, upsertPlan } from "../state/index.js";
17
+ import { RunDedupCollisionError, insertRun, loadPlan, queryActiveRun, reserveRunById, suspendAnalysis, updateRunStatus, upsertPlan } from "../state/index.js";
17
18
  import { routeAdHocRequest } from "./ad-hoc-router.js";
18
19
  import { adHocPlanId, adHocRunId } from "./analysis-invocation.js";
19
20
  import { defineTool } from "./define-tool.js";
20
21
  import { queryPackages, readInventorySections } from "./sandbox/list-available-packages.js";
22
+ import { callExtendAnalysisFarm } from "./sandbox/link-packages.js";
21
23
  const planIdSchema = z.string().regex(/^pln-[a-f0-9]{8}$/, "planId must be a pln-<8hex> value");
22
24
  const inputSchema = z
23
25
  .object({
@@ -119,16 +121,13 @@ async function linkPlanPackages(extendAnalysisFarm, analysisId, plan) {
119
121
  const queries = [...union.values()];
120
122
  if (queries.length === 0)
121
123
  return 0;
122
- let outcomes;
123
- try {
124
- outcomes = await extendAnalysisFarm(analysisId, queries);
125
- }
126
- catch (cause) {
127
- // A realization throw is an `unavailable` answer, not a driver error.
128
- // The sibling resolver seam maps a throw the same way (farm.ts), and
129
- // the planner can act on "the store cannot answer" — not on a stack.
130
- throw new PlanPackagesMissingError([], [], cause instanceof Error ? cause.message : String(cause));
124
+ const linked = await callExtendAnalysisFarm(extendAnalysisFarm, analysisId, queries);
125
+ if (linked.isErr()) {
126
+ // A call that gave no answer is an `unavailable` answer, not a driver
127
+ // error: the planner can act on "the store cannot answer".
128
+ throw new PlanPackagesMissingError([], [], linked.error);
131
129
  }
130
+ const outcomes = linked.value;
132
131
  // An `unavailable` outcome preempts everything: the link pass could not
133
132
  // answer at all (an unreadable graph, a locked farm), so a per-package
134
133
  // absence list would be a fabrication. One reason covers the batch.
@@ -383,19 +382,25 @@ export function createExecuteAnalysisTool(deps) {
383
382
  return ok({ runId: reservation.row.runId, status: "in_progress" });
384
383
  }
385
384
  }
386
- let authorization;
387
- try {
388
- authorization = await deps.runAuthorizer.authorize({
389
- auth: ctx.session.auth,
390
- scope: ctx.session.scope,
391
- provenance: ctx.session.provenance,
392
- frame: { runId },
393
- });
394
- }
395
- catch (error) {
396
- await updateRunStatus(deps.pool, runId, "failed", "run authorization failed").match(() => { }, () => { });
397
- throw error;
385
+ const authorized = await passGate("RunAuthorizer.authorize", deps.runAuthorizer.authorize({
386
+ auth: ctx.session.auth,
387
+ scope: ctx.session.scope,
388
+ provenance: ctx.session.provenance,
389
+ frame: { runId },
390
+ }));
391
+ if (authorized.isErr()) {
392
+ // The refusal ends the run before a workflow exists, thus the
393
+ // reserved row must not stay `running`.
394
+ const { kind, reason } = authorized.error;
395
+ if (kind === "suspended") {
396
+ await updateRunStatus(deps.pool, runId, "canceled", reason).match(() => { }, () => { });
397
+ unwrapOrThrow(await suspendAnalysis(deps.pool, analysisId));
398
+ return err({ error: `The analysis is suspended: ${reason}`, retryable: false });
399
+ }
400
+ await updateRunStatus(deps.pool, runId, "failed", reason).match(() => { }, () => { });
401
+ return err({ error: `The run was not authorized: ${reason}`, retryable: false });
398
402
  }
403
+ const authorization = authorized.value;
399
404
  const wfInput = workflowInput(planId, plan, {
400
405
  analysisId,
401
406
  threadId,
@@ -408,7 +413,7 @@ export function createExecuteAnalysisTool(deps) {
408
413
  await deps.runLauncher.launch(deps.executeAnalysisWorkflow, { workflowId: runId }, wfInput);
409
414
  }
410
415
  catch (error) {
411
- await deps.runAuthorizer.revoke(authorization, "workflow-start-failed").catch(() => { });
416
+ await deliverNotice(logger, "RunAuthorizer.revoke", deps.runAuthorizer.revoke(authorization, "workflow-start-failed"));
412
417
  await updateRunStatus(deps.pool, runId, "failed", "workflow start failed").match(() => { }, () => { });
413
418
  throw error;
414
419
  }
@@ -33,7 +33,10 @@
33
33
  * under the callback transport, and a report turn is not a body. Thus the tool takes an injected runner,
34
34
  * and a registered workflow owns the container. The tool holds no sandbox client, and it imports no DBOS.
35
35
  */
36
+ import { type Result } from "neverthrow";
36
37
  import { z } from "zod";
38
+ import type { RunSession } from "../../auth/types.js";
39
+ import type { Suspension } from "../../workflows/suspension.js";
37
40
  import type { RunAuthorizer } from "../../execution/run-authorizer.js";
38
41
  import { type Logger } from "../../lib/logger.js";
39
42
  import type { ResourceSpec } from "../../config/resource-limits.js";
@@ -90,6 +93,10 @@ export type DeriveTableResult = {
90
93
  } | {
91
94
  outcome: "exec-failed";
92
95
  detail: string;
96
+ } | {
97
+ outcome: "suspended";
98
+ reason: string;
99
+ detail: string;
93
100
  } | {
94
101
  outcome: "no-output";
95
102
  detail: string;
@@ -172,6 +179,7 @@ export declare function describeExecFailure(result: ExecResult): string | undefi
172
179
  */
173
180
  export interface DeriveTableExecInput {
174
181
  readonly analysisId: string;
182
+ readonly runSession: RunSession;
175
183
  readonly executionId: string;
176
184
  readonly script: string;
177
185
  readonly writableTail: string;
@@ -186,7 +194,7 @@ export interface DeriveTableExecInput {
186
194
  * and the await is legal under each transport. A fault of the sandbox rejects the promise, and the tool
187
195
  * turns that rejection into one short detail.
188
196
  */
189
- export type DeriveTableRunner = (input: DeriveTableExecInput) => Promise<ExecResult>;
197
+ export type DeriveTableRunner = (input: DeriveTableExecInput) => Promise<Result<ExecResult, Suspension>>;
190
198
  /**
191
199
  * Make the derivation tool over the session-state gateway, the derivation ledger, and the sandbox rails.
192
200
  *
@@ -38,6 +38,7 @@ import { unlink } from "node:fs/promises";
38
38
  import { join } from "node:path";
39
39
  import { z } from "zod";
40
40
  import { createNoopLogger } from "../../lib/console-logger.js";
41
+ import { deliverNotice, passGate } from "../../lib/hooks.js";
41
42
  import { classifyWithinRoot, computeSha256, computeSha256File } from "../../lib/fs-helpers.js";
42
43
  import { defaultErrorFields } from "../../lib/logger.js";
43
44
  import { snapshotEntry } from "../../report-model/reference-resolver.js";
@@ -223,19 +224,20 @@ export function createDeriveTableTool(deps) {
223
224
  logger.warn("the workspace root did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
224
225
  return ok({ outcome: "unavailable", detail: "the workspace root did not resolve, thus the derived table has no place to land" });
225
226
  }
226
- let authorization;
227
- try {
228
- authorization = await runAuthorizer.authorize({
229
- auth: ctx.session.auth,
230
- scope: { kind: "analysis", analysisId },
231
- provenance: { agentId: DERIVE_AGENT_ID, callPath: [DERIVE_AGENT_ID] },
232
- frame: { runId: DERIVE_RUN_LITERAL, stepId: DERIVE_STEP_LITERAL },
233
- });
234
- }
235
- catch (cause) {
236
- logger.error("the derivation was not authorized", { threadId, analysisId, ...defaultErrorFields(cause) });
237
- return ok({ outcome: "unavailable", detail: "the derivation was not authorized" });
227
+ const authorized = await passGate("RunAuthorizer.authorize", runAuthorizer.authorize({
228
+ auth: ctx.session.auth,
229
+ scope: { kind: "analysis", analysisId },
230
+ provenance: { agentId: DERIVE_AGENT_ID, callPath: [DERIVE_AGENT_ID] },
231
+ frame: { runId: DERIVE_RUN_LITERAL, stepId: DERIVE_STEP_LITERAL },
232
+ }));
233
+ if (authorized.isErr()) {
234
+ const { kind, reason } = authorized.error;
235
+ logger.error("the derivation was not authorized", { threadId, analysisId, reason, suspend: kind === "suspended" });
236
+ if (kind === "suspended")
237
+ return ok({ outcome: "suspended", reason, detail: `the analysis is suspended (${reason}), thus the derivation did not run` });
238
+ return ok({ outcome: "unavailable", detail: `the derivation was not authorized: ${reason}` });
238
239
  }
240
+ const authorization = authorized.value;
239
241
  // The work sits inside the `try`, and the revoke sits in the `finally`. Thus the authorization
240
242
  // revokes on every terminal path, the thrown one included: a path builder refuses a hostile
241
243
  // stored path with a throw, and that throw must not leave an authorization standing.
@@ -247,6 +249,7 @@ export function createDeriveTableTool(deps) {
247
249
  root,
248
250
  threadId,
249
251
  analysisId,
252
+ runSession: authorization.runSession,
250
253
  script: input.script,
251
254
  sources,
252
255
  writableTail,
@@ -270,14 +273,9 @@ export function createDeriveTableTool(deps) {
270
273
  return ok(result);
271
274
  }
272
275
  finally {
273
- // A revoke fault changes no outcome of the call, thus it reaches the log alone. An absent
274
- // result means that the work threw, and a throw is a failed derivation.
275
- try {
276
- await runAuthorizer.revoke(authorization, result?.outcome === "derived" ? "derive-table-completed" : "derive-table-failed");
277
- }
278
- catch (cause) {
279
- logger.warn("the derivation authorization did not revoke", { threadId, analysisId, ...defaultErrorFields(cause) });
280
- }
276
+ // The revoke is a notice: its failure changes no outcome of the call, thus it reaches the log
277
+ // alone. An absent result means that the work threw, and a throw is a failed derivation.
278
+ await deliverNotice(logger.with({ threadId, analysisId }), "RunAuthorizer.revoke", runAuthorizer.revoke(authorization, result?.outcome === "derived" ? "derive-table-completed" : "derive-table-failed"));
281
279
  }
282
280
  },
283
281
  });
@@ -318,6 +316,7 @@ async function derive(args) {
318
316
  // thus that refusal stays a throw and it never reads as a fault of the container.
319
317
  const execInput = {
320
318
  analysisId: args.analysisId,
319
+ runSession: args.runSession,
321
320
  executionId,
322
321
  script: args.script,
323
322
  writableTail: args.writableTail,
@@ -325,14 +324,18 @@ async function derive(args) {
325
324
  inputs: args.sources.map((source) => ({ path: toSandboxPath(args.root, args.analysisId, join(args.root, source.path)), hash: source.hash })),
326
325
  output: toSandboxPath(args.root, args.analysisId, absolute),
327
326
  };
328
- let executed;
327
+ let ran;
329
328
  try {
330
- executed = await args.runDerivation(execInput);
329
+ ran = await args.runDerivation(execInput);
331
330
  }
332
331
  catch (cause) {
333
332
  args.logger.error("the derivation exec did not complete", { threadId: args.threadId, ...defaultErrorFields(cause) });
334
333
  return { outcome: "exec-failed", detail: "the derivation exec did not complete" };
335
334
  }
335
+ if (ran.isErr()) {
336
+ return { outcome: "suspended", reason: ran.error.reason, detail: `the analysis is suspended (${ran.error.reason}), thus the derivation did not run` };
337
+ }
338
+ const executed = ran.value;
336
339
  const failure = describeExecFailure(executed);
337
340
  if (failure !== undefined) {
338
341
  return { outcome: "exec-failed", detail: failure };
@@ -37,16 +37,19 @@
37
37
  * one and the look cannot count. The tool then gives a missed-stamp outcome that directs a new preview,
38
38
  * because a repeated look never stamps a marker that no preview wrote.
39
39
  *
40
- * The eyes seam, the chrome navigation, and the workspace-root seam each speak the throw protocol. The tool
41
- * guards each of them, thus a fault of a look becomes a typed outcome and the loop never sees a throw.
40
+ * The eyes seam, the chrome navigation, and the workspace-root seam each speak the throw protocol. The URL
41
+ * seam gives a `Result`. The tool guards each of them, thus a fault of a look becomes a typed outcome and
42
+ * the loop never sees a throw.
42
43
  *
43
44
  * A hang is not a throw, and a guard alone never ends one. Thus the acquire and the release each carry a
44
45
  * deadline. A realization that hangs while it starts a browser would otherwise hold the whole agent turn,
45
46
  * and no outcome would ever arrive.
46
47
  */
48
+ import { type ResultAsync } from "neverthrow";
47
49
  import { z } from "zod";
48
50
  import type { AuthContext } from "../../auth/types.js";
49
51
  import { type ChromeConfig } from "../../lib/chrome.js";
52
+ import type { GateFailure } from "../../lib/hooks.js";
50
53
  import { type AcquireEyes } from "../../lib/eyes.js";
51
54
  import { type Logger } from "../../lib/logger.js";
52
55
  import { type CaptureCoverage, type CapturePage, type FailedRequest, type PageCapture } from "../../lib/page-capture.js";
@@ -65,7 +68,7 @@ export type ResolvePageUrl = (args: {
65
68
  analysisId: string;
66
69
  threadId: string;
67
70
  auth: AuthContext;
68
- }) => Promise<string>;
71
+ }) => ResultAsync<string, GateFailure>;
69
72
  /** The empty input. The tool examines the current page of the thread, thus it needs no field. */
70
73
  declare const examinePageInput: z.ZodObject<{}, z.core.$strip>;
71
74
  export type ExaminePageInput = z.infer<typeof examinePageInput>;
@@ -37,8 +37,9 @@
37
37
  * one and the look cannot count. The tool then gives a missed-stamp outcome that directs a new preview,
38
38
  * because a repeated look never stamps a marker that no preview wrote.
39
39
  *
40
- * The eyes seam, the chrome navigation, and the workspace-root seam each speak the throw protocol. The tool
41
- * guards each of them, thus a fault of a look becomes a typed outcome and the loop never sees a throw.
40
+ * The eyes seam, the chrome navigation, and the workspace-root seam each speak the throw protocol. The URL
41
+ * seam gives a `Result`. The tool guards each of them, thus a fault of a look becomes a typed outcome and
42
+ * the loop never sees a throw.
42
43
  *
43
44
  * A hang is not a throw, and a guard alone never ends one. Thus the acquire and the release each carry a
44
45
  * deadline. A realization that hangs while it starts a browser would otherwise hold the whole agent turn,
@@ -273,14 +274,21 @@ export function createExaminePageTool(deps) {
273
274
  logger.warn("the session page could not be read", { threadId, analysisId, ...defaultErrorFields(cause) });
274
275
  return ok({ outcome: "capture-failed", detail: "the session page could not be read" });
275
276
  }
276
- // The URL seam speaks the throw protocol like the other seams, thus a fault of the formation
277
- // becomes a typed outcome and the look never starts.
277
+ // An `err` of the URL seam and a rejected realization each become a typed outcome, and the look
278
+ // never starts.
278
279
  let url;
279
280
  try {
280
- url =
281
- deps.resolvePageUrl !== undefined
282
- ? await deps.resolvePageUrl({ pagePath, analysisId, threadId, auth: ctx.session.auth })
283
- : pathToFileURL(pagePath).href;
281
+ if (deps.resolvePageUrl === undefined) {
282
+ url = pathToFileURL(pagePath).href;
283
+ }
284
+ else {
285
+ const resolved = await deps.resolvePageUrl({ pagePath, analysisId, threadId, auth: ctx.session.auth });
286
+ if (resolved.isErr()) {
287
+ logger.warn("the page URL did not resolve", { threadId, analysisId, reason: resolved.error.reason });
288
+ return ok({ outcome: "capture-failed", detail: "the page URL did not resolve" });
289
+ }
290
+ url = resolved.value;
291
+ }
284
292
  }
285
293
  catch (cause) {
286
294
  logger.warn("the page URL did not resolve", { threadId, analysisId, ...defaultErrorFields(cause) });
@@ -7,7 +7,7 @@
7
7
  */
8
8
  export { createPreviewReportTool, type PreviewReportInput, type PreviewReportResult, type PreviewReportToolDeps, type ResolvePageAsset, type SessionPageAccess, } from "./preview-report.js";
9
9
  export { createExaminePageTool, type CaptureCoverage, type CapturePage, type ExaminedTile, type ExaminePageInput, type ExaminePageResult, type ExaminePageToolDeps, type FailedRequest, type PageCapture, type ResolvePageUrl, } from "./examine-page.js";
10
- export { describeSessionPageMintFailure, UnavailableSessionPagePublisher, type MakeSessionPagePublisher, type SessionPageMintFailure, type SessionPageMintResult, type SessionPagePublisher, } from "./session-page-publisher.js";
10
+ export { describeSessionPageMintFailure, UnavailableSessionPagePublisher, type MakeSessionPagePublisher, type SessionPageGrant, type SessionPageMintFailure, type SessionPagePublisher, } from "./session-page-publisher.js";
11
11
  export { createRecordVersionTool, type RecordVersionInput, type RecordVersionResult, type RecordVersionToolDeps } from "./record-version.js";
12
12
  export { createDeriveTableTool, type DeriveTableInput, type DeriveTableResult, type DeriveTableToolDeps } from "./derive-table.js";
13
13
  export { createListPinnedArtifactsTool, type ListPinnedArtifactsInput, type ListPinnedArtifactsResult, type ListPinnedArtifactsToolDeps, type PinnedArtifact, type PinnedCitation, } from "./list-artifacts.js";