@sema-agent/core 7.33.8 → 7.34.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 +339 -0
- 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/open-responses.js +1 -0
- package/dist/brain/openai.js +1 -0
- package/dist/brain/stream-engine.d.ts +3 -0
- package/dist/brain/stream-engine.js +2 -1
- package/dist/core/a2a.d.ts +3 -1
- package/dist/core/a2a.js +1 -1
- package/dist/core/ask-origin.d.ts +351 -154
- package/dist/core/ask-origin.js +223 -45
- 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 +50 -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 +17 -1
- package/dist/core/gate-lanes.js +111 -46
- package/dist/core/gate-outcome.d.ts +40 -7
- package/dist/core/gate-outcome.js +26 -4
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +12 -0
- package/dist/core/hooks.d.ts +50 -29
- package/dist/core/hooks.js +1 -3
- package/dist/core/mcp.js +1 -1
- package/dist/core/mode-policy.d.ts +182 -0
- package/dist/core/mode-policy.js +179 -0
- package/dist/core/oracle-isolation.d.ts +3 -3
- package/dist/core/permission-rule-model.d.ts +16 -3
- package/dist/core/permission-rule-model.js +36 -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 +35 -8
- package/dist/core/persisted-rule-arms.d.ts +10 -9
- package/dist/core/persisted-rule-arms.js +9 -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/classifier-ladder.d.ts +43 -1
- package/dist/core/runner/classifier-ladder.js +29 -0
- package/dist/core/runner/contracts.d.ts +37 -35
- package/dist/core/runner/denial-limit-arms.d.ts +30 -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/gate-exit.d.ts +30 -4
- package/dist/core/runner/gate-exit.js +28 -7
- package/dist/core/runner/permission-rule-lanes.d.ts +9 -5
- package/dist/core/runner/permission-rule-lanes.js +44 -40
- 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 +5 -3
- package/dist/core/runner/prepare-ask-lane.js +1 -1
- 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-gate-stations.d.ts +33 -6
- package/dist/core/runner/prepare-gate-stations.js +32 -19
- 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 +22 -22
- package/dist/core/runner/prepare-inherited-gate.js +37 -23
- package/dist/core/runner/prepare-park-ask.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.js +2 -1
- package/dist/core/runner/prepare-policy-chain.d.ts +22 -2
- package/dist/core/runner/prepare-policy-chain.js +39 -12
- 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 +51 -22
- 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-identity-wiring.js +1 -1
- package/dist/core/runner/run-leg.js +24 -15
- package/dist/core/runner/run-notification-lane.js +1 -0
- 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.js +3 -0
- 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/sensitive-path-policy.d.ts +4 -2
- package/dist/core/sensitive-path-policy.js +39 -30
- 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 +54 -25
- package/dist/core/tool-policy.js +43 -2
- package/dist/core/tool-registry.d.ts +2 -0
- package/dist/core/tool-registry.js +3 -0
- package/dist/core/tool-spec.d.ts +8 -9
- package/dist/core/trace.d.ts +7 -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 +3 -0
- package/dist/engine/execution-env/node-execution-env.js +11 -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/index.d.ts +15 -5
- package/dist/index.js +12 -3
- 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/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 +49 -0
- package/dist/tools/fs/file-read-check.js +71 -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 +42 -11
- package/dist/tools/fs/shell-read-check.js +188 -13
- 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 +103 -11
- package/wire-changes/core@7.34.0.json +2621 -0
|
@@ -6,6 +6,7 @@ import { inlineUntrusted } from "./untrusted-text.js";
|
|
|
6
6
|
import { pathTargetOf } from "./tool-registry.js";
|
|
7
7
|
import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
|
|
8
8
|
import { readShellCommand } from "./shell-lexer.js";
|
|
9
|
+
import { readOnlyLineObjection, readOnlyShellVerdict } from "./read-only-shell.js";
|
|
9
10
|
export const RULE_BEHAVIORS = ["deny", "ask", "allow"];
|
|
10
11
|
const RULE_BEHAVIOR_SET = new Set(RULE_BEHAVIORS);
|
|
11
12
|
export function isRuleBehavior(v) {
|
|
@@ -450,6 +451,29 @@ export function pathRuleReachOf(rule, target, bases) {
|
|
|
450
451
|
const segments = target.split("/").filter((sg) => sg !== "");
|
|
451
452
|
return globSegmentsReach(globs, segments) ? REACHED : NOT_REACHED;
|
|
452
453
|
}
|
|
454
|
+
export function pathRuleHeadOf(rule, bases) {
|
|
455
|
+
const family = pathFamilyOf(bases);
|
|
456
|
+
const tree = bases.root ?? bases.cwd;
|
|
457
|
+
let head;
|
|
458
|
+
if (rule.match === "subpath") {
|
|
459
|
+
if (carriesDotSegment(rule.command, family))
|
|
460
|
+
return undefined;
|
|
461
|
+
const dir = pathIdentityOf(rule.command, family, tree);
|
|
462
|
+
return dir.ok && isIdentityDir(dir.identity, family) ? dir.identity : undefined;
|
|
463
|
+
}
|
|
464
|
+
else if (rule.match === "path") {
|
|
465
|
+
const r = resolvePathPattern(rule.command, bases, rule.behavior);
|
|
466
|
+
if ("missingBase" in r)
|
|
467
|
+
return undefined;
|
|
468
|
+
const parts = r.resolved.split("/");
|
|
469
|
+
const wild = parts.findIndex((part) => part.includes("*"));
|
|
470
|
+
head = (wild === -1 ? parts : parts.slice(0, wild)).join("/");
|
|
471
|
+
}
|
|
472
|
+
else
|
|
473
|
+
return undefined;
|
|
474
|
+
const id = pathIdentityOf(head === "" ? "/" : head, family, tree);
|
|
475
|
+
return id.ok ? id.identity : undefined;
|
|
476
|
+
}
|
|
453
477
|
function hasUnescapedStar(s) {
|
|
454
478
|
for (let i = 0; i < s.length; i++) {
|
|
455
479
|
if (s[i] !== "*")
|
|
@@ -765,15 +789,22 @@ export function findAdmittingRule(rules, call) {
|
|
|
765
789
|
return lone !== undefined ? [lone] : undefined;
|
|
766
790
|
}
|
|
767
791
|
const covering = [];
|
|
792
|
+
const lineObjection = readOnlyLineObjection(call.command, call.execCwd ?? call.cwd);
|
|
768
793
|
for (const [i, segment] of shape.segments.entries()) {
|
|
769
794
|
const dir = cdDirs[i];
|
|
770
795
|
const first = firstEligibleAdmittingRule(rules, segment, call) ?? (dir !== undefined ? firstEligibleDirectoryRule(rules, dir, call) : undefined);
|
|
771
|
-
if (first === undefined)
|
|
796
|
+
if (first === undefined) {
|
|
797
|
+
if (readsOnlyAsSegment(segment, lineObjection))
|
|
798
|
+
continue;
|
|
772
799
|
return undefined;
|
|
800
|
+
}
|
|
773
801
|
if (!covering.includes(first))
|
|
774
802
|
covering.push(first);
|
|
775
803
|
}
|
|
776
|
-
return covering;
|
|
804
|
+
return covering.length > 0 ? covering : undefined;
|
|
805
|
+
}
|
|
806
|
+
function readsOnlyAsSegment(segment, lineObjection) {
|
|
807
|
+
return lineObjection === undefined && !movesShellCursor(segment) && readOnlyShellVerdict(segment).readOnly;
|
|
777
808
|
}
|
|
778
809
|
function firstEligibleDirectoryRule(rules, directory, call) {
|
|
779
810
|
for (const rule of rules) {
|
|
@@ -855,11 +886,13 @@ export function segmentCoverageOf(command, rules, call) {
|
|
|
855
886
|
return undefined;
|
|
856
887
|
const proposed = rules.proposed ?? [];
|
|
857
888
|
const cdDirs = resolveCdSegmentDirectories(shape.segments, call.execCwd ?? call.cwd);
|
|
889
|
+
const lineObjection = readOnlyLineObjection(folded, call.execCwd ?? call.cwd);
|
|
858
890
|
return shape.segments.map((segment, i) => {
|
|
859
891
|
const dir = cdDirs[i];
|
|
860
892
|
return {
|
|
861
893
|
segment: segment.trim(),
|
|
862
|
-
covered:
|
|
894
|
+
covered: readsOnlyAsSegment(segment, lineObjection) ||
|
|
895
|
+
firstEligibleAdmittingRule(rules.persisted, segment, call) !== undefined ||
|
|
863
896
|
(dir !== undefined && firstEligibleDirectoryRule(rules.persisted, dir, call) !== undefined) ||
|
|
864
897
|
proposed.some((p) => p.rule.tool === "Read"
|
|
865
898
|
? 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,8 +1,8 @@
|
|
|
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";
|
|
@@ -81,6 +81,27 @@ export const fileToolParamVocabulary = (canonicalTool) => {
|
|
|
81
81
|
const face = catalogRuleFaceOf(canonicalTool);
|
|
82
82
|
return face?.params === undefined || pathTargetOf(canonicalTool) === undefined ? undefined : new Set(face.params);
|
|
83
83
|
};
|
|
84
|
+
export const PATH_DENY_RULE_FACES = Object.freeze({ [READ_RULE_TOOL]: ["read", "write"], Edit: ["write"] });
|
|
85
|
+
export function pathRuleDenyRowOf(rule, bases) {
|
|
86
|
+
const family = pathFamilyOf(bases);
|
|
87
|
+
const tree = bases.root ?? bases.cwd;
|
|
88
|
+
const head = pathRuleHeadOf(rule.parsed, bases);
|
|
89
|
+
return {
|
|
90
|
+
rule: rule.ruleText,
|
|
91
|
+
tool: rule.parsed.tool,
|
|
92
|
+
pattern: parsePermissionRule(rule.ruleText).ruleContent ?? "",
|
|
93
|
+
faces: rule.faces,
|
|
94
|
+
reaches: (absolutePath) => {
|
|
95
|
+
const id = pathIdentityOf(absolutePath, family, tree);
|
|
96
|
+
return id.ok && pathRuleReachOf(rule.parsed, id.identity, bases).reach !== "not_reached";
|
|
97
|
+
},
|
|
98
|
+
...(head !== undefined ? { head: spellingOfIdentity(head, family) } : {}),
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function callPathFaceOf(req) {
|
|
102
|
+
const target = protectivePathTargetOf(req);
|
|
103
|
+
return target === undefined ? undefined : target.access === "read" ? "read" : "write";
|
|
104
|
+
}
|
|
84
105
|
function countStars(s) {
|
|
85
106
|
let n = 0;
|
|
86
107
|
for (const c of s)
|
|
@@ -196,7 +217,8 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
196
217
|
}
|
|
197
218
|
if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
|
|
198
219
|
continue;
|
|
199
|
-
|
|
220
|
+
const faces = r.behavior === "deny" ? PATH_DENY_RULE_FACES[toolName] : undefined;
|
|
221
|
+
entry.content[r.behavior].push({ parsed: parsedPath.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}), ...(faces !== undefined ? { faces } : {}) });
|
|
200
222
|
byTool.set(toolName, entry);
|
|
201
223
|
continue;
|
|
202
224
|
}
|
|
@@ -317,6 +339,8 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
317
339
|
}
|
|
318
340
|
const coveringHit = (lane, toolName) => lane.length === 0 ? undefined : lane.find((r) => namespacedRuleNameCovers(r.ruleName, toolName));
|
|
319
341
|
const hasCovering = namespacedCovering.deny.length > 0 || namespacedCovering.ask.length > 0 || namespacedCovering.allow.length > 0;
|
|
342
|
+
const faceDenyRules = [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined));
|
|
343
|
+
const pathDenyRules = faceDenyRules.map((r) => pathRuleDenyRowOf({ parsed: r.parsed, ruleText: r.ruleText, faces: r.faces }, pathBases));
|
|
320
344
|
const commandParam = catalogRuleFaceOf("Bash")?.primaryParams[0] ?? "command";
|
|
321
345
|
const contentHit = (lane, req) => {
|
|
322
346
|
if (lane.length === 0)
|
|
@@ -349,14 +373,15 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
349
373
|
}
|
|
350
374
|
return unreadable;
|
|
351
375
|
};
|
|
352
|
-
const unreadableAsk = (hit, req) => ({
|
|
376
|
+
const unreadableAsk = (hit, lane, req) => ({
|
|
353
377
|
action: "ask",
|
|
354
378
|
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
|
-
|
|
379
|
+
ruleUnreadable: lane,
|
|
356
380
|
requiresRealApproval: true,
|
|
357
381
|
});
|
|
358
382
|
return {
|
|
359
383
|
nameSets: [nameSets],
|
|
384
|
+
...(pathDenyRules.length > 0 ? { pathDenyRules } : {}),
|
|
360
385
|
check(req) {
|
|
361
386
|
const entry = byTool.get(req.toolName);
|
|
362
387
|
const covering = hasCovering && protocolOf(req.toolName) !== undefined;
|
|
@@ -368,7 +393,9 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
368
393
|
if (coveringDeny)
|
|
369
394
|
return { action: "deny", message: ruleDenial(req, coveringDeny.ruleText, coveringDeny.source) };
|
|
370
395
|
}
|
|
371
|
-
const
|
|
396
|
+
const callFace = faceDenyRules.length > 0 ? callPathFaceOf(req) : undefined;
|
|
397
|
+
const denyLane = [...(entry?.content.deny.filter((r) => r.faces === undefined) ?? []), ...(callFace !== undefined ? faceDenyRules.filter((r) => r.faces.includes(callFace)) : [])];
|
|
398
|
+
const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
|
|
372
399
|
if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
|
|
373
400
|
return { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
|
|
374
401
|
}
|
|
@@ -379,7 +406,7 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
379
406
|
}
|
|
380
407
|
}
|
|
381
408
|
if (contentDeny?.unreadable !== undefined)
|
|
382
|
-
return unreadableAsk({ rule: contentDeny.rule, unreadable: contentDeny.unreadable }, req);
|
|
409
|
+
return unreadableAsk({ rule: contentDeny.rule, unreadable: contentDeny.unreadable }, "content_deny", req);
|
|
383
410
|
if (entry) {
|
|
384
411
|
if (entry.bare.ask) {
|
|
385
412
|
return { action: "ask", message: ruleAskMessage(entry.bare.ask.ruleText, entry.bare.ask.source), matchedAskRule: entry.bare.ask.ruleText };
|
|
@@ -394,7 +421,7 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
394
421
|
const contentAsk = contentHit(entry.content.ask, req);
|
|
395
422
|
if (contentAsk !== undefined) {
|
|
396
423
|
if (contentAsk.unreadable !== undefined)
|
|
397
|
-
return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, req);
|
|
424
|
+
return unreadableAsk({ rule: contentAsk.rule, unreadable: contentAsk.unreadable }, "content_ask", req);
|
|
398
425
|
return { action: "ask", message: ruleAskMessage(contentAsk.rule.ruleText, contentAsk.rule.source), matchedAskRule: contentAsk.rule.ruleText };
|
|
399
426
|
}
|
|
400
427
|
const paramAsk = matchParamRules(entry.param.ask, req.args, caps.maxScalarValueChars);
|
|
@@ -17,9 +17,10 @@ import type { PersistedRuleHit, PersistedRuleHitRule } from "./hooks.js";
|
|
|
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
|
}
|
|
25
26
|
/** Display form of a rule set (design/375 §5.1): every rule text is a DISPLAY value on a human trust
|
|
@@ -45,14 +46,14 @@ export declare function disclosedRuleSet(rules: readonly PersistedRuleHitRule[])
|
|
|
45
46
|
* ask, the marker is stamped if absent and the message names the rule — the standing question
|
|
46
47
|
* outranks a remembered yes across lanes, exactly as the DSL's own order says within one; a deny
|
|
47
48
|
* stays a deny.
|
|
48
|
-
* · UNREADABLE — the
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* allow tightens to an ask carrying `requiresRealApproval` (no automatic lane clears it; the gate
|
|
49
|
+
* · UNREADABLE CALL — the store was read, but the CALL could not be read against the person's standing
|
|
50
|
+
* deny/ask rows (`reason` carries the lexer's word: an expansion where a rule reads a word, an
|
|
51
|
+
* unterminated quote, a syntax error): whether a row forbids this call is unknown, so it fails CLOSED —
|
|
52
|
+
* an allow tightens to an ask carrying `requiresRealApproval` (no automatic lane clears it; the gate
|
|
53
53
|
* 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
|
-
*
|
|
54
|
+
* bit, a deny stays a deny.
|
|
55
|
+
* A store that could not be READ (`unreadable` without a `reason`) contributes no row: the decision is returned
|
|
56
|
+
* untouched, as for no hit — the lane announces the skipped store, and the rows apply again once a read succeeds.
|
|
56
57
|
* An allow hit (or no hit) returns the decision untouched: the allow arm is the station's.
|
|
57
58
|
*/
|
|
58
59
|
export declare function applyPersistedTightening(decision: PermissionResult, read: PersistedRuleRead | undefined): {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { decisionAskClassesOf } from "./ask-origin.js";
|
|
1
2
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
2
3
|
export function disclosedRuleSet(rules) {
|
|
3
4
|
const shown = rules.slice(0, 5).map((r) => inlineUntrusted(r.rule, 200));
|
|
@@ -5,16 +6,18 @@ export function disclosedRuleSet(rules) {
|
|
|
5
6
|
return rest > 0 ? `${shown.join(", ")} and ${rest} more` : shown.join(", ");
|
|
6
7
|
}
|
|
7
8
|
export function applyPersistedTightening(decision, read) {
|
|
8
|
-
if (read?.unreadable === true) {
|
|
9
|
+
if (read?.unreadable === true && read.reason !== undefined) {
|
|
9
10
|
if (decision.action === "deny")
|
|
10
11
|
return { decision };
|
|
11
|
-
const message = read.reason
|
|
12
|
-
|
|
13
|
-
|
|
12
|
+
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`;
|
|
13
|
+
const unreadableCall = { ruleUnreadable: "persisted_call" };
|
|
14
|
+
const standingFence = decision.action === "ask" && decisionAskClassesOf({ ...decision, foldedAskClasses: undefined }).has("policy_rra")
|
|
15
|
+
? { foldedAskClasses: [...(decision.foldedAskClasses ?? []), "policy_rra"] }
|
|
16
|
+
: {};
|
|
14
17
|
return {
|
|
15
18
|
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})` },
|
|
19
|
+
? { action: "ask", message, decisionReason: "persisted_rule", requiresRealApproval: true, ...unreadableCall, ...(decision.updatedInput !== undefined ? { updatedInput: decision.updatedInput } : {}) }
|
|
20
|
+
: { ...decision, requiresRealApproval: true, ...unreadableCall, ...standingFence, message: `${decision.message !== undefined ? `${decision.message} ` : ""}(${message})` },
|
|
18
21
|
tightened: "unreadable",
|
|
19
22
|
};
|
|
20
23
|
}
|
|
@@ -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;
|
|
@@ -1,6 +1,22 @@
|
|
|
1
|
+
import { posix } from "node:path";
|
|
1
2
|
import { readShellCommand } from "./shell-lexer.js";
|
|
2
3
|
import { FIND_ACTION_PRIMARIES, FIND_NEWER_PRIMARY, FIND_VALUE_PRIMARIES, FLAG_VALUE_ACCEPTS, READ_ONLY_BARE_ONLY, READ_ONLY_BARE_PROGRAMS, READ_ONLY_COMMAND_TABLE, READ_ONLY_ENV_NAMES, READ_ONLY_EXACT_FORMS, READ_ONLY_GLOB_PROGRAMS, XARGS_READ_ONLY_TARGETS, dockerRetargetDanger, } from "./read-only-shell-table.js";
|
|
3
4
|
const NOT = (reason) => ({ readOnly: false, reason });
|
|
5
|
+
export 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.";
|
|
6
|
+
export function changesDirectoryBeforeGit(shape, cwd) {
|
|
7
|
+
const programs = shape.segments.map((s) => peelBuiltinPrefix(s.argv.slice(leadingAssignmentCount(s.argv)).map((w) => w.text))[0] ?? "");
|
|
8
|
+
if (shape.segments.length < 2 || !programs.includes("cd") || !programs.includes("git"))
|
|
9
|
+
return false;
|
|
10
|
+
const cds = shape.segments.filter((_, i) => programs[i] === "cd");
|
|
11
|
+
const [cd] = cds;
|
|
12
|
+
if (cwd === undefined || cds.length !== 1 || cd === undefined || cd.redirections.length > 0 || cd.argv.length !== 2 || cd.argv[0].raw !== "cd")
|
|
13
|
+
return true;
|
|
14
|
+
const target = cd.argv[1];
|
|
15
|
+
if (target.expands !== false || !/^(?:\.\.?(?:\/|$)|\/)/.test(target.text) || /[$`*?[\\]/.test(target.raw))
|
|
16
|
+
return true;
|
|
17
|
+
const trim = (p) => p.replace(/\/+$/, "") || "/";
|
|
18
|
+
return trim(posix.resolve(cwd, target.text)) !== trim(cwd);
|
|
19
|
+
}
|
|
4
20
|
const READ_ONLY = { readOnly: true };
|
|
5
21
|
const BARE_PROGRAMS = new Set(READ_ONLY_BARE_PROGRAMS.filter((p) => !p.includes(" ")));
|
|
6
22
|
const BARE_MULTIWORD = READ_ONLY_BARE_PROGRAMS.filter((p) => p.includes(" ")).map((p) => p.split(" "));
|
|
@@ -23,15 +39,10 @@ const PRINTF_ESCAPE = new RegExp(`%[^%a-zA-Z]*${PRINTF_LENGTH}\\\\[0-7xX]`);
|
|
|
23
39
|
const PRINTF_NUMERIC = new RegExp(`%[-+ 0#']*[0-9.*]*${PRINTF_LENGTH}[diouxXeEfFgGaAn]`);
|
|
24
40
|
const JQ_DANGER_FLAG = /^['"]?-[a-zA-Z]*[fL]/;
|
|
25
41
|
const JQ_DANGER_TEXT = /--from-file|--rawfile|--slurpfile|--run-tests|--library-path|\benv\b|\$ENV\b|\binclude\b|\bimport\b/;
|
|
26
|
-
export function readOnlyShellVerdict(command, shape = readShellCommand(command)) {
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return NOT("no command");
|
|
31
|
-
if (shape.backgrounded)
|
|
32
|
-
return NOT("a background `&` defers execution past the approval-time reading");
|
|
33
|
-
if (shape.strayRedirection)
|
|
34
|
-
return NOT("a redirection on a piece that runs no program — the shell still opens its target");
|
|
42
|
+
export function readOnlyShellVerdict(command, shape = readShellCommand(command), cwd) {
|
|
43
|
+
const line = lineStructureObjection(shape);
|
|
44
|
+
if (line !== undefined)
|
|
45
|
+
return NOT(line);
|
|
35
46
|
for (const seg of shape.segments) {
|
|
36
47
|
if (seg.unreadable !== undefined)
|
|
37
48
|
return NOT(seg.unreadable);
|
|
@@ -46,9 +57,8 @@ export function readOnlyShellVerdict(command, shape = readShellCommand(command))
|
|
|
46
57
|
return NOT("a shell-specific expansion spelling (`=cmd`, `~[…]`)");
|
|
47
58
|
}
|
|
48
59
|
}
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return NOT("a compound that changes directory and runs git");
|
|
60
|
+
if (changesDirectoryBeforeGit(shape, cwd))
|
|
61
|
+
return NOT(CD_GIT_REASON);
|
|
52
62
|
for (const seg of shape.segments) {
|
|
53
63
|
const verdict = segmentVerdict(seg);
|
|
54
64
|
if (!verdict.readOnly)
|
|
@@ -56,6 +66,22 @@ export function readOnlyShellVerdict(command, shape = readShellCommand(command))
|
|
|
56
66
|
}
|
|
57
67
|
return READ_ONLY;
|
|
58
68
|
}
|
|
69
|
+
const CD_GIT_REASON = "a compound that changes directory and runs git";
|
|
70
|
+
function lineStructureObjection(shape) {
|
|
71
|
+
if (shape.grouped)
|
|
72
|
+
return "a subshell, group or control structure — not a flat list of simple commands";
|
|
73
|
+
if (shape.segments.length === 0)
|
|
74
|
+
return "no command";
|
|
75
|
+
if (shape.backgrounded)
|
|
76
|
+
return "a background `&` defers execution past the approval-time reading";
|
|
77
|
+
if (shape.strayRedirection)
|
|
78
|
+
return "a redirection on a piece that runs no program — the shell still opens its target";
|
|
79
|
+
return undefined;
|
|
80
|
+
}
|
|
81
|
+
export function readOnlyLineObjection(command, cwd) {
|
|
82
|
+
const shape = readShellCommand(command);
|
|
83
|
+
return lineStructureObjection(shape) ?? (changesDirectoryBeforeGit(shape, cwd) ? CD_GIT_REASON : undefined);
|
|
84
|
+
}
|
|
59
85
|
function expansionKind(w) {
|
|
60
86
|
if (w.expands === false)
|
|
61
87
|
return "none";
|
|
@@ -12,8 +12,14 @@ export declare function retiredKeyOf(bag: object, table: Readonly<Record<string,
|
|
|
12
12
|
replacement: string;
|
|
13
13
|
} | undefined;
|
|
14
14
|
/**
|
|
15
|
-
* The
|
|
16
|
-
* instead of vanishing silently — a seat that is silently
|
|
17
|
-
* the bag's type name for the sentence (`RunnerDeps`,
|
|
15
|
+
* The retired-key door: a retired seat still wired by a deployment is refused BY NAME (`config.deps_retired_key`
|
|
16
|
+
* on a deps bag, `config.spec_retired_key` on a `TaskSpec`) instead of vanishing silently — a seat that is silently
|
|
17
|
+
* dropped reads to the caller as wired. `owner` is the bag's type name for the sentence (`RunnerDeps`,
|
|
18
|
+
* `RunWorkflowToolDeps`, `TaskSpec`).
|
|
18
19
|
*/
|
|
19
20
|
export declare function assertNoRetiredKeys(bag: object, table: Readonly<Record<string, string>>, owner: string): void;
|
|
21
|
+
/** The RETIRED `TaskSpec` keys, each with the sentence that names its replacement — the table the spec door reads
|
|
22
|
+
* ({@link assertNoRetiredKeys} with owner `TaskSpec`). A caller still writing one of these is refused by name
|
|
23
|
+
* (`config.spec_retired_key`), never read as absence: a mode a caller believes it declared must not silently run
|
|
24
|
+
* as another. */
|
|
25
|
+
export declare const RETIRED_TASK_SPEC_KEYS: Readonly<Record<string, string>>;
|
|
@@ -10,6 +10,10 @@ export function assertNoRetiredKeys(bag, table, owner) {
|
|
|
10
10
|
if (hit === undefined)
|
|
11
11
|
return;
|
|
12
12
|
const e = new Error(`${owner}.${hit.key} is retired and is not read by this engine — ${hit.replacement}. Refused rather than ignored: a seat that is silently dropped reads to the deployment as wired.`);
|
|
13
|
-
e.code = "config.deps_retired_key";
|
|
13
|
+
e.code = owner === "TaskSpec" ? "config.spec_retired_key" : "config.deps_retired_key";
|
|
14
14
|
throw e;
|
|
15
15
|
}
|
|
16
|
+
export const RETIRED_TASK_SPEC_KEYS = Object.freeze({
|
|
17
|
+
autoModeRequested: 'the permission mode is one key now — write `permissionMode: "auto"`',
|
|
18
|
+
approverPosture: 'the permission mode is one key now — write `permissionMode: "bypassPermissions"` (the engine renders the posture from the effective mode)',
|
|
19
|
+
});
|