@sema-agent/core 7.0.2 → 7.2.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/cross-session-envelope.d.ts +138 -0
- package/dist/agents/cross-session-envelope.js +191 -0
- package/dist/agents/cross-session-judge.d.ts +119 -0
- package/dist/agents/cross-session-judge.js +184 -0
- package/dist/agents/cross-session-ref.d.ts +52 -0
- package/dist/agents/cross-session-ref.js +64 -0
- package/dist/agents/repair-loop.d.ts +8 -7
- package/dist/agents/roster-store.d.ts +7 -2
- package/dist/agents/send-message-tool.d.ts +13 -0
- package/dist/agents/send-message-tool.js +36 -12
- package/dist/brain/errors.d.ts +18 -0
- package/dist/brain/errors.js +3 -0
- package/dist/brain/stream-engine.js +6 -4
- package/dist/core/checkpoint-store.d.ts +189 -3
- package/dist/core/checkpoint-store.js +56 -16
- package/dist/core/context-edit.d.ts +3 -0
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +4 -0
- package/dist/core/hooks.d.ts +34 -7
- package/dist/core/hooks.js +14 -8
- package/dist/core/image-downsample.d.ts +4 -3
- package/dist/core/permission-rule-consent.d.ts +72 -23
- package/dist/core/permission-rule-consent.js +115 -26
- package/dist/core/permission-rule-model.d.ts +245 -51
- package/dist/core/permission-rule-model.js +312 -54
- package/dist/core/permission-rule-org.js +13 -6
- package/dist/core/remote-env.d.ts +8 -1
- package/dist/core/roles.d.ts +30 -8
- package/dist/core/roles.js +12 -8
- package/dist/core/runner/assemble-result.js +2 -1
- package/dist/core/runner/prepare-task.d.ts +41 -2
- package/dist/core/runner/prepare-task.js +353 -152
- package/dist/core/runner/prepare-workspace-restore.d.ts +6 -1
- package/dist/core/runner/prepare-workspace-restore.js +2 -1
- package/dist/core/runner/runtask.d.ts +12 -3
- package/dist/core/runner/runtask.js +45 -7
- package/dist/core/safety-axis-vocab.d.ts +1 -1
- package/dist/core/strategy-store.d.ts +4 -1
- package/dist/core/task-notification.d.ts +64 -5
- package/dist/core/task-notification.js +25 -4
- package/dist/core/task-registry-shared.d.ts +7 -3
- package/dist/core/tool-errors.d.ts +1 -1
- package/dist/core/tool-policy.d.ts +51 -7
- package/dist/core/tool-policy.js +63 -9
- package/dist/core/types.d.ts +110 -9
- package/dist/core/untrusted-text.js +17 -1
- package/dist/engine/compaction/compaction.js +6 -2
- package/dist/engine/harness/agent-harness.d.ts +28 -6
- package/dist/engine/harness/agent-harness.js +34 -2
- package/dist/engine/harness/messages.js +4 -0
- package/dist/engine/harness/types.d.ts +37 -0
- package/dist/engine/harness/types.js +5 -0
- package/dist/engine/session/session.js +3 -2
- package/dist/index.d.ts +6 -3
- package/dist/index.js +5 -2
- package/dist/internal/harness.d.ts +1 -0
- package/dist/internal/harness.js +1 -0
- package/dist/orchestration/builtin-workflows.d.ts +17 -9
- package/dist/orchestration/run-workflow-tool.js +7 -2
- package/dist/orchestration/workflow-governance.js +1 -1
- package/dist/orchestration/workflow-types.d.ts +1 -0
- package/dist/orchestration/workflow.js +1 -1
- package/dist/stores/file/mailbox-store.d.ts +2 -1
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +125 -1
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* this by editing a file backend's file. That is the settings-file trust model, stated rather than
|
|
28
28
|
* defended against: for a file backend, host = user, no more and no less.
|
|
29
29
|
*/
|
|
30
|
-
import { type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
|
|
30
|
+
import { type EditedRuleBreadthWarning, type UncoveredSegmentDetail, 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
32
|
import type { SessionRuleOverlay } from "./permission-rule-session.js";
|
|
33
33
|
/** One candidate rule inside an approval record: the exact text and where it would apply. */
|
|
@@ -48,10 +48,16 @@ export type RuleApprovalKind = "card" | "import" | "starter";
|
|
|
48
48
|
* · `kind: "batch"` — a CONJUNCTION batch: confirming it is one yes to ALL of `candidates` at
|
|
49
49
|
* once, with no per-member sub-selection. On a CARD record the batch carries two MINT-TIME
|
|
50
50
|
* facts that cannot be re-derived from candidate texts and therefore must be stored:
|
|
51
|
-
* `segments` (each member's source segment, index-aligned with `candidates`)
|
|
51
|
+
* `segments` (each member's source segment, index-aligned with `candidates`),
|
|
52
52
|
* `uncoveredSegments` (the honest surplus count the wire offer disclosed, judged on the
|
|
53
|
-
* prepare-time coverage snapshot)
|
|
54
|
-
*
|
|
53
|
+
* prepare-time coverage snapshot) and — since `schema: 3` (design/382 §3.5) — `uncoveredDetail`,
|
|
54
|
+
* one reason row per counted segment in segment order: WHY each stayed uncovered is a mint-time
|
|
55
|
+
* fact that cannot be re-derived from candidate texts, so it must be stored or a crash/reload
|
|
56
|
+
* rebuild would render a card that says less than the first render did (record-is-what-you-see).
|
|
57
|
+
* REQUIRED on a card batch, all three: "optional, validated when present" is the
|
|
58
|
+
* silent-absence-passes shape this record form already refused once, and it is not re-imported
|
|
59
|
+
* for the new seat. On an import/starter record there is no compound command
|
|
60
|
+
* and no coverage snapshot, so all three fields must be ABSENT — honest absence, never an
|
|
55
61
|
* empty-string stand-in. The read-side integrity checks refuse either direction's violation.
|
|
56
62
|
*
|
|
57
63
|
* Cardinality: a batch references 1..N candidates where N is the record's own candidate count.
|
|
@@ -67,6 +73,7 @@ export type RuleOffer2 = {
|
|
|
67
73
|
readonly candidates: readonly number[];
|
|
68
74
|
readonly segments?: readonly string[];
|
|
69
75
|
readonly uncoveredSegments?: number;
|
|
76
|
+
readonly uncoveredDetail?: readonly UncoveredSegmentDetail[];
|
|
70
77
|
};
|
|
71
78
|
/** A durable record of one pending/settled human decision about minting rules.
|
|
72
79
|
*
|
|
@@ -81,12 +88,16 @@ export interface RuleApprovalRecord {
|
|
|
81
88
|
/** Structural owner (additive). Absent on every record a v1 tree minted. */
|
|
82
89
|
owner?: RuleOwner;
|
|
83
90
|
/**
|
|
84
|
-
* design/375 §4.5 — the record-form version stamp. A stored row WITHOUT it
|
|
85
|
-
* such a row confirms and redeems nothing (refused loudly,
|
|
86
|
-
* read through a compatibility shim: pending approvals are
|
|
87
|
-
* command draws a fresh card. Stores hand pre-form rows back
|
|
91
|
+
* design/375 §4.5 — the record-form version stamp. A stored row WITHOUT it — or carrying an older
|
|
92
|
+
* number — predates this form; such a row confirms and redeems nothing (refused loudly,
|
|
93
|
+
* `record_schema_stale`) and is never read through a compatibility shim: pending approvals are
|
|
94
|
+
* short-lived, and re-triggering the command draws a fresh card. Stores hand pre-form rows back
|
|
95
|
+
* as {@link StaleRuleApprovalRecord}. `3` (design/382 §3.5, B8/B7): the card batch's
|
|
96
|
+
* `uncoveredDetail` seat became REQUIRED and `edited.warnings` became storable — a `schema: 2`
|
|
97
|
+
* row is read back stale exactly like the pre-stamp rows before it (the short-lived-pending
|
|
98
|
+
* bargain, unchanged).
|
|
88
99
|
*/
|
|
89
|
-
schema:
|
|
100
|
+
schema: 3;
|
|
90
101
|
kind: RuleApprovalKind;
|
|
91
102
|
state: "pending" | "approved" | "redeemed";
|
|
92
103
|
candidates: RuleCandidate[];
|
|
@@ -137,18 +148,26 @@ export interface RuleApprovalRecord {
|
|
|
137
148
|
* same bytes); `at` is when the edit landed. Present ⇒ the record's chosen offer is the single
|
|
138
149
|
* offer minted over this candidate (the append, the offer and the choice are one CAS write).
|
|
139
150
|
* Absent on every card settled through an engine candidate.
|
|
151
|
+
*
|
|
152
|
+
* `warnings` (design/382 §3.3-5) — the BREADTH warning codes the text gates raised on this edit,
|
|
153
|
+
* written in the same CAS that settles it: "the width was named before the yes" becomes a fact the
|
|
154
|
+
* record itself carries, auditable after any crash or upgrade. Optional; present only when a
|
|
155
|
+
* warning was raised, and when present it is an array from the closed code set (the read-side
|
|
156
|
+
* integrity gate refuses anything else). Codes only — the sentences are derivable from the
|
|
157
|
+
* canonical rule text and are deliberately not stored (no second copy to drift).
|
|
140
158
|
*/
|
|
141
159
|
edited?: {
|
|
142
160
|
index: number;
|
|
143
161
|
text: string;
|
|
144
162
|
at: string;
|
|
163
|
+
warnings?: readonly EditedRuleBreadthWarning["code"][];
|
|
145
164
|
};
|
|
146
165
|
/** Dots already minted for this record, keyed by candidate index — the replay anchor. */
|
|
147
166
|
redeemedDots?: Record<number, RuleDot>;
|
|
148
167
|
}
|
|
149
168
|
/**
|
|
150
169
|
* design/375 §4.5 — the minimal ENVELOPE a store hands back for a durable row that predates the
|
|
151
|
-
* `schema:
|
|
170
|
+
* CURRENT (`schema: 3`) record form. A typed read cannot honestly return such a row as a
|
|
152
171
|
* {@link RuleApprovalRecord}, and dropping it at the store layer would misreport "stale" as
|
|
153
172
|
* corruption — so the store contract is this variant instead: `staleSchema: true` plus the
|
|
154
173
|
* IDENTITY fields, which are spelled the same way in every version and therefore safe to read.
|
|
@@ -209,6 +228,14 @@ export interface RuleConsentDeps {
|
|
|
209
228
|
* coverage reads see no session rows — both fail toward asking.
|
|
210
229
|
*/
|
|
211
230
|
sessionRules?: SessionRuleOverlay;
|
|
231
|
+
/**
|
|
232
|
+
* design/382 §2.4-6 — the deployment's sensitive-read negative control for the directoryRead MINT
|
|
233
|
+
* floor, same-source with its compiled read-deny judge (`(d) => readDeny.matchPath(d) !== null` is
|
|
234
|
+
* the canonical wiring). Consulted only when a card would PROPOSE a directory authorization from a
|
|
235
|
+
* cd segment — never by the explicit-consent doors (hand-written, import), which carry their own
|
|
236
|
+
* preview and validation. Absent ⇒ the engine's built-in default deny table stands in (refusal-only).
|
|
237
|
+
*/
|
|
238
|
+
deniesDirectoryRead?: (directory: string) => boolean;
|
|
212
239
|
}
|
|
213
240
|
/** In-memory approval records — the test backend and the reference CAS semantics, the stale-row
|
|
214
241
|
* envelope read included. */
|
|
@@ -297,6 +324,17 @@ export declare function prepareCardApproval(opts: {
|
|
|
297
324
|
* leans wide — the over-offer direction, which redemption's per-rule validation keeps safe.
|
|
298
325
|
*/
|
|
299
326
|
cwd?: string;
|
|
327
|
+
/**
|
|
328
|
+
* design/382 §2.4 (adversarial-review r2) — the EXECUTION working directory of the adjudicated
|
|
329
|
+
* call: the live tracked cwd the shell will resolve a relative `cd ./x` against, threaded by the
|
|
330
|
+
* caller exactly like `cwd` (the runner's ask-projection seat threads its `handsCwdRef.current`;
|
|
331
|
+
* a host preparing cards for its own calls threads whatever base those calls execute under).
|
|
332
|
+
* ONLY the relative-cd resolution reads it; scope eligibility and the derived project scope stay
|
|
333
|
+
* on `cwd`. Absent ⇒ `cwd` is the base (the single-value deployment, where the two coincide).
|
|
334
|
+
* Present and DIFFERENT, it is what keeps this durable consent path and the advisory ask-time
|
|
335
|
+
* projection minting the SAME directory member — two faces reading one world.
|
|
336
|
+
*/
|
|
337
|
+
execCwd?: string;
|
|
300
338
|
deps: RuleConsentDeps;
|
|
301
339
|
}): Promise<{
|
|
302
340
|
approvalId: string;
|
|
@@ -346,7 +384,7 @@ export declare function confirmRuleApproval(opts: {
|
|
|
346
384
|
/** Why a confirmation did not land. A closed set so a host can branch (re-present, re-fetch, give up).
|
|
347
385
|
* The two `record_*` members past `record_not_found` are the durable-row gates (design/375 §4.5/§4.7),
|
|
348
386
|
* disclosed to the record's own owner only — a non-owner gets `record_not_found` for every row:
|
|
349
|
-
* - `"record_schema_stale"` — the record predates the `schema:
|
|
387
|
+
* - `"record_schema_stale"` — the record predates the current (`schema: 3`) form; the decision it carried
|
|
350
388
|
* is void, and the recovery path is re-triggering the command for a fresh card (no
|
|
351
389
|
* compatibility read, no migration — `detail.message` says so);
|
|
352
390
|
* - `"record_malformed"` — the record's offer structure failed the read-side integrity checks
|
|
@@ -394,6 +432,16 @@ export type ConfirmResult = {
|
|
|
394
432
|
export type EditedRuleTextPrecheck = {
|
|
395
433
|
ok: true;
|
|
396
434
|
canonicalRule: string;
|
|
435
|
+
/**
|
|
436
|
+
* design/382 §3.3-5 — BREADTH notes about the accepted rule (additive; absent = nothing to
|
|
437
|
+
* say). A warning is never a refusal: the muscle-memory wide forms are legal (§3.4), and this
|
|
438
|
+
* seat is what makes accepting one an informed act. Rendering them beside the input line is
|
|
439
|
+
* the surface's duty (the engine cannot force a display); what the ENGINE guarantees is the
|
|
440
|
+
* durable half — a confirmation through the edit arm writes the hit codes into the record
|
|
441
|
+
* (`edited.warnings`), so "the width was named before the yes" is an auditable fact. The
|
|
442
|
+
* confirmation result deliberately does NOT repeat them (one fact, one loud place).
|
|
443
|
+
*/
|
|
444
|
+
warnings?: readonly EditedRuleBreadthWarning[];
|
|
397
445
|
} | {
|
|
398
446
|
ok: false;
|
|
399
447
|
code?: RuleRejectCode;
|
|
@@ -483,19 +531,20 @@ export interface ImportPreview {
|
|
|
483
531
|
reason: string;
|
|
484
532
|
}>;
|
|
485
533
|
/**
|
|
486
|
-
*
|
|
487
|
-
*
|
|
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).
|
|
534
|
+
* ALWAYS EMPTY in this version — a compatibility seat, kept on the wire shape so an existing reader
|
|
535
|
+
* keeps finding the key (removing a wire field would break readers for no gain).
|
|
494
536
|
*
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
537
|
+
* The seat once carried the entries whose MATCH FORM was rewritten on the way in (the space-star
|
|
538
|
+
* suggestion form of the other product translated to this lane's colon-star form, one row per
|
|
539
|
+
* translated candidate with its `scope`). That translation is retired: the space-star wildcard is
|
|
540
|
+
* now a first-class match form of this lane's own grammar, so an entry's match form imports as the
|
|
541
|
+
* file states it — `Bash(npm run *)` lands as `Bash(npm run *)`, never as a colon-star — and nothing
|
|
542
|
+
* is ever pushed here. What `candidates` carries is the grammar's CANONICAL re-projection of each
|
|
543
|
+
* entry (the parser folds spacing: `Bash(npm<TAB>run *)` lands as `Bash(npm run *)`); that is the
|
|
544
|
+
* ordinary normalization every rule text in this lane undergoes, the same one the store applies,
|
|
545
|
+
* and not a translation this seat reports. An entry whose star placement has no rule form is
|
|
546
|
+
* refused into `skipped` under `unsupported.wildcard`, named as the file writes it. A reader
|
|
547
|
+
* rendering a "rewritten spellings" column from this seat renders an empty column, correctly.
|
|
499
548
|
*/
|
|
500
549
|
translated: Array<{
|
|
501
550
|
from: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand,
|
|
2
|
+
import { escapeForDisclosure, hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
|
|
3
3
|
import { errText, isValidConsentScope, normalizePersistedRule, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
4
4
|
export class InMemoryRuleApprovalRecordStore {
|
|
5
5
|
rows = new Map();
|
|
@@ -7,7 +7,7 @@ export class InMemoryRuleApprovalRecordStore {
|
|
|
7
7
|
const r = this.rows.get(id);
|
|
8
8
|
if (r === undefined)
|
|
9
9
|
return undefined;
|
|
10
|
-
if (r.schema !==
|
|
10
|
+
if (r.schema !== 3) {
|
|
11
11
|
return {
|
|
12
12
|
staleSchema: true,
|
|
13
13
|
id: r.id,
|
|
@@ -139,8 +139,42 @@ function approvalRecordDamageOf(rec) {
|
|
|
139
139
|
if (!Number.isInteger(offer.uncoveredSegments) || offer.uncoveredSegments < 0) {
|
|
140
140
|
return `card batch offer ${at} must carry a non-negative uncovered-segment count`;
|
|
141
141
|
}
|
|
142
|
+
const detail = offer.uncoveredDetail;
|
|
143
|
+
if (!Array.isArray(detail))
|
|
144
|
+
return `card batch offer ${at} must carry its uncovered-segment reason rows`;
|
|
145
|
+
if (detail.length !== offer.uncoveredSegments) {
|
|
146
|
+
return `card batch offer ${at} carries ${detail.length} reason rows for an uncovered-segment count of ${offer.uncoveredSegments} — one fact, two seats, and they must agree`;
|
|
147
|
+
}
|
|
148
|
+
for (const [ri, row] of detail.entries()) {
|
|
149
|
+
if (typeof row !== "object" ||
|
|
150
|
+
row === null ||
|
|
151
|
+
typeof row.segment !== "string" ||
|
|
152
|
+
(row.reason !== "redirection" && row.reason !== "no_rule_form" && row.reason !== "cap_overflow")) {
|
|
153
|
+
return `card batch offer ${at} reason row ${ri} is not a segment/reason row from the closed reason set`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
if (detail.length > 0) {
|
|
157
|
+
const rows = detail;
|
|
158
|
+
const table = typeof rec.command === "string" ? segmentCoverageOf(rec.command, { persisted: [] }, { tool: CARD_RULE_TOOL, cwd: undefined }) : undefined;
|
|
159
|
+
if (table === undefined) {
|
|
160
|
+
return `card batch offer ${at} carries reason rows but the record's command cannot back them — a row without a readable source command is a row nobody can audit`;
|
|
161
|
+
}
|
|
162
|
+
let cursor = 0;
|
|
163
|
+
for (const [ri, row] of rows.entries()) {
|
|
164
|
+
let found = -1;
|
|
165
|
+
for (let j = cursor; j < table.length; j++) {
|
|
166
|
+
if (table[j]?.segment === row.segment) {
|
|
167
|
+
found = j;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (found === -1)
|
|
172
|
+
return `card batch offer ${at} reason row ${ri} names a segment the record's command does not carry at or after that position`;
|
|
173
|
+
cursor = found + 1;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
142
176
|
}
|
|
143
|
-
else if (offer.segments !== undefined || offer.uncoveredSegments !== undefined) {
|
|
177
|
+
else if (offer.segments !== undefined || offer.uncoveredSegments !== undefined || offer.uncoveredDetail !== undefined) {
|
|
144
178
|
return `${escapeForDisclosure(rec.kind)} batch offer ${at} must not carry segment metadata — there is no compound command behind it`;
|
|
145
179
|
}
|
|
146
180
|
}
|
|
@@ -149,6 +183,16 @@ function approvalRecordDamageOf(rec) {
|
|
|
149
183
|
if (!claimed.has(at))
|
|
150
184
|
return `candidate ${at} belongs to no offer — every candidate row is claimed by exactly one offer`;
|
|
151
185
|
}
|
|
186
|
+
if (rec.edited !== undefined) {
|
|
187
|
+
if (typeof rec.edited !== "object" || rec.edited === null)
|
|
188
|
+
return "the edited seat is not an object";
|
|
189
|
+
const w = rec.edited.warnings;
|
|
190
|
+
if (w !== undefined) {
|
|
191
|
+
if (!Array.isArray(w) || w.length !== 1 || (w[0] !== "broad_prefix" && w[0] !== "compound_prefix")) {
|
|
192
|
+
return "the edited seat's warnings are not a single-code array from the closed breadth-code set";
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
152
196
|
return undefined;
|
|
153
197
|
}
|
|
154
198
|
export function ruleOffersOfRecord(rec) {
|
|
@@ -163,21 +207,51 @@ export function ruleOffersOfRecord(rec) {
|
|
|
163
207
|
return { rule: parsed.rule.rule, match: parsed.rule.match, command: parsed.rule.command };
|
|
164
208
|
};
|
|
165
209
|
return rec.offers.map((offer) => {
|
|
166
|
-
if (offer.kind === "single")
|
|
167
|
-
|
|
210
|
+
if (offer.kind === "single") {
|
|
211
|
+
const t = tripleOf(offer.candidate);
|
|
212
|
+
if (t.match === "subpath") {
|
|
213
|
+
throw new Error("approval-record single offer references a directory rule — a shape this engine does not mint; refusing to render it");
|
|
214
|
+
}
|
|
215
|
+
return { kind: "single", ...t };
|
|
216
|
+
}
|
|
168
217
|
const segments = offer.segments;
|
|
169
|
-
if (segments === undefined || offer.uncoveredSegments === undefined) {
|
|
218
|
+
if (segments === undefined || offer.uncoveredSegments === undefined || offer.uncoveredDetail === undefined) {
|
|
170
219
|
throw new Error("a batch offer without mint-time segment metadata has no card face to project — import/starter batches render as previews, not offer cards");
|
|
171
220
|
}
|
|
221
|
+
const rawDetail = offer.uncoveredDetail;
|
|
222
|
+
const count = offer.uncoveredSegments;
|
|
223
|
+
const detail = [];
|
|
224
|
+
const detailDamaged = (() => {
|
|
225
|
+
if (!Array.isArray(rawDetail) || !Number.isInteger(count) || count < 0 || rawDetail.length !== count)
|
|
226
|
+
return true;
|
|
227
|
+
for (let i = 0; i < rawDetail.length; i++) {
|
|
228
|
+
const row = rawDetail[i];
|
|
229
|
+
if (typeof row !== "object" || row === null)
|
|
230
|
+
return true;
|
|
231
|
+
const segment = row.segment;
|
|
232
|
+
const reason = row.reason;
|
|
233
|
+
if (typeof segment !== "string" || (reason !== "redirection" && reason !== "no_rule_form" && reason !== "cap_overflow"))
|
|
234
|
+
return true;
|
|
235
|
+
detail.push({ segment, reason });
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
})();
|
|
239
|
+
if (detailDamaged) {
|
|
240
|
+
throw new Error("a batch offer's uncovered-segment reason rows do not match their count or the closed reason set — refusing to render damaged mint-time facts as a card face");
|
|
241
|
+
}
|
|
172
242
|
return {
|
|
173
243
|
kind: "batch",
|
|
174
244
|
rules: offer.candidates.map((candidate, i) => {
|
|
175
245
|
const segment = segments[i];
|
|
176
246
|
if (segment === undefined)
|
|
177
247
|
throw new Error(`batch offer member ${i} has no source segment recorded`);
|
|
178
|
-
|
|
248
|
+
const t = tripleOf(candidate);
|
|
249
|
+
return t.match === "subpath"
|
|
250
|
+
? { kind: "directoryRead", rule: t.rule, directory: t.command, segment }
|
|
251
|
+
: { kind: "command", rule: t.rule, match: t.match, command: t.command, segment };
|
|
179
252
|
}),
|
|
180
|
-
uncoveredSegments:
|
|
253
|
+
uncoveredSegments: count,
|
|
254
|
+
uncoveredDetail: detail,
|
|
181
255
|
};
|
|
182
256
|
});
|
|
183
257
|
}
|
|
@@ -234,12 +308,19 @@ export async function prepareCardApproval(opts) {
|
|
|
234
308
|
if (sessionRows.length > 0)
|
|
235
309
|
table = [...sessionRows, ...listed.rules];
|
|
236
310
|
}
|
|
237
|
-
coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool: CARD_RULE_TOOL, cwd: opts.cwd, sessionId: opts.sessionId });
|
|
311
|
+
coverage = segmentCoverageOf(opts.command, { persisted: table }, { tool: CARD_RULE_TOOL, cwd: opts.cwd, sessionId: opts.sessionId, ...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}) });
|
|
238
312
|
}
|
|
239
313
|
catch {
|
|
240
314
|
coverage = undefined;
|
|
241
315
|
}
|
|
242
|
-
const suggested = suggestRulesForCommand(opts.command,
|
|
316
|
+
const suggested = suggestRulesForCommand(opts.command, {
|
|
317
|
+
...(coverage !== undefined ? { coverage } : {}),
|
|
318
|
+
scope,
|
|
319
|
+
...(opts.cwd !== undefined ? { cwd: opts.cwd } : {}),
|
|
320
|
+
...(opts.execCwd !== undefined ? { execCwd: opts.execCwd } : {}),
|
|
321
|
+
...(opts.sessionId !== undefined ? { sessionId: opts.sessionId } : {}),
|
|
322
|
+
...(opts.deps.deniesDirectoryRead !== undefined ? { deniesDirectoryRead: opts.deps.deniesDirectoryRead } : {}),
|
|
323
|
+
});
|
|
243
324
|
const candidates = [];
|
|
244
325
|
const claimedBy = new Map();
|
|
245
326
|
const claim = (text) => {
|
|
@@ -257,13 +338,14 @@ export async function prepareCardApproval(opts) {
|
|
|
257
338
|
candidates: offer.rules.map((r) => claim(r.rule)),
|
|
258
339
|
segments: offer.rules.map((r) => r.segment),
|
|
259
340
|
uncoveredSegments: offer.uncoveredSegments,
|
|
341
|
+
uncoveredDetail: offer.uncoveredDetail ?? [],
|
|
260
342
|
});
|
|
261
343
|
if (candidates.length === 0)
|
|
262
344
|
return undefined;
|
|
263
345
|
const record = {
|
|
264
346
|
id: mintId(opts.deps, "rar"),
|
|
265
347
|
...recordIdentityOf(owner),
|
|
266
|
-
schema:
|
|
348
|
+
schema: 3,
|
|
267
349
|
kind: "card",
|
|
268
350
|
state: "pending",
|
|
269
351
|
rev: 0,
|
|
@@ -366,7 +448,8 @@ function checkEditedRuleText(text, command) {
|
|
|
366
448
|
message: `the edited rule "${parsed.rule.rule}" does not admit the command that was decided ("${escapeForDisclosure(command)}") — a card's edit may widen how much the rule covers, never move it to a different grant`,
|
|
367
449
|
};
|
|
368
450
|
}
|
|
369
|
-
|
|
451
|
+
const warnings = ruleBreadthWarningsOf(parsed.rule);
|
|
452
|
+
return { ok: true, canonicalRule: parsed.rule.rule, ...(warnings.length > 0 ? { warnings } : {}) };
|
|
370
453
|
}
|
|
371
454
|
export function precheckEditedRuleText(text, command) {
|
|
372
455
|
if (typeof text !== "string") {
|
|
@@ -443,7 +526,7 @@ async function confirmEditedCandidate(rec, edit, deps) {
|
|
|
443
526
|
candidates: [...rec.candidates, { rule: checked.canonicalRule, scope }],
|
|
444
527
|
offers: [...rec.offers, { kind: "single", candidate: index }],
|
|
445
528
|
selectedOffer: rec.offers.length,
|
|
446
|
-
edited: { index, text: edit.text, at: nowIso(deps) },
|
|
529
|
+
edited: { index, text: edit.text, at: nowIso(deps), ...(checked.warnings !== undefined ? { warnings: checked.warnings.map((w) => w.code) } : {}) },
|
|
447
530
|
};
|
|
448
531
|
const won = await deps.approvals.cas(rec.id, rec.rev, next);
|
|
449
532
|
if (!won) {
|
|
@@ -675,30 +758,36 @@ export async function prepareCcImport(opts) {
|
|
|
675
758
|
skipped.push({ rule: String(entry), reason: "settings entry is not a string" });
|
|
676
759
|
continue;
|
|
677
760
|
}
|
|
761
|
+
if (entry.startsWith("Read(")) {
|
|
762
|
+
const parsed = parseAllowRuleText(entry);
|
|
763
|
+
if ("reject" in parsed) {
|
|
764
|
+
skipped.push({
|
|
765
|
+
rule: entry,
|
|
766
|
+
reason: "unsupported.form: only the Read(//abs-dir/**) directory form imports in v1 — this entry stays in the settings file, unimported",
|
|
767
|
+
});
|
|
768
|
+
continue;
|
|
769
|
+
}
|
|
770
|
+
if (!candidates.some((c) => c.rule === parsed.rule.rule && sameScope(c.scope, scope))) {
|
|
771
|
+
candidates.push({ rule: parsed.rule.rule, scope });
|
|
772
|
+
}
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
678
775
|
if (!entry.startsWith("Bash(")) {
|
|
679
776
|
const reason = /^[A-Za-z][A-Za-z0-9_]*\(.*\)$/.test(entry)
|
|
680
|
-
? "unsupported.tool: only Bash(...) command rules import in v1 — this entry stays in the settings file, unimported"
|
|
777
|
+
? "unsupported.tool: only Bash(...) command rules and Read(//abs-dir/**) directory rules import in v1 — this entry stays in the settings file, unimported"
|
|
681
778
|
: /^[A-Za-z][A-Za-z0-9_-]*$/.test(entry)
|
|
682
779
|
? "unsupported.form: a bare tool-name entry is a name-set item, not a command rule — it stays in the settings file, unimported"
|
|
683
|
-
: "unsupported.form: not a
|
|
780
|
+
: "unsupported.form: not a command rule spelling this version imports — it stays in the settings file, unimported";
|
|
684
781
|
skipped.push({ rule: entry, reason });
|
|
685
782
|
continue;
|
|
686
783
|
}
|
|
687
|
-
const
|
|
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);
|
|
784
|
+
const parsed = parseAllowRuleText(entry);
|
|
694
785
|
if ("reject" in parsed) {
|
|
695
786
|
skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
|
|
696
787
|
continue;
|
|
697
788
|
}
|
|
698
789
|
if (candidates.some((c) => c.rule === parsed.rule.rule && sameScope(c.scope, scope)))
|
|
699
790
|
continue;
|
|
700
|
-
if (wildcard !== undefined)
|
|
701
|
-
translated.push({ from: entry, to: parsed.rule.rule, scope });
|
|
702
791
|
candidates.push({ rule: parsed.rule.rule, scope });
|
|
703
792
|
}
|
|
704
793
|
}
|
|
@@ -708,7 +797,7 @@ export async function prepareCcImport(opts) {
|
|
|
708
797
|
const record = {
|
|
709
798
|
id: mintId(opts.deps, "rar"),
|
|
710
799
|
...recordIdentityOf(owner),
|
|
711
|
-
schema:
|
|
800
|
+
schema: 3,
|
|
712
801
|
kind: "import",
|
|
713
802
|
state: "pending",
|
|
714
803
|
rev: 0,
|
|
@@ -742,7 +831,7 @@ export async function prepareStarterBatch(opts) {
|
|
|
742
831
|
const record = {
|
|
743
832
|
id: mintId(opts.deps, "rar"),
|
|
744
833
|
...recordIdentityOf(owner),
|
|
745
|
-
schema:
|
|
834
|
+
schema: 3,
|
|
746
835
|
kind: "starter",
|
|
747
836
|
state: "pending",
|
|
748
837
|
rev: 0,
|