@sema-agent/core 7.15.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 +202 -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 +4 -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-engine/delegation-settlement.d.ts +37 -12
- package/dist/core/memory-engine/delegation-settlement.js +128 -39
- package/dist/core/memory-engine/engine.d.ts +9 -8
- package/dist/core/memory-engine/engine.js +115 -120
- package/dist/core/memory-engine/file-backend.d.ts +17 -5
- package/dist/core/memory-engine/file-backend.js +78 -34
- package/dist/core/memory-engine/index.d.ts +2 -2
- package/dist/core/memory-engine/index.js +2 -2
- package/dist/core/memory-engine/layout.d.ts +152 -54
- package/dist/core/memory-engine/layout.js +441 -104
- package/dist/core/memory-engine/types.d.ts +8 -3
- package/dist/core/memory-engine/types.js +5 -0
- 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/abort-race.d.ts +3 -3
- package/dist/core/runner/active-skill-scope.d.ts +2 -2
- package/dist/core/runner/active-skill-scope.js +3 -3
- package/dist/core/runner/assemble-result.d.ts +42 -40
- package/dist/core/runner/assemble-result.js +5 -4
- package/dist/core/runner/checkpoint-scope.d.ts +4 -4
- package/dist/core/runner/clock-and-limits.d.ts +3 -3
- package/dist/core/runner/compaction-call-options.d.ts +35 -99
- package/dist/core/runner/compaction-knobs.d.ts +4 -4
- package/dist/core/runner/compaction-seams.d.ts +5 -5
- package/dist/core/runner/contracts.d.ts +232 -233
- package/dist/core/runner/decide-continuation.d.ts +6 -6
- package/dist/core/runner/denial-limit-arms.d.ts +8 -8
- package/dist/core/runner/derived-route-fallback.d.ts +2 -2
- package/dist/core/runner/gate-exit.d.ts +6 -6
- package/dist/core/runner/gate-exit.js +15 -1
- package/dist/core/runner/git-status-frame.d.ts +7 -7
- package/dist/core/runner/halt-attribution.d.ts +35 -0
- package/dist/core/runner/halt-attribution.js +16 -0
- package/dist/core/runner/inherited-ask-grants.d.ts +6 -6
- package/dist/core/runner/memory-capture-optout.d.ts +1 -1
- package/dist/core/runner/memory-consolidation-driver.d.ts +4 -4
- package/dist/core/runner/memory-consolidation.d.ts +6 -8
- package/dist/core/runner/memory-consolidation.js +0 -1
- package/dist/core/runner/park-commit.d.ts +6 -6
- package/dist/core/runner/permission-rule-lanes.d.ts +19 -19
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +4 -4
- package/dist/core/runner/prepare-artifact.d.ts +53 -0
- package/dist/core/runner/prepare-artifact.js +77 -0
- package/dist/core/runner/prepare-ask-lane.d.ts +3 -3
- package/dist/core/runner/prepare-boundary-parks.d.ts +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +9 -7
- package/dist/core/runner/prepare-caps-and-workflow.js +2 -3
- package/dist/core/runner/prepare-config-doors.d.ts +24 -24
- package/dist/core/runner/prepare-config-doors.js +5 -2
- package/dist/core/runner/prepare-context-lane.d.ts +8 -8
- package/dist/core/runner/prepare-context-lane.js +2 -13
- package/dist/core/runner/prepare-defer-classify.d.ts +2 -2
- package/dist/core/runner/prepare-delegation-surface.d.ts +1 -1
- package/dist/core/runner/prepare-file-history.d.ts +3 -3
- package/dist/core/runner/prepare-file-history.js +3 -52
- package/dist/core/runner/prepare-gate-stations.d.ts +5 -5
- package/dist/core/runner/prepare-hands-readface.d.ts +35 -38
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +9 -6
- package/dist/core/runner/prepare-inherited-gate.js +7 -8
- package/dist/core/runner/prepare-listings.d.ts +3 -3
- package/dist/core/runner/prepare-lsp.d.ts +3 -3
- package/dist/core/runner/prepare-memory-engine-session.d.ts +5 -5
- package/dist/core/runner/prepare-memory.d.ts +4 -6
- package/dist/core/runner/prepare-memory.js +10 -9
- package/dist/core/runner/prepare-offload-wrappers.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.d.ts +6 -6
- package/dist/core/runner/prepare-policy-chain.d.ts +7 -7
- 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-prompt-inputs.d.ts +5 -5
- package/dist/core/runner/prepare-protocol-tools.d.ts +1 -1
- package/dist/core/runner/prepare-question-face.d.ts +4 -4
- package/dist/core/runner/prepare-run-refs.d.ts +11 -11
- package/dist/core/runner/prepare-safety-scan.d.ts +2 -2
- package/dist/core/runner/prepare-suspend-saga.d.ts +4 -4
- package/dist/core/runner/prepare-suspend-saga.js +0 -2
- package/dist/core/runner/prepare-task.d.ts +12 -12
- package/dist/core/runner/prepare-task.js +10 -9
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +2 -3
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -4
- package/dist/core/runner/prepare-wiring-manifest.js +9 -7
- package/dist/core/runner/prepare-workspace-restore.d.ts +1 -1
- package/dist/core/runner/remote-env-retry.d.ts +3 -3
- package/dist/core/runner/resume-admission.d.ts +1 -1
- package/dist/core/runner/resume-admission.js +2 -1
- package/dist/core/runner/resume-claim.d.ts +1 -1
- package/dist/core/runner/resume-internals-and-config.d.ts +1 -1
- package/dist/core/runner/resume-internals-and-config.js +4 -0
- package/dist/core/runner/resume-policy-outcome.d.ts +1 -1
- package/dist/core/runner/resume-preflight.d.ts +1 -1
- package/dist/core/runner/resume-review-outcome.d.ts +1 -1
- 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-brain-sinks.d.ts +1 -1
- package/dist/core/runner/run-clock-and-content.d.ts +4 -4
- package/dist/core/runner/run-compaction-machinery.d.ts +3 -3
- package/dist/core/runner/run-git-lane.d.ts +1 -1
- package/dist/core/runner/run-harness-handlers.d.ts +2 -2
- package/dist/core/runner/run-harness-handlers.js +2 -3
- package/dist/core/runner/run-identity-wiring.d.ts +4 -4
- package/dist/core/runner/run-leg.d.ts +3 -3
- package/dist/core/runner/run-notification-lane.d.ts +2 -2
- package/dist/core/runner/run-reasoning-seat.d.ts +2 -2
- package/dist/core/runner/run-settle-and-teardown.d.ts +5 -5
- package/dist/core/runner/run-settle-and-teardown.js +3 -1
- package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
- package/dist/core/runner/run-terminal-adoption.d.ts +5 -5
- package/dist/core/runner/runtask.d.ts +14 -14
- package/dist/core/runner/session-file-state-replay.d.ts +3 -3
- package/dist/core/runner/session-file-state-replay.js +3 -3
- package/dist/core/runner/session-rule-policy.d.ts +2 -2
- package/dist/core/runner/session-rule-policy.js +3 -4
- package/dist/core/runner/steer-admission.d.ts +2 -2
- package/dist/core/runner/stream-halt-verbs.d.ts +1 -1
- package/dist/core/runner/stream-halt-verbs.js +1 -1
- package/dist/core/runner/stream-reap.d.ts +1 -1
- package/dist/core/runner/stream-steer-verb.js +1 -4
- package/dist/core/runner/synthetic-tools.d.ts +8 -8
- package/dist/core/runner/teardown-bounded.d.ts +2 -2
- package/dist/core/runner/tool-defer-gate.d.ts +1 -1
- package/dist/core/runner/tool-disclosure.d.ts +33 -33
- package/dist/core/runner/tool-end-body.d.ts +3 -3
- package/dist/core/runner/tool-face-overlay.d.ts +2 -2
- package/dist/core/runner/tool-output-projection.d.ts +2 -2
- package/dist/core/runner/turn-attachments.d.ts +53 -53
- package/dist/core/runner/usage-accounting.d.ts +4 -4
- package/dist/core/runner/workspace-path.d.ts +1 -1
- 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/llm/types.d.ts +1 -1
- package/dist/engine/loop/agent-loop.js +15 -8
- package/dist/engine/loop/types.d.ts +5 -0
- package/dist/index.d.ts +21 -16
- package/dist/index.js +17 -12
- 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 +114 -48
- package/dist/tools/fs/bash-readonly-classifier.js +171 -60
- package/dist/tools/fs/encoding.d.ts +31 -40
- package/dist/tools/fs/fs-bash.d.ts +49 -45
- package/dist/tools/fs/fs-bash.js +19 -27
- package/dist/tools/fs/fs-pdf.d.ts +8 -8
- package/dist/tools/fs/fs-search-tools.d.ts +4 -4
- package/dist/tools/fs/fs-search-tools.js +5 -2
- package/dist/tools/fs/fs-shared.d.ts +146 -158
- package/dist/tools/fs/fs-shared.js +3 -11
- package/dist/tools/fs/fs-write.d.ts +8 -8
- package/dist/tools/fs/gh-rate-limit.d.ts +13 -14
- package/dist/tools/fs/index.d.ts +78 -78
- package/dist/tools/fs/index.js +4 -1
- package/dist/tools/fs/notebook.d.ts +24 -25
- package/dist/tools/fs/pdf.d.ts +3 -3
- package/dist/tools/fs/read-deny.d.ts +22 -22
- package/dist/tools/fs/read-face.d.ts +21 -24
- package/dist/tools/fs/repo-map.d.ts +8 -8
- package/dist/tools/fs/safety.d.ts +233 -130
- package/dist/tools/fs/safety.js +131 -31
- package/dist/tools/fs/search.d.ts +63 -71
- package/dist/tools/fs/search.js +8 -6
- package/package.json +6 -2
- package/test/export-surface.snapshot.json +119 -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
|
}
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* is lossy:
|
|
2
|
+
* File encoding + line-ending detection for the Read/Edit/Write band, ported from CC's
|
|
3
|
+
* `utils/fileRead.ts` mechanism and hardened where CC is lossy:
|
|
5
4
|
*
|
|
6
|
-
* - BOM detection: `FF FE` → utf16le, `EF BB BF` → utf8-with-BOM, else plain utf8.
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* - BOM detection: `FF FE` → utf16le, `EF BB BF` → utf8-with-BOM, else plain utf8. Detection is
|
|
6
|
+
* BOM-only; UTF-16BE / heuristic sniffing is deliberately NOT done — a wrong guess silently
|
|
7
|
+
* corrupts, and the BOM-less case stays a refusal at the Read layer.
|
|
9
8
|
* - The MODEL-FACING text is BOM-stripped and CRLF-normalized (what the model quotes in `old_string`
|
|
10
|
-
* must match what Read showed it).
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
* hash coordinate cannot drift apart — an improvement over CC's split, recorded as such rather
|
|
19
|
-
* than claimed as parity.
|
|
20
|
-
* - Write-back re-encodes with the ORIGINAL encoding, PRESERVES the BOM (CC drops it on Write — its
|
|
21
|
-
* lossy face, dynamic-verified W1; we keep it, recorded as a deliberate improvement), and restores
|
|
22
|
-
* the file's dominant line endings for Edit (Write keeps the model's endings as-is, CC decision).
|
|
9
|
+
* must match what Read showed it). CRLF-normalize is parity on both of CC's read lanes; BOM-strip
|
|
10
|
+
* is parity on CC's READ lane only — CC's EDIT/WRITE lane hands the model text with the leading
|
|
11
|
+
* U+FEFF still in it and strips only inside its staleness/hash coordinate. sema strips in ONE
|
|
12
|
+
* shared decoder for every lane, so the model-facing view and the hash coordinate cannot drift
|
|
13
|
+
* apart: a registered delta, not parity.
|
|
14
|
+
* - Write-back re-encodes with the ORIGINAL encoding, PRESERVES the BOM (CC drops it on Write — a
|
|
15
|
+
* registered deliberate improvement), and restores the file's dominant line endings for Edit
|
|
16
|
+
* (Write keeps the model's endings as-is, CC decision).
|
|
23
17
|
*/
|
|
24
18
|
/** Detected byte-level encoding of a text file (BOM-driven; utf8 is the no-BOM default). */
|
|
25
19
|
export interface DetectedFileEncoding {
|
|
@@ -31,19 +25,17 @@ export interface DecodedTextFile {
|
|
|
31
25
|
/** Model-facing text: BOM stripped, `\r\n` normalized to `\n`. The staleness-hash coordinate. */
|
|
32
26
|
text: string;
|
|
33
27
|
encoding: DetectedFileEncoding;
|
|
34
|
-
/** Dominant line ending of the ORIGINAL bytes (majority vote
|
|
28
|
+
/** Dominant line ending of the ORIGINAL bytes (majority vote; CC's counting rule).
|
|
35
29
|
* Deliberately NOT per-line lossless: a mixed-endings file is unified to the dominant ending on
|
|
36
|
-
* the first edit (
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
* the
|
|
40
|
-
*
|
|
41
|
-
* the entire file, so sampling a prefix is exactly the case where the cheap answer flips the
|
|
42
|
-
* remaining 99% of the lines. Registered as a deliberate delta, not parity. */
|
|
30
|
+
* the first edit (accepted non-goal — CC behaves the same way).
|
|
31
|
+
* The SAMPLE is a registered delta: CC votes on the first 4096 characters only, while
|
|
32
|
+
* {@link detectLineEndings} below votes on the whole file. The verdict decides what an Edit WRITES
|
|
33
|
+
* BACK across the entire file, so sampling a prefix is exactly the case where the cheap answer
|
|
34
|
+
* flips the remaining 99% of the lines. */
|
|
43
35
|
endings: DetectedLineEndings;
|
|
44
|
-
/**
|
|
45
|
-
*
|
|
46
|
-
*
|
|
36
|
+
/** A utf16le body with an ODD byte count is TRUNCATED/corrupt — decoding would silently drop the
|
|
37
|
+
* dangling byte and a later write-back would destroy it permanently. Flagged so the tool layer
|
|
38
|
+
* fails closed instead of "repairing" the file. */
|
|
47
39
|
malformed?: true;
|
|
48
40
|
}
|
|
49
41
|
/** BOM-only encoding detection (CC `detectEncodingForResolvedPath` parity — no content heuristics). */
|
|
@@ -56,8 +48,8 @@ export declare function decodeTextBytes(bytes: Uint8Array): DecodedTextFile;
|
|
|
56
48
|
* @param text normalized (`\n`) text when `endings` is CRLF/LF (Edit path — restore the file's
|
|
57
49
|
* dominant endings); pass `endings:"preserve"` to write the text's OWN line endings untouched
|
|
58
50
|
* (Write path — CC decision: the model sent explicit endings in `content` and meant them).
|
|
59
|
-
* @returns a plain string for the no-BOM utf8 + LF-or-preserve fast path
|
|
60
|
-
*
|
|
51
|
+
* @returns a plain string for the no-BOM utf8 + LF-or-preserve fast path, else the exact bytes
|
|
52
|
+
* (BOM + encoded body).
|
|
61
53
|
*/
|
|
62
54
|
export declare function encodeTextForFile(text: string, encoding: DetectedFileEncoding, endings: DetectedLineEndings | "preserve"): string | Uint8Array;
|
|
63
55
|
/** Normalize model-supplied match/replacement text the same way the file text was normalized
|
|
@@ -65,16 +57,15 @@ export declare function encodeTextForFile(text: string, encoding: DetectedFileEn
|
|
|
65
57
|
* Deliberately does NOT strip a leading U+FEFF: an `old_string` starting with one may be quoting
|
|
66
58
|
* genuine file content — use {@link normalizeFileText} only where the string stands for a whole file. */
|
|
67
59
|
export declare function normalizeEditText(s: string): string;
|
|
68
|
-
/**
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
* content and must stay. */
|
|
60
|
+
/** Split a leading U+FEFF off MODEL-SUPPLIED whole-file text (CC parity). The BOM is a file-level
|
|
61
|
+
* byte marker, not text: decodeTextBytes strips it on the way in, so it can never live in the
|
|
62
|
+
* normalized coordinate. Only for strings that stand for a WHOLE file (Write `content`,
|
|
63
|
+
* readFileState seeding) — a decoded file text's leading U+FEFF is genuine content and must stay. */
|
|
73
64
|
export declare function splitLeadingBom(text: string): {
|
|
74
65
|
hadBom: boolean;
|
|
75
66
|
text: string;
|
|
76
67
|
};
|
|
77
|
-
/**
|
|
78
|
-
*
|
|
79
|
-
*
|
|
68
|
+
/** CC parity (BOM-strip + CRLF-normalize): THE coordinate every readFileState hash lives in
|
|
69
|
+
* (Read/Edit/Write staleness all hash `decodeTextBytes(...).text`). A hash taken on raw model content
|
|
70
|
+
* with a leading U+FEFF can never agree with any later re-read. */
|
|
80
71
|
export declare function normalizeFileText(s: string): string;
|