@sema-agent/core 7.56.0 → 7.57.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 +541 -0
- package/LICENSE +1 -1
- package/dist/agents/org-delivery-gate.js +3 -4
- package/dist/agents/peer-held-queue.d.ts +69 -49
- package/dist/agents/peer-held-queue.js +255 -106
- package/dist/agents/peer-session-drain.js +63 -43
- package/dist/agents/send-message-tool.js +3 -2
- package/dist/agents/subagent.js +8 -8
- package/dist/brain/anthropic.js +16 -8
- package/dist/brain/open-responses.js +10 -8
- package/dist/brain/openai.js +11 -9
- package/dist/brain/provider-dialect.d.ts +3 -2
- package/dist/brain/provider-dialect.js +2 -2
- package/dist/brain/request-params.d.ts +6 -0
- package/dist/brain/request-params.js +14 -1
- package/dist/brain/stream-engine.d.ts +9 -0
- package/dist/brain/stream-engine.js +24 -10
- package/dist/brain/stream-shared.d.ts +5 -4
- package/dist/brain/stream-shared.js +4 -4
- package/dist/core/allow-entry.d.ts +3 -10
- package/dist/core/allow-entry.js +17 -25
- package/dist/core/approval-object.d.ts +21 -5
- package/dist/core/approval-object.js +44 -17
- package/dist/core/ask-origin.d.ts +2 -2
- package/dist/core/auto-compaction.d.ts +71 -6
- package/dist/core/auto-compaction.js +42 -26
- package/dist/core/background-agent-store.d.ts +6 -2
- package/dist/core/background-agent-store.js +1 -1
- package/dist/core/checkpoint-requirements.d.ts +7 -1
- package/dist/core/checkpoint-requirements.js +2 -0
- package/dist/core/checkpoint-store.d.ts +17 -29
- package/dist/core/checkpoint-store.js +19 -11
- package/dist/core/cross-boundary-taint.js +4 -2
- package/dist/core/engine-notice.d.ts +3 -2
- package/dist/core/engine-notice.js +6 -0
- package/dist/core/file-history-store.d.ts +14 -8
- package/dist/core/file-history-store.js +14 -4
- package/dist/core/gate-lanes.js +2 -3
- package/dist/core/hooks.d.ts +2 -2
- package/dist/core/mailbox-store.d.ts +62 -1
- package/dist/core/mailbox-store.js +59 -0
- package/dist/core/mcp-name-judge.d.ts +46 -0
- package/dist/core/mcp-name-judge.js +61 -0
- package/dist/core/mcp-probe.d.ts +1 -1
- package/dist/core/mcp-probe.js +2 -5
- package/dist/core/mcp.js +10 -30
- package/dist/core/memory-engine/consolidation-driver.js +4 -3
- package/dist/core/memory-engine/distiller.js +26 -17
- package/dist/core/memory-engine/layout.js +5 -2
- package/dist/core/park-selfcheck.js +2 -0
- package/dist/core/permission-rule-consent.d.ts +5 -3
- package/dist/core/permission-rule-consent.js +20 -15
- package/dist/core/permission-rule-model.d.ts +43 -2
- package/dist/core/permission-rule-model.js +52 -8
- package/dist/core/permission-rule-org.js +1 -1
- package/dist/core/protocol-http-auth.d.ts +16 -2
- package/dist/core/protocol-http-auth.js +42 -11
- package/dist/core/protocol-naming.d.ts +20 -0
- package/dist/core/protocol-naming.js +20 -3
- package/dist/core/read-only-shell.d.ts +20 -5
- package/dist/core/read-only-shell.js +29 -14
- package/dist/core/remote-env.d.ts +37 -1
- package/dist/core/remote-env.js +11 -0
- package/dist/core/resume-startup-cause.d.ts +24 -2
- package/dist/core/resume-startup-cause.js +20 -1
- package/dist/core/roles.js +50 -7
- package/dist/core/runner/compact-session.d.ts +10 -0
- package/dist/core/runner/compact-session.js +202 -0
- package/dist/core/runner/compaction-call-options.d.ts +2 -2
- package/dist/core/runner/compaction-seams.d.ts +21 -1
- package/dist/core/runner/compaction-seams.js +11 -8
- package/dist/core/runner/contracts.d.ts +23 -16
- package/dist/core/runner/derived-route-fallback.d.ts +13 -0
- package/dist/core/runner/derived-route-fallback.js +6 -0
- package/dist/core/runner/env-capability-census.d.ts +3 -2
- package/dist/core/runner/env-capability-census.js +9 -3
- package/dist/core/runner/permission-rule-lanes.d.ts +7 -7
- package/dist/core/runner/permission-rule-lanes.js +17 -11
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -0
- package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
- package/dist/core/runner/prepare-config-doors.d.ts +6 -0
- package/dist/core/runner/prepare-config-doors.js +4 -3
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -1
- package/dist/core/runner/prepare-hands-readface.js +2 -0
- package/dist/core/runner/prepare-park-ask.d.ts +2 -0
- package/dist/core/runner/prepare-park-ask.js +2 -2
- package/dist/core/runner/prepare-policy-chain.js +2 -4
- package/dist/core/runner/prepare-task.d.ts +4 -1
- package/dist/core/runner/prepare-task.js +23 -27
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +13 -0
- package/dist/core/runner/prepare-tool-disclosure-mount.js +13 -7
- package/dist/core/runner/rewind-files.d.ts +2 -5
- package/dist/core/runner/rewind-files.js +9 -51
- package/dist/core/runner/run-leg.js +10 -21
- package/dist/core/runner/run-notification-lane.d.ts +2 -0
- package/dist/core/runner/run-notification-lane.js +2 -2
- package/dist/core/runner/run-recovery-lanes.js +3 -4
- package/dist/core/runner/run-settle-and-teardown.js +5 -11
- package/dist/core/runner/run-terminal-adoption.js +3 -4
- package/dist/core/runner/run-turn-boundary.js +3 -5
- package/dist/core/runner/runtask.d.ts +8 -1
- package/dist/core/runner/runtask.js +28 -34
- package/dist/core/runner/send-message-continuation.js +1 -1
- package/dist/core/runner/session-verb-doors.d.ts +28 -0
- package/dist/core/runner/session-verb-doors.js +48 -0
- package/dist/core/runner/session-write-ledger.d.ts +30 -0
- package/dist/core/runner/session-write-ledger.js +101 -0
- package/dist/core/runner/stream-settle-backstop.js +11 -14
- package/dist/core/runner/sub-agent-deny-lists.d.ts +25 -3
- package/dist/core/runner/sub-agent-deny-lists.js +21 -10
- package/dist/core/runner/teardown-bounded.d.ts +1 -0
- package/dist/core/runner/teardown-bounded.js +1 -0
- package/dist/core/runner-deps.d.ts +2 -2
- package/dist/core/secret-env.d.ts +4 -0
- package/dist/core/secret-env.js +3 -0
- package/dist/core/session-policy-store.d.ts +7 -2
- package/dist/core/session-policy-store.js +1 -1
- package/dist/core/shell-wrapper-projection.d.ts +1 -1
- package/dist/core/store-contracts/background-agent-store-contract.js +1 -0
- package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
- package/dist/core/store-contracts/file-history-store-contract.js +9 -0
- package/dist/core/store-contracts/mailbox-store-contract.js +49 -1
- package/dist/core/task-registry-agent.d.ts +8 -4
- package/dist/core/task-registry-agent.js +14 -8
- package/dist/core/task-registry-shared.d.ts +35 -6
- package/dist/core/task-registry-shared.js +11 -0
- package/dist/core/task-registry.d.ts +1 -1
- package/dist/core/task-registry.js +1 -1
- package/dist/core/tool-catalog-entries.d.ts +6 -0
- package/dist/core/tool-catalog-entries.js +1 -0
- package/dist/core/tool-name-aliases.d.ts +32 -2
- package/dist/core/tool-name-aliases.js +23 -0
- package/dist/core/tool-name-readings.d.ts +4 -0
- package/dist/core/tool-name-readings.js +1 -0
- package/dist/core/tool-policy.d.ts +10 -6
- package/dist/core/tool-policy.js +4 -7
- package/dist/core/trace.d.ts +4 -1
- package/dist/core/trace.js +2 -1
- package/dist/core/untrusted-egress.d.ts +20 -0
- package/dist/core/untrusted-egress.js +77 -3
- package/dist/engine/harness/agent-harness.d.ts +7 -5
- package/dist/engine/harness/agent-harness.js +11 -2
- package/dist/engine/loop/agent-loop.d.ts +1 -4
- package/dist/engine/loop/agent-loop.js +26 -4
- package/dist/index.d.ts +12 -7
- package/dist/index.js +7 -4
- package/dist/internal/harness.d.ts +2 -1
- package/dist/internal/harness.js +1 -0
- package/dist/primitives/credential-ref.d.ts +89 -0
- package/dist/primitives/credential-ref.js +125 -0
- package/dist/primitives/index.d.ts +2 -0
- package/dist/primitives/index.js +1 -0
- package/dist/stores/file/checkpoint-store.d.ts +0 -4
- package/dist/stores/file/checkpoint-store.js +0 -1
- package/dist/stores/file/file-history-store.js +12 -4
- package/dist/stores/file/mailbox-store.d.ts +2 -1
- package/dist/stores/file/mailbox-store.js +26 -1
- package/dist/tools/fs/bash-readonly-classifier.d.ts +15 -19
- package/dist/tools/fs/bash-readonly-classifier.js +5 -10
- package/dist/tools/fs/fs-bash.js +10 -3
- package/dist/tools/fs/pattern-kill-guard.d.ts +3 -2
- package/dist/tools/fs/pattern-kill-guard.js +68 -36
- package/dist/tools/fs/shell-read-check.d.ts +10 -0
- package/dist/tools/fs/shell-read-check.js +7 -1
- package/dist/tools/fs/shell-read-identity.d.ts +12 -9
- package/dist/tools/fs/shell-read-identity.js +9 -4
- package/dist/tools/web.js +9 -7
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +79 -3
- package/wire-changes/core-stores-sql@7.57.0.json +29 -0
- package/wire-changes/core@7.57.0.json +1487 -0
- package/wire-changes/mods-kernel@7.57.0.json +29 -0
- package/wire-changes/primitives@7.57.0.json +245 -0
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf,
|
|
2
|
+
import { COMMAND_RULE_TOOL, cardRuleFormOf, escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, segmentCoverageOf, suggestRuleForHost, suggestRuleForName, suggestRuleForTool, suggestRulesForCommand, EDITED_RULE_BREADTH_WARNING_CODES, RULE_OFFER_KINDS, UNCOVERED_SEGMENT_REASONS, RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, isRuleBehavior, } from "./permission-rule-model.js";
|
|
3
3
|
import { readRuleOwner, sameRuleOwner } from "./rule-owner.js";
|
|
4
4
|
import { principalNameOf, readPrincipal } from "./principal-name.js";
|
|
5
5
|
import { isPlainRecord } from "./mods/kernel/receive.js";
|
|
@@ -236,6 +236,12 @@ function approvalRecordDamageOf(rec) {
|
|
|
236
236
|
if (rec.kind === "card" && cand.behavior !== "allow") {
|
|
237
237
|
return `card offer ${at} references candidate ${member}, a ${cand.behavior} rule — a card mints allow candidates only`;
|
|
238
238
|
}
|
|
239
|
+
if (rec.kind === "card") {
|
|
240
|
+
const read = parseRuleText(cand.rule, cand.behavior);
|
|
241
|
+
if ("rule" in read && read.rule.match === "tool" && (typeof rec.command === "string" || cardRuleFormOf(read.rule.tool) !== "tool")) {
|
|
242
|
+
return `card offer ${at} references candidate ${member}, the whole-tool rule "${escapeForDisclosure(read.rule.rule)}" — the engine mints one only on the whole-tool card of a tool with no rule grammar`;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
239
245
|
if (!isValidConsentScope(cand.scope)) {
|
|
240
246
|
return `offer ${at} references candidate ${member}, whose scope is not a consent-face scope (global, project, or session)`;
|
|
241
247
|
}
|
|
@@ -262,7 +268,7 @@ function approvalRecordDamageOf(rec) {
|
|
|
262
268
|
}
|
|
263
269
|
if (detail.length > 0) {
|
|
264
270
|
const rows = detail;
|
|
265
|
-
const table = typeof rec.command === "string" ? segmentCoverageOf(rec.command, { persisted: [] }, { tool:
|
|
271
|
+
const table = typeof rec.command === "string" ? segmentCoverageOf(rec.command, { persisted: [] }, { tool: COMMAND_RULE_TOOL, cwd: undefined }) : undefined;
|
|
266
272
|
if (table === undefined) {
|
|
267
273
|
return `card batch offer ${at} carries reason rows but the record's command cannot back them — a row without a readable source command is a row nobody can audit`;
|
|
268
274
|
}
|
|
@@ -379,10 +385,10 @@ export async function prepareCardApproval(opts) {
|
|
|
379
385
|
e.code = "config.invalid_argument";
|
|
380
386
|
throw e;
|
|
381
387
|
}
|
|
382
|
-
const
|
|
383
|
-
|
|
384
|
-
if (opts.toolName !== CARD_RULE_TOOL && oneRuleCard === undefined)
|
|
388
|
+
const form = cardRuleFormOf(opts.toolName);
|
|
389
|
+
if (form === undefined)
|
|
385
390
|
return undefined;
|
|
391
|
+
const oneRuleCard = form === "command" ? undefined : form;
|
|
386
392
|
const scope = (() => {
|
|
387
393
|
if (opts.scope !== undefined) {
|
|
388
394
|
if (!isValidConsentScope(opts.scope)) {
|
|
@@ -409,14 +415,14 @@ export async function prepareCardApproval(opts) {
|
|
|
409
415
|
try {
|
|
410
416
|
const view = await effectiveOrThrow(storeForOwner(opts.deps.provider, owner), opts.sessionId !== undefined ? { sessionId: opts.sessionId } : undefined);
|
|
411
417
|
const table = view.rules;
|
|
412
|
-
coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool:
|
|
418
|
+
coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool: COMMAND_RULE_TOOL, cwd: opts.cwd, sessionId: opts.sessionId, ...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}) });
|
|
413
419
|
}
|
|
414
420
|
catch {
|
|
415
421
|
coverage = undefined;
|
|
416
422
|
}
|
|
417
423
|
}
|
|
418
424
|
const suggested = oneRuleCard !== undefined
|
|
419
|
-
? [oneRuleCard === "name" ? suggestRuleForName(opts.toolName, opts.command) : suggestRuleForHost(opts.toolName, opts.command)].filter((o) => o !== undefined)
|
|
425
|
+
? [oneRuleCard === "name" ? suggestRuleForName(opts.toolName, opts.command) : oneRuleCard === "domain" ? suggestRuleForHost(opts.toolName, opts.command) : opts.command === "" ? suggestRuleForTool(opts.toolName) : undefined].filter((o) => o !== undefined)
|
|
420
426
|
: suggestRulesForCommand(opts.command, {
|
|
421
427
|
...(coverage !== undefined ? { coverage } : {}),
|
|
422
428
|
scope,
|
|
@@ -468,7 +474,6 @@ export async function prepareCardApproval(opts) {
|
|
|
468
474
|
candidates,
|
|
469
475
|
};
|
|
470
476
|
}
|
|
471
|
-
const CARD_RULE_TOOL = "Bash";
|
|
472
477
|
export async function confirmRuleApproval(opts) {
|
|
473
478
|
const caller = resolveCallerOwner(opts.principal, opts.owner, "confirmRuleApproval");
|
|
474
479
|
const no = (reason, detail) => ({
|
|
@@ -546,6 +551,9 @@ function checkEditedRuleText(text, command) {
|
|
|
546
551
|
const parsed = parseSubmittedSpelling(text);
|
|
547
552
|
if ("reject" in parsed)
|
|
548
553
|
return { ok: false, code: parsed.reject.code, message: parsed.reject.message };
|
|
554
|
+
if (parsed.rule.match === "tool") {
|
|
555
|
+
return { ok: false, message: `the edited rule "${parsed.rule.rule}" names the whole tool — a card's edit names a command rule, and a standing yes to every command is not one a card mints` };
|
|
556
|
+
}
|
|
549
557
|
if (!ruleAdmitsCommand(parsed.rule, command)) {
|
|
550
558
|
return {
|
|
551
559
|
ok: false,
|
|
@@ -838,18 +846,15 @@ export async function prepareCcImport(opts) {
|
|
|
838
846
|
skipped.push({ rule: String(entry), reason: `settings ${behavior} entry is not a string` });
|
|
839
847
|
continue;
|
|
840
848
|
}
|
|
841
|
-
if (!/^[A-Za-z][A-Za-z0-9_]*\(.*\)$/.test(entry)) {
|
|
842
|
-
const reason = /^[A-Za-z][A-Za-z0-9_-]*$/.test(entry)
|
|
843
|
-
? "unsupported.form: a bare tool-name entry is a name-set item, not a command rule — it stays in the settings file, unimported"
|
|
844
|
-
: "unsupported.form: not a command rule spelling this version imports — it stays in the settings file, unimported";
|
|
845
|
-
skipped.push({ rule: entry, reason });
|
|
846
|
-
continue;
|
|
847
|
-
}
|
|
848
849
|
const parsed = parseRuleText(entry, behavior);
|
|
849
850
|
if ("reject" in parsed) {
|
|
850
851
|
skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
|
|
851
852
|
continue;
|
|
852
853
|
}
|
|
854
|
+
if (parsed.rule.match === "tool") {
|
|
855
|
+
skipped.push({ rule: entry, reason: "unsupported.form: a bare tool-name entry is a name-set item the host carries from the settings layer's own lists — it stays in the settings file, unimported" });
|
|
856
|
+
continue;
|
|
857
|
+
}
|
|
853
858
|
if (candidates.some((c) => c.behavior === behavior && c.rule === parsed.rule.rule && sameScope(c.scope, scope)))
|
|
854
859
|
continue;
|
|
855
860
|
candidates.push({ behavior, rule: parsed.rule.rule, scope });
|
|
@@ -77,6 +77,7 @@ import { type DenyFace } from "../tools/fs/read-deny.js";
|
|
|
77
77
|
import type { AssertAllKeysHandled, RuleOffersAbsence } from "./ask-origin.js";
|
|
78
78
|
import { type ToolCallLike } from "./tool-registry.js";
|
|
79
79
|
import { type HostReading } from "./host-target.js";
|
|
80
|
+
import { type NamedCall } from "./tool-name-readings.js";
|
|
80
81
|
import { type PathDenialEvidence, type PathFamily, type PathIdentity } from "../tools/fs/safety.js";
|
|
81
82
|
import { type TargetViews } from "./path-rule-binding.js";
|
|
82
83
|
/**
|
|
@@ -284,6 +285,18 @@ export declare const DOMAIN_RULE_MARKER = "domain:";
|
|
|
284
285
|
*/
|
|
285
286
|
export type RuleGrammar = "command" | "path" | "name" | "domain";
|
|
286
287
|
export declare function ruleToolGrammarOf(toolName: string): RuleGrammar | undefined;
|
|
288
|
+
/**
|
|
289
|
+
* THE rule a tool's approval card mints a "don't ask again" option in — one answer, read by the offer factory and the consent door
|
|
290
|
+
* alike: `command` for the command rule word (the shell card's command rules), `name` / `domain` for a tool of those grammars (the one
|
|
291
|
+
* exact value rule), `tool` for a call no rule word speaks for under any name it answers to ({@link laneToolNamesOf} empty: the
|
|
292
|
+
* whole-tool rule, the reference's fallback card), and `undefined` for the rest — a path tool (no path offer is minted), a shell-face
|
|
293
|
+
* sibling of the command word (its commands are `Bash(…)` rules), and a tool that answers to one of those under an alias it declares
|
|
294
|
+
* (a caller's shell wearing `Bash`: a one-click whole-tool yes on it would be a licence for every program): its card names that it has
|
|
295
|
+
* no rule to offer. `toolName` is the wire name the card mints on; `call` the call's names (absent: the wire name alone — the
|
|
296
|
+
* consent door, which holds no face).
|
|
297
|
+
*/
|
|
298
|
+
export type CardRuleForm = "command" | "name" | "domain" | "tool";
|
|
299
|
+
export declare function cardRuleFormOf(toolName: string, call?: NamedCall): CardRuleForm | undefined;
|
|
287
300
|
/** The parameter a shell-face tool's rule content names as one of the tool's own scalar arguments (`Monitor(description:*)`,
|
|
288
301
|
* `Bash(timeout:5)`), or undefined — the content is then a command spelling. */
|
|
289
302
|
export declare function shellRuleParamOf(tool: string, content: string): string | undefined;
|
|
@@ -311,11 +324,11 @@ export declare function nameRuleSubjectOf(toolName: string, args: unknown): stri
|
|
|
311
324
|
* domain tool's its own name — so a stored `Bash(…)` or `Edit(…)` row reaches a tool that answers to that name under another wire
|
|
312
325
|
* 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
|
|
313
326
|
* person's stored rows, the org partition, the allow lists. Empty ⇒ no grammar speaks for the call. */
|
|
314
|
-
export declare function laneToolNamesOf(req:
|
|
327
|
+
export declare function laneToolNamesOf(req: NamedCall): readonly string[];
|
|
315
328
|
/** The match words a persisted rule is stored with, spelled once: {@link PersistedRuleMatch} is derived from this
|
|
316
329
|
* tuple (each form is documented there). A store backend that checks a stored row's `match` reads this value
|
|
317
330
|
* rather than keeping its own list. */
|
|
318
|
-
export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "pattern", "subpath", "path", "name", "name_prefix", "host", "subdomain"];
|
|
331
|
+
export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildcard", "pattern", "subpath", "path", "name", "name_prefix", "host", "subdomain", "tool"];
|
|
319
332
|
/**
|
|
320
333
|
* Match forms, one family per grammar (`prefix` and `wildcard` are one predicate):
|
|
321
334
|
* · `exact` — admits the one string the rule spells.
|
|
@@ -347,6 +360,8 @@ export declare const PERSISTED_RULE_MATCHES: readonly ["exact", "prefix", "wildc
|
|
|
347
360
|
* · `name` / `name_prefix` — the NAME grammar ({@link parseNameRuleContent}).
|
|
348
361
|
* · `host` / `subdomain` — the DOMAIN grammar ({@link parseDomainRuleContent}): one exact host, or every
|
|
349
362
|
* host below one domain.
|
|
363
|
+
* · `tool` — the WHOLE-TOOL form, a rule with no content (`SendUserFile`, `Bash`, `mcp__srv__get_issue`), on any tool:
|
|
364
|
+
* `command` is empty and the rule reaches every call that answers to the name it spells ({@link wholeToolRuleReaches}).
|
|
350
365
|
*/
|
|
351
366
|
export type PersistedRuleMatch = (typeof PERSISTED_RULE_MATCHES)[number];
|
|
352
367
|
/**
|
|
@@ -1275,6 +1290,32 @@ export declare function adjudicatePersistedDomainRules(rules: readonly Persisted
|
|
|
1275
1290
|
sessionId?: string;
|
|
1276
1291
|
scopeIdentity?: ScopeRootIdentity;
|
|
1277
1292
|
}): PersistedRuleVerdict | undefined;
|
|
1293
|
+
/**
|
|
1294
|
+
* THE predicate of a whole-tool rule (`match: "tool"`) over one call: the rule reaches the call iff the call answers to the name it
|
|
1295
|
+
* spells, under every name the call answers to (the model's spelling, the wire name, the aliases that resolve to it) — the
|
|
1296
|
+
* reference's tool-wide matcher. Every reader of a rule with no content asks it: an entry list's whole-tool row and a person's stored
|
|
1297
|
+
* whole-tool row alike.
|
|
1298
|
+
*/
|
|
1299
|
+
export declare function wholeToolRuleReaches(tool: string, call: NamedCall): boolean;
|
|
1300
|
+
/**
|
|
1301
|
+
* The whole-tool sibling of {@link adjudicatePersistedRules}: the same precedence walk (deny > ask > allow) over the whole-tool rows
|
|
1302
|
+
* (`match: "tool"`) that reach the call ({@link wholeToolRuleReaches}) and are eligible under their own tool ({@link eligiblePersisted},
|
|
1303
|
+
* the one predicate); the first behavior with such a row decides, the first table-order row of it is the answer.
|
|
1304
|
+
*/
|
|
1305
|
+
export declare function adjudicatePersistedToolRules(rules: readonly PersistedRule[], call: {
|
|
1306
|
+
request: NamedCall;
|
|
1307
|
+
cwd: string | undefined;
|
|
1308
|
+
sessionId?: string;
|
|
1309
|
+
scopeIdentity?: ScopeRootIdentity;
|
|
1310
|
+
}): PersistedRuleVerdict | undefined;
|
|
1311
|
+
/**
|
|
1312
|
+
* The ONE offer the card of a tool with no rule grammar carries ({@link cardRuleFormOf} `tool`): the whole-tool rule on its name (the
|
|
1313
|
+
* reference's fallback card, "don't ask again for <tool>"), or undefined when the name has no such rule (the round-trip law: the bare
|
|
1314
|
+
* name, parsed back as an allow, must be the whole-tool rule on that very name).
|
|
1315
|
+
*/
|
|
1316
|
+
export declare function suggestRuleForTool(tool: string): Extract<RuleOffer, {
|
|
1317
|
+
kind: "single";
|
|
1318
|
+
}> | undefined;
|
|
1278
1319
|
/**
|
|
1279
1320
|
* The ONE offer a host tool's approval card carries: the exact rule for the host the call reaches (`WebFetch(domain:<host>)`, the
|
|
1280
1321
|
* reference's "don't ask again for <host>" row), or undefined when no rule spells exactly that host (the round-trip law: the
|
|
@@ -2,13 +2,15 @@ import { homedir } from "node:os";
|
|
|
2
2
|
import { shellWordsOf } from "../shell/shell-words.js";
|
|
3
3
|
import { lexShell } from "../shell/lex.js";
|
|
4
4
|
import { readProgramPosition } from "../tools/fs/bash-program-position.js";
|
|
5
|
-
import { parsePermissionRule, ruleSpellingOf } from "./permission-rule-syntax.js";
|
|
5
|
+
import { isNameableToolName, parsePermissionRule, ruleSpellingOf } from "./permission-rule-syntax.js";
|
|
6
|
+
import { isCoveringRuleName } from "./namespaced-rule-name.js";
|
|
7
|
+
import { toolNameFaultOf } from "./tool-name-aliases.js";
|
|
6
8
|
import { compileReadDeny } from "../tools/fs/read-deny.js";
|
|
7
9
|
import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
8
10
|
import { INLINE_TRUNCATION_MARK, inlineUntrusted } from "./untrusted-text.js";
|
|
9
|
-
import { catalogRuleFaceOf, hostTargetOf, isShellFaceToolName, pathTargetOf, protectivePathTargetOf } from "./tool-registry.js";
|
|
11
|
+
import { catalogRuleFaceOf, catalogToolNames, hostTargetOf, isShellFaceToolName, pathTargetOf, protectivePathTargetOf } from "./tool-registry.js";
|
|
10
12
|
import { HOST_UNREADABLE_SENTENCE, hostPatternAdmits, readHostPattern, spelledHostPattern } from "./host-target.js";
|
|
11
|
-
import { heldValues } from "./tool-name-readings.js";
|
|
13
|
+
import { heldBy, heldValues } from "./tool-name-readings.js";
|
|
12
14
|
import { carriesDotSegment, isAbsoluteForFamily, isIdentityDir, isPathIdentity, isShellRootedSpellingUnmapped, isUncIdentity, pathFamilyOf, pathIdentityOf } from "../tools/fs/safety.js";
|
|
13
15
|
import { bindPathRules, judgeRuleRows, rowsWhereTheyLead } from "./path-rule-binding.js";
|
|
14
16
|
import { readShellCommand } from "./shell-run-candidates.js";
|
|
@@ -135,6 +137,14 @@ export function ruleToolGrammarOf(toolName) {
|
|
|
135
137
|
return "domain";
|
|
136
138
|
return pathTargetOf(toolName) !== undefined ? "path" : undefined;
|
|
137
139
|
}
|
|
140
|
+
export function cardRuleFormOf(toolName, call = { toolName }) {
|
|
141
|
+
if (toolName === COMMAND_RULE_TOOL)
|
|
142
|
+
return "command";
|
|
143
|
+
const grammar = ruleToolGrammarOf(toolName);
|
|
144
|
+
if (grammar === "name" || grammar === "domain")
|
|
145
|
+
return grammar;
|
|
146
|
+
return grammar === undefined && laneToolNamesOf(call).length === 0 && laneToolNamesOf({ toolName }).length === 0 ? "tool" : undefined;
|
|
147
|
+
}
|
|
138
148
|
export function shellRuleParamOf(tool, content) {
|
|
139
149
|
const colon = content.indexOf(":");
|
|
140
150
|
if (colon <= 0)
|
|
@@ -172,7 +182,7 @@ function ruleWordOf(name) {
|
|
|
172
182
|
return COMMAND_RULE_TOOL;
|
|
173
183
|
return ruleToolGrammarOf(name) !== undefined ? name : undefined;
|
|
174
184
|
}
|
|
175
|
-
export const PERSISTED_RULE_MATCHES = Object.freeze(["exact", "prefix", "wildcard", "pattern", "subpath", "path", "name", "name_prefix", "host", "subdomain"]);
|
|
185
|
+
export const PERSISTED_RULE_MATCHES = Object.freeze(["exact", "prefix", "wildcard", "pattern", "subpath", "path", "name", "name_prefix", "host", "subdomain", "tool"]);
|
|
176
186
|
function isCommandMatch(match) {
|
|
177
187
|
return match === "exact" || match === "prefix" || match === "wildcard" || match === "pattern";
|
|
178
188
|
}
|
|
@@ -415,6 +425,19 @@ export function respelledRuleTextOf(text, behavior) {
|
|
|
415
425
|
const read = parseRuleText(candidate, behavior);
|
|
416
426
|
return "rule" in read ? read.rule.rule : undefined;
|
|
417
427
|
}
|
|
428
|
+
function parseWholeToolRule(text, tool, behavior) {
|
|
429
|
+
if (!isNameableToolName(tool) || isCoveringRuleName(tool, behavior)) {
|
|
430
|
+
return reject("invalid.grammar", `"${text}" names a set of tools — a stored rule names one tool, or one tool's content; a set of tools is a list entry's form`);
|
|
431
|
+
}
|
|
432
|
+
const fault = toolNameFaultOf(tool, catalogToolNames(), behavior);
|
|
433
|
+
if (fault === undefined)
|
|
434
|
+
return { rule: { behavior, rule: tool, tool, match: "tool", command: "" } };
|
|
435
|
+
const why = fault.reason === "near_miss"
|
|
436
|
+
? `is another spelling of "${fault.currentName}", which the engine's tool catalog names — a rule names its tool exactly as spelled, so this one would reach no tool`
|
|
437
|
+
: `names no tool under that name${fault.guidance !== undefined ? ` — ${fault.guidance}` : ""}`;
|
|
438
|
+
const write = fault.currentName !== undefined && !why.includes(`"${fault.currentName}"`) ? `; write "${fault.currentName}"` : "";
|
|
439
|
+
return reject("unsupported.tool", `"${text}" ${why}${write}`);
|
|
440
|
+
}
|
|
418
441
|
export function parseRuleText(text, behavior) {
|
|
419
442
|
if (!isRuleBehavior(behavior)) {
|
|
420
443
|
return reject("invalid.grammar", `rule "${text}" was parsed under behavior ${escapeForDisclosure(behavior)}, which is not one of ${RULE_BEHAVIORS.join("/")}`);
|
|
@@ -438,9 +461,8 @@ export function parseRuleText(text, behavior) {
|
|
|
438
461
|
};
|
|
439
462
|
}
|
|
440
463
|
const ruleContent = ruleContentOf(parsed);
|
|
441
|
-
if (ruleContent === undefined)
|
|
442
|
-
return
|
|
443
|
-
}
|
|
464
|
+
if (ruleContent === undefined)
|
|
465
|
+
return parseWholeToolRule(text, parsed.toolName, behavior);
|
|
444
466
|
if (parsed.toolName !== COMMAND_RULE_TOOL) {
|
|
445
467
|
const grammar = ruleToolGrammarOf(parsed.toolName);
|
|
446
468
|
if (grammar === undefined) {
|
|
@@ -510,6 +532,8 @@ export function parseRuleText(text, behavior) {
|
|
|
510
532
|
return { rule: { behavior, rule: formatRuleText(command, match, COMMAND_RULE_TOOL), tool: COMMAND_RULE_TOOL, match, command } };
|
|
511
533
|
}
|
|
512
534
|
export function formatRuleText(command, match, tool) {
|
|
535
|
+
if (match === "tool")
|
|
536
|
+
return tool;
|
|
513
537
|
if (match === "subpath")
|
|
514
538
|
return `${tool}(//${command.startsWith("/") ? command.slice(1) : command}/**)`;
|
|
515
539
|
if (match === "path")
|
|
@@ -1318,7 +1342,7 @@ export function commandCoverageOf(rules, call) {
|
|
|
1318
1342
|
return covering.length > 0 ? { covering } : { uncovered: undefined };
|
|
1319
1343
|
}
|
|
1320
1344
|
function readsOnlyAsSegment(segment, lineObjection, whole) {
|
|
1321
|
-
return lineObjection === undefined && !movesShellCursor(segment) && readOnlyShellVerdict(segment,
|
|
1345
|
+
return lineObjection === undefined && !movesShellCursor(segment) && readOnlyShellVerdict(segment, { bound: () => lineDescriptorsOfText(whole) }).readOnly;
|
|
1322
1346
|
}
|
|
1323
1347
|
function firstEligibleDirectoryRule(rules, directory, call) {
|
|
1324
1348
|
for (const rule of rules) {
|
|
@@ -1434,6 +1458,26 @@ export function adjudicatePersistedDomainRules(rules, call) {
|
|
|
1434
1458
|
}
|
|
1435
1459
|
return undefined;
|
|
1436
1460
|
}
|
|
1461
|
+
export function wholeToolRuleReaches(tool, call) {
|
|
1462
|
+
return heldBy("policy.persistedToolRule", { has: (name) => name === tool }, call);
|
|
1463
|
+
}
|
|
1464
|
+
export function adjudicatePersistedToolRules(rules, call) {
|
|
1465
|
+
const at = { cwd: call.cwd, ...(call.sessionId !== undefined ? { sessionId: call.sessionId } : {}), ...(call.scopeIdentity !== undefined ? { scopeIdentity: call.scopeIdentity } : {}) };
|
|
1466
|
+
for (const behavior of RULE_BEHAVIORS_BY_PRECEDENCE) {
|
|
1467
|
+
const row = rules.find((rule) => rule.match === "tool" && wholeToolRuleReaches(rule.tool, call.request) && eligiblePersisted(rule, { ...at, tool: rule.tool }, behavior));
|
|
1468
|
+
if (row !== undefined)
|
|
1469
|
+
return { behavior, rules: [row] };
|
|
1470
|
+
}
|
|
1471
|
+
return undefined;
|
|
1472
|
+
}
|
|
1473
|
+
export function suggestRuleForTool(tool) {
|
|
1474
|
+
if (cardRuleFormOf(tool) !== "tool")
|
|
1475
|
+
return undefined;
|
|
1476
|
+
const parsed = parseRuleText(formatRuleText("", "tool", tool), "allow");
|
|
1477
|
+
if (!("rule" in parsed) || parsed.rule.match !== "tool" || parsed.rule.tool !== tool)
|
|
1478
|
+
return undefined;
|
|
1479
|
+
return { kind: "single", rule: parsed.rule.rule, match: "tool", command: "" };
|
|
1480
|
+
}
|
|
1437
1481
|
export function suggestRuleForHost(tool, host) {
|
|
1438
1482
|
if (ruleToolGrammarOf(tool) !== "domain")
|
|
1439
1483
|
return undefined;
|
|
@@ -301,7 +301,7 @@ function readOrgRow(row, index) {
|
|
|
301
301
|
const parsed = parseRuleText(text, said);
|
|
302
302
|
if ("reject" in parsed)
|
|
303
303
|
return unreadable(parsed.reject.code, undefined, parsed.reject.respelling);
|
|
304
|
-
if (parsed.rule.tool !== COMMAND_RULE_TOOL)
|
|
304
|
+
if (parsed.rule.tool !== COMMAND_RULE_TOOL || parsed.rule.match === "tool")
|
|
305
305
|
return unreadable("unsupported.tool");
|
|
306
306
|
return { kind: "read", index, rule: text, behavior: said, parsed: parsed.rule };
|
|
307
307
|
}
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* declared headers follows no redirect, {@link carriesDeclaredHeaders}).
|
|
15
15
|
*/
|
|
16
16
|
import type { ProtocolId } from "./protocol-table.js";
|
|
17
|
+
import { type Egress } from "./untrusted-egress.js";
|
|
17
18
|
/** The per-peer HTTP auth declaration every protocol spec carries in the same two fields. */
|
|
18
19
|
export interface ProtocolHttpAuth {
|
|
19
20
|
headers?: Record<string, string>;
|
|
@@ -53,10 +54,23 @@ export declare function declaredHeaderValues(transport: unknown, principal?: str
|
|
|
53
54
|
* from outside takes its own range and the member's value goes with it. A declared value that is also an ordinary word or number (`en`, `1`,
|
|
54
55
|
* `application/json`) goes wherever it occurs in the remote's words, in any case — a legibility cost, never a leak; the failure's closed
|
|
55
56
|
* fields (code, status, delivery) are not text. A declaration too large for one matcher (tens of thousands of characters, which no header
|
|
56
|
-
* carries) makes the whole text the marker: the pass never throws and never shows what it could not match.
|
|
57
|
-
*
|
|
57
|
+
* carries) makes the whole text the marker: the pass never throws and never shows what it could not match. A hit that lies wholly inside
|
|
58
|
+
* a marker a pass already wrote (the exact literals, `redactionMarkerSpans`) is no hit, and one that reaches into a marker takes the
|
|
59
|
+
* whole marker with it, so the pass over its own output answers the same text.
|
|
58
60
|
*/
|
|
59
61
|
export declare function scrubRequestEcho(text: string, declared: readonly string[]): string;
|
|
62
|
+
/**
|
|
63
|
+
* THE one form a remote endpoint's words take when the engine carries them in its own words — an MCP server's failure, a model
|
|
64
|
+
* provider's error body, frame or transport failure, a memory distiller seat's failure: what the request was declared with
|
|
65
|
+
* ({@link scrubRequestEcho}: `declared` is required, so no mint reads a remote's words without naming its request), then the credential
|
|
66
|
+
* vocabulary. Words with no declared value in them read exactly as the vocabulary renders them. Where a declared value is there, the
|
|
67
|
+
* values the vocabulary finds in the ORIGINAL words are taken out with it, so neither disarms the other: removing a declared `Bearer`
|
|
68
|
+
* cannot hide the token after it from the vocabulary, and the vocabulary rewriting part of a declared value cannot leave the rest. A
|
|
69
|
+
* caller that bounds the text for display cuts what this returns, never the remote's raw words, so a cut can leave no part of a value.
|
|
70
|
+
* The function is a fixed point (its own output answers the same text). A store, a log or an event lane that receives the result does
|
|
71
|
+
* not redact again: persistence is not an exit, the mint is.
|
|
72
|
+
*/
|
|
73
|
+
export declare function remoteWords(text: string, declared: readonly string[]): Egress<"same-principal">;
|
|
60
74
|
/** Whether a resolved header set carries anything the declaration bound to its endpoints — a request that does follows no redirect (the
|
|
61
75
|
* next hop is not a declared endpoint; A2A's follow law reads this). */
|
|
62
76
|
export declare function carriesDeclaredHeaders(headers: Record<string, string> | undefined): boolean;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { authSchemeCredentials, credentialHeaderValues, holdOwnCredentialValues } from "./secret-env.js";
|
|
2
2
|
import { readMember } from "./thrown-value.js";
|
|
3
|
+
import { egressText, redactionMarkerSpans } from "./untrusted-egress.js";
|
|
3
4
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
4
5
|
export function headerOwnershipCollision(t) {
|
|
5
6
|
const owners = new Map();
|
|
@@ -106,17 +107,7 @@ function structuredEnd(text, at) {
|
|
|
106
107
|
return text.length;
|
|
107
108
|
}
|
|
108
109
|
export function scrubRequestEcho(text, declared) {
|
|
109
|
-
const
|
|
110
|
-
for (const value of declared)
|
|
111
|
-
for (const spelling of requestEchoSpellings(value))
|
|
112
|
-
forms.add(spelling);
|
|
113
|
-
forms.delete("");
|
|
114
|
-
const members = cookieMembers(text);
|
|
115
|
-
const removed = members.map(([, from, end]) => [from, end]);
|
|
116
|
-
for (const hit of valueHits(text, [...forms])) {
|
|
117
|
-
if (!members.some(([name, from]) => hit[0] >= name && hit[1] <= from))
|
|
118
|
-
removed.push(hit);
|
|
119
|
-
}
|
|
110
|
+
const removed = requestEchoRanges(text, declared);
|
|
120
111
|
if (removed.length === 0)
|
|
121
112
|
return text;
|
|
122
113
|
removed.sort((a, b) => a[0] - b[0] || b[1] - a[1]);
|
|
@@ -132,6 +123,46 @@ export function scrubRequestEcho(text, declared) {
|
|
|
132
123
|
}
|
|
133
124
|
return out + text.slice(kept);
|
|
134
125
|
}
|
|
126
|
+
function requestEchoRanges(text, declared) {
|
|
127
|
+
const forms = new Set();
|
|
128
|
+
for (const value of declared)
|
|
129
|
+
for (const spelling of requestEchoSpellings(value))
|
|
130
|
+
forms.add(spelling);
|
|
131
|
+
forms.delete("");
|
|
132
|
+
const members = cookieMembers(text);
|
|
133
|
+
const removed = members.map(([, from, end]) => [from, end]);
|
|
134
|
+
const markers = redactionMarkerSpans(text);
|
|
135
|
+
for (const hit of valueHits(text, [...forms])) {
|
|
136
|
+
if (members.some(([name, from]) => hit[0] >= name && hit[1] <= from))
|
|
137
|
+
continue;
|
|
138
|
+
if (markers.some(([from, end]) => hit[0] >= from && hit[1] <= end))
|
|
139
|
+
continue;
|
|
140
|
+
const reach = markers.filter(([from, end]) => hit[0] < end && hit[1] > from);
|
|
141
|
+
removed.push([Math.min(hit[0], ...reach.map(([from]) => from)), Math.max(hit[1], ...reach.map(([, end]) => end))]);
|
|
142
|
+
}
|
|
143
|
+
return removed;
|
|
144
|
+
}
|
|
145
|
+
function vocabularyValues(text) {
|
|
146
|
+
const report = { findings: [] };
|
|
147
|
+
egressText("same-principal", text, report);
|
|
148
|
+
return report.findings.map(({ span: [from, end] }) => {
|
|
149
|
+
const found = text.slice(from, end);
|
|
150
|
+
const rendered = egressText("same-principal", found);
|
|
151
|
+
let head = 0;
|
|
152
|
+
while (head < found.length && head < rendered.length && found[head] === rendered[head])
|
|
153
|
+
head++;
|
|
154
|
+
let tail = 0;
|
|
155
|
+
while (tail < found.length - head && tail < rendered.length - head && found[found.length - 1 - tail] === rendered[rendered.length - 1 - tail])
|
|
156
|
+
tail++;
|
|
157
|
+
const value = found.slice(head, found.length - tail);
|
|
158
|
+
return value === "" ? found : value;
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
export function remoteWords(text, declared) {
|
|
162
|
+
if (requestEchoRanges(text, declared).length === 0)
|
|
163
|
+
return egressText("same-principal", text);
|
|
164
|
+
return egressText("same-principal", scrubRequestEcho(text, [...declared, ...vocabularyValues(text)]));
|
|
165
|
+
}
|
|
135
166
|
function cookieMembers(text) {
|
|
136
167
|
const members = [];
|
|
137
168
|
const pattern = new RegExp(COOKIE_START, "g");
|
|
@@ -60,6 +60,26 @@ export declare function findNamespacePrefixCollision(ns: ProtocolNamespace, peer
|
|
|
60
60
|
prefix: string;
|
|
61
61
|
peers: [string, string];
|
|
62
62
|
} | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* EVERY peer in `peers` that mints a {@link mintNamespacePrefix} an earlier peer already minted, in list order, each paired with the
|
|
65
|
+
* FIRST peer that holds the prefix — empty when every peer owns its own. {@link findNamespacePrefixCollision} is its first row: one
|
|
66
|
+
* piece of arithmetic answers both the question "is there a collision" and the question "which names, all of them".
|
|
67
|
+
*/
|
|
68
|
+
export declare function findNamespacePrefixCollisions(ns: ProtocolNamespace, peers: readonly string[]): Array<{
|
|
69
|
+
prefix: string;
|
|
70
|
+
peers: [string, string];
|
|
71
|
+
}>;
|
|
72
|
+
/**
|
|
73
|
+
* One peer's claim ledger for tool names: the function it returns takes a REMOTE tool name, mints its wire name
|
|
74
|
+
* ({@link mintNamespacedToolName}) and answers `{ minted }` when the name is free, or `{ minted, heldBy }` when an earlier call
|
|
75
|
+
* already minted it — `heldBy` is the RAW name that took it first (it keeps the name; the ledger never re-binds). A listing's
|
|
76
|
+
* intake and a judge over declared names ask the same ledger; `heldBy === tool` (the same raw name again) stays distinguishable
|
|
77
|
+
* from two different raw names that folded together.
|
|
78
|
+
*/
|
|
79
|
+
export declare function createToolNameClaims(ns: ProtocolNamespace, peer: string): (tool: string) => {
|
|
80
|
+
minted: string;
|
|
81
|
+
heldBy?: string;
|
|
82
|
+
};
|
|
63
83
|
/**
|
|
64
84
|
* The full registered name for `(peer, tool)` in `ns`. Always starts with {@link mintNamespacePrefix}'s
|
|
65
85
|
* answer for the same peer (the invariant ticket #9's pins hold), and never exceeds
|
|
@@ -27,15 +27,32 @@ export function mintNamespacePrefix(ns, peer) {
|
|
|
27
27
|
return `${ns.prefix}${settlePeerSegment(clampNameSegment(normalizeNameSegment(peer), peerBudget))}${NAME_SEP}`;
|
|
28
28
|
}
|
|
29
29
|
export function findNamespacePrefixCollision(ns, peers) {
|
|
30
|
+
return findNamespacePrefixCollisions(ns, peers)[0];
|
|
31
|
+
}
|
|
32
|
+
export function findNamespacePrefixCollisions(ns, peers) {
|
|
30
33
|
const seen = new Map();
|
|
34
|
+
const collisions = [];
|
|
31
35
|
for (const peer of peers) {
|
|
32
36
|
const prefix = mintNamespacePrefix(ns, peer);
|
|
33
37
|
const first = seen.get(prefix);
|
|
34
|
-
if (first !== undefined)
|
|
35
|
-
|
|
38
|
+
if (first !== undefined) {
|
|
39
|
+
collisions.push({ prefix, peers: [first, peer] });
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
36
42
|
seen.set(prefix, peer);
|
|
37
43
|
}
|
|
38
|
-
return
|
|
44
|
+
return collisions;
|
|
45
|
+
}
|
|
46
|
+
export function createToolNameClaims(ns, peer) {
|
|
47
|
+
const held = new Map();
|
|
48
|
+
return (tool) => {
|
|
49
|
+
const minted = mintNamespacedToolName(ns, peer, tool);
|
|
50
|
+
const heldBy = held.get(minted);
|
|
51
|
+
if (heldBy !== undefined)
|
|
52
|
+
return { minted, heldBy };
|
|
53
|
+
held.set(minted, tool);
|
|
54
|
+
return { minted };
|
|
55
|
+
};
|
|
39
56
|
}
|
|
40
57
|
export function mintNamespacedToolName(ns, peer, tool) {
|
|
41
58
|
const prefix = mintNamespacePrefix(ns, peer);
|
|
@@ -8,6 +8,20 @@ export type ReadOnlyShellVerdict = {
|
|
|
8
8
|
readonly readOnly: false;
|
|
9
9
|
readonly reason: string;
|
|
10
10
|
};
|
|
11
|
+
/** How a caller asks: a shape it already read, the session's working directory, the whole line's descriptor
|
|
12
|
+
* bindings when it judges one segment of a longer line, and the deployment's programs. */
|
|
13
|
+
export interface ReadOnlyShellOptions {
|
|
14
|
+
/** The command already read with {@link readShellCommand} (the gate reads every shell command once). */
|
|
15
|
+
readonly shape?: ShellCommandShape | undefined;
|
|
16
|
+
/** Where the session stands: a `cd` beside a `git` that lands here is no line objection. */
|
|
17
|
+
readonly cwd?: string | undefined;
|
|
18
|
+
/** The descriptor bindings of the WHOLE line, for a caller that judges one segment of it. */
|
|
19
|
+
readonly bound?: LineDescriptors | (() => LineDescriptors) | undefined;
|
|
20
|
+
/** The programs a deployment lets a face run: every program the line names (each segment's program past a `command` /
|
|
21
|
+
* `builtin` prefix, and the program an `xargs` segment hands its input to) must be one of them, else the
|
|
22
|
+
* line is not read-only for that face. It only ever narrows — a program here whose call writes is still not read-only. */
|
|
23
|
+
readonly readers?: ReadonlySet<string> | undefined;
|
|
24
|
+
}
|
|
11
25
|
/** The reference implementation's question for a compound that changes directory and runs git (`cd-git-compound`). */
|
|
12
26
|
export declare const CD_GIT_COMPOUND_SENTENCE = "This command changes directory before running a version-control command, which can pick up untrusted hooks or repository configuration from the target directory. Approve only if you trust it.";
|
|
13
27
|
/**
|
|
@@ -17,8 +31,8 @@ export declare const CD_GIT_COMPOUND_SENTENCE = "This command changes directory
|
|
|
17
31
|
*/
|
|
18
32
|
export declare function changesDirectoryBeforeGit(shape: ShellCommandShape, cwd?: string): boolean;
|
|
19
33
|
/**
|
|
20
|
-
* Read `command` as
|
|
21
|
-
* the caller may be handed in (the gate reads every shell command once).
|
|
34
|
+
* Read `command` as every read-only consumer must: read-only, or the first reason it is not. A shape already read
|
|
35
|
+
* by the caller may be handed in (the gate reads every shell command once).
|
|
22
36
|
*
|
|
23
37
|
* @contract read_only_shell.every_segment — a command line is read-only iff the lexer reads it whole (no
|
|
24
38
|
* unreadable segment, no group/subshell/control structure, no background `&`), no word anywhere carries a
|
|
@@ -30,10 +44,11 @@ export declare function changesDirectoryBeforeGit(shape: ShellCommandShape, cwd?
|
|
|
30
44
|
* the exact forms, `echo`/`ls`/`cd`/`find`/`printf`/`history`/`arch`/`ifconfig`); else the flag-vetted
|
|
31
45
|
* table with its per-row danger predicates, and the `uniq`/`jq` forms. A `cd` beside a `git` in one line
|
|
32
46
|
* is never read-only (the git command's repository is then not the session's) unless the `cd` lands where the session
|
|
33
|
-
* stands ({@link changesDirectoryBeforeGit}).
|
|
34
|
-
*
|
|
47
|
+
* stands ({@link changesDirectoryBeforeGit}). Given {@link ReadOnlyShellOptions.readers}, a program the line names
|
|
48
|
+
* outside them is an objection of its own, met where the reading meets the program. Anything the reading does not
|
|
49
|
+
* positively admit is NOT read-only — the answer is never a guess, and a `false` costs one question.
|
|
35
50
|
*/
|
|
36
|
-
export declare function readOnlyShellVerdict(command: string,
|
|
51
|
+
export declare function readOnlyShellVerdict(command: string, opts?: ReadOnlyShellOptions): ReadOnlyShellVerdict;
|
|
37
52
|
/**
|
|
38
53
|
* The reader's LINE-level objection to `command` — what makes no segment of it read-only in its place, whatever the
|
|
39
54
|
* segment reads on its own: the line's structure, or a `cd` beside a `git` ({@link changesDirectoryBeforeGit}). A caller
|