@sema-agent/core 5.29.0 → 5.31.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 +141 -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 +115 -26
- 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.d.ts +4 -1
- package/dist/core/store-contracts/tool-result-store-contract.js +26 -1
- 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.d.ts +6 -4
- package/dist/core/tool-result-store.js +3 -1
- package/dist/core/types.d.ts +63 -1
- 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 +26 -0
- package/dist/orchestration/run-workflow-tool.js +7 -4
- package/dist/orchestration/workflow-governance.d.ts +53 -3
- package/dist/orchestration/workflow-governance.js +162 -25
- package/dist/orchestration/workflow-primitives.d.ts +15 -1
- package/dist/orchestration/workflow-primitives.js +13 -2
- 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 +53 -4
- package/dist/tools/fs/bash-readonly-classifier.js +148 -16
- 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 +110 -0
- package/dist/tools/fs/read-deny.js +159 -0
- package/dist/tools/fs/read-face.d.ts +49 -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 +34 -11
- package/dist/tools/fs/safety.js +108 -8
- package/dist/tools/fs/search.d.ts +54 -5
- package/dist/tools/fs/search.js +107 -23
- 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, assertReadFaceValue, 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";
|
|
@@ -342,6 +342,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
342
342
|
throw e;
|
|
343
343
|
}
|
|
344
344
|
}
|
|
345
|
+
assertReadFaceValue(spec.readFace, "TaskSpec.readFace");
|
|
346
|
+
assertReadFaceValue(deps.readFace, "readFace (deployment seat)");
|
|
345
347
|
if (spec.resumeAtMode !== undefined) {
|
|
346
348
|
if (spec.resumeAt === undefined) {
|
|
347
349
|
const e = new Error(`resumeAtMode "${spec.resumeAtMode}" requires resumeAt (there is no branch target to position against)`);
|
|
@@ -917,7 +919,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
917
919
|
internals.onWorkspaceResolved({
|
|
918
920
|
cwd: taskRootPath,
|
|
919
921
|
isolated: internals.isolation === "worktree",
|
|
920
|
-
remote: workspaceEnv !== undefined &&
|
|
922
|
+
remote: workspaceEnv !== undefined &&
|
|
923
|
+
(isRemoteExecutionEnv(workspaceEnv) || workspaceEnv.hostLocalPaths === false),
|
|
921
924
|
});
|
|
922
925
|
}
|
|
923
926
|
catch {
|
|
@@ -1081,6 +1084,17 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1081
1084
|
const memoryAdmittedOrgScopesRef = { current: [] };
|
|
1082
1085
|
const ownOrgVerdictRef = { current: undefined };
|
|
1083
1086
|
const orgAdmissionCheckpointState = () => inheritedAdmittedOrgScopes !== undefined || ownOrgVerdictRef.current !== undefined || orgGovernedProvenance;
|
|
1087
|
+
const faceCheckpointSection = () => {
|
|
1088
|
+
if (resolvedReadFace === undefined) {
|
|
1089
|
+
const seed = resume?.seed.readFace;
|
|
1090
|
+
return seed !== undefined ? { face: seed.face, ...(seed.denyEntries !== undefined ? { denyEntries: seed.denyEntries.map((e) => ({ ...e })) } : {}) } : undefined;
|
|
1091
|
+
}
|
|
1092
|
+
if (resolvedReadFace === "open" || readDenyAdditionsNormalized.length > 0) {
|
|
1093
|
+
return { face: resolvedReadFace, ...(readDenyAdditionsNormalized.length > 0 ? { denyEntries: readDenyAdditionsNormalized.map((e) => ({ ...e })) } : {}) };
|
|
1094
|
+
}
|
|
1095
|
+
return undefined;
|
|
1096
|
+
};
|
|
1097
|
+
const faceCheckpointState = () => faceCheckpointSection() !== undefined;
|
|
1084
1098
|
const f012CheckpointState = () => (inheritedParentConstraints?.length ?? 0) > 0 ||
|
|
1085
1099
|
seedInheritedGate?.constraintChain !== undefined ||
|
|
1086
1100
|
internals?.delegationProvenance !== undefined;
|
|
@@ -1151,6 +1165,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1151
1165
|
};
|
|
1152
1166
|
let agentForkDenial;
|
|
1153
1167
|
let observersActive = false;
|
|
1168
|
+
let resolvedReadFace;
|
|
1169
|
+
let readDenyAdditionsNormalized = [];
|
|
1170
|
+
const fullShellReachable = handsEnabled &&
|
|
1171
|
+
!(executionEnv instanceof StubExecutionEnv) &&
|
|
1172
|
+
spec.handsReadOnly !== true &&
|
|
1173
|
+
!(toolFaceSnapshot.exclude?.includes("Bash") ?? false);
|
|
1154
1174
|
let autoModeDecider;
|
|
1155
1175
|
const enrichSpecToolCtx = (ctx) => ({
|
|
1156
1176
|
...ctx,
|
|
@@ -1161,6 +1181,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1161
1181
|
...(frozenOnAsk !== undefined ? { onAsk: frozenOnAsk } : {}),
|
|
1162
1182
|
...(frozenOnQuestion !== undefined ? { onQuestion: frozenOnQuestion } : {}),
|
|
1163
1183
|
...(spec.handsReadOnly === true ? { handsReadOnly: true } : {}),
|
|
1184
|
+
...(resolvedReadFace === "roots" ? { readFace: "roots" } : {}),
|
|
1185
|
+
...(readDenyAdditionsNormalized.length > 0 ? { readDenyPatterns: Object.freeze(readDenyAdditionsNormalized.map((e) => ({ ...e }))) } : {}),
|
|
1164
1186
|
...(spec.interactiveTools === false ? { interactiveTools: false } : {}),
|
|
1165
1187
|
oneShot: spec.oneShot,
|
|
1166
1188
|
clientContext: spec.clientContext,
|
|
@@ -1366,6 +1388,12 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1366
1388
|
workflowDepth: internals?.workflowDepth,
|
|
1367
1389
|
parentCwd: taskRootPath,
|
|
1368
1390
|
parentThinking: () => harnessRef.current?.getThinkingLevel() ?? thinking,
|
|
1391
|
+
parentReadFace: () => resolvedReadFace,
|
|
1392
|
+
parentReadDenyPatterns: () => (readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized : undefined),
|
|
1393
|
+
onNotice: deps.onNotice,
|
|
1394
|
+
parentCheckpointStoreDisabled: spec.checkpointStore === null,
|
|
1395
|
+
parentCenterArtifactDigest: () => centerAdoption?.artifact.artifactDigest,
|
|
1396
|
+
parentCenterSourceRevision: () => centerAdoption?.sourceRevision,
|
|
1369
1397
|
...(forwardEvent ? { forwardEvent } : {}),
|
|
1370
1398
|
inheritedGateForChildren,
|
|
1371
1399
|
}));
|
|
@@ -1578,6 +1606,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1578
1606
|
const additionalRootsCanonical = [];
|
|
1579
1607
|
const additionalReadRootsCanonical = [];
|
|
1580
1608
|
let attachmentRootCanonical;
|
|
1609
|
+
let readDenyMatcher;
|
|
1581
1610
|
if (handsEnabled) {
|
|
1582
1611
|
const rootRaw = taskRootPath;
|
|
1583
1612
|
const canon = await executionEnv.canonicalPath(rootRaw);
|
|
@@ -1626,17 +1655,9 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1626
1655
|
}
|
|
1627
1656
|
const readFileState = new Map((resume?.seed.readFileState ?? []).map(([k, v]) => [rebaseRestoredPath(k), v]));
|
|
1628
1657
|
readFileStateForCheckpoint = readFileState;
|
|
1629
|
-
if (resume === undefined && spec.sessionId !== undefined) {
|
|
1630
|
-
const prior = await session.buildContext().catch(() => undefined);
|
|
1631
|
-
for (const rec of wholeFileRecordsFromTranscript(prior?.messages ?? [])) {
|
|
1632
|
-
const rk = await resolveKey(executionEnv, rootCanonical, rec.path, undefined, undefined, additionalRootsCanonical);
|
|
1633
|
-
if (rk.ok)
|
|
1634
|
-
seedReadFileStateFromTranscript(readFileState, rk.key, rec.content, rec.at);
|
|
1635
|
-
}
|
|
1636
|
-
}
|
|
1637
1658
|
seedContextFiles = async (files) => {
|
|
1638
1659
|
for (const f of files) {
|
|
1639
|
-
const rk = await resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, additionalRootsCanonical);
|
|
1660
|
+
const rk = await resolveKey(executionEnv, rootCanonical, f.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, resolvedReadFace);
|
|
1640
1661
|
if (rk.ok)
|
|
1641
1662
|
seedReadFileStateFromContext(readFileState, rk.key, f.content);
|
|
1642
1663
|
}
|
|
@@ -1658,9 +1679,36 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1658
1679
|
...(resume !== undefined ? { baselineUnknown: true } : {}),
|
|
1659
1680
|
};
|
|
1660
1681
|
}
|
|
1682
|
+
const seedReadFaceSection = resume?.seed.readFace;
|
|
1683
|
+
const readDenyAdditions = [...(deps.readDenyPatterns ?? []), ...(spec.readDenyPatterns ?? []), ...(seedReadFaceSection?.denyEntries ?? [])];
|
|
1684
|
+
readDenyMatcher = compileReadDeny(readDenyAdditions, "readDenyPatterns");
|
|
1685
|
+
{
|
|
1686
|
+
const builtinCount = compileReadDeny([], "built-in").entries.length;
|
|
1687
|
+
readDenyAdditionsNormalized = readDenyMatcher.entries.slice(builtinCount);
|
|
1688
|
+
}
|
|
1689
|
+
let liveReadFace = resolveReadFace({
|
|
1690
|
+
specReadFace: spec.readFace,
|
|
1691
|
+
depsReadFace: deps.readFace,
|
|
1692
|
+
readOnlyMount: handsReadOnly,
|
|
1693
|
+
orgGoverned: deps.permissionRuleOrg !== undefined,
|
|
1694
|
+
fullShellReachable,
|
|
1695
|
+
});
|
|
1696
|
+
if (resume !== undefined && (seedReadFaceSection === undefined || seedReadFaceSection.face === "roots"))
|
|
1697
|
+
liveReadFace = "roots";
|
|
1698
|
+
resolvedReadFace = liveReadFace;
|
|
1699
|
+
if (resume === undefined && spec.sessionId !== undefined) {
|
|
1700
|
+
const prior = await session.buildContext().catch(() => undefined);
|
|
1701
|
+
for (const rec of wholeFileRecordsFromTranscript(prior?.messages ?? [])) {
|
|
1702
|
+
const rk = await resolveKey(executionEnv, rootCanonical, rec.path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, liveReadFace);
|
|
1703
|
+
if (rk.ok)
|
|
1704
|
+
seedReadFileStateFromTranscript(readFileState, rk.key, rec.content, rec.at);
|
|
1705
|
+
}
|
|
1706
|
+
}
|
|
1661
1707
|
const band = createHandsToolkit(executionEnv, readFileState, rootCanonical, {
|
|
1662
1708
|
...(additionalRootsCanonical.length > 0 ? { additionalRoots: additionalRootsCanonical } : {}),
|
|
1663
1709
|
...(additionalReadRootsCanonical.length > 0 ? { additionalReadRoots: additionalReadRootsCanonical } : {}),
|
|
1710
|
+
...(readDenyAdditions.length > 0 ? { readDenyPatterns: readDenyAdditions } : {}),
|
|
1711
|
+
readFace: liveReadFace,
|
|
1664
1712
|
includeShell: handsIncludeShell,
|
|
1665
1713
|
readOnly: handsReadOnly,
|
|
1666
1714
|
...(handsCwdRef ? { cwdRef: handsCwdRef } : {}),
|
|
@@ -1714,6 +1762,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
1714
1762
|
const shellReadBoundary = () => ({
|
|
1715
1763
|
roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical],
|
|
1716
1764
|
...(handsCwdRef?.current !== undefined ? { cwd: handsCwdRef.current } : {}),
|
|
1765
|
+
denyMatch: (p) => readDenyMatcher?.matchPath(p)?.pattern ?? null,
|
|
1766
|
+
...(resolvedReadFace !== undefined ? { face: resolvedReadFace } : {}),
|
|
1717
1767
|
});
|
|
1718
1768
|
if (shellGate === "classify" && shellGatedBash)
|
|
1719
1769
|
reversibilityProbes.set("Bash", bashReversibilityProbe(undefined, shellReadBoundary));
|
|
@@ -2164,6 +2214,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2164
2214
|
policyEnabled: promptPolicyEnabled,
|
|
2165
2215
|
hooksEnabled: promptHooksEnabled,
|
|
2166
2216
|
isolationEnabled: isIsolated(executionEnv),
|
|
2217
|
+
readFaceOpen: resolvedReadFace === "open",
|
|
2167
2218
|
orchestrationEnabled: selfOrchestrationActive,
|
|
2168
2219
|
orchestrationDeferred: selfOrchestrationActive && (toolFaceSnapshot.defer?.includes("Workflow") ?? false),
|
|
2169
2220
|
promptProfile,
|
|
@@ -2216,6 +2267,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2216
2267
|
if (additionalRootsCanonical.length > 0) {
|
|
2217
2268
|
envFacts.additionalDirectories = [...additionalRootsCanonical];
|
|
2218
2269
|
}
|
|
2270
|
+
if (resolvedReadFace !== undefined)
|
|
2271
|
+
envFacts.readFace = resolvedReadFace;
|
|
2219
2272
|
if (additionalReadRootsCanonical.length > 0) {
|
|
2220
2273
|
envFacts.additionalReadDirectories = [...additionalReadRootsCanonical];
|
|
2221
2274
|
}
|
|
@@ -2309,6 +2362,16 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
2309
2362
|
if (excluded.has(tools[i].name))
|
|
2310
2363
|
tools.splice(i, 1);
|
|
2311
2364
|
}
|
|
2365
|
+
if (handsEnabled) {
|
|
2366
|
+
const fullShellOnRoster = tools.some((t) => t.name === "Bash" && getToolContract(t, "core").contractId === FULL_SHELL_CONTRACT_ID);
|
|
2367
|
+
if (fullShellOnRoster !== fullShellReachable) {
|
|
2368
|
+
const e = new Error(`internal invariant: fullShellReachable=${String(fullShellReachable)} but the post-exclusion roster ` +
|
|
2369
|
+
`${fullShellOnRoster ? "carries" : "does not carry"} the full shell (${FULL_SHELL_CONTRACT_ID}). ` +
|
|
2370
|
+
`A new roster-affecting mechanism was added without updating the predicate.`);
|
|
2371
|
+
e.code = "internal.full_shell_reachable_mismatch";
|
|
2372
|
+
throw e;
|
|
2373
|
+
}
|
|
2374
|
+
}
|
|
2312
2375
|
if (promptProfile === "classic") {
|
|
2313
2376
|
for (let i = 0; i < tools.length; i++) {
|
|
2314
2377
|
const t = tools[i];
|
|
@@ -3300,6 +3363,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3300
3363
|
const suggestions = suggestRulesForCommand(command);
|
|
3301
3364
|
return suggestions.length > 0 ? { ruleSuggestions: suggestions } : {};
|
|
3302
3365
|
};
|
|
3366
|
+
const frozenClassifierExcluded = (d) => d.decisionReason === "hook" || d.matchedAskRule !== undefined;
|
|
3303
3367
|
const recheckApprovedEdit = async (pol, onAskOf, creq, edit, csignal, ancestorDecider) => {
|
|
3304
3368
|
let editArgs = edit;
|
|
3305
3369
|
for (let round = 0;; round++) {
|
|
@@ -3326,7 +3390,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3326
3390
|
}
|
|
3327
3391
|
if (re.updatedInput !== undefined)
|
|
3328
3392
|
editArgs = re.updatedInput;
|
|
3329
|
-
if (ancestorDecider !== undefined) {
|
|
3393
|
+
if (ancestorDecider !== undefined && !frozenClassifierExcluded(re)) {
|
|
3330
3394
|
const verdict = await ancestorDecider
|
|
3331
3395
|
.decide({ req: { ...creq, args: editArgs }, ...(re.message !== undefined ? { askMessage: re.message } : {}) }, csignal ?? abortController.signal)
|
|
3332
3396
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
@@ -3405,7 +3469,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3405
3469
|
`ask cannot be reconstructed in a delegated child; denied fail-closed (tighten-only)`,
|
|
3406
3470
|
};
|
|
3407
3471
|
}
|
|
3408
|
-
if (pc.autoMode !== undefined) {
|
|
3472
|
+
if (pc.autoMode !== undefined && !frozenClassifierExcluded(first)) {
|
|
3409
3473
|
const verdict = await pc.autoMode.decider
|
|
3410
3474
|
.decide({ req: creq, ...(first.message !== undefined ? { askMessage: first.message } : {}) }, csignal ?? abortController.signal)
|
|
3411
3475
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
@@ -3492,7 +3556,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3492
3556
|
};
|
|
3493
3557
|
}
|
|
3494
3558
|
const presentedArgs = decision.updatedInput !== undefined ? decision.updatedInput : creq.args;
|
|
3495
|
-
if (pc.autoMode !== undefined) {
|
|
3559
|
+
if (pc.autoMode !== undefined && !frozenClassifierExcluded(decision)) {
|
|
3496
3560
|
const verdict = await pc.autoMode.decider
|
|
3497
3561
|
.decide({ req: { ...creq, args: presentedArgs }, ...(decision.message !== undefined ? { askMessage: decision.message } : {}) }, csignal ?? abortController.signal)
|
|
3498
3562
|
.catch(() => ({ kind: "unavailable", cause: "error" }));
|
|
@@ -3902,6 +3966,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
3902
3966
|
nestedStats: { ...nestedStats },
|
|
3903
3967
|
consolidationNotes: undefined,
|
|
3904
3968
|
readFileState: readFileStateForCheckpoint ? [...readFileStateForCheckpoint.entries()] : undefined,
|
|
3969
|
+
readFace: faceCheckpointSection(),
|
|
3905
3970
|
repairBundle: repairBundleForCheckpoint(parkedSpendMicroUsd),
|
|
3906
3971
|
workspaceHandle,
|
|
3907
3972
|
handsCwd: handsCwdRef?.current,
|
|
@@ -4121,7 +4186,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4121
4186
|
const mintedAt = Date.now();
|
|
4122
4187
|
const cp = {
|
|
4123
4188
|
token,
|
|
4124
|
-
version: f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
|
|
4189
|
+
version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : resourceLedgerOut.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : RESOURCE_CHECKPOINT_VERSION,
|
|
4125
4190
|
scope,
|
|
4126
4191
|
sessionId,
|
|
4127
4192
|
leafId,
|
|
@@ -4203,7 +4268,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4203
4268
|
const mintedAt = Date.now();
|
|
4204
4269
|
const cp = {
|
|
4205
4270
|
token,
|
|
4206
|
-
version: f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4271
|
+
version: faceCheckpointState() ? FACE_CHECKPOINT_VERSION : f012CheckpointState() ? F012_CHECKPOINT_VERSION : orgAdmissionCheckpointState() ? ORG_ADMISSION_CHECKPOINT_VERSION : reviewLedger.totalTokens !== undefined ? TOKEN_CHECKPOINT_VERSION : BINDING_CHECKPOINT_VERSION,
|
|
4207
4272
|
scope,
|
|
4208
4273
|
sessionId,
|
|
4209
4274
|
leafId,
|
|
@@ -4455,11 +4520,14 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4455
4520
|
const scope = durableApproval?.scope || checkpointScopeOf({ principal: spec.principal });
|
|
4456
4521
|
const ttlMs = sanitizedTtlMs(durableApproval?.ttlMs);
|
|
4457
4522
|
const checkpointState = serializeCheckpointState(remoteHandle, inFlightSpendMicroUsd());
|
|
4523
|
+
if (realApproval !== undefined && checkpointState.readFace !== undefined) {
|
|
4524
|
+
checkpointState.readFace = { ...checkpointState.readFace, realApproval: true };
|
|
4525
|
+
}
|
|
4458
4526
|
const approvalLedger = debitLedger(priorLedger, liveSpendRef.get?.() ?? { costMicroUsd: 0, tokens: 0, turns: 0, walltimeMs: 0 }, resourceTotal, { countSlice: false });
|
|
4459
4527
|
const mintedAt = Date.now();
|
|
4460
4528
|
cp = {
|
|
4461
4529
|
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,
|
|
4530
|
+
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
4531
|
scope,
|
|
4464
4532
|
sessionId,
|
|
4465
4533
|
leafId,
|
|
@@ -4809,21 +4877,42 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4809
4877
|
promptManifest.lowering = turnSnapshot.lowering;
|
|
4810
4878
|
const promptOverheadTokens = Math.ceil((systemPrompt.length +
|
|
4811
4879
|
harnessTools.reduce((a, t) => a + t.name.length + (t.description?.length ?? 0) + JSON.stringify(t.parameters ?? {}).length, 0)) / charsPerToken);
|
|
4880
|
+
const announceAttachmentSkip = (path, reason) => {
|
|
4881
|
+
deps.onError?.(new Error(`working-file attachment skipped — ${path}: ${reason}`), {
|
|
4882
|
+
phase: "degraded",
|
|
4883
|
+
sessionId,
|
|
4884
|
+
classification: "attachment-read",
|
|
4885
|
+
});
|
|
4886
|
+
};
|
|
4812
4887
|
const readTaskFile = handsEnabled
|
|
4813
4888
|
? async (path) => {
|
|
4814
4889
|
try {
|
|
4890
|
+
let key = path;
|
|
4815
4891
|
if (attachmentRootCanonical !== undefined) {
|
|
4816
|
-
const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, undefined, additionalRootsCanonical);
|
|
4817
|
-
if (!rk.ok)
|
|
4892
|
+
const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, undefined, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, readDenyMatcher, resolvedReadFace);
|
|
4893
|
+
if (!rk.ok) {
|
|
4894
|
+
announceAttachmentSkip(path, rk.violation.message);
|
|
4895
|
+
if (rk.violation.code === "read_path_denied" && rk.violation.pattern !== undefined) {
|
|
4896
|
+
return { withheld: { pattern: rk.violation.pattern } };
|
|
4897
|
+
}
|
|
4818
4898
|
return null;
|
|
4899
|
+
}
|
|
4900
|
+
key = rk.key;
|
|
4819
4901
|
}
|
|
4820
|
-
const r = await executionEnv.readBinaryFile(
|
|
4821
|
-
if (!r.ok)
|
|
4902
|
+
const r = await executionEnv.readBinaryFile(key);
|
|
4903
|
+
if (!r.ok) {
|
|
4904
|
+
announceAttachmentSkip(path, r.error.message);
|
|
4822
4905
|
return null;
|
|
4906
|
+
}
|
|
4823
4907
|
const d = decodeTextBytes(r.value);
|
|
4824
|
-
|
|
4908
|
+
if (d.malformed) {
|
|
4909
|
+
announceAttachmentSkip(path, "malformed text encoding (truncated UTF-16 body)");
|
|
4910
|
+
return null;
|
|
4911
|
+
}
|
|
4912
|
+
return d.text;
|
|
4825
4913
|
}
|
|
4826
|
-
catch {
|
|
4914
|
+
catch (err) {
|
|
4915
|
+
announceAttachmentSkip(path, err instanceof Error ? err.message : String(err));
|
|
4827
4916
|
return null;
|
|
4828
4917
|
}
|
|
4829
4918
|
}
|
|
@@ -4833,7 +4922,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
|
|
|
4833
4922
|
if (attachmentRootCanonical === undefined)
|
|
4834
4923
|
return path;
|
|
4835
4924
|
try {
|
|
4836
|
-
const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, handsCwdRef?.current, additionalRootsCanonical);
|
|
4925
|
+
const rk = await resolveKey(executionEnv, attachmentRootCanonical, path, undefined, handsCwdRef?.current, [...additionalRootsCanonical, ...additionalReadRootsCanonical], undefined, undefined, resolvedReadFace);
|
|
4837
4926
|
return rk.ok ? rk.key : path;
|
|
4838
4927
|
}
|
|
4839
4928
|
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);
|
|
@@ -19,4 +19,7 @@ import { type ContractAssertionRunner } from "./contract-harness.js";
|
|
|
19
19
|
* host-side read face, and an optional parameter one store honors while another drops it silently is
|
|
20
20
|
* exactly the divergence the two legs above exist to prevent.
|
|
21
21
|
*/
|
|
22
|
-
export declare function toolResultStoreContract(make: () => ToolResultStore, runAssertion?: ContractAssertionRunner
|
|
22
|
+
export declare function toolResultStoreContract(make: () => ToolResultStore, runAssertion?: ContractAssertionRunner, onOptionalMember?: (report: {
|
|
23
|
+
member: "deleteBySession";
|
|
24
|
+
status: "verified" | "absent";
|
|
25
|
+
}) => void): Promise<void>;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { strict as assert } from "node:assert";
|
|
2
2
|
import { beginContract } from "./contract-harness.js";
|
|
3
|
-
export async function toolResultStoreContract(make, runAssertion) {
|
|
3
|
+
export async function toolResultStoreContract(make, runAssertion, onOptionalMember) {
|
|
4
4
|
const { run, settle } = beginContract(runAssertion);
|
|
5
5
|
run("put/get full + sliced; write-once no-op; unknown → undefined", async () => {
|
|
6
6
|
const store = make();
|
|
@@ -55,5 +55,30 @@ 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 (probe.deleteBySession === undefined) {
|
|
61
|
+
onOptionalMember?.({ member: "deleteBySession", status: "absent" });
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
assert.equal(typeof probe.deleteBySession, "function", `deleteBySession is present but not callable (got ${typeof probe.deleteBySession}) — a garbage member is a DEFECT, not an optional-member absence`);
|
|
65
|
+
const store = make();
|
|
66
|
+
const del = (sessionId) => Promise.resolve(store.deleteBySession(sessionId));
|
|
67
|
+
await store.put("tr_sweep~a", "A-bytes", { sessionId: "sess-X", taskId: "t1" });
|
|
68
|
+
await store.put("tr_sweep~b", "B-bytes", { sessionId: "sess-X" });
|
|
69
|
+
await store.put("tr_sweep~other", "OTHER-bytes", { sessionId: "sess-Y" });
|
|
70
|
+
await store.put("tr_sweep~unowned", "UNOWNED-bytes");
|
|
71
|
+
const first = await del("sess-X");
|
|
72
|
+
assert.equal(first.deleted, 2, "both rows recorded for the session are deleted (taskId presence does not split the key)");
|
|
73
|
+
assert.equal(await store.get("tr_sweep~a"), undefined, "a swept row is gone");
|
|
74
|
+
assert.equal(await store.get("tr_sweep~b"), undefined);
|
|
75
|
+
assert.equal((await store.get("tr_sweep~other")).content, "OTHER-bytes", "another session's row survives the sweep");
|
|
76
|
+
assert.equal(first.unattributable >= 1, true, "an unowned row is counted unattributable");
|
|
77
|
+
assert.equal((await store.get("tr_sweep~unowned")).content, "UNOWNED-bytes", "an unowned row is never deleted by a session sweep");
|
|
78
|
+
const second = await del("sess-X");
|
|
79
|
+
assert.equal(second.deleted, 0, "a repeated sweep deletes nothing");
|
|
80
|
+
assert.equal((await store.get("tr_sweep~other")).content, "OTHER-bytes");
|
|
81
|
+
onOptionalMember?.({ member: "deleteBySession", status: "verified" });
|
|
82
|
+
});
|
|
58
83
|
await settle();
|
|
59
84
|
}
|
|
@@ -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",
|