@sema-agent/core 7.40.0 → 7.41.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.
Files changed (137) hide show
  1. package/CHANGELOG.md +218 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +10 -2
  3. package/dist/agents/agent-transcript-tool.js +52 -26
  4. package/dist/agents/list-agents-tool.d.ts +17 -2
  5. package/dist/agents/list-agents-tool.js +28 -16
  6. package/dist/agents/peer-session-drain.d.ts +3 -1
  7. package/dist/agents/peer-session-drain.js +1 -0
  8. package/dist/agents/subagent-steps.d.ts +12 -2
  9. package/dist/agents/subagent-steps.js +8 -4
  10. package/dist/agents/subagent.d.ts +1 -17
  11. package/dist/agents/subagent.js +12 -24
  12. package/dist/core/agent-definition.d.ts +19 -40
  13. package/dist/core/agent-state.d.ts +91 -0
  14. package/dist/core/agent-state.js +79 -0
  15. package/dist/core/ask-origin.d.ts +28 -6
  16. package/dist/core/ask-origin.js +9 -5
  17. package/dist/core/ask-unresolvable-notice.d.ts +27 -4
  18. package/dist/core/ask-unresolvable-notice.js +8 -0
  19. package/dist/core/auto-promote.d.ts +14 -27
  20. package/dist/core/auto-promote.js +5 -5
  21. package/dist/core/checkpoint-store.d.ts +19 -20
  22. package/dist/core/decision-receipt.d.ts +5 -2
  23. package/dist/core/decision-receipt.js +1 -1
  24. package/dist/core/engine-notice.d.ts +7 -1
  25. package/dist/core/gate-crash-trace.d.ts +1 -1
  26. package/dist/core/gate-fold.js +7 -3
  27. package/dist/core/gate-lanes.js +22 -3
  28. package/dist/core/governance-codes.d.ts +3 -1
  29. package/dist/core/governance-codes.js +3 -0
  30. package/dist/core/hooks.d.ts +20 -6
  31. package/dist/core/hooks.js +28 -28
  32. package/dist/core/mcp-tool-meta.d.ts +24 -0
  33. package/dist/core/mcp-tool-meta.js +17 -0
  34. package/dist/core/mcp.d.ts +1 -6
  35. package/dist/core/mcp.js +6 -7
  36. package/dist/core/mode-policy.d.ts +25 -0
  37. package/dist/core/mode-policy.js +8 -0
  38. package/dist/core/protocol-table.d.ts +3 -0
  39. package/dist/core/protocol-table.js +3 -0
  40. package/dist/core/reminder-mint.d.ts +21 -1
  41. package/dist/core/reminder-mint.js +20 -0
  42. package/dist/core/retired-keys.d.ts +2 -1
  43. package/dist/core/retired-keys.js +13 -2
  44. package/dist/core/rule-denial-sentence.d.ts +2 -2
  45. package/dist/core/runner/call-hook-stations.d.ts +106 -0
  46. package/dist/core/runner/call-hook-stations.js +118 -0
  47. package/dist/core/runner/contracts.d.ts +35 -25
  48. package/dist/core/runner/gate-exit.d.ts +5 -1
  49. package/dist/core/runner/gate-exit.js +10 -6
  50. package/dist/core/runner/prepare-delegation-surface.d.ts +1 -0
  51. package/dist/core/runner/prepare-delegation-surface.js +4 -4
  52. package/dist/core/runner/prepare-gate-stations.d.ts +8 -3
  53. package/dist/core/runner/prepare-gate-stations.js +13 -70
  54. package/dist/core/runner/prepare-inherited-gate.d.ts +6 -3
  55. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  56. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  57. package/dist/core/runner/prepare-park-ask.js +4 -1
  58. package/dist/core/runner/prepare-policy-chain.d.ts +6 -5
  59. package/dist/core/runner/prepare-policy-chain.js +14 -25
  60. package/dist/core/runner/prepare-project-context.d.ts +15 -16
  61. package/dist/core/runner/prepare-project-context.js +15 -11
  62. package/dist/core/runner/prepare-run-refs.d.ts +8 -8
  63. package/dist/core/runner/prepare-run-refs.js +3 -3
  64. package/dist/core/runner/prepare-safety-scan.js +3 -3
  65. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -1
  66. package/dist/core/runner/prepare-suspend-saga.js +2 -1
  67. package/dist/core/runner/prepare-task.js +14 -12
  68. package/dist/core/runner/prepare-turn-wiring.d.ts +3 -1
  69. package/dist/core/runner/prepare-turn-wiring.js +16 -13
  70. package/dist/core/runner/resume-admission.js +2 -1
  71. package/dist/core/runner/resume-apply.js +15 -26
  72. package/dist/core/runner/resume-checkpoint-screen.js +3 -0
  73. package/dist/core/runner/resume-preflight.js +3 -3
  74. package/dist/core/runner/run-leg.js +61 -47
  75. package/dist/core/runner/runtask.d.ts +1 -1
  76. package/dist/core/runner/session-rule-policy.d.ts +7 -19
  77. package/dist/core/runner/session-rule-policy.js +1 -2
  78. package/dist/core/runner/skill-base-dir.d.ts +10 -0
  79. package/dist/core/runner/skill-base-dir.js +4 -0
  80. package/dist/core/runner/synthetic-tools.d.ts +11 -17
  81. package/dist/core/runner/synthetic-tools.js +7 -8
  82. package/dist/core/runner-deps.d.ts +3 -1
  83. package/dist/core/safety-axis-vocab.d.ts +2 -2
  84. package/dist/core/safety-axis-vocab.js +2 -0
  85. package/dist/core/session-policy-store.d.ts +63 -11
  86. package/dist/core/session-policy-store.js +52 -6
  87. package/dist/core/skill-grant.d.ts +239 -0
  88. package/dist/core/skill-grant.js +313 -0
  89. package/dist/core/skills-directory.d.ts +5 -34
  90. package/dist/core/skills-directory.js +9 -58
  91. package/dist/core/task-registry-agent.d.ts +14 -2
  92. package/dist/core/task-registry-agent.js +23 -8
  93. package/dist/core/task-registry-shared.d.ts +6 -0
  94. package/dist/core/task-registry.d.ts +16 -0
  95. package/dist/core/task-registry.js +19 -6
  96. package/dist/core/task-spec.d.ts +10 -0
  97. package/dist/core/tool-face.d.ts +3 -2
  98. package/dist/core/tool-name-aliases.d.ts +38 -12
  99. package/dist/core/tool-name-aliases.js +16 -0
  100. package/dist/core/tool-name-readings.d.ts +6 -2
  101. package/dist/core/tool-name-readings.js +6 -4
  102. package/dist/core/tool-policy.d.ts +2 -2
  103. package/dist/core/tool-registry.d.ts +1 -4
  104. package/dist/core/tool-registry.js +0 -3
  105. package/dist/core/tool-spec.d.ts +11 -13
  106. package/dist/core/tools.d.ts +1 -1
  107. package/dist/core/trace.d.ts +15 -0
  108. package/dist/core/types.d.ts +1 -1
  109. package/dist/engine/harness/agent-harness.d.ts +12 -1
  110. package/dist/engine/harness/agent-harness.js +17 -13
  111. package/dist/engine/loop/agent-loop.d.ts +16 -1
  112. package/dist/engine/loop/agent-loop.js +9 -15
  113. package/dist/engine/loop/types.d.ts +2 -0
  114. package/dist/index.d.ts +9 -4
  115. package/dist/index.js +4 -2
  116. package/dist/stores/file/adoption/adopt.js +23 -16
  117. package/dist/stores/file/index.d.ts +8 -2
  118. package/dist/stores/file/index.js +2 -2
  119. package/dist/stores/file/mailbox-store.js +2 -2
  120. package/dist/stores/file/permission-rule-adopt.js +10 -1
  121. package/dist/stores/file/permission-rule-store.d.ts +6 -4
  122. package/dist/stores/file/permission-rule-store.js +127 -59
  123. package/dist/stores/file/session-policy-store.d.ts +86 -14
  124. package/dist/stores/file/session-policy-store.js +135 -118
  125. package/dist/stores/file/task-list-store.d.ts +5 -0
  126. package/dist/stores/file/task-list-store.js +18 -4
  127. package/dist/stores/file/usage-window-store.d.ts +9 -1
  128. package/dist/stores/file/usage-window-store.js +47 -33
  129. package/dist/tools/fs/safety.d.ts +1 -1
  130. package/package.json +1 -1
  131. package/test/export-surface.snapshot.json +45 -5
  132. package/wire-changes/core@7.40.1.json +39 -0
  133. package/wire-changes/core@7.41.0.json +1865 -0
  134. package/dist/core/runner/active-skill-scope.d.ts +0 -58
  135. package/dist/core/runner/active-skill-scope.js +0 -133
  136. package/dist/core/skill-tool-specifier.d.ts +0 -80
  137. package/dist/core/skill-tool-specifier.js +0 -57
