@stigmer/runner 3.40.0 → 3.41.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.
- package/README.md +3 -1
- package/dist/.build-fingerprint +1 -1
- package/dist/__test-utils__/execution-record-fixture.d.ts +2 -1
- package/dist/__test-utils__/execution-record-fixture.js.map +1 -1
- package/dist/activities/call-agent.js +1 -1
- package/dist/activities/discover-mcp-server.d.ts +7 -1
- package/dist/activities/discover-mcp-server.js +13 -29
- package/dist/activities/discover-mcp-server.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/fold.d.ts +4 -2
- package/dist/activities/execute-cursor/__test-utils__/fold.js +3 -4
- package/dist/activities/execute-cursor/__test-utils__/fold.js.map +1 -1
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js +1 -0
- package/dist/activities/execute-cursor/__test-utils__/hermetic-cursor.js.map +1 -1
- package/dist/activities/execute-cursor/approval-policy.d.ts +1 -29
- package/dist/activities/execute-cursor/approval-policy.js +11 -35
- package/dist/activities/execute-cursor/approval-policy.js.map +1 -1
- package/dist/activities/execute-cursor/boundary-rows.js +4 -4
- package/dist/activities/execute-cursor/boundary-rows.js.map +1 -1
- package/dist/activities/execute-cursor/translator.d.ts +12 -17
- package/dist/activities/execute-cursor/translator.js +13 -30
- package/dist/activities/execute-cursor/translator.js.map +1 -1
- package/dist/activities/execute-cursor/turn-stream.js +4 -2
- package/dist/activities/execute-cursor/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.d.ts +11 -1
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js +22 -3
- package/dist/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.js.map +1 -1
- package/dist/activities/execute-deep-agent/inline-publisher.d.ts +33 -2
- package/dist/activities/execute-deep-agent/inline-publisher.js +33 -7
- package/dist/activities/execute-deep-agent/inline-publisher.js.map +1 -1
- package/dist/activities/execute-deep-agent/platform-route.d.ts +48 -13
- package/dist/activities/execute-deep-agent/platform-route.js +87 -19
- package/dist/activities/execute-deep-agent/platform-route.js.map +1 -1
- package/dist/activities/execute-deep-agent/prompt-builder.d.ts +70 -35
- package/dist/activities/execute-deep-agent/prompt-builder.js +70 -55
- package/dist/activities/execute-deep-agent/prompt-builder.js.map +1 -1
- package/dist/activities/execute-deep-agent/streaming-side-effects.d.ts +20 -3
- package/dist/activities/execute-deep-agent/streaming-side-effects.js +26 -4
- package/dist/activities/execute-deep-agent/streaming-side-effects.js.map +1 -1
- package/dist/activities/execute-deep-agent/translator.d.ts +5 -4
- package/dist/activities/execute-deep-agent/translator.js +5 -4
- package/dist/activities/execute-deep-agent/translator.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-settle.js +6 -6
- package/dist/activities/execute-deep-agent/turn-settle.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-setup.d.ts +18 -13
- package/dist/activities/execute-deep-agent/turn-setup.js +42 -36
- package/dist/activities/execute-deep-agent/turn-setup.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn-stream.d.ts +10 -6
- package/dist/activities/execute-deep-agent/turn-stream.js +6 -16
- package/dist/activities/execute-deep-agent/turn-stream.js.map +1 -1
- package/dist/activities/execute-deep-agent/turn.d.ts +4 -1
- package/dist/activities/execute-deep-agent/turn.js +15 -6
- package/dist/activities/execute-deep-agent/turn.js.map +1 -1
- package/dist/activities/workflow-event-activities.js +2 -0
- package/dist/activities/workflow-event-activities.js.map +1 -1
- package/dist/config.d.ts +9 -0
- package/dist/config.js +2 -0
- package/dist/config.js.map +1 -1
- package/dist/exit-backstop.d.ts +64 -0
- package/dist/exit-backstop.js +79 -0
- package/dist/exit-backstop.js.map +1 -0
- package/dist/harness/terminal-table.d.ts +12 -5
- package/dist/harness/terminal-table.js +13 -13
- package/dist/harness/terminal-table.js.map +1 -1
- package/dist/harness/transcript/builder.d.ts +21 -0
- package/dist/harness/transcript/builder.js +53 -31
- package/dist/harness/transcript/builder.js.map +1 -1
- package/dist/harness/transcript/events.d.ts +22 -26
- package/dist/harness/transcript/events.js +6 -5
- package/dist/harness/transcript/events.js.map +1 -1
- package/dist/harness/turn-context.d.ts +3 -1
- package/dist/harness/turn-context.js +5 -3
- package/dist/harness/turn-context.js.map +1 -1
- package/dist/main.js +10 -4
- package/dist/main.js.map +1 -1
- package/dist/middleware/cost-advisory.d.ts +6 -3
- package/dist/middleware/cost-advisory.js +6 -3
- package/dist/middleware/cost-advisory.js.map +1 -1
- package/dist/middleware/execution-budget.d.ts +22 -7
- package/dist/middleware/execution-budget.js +57 -35
- package/dist/middleware/execution-budget.js.map +1 -1
- package/dist/middleware/index.d.ts +10 -7
- package/dist/middleware/index.js +10 -7
- package/dist/middleware/index.js.map +1 -1
- package/dist/middleware/types.d.ts +6 -1
- package/dist/runner-manager.js +3 -2
- package/dist/runner-manager.js.map +1 -1
- package/dist/runner.d.ts +25 -0
- package/dist/runner.js +41 -14
- package/dist/runner.js.map +1 -1
- package/dist/shared/approval-policy.d.ts +23 -13
- package/dist/shared/approval-policy.js +47 -14
- package/dist/shared/approval-policy.js.map +1 -1
- package/dist/shared/caller-identity.d.ts +4 -5
- package/dist/shared/caller-identity.js +4 -5
- package/dist/shared/caller-identity.js.map +1 -1
- package/dist/shared/channel-attachment.js +3 -2
- package/dist/shared/channel-attachment.js.map +1 -1
- package/dist/shared/connect-backfill.d.ts +2 -1
- package/dist/shared/connect-backfill.js +2 -2
- package/dist/shared/connect-backfill.js.map +1 -1
- package/dist/shared/mcp-resolver.d.ts +10 -1
- package/dist/shared/mcp-resolver.js +11 -2
- package/dist/shared/mcp-resolver.js.map +1 -1
- package/dist/shared/memory-attachment.js +3 -2
- package/dist/shared/memory-attachment.js.map +1 -1
- package/dist/shared/model-error.d.ts +25 -3
- package/dist/shared/model-error.js +78 -10
- package/dist/shared/model-error.js.map +1 -1
- package/dist/shared/platform-server-address.d.ts +71 -0
- package/dist/shared/platform-server-address.js +103 -0
- package/dist/shared/platform-server-address.js.map +1 -0
- package/dist/shared/prompt-sections.d.ts +6 -28
- package/dist/shared/prompt-sections.js +6 -35
- package/dist/shared/prompt-sections.js.map +1 -1
- package/dist/shared/skill-resolver.d.ts +2 -3
- package/dist/shared/skill-resolver.js +2 -3
- package/dist/shared/skill-resolver.js.map +1 -1
- package/dist/shared/synthesized-attachment.d.ts +0 -5
- package/dist/shared/synthesized-attachment.js +0 -12
- package/dist/shared/synthesized-attachment.js.map +1 -1
- package/dist/shared/tool-rounds.d.ts +51 -23
- package/dist/shared/tool-rounds.js +67 -24
- package/dist/shared/tool-rounds.js.map +1 -1
- package/dist/shared/workspace/platform-dir.d.ts +15 -0
- package/dist/shared/workspace/platform-dir.js +18 -1
- package/dist/shared/workspace/platform-dir.js.map +1 -1
- package/dist/shared/workspace/sources/git.js +13 -6
- package/dist/shared/workspace/sources/git.js.map +1 -1
- package/dist/shared/workspace/stigmer-link.d.ts +21 -14
- package/dist/shared/workspace/stigmer-link.js +91 -29
- package/dist/shared/workspace/stigmer-link.js.map +1 -1
- package/dist/tools/url-guard.d.ts +5 -1
- package/dist/tools/url-guard.js +9 -2
- package/dist/tools/url-guard.js.map +1 -1
- package/dist/worker.d.ts +11 -2
- package/dist/worker.js +1 -1
- package/dist/worker.js.map +1 -1
- package/dist/workflow-engine/tasks/human-input.d.ts +16 -0
- package/dist/workflow-engine/tasks/human-input.js +79 -37
- package/dist/workflow-engine/tasks/human-input.js.map +1 -1
- package/dist/workflow-engine/types.d.ts +14 -4
- package/dist/workflow-engine/types.js.map +1 -1
- package/dist/workflows/call-agent-orchestrator.d.ts +13 -0
- package/dist/workflows/call-agent-orchestrator.js.map +1 -1
- package/dist/workflows/engine-core.d.ts +25 -0
- package/dist/workflows/engine-core.js.map +1 -1
- package/dist/workflows/execute-from-execution.d.ts +3 -0
- package/dist/workflows/execute-from-execution.js.map +1 -1
- package/dist/workflows/human-input-orchestrator.d.ts +9 -12
- package/dist/workflows/human-input-orchestrator.js +12 -44
- package/dist/workflows/human-input-orchestrator.js.map +1 -1
- package/package.json +5 -5
- package/src/__test-utils__/config-fixture.ts +1 -0
- package/src/__test-utils__/execution-record-fixture.ts +2 -1
- package/src/__test-utils__/harness-contract/runtime-contract.ts +5 -3
- package/src/__test-utils__/test-gate.ts +8 -0
- package/src/__tests__/exit-backstop.test.ts +91 -0
- package/src/__tests__/golden-e2e.test.ts +80 -62
- package/src/__tests__/history-encryption-e2e.test.ts +51 -27
- package/src/__tests__/runner.test.ts +60 -2
- package/src/activities/__tests__/classify-tool-approvals.test.ts +1 -0
- package/src/activities/__tests__/discover-mcp-server.hang.test.ts +5 -1
- package/src/activities/__tests__/discover-mcp-server.test.ts +90 -49
- package/src/activities/__tests__/workflow-event-activities.test.ts +28 -7
- package/src/activities/call-agent.ts +1 -1
- package/src/activities/discover-mcp-server.ts +27 -37
- package/src/activities/execute-cursor/__test-utils__/fold.ts +6 -4
- package/src/activities/execute-cursor/__test-utils__/hermetic-cursor.ts +1 -0
- package/src/activities/execute-cursor/__tests__/approval-gate.test.ts +4 -29
- package/src/activities/execute-cursor/__tests__/hermetic/bypassed-tool-call.test.ts +120 -0
- package/src/activities/execute-cursor/__tests__/hermetic/file-review-capture.test.ts +5 -5
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/file-review-capture.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.error-delta.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.output-first.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/shell-output-delta.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/stall.status.json +0 -2
- package/src/activities/execute-cursor/__tests__/hermetic/goldens/unattributed-hook-block.status.json +0 -3
- package/src/activities/execute-cursor/__tests__/hermetic/shell-output-delta.test.ts +9 -7
- package/src/activities/execute-cursor/__tests__/hermetic/thrown-error-arms.test.ts +6 -1
- package/src/activities/execute-cursor/__tests__/hitl-resume-history.test.ts +8 -11
- package/src/activities/execute-cursor/__tests__/sequential-gate-resume.test.ts +1 -3
- package/src/activities/execute-cursor/__tests__/translator.test.ts +68 -15
- package/src/activities/execute-cursor/approval-policy.ts +11 -36
- package/src/activities/execute-cursor/boundary-rows.ts +3 -3
- package/src/activities/execute-cursor/translator.ts +15 -32
- package/src/activities/execute-cursor/turn-stream.ts +4 -1
- package/src/activities/execute-deep-agent/__test-utils__/hermetic-deep-agent.ts +33 -2
- package/src/activities/execute-deep-agent/__tests__/adapter.test.ts +1 -0
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.build-from-plan.prompt.md +1 -27
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.everything.prompt.md +26 -30
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.plan-mode.prompt.md +1 -26
- package/src/activities/execute-deep-agent/__tests__/goldens/user-message.everything.md +36 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/file-review-capture.test.ts +4 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/file-review-capture.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn1.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/inline-artifact.turn2.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/recursion-limit.persisted.status.json +8 -73
- package/src/activities/execute-deep-agent/__tests__/hermetic/goldens/resolution-error.status.json +1 -1
- package/src/activities/execute-deep-agent/__tests__/hermetic/inline-artifact-across-gate.test.ts +17 -0
- package/src/activities/execute-deep-agent/__tests__/hermetic/recursion-limit.test.ts +86 -26
- package/src/activities/execute-deep-agent/__tests__/hermetic/resolution-error.test.ts +9 -5
- package/src/activities/execute-deep-agent/__tests__/hermetic/thrown-turn-drains-publishes.test.ts +159 -0
- package/src/activities/execute-deep-agent/__tests__/inline-publisher.test.ts +84 -0
- package/src/activities/execute-deep-agent/__tests__/platform-route.test.ts +47 -10
- package/src/activities/execute-deep-agent/__tests__/prompt-builder.test.ts +127 -199
- package/src/activities/execute-deep-agent/__tests__/prompt-goldens.test.ts +31 -26
- package/src/activities/execute-deep-agent/__tests__/streaming-side-effects.test.ts +34 -0
- package/src/activities/execute-deep-agent/__tests__/turn-stream.test.ts +13 -5
- package/src/activities/execute-deep-agent/inline-publisher.ts +47 -7
- package/src/activities/execute-deep-agent/platform-route.ts +109 -20
- package/src/activities/execute-deep-agent/prompt-builder.ts +104 -92
- package/src/activities/execute-deep-agent/streaming-side-effects.ts +27 -4
- package/src/activities/execute-deep-agent/translator.ts +5 -4
- package/src/activities/execute-deep-agent/turn-settle.ts +6 -6
- package/src/activities/execute-deep-agent/turn-setup.ts +43 -38
- package/src/activities/execute-deep-agent/turn-stream.ts +15 -22
- package/src/activities/execute-deep-agent/turn.ts +16 -7
- package/src/activities/workflow-event-activities.ts +2 -0
- package/src/config.ts +11 -0
- package/src/exit-backstop.ts +97 -0
- package/src/harness/README.md +5 -5
- package/src/harness/terminal-table.ts +13 -13
- package/src/harness/transcript/__tests__/builder.test.ts +93 -21
- package/src/harness/transcript/builder.ts +53 -30
- package/src/harness/transcript/events.ts +22 -27
- package/src/harness/turn-context.ts +5 -3
- package/src/main.ts +9 -5
- package/src/middleware/__tests__/execution-budget.test.ts +181 -103
- package/src/middleware/cost-advisory.ts +6 -3
- package/src/middleware/execution-budget.ts +60 -34
- package/src/middleware/index.ts +10 -7
- package/src/middleware/types.ts +6 -1
- package/src/runner-manager.ts +3 -2
- package/src/runner.ts +57 -15
- package/src/shared/__tests__/approval-policy.test.ts +41 -0
- package/src/shared/__tests__/artifact-storage.test.ts +1 -0
- package/src/shared/__tests__/connect-backfill.test.ts +15 -11
- package/src/shared/__tests__/mcp-resolver.test.ts +72 -10
- package/src/shared/__tests__/model-error.test.ts +71 -3
- package/src/shared/__tests__/platform-server-address.test.ts +122 -0
- package/src/shared/__tests__/prompt-sections.test.ts +2 -54
- package/src/shared/__tests__/tool-rounds.test.ts +64 -14
- package/src/shared/approval-policy.ts +57 -14
- package/src/shared/caller-identity.ts +4 -5
- package/src/shared/channel-attachment.ts +2 -2
- package/src/shared/connect-backfill.ts +5 -1
- package/src/shared/mcp-resolver.ts +11 -1
- package/src/shared/memory-attachment.ts +2 -2
- package/src/shared/model-error.ts +87 -9
- package/src/shared/platform-server-address.ts +123 -0
- package/src/shared/prompt-sections.ts +6 -55
- package/src/shared/skill-resolver.ts +2 -3
- package/src/shared/synthesized-attachment.ts +0 -12
- package/src/shared/tool-rounds.ts +70 -24
- package/src/shared/workspace/__tests__/git-source.test.ts +11 -5
- package/src/shared/workspace/__tests__/stigmer-link.test.ts +107 -10
- package/src/shared/workspace/platform-dir.ts +19 -1
- package/src/shared/workspace/sources/git.ts +14 -8
- package/src/shared/workspace/stigmer-link.ts +99 -29
- package/src/tools/__tests__/url-guard.test.ts +19 -1
- package/src/tools/url-guard.ts +9 -1
- package/src/worker.ts +12 -2
- package/src/workflow-engine/__tests__/golden-execution.test.ts +2 -1
- package/src/workflow-engine/__tests__/loader.test.ts +1 -1
- package/src/workflow-engine/__tests__/tasks/human-input.test.ts +133 -57
- package/src/workflow-engine/tasks/human-input.ts +92 -42
- package/src/workflow-engine/types.ts +16 -4
- package/src/workflows/call-agent-orchestrator.ts +7 -0
- package/src/workflows/engine-core.ts +18 -0
- package/src/workflows/execute-from-execution.ts +2 -1
- package/src/workflows/human-input-orchestrator.ts +15 -56
- package/dist/shared/skill-relevance.d.ts +0 -65
- package/dist/shared/skill-relevance.js +0 -175
- package/dist/shared/skill-relevance.js.map +0 -1
- package/src/activities/execute-deep-agent/__tests__/goldens/system-prompt.skills-below-threshold.prompt.md +0 -146
- package/src/shared/__tests__/skill-relevance.test.ts +0 -292
- package/src/shared/skill-relevance.ts +0 -222
|
@@ -11,10 +11,10 @@
|
|
|
11
11
|
* `.stigmer/…` paths those agents are prompted with (skill locations,
|
|
12
12
|
* `.stigmer/inputs/…` attachments) actually resolve. The native harness's
|
|
13
13
|
* FILE tools are the exception: their virtual-rooted backend refuses a path
|
|
14
|
-
* whose real location leaves the workspace, so they
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
14
|
+
* whose real location leaves the workspace, so they reach `.stigmer/` through
|
|
15
|
+
* a route instead (`activities/execute-deep-agent/platform-route.ts`),
|
|
16
|
+
* mounted on every turn and exposing the platform dir on exactly the turns
|
|
17
|
+
* this link exists ({@link stigmerSymlinkPointsAt}).
|
|
18
18
|
*
|
|
19
19
|
* Lifecycle contract (same for both harnesses):
|
|
20
20
|
* - Created per turn, under the workspace turn lock — the link is a
|
|
@@ -29,26 +29,35 @@
|
|
|
29
29
|
* - {@link ensureStigmerSymlink} is idempotent, so multiple populaters may
|
|
30
30
|
* run in any order within a turn.
|
|
31
31
|
*
|
|
32
|
-
* OWNERSHIP
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
* `
|
|
38
|
-
*
|
|
39
|
-
*
|
|
32
|
+
* OWNERSHIP: the platform owns the `.stigmer` name inside a workspace, but
|
|
33
|
+
* never deletes what it did not make. A real (non-symlink) `.stigmer` entry
|
|
34
|
+
* found where the link goes — left by an older runner, or written by a shell
|
|
35
|
+
* command or a Cursor edit on a turn with no link, where nothing sits between
|
|
36
|
+
* those tools and the disk (#1123) — is MOVED out of the workspace to the
|
|
37
|
+
* session's `displaced/` dir (platform-dir.ts `getDisplacedDir`) and named in
|
|
38
|
+
* a warning; an empty directory is simply removed. Out of the workspace
|
|
39
|
+
* because a write-back commits everything the tree holds (`git add -A`) and
|
|
40
|
+
* git excludes only the exact `.stigmer` name. One case is refused outright:
|
|
41
|
+
* a `.stigmer` that is, or holds, the platform dir itself — a workspace
|
|
42
|
+
* rooted at the home directory, whose `.stigmer` is the Stigmer home (#1424).
|
|
43
|
+
* Turn-end removal is the conservative half: it only ever removes a SYMLINK.
|
|
40
44
|
*/
|
|
41
45
|
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
46
|
+
import { cp, lstat, mkdir, readdir, readlink, realpath, rename, rm, rmdir, symlink, unlink } from "node:fs/promises";
|
|
47
|
+
import type { Stats } from "node:fs";
|
|
48
|
+
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
49
|
+
import { getDisplacedDir } from "./platform-dir.js";
|
|
44
50
|
|
|
45
51
|
/** The workspace-visible name of the platform namespace. */
|
|
46
52
|
export const STIGMER_LOCAL_STATE_DIR = ".stigmer";
|
|
47
53
|
|
|
48
54
|
/**
|
|
49
55
|
* Ensure the workspace `.stigmer` symlink points to the platform dir.
|
|
50
|
-
* Idempotent: an existing correct link is kept
|
|
51
|
-
*
|
|
56
|
+
* Idempotent: an existing correct link is kept and a stale link is
|
|
57
|
+
* re-pointed. A real entry in the link's place is moved to the session's
|
|
58
|
+
* `displaced/` dir first (an empty directory is removed); a real `.stigmer`
|
|
59
|
+
* that holds the platform dir is refused with an error naming it, before
|
|
60
|
+
* anything is touched. See the module header, OWNERSHIP.
|
|
52
61
|
*/
|
|
53
62
|
export async function ensureStigmerSymlink(
|
|
54
63
|
workspaceDir: string,
|
|
@@ -56,24 +65,85 @@ export async function ensureStigmerSymlink(
|
|
|
56
65
|
): Promise<void> {
|
|
57
66
|
const linkPath = join(workspaceDir, STIGMER_LOCAL_STATE_DIR);
|
|
58
67
|
|
|
68
|
+
let entry: Stats | undefined;
|
|
59
69
|
try {
|
|
60
|
-
|
|
61
|
-
|
|
70
|
+
entry = await lstat(linkPath);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (errnoCode(err) !== "ENOENT") throw err;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (entry?.isSymbolicLink()) {
|
|
76
|
+
if ((await readlink(linkPath)) === platformDir) return;
|
|
62
77
|
await unlink(linkPath);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// No existing symlink
|
|
66
|
-
} else if (err.code === "EINVAL") {
|
|
67
|
-
// Exists but is not a symlink — remove the directory
|
|
68
|
-
await rm(linkPath, { recursive: true, force: true });
|
|
69
|
-
} else {
|
|
70
|
-
throw err;
|
|
71
|
-
}
|
|
78
|
+
} else if (entry !== undefined) {
|
|
79
|
+
await displaceRealEntry(linkPath, platformDir, entry);
|
|
72
80
|
}
|
|
73
81
|
|
|
74
82
|
await symlink(platformDir, linkPath, "dir");
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Clear a real `.stigmer` entry out of the link's place without losing it:
|
|
87
|
+
* refuse the Stigmer home, remove an empty directory, move anything else.
|
|
88
|
+
*/
|
|
89
|
+
async function displaceRealEntry(linkPath: string, platformDir: string, entry: Stats): Promise<void> {
|
|
90
|
+
if (entry.isDirectory()) {
|
|
91
|
+
await refuseDirHoldingPlatform(linkPath, platformDir);
|
|
92
|
+
if ((await readdir(linkPath)).length === 0) {
|
|
93
|
+
await rmdir(linkPath);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const destination = getDisplacedDir(platformDir, displacementStamp(new Date()));
|
|
98
|
+
await mkdir(dirname(destination), { recursive: true });
|
|
99
|
+
await moveEntry(linkPath, destination);
|
|
100
|
+
console.warn(
|
|
101
|
+
`ensureStigmerSymlink: moved the real ${linkPath} out of the workspace to ${destination}; ` +
|
|
102
|
+
"the platform owns the .stigmer name, so the link to the session's platform dir takes its place",
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Throw when the real `.stigmer` directory is, or contains, the platform dir:
|
|
108
|
+
* a workspace rooted at the home directory resolves `.stigmer` to the Stigmer
|
|
109
|
+
* home, where the platform dir, the CLI's config and every session live.
|
|
110
|
+
* Compared by real path, so a symlinked or `HOME`-overridden spelling of
|
|
111
|
+
* either side cannot slip past.
|
|
112
|
+
*/
|
|
113
|
+
async function refuseDirHoldingPlatform(linkPath: string, platformDir: string): Promise<void> {
|
|
114
|
+
const [realLink, realPlatform] = await Promise.all([realpath(linkPath), realpath(platformDir)]);
|
|
115
|
+
const fromLink = relative(realLink, realPlatform);
|
|
116
|
+
const outside = fromLink === ".." || fromLink.startsWith(`..${sep}`) || isAbsolute(fromLink);
|
|
117
|
+
if (!outside) {
|
|
118
|
+
throw new Error(
|
|
119
|
+
`The workspace's ${linkPath} is the Stigmer home (it holds this session's platform dir, ${platformDir}); ` +
|
|
120
|
+
"use a project directory as the workspace, not the home directory",
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Rename, or copy then remove when the destination is on another filesystem (a cloud workspace volume and `$HOME` are). */
|
|
126
|
+
async function moveEntry(source: string, destination: string): Promise<void> {
|
|
127
|
+
try {
|
|
128
|
+
await rename(source, destination);
|
|
129
|
+
} catch (err) {
|
|
130
|
+
if (errnoCode(err) !== "EXDEV") throw err;
|
|
131
|
+
await cp(source, destination, { recursive: true, errorOnExist: true, force: false, verbatimSymlinks: true });
|
|
132
|
+
await rm(source, { recursive: true, force: true });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** A compact UTC stamp with milliseconds (`20260929T101500123Z`): sortable, and free of the colons a path should not carry. */
|
|
137
|
+
function displacementStamp(now: Date): string {
|
|
138
|
+
return now.toISOString().replace(/[-:.]/g, "");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The errno code of a Node filesystem error, or `undefined` for anything else. */
|
|
142
|
+
function errnoCode(err: unknown): string | undefined {
|
|
143
|
+
if (typeof err !== "object" || err === null || !("code" in err)) return undefined;
|
|
144
|
+
return typeof err.code === "string" ? err.code : undefined;
|
|
145
|
+
}
|
|
146
|
+
|
|
77
147
|
/**
|
|
78
148
|
* Whether the workspace's `.stigmer` is, right now, the link
|
|
79
149
|
* {@link ensureStigmerSymlink} makes to `platformDir`. A turn that mounts no
|
|
@@ -105,8 +175,8 @@ export async function removeStigmerSymlink(workspaceDir: string): Promise<void>
|
|
|
105
175
|
if (stat.isSymbolicLink()) {
|
|
106
176
|
await unlink(linkPath);
|
|
107
177
|
}
|
|
108
|
-
} catch (err
|
|
109
|
-
if (err
|
|
178
|
+
} catch (err) {
|
|
179
|
+
if (errnoCode(err) !== "ENOENT") {
|
|
110
180
|
console.warn(
|
|
111
181
|
`removeStigmerSymlink: failed to remove ${linkPath} (non-fatal): ` +
|
|
112
182
|
`${err instanceof Error ? err.message : err}`,
|
|
@@ -131,7 +131,25 @@ describe("validateFetchUrl", () => {
|
|
|
131
131
|
|
|
132
132
|
it("rejects unresolvable hostnames", async () => {
|
|
133
133
|
await expect(
|
|
134
|
-
validateFetchUrl("http://definitely-not-a-real-host.stigmer.invalid/", "
|
|
134
|
+
validateFetchUrl("http://definitely-not-a-real-host.stigmer.invalid/", "relaxed"),
|
|
135
135
|
).rejects.toThrow(/Could not resolve/);
|
|
136
136
|
});
|
|
137
|
+
|
|
138
|
+
it("on a strict runner, a name that does not resolve and one that resolves to a refused address read alike and name no address", async () => {
|
|
139
|
+
const refusals = await Promise.all(
|
|
140
|
+
["http://localhost:9999/", "http://definitely-not-a-real-host.stigmer.invalid/"].map((url) =>
|
|
141
|
+
validateFetchUrl(url, "strict").then(
|
|
142
|
+
() => {
|
|
143
|
+
throw new Error(`expected ${url} to be refused`);
|
|
144
|
+
},
|
|
145
|
+
(error: unknown) => (error instanceof UrlGuardError ? error.message : String(error)),
|
|
146
|
+
),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
expect(refusals).toEqual([
|
|
150
|
+
"Refusing to fetch localhost: it does not resolve to a public address.",
|
|
151
|
+
"Refusing to fetch definitely-not-a-real-host.stigmer.invalid: it does not resolve to a public address.",
|
|
152
|
+
]);
|
|
153
|
+
expect(refusals.join(" ")).not.toMatch(/127\.0\.0\.1|::1|loopback/);
|
|
154
|
+
});
|
|
137
155
|
});
|
package/src/tools/url-guard.ts
CHANGED
|
@@ -12,7 +12,11 @@
|
|
|
12
12
|
* IPv4-mapped IPv6 is unwrapped, how every resolved address is judged) has
|
|
13
13
|
* one home, `@stigmer/outbound/egress`, shared with the control plane, which
|
|
14
14
|
* dials user-supplied URLs too. This module keeps what is the runner's:
|
|
15
|
-
* deriving the posture from its mode, and the sentence a model reads.
|
|
15
|
+
* deriving the posture from its mode, and the sentence a model reads. What
|
|
16
|
+
* that sentence may say is the shared rule too (`refusalWithholdsResolution`):
|
|
17
|
+
* on a strict runner a name that does not resolve and one that resolves to
|
|
18
|
+
* a refused address read alike and name no address, because the runner
|
|
19
|
+
* resolves through a network its caller does not own.
|
|
16
20
|
*
|
|
17
21
|
* Posture is mode-aware because locality differs, not trust:
|
|
18
22
|
*
|
|
@@ -41,6 +45,7 @@ import {
|
|
|
41
45
|
checkEgress,
|
|
42
46
|
egressPolicyForPosture,
|
|
43
47
|
nodeLookup,
|
|
48
|
+
refusalWithholdsResolution,
|
|
44
49
|
blockedReason as sharedBlockedReason,
|
|
45
50
|
type EgressPosture,
|
|
46
51
|
} from "@stigmer/outbound/egress";
|
|
@@ -91,6 +96,9 @@ export async function validateFetchUrl(
|
|
|
91
96
|
if (check.ok) return check.url;
|
|
92
97
|
|
|
93
98
|
const refusal = check.refusal;
|
|
99
|
+
if (refusalWithholdsResolution(refusal) && (refusal.kind === "unresolvable" || refusal.kind === "blocked")) {
|
|
100
|
+
throw new UrlGuardError(`Refusing to fetch ${refusal.hostname}: it does not resolve to a public address.`);
|
|
101
|
+
}
|
|
94
102
|
switch (refusal.kind) {
|
|
95
103
|
case "invalid-url":
|
|
96
104
|
throw new UrlGuardError(`Invalid URL: ${refusal.url}`);
|
package/src/worker.ts
CHANGED
|
@@ -34,7 +34,17 @@ export interface StartWorkerOptions {
|
|
|
34
34
|
payloadCodecs?: PayloadCodec[];
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* The worker and the connection it polls on. The caller owns both: it runs
|
|
39
|
+
* and shuts down the worker, then closes the connection once `run()` has
|
|
40
|
+
* resolved (the SDK refuses to close a connection a worker still holds).
|
|
41
|
+
*/
|
|
42
|
+
export interface StartedWorker {
|
|
43
|
+
readonly worker: Worker;
|
|
44
|
+
readonly connection: NativeConnection;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function startWorker(opts: StartWorkerOptions): Promise<StartedWorker> {
|
|
38
48
|
const { config, activities, payloadCodecs } = opts;
|
|
39
49
|
|
|
40
50
|
const connection = await NativeConnection.connect({
|
|
@@ -109,5 +119,5 @@ export async function startWorker(opts: StartWorkerOptions): Promise<Worker> {
|
|
|
109
119
|
},
|
|
110
120
|
});
|
|
111
121
|
|
|
112
|
-
return worker;
|
|
122
|
+
return { worker, connection };
|
|
113
123
|
}
|
|
@@ -164,7 +164,8 @@ describe("Golden Execution — Tier 1c: External Calls", () => {
|
|
|
164
164
|
it("#03 foreach-loop — for loop with HTTP call per item", async () => {
|
|
165
165
|
const model = loadWorkflowFromYaml(loadGolden("03-foreach-loop.yaml"));
|
|
166
166
|
const state = createState();
|
|
167
|
-
|
|
167
|
+
// The workflow's input, where the engine puts it (engine-core.ts: state.input = workflow_input).
|
|
168
|
+
state.input = { items: ["item1", "item2", "item3"] };
|
|
168
169
|
|
|
169
170
|
const mockCallHttp = vi.fn(async () => fakeHttpResponse());
|
|
170
171
|
const ctx = makeCtx({ callHttp: mockCallHttp });
|
|
@@ -391,7 +391,7 @@ do:
|
|
|
391
391
|
expect(forEntry).toBeDefined();
|
|
392
392
|
if (forEntry?.task.kind === "for") {
|
|
393
393
|
expect(forEntry.task.for.each).toBe("item");
|
|
394
|
-
expect(forEntry.task.for.in).toBe("${ $
|
|
394
|
+
expect(forEntry.task.for.in).toBe("${ $input.items }");
|
|
395
395
|
expect(forEntry.task.do.length).toBeGreaterThan(0);
|
|
396
396
|
}
|
|
397
397
|
});
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi } from "vitest";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
HUMAN_INPUT_FAIL_TIMEOUT_RESOLVED_PATCH,
|
|
4
|
+
executeHumanInputTask,
|
|
5
|
+
} from "../../tasks/human-input.js";
|
|
3
6
|
import { createState } from "../../state.js";
|
|
4
7
|
import { evaluateExpressionBatch } from "../../expression.js";
|
|
5
8
|
import { TaskStatusAccumulator } from "../../task-status-accumulator.js";
|
|
@@ -8,6 +11,7 @@ import type {
|
|
|
8
11
|
TaskExecutionContext,
|
|
9
12
|
HumanInputExecutionConfig,
|
|
10
13
|
HumanInputResult,
|
|
14
|
+
HumanInputTimeout,
|
|
11
15
|
AwaitHumanInputFn,
|
|
12
16
|
EmitEventsFn,
|
|
13
17
|
ExpressionEvaluator,
|
|
@@ -17,12 +21,16 @@ import type {
|
|
|
17
21
|
|
|
18
22
|
const notAvailable = () => { throw new Error("not available in test"); };
|
|
19
23
|
|
|
24
|
+
/** What the workflow layer reports when the gate's timer wins. */
|
|
25
|
+
const TIMED_OUT: HumanInputTimeout = { timedOut: true };
|
|
26
|
+
|
|
20
27
|
interface CtxOptions {
|
|
21
28
|
readonly awaitFn?: AwaitHumanInputFn;
|
|
22
29
|
readonly emitFn?: EmitEventsFn;
|
|
23
30
|
readonly evaluateExpressions?: ExpressionEvaluator;
|
|
24
31
|
readonly promoteTaskOutput?: PromoteTaskOutputFn;
|
|
25
32
|
readonly taskStatusAccumulator?: TaskStatusAccumulator;
|
|
33
|
+
readonly isPatched?: (changeId: string) => boolean;
|
|
26
34
|
}
|
|
27
35
|
|
|
28
36
|
function makeCtx(
|
|
@@ -45,6 +53,7 @@ function makeCtx(
|
|
|
45
53
|
emitEvents: emitFn,
|
|
46
54
|
promoteTaskOutput: options?.promoteTaskOutput,
|
|
47
55
|
taskStatusAccumulator: options?.taskStatusAccumulator,
|
|
56
|
+
isPatched: options?.isPatched,
|
|
48
57
|
};
|
|
49
58
|
}
|
|
50
59
|
|
|
@@ -156,11 +165,11 @@ describe("executeHumanInputTask", () => {
|
|
|
156
165
|
expect(capturedConfig!.timeoutSeconds).toBe(0);
|
|
157
166
|
});
|
|
158
167
|
|
|
159
|
-
it("
|
|
168
|
+
it("hands the wait only the signal and the timeout; the policy stays with the kernel", async () => {
|
|
160
169
|
let capturedConfig: HumanInputExecutionConfig | undefined;
|
|
161
170
|
const awaitFn: AwaitHumanInputFn = async (config) => {
|
|
162
171
|
capturedConfig = config;
|
|
163
|
-
return { outcome: "approve"
|
|
172
|
+
return { outcome: "approve" };
|
|
164
173
|
};
|
|
165
174
|
|
|
166
175
|
const taskDef: HumanInputTaskDef = {
|
|
@@ -170,31 +179,27 @@ describe("executeHumanInputTask", () => {
|
|
|
170
179
|
|
|
171
180
|
await executeHumanInputTask(taskDef, "t", createState(), makeCtx(awaitFn));
|
|
172
181
|
|
|
173
|
-
expect(capturedConfig
|
|
182
|
+
expect(capturedConfig).toEqual({ signalName: "human_input_t", timeoutSeconds: 60 });
|
|
174
183
|
});
|
|
175
184
|
|
|
176
|
-
it("
|
|
177
|
-
let capturedConfig: HumanInputExecutionConfig | undefined;
|
|
178
|
-
const awaitFn: AwaitHumanInputFn = async (config) => {
|
|
179
|
-
capturedConfig = config;
|
|
180
|
-
return { outcome: "approve" };
|
|
181
|
-
};
|
|
182
|
-
|
|
185
|
+
it("fails the task on a timeout when onTimeout is not set", async () => {
|
|
183
186
|
const taskDef: HumanInputTaskDef = {
|
|
184
187
|
kind: "human_input",
|
|
185
188
|
humanInput: { prompt: "Review", timeout: 60 },
|
|
186
189
|
};
|
|
187
190
|
|
|
188
|
-
await
|
|
189
|
-
|
|
190
|
-
|
|
191
|
+
await expect(
|
|
192
|
+
executeHumanInputTask(taskDef, "t", createState(), makeCtx(async () => TIMED_OUT)),
|
|
193
|
+
).rejects.toThrow("human_input task timed out waiting for signal 'human_input_t'");
|
|
191
194
|
});
|
|
192
195
|
});
|
|
193
196
|
|
|
194
197
|
describe("error propagation", () => {
|
|
195
|
-
it("propagates
|
|
198
|
+
it("propagates an error from the wait untouched and logs no resolution", async () => {
|
|
199
|
+
const emitted: WorkflowEventDescriptor[][] = [];
|
|
200
|
+
const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
|
|
196
201
|
const awaitFn: AwaitHumanInputFn = async () => {
|
|
197
|
-
throw new Error("
|
|
202
|
+
throw new Error("workflow cancelled");
|
|
198
203
|
};
|
|
199
204
|
|
|
200
205
|
const taskDef: HumanInputTaskDef = {
|
|
@@ -203,18 +208,86 @@ describe("executeHumanInputTask", () => {
|
|
|
203
208
|
};
|
|
204
209
|
|
|
205
210
|
await expect(
|
|
206
|
-
executeHumanInputTask(taskDef, "t", createState(), makeCtx(awaitFn)),
|
|
211
|
+
executeHumanInputTask(taskDef, "t", createState(), makeCtx(awaitFn, emitFn)),
|
|
212
|
+
).rejects.toThrow("workflow cancelled");
|
|
213
|
+
expect(emitted.flat().map((e) => e.type)).toEqual(["approval_requested"]);
|
|
214
|
+
});
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
// A fail-policy timeout ends the gate without a decision. It still closes
|
|
218
|
+
// the approval in the event log, so a reader never sees a request left
|
|
219
|
+
// open, and then fails the task with the error it always raised
|
|
220
|
+
// (stigmer/stigmer#1442).
|
|
221
|
+
describe("fail-policy timeout", () => {
|
|
222
|
+
const taskDef: HumanInputTaskDef = {
|
|
223
|
+
kind: "human_input",
|
|
224
|
+
humanInput: { prompt: "Review", timeout: 10, onTimeout: "fail" },
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
it("logs one approval_resolved with auto_resolved and no outcome, then fails the task", async () => {
|
|
228
|
+
const emitted: WorkflowEventDescriptor[][] = [];
|
|
229
|
+
const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
|
|
230
|
+
const state = createState();
|
|
231
|
+
|
|
232
|
+
await expect(
|
|
233
|
+
executeHumanInputTask(taskDef, "gate", state, makeCtx(async () => TIMED_OUT, emitFn)),
|
|
234
|
+
).rejects.toThrow("human_input task timed out waiting for signal 'human_input_gate'");
|
|
235
|
+
|
|
236
|
+
const resolutions = emitted.flat().filter((e) => e.type === "approval_resolved");
|
|
237
|
+
expect(resolutions).toHaveLength(1);
|
|
238
|
+
expect(resolutions[0]).toMatchObject({
|
|
239
|
+
taskName: "gate",
|
|
240
|
+
outcome: "",
|
|
241
|
+
resolvedBy: "",
|
|
242
|
+
resolvedByActor: undefined,
|
|
243
|
+
comment: "",
|
|
244
|
+
autoResolved: true,
|
|
245
|
+
});
|
|
246
|
+
expect(state.data.gate, "a failed gate records no decision").toBeUndefined();
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("asks the patch gate by its pinned id before logging", async () => {
|
|
250
|
+
const isPatched = vi.fn(() => true);
|
|
251
|
+
|
|
252
|
+
await expect(
|
|
253
|
+
executeHumanInputTask(taskDef, "gate", createState(), makeCtx(async () => TIMED_OUT, undefined, { isPatched })),
|
|
207
254
|
).rejects.toThrow("timed out");
|
|
255
|
+
|
|
256
|
+
expect(HUMAN_INPUT_FAIL_TIMEOUT_RESOLVED_PATCH).toBe("human-input-fail-timeout-resolved");
|
|
257
|
+
expect(isPatched).toHaveBeenCalledWith(HUMAN_INPUT_FAIL_TIMEOUT_RESOLVED_PATCH);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("replays a history from before the patch in the old order: no resolution, the same failure", async () => {
|
|
261
|
+
const emitted: WorkflowEventDescriptor[][] = [];
|
|
262
|
+
const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
|
|
263
|
+
|
|
264
|
+
await expect(
|
|
265
|
+
executeHumanInputTask(
|
|
266
|
+
taskDef, "gate", createState(),
|
|
267
|
+
makeCtx(async () => TIMED_OUT, emitFn, { isPatched: () => false }),
|
|
268
|
+
),
|
|
269
|
+
).rejects.toThrow("human_input task timed out waiting for signal 'human_input_gate'");
|
|
270
|
+
|
|
271
|
+
expect(emitted.flat().map((e) => e.type)).toEqual(["approval_requested"]);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("never asks the patch gate for a gate a policy or a reviewer decided", async () => {
|
|
275
|
+
const isPatched = vi.fn(() => true);
|
|
276
|
+
const approveOnTimeout: HumanInputTaskDef = {
|
|
277
|
+
kind: "human_input",
|
|
278
|
+
humanInput: { prompt: "Review", timeout: 10, onTimeout: "approve" },
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
await executeHumanInputTask(approveOnTimeout, "a", createState(), makeCtx(async () => TIMED_OUT, undefined, { isPatched }));
|
|
282
|
+
await executeHumanInputTask(taskDef, "b", createState(), makeCtx(async () => ({ outcome: "approve" }), undefined, { isPatched }));
|
|
283
|
+
|
|
284
|
+
expect(isPatched).not.toHaveBeenCalled();
|
|
208
285
|
});
|
|
209
286
|
});
|
|
210
287
|
|
|
211
288
|
describe("auto-resolved results", () => {
|
|
212
289
|
it("handles auto-approve timeout result", async () => {
|
|
213
|
-
const awaitFn: AwaitHumanInputFn = async () =>
|
|
214
|
-
outcome: "approve",
|
|
215
|
-
auto_resolved: true,
|
|
216
|
-
reason: "timeout",
|
|
217
|
-
});
|
|
290
|
+
const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
|
|
218
291
|
|
|
219
292
|
const taskDef: HumanInputTaskDef = {
|
|
220
293
|
kind: "human_input",
|
|
@@ -229,11 +302,7 @@ describe("executeHumanInputTask", () => {
|
|
|
229
302
|
});
|
|
230
303
|
|
|
231
304
|
it("handles auto-deny timeout result", async () => {
|
|
232
|
-
const awaitFn: AwaitHumanInputFn = async () =>
|
|
233
|
-
outcome: "deny",
|
|
234
|
-
auto_resolved: true,
|
|
235
|
-
reason: "timeout",
|
|
236
|
-
});
|
|
305
|
+
const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
|
|
237
306
|
|
|
238
307
|
const taskDef: HumanInputTaskDef = {
|
|
239
308
|
kind: "human_input",
|
|
@@ -260,11 +329,7 @@ describe("executeHumanInputTask", () => {
|
|
|
260
329
|
];
|
|
261
330
|
|
|
262
331
|
it("maps timeout auto-approve to the FIRST outcome and routes its then", async () => {
|
|
263
|
-
const awaitFn: AwaitHumanInputFn = async () =>
|
|
264
|
-
outcome: "approve",
|
|
265
|
-
auto_resolved: true,
|
|
266
|
-
reason: "timeout",
|
|
267
|
-
});
|
|
332
|
+
const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
|
|
268
333
|
|
|
269
334
|
const taskDef: HumanInputTaskDef = {
|
|
270
335
|
kind: "human_input",
|
|
@@ -288,11 +353,7 @@ describe("executeHumanInputTask", () => {
|
|
|
288
353
|
});
|
|
289
354
|
|
|
290
355
|
it("maps timeout auto-deny to the LAST outcome", async () => {
|
|
291
|
-
const awaitFn: AwaitHumanInputFn = async () =>
|
|
292
|
-
outcome: "deny",
|
|
293
|
-
auto_resolved: true,
|
|
294
|
-
reason: "timeout",
|
|
295
|
-
});
|
|
356
|
+
const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
|
|
296
357
|
|
|
297
358
|
const taskDef: HumanInputTaskDef = {
|
|
298
359
|
kind: "human_input",
|
|
@@ -310,11 +371,7 @@ describe("executeHumanInputTask", () => {
|
|
|
310
371
|
// — the ordinary name lookup finds the "escalate" outcome and routes its
|
|
311
372
|
// `then`, regardless of the outcome's position in the list.
|
|
312
373
|
it("resolves timeout escalate to the escalate-named outcome and routes its then", async () => {
|
|
313
|
-
const awaitFn: AwaitHumanInputFn = async () =>
|
|
314
|
-
outcome: "escalate",
|
|
315
|
-
auto_resolved: true,
|
|
316
|
-
reason: "timeout",
|
|
317
|
-
});
|
|
374
|
+
const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
|
|
318
375
|
|
|
319
376
|
const escalateOutcomes = [
|
|
320
377
|
{ name: "proceed", label: "Proceed", then: "deployStep" },
|
|
@@ -347,11 +404,7 @@ describe("executeHumanInputTask", () => {
|
|
|
347
404
|
it("reports the mapped outcome on the approval_resolved event", async () => {
|
|
348
405
|
const emitted: WorkflowEventDescriptor[][] = [];
|
|
349
406
|
const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
|
|
350
|
-
const awaitFn: AwaitHumanInputFn = async () =>
|
|
351
|
-
outcome: "approve",
|
|
352
|
-
auto_resolved: true,
|
|
353
|
-
reason: "timeout",
|
|
354
|
-
});
|
|
407
|
+
const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
|
|
355
408
|
|
|
356
409
|
const taskDef: HumanInputTaskDef = {
|
|
357
410
|
kind: "human_input",
|
|
@@ -385,11 +438,7 @@ describe("executeHumanInputTask", () => {
|
|
|
385
438
|
});
|
|
386
439
|
|
|
387
440
|
it("keeps plain approve/deny for binary gates without custom outcomes", async () => {
|
|
388
|
-
const awaitFn: AwaitHumanInputFn = async () =>
|
|
389
|
-
outcome: "approve",
|
|
390
|
-
auto_resolved: true,
|
|
391
|
-
reason: "timeout",
|
|
392
|
-
});
|
|
441
|
+
const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
|
|
393
442
|
|
|
394
443
|
const taskDef: HumanInputTaskDef = {
|
|
395
444
|
kind: "human_input",
|
|
@@ -474,10 +523,40 @@ describe("executeHumanInputTask", () => {
|
|
|
474
523
|
expect(resolved.outcome).toBe("reject");
|
|
475
524
|
expect(resolved.resolvedBy).toBe("carol");
|
|
476
525
|
expect(resolved.resolvedByActor).toBeUndefined();
|
|
526
|
+
expect(resolved.comment).toBe("");
|
|
477
527
|
expect(resolved.autoResolved).toBe(false);
|
|
478
528
|
expect(resolved.waitDurationMs).toBeGreaterThanOrEqual(0);
|
|
479
529
|
});
|
|
480
530
|
|
|
531
|
+
it("carries the reviewer's comment into approval_resolved and the task output", async () => {
|
|
532
|
+
const emitted: WorkflowEventDescriptor[][] = [];
|
|
533
|
+
const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
|
|
534
|
+
const awaitFn: AwaitHumanInputFn = async () => ({
|
|
535
|
+
outcome: "deny",
|
|
536
|
+
reviewer: "ida_01abc",
|
|
537
|
+
comment: "Rollback plan is missing",
|
|
538
|
+
responded_at: "2026-05-20T10:00:00Z",
|
|
539
|
+
});
|
|
540
|
+
|
|
541
|
+
const taskDef: HumanInputTaskDef = {
|
|
542
|
+
kind: "human_input",
|
|
543
|
+
humanInput: { prompt: "Confirm?" },
|
|
544
|
+
};
|
|
545
|
+
|
|
546
|
+
const state = createState();
|
|
547
|
+
await executeHumanInputTask(taskDef, "gate", state, makeCtx(awaitFn, emitFn));
|
|
548
|
+
|
|
549
|
+
const resolved = emitted[1][0];
|
|
550
|
+
if (resolved.type !== "approval_resolved") throw new Error("unexpected");
|
|
551
|
+
expect(resolved.comment).toBe("Rollback plan is missing");
|
|
552
|
+
expect(state.data.gate).toEqual({
|
|
553
|
+
outcome: "deny",
|
|
554
|
+
reviewer: "ida_01abc",
|
|
555
|
+
comment: "Rollback plan is missing",
|
|
556
|
+
responded_at: "2026-05-20T10:00:00Z",
|
|
557
|
+
});
|
|
558
|
+
});
|
|
559
|
+
|
|
481
560
|
it("carries the reviewer_actor display snapshot into approval_resolved and the task output", async () => {
|
|
482
561
|
const emitted: WorkflowEventDescriptor[][] = [];
|
|
483
562
|
const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
|
|
@@ -607,11 +686,7 @@ describe("executeHumanInputTask", () => {
|
|
|
607
686
|
it("emits approval_resolved with autoResolved true on timeout auto-approve", async () => {
|
|
608
687
|
const emitted: WorkflowEventDescriptor[][] = [];
|
|
609
688
|
const emitFn: EmitEventsFn = async (events) => { emitted.push(events); };
|
|
610
|
-
const awaitFn: AwaitHumanInputFn = async () =>
|
|
611
|
-
outcome: "approve",
|
|
612
|
-
auto_resolved: true,
|
|
613
|
-
reason: "timeout",
|
|
614
|
-
});
|
|
689
|
+
const awaitFn: AwaitHumanInputFn = async () => TIMED_OUT;
|
|
615
690
|
|
|
616
691
|
const taskDef: HumanInputTaskDef = {
|
|
617
692
|
kind: "human_input",
|
|
@@ -625,6 +700,7 @@ describe("executeHumanInputTask", () => {
|
|
|
625
700
|
|
|
626
701
|
expect(resolved.autoResolved).toBe(true);
|
|
627
702
|
expect(resolved.outcome).toBe("approve");
|
|
703
|
+
expect(resolved.comment).toBe("");
|
|
628
704
|
});
|
|
629
705
|
});
|
|
630
706
|
|