@sema-agent/core 7.1.0 → 7.2.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 +36 -0
- package/dist/agents/cross-session-envelope.d.ts +138 -0
- package/dist/agents/cross-session-envelope.js +191 -0
- package/dist/agents/cross-session-judge.d.ts +119 -0
- package/dist/agents/cross-session-judge.js +184 -0
- package/dist/agents/cross-session-ref.d.ts +52 -0
- package/dist/agents/cross-session-ref.js +64 -0
- package/dist/agents/send-message-tool.d.ts +13 -0
- package/dist/agents/send-message-tool.js +36 -12
- package/dist/core/checkpoint-store.d.ts +189 -3
- package/dist/core/checkpoint-store.js +56 -16
- package/dist/core/hooks.d.ts +15 -8
- package/dist/core/hooks.js +6 -3
- package/dist/core/permission-rule-consent.d.ts +72 -23
- package/dist/core/permission-rule-consent.js +115 -26
- package/dist/core/permission-rule-model.d.ts +245 -51
- package/dist/core/permission-rule-model.js +312 -54
- package/dist/core/permission-rule-org.js +13 -6
- package/dist/core/remote-env.d.ts +8 -1
- package/dist/core/runner/assemble-result.js +2 -1
- package/dist/core/runner/prepare-task.d.ts +39 -1
- package/dist/core/runner/prepare-task.js +278 -113
- package/dist/core/runner/prepare-workspace-restore.d.ts +6 -1
- package/dist/core/runner/prepare-workspace-restore.js +2 -1
- package/dist/core/runner/runtask.js +13 -3
- package/dist/core/task-notification.d.ts +64 -5
- package/dist/core/task-notification.js +25 -4
- package/dist/core/tool-policy.d.ts +11 -0
- package/dist/core/types.d.ts +23 -0
- package/dist/core/untrusted-text.js +17 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +125 -1
|
@@ -23,7 +23,8 @@ import { createSendMessageTool, SEND_MESSAGE_TOOL_NAME } from "../../agents/send
|
|
|
23
23
|
import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
|
|
24
24
|
import { askApproverIdentity, carriesBidiControls, checkToolPolicyProjection, combinePolicies, constraintChainDigest, constraintChainEntryOfLayer, isApprovalSettledBy, isAskDenyResolution, screenApproverAttribution, createTranscriptIntegrityPolicy, createUnverifiableDeletePolicy, describeThrown, refuseOutOfContractDecision, resolveAsk, toolPolicyNameSets, tryCloneArgs } from "../tool-policy.js";
|
|
25
25
|
const PERSISTED_RULE_TOOL = "Bash";
|
|
26
|
-
|
|
26
|
+
const DIRECTORY_RULE_TOOL = "Read";
|
|
27
|
+
import { directoryRuleAdmits, eligiblePersisted, findAdmittingRule, lexicalNormalAbsolutePathOf, segmentCoverageOf, suggestRulesForCommand } from "../permission-rule-model.js";
|
|
27
28
|
import { normalizePersistedRule } from "../permission-rule-store.js";
|
|
28
29
|
import { ActiveSkillScope, createActiveSkillScopePolicy } from "./active-skill-scope.js";
|
|
29
30
|
import { CHANGED_FILES_MTIME_EPS_MS, fenceMcpServerInstructions, renderAgentListingDelta } from "./turn-attachments.js";
|
|
@@ -76,7 +77,7 @@ import { capAggregateToolResults } from "../tool-result-budget.js";
|
|
|
76
77
|
import { capAggregateMediaBytes, AGGREGATE_MEDIA_BUDGET_BYTES } from "../media-byte-cap.js";
|
|
77
78
|
import { dropOrphanToolResults, guardBudget, insertTrimNotice, trimToBudget } from "../context-guard.js";
|
|
78
79
|
import { StubExecutionEnv } from "../stub-env.js";
|
|
79
|
-
import { hasDestroy, isIsolated, isRemoteExecutionEnv, isSuspendable, missingRestoreSurface } from "../remote-env.js";
|
|
80
|
+
import { hasDestroy, isIsolated, isRemoteExecutionEnv, isSuspendable, missingRestoreSurface, RemoteExecutionError } from "../remote-env.js";
|
|
80
81
|
import { settleTeardownLeg } from "./teardown-bounded.js";
|
|
81
82
|
import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.js";
|
|
82
83
|
import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../task-registry.js";
|
|
@@ -713,6 +714,93 @@ function consumeInheritedAskGrant(grants, onAsk, humanReviewRef, decision, req)
|
|
|
713
714
|
});
|
|
714
715
|
return { action: "allow", presentedInput: grant.presented };
|
|
715
716
|
}
|
|
717
|
+
function raceSettlementAgainstSignal(p, signal) {
|
|
718
|
+
const settled = p.then((value) => ({ tag: "value", value }), (error) => ({ tag: "threw", error }));
|
|
719
|
+
if (signal.aborted)
|
|
720
|
+
return Promise.resolve({ tag: "aborted" });
|
|
721
|
+
return new Promise((resolve) => {
|
|
722
|
+
let done = false;
|
|
723
|
+
const finish = (r) => {
|
|
724
|
+
if (done)
|
|
725
|
+
return;
|
|
726
|
+
done = true;
|
|
727
|
+
signal.removeEventListener("abort", onAbortEvent);
|
|
728
|
+
resolve(r);
|
|
729
|
+
};
|
|
730
|
+
const onAbortEvent = () => finish({ tag: "aborted" });
|
|
731
|
+
signal.addEventListener("abort", onAbortEvent, { once: true });
|
|
732
|
+
void settled.then(finish);
|
|
733
|
+
});
|
|
734
|
+
}
|
|
735
|
+
const PARK_COMPENSATION_TIMEOUT_MS = 30_000;
|
|
736
|
+
export async function compensateUnparkedPause(remoteEnv, snapshotId, io) {
|
|
737
|
+
const boundCtl = new AbortController();
|
|
738
|
+
const boundTimer = setTimeout(() => boundCtl.abort(), io.boundMs);
|
|
739
|
+
const bound = boundCtl.signal;
|
|
740
|
+
let phase = "resumeVM";
|
|
741
|
+
let restoreAttempts = 1;
|
|
742
|
+
let vmRunning = false;
|
|
743
|
+
const work = (async () => {
|
|
744
|
+
const { outcome: back, attempts: backAttempts } = await restoreWorkspaceWithRetry(remoteEnv, snapshotId, { abortSignal: bound }, (attempt) => (restoreAttempts = attempt));
|
|
745
|
+
if (!back.ok) {
|
|
746
|
+
io.noteFailure(remoteEnvFailureNote("resumeVM", back.error, backAttempts));
|
|
747
|
+
io.disclose(back.error);
|
|
748
|
+
return { ok: false, reason: `resumeVM failed (${back.error.code}) while compensating an unparked pause` };
|
|
749
|
+
}
|
|
750
|
+
vmRunning = true;
|
|
751
|
+
if (bound.aborted) {
|
|
752
|
+
return { ok: false, reason: "resumeVM settled only after the decision bound" };
|
|
753
|
+
}
|
|
754
|
+
phase = "postResumeInit";
|
|
755
|
+
const init = await remoteEnv.postResumeInit({ abortSignal: bound });
|
|
756
|
+
if (!init.ok) {
|
|
757
|
+
io.noteFailure(remoteEnvFailureNote("postResumeInit", init.error, 1));
|
|
758
|
+
io.disclose(init.error);
|
|
759
|
+
return { ok: false, reason: `postResumeInit failed (${init.error.code}) while compensating an unparked pause` };
|
|
760
|
+
}
|
|
761
|
+
return { ok: true };
|
|
762
|
+
})();
|
|
763
|
+
const raced = await raceSettlementAgainstSignal(work, bound);
|
|
764
|
+
clearTimeout(boundTimer);
|
|
765
|
+
if (raced.tag === "value")
|
|
766
|
+
return raced.value;
|
|
767
|
+
if (raced.tag === "threw") {
|
|
768
|
+
io.noteFailure(remoteEnvFailureNote(phase, raced.error instanceof RemoteExecutionError
|
|
769
|
+
? raced.error
|
|
770
|
+
: new RemoteExecutionError("unknown", `the pause compensation's ${phase} call threw: ${raced.error instanceof Error ? raced.error.message : String(raced.error)}`), phase === "resumeVM" ? restoreAttempts : 1));
|
|
771
|
+
io.disclose(raced.error);
|
|
772
|
+
return {
|
|
773
|
+
ok: false,
|
|
774
|
+
reason: "the pause compensation threw (adapter contract violation; the deployment's error face carries the exception)",
|
|
775
|
+
};
|
|
776
|
+
}
|
|
777
|
+
const timedOut = new RemoteExecutionError("timeout", `the pause compensation's ${phase} call did not settle within the ${io.boundMs}ms decision bound`);
|
|
778
|
+
io.noteFailure(remoteEnvFailureNote(phase, timedOut, phase === "resumeVM" ? restoreAttempts : 1));
|
|
779
|
+
io.disclose(timedOut);
|
|
780
|
+
const destroyOwnerlessLate = () => {
|
|
781
|
+
io.disclose(new Error(`park compensation settled after its ${io.boundMs}ms decision bound: the VM is running with no run ` +
|
|
782
|
+
`left to own it — destroying the env (an ownerless running VM must not linger; the run already took ` +
|
|
783
|
+
`the fail-closed arm when the bound fired)`));
|
|
784
|
+
void Promise.resolve()
|
|
785
|
+
.then(() => remoteEnv.destroy())
|
|
786
|
+
.catch((destroyErr) => io.disclose(destroyErr));
|
|
787
|
+
};
|
|
788
|
+
void work.then((late) => {
|
|
789
|
+
if (vmRunning) {
|
|
790
|
+
destroyOwnerlessLate();
|
|
791
|
+
return;
|
|
792
|
+
}
|
|
793
|
+
void late;
|
|
794
|
+
}, (lateErr) => {
|
|
795
|
+
io.disclose(lateErr);
|
|
796
|
+
if (vmRunning)
|
|
797
|
+
destroyOwnerlessLate();
|
|
798
|
+
});
|
|
799
|
+
return {
|
|
800
|
+
ok: false,
|
|
801
|
+
reason: `the pause compensation did not settle within ${io.boundMs}ms — treated as failed (a late-restored VM is destroyed; a late restore failure is disclosed)`,
|
|
802
|
+
};
|
|
803
|
+
}
|
|
716
804
|
function raceAbort(p, signal, onAbort) {
|
|
717
805
|
if (signal.aborted)
|
|
718
806
|
return Promise.resolve(onAbort());
|
|
@@ -869,6 +957,87 @@ function persistedRuleHitOf(admitting) {
|
|
|
869
957
|
? undefined
|
|
870
958
|
: { rules: admitting.map((r) => ({ rule: r.rule, dots: r.adds.map((a) => ({ actor: a.dot.actor, counter: a.dot.counter })) })) };
|
|
871
959
|
}
|
|
960
|
+
function directoryRuleLaneAnswer(table, args, ctx) {
|
|
961
|
+
const filePath = args?.file_path;
|
|
962
|
+
if (typeof filePath !== "string" || filePath === "")
|
|
963
|
+
return undefined;
|
|
964
|
+
const spelled = filePath.startsWith("/") ? filePath : `${(ctx.liveCwd ?? ctx.root ?? "").replace(/\/+$/, "")}/${filePath}`;
|
|
965
|
+
const target = lexicalNormalAbsolutePathOf(spelled);
|
|
966
|
+
if (target === undefined)
|
|
967
|
+
return undefined;
|
|
968
|
+
const hit = table.find((r) => eligiblePersisted(r, { tool: DIRECTORY_RULE_TOOL, cwd: ctx.root, sessionId: ctx.sessionId }) && directoryRuleAdmits(r, target));
|
|
969
|
+
return hit !== undefined ? persistedRuleHitOf([hit]) : undefined;
|
|
970
|
+
}
|
|
971
|
+
function makeRuleOffersOf(cfg) {
|
|
972
|
+
return (toolName, args, ask) => {
|
|
973
|
+
if (!cfg.laneArmed || toolName !== PERSISTED_RULE_TOOL)
|
|
974
|
+
return {};
|
|
975
|
+
if ((cfg.principal === undefined || cfg.principal === "") && !cfg.localOwnerDeclared)
|
|
976
|
+
return {};
|
|
977
|
+
const closedDoor = (() => {
|
|
978
|
+
if (ask?.requiresRealApproval === true)
|
|
979
|
+
return "mandated";
|
|
980
|
+
if (ask?.persistedRuleShadowed !== undefined)
|
|
981
|
+
return "shadowed";
|
|
982
|
+
if (ask?.decisionReason === "hook")
|
|
983
|
+
return "mandated";
|
|
984
|
+
if (ask?.matchedAskRule !== undefined)
|
|
985
|
+
return "shadowed";
|
|
986
|
+
if (ask?.inheritedUnresolved === true)
|
|
987
|
+
return "mandated";
|
|
988
|
+
if (ask?.ancestorResolved === true)
|
|
989
|
+
return "mandated";
|
|
990
|
+
return undefined;
|
|
991
|
+
})();
|
|
992
|
+
if (closedDoor !== undefined)
|
|
993
|
+
return { ruleOffersAbsence: closedDoor };
|
|
994
|
+
if (persistedRuleMandateOf({
|
|
995
|
+
egress: cfg.egressTools.has(toolName),
|
|
996
|
+
irreversibility: cfg.irreversibilityTier.get(toolName),
|
|
997
|
+
shellGated: cfg.shellGatedBash,
|
|
998
|
+
probeMandated: ask?.probeMandated === true,
|
|
999
|
+
}) !== undefined) {
|
|
1000
|
+
return { ruleOffersAbsence: "mandated" };
|
|
1001
|
+
}
|
|
1002
|
+
const command = args?.command;
|
|
1003
|
+
if (typeof command !== "string")
|
|
1004
|
+
return { ruleOffersAbsence: "lane_cannot_speak" };
|
|
1005
|
+
const offers = suggestRulesForCommand(command, {
|
|
1006
|
+
...(ask?.segmentCoverage !== undefined ? { coverage: ask.segmentCoverage } : {}),
|
|
1007
|
+
...(cfg.taskRoot !== undefined ? { scope: { kind: "project", root: cfg.taskRoot }, cwd: cfg.taskRoot } : {}),
|
|
1008
|
+
...(cfg.cwdRef?.current !== undefined ? { execCwd: cfg.cwdRef.current } : {}),
|
|
1009
|
+
...(cfg.deniesDirectoryRead !== undefined ? { deniesDirectoryRead: cfg.deniesDirectoryRead } : {}),
|
|
1010
|
+
});
|
|
1011
|
+
return offers.length > 0
|
|
1012
|
+
? { ruleOffers: offers, ...(cfg.cwdRef?.current !== undefined ? { execCwd: cfg.cwdRef.current } : {}) }
|
|
1013
|
+
: { ruleOffersAbsence: "lane_cannot_speak" };
|
|
1014
|
+
};
|
|
1015
|
+
}
|
|
1016
|
+
function makeOrgAdjudicationLane(overlay, questionToolMounted, onRevisionDefect) {
|
|
1017
|
+
if (overlay === undefined)
|
|
1018
|
+
return undefined;
|
|
1019
|
+
return {
|
|
1020
|
+
adjudicate: async (req) => {
|
|
1021
|
+
if (req.toolName === ASK_USER_QUESTION_TOOL_NAME && questionToolMounted)
|
|
1022
|
+
return { status: "available" };
|
|
1023
|
+
let resolution;
|
|
1024
|
+
try {
|
|
1025
|
+
resolution = await overlay.resolve();
|
|
1026
|
+
}
|
|
1027
|
+
catch (err) {
|
|
1028
|
+
return { status: "unavailable", disclosures: [`the org rule overlay threw: ${err instanceof Error ? err.message : String(err)}`] };
|
|
1029
|
+
}
|
|
1030
|
+
if (resolution.status === "unavailable")
|
|
1031
|
+
return { status: "unavailable", disclosures: resolution.disclosures };
|
|
1032
|
+
const revisionCell = orgRevisionEvidenceOf(resolution, onRevisionDefect);
|
|
1033
|
+
const command = req.args?.command;
|
|
1034
|
+
if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
|
|
1035
|
+
return { status: "available", ...revisionCell };
|
|
1036
|
+
const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
|
|
1037
|
+
return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
|
|
1038
|
+
},
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
872
1041
|
function cwdConflictsRestoreError(requestedCwd) {
|
|
873
1042
|
const e = new Error(`RunInternals.requestedCwd ("${requestedCwd}") cannot be combined with a checkpoint workspace restore — ` +
|
|
874
1043
|
`the restored workspace's own mount path is authoritative for the task root, so a requested cwd on this leg ` +
|
|
@@ -2037,10 +2206,13 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2037
2206
|
handsLessResolvedFace = handsReadFace.handsLessResolvedFace;
|
|
2038
2207
|
shellGatedBash = handsReadFace.shellGatedBash;
|
|
2039
2208
|
shellGatedMonitor = handsReadFace.shellGatedMonitor;
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2209
|
+
const delegationSurfaceActive = backgroundTaskToolsActive || workflowToolsActive;
|
|
2210
|
+
if (delegationSurfaceActive || internals?.parentNotify !== undefined) {
|
|
2211
|
+
if (delegationSurfaceActive) {
|
|
2212
|
+
toolEffects.set("TaskOutput", "read");
|
|
2213
|
+
toolEffects.set("TaskStop", "write");
|
|
2214
|
+
tools.push(firstPartyOffload(createTaskOutputTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore, notificationWired: internals?.onTaskNotification !== undefined, oneShot: spec.oneShot, toolResultStore: offloadStore })), firstPartyOffload(createTaskStopTool({ registry: defaultTaskRegistry, owner: hostTaskId, scope: taskScope, sessionId, workflowStore: deps.workflowRunStore, agentStore: deps.backgroundAgentStore })));
|
|
2215
|
+
}
|
|
2044
2216
|
if (runnerSelf && !(spec.tools ?? []).some((t) => t.name === SEND_MESSAGE_TOOL_NAME)) {
|
|
2045
2217
|
toolEffects.set(SEND_MESSAGE_TOOL_NAME, "write");
|
|
2046
2218
|
axisExplicitNegatives.set(SEND_MESSAGE_TOOL_NAME, { ...axisExplicitNegatives.get(SEND_MESSAGE_TOOL_NAME), egress: false });
|
|
@@ -2070,7 +2242,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2070
2242
|
owner: hostTaskId,
|
|
2071
2243
|
scope: taskScope,
|
|
2072
2244
|
...(sessionId !== undefined ? { sessionId } : {}),
|
|
2073
|
-
...(internals?.onTaskNotification !== undefined ? { notify: internals.onTaskNotification } : {}), ...(spec.oneShot !== undefined ? { oneShot: spec.oneShot } : {}),
|
|
2245
|
+
...(internals?.onTaskNotification !== undefined ? { notify: internals.onTaskNotification } : {}), ...(spec.oneShot !== undefined ? { oneShot: spec.oneShot } : {}), retrievalToolMounted: delegationSurfaceActive,
|
|
2074
2246
|
...(internals?.onSubagentSpawn !== undefined ? { sink: internals.onSubagentSpawn } : {}),
|
|
2075
2247
|
...(internals?.parentNotify !== undefined
|
|
2076
2248
|
? { uplink: internals.parentNotify, ...(internals.parentPeerRef !== undefined ? { uplinkRecipient: internals.parentPeerRef } : {}) }
|
|
@@ -2095,7 +2267,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2095
2267
|
detail: { handle, ...(scope !== undefined ? { scope } : {}) },
|
|
2096
2268
|
}),
|
|
2097
2269
|
})));
|
|
2098
|
-
if (!(spec.tools ?? []).some((t) => t.name === AGENT_TRANSCRIPT_TOOL_NAME)) {
|
|
2270
|
+
if (delegationSurfaceActive && !(spec.tools ?? []).some((t) => t.name === AGENT_TRANSCRIPT_TOOL_NAME)) {
|
|
2099
2271
|
tools.push(firstPartyOffload(createAgentTranscriptTool({
|
|
2100
2272
|
runner: runnerSelf,
|
|
2101
2273
|
registry: defaultTaskRegistry,
|
|
@@ -3659,10 +3831,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3659
3831
|
const anonymous = spec.principal === undefined || spec.principal === "";
|
|
3660
3832
|
if (anonymous && !localOwnerDeclared)
|
|
3661
3833
|
return undefined;
|
|
3662
|
-
if (req.toolName !== PERSISTED_RULE_TOOL)
|
|
3663
|
-
return undefined;
|
|
3664
|
-
const command = req.args?.command;
|
|
3665
|
-
if (typeof command !== "string")
|
|
3834
|
+
if (req.toolName !== PERSISTED_RULE_TOOL && req.toolName !== DIRECTORY_RULE_TOOL)
|
|
3666
3835
|
return undefined;
|
|
3667
3836
|
let listed;
|
|
3668
3837
|
try {
|
|
@@ -3682,76 +3851,32 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3682
3851
|
return { unreadable: true };
|
|
3683
3852
|
}
|
|
3684
3853
|
const table = await spliceSessionOverlayRows(deps.sessionPermissionRules, sessionId, listed.rules, deps.tracer, hostTaskId);
|
|
3685
|
-
|
|
3854
|
+
if (req.toolName === DIRECTORY_RULE_TOOL) {
|
|
3855
|
+
return directoryRuleLaneAnswer(table, req.args, { root, sessionId, liveCwd: handsCwdRef?.current });
|
|
3856
|
+
}
|
|
3857
|
+
const command = req.args?.command;
|
|
3858
|
+
if (typeof command !== "string")
|
|
3859
|
+
return undefined;
|
|
3860
|
+
const admitting = findAdmittingRule(table, { tool: req.toolName, command, cwd: root, sessionId, ...(handsCwdRef?.current !== undefined ? { execCwd: handsCwdRef.current } : {}) });
|
|
3686
3861
|
if (admitting !== undefined)
|
|
3687
3862
|
return persistedRuleHitOf(admitting);
|
|
3688
|
-
const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: root, sessionId });
|
|
3863
|
+
const coverage = segmentCoverageOf(command, { persisted: table }, { tool: req.toolName, cwd: root, sessionId, ...(handsCwdRef?.current !== undefined ? { execCwd: handsCwdRef.current } : {}) });
|
|
3689
3864
|
return coverage !== undefined ? { segmentCoverage: coverage } : undefined;
|
|
3690
3865
|
},
|
|
3691
3866
|
};
|
|
3692
3867
|
})();
|
|
3693
|
-
const permissionRuleOrgLane = (() => {
|
|
3694
|
-
|
|
3695
|
-
|
|
3696
|
-
|
|
3697
|
-
|
|
3698
|
-
|
|
3699
|
-
|
|
3700
|
-
|
|
3701
|
-
|
|
3702
|
-
|
|
3703
|
-
|
|
3704
|
-
|
|
3705
|
-
catch (err) {
|
|
3706
|
-
return { status: "unavailable", disclosures: [`the org rule overlay threw: ${err instanceof Error ? err.message : String(err)}`] };
|
|
3707
|
-
}
|
|
3708
|
-
if (resolution.status === "unavailable")
|
|
3709
|
-
return { status: "unavailable", disclosures: resolution.disclosures };
|
|
3710
|
-
const revisionCell = orgRevisionEvidenceOf(resolution, (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }));
|
|
3711
|
-
const command = req.args?.command;
|
|
3712
|
-
if (req.toolName !== PERSISTED_RULE_TOOL || typeof command !== "string")
|
|
3713
|
-
return { status: "available", ...revisionCell };
|
|
3714
|
-
const verdict = orgRuleVerdictFor(resolution.rules, { tool: req.toolName, command });
|
|
3715
|
-
return verdict === undefined ? { status: "available", ...revisionCell } : { status: "available", verdict, ...revisionCell };
|
|
3716
|
-
},
|
|
3717
|
-
};
|
|
3718
|
-
})();
|
|
3719
|
-
const ruleOffersOf = (toolName, args, ask) => {
|
|
3720
|
-
if (permissionRuleLane === undefined || toolName !== PERSISTED_RULE_TOOL)
|
|
3721
|
-
return {};
|
|
3722
|
-
if ((spec.principal === undefined || spec.principal === "") && deps.localOwnerRules !== true)
|
|
3723
|
-
return {};
|
|
3724
|
-
const closedDoor = (() => {
|
|
3725
|
-
if (ask?.requiresRealApproval === true)
|
|
3726
|
-
return "mandated";
|
|
3727
|
-
if (ask?.persistedRuleShadowed !== undefined)
|
|
3728
|
-
return "shadowed";
|
|
3729
|
-
if (ask?.decisionReason === "hook")
|
|
3730
|
-
return "mandated";
|
|
3731
|
-
if (ask?.matchedAskRule !== undefined)
|
|
3732
|
-
return "shadowed";
|
|
3733
|
-
if (ask?.inheritedUnresolved === true)
|
|
3734
|
-
return "mandated";
|
|
3735
|
-
if (ask?.ancestorResolved === true)
|
|
3736
|
-
return "mandated";
|
|
3737
|
-
return undefined;
|
|
3738
|
-
})();
|
|
3739
|
-
if (closedDoor !== undefined)
|
|
3740
|
-
return { ruleOffersAbsence: closedDoor };
|
|
3741
|
-
if (persistedRuleMandateOf({
|
|
3742
|
-
egress: egressTools.has(toolName),
|
|
3743
|
-
irreversibility: irreversibilityTier.get(toolName),
|
|
3744
|
-
shellGated: shellGatedBash,
|
|
3745
|
-
probeMandated: ask?.probeMandated === true,
|
|
3746
|
-
}) !== undefined) {
|
|
3747
|
-
return { ruleOffersAbsence: "mandated" };
|
|
3748
|
-
}
|
|
3749
|
-
const command = args?.command;
|
|
3750
|
-
if (typeof command !== "string")
|
|
3751
|
-
return { ruleOffersAbsence: "lane_cannot_speak" };
|
|
3752
|
-
const offers = suggestRulesForCommand(command, ask?.segmentCoverage !== undefined ? { coverage: ask.segmentCoverage } : undefined);
|
|
3753
|
-
return offers.length > 0 ? { ruleOffers: offers } : { ruleOffersAbsence: "lane_cannot_speak" };
|
|
3754
|
-
};
|
|
3868
|
+
const permissionRuleOrgLane = makeOrgAdjudicationLane(deps.permissionRuleOrg, questionToolMounted, (message) => deps.onError?.(new Error(message), { phase: "config", sessionId }));
|
|
3869
|
+
const ruleOffersOf = makeRuleOffersOf({
|
|
3870
|
+
laneArmed: permissionRuleLane !== undefined,
|
|
3871
|
+
principal: spec.principal,
|
|
3872
|
+
localOwnerDeclared: deps.localOwnerRules === true,
|
|
3873
|
+
egressTools,
|
|
3874
|
+
irreversibilityTier,
|
|
3875
|
+
shellGatedBash,
|
|
3876
|
+
taskRoot: taskRootFinal,
|
|
3877
|
+
cwdRef: handsCwdRef,
|
|
3878
|
+
deniesDirectoryRead: readDenyMatcher !== undefined ? (d) => readDenyMatcher.matchPath(d) !== null : undefined,
|
|
3879
|
+
});
|
|
3755
3880
|
const frozenClassifierExcluded = (d) => d.decisionReason === "hook" || d.matchedAskRule !== undefined;
|
|
3756
3881
|
const recheckApprovedEdit = async (pol, onAskOf, creq, edit, csignal, ancestorDecider) => {
|
|
3757
3882
|
let editArgs = edit;
|
|
@@ -4359,11 +4484,33 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4359
4484
|
placementRootSessionId: stampPlacementRootSessionId(placementRootResolved),
|
|
4360
4485
|
externalContentTarget: externalContentTargetActive ? true : undefined,
|
|
4361
4486
|
});
|
|
4362
|
-
const
|
|
4487
|
+
const compensatePausedVM = (remoteEnv, snapshotId) => compensateUnparkedPause(remoteEnv, snapshotId, {
|
|
4488
|
+
boundMs: PARK_COMPENSATION_TIMEOUT_MS,
|
|
4489
|
+
noteFailure: (note) => remoteEnvFailures.push(note),
|
|
4490
|
+
disclose: (err) => {
|
|
4491
|
+
try {
|
|
4492
|
+
deps.onError?.(err, { phase: "config", sessionId });
|
|
4493
|
+
}
|
|
4494
|
+
catch {
|
|
4495
|
+
}
|
|
4496
|
+
},
|
|
4497
|
+
});
|
|
4498
|
+
const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle, cutSignal) => {
|
|
4363
4499
|
if (!checkpointStore)
|
|
4364
|
-
return {
|
|
4500
|
+
return { tag: "absent" };
|
|
4365
4501
|
if (memoryEngineSession)
|
|
4366
4502
|
await memoryEngineSession.harvest("checkpoint");
|
|
4503
|
+
if (cutSignal?.aborted) {
|
|
4504
|
+
if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
|
|
4505
|
+
const back = await compensatePausedVM(remoteEnv, remoteHandle.snapshotId);
|
|
4506
|
+
if (!back.ok) {
|
|
4507
|
+
abortController.abort();
|
|
4508
|
+
void harness.abort();
|
|
4509
|
+
return { tag: "compensation_failed", reason: back.reason };
|
|
4510
|
+
}
|
|
4511
|
+
}
|
|
4512
|
+
return { tag: "cut" };
|
|
4513
|
+
}
|
|
4367
4514
|
const announceCommittedScreeningPark = () => {
|
|
4368
4515
|
const committedCount = cp.state.inheritedGate?.parentConstraintCount;
|
|
4369
4516
|
if (cp.state.inheritedGate?.requiresParentConstraint === true &&
|
|
@@ -4401,7 +4548,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4401
4548
|
try {
|
|
4402
4549
|
await checkpointStore.put(token, cp);
|
|
4403
4550
|
announceCommittedScreeningPark();
|
|
4404
|
-
return {
|
|
4551
|
+
return { tag: "committed" };
|
|
4405
4552
|
}
|
|
4406
4553
|
catch (putErr) {
|
|
4407
4554
|
deps.onError?.(putErr, { phase: "config", sessionId });
|
|
@@ -4417,7 +4564,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4417
4564
|
catch {
|
|
4418
4565
|
}
|
|
4419
4566
|
announceCommittedScreeningPark();
|
|
4420
|
-
return {
|
|
4567
|
+
return { tag: "committed" };
|
|
4421
4568
|
}
|
|
4422
4569
|
if (confirmed === "unknown") {
|
|
4423
4570
|
const unknownReason = "the approval checkpoint's state cannot be established (the store rejected the write and then could not be read back; a committed-but-unacknowledged row may exist) — the run is stopped rather than continued past an approval whose durable record is unknown";
|
|
@@ -4432,29 +4579,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4432
4579
|
}
|
|
4433
4580
|
abortController.abort();
|
|
4434
4581
|
void harness.abort();
|
|
4435
|
-
return {
|
|
4582
|
+
return { tag: "unknown", reason: unknownReason };
|
|
4436
4583
|
}
|
|
4437
4584
|
const reason = "the approval checkpoint could not be persisted (the store rejected the write; the deployment's error face carries the store's own message)";
|
|
4438
4585
|
if (remoteEnv !== undefined && remoteHandle?.snapshotId !== undefined) {
|
|
4439
|
-
const
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
if (back.ok) {
|
|
4443
|
-
const init = await remoteEnv.postResumeInit();
|
|
4444
|
-
if (init.ok)
|
|
4445
|
-
return { ok: false, reason };
|
|
4446
|
-
remoteEnvFailures.push(remoteEnvFailureNote("postResumeInit", init.error, 1));
|
|
4447
|
-
deps.onError?.(init.error, { phase: "config", sessionId });
|
|
4448
|
-
}
|
|
4449
|
-
else {
|
|
4450
|
-
remoteEnvFailures.push(remoteEnvFailureNote("resumeVM", back.error, backAttempts));
|
|
4451
|
-
deps.onError?.(back.error, { phase: "config", sessionId });
|
|
4452
|
-
}
|
|
4586
|
+
const back = await compensatePausedVM(remoteEnv, remoteHandle.snapshotId);
|
|
4587
|
+
if (back.ok)
|
|
4588
|
+
return { tag: "absent", reason };
|
|
4453
4589
|
abortController.abort();
|
|
4454
4590
|
void harness.abort();
|
|
4455
|
-
return {
|
|
4591
|
+
return { tag: "compensation_failed", reason };
|
|
4456
4592
|
}
|
|
4457
|
-
return {
|
|
4593
|
+
return { tag: "absent", reason };
|
|
4458
4594
|
}
|
|
4459
4595
|
};
|
|
4460
4596
|
const suspendLoopCapHit = (count, cap, detail) => {
|
|
@@ -4536,7 +4672,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4536
4672
|
...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
|
|
4537
4673
|
...(spec.principal ? { principal: spec.principal } : {}),
|
|
4538
4674
|
};
|
|
4539
|
-
if (
|
|
4675
|
+
if ((await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).tag !== "committed")
|
|
4540
4676
|
return false;
|
|
4541
4677
|
publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
|
|
4542
4678
|
try {
|
|
@@ -4622,7 +4758,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4622
4758
|
...(spec.principal ? { principal: spec.principal } : {}),
|
|
4623
4759
|
...(spec.durableApproval !== undefined ? { durableApproval: { ...spec.durableApproval } } : {}),
|
|
4624
4760
|
};
|
|
4625
|
-
if (
|
|
4761
|
+
if ((await commitSuspendSaga(token, cp, suspendableEnv, remoteHandle)).tag !== "committed")
|
|
4626
4762
|
return false;
|
|
4627
4763
|
publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, scope, remoteHandle, cp.checkpointId, cp.pendingAction);
|
|
4628
4764
|
try {
|
|
@@ -4718,7 +4854,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4718
4854
|
}
|
|
4719
4855
|
};
|
|
4720
4856
|
const suspendAsk = parkLaneArmed && checkpointStore !== undefined
|
|
4721
|
-
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause, segmentCoverage, matchedAskRule, probeMandated) => {
|
|
4857
|
+
? async (req, postHookArgs, safety, liveFaceUnavailable, realApproval, shadowedRule, askDecisionReason, probeReason, probeCause, segmentCoverage, matchedAskRule, probeMandated, callSignal) => {
|
|
4722
4858
|
const syncFirstEligible = req.toolName === ASK_USER_QUESTION_TOOL_NAME ? contentAskRoutable(req.toolCallId) : isLiveApproverSeat(onAsk);
|
|
4723
4859
|
if (syncFirstEligible &&
|
|
4724
4860
|
runtimeCaps?.forceDurableGate !== true &&
|
|
@@ -4726,6 +4862,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4726
4862
|
!inheritedUnavailableAsks.has(req.toolCallId)) {
|
|
4727
4863
|
return undefined;
|
|
4728
4864
|
}
|
|
4865
|
+
const cutSignal = composedCallSignal(callSignal);
|
|
4729
4866
|
let token;
|
|
4730
4867
|
let gate;
|
|
4731
4868
|
let cp;
|
|
@@ -4761,13 +4898,18 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4761
4898
|
if (!presented.ok) {
|
|
4762
4899
|
throw new ParkRefusal(`the stored form of "${req.toolName}"'s arguments could not be presented for re-adjudication (${describeThrown(presented.cause)})`, { cause: presented.cause });
|
|
4763
4900
|
}
|
|
4764
|
-
const
|
|
4901
|
+
const reprojectedRaced = await raceSettlementAgainstSignal(Promise.resolve(basePolicyForResumeEdit.check({
|
|
4765
4902
|
toolName: req.toolName,
|
|
4766
4903
|
args: presented.value,
|
|
4767
4904
|
toolCallId: req.toolCallId,
|
|
4768
4905
|
budget: budgetSnapshot,
|
|
4769
4906
|
...(handsCwdRef !== undefined ? { cwd: handsCwdRef.current } : {}),
|
|
4770
|
-
},
|
|
4907
|
+
}, cutSignal)), cutSignal);
|
|
4908
|
+
if (reprojectedRaced.tag === "aborted")
|
|
4909
|
+
return undefined;
|
|
4910
|
+
if (reprojectedRaced.tag === "threw")
|
|
4911
|
+
throw reprojectedRaced.error;
|
|
4912
|
+
const reprojected = refuseOutOfContractDecision(reprojectedRaced.value);
|
|
4771
4913
|
const demanded = reprojected.updatedInput !== undefined ? tryCloneArgs(reprojected.updatedInput) : undefined;
|
|
4772
4914
|
const rewroteTheFiledValue = reprojected.updatedInput !== undefined &&
|
|
4773
4915
|
!(demanded?.ok === true &&
|
|
@@ -4798,7 +4940,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4798
4940
|
}
|
|
4799
4941
|
const { messages } = await session.buildContext();
|
|
4800
4942
|
const { batchToolCallIds, completedCallIds } = batchContextAt(messages, req.toolCallId);
|
|
4801
|
-
if (
|
|
4943
|
+
if (cutSignal.aborted) {
|
|
4802
4944
|
return undefined;
|
|
4803
4945
|
}
|
|
4804
4946
|
if (suspendLoopCapHit(suspendChainBase(), maxSuspends, ` for tool "${req.toolName}" — likely a resume/restart loop.`))
|
|
@@ -4806,12 +4948,20 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4806
4948
|
if (remoteEnv !== undefined) {
|
|
4807
4949
|
if (hasBackgroundShell(remoteEnv))
|
|
4808
4950
|
await sweepBackgroundShells(remoteEnv, defaultTaskRegistry);
|
|
4809
|
-
const snap = await remoteEnv.suspendVM({ abortSignal:
|
|
4951
|
+
const snap = await remoteEnv.suspendVM({ abortSignal: cutSignal });
|
|
4810
4952
|
if (!snap.ok) {
|
|
4811
4953
|
remoteEnvFailures.push(remoteEnvFailureNote("suspendVM", snap.error, 1));
|
|
4812
4954
|
throw new Error(`suspendVM failed (${snap.error.code}): ${snap.error.message}`, { cause: snap.error });
|
|
4813
4955
|
}
|
|
4814
4956
|
remoteHandle = { ...remoteEnv.workspaceHandle(), snapshotId: snap.value };
|
|
4957
|
+
if (cutSignal.aborted) {
|
|
4958
|
+
const back = await compensatePausedVM(remoteEnv, snap.value);
|
|
4959
|
+
if (!back.ok) {
|
|
4960
|
+
abortController.abort();
|
|
4961
|
+
void harness.abort();
|
|
4962
|
+
}
|
|
4963
|
+
return undefined;
|
|
4964
|
+
}
|
|
4815
4965
|
}
|
|
4816
4966
|
else if (parkOnlyRemoteEnv !== undefined) {
|
|
4817
4967
|
if (hasBackgroundShell(parkOnlyRemoteEnv) && spec.retainBackgroundProcesses !== true)
|
|
@@ -4917,16 +5067,31 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4917
5067
|
};
|
|
4918
5068
|
}
|
|
4919
5069
|
catch (err) {
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
4924
|
-
:
|
|
4925
|
-
|
|
5070
|
+
const cutWon = cutSignal.aborted;
|
|
5071
|
+
const preselected = cutWon
|
|
5072
|
+
? undefined
|
|
5073
|
+
: {
|
|
5074
|
+
parkFailed: err instanceof ParkRefusal
|
|
5075
|
+
? err.message
|
|
5076
|
+
: "the durable approval park could not be prepared (the deployment's error face carries the exception)",
|
|
5077
|
+
};
|
|
5078
|
+
try {
|
|
5079
|
+
deps.onError?.(err, { phase: "config", sessionId });
|
|
5080
|
+
}
|
|
5081
|
+
catch {
|
|
5082
|
+
}
|
|
5083
|
+
return preselected;
|
|
4926
5084
|
}
|
|
4927
|
-
const committed = await commitSuspendSaga(token, cp, remoteEnv, remoteHandle);
|
|
4928
|
-
if (
|
|
4929
|
-
|
|
5085
|
+
const committed = await commitSuspendSaga(token, cp, remoteEnv, remoteHandle, cutSignal);
|
|
5086
|
+
if (committed.tag !== "committed") {
|
|
5087
|
+
if (committed.tag === "cut")
|
|
5088
|
+
return undefined;
|
|
5089
|
+
if (committed.tag === "absent") {
|
|
5090
|
+
if (cutSignal.aborted)
|
|
5091
|
+
return undefined;
|
|
5092
|
+
return committed.reason !== undefined ? { parkFailed: committed.reason } : undefined;
|
|
5093
|
+
}
|
|
5094
|
+
return { parkFailed: committed.reason };
|
|
4930
5095
|
}
|
|
4931
5096
|
publishCommittedSuspend({ suspendRef, reviewRef }, token, gate, cp.scope, remoteHandle, cp.checkpointId, cp.pendingAction);
|
|
4932
5097
|
try {
|
|
@@ -32,7 +32,12 @@ export declare function remoteEnvFailureNote(op: RemoteEnvFailureNote["op"], err
|
|
|
32
32
|
* decides. A code outside the retryable family (`unsupported`, `auth_failed`) returns on the first
|
|
33
33
|
* attempt — `withRetry` will not spend a second call on a permanent refusal.
|
|
34
34
|
*/
|
|
35
|
-
export declare function restoreWorkspaceWithRetry(env: RemoteExecutionEnv, snapshotId: SnapshotId, options: VmLifecycleOptions
|
|
35
|
+
export declare function restoreWorkspaceWithRetry(env: RemoteExecutionEnv, snapshotId: SnapshotId, options: VmLifecycleOptions,
|
|
36
|
+
/** design/384 slice 2 (additive): observe each attempt AS IT STARTS. The returned `attempts` is
|
|
37
|
+
* only readable after settlement, and a caller whose wait on this promise is BOUNDED (the park
|
|
38
|
+
* compensation) must report the live attempt count when its bound fires mid-call — without this
|
|
39
|
+
* seat a deaf second attempt was reported as `attempts: 1`. */
|
|
40
|
+
onAttempt?: (attempt: number) => void): Promise<{
|
|
36
41
|
outcome: Awaited<ReturnType<RemoteExecutionEnv["resumeVM"]>>;
|
|
37
42
|
attempts: number;
|
|
38
43
|
}>;
|
|
@@ -19,10 +19,11 @@ export function remoteEnvFailureNote(op, error, attempts) {
|
|
|
19
19
|
}
|
|
20
20
|
const REMOTE_RESTORE_MAX_ATTEMPTS = 2;
|
|
21
21
|
const REMOTE_RESTORE_BACKOFF_MS = 200;
|
|
22
|
-
export async function restoreWorkspaceWithRetry(env, snapshotId, options) {
|
|
22
|
+
export async function restoreWorkspaceWithRetry(env, snapshotId, options, onAttempt) {
|
|
23
23
|
let attempts = 0;
|
|
24
24
|
const outcome = await withRetry(async (attempt) => {
|
|
25
25
|
attempts = attempt;
|
|
26
|
+
onAttempt?.(attempt);
|
|
26
27
|
return env.resumeVM(snapshotId, options);
|
|
27
28
|
}, { retryableCodes: RETRYABLE_REMOTE_ERROR_CODES, maxAttempts: REMOTE_RESTORE_MAX_ATTEMPTS, backoffMs: () => REMOTE_RESTORE_BACKOFF_MS }, { ...(options.abortSignal !== undefined ? { signal: options.abortSignal } : {}) });
|
|
28
29
|
return { outcome, attempts };
|
|
@@ -5080,12 +5080,22 @@ export class Runner {
|
|
|
5080
5080
|
await recheckGovernanceWindow();
|
|
5081
5081
|
}
|
|
5082
5082
|
this.locallyClaimedTokens.add(token);
|
|
5083
|
-
|
|
5083
|
+
let won;
|
|
5084
|
+
let claimLossStatus;
|
|
5085
|
+
if (store.claimTerminal !== undefined) {
|
|
5086
|
+
const claim = await store.claimTerminal(token, cp.scope, { kind: "resolve", outcome: outcomeForStore, expect: { rev: cp.rev ?? 0 } });
|
|
5087
|
+
won = claim.claimed;
|
|
5088
|
+
if (!claim.claimed)
|
|
5089
|
+
claimLossStatus = claim.current.status;
|
|
5090
|
+
}
|
|
5091
|
+
else {
|
|
5092
|
+
won = await store.resolve(token, cp.scope, outcomeForStore, { rev: cp.rev ?? 0 });
|
|
5093
|
+
}
|
|
5084
5094
|
if (!won)
|
|
5085
5095
|
this.locallyClaimedTokens.delete(token);
|
|
5086
5096
|
if (!won) {
|
|
5087
|
-
const
|
|
5088
|
-
if (
|
|
5097
|
+
const stillPending = claimLossStatus !== undefined ? claimLossStatus === "pending" : (await store.get(token))?.status === "pending";
|
|
5098
|
+
if (stillPending) {
|
|
5089
5099
|
throw new CheckpointError("checkpoint.reopened_concurrently", "checkpoint changed concurrently (its revision advanced via a resolve/reopen cycle since this resume validated) — not executed; re-resume against the current state");
|
|
5090
5100
|
}
|
|
5091
5101
|
this.parentConstraintRegistry.delete(token);
|