@@ -1,58 +0,0 @@
1
- import type { ExecutionEnv } from "../../internal/harness-types.js";
2
- import type { SkillManifest, ToolEffect } from "../types.js";
3
- import type { ToolPolicy } from "../tool-policy.js";
4
- /** One active skill frame: either a resolved manifest, or a fail-closed marker that the manifest for a
5
- * loaded skill could not be resolved (→ DENY-ALL while this frame is active). */
6
- export type ActiveSkillFrame = {
7
- kind: "manifest";
8
- manifest: SkillManifest;
9
- } | {
10
- kind: "unresolved";
11
- lineageId: string;
12
- reason: string;
13
- };
14
- /**
15
- * The per-task LIFO stack of active skill frames. Owned by `prepare-task`'s closure;
16
- * the injected `skill` tool pushes onto it on a successful load, and a deny-narrowing policy reads it
17
- * on every tool call. A simple array is the stack — `push` on load, and the policy reads ALL frames
18
- * currently on it (the intersection). v1 keeps lifetime heuristic (see module header).
19
- */
20
- export declare class ActiveSkillScope {
21
- private readonly frames;
22
- /** Push a resolved manifest frame (skill loaded successfully WITH a manifest). */
23
- push(frame: ActiveSkillFrame): void;
24
- /** Pop the most-recently-pushed frame (LIFO). Returns it, or undefined when empty. v1 surfaces this
25
- * for the "next-skill-load pops the previous sibling frame" lifetime — see prepare-task wiring. */
26
- pop(): ActiveSkillFrame | undefined;
27
- /** Snapshot of the live frames (a copy — callers must not mutate the stack through it). */
28
- active(): readonly ActiveSkillFrame[];
29
- get size(): number;
30
- }
31
- /**
32
- * Build the Gate-3 deny-narrowing policy over a live {@link ActiveSkillScope}. For each
33
- * tool call, WHILE any skill frame is active:
34
- * - DENY any tool not in the INTERSECTION of every active frame's `allowTools`.
35
- * - For a WRITE tool with an `allowPaths` constraint on ANY active frame, DENY a write whose
36
- * canonicalized target is outside every listed prefix (reuses {@link canonicalizeTarget}, so a
37
- * symlink can't carry across a write outside the allowed paths).
38
- * - A frame whose manifest could not be resolved (`kind:"unresolved"`) → DENY-ALL (fail-closed: no
39
- * allowlist means nothing is allowed — NOT the bare task policy).
40
- * When NO frame is active, the policy returns `allow` for everything (it only ever subtracts), leaving
41
- * the task policy unmodified — so a skill WITHOUT a manifest, or a task before any skill load, behaves
42
- * exactly as before (backward-compatible).
43
- *
44
- * This policy must be composed via `combinePolicies(scopePolicy, taskPolicy)` so its `deny`
45
- * short-circuits the fold and a task-policy `allow` can never widen it.
46
- *
47
- * `env`/`rootPath` are the SAME execution env + tracked cwd the hand tools resolve against, so
48
- * `allowPaths` matching sees the task's real filesystem (a remote/E2B task resolves in its container).
49
- */
50
- export declare function createActiveSkillScopePolicy(opts: {
51
- scope: ActiveSkillScope;
52
- env: ExecutionEnv;
53
- rootPath?: string;
54
- /** Per-tool side-effect class (the SAME `toolEffects` map prepare-task builds). Lets the scope policy
55
- * fail CLOSED on a mutating tool that `allowPaths` can NOT confine (e.g. `bash` — no parseable path):
56
- * while an `allowPaths` constraint is active, such a tool is DENIED, never allowed to write anywhere. */
57
- toolEffects?: ReadonlyMap<string, ToolEffect>;
58
- }): ToolPolicy;
@@ -1,133 +0,0 @@
1
- import { canonicalizeTarget, pathFamilyOf } from "../../tools/fs/safety.js";
2
- import { pathTargetValue } from "../tool-registry.js";
3
- import { pathTargetBaseOf } from "../effective-path-target.js";
4
- import { PATH_WRITE_TOOLS, isWithin } from "./session-rule-policy.js";
5
- import { parseSkillToolEntry, skillSpecifierRejection } from "../skill-tool-specifier.js";
6
- import { heldValues, wireOf } from "../tool-name-readings.js";
7
- export class ActiveSkillScope {
8
- frames = [];
9
- push(frame) {
10
- this.frames.push(frame);
11
- }
12
- pop() {
13
- return this.frames.pop();
14
- }
15
- active() {
16
- return [...this.frames];
17
- }
18
- get size() {
19
- return this.frames.length;
20
- }
21
- }
22
- export function createActiveSkillScopePolicy(opts) {
23
- const { scope, env, rootPath, toolEffects } = opts;
24
- return {
25
- async check(req, signal) {
26
- const entriesFor = (byName) => heldValues("policy.skillScope", byName, req).flat();
27
- const frames = scope.active();
28
- if (frames.length === 0)
29
- return { action: "allow" };
30
- const unresolved = frames.find((f) => f.kind === "unresolved");
31
- if (unresolved && unresolved.kind === "unresolved") {
32
- return {
33
- action: "deny",
34
- message: `tool "${req.toolName}" denied: active skill artifact "${unresolved.lineageId}" manifest could not be resolved (${unresolved.reason}) — fail-closed deny-all`,
35
- decisionReason: "safety",
36
- };
37
- }
38
- const manifests = frames.flatMap((f) => (f.kind === "manifest" ? [f.manifest] : []));
39
- const perFrame = manifests.map((m) => {
40
- const byName = new Map();
41
- for (const raw of m.allowTools) {
42
- const entry = parseSkillToolEntry(raw);
43
- const bucket = byName.get(entry.name);
44
- if (bucket)
45
- bucket.push(entry);
46
- else
47
- byName.set(entry.name, [entry]);
48
- }
49
- return { manifest: m, byName };
50
- });
51
- if (perFrame.length === 0 || perFrame.some((f) => entriesFor(f.byName).length === 0)) {
52
- const ids = manifests.map((m) => m.lineageId).join(", ");
53
- return {
54
- action: "deny",
55
- message: `tool "${req.toolName}" is not in the active skill manifest allowlist (artifact(s): ${ids})`,
56
- decisionReason: "safety",
57
- };
58
- }
59
- for (const frame of perFrame) {
60
- const alternatives = entriesFor(frame.byName);
61
- const rejections = [];
62
- let admitted = false;
63
- for (const entry of alternatives) {
64
- const rejection = skillSpecifierRejection(entry, req.args);
65
- if (rejection === undefined) {
66
- admitted = true;
67
- break;
68
- }
69
- rejections.push(rejection);
70
- }
71
- if (!admitted) {
72
- return {
73
- action: "deny",
74
- message: `tool "${req.toolName}" is narrowed by skill manifest "${frame.manifest.lineageId}": ${rejections.join("; ")}`,
75
- decisionReason: "safety",
76
- };
77
- }
78
- }
79
- const pathConstrainingActive = manifests.some((m) => m.allowPaths && m.allowPaths.length > 0);
80
- const declaredTarget = req.face?.pathTarget;
81
- const skillScopeEligible = declaredTarget !== undefined && declaredTarget.skillScopeEligible === true;
82
- if (pathConstrainingActive && !skillScopeEligible) {
83
- const eff = toolEffects?.get(wireOf("policy.skillScope.effect", req)) ?? "write";
84
- if (eff !== "read") {
85
- const ids = manifests.filter((m) => m.allowPaths?.length).map((m) => m.lineageId).join(", ");
86
- return {
87
- action: "deny",
88
- message: `write-capable tool "${req.toolName}" denied: active skill manifest constrains writes to allowPaths but this tool's target cannot be path-confined (only ${[...PATH_WRITE_TOOLS].join("/")} are) — fail-closed (artifact(s): ${ids})`,
89
- decisionReason: "safety",
90
- };
91
- }
92
- }
93
- if (skillScopeEligible) {
94
- const constraining = manifests.filter((m) => m.allowPaths && m.allowPaths.length > 0);
95
- if (constraining.length > 0) {
96
- const path = pathTargetValue(req.args, declaredTarget);
97
- if (typeof path !== "string" || path.length === 0) {
98
- return {
99
- action: "deny",
100
- message: `write tool "${req.toolName}" denied: active skill manifest constrains writes to allowPaths but the call has no resolvable path`,
101
- decisionReason: "safety",
102
- };
103
- }
104
- const canon = await canonicalizeTarget(env, path, signal, pathFamilyOf({ root: rootPath, cwd: req.cwd ?? env.cwd }), pathTargetBaseOf(declaredTarget, { root: rootPath, cwd: req.cwd }));
105
- if (!canon.ok) {
106
- return {
107
- action: "deny",
108
- message: `write to "${path}" denied: its real target could not be resolved against the active skill manifest allowPaths`,
109
- decisionReason: "safety",
110
- };
111
- }
112
- for (const m of constraining) {
113
- const roots = [];
114
- for (const raw of m.allowPaths ?? []) {
115
- const r = await canonicalizeTarget(env, raw, signal, pathFamilyOf({ root: rootPath ?? env.cwd }), rootPath);
116
- if (r.ok)
117
- roots.push(r.key);
118
- }
119
- const inside = roots.some((root) => isWithin(root, canon.key));
120
- if (!inside) {
121
- return {
122
- action: "deny",
123
- message: `write to "${path}" is outside the paths allowed by skill manifest "${m.lineageId}" (allowPaths: ${(m.allowPaths ?? []).join(", ")})`,
124
- decisionReason: "safety",
125
- };
126
- }
127
- }
128
- }
129
- }
130
- return { action: "allow" };
131
- },
132
- };
133
- }
@@ -1,80 +0,0 @@
1
- /**
2
- * Pattern specifiers inside a {@link import("./types.js").SkillManifest}'s `allowTools`.
3
- *
4
- * An entry may be a bare tool name (`Bash`) or a name carrying a specifier (`Bash(git:*)`). A bare name
5
- * claims the whole tool; a specifier claims a NARROWER slice of it. Both directions of that sentence are
6
- * load-bearing: a specifier is only ever a further restriction on a tool the frame already admits, and
7
- * every shape this module cannot decide resolves to DENY — never to the bare name (that would turn
8
- * "only git" into "all of Bash", which is the one thing a narrowing-only structure must never do).
9
- *
10
- * ## Upstream semantics (decoded Claude Code 2.1.220 bundle)
11
- *
12
- * The rule-string grammar — `Tool(content)`, `\(`/`\)` escapes, `Tool()` and `Tool(*)` collapsing to the
13
- * bare name — is already mirrored by {@link parsePermissionRule}, so this module REUSES it rather than
14
- * growing a second parser for the same syntax.
15
- *
16
- * A rule content is classified into one of three forms, and each form has its own comparison:
17
- * - **prefix** (`git:*`, the trailing `:*` form): matches when the command EQUALS the prefix or starts
18
- * with `prefix + " "`. The space is the whole point — a raw string prefix would let `Bash(git:*)`
19
- * admit `gitk` and `git-lfs`, two unrelated programs.
20
- * - **wildcard** (contains an unescaped `*`, e.g. `git *`): a full-string anchored `*` match, done here
21
- * with {@link wildcardMatch} (the existing linear-time, ReDoS-free matcher).
22
- * - **exact** (no `*` at all): string equality.
23
- * Runs of spaces/tabs are collapsed on both sides before comparing, and comparison is case-sensitive —
24
- * both mirroring the upstream POSIX shell lane.
25
- *
26
- * ## Why a fail-closed simple-command floor comes first
27
- *
28
- * Upstream, a rule in the ALLOW direction never matches a compound command: its matcher short-circuits
29
- * on a multi-command string, so `git status && rm -rf /` can never be admitted by `Bash(git:*)`. The
30
- * same floor is built here out of {@link parseLeadingCommandName} — the single simple-command parser
31
- * already shared by `bash_readonly`, the `bash` reversibility classifier and the coarse command-name
32
- * policy. It rejects shell operators, substitution, redirection, backgrounding, a path-prefixed argv[0],
33
- * a leading env assignment, and an argv[0] carrying quote/brace/glob metacharacters. Nothing it rejects
34
- * ever reaches pattern matching, so no specifier form — wildcards included — can be talked into
35
- * admitting a second program smuggled in behind the first.
36
- *
37
- * Reusing that parser rather than writing a second tokenizer is deliberate: two argv[0] parsers drift,
38
- * and the drift shows up as a circumvention rather than as a test failure.
39
- *
40
- * ## Scope: the shell-command lane only
41
- *
42
- * Specifiers are enforced for the tools whose primary argument IS a shell command string — the shared
43
- * {@link COARSE_SHELL_TOOLS} set, which the other command-name gates in `tool-policy.ts` already key on
44
- * (so a shell tool added there is covered here without a second list to remember).
45
- *
46
- * A specifier on any other tool is deliberately NOT enforced, and stays denied-and-disclosed:
47
- * - upstream does not carry specifiers through this face either — its agent tool-list resolution parses
48
- * a `Tool(spec)` entry and then uses only the tool NAME for everything except its delegation tool;
49
- * - a path-shaped specifier (`Read(src/**)`) would be a SECOND, weaker path-narrowing face next to
50
- * `SkillManifest.allowPaths`, which canonicalizes its target and therefore survives a symlink. A
51
- * string-level glob does not, and shipping the weaker one beside the stronger one invites the wrong
52
- * choice;
53
- * - reads are deliberately never path-narrowed by this gate (it restricts capability, not visibility);
54
- * `Read(src/**)` would reverse that settled decision as a side effect of syntax support.
55
- */
56
- /** One `allowTools` entry, split into the tool it names and the optional narrowing it claims. */
57
- export interface SkillToolEntry {
58
- /** The raw entry, kept verbatim so a deny reason can quote what the author actually wrote. */
59
- readonly raw: string;
60
- /** Tool name as parsed, RAW (RB-476-A: legacy-name normalization is retired repo-wide). */
61
- readonly name: string;
62
- /** The specifier, or `undefined` for a bare entry (including `Tool()` / `Tool(*)`, which claim nothing). */
63
- readonly specifier?: string;
64
- }
65
- /**
66
- * Split one `allowTools` entry. Delegates the `Tool(content)` grammar to {@link parsePermissionRule}, so
67
- * escaping and the `Tool()`/`Tool(*)` collapse behave identically to every other rule-string site.
68
- */
69
- export declare function parseSkillToolEntry(entry: string): SkillToolEntry;
70
- /** Whether a specifier on this (canonical) tool is a shape this lane can actually decide. */
71
- export declare function isSkillSpecifierEnforced(canonicalName: string): boolean;
72
- /**
73
- * Decide one specifier against one tool call. Returns `undefined` when the call is admitted, or a
74
- * human-readable reason when it is not.
75
- *
76
- * Every path that is not a positive match returns a reason: an unenforced tool, an unreadable command,
77
- * a command the shared simple-command parser refuses, and a command the pattern does not cover. The
78
- * caller turns a reason into a deny — a specifier never falls back to its bare tool name.
79
- */
80
- export declare function skillSpecifierRejection(entry: SkillToolEntry, args: unknown): string | undefined;
@@ -1,57 +0,0 @@
1
- import { parsePermissionRule, wildcardMatch } from "./permission-rules.js";
2
- import { COARSE_SHELL_TOOLS } from "./tool-policy.js";
3
- import { parseLeadingCommandName } from "../tools/fs/bash-readonly-classifier.js";
4
- const SPECIFIER_ENFORCED_TOOLS = new Set(COARSE_SHELL_TOOLS);
5
- export function parseSkillToolEntry(entry) {
6
- const parsed = parsePermissionRule(entry);
7
- const name = parsed.toolName;
8
- return parsed.ruleContent === undefined ? { raw: entry, name } : { raw: entry, name, specifier: parsed.ruleContent };
9
- }
10
- export function isSkillSpecifierEnforced(canonicalName) {
11
- return SPECIFIER_ENFORCED_TOOLS.has(canonicalName);
12
- }
13
- function normalizeSpacing(s) {
14
- return s.trim().replace(/[ \t]+/g, " ");
15
- }
16
- function hasUnescapedStar(content) {
17
- for (let i = 0; i < content.length; i++) {
18
- if (content[i] !== "*")
19
- continue;
20
- let backslashes = 0;
21
- for (let j = i - 1; j >= 0 && content[j] === "\\"; j--)
22
- backslashes++;
23
- if (backslashes % 2 === 0)
24
- return true;
25
- }
26
- return false;
27
- }
28
- function specifierMatchesCommand(specifier, command) {
29
- const spec = normalizeSpacing(specifier);
30
- const prefix = /^(.+):\*$/.exec(spec)?.[1];
31
- if (prefix !== undefined) {
32
- return command === prefix || command.startsWith(prefix + " ");
33
- }
34
- if (hasUnescapedStar(spec))
35
- return wildcardMatch(spec, command);
36
- return command === spec;
37
- }
38
- export function skillSpecifierRejection(entry, args) {
39
- const { specifier } = entry;
40
- if (specifier === undefined)
41
- return undefined;
42
- if (!isSkillSpecifierEnforced(entry.name)) {
43
- return `entry "${entry.raw}" narrows a tool whose calls this gate cannot match against a command pattern — only the shell tools (${[...SPECIFIER_ENFORCED_TOOLS].join(", ")}) carry a command string, so the entry admits nothing`;
44
- }
45
- const command = args?.command;
46
- if (typeof command !== "string") {
47
- return `entry "${entry.raw}" requires a command string to match against, and this call has none`;
48
- }
49
- const parsedCommand = parseLeadingCommandName(command);
50
- if ("reject" in parsedCommand) {
51
- return `entry "${entry.raw}" only admits a single simple command (${parsedCommand.reject})`;
52
- }
53
- if (!specifierMatchesCommand(specifier, normalizeSpacing(command))) {
54
- return `command "${command.trim()}" is not admitted by entry "${entry.raw}"`;
55
- }
56
- return undefined;
57
- }