@sema-agent/core 7.46.0 → 7.48.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 +106 -0
- package/dist/core/allow-entry.d.ts +1 -1
- package/dist/core/allow-entry.js +4 -5
- package/dist/core/ask-origin.d.ts +35 -12
- package/dist/core/ask-origin.js +23 -6
- package/dist/core/checkpoint-store.d.ts +1 -1
- package/dist/core/engine-notice.d.ts +7 -4
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/gate-lanes.js +2 -1
- package/dist/core/gate-outcome.d.ts +4 -2
- package/dist/core/gate-outcome.js +2 -2
- package/dist/core/mcp-server-spec.d.ts +1 -1
- package/dist/core/permission-rule-model.d.ts +12 -6
- package/dist/core/permission-rule-model.js +10 -2
- package/dist/core/permission-rules.d.ts +4 -5
- package/dist/core/permission-rules.js +28 -15
- package/dist/core/runner/gate-exit.d.ts +4 -1
- package/dist/core/runner/gate-exit.js +5 -1
- package/dist/core/runner/park-commit.d.ts +1 -1
- package/dist/core/runner/prepare-hands-readface.js +5 -5
- package/dist/core/runner/prepare-policy-chain.js +2 -1
- package/dist/core/safety-axis-vocab.d.ts +1 -1
- package/dist/core/safety-merge-corpus.d.ts +1 -1
- package/dist/core/session-policy-store.d.ts +6 -3
- package/dist/core/session-policy-store.js +28 -10
- package/dist/core/shell-wrapper-projection.js +1 -1
- package/dist/core/shell-wrapper-table.d.ts +8 -8
- package/dist/core/shell-wrapper-table.js +3 -3
- package/dist/core/tool-catalog-entries.js +3 -2
- package/dist/core/tool-face.d.ts +10 -1
- package/dist/core/tool-name-aliases.d.ts +9 -8
- package/dist/core/tool-name-aliases.js +2 -0
- package/dist/core/tool-name-readings.d.ts +8 -0
- package/dist/core/tool-name-readings.js +2 -0
- package/dist/core/tool-policy.d.ts +10 -9
- package/dist/core/tool-policy.js +6 -7
- package/dist/core/tool-registry.d.ts +9 -0
- package/dist/core/tool-registry.js +10 -0
- package/dist/core/write-protect.d.ts +10 -3
- package/dist/core/write-protect.js +11 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/orchestration/workflow.js +2 -2
- package/dist/prompt-assembly/types.d.ts +1 -1
- package/dist/shell/lex.d.ts +15 -0
- package/dist/shell/lex.js +86 -15
- package/dist/tools/fs/bash-program-position.d.ts +16 -6
- package/dist/tools/fs/bash-program-position.js +18 -12
- package/dist/tools/fs/index.d.ts +3 -2
- package/dist/tools/fs/index.js +2 -2
- package/dist/tools/fs/program-read-model.d.ts +84 -12
- package/dist/tools/fs/program-read-model.js +105 -34
- package/dist/tools/fs/program-read-model.rows.js +26 -7
- package/dist/tools/fs/read-deny-home.d.ts +20 -1
- package/dist/tools/fs/read-deny-home.js +28 -7
- package/dist/tools/fs/read-deny.d.ts +13 -6
- package/dist/tools/fs/read-deny.js +12 -3
- package/dist/tools/fs/removal-targets.js +24 -5
- package/dist/tools/fs/shell-read-argv.d.ts +6 -8
- package/dist/tools/fs/shell-read-argv.js +5 -5
- package/dist/tools/fs/shell-read-bindings.d.ts +71 -19
- package/dist/tools/fs/shell-read-bindings.js +190 -75
- package/dist/tools/fs/shell-read-establish.js +6 -7
- package/dist/tools/fs/shell-read-plan.d.ts +3 -15
- package/dist/tools/fs/shell-read-plan.js +126 -129
- package/dist/tools/fs/shell-read-start.d.ts +57 -0
- package/dist/tools/fs/shell-read-start.js +92 -0
- package/dist/tools/fs/shell-read-word.d.ts +68 -8
- package/dist/tools/fs/shell-read-word.js +106 -12
- package/dist/tools/fs/shell-read-write-targets.js +4 -3
- package/dist/tools/fs/shell-reentry-text.d.ts +103 -3
- package/dist/tools/fs/shell-reentry-text.js +122 -4
- package/dist/tools/monitor.d.ts +3 -3
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +9 -3
- package/wire-changes/core@7.47.0.json +412 -0
- package/wire-changes/core@7.48.0.json +252 -0
|
@@ -131,7 +131,9 @@ export declare function readRowBehavior(v: unknown): RuleBehavior | undefined;
|
|
|
131
131
|
* clears it. The org/hook/matchedAskRule immunities stay: those conjuncts live in the gate's
|
|
132
132
|
* persisted-rule lane and do not loosen with this set. */
|
|
133
133
|
export type PersistedRuleTool = string;
|
|
134
|
-
/** The
|
|
134
|
+
/** The command grammar's ONE rule word — the spelling a shell command rule is written in (`Bash(<command>)`, upstream's).
|
|
135
|
+
* A rule word, not a tool list: the calls a command rule reaches are the SHELL FACE's — every catalog definition whose
|
|
136
|
+
* `command` the command grammar judges ({@link laneToolNamesOf}). */
|
|
135
137
|
export declare const COMMAND_RULE_TOOL = "Bash";
|
|
136
138
|
/** The one tool whose path family has an ALLOW form (the `Read(//abs-dir/**)` directory grant). */
|
|
137
139
|
export declare const READ_RULE_TOOL = "Read";
|
|
@@ -167,8 +169,9 @@ export declare const NAME_RULE_FACES: Readonly<Record<string, {
|
|
|
167
169
|
readonly params: readonly string[];
|
|
168
170
|
}>>;
|
|
169
171
|
/**
|
|
170
|
-
* Which tools a rule may name — the model's tool set is DERIVED, never a hand-kept list: the
|
|
171
|
-
* takes the COMMAND grammar
|
|
172
|
+
* Which tools a rule may name — the model's tool set is DERIVED, never a hand-kept list: the command rule word
|
|
173
|
+
* ({@link COMMAND_RULE_TOOL}) takes the COMMAND grammar — the other shell-face tools take none of their own, their command is
|
|
174
|
+
* judged by the `Bash(…)` rules ({@link laneToolNamesOf}); every tool the catalog declares a path target for (Read / Edit / Write /
|
|
172
175
|
* Glob / NotebookEdit / …, `pathTargetOf`) takes the PATH grammar; the tools of {@link NAME_RULE_FACES} take the NAME grammar.
|
|
173
176
|
* A rule on any other tool has no content this model can judge and is refused (`unsupported.tool`). The tool set therefore
|
|
174
177
|
* moves with the catalog's declarations, exactly as the protective fences' covered set does.
|
|
@@ -177,9 +180,12 @@ export declare function ruleToolGrammarOf(toolName: string): "command" | "path"
|
|
|
177
180
|
/** The value a name-grammar tool's call carries in its name argument — the string every NAME rule is read against, the one a
|
|
178
181
|
* `Skill` call is resolved by — or undefined (not a name-grammar tool; no such argument; not a string). */
|
|
179
182
|
export declare function nameRuleSubjectOf(toolName: string, args: unknown): string | undefined;
|
|
180
|
-
/** The
|
|
181
|
-
* name, the other aliases that resolve to it) the
|
|
182
|
-
*
|
|
183
|
+
/** The rule WORDS a call is judged under, once each: for every name the call answers to (the model's spelling, the wire
|
|
184
|
+
* name, the other aliases that resolve to it), the word its rules are written in — a shell-face name's is the command
|
|
185
|
+
* grammar's {@link COMMAND_RULE_TOOL} (a `Bash(<command>)` rule reaches `Monitor` as it reaches `Bash`), a path or name
|
|
186
|
+
* tool's its own name — so a stored `Bash(…)` or `Edit(…)` row reaches a tool that answers to that name under another wire
|
|
187
|
+
* name, as a policy's rule does. Every lane that judges a call by a rule's word reads it here: the policy compiler, the
|
|
188
|
+
* person's stored rows, the org partition, the allow lists. Empty ⇒ no grammar speaks for the call. */
|
|
183
189
|
export declare function laneToolNamesOf(req: Pick<import("./tool-policy.js").ToolCallRequest, "toolName" | "face">): readonly string[];
|
|
184
190
|
/**
|
|
185
191
|
* Match forms. Four members, two of them one predicate:
|
|
@@ -3,7 +3,7 @@ import { parsePermissionRule } from "./permission-rule-syntax.js";
|
|
|
3
3
|
import { compileReadDeny } from "../tools/fs/read-deny.js";
|
|
4
4
|
import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
5
5
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
6
|
-
import { pathTargetOf, protectivePathTargetOf } from "./tool-registry.js";
|
|
6
|
+
import { isShellFaceToolName, pathTargetOf, protectivePathTargetOf } from "./tool-registry.js";
|
|
7
7
|
import { heldValues } from "./tool-name-readings.js";
|
|
8
8
|
import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
|
|
9
9
|
import { bindPathRules, pathRowsReach, rowsWhereTheyLead } from "./path-rule-binding.js";
|
|
@@ -26,6 +26,8 @@ export function readRowBehavior(v) {
|
|
|
26
26
|
return isRuleBehavior(v) ? v : undefined;
|
|
27
27
|
}
|
|
28
28
|
export const COMMAND_RULE_TOOL = "Bash";
|
|
29
|
+
if (!isShellFaceToolName(COMMAND_RULE_TOOL))
|
|
30
|
+
throw new Error(`permission rules: the command rule word ${JSON.stringify(COMMAND_RULE_TOOL)} names no definition on the catalog's shell face`);
|
|
29
31
|
export const READ_RULE_TOOL = "Read";
|
|
30
32
|
export const PATH_RULE_FACES = Object.freeze({
|
|
31
33
|
[READ_RULE_TOOL]: Object.freeze({ deny: Object.freeze(["read", "write"]), ask: Object.freeze(["read"]) }),
|
|
@@ -56,7 +58,13 @@ export function nameRuleSubjectOf(toolName, args) {
|
|
|
56
58
|
return typeof value === "string" ? value : undefined;
|
|
57
59
|
}
|
|
58
60
|
export function laneToolNamesOf(req) {
|
|
59
|
-
|
|
61
|
+
const words = heldValues("policy.persistedGrammar", { get: ruleWordOf }, req);
|
|
62
|
+
return words.length <= 1 ? words : [...new Set(words)];
|
|
63
|
+
}
|
|
64
|
+
function ruleWordOf(name) {
|
|
65
|
+
if (isShellFaceToolName(name))
|
|
66
|
+
return COMMAND_RULE_TOOL;
|
|
67
|
+
return ruleToolGrammarOf(name) !== undefined ? name : undefined;
|
|
60
68
|
}
|
|
61
69
|
function isCommandMatch(match) {
|
|
62
70
|
return match === "exact" || match === "prefix" || match === "wildcard";
|
|
@@ -22,6 +22,10 @@
|
|
|
22
22
|
* (`programRunReachOf`: the whole command line or any program run in a connector chain — upstream's
|
|
23
23
|
* per-subcommand deny, the same instrument the org partition and the personal store's deny/ask rows use;
|
|
24
24
|
* a call the lexer cannot read against a rule is a fail-closed ask, never silence).
|
|
25
|
+
* The rule is written in the command word (`Bash`) and reaches every call on the catalog's SHELL FACE — `Bash`, and
|
|
26
|
+
* `Monitor`, which runs its `command` through the same execution seam — under any name the call answers to; a command
|
|
27
|
+
* spelling on another shell-face name (`Monitor(touch:*)`) is refused (`invalid.command_rule`, naming the `Bash(…)` form):
|
|
28
|
+
* upstream reads no such rule. A bare `Bash` rule is a tool-name rule and reaches the tool of that name only.
|
|
25
29
|
* It sits in the field-level tier of the evaluation order — right after the bare deny (resp. ask) — which
|
|
26
30
|
* is upstream's placement (`bs()` whole-tool deny → `AN(…,"deny")` field-level deny → `ph()` whole-tool
|
|
27
31
|
* ask → `AN(…,"ask")` field-level ask).
|
|
@@ -57,11 +61,6 @@ import type { NamedToolPolicy } from "./tool-policy.js";
|
|
|
57
61
|
import { parsePermissionRule, type ParsedPermissionRule } from "./permission-rule-syntax.js";
|
|
58
62
|
import { type PathRuleBases } from "./permission-rule-model.js";
|
|
59
63
|
export { parsePermissionRule, type ParsedPermissionRule };
|
|
60
|
-
/** [K-PLATFORM-SWEEP] MINOR#11: Bash's REAL non-primary arg names, accepted as generic param rules
|
|
61
|
-
* (`Bash(timeout:...)`). Exported SOLELY so a lockstep test can assert it against the live Bash tool
|
|
62
|
-
* schema — a schema param added without updating this set would compile to silently-never-matching
|
|
63
|
-
* rules (the exact drift class the Opus 复审 m note warned about). */
|
|
64
|
-
export declare const BASH_GENERIC_PARAMS: ReadonlySet<string>;
|
|
65
64
|
export interface PermissionRule {
|
|
66
65
|
/** CC 语法:`"Tool"` 或 `"Tool(param:pattern)"`。`Tool()`/`Tool(*)` 归一为裸规则;括号可 `\(`/`\)` 转义。 */
|
|
67
66
|
rule: string;
|
|
@@ -1,15 +1,14 @@
|
|
|
1
1
|
import { MCP_NAMESPACE, protocolOf } from "./protocol-table.js";
|
|
2
2
|
import { splitNamespacedName } from "./tool-name-aliases.js";
|
|
3
|
-
import { catalogRuleFaceOf, pathTargetOf } from "./tool-registry.js";
|
|
3
|
+
import { catalogRuleFaceOf, isShellFaceToolName, pathTargetOf } from "./tool-registry.js";
|
|
4
4
|
import { indexOfUnescaped, lastIndexOfUnescaped, parsePermissionRule } from "./permission-rule-syntax.js";
|
|
5
|
-
import { NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
|
|
5
|
+
import { COMMAND_RULE_TOOL, NAME_RULE_FACES, PATH_RULE_BASE_LABEL, PATH_RULE_FACES, callPathFaceOf, isUsablePathBase, laneToolNamesOf, nameRuleAdmits, nameRuleSubjectOf, parseRuleText, programRunReachOf, ruleBasesNeeded, ruleToolGrammarOf } from "./permission-rule-model.js";
|
|
6
6
|
import { pathFamilyOf } from "../tools/fs/safety.js";
|
|
7
7
|
import { callTargetReadingOf, callTargetViewsOf } from "./effective-path-target.js";
|
|
8
8
|
import { bindPathRules, pathRowsReach, policyAnchors, rowsWhereTheyLead } from "./path-rule-binding.js";
|
|
9
9
|
import { ruleDenialSentence } from "./rule-denial-sentence.js";
|
|
10
10
|
import { heldValues } from "./tool-name-readings.js";
|
|
11
11
|
export { parsePermissionRule };
|
|
12
|
-
export const BASH_GENERIC_PARAMS = new Set(catalogRuleFaceOf("Bash")?.params ?? []);
|
|
13
12
|
function describeBehavior(v) {
|
|
14
13
|
switch (typeof v) {
|
|
15
14
|
case "undefined": return "undefined";
|
|
@@ -110,13 +109,14 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
110
109
|
return { face, judged: face !== undefined };
|
|
111
110
|
};
|
|
112
111
|
const byTool = new Map();
|
|
112
|
+
const command = { deny: [], ask: [] };
|
|
113
113
|
const namespacedCovering = { deny: [], ask: [] };
|
|
114
114
|
const issues = [];
|
|
115
115
|
const bad = (rule, code, message) => {
|
|
116
116
|
issues.push({ rule, code, message });
|
|
117
117
|
};
|
|
118
118
|
if (rules.length > caps.maxRules) {
|
|
119
|
-
return { byTool, namespacedCovering, issues: [{ rule: "", code: "invalid.cap_exceeded", message: `${rules.length} rules > maxRules ${caps.maxRules}` }] };
|
|
119
|
+
return { byTool, command, namespacedCovering, issues: [{ rule: "", code: "invalid.cap_exceeded", message: `${rules.length} rules > maxRules ${caps.maxRules}` }] };
|
|
120
120
|
}
|
|
121
121
|
for (const r of rules) {
|
|
122
122
|
const text = r.rule;
|
|
@@ -174,17 +174,24 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
174
174
|
}
|
|
175
175
|
const content = parsed.ruleContent;
|
|
176
176
|
const colon = content.indexOf(":");
|
|
177
|
-
if (toolName
|
|
177
|
+
if (isShellFaceToolName(toolName)) {
|
|
178
178
|
const paramPart = colon > 0 ? content.slice(0, colon).trim() : "";
|
|
179
|
-
const
|
|
180
|
-
|
|
179
|
+
const declaresParam = (tool) => colon > 0 && /^[A-Za-z_][A-Za-z0-9_]*$/.test(paramPart) && (catalogRuleFaceOf(tool)?.params ?? []).includes(paramPart);
|
|
180
|
+
const looksLikeParamRule = declaresParam(toolName);
|
|
181
|
+
if (toolName !== COMMAND_RULE_TOOL && !looksLikeParamRule) {
|
|
182
|
+
const commandWordParam = declaresParam(COMMAND_RULE_TOOL) && !content.endsWith(":*");
|
|
183
|
+
bad(text, "invalid.command_rule", commandWordParam
|
|
184
|
+
? `"${text}" is a parameter rule on "${paramPart}", which ${toolName} does not declare (its rule parameters: ${(catalogRuleFaceOf(toolName)?.params ?? []).join(", ")}); a parameter rule reaches only the tool that declares the parameter, so ${COMMAND_RULE_TOOL}(${content}) is a rule on ${COMMAND_RULE_TOOL}'s own argument and does not reach ${toolName}. A shell command rule is written ${COMMAND_RULE_TOOL}(<command>) — it reaches ${toolName} and every other tool that runs a shell command by the command it runs.`
|
|
185
|
+
: `a shell command rule is written ${COMMAND_RULE_TOOL}(…) — it reaches ${toolName} and every other tool that runs a shell command by the command it runs; "${text}" names no rule upstream reads. Write ${COMMAND_RULE_TOOL}(${content}).`);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (toolName === COMMAND_RULE_TOOL && (!looksLikeParamRule || content.endsWith(":*"))) {
|
|
181
189
|
const parsedCommand = parseRuleText(text, verb);
|
|
182
190
|
if ("reject" in parsedCommand) {
|
|
183
191
|
bad(text, "invalid.command_rule", `${parsedCommand.reject.code}: ${parsedCommand.reject.message}`);
|
|
184
192
|
continue;
|
|
185
193
|
}
|
|
186
|
-
|
|
187
|
-
byTool.set(toolName, entry);
|
|
194
|
+
command[verb].push({ parsed: parsedCommand.rule, ruleText: text, ...(r.source !== undefined ? { source: r.source } : {}) });
|
|
188
195
|
continue;
|
|
189
196
|
}
|
|
190
197
|
}
|
|
@@ -264,7 +271,7 @@ function compile(rules, caps, primaryFieldGeneric, ruleFaces, pathBases = {}) {
|
|
|
264
271
|
entry.param[verb].push({ param, pattern, behavior: verb, ruleText: text, source: r.source });
|
|
265
272
|
byTool.set(toolName, entry);
|
|
266
273
|
}
|
|
267
|
-
return { byTool, namespacedCovering, issues };
|
|
274
|
+
return { byTool, command, namespacedCovering, issues };
|
|
268
275
|
}
|
|
269
276
|
export function validatePermissionRules(rules, opts) {
|
|
270
277
|
const caps = { ...DEFAULT_CAPS, ...opts?.caps };
|
|
@@ -303,7 +310,7 @@ const NO_LEADS = new Map();
|
|
|
303
310
|
export function createPermissionRulePolicy(rules, opts) {
|
|
304
311
|
const caps = { ...DEFAULT_CAPS, ...opts?.caps };
|
|
305
312
|
const pathBases = opts?.pathBases ?? {};
|
|
306
|
-
const { byTool, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.ruleFaces, pathBases);
|
|
313
|
+
const { byTool, command, namespacedCovering, issues } = compile(rules, caps, opts?.primaryFieldGeneric ?? "reject", opts?.ruleFaces, pathBases);
|
|
307
314
|
const setLevelCap = issues.some((i) => i.code === "invalid.cap_exceeded" && i.rule === "");
|
|
308
315
|
if (issues.length > 0 && ((opts?.onInvalidRule ?? "throw") === "throw" || setLevelCap)) {
|
|
309
316
|
const e = new Error(`createPermissionRulePolicy: ${issues.length} invalid/unsupported rule(s):\n` +
|
|
@@ -335,6 +342,9 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
335
342
|
if (entry.bare.ask !== undefined || entry.param.ask.length > 0 || entry.content.ask.length > 0)
|
|
336
343
|
nameSets.ask.push(toolName);
|
|
337
344
|
}
|
|
345
|
+
for (const behavior of ["deny", "ask"])
|
|
346
|
+
if (command[behavior].length > 0 && !nameSets[behavior].includes(COMMAND_RULE_TOOL))
|
|
347
|
+
nameSets[behavior].push(COMMAND_RULE_TOOL);
|
|
338
348
|
for (const behavior of ["deny", "ask"]) {
|
|
339
349
|
for (const r of namespacedCovering[behavior])
|
|
340
350
|
nameSets[behavior].push(r.ruleName);
|
|
@@ -345,11 +355,13 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
345
355
|
deny: [...byTool.values()].flatMap((e) => e.content.deny.filter((r) => r.faces !== undefined)),
|
|
346
356
|
ask: [...byTool.values()].flatMap((e) => e.content.ask.filter((r) => r.faces !== undefined)),
|
|
347
357
|
};
|
|
348
|
-
const contentLaneOf = (behavior, entry, callFace) => [
|
|
358
|
+
const contentLaneOf = (behavior, entry, shellCall, callFace) => [
|
|
349
359
|
...(entry?.content[behavior].filter((r) => r.faces === undefined) ?? []),
|
|
360
|
+
...(shellCall ? command[behavior] : []),
|
|
350
361
|
...(callFace !== undefined ? faceRules[behavior].filter((r) => r.faces.includes(callFace)) : []),
|
|
351
362
|
];
|
|
352
|
-
const
|
|
363
|
+
const hasCommandRules = command.deny.length > 0 || command.ask.length > 0;
|
|
364
|
+
const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
|
|
353
365
|
const pathRuleOf = new Map();
|
|
354
366
|
for (const e of byTool.values()) {
|
|
355
367
|
for (const behavior of ["deny", "ask"]) {
|
|
@@ -450,7 +462,8 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
450
462
|
return { action: "deny", message: ruleDenial(req, coveringDeny.ruleText, coveringDeny.source) };
|
|
451
463
|
}
|
|
452
464
|
const callFace = faceRules.deny.length > 0 || faceRules.ask.length > 0 ? callPathFaceOf(req) : undefined;
|
|
453
|
-
const
|
|
465
|
+
const shellCall = hasCommandRules && laneToolNamesOf(req).includes(COMMAND_RULE_TOOL);
|
|
466
|
+
const denyLane = contentLaneOf("deny", entry, shellCall, callFace);
|
|
454
467
|
const contentDeny = denyLane.length > 0 ? contentHit(denyLane, req) : undefined;
|
|
455
468
|
if (contentDeny !== undefined && contentDeny.unreadable === undefined) {
|
|
456
469
|
return { action: "deny", message: ruleDenial(req, contentDeny.rule.ruleText, contentDeny.rule.source) };
|
|
@@ -473,7 +486,7 @@ export function createPermissionRulePolicy(rules, opts) {
|
|
|
473
486
|
if (coveringAsk)
|
|
474
487
|
return { action: "ask", message: ruleAskMessage(coveringAsk.ruleText, coveringAsk.source), matchedAskRule: coveringAsk.ruleText };
|
|
475
488
|
}
|
|
476
|
-
const askLane = contentLaneOf("ask", entry, callFace);
|
|
489
|
+
const askLane = contentLaneOf("ask", entry, shellCall, callFace);
|
|
477
490
|
const contentAsk = askLane.length > 0 ? contentHit(askLane, req) : undefined;
|
|
478
491
|
if (contentAsk !== undefined) {
|
|
479
492
|
if (contentAsk.unreadable !== undefined)
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import { type ClassifierDenyCause, type DeniedBy, type GateOutcome, type ModeRefusal, type ModeRelease, type Settlement } from "../gate-outcome.js";
|
|
25
25
|
import type { AutoModeClassifierRound } from "../auto-mode.js";
|
|
26
|
-
import { type Arbitration, type AskClassification, type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
|
|
26
|
+
import { type Arbitration, type AskClassification, type AskFace, type AskOrigin, type AskOriginFacts } from "../ask-origin.js";
|
|
27
27
|
import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
|
|
28
28
|
import type { SafeNotifier } from "../safe-notify.js";
|
|
29
29
|
import type { HookSeatOutcome, PermissionDeniedPayload, PreToolUseResult, ToolGateInput, ToolGateResult } from "../hooks.js";
|
|
@@ -143,6 +143,9 @@ export interface PreToolUseScreening {
|
|
|
143
143
|
/** The model-facing text for an answer the decision receipt refused (an ENGINE fail-closed, not the face's deny). */
|
|
144
144
|
refusedReason(answer: PreToolUseResult): string;
|
|
145
145
|
}
|
|
146
|
+
/** The FACE a pass's questions are put on ({@link askFaceOf} of the call's wire name) — the one reading of it the
|
|
147
|
+
* write-protection tighten (the fold) and every class derivation (the lanes) make. */
|
|
148
|
+
export declare function passAskFace(pass: Pick<GatePass, "req">): AskFace;
|
|
146
149
|
/**
|
|
147
150
|
* ONE gate pass — the record the station groups (`runGateFold`, `runGateLanes`) and the exits read and write,
|
|
148
151
|
* built once per call by `runToolGate` before the screening pass. What used to be the closure-captured `let`
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
|
|
2
|
-
import { classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
|
|
2
|
+
import { askFaceOf, classifyAsk, headlessApprovalRemedy } from "../ask-origin.js";
|
|
3
|
+
import { wireOf } from "../tool-name-readings.js";
|
|
3
4
|
import { isProtectedWrite } from "../tool-registry.js";
|
|
4
5
|
import { askUnresolvableCauseOf, classifierDenyCauseOf, classifierRoundOf, decisionText, engineSettlementOf } from "../tool-policy.js";
|
|
5
6
|
import { askUnresolvableRemedySeat } from "../ask-unresolvable-notice.js";
|
|
@@ -97,6 +98,9 @@ function withParkFailureCause(reason, parkFailed) {
|
|
|
97
98
|
`(${inlineUntrusted(parkFailed, PARK_FAILURE_CAUSE_MAX)}), so the refusal above is what the fallback had ` +
|
|
98
99
|
`left to say, not the reason the call stopped.`);
|
|
99
100
|
}
|
|
101
|
+
export function passAskFace(pass) {
|
|
102
|
+
return askFaceOf(wireOf("gate.askFace", pass.req));
|
|
103
|
+
}
|
|
100
104
|
export async function engineFailClosedExit(pass, reason) {
|
|
101
105
|
const { input, toolName, toolCallId, notifyPermissionDenied } = pass;
|
|
102
106
|
const gate = directDeny("hook");
|
|
@@ -33,7 +33,7 @@ export declare const DEFAULT_UNATTENDED_APPROVAL_TTL_MS: number;
|
|
|
33
33
|
* A caller-supplied TTL is only a TTL if it is a finite, positive number.
|
|
34
34
|
* `TaskSpec.durableApproval.ttlMs` / `resourceSuspend.ttlMs` are typed as a bare `number`, and the mint
|
|
35
35
|
* arithmetic was `Date.now() + ttlMs` with NO validation — so `Number(process.env.APPROVAL_TTL_MS)` on an
|
|
36
|
-
* unset/malformed env var (a
|
|
36
|
+
* unset/malformed env var (a web-admin miss, an upstream arithmetic slip) silently produced
|
|
37
37
|
* `deadline = NaN`. Every reaper comparison is `deadline <= cutoff`, and `NaN <= anything` is FALSE, so a
|
|
38
38
|
* checkpoint that LOOKS bounded (it carries a ttlMs and a deadline) became permanently IMMORTAL: no reap
|
|
39
39
|
* cutoff, however far in the future, could ever expire it → a pinned session + a stranded paused VM, and
|
|
@@ -8,7 +8,7 @@ import { placeReadDenyAtHome } from "../../tools/fs/read-deny-home.js";
|
|
|
8
8
|
import { createShellOverflowSpoolFence } from "../../tools/fs/fs-shared.js";
|
|
9
9
|
import { createShellReadStation } from "../../tools/fs/shell-read-check.js";
|
|
10
10
|
import { composeReadChecks, createFileReadStation } from "../../tools/fs/file-read-check.js";
|
|
11
|
-
import { readTargetToolNames } from "../tool-registry.js";
|
|
11
|
+
import { isShellFaceToolName, predictMountedToolNames, readTargetToolNames } from "../tool-registry.js";
|
|
12
12
|
import { normalizeFileText } from "../../tools/fs/encoding.js";
|
|
13
13
|
import { wholeFileRecordsFromTranscript } from "./session-file-state-replay.js";
|
|
14
14
|
import { deliverEngineNotice } from "../types.js";
|
|
@@ -203,6 +203,7 @@ export async function prepareHandsMount(input) {
|
|
|
203
203
|
const handsIncludeShell = !(executionEnv instanceof StubExecutionEnv);
|
|
204
204
|
const handsReadOnly = spec.handsReadOnly === true;
|
|
205
205
|
backgroundTaskToolsActive = handsIncludeShell && !handsReadOnly && hasBackgroundShell(executionEnv);
|
|
206
|
+
const shellFaceHere = [...predictMountedToolNames(new Set(backgroundTaskToolsActive ? ["hands", "backgroundTasks"] : ["hands"]))].filter(isShellFaceToolName);
|
|
206
207
|
wsSnapshot = resume !== undefined || !(handsIncludeShell && !handsReadOnly) ? undefined : await session.getWorkspaceState().catch(() => undefined);
|
|
207
208
|
rebaseWsPath = (p) => (wsSnapshot === undefined || wsSnapshot.taskRoot === rootCanonical ? p : rebaseWorkspacePath(p, wsSnapshot.taskRoot, rootCanonical));
|
|
208
209
|
if (handsIncludeShell && !handsReadOnly) {
|
|
@@ -307,7 +308,7 @@ export async function prepareHandsMount(input) {
|
|
|
307
308
|
if (handsIncludeShell) {
|
|
308
309
|
const spoolFence = handsReadOnly ? createShellOverflowSpoolFence(executionEnv) : undefined;
|
|
309
310
|
const readonlyBoundary = () => spoolFence !== undefined ? { roots: [rootCanonical, ...additionalRootsCanonical, ...additionalReadRootsCanonical], cwd: rootCanonical, ...denySeat, ...writeDenySeat, alsoInside: spoolFence } : undefined;
|
|
310
|
-
shellReadTools = new Set(
|
|
311
|
+
shellReadTools = new Set(shellFaceHere);
|
|
311
312
|
shellReadStation = createShellReadStation({
|
|
312
313
|
env: executionEnv,
|
|
313
314
|
tools: shellReadTools,
|
|
@@ -400,9 +401,8 @@ export async function prepareHandsMount(input) {
|
|
|
400
401
|
if (seatOwned)
|
|
401
402
|
shellGatedTools.add(name);
|
|
402
403
|
};
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
installShellSeat("Monitor");
|
|
404
|
+
for (const name of shellFaceHere)
|
|
405
|
+
installShellSeat(name);
|
|
406
406
|
}
|
|
407
407
|
}
|
|
408
408
|
return {
|
|
@@ -14,7 +14,7 @@ import { emitTrace } from "../trace.js";
|
|
|
14
14
|
import { skillGrantBindingMembers } from "../skill-grant.js";
|
|
15
15
|
import { SKILL_TOOL_NAME } from "./synthetic-tools.js";
|
|
16
16
|
import { autoStrippedRule, cardDisclosureOf } from "../allow-entry.js";
|
|
17
|
-
import { arbitrateByMode, askClassMembersOf, askMandateMember } from "../ask-origin.js";
|
|
17
|
+
import { arbitrateByMode, askClassMembersOf, askFaceOf, askMandateMember } from "../ask-origin.js";
|
|
18
18
|
import { createModeGatePolicy, mountedToolDeclaresReadOnly } from "../mode-policy.js";
|
|
19
19
|
const ancestorFacts = (ask) => ({ ...(ask ?? {}), ancestorResolved: true });
|
|
20
20
|
import { headlessDenyAtFold, headlessDenyAtRecheck, INHERITED_STATION_FACTS, inheritedAskCarry, inheritedAskFallbackMessage, judgeInheritedClassifier, lateAskSettlementObserver, resolveInheritedAsk, settleDenialLimitFallback } from "./denial-limit-arms.js";
|
|
@@ -313,6 +313,7 @@ export async function preparePolicyChain(input) {
|
|
|
313
313
|
...(seat.sources.size > 0 ? { toolMarkSources: seat.sources } : {}),
|
|
314
314
|
...(deps.a2a?.isolatePeers === true ? { a2aIsolatePeers: true } : {}),
|
|
315
315
|
...(face !== undefined ? { readFace: face } : {}),
|
|
316
|
+
askFace: askFaceOf(wireOf("inherited.askFace", call)),
|
|
316
317
|
};
|
|
317
318
|
};
|
|
318
319
|
const inheritedAskEvidence = inheritedAskRuleEvidence(deps.permissionRuleStore);
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* `SAFETY_AXIS_VOCABULARY` — the PUBLISHED safety-axis vocabulary as a zero-logic, zero-runtime-dependency DATA
|
|
3
3
|
* artifact (pure string / number / boolean literal constants).
|
|
4
4
|
*
|
|
5
|
-
* **design/99 center control-plane seam (2026-06-27).** The
|
|
5
|
+
* **design/99 center control-plane seam (2026-06-27).** The web-admin control plane's per-field
|
|
6
6
|
* safety-classification table lives in `@sema-agent/registry-core` and, for every safety-critical config field, cites a
|
|
7
7
|
* core axis VALUE (e.g. `effect: "write"`, `irreversibility: "always"`). That contract package **must not depend on
|
|
8
8
|
* `@sema-agent/core` (the engine) at runtime** — so it dev-imports THIS const (test-only) and runs a conformance test
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { SessionPermissionRules } from "./session-policy-store.js";
|
|
2
2
|
/**
|
|
3
|
-
* design/99 (
|
|
3
|
+
* design/99 (web-admin control-plane seam, channel review R2): the PUBLISHED golden conformance corpus
|
|
4
4
|
* for the safety-domain MERGE predicate {@link loosenReasons}.
|
|
5
5
|
*
|
|
6
6
|
* THE SEAM. The center control-plane's ConfigBundle merger is a DISPATCHER: for any field it classifies
|
|
@@ -67,7 +67,7 @@ export interface SessionRuleNameRefusal {
|
|
|
67
67
|
/** Typed error from {@link SessionPolicyStore.putRules}. `conflict` = stale `expectedRev`; `loosen_forbidden`
|
|
68
68
|
* = a non-operator write tried to relax the rules; `corrupt` = a backend could not read the erase-surviving
|
|
69
69
|
* rev mark ({@link SessionPolicyStore.deleteBySession}) and refuses rather than stamp an unproven rev; the fourth
|
|
70
|
-
* code = a tool-name bucket names a retired name, a prefix-less `__` name or a
|
|
70
|
+
* code = a tool-name bucket names a retired name, a prefix-less `__` name or a segment no mint spells (an empty one, or a peer spelled otherwise)
|
|
71
71
|
* ({@link assertSessionRuleNamesCurrent}) — `detail` is present on that code and only on it. */
|
|
72
72
|
export declare class SessionPolicyError extends Error {
|
|
73
73
|
readonly code: "conflict" | "loosen_forbidden" | "corrupt" | "legacy_tool_name";
|
|
@@ -121,7 +121,8 @@ export interface SessionPolicyStore {
|
|
|
121
121
|
/** Write the rules for a key: CAS on `expectedRev`, then the name judge ({@link assertSessionRuleNamesCurrent} —
|
|
122
122
|
* operator writes included), then, for a non-operator write over an existing row, the
|
|
123
123
|
* tighten-only check ({@link loosenReasons}) — both against the names the deployment declares it mounts its own
|
|
124
|
-
* tools under.
|
|
124
|
+
* tools under. A bucket that is not a list of strings (a bare `"rm"` for `commandDeny`) throws a `TypeError` from
|
|
125
|
+
* {@link normalizeRules} before any of those is judged, and nothing is written. */
|
|
125
126
|
putRules(sessionId: string, principal: string | undefined, rules: SessionPermissionRules, opts?: PutRulesOptions): Promise<StoredSessionRules>;
|
|
126
127
|
/**
|
|
127
128
|
* 2c session-sync: every (principal, rules) record for `sessionId` across ALL principals — `getRules`
|
|
@@ -172,7 +173,9 @@ export interface SessionPolicyStore {
|
|
|
172
173
|
*/
|
|
173
174
|
declare function lexWithin(root: string, p: string): boolean;
|
|
174
175
|
/** Normalize a rules object's `allowDirs` (lexical `posix.normalize`) so stored rules are `..`-free and the
|
|
175
|
-
* tighten diff compares the same canonical-ish form the runtime resolves.
|
|
176
|
+
* tighten diff compares the same canonical-ish form the runtime resolves. Every bucket must be a list of strings
|
|
177
|
+
* ({@link ruleList}); a bucket that is not one throws `TypeError`, so neither bundled store (nor a durable twin that
|
|
178
|
+
* normalizes through this function) can store a rule list the matcher would read differently from the one written. */
|
|
176
179
|
export declare function normalizeRules(rules: SessionPermissionRules): SessionPermissionRules;
|
|
177
180
|
/**
|
|
178
181
|
* Pure tighten-only diff: the reasons `next` LOOSENS `prior` (empty ⇒ `next` is a pure tightening or no-op).
|
|
@@ -75,18 +75,36 @@ function lexWithin(root, p) {
|
|
|
75
75
|
return true;
|
|
76
76
|
return np === nr || np.startsWith(nr + "/");
|
|
77
77
|
}
|
|
78
|
+
function ruleList(field, value) {
|
|
79
|
+
if (value === undefined)
|
|
80
|
+
return undefined;
|
|
81
|
+
const refuse = (got) => {
|
|
82
|
+
throw new TypeError(`session rules ${field} must be an array of strings, got ${got} — a rule list is written as a list of names (["rm"], not "rm")`);
|
|
83
|
+
};
|
|
84
|
+
if (Array.isArray(value) && Array.from(value).every((entry) => typeof entry === "string")) {
|
|
85
|
+
if (value.some((entry) => entry.trim() === ""))
|
|
86
|
+
refuse("an array holding an empty or blank entry");
|
|
87
|
+
return value;
|
|
88
|
+
}
|
|
89
|
+
return refuse(Array.isArray(value) ? "an array holding an entry that is not a string" : value === null ? "null" : `a ${typeof value}`);
|
|
90
|
+
}
|
|
78
91
|
export function normalizeRules(rules) {
|
|
79
92
|
const out = {};
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
const toolAllow = ruleList("toolAllow", rules.toolAllow);
|
|
94
|
+
const toolDeny = ruleList("toolDeny", rules.toolDeny);
|
|
95
|
+
const commandAllow = ruleList("commandAllow", rules.commandAllow);
|
|
96
|
+
const commandDeny = ruleList("commandDeny", rules.commandDeny);
|
|
97
|
+
const allowDirs = ruleList("allowDirs", rules.allowDirs);
|
|
98
|
+
if (toolAllow !== undefined)
|
|
99
|
+
out.toolAllow = [...toolAllow];
|
|
100
|
+
if (toolDeny !== undefined)
|
|
101
|
+
out.toolDeny = [...toolDeny];
|
|
102
|
+
if (commandAllow !== undefined)
|
|
103
|
+
out.commandAllow = [...commandAllow];
|
|
104
|
+
if (commandDeny !== undefined)
|
|
105
|
+
out.commandDeny = [...commandDeny];
|
|
106
|
+
if (allowDirs !== undefined)
|
|
107
|
+
out.allowDirs = allowDirs.map((d) => posix.normalize(d).replace(/\/+$/, "") || "/");
|
|
90
108
|
return out;
|
|
91
109
|
}
|
|
92
110
|
export function loosenReasons(prior, next, mountedToolNames = NO_DECLARED_TOOLS) {
|
|
@@ -30,7 +30,7 @@ const runsCommand = (spec) => spec.effect === "executes" || spec.value?.kind ===
|
|
|
30
30
|
function projectRow(row) {
|
|
31
31
|
const sorted = (xs) => [...new Set(xs)].sort();
|
|
32
32
|
const all = spellings(row);
|
|
33
|
-
const viaShell = row.reentry?.kind === "shell" || row.launcher?.shellText
|
|
33
|
+
const viaShell = row.reentry?.kind === "shell" || row.launcher?.shellText !== undefined || row.body === "first-operand" || row.body === "assignment-values" || (row.operands?.rest?.kind === "program-text" && row.operands.rest.language === "arithmetic");
|
|
34
34
|
if (row.launcher === undefined) {
|
|
35
35
|
if (viaShell)
|
|
36
36
|
return { valueFlags: [], flags: [], commandString: [], viaShell: true };
|
|
@@ -114,8 +114,8 @@ export declare const SHELL_WRAPPER_TABLE: {
|
|
|
114
114
|
readonly commandString: readonly [];
|
|
115
115
|
};
|
|
116
116
|
readonly watch: {
|
|
117
|
-
readonly valueFlags: readonly [];
|
|
118
|
-
readonly flags: readonly [];
|
|
117
|
+
readonly valueFlags: readonly ["--equexit", "--interval", "--shotsdir", "-n", "-q", "-s"];
|
|
118
|
+
readonly flags: readonly ["--beep", "--chgexit", "--color", "--differences", "--errexit", "--exec", "--follow", "--help", "--no-color", "--no-rerun", "--no-title", "--no-wrap", "--precise", "--version", "-C", "-b", "-c", "-e", "-f", "-g", "-h", "-p", "-r", "-t", "-v", "-w", "-x"];
|
|
119
119
|
readonly commandString: readonly [];
|
|
120
120
|
readonly viaShell: true;
|
|
121
121
|
};
|
|
@@ -276,15 +276,15 @@ export declare const SHELL_WRAPPER_TABLE: {
|
|
|
276
276
|
readonly commandString: readonly [];
|
|
277
277
|
};
|
|
278
278
|
readonly su: {
|
|
279
|
-
readonly valueFlags: readonly [];
|
|
280
|
-
readonly flags: readonly [];
|
|
281
|
-
readonly commandString: readonly [];
|
|
279
|
+
readonly valueFlags: readonly ["--group", "--shell", "--supp-group", "--whitelist-environment", "-G", "-g", "-s", "-w"];
|
|
280
|
+
readonly flags: readonly ["--fast", "--help", "--login", "--no-pty", "--preserve-environment", "--pty", "--version", "-P", "-T", "-V", "-f", "-h", "-l", "-m", "-p"];
|
|
281
|
+
readonly commandString: readonly ["--command", "--session-command", "-c"];
|
|
282
282
|
readonly viaShell: true;
|
|
283
283
|
};
|
|
284
284
|
readonly runuser: {
|
|
285
|
-
readonly valueFlags: readonly [];
|
|
286
|
-
readonly flags: readonly [];
|
|
287
|
-
readonly commandString: readonly [];
|
|
285
|
+
readonly valueFlags: readonly ["--group", "--shell", "--supp-group", "--user", "--whitelist-environment", "-G", "-g", "-s", "-u", "-w"];
|
|
286
|
+
readonly flags: readonly ["--fast", "--help", "--login", "--no-pty", "--preserve-environment", "--pty", "--version", "-P", "-T", "-V", "-f", "-h", "-l", "-m", "-p"];
|
|
287
|
+
readonly commandString: readonly ["--command", "--session-command", "-c"];
|
|
288
288
|
readonly viaShell: true;
|
|
289
289
|
};
|
|
290
290
|
readonly chroot: {
|
|
@@ -15,7 +15,7 @@ export const SHELL_WRAPPER_TABLE = {
|
|
|
15
15
|
timeout: { valueFlags: ["--kill-after", "--signal", "-k", "-s"], flags: ["--foreground", "--help", "--preserve-status", "--verbose", "--version", "-v"], commandString: [], duration: true },
|
|
16
16
|
stdbuf: { valueFlags: ["--error", "--input", "--output", "-e", "-i", "-o"], flags: ["--help", "--version"], commandString: [] },
|
|
17
17
|
xargs: { valueFlags: ["--arg-file", "--delimiter", "--max-args", "--max-chars", "--max-procs", "--process-slot-var", "-E", "-I", "-L", "-P", "-a", "-d", "-n", "-s"], flags: ["--eof", "--exit", "--help", "--interactive", "--max-lines", "--no-run-if-empty", "--null", "--open-tty", "--replace", "--show-limits", "--verbose", "--version", "-0", "-o", "-p", "-r", "-t", "-x"], attached: ["-i", "-l"], commandString: [] },
|
|
18
|
-
watch: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
18
|
+
watch: { valueFlags: ["--equexit", "--interval", "--shotsdir", "-n", "-q", "-s"], flags: ["--beep", "--chgexit", "--color", "--differences", "--errexit", "--exec", "--follow", "--help", "--no-color", "--no-rerun", "--no-title", "--no-wrap", "--precise", "--version", "-C", "-b", "-c", "-e", "-f", "-g", "-h", "-p", "-r", "-t", "-v", "-w", "-x"], commandString: [], viaShell: true },
|
|
19
19
|
ionice: { valueFlags: ["--class", "--classdata", "--pgid", "--pid", "--uid", "-P", "-c", "-n", "-p", "-u"], flags: ["--help", "--ignore", "--version", "-V", "-h", "-t"], commandString: [] },
|
|
20
20
|
setsid: { valueFlags: [], flags: ["--ctty", "--fork", "--help", "--version", "--wait", "-c", "-f", "-w"], commandString: [] },
|
|
21
21
|
taskset: { valueFlags: [], flags: ["--all-tasks", "--cpu-list", "--help", "--pid", "--version", "-a", "-c", "-p"], commandString: [], positional: /^(?:0x)?[\da-f]+$|^[\d,:-]+$/i },
|
|
@@ -43,8 +43,8 @@ export const SHELL_WRAPPER_TABLE = {
|
|
|
43
43
|
compgen: { valueFlags: [], flags: [], commandString: [], executes: ["-C", "-F"] },
|
|
44
44
|
complete: { valueFlags: [], flags: [], commandString: [], executes: ["-C", "-F"] },
|
|
45
45
|
busybox: { valueFlags: [], flags: [], commandString: [] },
|
|
46
|
-
su: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
47
|
-
runuser: { valueFlags: [], flags: [], commandString: [], viaShell: true },
|
|
46
|
+
su: { valueFlags: ["--group", "--shell", "--supp-group", "--whitelist-environment", "-G", "-g", "-s", "-w"], flags: ["--fast", "--help", "--login", "--no-pty", "--preserve-environment", "--pty", "--version", "-P", "-T", "-V", "-f", "-h", "-l", "-m", "-p"], commandString: ["--command", "--session-command", "-c"], viaShell: true },
|
|
47
|
+
runuser: { valueFlags: ["--group", "--shell", "--supp-group", "--user", "--whitelist-environment", "-G", "-g", "-s", "-u", "-w"], flags: ["--fast", "--help", "--login", "--no-pty", "--preserve-environment", "--pty", "--version", "-P", "-T", "-V", "-f", "-h", "-l", "-m", "-p"], commandString: ["--command", "--session-command", "-c"], viaShell: true },
|
|
48
48
|
chroot: { valueFlags: [], flags: [], commandString: [], positional: /^/ },
|
|
49
49
|
setpriv: { valueFlags: [], flags: [], commandString: [] },
|
|
50
50
|
numactl: { valueFlags: [], flags: [], commandString: [] },
|
|
@@ -8,7 +8,8 @@ export const TASK_LIST_TOOL_NAME = "TaskList";
|
|
|
8
8
|
export const TODO_WRITE_TOOL_NAME = "TodoWrite";
|
|
9
9
|
export const REFRESH_MCP_TOOLS_TOOL_NAME = "RefreshMcpTools";
|
|
10
10
|
export const ARTIFACT_TOOL_NAME = "Artifact";
|
|
11
|
-
const
|
|
11
|
+
const shellRuleFace = (params) => ({ grammar: "command", primaryParams: ["command"], params });
|
|
12
|
+
const SHELL_RULE_FACE = shellRuleFace(["command", "timeout", "description", "run_in_background"]);
|
|
12
13
|
const c = (contractId, implementationRevision = "1") => ({ contractId, implementationRevision });
|
|
13
14
|
export const TOOL_CATALOG_ENTRIES = [
|
|
14
15
|
{
|
|
@@ -108,7 +109,7 @@ export const TOOL_CATALOG_ENTRIES = [
|
|
|
108
109
|
{ id: "task-stop-hands", name: "TaskStop", source: "builtin", definedIn: "src/tools/fs/fs-bash.ts", mountedBy: ["hands"], cards: [], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
|
|
109
110
|
{ id: "task-output", name: "TaskOutput", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-output"], face: { family: "delegate", effect: "read", contract: TASK_OUTPUT_CONTRACT } },
|
|
110
111
|
{ id: "task-stop", name: "TaskStop", source: "builtin", definedIn: "src/core/task-registry.ts", mountedBy: ["delegation"], cards: ["task-stop"], face: { family: "delegate", effect: "write", contract: TASK_STOP_CONTRACT } },
|
|
111
|
-
{ id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root", "read_unestablished"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), offloadThresholdChars: 10_000 } },
|
|
112
|
+
{ id: "monitor", name: "Monitor", source: "builtin", definedIn: "src/tools/monitor.ts", mountedBy: ["backgroundTasks"], cards: ["monitor-start", "readonly_out_of_root", "read_unestablished"], face: { family: "other", effect: "write", contract: c("core.monitor@1"), ruleFace: shellRuleFace(["command", "description", "timeout_ms", "persistent"]), offloadThresholdChars: 10_000 } },
|
|
112
113
|
{ id: "send-message", name: "SendMessage", source: "builtin", definedIn: "src/agents/send-message-tool.ts", mountedBy: ["delegation", "hostInternals", "peerLane"], cards: ["send-message"], face: { family: "protocol", effect: "write", contract: c("core.send_message@1") } },
|
|
113
114
|
{ id: "agent-transcript", name: "AgentTranscript", source: "builtin", definedIn: "src/agents/agent-transcript-tool.ts", mountedBy: ["delegation"], cards: ["agent-transcript"], face: { family: "delegate", effect: "read", contract: c("core.agent_transcript@1") } },
|
|
114
115
|
{ id: "list-agents", name: "ListAgents", source: "builtin", definedIn: "src/agents/list-agents-tool.ts", mountedBy: ["delegation", "peerLane"], cards: ["list-agents"], face: { family: "protocol", effect: "read", aliases: ["ListPeers"], contract: c("core.list_agents@1") } },
|
package/dist/core/tool-face.d.ts
CHANGED
|
@@ -201,7 +201,16 @@ export interface ToolFaceDeclaration {
|
|
|
201
201
|
};
|
|
202
202
|
aliases?: string[];
|
|
203
203
|
pathTarget?: ToolPathTarget;
|
|
204
|
-
|
|
204
|
+
/**
|
|
205
|
+
* The rule face ({@link ToolRuleFace}) and, for a definition whose primary parameter is a shell command line, the grammar
|
|
206
|
+
* that judges it — `grammar: "command"`, the SHELL FACE. Every `Bash(<command>)` rule (the command grammar's one spelling,
|
|
207
|
+
* deny / ask / allow, from every source) is judged on a call that answers to the name of such a definition, against that
|
|
208
|
+
* parameter; a bare `Bash` rule stays a rule on the tool of that name. Only a catalog row declares it (a caller's
|
|
209
|
+
* `ToolSpec.ruleFace` is a {@link ToolRuleFace}): the static catalog is the one place the shell face is read from.
|
|
210
|
+
*/
|
|
211
|
+
ruleFace?: ToolRuleFace & {
|
|
212
|
+
readonly grammar?: "command";
|
|
213
|
+
};
|
|
205
214
|
renderHints?: ToolRenderHints;
|
|
206
215
|
offload?: boolean;
|
|
207
216
|
offloadThresholdChars?: number;
|
|
@@ -38,12 +38,13 @@ export declare const RULE_TOOL_NAME_VERDICTS: readonly ["current", "retired", "u
|
|
|
38
38
|
* - `unprefixed_namespace` — carries the `__` namespace separator with no protocol prefix. The roster cannot clear it:
|
|
39
39
|
* a caller tool name or alias carrying `__` is refused at prepare, every engine tool name is `__`-free, and every
|
|
40
40
|
* protocol tool is minted with its prefix.
|
|
41
|
-
* - `unmintable_segment` — carries a protocol prefix, but
|
|
42
|
-
* the mint produces for any peer
|
|
43
|
-
* the minted spelling, so the name reaches none of them
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
41
|
+
* - `unmintable_segment` — carries a protocol prefix, but a segment of it is not one the mint produces: the peer segment
|
|
42
|
+
* (the server / remote agent) is empty or not a spelling the mint produces for any peer (the name is read as written —
|
|
43
|
+
* peers compare exactly — and every mounted tool carries the minted spelling, so the name reaches none of them), or the
|
|
44
|
+
* tool segment is EMPTY (a trailing separator, `mcp__srv__`: no tool is minted under an empty segment). `guidance` says
|
|
45
|
+
* so in one line; `currentName` is the whole name rewritten with the spelling the mint produces (`mcp__github.com__*` →
|
|
46
|
+
* `mcp__github_com__*`), absent when a segment is empty and there is nothing to rewrite. A tool segment that is not empty
|
|
47
|
+
* is never judged: it is a literal tool name or a glob over one, and an unmatched one is the audit's advisory.
|
|
47
48
|
*
|
|
48
49
|
* `roster` = the tool names (aliases included) the reader knows are mounted: a run's assembled roster at prepare, the
|
|
49
50
|
* names a deployment DECLARES it mounts its own tools under at a session-rule store (an empty set when it declares none).
|
|
@@ -67,8 +68,8 @@ export type RuleToolNameVerdict = {
|
|
|
67
68
|
* `undefined` for a name that is not namespaced (no protocol prefix). The peer is the first segment — EMPTY for `mcp____x` and
|
|
68
69
|
* for the bare prefix, which name no peer; the tool segment is the REST of the split rejoined, so a tool whose minted segment
|
|
69
70
|
* itself carries the separator (charset normalization maps `get..issue` → `get__issue`) decomposes back to one tool name, and
|
|
70
|
-
* is absent when the name stops at the peer (`mcp__srv`)
|
|
71
|
-
* allow-entry grammar all read a namespaced name through it.
|
|
71
|
+
* is absent when the name stops at the peer (`mcp__srv`) and EMPTY when it ends in a trailing separator (`mcp__srv__`). The
|
|
72
|
+
* one decomposition: the judgment, the covering matcher and the allow-entry grammar all read a namespaced name through it.
|
|
72
73
|
*/
|
|
73
74
|
export declare function splitNamespacedName(name: string): {
|
|
74
75
|
readonly ns: ProtocolNamespace;
|
|
@@ -56,6 +56,8 @@ export function splitNamespacedName(name) {
|
|
|
56
56
|
function unmintableSegmentOf({ ns, peer, tool }) {
|
|
57
57
|
if (peer === "")
|
|
58
58
|
return { kind: "unmintable_segment", guidance: `the ${ns.id} name has an empty peer segment — no tool is minted under it` };
|
|
59
|
+
if (tool === "")
|
|
60
|
+
return { kind: "unmintable_segment", guidance: `the ${ns.id} name has an empty tool segment — no tool is minted under it; write ${ns.prefix}<peer> to cover every tool of a peer, or ${ns.prefix}<peer>${NAMESPACE_SEPARATOR}<tool> for one tool` };
|
|
59
61
|
const minted = mintNamespacePrefix(ns, peer).slice(ns.prefix.length, -NAMESPACE_SEPARATOR.length);
|
|
60
62
|
if (minted === peer)
|
|
61
63
|
return undefined;
|