@stigmer/runner-slim 3.0.9-dev.20260615153829 → 3.1.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/main.js +378 -199
- package/package.json +6 -6
- package/workflow-bundle.js +196 -24
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stigmer/runner-slim",
|
|
3
|
-
"version": "3.0
|
|
3
|
+
"version": "3.1.0",
|
|
4
4
|
"description": "Self-contained Stigmer runner build for embedding in desktop apps — the bundle-friendly @stigmer/runner",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"engines": {
|
|
@@ -16,11 +16,11 @@
|
|
|
16
16
|
"jq-wasm": "^1.1.0-jq-1.8.1"
|
|
17
17
|
},
|
|
18
18
|
"optionalDependencies": {
|
|
19
|
-
"@stigmer/runner-slim-darwin-arm64": "3.0
|
|
20
|
-
"@stigmer/runner-slim-darwin-x64": "3.0
|
|
21
|
-
"@stigmer/runner-slim-linux-x64": "3.0
|
|
22
|
-
"@stigmer/runner-slim-linux-arm64": "3.0
|
|
23
|
-
"@stigmer/runner-slim-win32-x64": "3.0
|
|
19
|
+
"@stigmer/runner-slim-darwin-arm64": "3.1.0",
|
|
20
|
+
"@stigmer/runner-slim-darwin-x64": "3.1.0",
|
|
21
|
+
"@stigmer/runner-slim-linux-x64": "3.1.0",
|
|
22
|
+
"@stigmer/runner-slim-linux-arm64": "3.1.0",
|
|
23
|
+
"@stigmer/runner-slim-win32-x64": "3.1.0"
|
|
24
24
|
},
|
|
25
25
|
"keywords": [
|
|
26
26
|
"stigmer",
|
package/workflow-bundle.js
CHANGED
|
@@ -27739,6 +27739,40 @@ async function orchestrateAgentCall(input) {
|
|
|
27739
27739
|
activityError = err;
|
|
27740
27740
|
activityDone = true;
|
|
27741
27741
|
});
|
|
27742
|
+
// Last-written file-review reference for this child, as a deterministic key, so
|
|
27743
|
+
// we write pending_file_reviews only when the child's AWAITING_REVIEW set changes
|
|
27744
|
+
// (avoids status-write storms). undefined = never written yet.
|
|
27745
|
+
let lastFileReviewKey;
|
|
27746
|
+
// Poll-derive the child's file-review gate and surface it (reference-only) on the
|
|
27747
|
+
// parent. No new Temporal signal: the orchestrator already holds the child open
|
|
27748
|
+
// (async-completion) and reads its status here on the same cadence as progress.
|
|
27749
|
+
async function syncFileReviews(childId) {
|
|
27750
|
+
let ids;
|
|
27751
|
+
try {
|
|
27752
|
+
ids = await statusProxy.GetAwaitingFileReviewChangeSetIds(childId);
|
|
27753
|
+
}
|
|
27754
|
+
catch (err) {
|
|
27755
|
+
_temporalio_workflow__WEBPACK_IMPORTED_MODULE_0__.log.warn("Failed to derive child file-review state (non-fatal)", {
|
|
27756
|
+
error: String(err),
|
|
27757
|
+
taskName: input.taskName,
|
|
27758
|
+
});
|
|
27759
|
+
return;
|
|
27760
|
+
}
|
|
27761
|
+
// Deterministic change key (sorted): a stable comparison across replays.
|
|
27762
|
+
const key = [...ids].sort().join(",");
|
|
27763
|
+
if (key === lastFileReviewKey)
|
|
27764
|
+
return;
|
|
27765
|
+
lastFileReviewKey = key;
|
|
27766
|
+
try {
|
|
27767
|
+
await statusProxy.UpdateWorkflowFileReviewStatus(input.workflowExecutionId, childId, ids);
|
|
27768
|
+
}
|
|
27769
|
+
catch (err) {
|
|
27770
|
+
_temporalio_workflow__WEBPACK_IMPORTED_MODULE_0__.log.warn("Failed to update workflow file-review status (non-fatal)", {
|
|
27771
|
+
error: String(err),
|
|
27772
|
+
taskName: input.taskName,
|
|
27773
|
+
});
|
|
27774
|
+
}
|
|
27775
|
+
}
|
|
27742
27776
|
while (!activityDone) {
|
|
27743
27777
|
// Wait for a signal, activity completion, or periodic timeout for progress polling.
|
|
27744
27778
|
// condition() returns false on timeout, true when the predicate became true.
|
|
@@ -27749,6 +27783,7 @@ async function orchestrateAgentCall(input) {
|
|
|
27749
27783
|
if (childExecId && !initialProgressEmitted) {
|
|
27750
27784
|
initialProgressEmitted = true;
|
|
27751
27785
|
await emitProgress(input, childExecId, null);
|
|
27786
|
+
await syncFileReviews(childExecId);
|
|
27752
27787
|
}
|
|
27753
27788
|
// Periodic progress: on timeout, poll the child execution for live data
|
|
27754
27789
|
if (!conditionMet && childExecId) {
|
|
@@ -27765,6 +27800,7 @@ async function orchestrateAgentCall(input) {
|
|
|
27765
27800
|
if (progress) {
|
|
27766
27801
|
await emitProgress(input, childExecId, progress);
|
|
27767
27802
|
}
|
|
27803
|
+
await syncFileReviews(childExecId);
|
|
27768
27804
|
}
|
|
27769
27805
|
// Handle HITL approval notification
|
|
27770
27806
|
if (pendingNotification) {
|
|
@@ -27787,13 +27823,26 @@ async function orchestrateAgentCall(input) {
|
|
|
27787
27823
|
if (childExecId && !initialProgressEmitted) {
|
|
27788
27824
|
await emitProgress(input, childExecId, null);
|
|
27789
27825
|
}
|
|
27790
|
-
|
|
27791
|
-
|
|
27792
|
-
|
|
27793
|
-
|
|
27794
|
-
|
|
27795
|
-
|
|
27796
|
-
}
|
|
27826
|
+
// Scoped clears for this child only (per-child merge) — a completing child must
|
|
27827
|
+
// not wipe a parallel sibling's still-pending gate. Skipped if the child never
|
|
27828
|
+
// started (nothing was ever surfaced for it).
|
|
27829
|
+
if (childExecId) {
|
|
27830
|
+
try {
|
|
27831
|
+
await statusProxy.ClearWorkflowApprovalStatus(input.workflowExecutionId, childExecId);
|
|
27832
|
+
}
|
|
27833
|
+
catch (clearErr) {
|
|
27834
|
+
_temporalio_workflow__WEBPACK_IMPORTED_MODULE_0__.log.warn("Failed to clear workflow approval status (non-fatal)", {
|
|
27835
|
+
error: String(clearErr),
|
|
27836
|
+
});
|
|
27837
|
+
}
|
|
27838
|
+
try {
|
|
27839
|
+
await statusProxy.UpdateWorkflowFileReviewStatus(input.workflowExecutionId, childExecId, []);
|
|
27840
|
+
}
|
|
27841
|
+
catch (clearErr) {
|
|
27842
|
+
_temporalio_workflow__WEBPACK_IMPORTED_MODULE_0__.log.warn("Failed to clear workflow file-review status (non-fatal)", {
|
|
27843
|
+
error: String(clearErr),
|
|
27844
|
+
});
|
|
27845
|
+
}
|
|
27797
27846
|
}
|
|
27798
27847
|
if (activityError) {
|
|
27799
27848
|
if (childExecId) {
|
|
@@ -27847,8 +27896,10 @@ async function emitProgress(input, childExecId, progress) {
|
|
|
27847
27896
|
"use strict";
|
|
27848
27897
|
__webpack_require__.r(__webpack_exports__);
|
|
27849
27898
|
/* harmony export */ __webpack_require__.d(__webpack_exports__, {
|
|
27899
|
+
/* harmony export */ applyDestructiveHintTightener: () => (/* binding */ applyDestructiveHintTightener),
|
|
27850
27900
|
/* harmony export */ connectMcpServer: () => (/* binding */ connectMcpServer),
|
|
27851
|
-
/* harmony export */ discoverMcpServerLegacy: () => (/* binding */ discoverMcpServerLegacy)
|
|
27901
|
+
/* harmony export */ discoverMcpServerLegacy: () => (/* binding */ discoverMcpServerLegacy),
|
|
27902
|
+
/* harmony export */ planIncrementalClassification: () => (/* binding */ planIncrementalClassification)
|
|
27852
27903
|
/* harmony export */ });
|
|
27853
27904
|
/* harmony import */ var _temporalio_workflow__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! @temporalio/workflow */ "./node_modules/@temporalio/workflow/lib/index.js");
|
|
27854
27905
|
/**
|
|
@@ -27882,6 +27933,115 @@ function classifyWithTimeout(numTools) {
|
|
|
27882
27933
|
});
|
|
27883
27934
|
}
|
|
27884
27935
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
27936
|
+
// Incremental classification planner (pure, deterministic, sandbox-safe)
|
|
27937
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27938
|
+
/**
|
|
27939
|
+
* Canonical, order-stable signature of a single tool's definition.
|
|
27940
|
+
*
|
|
27941
|
+
* Mirrors the shape `toolsFingerprint` hashes (name + description + input_schema)
|
|
27942
|
+
* so "unchanged" here means the same thing it means for the whole-server
|
|
27943
|
+
* fingerprint. Uses only JSON — no `node:crypto` — so it is safe to call from
|
|
27944
|
+
* inside the Temporal deterministic V8 isolate.
|
|
27945
|
+
*/
|
|
27946
|
+
function toolSignature(tool) {
|
|
27947
|
+
return JSON.stringify({
|
|
27948
|
+
name: tool.name,
|
|
27949
|
+
description: tool.description,
|
|
27950
|
+
input_schema: tool.inputSchema ?? null,
|
|
27951
|
+
});
|
|
27952
|
+
}
|
|
27953
|
+
/**
|
|
27954
|
+
* Partition the freshly discovered tools into those that must be classified and
|
|
27955
|
+
* the prior approval decisions that can be carried forward verbatim.
|
|
27956
|
+
*
|
|
27957
|
+
* Reuse is **content-addressed**: a prior decision is kept only when a tool's
|
|
27958
|
+
* name AND full definition are byte-identical to the previous connect. This is
|
|
27959
|
+
* deliberately stricter than reusing by name alone — a tool can keep its name
|
|
27960
|
+
* while its schema changes from benign to destructive, and such a tool MUST be
|
|
27961
|
+
* re-evaluated rather than left with a stale "auto-approve". Tools that are
|
|
27962
|
+
* unchanged are never re-classified (stable, deterministic, no LLM cost; no
|
|
27963
|
+
* flapping for borderline tools), and a tool present last time but gone now is
|
|
27964
|
+
* simply absent from both outputs (dropped).
|
|
27965
|
+
*
|
|
27966
|
+
* The previous approval list is a presence-set of *gated* tools (a tool in
|
|
27967
|
+
* `previousToolApprovals` requires approval; a known tool absent from it was
|
|
27968
|
+
* auto-approved). So a reused tool emits a carried-forward entry only when it
|
|
27969
|
+
* was gated; reused auto-approved tools emit nothing, which correctly keeps them
|
|
27970
|
+
* un-gated. `ClassifyToolApprovals` likewise returns only gated entries, so the
|
|
27971
|
+
* union `[...carriedForward, ...classified]` is the complete gated set.
|
|
27972
|
+
*
|
|
27973
|
+
* Pure and free of Temporal APIs so it can be exhaustively unit-tested and is
|
|
27974
|
+
* safe to evaluate inside the workflow sandbox.
|
|
27975
|
+
*/
|
|
27976
|
+
function planIncrementalClassification(previousTools, previousToolApprovals, currentTools) {
|
|
27977
|
+
const prevSigByName = new Map();
|
|
27978
|
+
for (const tool of previousTools) {
|
|
27979
|
+
prevSigByName.set(tool.name, toolSignature(tool));
|
|
27980
|
+
}
|
|
27981
|
+
const prevGatedByName = new Map();
|
|
27982
|
+
for (const approval of previousToolApprovals) {
|
|
27983
|
+
prevGatedByName.set(approval.toolName, approval);
|
|
27984
|
+
}
|
|
27985
|
+
const toolsToClassify = [];
|
|
27986
|
+
const carriedForward = [];
|
|
27987
|
+
for (const tool of currentTools) {
|
|
27988
|
+
const prevSig = prevSigByName.get(tool.name);
|
|
27989
|
+
const unchanged = prevSig !== undefined && prevSig === toolSignature(tool);
|
|
27990
|
+
if (!unchanged) {
|
|
27991
|
+
toolsToClassify.push(tool);
|
|
27992
|
+
continue;
|
|
27993
|
+
}
|
|
27994
|
+
const gated = prevGatedByName.get(tool.name);
|
|
27995
|
+
if (gated) {
|
|
27996
|
+
carriedForward.push({
|
|
27997
|
+
tool_name: gated.toolName,
|
|
27998
|
+
requires_approval: true,
|
|
27999
|
+
message: gated.message,
|
|
28000
|
+
});
|
|
28001
|
+
}
|
|
28002
|
+
// An unchanged tool that was not gated stays auto-approved — emit nothing.
|
|
28003
|
+
}
|
|
28004
|
+
return { toolsToClassify, carriedForward };
|
|
28005
|
+
}
|
|
28006
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28007
|
+
// destructiveHint fail-closed tightener (pure, deterministic, sandbox-safe)
|
|
28008
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
28009
|
+
/**
|
|
28010
|
+
* Force-gate any tool whose live MCP annotation declares `destructiveHint:true`
|
|
28011
|
+
* but that the classifier (or carry-forward) left un-gated.
|
|
28012
|
+
*
|
|
28013
|
+
* This is the ONLY way annotations influence policy, and it is deliberately
|
|
28014
|
+
* one-directional. The MCP spec warns that clients must never make tool-use
|
|
28015
|
+
* decisions on annotations from untrusted servers; trusting a server's
|
|
28016
|
+
* "I am destructive" claim only ever ADDS an approval prompt (the safe
|
|
28017
|
+
* direction), so it cannot be abused. The inverse — trusting `readOnlyHint` to
|
|
28018
|
+
* AUTO-APPROVE — is exactly the unsafe direction the spec forbids, so a spoofed
|
|
28019
|
+
* `readOnlyHint:true` on a destructive tool must never relax it. Read-only
|
|
28020
|
+
* auto-approval authority lives solely with the trusted LLM classifier.
|
|
28021
|
+
*
|
|
28022
|
+
* Recomputed from live discovery on every connect, so it has zero coupling to
|
|
28023
|
+
* `toolSignature`/incremental reuse and needs no persistence. Pure JS so it is
|
|
28024
|
+
* safe to evaluate inside the Temporal deterministic isolate.
|
|
28025
|
+
*/
|
|
28026
|
+
function applyDestructiveHintTightener(gated, currentTools) {
|
|
28027
|
+
const gatedNames = new Set(gated.map((g) => g.tool_name));
|
|
28028
|
+
const tightened = [...gated];
|
|
28029
|
+
let addedCount = 0;
|
|
28030
|
+
for (const tool of currentTools) {
|
|
28031
|
+
if (tool.annotations?.destructiveHint === true && !gatedNames.has(tool.name)) {
|
|
28032
|
+
tightened.push({
|
|
28033
|
+
tool_name: tool.name,
|
|
28034
|
+
requires_approval: true,
|
|
28035
|
+
message: `Execute ${tool.name}`,
|
|
28036
|
+
from_destructive_hint: true,
|
|
28037
|
+
});
|
|
28038
|
+
gatedNames.add(tool.name);
|
|
28039
|
+
addedCount++;
|
|
28040
|
+
}
|
|
28041
|
+
}
|
|
28042
|
+
return { tightened, addedCount };
|
|
28043
|
+
}
|
|
28044
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
27885
28045
|
// ConnectMcpServerWorkflow — primary connect flow
|
|
27886
28046
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
27887
28047
|
async function connectMcpServer(input) {
|
|
@@ -27890,23 +28050,22 @@ async function connectMcpServer(input) {
|
|
|
27890
28050
|
executionContextId: input.execution_context_id ?? null,
|
|
27891
28051
|
invokerIdentityAccountId: input.invoker_identity_account_id ?? null,
|
|
27892
28052
|
});
|
|
27893
|
-
|
|
27894
|
-
|
|
27895
|
-
|
|
28053
|
+
// Content-addressed incremental classification: reuse prior decisions for
|
|
28054
|
+
// tools whose definition is unchanged, and classify only the new or changed
|
|
28055
|
+
// ones. Keeps decisions stable/deterministic across reconnects and avoids
|
|
28056
|
+
// redundant LLM calls, while still re-evaluating a tool whose schema changed.
|
|
28057
|
+
const { toolsToClassify, carriedForward } = planIncrementalClassification(discovery.previousTools, discovery.previousToolApprovals, discovery.tools);
|
|
27896
28058
|
let toolApprovals;
|
|
27897
|
-
if (
|
|
27898
|
-
_temporalio_workflow__WEBPACK_IMPORTED_MODULE_0__.log.info(`Tools unchanged for '${input.mcp_server_id}' ` +
|
|
27899
|
-
|
|
27900
|
-
|
|
27901
|
-
toolApprovals = discovery.previousToolApprovals.map((a) => ({
|
|
27902
|
-
tool_name: a.toolName,
|
|
27903
|
-
requires_approval: a.requiresApproval,
|
|
27904
|
-
message: a.message,
|
|
27905
|
-
}));
|
|
28059
|
+
if (toolsToClassify.length === 0) {
|
|
28060
|
+
_temporalio_workflow__WEBPACK_IMPORTED_MODULE_0__.log.info(`Tools unchanged for '${input.mcp_server_id}' — reusing ` +
|
|
28061
|
+
`${carriedForward.length} previous approval(s), no classification needed`);
|
|
28062
|
+
toolApprovals = carriedForward;
|
|
27906
28063
|
}
|
|
27907
28064
|
else {
|
|
28065
|
+
_temporalio_workflow__WEBPACK_IMPORTED_MODULE_0__.log.info(`Classifying ${toolsToClassify.length} new/changed tool(s) for ` +
|
|
28066
|
+
`'${input.mcp_server_id}', reusing ${carriedForward.length} prior decision(s)`);
|
|
27908
28067
|
const classifyInput = {
|
|
27909
|
-
tools:
|
|
28068
|
+
tools: toolsToClassify.map((t) => ({
|
|
27910
28069
|
name: t.name,
|
|
27911
28070
|
description: t.description,
|
|
27912
28071
|
input_schema: t.inputSchema ?? null,
|
|
@@ -27915,9 +28074,22 @@ async function connectMcpServer(input) {
|
|
|
27915
28074
|
serverDescription: "",
|
|
27916
28075
|
mcpServerId: input.mcp_server_id,
|
|
27917
28076
|
};
|
|
27918
|
-
const classify = classifyWithTimeout(
|
|
27919
|
-
|
|
27920
|
-
|
|
28077
|
+
const classify = classifyWithTimeout(toolsToClassify.length);
|
|
28078
|
+
const classified = await classify.ClassifyToolApprovals(classifyInput);
|
|
28079
|
+
toolApprovals = [...carriedForward, ...classified];
|
|
28080
|
+
}
|
|
28081
|
+
// Fail-closed tightener over the FULL live tool set: a tool the server's own
|
|
28082
|
+
// annotation marks destructiveHint=true is force-gated if it slipped through
|
|
28083
|
+
// un-gated. Runs on live discovery (not the reused/classified subset), so it
|
|
28084
|
+
// also re-asserts gating for carried-forward tools whose server later flips a
|
|
28085
|
+
// tool to destructive. We never trust readOnlyHint to relax — see the
|
|
28086
|
+
// tightener's contract for the MCP untrusted-hints rationale.
|
|
28087
|
+
const { tightened, addedCount } = applyDestructiveHintTightener(toolApprovals, discovery.tools);
|
|
28088
|
+
if (addedCount > 0) {
|
|
28089
|
+
_temporalio_workflow__WEBPACK_IMPORTED_MODULE_0__.log.info(`Force-gated ${addedCount} tool(s) via destructiveHint annotation for ` +
|
|
28090
|
+
`'${input.mcp_server_id}'`);
|
|
28091
|
+
}
|
|
28092
|
+
toolApprovals = tightened;
|
|
27921
28093
|
return {
|
|
27922
28094
|
tools: discovery.tools.map((t) => ({
|
|
27923
28095
|
name: t.name,
|