@sema-agent/core 7.33.8 → 7.35.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 +462 -3
- package/dist/agents/cross-session-judge.d.ts +16 -7
- package/dist/agents/cross-session-judge.js +18 -15
- package/dist/agents/peer-session-drain.d.ts +4 -0
- package/dist/agents/peer-session-drain.js +5 -2
- package/dist/agents/repair-loop.d.ts +1 -1
- package/dist/agents/subagent.d.ts +3 -0
- package/dist/agents/subagent.js +12 -3
- package/dist/brain/anthropic.js +1 -0
- package/dist/brain/errors.d.ts +23 -1
- package/dist/brain/errors.js +19 -2
- package/dist/brain/open-responses.js +1 -0
- package/dist/brain/openai.js +1 -0
- package/dist/brain/status-sink.d.ts +7 -1
- package/dist/brain/stream-engine.d.ts +3 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/brain/stream-shared.d.ts +3 -1
- package/dist/brain/stream-shared.js +16 -4
- package/dist/core/a2a.d.ts +3 -1
- package/dist/core/a2a.js +1 -1
- package/dist/core/ask-origin.d.ts +379 -154
- package/dist/core/ask-origin.js +224 -45
- package/dist/core/ask-question.js +2 -1
- package/dist/core/ask-unresolvable-notice.d.ts +5 -2
- package/dist/core/ask-unresolvable-notice.js +4 -3
- package/dist/core/auto-mode-arming.d.ts +17 -4
- package/dist/core/auto-mode-arming.js +31 -6
- package/dist/core/auto-mode-defaults.d.ts +16 -7
- package/dist/core/auto-mode-defaults.js +2 -1
- package/dist/core/auto-mode-directive.d.ts +18 -30
- package/dist/core/auto-mode-directive.js +9 -18
- package/dist/core/auto-mode-prompt.d.ts +62 -1
- package/dist/core/auto-mode-prompt.js +42 -4
- package/dist/core/auto-mode-rebuild.d.ts +2 -1
- package/dist/core/auto-mode-rebuild.js +1 -0
- package/dist/core/auto-mode.d.ts +32 -3
- package/dist/core/auto-mode.js +80 -41
- package/dist/core/checkpoint-store.d.ts +54 -35
- package/dist/core/checkpoint-store.js +58 -3
- package/dist/core/child-clamp-seats.js +1 -0
- package/dist/core/classifier-unavailable-arm.d.ts +57 -0
- package/dist/core/classifier-unavailable-arm.js +17 -0
- package/dist/core/effective-path-target.d.ts +12 -0
- package/dist/core/effective-path-target.js +18 -7
- package/dist/core/engine-notice.d.ts +53 -2
- package/dist/core/fs-write-gate-policy.d.ts +5 -1
- package/dist/core/fs-write-gate-policy.js +4 -1
- package/dist/core/gate-fold.js +19 -3
- package/dist/core/gate-lanes.js +116 -50
- package/dist/core/gate-outcome.d.ts +40 -7
- package/dist/core/gate-outcome.js +26 -4
- package/dist/core/governance-codes.d.ts +18 -2
- package/dist/core/governance-codes.js +23 -1
- package/dist/core/hooks.d.ts +53 -29
- package/dist/core/hooks.js +1 -3
- package/dist/core/mcp.js +1 -1
- package/dist/core/message-utils.d.ts +2 -1
- package/dist/core/message-utils.js +2 -1
- package/dist/core/mode-policy.d.ts +185 -0
- package/dist/core/mode-policy.js +184 -0
- package/dist/core/oracle-isolation.d.ts +3 -3
- package/dist/core/permission-rule-model.d.ts +20 -3
- package/dist/core/permission-rule-model.js +40 -3
- package/dist/core/permission-rule-provider.d.ts +22 -11
- package/dist/core/permission-rule-provider.js +17 -7
- package/dist/core/permission-rules.d.ts +23 -1
- package/dist/core/permission-rules.js +56 -9
- package/dist/core/persisted-rule-arms.d.ts +14 -10
- package/dist/core/persisted-rule-arms.js +13 -6
- package/dist/core/present-plan-tool.d.ts +1 -1
- package/dist/core/present-plan-tool.js +3 -3
- package/dist/core/read-only-shell.d.ts +19 -19
- package/dist/core/read-only-shell.js +38 -12
- package/dist/core/retired-keys.d.ts +9 -3
- package/dist/core/retired-keys.js +5 -1
- package/dist/core/runner/active-skill-scope.js +5 -4
- package/dist/core/runner/assemble-result.js +2 -2
- package/dist/core/runner/classifier-ladder.d.ts +43 -1
- package/dist/core/runner/classifier-ladder.js +29 -0
- package/dist/core/runner/contracts.d.ts +62 -38
- package/dist/core/runner/denial-limit-arms.d.ts +35 -7
- package/dist/core/runner/denial-limit-arms.js +15 -9
- package/dist/core/runner/env-capability-census.js +1 -1
- package/dist/core/runner/file-history-request.d.ts +26 -0
- package/dist/core/runner/file-history-request.js +90 -0
- package/dist/core/runner/gate-exit.d.ts +30 -4
- package/dist/core/runner/gate-exit.js +28 -7
- package/dist/core/runner/permission-rule-lanes.d.ts +11 -6
- package/dist/core/runner/permission-rule-lanes.js +55 -48
- package/dist/core/runner/persisted-path-rows.d.ts +64 -0
- package/dist/core/runner/persisted-path-rows.js +89 -0
- package/dist/core/runner/prepare-artifact.d.ts +5 -1
- package/dist/core/runner/prepare-artifact.js +9 -3
- package/dist/core/runner/prepare-ask-lane.d.ts +6 -4
- package/dist/core/runner/prepare-ask-lane.js +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +39 -13
- package/dist/core/runner/prepare-caps-and-workflow.js +58 -29
- package/dist/core/runner/prepare-file-history.d.ts +9 -3
- package/dist/core/runner/prepare-file-history.js +4 -90
- package/dist/core/runner/prepare-gate-stations.d.ts +37 -9
- package/dist/core/runner/prepare-gate-stations.js +46 -29
- package/dist/core/runner/prepare-hands-readface.d.ts +40 -17
- package/dist/core/runner/prepare-hands-readface.js +64 -53
- package/dist/core/runner/prepare-inherited-gate.d.ts +30 -23
- package/dist/core/runner/prepare-inherited-gate.js +45 -25
- package/dist/core/runner/prepare-park-ask.d.ts +6 -4
- package/dist/core/runner/prepare-park-ask.js +7 -5
- package/dist/core/runner/prepare-policy-chain.d.ts +25 -4
- package/dist/core/runner/prepare-policy-chain.js +203 -180
- package/dist/core/runner/prepare-protocol-tools.d.ts +28 -0
- package/dist/core/runner/prepare-protocol-tools.js +13 -3
- package/dist/core/runner/prepare-run-refs.d.ts +6 -6
- package/dist/core/runner/prepare-run-refs.js +9 -4
- package/dist/core/runner/prepare-safety-scan.d.ts +5 -0
- package/dist/core/runner/prepare-safety-scan.js +19 -3
- package/dist/core/runner/prepare-suspend-saga.d.ts +5 -5
- package/dist/core/runner/prepare-suspend-saga.js +6 -6
- package/dist/core/runner/prepare-task.js +56 -24
- package/dist/core/runner/prepare-wiring-manifest.d.ts +10 -6
- package/dist/core/runner/prepare-wiring-manifest.js +5 -3
- package/dist/core/runner/read-root-grant.d.ts +9 -0
- package/dist/core/runner/read-root-grant.js +12 -0
- package/dist/core/runner/retired-runner-deps.js +1 -0
- package/dist/core/runner/run-brain-sinks.js +3 -1
- package/dist/core/runner/run-identity-wiring.js +1 -1
- package/dist/core/runner/run-leg.js +29 -18
- package/dist/core/runner/run-notification-lane.d.ts +3 -0
- package/dist/core/runner/run-notification-lane.js +3 -1
- package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
- package/dist/core/runner/run-telemetry-and-budget-seats.js +1 -1
- package/dist/core/runner/run-turn-boundary.js +1 -1
- package/dist/core/runner/runtask.d.ts +1 -0
- package/dist/core/runner/runtask.js +8 -11
- package/dist/core/runner/session-mode-seats.d.ts +48 -0
- package/dist/core/runner/session-mode-seats.js +73 -0
- package/dist/core/runner/session-rule-policy.js +3 -2
- package/dist/core/runner/stream-mode-verb.d.ts +27 -0
- package/dist/core/runner/stream-mode-verb.js +19 -0
- package/dist/core/runner/turn-attachments.d.ts +3 -3
- package/dist/core/runner/turn-attachments.js +1 -1
- package/dist/core/runner/write-protection-station.d.ts +7 -1
- package/dist/core/runner/write-protection-station.js +6 -3
- package/dist/core/runner-deps.d.ts +31 -34
- package/dist/core/runtime-caps.d.ts +12 -3
- package/dist/core/safety-axis-vocab.d.ts +2 -2
- package/dist/core/safety-axis-vocab.js +1 -1
- package/dist/core/secret-env.d.ts +30 -58
- package/dist/core/secret-env.js +149 -586
- package/dist/core/sensitive-path-policy.d.ts +4 -2
- package/dist/core/sensitive-path-policy.js +41 -31
- package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
- package/dist/core/task-event.d.ts +1 -1
- package/dist/core/task-result.d.ts +1 -1
- package/dist/core/task-spec.d.ts +41 -89
- package/dist/core/task-stream.d.ts +16 -0
- package/dist/core/terminal-cause.js +1 -0
- package/dist/core/tighten-task-spec.d.ts +2 -2
- package/dist/core/tighten-task-spec.js +12 -14
- package/dist/core/tool-catalog-entries.js +4 -3
- package/dist/core/tool-policy.d.ts +84 -30
- package/dist/core/tool-policy.js +76 -16
- package/dist/core/tool-registry.d.ts +38 -0
- package/dist/core/tool-registry.js +25 -1
- package/dist/core/tool-roster.d.ts +17 -2
- package/dist/core/tool-roster.js +11 -1
- package/dist/core/tool-spec.d.ts +8 -9
- package/dist/core/trace.d.ts +18 -6
- package/dist/core/types.d.ts +2 -1
- package/dist/core/wiring-manifest.d.ts +31 -5
- package/dist/core/wiring-manifest.js +28 -1
- package/dist/core/write-protect.d.ts +11 -1
- package/dist/core/write-protect.js +26 -15
- package/dist/engine/execution-env/node-execution-env.d.ts +6 -1
- package/dist/engine/execution-env/node-execution-env.js +16 -3
- package/dist/engine/harness/agent-harness.js +2 -1
- package/dist/engine/harness/types.d.ts +6 -7
- package/dist/engine/llm/types.d.ts +2 -4
- package/dist/engine/loop/types.d.ts +7 -0
- package/dist/engine/lsp/node-lsp-manager.js +1 -1
- package/dist/index.d.ts +18 -8
- package/dist/index.js +15 -6
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-primitives.d.ts +1 -1
- package/dist/orchestration/workflow-size-guideline.js +2 -2
- package/dist/stores/file/index.d.ts +5 -3
- package/dist/stores/file/session-store.d.ts +8 -2
- package/dist/stores/file/session-store.js +5 -5
- package/dist/tools/artifact/artifact-tool.d.ts +17 -0
- package/dist/tools/artifact/artifact-tool.js +16 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +28 -7
- package/dist/tools/fs/bash-readonly-classifier.js +88 -0
- package/dist/tools/fs/containment-face.d.ts +9 -3
- package/dist/tools/fs/containment-face.js +2 -0
- package/dist/tools/fs/file-read-check.d.ts +50 -0
- package/dist/tools/fs/file-read-check.js +79 -0
- package/dist/tools/fs/fs-bash.d.ts +3 -3
- package/dist/tools/fs/fs-read.d.ts +1 -0
- package/dist/tools/fs/fs-search-tools.d.ts +2 -2
- package/dist/tools/fs/fs-search-tools.js +4 -4
- package/dist/tools/fs/fs-shared.js +20 -6
- package/dist/tools/fs/fs-write.d.ts +4 -3
- package/dist/tools/fs/fs-write.js +9 -9
- package/dist/tools/fs/index.d.ts +39 -3
- package/dist/tools/fs/index.js +38 -15
- package/dist/tools/fs/read-deny-home.d.ts +93 -0
- package/dist/tools/fs/read-deny-home.js +225 -0
- package/dist/tools/fs/read-deny.d.ts +176 -34
- package/dist/tools/fs/read-deny.js +268 -61
- package/dist/tools/fs/safety.d.ts +6 -1
- package/dist/tools/fs/safety.js +18 -5
- package/dist/tools/fs/search.d.ts +3 -0
- package/dist/tools/fs/search.js +47 -9
- package/dist/tools/fs/shell-read-check.d.ts +52 -13
- package/dist/tools/fs/shell-read-check.js +199 -20
- package/dist/tools/fs/shell-read-establish.d.ts +7 -3
- package/dist/tools/fs/shell-read-establish.js +134 -21
- package/dist/tools/fs/shell-read-identity.d.ts +7 -0
- package/dist/tools/fs/shell-read-identity.js +21 -0
- package/dist/tools/fs/shell-read-judge.d.ts +92 -32
- package/dist/tools/fs/shell-read-judge.js +43 -18
- package/dist/tools/fs/shell-read-plan.d.ts +4 -0
- package/dist/tools/fs/shell-read-plan.js +26 -4
- package/dist/tools/fs/shell-read-write-targets.d.ts +73 -0
- package/dist/tools/fs/shell-read-write-targets.js +173 -0
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +117 -11
- package/wire-changes/core@7.34.0.json +2621 -0
- package/wire-changes/core@7.35.0.json +584 -0
|
@@ -141,6 +141,10 @@ export declare const READ_RULE_TOOL = "Read";
|
|
|
141
141
|
* catalog's declarations, exactly as the protective fences' covered set does.
|
|
142
142
|
*/
|
|
143
143
|
export declare function ruleToolGrammarOf(toolName: string): "command" | "path" | undefined;
|
|
144
|
+
/** The names the persisted grammar judges a call under: every one of the call's rule names (`ruleNamesOf` — the wire
|
|
145
|
+
* name, then the aliases that resolve to it) the grammar speaks for, so a stored `Bash(…)` or `Edit(…)` row reaches a
|
|
146
|
+
* tool that answers to that name under another wire name, as a policy's rule does. Empty ⇒ the grammar speaks for none. */
|
|
147
|
+
export declare function laneToolNamesOf(req: Pick<import("./tool-policy.js").ToolCallRequest, "toolName" | "face">): readonly string[];
|
|
144
148
|
/**
|
|
145
149
|
* Match forms. Four members, two of them one predicate:
|
|
146
150
|
* · `exact` — admits the one string the rule spells.
|
|
@@ -558,6 +562,18 @@ export declare function isUsablePathBase(base: string | undefined, family: PathF
|
|
|
558
562
|
* no root reached nothing at all).
|
|
559
563
|
*/
|
|
560
564
|
export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, target: PathIdentity, bases: PathRuleBases): ProgramRunReachOutcome;
|
|
565
|
+
/**
|
|
566
|
+
* Where a path rule's reach STARTS under the call's bases — its HEAD: the location the resolved pattern names before
|
|
567
|
+
* its first segment carrying `*` (the whole path when none does), or a directory rule's directory. Everything the rule
|
|
568
|
+
* reaches lies at or below the head, so a spelling of the head that the environment resolves elsewhere (a link, a
|
|
569
|
+
* firmlink, `/var` → `/private/var`) is a second spelling of everything the rule reaches — the deny tables judge the
|
|
570
|
+
* rule at both ({@link import("../tools/fs/read-deny.js").withRuleRowsWhereTheyLead}; the reference's "physical twin"
|
|
571
|
+
* of a rule prefix, cli280.js 9766583). The head may be the bare anchor (`/`, a drive or share root — `Read(/**)` at a
|
|
572
|
+
* drive root): an environment can resolve a drive or share root elsewhere. `undefined`: the pattern needs a base the
|
|
573
|
+
* call does not supply (the rule then reads `unreadable`, a reach, everywhere), or a directory rule names the bare
|
|
574
|
+
* anchor (it admits nothing).
|
|
575
|
+
*/
|
|
576
|
+
export declare function pathRuleHeadOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, bases: PathRuleBases): PathIdentity | undefined;
|
|
561
577
|
/**
|
|
562
578
|
* Does this rule's command pattern admit `command`?
|
|
563
579
|
*
|
|
@@ -793,7 +809,8 @@ export declare function eligiblePersisted(rule: PersistedRule, call: {
|
|
|
793
809
|
* found BEFORE any conjunction: reporting one rule is narrower than reporting a set;
|
|
794
810
|
* 3. the conjunction arm, compounds only: every segment must be admitted by some eligible rule under
|
|
795
811
|
* the MATCH reading (the allow side's own under-approximating instrument — never the deny side's
|
|
796
|
-
* wider PROGRAM_RUNS reading)
|
|
812
|
+
* wider PROGRAM_RUNS reading), or be a read-only segment that allows itself ({@link readsOnlyAsSegment});
|
|
813
|
+
* at least one rule must speak. Per segment the FIRST table-order admitting rule is selected, and
|
|
797
814
|
* the coverage set is those selections in segment order, de-duplicated — the same command against
|
|
798
815
|
* the same rule table always names the same set (reporting-order determinism, the historical
|
|
799
816
|
* "order is defined only for reporting" contract extended to sets).
|
|
@@ -832,8 +849,8 @@ export interface PersistedRuleVerdict {
|
|
|
832
849
|
readonly rules: readonly PersistedRule[];
|
|
833
850
|
/** The call could not be READ against the person's deny/ask rows (see {@link programRunReachOf}):
|
|
834
851
|
* `behavior` is `ask`, `rules` names the first tightening row that could not be judged, and this is
|
|
835
|
-
* the lexer's reason. The lane projects it as an UNREADABLE answer
|
|
836
|
-
*
|
|
852
|
+
* the lexer's reason. The lane projects it as an UNREADABLE answer carrying that reason (it fails closed)
|
|
853
|
+
* — never as a matched ask rule. */
|
|
837
854
|
readonly unreadable?: string;
|
|
838
855
|
}
|
|
839
856
|
/**
|
|
@@ -4,8 +4,10 @@ import { compileReadDeny } from "../tools/fs/read-deny.js";
|
|
|
4
4
|
import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
5
5
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
6
6
|
import { pathTargetOf } from "./tool-registry.js";
|
|
7
|
+
import { ruleNamesOf } from "./tool-roster.js";
|
|
7
8
|
import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
|
|
8
9
|
import { readShellCommand } from "./shell-lexer.js";
|
|
10
|
+
import { readOnlyLineObjection, readOnlyShellVerdict } from "./read-only-shell.js";
|
|
9
11
|
export const RULE_BEHAVIORS = ["deny", "ask", "allow"];
|
|
10
12
|
const RULE_BEHAVIOR_SET = new Set(RULE_BEHAVIORS);
|
|
11
13
|
export function isRuleBehavior(v) {
|
|
@@ -29,6 +31,9 @@ export function ruleToolGrammarOf(toolName) {
|
|
|
29
31
|
return "command";
|
|
30
32
|
return pathTargetOf(toolName) !== undefined ? "path" : undefined;
|
|
31
33
|
}
|
|
34
|
+
export function laneToolNamesOf(req) {
|
|
35
|
+
return ruleNamesOf(req).filter((n) => ruleToolGrammarOf(n) !== undefined);
|
|
36
|
+
}
|
|
32
37
|
export const MAX_RULE_TEXT_CHARS = 512;
|
|
33
38
|
export const BARE_INTERPRETER_NAMES = new Set([
|
|
34
39
|
"node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
|
|
@@ -450,6 +455,29 @@ export function pathRuleReachOf(rule, target, bases) {
|
|
|
450
455
|
const segments = target.split("/").filter((sg) => sg !== "");
|
|
451
456
|
return globSegmentsReach(globs, segments) ? REACHED : NOT_REACHED;
|
|
452
457
|
}
|
|
458
|
+
export function pathRuleHeadOf(rule, bases) {
|
|
459
|
+
const family = pathFamilyOf(bases);
|
|
460
|
+
const tree = bases.root ?? bases.cwd;
|
|
461
|
+
let head;
|
|
462
|
+
if (rule.match === "subpath") {
|
|
463
|
+
if (carriesDotSegment(rule.command, family))
|
|
464
|
+
return undefined;
|
|
465
|
+
const dir = pathIdentityOf(rule.command, family, tree);
|
|
466
|
+
return dir.ok && isIdentityDir(dir.identity, family) ? dir.identity : undefined;
|
|
467
|
+
}
|
|
468
|
+
else if (rule.match === "path") {
|
|
469
|
+
const r = resolvePathPattern(rule.command, bases, rule.behavior);
|
|
470
|
+
if ("missingBase" in r)
|
|
471
|
+
return undefined;
|
|
472
|
+
const parts = r.resolved.split("/");
|
|
473
|
+
const wild = parts.findIndex((part) => part.includes("*"));
|
|
474
|
+
head = (wild === -1 ? parts : parts.slice(0, wild)).join("/");
|
|
475
|
+
}
|
|
476
|
+
else
|
|
477
|
+
return undefined;
|
|
478
|
+
const id = pathIdentityOf(head === "" ? "/" : head, family, tree);
|
|
479
|
+
return id.ok ? id.identity : undefined;
|
|
480
|
+
}
|
|
453
481
|
function hasUnescapedStar(s) {
|
|
454
482
|
for (let i = 0; i < s.length; i++) {
|
|
455
483
|
if (s[i] !== "*")
|
|
@@ -765,15 +793,22 @@ export function findAdmittingRule(rules, call) {
|
|
|
765
793
|
return lone !== undefined ? [lone] : undefined;
|
|
766
794
|
}
|
|
767
795
|
const covering = [];
|
|
796
|
+
const lineObjection = readOnlyLineObjection(call.command, call.execCwd ?? call.cwd);
|
|
768
797
|
for (const [i, segment] of shape.segments.entries()) {
|
|
769
798
|
const dir = cdDirs[i];
|
|
770
799
|
const first = firstEligibleAdmittingRule(rules, segment, call) ?? (dir !== undefined ? firstEligibleDirectoryRule(rules, dir, call) : undefined);
|
|
771
|
-
if (first === undefined)
|
|
800
|
+
if (first === undefined) {
|
|
801
|
+
if (readsOnlyAsSegment(segment, lineObjection))
|
|
802
|
+
continue;
|
|
772
803
|
return undefined;
|
|
804
|
+
}
|
|
773
805
|
if (!covering.includes(first))
|
|
774
806
|
covering.push(first);
|
|
775
807
|
}
|
|
776
|
-
return covering;
|
|
808
|
+
return covering.length > 0 ? covering : undefined;
|
|
809
|
+
}
|
|
810
|
+
function readsOnlyAsSegment(segment, lineObjection) {
|
|
811
|
+
return lineObjection === undefined && !movesShellCursor(segment) && readOnlyShellVerdict(segment).readOnly;
|
|
777
812
|
}
|
|
778
813
|
function firstEligibleDirectoryRule(rules, directory, call) {
|
|
779
814
|
for (const rule of rules) {
|
|
@@ -855,11 +890,13 @@ export function segmentCoverageOf(command, rules, call) {
|
|
|
855
890
|
return undefined;
|
|
856
891
|
const proposed = rules.proposed ?? [];
|
|
857
892
|
const cdDirs = resolveCdSegmentDirectories(shape.segments, call.execCwd ?? call.cwd);
|
|
893
|
+
const lineObjection = readOnlyLineObjection(folded, call.execCwd ?? call.cwd);
|
|
858
894
|
return shape.segments.map((segment, i) => {
|
|
859
895
|
const dir = cdDirs[i];
|
|
860
896
|
return {
|
|
861
897
|
segment: segment.trim(),
|
|
862
|
-
covered:
|
|
898
|
+
covered: readsOnlyAsSegment(segment, lineObjection) ||
|
|
899
|
+
firstEligibleAdmittingRule(rules.persisted, segment, call) !== undefined ||
|
|
863
900
|
(dir !== undefined && firstEligibleDirectoryRule(rules.persisted, dir, call) !== undefined) ||
|
|
864
901
|
proposed.some((p) => p.rule.tool === "Read"
|
|
865
902
|
? dir !== undefined &&
|
|
@@ -72,12 +72,16 @@ export interface EffectivePermissionRules {
|
|
|
72
72
|
org: OrgRuleResolution;
|
|
73
73
|
/** The durable partition's revision — the optimistic-concurrency anchor for its writes. */
|
|
74
74
|
rev: number;
|
|
75
|
-
/** Present iff
|
|
76
|
-
*
|
|
77
|
-
*
|
|
78
|
-
*
|
|
79
|
-
*
|
|
75
|
+
/** Present iff a PERSONAL partition could not be read — the durable one (its user / project rows; `removed` is then
|
|
76
|
+
* empty and `rev` 0) or the session one: the failed partition's rows are absent from `rules`, while the rows of the
|
|
77
|
+
* partition that WAS read stand, and so does the org resolution beside them (a flaky personal store must not become an
|
|
78
|
+
* org governance outage). The line names the failure (both lines, `; `-joined, when both failed). A consumer that needs
|
|
79
|
+
* the durable rows treats this as a read failure — {@link effectiveOrThrow} is that reading; the gate's personal lane
|
|
80
|
+
* and the leg's path-row read skip the failed partition's rows, disclosed, and judge by the rest. */
|
|
80
81
|
unreadable?: string;
|
|
82
|
+
/** Beside {@link unreadable}: the sources whose partition could not be read (`user` and `project` — the durable
|
|
83
|
+
* partition; `session`), in that order — the rows `rules` does not carry. */
|
|
84
|
+
unreadableSources?: readonly Exclude<RuleSource, "org">[];
|
|
81
85
|
/** Disclosure lines from the personal partitions (a session partition that could not be read, rows it
|
|
82
86
|
* served that are not canonical session rows of this session). The org partition's lines ride
|
|
83
87
|
* `org.disclosures`. */
|
|
@@ -85,12 +89,11 @@ export interface EffectivePermissionRules {
|
|
|
85
89
|
}
|
|
86
90
|
/**
|
|
87
91
|
* The host-visible store, already anchored to one owner. One read verb, which never throws: each
|
|
88
|
-
* partition reports its own failure in the view —
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* partition is down, and the personal rows stand while an org source is down.
|
|
92
|
+
* partition reports its own failure in the view — a PERSONAL partition's read failure (the durable one's, the
|
|
93
|
+
* session one's) as `unreadable` / `unreadableSources`, with that partition's rows absent and the other's present
|
|
94
|
+
* (served-but-non-canonical session rows stay a `disclosures` line), the ORG partition's as its `status`. The unit
|
|
95
|
+
* that fails is the partition: the org verdict stands while a personal partition is down, each personal partition's
|
|
96
|
+
* rows stand while the other is down, and the personal rows stand while an org source is down.
|
|
94
97
|
*/
|
|
95
98
|
export interface PermissionRuleStore {
|
|
96
99
|
effective(query?: {
|
|
@@ -119,6 +122,14 @@ export interface PermissionRuleStoreProvider {
|
|
|
119
122
|
session: boolean;
|
|
120
123
|
}>;
|
|
121
124
|
}
|
|
125
|
+
/**
|
|
126
|
+
* The store one task reads its person's rules from. The unauthenticated floor is enforced HERE, not merely asked of the
|
|
127
|
+
* provider: an absent principal resolves to zero personal rules, and a provider that forgot it would hand one person's
|
|
128
|
+
* standing approvals to an anonymous task. The ONE exception is an explicitly declared LOCAL-OWNER deployment, where "no
|
|
129
|
+
* principal" is the typed absence of identity — the machine owner's own bucket (a provider with no such face resolves
|
|
130
|
+
* the anonymous store; {@link createPermissionRuleLanes} refuses that declaration loudly).
|
|
131
|
+
*/
|
|
132
|
+
export declare function ownerStoreOf(provider: PermissionRuleStoreProvider, principal: string | undefined, localOwnerDeclared: boolean): PermissionRuleStore;
|
|
122
133
|
/** What {@link createPermissionRuleStoreProvider} composes. */
|
|
123
134
|
export interface PermissionRuleStoreConfig {
|
|
124
135
|
/** The durable CRDT partition (user + project sources). Absent ⇒ no personal rules exist on this
|
|
@@ -4,6 +4,10 @@ import { UNGOVERNED_ORG_RESOLUTION, compileOrgShadowPredicate, createOrgRulePart
|
|
|
4
4
|
export function ruleSourceOf(scope) {
|
|
5
5
|
return scope.kind === "global" ? "user" : scope.kind;
|
|
6
6
|
}
|
|
7
|
+
export function ownerStoreOf(provider, principal, localOwnerDeclared) {
|
|
8
|
+
const anonymous = principal === undefined || principal === "";
|
|
9
|
+
return anonymous && localOwnerDeclared && provider.forLocalOwner !== undefined ? provider.forLocalOwner() : provider.forPrincipal(anonymous ? undefined : principal);
|
|
10
|
+
}
|
|
7
11
|
function usableSessionId(sessionId) {
|
|
8
12
|
return typeof sessionId === "string" && sessionId !== "" ? sessionId : undefined;
|
|
9
13
|
}
|
|
@@ -61,16 +65,22 @@ class UnifiedPermissionRuleStore {
|
|
|
61
65
|
this.readSessionRows(usableSessionId(query?.sessionId), disclosures),
|
|
62
66
|
]);
|
|
63
67
|
const orgResolution = orgSettled;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
}
|
|
68
|
+
const unreadable = [];
|
|
69
|
+
const unreadableSources = [];
|
|
67
70
|
if (!durableSettled.ok) {
|
|
68
|
-
|
|
71
|
+
unreadable.push(errText(durableSettled.e));
|
|
72
|
+
unreadableSources.push("user", "project");
|
|
73
|
+
}
|
|
74
|
+
if ("unreadable" in sessionRows) {
|
|
75
|
+
unreadable.push(sessionRows.unreadable);
|
|
76
|
+
unreadableSources.push("session");
|
|
69
77
|
}
|
|
70
|
-
const
|
|
78
|
+
const failed = unreadable.length > 0 ? { unreadable: unreadable.join("; "), unreadableSources } : {};
|
|
79
|
+
const durableRead = durableSettled.ok ? durableSettled.v : EMPTY_DURABLE;
|
|
80
|
+
const sessionRead = "unreadable" in sessionRows ? [] : sessionRows;
|
|
71
81
|
const shadowed = compileOrgShadowPredicate(orgResolution.rules);
|
|
72
82
|
const rules = [];
|
|
73
|
-
for (const r of [...
|
|
83
|
+
for (const r of [...sessionRead, ...durableRead.rules]) {
|
|
74
84
|
rules.push({ ...r, source: ruleSourceOf(r.scope), status: r.behavior === "allow" && shadowed(r) ? "shadowed-by-org" : "live" });
|
|
75
85
|
}
|
|
76
86
|
const removed = [];
|
|
@@ -83,7 +93,7 @@ class UnifiedPermissionRuleStore {
|
|
|
83
93
|
continue;
|
|
84
94
|
removed.push({ behavior: t.behavior, rule: t.rule, scope: t.scope, source: ruleSourceOf(t.scope), status: "removed" });
|
|
85
95
|
}
|
|
86
|
-
return { rules, removed, org: orgResolution, rev: durableRead.rev, disclosures };
|
|
96
|
+
return { rules, removed, org: orgResolution, rev: durableRead.rev, disclosures, ...failed };
|
|
87
97
|
}
|
|
88
98
|
async readSessionRows(sessionId, disclosures) {
|
|
89
99
|
if (this.session === undefined || sessionId === undefined)
|
|
@@ -47,7 +47,8 @@
|
|
|
47
47
|
*/
|
|
48
48
|
import type { NamedToolPolicy } from "./tool-policy.js";
|
|
49
49
|
import { parsePermissionRule, type ParsedPermissionRule } from "./permission-rule-syntax.js";
|
|
50
|
-
import { type PathRuleBases } from "./permission-rule-model.js";
|
|
50
|
+
import { type ParsedRule, type PathRuleBases } from "./permission-rule-model.js";
|
|
51
|
+
import type { DenyFace, PathRuleDenyRow } from "../tools/fs/read-deny.js";
|
|
51
52
|
export { parsePermissionRule, type ParsedPermissionRule };
|
|
52
53
|
/** [K-PLATFORM-SWEEP] MINOR#11: Bash's REAL non-primary arg names, accepted as generic param rules
|
|
53
54
|
* (`Bash(timeout:...)`). Exported SOLELY so a lockstep test can assert it against the live Bash tool
|
|
@@ -190,6 +191,27 @@ export interface RuleFaceView {
|
|
|
190
191
|
export declare function catalogRuleFaceView(toolName: string): RuleFaceView | undefined;
|
|
191
192
|
/** Exported for the lockstep guard only (the scalar vocabulary of a catalog tool) — not part of the rule DSL. */
|
|
192
193
|
export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
|
|
194
|
+
/**
|
|
195
|
+
* The path DENY rules that speak for a FACE rather than for one tool (the reference implementation's reading: a Read
|
|
196
|
+
* deny rule covers every built-in tool that reads a file and the shell's file commands; an Edit deny rule every tool
|
|
197
|
+
* that edits one; a Read deny also refuses writing the same path). Such a rule reaches any call whose declared path
|
|
198
|
+
* target is on one of its faces — the write tools, the read tools, the search roots — and it is exported to the
|
|
199
|
+
* engine's deny tables (`ToolPolicy.pathDenyRules`) so the shell's reads and writes and the
|
|
200
|
+
* traversals meet it too. A path rule on any other tool keeps its tool's name as its reach (`Write(<path>)` is judged on
|
|
201
|
+
* the Write tool, a stricter reading the reference does not make).
|
|
202
|
+
*/
|
|
203
|
+
export declare const PATH_DENY_RULE_FACES: Readonly<Record<string, readonly DenyFace[]>>;
|
|
204
|
+
/**
|
|
205
|
+
* The deny-table row of ONE face-keyed path deny rule, resolved against `bases` — the row's reach IS the rule's (the same
|
|
206
|
+
* parsed rule, the same bases). Every source of such rows builds them here: a policy's compiled rules, and the person's
|
|
207
|
+
* persisted store rows. The caller holds the bases the pattern needs (`ruleBasesNeeded`): a row whose base is missing
|
|
208
|
+
* reads `unreadable`, which this row treats as a reach — a deny row that cannot be judged holds.
|
|
209
|
+
*/
|
|
210
|
+
export declare function pathRuleDenyRowOf(rule: {
|
|
211
|
+
readonly parsed: ParsedRule;
|
|
212
|
+
readonly ruleText: string;
|
|
213
|
+
readonly faces: readonly DenyFace[];
|
|
214
|
+
}, bases: PathRuleBases): PathRuleDenyRow;
|
|
193
215
|
/** Dry-run 校验/迁移报告:不 throw,返回全部 issues(含 `unsupported.*` 分类,
|
|
194
216
|
* 供从 CC settings 迁移的部署逐条对照)。 */
|
|
195
217
|
export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps" | "ruleFaces" | "pathBases">): PermissionRuleIssue[];
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
|
|
2
2
|
import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
|
|
3
3
|
import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
|
|
4
|
-
import { PATH_RULE_BASE_LABEL, isUsablePathBase, parseRuleText, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
|
|
5
|
-
import { pathFamilyOf } from "../tools/fs/safety.js";
|
|
4
|
+
import { PATH_RULE_BASE_LABEL, READ_RULE_TOOL, isUsablePathBase, parseRuleText, pathRuleHeadOf, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
|
|
5
|
+
import { pathFamilyOf, pathIdentityOf, spellingOfIdentity } from "../tools/fs/safety.js";
|
|
6
6
|
import { effectivePathTargetOf } from "./effective-path-target.js";
|
|
7
7
|
import { protectivePathTargetOf } from "./tool-registry.js";
|
|
8
8
|
import { ruleDenialSentence } from "./rule-denial-sentence.js";
|
|
9
|
+
import { ruleNamesOf } from "./tool-roster.js";
|
|
9
10
|
export { parsePermissionRule };
|
|
10
11
|
export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
|
|
11
12
|
const DEFAULT_CAPS = {
|
|
@@ -81,6 +82,27 @@ export const fileToolParamVocabulary = (canonicalTool) => {
|
|
|
81
82
|
const face = catalogRuleFaceOf(canonicalTool);
|
|
82
83
|
return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
|
|
83
84
|
};
|
|
85
|
+
export const PATH_DENY_RULE_FACES = Object.freeze({ [READ_RULE_TOOL]: ["read", "write"], Edit: ["write"] });
|
|
86
|
+
export function pathRuleDenyRowOf(rule, bases) {
|
|
87
|
+
const family = pathFamilyOf(bases);
|
|
88
|
+
const tree = bases.root ?? bases.cwd;
|
|
89
|
+
const head = pathRuleHeadOf(rule.parsed, bases);
|
|
90
|
+
return {
|
|
91
|
+
rule: rule.ruleText,
|
|
92
|
+
tool: rule.parsed.tool,
|
|
93
|
+
pattern: parsePermissionRule(rule.ruleText).ruleContent ?? "",
|
|
94
|
+
faces: rule.faces,
|
|
95
|
+
reaches: (absolutePath) => {
|
|
96
|
+
const id = pathIdentityOf(absolutePath, family, tree);
|
|
97
|
+
return id.ok && pathRuleReachOf(rule.parsed, id.identity, bases).reach !== "not_reached";
|
|
98
|
+
},
|
|
99
|
+
...(head !== undefined ? { head: spellingOfIdentity(head, family) } : {}),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function callPathFaceOf(req) {
|
|
103
|
+
const target = protectivePathTargetOf(req);
|
|
104
|
+
return target === undefined ? undefined : target.access === "read" ? "read" : "write";
|
|
105
|
+
}
|
|
84
106
|
function countStars(s) {
|
|
85
107
|
let n = 0;
|
|
86
108
|
for (const c of s)
|
|
@@ -196,7 +218,8 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
196
218
|
}
|
|
197
219
|
if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
|
|
198
220
|
continue;
|
|
199
|
-
|
|
221
|
+
const faces = r.behavior === "deny" ? PATH_DENY_RULE_FACES[toolName] : undefined;
|
|
222
|
+
entry.content[r.behavior].push({ parsed: parsedPath.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}), ...(faces !== undefined ? { faces } : {}) });
|
|
200
223
|
byTool.set(toolName, entry);
|
|
201
224
|
continue;
|
|
202
225
|
}
|
|
@@ -317,6 +340,8 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
317
340
|
}
|
|
318
341
|
const coveringHit = (lane, toolName) => lane.length === 0 ? undefined : lane.find((r) => namespacedRuleNameCovers(r.ruleName, toolName));
|
|
319
342
|
const hasCovering = namespacedCovering.deny.length > 0 || namespacedCovering.ask.length > 0 || namespacedCovering.allow.length > 0;
|
|
343
|
+
const faceDenyRules = [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined));
|
|
344
|
+
const pathDenyRules = faceDenyRules.map((r) => pathRuleDenyRowOf({ parsed: r.parsed, ruleText: r.ruleText, faces: r.faces }, pathBases));
|
|
320
345
|
const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
|
|
321
346
|
const contentHit = (lane, req) => {
|
|
322
347
|
if (lane.length === 0)
|
|
@@ -349,16 +374,36 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
349
374
|
}
|
|
350
375
|
return unreadable;
|
|
351
376
|
};
|
|
352
|
-
const unreadableAsk = (hit, req) => ({
|
|
377
|
+
const unreadableAsk = (hit, lane, req) => ({
|
|
353
378
|
action: "ask",
|
|
354
379
|
message: `tool "${req.toolName}" needs approval: the call could not be read against the permission rule ${hit.rule.ruleText} (${hit.unreadable}) — a person must decide`,
|
|
355
|
-
|
|
380
|
+
ruleUnreadable: lane,
|
|
356
381
|
requiresRealApproval: true,
|
|
357
382
|
});
|
|
383
|
+
const entryFor = (req) => {
|
|
384
|
+
const entries = ruleNamesOf(req).flatMap((n) => {
|
|
385
|
+
const e = byTool.get(n);
|
|
386
|
+
return e !== undefined ? [e] : [];
|
|
387
|
+
});
|
|
388
|
+
if (entries.length <= 1)
|
|
389
|
+
return entries[0];
|
|
390
|
+
const bare = {};
|
|
391
|
+
for (const b of ["deny", "ask", "allow"]) {
|
|
392
|
+
const hit = entries.find((e) => e.bare[b] !== undefined)?.bare[b];
|
|
393
|
+
if (hit !== undefined)
|
|
394
|
+
bare[b] = hit;
|
|
395
|
+
}
|
|
396
|
+
return {
|
|
397
|
+
bare,
|
|
398
|
+
param: { deny: entries.flatMap((e) => e.param.deny), ask: entries.flatMap((e) => e.param.ask) },
|
|
399
|
+
content: { deny: entries.flatMap((e) => e.content.deny), ask: entries.flatMap((e) => e.content.ask) },
|
|
400
|
+
};
|
|
401
|
+
};
|
|
358
402
|
return {
|
|
359
403
|
nameSets: [nameSets],
|
|
404
|
+
...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
|
|
360
405
|
check(req) {
|
|
361
|
-
const entry =
|
|
406
|
+
const entry = entryFor(req);
|
|
362
407
|
const covering = hasCovering && protocolOf(req.toolName) !== undefined;
|
|
363
408
|
if (entry?.bare.deny) {
|
|
364
409
|
return { action: "deny", message: ruleDenial(req, entry.bare.deny.ruleText, entry.bare.deny.source) };
|
|
@@ -368,7 +413,9 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
368
413
|
if (coveringDeny)
|
|
369
414
|
return { action: "deny", message: ruleDenial(req, coveringDeny.ruleText, coveringDeny.source) };
|
|
370
415
|
}
|
|
371
|
-
const
|
|
416
|
+
const callFace = faceDenyRules.length > 0 ? callPathFaceOf(req) : undefined;
|
|
417
|
+
const denyLane = [...(entry?.content.deny.filter((r) => r.faces === undefined) ?? []), ...(callFace !== undefined ? faceDenyRules.filter((r) => r.faces.includes(callFace)) : [])];
|
|
418
|
+
const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
|
|
372
419
|
if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
|
|
373
420
|
return { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
|
|
374
421
|
}
|
|
@@ -379,7 +426,7 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
379
426
|
}
|
|
380
427
|
}
|
|
381
428
|
if (contentDeny?.unreadable !== undefined)
|
|
382
|
-
return unreadableAsk({ rule: contentDeny.rule, unreadable: contentDeny.unreadable }, req);
|
|
429
|
+
return unreadableAsk({ rule: contentDeny.rule, unreadable: contentDeny.unreadable }, "content_deny", req);
|
|
383
430
|
if (entry) {
|
|
384
431
|
if (entry.bare.ask) {
|
|
385
432
|
return { action: "ask", message: ruleAskMessage(entry.bare.ask.ruleText, entry.bare.ask.source), matchedAskRule: entry.bare.ask.ruleText };
|
|
@@ -394,7 +441,7 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
394
441
|
const contentAsk = contentHit(entry.content.ask, req);
|
|
395
442
|
if (contentAsk !== undefined) {
|
|
396
443
|
if (contentAsk.unreadable !== undefined)
|
|
397
|
-
return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, req);
|
|
444
|
+
return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, "content_ask", req);
|
|
398
445
|
return { action: "ask", message: ruleAskMessage(contentAsk.rule.ruleText, contentAsk.rule.source), matchedAskRule: contentAsk.rule.ruleText };
|
|
399
446
|
}
|
|
400
447
|
const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
|
|
@@ -12,16 +12,20 @@
|
|
|
12
12
|
* (mandates, the org latch, the marked-call seat) rather than a function of the answer alone.
|
|
13
13
|
*/
|
|
14
14
|
import type { PermissionResult } from "./tool-policy.js";
|
|
15
|
-
import type { PersistedRuleHit, PersistedRuleHitRule } from "./hooks.js";
|
|
15
|
+
import type { PersistedRuleAnswer, PersistedRuleHit, PersistedRuleHitRule } from "./hooks.js";
|
|
16
16
|
/** The lane's one read, normalized: at most one of `hit` / `unreadable` / `coverage` (the gate's reader
|
|
17
17
|
* produces it; the arms below read it). */
|
|
18
18
|
export interface PersistedRuleRead {
|
|
19
19
|
readonly hit?: PersistedRuleHit;
|
|
20
|
+
/** The lane could not answer: with `reason`, the CALL could not be read against the person's deny/ask rows;
|
|
21
|
+
* without, the store could not be read — its rows take no part in the decision (the record says so). */
|
|
20
22
|
readonly unreadable?: true;
|
|
21
|
-
/** The lexer's reason when the CALL could not be read against the person's deny/ask rows
|
|
22
|
-
* a store read failure) — the message says which it was; the decision is the same. */
|
|
23
|
+
/** The lexer's reason when the CALL could not be read against the person's deny/ask rows. */
|
|
23
24
|
readonly reason?: string;
|
|
24
25
|
}
|
|
26
|
+
/** The answers of one store read judged under each of a call's names, folded as one table folds its rows (deny >
|
|
27
|
+
* unreadable > ask > allow > coverage > nothing); among equals the first (the wire name's) stands. */
|
|
28
|
+
export declare function strongestAnswer(answers: readonly PersistedRuleAnswer[]): PersistedRuleAnswer;
|
|
25
29
|
/** Display form of a rule set (design/375 §5.1): every rule text is a DISPLAY value on a human trust
|
|
26
30
|
* boundary, and the set must stay readable however large a lane makes it — each member rides
|
|
27
31
|
* `inlineUntrusted` (200 cap, the same cap the durable leg's `RiskDescriptor.shadowedRule` carries),
|
|
@@ -45,14 +49,14 @@ export declare function disclosedRuleSet(rules: readonly PersistedRuleHitRule[])
|
|
|
45
49
|
* ask, the marker is stamped if absent and the message names the rule — the standing question
|
|
46
50
|
* outranks a remembered yes across lanes, exactly as the DSL's own order says within one; a deny
|
|
47
51
|
* stays a deny.
|
|
48
|
-
* · UNREADABLE — the
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* allow tightens to an ask carrying `requiresRealApproval` (no automatic lane clears it; the gate
|
|
52
|
+
* · UNREADABLE CALL — the store was read, but the CALL could not be read against the person's standing
|
|
53
|
+
* deny/ask rows (`reason` carries the lexer's word: an expansion where a rule reads a word, an
|
|
54
|
+
* unterminated quote, a syntax error): whether a row forbids this call is unknown, so it fails CLOSED —
|
|
55
|
+
* an allow tightens to an ask carrying `requiresRealApproval` (no automatic lane clears it; the gate
|
|
53
56
|
* derives the origin `rule_store_unavailable` from its own record of the read), an ask acquires the
|
|
54
|
-
* bit, a deny stays a deny.
|
|
55
|
-
*
|
|
57
|
+
* bit, a deny stays a deny.
|
|
58
|
+
* A store that could not be READ (`unreadable` without a `reason`) contributes no row: the decision is returned
|
|
59
|
+
* untouched, as for no hit — the lane announces the skipped store, and the rows apply again once a read succeeds.
|
|
56
60
|
* An allow hit (or no hit) returns the decision untouched: the allow arm is the station's.
|
|
57
61
|
*/
|
|
58
62
|
export declare function applyPersistedTightening(decision: PermissionResult, read: PersistedRuleRead | undefined): {
|
|
@@ -1,20 +1,27 @@
|
|
|
1
|
+
import { decisionAskClassesOf } from "./ask-origin.js";
|
|
1
2
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
3
|
+
export function strongestAnswer(answers) {
|
|
4
|
+
const rank = (a) => (a === undefined ? 0 : "unreadable" in a ? 4 : "behavior" in a ? { deny: 5, ask: 3, allow: 2 }[a.behavior] : 1);
|
|
5
|
+
return answers.reduce((best, a) => (rank(a) > rank(best) ? a : best), undefined);
|
|
6
|
+
}
|
|
2
7
|
export function disclosedRuleSet(rules) {
|
|
3
8
|
const shown = rules.slice(0, 5).map((r) => inlineUntrusted(r.rule, 200));
|
|
4
9
|
const rest = rules.length - shown.length;
|
|
5
10
|
return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
|
|
6
11
|
}
|
|
7
12
|
export function applyPersistedTightening(decision, read) {
|
|
8
|
-
if (read?.unreadable === true) {
|
|
13
|
+
if (read?.unreadable === true && read.reason !== undefined) {
|
|
9
14
|
if (decision.action === "deny")
|
|
10
15
|
return { decision };
|
|
11
|
-
const message = read.reason
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
const message = `this call could not be read against the person's standing deny/ask rules (${read.reason}) — whether one of them forbids it is unknown, so it needs a real approval`;
|
|
17
|
+
const unreadableCall = { ruleUnreadable: "persisted_call" };
|
|
18
|
+
const standingFence = decision.action === "ask" && decisionAskClassesOf({ ...decision, foldedAskClasses: undefined }).has("policy_rra")
|
|
19
|
+
? { foldedAskClasses: [...(decision.foldedAskClasses ?? []), "policy_rra"] }
|
|
20
|
+
: {};
|
|
14
21
|
return {
|
|
15
22
|
decision: decision.action === "allow"
|
|
16
|
-
? { action: "ask", message, decisionReason: "persisted_rule", requiresRealApproval: true, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) }
|
|
17
|
-
: { ...decision, requiresRealApproval: true, message: `${decision.message !== undefined ? `${decision.message} ` : ""}(${message})` },
|
|
23
|
+
? { action: "ask", message, decisionReason: "persisted_rule", requiresRealApproval: true, ...unreadableCall, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) }
|
|
24
|
+
: { ...decision, requiresRealApproval: true, ...unreadableCall, ...standingFence, message: `${decision.message !== undefined ? `${decision.message} ` : ""}(${message})` },
|
|
18
25
|
tightened: "unreadable",
|
|
19
26
|
};
|
|
20
27
|
}
|
|
@@ -13,7 +13,7 @@ export declare const ENTER_PLAN_MODE_TOOL_NAME = "EnterPlanMode";
|
|
|
13
13
|
* making enter execute (set the flag) BEFORE the sibling edit is prepared+gated — closing the same-batch window.
|
|
14
14
|
* Exit is via `present_plan` (→ plan_review → human approve-resume), not by this tool.
|
|
15
15
|
*/
|
|
16
|
-
export declare function createEnterPlanModeTool(enterPlanMode: () => void): ToolSpec;
|
|
16
|
+
export declare function createEnterPlanModeTool(enterPlanMode: () => Promise<void> | void): ToolSpec;
|
|
17
17
|
/**
|
|
18
18
|
* design/80 D-B — the first-party `present_plan` tool (CC `ExitPlanMode` parity). A THIN composition over the
|
|
19
19
|
* general `ctx.requestReview()` primitive: the model calls it with a plan; the tool returns the plan as its
|
|
@@ -3,7 +3,7 @@ import { delimitUntrusted } from "./untrusted-text.js";
|
|
|
3
3
|
import { toolFace } from "./tool-catalog-entries.js";
|
|
4
4
|
export const PRESENT_PLAN_TOOL_NAME = "ExitPlanMode";
|
|
5
5
|
export const ENTER_PLAN_MODE_TOOL_NAME = "EnterPlanMode";
|
|
6
|
-
const ENTER_PLAN_MODE_DESCRIPTION = `Enter PLAN MODE (read-only): stop making changes and switch to exploration/planning. For the rest of this run, write/edit/mutating tool
|
|
6
|
+
const ENTER_PLAN_MODE_DESCRIPTION = `Enter PLAN MODE (read-only): stop making changes and switch to exploration/planning. For the rest of this run, every write/edit/mutating tool call ASKS the user first ("Cannot write to <path> while in plan mode."; where nobody can answer, it is refused) while read-only tools (Read, Grep, Glob, etc.) still work. When your plan is ready, call ${PRESENT_PLAN_TOOL_NAME} with it — the run PAUSES for a human to approve it, then continues (read-only lifted) so you can execute.
|
|
7
7
|
|
|
8
8
|
Use this tool proactively when you're about to start a non-trivial implementation task. If you would use AskUserQuestion to clarify the approach, use EnterPlanMode instead — getting alignment on the plan itself is the point.
|
|
9
9
|
|
|
@@ -37,8 +37,8 @@ export function createEnterPlanModeTool(enterPlanMode) {
|
|
|
37
37
|
parameters: Type.Object({}),
|
|
38
38
|
executionMode: "sequential",
|
|
39
39
|
execute: async () => {
|
|
40
|
-
enterPlanMode();
|
|
41
|
-
const message = "Entered plan mode (read-only).
|
|
40
|
+
await enterPlanMode();
|
|
41
|
+
const message = "Entered plan mode (read-only). Every write/edit/mutating tool call now asks the user first for the rest of this run; " +
|
|
42
42
|
`research with read-only tools, then call ${PRESENT_PLAN_TOOL_NAME} with your plan to get it approved before acting.`;
|
|
43
43
|
return {
|
|
44
44
|
content: message,
|
|
@@ -1,19 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The READ-ONLY shell reader — "does this command line provably only READ?" — the allow-layer member
|
|
3
|
-
* the tool gate consults for the shell tool after every tightening lane and after the person's own allow
|
|
4
|
-
* rules, and before the classifier or a person is asked (`gate-lanes.ts`). Upstream (CC 2.1.250) has the
|
|
5
|
-
* same station: its shell tool's `isReadOnly` runs the command through a parser, splits it into its
|
|
6
|
-
* simple commands, and admits the whole line only when EVERY simple command is read-only by its tables;
|
|
7
|
-
* this module is that reading over this engine's own lexer ({@link readShellCommand}, the tightening
|
|
8
|
-
* reader's) and the transcribed tables (`read-only-shell-table.ts`).
|
|
9
|
-
*
|
|
10
|
-
* The reading itself is the contract on {@link readOnlyShellVerdict}.
|
|
11
|
-
*
|
|
12
|
-
* What the verdict is NOT: a security boundary of its own. The deny/ask lanes run before it and win; the
|
|
13
|
-
* mandated asks (an operator's per-call shell confirmation, a tool's own egress/irreversibility marks, an
|
|
14
|
-
* explicit ask rule, a hook's ask, governance) are never cleared by it — the gate's allow-layer predicate
|
|
15
|
-
* decides where it may speak, exactly as it decides for a person's stored allow rule.
|
|
16
|
-
*/
|
|
17
1
|
import { type ShellCommandShape } from "./shell-lexer.js";
|
|
18
2
|
/** The reader's answer: read-only, or not — with the first reason the reading stopped on (a sentence a
|
|
19
3
|
* trace or a card can show; never adjudication input). */
|
|
@@ -23,6 +7,14 @@ export type ReadOnlyShellVerdict = {
|
|
|
23
7
|
readonly readOnly: false;
|
|
24
8
|
readonly reason: string;
|
|
25
9
|
};
|
|
10
|
+
/** The reference implementation's question for a compound that changes directory and runs git (2.1.280, `cd-git-compound`). */
|
|
11
|
+
export declare const CD_GIT_COMPOUND_SENTENCE = "This command changes directory before running a version-control command, which can pick up untrusted hooks or repository configuration from the target directory. Approve only if you trust it.";
|
|
12
|
+
/**
|
|
13
|
+
* Does this line change directory and run git — a `cd` beside a `git` — other than by ONE plain `cd <path>` that lands
|
|
14
|
+
* where the session stands (`cwd`)? The reference's no-op reading, lexical here: the path is `.` / `..`, starts `./` /
|
|
15
|
+
* `../` / `/`, carries no expansion, quote-removed text only, and resolves to `cwd` itself. No `cwd` ⇒ no no-op.
|
|
16
|
+
*/
|
|
17
|
+
export declare function changesDirectoryBeforeGit(shape: ShellCommandShape, cwd?: string): boolean;
|
|
26
18
|
/**
|
|
27
19
|
* Read `command` as the allow layer must: read-only, or the first reason it is not. A shape already read by
|
|
28
20
|
* the caller may be handed in (the gate reads every shell command once).
|
|
@@ -36,7 +28,15 @@ export type ReadOnlyShellVerdict = {
|
|
|
36
28
|
* admitted only under {@link READ_ONLY_GLOB_PROGRAMS}; else the structural reading (the bare-program set,
|
|
37
29
|
* the exact forms, `echo`/`ls`/`cd`/`find`/`printf`/`history`/`arch`/`ifconfig`); else the flag-vetted
|
|
38
30
|
* table with its per-row danger predicates, and the `uniq`/`jq` forms. A `cd` beside a `git` in one line
|
|
39
|
-
* is never read-only (the git command's repository is then not the session's)
|
|
40
|
-
* not positively admit is NOT read-only — the
|
|
31
|
+
* is never read-only (the git command's repository is then not the session's) unless the `cd` lands where the session
|
|
32
|
+
* stands ({@link changesDirectoryBeforeGit}). Anything the reading does not positively admit is NOT read-only — the
|
|
33
|
+
* answer is never a guess, and a `false` costs one question.
|
|
34
|
+
*/
|
|
35
|
+
export declare function readOnlyShellVerdict(command: string, shape?: ShellCommandShape, cwd?: string): ReadOnlyShellVerdict;
|
|
36
|
+
/**
|
|
37
|
+
* The reader's LINE-level objection to `command` — what makes no segment of it read-only in its place, whatever the
|
|
38
|
+
* segment reads on its own: the line's structure, or a `cd` beside a `git` ({@link changesDirectoryBeforeGit}). A caller
|
|
39
|
+
* that judges a compound's segments one by one ({@link readOnlyShellVerdict} over a segment's text) asks this of the
|
|
40
|
+
* line first.
|
|
41
41
|
*/
|
|
42
|
-
export declare function
|
|
42
|
+
export declare function readOnlyLineObjection(command: string, cwd?: string): string | undefined;
|