@sema-agent/core 7.16.0 → 7.17.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 +173 -0
- package/dist/agents/builtin-agents.d.ts +3 -3
- package/dist/agents/cross-session-envelope.d.ts +0 -1
- package/dist/agents/cross-session-envelope.js +0 -1
- package/dist/agents/peer-notices.d.ts +0 -9
- package/dist/agents/peer-notices.js +0 -63
- package/dist/agents/subagent.js +7 -15
- package/dist/brain/request-params.d.ts +2 -2
- package/dist/config/defaults.d.ts +0 -7
- package/dist/config/defaults.js +0 -2
- package/dist/core/artifact-host.d.ts +218 -0
- package/dist/core/artifact-host.js +92 -0
- package/dist/core/ask-origin.d.ts +29 -3
- package/dist/core/ask-origin.js +17 -9
- package/dist/core/ask-question.d.ts +25 -0
- package/dist/core/ask-question.js +4 -0
- package/dist/core/ask-unresolvable-notice.d.ts +30 -21
- package/dist/core/ask-unresolvable-notice.js +16 -8
- package/dist/core/auto-compaction.d.ts +10 -10
- package/dist/core/auto-mode-arming.d.ts +2 -2
- package/dist/core/background-agent-store.d.ts +5 -5
- package/dist/core/background-shell.d.ts +1 -1
- package/dist/core/checkpoint-execution-record.d.ts +0 -3
- package/dist/core/checkpoint-execution-record.js +0 -4
- package/dist/core/checkpoint-store.d.ts +71 -99
- package/dist/core/checkpoint-store.js +9 -16
- package/dist/core/context-edit.d.ts +36 -104
- package/dist/core/context-edit.js +37 -60
- package/dist/core/effective-path-target.d.ts +68 -8
- package/dist/core/effective-path-target.js +42 -30
- package/dist/core/engine-notice.d.ts +46 -32
- package/dist/core/env-budget.d.ts +31 -0
- package/dist/core/env-budget.js +39 -0
- package/dist/core/exec-output-tail.d.ts +5 -5
- package/dist/core/file-history-store.js +3 -6
- package/dist/core/fs-write-gate-policy.d.ts +1 -1
- package/dist/core/fs-write-gate-policy.js +3 -3
- package/dist/core/gate-lanes.js +9 -15
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hands-band.d.ts +1 -1
- package/dist/core/hooks.d.ts +18 -17
- package/dist/core/lsp-session.d.ts +1 -1
- package/dist/core/lsp.d.ts +1 -1
- package/dist/core/lsp.js +5 -4
- package/dist/core/mcp.d.ts +5 -5
- package/dist/core/mcp.js +4 -48
- package/dist/core/memory-recall.d.ts +0 -8
- package/dist/core/memory.d.ts +1 -1
- package/dist/core/oracle-isolation.js +2 -2
- package/dist/core/parent-spec-seat.d.ts +66 -0
- package/dist/core/parent-spec-seat.js +47 -0
- package/dist/core/permission-rule-model.d.ts +11 -6
- package/dist/core/permission-rule-model.js +53 -37
- package/dist/core/permission-rules.js +8 -3
- package/dist/core/protocol-naming.d.ts +2 -2
- package/dist/core/remote-env.d.ts +1 -1
- package/dist/core/retired-keys.d.ts +19 -0
- package/dist/core/retired-keys.js +15 -0
- package/dist/core/runner/active-skill-scope.js +3 -3
- package/dist/core/runner/assemble-result.d.ts +15 -13
- package/dist/core/runner/assemble-result.js +5 -4
- package/dist/core/runner/compaction-call-options.d.ts +25 -89
- package/dist/core/runner/contracts.d.ts +10 -9
- package/dist/core/runner/gate-exit.js +15 -1
- package/dist/core/runner/halt-attribution.d.ts +35 -0
- package/dist/core/runner/halt-attribution.js +16 -0
- package/dist/core/runner/memory-consolidation.d.ts +0 -2
- package/dist/core/runner/memory-consolidation.js +0 -1
- package/dist/core/runner/prepare-artifact.d.ts +53 -0
- package/dist/core/runner/prepare-artifact.js +77 -0
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
- package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
- package/dist/core/runner/prepare-config-doors.d.ts +3 -3
- package/dist/core/runner/prepare-config-doors.js +5 -2
- package/dist/core/runner/prepare-context-lane.d.ts +2 -2
- package/dist/core/runner/prepare-context-lane.js +2 -13
- package/dist/core/runner/prepare-file-history.d.ts +1 -1
- package/dist/core/runner/prepare-file-history.js +3 -52
- package/dist/core/runner/prepare-hands-readface.d.ts +4 -7
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +5 -2
- package/dist/core/runner/prepare-inherited-gate.js +7 -8
- package/dist/core/runner/prepare-memory.d.ts +4 -6
- package/dist/core/runner/prepare-memory.js +4 -2
- package/dist/core/runner/prepare-policy-chain.d.ts +3 -3
- package/dist/core/runner/prepare-policy-chain.js +15 -30
- package/dist/core/runner/prepare-prompt-assembly.js +1 -1
- package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
- package/dist/core/runner/prepare-suspend-saga.js +0 -2
- package/dist/core/runner/prepare-task.js +10 -9
- package/dist/core/runner/prepare-wiring-manifest.js +9 -7
- package/dist/core/runner/resume-admission.js +2 -1
- package/dist/core/runner/resume-internals-and-config.js +4 -0
- package/dist/core/runner/retired-runner-deps.d.ts +3 -0
- package/dist/core/runner/retired-runner-deps.js +7 -0
- package/dist/core/runner/run-harness-handlers.js +2 -3
- package/dist/core/runner/run-settle-and-teardown.d.ts +1 -1
- package/dist/core/runner/run-settle-and-teardown.js +3 -1
- package/dist/core/runner/session-file-state-replay.js +3 -3
- package/dist/core/runner/session-rule-policy.js +3 -4
- package/dist/core/runner/stream-halt-verbs.js +1 -1
- package/dist/core/runner/stream-steer-verb.js +1 -4
- package/dist/core/runner/workspace-path.js +3 -0
- package/dist/core/runner-deps.d.ts +30 -49
- package/dist/core/safe-notify.d.ts +1 -1
- package/dist/core/sensitive-path-policy.d.ts +1 -1
- package/dist/core/sensitive-path-policy.js +4 -4
- package/dist/core/session.d.ts +2 -2
- package/dist/core/skill-tool-specifier.d.ts +1 -1
- package/dist/core/skills-directory.d.ts +1 -1
- package/dist/core/store-contracts/checkpoint-store-contract.js +1 -1
- package/dist/core/strategy-store.d.ts +8 -11
- package/dist/core/task-event.d.ts +20 -17
- package/dist/core/task-limits.d.ts +1 -1
- package/dist/core/task-notification.d.ts +5 -5
- package/dist/core/task-registry-agent.d.ts +5 -5
- package/dist/core/task-registry-monitor.d.ts +2 -2
- package/dist/core/task-registry-shared.d.ts +27 -27
- package/dist/core/task-registry-shared.js +3 -13
- package/dist/core/task-registry.d.ts +8 -8
- package/dist/core/task-result.d.ts +37 -37
- package/dist/core/task-spec.d.ts +5 -5
- package/dist/core/task-stream.d.ts +16 -11
- package/dist/core/tool-catalog-entries.d.ts +1 -0
- package/dist/core/tool-catalog-entries.js +5 -0
- package/dist/core/tool-conformance.d.ts +0 -2
- package/dist/core/tool-conformance.js +1 -3
- package/dist/core/tool-errors.d.ts +5 -0
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +51 -22
- package/dist/core/tool-policy.js +28 -5
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-store.d.ts +11 -11
- package/dist/core/tool-spec.d.ts +34 -38
- package/dist/core/tools.d.ts +2 -2
- package/dist/core/trace.d.ts +12 -15
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/wiring-manifest.d.ts +4 -3
- package/dist/core/workflow-journal-store.d.ts +11 -11
- package/dist/engine/execution-env/node-execution-env.d.ts +3 -2
- package/dist/engine/execution-env/node-execution-env.js +4 -2
- package/dist/engine/harness/agent-harness.d.ts +47 -4
- package/dist/engine/harness/agent-harness.js +52 -9
- package/dist/engine/loop/agent-loop.js +15 -8
- package/dist/engine/loop/types.d.ts +5 -0
- package/dist/index.d.ts +19 -14
- package/dist/index.js +15 -10
- package/dist/internal/harness.d.ts +1 -0
- package/dist/orchestration/run-spec.d.ts +3 -0
- package/dist/orchestration/run-spec.js +3 -3
- package/dist/orchestration/run-workflow-tool.d.ts +12 -6
- package/dist/orchestration/run-workflow-tool.js +23 -5
- package/dist/orchestration/workflow-primitives.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +1 -4
- package/dist/orchestration/workflow-types.d.ts +34 -4
- package/dist/orchestration/workflow-types.js +2 -0
- package/dist/orchestration/workflow.d.ts +39 -7
- package/dist/orchestration/workflow.js +234 -113
- package/dist/prompt-assembly/assemble.d.ts +2 -2
- package/dist/prompt-assembly/assemble.js +9 -9
- package/dist/prompt-assembly/composer.d.ts +6 -7
- package/dist/prompt-assembly/composer.js +10 -10
- package/dist/prompt-assembly/epoch.js +1 -1
- package/dist/prompt-assembly/event-registry.d.ts +0 -21
- package/dist/prompt-assembly/packs/sema-default.js +30 -30
- package/dist/prompt-assembly/types.d.ts +14 -14
- package/dist/prompts/coordinator.d.ts +0 -13
- package/dist/prompts/coordinator.js +0 -3
- package/dist/stores/file/checkpoint-store.js +1 -5
- package/dist/stores/file/file-history-store.js +2 -2
- package/dist/stores/file/permission-rule-store.d.ts +4 -35
- package/dist/stores/file/permission-rule-store.js +4 -148
- package/dist/tools/artifact/artifact-text.d.ts +62 -0
- package/dist/tools/artifact/artifact-text.js +68 -0
- package/dist/tools/artifact/artifact-tool.d.ts +52 -0
- package/dist/tools/artifact/artifact-tool.js +351 -0
- package/dist/tools/artifact/local-stub.d.ts +26 -0
- package/dist/tools/artifact/local-stub.js +296 -0
- package/dist/tools/fs/bash-readonly-classifier.d.ts +74 -8
- package/dist/tools/fs/bash-readonly-classifier.js +171 -60
- package/dist/tools/fs/fs-bash.d.ts +14 -10
- package/dist/tools/fs/fs-bash.js +19 -27
- package/dist/tools/fs/fs-search-tools.js +5 -2
- package/dist/tools/fs/fs-shared.d.ts +2 -0
- package/dist/tools/fs/fs-shared.js +3 -11
- package/dist/tools/fs/index.d.ts +4 -0
- package/dist/tools/fs/index.js +4 -1
- package/dist/tools/fs/safety.d.ts +117 -1
- package/dist/tools/fs/safety.js +131 -31
- package/dist/tools/fs/search.js +8 -6
- package/package.json +2 -1
- package/test/export-surface.snapshot.json +115 -39
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAbsolutePathForm, isBlockedDevicePath, normalizeAbsPathLexically, withinAnyRoot } from "./safety.js";
|
|
1
|
+
import { isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, pathFamilyOf, withinAnyRoot } from "./safety.js";
|
|
2
2
|
export const NOT_AUTO_ALLOWED = "— not auto-allowed";
|
|
3
3
|
export const BASH_READONLY_DEFAULT_ALLOW = [
|
|
4
4
|
"ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
|
|
@@ -213,7 +213,7 @@ const NO_PATH_OPERAND_COMMANDS = new Set([
|
|
|
213
213
|
function isPathShapedToken(tok) {
|
|
214
214
|
return tok.includes("/") || tok.startsWith("~") || tok === "." || tok === "..";
|
|
215
215
|
}
|
|
216
|
-
function resolveOperandLexically(base, operand, homeDir) {
|
|
216
|
+
export function resolveOperandLexically(base, operand, homeDir) {
|
|
217
217
|
let raw = operand;
|
|
218
218
|
if (raw === "~" || raw.startsWith("~/")) {
|
|
219
219
|
if (homeDir === undefined || !isAbsolutePathForm(homeDir))
|
|
@@ -223,15 +223,33 @@ function resolveOperandLexically(base, operand, homeDir) {
|
|
|
223
223
|
else if (raw.startsWith("~")) {
|
|
224
224
|
return undefined;
|
|
225
225
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
226
|
+
const family = base === undefined ? undefined : pathFamilyOf({ root: base });
|
|
227
|
+
if (isAbsoluteForFamily(family, raw))
|
|
228
|
+
return normalizeAbsPathLexically(nativeUncSpellingOf(family, raw));
|
|
229
|
+
if (base === undefined || family === undefined || !isAbsoluteForFamily(family, base))
|
|
229
230
|
return undefined;
|
|
230
|
-
|
|
231
|
+
if (isShellRootedSpellingUnmapped(family, raw))
|
|
232
|
+
return undefined;
|
|
233
|
+
return normalizeAbsPathLexically(joinForFamily(family, base, raw));
|
|
231
234
|
}
|
|
232
235
|
function tokenizeSegment(segment) {
|
|
233
236
|
const raw = splitWordsQuoteAware(segment);
|
|
234
|
-
return { folded: raw.map(foldQuoteRemovalToken), raw };
|
|
237
|
+
return { folded: raw.map((r) => { const f = foldQuoteRemovalToken(r); return tildeIsLiteral(r) ? literalTildeSpelling(f) : f; }), raw };
|
|
238
|
+
}
|
|
239
|
+
function literalTildeSpelling(word) {
|
|
240
|
+
return word.startsWith("~") ? `./${word}` : word;
|
|
241
|
+
}
|
|
242
|
+
function tildeIsLiteral(raw) {
|
|
243
|
+
if (!raw.startsWith("~"))
|
|
244
|
+
return true;
|
|
245
|
+
for (let i = 1; i < raw.length; i++) {
|
|
246
|
+
const ch = raw[i];
|
|
247
|
+
if (ch === '"' || ch === "'")
|
|
248
|
+
return true;
|
|
249
|
+
if (ch === "/")
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
return false;
|
|
235
253
|
}
|
|
236
254
|
function splitWordsQuoteAware(segment) {
|
|
237
255
|
const s = segment.trim();
|
|
@@ -575,11 +593,12 @@ function grepClusterValueOwner(tok) {
|
|
|
575
593
|
return undefined;
|
|
576
594
|
}
|
|
577
595
|
function attachedOptionPayloads(tok) {
|
|
596
|
+
const literal = literalTildeSpelling;
|
|
578
597
|
const out = [];
|
|
579
598
|
if (tok.startsWith("--")) {
|
|
580
599
|
const eq = tok.indexOf("=");
|
|
581
600
|
if (eq > 0 && eq + 1 < tok.length)
|
|
582
|
-
out.push(tok.slice(eq + 1));
|
|
601
|
+
out.push(literal(tok.slice(eq + 1)));
|
|
583
602
|
return out;
|
|
584
603
|
}
|
|
585
604
|
for (let k = 1; k <= tok.length; k++) {
|
|
@@ -587,7 +606,7 @@ function attachedOptionPayloads(tok) {
|
|
|
587
606
|
break;
|
|
588
607
|
const payload = tok.slice(k);
|
|
589
608
|
if (payload.length > 0)
|
|
590
|
-
out.push(payload);
|
|
609
|
+
out.push(literal(payload));
|
|
591
610
|
}
|
|
592
611
|
return out;
|
|
593
612
|
}
|
|
@@ -600,31 +619,35 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
600
619
|
const candidates = [];
|
|
601
620
|
const bareWordOperands = [];
|
|
602
621
|
const positionalOperands = [];
|
|
622
|
+
let stdinOperand = false;
|
|
603
623
|
const argGlobs = (k) => {
|
|
604
624
|
const raw = tokens.raw[k + 1];
|
|
605
625
|
return raw !== undefined && hasUnquotedGlobMetachar(raw);
|
|
606
626
|
};
|
|
607
|
-
|
|
627
|
+
const expandingArgs = new Set();
|
|
628
|
+
tokens.raw.slice(1).forEach((rawArg, k) => {
|
|
608
629
|
if (!hasUnquotedExpansionMetachar(rawArg))
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
},
|
|
615
|
-
|
|
616
|
-
}
|
|
630
|
+
return;
|
|
631
|
+
expandingArgs.add(k);
|
|
632
|
+
findings.push({
|
|
633
|
+
kind: "unresolvable",
|
|
634
|
+
operand: rawArg,
|
|
635
|
+
reason: `"${name}" is given the argument "${rawArg}", which the shell expands (brace/variable/command expansion) before the command runs — the path it would actually read cannot be resolved statically, so it is not auto-allowed`,
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
if (name === "cd" && expandingArgs.size > 0)
|
|
639
|
+
return findings;
|
|
617
640
|
if (name === "cd") {
|
|
618
641
|
const targetIdx = args.findIndex((t) => !t.startsWith("-") || t === "-");
|
|
619
642
|
const target = targetIdx === -1 ? undefined : args[targetIdx];
|
|
620
643
|
if (target !== undefined && argGlobs(targetIdx)) {
|
|
621
|
-
return [{ kind: "unresolvable", reason: `\`cd\` is given the pattern "${target}", which the shell expands to a directory this check cannot know ${NOT_AUTO_ALLOWED}` }];
|
|
644
|
+
return [{ kind: "unresolvable", operand: target, reason: `\`cd\` is given the pattern "${target}", which the shell expands to a directory this check cannot know ${NOT_AUTO_ALLOWED}` }];
|
|
622
645
|
}
|
|
623
646
|
if (target === undefined) {
|
|
624
|
-
return [{ kind: "unresolvable", reason: '`cd` with no argument targets the home directory, which cannot be checked against the allowed directories ' + NOT_AUTO_ALLOWED }];
|
|
647
|
+
return [{ kind: "unresolvable", operand: "cd", reason: '`cd` with no argument targets the home directory, which cannot be checked against the allowed directories ' + NOT_AUTO_ALLOWED }];
|
|
625
648
|
}
|
|
626
649
|
if (target === "-") {
|
|
627
|
-
return [{ kind: "unresolvable", reason: '`cd -` targets the previous working directory, which cannot be resolved statically ' + NOT_AUTO_ALLOWED }];
|
|
650
|
+
return [{ kind: "unresolvable", operand: "-", reason: '`cd -` targets the previous working directory, which cannot be resolved statically ' + NOT_AUTO_ALLOWED }];
|
|
628
651
|
}
|
|
629
652
|
candidates.push({ text: target, globbed: false });
|
|
630
653
|
}
|
|
@@ -635,6 +658,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
635
658
|
let endOfOptions = false;
|
|
636
659
|
for (let k = 0; k < args.length; k++) {
|
|
637
660
|
const t = args[k];
|
|
661
|
+
if (expandingArgs.has(k))
|
|
662
|
+
continue;
|
|
638
663
|
if (!endOfOptions) {
|
|
639
664
|
if (t === "--") {
|
|
640
665
|
endOfOptions = true;
|
|
@@ -662,8 +687,10 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
662
687
|
if (isGrepPatternSlot)
|
|
663
688
|
continue;
|
|
664
689
|
const m = RECURSIVE_READ_FORMS[name];
|
|
665
|
-
if (m === undefined || m.dashIsStdin === true)
|
|
690
|
+
if (m === undefined || m.dashIsStdin === true) {
|
|
691
|
+
stdinOperand = true;
|
|
666
692
|
continue;
|
|
693
|
+
}
|
|
667
694
|
positionalOperands.push(t);
|
|
668
695
|
continue;
|
|
669
696
|
}
|
|
@@ -677,7 +704,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
677
704
|
}
|
|
678
705
|
}
|
|
679
706
|
for (let k = 0; k < args.length; k++) {
|
|
680
|
-
if (!argGlobs(k))
|
|
707
|
+
if (!argGlobs(k) || expandingArgs.has(k))
|
|
681
708
|
continue;
|
|
682
709
|
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], args[k], boundary.homeDir);
|
|
683
710
|
findings.push({ kind: "undecided", path: resolved ?? args[k] });
|
|
@@ -687,6 +714,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
687
714
|
if (resolved === undefined) {
|
|
688
715
|
findings.push({
|
|
689
716
|
kind: "unresolvable",
|
|
717
|
+
operand: candidate,
|
|
690
718
|
reason: `"${name}" names the path "${candidate}", which cannot be resolved statically ${NOT_AUTO_ALLOWED}`,
|
|
691
719
|
});
|
|
692
720
|
continue;
|
|
@@ -706,7 +734,7 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
706
734
|
findings.push({ kind: "inside", path: resolved });
|
|
707
735
|
}
|
|
708
736
|
if (boundary.denyMatch !== undefined && segmentSelectsRecursiveRead(name, args)) {
|
|
709
|
-
const roots = positionalOperands.length > 0 ? positionalOperands : ["."];
|
|
737
|
+
const roots = positionalOperands.length > 0 ? positionalOperands : stdinOperand ? [] : ["."];
|
|
710
738
|
for (const operand of roots) {
|
|
711
739
|
const resolved = resolveOperandLexically(boundary.cwd ?? boundary.roots[0], operand, boundary.homeDir);
|
|
712
740
|
findings.push({ kind: "recursive", path: resolved ?? operand });
|
|
@@ -714,8 +742,56 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
|
|
|
714
742
|
}
|
|
715
743
|
return findings;
|
|
716
744
|
}
|
|
717
|
-
function
|
|
718
|
-
|
|
745
|
+
export function foldHomeVariableSpelling(command, homeDir) {
|
|
746
|
+
if (homeDir === undefined || !command.includes("$") || /[\\`]/.test(command))
|
|
747
|
+
return command;
|
|
748
|
+
const unquotedIsExact = /^[A-Za-z0-9_./+@:%=,-]+$/.test(homeDir);
|
|
749
|
+
const quotedIsExact = !/["$`\\\n\r]/.test(homeDir);
|
|
750
|
+
const UNQUOTED_HOME_WORD = /^\$(?:HOME|\{HOME\})(?=[/\s;|&"]|$)/;
|
|
751
|
+
const DOUBLE_QUOTED_HOME_WORD = /^\$(?:HOME|\{HOME\})(?=[/"]|$)/;
|
|
752
|
+
let open;
|
|
753
|
+
let wordStart = true;
|
|
754
|
+
let quoteHadContent = false;
|
|
755
|
+
let out = "";
|
|
756
|
+
for (let i = 0; i < command.length; i++) {
|
|
757
|
+
const ch = command[i];
|
|
758
|
+
if (open === undefined && (ch === '"' || ch === "'")) {
|
|
759
|
+
open = ch;
|
|
760
|
+
quoteHadContent = false;
|
|
761
|
+
out += ch;
|
|
762
|
+
continue;
|
|
763
|
+
}
|
|
764
|
+
if (open !== undefined && open === ch) {
|
|
765
|
+
open = undefined;
|
|
766
|
+
wordStart = wordStart && !quoteHadContent;
|
|
767
|
+
out += ch;
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (open !== undefined)
|
|
771
|
+
quoteHadContent = true;
|
|
772
|
+
if (open === undefined && /[\s;|&]/.test(ch)) {
|
|
773
|
+
wordStart = true;
|
|
774
|
+
out += ch;
|
|
775
|
+
continue;
|
|
776
|
+
}
|
|
777
|
+
if (ch === "$" && wordStart && ((open === '"' && quotedIsExact) || (open === undefined && unquotedIsExact))) {
|
|
778
|
+
const m = (open === '"' ? DOUBLE_QUOTED_HOME_WORD : UNQUOTED_HOME_WORD).exec(command.slice(i));
|
|
779
|
+
if (m !== null) {
|
|
780
|
+
wordStart = false;
|
|
781
|
+
if (open === '"')
|
|
782
|
+
quoteHadContent = true;
|
|
783
|
+
out += homeDir;
|
|
784
|
+
i += m[0].length - 1;
|
|
785
|
+
continue;
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
wordStart = false;
|
|
789
|
+
out += ch;
|
|
790
|
+
}
|
|
791
|
+
return open === undefined ? out : command;
|
|
792
|
+
}
|
|
793
|
+
function segmentCompoundForReadonly(command, homeDir) {
|
|
794
|
+
const trimmed = foldHomeVariableSpelling(command, homeDir).trim();
|
|
719
795
|
if (!trimmed)
|
|
720
796
|
return { reject: "empty command" };
|
|
721
797
|
const redirectionStripped = stripExemptRedirections(trimmed);
|
|
@@ -725,7 +801,7 @@ function segmentCompoundForReadonly(command) {
|
|
|
725
801
|
return splitShellCompoundSegments(terminatorStripped, { trailingTerminator: "keep" });
|
|
726
802
|
}
|
|
727
803
|
export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts) {
|
|
728
|
-
const split = segmentCompoundForReadonly(command);
|
|
804
|
+
const split = segmentCompoundForReadonly(command, boundary?.homeDir);
|
|
729
805
|
if ("reject" in split)
|
|
730
806
|
return { reason: split.reject };
|
|
731
807
|
const { segments, pipeFed } = split;
|
|
@@ -764,6 +840,7 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
764
840
|
};
|
|
765
841
|
const STDIN_FILE_FLOOR = { cat: 1, grep: 2, head: 1, tail: 1, wc: 1, cut: 1, tr: Infinity, diff: 2, cmp: 2, comm: 2, sed: 2 };
|
|
766
842
|
const foldedSegments = [];
|
|
843
|
+
let shapeRefusal;
|
|
767
844
|
for (let si = 0; si < segments.length; si++) {
|
|
768
845
|
const segmentTokens = tokenizeSegment(segments[si]);
|
|
769
846
|
const toks = segmentTokens.folded;
|
|
@@ -772,8 +849,10 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
772
849
|
foldedSegments.push(segmentTokens);
|
|
773
850
|
const name = toks[0];
|
|
774
851
|
const guardReason = guardedVerbFormReason(name, toks, segmentTokens.raw);
|
|
775
|
-
if (guardReason !== undefined)
|
|
776
|
-
|
|
852
|
+
if (guardReason !== undefined) {
|
|
853
|
+
shapeRefusal ??= guardReason;
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
777
856
|
if (!(pipeFed[si] ?? false)) {
|
|
778
857
|
const floor = STDIN_FILE_FLOOR[name];
|
|
779
858
|
const restArgs = toks.slice(1);
|
|
@@ -815,14 +894,17 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
815
894
|
}
|
|
816
895
|
}
|
|
817
896
|
if (floor !== undefined && hasStdinDash) {
|
|
818
|
-
|
|
897
|
+
shapeRefusal ??= `"${name}" reads stdin via an explicit "-" argument and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
|
|
898
|
+
continue;
|
|
819
899
|
}
|
|
820
900
|
if (floor !== undefined && operandCount < floor) {
|
|
821
|
-
|
|
901
|
+
shapeRefusal ??= `"${name}" with no file argument reads stdin and would block until the tool timeout ${NOT_AUTO_ALLOWED}`;
|
|
902
|
+
continue;
|
|
822
903
|
}
|
|
823
904
|
}
|
|
824
905
|
if (name === "tail" && toks.slice(1).some((t) => t === "--follow" || t.startsWith("--follow=") || /^[-+][^\s]*[fF]/.test(t))) {
|
|
825
|
-
|
|
906
|
+
shapeRefusal ??= "`tail` in follow mode never terminates " + NOT_AUTO_ALLOWED;
|
|
907
|
+
continue;
|
|
826
908
|
}
|
|
827
909
|
const GENERATOR_DEVICES = new Set(["/dev/zero", "/dev/random", "/dev/urandom", "/dev/full"]);
|
|
828
910
|
const deviceCandidates = [];
|
|
@@ -833,20 +915,26 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
833
915
|
}
|
|
834
916
|
deviceCandidates.push(t);
|
|
835
917
|
}
|
|
836
|
-
const deviceArgs = deviceCandidates.map(normalizeAbsPathLexically).filter(isBlockedDevicePath);
|
|
918
|
+
const deviceArgs = deviceCandidates.map((t) => resolveOperandLexically(undefined, t, boundary?.homeDir) ?? normalizeAbsPathLexically(t)).filter(isBlockedDevicePath);
|
|
837
919
|
const rescuedByHead = headBoundIsSmall(name, toks) && deviceArgs.every((d) => GENERATOR_DEVICES.has(d));
|
|
838
920
|
if (!rescuedByHead && deviceArgs.length > 0) {
|
|
839
|
-
|
|
921
|
+
shapeRefusal ??= "reads a device/special file that is either unbounded (/dev/zero, /dev/stdin, /proc/<pid>/fd/0, … — blocks the pipeline until the tool timeout) or process-private (/proc/<pid>/environ, /proc/<pid>/mem, …) " + NOT_AUTO_ALLOWED;
|
|
840
922
|
}
|
|
841
923
|
}
|
|
842
924
|
if (opts?.iterated !== true) {
|
|
843
925
|
const cdSegments = foldedSegments.filter((st) => st.folded[0] === "cd");
|
|
844
|
-
if (cdSegments.length > 1)
|
|
845
|
-
|
|
846
|
-
|
|
926
|
+
if (cdSegments.length > 1)
|
|
927
|
+
shapeRefusal ??= `multiple directory changes in one command require approval for clarity ${NOT_AUTO_ALLOWED}`;
|
|
928
|
+
}
|
|
929
|
+
if (boundary !== undefined) {
|
|
930
|
+
const walk = evaluateReadBoundary(foldedSegments, boundary);
|
|
931
|
+
if (shapeRefusal === undefined)
|
|
932
|
+
return walk;
|
|
933
|
+
const { reason: _walkSentence, ...structure } = walk;
|
|
934
|
+
return { ...structure, reason: shapeRefusal };
|
|
847
935
|
}
|
|
848
|
-
if (
|
|
849
|
-
return
|
|
936
|
+
if (shapeRefusal !== undefined)
|
|
937
|
+
return { reason: shapeRefusal };
|
|
850
938
|
if (foldedSegments.some((st) => st.folded[0] === "cd")) {
|
|
851
939
|
return { reason: `\`cd\` cannot be judged without a read boundary (no base to resolve the directory change against) ${NOT_AUTO_ALLOWED}` };
|
|
852
940
|
}
|
|
@@ -855,14 +943,25 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
|
|
|
855
943
|
function evaluateReadBoundary(foldedSegments, boundary) {
|
|
856
944
|
const outside = [];
|
|
857
945
|
const inside = [];
|
|
946
|
+
let unresolvable;
|
|
947
|
+
const unresolved = [];
|
|
948
|
+
let baseLost = false;
|
|
858
949
|
const undecided = [];
|
|
859
950
|
const recursive = [];
|
|
860
951
|
let currentBase = boundary.cwd ?? boundary.roots[0];
|
|
861
952
|
for (const toks of foldedSegments) {
|
|
953
|
+
if (baseLost)
|
|
954
|
+
break;
|
|
862
955
|
const segBoundary = currentBase === (boundary.cwd ?? boundary.roots[0]) ? boundary : { ...boundary, cwd: currentBase };
|
|
863
956
|
for (const finding of collectSegmentBoundaryFindings(toks, segBoundary)) {
|
|
864
|
-
if (finding.kind === "unresolvable")
|
|
865
|
-
|
|
957
|
+
if (finding.kind === "unresolvable") {
|
|
958
|
+
unresolvable ??= finding.reason;
|
|
959
|
+
if (!unresolved.includes(finding.operand))
|
|
960
|
+
unresolved.push(finding.operand);
|
|
961
|
+
if (toks.folded[0] === "cd")
|
|
962
|
+
baseLost = true;
|
|
963
|
+
continue;
|
|
964
|
+
}
|
|
866
965
|
const denied = boundary.denyMatch?.(finding.path);
|
|
867
966
|
if (denied != null) {
|
|
868
967
|
return {
|
|
@@ -893,7 +992,7 @@ function evaluateReadBoundary(foldedSegments, boundary) {
|
|
|
893
992
|
if (!outside.some((o) => o.path === finding.path))
|
|
894
993
|
outside.push(finding);
|
|
895
994
|
}
|
|
896
|
-
if (toks.folded[0] === "cd") {
|
|
995
|
+
if (!baseLost && toks.folded[0] === "cd") {
|
|
897
996
|
const cdArgs = toks.folded.slice(1);
|
|
898
997
|
const cdTargetIdx = cdArgs.findIndex((t) => !t.startsWith("-") || t === "-");
|
|
899
998
|
const cdTarget = cdTargetIdx === -1 ? undefined : cdArgs[cdTargetIdx];
|
|
@@ -907,6 +1006,15 @@ function evaluateReadBoundary(foldedSegments, boundary) {
|
|
|
907
1006
|
...(undecidedAll.length > 0 ? { undecidedPaths: undecidedAll } : {}),
|
|
908
1007
|
...(recursive.length > 0 ? { recursiveReadPaths: recursive } : {}),
|
|
909
1008
|
};
|
|
1009
|
+
if (unresolvable !== undefined) {
|
|
1010
|
+
return {
|
|
1011
|
+
reason: unresolvable,
|
|
1012
|
+
unresolvedOperands: unresolved,
|
|
1013
|
+
...(outside.length > 0 ? { outOfRootRead: true, outOfRootPaths: outside.map((o) => o.path) } : {}),
|
|
1014
|
+
...(inside.length > 0 ? { checkedPaths: inside } : {}),
|
|
1015
|
+
...undecidedField,
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
910
1018
|
if (outside.length === 0)
|
|
911
1019
|
return inside.length > 0 ? { checkedPaths: inside, ...undecidedField } : { ...undecidedField };
|
|
912
1020
|
const paths = outside.map((o) => `"${o.path}"`).join(", ");
|
|
@@ -948,7 +1056,7 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
|
|
|
948
1056
|
...(boundary.face !== undefined ? { face: boundary.face } : {}),
|
|
949
1057
|
};
|
|
950
1058
|
const judge = (text) => {
|
|
951
|
-
const split = segmentCompoundForReadonly(text);
|
|
1059
|
+
const split = segmentCompoundForReadonly(text, containmentOnly.homeDir);
|
|
952
1060
|
if ("reject" in split)
|
|
953
1061
|
return NOT_GATED;
|
|
954
1062
|
const readers = [];
|
|
@@ -1028,53 +1136,56 @@ function pollLoopSleepReason(segment) {
|
|
|
1028
1136
|
return undefined;
|
|
1029
1137
|
}
|
|
1030
1138
|
export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
|
|
1139
|
+
return classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary).reason;
|
|
1140
|
+
}
|
|
1141
|
+
export function classifyBoundedReadonlyPollLoopDetailed(command, allow, boundary) {
|
|
1031
1142
|
if (NON_ASCII_WHITESPACE.test(command)) {
|
|
1032
|
-
return "the command contains a non-ASCII or control whitespace character (only space and tab are allowed) — its word boundaries cannot be read the way bash would split them, so it is not auto-allowed";
|
|
1143
|
+
return { reason: "the command contains a non-ASCII or control whitespace character (only space and tab are allowed) — its word boundaries cannot be read the way bash would split them, so it is not auto-allowed" };
|
|
1033
1144
|
}
|
|
1034
|
-
const trimmed = command.trim();
|
|
1145
|
+
const trimmed = foldHomeVariableSpelling(command, boundary?.homeDir).trim();
|
|
1035
1146
|
if (!trimmed)
|
|
1036
|
-
return "empty command";
|
|
1147
|
+
return { reason: "empty command" };
|
|
1037
1148
|
if (!pollLoopQuotesBalanced(trimmed)) {
|
|
1038
|
-
return "unbalanced quote — the command's structure cannot be read reliably, so it is not auto-allowed";
|
|
1149
|
+
return { reason: "unbalanced quote — the command's structure cannot be read reliably, so it is not auto-allowed" };
|
|
1039
1150
|
}
|
|
1040
1151
|
const m = POLL_LOOP_SHAPE.exec(trimmed);
|
|
1041
1152
|
if (m === null) {
|
|
1042
|
-
return "not a bounded read-only poll loop (`for <v> in <literal bound>; do <read commands>; done` is the only accepted control structure)";
|
|
1153
|
+
return { reason: "not a bounded read-only poll loop (`for <v> in <literal bound>; do <read commands>; done` is the only accepted control structure)" };
|
|
1043
1154
|
}
|
|
1044
1155
|
const varName = m[1];
|
|
1045
1156
|
const iterable = m[2];
|
|
1046
1157
|
const body = m[3];
|
|
1047
1158
|
if (!/^[a-z]$/.test(varName)) {
|
|
1048
|
-
return `the loop variable "${varName}" must be a single lowercase letter — a name like PATH/IFS/LD_PRELOAD would change how the body's commands resolve
|
|
1159
|
+
return { reason: `the loop variable "${varName}" must be a single lowercase letter — a name like PATH/IFS/LD_PRELOAD would change how the body's commands resolve` };
|
|
1049
1160
|
}
|
|
1050
1161
|
const beats = pollLoopBeatsOf(iterable);
|
|
1051
1162
|
if (beats === undefined) {
|
|
1052
|
-
return `the loop bound "${iterable}" is not a recognized literal — only \`$(seq <int> <int>)\`, \`{<int>..<int>}\` (ascending) or a literal word list is accepted
|
|
1163
|
+
return { reason: `the loop bound "${iterable}" is not a recognized literal — only \`$(seq <int> <int>)\`, \`{<int>..<int>}\` (ascending) or a literal word list is accepted` };
|
|
1053
1164
|
}
|
|
1054
1165
|
if (beats > POLL_LOOP_MAX_BEATS) {
|
|
1055
|
-
return `the loop runs ${beats} iterations, above the ${POLL_LOOP_MAX_BEATS}-iteration cap for auto-allow
|
|
1166
|
+
return { reason: `the loop runs ${beats} iterations, above the ${POLL_LOOP_MAX_BEATS}-iteration cap for auto-allow` };
|
|
1056
1167
|
}
|
|
1057
1168
|
if (POLL_BODY_HARD_REJECT.test(body)) {
|
|
1058
|
-
return "the loop body may not contain redirection, pipes, backgrounding, substitution, subshells, braces, escapes, or line breaks";
|
|
1169
|
+
return { reason: "the loop body may not contain redirection, pipes, backgrounding, substitution, subshells, braces, escapes, or line breaks" };
|
|
1059
1170
|
}
|
|
1060
1171
|
const readSegments = [];
|
|
1061
1172
|
for (const rawSegment of body.split(";")) {
|
|
1062
1173
|
const segment = rawSegment.trim();
|
|
1063
1174
|
if (segment.length === 0)
|
|
1064
|
-
return "empty command in the loop body";
|
|
1175
|
+
return { reason: "empty command in the loop body" };
|
|
1065
1176
|
const parsed = parseLeadingCommandName(segment);
|
|
1066
1177
|
if ("reject" in parsed)
|
|
1067
|
-
return parsed.reject;
|
|
1178
|
+
return { reason: parsed.reject };
|
|
1068
1179
|
if (parsed.name === "sleep") {
|
|
1069
1180
|
const sleepReason = pollLoopSleepReason(segment);
|
|
1070
1181
|
if (sleepReason !== undefined)
|
|
1071
|
-
return sleepReason;
|
|
1182
|
+
return { reason: sleepReason };
|
|
1072
1183
|
continue;
|
|
1073
1184
|
}
|
|
1074
1185
|
readSegments.push(segment);
|
|
1075
1186
|
}
|
|
1076
1187
|
if (readSegments.length === 0) {
|
|
1077
|
-
return "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed";
|
|
1188
|
+
return { reason: "the loop body has no read command — a sleep-only loop observes nothing and is not auto-allowed" };
|
|
1078
1189
|
}
|
|
1079
1190
|
const bodyHasCd = readSegments.some((seg) => {
|
|
1080
1191
|
const p = parseLeadingCommandName(seg);
|
|
@@ -1083,14 +1194,14 @@ export function classifyBoundedReadonlyPollLoop(command, allow, boundary) {
|
|
|
1083
1194
|
const modelled = bodyHasCd ? Array.from({ length: beats }, () => readSegments.join("; ")).join("; ") : readSegments.join("; ");
|
|
1084
1195
|
const verdict = classifyCompoundReadonlyDetailed(modelled, allow, boundary, { iterated: bodyHasCd });
|
|
1085
1196
|
if (verdict.reason !== undefined)
|
|
1086
|
-
return verdict
|
|
1197
|
+
return verdict;
|
|
1087
1198
|
if (verdict.recursiveReadPaths !== undefined) {
|
|
1088
1199
|
const roots = verdict.recursiveReadPaths;
|
|
1089
1200
|
const shown = roots.slice(0, POLL_LOOP_CAUSE_MAX_PATHS);
|
|
1090
|
-
return `this lexical check cannot bound what a recursive read form in the loop body traverses — ${roots.length} path(s) taken as traversal roots: ${shown.join(", ")}${roots.length > shown.length ? `, and ${roots.length - shown.length} more` : ""}
|
|
1201
|
+
return { ...verdict, reason: `this lexical check cannot bound what a recursive read form in the loop body traverses — ${roots.length} path(s) taken as traversal roots: ${shown.join(", ")}${roots.length > shown.length ? `, and ${roots.length - shown.length} more` : ""}` };
|
|
1091
1202
|
}
|
|
1092
1203
|
if (verdict.undecidedPaths !== undefined) {
|
|
1093
|
-
return `the loop body carries an unexpanded glob (${verdict.undecidedPaths.join(", ")}) — what a REPEATED read touches is decided at run time, so it is not auto-allowed
|
|
1204
|
+
return { ...verdict, reason: `the loop body carries an unexpanded glob (${verdict.undecidedPaths.join(", ")}) — what a REPEATED read touches is decided at run time, so it is not auto-allowed` };
|
|
1094
1205
|
}
|
|
1095
|
-
return
|
|
1206
|
+
return verdict;
|
|
1096
1207
|
}
|
|
@@ -73,24 +73,28 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
|
|
|
73
73
|
* · the boundary DECLARED an objection — a deny-listed operand, a recursive walk under the deny judge, an
|
|
74
74
|
* operand outside the roots — ⇒ `{ reversible: false, mandated: true }` (+ the recursive-reach `cause`);
|
|
75
75
|
* · the boundary could NOT READ where the command reads — an unexpanded glob operand (the compound face
|
|
76
|
-
* reports it as undecided), a `~`-rooted operand
|
|
77
|
-
* cannot model — ⇒ `{ reversible: false }` (fail-closed; a plain ask, which a stored allow rule
|
|
78
|
-
* the seat did not declare anything, it declined to guess)
|
|
76
|
+
* reports it as undecided), a `~`-rooted operand under an adapter that declares no home, a working-directory
|
|
77
|
+
* move the walk cannot model — ⇒ `{ reversible: false }` (fail-closed; a plain ask, which a stored allow rule
|
|
78
|
+
* may retire — the seat did not declare anything, it declined to guess). The engine wires the environment's
|
|
79
|
+
* declared home, so `~/x`, `$HOME/x` and `${HOME}/x` RESOLVE and are judged like the absolute spelling;
|
|
79
80
|
* · otherwise ⇒ `{ reversible: true }`. This includes every command whose SHAPE the read faces refuse whole
|
|
80
81
|
* (an unlisted program, a redirection, a substitution, a segment that is not a reader): those shapes carry
|
|
81
82
|
* no read-face verdict, so this seat has no question to ask — the residual is the doctrine's, and under
|
|
82
|
-
* `"off"` the doctrine asks nothing.
|
|
83
|
-
*
|
|
83
|
+
* `"off"` the doctrine asks nothing. A listed reader refused for a PER-SEGMENT shape (follow mode, a device,
|
|
84
|
+
* a blocking stdin read) is different: the compound face still walks its operands and carries the boundary's
|
|
85
|
+
* structural findings beside the sentence, so `tail -f <denied>` is the declared ask `cat <denied>` is.
|
|
86
|
+
* `run_in_background` is likewise not this seat's question (it changes WHEN the command runs, never WHERE it
|
|
87
|
+
* reads).
|
|
84
88
|
*
|
|
85
89
|
* Reach = the read faces' reach, and nothing wider: the compound face walks the operands of a command whose
|
|
86
90
|
* every segment is a listed reader, and the bounded poll-loop face walks a loop body of listed readers. A
|
|
87
|
-
* read hidden in a shape the faces refuse whole (`cat x | <unlisted>`, `$(cat x)`, `cat $DIR/x
|
|
88
|
-
* `env cat x`, a path-prefixed `/bin/cat x`) is not
|
|
91
|
+
* read hidden in a shape the faces refuse whole (`cat x | <unlisted>`, `$(cat x)`, `cat $DIR/x` — a variable other
|
|
92
|
+
* than the home's own deterministic spellings — a launcher `env cat x`, a path-prefixed `/bin/cat x`) is not
|
|
93
|
+
* seen here — deny-listed or out-of-root alike — exactly
|
|
89
94
|
* the shapes the `"classify"` doctrine exists to ask about. The text-level containment gate the classify
|
|
90
95
|
* seat consults on its demotion arms is deliberately not consulted here ({@link boundaryDeclared}: it may
|
|
91
|
-
* add a mandate to an ask, never originate one). The poll-loop face
|
|
92
|
-
*
|
|
93
|
-
* ({@link pollLoopBoundaryVerdict}).
|
|
96
|
+
* add a mandate to an ask, never originate one). The poll-loop face's structured verdict
|
|
97
|
+
* ({@link classifyBoundedReadonlyPollLoopDetailed}) is read the same three ways as the compound face's.
|
|
94
98
|
*
|
|
95
99
|
* The reader set is the engine's classify set ({@link BASH_CLASSIFY_DEFAULT_ALLOW}) — here it is NOT an
|
|
96
100
|
* auto-allow list but the set of programs whose operands the faces read; a wider set means more reads are
|
package/dist/tools/fs/fs-bash.js
CHANGED
|
@@ -11,7 +11,7 @@ import { isRemoteExecutionEnv, hasDestroy, isIsolated } from "../../core/remote-
|
|
|
11
11
|
import { ghRateLimitHint } from "./gh-rate-limit.js";
|
|
12
12
|
import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
|
|
13
13
|
import { PROBE_CAUSE_PATH_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
|
|
14
|
-
import { BASH_CLASSIFY_DEFAULT_ALLOW, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck,
|
|
14
|
+
import { BASH_CLASSIFY_DEFAULT_ALLOW, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
|
|
15
15
|
import { toolFace } from "../../core/tool-catalog-entries.js";
|
|
16
16
|
const RECURSIVE_CAUSE_MAX_PATHS = 3;
|
|
17
17
|
const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
|
|
@@ -36,7 +36,7 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
36
36
|
if (backgrounded)
|
|
37
37
|
return { reversible: false, ...boundaryGate(detailed) };
|
|
38
38
|
if (detailed.reason === undefined) {
|
|
39
|
-
if (detailed
|
|
39
|
+
if (boundaryUnread(detailed)) {
|
|
40
40
|
const cause = recursiveReadCause(detailed);
|
|
41
41
|
if (cause !== undefined)
|
|
42
42
|
return { reversible: false, ...boundaryGate(detailed), cause };
|
|
@@ -44,23 +44,16 @@ export function bashReversibilityProbe(allow, boundary) {
|
|
|
44
44
|
}
|
|
45
45
|
return { reversible: true };
|
|
46
46
|
}
|
|
47
|
-
const
|
|
48
|
-
if (
|
|
47
|
+
const loop = classifyBoundedReadonlyPollLoopDetailed(command, allowSet, resolved);
|
|
48
|
+
if (loop.reason === undefined)
|
|
49
49
|
return { reversible: true };
|
|
50
|
-
|
|
50
|
+
if (boundaryDeclared(loop))
|
|
51
|
+
return { reversible: false, mandated: true };
|
|
52
|
+
return { reversible: false, ...boundaryGate(detailed) };
|
|
51
53
|
};
|
|
52
54
|
}
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
return "shape";
|
|
56
|
-
const { denyMatch: _deny, ...withoutDenyJudge } = resolved;
|
|
57
|
-
const opened = face({ ...withoutDenyJudge, face: "open" });
|
|
58
|
-
if (opened === undefined || opened !== fullRefusal)
|
|
59
|
-
return "declared";
|
|
60
|
-
return face(undefined) === opened ? "shape" : "unread";
|
|
61
|
-
}
|
|
62
|
-
function pollLoopBoundaryVerdict(command, allowSet, resolved, fullRefusal) {
|
|
63
|
-
return attributeRefusal((b) => classifyBoundedReadonlyPollLoop(command, allowSet, b), resolved, fullRefusal);
|
|
55
|
+
function boundaryUnread(verdict) {
|
|
56
|
+
return (verdict.undecidedPaths !== undefined && verdict.undecidedPaths.length > 0) || (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0);
|
|
64
57
|
}
|
|
65
58
|
function boundaryDeclared(verdict) {
|
|
66
59
|
return denyJudgeSpoke(verdict) || verdict.outOfRootRead === true;
|
|
@@ -106,19 +99,18 @@ export function bashReadBoundaryProbe(boundary) {
|
|
|
106
99
|
const cause = recursiveReadCause(detailed);
|
|
107
100
|
return { reversible: false, mandated: true, ...(cause !== undefined ? { cause } : {}) };
|
|
108
101
|
}
|
|
109
|
-
if (detailed
|
|
110
|
-
return detailed.undecidedPaths !== undefined && detailed.undecidedPaths.length > 0 ? { reversible: false } : { reversible: true };
|
|
111
|
-
}
|
|
112
|
-
const compound = attributeRefusal((b) => classifyCompoundReadonlyDetailed(command, allowSet, b).reason, resolved, detailed.reason);
|
|
113
|
-
if (compound === "declared")
|
|
114
|
-
return { reversible: false, mandated: true };
|
|
115
|
-
if (compound === "unread")
|
|
102
|
+
if (boundaryUnread(detailed))
|
|
116
103
|
return { reversible: false };
|
|
117
|
-
|
|
118
|
-
|
|
104
|
+
if (detailed.reason === undefined)
|
|
105
|
+
return { reversible: true };
|
|
106
|
+
const loop = classifyBoundedReadonlyPollLoopDetailed(command, allowSet, resolved);
|
|
107
|
+
if (loop.reason === undefined)
|
|
119
108
|
return { reversible: true };
|
|
120
|
-
|
|
121
|
-
|
|
109
|
+
if (boundaryDeclared(loop))
|
|
110
|
+
return { reversible: false, mandated: true };
|
|
111
|
+
if (boundaryUnread(loop))
|
|
112
|
+
return { reversible: false };
|
|
113
|
+
return { reversible: true };
|
|
122
114
|
};
|
|
123
115
|
}
|
|
124
116
|
export { FULL_SHELL_CONTRACT_ID } from "../../core/tool-catalog-entries.js";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Type } from "typebox";
|
|
2
2
|
import { defineTool, errorResult } from "../../core/tools.js";
|
|
3
|
-
import { resolveKey, violationText, violationDetails } from "./safety.js";
|
|
3
|
+
import { clipSpelling, pathFamilyOf, resolveKey, violationText, violationDetails, WIN32_NAMESPACE_REFUSAL_TEXT } from "./safety.js";
|
|
4
4
|
import { runGrepDetailed, runGlobDetailed, invalidGlobTokens } from "./search.js";
|
|
5
5
|
import { toolFace } from "../../core/tool-catalog-entries.js";
|
|
6
6
|
import { pathTargetBaseOf, splitAbsoluteGlobPattern } from "../../core/effective-path-target.js";
|
|
@@ -205,7 +205,10 @@ export function createGlobTool(env, rootCanonical, additionalRoots, readDeny, re
|
|
|
205
205
|
let path = declaredTarget === undefined ? undefined : pathTargetValue(args, declaredTarget);
|
|
206
206
|
const { max_results } = args;
|
|
207
207
|
if (path === undefined) {
|
|
208
|
-
const split = splitAbsoluteGlobPattern(pattern);
|
|
208
|
+
const split = splitAbsoluteGlobPattern(pattern, pathFamilyOf({ root: rootCanonical }));
|
|
209
|
+
if (split !== undefined && "refused" in split) {
|
|
210
|
+
return errorResult(`Error (Glob): pattern "${clipSpelling(pattern)}" ${WIN32_NAMESPACE_REFUSAL_TEXT[split.refused]}; refused (${split.refused}).`);
|
|
211
|
+
}
|
|
209
212
|
if (split) {
|
|
210
213
|
path = split.baseDir;
|
|
211
214
|
pattern = split.relativePattern;
|
|
@@ -273,6 +273,8 @@ export declare function bashTimeoutCapsSec(caps: {
|
|
|
273
273
|
defaultSec: number;
|
|
274
274
|
maxSec: number;
|
|
275
275
|
};
|
|
276
|
+
/** Read per call (a deployment steers it without a restart); the one shared resolver holds the grammar,
|
|
277
|
+
* the `[OUTPUT_BUDGET_MIN_CHARS, ceiling]` range and the loud outlet for a value it could not honor. */
|
|
276
278
|
export declare function bashMaxOutputChars(): number;
|
|
277
279
|
export declare const FILE_PATH_PARAMS: {
|
|
278
280
|
file_path: Type.TString;
|