@sema-agent/core 5.29.0 → 5.30.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 +63 -0
- package/dist/agents/send-message-tool.js +2 -0
- package/dist/agents/subagent.d.ts +2 -0
- package/dist/agents/subagent.js +6 -0
- package/dist/agents/teacher.js +2 -0
- package/dist/agents/verify.js +2 -0
- package/dist/core/auto-compaction.d.ts +5 -1
- package/dist/core/auto-compaction.js +10 -1
- package/dist/core/checkpoint-store.d.ts +51 -5
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/hooks.d.ts +12 -1
- package/dist/core/hooks.js +8 -2
- package/dist/core/permission-rules.js +2 -2
- package/dist/core/runner/prepare-task.d.ts +21 -5
- package/dist/core/runner/prepare-task.js +105 -19
- package/dist/core/runner/runtask.js +29 -3
- package/dist/core/runner/session-rule-policy.d.ts +3 -2
- package/dist/core/runner/tool-output-projection.js +1 -1
- package/dist/core/sensitive-path-policy.js +5 -16
- package/dist/core/store-contracts/tool-result-store-contract.js +23 -0
- package/dist/core/tighten-task-spec.js +18 -0
- package/dist/core/tool-policy.d.ts +20 -1
- package/dist/core/tool-policy.js +31 -4
- package/dist/core/tool-result-store.js +3 -1
- package/dist/core/types.d.ts +55 -0
- package/dist/engine/harness/types.d.ts +10 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -1
- package/dist/orchestration/run-workflow-tool.d.ts +21 -0
- package/dist/orchestration/run-workflow-tool.js +6 -3
- package/dist/orchestration/workflow-primitives.d.ts +10 -1
- package/dist/orchestration/workflow-primitives.js +12 -1
- package/dist/prompt-assembly/epoch.js +2 -0
- package/dist/prompt-assembly/packs/sema-default.js +2 -2
- package/dist/prompt-assembly/types.d.ts +4 -0
- package/dist/prompts/default.d.ts +14 -9
- package/dist/prompts/default.js +13 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +21 -0
- package/dist/tools/fs/bash-readonly-classifier.js +11 -0
- package/dist/tools/fs/fs-bash.d.ts +7 -0
- package/dist/tools/fs/fs-bash.js +8 -3
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-pdf.js +2 -2
- package/dist/tools/fs/fs-read.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +11 -7
- package/dist/tools/fs/fs-search-tools.d.ts +4 -2
- package/dist/tools/fs/fs-search-tools.js +15 -8
- package/dist/tools/fs/fs-shared.d.ts +5 -1
- package/dist/tools/fs/fs-shared.js +8 -3
- package/dist/tools/fs/index.d.ts +18 -0
- package/dist/tools/fs/index.js +13 -2
- package/dist/tools/fs/read-deny.d.ts +105 -0
- package/dist/tools/fs/read-deny.js +151 -0
- package/dist/tools/fs/read-face.d.ts +43 -0
- package/dist/tools/fs/read-face.js +38 -0
- package/dist/tools/fs/repo-map.d.ts +3 -1
- package/dist/tools/fs/repo-map.js +11 -5
- package/dist/tools/fs/safety.d.ts +33 -11
- package/dist/tools/fs/safety.js +88 -7
- package/dist/tools/fs/search.d.ts +54 -5
- package/dist/tools/fs/search.js +103 -21
- package/package.json +1 -1
|
@@ -57,7 +57,7 @@ import { foldAdmissionFreeze } from "../memory-admission.js";
|
|
|
57
57
|
import { prepareMemory } from "./prepare-memory.js";
|
|
58
58
|
import { defaultPromptProvider, buildEnvironmentContext, buildGitSnapshot, formatLocalDate, isValidTimeZone, PROJECT_CONTEXT_FRAMING } from "../../prompts/default.js";
|
|
59
59
|
import { assemblePrompt } from "../../prompt-assembly/assemble.js";
|
|
60
|
-
import { auditToolCollisions, projectToolManifest } from "../../prompt-assembly/tool-catalog.js";
|
|
60
|
+
import { auditToolCollisions, getToolContract, projectToolManifest } from "../../prompt-assembly/tool-catalog.js";
|
|
61
61
|
import { resolveEpochAgainstBundled } from "../../prompt-assembly/epoch.js";
|
|
62
62
|
import { artifactDeclarations } from "../../prompt-assembly/artifact.js";
|
|
63
63
|
import { buildTurnPromptSnapshot } from "../../prompt-assembly/turn-snapshot.js";
|
|
@@ -74,7 +74,7 @@ import { hasBackgroundShell, sweepBackgroundShells } from "../background-shell.j
|
|
|
74
74
|
import { createTaskOutputTool, createTaskStopTool, defaultTaskRegistry } from "../task-registry.js";
|
|
75
75
|
import { createMonitorTool } from "../../tools/monitor.js";
|
|
76
76
|
import { createWorktreeTools } from "../../tools/worktree.js";
|
|
77
|
-
import { applyCompactionToReadFileState, bashReversibilityProbe, createHandsToolkit, isReadDedupStubResult, seedReadFileStateFromContext, seedReadFileStateFromTranscript, HAND_TOOL_EFFECTS, pdfModelCapabilitiesOf } from "../../tools/fs/index.js";
|
|
77
|
+
import { applyCompactionToReadFileState, bashReversibilityProbe, compileReadDeny, createHandsToolkit, isReadDedupStubResult, resolveReadFace, seedReadFileStateFromContext, seedReadFileStateFromTranscript, FULL_SHELL_CONTRACT_ID, HAND_TOOL_EFFECTS, pdfModelCapabilitiesOf } from "../../tools/fs/index.js";
|
|
78
78
|
import { decodeTextBytes } from "../../tools/fs/encoding.js";
|
|
79
79
|
import { ASK_USER_QUESTION_TOOL_NAME, createAskUserQuestionTool, classifyQuestionOutcome, isLiveQuestionFace, validateAskQuestions, } from "../ask-question.js";
|
|
80
80
|
import { createSchedulerTools } from "../../tools/scheduler-tools.js";
|
|
@@ -85,7 +85,7 @@ import { resolveWorkflowSizeGuideline } from "../../orchestration/workflow-size-
|
|
|
85
85
|
import { createLspTool, gitCheckIgnoreFilter, resolveLspPath } from "../lsp.js";
|
|
86
86
|
import { resolveKey } from "../../tools/fs/safety.js";
|
|
87
87
|
import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
|
|
88
|
-
import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
|
|
88
|
+
import { BINDING_CHECKPOINT_VERSION, mintCheckpointToken, ORG_ADMISSION_CHECKPOINT_VERSION, F012_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, RESOURCE_CHECKPOINT_VERSION, TOKEN_CHECKPOINT_VERSION, buildRiskDescriptor, debitLedger, encodeAtFidelity, remainingBudgetMicroUsd, resolveCheckpointStore, resolveDeclaredFidelity, samePlainValue, } from "../checkpoint-store.js";
|
|
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";
|
|
@@ -917,7 +917,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
917
917
|
internals.onWorkspaceResolved({
|
|
918
918
|
cwd: taskRootPath,
|
|
919
919
|
isolated: internals.isolation === "worktree",
|
|
920
|
-
remote: workspaceEnv !== undefined &&
|
|
920
|
+
remote: workspaceEnv !== undefined &&
|
|
921
|
+
(isRemoteExecutionEnv(workspaceEnv) || workspaceEnv.hostLocalPaths === false),
|
|
921
922
|
});
|
|
922
923
|
}
|
|
923
924
|
catch {
|
|
@@ -1081,6 +1082,17 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1081
1082
|
const memoryAdmittedOrgScopesRef = { current: [] };
|
|
1082
1083
|
const ownOrgVerdictRef = { current: undefined };
|
|
1083
1084
|
const orgAdmissionCheckpointState = () => inheritedAdmittedOrgScopes !== undefined || ownOrgVerdictRef.current !== undefined || orgGovernedProvenance;
|
|
1085
|
+
const faceCheckpointSection = () => {
|
|
1086
|
+
if (resolvedReadFace === undefined) {
|
|
1087
|
+
const seed = resume?.seed.readFace;
|
|
1088
|
+
return seed !== undefined ? { face: seed.face, ...(seed.denyEntries !== undefined ? { denyEntries: seed.denyEntries.map((e) => ({ ...e })) } : {}) } : undefined;
|
|
1089
|
+
}
|
|
1090
|
+
if (resolvedReadFace === "open" || readDenyAdditionsNormalized.length > 0) {
|
|
1091
|
+
return { face: resolvedReadFace, ...(readDenyAdditionsNormalized.length > 0 ? { denyEntries: readDenyAdditionsNormalized.map((e) => ({ ...e })) } : {}) };
|
|
1092
|
+
}
|
|
1093
|
+
return undefined;
|
|
1094
|
+
};
|
|
1095
|
+
const faceCheckpointState = () => faceCheckpointSection() !== undefined;
|
|
1084
1096
|
const f012CheckpointState = () => (inheritedParentConstraints?.length ?? 0) > 0 ||
|
|
1085
1097
|
seedInheritedGate?.constraintChain !== undefined ||
|
|
1086
1098
|
internals?.delegationProvenance !== undefined;
|
|
@@ -1151,6 +1163,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1151
1163
|
};
|
|
1152
1164
|
let agentForkDenial;
|
|
1153
1165
|
let observersActive = false;
|
|
1166
|
+
let resolvedReadFace;
|
|
1167
|
+
let readDenyAdditionsNormalized = [];
|
|
1168
|
+
const fullShellReachable = handsEnabled &&
|
|
1169
|
+
!(executionEnv instanceof StubExecutionEnv) &&
|
|
1170
|
+
spec.handsReadOnly !== true &&
|
|
1171
|
+
!(toolFaceSnapshot.exclude?.includes("Bash") ?? false);
|
|
1154
1172
|
let autoModeDecider;
|
|
1155
1173
|
const enrichSpecToolCtx = (ctx) => ({
|
|
1156
1174
|
...ctx,
|
|
@@ -1161,6 +1179,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1161
1179
|
...(frozenOnAsk !== undefined ? { onAsk: frozenOnAsk } : {}),
|
|
1162
1180
|
...(frozenOnQuestion !== undefined ? { onQuestion: frozenOnQuestion } : {}),
|
|
1163
1181
|
...(spec.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
1182
|
+
...(resolvedReadFace === "roots" ? { readFace: "roots" } : {}),
|
|
1183
|
+
...(readDenyAdditionsNormalized.length > 0 ? { readDenyPatterns: Object.freeze(readDenyAdditionsNormalized.map((e) => ({ ...e }))) } : {}),
|
|
1164
1184
|
...(spec.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
1165
1185
|
oneShot: spec.oneShot,
|
|
1166
1186
|
clientContext: spec.clientContext,
|
|
@@ -1366,6 +1386,11 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1366
1386
|
workflowDepth: internals?.workflowDepth,
|
|
1367
1387
|
parentCwd: taskRootPath,
|
|
1368
1388
|
parentThinking: () => harnessRef.current?.getThinkingLevel() ?? thinking,
|
|
1389
|
+
parentReadFace: () => resolvedReadFace,
|
|
1390
|
+
parentReadDenyPatterns: () => (readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized : undefined),
|
|
1391
|
+
parentCheckpointStoreDisabled: spec.checkpointStore === null,
|
|
1392
|
+
parentCenterArtifactDigest: () => centerAdoption?.artifact.artifactDigest,
|
|
1393
|
+
parentCenterSourceRevision: () => centerAdoption?.sourceRevision,
|
|
1369
1394
|
...(forwardEvent ? { forwardEvent } : {}),
|
|
1370
1395
|
inheritedGateForChildren,
|
|
1371
1396
|
}));
|
|
@@ -1578,6 +1603,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1578
1603
|
const additionalRootsCanonical = [];
|
|
1579
1604
|
const additionalReadRootsCanonical = [];
|
|
1580
1605
|
let attachmentRootCanonical;
|
|
1606
|
+
let readDenyMatcher;
|
|
1581
1607
|
if (handsEnabled) {
|
|
1582
1608
|
const rootRaw = taskRootPath;
|
|
1583
1609
|
const canon = await executionEnv.canonicalPath(rootRaw);
|
|
@@ -1629,14 +1655,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1629
1655
|
if (resume === undefined && spec.sessionId !== undefined) {
|
|
1630
1656
|
const prior = await session.buildContext().catch(() => undefined);
|
|
1631
1657
|
for (const rec of wholeFileRecordsFromTranscript(prior?.messages ?? [])) {
|
|
1632
|
-
const rk = await resolveKey(executionEnv, rootCanonical, rec.path, undefined, undefined, additionalRootsCanonical);
|
|
1658
|
+
const rk = await resolveKey(executionEnv, rootCanonical, rec.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical]);
|
|
1633
1659
|
if (rk.ok)
|
|
1634
1660
|
seedReadFileStateFromTranscript(readFileState, rk.key, rec.content, rec.at);
|
|
1635
1661
|
}
|
|
1636
1662
|
}
|
|
1637
1663
|
seedContextFiles = async (files) => {
|
|
1638
1664
|
for (const f of files) {
|
|
1639
|
-
const rk = await resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, additionalRootsCanonical);
|
|
1665
|
+
const rk = await resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical]);
|
|
1640
1666
|
if (rk.ok)
|
|
1641
1667
|
seedReadFileStateFromContext(readFileState, rk.key, f.content);
|
|
1642
1668
|
}
|
|
@@ -1658,9 +1684,28 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1658
1684
|
...(resume !== undefined ? { baselineUnknown: true } : {}),
|
|
1659
1685
|
};
|
|
1660
1686
|
}
|
|
1687
|
+
const seedReadFaceSection = resume?.seed.readFace;
|
|
1688
|
+
const readDenyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seedReadFaceSection?.denyEntries ?? [])];
|
|
1689
|
+
readDenyMatcher = compileReadDeny(readDenyAdditions, "readDenyPatterns");
|
|
1690
|
+
{
|
|
1691
|
+
const builtinCount = compileReadDeny([], "built-in").entries.length;
|
|
1692
|
+
readDenyAdditionsNormalized = readDenyMatcher.entries.slice(builtinCount);
|
|
1693
|
+
}
|
|
1694
|
+
let liveReadFace = resolveReadFace({
|
|
1695
|
+
specReadFace: spec.readFace,
|
|
1696
|
+
depsReadFace: deps.readFace,
|
|
1697
|
+
readOnlyMount: handsReadOnly,
|
|
1698
|
+
orgGoverned: deps.permissionRuleOrg !== undefined,
|
|
1699
|
+
fullShellReachable,
|
|
1700
|
+
});
|
|
1701
|
+
if (resume !== undefined && (seedReadFaceSection === undefined || seedReadFaceSection.face === "roots"))
|
|
1702
|
+
liveReadFace = "roots";
|
|
1703
|
+
resolvedReadFace = liveReadFace;
|
|
1661
1704
|
const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
|
|
1662
1705
|
...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
|
|
1663
1706
|
...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
|
|
1707
|
+
...(readDenyAdditions.length > 0 ? { readDenyPatterns: readDenyAdditions } : {}),
|
|
1708
|
+
readFace: liveReadFace,
|
|
1664
1709
|
includeShell: handsIncludeShell,
|
|
1665
1710
|
readOnly: handsReadOnly,
|
|
1666
1711
|
...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
|
|
@@ -1714,6 +1759,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1714
1759
|
const shellReadBoundary = () => ({
|
|
1715
1760
|
roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
|
|
1716
1761
|
...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
|
|
1762
|
+
denyMatch: (p) => readDenyMatcher?.matchPath(p)?.pattern ?? null,
|
|
1763
|
+
...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
|
|
1717
1764
|
});
|
|
1718
1765
|
if (shellGate === "classify" && shellGatedBash)
|
|
1719
1766
|
reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
|
|
@@ -2164,6 +2211,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2164
2211
|
policyEnabled: promptPolicyEnabled,
|
|
2165
2212
|
hooksEnabled: promptHooksEnabled,
|
|
2166
2213
|
isolationEnabled: isIsolated(executionEnv),
|
|
2214
|
+
readFaceOpen: resolvedReadFace === "open",
|
|
2167
2215
|
orchestrationEnabled: selfOrchestrationActive,
|
|
2168
2216
|
orchestrationDeferred: selfOrchestrationActive && (toolFaceSnapshot.defer?.includes("Workflow") ?? false),
|
|
2169
2217
|
promptProfile,
|
|
@@ -2216,6 +2264,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2216
2264
|
if (additionalRootsCanonical.length > 0) {
|
|
2217
2265
|
envFacts.additionalDirectories = [...additionalRootsCanonical];
|
|
2218
2266
|
}
|
|
2267
|
+
if (resolvedReadFace !== undefined)
|
|
2268
|
+
envFacts.readFace = resolvedReadFace;
|
|
2219
2269
|
if (additionalReadRootsCanonical.length > 0) {
|
|
2220
2270
|
envFacts.additionalReadDirectories = [...additionalReadRootsCanonical];
|
|
2221
2271
|
}
|
|
@@ -2309,6 +2359,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2309
2359
|
if (excluded.has(tools[i].name))
|
|
2310
2360
|
tools.splice(i, 1);
|
|
2311
2361
|
}
|
|
2362
|
+
if (handsEnabled) {
|
|
2363
|
+
const fullShellOnRoster = tools.some((t) => t.name === "Bash" && getToolContract(t, "core").contractId === FULL_SHELL_CONTRACT_ID);
|
|
2364
|
+
if (fullShellOnRoster !== fullShellReachable) {
|
|
2365
|
+
const e = new Error(`internal invariant: fullShellReachable=${String(fullShellReachable)} but the post-exclusion roster ` +
|
|
2366
|
+
`${fullShellOnRoster ? "carries" : "does not carry"} the full shell (${FULL_SHELL_CONTRACT_ID}). ` +
|
|
2367
|
+
`A new roster-affecting mechanism was added without updating the predicate.`);
|
|
2368
|
+
e.code = "internal.full_shell_reachable_mismatch";
|
|
2369
|
+
throw e;
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2312
2372
|
if (promptProfile === "classic") {
|
|
2313
2373
|
for (let i = 0; i < tools.length; i++) {
|
|
2314
2374
|
const t = tools[i];
|
|
@@ -3300,6 +3360,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3300
3360
|
const suggestions = suggestRulesForCommand(command);
|
|
3301
3361
|
return suggestions.length > 0 ? { ruleSuggestions: suggestions } : {};
|
|
3302
3362
|
};
|
|
3363
|
+
const frozenClassifierExcluded = (d) => d.decisionReason === "hook" || d.matchedAskRule !== undefined;
|
|
3303
3364
|
const recheckApprovedEdit = async (pol, onAskOf, creq, edit, csignal, ancestorDecider) => {
|
|
3304
3365
|
let editArgs = edit;
|
|
3305
3366
|
for (let round = 0;; round++) {
|
|
@@ -3326,7 +3387,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3326
3387
|
}
|
|
3327
3388
|
if (re.updatedInput !== undefined)
|
|
3328
3389
|
editArgs = re.updatedInput;
|
|
3329
|
-
if (ancestorDecider !== undefined) {
|
|
3390
|
+
if (ancestorDecider !== undefined && !frozenClassifierExcluded(re)) {
|
|
3330
3391
|
const verdict = await ancestorDecider
|
|
3331
3392
|
.decide({ req: { ...creq, args: editArgs }, ...(re.message !== undefined ? { askMessage: re.message } : {}) }, csignal ?? abortController.signal)
|
|
3332
3393
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
@@ -3405,7 +3466,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3405
3466
|
`ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
|
|
3406
3467
|
};
|
|
3407
3468
|
}
|
|
3408
|
-
if (pc.autoMode !== undefined) {
|
|
3469
|
+
if (pc.autoMode !== undefined && !frozenClassifierExcluded(first)) {
|
|
3409
3470
|
const verdict = await pc.autoMode.decider
|
|
3410
3471
|
.decide({ req: creq, ...(first.message !== undefined ? { askMessage: first.message } : {}) }, csignal ?? abortController.signal)
|
|
3411
3472
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
@@ -3492,7 +3553,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3492
3553
|
};
|
|
3493
3554
|
}
|
|
3494
3555
|
const presentedArgs = decision.updatedInput !== undefined ? decision.updatedInput : creq.args;
|
|
3495
|
-
if (pc.autoMode !== undefined) {
|
|
3556
|
+
if (pc.autoMode !== undefined && !frozenClassifierExcluded(decision)) {
|
|
3496
3557
|
const verdict = await pc.autoMode.decider
|
|
3497
3558
|
.decide({ req: { ...creq, args: presentedArgs }, ...(decision.message !== undefined ? { askMessage: decision.message } : {}) }, csignal ?? abortController.signal)
|
|
3498
3559
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
@@ -3902,6 +3963,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3902
3963
|
nestedStats: { ...nestedStats },
|
|
3903
3964
|
consolidationNotes: undefined,
|
|
3904
3965
|
readFileState: readFileStateForCheckpoint ? [...readFileStateForCheckpoint.entries()] : undefined,
|
|
3966
|
+
readFace: faceCheckpointSection(),
|
|
3905
3967
|
repairBundle: repairBundleForCheckpoint(parkedSpendMicroUsd),
|
|
3906
3968
|
workspaceHandle,
|
|
3907
3969
|
handsCwd: handsCwdRef?.current,
|
|
@@ -4121,7 +4183,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4121
4183
|
const mintedAt = Date.now();
|
|
4122
4184
|
const cp = {
|
|
4123
4185
|
token,
|
|
4124
|
-
version: f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
|
|
4186
|
+
version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
|
|
4125
4187
|
scope,
|
|
4126
4188
|
sessionId,
|
|
4127
4189
|
leafId,
|
|
@@ -4203,7 +4265,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4203
4265
|
const mintedAt = Date.now();
|
|
4204
4266
|
const cp = {
|
|
4205
4267
|
token,
|
|
4206
|
-
version: f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4268
|
+
version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4207
4269
|
scope,
|
|
4208
4270
|
sessionId,
|
|
4209
4271
|
leafId,
|
|
@@ -4455,11 +4517,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4455
4517
|
const scope = durableApproval?.scope || checkpointScopeOf({ principal: spec.principal });
|
|
4456
4518
|
const ttlMs = sanitizedTtlMs(durableApproval?.ttlMs);
|
|
4457
4519
|
const checkpointState = serializeCheckpointState(remoteHandle, inFlightSpendMicroUsd());
|
|
4520
|
+
if (realApproval !== undefined && checkpointState.readFace !== undefined) {
|
|
4521
|
+
checkpointState.readFace = { ...checkpointState.readFace, realApproval: true };
|
|
4522
|
+
}
|
|
4458
4523
|
const approvalLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
|
|
4459
4524
|
const mintedAt = Date.now();
|
|
4460
4525
|
cp = {
|
|
4461
4526
|
token,
|
|
4462
|
-
version: realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4527
|
+
version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : realApproval !== undefined ? REAL_APPROVAL_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : approvalLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4463
4528
|
scope,
|
|
4464
4529
|
sessionId,
|
|
4465
4530
|
leafId,
|
|
@@ -4809,21 +4874,42 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4809
4874
|
promptManifest.lowering = turnSnapshot.lowering;
|
|
4810
4875
|
const promptOverheadTokens = Math.ceil((systemPrompt.length +
|
|
4811
4876
|
harnessTools.reduce((a, t) => a + t.name.length + (t.description?.length ?? 0) + JSON.stringify(t.parameters ?? {}).length, 0)) / charsPerToken);
|
|
4877
|
+
const announceAttachmentSkip = (path, reason) => {
|
|
4878
|
+
deps.onError?.(new Error(`working-file attachment skipped — ${path}: ${reason}`), {
|
|
4879
|
+
phase: "degraded",
|
|
4880
|
+
sessionId,
|
|
4881
|
+
classification: "attachment-read",
|
|
4882
|
+
});
|
|
4883
|
+
};
|
|
4812
4884
|
const readTaskFile = handsEnabled
|
|
4813
4885
|
? async (path) => {
|
|
4814
4886
|
try {
|
|
4887
|
+
let key = path;
|
|
4815
4888
|
if (attachmentRootCanonical !== undefined) {
|
|
4816
|
-
const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, undefined, additionalRootsCanonical);
|
|
4817
|
-
if (!rk.ok)
|
|
4889
|
+
const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, readDenyMatcher, resolvedReadFace);
|
|
4890
|
+
if (!rk.ok) {
|
|
4891
|
+
announceAttachmentSkip(path, rk.violation.message);
|
|
4892
|
+
if (rk.violation.code === "read_path_denied" && rk.violation.pattern !== undefined) {
|
|
4893
|
+
return { withheld: { pattern: rk.violation.pattern } };
|
|
4894
|
+
}
|
|
4818
4895
|
return null;
|
|
4896
|
+
}
|
|
4897
|
+
key = rk.key;
|
|
4819
4898
|
}
|
|
4820
|
-
const r = await executionEnv.readBinaryFile(
|
|
4821
|
-
if (!r.ok)
|
|
4899
|
+
const r = await executionEnv.readBinaryFile(key);
|
|
4900
|
+
if (!r.ok) {
|
|
4901
|
+
announceAttachmentSkip(path, r.error.message);
|
|
4822
4902
|
return null;
|
|
4903
|
+
}
|
|
4823
4904
|
const d = decodeTextBytes(r.value);
|
|
4824
|
-
|
|
4905
|
+
if (d.malformed) {
|
|
4906
|
+
announceAttachmentSkip(path, "malformed text encoding (truncated UTF-16 body)");
|
|
4907
|
+
return null;
|
|
4908
|
+
}
|
|
4909
|
+
return d.text;
|
|
4825
4910
|
}
|
|
4826
|
-
catch {
|
|
4911
|
+
catch (err) {
|
|
4912
|
+
announceAttachmentSkip(path, err instanceof Error ? err.message : String(err));
|
|
4827
4913
|
return null;
|
|
4828
4914
|
}
|
|
4829
4915
|
}
|
|
@@ -4833,7 +4919,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4833
4919
|
if (attachmentRootCanonical === undefined)
|
|
4834
4920
|
return path;
|
|
4835
4921
|
try {
|
|
4836
|
-
const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, handsCwdRef?.current, additionalRootsCanonical);
|
|
4922
|
+
const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, handsCwdRef?.current, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, resolvedReadFace);
|
|
4837
4923
|
return rk.ok ? rk.key : path;
|
|
4838
4924
|
}
|
|
4839
4925
|
catch {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
|
|
1
2
|
import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
|
|
2
3
|
import { snapshotActorAssertion } from "../../internal/llm.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
|
+
import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
|
|
4
5
|
import { engineVersion } from "../version.js";
|
|
5
6
|
import { CONFIG_CATALOG_VERSION, declarationReasons, resolveEffectiveConfig } from "../../config/catalog.js";
|
|
6
7
|
import { eventDefaultOn } from "../../prompt-assembly/event-registry.js";
|
|
@@ -3718,11 +3719,36 @@ export class Runner {
|
|
|
3718
3719
|
(preCasGateBit.origin === "org_rule" ||
|
|
3719
3720
|
preCasGateBit.origin === "org_unavailable" ||
|
|
3720
3721
|
preCasGateBit.origin === "policy");
|
|
3721
|
-
|
|
3722
|
+
const faceSaysGoverned = (cp.state.readFace?.realApproval) === true;
|
|
3723
|
+
const preCasBitViolation = checkpointVersionOf(cp) === REAL_APPROVAL_CHECKPOINT_VERSION
|
|
3724
|
+
? !preCasBitWellFormed
|
|
3725
|
+
: checkpointVersionOf(cp) < REAL_APPROVAL_CHECKPOINT_VERSION
|
|
3726
|
+
? preCasGateBit !== undefined
|
|
3727
|
+
: faceSaysGoverned
|
|
3728
|
+
? !preCasBitWellFormed
|
|
3729
|
+
: preCasGateBit !== undefined && !preCasBitWellFormed;
|
|
3730
|
+
if (preCasBitViolation) {
|
|
3722
3731
|
throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= REAL_APPROVAL_CHECKPOINT_VERSION
|
|
3723
|
-
? `a v${checkpointVersionOf(cp)} checkpoint must
|
|
3732
|
+
? `a v${checkpointVersionOf(cp)} checkpoint's realApproval gate bit must be well-formed (origin org_rule/org_unavailable/policy)${checkpointVersionOf(cp) === REAL_APPROVAL_CHECKPOINT_VERSION ? " and present on an irreversible_ask gate" : ""} — this row's is not; refusing to resume a damaged real-approval row (corruption / downgrade guard), the checkpoint stays pending`
|
|
3724
3733
|
: `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" });
|
|
3725
3734
|
}
|
|
3735
|
+
{
|
|
3736
|
+
const faceSectionRaw = cp.state.readFace;
|
|
3737
|
+
const faceCandidate = faceSectionRaw !== undefined && typeof faceSectionRaw === "object" && faceSectionRaw !== null
|
|
3738
|
+
? faceSectionRaw
|
|
3739
|
+
: undefined;
|
|
3740
|
+
const faceWellFormed = faceCandidate !== undefined &&
|
|
3741
|
+
(faceCandidate.face === "open" || faceCandidate.face === "roots") &&
|
|
3742
|
+
(faceCandidate.realApproval === undefined || faceCandidate.realApproval === true) &&
|
|
3743
|
+
(faceCandidate.denyEntries === undefined ||
|
|
3744
|
+
(Array.isArray(faceCandidate.denyEntries) &&
|
|
3745
|
+
faceCandidate.denyEntries.every((e) => persistedReadDenyEntryProblem(e) === null)));
|
|
3746
|
+
if (checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION ? !faceWellFormed : faceSectionRaw !== undefined) {
|
|
3747
|
+
throw new CheckpointError("checkpoint.invalid_outcome", checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION
|
|
3748
|
+
? `a v${checkpointVersionOf(cp)} checkpoint must carry a well-formed read-face section (state.readFace) — this row's is missing or malformed; refusing to resume a damaged read-face row (corruption / downgrade guard), the checkpoint stays pending`
|
|
3749
|
+
: `a v${checkpointVersionOf(cp)} checkpoint carries a read-face section no release of that version ever minted — refusing to honor a fabricated read-face state (corruption / forgery guard), the checkpoint stays pending`, { reason: checkpointVersionOf(cp) >= FACE_CHECKPOINT_VERSION ? "read_face_damaged" : "read_face_forged" });
|
|
3750
|
+
}
|
|
3751
|
+
}
|
|
3726
3752
|
if ((preCasGateBit?.origin === "org_rule" || preCasGateBit?.origin === "org_unavailable") &&
|
|
3727
3753
|
this.deps.permissionRuleOrg === undefined) {
|
|
3728
3754
|
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" });
|
|
@@ -53,8 +53,9 @@ export declare function isWithin(root: string, p: string): boolean;
|
|
|
53
53
|
* - `toolDeny` → deny the listed tools.
|
|
54
54
|
* - `toolAllow` (if set) → deny any tool NOT listed (a narrowing allowlist).
|
|
55
55
|
* - `commandAllow`/`commandDeny` → delegate to {@link createCoarseCommandNamePolicy} (argv[0] names; only
|
|
56
|
-
* speaks about shell tools)
|
|
57
|
-
* un-parseable
|
|
56
|
+
* speaks about shell tools) with `defaultAction:"deny"` in BOTH modes — so this policy structurally
|
|
57
|
+
* emits no ask. Allowlist mode denies the un-listed and the un-parseable; blocklist mode denies the
|
|
58
|
+
* listed and the un-parseable (a parseable command absent from the deny list stays allowed).
|
|
58
59
|
* - `allowDirs` (if set) → a WRITE tool must land inside one of the dirs (resolved via `canonicalizeTarget`,
|
|
59
60
|
* so a symlink can't carry across a write out); a write-capable tool that cannot be path-confined (e.g. `bash`)
|
|
60
61
|
* is DENIED while `allowDirs` is set (fail-closed, mirrors active-skill-scope MAJOR-2).
|
|
@@ -62,7 +62,7 @@ const CC_DETAIL_TYPES = new Set([
|
|
|
62
62
|
"agent", "task", "task-list", "task-output", "workflow-run",
|
|
63
63
|
"web-fetch", "web-search", "todo", "cron-create", "cron-delete", "cron-list", "image",
|
|
64
64
|
"task-stop", "tool-search", "repo-map", "fork", "enter-plan-mode", "exit-plan-mode",
|
|
65
|
-
"monitor-start", "path_not_in_root", "readonly_out_of_root", "bash_invalid_timeout",
|
|
65
|
+
"monitor-start", "path_not_in_root", "read_path_denied", "readonly_out_of_root", "bash_invalid_timeout",
|
|
66
66
|
"report-findings", "schedule-wakeup", "send-message", "agent-transcript", "a2a", "document",
|
|
67
67
|
]);
|
|
68
68
|
export const structuredFrom = (result) => {
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { canonicalizeTarget, writeTargetPath } from "../tools/fs/safety.js";
|
|
2
|
+
import { compileSegmentPattern, matchSegmentPatterns } from "../tools/fs/read-deny.js";
|
|
2
3
|
const DEFAULT_GUARDED_TOOLS = ["Write", "Edit", "MultiEdit", "NotebookEdit"];
|
|
3
4
|
export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
4
5
|
".env",
|
|
@@ -25,33 +26,21 @@ export const RECOMMENDED_SENSITIVE_PATTERNS = [
|
|
|
25
26
|
".bash_history",
|
|
26
27
|
".zsh_history",
|
|
27
28
|
];
|
|
28
|
-
function segmentGlobToRegExp(segment) {
|
|
29
|
-
const escaped = segment.replace(/[.+^${}()|[\]\\?]/g, "\\$&").replace(/\*/g, "[^/]*");
|
|
30
|
-
return new RegExp(`^${escaped}$`, CASE_INSENSITIVE_FS ? "i" : "");
|
|
31
|
-
}
|
|
32
29
|
const CASE_INSENSITIVE_FS = process.platform === "darwin" || process.platform === "win32";
|
|
33
30
|
function compilePatterns(patterns) {
|
|
34
31
|
const out = [];
|
|
35
32
|
for (const raw of patterns) {
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
33
|
+
const compiled = compileSegmentPattern(raw, CASE_INSENSITIVE_FS ? "unicode" : "none");
|
|
34
|
+
if (compiled === null) {
|
|
38
35
|
throw new Error(`createSensitivePathPolicy: pattern ${JSON.stringify(raw)} contains no path segments and would guard nothing. ` +
|
|
39
36
|
`Patterns are "/"-separated runs of path SEGMENTS (e.g. ".ssh", ".git/hooks", "*.pem"); remove the entry or spell the segments.`);
|
|
40
37
|
}
|
|
41
|
-
out.push(
|
|
38
|
+
out.push(compiled);
|
|
42
39
|
}
|
|
43
40
|
return out;
|
|
44
41
|
}
|
|
45
42
|
function matchSensitive(canonicalKey, compiled) {
|
|
46
|
-
|
|
47
|
-
for (const pat of compiled) {
|
|
48
|
-
const n = pat.segments.length;
|
|
49
|
-
for (let i = 0; i + n <= segs.length; i++) {
|
|
50
|
-
if (pat.segments.every((re, j) => re.test(segs[i + j])))
|
|
51
|
-
return pat.raw;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
43
|
+
return matchSegmentPatterns(canonicalKey, compiled);
|
|
55
44
|
}
|
|
56
45
|
export function createSensitivePathPolicy(opts) {
|
|
57
46
|
const compiled = compilePatterns(opts.patterns);
|
|
@@ -55,5 +55,28 @@ export async function toolResultStoreContract(make, runAssertion) {
|
|
|
55
55
|
assert.equal((await store.get(unowned)).content, "bytes from a write site that stated no owner");
|
|
56
56
|
assert.equal(await ownerOf("tr_never~written"), undefined);
|
|
57
57
|
});
|
|
58
|
+
run("#196 deleteBySession (optional): four-state semantics when present; absence is REPORTED, not silently green", async () => {
|
|
59
|
+
const probe = make();
|
|
60
|
+
if (typeof probe.deleteBySession !== "function") {
|
|
61
|
+
assert.equal(probe.deleteBySession, undefined, "deleteBySession is NOT IMPLEMENTED on this backend — sweep semantics unverified (optional member; this is a disclosure, not a failure)");
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const store = make();
|
|
65
|
+
const del = (sessionId) => Promise.resolve(store.deleteBySession(sessionId));
|
|
66
|
+
await store.put("tr_sweep~a", "A-bytes", { sessionId: "sess-X", taskId: "t1" });
|
|
67
|
+
await store.put("tr_sweep~b", "B-bytes", { sessionId: "sess-X" });
|
|
68
|
+
await store.put("tr_sweep~other", "OTHER-bytes", { sessionId: "sess-Y" });
|
|
69
|
+
await store.put("tr_sweep~unowned", "UNOWNED-bytes");
|
|
70
|
+
const first = await del("sess-X");
|
|
71
|
+
assert.equal(first.deleted, 2, "both rows recorded for the session are deleted (taskId presence does not split the key)");
|
|
72
|
+
assert.equal(await store.get("tr_sweep~a"), undefined, "a swept row is gone");
|
|
73
|
+
assert.equal(await store.get("tr_sweep~b"), undefined);
|
|
74
|
+
assert.equal((await store.get("tr_sweep~other")).content, "OTHER-bytes", "another session's row survives the sweep");
|
|
75
|
+
assert.equal(first.unattributable >= 1, true, "an unowned row is counted unattributable");
|
|
76
|
+
assert.equal((await store.get("tr_sweep~unowned")).content, "UNOWNED-bytes", "an unowned row is never deleted by a session sweep");
|
|
77
|
+
const second = await del("sess-X");
|
|
78
|
+
assert.equal(second.deleted, 0, "a repeated sweep deletes nothing");
|
|
79
|
+
assert.equal((await store.get("tr_sweep~other")).content, "OTHER-bytes");
|
|
80
|
+
});
|
|
58
81
|
await settle();
|
|
59
82
|
}
|
|
@@ -25,6 +25,24 @@ export function tightenTaskSpec(base, overrides) {
|
|
|
25
25
|
merged[field] = [...new Set([...(b ?? []), ...(o ?? [])])];
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
|
+
if (base.readFace === "roots" && overrides.readFace === "open") {
|
|
29
|
+
throw new TaskSpecTightenError('tightenTaskSpec: cannot loosen readFace from "roots" to "open" (override may only tighten)');
|
|
30
|
+
}
|
|
31
|
+
if (base.readFace !== undefined || overrides.readFace !== undefined) {
|
|
32
|
+
merged.readFace = base.readFace === "roots" || overrides.readFace === "roots" ? "roots" : (overrides.readFace ?? base.readFace);
|
|
33
|
+
}
|
|
34
|
+
if (base.readDenyPatterns !== undefined || overrides.readDenyPatterns !== undefined) {
|
|
35
|
+
const seen = new Set();
|
|
36
|
+
const union = [];
|
|
37
|
+
for (const e of [...(base.readDenyPatterns ?? []), ...(overrides.readDenyPatterns ?? [])]) {
|
|
38
|
+
const key = typeof e === "string" ? `i:${e}` : `${e.caseSensitive === true ? "s" : "i"}:${e.pattern}`;
|
|
39
|
+
if (seen.has(key))
|
|
40
|
+
continue;
|
|
41
|
+
seen.add(key);
|
|
42
|
+
union.push(e);
|
|
43
|
+
}
|
|
44
|
+
merged.readDenyPatterns = union;
|
|
45
|
+
}
|
|
28
46
|
if (base.handsReadOnly === true && overrides.handsReadOnly === false) {
|
|
29
47
|
throw new TaskSpecTightenError("tightenTaskSpec: cannot loosen handsReadOnly from true to false (override may only tighten)");
|
|
30
48
|
}
|
|
@@ -175,6 +175,19 @@ export type PermissionResult = {
|
|
|
175
175
|
* consumer (approval card, wire frame) can tell the person their rule is alive, just outranked.
|
|
176
176
|
* Absent ⇒ no rule matched, or the ask was cleared normally. */
|
|
177
177
|
persistedRuleShadowed?: string;
|
|
178
|
+
/** An EXPLICIT `ask` permission rule matched this call (design/127 DSL — the CC `alwaysAskRules`
|
|
179
|
+
* shape); carries the matched rule's text. Present ⇔ a rule someone WROTE says "ask about this",
|
|
180
|
+
* never for a `defaultAction:"ask"` fallback (an unmatched call is default-closed posture, not a
|
|
181
|
+
* per-call instruction — CC draws the same line: only a matched ask rule diverts the auto-mode
|
|
182
|
+
* lane, a mode-default ask stays classifier-eligible). Consumer: the gate's auto-mode classifier
|
|
183
|
+
* step refuses to resolve an ask carrying it — a person's standing "ask me each time" is not
|
|
184
|
+
* classifier hesitation, so a classifier verdict must not be the thing that clears it. A policy
|
|
185
|
+
* self-declaring the member only ever OPTS ITS OWN ask OUT of classifier auto-resolution
|
|
186
|
+
* (tightening — the false-claim direction is safe, unlike the engine-stamped `"hook"` word whose
|
|
187
|
+
* risk was laundering provenance AWAY). {@link combinePolicies} carries it onto the surviving ask
|
|
188
|
+
* when ANY folded-away concurrent ask bore it (monotone, tighten-only); it is consumed inside the
|
|
189
|
+
* gate and deliberately NOT copied onto the park/approval-card request. */
|
|
190
|
+
matchedAskRule?: string;
|
|
178
191
|
} | {
|
|
179
192
|
action: "deny";
|
|
180
193
|
updatedInput?: unknown;
|
|
@@ -723,7 +736,13 @@ export interface AskRequest {
|
|
|
723
736
|
* (codex adversarial round, confirmed). NOT the same fact as {@link fromSubagent} either: that is
|
|
724
737
|
* the design/153 attribution discriminator (`parentTaskId` presence), which a directly-started
|
|
725
738
|
* workflow's children lack even though refusals must still speak the child posture to them.
|
|
726
|
-
* `readonly`, trusted (internals never ride a TaskSpec), sync-path-only like its siblings.
|
|
739
|
+
* `readonly`, trusted (internals never ride a TaskSpec), sync-path-only like its siblings.
|
|
740
|
+
*
|
|
741
|
+
* Key division, at a glance (the two child-ish keys serve DIFFERENT faces — neither implies the other):
|
|
742
|
+
* - `fromSubagent` — ATTRIBUTION: "a nameable parent task issued this" (`parentTaskId` presence);
|
|
743
|
+
* drives display/identity surfaces.
|
|
744
|
+
* - `isDelegatedChild` — REFUSAL POSTURE: "no user turn ever lands in this run's transcript";
|
|
745
|
+
* drives which deny text the model is told (stop-and-wait vs adapt-or-report), forks included. */
|
|
727
746
|
readonly isDelegatedChild?: true;
|
|
728
747
|
/** RB-203 (codex review, confirmed P1) — carried from the originating {@link PermissionResult}'s ask
|
|
729
748
|
* variant of the same name: `true` ⇒ {@link resolveAsk} must not let a blanket `onAsk: "allow"`
|
package/dist/core/tool-policy.js
CHANGED
|
@@ -317,6 +317,7 @@ export function combinePolicies(...policies) {
|
|
|
317
317
|
let current = req;
|
|
318
318
|
let rewrite;
|
|
319
319
|
let settled;
|
|
320
|
+
let ruleAskText;
|
|
320
321
|
for (const p of policies) {
|
|
321
322
|
const d = refuseOutOfContractDecision(await p.check(current, signal));
|
|
322
323
|
if (d.action === "deny") {
|
|
@@ -334,10 +335,14 @@ export function combinePolicies(...policies) {
|
|
|
334
335
|
if (d.action === "ask" && (asked === undefined || (d.requiresRealApproval === true && asked.requiresRealApproval !== true))) {
|
|
335
336
|
asked = d;
|
|
336
337
|
}
|
|
338
|
+
if (d.action === "ask" && d.matchedAskRule !== undefined && ruleAskText === undefined) {
|
|
339
|
+
ruleAskText = d.matchedAskRule;
|
|
340
|
+
}
|
|
337
341
|
}
|
|
338
342
|
if (asked) {
|
|
339
343
|
const merged = rewrite?.updatedInput;
|
|
340
|
-
|
|
344
|
+
const withMark = ruleAskText !== undefined && asked.matchedAskRule === undefined ? { ...asked, matchedAskRule: ruleAskText } : asked;
|
|
345
|
+
return merged !== undefined ? { ...withMark, updatedInput: merged } : withMark;
|
|
341
346
|
}
|
|
342
347
|
const allowed = rewrite ?? ALLOW;
|
|
343
348
|
return settled !== undefined ? { ...allowed, settledBy: settled } : allowed;
|
|
@@ -891,9 +896,31 @@ export async function resolveAsk(req, onAsk, signal) {
|
|
|
891
896
|
};
|
|
892
897
|
}
|
|
893
898
|
if (typeof ok === "object" && ok !== null) {
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
899
|
+
let supplied;
|
|
900
|
+
let allowed;
|
|
901
|
+
let suppliedEdit;
|
|
902
|
+
try {
|
|
903
|
+
supplied = ok.settledBy;
|
|
904
|
+
allowed = ok.allow;
|
|
905
|
+
suppliedEdit = ok.updatedInput;
|
|
906
|
+
}
|
|
907
|
+
catch (err) {
|
|
908
|
+
return {
|
|
909
|
+
action: "deny",
|
|
910
|
+
message: `the approver's outcome for "${req.toolName}" could not be read (${describeThrown(err)}) — denied fail-closed`,
|
|
911
|
+
decisionReason: "mode",
|
|
912
|
+
settledBy: "aborted",
|
|
913
|
+
};
|
|
914
|
+
}
|
|
915
|
+
if (typeof allowed !== "boolean") {
|
|
916
|
+
return {
|
|
917
|
+
action: "deny",
|
|
918
|
+
message: `the approver for "${req.toolName}" returned an object whose allow is not a boolean ` +
|
|
919
|
+
`(got ${allowed === null ? "null" : typeof allowed}) — a verdict is exactly true or false; denied fail-closed`,
|
|
920
|
+
decisionReason: "mode",
|
|
921
|
+
settledBy: "aborted",
|
|
922
|
+
};
|
|
923
|
+
}
|
|
897
924
|
if (supplied !== undefined && supplied !== "human" && supplied !== "timeout") {
|
|
898
925
|
return {
|
|
899
926
|
action: "deny",
|
|
@@ -366,7 +366,9 @@ async function offloadOversizedDetailStrings(details, store, thresholdChars, ses
|
|
|
366
366
|
const value = walk(details, []);
|
|
367
367
|
const settled = await Promise.allSettled(pending.map((p) => p.done));
|
|
368
368
|
const notices = new Map();
|
|
369
|
-
pending.forEach((p, i) =>
|
|
369
|
+
pending.forEach((p, i) => {
|
|
370
|
+
notices.set(p.marker, p.render(settled[i].status === "fulfilled"));
|
|
371
|
+
});
|
|
370
372
|
const seen = new Set();
|
|
371
373
|
const finalize = (node) => {
|
|
372
374
|
if (typeof node !== "object" || node === null)
|