@sema-agent/core 5.57.0 → 5.59.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 +96 -0
- package/dist/agents/cascade.d.ts +1 -1
- package/dist/agents/cumulative-stats.d.ts +1 -1
- package/dist/agents/observer.d.ts +2 -2
- package/dist/agents/peer-admission.d.ts +1 -1
- package/dist/agents/retain-ledger.d.ts +2 -2
- package/dist/agents/roster-store.d.ts +8 -8
- package/dist/agents/send-message-tool.d.ts +2 -2
- package/dist/agents/subagent-steps.d.ts +1 -1
- package/dist/agents/subagent.d.ts +13 -13
- package/dist/agents/team.d.ts +5 -5
- package/dist/agents/tool-filter.d.ts +2 -2
- package/dist/agents/verify.d.ts +1 -1
- package/dist/bench/metrics.d.ts +35 -35
- package/dist/brain/degrading.d.ts +1 -1
- package/dist/brain/errors.d.ts +3 -3
- package/dist/brain/reasoning.d.ts +2 -2
- package/dist/brain/repetition.d.ts +1 -1
- package/dist/brain/route-adjudicator.d.ts +8 -1
- package/dist/brain/route-adjudicator.js +8 -1
- package/dist/brain/status-sink.d.ts +2 -2
- package/dist/brain/stream-shared.d.ts +1 -1
- package/dist/config/catalog.d.ts +5 -5
- package/dist/core/arg-summary.d.ts +4 -4
- package/dist/core/ask-class.d.ts +2 -2
- package/dist/core/ask-question.d.ts +1 -1
- package/dist/core/auto-compaction.d.ts +15 -15
- package/dist/core/auto-mode.d.ts +5 -5
- package/dist/core/background-agent-store.d.ts +20 -20
- package/dist/core/background-shell.d.ts +4 -4
- package/dist/core/checkpoint-store.d.ts +35 -27
- package/dist/core/context-edit.d.ts +1 -1
- package/dist/core/context-guard.d.ts +1 -1
- package/dist/core/exec-output-tail.d.ts +6 -6
- package/dist/core/file-snapshot-store.d.ts +8 -8
- package/dist/core/git-worktree-env.d.ts +3 -3
- package/dist/core/governance-codes.d.ts +37 -10
- package/dist/core/governance-codes.js +54 -1
- package/dist/core/hooks.d.ts +73 -33
- package/dist/core/hooks.js +87 -25
- package/dist/core/image-downsample.d.ts +1 -1
- package/dist/core/locked-config.d.ts +1 -1
- package/dist/core/lsp.d.ts +1 -1
- package/dist/core/mailbox-store.d.ts +1 -1
- package/dist/core/mcp.d.ts +3 -3
- package/dist/core/memory-engine/consolidation-driver.d.ts +211 -0
- package/dist/core/memory-engine/consolidation-driver.js +427 -0
- package/dist/core/memory-engine/consolidation.d.ts +119 -3
- package/dist/core/memory-engine/consolidation.js +22 -1
- package/dist/core/memory-engine/data-plane.d.ts +1 -1
- package/dist/core/memory-engine/distiller.d.ts +550 -0
- package/dist/core/memory-engine/distiller.js +598 -0
- package/dist/core/memory-engine/dual-root.d.ts +1 -1
- package/dist/core/memory-engine/engine.d.ts +143 -10
- package/dist/core/memory-engine/engine.js +149 -23
- package/dist/core/memory-engine/file-backend.d.ts +14 -2
- package/dist/core/memory-engine/file-backend.js +3 -0
- package/dist/core/memory-engine/index.d.ts +6 -3
- package/dist/core/memory-engine/index.js +4 -2
- package/dist/core/memory-engine/layout.js +20 -6
- package/dist/core/memory-engine/origin-clearance.d.ts +1 -1
- package/dist/core/memory-engine/scope-contract.d.ts +4 -4
- package/dist/core/memory-engine/sync-client.d.ts +16 -16
- package/dist/core/memory-engine/sync.d.ts +4 -4
- package/dist/core/memory-engine/types.d.ts +17 -0
- package/dist/core/memory-recall.d.ts +1 -1
- package/dist/core/memory.d.ts +2 -2
- package/dist/core/permission-rule-consent.d.ts +185 -36
- package/dist/core/permission-rule-consent.js +219 -44
- package/dist/core/permission-rule-model.d.ts +235 -31
- package/dist/core/permission-rule-model.js +105 -35
- package/dist/core/permission-rules.d.ts +9 -9
- package/dist/core/remote-env.d.ts +8 -8
- package/dist/core/roles.d.ts +3 -3
- package/dist/core/roles.js +1 -0
- package/dist/core/runner/assemble-result.d.ts +2 -2
- package/dist/core/runner/compaction-call-options.d.ts +3 -3
- package/dist/core/runner/memory-consolidation-driver.d.ts +49 -0
- package/dist/core/runner/memory-consolidation-driver.js +60 -0
- package/dist/core/runner/memory-consolidation.d.ts +1 -1
- package/dist/core/runner/prepare-config-doors.d.ts +3 -3
- package/dist/core/runner/prepare-task.d.ts +21 -21
- package/dist/core/runner/prepare-task.js +24 -17
- package/dist/core/runner/prepare-workspace-restore.d.ts +2 -2
- package/dist/core/runner/runtask.d.ts +11 -11
- package/dist/core/runner/runtask.js +4 -4
- package/dist/core/runner/session-rule-policy.d.ts +1 -1
- package/dist/core/runner/teardown-bounded.d.ts +1 -1
- package/dist/core/runner/tool-disclosure.d.ts +2 -2
- package/dist/core/runner/turn-attachments.d.ts +11 -11
- package/dist/core/scheduler.d.ts +5 -5
- package/dist/core/secret-env.d.ts +1 -1
- package/dist/core/sensitive-path-policy.d.ts +1 -1
- package/dist/core/session-policy-store.d.ts +2 -2
- package/dist/core/session-reconcile.d.ts +2 -2
- package/dist/core/session-store.d.ts +3 -3
- package/dist/core/session.d.ts +1 -1
- package/dist/core/shutdown-debug.d.ts +2 -2
- package/dist/core/side-query.d.ts +2 -2
- package/dist/core/spec-contract.d.ts +1 -1
- package/dist/core/store-contracts/contract-harness.d.ts +2 -2
- package/dist/core/store-contracts/contract-kit-version.d.ts +2 -2
- package/dist/core/store-contracts/mailbox-store-contract.d.ts +1 -1
- package/dist/core/store-contracts/mailbox-store-contract.js +1 -1
- package/dist/core/task-notification.d.ts +5 -5
- package/dist/core/task-registry-agent.d.ts +12 -12
- package/dist/core/task-registry-monitor.d.ts +1 -1
- package/dist/core/task-registry-shared.d.ts +41 -41
- package/dist/core/task-registry.d.ts +12 -12
- package/dist/core/tool-detach.d.ts +2 -2
- package/dist/core/tool-errors.d.ts +3 -3
- package/dist/core/tool-policy.d.ts +113 -28
- package/dist/core/tool-policy.js +80 -1
- package/dist/core/tool-result-budget.d.ts +1 -1
- package/dist/core/tool-result-store.d.ts +2 -2
- package/dist/core/tools.d.ts +1 -1
- package/dist/core/trace.d.ts +26 -23
- package/dist/core/types.d.ts +164 -86
- package/dist/core/types.js +4 -3
- package/dist/core/untrusted-egress.d.ts +1 -1
- package/dist/core/untrusted-text.d.ts +7 -7
- package/dist/core/wiring-manifest.d.ts +5 -5
- package/dist/core/workflow-journal-store.d.ts +14 -14
- package/dist/core/workflow-run-store-contract.d.ts +1 -1
- package/dist/core/workflow-run-store-contract.js +1 -1
- package/dist/core/workflow-run-store.d.ts +4 -4
- package/dist/engine/compaction/compaction.d.ts +3 -3
- package/dist/engine/compaction/utils.d.ts +2 -2
- package/dist/engine/execution-env/kill-tree.d.ts +1 -1
- package/dist/engine/execution-env/node-execution-env.d.ts +8 -8
- package/dist/engine/harness/agent-harness.d.ts +6 -6
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/types.d.ts +10 -10
- package/dist/engine/llm/types.d.ts +14 -14
- package/dist/engine/loop/agent-loop.d.ts +3 -3
- package/dist/engine/loop/types.d.ts +4 -4
- package/dist/engine/lsp/node-lsp-manager.d.ts +2 -2
- package/dist/engine/session/import-validate.d.ts +1 -1
- package/dist/engine/session/log-digest.d.ts +1 -1
- package/dist/engine/session/memory-repo.d.ts +2 -2
- package/dist/engine/session/session.d.ts +4 -4
- package/dist/fixtures/index.d.ts +4 -4
- package/dist/index.d.ts +6 -5
- package/dist/index.js +5 -4
- package/dist/orchestration/goal.d.ts +1 -1
- package/dist/orchestration/run-spec.d.ts +1 -1
- package/dist/orchestration/run-workflow-tool.d.ts +12 -12
- package/dist/orchestration/workflow-governance.d.ts +4 -4
- package/dist/orchestration/workflow-observe.d.ts +1 -1
- package/dist/orchestration/workflow-script-runner.d.ts +1 -1
- package/dist/orchestration/workflow-script-store.d.ts +9 -9
- package/dist/orchestration/workflow-size-guideline.d.ts +1 -1
- package/dist/orchestration/workflow-types.d.ts +5 -5
- package/dist/orchestration/workflow.d.ts +10 -10
- package/dist/prompt-assembly/artifact-store.d.ts +1 -1
- package/dist/prompt-assembly/artifact.d.ts +1 -1
- package/dist/prompt-assembly/assemble.d.ts +1 -1
- package/dist/prompt-assembly/composer.d.ts +2 -2
- package/dist/prompt-assembly/epoch.d.ts +2 -2
- package/dist/prompt-assembly/event-registry.d.ts +1 -1
- package/dist/prompt-assembly/explain.d.ts +3 -3
- package/dist/prompt-assembly/tool-catalog.d.ts +1 -1
- package/dist/prompt-assembly/turn-snapshot.d.ts +4 -4
- package/dist/prompt-assembly/types.d.ts +12 -12
- package/dist/prompts/coordinator.d.ts +1 -1
- package/dist/prompts/default.d.ts +8 -8
- package/dist/prompts/simple-sections.d.ts +3 -3
- package/dist/prompts/supervisor.d.ts +2 -2
- package/dist/scenarios/full-body.d.ts +3 -3
- package/dist/scenarios/scenario-registry.d.ts +1 -1
- package/dist/stores/cc/sidecar-transcript.d.ts +3 -3
- package/dist/stores/file/fs-atomic.d.ts +2 -2
- package/dist/stores/file/index.d.ts +1 -1
- package/dist/stores/file/session-store.d.ts +2 -2
- package/dist/stores/file/workflow-journal-store.d.ts +4 -4
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/encoding.d.ts +4 -4
- package/dist/tools/fs/fs-bash.d.ts +3 -3
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-shared.d.ts +6 -6
- package/dist/tools/fs/index.d.ts +2 -2
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/pdf.d.ts +1 -1
- package/dist/tools/fs/read-deny.d.ts +1 -1
- package/dist/tools/fs/safety.d.ts +9 -9
- package/dist/tools/fs/search.d.ts +2 -2
- package/dist/tools/monitor.d.ts +3 -3
- package/dist/tools/task-list.d.ts +2 -2
- package/dist/tools/web.d.ts +4 -4
- package/dist/tools/worktree.d.ts +5 -5
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +1826 -3
|
@@ -32,6 +32,12 @@
|
|
|
32
32
|
* deny/ask layer (`permission-rule-org.ts`) judges every segment and runs ahead of the allow lane at
|
|
33
33
|
* the gate — a compound-prefix allow can therefore never outrun a deny on any segment it admits.
|
|
34
34
|
*
|
|
35
|
+
* design/375 adds upstream's OTHER half of that per-segment evaluation on the allow side: a compound
|
|
36
|
+
* whose EVERY segment is admitted by some eligible rule is covered by the CONJUNCTION of those rules
|
|
37
|
+
* (`findAdmittingRule` answers with the coverage SET). No single rule widens: the matcher predicates
|
|
38
|
+
* above are untouched, and the set form is the decision layer's answer, fenced by the same
|
|
39
|
+
* ahead-running per-segment deny/ask layer.
|
|
40
|
+
*
|
|
35
41
|
* The floor is `parseLeadingCommandName` + `splitShellCompoundSegments` — the one simple-command parser
|
|
36
42
|
* and the one segmentation, both already shared with the read-only classifier, the reversibility probe,
|
|
37
43
|
* the coarse command-name policy and the skill tool specifier. A second tokenizer would drift, and drift
|
|
@@ -276,6 +282,40 @@ export declare const SUGGESTION_LEXICON: readonly string[];
|
|
|
276
282
|
* through, so an ordinary rule text reads normally. The result is length-bounded.
|
|
277
283
|
*/
|
|
278
284
|
export declare function escapeForDisclosure(value: unknown): string;
|
|
285
|
+
/**
|
|
286
|
+
* Render raw, untrusted COMMAND text for a display surface — the minimal safe baseline for the seats
|
|
287
|
+
* that carry post-rewrite command bytes verbatim ({@link SegmentRuleSuggestion.segment} is the
|
|
288
|
+
* motivating one; a card's own `command`/`args` echo has the same hazard).
|
|
289
|
+
*
|
|
290
|
+
* WHAT IT DOES, and why the two character families are not handled the same way:
|
|
291
|
+
* · `\p{Cf}` (the bidirectional controls U+061C/U+200E/U+200F/U+202A–U+202E/U+2066–U+2069 and the
|
|
292
|
+
* zero-width formatters ZWSP/ZWNJ/ZWJ/WORD JOINER/SOFT HYPHEN/BOM) is REMOVED. These occupy no
|
|
293
|
+
* width, so deleting them is what makes the rendered line agree with what a reader believes they
|
|
294
|
+
* are reading — a `rm -rf x` that DISPLAYS as something benign is exactly the deception the seat
|
|
295
|
+
* would otherwise hand on. This is the same family
|
|
296
|
+
* {@link import("./tool-policy.js").AskRequest.hasBidiControls} raises its warning bit for; the
|
|
297
|
+
* bit says "the raw bytes carry them", this function is what a surface renders instead.
|
|
298
|
+
* · Controls and line/paragraph separators (C0 incl. TAB and NEWLINE, DEL, C1, U+2028/U+2029) are
|
|
299
|
+
* FOLDED TO A SPACE with runs collapsed, not removed: they occupy width (or a whole line), and
|
|
300
|
+
* deleting them would glue two tokens into one word that reads as a different command. TAB is not
|
|
301
|
+
* excepted here — a one-line display value has no use for one, and a rule text's foldable-tab
|
|
302
|
+
* exception belongs to the VALIDATOR class, not to this one.
|
|
303
|
+
* The result is then capped by code point (never splitting a surrogate pair) with a `…` marker, and
|
|
304
|
+
* carries the shared authority-envelope neutralization every untrusted inline value gets — this text
|
|
305
|
+
* can land on a model-visible line as easily as on a human one.
|
|
306
|
+
*
|
|
307
|
+
* WHAT IT IS NOT. A DISPLAY baseline, never an identity channel and never adjudication input: the
|
|
308
|
+
* transform is LOSSY and NOT injective (two distinct commands can render identically — that is the
|
|
309
|
+
* price of a readable line). Correlate, match, store and adjudicate on the RAW bytes, which the
|
|
310
|
+
* 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 hostile texts APART wants
|
|
312
|
+
* {@link escapeForDisclosure} (which escapes rather than folds, so nothing collides) or
|
|
313
|
+
* {@link hasUnrenderableCharacters} (refuse instead of render).
|
|
314
|
+
*
|
|
315
|
+
* @param text the raw untrusted text (a `String(x)` is applied to anything that is not one).
|
|
316
|
+
* @param maxLen code-point cap for the rendered result; defaults to the disclosure-line budget.
|
|
317
|
+
*/
|
|
318
|
+
export declare function renderUntrustedCommandText(text: unknown, maxLen?: number): string;
|
|
279
319
|
/**
|
|
280
320
|
* Does this text carry a character of the class {@link escapeForDisclosure} exists for — the class
|
|
281
321
|
* {@link CONTROL_CHARS_RE} names? Asked by a caller that must REFUSE such a value rather than render
|
|
@@ -367,35 +407,188 @@ export declare function scopeCoversCwd(scope: RuleScope, cwd: string | undefined
|
|
|
367
407
|
* when tombstones are applied, so a rule with an empty `adds` is a rule that no longer exists. */
|
|
368
408
|
export declare function isRuleLive(rule: PersistedAllowRule): boolean;
|
|
369
409
|
/**
|
|
370
|
-
*
|
|
410
|
+
* design/375 §5.1 — MAY this rule speak about this call's CONTEXT? Tool identity plus scope coverage,
|
|
411
|
+
* and nothing about the rule's liveness: the CONTEXT question is the half a not-yet-persisted rule (a
|
|
412
|
+
* card's proposed candidate, which has no add dots yet) can already answer.
|
|
413
|
+
*
|
|
414
|
+
* The tool conjunct is not decoration: more than one tool takes a shell command string, and matching on
|
|
415
|
+
* the command alone would let an approval given on one surface resolve a call on another. The scope
|
|
416
|
+
* conjunct is the project fence — project A's approval is not project B's, and a project rule with no
|
|
417
|
+
* cwd to compare against covers nothing (fail-closed, {@link scopeCoversCwd}).
|
|
371
418
|
*
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
|
|
419
|
+
* `scope` rides beside the rule rather than inside it for the PROPOSED half's sake: a
|
|
420
|
+
* {@link ParsedAllowRule} carries no scope, so a proposal is judged against the scope it would LAND in.
|
|
421
|
+
*/
|
|
422
|
+
export declare function eligibleContext(rule: {
|
|
423
|
+
tool: string;
|
|
424
|
+
scope: RuleScope;
|
|
425
|
+
}, call: {
|
|
426
|
+
tool: string;
|
|
427
|
+
cwd: string | undefined;
|
|
428
|
+
}): boolean;
|
|
429
|
+
/**
|
|
430
|
+
* design/375 §5.1 — MAY this PERSISTED rule participate in admitting this call at all? The liveness
|
|
431
|
+
* layer on top of {@link eligibleContext}: a tombstoned rule (zero surviving adds) is a rule that no
|
|
432
|
+
* longer exists and must not count as coverage anywhere.
|
|
433
|
+
*
|
|
434
|
+
* THE single eligibility predicate for every consumer that reads the persisted store — the gate's
|
|
435
|
+
* conjunction arm and its coverage-set selection here, the suggestion face's coverage judgement and the
|
|
436
|
+
* consent prepare's ({@link segmentCoverageOf} is the shared instrument). One predicate, so a rule that
|
|
437
|
+
* one consumer refuses cannot be a rule another counts: the historically inlined trio of checks
|
|
438
|
+
* (live / tool / scope) is this function, verbatim.
|
|
439
|
+
*/
|
|
440
|
+
export declare function eligiblePersisted(rule: PersistedAllowRule, call: {
|
|
441
|
+
tool: string;
|
|
442
|
+
cwd: string | undefined;
|
|
443
|
+
}): boolean;
|
|
444
|
+
/**
|
|
445
|
+
* Find the rules that admit this command for a task in `cwd`, or `undefined`.
|
|
446
|
+
*
|
|
447
|
+
* THE ANSWER IS A COVERAGE SET (design/375 §5.1, the upstream per-segment form): a single rule that
|
|
448
|
+
* admits the whole command is a one-element set — the historical arm, table order preserved verbatim —
|
|
449
|
+
* and a COMPOUND whose every segment is admitted by some eligible rule is covered by the CONJUNCTION of
|
|
450
|
+
* those rules, returned as the set that covered it. The judging ladder, in order:
|
|
451
|
+
* 1. the fail-closed floor (a shape the lane cannot read matches nothing), evaluated once;
|
|
452
|
+
* 2. the single-rule arm — whole-string exact, single/compound-prefix — exactly the historical loop:
|
|
453
|
+
* first table-order match wins and is what the decision names. A single rule naming the command is
|
|
454
|
+
* found BEFORE any conjunction: reporting one rule is narrower than reporting a set;
|
|
455
|
+
* 3. the conjunction arm, compounds only: every segment must be admitted by some eligible rule under
|
|
456
|
+
* the MATCH reading (the allow side's own under-approximating instrument — never the deny side's
|
|
457
|
+
* wider PROGRAM_RUNS reading). Per segment the FIRST table-order admitting rule is selected, and
|
|
458
|
+
* the coverage set is those selections in segment order, de-duplicated — the same command against
|
|
459
|
+
* the same rule table always names the same set (reporting-order determinism, the historical
|
|
460
|
+
* "order is defined only for reporting" contract extended to sets).
|
|
461
|
+
*
|
|
462
|
+
* What the conjunction arm deliberately does NOT change: {@link ruleAdmitsCommand} itself. A
|
|
463
|
+
* single-command prefix body still never admits a chain — `Bash(npm:*)` does not admit
|
|
464
|
+
* `npm test && curl evil.example` — but the chain is now admitted when EVERY segment has a standing
|
|
465
|
+
* yes, which is the upstream decision form (per-segment allow, any-segment deny decided strictly
|
|
466
|
+
* earlier at the gate's org layer). A trailing-terminator spelling (`a && b ;`) keeps the conservative
|
|
467
|
+
* answer by construction: under the MATCH reading its tail is an empty segment the floor refuses, so
|
|
468
|
+
* the whole command is outside the lane and asks.
|
|
469
|
+
*
|
|
470
|
+
* Order is defined only for reporting (what the decision names); no rule is stronger than another —
|
|
471
|
+
* they are all the same one answer, "the person already said yes to this".
|
|
375
472
|
*/
|
|
376
473
|
export declare function findAdmittingRule(rules: readonly PersistedAllowRule[], call: {
|
|
377
474
|
tool: string;
|
|
378
475
|
command: string;
|
|
379
476
|
cwd: string | undefined;
|
|
380
|
-
}): PersistedAllowRule | undefined;
|
|
381
|
-
/**
|
|
382
|
-
|
|
477
|
+
}): readonly PersistedAllowRule[] | undefined;
|
|
478
|
+
/**
|
|
479
|
+
* design/375 §5.2 — the per-segment coverage table for `command`: which segments an eligible rule
|
|
480
|
+
* already admits, judged with the SAME split, the same MATCH reading and the same eligibility
|
|
481
|
+
* predicates as the gate's conjunction arm — one instrument, so the suggestion face and the consent
|
|
482
|
+
* prepare can never call a segment covered that the gate would ask about (or vice versa).
|
|
483
|
+
*
|
|
484
|
+
* TWO SOURCES, two predicates, and the seat labels are the contract:
|
|
485
|
+
* · `persisted` — rules already in the store, judged {@link eligiblePersisted} ∧ admit: a tombstoned
|
|
486
|
+
* rule, another project's rule and another tool's rule are uniformly NOT coverage;
|
|
487
|
+
* · `proposed` — rules of the CURRENT batch that are not yet persisted (no add dots yet), judged
|
|
488
|
+
* {@link eligibleContext} ∧ admit against the scope each would land in. Judging these with the
|
|
489
|
+
* persisted predicate would make "this batch, once redeemed, covers the command" permanently false
|
|
490
|
+
* — the liveness layer is the one thing a proposal legitimately lacks.
|
|
491
|
+
*
|
|
492
|
+
* Returns `undefined` for a command the lane cannot read at all (the fail-closed floor — the caller's
|
|
493
|
+
* honest answer is then "no coverage table", which every consumer must treat as all-uncovered). The
|
|
494
|
+
* reported `segment` texts are the FOLDED spellings, trimmed — display/correlation seats, never
|
|
495
|
+
* adjudication inputs.
|
|
496
|
+
*/
|
|
497
|
+
export declare function segmentCoverageOf(command: string, rules: {
|
|
498
|
+
persisted: readonly PersistedAllowRule[];
|
|
499
|
+
proposed?: readonly {
|
|
500
|
+
rule: ParsedAllowRule;
|
|
501
|
+
scope: RuleScope;
|
|
502
|
+
}[];
|
|
503
|
+
}, call: {
|
|
504
|
+
tool: string;
|
|
505
|
+
cwd: string | undefined;
|
|
506
|
+
}): readonly SegmentCoverage[] | undefined;
|
|
507
|
+
/** One row of a per-segment coverage table (see {@link segmentCoverageOf}): the FOLDED, trimmed
|
|
508
|
+
* segment text (a display/correlation seat, never adjudication input) and whether an eligible rule
|
|
509
|
+
* already admits it. */
|
|
510
|
+
export interface SegmentCoverage {
|
|
511
|
+
readonly segment: string;
|
|
512
|
+
readonly covered: boolean;
|
|
513
|
+
}
|
|
514
|
+
/** One member of a {@link RuleOffer} batch: a rule the engine offers to mint for ONE segment of a
|
|
515
|
+
* compound command. `segment` is the FOLDED segment text the member was minted from — a rendering
|
|
516
|
+
* seat, never adjudication input. */
|
|
517
|
+
export interface SegmentRuleSuggestion {
|
|
383
518
|
/** The canonical rule text the host would redeem. */
|
|
384
|
-
rule: string;
|
|
385
|
-
match: PersistedRuleMatch;
|
|
519
|
+
readonly rule: string;
|
|
520
|
+
readonly match: PersistedRuleMatch;
|
|
386
521
|
/** The command pattern in the rule, for a host that wants to render it without re-parsing. */
|
|
387
|
-
command: string;
|
|
522
|
+
readonly command: string;
|
|
523
|
+
/** The (folded) segment this suggestion came from. Raw post-rewrite command bytes — a substring
|
|
524
|
+
* of the same call's `args`/`command` values riding beside it on every surface that carries
|
|
525
|
+
* this seat — so render it with contextual escaping, exactly like those.
|
|
526
|
+
*
|
|
527
|
+
* RAW is the contract here and does not change: this seat carries the bytes, because the bytes are
|
|
528
|
+
* what a rule would be minted from and what a correlation keys on. The rendering side of that
|
|
529
|
+
* contract is {@link renderUntrustedCommandText} — the shipped display baseline (bidi and
|
|
530
|
+
* zero-width controls removed, controls/line separators folded, length capped, envelope-neutralized)
|
|
531
|
+
* a surface can adopt instead of writing its own, since raw command bytes displayed unprocessed can
|
|
532
|
+
* show a reader something other than what they authorize. */
|
|
533
|
+
readonly segment: string;
|
|
388
534
|
}
|
|
389
535
|
/**
|
|
390
|
-
*
|
|
536
|
+
* design/375 §3.1 — ONE "don't ask again" option on an approval card. A CLOSED discriminated union
|
|
537
|
+
* (`kind`); consumers parse it per element, and an element whose `kind` they do not know is dropped
|
|
538
|
+
* (single-row degrade) while keeping the ORIGINAL wire index for every element they keep — selection
|
|
539
|
+
* and redemption are keyed on original indices, so a consumer that cannot preserve the mapping must
|
|
540
|
+
* suppress its persistence actions entirely (fail toward asking).
|
|
541
|
+
*
|
|
542
|
+
* · `kind: "single"` — one rule covering the WHOLE command (the exact spelling, or a broader
|
|
543
|
+
* reviewed prefix form for a simple command).
|
|
544
|
+
* · `kind: "batch"` — a CONJUNCTION batch of per-segment rules for a compound command. Choosing it
|
|
545
|
+
* is one yes to ALL of `rules` at once — there is no per-member selection inside a batch; a
|
|
546
|
+
* consumer that wants a narrower answer picks the single offer or approves once without a rule.
|
|
547
|
+
* `rules` has 1..5 members (the card-lane minting cap), in segment order, deduplicated by rule
|
|
548
|
+
* text. `uncoveredSegments` is the honest surplus disclosure of the cap: judged at MINT time
|
|
549
|
+
* against the coverage snapshot the card was drawn from ∪ this batch's FINAL (deduplicated,
|
|
550
|
+
* capped) rules, it counts the violating segments still admitted by neither — 0 means "once this
|
|
551
|
+
* batch is redeemed, this compound is fully covered by that snapshot's lights". It is a statement
|
|
552
|
+
* about the minting snapshot, not a standing guarantee: a concurrent rule removal can date it.
|
|
553
|
+
*
|
|
554
|
+
* Array contract (`ruleOffers`, the durable park row, and the consent record's projection while
|
|
555
|
+
* the card is PENDING all share it): at most 2 offers; the whole-string exact single is index 0
|
|
556
|
+
* whenever present; the batch offer — at most one — is always last. It is a draw-time contract,
|
|
557
|
+
* not a settled-record invariant: confirming an EDITED candidate appends a third single offer
|
|
558
|
+
* AFTER the batch in the same write that settles the record, so a SETTLED record's projection may
|
|
559
|
+
* carry it. ADVISORY display metadata, never adjudication input, and never a
|
|
560
|
+
* rule by itself: minting is a separate act that goes through the approval-record protocol, so a
|
|
561
|
+
* surface that ignores offers entirely changes nothing about what is allowed. The authoritative
|
|
562
|
+
* render source for a card is the PREPARED record's own offers (record-is-what-you-see); the ask-time
|
|
563
|
+
* projection here may drift from it across a coverage change, and the drift direction is harmless
|
|
564
|
+
* (the record is what gets confirmed).
|
|
565
|
+
*/
|
|
566
|
+
export type RuleOffer = {
|
|
567
|
+
readonly kind: "single";
|
|
568
|
+
/** The canonical rule text the host would redeem. */
|
|
569
|
+
readonly rule: string;
|
|
570
|
+
readonly match: PersistedRuleMatch;
|
|
571
|
+
/** The command pattern in the rule, for a host that wants to render it without re-parsing. */
|
|
572
|
+
readonly command: string;
|
|
573
|
+
} | {
|
|
574
|
+
readonly kind: "batch";
|
|
575
|
+
/** 1..5 per-segment rules, segment order, deduplicated by rule text. */
|
|
576
|
+
readonly rules: readonly SegmentRuleSuggestion[];
|
|
577
|
+
/** Mint-time honest surplus: violating segments admitted by neither the coverage snapshot nor
|
|
578
|
+
* this batch's final rules (see the union doc above for the exact arithmetic). */
|
|
579
|
+
readonly uncoveredSegments: number;
|
|
580
|
+
};
|
|
581
|
+
/**
|
|
582
|
+
* The offers presented on an approval card for `command` (design/375 §5.2).
|
|
391
583
|
*
|
|
392
|
-
* **Array order is a documented CONTRACT, not an implementation accident
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* Selection indices
|
|
396
|
-
*
|
|
584
|
+
* **Array order is a documented CONTRACT, not an implementation accident** (see {@link RuleOffer}):
|
|
585
|
+
* the whole-string EXACT single is index 0 whenever present; a SIMPLE command may carry a broader
|
|
586
|
+
* reviewed PREFIX single after it; a COMPOUND carries at most one BATCH offer, always last.
|
|
587
|
+
* Selection indices are keyed against this order, so consumers may rely on it; redemption TICKETS
|
|
588
|
+
* are NOT — they key on the consent record's flat CANDIDATE list, and a chosen offer is redeemed
|
|
589
|
+
* via `redeemRuleBatch` (see the consent record's own doc for the two index spaces).
|
|
397
590
|
*
|
|
398
|
-
* The prefix
|
|
591
|
+
* The prefix single is the LONGEST body two sources agree to offer: {@link SUGGESTION_LEXICON}'s
|
|
399
592
|
* deeper-than-two-words rows first, and otherwise {@link genericPrefixBody}'s general two-word form
|
|
400
593
|
* (`argv[0]` + a token shaped like a subcommand). Read those two for the grammar, its adopted origin,
|
|
401
594
|
* and the operand false positive it accepts; the short version is that a bare verb (`git`), an
|
|
@@ -407,9 +600,10 @@ export interface RuleSuggestion {
|
|
|
407
600
|
* "no prefix".
|
|
408
601
|
*
|
|
409
602
|
* Every produced candidate must survive the round trip — parse as a rule, come back as the match form
|
|
410
|
-
*
|
|
411
|
-
*
|
|
412
|
-
* redemption would refuse is worse
|
|
603
|
+
* its seat is offering, AND admit the very text it was minted from (the whole command for a single,
|
|
604
|
+
* its own segment for a batch member). Enforced on EVERY seat, fail-closed: a candidate that would
|
|
605
|
+
* not round-trip is silently not offered, since offering an option redemption would refuse is worse
|
|
606
|
+
* than offering one fewer.
|
|
413
607
|
*
|
|
414
608
|
* The FORM half of that check is not decoration. A command may end in the rule grammar's own prefix
|
|
415
609
|
* marker — `rm :*` is a legal thing to type — and wrapping it as an exact rule produces the text
|
|
@@ -419,19 +613,29 @@ export interface RuleSuggestion {
|
|
|
419
613
|
* for a lexicon command like `git status :*`, emitted twice — once mislabelled, once as the real prefix
|
|
420
614
|
* candidate). The seat therefore believes the PARSER about what it got back, never its own request.
|
|
421
615
|
*
|
|
422
|
-
* A COMPOUND (`./gradlew build && ./gradlew test`) fills the exact
|
|
423
|
-
* whole chain and admits exactly it
|
|
424
|
-
*
|
|
425
|
-
*
|
|
426
|
-
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
616
|
+
* A COMPOUND (`./gradlew build && ./gradlew test`) fills the exact single — the offered rule spells
|
|
617
|
+
* the whole chain and admits exactly it — and a BATCH offer besides: every segment NOT already
|
|
618
|
+
* covered (per `ctx.coverage`) yields its own single-segment best form — a prefix when the
|
|
619
|
+
* single-command grammar grants one for that segment's tokens (the interception screens apply
|
|
620
|
+
* unchanged), otherwise the segment's exact — deduplicated by rule text in segment order and capped
|
|
621
|
+
* at FIVE (upstream's own batch cap; the overflow is disclosed via `uncoveredSegments` rather than
|
|
622
|
+
* silently truncated away). A segment whose every form the floor refuses simply contributes no
|
|
623
|
+
* member (and stays counted as uncovered — the honest gap). The COMPOUND-PREFIX candidate this seat
|
|
624
|
+
* used to mint here is deliberately NO LONGER OFFERED (design/375 §5.3): the batch takes its card
|
|
625
|
+
* position, while the compound-prefix GRAMMAR remains a fully supported rule family — the matcher,
|
|
626
|
+
* the hand-written/edited/import channels and every persisted such rule are untouched.
|
|
627
|
+
*
|
|
628
|
+
* `ctx.coverage` is the per-segment coverage table minted by the SAME store read that adjudicated
|
|
629
|
+
* the call (see {@link segmentCoverageOf} and the gate's engine-stamped seat) — positional over this
|
|
630
|
+
* function's own folded segments, belt-checked by text: a table whose length or segment texts do not
|
|
631
|
+
* line up is discarded whole, and an absent or discarded table reads as ALL-UNCOVERED (fail toward
|
|
632
|
+
* over-offering; every offered rule still passes the validator at redemption, so an over-offer widens
|
|
633
|
+
* nothing).
|
|
432
634
|
*
|
|
433
635
|
* Returns an empty array for anything the rule lane cannot speak for (redirections, substitutions,
|
|
434
636
|
* subshells, backgrounding) — the card then simply carries no "don't ask again" option, which is the
|
|
435
637
|
* honest answer.
|
|
436
638
|
*/
|
|
437
|
-
export declare function suggestRulesForCommand(command: string
|
|
639
|
+
export declare function suggestRulesForCommand(command: string, ctx?: {
|
|
640
|
+
readonly coverage?: readonly SegmentCoverage[];
|
|
641
|
+
}): RuleOffer[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parsePermissionRule } from "./permission-rules.js";
|
|
2
2
|
import { parseLeadingCommandName, splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
|
|
3
|
+
import { inlineUntrusted } from "./untrusted-text.js";
|
|
3
4
|
export const MAX_RULE_TEXT_CHARS = 512;
|
|
4
5
|
export const BARE_INTERPRETER_NAMES = new Set([
|
|
5
6
|
"node", "deno", "bun", "python", "python2", "python3", "perl", "ruby", "php", "osascript",
|
|
@@ -162,6 +163,17 @@ export function escapeForDisclosure(value) {
|
|
|
162
163
|
});
|
|
163
164
|
return escaped.length <= DISCLOSED_RULE_TEXT_MAX_CHARS ? escaped : `${escaped.slice(0, DISCLOSED_RULE_TEXT_MAX_CHARS)}…`;
|
|
164
165
|
}
|
|
166
|
+
const DISPLAY_STRIP_FORMAT_RE = /\p{Cf}/gu;
|
|
167
|
+
export function renderUntrustedCommandText(text, maxLen = DISCLOSED_RULE_TEXT_MAX_CHARS) {
|
|
168
|
+
let raw;
|
|
169
|
+
try {
|
|
170
|
+
raw = typeof text === "string" ? text : String(text);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return "<unprintable>";
|
|
174
|
+
}
|
|
175
|
+
return inlineUntrusted(raw.replace(DISPLAY_STRIP_FORMAT_RE, ""), maxLen);
|
|
176
|
+
}
|
|
165
177
|
export function hasUnrenderableCharacters(text) {
|
|
166
178
|
return CONTROL_CHARS_RE.test(text);
|
|
167
179
|
}
|
|
@@ -247,60 +259,118 @@ export function scopeCoversCwd(scope, cwd) {
|
|
|
247
259
|
export function isRuleLive(rule) {
|
|
248
260
|
return rule.adds.length > 0;
|
|
249
261
|
}
|
|
250
|
-
export function
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
262
|
+
export function eligibleContext(rule, call) {
|
|
263
|
+
return rule.tool === call.tool && scopeCoversCwd(rule.scope, call.cwd);
|
|
264
|
+
}
|
|
265
|
+
export function eligiblePersisted(rule, call) {
|
|
266
|
+
return isRuleLive(rule) && eligibleContext(rule, call);
|
|
267
|
+
}
|
|
268
|
+
function firstEligibleAdmittingRule(rules, commandText, call) {
|
|
254
269
|
for (const rule of rules) {
|
|
255
|
-
if (!
|
|
256
|
-
continue;
|
|
257
|
-
if (rule.tool !== call.tool)
|
|
270
|
+
if (!eligiblePersisted(rule, call))
|
|
258
271
|
continue;
|
|
259
|
-
if (
|
|
260
|
-
continue;
|
|
261
|
-
if (ruleAdmitsCommand(rule, call.command))
|
|
272
|
+
if (ruleAdmitsCommand(rule, commandText))
|
|
262
273
|
return rule;
|
|
263
274
|
}
|
|
264
275
|
return undefined;
|
|
265
276
|
}
|
|
266
|
-
export function
|
|
277
|
+
export function findAdmittingRule(rules, call) {
|
|
278
|
+
const shape = ruleLaneShapeOf(call.command, MATCH_READING);
|
|
279
|
+
if ("reject" in shape)
|
|
280
|
+
return undefined;
|
|
281
|
+
const whole = firstEligibleAdmittingRule(rules, call.command, call);
|
|
282
|
+
if (whole !== undefined)
|
|
283
|
+
return [whole];
|
|
284
|
+
if (shape.segments.length < 2)
|
|
285
|
+
return undefined;
|
|
286
|
+
const covering = [];
|
|
287
|
+
for (const segment of shape.segments) {
|
|
288
|
+
const first = firstEligibleAdmittingRule(rules, segment, call);
|
|
289
|
+
if (first === undefined)
|
|
290
|
+
return undefined;
|
|
291
|
+
if (!covering.includes(first))
|
|
292
|
+
covering.push(first);
|
|
293
|
+
}
|
|
294
|
+
return covering;
|
|
295
|
+
}
|
|
296
|
+
export function segmentCoverageOf(command, rules, call) {
|
|
297
|
+
const folded = foldSpacing(command);
|
|
298
|
+
if (folded === undefined)
|
|
299
|
+
return undefined;
|
|
300
|
+
const shape = ruleLaneShapeOf(folded, MATCH_READING);
|
|
301
|
+
if ("reject" in shape)
|
|
302
|
+
return undefined;
|
|
303
|
+
const proposed = rules.proposed ?? [];
|
|
304
|
+
return shape.segments.map((segment) => ({
|
|
305
|
+
segment: segment.trim(),
|
|
306
|
+
covered: firstEligibleAdmittingRule(rules.persisted, segment, call) !== undefined ||
|
|
307
|
+
proposed.some((p) => eligibleContext({ tool: p.rule.tool, scope: p.scope }, call) && ruleAdmitsCommand(p.rule, segment)),
|
|
308
|
+
}));
|
|
309
|
+
}
|
|
310
|
+
export function suggestRulesForCommand(command, ctx) {
|
|
267
311
|
const shape = ruleLaneShapeOf(command, MATCH_READING);
|
|
268
312
|
if ("reject" in shape)
|
|
269
313
|
return [];
|
|
270
314
|
const folded = foldSpacing(command);
|
|
271
315
|
if (folded === undefined)
|
|
272
316
|
return [];
|
|
273
|
-
const
|
|
317
|
+
const offers = [];
|
|
274
318
|
const exact = parseAllowRuleText(formatAllowRuleText(folded, "exact"));
|
|
275
319
|
if ("rule" in exact && exact.rule.match === "exact" && ruleAdmitsCommand(exact.rule, command)) {
|
|
276
|
-
|
|
320
|
+
offers.push({ kind: "single", rule: exact.rule.rule, match: "exact", command: exact.rule.command });
|
|
277
321
|
}
|
|
278
|
-
const offerPrefix = (body) => {
|
|
279
|
-
if (body === undefined)
|
|
280
|
-
return;
|
|
281
|
-
const text = formatAllowRuleText(body, "prefix");
|
|
282
|
-
const parsed = parseAllowRuleText(text);
|
|
283
|
-
if ("rule" in parsed && parsed.rule.match === "prefix" && ruleAdmitsCommand(parsed.rule, command)) {
|
|
284
|
-
out.push({ rule: parsed.rule.rule, match: "prefix", command: parsed.rule.command });
|
|
285
|
-
}
|
|
286
|
-
};
|
|
287
322
|
const tokensOf = (s) => s.split(/\s+/).filter((t) => t !== "");
|
|
288
323
|
const bodyOf = (tokens) => longestReviewedBody(tokens) ?? genericPrefixBody(tokens);
|
|
289
|
-
if (
|
|
290
|
-
if (
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
const rawLast = foldedShape.segments[foldedShape.segments.length - 1] ?? "";
|
|
297
|
-
const lastBody = bodyOf(tokensOf(rawLast));
|
|
298
|
-
if (lastBody !== undefined) {
|
|
299
|
-
const cut = folded.length - rawLast.length + (rawLast.length - rawLast.trimStart().length);
|
|
300
|
-
offerPrefix(folded.slice(0, cut) + lastBody);
|
|
324
|
+
if (shape.segments.length === 1) {
|
|
325
|
+
if (offers.length === 1) {
|
|
326
|
+
const body = bodyOf(tokensOf(folded));
|
|
327
|
+
if (body !== undefined) {
|
|
328
|
+
const parsed = parseAllowRuleText(formatAllowRuleText(body, "prefix"));
|
|
329
|
+
if ("rule" in parsed && parsed.rule.match === "prefix" && ruleAdmitsCommand(parsed.rule, command)) {
|
|
330
|
+
offers.push({ kind: "single", rule: parsed.rule.rule, match: "prefix", command: parsed.rule.command });
|
|
301
331
|
}
|
|
302
332
|
}
|
|
303
333
|
}
|
|
334
|
+
return offers;
|
|
335
|
+
}
|
|
336
|
+
const foldedShape = ruleLaneShapeOf(folded, MATCH_READING);
|
|
337
|
+
if ("reject" in foldedShape || foldedShape.segments.length !== shape.segments.length)
|
|
338
|
+
return offers;
|
|
339
|
+
const segments = foldedShape.segments.map((s) => s.trim());
|
|
340
|
+
const table = ctx?.coverage;
|
|
341
|
+
const alignedTable = table !== undefined && table.length === segments.length && table.every((row, i) => row.segment === segments[i]) ? table : undefined;
|
|
342
|
+
const coveredAt = (i) => alignedTable?.[i]?.covered === true;
|
|
343
|
+
const mintSegmentRule = (segment) => {
|
|
344
|
+
const body = bodyOf(tokensOf(segment));
|
|
345
|
+
if (body !== undefined) {
|
|
346
|
+
const parsed = parseAllowRuleText(formatAllowRuleText(body, "prefix"));
|
|
347
|
+
if ("rule" in parsed && parsed.rule.match === "prefix" && ruleAdmitsCommand(parsed.rule, segment)) {
|
|
348
|
+
return { rule: parsed.rule.rule, match: "prefix", command: parsed.rule.command };
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
const segExact = parseAllowRuleText(formatAllowRuleText(segment, "exact"));
|
|
352
|
+
if ("rule" in segExact && segExact.rule.match === "exact" && ruleAdmitsCommand(segExact.rule, segment)) {
|
|
353
|
+
return { rule: segExact.rule.rule, match: "exact", command: segExact.rule.command };
|
|
354
|
+
}
|
|
355
|
+
return undefined;
|
|
356
|
+
};
|
|
357
|
+
const minted = [];
|
|
358
|
+
for (let i = 0; i < segments.length; i++) {
|
|
359
|
+
if (coveredAt(i))
|
|
360
|
+
continue;
|
|
361
|
+
const segment = segments[i];
|
|
362
|
+
const rule = mintSegmentRule(segment);
|
|
363
|
+
if (rule !== undefined && !minted.some((m) => m.rule === rule.rule))
|
|
364
|
+
minted.push({ ...rule, segment });
|
|
365
|
+
}
|
|
366
|
+
const batchRules = minted.slice(0, 5);
|
|
367
|
+
if (batchRules.length > 0) {
|
|
368
|
+
const parsedBatch = batchRules.flatMap((r) => {
|
|
369
|
+
const p = parseAllowRuleText(r.rule);
|
|
370
|
+
return "rule" in p ? [p.rule] : [];
|
|
371
|
+
});
|
|
372
|
+
const uncoveredSegments = segments.filter((segment, i) => !coveredAt(i) && !parsedBatch.some((p) => ruleAdmitsCommand(p, segment))).length;
|
|
373
|
+
offers.push({ kind: "batch", rules: batchRules, uncoveredSegments });
|
|
304
374
|
}
|
|
305
|
-
return
|
|
375
|
+
return offers;
|
|
306
376
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* run-loop 介入的「声明式数据→ToolPolicy」工厂(`createCoarseCommandNamePolicy` 同形态先例)。
|
|
8
8
|
* core 不读设置文件、不管 source 层级合并 —— Settings-loader 把各层合并成**单一规则表**后编译
|
|
9
9
|
* **一个** policy(不要按层各编译再组合:combinePolicies 的 fold 里 ask 胜 allow,跨 policy 的
|
|
10
|
-
* 显式 allow 规则会被另一 policy 的 ask 吞掉
|
|
10
|
+
* 显式 allow 规则会被另一 policy 的 ask 吞掉)。
|
|
11
11
|
*
|
|
12
12
|
* 🔴 与 CC 的三处有意差(design/127 r2):
|
|
13
13
|
* - CC 参数级 generic 道无 allow lane(gnn 只有 deny/ask)—— 我们同样:`allow` + 参数级 content =
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* **工厂 validator strict**:降级即 invalid(fail-fast 世界观,写错的规则不能静默变宽)。
|
|
17
17
|
* - 已知主字段参数规则(如 `Bash(command:…)`)默认 **reject**:通配是 trim 后**整串 lexical**
|
|
18
18
|
* 匹配,不是 shell AST / 前缀 lane —— `Bash(command:git push*)` 匹配不了 `cd /x && git push`,
|
|
19
|
-
*
|
|
19
|
+
* 「以为挡住了」比没挡更危险。`primaryFieldGeneric:"allow-lexical"` 显式解锁。
|
|
20
20
|
*/
|
|
21
21
|
import type { NamedToolPolicy } from "./tool-policy.js";
|
|
22
22
|
/** [K-PLATFORM-SWEEP] MINOR#11: Bash's REAL non-primary arg names, accepted as generic param rules
|
|
@@ -28,7 +28,7 @@ export interface PermissionRule {
|
|
|
28
28
|
/** CC 语法:`"Tool"` 或 `"Tool(param:pattern)"`。`Tool()`/`Tool(*)` 归一为裸规则;括号可 `\(`/`\)` 转义。 */
|
|
29
29
|
rule: string;
|
|
30
30
|
behavior: "deny" | "ask" | "allow";
|
|
31
|
-
/** Provenance
|
|
31
|
+
/** Provenance:进 deny/ask 的 message 供审计(e.g. `"policySettings"`)。层级
|
|
32
32
|
* 优先级/tighten-only 语义在 Settings-loader,不在这里。 */
|
|
33
33
|
source?: string;
|
|
34
34
|
}
|
|
@@ -40,7 +40,7 @@ export interface ParsedPermissionRule {
|
|
|
40
40
|
}
|
|
41
41
|
export interface PermissionRuleIssue {
|
|
42
42
|
rule: string;
|
|
43
|
-
/** `invalid.*` = 语法/语义错;`unsupported.*` = CC 合法但 v1 不支持的 lane(
|
|
43
|
+
/** `invalid.*` = 语法/语义错;`unsupported.*` = CC 合法但 v1 不支持的 lane(独立
|
|
44
44
|
* 分类,绝不静默 no-op —— `Bash(npm run:*)` 被当 generic 参数规则接受后永不匹配是最危险的失败形态)。 */
|
|
45
45
|
code: "invalid.empty_tool" | "invalid.paren" | "invalid.param_split" | "invalid.allow_param_rule" | "invalid.primary_field" | "invalid.cap_exceeded" | "unsupported.bash_prefix"
|
|
46
46
|
/** design/179 D7: a Bash prefix rule under `bashPrefixLane:"rules-store"` — reported so a migration
|
|
@@ -65,20 +65,20 @@ export interface PermissionRuleCaps {
|
|
|
65
65
|
maxStars: number;
|
|
66
66
|
/** Scalar arg values longer than this are NOT matched textually — the rule **fail-closes** to its
|
|
67
67
|
* lane (a deny rule targeting an oversized value denies; an ask rule asks). 租户规则 × 模型巨串
|
|
68
|
-
*
|
|
68
|
+
* 的热路径成本上界。 */
|
|
69
69
|
maxScalarValueChars: number;
|
|
70
70
|
}
|
|
71
71
|
export interface PermissionRulePolicyOptions {
|
|
72
72
|
/** Invalid/unsupported rules: `"throw"`(default — 部署作者写错要立刻知道)aggregates ALL issues
|
|
73
73
|
* into one error; `"skip"` drops them (pair with {@link validatePermissionRules} for a report, and/or
|
|
74
|
-
* supply {@link onIssue}). Exception(
|
|
74
|
+
* supply {@link onIssue}). Exception(实现审 M3):`invalid.cap_exceeded` on the WHOLE rule set
|
|
75
75
|
* (maxRules) throws EVEN under `"skip"` — skipping it would compile an EMPTY table and fail-open. */
|
|
76
76
|
onInvalidRule?: "throw" | "skip";
|
|
77
77
|
/** Called once per dropped rule under `"skip"` (audit surface — silent drops are how a deployment
|
|
78
78
|
* discovers, months later, that its deny never existed). Default: one aggregated `console.warn`. */
|
|
79
79
|
onIssue?: (issue: PermissionRuleIssue) => void;
|
|
80
80
|
/** What the policy returns when NO rule matches. Default `"allow"` = 本 policy 无意见(组合语义)。
|
|
81
|
-
* default-closed 部署用 `"ask"`/`"deny"` —— 必须在这 INSIDE the policy(
|
|
81
|
+
* default-closed 部署用 `"ask"`/`"deny"` —— 必须在这 INSIDE the policy(链尾
|
|
82
82
|
* ask policy 会在 fold 里吞掉本 policy 的显式 allow)。 */
|
|
83
83
|
defaultAction?: "allow" | "ask" | "deny";
|
|
84
84
|
/** 已知主字段参数规则(`Bash(command:…)` 等)的处置。默认 `"reject"`(整串 lexical ≠ 前缀/AST,
|
|
@@ -136,7 +136,7 @@ export declare function namespacedRuleNameCovers(ruleName: string, toolName: str
|
|
|
136
136
|
export declare function isNamespacedCoveringRuleName(name: string): boolean;
|
|
137
137
|
/** Exported for the lockstep guard only (see {@link FILE_TOOL_PARAMS}) — not part of the rule DSL. */
|
|
138
138
|
export declare const fileToolParamVocabulary: (canonicalTool: string) => ReadonlySet<string> | undefined;
|
|
139
|
-
/** Dry-run
|
|
139
|
+
/** Dry-run 校验/迁移报告:不 throw,返回全部 issues(含 `unsupported.*` 分类,
|
|
140
140
|
* 供从 CC settings 迁移的部署逐条对照)。 */
|
|
141
141
|
export declare function validatePermissionRules(rules: PermissionRule[], opts?: Pick<PermissionRulePolicyOptions, "primaryFieldGeneric" | "caps" | "bashPrefixLane">): PermissionRuleIssue[];
|
|
142
142
|
/**
|
|
@@ -145,7 +145,7 @@ export declare function validatePermissionRules(rules: PermissionRule[], opts?:
|
|
|
145
145
|
* bare allow → `defaultAction`. `check()` is idempotent and side-effect-free (combinePolicies 契约);
|
|
146
146
|
* all parsing/validation happens HERE, the hot path never re-parses.
|
|
147
147
|
*
|
|
148
|
-
* ⚠️ Composition note
|
|
148
|
+
* ⚠️ Composition note: an `allow` from this policy is "no objection", NOT a
|
|
149
149
|
* cross-policy override — another policy's ask/deny still outranks it in the combinePolicies fold.
|
|
150
150
|
* Express default-closed via `defaultAction`, never via a trailing always-ask policy.
|
|
151
151
|
*/
|