@sema-agent/core 5.65.0 → 7.0.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 +58 -0
- package/dist/agents/subagent.d.ts +2 -2
- package/dist/agents/subagent.js +11 -0
- package/dist/agents/verify.d.ts +1 -1
- package/dist/brain/anthropic.js +1 -1
- package/dist/brain/errors.d.ts +29 -0
- package/dist/brain/errors.js +20 -0
- package/dist/brain/open-responses.js +2 -2
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +1 -0
- package/dist/brain/status-sink.js +12 -1
- package/dist/brain/stream-engine.js +17 -6
- package/dist/core/auto-compaction.d.ts +26 -0
- package/dist/core/auto-compaction.js +7 -2
- package/dist/core/auto-mode-arming.d.ts +138 -0
- package/dist/core/auto-mode-arming.js +181 -0
- package/dist/core/auto-mode-defaults.d.ts +13 -0
- package/dist/core/auto-mode-defaults.js +5 -0
- package/dist/core/auto-mode-prompt.d.ts +14 -3
- package/dist/core/auto-mode-prompt.js +10 -7
- package/dist/core/auto-mode-rebuild.d.ts +75 -0
- package/dist/core/auto-mode-rebuild.js +41 -0
- package/dist/core/auto-mode.d.ts +15 -0
- package/dist/core/auto-mode.js +4 -2
- package/dist/core/checkpoint-store.d.ts +18 -0
- package/dist/core/context-edit.d.ts +47 -5
- package/dist/core/context-guard.d.ts +1 -1
- package/dist/core/file-history-retention.d.ts +106 -0
- package/dist/core/file-history-retention.js +36 -0
- package/dist/core/file-history-store.d.ts +768 -0
- package/dist/core/file-history-store.js +880 -0
- package/dist/core/governance-codes.d.ts +13 -3
- package/dist/core/governance-codes.js +15 -0
- package/dist/core/hooks.d.ts +39 -7
- package/dist/core/hooks.js +38 -21
- package/dist/core/lsp.d.ts +2 -2
- package/dist/core/mcp.d.ts +29 -7
- package/dist/core/memory-engine/consolidation-driver.d.ts +11 -0
- package/dist/core/memory-engine/consolidation-driver.js +71 -4
- package/dist/core/memory-engine/consolidation.d.ts +25 -2
- package/dist/core/memory-engine/consolidation.js +4 -1
- package/dist/core/memory-engine/distiller.d.ts +84 -1
- package/dist/core/memory-engine/distiller.js +68 -0
- package/dist/core/memory-engine/dual-root.js +3 -0
- package/dist/core/memory-engine/engine.d.ts +328 -15
- package/dist/core/memory-engine/engine.js +355 -29
- package/dist/core/memory-engine/file-backend.d.ts +30 -0
- package/dist/core/memory-engine/file-backend.js +14 -13
- package/dist/core/memory-engine/frontmatter.d.ts +22 -1
- package/dist/core/memory-engine/frontmatter.js +3 -0
- package/dist/core/memory-engine/header-hints.d.ts +5 -0
- package/dist/core/memory-engine/index.d.ts +5 -4
- package/dist/core/memory-engine/index.js +5 -4
- package/dist/core/memory-engine/layout.d.ts +88 -2
- package/dist/core/memory-engine/layout.js +112 -3
- package/dist/core/memory-engine/provenance-wording.d.ts +7 -0
- package/dist/core/memory-engine/provenance-wording.js +3 -0
- package/dist/core/memory-engine/tools.d.ts +89 -8
- package/dist/core/memory-engine/tools.js +263 -22
- package/dist/core/memory-engine/types.d.ts +64 -1
- package/dist/core/memory-recall.d.ts +6 -0
- package/dist/core/memory.d.ts +27 -1
- package/dist/core/memory.js +16 -2
- package/dist/core/permission-rule-consent.d.ts +62 -2
- package/dist/core/permission-rule-consent.js +105 -14
- package/dist/core/permission-rule-model.d.ts +118 -16
- package/dist/core/permission-rule-model.js +57 -9
- package/dist/core/permission-rule-session.d.ts +124 -0
- package/dist/core/permission-rule-session.js +121 -0
- package/dist/core/permission-rule-store.d.ts +65 -2
- package/dist/core/permission-rule-store.js +75 -16
- package/dist/core/permission-rule-sync.d.ts +9 -0
- package/dist/core/permission-rule-sync.js +52 -19
- package/dist/core/retention-policy.d.ts +9 -0
- package/dist/core/retention-policy.js +5 -2
- package/dist/core/retention.d.ts +13 -2
- package/dist/core/runner/assemble-result.d.ts +19 -1
- package/dist/core/runner/assemble-result.js +17 -2
- package/dist/core/runner/compaction-call-options.d.ts +93 -0
- package/dist/core/runner/compaction-call-options.js +3 -0
- package/dist/core/runner/memory-capture-optout.d.ts +80 -0
- package/dist/core/runner/memory-capture-optout.js +53 -0
- package/dist/core/runner/prepare-config-doors.d.ts +5 -0
- package/dist/core/runner/prepare-config-doors.js +16 -0
- package/dist/core/runner/prepare-hands-readface.d.ts +110 -5
- package/dist/core/runner/prepare-hands-readface.js +99 -7
- package/dist/core/runner/prepare-memory.d.ts +88 -0
- package/dist/core/runner/prepare-memory.js +305 -24
- package/dist/core/runner/prepare-task.d.ts +141 -1
- package/dist/core/runner/prepare-task.js +478 -81
- package/dist/core/runner/runtask.d.ts +9 -20
- package/dist/core/runner/runtask.js +133 -96
- package/dist/core/runner/session-file-state-replay.d.ts +18 -10
- package/dist/core/runner/session-file-state-replay.js +52 -1
- package/dist/core/runner/tool-disclosure.js +2 -1
- package/dist/core/runner/turn-attachments.d.ts +22 -12
- package/dist/core/session-store.d.ts +1 -1
- package/dist/core/session-store.js +6 -1
- package/dist/core/session.d.ts +34 -1
- package/dist/core/store-contracts/file-history-store-contract.d.ts +3 -0
- package/dist/core/store-contracts/file-history-store-contract.js +720 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +15 -1
- package/dist/core/task-registry-shared.js +11 -1
- package/dist/core/tool-errors.js +1 -0
- package/dist/core/tool-policy.d.ts +172 -1
- package/dist/core/tool-policy.js +32 -1
- package/dist/core/tool-result-store.js +2 -1
- package/dist/core/trace.d.ts +31 -2
- package/dist/core/types.d.ts +795 -89
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-text.d.ts +1 -1
- package/dist/core/untrusted-text.js +8 -0
- package/dist/core/workflow-run-store-contract.js +6 -2
- package/dist/core/workflow-run-store.d.ts +4 -1
- package/dist/engine/compaction/compaction.d.ts +88 -10
- package/dist/engine/compaction/compaction.js +109 -30
- package/dist/engine/execution-env/node-execution-env.d.ts +9 -1
- package/dist/engine/execution-env/node-execution-env.js +28 -0
- package/dist/engine/harness/agent-harness.d.ts +52 -1
- package/dist/engine/harness/agent-harness.js +36 -1
- package/dist/engine/harness/types.d.ts +26 -1
- package/dist/engine/llm/types.d.ts +50 -4
- package/dist/engine/loop/agent-loop.d.ts +5 -1
- package/dist/engine/loop/agent-loop.js +25 -0
- package/dist/engine/loop/types.d.ts +19 -0
- package/dist/engine/lsp/node-lsp-manager.d.ts +1 -1
- package/dist/engine/session/session.js +1 -1
- package/dist/index.d.ts +20 -9
- package/dist/index.js +16 -7
- package/dist/orchestration/run-workflow-tool.d.ts +20 -2
- package/dist/orchestration/run-workflow-tool.js +22 -3
- package/dist/orchestration/workflow-governance.d.ts +59 -1
- package/dist/orchestration/workflow-governance.js +61 -8
- package/dist/orchestration/workflow-meta.d.ts +4 -2
- package/dist/orchestration/workflow-primitives.js +56 -13
- package/dist/orchestration/workflow-types.d.ts +78 -2
- package/dist/orchestration/workflow.d.ts +20 -0
- package/dist/orchestration/workflow.js +163 -14
- package/dist/prompt-assembly/event-registry.js +1 -1
- package/dist/prompts/default.d.ts +7 -7
- package/dist/stores/file/file-history-store.d.ts +368 -0
- package/dist/stores/file/file-history-store.js +1248 -0
- package/dist/stores/file/index.d.ts +22 -13
- package/dist/stores/file/index.js +4 -4
- package/dist/stores/file/permission-rule-store.d.ts +11 -0
- package/dist/stores/file/permission-rule-store.js +23 -9
- package/dist/stores/file/strategy-store.d.ts +3 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +87 -3
- package/dist/tools/fs/bash-readonly-classifier.js +106 -4
- package/dist/tools/fs/fs-bash.js +9 -5
- package/dist/tools/fs/fs-shared.d.ts +52 -1
- package/dist/tools/fs/fs-shared.js +14 -0
- package/dist/tools/fs/fs-write.d.ts +5 -5
- package/dist/tools/fs/fs-write.js +71 -14
- package/dist/tools/fs/index.d.ts +6 -1
- package/dist/tools/fs/index.js +1 -1
- package/dist/tools/web.js +2 -1
- package/package.json +5 -1
- package/test/export-surface.snapshot.json +169 -23
- package/dist/core/file-snapshot-store.d.ts +0 -165
- package/dist/core/file-snapshot-store.js +0 -259
- package/dist/core/store-contracts/file-snapshot-store-contract.d.ts +0 -13
- package/dist/core/store-contracts/file-snapshot-store-contract.js +0 -134
- package/dist/stores/file/file-snapshot-store.d.ts +0 -58
- package/dist/stores/file/file-snapshot-store.js +0 -353
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
*/
|
|
30
30
|
import { type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
|
|
31
31
|
import type { PermissionRuleStoreProvider, RuleOwner } from "./permission-rule-store.js";
|
|
32
|
+
import type { SessionRuleOverlay } from "./permission-rule-session.js";
|
|
32
33
|
/** One candidate rule inside an approval record: the exact text and where it would apply. */
|
|
33
34
|
export interface RuleCandidate {
|
|
34
35
|
rule: string;
|
|
@@ -199,6 +200,15 @@ export interface RuleConsentDeps {
|
|
|
199
200
|
* happened, and withholding the receipt helps no one.
|
|
200
201
|
*/
|
|
201
202
|
cardEdits?: boolean;
|
|
203
|
+
/**
|
|
204
|
+
* design/382 §4.3 — the SESSION-RULE OVERLAY: where a `{kind:"session"}` scoped candidate lands at
|
|
205
|
+
* redemption (the store write leg is never taken for one), and where the prepare-time coverage read
|
|
206
|
+
* merges a session's standing rows from. Host-provided, session-lifetime, never synced — see
|
|
207
|
+
* `permission-rule-session.ts` for the contract and the reference implementation. Absent ⇒ a
|
|
208
|
+
* session-scope candidate's redemption refuses loudly (there is nowhere for it to land), and
|
|
209
|
+
* coverage reads see no session rows — both fail toward asking.
|
|
210
|
+
*/
|
|
211
|
+
sessionRules?: SessionRuleOverlay;
|
|
202
212
|
}
|
|
203
213
|
/** In-memory approval records — the test backend and the reference CAS semantics, the stale-row
|
|
204
214
|
* envelope read included. */
|
|
@@ -257,8 +267,27 @@ export declare function prepareCardApproval(opts: {
|
|
|
257
267
|
/** The ask's call id and argument digest, recorded for reconciliation. */
|
|
258
268
|
toolCallId?: string;
|
|
259
269
|
boundInputHash?: string;
|
|
260
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* Where a redeemed rule would apply — any of the three consent dimensions (design/382 §4.1).
|
|
272
|
+
*
|
|
273
|
+
* design/382 §4.4 (#490②, BREAKING B2) — the DEFAULT is no longer global:
|
|
274
|
+
* · present ⇒ used as given (the explicit channel; all three members legal, garbage refused loudly);
|
|
275
|
+
* · absent with `cwd` present ⇒ `{ kind: "project", root: cwd }`. Root = the adjudicated call's
|
|
276
|
+
* cwd is a deliberately NARROW default — the engine is a library and repository semantics are
|
|
277
|
+
* host business; a host wanting the true project root passes an explicit scope. The narrow cost
|
|
278
|
+
* is more asks (a nested-directory rule does not cover siblings), the safe direction;
|
|
279
|
+
* · both absent ⇒ a LOUD refusal (`config.missing_scope`): silent-global was the #490② defect,
|
|
280
|
+
* silent-project has no root to anchor, and "where does this consent land" is not guessable.
|
|
281
|
+
*/
|
|
261
282
|
scope?: RuleScope;
|
|
283
|
+
/**
|
|
284
|
+
* design/382 §4.3 — the SESSION IDENTITY of the adjudicated call, threaded by the caller from the
|
|
285
|
+
* original call context exactly like `cwd` (never inferred from the process). It is the coverage
|
|
286
|
+
* read's third eligibility axis: with it, a standing session rule of that session counts as
|
|
287
|
+
* coverage; without it, no session rule covers anything (fail-closed). Independent of `scope` on
|
|
288
|
+
* purpose — `scope` names where a NEW consent would land, this names where the CALL is running.
|
|
289
|
+
*/
|
|
290
|
+
sessionId?: string;
|
|
262
291
|
/**
|
|
263
292
|
* The working directory of the ADJUDICATED CALL — the same task root the gate's lane judged
|
|
264
293
|
* with — threaded by the caller from the original call context. Never inferred from `scope`
|
|
@@ -400,7 +429,12 @@ export type EditedRuleTextPrecheck = {
|
|
|
400
429
|
* so the surface's move is to not offer the edit box at all.
|
|
401
430
|
*/
|
|
402
431
|
export declare function precheckEditedRuleText(text: string, command: string): EditedRuleTextPrecheck;
|
|
403
|
-
/** What a redemption produced. `alreadyRedeemed` marks the replay path — the same dot, no second rule.
|
|
432
|
+
/** What a redemption produced. `alreadyRedeemed` marks the replay path — the same dot, no second rule.
|
|
433
|
+
*
|
|
434
|
+
* `rev` is the DURABLE store's revision. A session-scope redemption (design/382 §4.3) lands in the
|
|
435
|
+
* session overlay and does not move it: its `rev` is a best-effort read of the store's current
|
|
436
|
+
* revision (0 when the store could not be read — the landing is in the overlay either way, and the
|
|
437
|
+
* durable revision is reporting, not the landing's identity). */
|
|
404
438
|
export type RedeemResult = {
|
|
405
439
|
status: "redeemed";
|
|
406
440
|
rule: string;
|
|
@@ -448,6 +482,26 @@ export interface ImportPreview {
|
|
|
448
482
|
rule: string;
|
|
449
483
|
reason: string;
|
|
450
484
|
}>;
|
|
485
|
+
/**
|
|
486
|
+
* #490 件4 — the entries whose SPELLING was rewritten on the way in, `from` as the settings file
|
|
487
|
+
* writes it and `to` as this lane will hold it (`Bash(npm run *)` → `Bash(npm run:*)`).
|
|
488
|
+
*
|
|
489
|
+
* Exactly ONE row per candidate the translation produced, in candidate order, carrying that
|
|
490
|
+
* candidate's `scope`. The scope is load-bearing rather than decoration: the same file line can
|
|
491
|
+
* appear in two layers and become two candidates in two scopes, and a report keyed on the text alone
|
|
492
|
+
* would show one row for two consents (independent-review finding — the row count and the candidate
|
|
493
|
+
* count must be readable against each other).
|
|
494
|
+
*
|
|
495
|
+
* Always present, empty when nothing was translated: a person confirming this batch is being shown
|
|
496
|
+
* rule texts that are not the ones in their file, and a preview that let that pass silently would be
|
|
497
|
+
* asking for consent to something it had not shown them. Entries refused AFTER translation are not
|
|
498
|
+
* here — they are in `skipped`, named as the file writes them.
|
|
499
|
+
*/
|
|
500
|
+
translated: Array<{
|
|
501
|
+
from: string;
|
|
502
|
+
to: string;
|
|
503
|
+
scope: RuleScope;
|
|
504
|
+
}>;
|
|
451
505
|
layers: Array<{
|
|
452
506
|
path: string;
|
|
453
507
|
layer: ImportedSettingsLayer;
|
|
@@ -488,6 +542,12 @@ export interface ImportPreview {
|
|
|
488
542
|
* `false` AND `deduped` — the two axes answer different questions and neither implies the other).
|
|
489
543
|
* A refused row always carries its `reason`. `deduped` counts as LANDED: an equivalent rule
|
|
490
544
|
* already standing means the consent is already in effect.
|
|
545
|
+
*
|
|
546
|
+
* design/382 §4.3 — on a SESSION-scope member the two landed words mean the same thing about a
|
|
547
|
+
* different home: landed in / already in the session's OVERLAY (a session rule's home store), never
|
|
548
|
+
* the persisted store. The words state that the landing happened; DURABILITY is what the member's own
|
|
549
|
+
* `scope` says, so a consumer never has to guess it off the status (deliberately no fourth status
|
|
550
|
+
* value — the closed set names landing outcomes, not storage classes).
|
|
491
551
|
*/
|
|
492
552
|
export type RedeemedBatchMember = {
|
|
493
553
|
readonly candidateIndex: number;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
|
|
3
|
-
import { errText, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
2
|
+
import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, translateImportedWildcardRule, } from "./permission-rule-model.js";
|
|
3
|
+
import { errText, isValidConsentScope, normalizePersistedRule, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
4
4
|
export class InMemoryRuleApprovalRecordStore {
|
|
5
5
|
rows = new Map();
|
|
6
6
|
async get(id) {
|
|
@@ -121,6 +121,9 @@ function approvalRecordDamageOf(rec) {
|
|
|
121
121
|
if (typeof cand !== "object" || cand === null || typeof cand.rule !== "string") {
|
|
122
122
|
return `offer ${at} references candidate ${member}, which is not a candidate row`;
|
|
123
123
|
}
|
|
124
|
+
if (!isValidConsentScope(cand.scope)) {
|
|
125
|
+
return `offer ${at} references candidate ${member}, whose scope is not a consent-face scope (global, project, or session)`;
|
|
126
|
+
}
|
|
124
127
|
if (seen.has(member))
|
|
125
128
|
return `batch offer ${at} references candidate ${member} twice`;
|
|
126
129
|
seen.add(member);
|
|
@@ -196,11 +199,42 @@ export async function prepareCardApproval(opts) {
|
|
|
196
199
|
}
|
|
197
200
|
if (opts.toolName !== CARD_RULE_TOOL)
|
|
198
201
|
return undefined;
|
|
199
|
-
const scope =
|
|
202
|
+
const scope = (() => {
|
|
203
|
+
if (opts.scope !== undefined) {
|
|
204
|
+
if (!isValidConsentScope(opts.scope)) {
|
|
205
|
+
const e = new Error("prepareCardApproval got a scope that is not a consent-face scope (global, project with a non-empty root, or session with a non-empty sessionId) — refusing to guess where this consent should land");
|
|
206
|
+
e.code = "config.invalid_argument";
|
|
207
|
+
throw e;
|
|
208
|
+
}
|
|
209
|
+
return opts.scope;
|
|
210
|
+
}
|
|
211
|
+
if (opts.cwd !== undefined) {
|
|
212
|
+
if (typeof opts.cwd !== "string" || opts.cwd === "") {
|
|
213
|
+
const e = new Error("prepareCardApproval got a cwd that is not a usable path — it cannot anchor the derived project scope, and refusing beats minting a rule scoped to garbage");
|
|
214
|
+
e.code = "config.invalid_argument";
|
|
215
|
+
throw e;
|
|
216
|
+
}
|
|
217
|
+
return { kind: "project", root: opts.cwd };
|
|
218
|
+
}
|
|
219
|
+
const e = new Error("prepareCardApproval was given neither a scope nor a cwd — there is nowhere for this consent to land, and no direction of silence is honest (config.missing_scope): pass an explicit scope, or thread the adjudicated call's cwd");
|
|
220
|
+
e.code = "config.missing_scope";
|
|
221
|
+
throw e;
|
|
222
|
+
})();
|
|
200
223
|
let coverage;
|
|
201
224
|
try {
|
|
202
225
|
const listed = await storeForOwner(opts.deps.provider, owner).list();
|
|
203
|
-
|
|
226
|
+
let table = listed.rules;
|
|
227
|
+
const overlay = opts.deps.sessionRules;
|
|
228
|
+
if (overlay !== undefined && opts.sessionId !== undefined) {
|
|
229
|
+
const served = structuredClone(await overlay.read(opts.sessionId));
|
|
230
|
+
const sessionRows = served.filter((r) => {
|
|
231
|
+
const scope = r?.scope;
|
|
232
|
+
return scope?.kind === "session" && scope.sessionId === opts.sessionId && !("reject" in normalizePersistedRule(r));
|
|
233
|
+
});
|
|
234
|
+
if (sessionRows.length > 0)
|
|
235
|
+
table = [...sessionRows, ...listed.rules];
|
|
236
|
+
}
|
|
237
|
+
coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool: CARD_RULE_TOOL, cwd: opts.cwd, sessionId: opts.sessionId });
|
|
204
238
|
}
|
|
205
239
|
catch {
|
|
206
240
|
coverage = undefined;
|
|
@@ -497,6 +531,41 @@ async function applyRedemption(args) {
|
|
|
497
531
|
if (writer === undefined)
|
|
498
532
|
return { status: "refused", reason: "the resolved permission-rule store has no write face" };
|
|
499
533
|
const origin = originOfRecordKind(args.kind);
|
|
534
|
+
if (args.candidate.scope.kind === "session") {
|
|
535
|
+
const overlay = args.deps.sessionRules;
|
|
536
|
+
if (overlay === undefined) {
|
|
537
|
+
return {
|
|
538
|
+
status: "refused",
|
|
539
|
+
reason: "this deployment wired no session-rule overlay (RuleConsentDeps.sessionRules) — a session-scope authorization has nowhere to land",
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
let outcome;
|
|
543
|
+
try {
|
|
544
|
+
outcome = await overlay.apply(args.candidate.scope.sessionId, {
|
|
545
|
+
rule: args.parsedRule.rule,
|
|
546
|
+
tool: args.parsedRule.tool,
|
|
547
|
+
match: args.parsedRule.match,
|
|
548
|
+
command: args.parsedRule.command,
|
|
549
|
+
add: { dot: args.dot, origin, createdAt: nowIso(args.deps) },
|
|
550
|
+
});
|
|
551
|
+
}
|
|
552
|
+
catch (err) {
|
|
553
|
+
return { status: "refused", reason: `the session-rule overlay refused the write: ${errText(err)}` };
|
|
554
|
+
}
|
|
555
|
+
if ("refused" in outcome) {
|
|
556
|
+
return {
|
|
557
|
+
status: "refused",
|
|
558
|
+
reason: "the session this authorization targets has ended — a session rule dies with its session, and a late or replayed redemption cannot revive it (re-trigger the command in a live session for a fresh card)",
|
|
559
|
+
};
|
|
560
|
+
}
|
|
561
|
+
let rev = 0;
|
|
562
|
+
try {
|
|
563
|
+
rev = (await store.list()).rev;
|
|
564
|
+
}
|
|
565
|
+
catch {
|
|
566
|
+
}
|
|
567
|
+
return { status: "redeemed", rule: args.parsedRule.rule, scope: args.candidate.scope, dot: args.dot, rev, alreadyRedeemed: args.replay };
|
|
568
|
+
}
|
|
500
569
|
for (let attempt = 0; attempt < REDEEM_MAX_ATTEMPTS; attempt++) {
|
|
501
570
|
let rev;
|
|
502
571
|
try {
|
|
@@ -538,6 +607,7 @@ export async function prepareCcImport(opts) {
|
|
|
538
607
|
const owner = resolveCallerOwner(opts.principal, opts.owner, "prepareCcImport");
|
|
539
608
|
const candidates = [];
|
|
540
609
|
const skipped = [];
|
|
610
|
+
const translated = [];
|
|
541
611
|
const layers = [];
|
|
542
612
|
const denyAskBuckets = { deny: 0, ask: 0 };
|
|
543
613
|
for (const layer of opts.layers) {
|
|
@@ -614,17 +684,25 @@ export async function prepareCcImport(opts) {
|
|
|
614
684
|
skipped.push({ rule: entry, reason });
|
|
615
685
|
continue;
|
|
616
686
|
}
|
|
617
|
-
const
|
|
687
|
+
const wildcard = translateImportedWildcardRule(entry);
|
|
688
|
+
if (wildcard !== undefined && "skip" in wildcard) {
|
|
689
|
+
skipped.push({ rule: entry, reason: wildcard.skip });
|
|
690
|
+
continue;
|
|
691
|
+
}
|
|
692
|
+
const text = wildcard?.rule ?? entry;
|
|
693
|
+
const parsed = parseAllowRuleText(text);
|
|
618
694
|
if ("reject" in parsed) {
|
|
619
695
|
skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
|
|
620
696
|
continue;
|
|
621
697
|
}
|
|
622
698
|
if (candidates.some((c) => c.rule === parsed.rule.rule && sameScope(c.scope, scope)))
|
|
623
699
|
continue;
|
|
700
|
+
if (wildcard !== undefined)
|
|
701
|
+
translated.push({ from: entry, to: parsed.rule.rule, scope });
|
|
624
702
|
candidates.push({ rule: parsed.rule.rule, scope });
|
|
625
703
|
}
|
|
626
704
|
}
|
|
627
|
-
const preview = { candidates, skipped, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } };
|
|
705
|
+
const preview = { candidates, skipped, translated, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } };
|
|
628
706
|
if (candidates.length === 0)
|
|
629
707
|
return { preview };
|
|
630
708
|
const record = {
|
|
@@ -708,14 +786,27 @@ export async function redeemRuleBatch(opts) {
|
|
|
708
786
|
const candidate = rec.candidates[candidateIndex];
|
|
709
787
|
const row = { candidateIndex, rule: candidate.rule, scope: candidate.scope };
|
|
710
788
|
let alreadyThere = false;
|
|
711
|
-
|
|
712
|
-
const
|
|
713
|
-
|
|
714
|
-
|
|
789
|
+
if (candidate.scope.kind === "session") {
|
|
790
|
+
const sessionScope = candidate.scope;
|
|
791
|
+
try {
|
|
792
|
+
const overlayRows = opts.deps.sessionRules === undefined ? [] : await opts.deps.sessionRules.read(sessionScope.sessionId);
|
|
793
|
+
alreadyThere = overlayRows.some((r) => r.rule === candidate.rule && sameScope(r.scope, sessionScope));
|
|
794
|
+
}
|
|
795
|
+
catch (err) {
|
|
796
|
+
members.push({ ...row, status: "refused", reason: `could not read the session-rule overlay: ${errText(err)}` });
|
|
797
|
+
continue;
|
|
798
|
+
}
|
|
715
799
|
}
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
800
|
+
else {
|
|
801
|
+
try {
|
|
802
|
+
const snap = await store.list();
|
|
803
|
+
rev = snap.rev;
|
|
804
|
+
alreadyThere = snap.rules.some((r) => r.rule === candidate.rule && sameScope(r.scope, candidate.scope));
|
|
805
|
+
}
|
|
806
|
+
catch (err) {
|
|
807
|
+
members.push({ ...row, status: "refused", reason: `could not read the permission-rule store: ${errText(err)}` });
|
|
808
|
+
continue;
|
|
809
|
+
}
|
|
719
810
|
}
|
|
720
811
|
const res = await redeemRuleTicket({
|
|
721
812
|
ticket: mintRuleTicket(rec.id, candidateIndex),
|
|
@@ -726,7 +817,7 @@ export async function redeemRuleBatch(opts) {
|
|
|
726
817
|
members.push({ ...row, status: "refused", reason: res.reason });
|
|
727
818
|
continue;
|
|
728
819
|
}
|
|
729
|
-
rev = res.rev;
|
|
820
|
+
rev = candidate.scope.kind === "session" ? Math.max(rev, res.rev) : res.rev;
|
|
730
821
|
members.push({ ...row, status: alreadyThere ? "deduped" : "persisted", alreadyRedeemed: res.alreadyRedeemed, dot: res.dot });
|
|
731
822
|
}
|
|
732
823
|
return { members, rev };
|
|
@@ -69,18 +69,43 @@ export type PersistedRuleTool = "Bash";
|
|
|
69
69
|
/** v1 match forms. `"wildcard"` is reserved for v2 and is not a value this version ever produces. */
|
|
70
70
|
export type PersistedRuleMatch = "exact" | "prefix";
|
|
71
71
|
/**
|
|
72
|
-
* Where a rule applies
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
72
|
+
* Where a rule applies — design/382 §4.1, the THREE consent dimensions.
|
|
73
|
+
*
|
|
74
|
+
* · `global` — the personal-global dimension: this OWNER's rules, everywhere (the bucket is already
|
|
75
|
+
* per-owner, {@link import("./permission-rule-store.js").RuleOwner}). The member keeps its historical
|
|
76
|
+
* spelling on purpose: renaming it `personal-global` would be a no-benefit migration of every stored
|
|
77
|
+
* row; the "personal" half lives in documentation and card wording, not in the wire byte.
|
|
78
|
+
* · `project` — applies ONLY inside `root`. `root` is expected to be already canonicalized (realpath)
|
|
79
|
+
* by whoever constructs the scope; the matcher compares with word-boundary containment, never a bare
|
|
80
|
+
* `startsWith` (`/a` must not reach `/ab`).
|
|
81
|
+
* · `session` — applies ONLY to calls of the ONE session it names, and lives in that session's own
|
|
82
|
+
* state (the host's session-rule overlay), NEVER in the persisted store: every durable entrance —
|
|
83
|
+
* the write deltas, sync in both directions, at-rest bytes — refuses or drops a session row
|
|
84
|
+
* (design/382 §4.3, the durable two-member face). `sessionId` uniqueness is the HOST's duty (the
|
|
85
|
+
* `principal` posture): reusing an id is a declaration that it is the same session. A session row
|
|
86
|
+
* cannot be individually deleted — it dies with its session (design/382 §10, ruled; the durable
|
|
87
|
+
* removal entry refuses it rather than answering a false no-op).
|
|
88
|
+
*
|
|
89
|
+
* ORG is deliberately NOT a member (design/382 §4.1): an approval card must be structurally unable to
|
|
90
|
+
* spell an org-wide grant — org governance is a deny/ask snapshot published by an admin surface, with
|
|
91
|
+
* no allow bucket at all. Unspellable beats runtime-refused; the consent validators refuse the spelling
|
|
92
|
+
* anyway if a foreign writer plants it.
|
|
93
|
+
*
|
|
94
|
+
* TWO NAMED VALIDATION FACES read this union (design/382 §4.3): the CONSENT face (prepare candidates,
|
|
95
|
+
* approval records, confirm/redeem, batch outcomes) admits all three members
|
|
96
|
+
* ({@link import("./permission-rule-store.js").isValidConsentScope}); the DURABLE face (the persisted
|
|
97
|
+
* store's write deltas, rule-sync in both directions, at-rest decode) admits only
|
|
98
|
+
* `{global, project}` ({@link import("./permission-rule-store.js").isValidDurableScope}). One table
|
|
99
|
+
* "for both" is exactly what would let a session row leak into the store silently.
|
|
78
100
|
*/
|
|
79
101
|
export type RuleScope = {
|
|
80
102
|
kind: "global";
|
|
81
103
|
} | {
|
|
82
104
|
kind: "project";
|
|
83
105
|
root: string;
|
|
106
|
+
} | {
|
|
107
|
+
kind: "session";
|
|
108
|
+
sessionId: string;
|
|
84
109
|
};
|
|
85
110
|
/** The immutable causal identity of one add: a replica identity plus a monotonic counter. */
|
|
86
111
|
export interface RuleDot {
|
|
@@ -308,7 +333,7 @@ export declare function escapeForDisclosure(value: unknown): string;
|
|
|
308
333
|
* transform is LOSSY and NOT injective (two distinct commands can render identically — that is the
|
|
309
334
|
* price of a readable line). Correlate, match, store and adjudicate on the RAW bytes, which the
|
|
310
335
|
* carrying seats keep unchanged: this function neither changes what those seats contain nor what any
|
|
311
|
-
* of them means. A caller that must instead tell two
|
|
336
|
+
* of them means. A caller that must instead tell two adversarial texts APART wants
|
|
312
337
|
* {@link escapeForDisclosure} (which escapes rather than folds, so nothing collides) or
|
|
313
338
|
* {@link hasUnrenderableCharacters} (refuse instead of render).
|
|
314
339
|
*
|
|
@@ -348,6 +373,47 @@ export declare function parseAllowRuleText(text: string, opts?: {
|
|
|
348
373
|
};
|
|
349
374
|
/** The canonical spelling of a rule for a (already folded) command and match form. */
|
|
350
375
|
export declare function formatAllowRuleText(command: string, match: PersistedRuleMatch): string;
|
|
376
|
+
/**
|
|
377
|
+
* #490 件4 — read ANOTHER product's `Bash(<body> *)` SPACE-STAR spelling as this lane's colon-star
|
|
378
|
+
* prefix rule, for the IMPORT door only.
|
|
379
|
+
*
|
|
380
|
+
* Why a translation is owed at all: the other product mints its prefix suggestions in that spelling
|
|
381
|
+
* (its own classifier reads `X:*` as its earlier prefix form and anything else containing an unescaped star as
|
|
382
|
+
* a wildcard), so a person migrating settings had EVERY prefix rule they had ever approved refused as
|
|
383
|
+
* `unsupported.wildcard` — the import silently emptied out exactly the rules that were the point of it.
|
|
384
|
+
*
|
|
385
|
+
* Translated ONLY for the ONE spelling whose semantics this lane can state exactly: content ending in
|
|
386
|
+
* ` *` with that star the only unescaped one. Upstream compiles a single trailing ` *` into an
|
|
387
|
+
* OPTIONAL suffix (`^body( .*)?$`), which is precisely "the body, or the body followed by a space and
|
|
388
|
+
* more" — this lane's prefix predicate. Any other star placement compiles to a real glob with no rule
|
|
389
|
+
* form here, and a bare `*` is a whole-tool grant that must never become a command rule; both are
|
|
390
|
+
* refused with a reason of their own so the import can DISCLOSE them rather than drop them.
|
|
391
|
+
*
|
|
392
|
+
* WHAT THE TRANSLATED RULE GRANTS, against the source spelling, axis by axis (the direction matters,
|
|
393
|
+
* and it is one-way with one named exception): identical on the bare body, on body-plus-arguments, on
|
|
394
|
+
* the word boundary (`npm run` never reaches `npm run-x`) and on compounds (both sides refuse a chain
|
|
395
|
+
* against a single-command body). NARROWER here on four axes, because upstream normalizes the
|
|
396
|
+
* CANDIDATE before matching and this lane never does: a candidate carrying a redirection, wrapped in
|
|
397
|
+
* `timeout`/`nice`/`nohup`-style words, fed through `xargs`, or prefixed with a `VAR=value` assignment
|
|
398
|
+
* matches there and matches nothing here. WIDER on exactly one: this lane folds runs of whitespace on
|
|
399
|
+
* both sides, so `npm run build` matches the imported rule where upstream's wildcard arm (which does
|
|
400
|
+
* not fold) would refuse it. That axis is NOT introduced by this translation — whitespace folding is
|
|
401
|
+
* this lane's universal reading and already applies to every imported EXACT rule, against an upstream
|
|
402
|
+
* exact arm that is byte-strict — so it is disclosed as a lane property rather than treated as a
|
|
403
|
+
* translation defect. Every other difference runs toward granting less.
|
|
404
|
+
*
|
|
405
|
+
* Returns `undefined` when the text is not a Bash wildcard spelling at all (nothing to translate; the
|
|
406
|
+
* caller proceeds with the text as written), `{ rule }` with the spelling to parse INSTEAD, or
|
|
407
|
+
* `{ skip }` with the reason a star spelling has no form here. Never bypasses the validator: the
|
|
408
|
+
* returned text goes through {@link parseAllowRuleText} exactly like a hand-written one, so the
|
|
409
|
+
* interpreter-head refusal, the control-character screen and the length bound all still apply — a
|
|
410
|
+
* translated `Bash(node *)` is refused `invalid.bare_interpreter_prefix` just as a typed one is.
|
|
411
|
+
*/
|
|
412
|
+
export declare function translateImportedWildcardRule(text: string): {
|
|
413
|
+
rule: string;
|
|
414
|
+
} | {
|
|
415
|
+
skip: string;
|
|
416
|
+
} | undefined;
|
|
351
417
|
/**
|
|
352
418
|
* Does this rule's command pattern admit `command`?
|
|
353
419
|
*
|
|
@@ -400,9 +466,16 @@ export declare function ruleLaneSegmentsOf(command: string): readonly string[] |
|
|
|
400
466
|
* not contain `/ab`. Both sides are expected to be canonical already.
|
|
401
467
|
*/
|
|
402
468
|
export declare function pathWithinRoot(path: string, root: string): boolean;
|
|
403
|
-
/** Does a rule's scope cover a task running in `cwd
|
|
404
|
-
* without one it covers nothing
|
|
405
|
-
|
|
469
|
+
/** Does a rule's scope cover a task running in `cwd` (and, for the session dimension, in the session
|
|
470
|
+
* named by `sessionId`)? A project rule needs a cwd to compare against; without one it covers nothing
|
|
471
|
+
* (fail-closed). A session rule (design/382 §4.3) covers a call iff the call's `sessionId` equals the
|
|
472
|
+
* scope's — and a call that carries NO session identity is covered by no session rule, the same
|
|
473
|
+
* fail-closed arm as project-without-cwd: an axis the caller did not thread is an axis the rule
|
|
474
|
+
* cannot speak on. `sessionId` is threaded from the ORIGINAL call context by the caller (the 375
|
|
475
|
+
* r3-② cwd posture), never inferred from the process. ONE function on purpose: this is the scope
|
|
476
|
+
* conjunct of the single eligibility predicate ({@link eligibleContext}), and a sister predicate
|
|
477
|
+
* would be the second fact source the 375 §5.1 discipline exists to forbid. */
|
|
478
|
+
export declare function scopeCoversCwd(scope: RuleScope, cwd: string | undefined, sessionId?: string): boolean;
|
|
406
479
|
/** Is this rule live — i.e. does it still carry at least one add? Deleted adds are removed by the store
|
|
407
480
|
* when tombstones are applied, so a rule with an empty `adds` is a rule that no longer exists. */
|
|
408
481
|
export declare function isRuleLive(rule: PersistedAllowRule): boolean;
|
|
@@ -418,6 +491,12 @@ export declare function isRuleLive(rule: PersistedAllowRule): boolean;
|
|
|
418
491
|
*
|
|
419
492
|
* `scope` rides beside the rule rather than inside it for the PROPOSED half's sake: a
|
|
420
493
|
* {@link ParsedAllowRule} carries no scope, so a proposal is judged against the scope it would LAND in.
|
|
494
|
+
*
|
|
495
|
+
* design/382 §4.3 — the call context carries a THIRD axis, `sessionId` (optional: an existing caller
|
|
496
|
+
* that threads only `{tool, cwd}` still compiles and still means what it meant — no session rule can
|
|
497
|
+
* cover its calls, the fail-closed direction). It is the session dimension's whole eligibility story:
|
|
498
|
+
* every consumer of this ONE predicate (the gate's conjunction arm, the coverage table, the consent
|
|
499
|
+
* prepare) inherits it without a second judge existing anywhere.
|
|
421
500
|
*/
|
|
422
501
|
export declare function eligibleContext(rule: {
|
|
423
502
|
tool: string;
|
|
@@ -425,6 +504,7 @@ export declare function eligibleContext(rule: {
|
|
|
425
504
|
}, call: {
|
|
426
505
|
tool: string;
|
|
427
506
|
cwd: string | undefined;
|
|
507
|
+
sessionId?: string;
|
|
428
508
|
}): boolean;
|
|
429
509
|
/**
|
|
430
510
|
* design/375 §5.1 — MAY this PERSISTED rule participate in admitting this call at all? The liveness
|
|
@@ -440,6 +520,7 @@ export declare function eligibleContext(rule: {
|
|
|
440
520
|
export declare function eligiblePersisted(rule: PersistedAllowRule, call: {
|
|
441
521
|
tool: string;
|
|
442
522
|
cwd: string | undefined;
|
|
523
|
+
sessionId?: string;
|
|
443
524
|
}): boolean;
|
|
444
525
|
/**
|
|
445
526
|
* Find the rules that admit this command for a task in `cwd`, or `undefined`.
|
|
@@ -474,12 +555,24 @@ export declare function findAdmittingRule(rules: readonly PersistedAllowRule[],
|
|
|
474
555
|
tool: string;
|
|
475
556
|
command: string;
|
|
476
557
|
cwd: string | undefined;
|
|
558
|
+
sessionId?: string;
|
|
477
559
|
}): readonly PersistedAllowRule[] | undefined;
|
|
478
560
|
/**
|
|
479
561
|
* design/375 §5.2 — the per-segment coverage table for `command`: which segments an eligible rule
|
|
480
|
-
* already admits, judged with the SAME split
|
|
481
|
-
*
|
|
482
|
-
*
|
|
562
|
+
* already admits, judged with the SAME split and the same eligibility predicates as the gate's
|
|
563
|
+
* conjunction arm — one instrument, so the suggestion face and the consent prepare can never call a
|
|
564
|
+
* segment covered that the gate would ask about (or vice versa).
|
|
565
|
+
*
|
|
566
|
+
* The split is the OFFER reading ({@link OFFER_READING}, #490 修①), which is the MATCH reading on
|
|
567
|
+
* every axis but where a redirection is refused — this table exists to align, row for row, with
|
|
568
|
+
* {@link suggestRulesForCommand}'s own segmentation, and a table that stopped one command short of the
|
|
569
|
+
* face it aligns with is a table that face discards (its positional belt), leaving every segment
|
|
570
|
+
* reading UNCOVERED. Each ROW's verdict is still the matcher's, unchanged: `covered` is
|
|
571
|
+
* {@link ruleAdmitsCommand} under the MATCH reading, so a segment carrying a redirection is admitted
|
|
572
|
+
* by nothing and reports `false`. The gate's own conjunction arm keeps the whole-string floor and is
|
|
573
|
+
* not consulted here: for a redirection-bearing compound it has no segments at all, and this table
|
|
574
|
+
* saying "an eligible rule admits `npm test`" is a true statement about that segment either way — it
|
|
575
|
+
* is read only to keep an already-covered segment out of a batch offer, never as an admission.
|
|
483
576
|
*
|
|
484
577
|
* TWO SOURCES, two predicates, and the seat labels are the contract:
|
|
485
578
|
* · `persisted` — rules already in the store, judged {@link eligiblePersisted} ∧ admit: a tombstoned
|
|
@@ -503,6 +596,7 @@ export declare function segmentCoverageOf(command: string, rules: {
|
|
|
503
596
|
}, call: {
|
|
504
597
|
tool: string;
|
|
505
598
|
cwd: string | undefined;
|
|
599
|
+
sessionId?: string;
|
|
506
600
|
}): readonly SegmentCoverage[] | undefined;
|
|
507
601
|
/** One row of a per-segment coverage table (see {@link segmentCoverageOf}): the FOLDED, trimmed
|
|
508
602
|
* segment text (a display/correlation seat, never adjudication input) and whether an eligible rule
|
|
@@ -632,9 +726,17 @@ export type RuleOffer = {
|
|
|
632
726
|
* over-offering; every offered rule still passes the validator at redemption, so an over-offer widens
|
|
633
727
|
* nothing).
|
|
634
728
|
*
|
|
635
|
-
* Returns an empty array for anything the rule lane cannot speak for (
|
|
636
|
-
*
|
|
637
|
-
* honest answer.
|
|
729
|
+
* Returns an empty array for anything the rule lane cannot speak for (substitutions, subshells,
|
|
730
|
+
* backgrounding, an unreadable segment) — the card then simply carries no "don't ask again" option,
|
|
731
|
+
* which is the honest answer.
|
|
732
|
+
*
|
|
733
|
+
* A REDIRECTION is the one member of that list judged per SEGMENT rather than over the whole string
|
|
734
|
+
* (#490 修①, {@link OFFER_READING}): `a && b 2>/dev/null && c` offers a batch for `a` and `c` with
|
|
735
|
+
* `uncoveredSegments: 1`, exactly as the same compound dirtied by a glob always has. A SIMPLE command
|
|
736
|
+
* carrying one still offers nothing — its only segment is the dirty one — and no offer this function
|
|
737
|
+
* mints ever admits the redirection-bearing text: the minting grammar and the matcher keep the
|
|
738
|
+
* whole-string floor, so the compound stays un-admitted and the honest `uncoveredSegments` count says
|
|
739
|
+
* so (design/382 §3.2: neither a sanitized rule text nor a widened matcher is on the table).
|
|
638
740
|
*/
|
|
639
741
|
export declare function suggestRulesForCommand(command: string, ctx?: {
|
|
640
742
|
readonly coverage?: readonly SegmentCoverage[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { parsePermissionRule } from "./permission-rules.js";
|
|
2
|
-
import { parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
2
|
+
import { carriesShellRedirection, parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
3
3
|
import { inlineUntrusted } from "./untrusted-text.js";
|
|
4
4
|
export const MAX_RULE_TEXT_CHARS = 512;
|
|
5
5
|
export const BARE_INTERPRETER_NAMES = new Set([
|
|
@@ -117,17 +117,22 @@ function reject(code, message) {
|
|
|
117
117
|
return { reject: { code, message } };
|
|
118
118
|
}
|
|
119
119
|
const RULE_LANE_FLOOR = { pathPrefixedNameIsText: true };
|
|
120
|
-
const MATCH_READING = { terminator: "keep", quotedOperatorsAreText: false };
|
|
121
|
-
const PROGRAM_RUNS_READING = { terminator: "strip", quotedOperatorsAreText: true };
|
|
120
|
+
const MATCH_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "reject" };
|
|
121
|
+
const PROGRAM_RUNS_READING = { terminator: "strip", quotedOperatorsAreText: true, redirection: "reject" };
|
|
122
|
+
const OFFER_READING = { terminator: "keep", quotedOperatorsAreText: false, redirection: "tolerate" };
|
|
122
123
|
function commandBasename(name) {
|
|
123
124
|
return name.slice(name.lastIndexOf("/") + 1);
|
|
124
125
|
}
|
|
125
126
|
function ruleLaneShapeOf(command, reading) {
|
|
126
|
-
const split = splitShellCompoundSegments(command, { trailingTerminator: reading.terminator });
|
|
127
|
+
const split = splitShellCompoundSegments(command, { trailingTerminator: reading.terminator, redirection: reading.redirection });
|
|
127
128
|
if ("reject" in split)
|
|
128
129
|
return { reject: split.reject };
|
|
129
130
|
const names = [];
|
|
130
131
|
for (const segment of split.segments) {
|
|
132
|
+
if (reading.redirection === "tolerate" && carriesShellRedirection(segment)) {
|
|
133
|
+
names.push(undefined);
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
131
136
|
const floor = parseLeadingCommandName(segment, { ...RULE_LANE_FLOOR, quotedOperatorsAreText: reading.quotedOperatorsAreText });
|
|
132
137
|
if ("reject" in floor) {
|
|
133
138
|
return {
|
|
@@ -219,6 +224,45 @@ export function parseAllowRuleText(text, opts) {
|
|
|
219
224
|
export function formatAllowRuleText(command, match) {
|
|
220
225
|
return `Bash(${command}${match === "prefix" ? ":*" : ""})`;
|
|
221
226
|
}
|
|
227
|
+
function hasUnescapedStar(s) {
|
|
228
|
+
for (let i = 0; i < s.length; i++) {
|
|
229
|
+
if (s[i] !== "*")
|
|
230
|
+
continue;
|
|
231
|
+
let backslashes = 0;
|
|
232
|
+
for (let j = i - 1; j >= 0 && s[j] === "\\"; j--)
|
|
233
|
+
backslashes++;
|
|
234
|
+
if (backslashes % 2 === 0)
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
239
|
+
function isImportedWildcardContent(content) {
|
|
240
|
+
return !content.endsWith(":*") && hasUnescapedStar(content);
|
|
241
|
+
}
|
|
242
|
+
export function translateImportedWildcardRule(text) {
|
|
243
|
+
const parsed = parsePermissionRule(text);
|
|
244
|
+
const content = parsed.ruleContent;
|
|
245
|
+
if (content === undefined || parsed.toolName !== "Bash" || !isImportedWildcardContent(content))
|
|
246
|
+
return undefined;
|
|
247
|
+
const body = /^(.+) \*$/.exec(content)?.[1];
|
|
248
|
+
if (body === undefined || body.trim() === "") {
|
|
249
|
+
return {
|
|
250
|
+
skip: "unsupported.wildcard: only the trailing `<command> *` form has a prefix-rule equivalent here — it stays in the settings file, unimported",
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
if (hasUnescapedStar(body)) {
|
|
254
|
+
return {
|
|
255
|
+
skip: "unsupported.wildcard: a `*` before the end of the pattern matches anything at that position, which has no rule form in this version — it stays in the settings file, unimported",
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
const bodyShape = ruleLaneShapeOf(body, MATCH_READING);
|
|
259
|
+
if (!("reject" in bodyShape) && bodyShape.segments.length > 1) {
|
|
260
|
+
return {
|
|
261
|
+
skip: "unsupported.wildcard: a `<chain> *` pattern names more than one command, and the trailing-star form has no equivalent for a chain — it stays in the settings file, unimported",
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
return { rule: formatAllowRuleText(body, "prefix") };
|
|
265
|
+
}
|
|
222
266
|
export function ruleAdmitsCommand(rule, command) {
|
|
223
267
|
return admitsUnder(rule, command, MATCH_READING);
|
|
224
268
|
}
|
|
@@ -251,16 +295,18 @@ export function pathWithinRoot(path, root) {
|
|
|
251
295
|
const base = root.endsWith("/") ? root : root + "/";
|
|
252
296
|
return path.startsWith(base);
|
|
253
297
|
}
|
|
254
|
-
export function scopeCoversCwd(scope, cwd) {
|
|
298
|
+
export function scopeCoversCwd(scope, cwd, sessionId) {
|
|
255
299
|
if (scope.kind === "global")
|
|
256
300
|
return true;
|
|
301
|
+
if (scope.kind === "session")
|
|
302
|
+
return sessionId !== undefined && sessionId === scope.sessionId;
|
|
257
303
|
return cwd !== undefined && pathWithinRoot(cwd, scope.root);
|
|
258
304
|
}
|
|
259
305
|
export function isRuleLive(rule) {
|
|
260
306
|
return rule.adds.length > 0;
|
|
261
307
|
}
|
|
262
308
|
export function eligibleContext(rule, call) {
|
|
263
|
-
return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd);
|
|
309
|
+
return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd, call.sessionId);
|
|
264
310
|
}
|
|
265
311
|
export function eligiblePersisted(rule, call) {
|
|
266
312
|
return isRuleLive(rule) && eligibleContext(rule, call);
|
|
@@ -297,7 +343,7 @@ export function segmentCoverageOf(command, rules, call) {
|
|
|
297
343
|
const folded = foldSpacing(command);
|
|
298
344
|
if (folded === undefined)
|
|
299
345
|
return undefined;
|
|
300
|
-
const shape = ruleLaneShapeOf(folded,
|
|
346
|
+
const shape = ruleLaneShapeOf(folded, OFFER_READING);
|
|
301
347
|
if ("reject" in shape)
|
|
302
348
|
return undefined;
|
|
303
349
|
const proposed = rules.proposed ?? [];
|
|
@@ -308,7 +354,7 @@ export function segmentCoverageOf(command, rules, call) {
|
|
|
308
354
|
}));
|
|
309
355
|
}
|
|
310
356
|
export function suggestRulesForCommand(command, ctx) {
|
|
311
|
-
const shape = ruleLaneShapeOf(command,
|
|
357
|
+
const shape = ruleLaneShapeOf(command, OFFER_READING);
|
|
312
358
|
if ("reject" in shape)
|
|
313
359
|
return [];
|
|
314
360
|
const folded = foldSpacing(command);
|
|
@@ -333,7 +379,7 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
333
379
|
}
|
|
334
380
|
return offers;
|
|
335
381
|
}
|
|
336
|
-
const foldedShape = ruleLaneShapeOf(folded,
|
|
382
|
+
const foldedShape = ruleLaneShapeOf(folded, OFFER_READING);
|
|
337
383
|
if ("reject" in foldedShape || foldedShape.segments.length !== shape.segments.length)
|
|
338
384
|
return offers;
|
|
339
385
|
const segments = foldedShape.segments.map((s) => s.trim());
|
|
@@ -341,6 +387,8 @@ export function suggestRulesForCommand(command, ctx) {
|
|
|
341
387
|
const alignedTable = table !== undefined && table.length === segments.length && table.every((row, i) => row.segment === segments[i]) ? table : undefined;
|
|
342
388
|
const coveredAt = (i) => alignedTable?.[i]?.covered === true;
|
|
343
389
|
const mintSegmentRule = (segment) => {
|
|
390
|
+
if (carriesShellRedirection(segment))
|
|
391
|
+
return undefined;
|
|
344
392
|
const body = bodyOf(tokensOf(segment));
|
|
345
393
|
if (body !== undefined) {
|
|
346
394
|
const parsed = parseAllowRuleText(formatAllowRuleText(body, "prefix"));
|