@sema-agent/core 5.24.0 → 5.26.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/CHANGELOG.md +136 -0
- package/dist/agents/agent-definition.js +5 -0
- package/dist/agents/send-message-tool.js +1 -0
- package/dist/agents/subagent.d.ts +1 -0
- package/dist/agents/subagent.js +5 -0
- package/dist/core/checkpoint-store.d.ts +47 -8
- package/dist/core/checkpoint-store.js +1 -0
- package/dist/core/hooks.d.ts +12 -5
- package/dist/core/hooks.js +22 -4
- package/dist/core/memory-engine/dual-root.js +3 -1
- package/dist/core/memory-engine/engine.d.ts +45 -1
- package/dist/core/memory-engine/engine.js +40 -7
- package/dist/core/memory-engine/index.d.ts +1 -1
- package/dist/core/memory-engine/index.js +1 -1
- package/dist/core/permission-rule-consent.js +8 -1
- package/dist/core/permission-rule-org.d.ts +9 -0
- package/dist/core/permission-rule-org.js +12 -5
- package/dist/core/runner/compaction-call-options.d.ts +4 -4
- package/dist/core/runner/compaction-call-options.js +3 -4
- package/dist/core/runner/prepare-memory.d.ts +34 -15
- package/dist/core/runner/prepare-memory.js +85 -17
- package/dist/core/runner/prepare-task.d.ts +2 -0
- package/dist/core/runner/prepare-task.js +63 -11
- package/dist/core/runner/runtask.js +25 -8
- package/dist/core/store-contracts/tool-result-store-contract.d.ts +6 -0
- package/dist/core/store-contracts/tool-result-store-contract.js +24 -0
- package/dist/core/task-registry-agent.js +3 -3
- package/dist/core/task-registry-monitor.js +6 -5
- package/dist/core/tool-policy.d.ts +11 -0
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-budget.js +3 -3
- package/dist/core/tool-result-store.d.ts +164 -9
- package/dist/core/tool-result-store.js +82 -23
- package/dist/core/types.d.ts +68 -0
- package/dist/core/untrusted-text.d.ts +6 -2
- package/dist/core/untrusted-text.js +1 -1
- package/dist/engine/session/import-validate.js +2 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +4 -4
- package/dist/orchestration/workflow.js +2 -0
- package/dist/prompts/default.d.ts +11 -0
- package/dist/prompts/default.js +3 -0
- package/dist/stores/file/adoption/adopt.d.ts +23 -3
- package/dist/stores/file/adoption/adopt.js +1 -0
- package/dist/stores/file/adoption/marker.d.ts +26 -11
- package/dist/stores/file/fs-atomic.d.ts +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +15 -1
- package/dist/stores/file/permission-rule-store.js +4 -1
- package/dist/stores/file/task-list-store.d.ts +15 -1
- package/dist/stores/file/task-list-store.js +2 -2
- package/dist/stores/file/tool-result-store.d.ts +45 -9
- package/dist/stores/file/tool-result-store.js +76 -9
- package/dist/tools/fs/fs-shared.js +26 -9
- package/package.json +1 -1
|
@@ -89,6 +89,10 @@ import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOI
|
|
|
89
89
|
import { boundInputHashOf } from "../canonical-json.js";
|
|
90
90
|
import { countElicitOptIns, deriveAskEffective, deriveWiringManifest, resolveAskSeamForm, resolveDeclaredDurability, resolveElicitSeam, resolveQuestionSeam } from "../wiring-manifest.js";
|
|
91
91
|
import { GLOBAL_USAGE_KEY, resolveUsageWindows, usageRetryAfterMs } from "../usage-window-store.js";
|
|
92
|
+
const announcedMaterializeEnv = new Set();
|
|
93
|
+
export function __resetMaterializeEnvAnnouncements() {
|
|
94
|
+
announcedMaterializeEnv.clear();
|
|
95
|
+
}
|
|
92
96
|
const RECONCILE_MAX_RETRIES = 3;
|
|
93
97
|
const DEFAULT_MAX_SUSPENDS = 5;
|
|
94
98
|
const TASK_LIMIT_KEY_DICT = {
|
|
@@ -348,6 +352,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
348
352
|
e.code = "config.tool_materialize_invalid";
|
|
349
353
|
throw e;
|
|
350
354
|
}
|
|
355
|
+
if (spec.memoryPersistenceCapable !== undefined && typeof spec.memoryPersistenceCapable !== "boolean") {
|
|
356
|
+
const e = new Error(`memoryPersistenceCapable must be a boolean when present (got ${JSON.stringify(spec.memoryPersistenceCapable)}) — a non-boolean would silently read as capable.`);
|
|
357
|
+
e.code = "config.memory_persistence_invalid";
|
|
358
|
+
throw e;
|
|
359
|
+
}
|
|
351
360
|
if (spec.toolMaterializeStrategy === "static" && spec.deferSelfResolve === false) {
|
|
352
361
|
const e = new Error(`toolMaterializeStrategy "static" cannot be combined with deferSelfResolve: false — with the direct-call ` +
|
|
353
362
|
`lane disabled a placeholder is never swapped and never self-resolves, so no deferred tool could ever be ` +
|
|
@@ -1150,6 +1159,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1150
1159
|
...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
|
|
1151
1160
|
...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
|
|
1152
1161
|
...(spec.envFacts !== undefined ? { envFacts: { ...spec.envFacts } } : {}),
|
|
1162
|
+
...(spec.memoryPersistenceCapable !== undefined ? { memoryPersistenceCapable: spec.memoryPersistenceCapable } : {}),
|
|
1153
1163
|
getApiKeyAndHeaders: spec.getApiKeyAndHeaders,
|
|
1154
1164
|
parentCwd: taskRootPath,
|
|
1155
1165
|
...(centerAdoption !== undefined ? { centerArtifactDigest: centerAdoption.artifact.artifactDigest } : {}),
|
|
@@ -1683,20 +1693,24 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1683
1693
|
`Set spec.shellGate to "classify" or "always" if this deployment expects doctrine-gated shell behavior.`), { phase: "config", sessionId, classification: "shell-gate-off" });
|
|
1684
1694
|
}
|
|
1685
1695
|
if (shellGate !== "off" && !(executionEnv instanceof StubExecutionEnv) && spec.handsReadOnly !== true) {
|
|
1686
|
-
|
|
1687
|
-
|
|
1696
|
+
const bashTierBefore = irreversibilityTier.get("Bash");
|
|
1697
|
+
shellGatedBash = !egressTools.has("Bash") && bashTierBefore !== "always" && bashTierBefore !== "maybe";
|
|
1698
|
+
const bashEffectiveTier = shellGate === "always" || bashTierBefore === "always" ? "always" : "maybe";
|
|
1699
|
+
irreversibilityTier.set("Bash", bashEffectiveTier);
|
|
1688
1700
|
irreversibleTools.add("Bash");
|
|
1689
1701
|
const shellReadBoundary = () => ({
|
|
1690
1702
|
roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
|
|
1691
1703
|
...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
|
|
1692
1704
|
});
|
|
1693
|
-
if (shellGate === "classify")
|
|
1705
|
+
if (shellGate === "classify" && shellGatedBash)
|
|
1694
1706
|
reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
|
|
1695
1707
|
if (backgroundTaskToolsActive) {
|
|
1696
|
-
|
|
1697
|
-
|
|
1708
|
+
const monitorTierBefore = irreversibilityTier.get("Monitor");
|
|
1709
|
+
shellGatedMonitor = !egressTools.has("Monitor") && monitorTierBefore !== "always" && monitorTierBefore !== "maybe";
|
|
1710
|
+
const monitorEffectiveTier = shellGate === "always" || monitorTierBefore === "always" ? "always" : "maybe";
|
|
1711
|
+
irreversibilityTier.set("Monitor", monitorEffectiveTier);
|
|
1698
1712
|
irreversibleTools.add("Monitor");
|
|
1699
|
-
if (shellGate === "classify")
|
|
1713
|
+
if (shellGate === "classify" && shellGatedMonitor)
|
|
1700
1714
|
reversibilityProbes.set("Monitor", bashReversibilityProbe(undefined, shellReadBoundary));
|
|
1701
1715
|
}
|
|
1702
1716
|
}
|
|
@@ -1932,7 +1946,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1932
1946
|
sessionId,
|
|
1933
1947
|
taskRootPath,
|
|
1934
1948
|
memoryWriteGateRef,
|
|
1935
|
-
writeToolsMounted: tools.some((t) => t.name === "Write") &&
|
|
1949
|
+
writeToolsMounted: tools.some((t) => t.name === "Write") &&
|
|
1950
|
+
!(toolFaceSnapshot.exclude?.includes("Write") ?? false) &&
|
|
1951
|
+
!(handsEnabled && isRemoteExecutionEnv(executionEnv) && spec.memoryPersistenceCapable !== true),
|
|
1952
|
+
memoryPersistenceDeclared: spec.memoryPersistenceCapable,
|
|
1953
|
+
rosterCanPersist: spec.memoryPersistenceCapable ??
|
|
1954
|
+
tools.some((t) => {
|
|
1955
|
+
if (toolFaceSnapshot.exclude?.includes(t.name) ?? false)
|
|
1956
|
+
return false;
|
|
1957
|
+
const effect = (t.effect ?? toolEffects.get(t.name) ?? "write");
|
|
1958
|
+
if (effect === "read")
|
|
1959
|
+
return false;
|
|
1960
|
+
if (t.name !== "Write" && t.name !== "Edit" && t.name !== "NotebookEdit" && t.name !== "Bash")
|
|
1961
|
+
return false;
|
|
1962
|
+
return !(handsEnabled && isRemoteExecutionEnv(executionEnv));
|
|
1963
|
+
}),
|
|
1936
1964
|
memorySearchToolsPlanned,
|
|
1937
1965
|
admissionCtx: {
|
|
1938
1966
|
orgMemoryDenied: complianceDenies.has("org_memory_mount"),
|
|
@@ -2572,6 +2600,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2572
2600
|
toolsDeltaRef = { pending: [], pendingRemoved: [], pendingReadded: [], pendingFailed: failedMcpServers };
|
|
2573
2601
|
}
|
|
2574
2602
|
const listingRideRef = {};
|
|
2603
|
+
{
|
|
2604
|
+
const raw = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
|
|
2605
|
+
if (raw !== undefined && raw !== "swap" && raw !== "static" && deferred.size === 0) {
|
|
2606
|
+
const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(raw)} is not "swap" or "static" — inert on this task (no deferred tools), but a deferring task WITHOUT an explicit spec strategy will refuse to prepare under it (an explicit legal spec outranks and discards it, loudly). Fix or unset the flag.`;
|
|
2607
|
+
if (!announcedMaterializeEnv.has(line)) {
|
|
2608
|
+
announcedMaterializeEnv.add(line);
|
|
2609
|
+
console.warn(line);
|
|
2610
|
+
}
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2575
2613
|
if (deferred.size > 0) {
|
|
2576
2614
|
if (deferred.has(TOOL_SEARCH_NAME) || tools.some((t) => t.name === TOOL_SEARCH_NAME)) {
|
|
2577
2615
|
const e = new Error(`Tool name "${TOOL_SEARCH_NAME}" is reserved when deferred tools are present.`);
|
|
@@ -2579,12 +2617,21 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2579
2617
|
throw e;
|
|
2580
2618
|
}
|
|
2581
2619
|
const registry = buildDeferredRegistry(deferred, tools);
|
|
2582
|
-
const
|
|
2583
|
-
|
|
2584
|
-
|
|
2620
|
+
const rawEnvStrategy = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
|
|
2621
|
+
const envStrategyInvalid = rawEnvStrategy !== undefined && rawEnvStrategy !== "swap" && rawEnvStrategy !== "static";
|
|
2622
|
+
if (envStrategyInvalid && spec.toolMaterializeStrategy === undefined) {
|
|
2623
|
+
const e = new Error(`SEMA_TOOL_MATERIALIZE_STRATEGY must be "swap" or "static" (got ${JSON.stringify(rawEnvStrategy)}).`);
|
|
2585
2624
|
e.code = "config.tool_materialize_invalid";
|
|
2586
2625
|
throw e;
|
|
2587
2626
|
}
|
|
2627
|
+
if (envStrategyInvalid) {
|
|
2628
|
+
const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(rawEnvStrategy)} was ignored — not "swap" or "static", and the task spec pins toolMaterializeStrategy=${JSON.stringify(spec.toolMaterializeStrategy)} which outranks it. Fix or unset the env flag.`;
|
|
2629
|
+
if (!announcedMaterializeEnv.has(line)) {
|
|
2630
|
+
announcedMaterializeEnv.add(line);
|
|
2631
|
+
console.warn(line);
|
|
2632
|
+
}
|
|
2633
|
+
}
|
|
2634
|
+
const envStrategy = envStrategyInvalid ? undefined : rawEnvStrategy;
|
|
2588
2635
|
const requestedStrategy = spec.toolMaterializeStrategy ?? envStrategy ?? "swap";
|
|
2589
2636
|
const laneDegrade = requestedStrategy === "static" && spec.deferSelfResolve === false;
|
|
2590
2637
|
const materializeStatic = requestedStrategy === "static" && !laneDegrade;
|
|
@@ -3273,6 +3320,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3273
3320
|
...askSourceIdentity(),
|
|
3274
3321
|
...riskAxesOf(creq.toolName),
|
|
3275
3322
|
...(re.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3323
|
+
...(re.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: re.persistedRuleShadowed } : {}),
|
|
3276
3324
|
}, onAskOf, csignal ?? abortController.signal);
|
|
3277
3325
|
if (rr.action !== "allow")
|
|
3278
3326
|
return rr;
|
|
@@ -3357,6 +3405,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3357
3405
|
...askSourceIdentity(),
|
|
3358
3406
|
...riskAxesOf(creq.toolName),
|
|
3359
3407
|
...(first.action === "ask" && first.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3408
|
+
...(first.action === "ask" && first.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: first.persistedRuleShadowed } : {}),
|
|
3360
3409
|
}, pc.onAsk, csignal ?? abortController.signal);
|
|
3361
3410
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
3362
3411
|
if (resolved.action === "deny" && resolved.approverUnavailable === true) {
|
|
@@ -3443,6 +3492,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3443
3492
|
...askSourceIdentity(),
|
|
3444
3493
|
...riskAxesOf(creq.toolName),
|
|
3445
3494
|
...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3495
|
+
...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
|
|
3446
3496
|
}, pc.onAsk, csignal ?? abortController.signal);
|
|
3447
3497
|
const askWaitMs = Math.max(0, now() - askT0);
|
|
3448
3498
|
if (resolved.action === "deny" && resolved.approverUnavailable === true) {
|
|
@@ -3755,6 +3805,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3755
3805
|
...askSourceIdentity(),
|
|
3756
3806
|
...riskAxesOf(req.toolName),
|
|
3757
3807
|
...(decision.action === "ask" && decision.requiresRealApproval === true ? { requiresRealApproval: true } : {}),
|
|
3808
|
+
...(decision.action === "ask" && decision.persistedRuleShadowed !== undefined ? { persistedRuleShadowed: decision.persistedRuleShadowed } : {}),
|
|
3758
3809
|
}, onAsk, abortController.signal);
|
|
3759
3810
|
const waitMs = Math.max(0, now() - t0);
|
|
3760
3811
|
if (resolved.approverUnavailable !== true) {
|
|
@@ -4236,7 +4287,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4236
4287
|
}
|
|
4237
4288
|
};
|
|
4238
4289
|
const suspendAsk = parkLaneArmed && checkpointStore !== undefined
|
|
4239
|
-
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval) => {
|
|
4290
|
+
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule) => {
|
|
4240
4291
|
const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
|
|
4241
4292
|
if (syncFirstEligible &&
|
|
4242
4293
|
runtimeCaps?.forceDurableGate !== true &&
|
|
@@ -4341,6 +4392,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4341
4392
|
toolName: req.toolName,
|
|
4342
4393
|
args: parkedArgs,
|
|
4343
4394
|
safety,
|
|
4395
|
+
...(shadowedRule !== undefined ? { shadowedRule } : {}),
|
|
4344
4396
|
shellGated: (req.toolName === "Bash" && shellGatedBash) || (req.toolName === "Monitor" && shellGatedMonitor),
|
|
4345
4397
|
...(effectiveShellGate !== "off" ? { shellGateDoctrine: effectiveShellGate } : {}),
|
|
4346
4398
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
2
2
|
import { snapshotActorAssertion } from "../../internal/llm.js";
|
|
3
|
-
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
3
|
+
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
4
4
|
import { engineVersion } from "../version.js";
|
|
5
5
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
6
6
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
@@ -71,6 +71,7 @@ const STOP_HOOK_BLOCK_CAP = 8;
|
|
|
71
71
|
const COMPACTION_REGROWTH_FACTOR = 1.5;
|
|
72
72
|
const COMPACTION_FREED_EPSILON = 256;
|
|
73
73
|
const BATCH_RESPONSE_MAX_CHARS = 500;
|
|
74
|
+
const ORG_DISCLOSURE_MAX_CHARS = 600;
|
|
74
75
|
function batchResponseDigest(result) {
|
|
75
76
|
const content = result !== null && typeof result === "object" ? result.content : result;
|
|
76
77
|
if (content === undefined || content === null)
|
|
@@ -3702,7 +3703,7 @@ export class Runner {
|
|
|
3702
3703
|
}
|
|
3703
3704
|
}
|
|
3704
3705
|
if (checkpointVersionOf(cp) > MAX_SUPPORTED_CHECKPOINT_VERSION) {
|
|
3705
|
-
throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})
|
|
3706
|
+
throw new CheckpointError("checkpoint.unsupported_version", `checkpoint version ${checkpointVersionOf(cp)} is newer than this worker supports (max ${MAX_SUPPORTED_CHECKPOINT_VERSION})`, { reason: "version_newer" });
|
|
3706
3707
|
}
|
|
3707
3708
|
const preCasGateBit = cp.gate.kind === "irreversible_ask" ? cp.gate.realApproval : undefined;
|
|
3708
3709
|
const preCasBitWellFormed = preCasGateBit !== undefined &&
|
|
@@ -3713,11 +3714,11 @@ export class Runner {
|
|
|
3713
3714
|
if (checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? !preCasBitWellFormed : preCasGateBit !== undefined) {
|
|
3714
3715
|
throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
|
|
3715
3716
|
? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed non-budgetable realApproval gate bit (origin org_rule/org_unavailable/policy) on an irreversible_ask gate — this row does not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
|
|
3716
|
-
: `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending
|
|
3717
|
+
: `a v${checkpointVersionOf(cp)} checkpoint carries a realApproval gate bit no release of that version ever minted — refusing to honor a fabricated origin (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION ? "real_approval_damaged" : "real_approval_forged" });
|
|
3717
3718
|
}
|
|
3718
3719
|
if ((preCasGateBit?.origin === "org_rule" || preCasGateBit?.origin === "org_unavailable") &&
|
|
3719
3720
|
this.deps.permissionRuleOrg === undefined) {
|
|
3720
|
-
throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker
|
|
3721
|
+
throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's approval was minted under organization governance (${preCasGateBit.origin}) and this worker has no org adjudication wiring (permissionRuleOrg) — a governed approval may only be redeemed where governance can be enforced; the checkpoint stays pending, resume it on an org-wired worker`, { reason: "governed_unwired" });
|
|
3721
3722
|
}
|
|
3722
3723
|
const retiredWalltimeTotal = cp.resourceLedger?.totalWalltimeSec;
|
|
3723
3724
|
if (retiredWalltimeTotal !== undefined) {
|
|
@@ -3729,7 +3730,7 @@ export class Runner {
|
|
|
3729
3730
|
throw new CheckpointError("checkpoint.resume_aborted", "the resume was handed an ALREADY-ABORTED signal — refusing to consume the approval on a leg that cannot run it (the checkpoint stays pending and is resumable with a live signal)");
|
|
3730
3731
|
}
|
|
3731
3732
|
if (cp.state.workspaceHandle !== undefined && this.deps.executionEnvFactory === undefined) {
|
|
3732
|
-
throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env");
|
|
3733
|
+
throw new CheckpointError("checkpoint.unsupported_version", "checkpoint has a remote workspaceHandle but no RunnerDeps.executionEnvFactory is wired to rebuild the env", { reason: "env_factory_missing" });
|
|
3733
3734
|
}
|
|
3734
3735
|
if (cp.state.inheritedGate?.requiresParentConstraint === true) {
|
|
3735
3736
|
const supplied = internals?.inheritedGate?.parentConstraints?.length ?? 0;
|
|
@@ -3745,6 +3746,10 @@ export class Runner {
|
|
|
3745
3746
|
"re-supply would run the resumed leg under a different ancestor chain than it suspended with; " +
|
|
3746
3747
|
"rejected pre-CAS (the checkpoint stays pending) — re-resume with the full original chain");
|
|
3747
3748
|
}
|
|
3749
|
+
if (checkpointVersionOf(cp) >= F012_CHECKPOINT_VERSION &&
|
|
3750
|
+
(cp.state.inheritedGate.constraintChain === undefined || cp.state.inheritedGate.constraintDigest === undefined)) {
|
|
3751
|
+
throw new CheckpointError("checkpoint.invalid_outcome", `a v${checkpointVersionOf(cp)} checkpoint that requires parent constraints must carry BOTH the frozen constraintChain and its constraintDigest (they are minted in one write with the version stamp) — this row carries neither or only one; refusing to fall back to the count-only contract on a damaged row (corruption / downgrade guard), the checkpoint stays pending`, { reason: "constraint_chain_missing" });
|
|
3752
|
+
}
|
|
3748
3753
|
const expectedDigest = cp.state.inheritedGate.constraintDigest;
|
|
3749
3754
|
if (expectedDigest !== undefined) {
|
|
3750
3755
|
const persistedChain = cp.state.inheritedGate.constraintChain;
|
|
@@ -4037,16 +4042,28 @@ export class Runner {
|
|
|
4037
4042
|
const orgVerdict = prepared.permissionRuleOrg
|
|
4038
4043
|
.adjudicate({ toolName: pendingAction.toolName, args: resolvedArgs, toolCallId: pendingAction.toolCallId })
|
|
4039
4044
|
.catch(() => ({ status: "unavailable", disclosures: ["the org adjudication face threw on resume"] }));
|
|
4040
|
-
|
|
4045
|
+
let orgWaitCancelled = false;
|
|
4046
|
+
const org = await settleOrgVerdictWithin(orgVerdict, { status: "unavailable", disclosures: [`the org adjudication face did not answer within ${ORG_ADJUDICATION_TIMEOUT_MS}ms (or the task ended first)`] }, {
|
|
4047
|
+
signal: prepared.abortController.signal,
|
|
4048
|
+
timeoutMs: ORG_ADJUDICATION_TIMEOUT_MS,
|
|
4049
|
+
onFallback: (cause) => {
|
|
4050
|
+
orgWaitCancelled = cause === "aborted";
|
|
4051
|
+
},
|
|
4052
|
+
});
|
|
4041
4053
|
const blocked = org.status === "unavailable"
|
|
4042
4054
|
? gateRealApproval?.origin === "org_unavailable"
|
|
4043
4055
|
? undefined
|
|
4044
|
-
:
|
|
4056
|
+
: orgWaitCancelled
|
|
4057
|
+
? "this deployment is org-governed and the task was cancelled before the organization policy snapshot could be adjudicated"
|
|
4058
|
+
: "this deployment is org-governed and cannot currently adjudicate against an organization policy snapshot"
|
|
4045
4059
|
: org.verdict?.behavior === "deny"
|
|
4046
4060
|
? `an organization policy rule (${org.verdict.rule}) denies it`
|
|
4047
4061
|
: undefined;
|
|
4048
4062
|
if (blocked !== undefined) {
|
|
4049
|
-
const
|
|
4063
|
+
const orgDisclosures = org.status === "unavailable" && org.disclosures.length > 0
|
|
4064
|
+
? ` Governance disclosures: ${inlineUntrusted(org.disclosures.join("; "), ORG_DISCLOSURE_MAX_CHARS)}.`
|
|
4065
|
+
: "";
|
|
4066
|
+
const orgDenial = formatHookFeedback(`The approved tool call "${pendingAction.toolName}" was not executed: ${blocked}.${orgDisclosures} This approval is spent — the call has to be re-issued and approved again once organization policy permits it.`);
|
|
4050
4067
|
emitEnd(true, { content: orgDenial });
|
|
4051
4068
|
const eid = await prepared.session.appendMessage(toolResultMsg(pendingAction.toolCallId, pendingAction.toolName, orgDenial, true));
|
|
4052
4069
|
emitCommitted(eid, "toolResult", pendingAction.toolCallId);
|
|
@@ -12,5 +12,11 @@ import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
|
12
12
|
* with a narrower native key space owns an injective encoding, not a rejection). Before 2.1.0 the
|
|
13
13
|
* bundled backends genuinely diverged on both halves (dev-green / deployment-red), so running this
|
|
14
14
|
* kit against a pre-2.1.0-modeled backend is expected to go red on those two entries.
|
|
15
|
+
*
|
|
16
|
+
* Backlog #119 adds the provenance leg: `put`'s third argument and the `ownerOf` read face. `ownerOf`
|
|
17
|
+
* is typed OPTIONAL on the interface (source compatibility for a backend written against an older
|
|
18
|
+
* engine), but this kit REQUIRES it — a backend that cannot say who owns a ref cannot authorize a
|
|
19
|
+
* host-side read face, and an optional parameter one store honors while another drops it silently is
|
|
20
|
+
* exactly the divergence the two legs above exist to prevent.
|
|
15
21
|
*/
|
|
16
22
|
export declare function toolResultStoreContract(make: () => ToolResultStore, runAssertion?: ContractAssertionRunner): Promise<void>;
|
|
@@ -31,5 +31,29 @@ export async function toolResultStoreContract(make, runAssertion) {
|
|
|
31
31
|
}
|
|
32
32
|
assert.equal((await store.get("tr_sess_call:1")).content, "payload-tr_sess_call:1");
|
|
33
33
|
});
|
|
34
|
+
run("#119 provenance: content+owner are one write; ownerOf answers; a different owner is a TYPED refusal; unowned rows stay unowned", async () => {
|
|
35
|
+
const store = make();
|
|
36
|
+
assert.equal(typeof store.ownerOf, "function", "a backend must implement ownerOf — a store that cannot say who owns a ref cannot back a host read face");
|
|
37
|
+
const ownerOf = (ref) => Promise.resolve(store.ownerOf(ref));
|
|
38
|
+
const ref = "tr_s1~c1";
|
|
39
|
+
await store.put(ref, "0123456789", { sessionId: "sess-A", taskId: "task-1" });
|
|
40
|
+
assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" }, "the winning write's owner round-trips");
|
|
41
|
+
await store.put(ref, "IGNORED", { sessionId: "sess-A", taskId: "task-1" });
|
|
42
|
+
assert.equal((await store.get(ref)).content, "0123456789");
|
|
43
|
+
assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" });
|
|
44
|
+
await store.put(ref, "IGNORED");
|
|
45
|
+
assert.deepEqual(await ownerOf(ref), { sessionId: "sess-A", taskId: "task-1" }, "an ownerless put must not clear the owner");
|
|
46
|
+
for (const other of [{ sessionId: "sess-B" }, { sessionId: "sess-A", taskId: "task-2" }]) {
|
|
47
|
+
await assert.rejects((async () => store.put(ref, "other tenant's bytes", other))(), (err) => err.code === "tool_result.ref_conflict", `put(${JSON.stringify(other)}) on an occupied ref must reject with code tool_result.ref_conflict`);
|
|
48
|
+
}
|
|
49
|
+
assert.equal((await store.get(ref)).content, "0123456789", "a refused put must not have overwritten anything");
|
|
50
|
+
const unowned = "tr_no_owner~c";
|
|
51
|
+
await store.put(unowned, "bytes from a write site that stated no owner");
|
|
52
|
+
assert.equal(await ownerOf(unowned), undefined, "a row stored without provenance is UNOWNED");
|
|
53
|
+
await store.put(unowned, "bytes from a write site that stated no owner", { sessionId: "sess-A" });
|
|
54
|
+
assert.equal(await ownerOf(unowned), undefined, "put must not back-fill an owner onto an unowned row");
|
|
55
|
+
assert.equal((await store.get(unowned)).content, "bytes from a write site that stated no owner");
|
|
56
|
+
assert.equal(await ownerOf("tr_never~written"), undefined);
|
|
57
|
+
});
|
|
34
58
|
await settle();
|
|
35
59
|
}
|
|
@@ -6,7 +6,7 @@ import { shutdownDebug } from "./shutdown-debug.js";
|
|
|
6
6
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
7
7
|
import { boundedRedactedSummary } from "./untrusted-egress.js";
|
|
8
8
|
import { mintCompletionId, commitCompletionIdIfEmpty, clipTaskOutput, assertOwnership, sleepPollStep, alreadyTerminalStopNote, canAccess, normalizeAgentName, closestName, DURABLE_AGENT_HEARTBEAT_MS, DURABLE_AGENT_HANDLE_RE, BG_AGENT_REAP_STOP_ERROR, } from "./task-registry-shared.js";
|
|
9
|
-
import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
|
|
9
|
+
import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
|
|
10
10
|
export function ensureDurableHeartbeatLane(core) {
|
|
11
11
|
if (core.durableHeartbeatTimer !== undefined)
|
|
12
12
|
return;
|
|
@@ -1232,8 +1232,8 @@ export async function spillClippedAgentResult(handle, full, clipped, store, sess
|
|
|
1232
1232
|
if (store === undefined)
|
|
1233
1233
|
return clipped;
|
|
1234
1234
|
if (handle.spillRef === undefined) {
|
|
1235
|
-
const ref = buildToolResultRef(sessionId ?? "no-session",
|
|
1236
|
-
await store.put(ref, full);
|
|
1235
|
+
const ref = buildToolResultRef(sessionId ?? "no-session", handle.id, `c${handle.reviveCycle ?? 0}`);
|
|
1236
|
+
await store.put(ref, full, sessionId === undefined ? undefined : toolResultProvenanceOf(sessionId, handle.id));
|
|
1237
1237
|
handle.spillRef = ref;
|
|
1238
1238
|
}
|
|
1239
1239
|
return `${clipped}\n\n[full output persisted — call ${OFFLOAD_TOOL_NAME} with ref "${handle.spillRef}" to read it back.]`;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { delimitUntrusted } from "./untrusted-text.js";
|
|
2
2
|
import { assertOwnership, defaultMonitorTimers, MONITOR_MAX_TIMEOUT_MS, MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_BATCH_WINDOW_MS, MONITOR_MAX_BATCHES_PER_MINUTE, MONITOR_STORM_BURST, MONITOR_STORM_KILL_AFTER_MS, MONITOR_LINE_BUF_CAP, MONITOR_SPILL_CAP_CHARS, TASK_OUTPUT_MAX_CHARS, mintCompletionId, clipMonitorEvent, clipMonitorLine, terminalTaskSummary, accountDroppedBytes, renderSpoolBody, rollSpoolText, statusFromBackground, droppedGapNote, firstDropNote, alreadyTerminalStopNote, clipTaskOutput, sleepPollStep, } from "./task-registry-shared.js";
|
|
3
|
-
import { buildToolResultRef, OFFLOAD_TOOL_NAME } from "./tool-result-store.js";
|
|
3
|
+
import { buildToolResultRef, OFFLOAD_TOOL_NAME, toolResultProvenanceOf } from "./tool-result-store.js";
|
|
4
4
|
export function registerMonitorLane(core, input) {
|
|
5
5
|
assertOwnership(input, "registerMonitor");
|
|
6
6
|
const id = core.mintTaskId("monitor");
|
|
@@ -61,14 +61,15 @@ function spillRolledMonitorChunk(handle, stream, dropped) {
|
|
|
61
61
|
return;
|
|
62
62
|
}
|
|
63
63
|
const n = stream === "out" ? (handle.spillSegCount ?? 0) : (handle.spillErrSegCount ?? 0);
|
|
64
|
-
const ref = buildToolResultRef(handle.spillSessionId ?? "no-session",
|
|
64
|
+
const ref = buildToolResultRef(handle.spillSessionId ?? "no-session", handle.id, `${stream}_seg${n}`);
|
|
65
65
|
handle.spillCharsUsed = used + dropped.length;
|
|
66
66
|
if (stream === "out")
|
|
67
67
|
handle.spillSegCount = n + 1;
|
|
68
68
|
else
|
|
69
69
|
handle.spillErrSegCount = n + 1;
|
|
70
70
|
try {
|
|
71
|
-
|
|
71
|
+
const provenance = handle.spillSessionId === undefined ? undefined : toolResultProvenanceOf(handle.spillSessionId, handle.id);
|
|
72
|
+
void Promise.resolve(store.put(ref, dropped, provenance)).catch(() => {
|
|
72
73
|
handle.spillFailed = true;
|
|
73
74
|
});
|
|
74
75
|
}
|
|
@@ -83,8 +84,8 @@ function monitorSpillNote(handle) {
|
|
|
83
84
|
return "";
|
|
84
85
|
const sid = handle.spillSessionId ?? "no-session";
|
|
85
86
|
const segLabel = (stream, n) => {
|
|
86
|
-
const first = buildToolResultRef(sid,
|
|
87
|
-
return n <= 1 ? `ref "${first}"` : `refs "${first}" .. "${buildToolResultRef(sid,
|
|
87
|
+
const first = buildToolResultRef(sid, handle.id, `${stream}_seg0`);
|
|
88
|
+
return n <= 1 ? `ref "${first}"` : `refs "${first}" .. "${buildToolResultRef(sid, handle.id, `${stream}_seg${n - 1}`)}"`;
|
|
88
89
|
};
|
|
89
90
|
const clauses = [];
|
|
90
91
|
if (outN > 0)
|
|
@@ -167,6 +167,13 @@ export type PermissionResult = {
|
|
|
167
167
|
message?: string;
|
|
168
168
|
decisionReason?: DecisionReason;
|
|
169
169
|
requiresRealApproval?: boolean;
|
|
170
|
+
/** #144 disclosure (additive): a persisted allow rule MATCHED this call but could not clear the
|
|
171
|
+
* ask, because the ask is MANDATED (operator shellGate:"always", or the tool's own
|
|
172
|
+
* egress/irreversibility marks) rather than a classifier's hesitation — "allow rules silence
|
|
173
|
+
* the classifier's questions, never a mandated one". Carries the matched rule text so a
|
|
174
|
+
* consumer (approval card, wire frame) can tell the person their rule is alive, just outranked.
|
|
175
|
+
* Absent ⇒ no rule matched, or the ask was cleared normally. */
|
|
176
|
+
persistedRuleShadowed?: string;
|
|
170
177
|
} | {
|
|
171
178
|
action: "deny";
|
|
172
179
|
updatedInput?: unknown;
|
|
@@ -601,6 +608,10 @@ export interface AskDelegationProvenance {
|
|
|
601
608
|
/** The structured context an `onAsk` approver receives for an `ask` decision (design/37). */
|
|
602
609
|
export interface AskRequest {
|
|
603
610
|
toolName: string;
|
|
611
|
+
/** #144: a persisted allow rule MATCHED this call but could not clear the ask (mandated — see
|
|
612
|
+
* {@link PermissionResult}'s ask arm). The matched rule text, so the approval card renders "your
|
|
613
|
+
* rule is alive, just outranked" instead of leaving the person to regex the message prose. */
|
|
614
|
+
persistedRuleShadowed?: string;
|
|
604
615
|
toolCallId: string;
|
|
605
616
|
/** The (post-rewrite) args the tool would run with. */
|
|
606
617
|
args: unknown;
|
|
@@ -10,7 +10,7 @@ import { type ToolResultStore } from "./tool-result-store.js";
|
|
|
10
10
|
* **Simpler than CC by construction (§24.2):** a request-only, non-destructive transform (returns a new
|
|
11
11
|
* array; the durable session keeps full results) applied in the existing `harness.on("context")` hook —
|
|
12
12
|
* the same per-query point as `clearStaleToolResults` (the mirror of CC's query.ts:379). It is DETERMINISTIC
|
|
13
|
-
* (stable `ref = tr_<sessionId
|
|
13
|
+
* (stable `ref = tr_<sessionId>~<toolCallId>` + deterministic {@link buildPreview}), so re-running it every
|
|
14
14
|
* query yields byte-identical previews → prompt-cache safe **without** CC's ContentReplacementState freeze
|
|
15
15
|
* machine (the determinism IS the freeze), and resume-free (the transcript holds originals; this re-applies
|
|
16
16
|
* on replay). Reuses design/30's offload store + preview format; with no store it falls back to a
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isToolResult } from "./message-utils.js";
|
|
2
|
-
import { buildPreview, buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX } from "./tool-result-store.js";
|
|
2
|
+
import { buildPreview, buildToolResultRef, OFFLOAD_TOOL_NAME, PERSISTED_OUTPUT_PREFIX, toolResultContentSegment, toolResultProvenanceOf, } from "./tool-result-store.js";
|
|
3
3
|
export const AGGREGATE_TOOL_RESULT_BUDGET_CHARS = 200_000;
|
|
4
4
|
const HEAD = 1_000;
|
|
5
5
|
const TAIL = 1_000;
|
|
@@ -61,13 +61,13 @@ export async function capAggregateToolResults(messages, opts) {
|
|
|
61
61
|
const previewOne = async (k, before, head, tail, fromOriginal) => {
|
|
62
62
|
const m = out[k];
|
|
63
63
|
const full = textOf((fromOriginal ? messages[k] : m).content);
|
|
64
|
-
const ref = buildToolResultRef(opts.sessionId, m.toolCallId ?? `idx${k}
|
|
64
|
+
const ref = buildToolResultRef(opts.sessionId, m.toolCallId ?? `idx${k}`, toolResultContentSegment(full));
|
|
65
65
|
const sizes = head === HEAD && tail === TAIL ? undefined : { head, tail };
|
|
66
66
|
let previewText;
|
|
67
67
|
let storeFallback;
|
|
68
68
|
if (opts.store) {
|
|
69
69
|
try {
|
|
70
|
-
await opts.store.put(ref, full);
|
|
70
|
+
await opts.store.put(ref, full, toolResultProvenanceOf(opts.sessionId));
|
|
71
71
|
previewText = buildPreview(full, ref, sizes);
|
|
72
72
|
storeFallback = false;
|
|
73
73
|
}
|