@sema-agent/core 7.9.2 → 7.11.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 +102 -0
- package/dist/agents/child-model-seat.d.ts +71 -14
- package/dist/agents/child-model-seat.js +23 -7
- package/dist/agents/subagent.d.ts +2 -2
- package/dist/agents/subagent.js +8 -5
- package/dist/agents/teacher.js +2 -2
- package/dist/core/ask-unresolvable-notice.d.ts +52 -0
- package/dist/core/ask-unresolvable-notice.js +25 -0
- package/dist/core/auto-mode-defaults.d.ts +15 -3
- package/dist/core/auto-mode-defaults.js +1 -0
- package/dist/core/auto-mode.d.ts +86 -23
- package/dist/core/auto-mode.js +43 -12
- package/dist/core/checkpoint-store.d.ts +14 -0
- package/dist/core/checkpoint-store.js +2 -1
- package/dist/core/engine-notice.d.ts +28 -7
- package/dist/core/gate-fold.js +1 -0
- package/dist/core/gate-lanes.d.ts +6 -1
- package/dist/core/gate-lanes.js +60 -18
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +37 -1
- package/dist/core/hooks.js +2 -0
- package/dist/core/permission-rule-model.d.ts +53 -16
- package/dist/core/permission-rule-model.js +59 -21
- package/dist/core/permission-rules.d.ts +6 -4
- package/dist/core/permission-rules.js +14 -14
- package/dist/core/persisted-rule-arms.js +4 -3
- package/dist/core/read-only-shell-table.d.ts +87 -0
- package/dist/core/read-only-shell-table.js +485 -0
- package/dist/core/read-only-shell.d.ts +42 -0
- package/dist/core/read-only-shell.js +316 -0
- package/dist/core/roles.d.ts +3 -2
- package/dist/core/runner/contracts.d.ts +26 -2
- package/dist/core/runner/denial-limit-arms.d.ts +14 -3
- package/dist/core/runner/denial-limit-arms.js +15 -5
- package/dist/core/runner/gate-exit.d.ts +5 -0
- package/dist/core/runner/permission-rule-lanes.d.ts +7 -1
- package/dist/core/runner/permission-rule-lanes.js +9 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.js +35 -14
- package/dist/core/runner/prepare-gate-stations.d.ts +3 -2
- package/dist/core/runner/prepare-gate-stations.js +12 -0
- package/dist/core/runner/prepare-policy-chain.js +7 -6
- package/dist/core/runner/prepare-task.js +1 -1
- package/dist/core/runner/prepare-turn-wiring.js +1 -1
- package/dist/core/runner/prepare-wiring-manifest.d.ts +1 -1
- package/dist/core/runner/prepare-wiring-manifest.js +8 -1
- package/dist/core/runner/runtask.d.ts +33 -32
- package/dist/core/runner/runtask.js +63 -33
- package/dist/core/runner-deps.d.ts +9 -2
- package/dist/core/shell-lexer.d.ts +18 -0
- package/dist/core/shell-lexer.js +17 -10
- package/dist/core/shell-wrapper-table.js +8 -5
- package/dist/core/swappable-deps.d.ts +90 -0
- package/dist/core/swappable-deps.js +55 -0
- package/dist/core/tool-policy.d.ts +30 -1
- package/dist/core/tool-policy.js +6 -2
- package/dist/core/tools.d.ts +28 -7
- package/dist/core/tools.js +44 -4
- package/dist/core/trace.d.ts +15 -0
- package/dist/core/wiring-manifest.d.ts +15 -1
- package/dist/core/wiring-manifest.js +10 -2
- package/dist/core/workflow-journal-store.d.ts +21 -2
- package/dist/core/workflow-journal-store.js +1 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +2 -0
- package/dist/engine/execution-env/node-execution-env.js +2 -1
- package/dist/engine/harness/agent-harness.d.ts +3 -1
- package/dist/engine/harness/agent-harness.js +1 -1
- package/dist/engine/harness/types.d.ts +15 -2
- package/dist/index.d.ts +8 -4
- package/dist/index.js +7 -3
- package/dist/orchestration/run-workflow-tool.d.ts +22 -2
- package/dist/orchestration/run-workflow-tool.js +14 -1
- package/dist/orchestration/workflow-governance.d.ts +3 -2
- package/dist/orchestration/workflow-observe.d.ts +1 -1
- package/dist/orchestration/workflow-observe.js +2 -0
- package/dist/orchestration/workflow-primitives.d.ts +4 -1
- package/dist/orchestration/workflow-primitives.js +1 -6
- package/dist/orchestration/workflow-types.d.ts +37 -2
- package/dist/orchestration/workflow-types.js +16 -0
- package/dist/orchestration/workflow.d.ts +53 -6
- package/dist/orchestration/workflow.js +319 -51
- package/dist/prompt-assembly/turn-snapshot.d.ts +4 -2
- package/dist/stores/file/workflow-journal-store.js +10 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +85 -5
package/dist/core/hooks.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { GateOutcome } from "./gate-outcome.js";
|
|
|
5
5
|
export { normalizeOrgGateVerdict, normalizePersistedRuleHit, persistedRuleMandateOf } from "./gate-lanes.js";
|
|
6
6
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
7
7
|
import { type AskClass } from "./ask-class.js";
|
|
8
|
-
import type
|
|
8
|
+
import { type AutoModeDenialTracker } from "./auto-mode.js";
|
|
9
9
|
export { formatHookFeedback } from "./reminder-mint.js";
|
|
10
10
|
import type { WiringLegKind } from "./wiring-manifest.js";
|
|
11
11
|
/**
|
|
@@ -1405,6 +1405,20 @@ export interface ToolGateInput {
|
|
|
1405
1405
|
toolCallId: string;
|
|
1406
1406
|
fallback: import("./auto-mode.js").DenialLimitFallback;
|
|
1407
1407
|
}) => void;
|
|
1408
|
+
/**
|
|
1409
|
+
* #648 — the deny exit nobody judged: the ask's approver (the run's own, or an ancestor's frozen seat on
|
|
1410
|
+
* a delegated child) answered `unavailable` and the durable park re-route did not take the call, so the
|
|
1411
|
+
* fail-closed deny STANDS. Fired once per such call, after the park has had its one attempt, with the
|
|
1412
|
+
* facts the `delegation.ask_unresolvable` notice is composed from (`ask-unresolvable-notice.ts`);
|
|
1413
|
+
* `parkLaneExisted` = a park lane was armed for this run. Observe-only: a throwing seat never alters
|
|
1414
|
+
* the deny. Absent ⇒ the deny stands silently (a host driving the gate directly, no notice sink).
|
|
1415
|
+
*/
|
|
1416
|
+
onAskUnresolvable?: (info: {
|
|
1417
|
+
toolName: string;
|
|
1418
|
+
toolCallId: string;
|
|
1419
|
+
settlementKind: "approver_unavailable";
|
|
1420
|
+
parkLaneExisted: boolean;
|
|
1421
|
+
}) => void;
|
|
1408
1422
|
/**
|
|
1409
1423
|
* design/153 §2/§7.4 (件4 复审, MED): true means this call's ask is MARKED — an inherited ancestor
|
|
1410
1424
|
* constraint already determined "no synchronous layer may resolve this ask" (the ancestor's frozen
|
|
@@ -1497,6 +1511,19 @@ export interface ToolGateInput {
|
|
|
1497
1511
|
rules: readonly string[];
|
|
1498
1512
|
}) => void;
|
|
1499
1513
|
};
|
|
1514
|
+
/**
|
|
1515
|
+
* #619 — observation sink for a shell call the READ-ONLY reader cleared (`readOnlyShellVerdict`, the
|
|
1516
|
+
* allow layer's second member beside the persisted allow rule: consulted for the shell tool only, after
|
|
1517
|
+
* the person's allow rules, under the SAME consumption predicate — never over a mandated ask, a hook's
|
|
1518
|
+
* ask, an explicit ask rule, governance or a marked call). The allow-side disclosure of "why did this run
|
|
1519
|
+
* without asking me?" for that arm; `command` is the FINAL command the gate judged (a policy rewrite
|
|
1520
|
+
* included). Never affects the outcome. Absent ⇒ the arm still runs, silently.
|
|
1521
|
+
*/
|
|
1522
|
+
onReadOnlyAllowed?: (info: {
|
|
1523
|
+
toolName: string;
|
|
1524
|
+
toolCallId: string;
|
|
1525
|
+
command: string;
|
|
1526
|
+
}) => void;
|
|
1500
1527
|
/**
|
|
1501
1528
|
* design/182 §7 — the ORG layer. Present ONLY when the deployment DECLARED org governance (the
|
|
1502
1529
|
* overlay constructor is the boot gate: a governed declaration with no snapshot provider refuses to
|
|
@@ -1643,6 +1670,12 @@ export interface AskCarry {
|
|
|
1643
1670
|
/** The surviving ask's engine-stamped origin word ({@link import("./ask-origin.js").AskOrigin}), persisted
|
|
1644
1671
|
* on the row so a durable card is rendered by the same word the synchronous card is. */
|
|
1645
1672
|
readonly origin?: import("./ask-origin.js").AskOrigin;
|
|
1673
|
+
/** #616: the surviving ask's classifier-unavailable fact (the classifier was consulted on this ask and could
|
|
1674
|
+
* not run — `cause` is the verdict's word), persisted on the row so a durable card says why it is asking
|
|
1675
|
+
* exactly as the synchronous card does. Absent ⇒ the classifier answered, was not eligible, or was not wired. */
|
|
1676
|
+
readonly classifierUnavailable?: {
|
|
1677
|
+
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
1678
|
+
};
|
|
1646
1679
|
}
|
|
1647
1680
|
/** What a durable row persists of an {@link AskCarry}: the fallback member COPIED (the row never aliases
|
|
1648
1681
|
* the gate's object; its window is already `0` — the gate's carry mint is the one writer of that
|
|
@@ -1651,6 +1684,9 @@ export interface AskCarry {
|
|
|
1651
1684
|
export declare function askCarryRowMembers(carry: AskCarry | undefined): {
|
|
1652
1685
|
denialLimitFallback?: import("./auto-mode.js").DenialLimitFallback;
|
|
1653
1686
|
origin?: import("./ask-origin.js").AskOrigin;
|
|
1687
|
+
classifierUnavailable?: {
|
|
1688
|
+
readonly cause: import("./auto-mode.js").AutoModeUnavailableCause;
|
|
1689
|
+
};
|
|
1654
1690
|
};
|
|
1655
1691
|
/**
|
|
1656
1692
|
* The design/37 **two-phase tool gate** — the single chokepoint that makes the load-bearing invariant
|
package/dist/core/hooks.js
CHANGED
|
@@ -6,6 +6,7 @@ export { normalizeOrgGateVerdict, normalizePersistedRuleHit, persistedRuleMandat
|
|
|
6
6
|
export { cloneObserverInput } from "./runner/gate-exit.js";
|
|
7
7
|
import { brandPolicyAskClass } from "./ask-class.js";
|
|
8
8
|
import { isAskOrigin } from "./ask-origin.js";
|
|
9
|
+
import { isAutoModeUnavailableCause } from "./auto-mode.js";
|
|
9
10
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
10
11
|
export { formatHookFeedback } from "./reminder-mint.js";
|
|
11
12
|
import { createSafeNotifier } from "./safe-notify.js";
|
|
@@ -237,6 +238,7 @@ export function askCarryRowMembers(carry) {
|
|
|
237
238
|
return {
|
|
238
239
|
...(carry.denialLimitFallback !== undefined ? { denialLimitFallback: { ...carry.denialLimitFallback } } : {}),
|
|
239
240
|
...(isAskOrigin(carry.origin) ? { origin: carry.origin } : {}),
|
|
241
|
+
...(isAutoModeUnavailableCause(carry.classifierUnavailable?.cause) ? { classifierUnavailable: { cause: carry.classifierUnavailable.cause } } : {}),
|
|
240
242
|
};
|
|
241
243
|
}
|
|
242
244
|
export async function runToolGate(input) {
|
|
@@ -497,28 +497,65 @@ export declare function parseRuleText(text: string, behavior: RuleBehavior): {
|
|
|
497
497
|
* double-slash directory form; for `path` it is the pattern as spelled. */
|
|
498
498
|
export declare function formatRuleText(command: string, match: PersistedRuleMatch, tool: PersistedRuleTool): string;
|
|
499
499
|
/**
|
|
500
|
-
* The
|
|
501
|
-
*
|
|
502
|
-
* relative pattern (
|
|
503
|
-
*
|
|
500
|
+
* The closed set of BASES a path-form pattern resolves against — facts of the CALL (its execution
|
|
501
|
+
* environment and its task), never of the rule:
|
|
502
|
+
* - `cwd` — a relative pattern (`dist/**`, `./x`, a bare `x` read at any depth): the live tracked working directory; when no tracker
|
|
503
|
+
* moved (or the caller keeps none) the task root stands in for it — that stand-in is the WORD's
|
|
504
|
+
* own meaning (the same reading `TOOL_PATH_BASES.cwd` gives a tool's relative slot), not a
|
|
505
|
+
* fallback to a different base;
|
|
506
|
+
* - `root` — a root-relative pattern (`/dist/**`): the task's project root;
|
|
507
|
+
* - `home` — a home-relative pattern (`~/.ssh/**`): the home directory OF THE EXECUTION ENVIRONMENT the
|
|
508
|
+
* call runs in (`ExecutionEnv.homeDir`, declared by the adapter). NEVER the engine process's
|
|
509
|
+
* own home: on a remote or sandboxed leg the two differ, and a `~/` deny resolved against the
|
|
510
|
+
* engine host's home silently guarded the wrong directory (#644).
|
|
511
|
+
* A pattern whose base is ABSENT (or not an absolute path) cannot be judged: the reach is `unreadable`
|
|
512
|
+
* (fail-closed — a standing deny/ask the lane cannot read becomes an ask a person clears), never
|
|
513
|
+
* silence and never a guess. `//abs` patterns need no base. {@link ruleBasesNeeded} tells a host BEFORE
|
|
514
|
+
* prepare which base a rule will need, so it can declare it (or expect the ask).
|
|
504
515
|
*/
|
|
505
|
-
export
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
516
|
+
export declare const PATH_RULE_BASES: readonly ["cwd", "root", "home"];
|
|
517
|
+
export type PathRuleBase = (typeof PATH_RULE_BASES)[number];
|
|
518
|
+
/** The bases a call supplies, keyed by {@link PathRuleBase}; every member optional (absent = not supplied). */
|
|
519
|
+
export type PathRuleBases = {
|
|
520
|
+
readonly [K in PathRuleBase]?: string;
|
|
521
|
+
};
|
|
522
|
+
/** The noun a refusal or an unreadable reason names each base by — the disposition table over the set
|
|
523
|
+
* (a base without a row does not compile: {@link PathRuleBaseLabelCoversEveryBase}). */
|
|
524
|
+
export declare const PATH_RULE_BASE_LABEL: {
|
|
525
|
+
readonly cwd: "the working directory";
|
|
526
|
+
readonly root: "the project root";
|
|
527
|
+
readonly home: "the home directory of the execution environment";
|
|
528
|
+
};
|
|
529
|
+
/** The fence over the label table: `never` while every base has a row. */
|
|
530
|
+
export type PathRuleBaseLabelCoversEveryBase = AssertAllKeysHandled<Exclude<PathRuleBase, keyof typeof PATH_RULE_BASE_LABEL>>;
|
|
531
|
+
/**
|
|
532
|
+
* WHICH base a rule's pattern resolves against — the one classification `resolvePathPattern`, the rule
|
|
533
|
+
* compiler's compile-time check and a host's pre-prepare "can this environment judge this rule?" all read.
|
|
534
|
+
* Empty for a rule that needs none: a `//abs` pattern, a `subpath` (directory) rule, a command rule. The
|
|
535
|
+
* relative form names `cwd`, whose own meaning includes "the root stands in when no tracker moved" — a
|
|
536
|
+
* host that supplies `root` has therefore supplied `cwd`'s stand-in too.
|
|
537
|
+
*/
|
|
538
|
+
export declare function ruleBasesNeeded(rule: Pick<PersistedRule, "match" | "command">): readonly PathRuleBase[];
|
|
510
539
|
/** Is this base usable by {@link resolvePathPattern} — an absolute path with a lexical normal form? A rule
|
|
511
540
|
* compiler asks this at compile time so a pattern is refused rather than compiled inert. */
|
|
512
541
|
export declare function isUsablePathBase(base: string | undefined): boolean;
|
|
513
542
|
/**
|
|
514
|
-
* The PATH family's tightening
|
|
515
|
-
*
|
|
516
|
-
* fences judge with). A `subpath` rule
|
|
517
|
-
* ({@link directoryRuleAdmits}); a `path` rule resolves its
|
|
518
|
-
*
|
|
519
|
-
*
|
|
543
|
+
* The PATH family's tightening reach — three-valued like the command family's ({@link programRunReachOf}):
|
|
544
|
+
* does this deny/ask rule reach the call's target path? The target is the caller's ALREADY-RESOLVED
|
|
545
|
+
* lexical-normal absolute path (the same identity the read/write fences judge with). A `subpath` rule
|
|
546
|
+
* reaches the directory and everything under it ({@link directoryRuleAdmits}); a `path` rule resolves its
|
|
547
|
+
* spelling against the call's bases ({@link resolvePathPattern} — a cwd-relative bare name reads at any
|
|
548
|
+
* depth, a trailing `/**` peeled first and, for that peeled form alone, by the rule's BEHAVIOR:
|
|
549
|
+
* `path_rule.bare_name`) and matches segment-wise (`*` within a segment, `**` across
|
|
550
|
+
* segments), reaching the path it names and everything under a directory it names. A command-family rule
|
|
551
|
+
* reaches no path. `unreadable` (#644): the pattern needs a base the call did not supply (or supplied as
|
|
552
|
+
* a non-absolute spelling) — the rule CANNOT be judged, and the answer says which base; a lane that must
|
|
553
|
+
* fail closed on it turns it into the ask a person clears (the persisted lane's unreadable answer, the
|
|
554
|
+
* content lane's unreadable ask), exactly as an unreadable command is treated. Silence here was the #644
|
|
555
|
+
* defect (a `~/` deny falling back to the engine host's home guarded the wrong directory; a `/…` deny with
|
|
556
|
+
* no root reached nothing at all).
|
|
520
557
|
*/
|
|
521
|
-
export declare function
|
|
558
|
+
export declare function pathRuleReachOf(rule: Pick<PersistedRule, "match" | "command" | "behavior">, target: string, bases: PathRuleBases): ProgramRunReachOutcome;
|
|
522
559
|
/**
|
|
523
560
|
* Does this rule's command pattern admit `command`?
|
|
524
561
|
*
|
|
@@ -331,20 +331,51 @@ function parsePathRuleContent(text, tool, content, behavior) {
|
|
|
331
331
|
}
|
|
332
332
|
return { rule: { behavior, rule: formatRuleText(content, "path", tool), tool, match: "path", command: content } };
|
|
333
333
|
}
|
|
334
|
-
|
|
334
|
+
export const PATH_RULE_BASES = ["cwd", "root", "home"];
|
|
335
|
+
export const PATH_RULE_BASE_LABEL = {
|
|
336
|
+
cwd: "the working directory",
|
|
337
|
+
root: "the project root",
|
|
338
|
+
home: "the home directory of the execution environment",
|
|
339
|
+
};
|
|
340
|
+
export function ruleBasesNeeded(rule) {
|
|
341
|
+
if (rule.match !== "path")
|
|
342
|
+
return [];
|
|
343
|
+
const pattern = rule.command;
|
|
335
344
|
if (pattern.startsWith("//"))
|
|
336
|
-
return
|
|
337
|
-
const under = (base, rest) => {
|
|
338
|
-
const normal = base === undefined ? undefined : lexicalNormalAbsolutePathOf(base);
|
|
339
|
-
if (normal === undefined)
|
|
340
|
-
return undefined;
|
|
341
|
-
return (normal === "/" ? "" : normal) + "/" + rest;
|
|
342
|
-
};
|
|
345
|
+
return [];
|
|
343
346
|
if (pattern.startsWith("~/"))
|
|
344
|
-
return
|
|
347
|
+
return ["home"];
|
|
345
348
|
if (pattern.startsWith("/"))
|
|
346
|
-
return
|
|
347
|
-
return
|
|
349
|
+
return ["root"];
|
|
350
|
+
return ["cwd"];
|
|
351
|
+
}
|
|
352
|
+
function baseValueOf(base, bases) {
|
|
353
|
+
switch (base) {
|
|
354
|
+
case "home":
|
|
355
|
+
return bases.home;
|
|
356
|
+
case "root":
|
|
357
|
+
return bases.root;
|
|
358
|
+
case "cwd":
|
|
359
|
+
return bases.cwd ?? bases.root;
|
|
360
|
+
default: {
|
|
361
|
+
const _exhaustive = base;
|
|
362
|
+
return _exhaustive;
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
function resolvePathPattern(pattern, bases, behavior) {
|
|
367
|
+
if (pattern.startsWith("//"))
|
|
368
|
+
return { resolved: "/" + pattern.slice(2) };
|
|
369
|
+
const needed = ruleBasesNeeded({ match: "path", command: pattern })[0];
|
|
370
|
+
const normal = lexicalNormalAbsolutePathOf(baseValueOf(needed, bases) ?? "");
|
|
371
|
+
if (normal === undefined)
|
|
372
|
+
return { missingBase: needed };
|
|
373
|
+
const rest = pattern.startsWith("~/") ? pattern.slice(2) : pattern.startsWith("/") ? pattern.slice(1) : pattern.startsWith("./") ? pattern.slice(2) : pattern;
|
|
374
|
+
const peeled = rest.endsWith("/**") ? rest.slice(0, -3) : rest;
|
|
375
|
+
const bare = needed === "cwd" && peeled !== "" && !peeled.includes("/");
|
|
376
|
+
const anyDepth = bare && (peeled === rest || behavior !== "allow");
|
|
377
|
+
const body = anyDepth ? "**/" + peeled : rest;
|
|
378
|
+
return { resolved: (normal === "/" ? "" : normal) + "/" + body };
|
|
348
379
|
}
|
|
349
380
|
export function isUsablePathBase(base) {
|
|
350
381
|
return base !== undefined && lexicalNormalAbsolutePathOf(base) !== undefined;
|
|
@@ -389,19 +420,20 @@ function globSegmentsReach(globs, target) {
|
|
|
389
420
|
};
|
|
390
421
|
return walk(0, 0);
|
|
391
422
|
}
|
|
392
|
-
export function
|
|
423
|
+
export function pathRuleReachOf(rule, target, bases) {
|
|
393
424
|
if (rule.match === "subpath")
|
|
394
|
-
return directoryRuleAdmits(rule, target);
|
|
425
|
+
return directoryRuleAdmits(rule, target) ? REACHED : NOT_REACHED;
|
|
395
426
|
if (rule.match !== "path")
|
|
396
|
-
return
|
|
427
|
+
return NOT_REACHED;
|
|
397
428
|
if (!isLexicalNormalAbsoluteDir(target) && target !== "/")
|
|
398
|
-
return
|
|
399
|
-
const
|
|
400
|
-
if (
|
|
401
|
-
return
|
|
402
|
-
|
|
429
|
+
return NOT_REACHED;
|
|
430
|
+
const r = resolvePathPattern(rule.command, bases, rule.behavior);
|
|
431
|
+
if ("missingBase" in r) {
|
|
432
|
+
return { reach: "unreadable", reason: `the rule is relative to ${PATH_RULE_BASE_LABEL[r.missingBase]} and this call supplies no absolute \`${r.missingBase}\` base to resolve it against` };
|
|
433
|
+
}
|
|
434
|
+
const globs = r.resolved.split("/").filter((sg) => sg !== "");
|
|
403
435
|
const segments = target.split("/").filter((sg) => sg !== "");
|
|
404
|
-
return globSegmentsReach(globs, segments);
|
|
436
|
+
return globSegmentsReach(globs, segments) ? REACHED : NOT_REACHED;
|
|
405
437
|
}
|
|
406
438
|
function hasUnescapedStar(s) {
|
|
407
439
|
for (let i = 0; i < s.length; i++) {
|
|
@@ -778,12 +810,18 @@ export function adjudicatePersistedPathRules(rules, call, targets, bases) {
|
|
|
778
810
|
}
|
|
779
811
|
if (targets.tighten === undefined)
|
|
780
812
|
continue;
|
|
813
|
+
let unreadable;
|
|
781
814
|
for (const rule of rules) {
|
|
782
815
|
if (!eligiblePersisted(rule, call, behavior))
|
|
783
816
|
continue;
|
|
784
|
-
|
|
817
|
+
const outcome = pathRuleReachOf(rule, targets.tighten, bases);
|
|
818
|
+
if (outcome.reach === "reached")
|
|
785
819
|
return { behavior, rules: [rule] };
|
|
820
|
+
if (outcome.reach === "unreadable")
|
|
821
|
+
unreadable ??= { behavior: "ask", rules: [rule], unreadable: outcome.reason };
|
|
786
822
|
}
|
|
823
|
+
if (unreadable !== undefined)
|
|
824
|
+
return unreadable;
|
|
787
825
|
}
|
|
788
826
|
return undefined;
|
|
789
827
|
}
|
|
@@ -132,10 +132,12 @@ export interface PermissionRulePolicyOptions {
|
|
|
132
132
|
*/
|
|
133
133
|
ruleFaces?: (toolName: string) => RuleFaceView | undefined;
|
|
134
134
|
/**
|
|
135
|
-
* The bases a path-form deny/ask pattern resolves against at match time: `root`
|
|
136
|
-
* project root), `cwd` for a relative pattern (
|
|
137
|
-
*
|
|
138
|
-
*
|
|
135
|
+
* The bases a path-form deny/ask pattern resolves against at match time ({@link PathRuleBases}): `root`
|
|
136
|
+
* for `/…` (the task's project root), `cwd` for a relative pattern (the root stands in when absent — the
|
|
137
|
+
* word's own meaning), `home` for `~/…` (the EXECUTION ENVIRONMENT's home — never the engine process's
|
|
138
|
+
* own; #644). Facts of the DEPLOYMENT and the task, never of the rule — a pattern is stored as spelled.
|
|
139
|
+
* A rule whose base is absent (or not an absolute path) here is refused at compile time
|
|
140
|
+
* (`unsupported.path_base`, naming the base — {@link ruleBasesNeeded} is the same classification).
|
|
139
141
|
*/
|
|
140
142
|
pathBases?: PathRuleBases;
|
|
141
143
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
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 { isUsablePathBase, parseRuleText,
|
|
4
|
+
import { PATH_RULE_BASE_LABEL, isUsablePathBase, parseRuleText, pathRuleReachOf, programRunReachOf, ruleBasesNeeded } from "./permission-rule-model.js";
|
|
5
5
|
import { effectivePathTargetOf } from "./effective-path-target.js";
|
|
6
6
|
import { protectivePathTargetOf } from "./tool-registry.js";
|
|
7
7
|
export { parsePermissionRule };
|
|
@@ -186,19 +186,14 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
186
186
|
bad(text, "invalid.command_rule", `${parsedPath.reject.code}: ${parsedPath.reject.message}`);
|
|
187
187
|
continue;
|
|
188
188
|
}
|
|
189
|
-
const
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
if (!isUsablePathBase(base)) {
|
|
194
|
-
bad(text, "unsupported.path_base", pattern.startsWith("~/")
|
|
195
|
-
? `"${text}" is relative to the home directory, and this policy's \`pathBases.home\` is not an absolute path — the rule would reach nothing`
|
|
196
|
-
: needsRoot
|
|
197
|
-
? `"${text}" is relative to the project root, and this policy was given no absolute \`pathBases.root\` to resolve it against — the rule would reach nothing`
|
|
198
|
-
: `"${text}" is relative to the working directory, and this policy was given no absolute \`pathBases.cwd\` (or \`root\`) to resolve it against — the rule would reach nothing`);
|
|
199
|
-
continue;
|
|
189
|
+
for (const base of ruleBasesNeeded(parsedPath.rule)) {
|
|
190
|
+
const value = base === "cwd" ? (pathBases.cwd ?? pathBases.root) : pathBases[base];
|
|
191
|
+
if (!isUsablePathBase(value)) {
|
|
192
|
+
bad(text, "unsupported.path_base", `"${text}" is relative to ${PATH_RULE_BASE_LABEL[base]}, and this policy was given no absolute \`pathBases.${base}\`${base === "cwd" ? " (or `root`)" : ""} to resolve it against — the rule could not be judged on any call`);
|
|
200
193
|
}
|
|
201
194
|
}
|
|
195
|
+
if (issues.length > 0 && issues[issues.length - 1].rule === text && issues[issues.length - 1].code === "unsupported.path_base")
|
|
196
|
+
continue;
|
|
202
197
|
entry.content[r.behavior].push({ parsed: parsedPath.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
|
|
203
198
|
byTool.set(toolName, entry);
|
|
204
199
|
continue;
|
|
@@ -326,8 +321,13 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
326
321
|
let unreadable;
|
|
327
322
|
for (const r of lane) {
|
|
328
323
|
if (r.parsed.match === "subpath" || r.parsed.match === "path") {
|
|
329
|
-
if (target
|
|
324
|
+
if (target === undefined)
|
|
325
|
+
continue;
|
|
326
|
+
const outcome = pathRuleReachOf(r.parsed, target, pathBases);
|
|
327
|
+
if (outcome.reach === "reached")
|
|
330
328
|
return { rule: r };
|
|
329
|
+
if (outcome.reach === "unreadable")
|
|
330
|
+
unreadable ??= { rule: r, unreadable: outcome.reason };
|
|
331
331
|
continue;
|
|
332
332
|
}
|
|
333
333
|
if (typeof command !== "string")
|
|
@@ -342,7 +342,7 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
342
342
|
};
|
|
343
343
|
const unreadableAsk = (hit, req) => ({
|
|
344
344
|
action: "ask",
|
|
345
|
-
message: `tool "${req.toolName}" needs approval: the
|
|
345
|
+
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`,
|
|
346
346
|
matchedAskRule: hit.rule.ruleText,
|
|
347
347
|
requiresRealApproval: true,
|
|
348
348
|
});
|
|
@@ -39,11 +39,12 @@ export function applyPersistedTightening(decision, read) {
|
|
|
39
39
|
tightened: "ask",
|
|
40
40
|
};
|
|
41
41
|
}
|
|
42
|
+
const asked = { ...decision };
|
|
42
43
|
return {
|
|
43
44
|
decision: {
|
|
44
|
-
...
|
|
45
|
-
...(
|
|
46
|
-
message: `${
|
|
45
|
+
...asked,
|
|
46
|
+
...(asked.matchedAskRule === undefined ? { matchedAskRule: shown } : {}),
|
|
47
|
+
message: `${asked.message !== undefined ? `${asked.message} ` : ""}(a persisted ask rule (${shown}) also requires approval for this call)`,
|
|
47
48
|
},
|
|
48
49
|
tightened: "ask",
|
|
49
50
|
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The READ-ONLY shell tables — the closed data the read-only reader (`read-only-shell.ts`) judges a
|
|
3
|
+
* command against, transcribed from upstream's own tables (CC 2.1.250) and kept as DATA so the parity
|
|
4
|
+
* question is answered by diffing two tables, never by reading two programs:
|
|
5
|
+
* - {@link READ_ONLY_COMMAND_TABLE} — the flag-vetted commands: a command (one or more leading words,
|
|
6
|
+
* `git status`, `gh pr view`, `grep`) with the closed set of flags it may carry, each flag's value
|
|
7
|
+
* ARITY, and where upstream carries one, its extra danger predicate over the remaining words
|
|
8
|
+
* (upstream `w3t` + `tqe` + `nqe` + `XGn` + `JGn` + `dmt`);
|
|
9
|
+
* - {@link READ_ONLY_BARE_PROGRAMS} — programs read-only under ANY literal arguments (upstream `jpe`);
|
|
10
|
+
* - {@link READ_ONLY_GLOB_PROGRAMS} — the programs a segment carrying a GLOB may still be read-only
|
|
11
|
+
* under (upstream `B3t`; a glob may expand to any name, so only a program that cannot write however
|
|
12
|
+
* it is named is admitted);
|
|
13
|
+
* - {@link READ_ONLY_EXACT_FORMS} / {@link READ_ONLY_BARE_ONLY} — whole-command forms (`node -v`) and
|
|
14
|
+
* programs admitted only bare (`pwd`) (upstream `N3t` / `D3t`);
|
|
15
|
+
* - {@link FIND_ACTION_PRIMARIES} / {@link FIND_VALUE_PRIMARIES} / {@link FIND_NEWER_PRIMARY} — the
|
|
16
|
+
* `find` primaries that ACT (write, execute) and the ones that take a value (upstream `I3t`/`smt`/`amt`);
|
|
17
|
+
* - {@link READ_ONLY_ENV_NAMES} — the environment variables a read-only command may be prefixed with
|
|
18
|
+
* (upstream `nH`: a variable that cannot change what a program DOES);
|
|
19
|
+
* - {@link XARGS_READ_ONLY_TARGETS} — the programs `xargs` may hand its input to (upstream `v3t`).
|
|
20
|
+
*
|
|
21
|
+
* NOT transcribed, each a stated gap rather than a silent one: `sed` (upstream vets the sed SCRIPT with a
|
|
22
|
+
* dedicated analyzer; without it a `sed` is not read-only here — fail-closed), the Windows PowerShell table
|
|
23
|
+
* and every Windows-only arm (UNC paths), and the two filesystem probes upstream runs beside the tables (a
|
|
24
|
+
* bare-repository indicator check and a `.git` redirection check — a shell tool here has no filesystem seat).
|
|
25
|
+
*
|
|
26
|
+
* Every predicate over the remaining words receives the words AFTER the command's own (upstream's `t`),
|
|
27
|
+
* dequoted. Upstream's substitution placeholders never appear here: a word carrying an expansion has
|
|
28
|
+
* already taken the whole command out of the read-only reading before a table is consulted.
|
|
29
|
+
*/
|
|
30
|
+
import type { AssertAllKeysHandled } from "./ask-origin.js";
|
|
31
|
+
/** How many words a flag's VALUE takes, and what the value must look like (upstream `ni`): `none` — the flag
|
|
32
|
+
* takes no value (`--flag=x` is refused); `number` — one word of digits; `string` — one word, any text
|
|
33
|
+
* (a value that starts with `-` is refused: it is another flag); `char` — one word of length 1; `{}` and
|
|
34
|
+
* `EOF` — one word spelled exactly so (`xargs -I {}`, `xargs -E EOF`). Registered in docs/CLOSED-SETS.md. */
|
|
35
|
+
export declare const READ_ONLY_FLAG_ARITIES: readonly ["none", "number", "string", "char", "{}", "EOF"];
|
|
36
|
+
export type ReadOnlyFlagArity = (typeof READ_ONLY_FLAG_ARITIES)[number];
|
|
37
|
+
/** The disposition table over the arity set: does `value` satisfy the arity? (`none` accepts NO value.) */
|
|
38
|
+
export declare const FLAG_VALUE_ACCEPTS: {
|
|
39
|
+
readonly none: () => boolean;
|
|
40
|
+
readonly number: (value: string) => boolean;
|
|
41
|
+
readonly string: () => boolean;
|
|
42
|
+
readonly char: (value: string) => boolean;
|
|
43
|
+
readonly "{}": (value: string) => boolean;
|
|
44
|
+
readonly EOF: (value: string) => boolean;
|
|
45
|
+
};
|
|
46
|
+
/** Compile-time fence: `never` while every arity has a row. */
|
|
47
|
+
export type FlagArityTableCoversEveryArity = AssertAllKeysHandled<Exclude<ReadOnlyFlagArity, keyof typeof FLAG_VALUE_ACCEPTS>>;
|
|
48
|
+
/** One row of {@link READ_ONLY_COMMAND_TABLE}. */
|
|
49
|
+
export interface ReadOnlyCommandRow {
|
|
50
|
+
/** Flag → the arity of its value. A flag not in the table refuses the command (a short cluster `-abc`
|
|
51
|
+
* is admitted only when every letter is a `none` flag). */
|
|
52
|
+
readonly safeFlags: Readonly<Record<string, ReadOnlyFlagArity>>;
|
|
53
|
+
/** `false`: a `--` word is skipped like any other rather than ending the flag walk (the words after it
|
|
54
|
+
* keep being vetted). Default: `--` ends the walk and the rest are operands. */
|
|
55
|
+
readonly respectsDoubleDash?: false;
|
|
56
|
+
/** Upstream carries a whole-text regex for this row; expressed here over the remaining words. */
|
|
57
|
+
readonly wordsShape?: (args: readonly string[]) => boolean;
|
|
58
|
+
/** Upstream's `additionalCommandIsDangerousCallback` over the remaining words: `true` refuses. */
|
|
59
|
+
readonly dangerous?: (args: readonly string[]) => boolean;
|
|
60
|
+
}
|
|
61
|
+
export declare const dockerRetargetDanger: (args: readonly string[]) => boolean;
|
|
62
|
+
/**
|
|
63
|
+
* The flag-vetted command table (upstream `w3t` and everything it spreads; `sed` withheld — see the module
|
|
64
|
+
* note). Keys with a space are multi-word commands and are matched on the leading words.
|
|
65
|
+
*/
|
|
66
|
+
export declare const READ_ONLY_COMMAND_TABLE: Readonly<Record<string, ReadOnlyCommandRow>>;
|
|
67
|
+
/** Programs read-only under any LITERAL arguments (upstream `jpe`): `cat x`, `wc -l x`, `diff a b`, `sleep 3`…
|
|
68
|
+
* The two-word `docker ps` / `docker images` are matched on both words. */
|
|
69
|
+
export declare const READ_ONLY_BARE_PROGRAMS: readonly ["docker ps", "docker images", "cal", "uptime", "cat", "head", "tail", "wc", "stat", "strings", "hexdump", "od", "nl", "id", "uname", "free", "df", "du", "locale", "groups", "nproc", "basename", "dirname", "realpath", "cut", "paste", "tr", "column", "tac", "rev", "fold", "expand", "unexpand", "fmt", "comm", "cmp", "numfmt", "readlink", "diff", "true", "false", "sleep", "which", "type", "expr", "seq", "tsort", "pr"];
|
|
70
|
+
/** Programs a segment carrying a GLOB may still be read-only under (upstream `B3t`): the glob may name
|
|
71
|
+
* anything, so only a program that writes nothing whatever it is handed. */
|
|
72
|
+
export declare const READ_ONLY_GLOB_PROGRAMS: readonly ["ls", "cat", "head", "tail", "wc", "stat", "grep", "egrep", "fgrep", "diff", "du", "df", "echo", "strings", "hexdump", "od", "nl", "cut", "column", "tr", "tac", "rev", "cmp", "basename", "dirname", "realpath", "readlink", "sha256sum", "sha1sum", "md5sum", "cd"];
|
|
73
|
+
/** Whole-command forms admitted exactly as spelled (upstream `N3t`). */
|
|
74
|
+
export declare const READ_ONLY_EXACT_FORMS: readonly (readonly string[])[];
|
|
75
|
+
/** Programs admitted only BARE (upstream `D3t`: `pwd`, `whoami`, `alias` — with an operand `alias` DEFINES one). */
|
|
76
|
+
export declare const READ_ONLY_BARE_ONLY: readonly ["pwd", "whoami", "alias"];
|
|
77
|
+
/** `find` primaries that ACT — delete, execute, write a file (upstream `I3t`). */
|
|
78
|
+
export declare const FIND_ACTION_PRIMARIES: readonly ["-delete", "-exec", "-execdir", "-ok", "-okdir", "-fprint", "-fprint0", "-fls", "-fprintf", "-files0-from"];
|
|
79
|
+
/** `find` primaries that take ONE value word (the value is skipped, not vetted) (upstream `smt`). */
|
|
80
|
+
export declare const FIND_VALUE_PRIMARIES: readonly ["-name", "-iname", "-path", "-ipath", "-lname", "-ilname", "-regex", "-iregex", "-wholename", "-iwholename", "-samefile", "-newer", "-anewer", "-cnewer", "-mnewer", "-perm", "-user", "-group", "-uid", "-gid", "-size", "-type", "-xtype", "-fstype", "-inum", "-links", "-used", "-context", "-amin", "-cmin", "-mmin", "-atime", "-ctime", "-mtime", "-mindepth", "-maxdepth", "-printf", "-regextype", "-D", "-f", "-flags", "-Bnewer", "-Btime", "-Bmin", "-files0-from", "-xattrname"];
|
|
81
|
+
/** The `-newerXY` family, a value primary spelled by pattern (upstream `amt`). */
|
|
82
|
+
export declare const FIND_NEWER_PRIMARY: RegExp;
|
|
83
|
+
/** Environment variables a read-only command may be prefixed with (upstream `nH`): locale, colour, terminal
|
|
84
|
+
* and toolchain switches that change how output LOOKS, never what a program does to the filesystem. */
|
|
85
|
+
export declare const READ_ONLY_ENV_NAMES: readonly ["GOEXPERIMENT", "GOOS", "GOARCH", "CGO_ENABLED", "GO111MODULE", "RUST_BACKTRACE", "RUST_LOG", "NODE_ENV", "PYTHONUNBUFFERED", "PYTHONDONTWRITEBYTECODE", "PYTEST_DISABLE_PLUGIN_AUTOLOAD", "PYTEST_DEBUG", "ANTHROPIC_API_KEY", "LANG", "LANGUAGE", "LC_ALL", "LC_CTYPE", "LC_TIME", "CHARSET", "TERM", "COLORTERM", "NO_COLOR", "FORCE_COLOR", "TZ", "LS_COLORS", "LSCOLORS", "GREP_COLOR", "GREP_COLORS", "GCC_COLORS", "TIME_STYLE", "BLOCK_SIZE", "BLOCKSIZE", "COLUMNS", "LINES", "CLICOLOR", "CLICOLOR_FORCE", "CI", "DEBIAN_FRONTEND", "GIT_TERMINAL_PROMPT"];
|
|
86
|
+
/** The programs `xargs` may hand its input to and stay read-only (upstream `v3t`). */
|
|
87
|
+
export declare const XARGS_READ_ONLY_TARGETS: readonly ["echo", "printf", "wc", "grep", "egrep", "fgrep", "head", "tail"];
|