@sema-agent/core 7.7.0 → 7.9.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 +88 -0
- package/dist/agents/agent-transcript-tool.js +2 -2
- package/dist/agents/list-agents-tool.js +2 -3
- package/dist/agents/observer.js +2 -2
- package/dist/agents/send-message-tool.js +2 -1
- package/dist/agents/subagent-steps.js +5 -4
- package/dist/agents/subagent.d.ts +1 -1
- package/dist/agents/subagent.js +2 -3
- package/dist/core/ask-origin.d.ts +12 -1
- package/dist/core/ask-origin.js +5 -1
- package/dist/core/ask-question.js +2 -2
- package/dist/core/checkpoint-store.d.ts +64 -24
- package/dist/core/context-edit.d.ts +0 -22
- package/dist/core/context-edit.js +2 -11
- package/dist/core/engine-notice.d.ts +6 -0
- package/dist/core/fs-write-gate-policy.d.ts +7 -1
- package/dist/core/fs-write-gate-policy.js +7 -7
- package/dist/core/gate-fold.js +2 -2
- package/dist/core/gate-lanes.js +53 -26
- package/dist/core/gate-outcome.d.ts +8 -4
- package/dist/core/gate-outcome.js +2 -1
- package/dist/core/governance-codes.d.ts +1 -1
- package/dist/core/governance-codes.js +6 -0
- package/dist/core/hooks.d.ts +64 -33
- package/dist/core/hooks.js +1 -1
- package/dist/core/lsp.js +2 -2
- package/dist/core/mcp-server-spec.d.ts +17 -0
- package/dist/core/mcp.js +3 -1
- package/dist/core/memory-engine/tools.js +4 -12
- package/dist/core/permission-rule-consent.d.ts +29 -36
- package/dist/core/permission-rule-consent.js +110 -60
- package/dist/core/permission-rule-model.d.ts +254 -55
- package/dist/core/permission-rule-model.js +323 -43
- package/dist/core/permission-rule-org.d.ts +9 -8
- package/dist/core/permission-rule-org.js +6 -15
- package/dist/core/permission-rule-provider.d.ts +10 -6
- package/dist/core/permission-rule-provider.js +12 -8
- package/dist/core/permission-rule-session.d.ts +7 -6
- package/dist/core/permission-rule-session.js +29 -10
- package/dist/core/permission-rule-store.d.ts +40 -18
- package/dist/core/permission-rule-store.js +68 -42
- package/dist/core/permission-rule-sync.d.ts +9 -3
- package/dist/core/permission-rule-sync.js +29 -25
- package/dist/core/permission-rule-syntax.d.ts +30 -0
- package/dist/core/permission-rule-syntax.js +44 -0
- package/dist/core/permission-rules.d.ts +85 -33
- package/dist/core/permission-rules.js +104 -88
- package/dist/core/persisted-rule-arms.d.ts +56 -0
- package/dist/core/persisted-rule-arms.js +48 -0
- package/dist/core/present-plan-tool.js +3 -4
- package/dist/core/runner/active-skill-scope.js +7 -4
- package/dist/core/runner/compaction-call-options.d.ts +1 -1
- package/dist/core/runner/contracts.d.ts +46 -3
- package/dist/core/runner/permission-rule-lanes.d.ts +33 -26
- package/dist/core/runner/permission-rule-lanes.js +27 -21
- package/dist/core/runner/prepare-ask-lane.d.ts +1 -1
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -4
- package/dist/core/runner/prepare-caps-and-workflow.js +16 -11
- package/dist/core/runner/prepare-defer-classify.d.ts +5 -26
- package/dist/core/runner/prepare-defer-classify.js +10 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +5 -4
- package/dist/core/runner/prepare-delegation-surface.js +15 -17
- package/dist/core/runner/prepare-gate-stations.d.ts +4 -1
- package/dist/core/runner/prepare-gate-stations.js +4 -2
- package/dist/core/runner/prepare-hands-readface.d.ts +3 -2
- package/dist/core/runner/prepare-hands-readface.js +3 -2
- package/dist/core/runner/prepare-inherited-gate.d.ts +1 -1
- package/dist/core/runner/prepare-inherited-gate.js +1 -0
- package/dist/core/runner/prepare-lsp.d.ts +4 -3
- package/dist/core/runner/prepare-lsp.js +3 -2
- package/dist/core/runner/prepare-memory-engine-session.d.ts +4 -4
- package/dist/core/runner/prepare-memory-engine-session.js +4 -2
- package/dist/core/runner/prepare-offload-wrappers.js +7 -5
- package/dist/core/runner/prepare-park-ask.d.ts +4 -0
- package/dist/core/runner/prepare-park-ask.js +4 -1
- package/dist/core/runner/prepare-project-context.d.ts +5 -23
- package/dist/core/runner/prepare-project-context.js +7 -5
- package/dist/core/runner/prepare-prompt-assembly.d.ts +1 -1
- package/dist/core/runner/prepare-protocol-tools.d.ts +5 -1
- package/dist/core/runner/prepare-protocol-tools.js +38 -21
- package/dist/core/runner/prepare-question-face.d.ts +4 -3
- package/dist/core/runner/prepare-question-face.js +5 -3
- package/dist/core/runner/prepare-run-refs.d.ts +16 -0
- package/dist/core/runner/prepare-run-refs.js +6 -0
- package/dist/core/runner/prepare-safety-scan.js +48 -2
- package/dist/core/runner/prepare-task.js +37 -19
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -2
- package/dist/core/runner/prepare-tool-disclosure-mount.js +3 -1
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -0
- package/dist/core/runner/prepare-wiring-manifest.js +3 -8
- package/dist/core/runner/resume-admission.d.ts +53 -0
- package/dist/core/runner/resume-admission.js +83 -0
- package/dist/core/runner/resume-apply.d.ts +50 -0
- package/dist/core/runner/resume-apply.js +184 -0
- package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
- package/dist/core/runner/resume-checkpoint-screen.js +108 -0
- package/dist/core/runner/resume-claim.d.ts +32 -0
- package/dist/core/runner/resume-claim.js +27 -0
- package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
- package/dist/core/runner/resume-internals-and-config.js +50 -0
- package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
- package/dist/core/runner/resume-policy-outcome.js +127 -0
- package/dist/core/runner/resume-preflight.d.ts +40 -0
- package/dist/core/runner/resume-preflight.js +122 -0
- package/dist/core/runner/resume-review-outcome.d.ts +30 -0
- package/dist/core/runner/resume-review-outcome.js +88 -0
- package/dist/core/runner/run-harness-handlers.d.ts +46 -0
- package/dist/core/runner/run-harness-handlers.js +345 -0
- package/dist/core/runner/run-turn-boundary.d.ts +69 -0
- package/dist/core/runner/run-turn-boundary.js +694 -0
- package/dist/core/runner/runtask.d.ts +20 -0
- package/dist/core/runner/runtask.js +129 -1774
- package/dist/core/runner/session-rule-policy.js +9 -4
- package/dist/core/runner/synthetic-tools.js +5 -8
- package/dist/core/runner/tool-disclosure.d.ts +4 -0
- package/dist/core/runner/tool-disclosure.js +9 -5
- package/dist/core/runner/tool-end-body.d.ts +12 -5
- package/dist/core/runner/tool-end-body.js +8 -7
- package/dist/core/runner/tool-face-overlay.d.ts +40 -0
- package/dist/core/runner/tool-face-overlay.js +125 -0
- package/dist/core/runner/tool-output-projection.d.ts +14 -1
- package/dist/core/runner/tool-output-projection.js +24 -18
- package/dist/core/runner/turn-attachments.d.ts +2 -2
- package/dist/core/sensitive-path-policy.d.ts +3 -0
- package/dist/core/sensitive-path-policy.js +6 -5
- package/dist/core/shared-memory/tools.js +3 -6
- package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
- package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
- package/dist/core/task-event.d.ts +10 -0
- package/dist/core/task-registry.js +4 -5
- package/dist/core/task-spec.d.ts +8 -0
- package/dist/core/tool-catalog-entries.d.ts +48 -0
- package/dist/core/tool-catalog-entries.js +174 -0
- package/dist/core/tool-conformance.d.ts +22 -0
- package/dist/core/tool-conformance.js +37 -0
- package/dist/core/tool-face.d.ts +171 -0
- package/dist/core/tool-face.js +25 -0
- package/dist/core/tool-policy.d.ts +24 -0
- package/dist/core/tool-policy.js +4 -2
- package/dist/core/tool-registry.d.ts +114 -0
- package/dist/core/tool-registry.js +114 -0
- package/dist/core/tool-result-budget.d.ts +0 -3
- package/dist/core/tool-result-budget.js +4 -3
- package/dist/core/tool-result-store.d.ts +0 -18
- package/dist/core/tool-result-store.js +2 -18
- package/dist/core/tool-roster.d.ts +459 -0
- package/dist/core/tool-roster.js +507 -0
- package/dist/core/tool-spec.d.ts +54 -3
- package/dist/core/tools.js +7 -0
- package/dist/core/wiring-manifest.d.ts +13 -0
- package/dist/core/wiring-manifest.js +2 -1
- package/dist/core/write-protect.d.ts +2 -1
- package/dist/core/write-protect.js +5 -4
- package/dist/engine/compaction/utils.d.ts +6 -0
- package/dist/engine/compaction/utils.js +19 -19
- package/dist/engine/loop/types.d.ts +6 -0
- package/dist/index.d.ts +17 -6
- package/dist/index.js +17 -6
- package/dist/orchestration/goal.js +2 -1
- package/dist/orchestration/run-spec.js +3 -5
- package/dist/orchestration/run-workflow-tool.js +2 -2
- package/dist/prompt-assembly/event-registry.js +3 -3
- package/dist/prompt-assembly/tool-catalog.d.ts +2 -2
- package/dist/stores/file/checkpoint-store.js +1 -1
- package/dist/stores/file/permission-rule-store.d.ts +28 -23
- package/dist/stores/file/permission-rule-store.js +64 -16
- package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
- package/dist/tools/fs/fs-bash.d.ts +1 -1
- package/dist/tools/fs/fs-bash.js +7 -11
- package/dist/tools/fs/fs-pdf.d.ts +1 -1
- package/dist/tools/fs/fs-read.js +2 -2
- package/dist/tools/fs/fs-search-tools.d.ts +0 -7
- package/dist/tools/fs/fs-search-tools.js +3 -16
- package/dist/tools/fs/fs-write.js +4 -6
- package/dist/tools/fs/notebook.d.ts +1 -1
- package/dist/tools/fs/repo-map.js +2 -2
- package/dist/tools/fs/safety.d.ts +3 -2
- package/dist/tools/fs/safety.js +6 -5
- package/dist/tools/monitor.js +2 -2
- package/dist/tools/scheduler-tools.js +5 -8
- package/dist/tools/task-list.js +5 -8
- package/dist/tools/todo.js +2 -2
- package/dist/tools/web.js +3 -6
- package/dist/tools/worktree.js +3 -4
- package/package.json +4 -1
- package/test/export-surface.snapshot.json +189 -17
|
@@ -6,6 +6,7 @@ import { formatMemoryAge } from "../memory-recall.js";
|
|
|
6
6
|
import { committedOriginOf } from "./frontmatter.js";
|
|
7
7
|
import { MEMORY_EXPOSURE_BANNER, MEMORY_EXPOSURE_HANDLE_TAG, MEMORY_PROVENANCE_INDEX_SENTENCE, MEMORY_PROVENANCE_SEARCH_SENTENCE, MEMORY_SEARCH_BAND_ORDER_HEADER } from "./provenance-wording.js";
|
|
8
8
|
import { deriveSupersededSet, memorySupersededNote, MEMORY_SEARCH_SUPERSEDED_TAG } from "./consolidation.js";
|
|
9
|
+
import { toolFace } from "../tool-catalog-entries.js";
|
|
9
10
|
export const MEMORY_SEARCH_TOOL_NAME = "memory_search";
|
|
10
11
|
export const MEMORY_GET_TOOL_NAME = "memory_get";
|
|
11
12
|
export const MEMORY_INDEX_TOOL_NAME = "memory_index";
|
|
@@ -175,11 +176,8 @@ export function createMemoryEngineTools(opts) {
|
|
|
175
176
|
};
|
|
176
177
|
const searchTool = {
|
|
177
178
|
name: MEMORY_SEARCH_TOOL_NAME,
|
|
179
|
+
...toolFace("memory-search"),
|
|
178
180
|
description: carry ? `${SEARCH_DESCRIPTION}\n\n${MEMORY_PROVENANCE_SEARCH_SENTENCE}` : SEARCH_DESCRIPTION,
|
|
179
|
-
effect: "read",
|
|
180
|
-
offload: false,
|
|
181
|
-
contentOrigin: "local",
|
|
182
|
-
contract: { contractId: "core.memory_search@1", implementationRevision: "6" },
|
|
183
181
|
parameters: Type.Object({
|
|
184
182
|
query: Type.String({ description: "Keywords to look for (lexical match against entry names, descriptions and bodies)." }),
|
|
185
183
|
limit: Type.Optional(Type.Number({ description: `Maximum hits to return (default ${MEMORY_SEARCH_DEFAULT_LIMIT}, max ${MEMORY_SEARCH_MAX_LIMIT}).` })),
|
|
@@ -319,11 +317,8 @@ export function createMemoryEngineTools(opts) {
|
|
|
319
317
|
};
|
|
320
318
|
const getTool = {
|
|
321
319
|
name: MEMORY_GET_TOOL_NAME,
|
|
320
|
+
...toolFace("memory-get"),
|
|
322
321
|
description: GET_DESCRIPTION,
|
|
323
|
-
effect: "read",
|
|
324
|
-
offload: false,
|
|
325
|
-
contentOrigin: "local",
|
|
326
|
-
contract: { contractId: "core.memory_get@1", implementationRevision: "6" },
|
|
327
322
|
parameters: Type.Object({
|
|
328
323
|
id: Type.Optional(Type.String({ description: "Entry id (exact lookup). Pass either id or slug, not both." })),
|
|
329
324
|
slug: Type.Optional(Type.String({ description: "Entry slug (its file path without .md). Ambiguous across scopes unless scope is also passed." })),
|
|
@@ -545,11 +540,8 @@ export function createMemoryEngineTools(opts) {
|
|
|
545
540
|
};
|
|
546
541
|
const indexTool = {
|
|
547
542
|
name: MEMORY_INDEX_TOOL_NAME,
|
|
543
|
+
...toolFace("memory-index"),
|
|
548
544
|
description: carry ? `${INDEX_DESCRIPTION}\n\n${MEMORY_PROVENANCE_INDEX_SENTENCE}` : INDEX_DESCRIPTION,
|
|
549
|
-
effect: "read",
|
|
550
|
-
offload: false,
|
|
551
|
-
contentOrigin: "local",
|
|
552
|
-
contract: { contractId: "core.memory_index@1", implementationRevision: "1" },
|
|
553
545
|
parameters: Type.Object({
|
|
554
546
|
offset: Type.Optional(Type.Number({ description: "Zero-based row offset into the listing (default 0) — pass the previous page's footer value to continue." })),
|
|
555
547
|
limit: Type.Optional(Type.Number({ description: `Maximum rows for this page (default ${MEMORY_INDEX_PAGE_ROWS}, max ${MEMORY_INDEX_MAX_PAGE_ROWS}).` })),
|
|
@@ -27,11 +27,14 @@
|
|
|
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 EditedRuleBreadthWarning, type UncoveredSegmentDetail, type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
|
|
30
|
+
import { type EditedRuleBreadthWarning, type UncoveredSegmentDetail, type RuleBehavior, type RuleOffer, type RuleRejectCode, type RuleScope, type RuleDot } from "./permission-rule-model.js";
|
|
31
31
|
import type { RuleOwner } from "./permission-rule-store.js";
|
|
32
32
|
import { type PermissionRuleStoreProvider } from "./permission-rule-provider.js";
|
|
33
|
-
/** One candidate rule inside an approval record: the exact text and where it
|
|
33
|
+
/** One candidate rule inside an approval record: what it says (`behavior`), the exact text and where it
|
|
34
|
+
* would apply. A card mints `allow` candidates only (a card is one person's yes); the settings import
|
|
35
|
+
* carries all three behaviors, each entry under the list it was read from. */
|
|
34
36
|
export interface RuleCandidate {
|
|
37
|
+
behavior: RuleBehavior;
|
|
35
38
|
rule: string;
|
|
36
39
|
scope: RuleScope;
|
|
37
40
|
}
|
|
@@ -95,9 +98,10 @@ export interface RuleApprovalRecord {
|
|
|
95
98
|
* as {@link StaleRuleApprovalRecord}. `3` (design/382 §3.5, B8/B7): the card batch's
|
|
96
99
|
* `uncoveredDetail` seat became REQUIRED and `edited.warnings` became storable — a `schema: 2`
|
|
97
100
|
* row is read back stale exactly like the pre-stamp rows before it (the short-lived-pending
|
|
98
|
-
* bargain, unchanged).
|
|
101
|
+
* bargain, unchanged). `4`: every candidate carries its `behavior` (the settings import now carries
|
|
102
|
+
* the deny/ask lists too) — a `schema: 3` row is stale by the same equality gate.
|
|
99
103
|
*/
|
|
100
|
-
schema:
|
|
104
|
+
schema: 4;
|
|
101
105
|
kind: RuleApprovalKind;
|
|
102
106
|
state: "pending" | "approved" | "redeemed";
|
|
103
107
|
candidates: RuleCandidate[];
|
|
@@ -167,7 +171,7 @@ export interface RuleApprovalRecord {
|
|
|
167
171
|
}
|
|
168
172
|
/**
|
|
169
173
|
* design/375 §4.5 — the minimal ENVELOPE a store hands back for a durable row that predates the
|
|
170
|
-
* CURRENT (`schema:
|
|
174
|
+
* CURRENT (`schema: 4`) record form. A typed read cannot honestly return such a row as a
|
|
171
175
|
* {@link RuleApprovalRecord}, and dropping it at the store layer would misreport "stale" as
|
|
172
176
|
* corruption — so the store contract is this variant instead: `staleSchema: true` plus the
|
|
173
177
|
* IDENTITY fields, which are spelled the same way in every version and therefore safe to read.
|
|
@@ -375,7 +379,7 @@ export declare function confirmRuleApproval(opts: {
|
|
|
375
379
|
/** Why a confirmation did not land. A closed set so a host can branch (re-present, re-fetch, give up).
|
|
376
380
|
* The two `record_*` members past `record_not_found` are the durable-row gates (design/375 §4.5/§4.7),
|
|
377
381
|
* disclosed to the record's own owner only — a non-owner gets `record_not_found` for every row:
|
|
378
|
-
* - `"record_schema_stale"` — the record predates the current (`schema:
|
|
382
|
+
* - `"record_schema_stale"` — the record predates the current (`schema: 4`) form; the decision it carried
|
|
379
383
|
* is void, and the recovery path is re-triggering the command for a fresh card (no
|
|
380
384
|
* compatibility read, no migration — `detail.message` says so);
|
|
381
385
|
* - `"record_malformed"` — the record's offer structure failed the read-side integrity checks
|
|
@@ -443,7 +447,7 @@ export type EditedRuleTextPrecheck = {
|
|
|
443
447
|
* for a surface that wants to answer while the person is still typing instead of after a round trip.
|
|
444
448
|
*
|
|
445
449
|
* It is the SAME body the confirmation arm runs (`checkEditedRuleText`), which is the whole point: a
|
|
446
|
-
* boundary that re-implements the check — or calls `
|
|
450
|
+
* boundary that re-implements the check — or calls `parseRuleText` directly — installs a SECOND,
|
|
447
451
|
* STRICTER judge that refuses spellings the edit face accepts (the muscle-memory `Bash(adb *)` form is
|
|
448
452
|
* exactly such a case: the validator alone refuses it as a wildcard; the edit face normalizes it first).
|
|
449
453
|
*
|
|
@@ -476,6 +480,7 @@ export declare function precheckEditedRuleText(text: string, command: string): E
|
|
|
476
480
|
* durable revision is reporting, not the landing's identity). */
|
|
477
481
|
export type RedeemResult = {
|
|
478
482
|
status: "redeemed";
|
|
483
|
+
behavior: RuleBehavior;
|
|
479
484
|
rule: string;
|
|
480
485
|
scope: RuleScope;
|
|
481
486
|
dot: RuleDot;
|
|
@@ -547,29 +552,13 @@ export interface ImportPreview {
|
|
|
547
552
|
layer: ImportedSettingsLayer;
|
|
548
553
|
found: boolean;
|
|
549
554
|
}>;
|
|
555
|
+
/** The two SETTINGS LAYERS this version does not read at all. The three permission lists of the layers it
|
|
556
|
+
* does read — allow, deny AND ask — are all imported (each candidate carries its `behavior`), so there
|
|
557
|
+
* is no longer a "left in place" count beside them: an entry that did not become a candidate is in
|
|
558
|
+
* `skipped` with its reason. */
|
|
550
559
|
uncovered: {
|
|
551
560
|
flagSettings: "not-imported-v1";
|
|
552
561
|
policySettings: "not-imported-v1";
|
|
553
|
-
/**
|
|
554
|
-
* The deny/ask buckets of the layers actually read: how many entries are SITTING THERE, across all
|
|
555
|
-
* layers, that this import deliberately leaves in place (they are the tightening direction and have
|
|
556
|
-
* their own channel — importing them through a loosening lane would be the wrong door).
|
|
557
|
-
*
|
|
558
|
-
* Always present, zero when the buckets are absent or empty: "this settings file has 14 deny entries
|
|
559
|
-
* we did not touch" and "there was nothing there" are different facts, and a key that appears only in
|
|
560
|
-
* the first case makes them indistinguishable for a reader who sees one preview. The count is of
|
|
561
|
-
* ENTRIES IN PLACE, not of a decision — nothing here is skipped-and-lost; `skipped` stays the list of
|
|
562
|
-
* allow-bucket entries that did not become candidates.
|
|
563
|
-
*
|
|
564
|
-
* A bucket that is PRESENT but not an array cannot be counted; it contributes 0 and is disclosed in
|
|
565
|
-
* `skipped` under the layer's path, so a zero is never the report for something unreadable. Same for
|
|
566
|
-
* a layer whose whole `permissions` member is not an object — every bucket in it reads as absent,
|
|
567
|
-
* which is exactly the shape that would otherwise report two clean zeros.
|
|
568
|
-
*/
|
|
569
|
-
denyAskBuckets: {
|
|
570
|
-
deny: number;
|
|
571
|
-
ask: number;
|
|
572
|
-
};
|
|
573
562
|
};
|
|
574
563
|
}
|
|
575
564
|
/**
|
|
@@ -591,6 +580,7 @@ export interface ImportPreview {
|
|
|
591
580
|
*/
|
|
592
581
|
export type RedeemedBatchMember = {
|
|
593
582
|
readonly candidateIndex: number;
|
|
583
|
+
readonly behavior: RuleBehavior;
|
|
594
584
|
readonly rule: string;
|
|
595
585
|
readonly scope: RuleScope;
|
|
596
586
|
readonly status: "persisted" | "deduped";
|
|
@@ -598,22 +588,25 @@ export type RedeemedBatchMember = {
|
|
|
598
588
|
readonly dot: RuleDot;
|
|
599
589
|
} | {
|
|
600
590
|
readonly candidateIndex: number;
|
|
591
|
+
readonly behavior: RuleBehavior;
|
|
601
592
|
readonly rule: string;
|
|
602
593
|
readonly scope: RuleScope;
|
|
603
594
|
readonly status: "refused";
|
|
604
595
|
readonly reason: string;
|
|
605
596
|
};
|
|
606
597
|
/**
|
|
607
|
-
* Read the allow
|
|
608
|
-
* approval record. Nothing is stored until someone confirms that record and the
|
|
609
|
-
* A preview with zero importable candidates returns NO `approvalId` — there is nothing
|
|
610
|
-
* and the absence says so (design/375 §4.1).
|
|
598
|
+
* Read the THREE permission lists (allow, deny, ask) of the user-editable settings layers and produce a
|
|
599
|
+
* preview plus a PENDING approval record. Nothing is stored until someone confirms that record and the
|
|
600
|
+
* batch is redeemed. A preview with zero importable candidates returns NO `approvalId` — there is nothing
|
|
601
|
+
* to confirm, and the absence says so (design/375 §4.1).
|
|
611
602
|
*
|
|
612
|
-
*
|
|
613
|
-
*
|
|
614
|
-
*
|
|
615
|
-
*
|
|
616
|
-
*
|
|
603
|
+
* Every list is read through the ONE validator under its own behavior: a deny/ask entry takes the
|
|
604
|
+
* tightening floor (`Bash(node:*)` as a deny is a legitimately wide tightening and imports), an allow
|
|
605
|
+
* entry the allow floor. A content-form deny/ask so imported is the person's own standing instruction
|
|
606
|
+
* and adjudicates in the persisted-rule lane beside their allows — deny > ask > allow — instead of
|
|
607
|
+
* being escorted by a shell into a bare tool-name ask that widens `Bash(rm -r:*)` into "every Bash call
|
|
608
|
+
* asks". An entry that does not import (a spelling with no rule form, a tool the lane does not speak
|
|
609
|
+
* for, a non-string) is DISCLOSED in `skipped` with its reason and stays in the settings file.
|
|
617
610
|
*/
|
|
618
611
|
export declare function prepareCcImport(opts: {
|
|
619
612
|
layers: CcImportLayer[];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { escapeForDisclosure, hasUnrenderableCharacters,
|
|
2
|
+
import { escapeForDisclosure, hasUnrenderableCharacters, parseRuleText, ruleAdmitsCommand, ruleBreadthWarningsOf, segmentCoverageOf, suggestRulesForCommand, RULE_BEHAVIORS, RULE_BEHAVIOR_PRECEDENCE, isRuleBehavior, } from "./permission-rule-model.js";
|
|
3
3
|
import { effectiveOrThrow } from "./permission-rule-provider.js";
|
|
4
4
|
import { errText, isValidConsentScope, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
5
5
|
export class InMemoryRuleApprovalRecordStore {
|
|
@@ -8,7 +8,7 @@ export class InMemoryRuleApprovalRecordStore {
|
|
|
8
8
|
const r = this.rows.get(id);
|
|
9
9
|
if (r === undefined)
|
|
10
10
|
return undefined;
|
|
11
|
-
if (r.schema !==
|
|
11
|
+
if (r.schema !== 4) {
|
|
12
12
|
return {
|
|
13
13
|
staleSchema: true,
|
|
14
14
|
id: r.id,
|
|
@@ -122,6 +122,12 @@ function approvalRecordDamageOf(rec) {
|
|
|
122
122
|
if (typeof cand !== "object" || cand === null || typeof cand.rule !== "string") {
|
|
123
123
|
return `offer ${at} references candidate ${member}, which is not a candidate row`;
|
|
124
124
|
}
|
|
125
|
+
if (!isRuleBehavior(cand.behavior)) {
|
|
126
|
+
return `offer ${at} references candidate ${member}, whose behavior is not one of ${RULE_BEHAVIORS.join("/")}`;
|
|
127
|
+
}
|
|
128
|
+
if (rec.kind === "card" && cand.behavior !== "allow") {
|
|
129
|
+
return `card offer ${at} references candidate ${member}, a ${cand.behavior} rule — a card mints allow candidates only`;
|
|
130
|
+
}
|
|
125
131
|
if (!isValidConsentScope(cand.scope)) {
|
|
126
132
|
return `offer ${at} references candidate ${member}, whose scope is not a consent-face scope (global, project, or session)`;
|
|
127
133
|
}
|
|
@@ -198,10 +204,11 @@ function approvalRecordDamageOf(rec) {
|
|
|
198
204
|
}
|
|
199
205
|
export function ruleOffersOfRecord(rec) {
|
|
200
206
|
const tripleOf = (index) => {
|
|
201
|
-
const
|
|
202
|
-
|
|
207
|
+
const cand = rec.candidates[index];
|
|
208
|
+
const text = cand?.rule;
|
|
209
|
+
if (cand === undefined || text === undefined)
|
|
203
210
|
throw new Error(`approval-record offer references candidate ${index}, which the record does not carry`);
|
|
204
|
-
const parsed =
|
|
211
|
+
const parsed = parseRuleText(text, cand.behavior);
|
|
205
212
|
if ("reject" in parsed) {
|
|
206
213
|
throw new Error(`approval-record candidate ${index} does not parse as a rule (${parsed.reject.code}) — refusing to render it as one`);
|
|
207
214
|
}
|
|
@@ -318,7 +325,7 @@ export async function prepareCardApproval(opts) {
|
|
|
318
325
|
if (claimedBy.has(text))
|
|
319
326
|
throw new Error(`engine defect: rule "${text}" was minted into two offers on one card`);
|
|
320
327
|
const index = candidates.length;
|
|
321
|
-
candidates.push({ rule: text, scope });
|
|
328
|
+
candidates.push({ behavior: "allow", rule: text, scope });
|
|
322
329
|
claimedBy.set(text, index);
|
|
323
330
|
return index;
|
|
324
331
|
};
|
|
@@ -336,7 +343,7 @@ export async function prepareCardApproval(opts) {
|
|
|
336
343
|
const record = {
|
|
337
344
|
id: mintId(opts.deps, "rar"),
|
|
338
345
|
...recordIdentityOf(owner),
|
|
339
|
-
schema:
|
|
346
|
+
schema: 4,
|
|
340
347
|
kind: "card",
|
|
341
348
|
state: "pending",
|
|
342
349
|
rev: 0,
|
|
@@ -427,7 +434,7 @@ function normalizeEditedSpelling(text) {
|
|
|
427
434
|
return `${head}(${body}:*)`;
|
|
428
435
|
}
|
|
429
436
|
function parseSubmittedSpelling(text) {
|
|
430
|
-
return
|
|
437
|
+
return parseRuleText(normalizeEditedSpelling(text), "allow");
|
|
431
438
|
}
|
|
432
439
|
function checkEditedRuleText(text, command) {
|
|
433
440
|
const parsed = parseSubmittedSpelling(text);
|
|
@@ -514,7 +521,7 @@ async function confirmEditedCandidate(rec, edit, deps) {
|
|
|
514
521
|
...rec,
|
|
515
522
|
rev: rec.rev + 1,
|
|
516
523
|
state: "approved",
|
|
517
|
-
candidates: [...rec.candidates, { rule: checked.canonicalRule, scope }],
|
|
524
|
+
candidates: [...rec.candidates, { behavior: "allow", rule: checked.canonicalRule, scope }],
|
|
518
525
|
offers: [...rec.offers, { kind: "single", candidate: index }],
|
|
519
526
|
selectedOffer: rec.offers.length,
|
|
520
527
|
edited: { index, text: edit.text, at: nowIso(deps), ...(checked.warnings !== undefined ? { warnings: checked.warnings.map((w) => w.code) } : {}) },
|
|
@@ -559,7 +566,8 @@ export async function redeemRuleTicket(opts) {
|
|
|
559
566
|
if (!offerMembersOf(chosenOffer).includes(parsed.index)) {
|
|
560
567
|
return { status: "refused", reason: "the ticket names an option the person did not choose" };
|
|
561
568
|
}
|
|
562
|
-
const
|
|
569
|
+
const siblingsOf = candidate.behavior === "allow" ? { rec, offer: chosenOffer, index: parsed.index } : undefined;
|
|
570
|
+
const parsedRule = parseRuleText(candidate.rule, candidate.behavior);
|
|
563
571
|
if ("reject" in parsedRule)
|
|
564
572
|
return { status: "refused", reason: `${parsedRule.reject.code}: ${parsedRule.reject.message}` };
|
|
565
573
|
let store;
|
|
@@ -587,12 +595,29 @@ export async function redeemRuleTicket(opts) {
|
|
|
587
595
|
const theirs = again === undefined || isStale(again) ? undefined : again.redeemedDots?.[parsed.index];
|
|
588
596
|
if (theirs === undefined)
|
|
589
597
|
return { status: "refused", reason: "the approval record changed state concurrently" };
|
|
590
|
-
return await applyRedemption({ deps: opts.deps, owner: caller, candidate, parsedRule: parsedRule.rule, dot: theirs, recordId: rec.id, kind: rec.kind, replay: true });
|
|
598
|
+
return await applyRedemption({ deps: opts.deps, owner: caller, candidate, parsedRule: parsedRule.rule, dot: theirs, recordId: rec.id, kind: rec.kind, replay: true, ...(siblingsOf !== undefined ? { siblingsOf } : {}) });
|
|
591
599
|
}
|
|
592
600
|
}
|
|
593
|
-
return await applyRedemption({ deps: opts.deps, owner: caller, candidate, parsedRule: parsedRule.rule, dot, recordId: rec.id, kind: rec.kind, replay: known !== undefined });
|
|
601
|
+
return await applyRedemption({ deps: opts.deps, owner: caller, candidate, parsedRule: parsedRule.rule, dot, recordId: rec.id, kind: rec.kind, replay: known !== undefined, ...(siblingsOf !== undefined ? { siblingsOf } : {}) });
|
|
594
602
|
}
|
|
595
603
|
const REDEEM_MAX_ATTEMPTS = 8;
|
|
604
|
+
async function tighteningSiblingsStand(rec, offer, index, store, durableSnapshot) {
|
|
605
|
+
for (const member of offerMembersOf(offer)) {
|
|
606
|
+
if (member === index)
|
|
607
|
+
continue;
|
|
608
|
+
const sibling = rec.candidates[member];
|
|
609
|
+
if (sibling === undefined || sibling.behavior === "allow")
|
|
610
|
+
continue;
|
|
611
|
+
const dot = rec.redeemedDots?.[member];
|
|
612
|
+
if (dot === undefined)
|
|
613
|
+
return false;
|
|
614
|
+
const view = sibling.scope.kind === "session" ? await effectiveOrThrow(store, { sessionId: sibling.scope.sessionId }) : durableSnapshot;
|
|
615
|
+
const live = view.rules.some((r) => r.behavior === sibling.behavior && r.rule === sibling.rule && sameScope(r.scope, sibling.scope) && r.adds.some((a) => a.dot.actor === dot.actor && a.dot.counter === dot.counter));
|
|
616
|
+
if (!live)
|
|
617
|
+
return false;
|
|
618
|
+
}
|
|
619
|
+
return true;
|
|
620
|
+
}
|
|
596
621
|
async function applyRedemption(args) {
|
|
597
622
|
let store;
|
|
598
623
|
try {
|
|
@@ -608,7 +633,14 @@ async function applyRedemption(args) {
|
|
|
608
633
|
for (let attempt = 0; attempt < REDEEM_MAX_ATTEMPTS; attempt++) {
|
|
609
634
|
let rev;
|
|
610
635
|
try {
|
|
611
|
-
|
|
636
|
+
const snapshot = await effectiveOrThrow(store);
|
|
637
|
+
rev = snapshot.rev;
|
|
638
|
+
if (args.siblingsOf !== undefined && !(await tighteningSiblingsStand(args.siblingsOf.rec, args.siblingsOf.offer, args.siblingsOf.index, store, snapshot))) {
|
|
639
|
+
return {
|
|
640
|
+
status: "refused",
|
|
641
|
+
reason: "withheld: this allow belongs to a batch whose deny/ask members do not all stand in the store — an allow must not stand wider than the list that was confirmed; redeem the batch (redeemRuleBatch) so the tightening members land first, or draw a fresh card",
|
|
642
|
+
};
|
|
643
|
+
}
|
|
612
644
|
}
|
|
613
645
|
catch (err) {
|
|
614
646
|
return { status: "refused", reason: `could not read the permission-rule store: ${errText(err)}` };
|
|
@@ -617,6 +649,7 @@ async function applyRedemption(args) {
|
|
|
617
649
|
try {
|
|
618
650
|
res = await writer.apply({
|
|
619
651
|
kind: "redemption-add",
|
|
652
|
+
behavior: args.parsedRule.behavior,
|
|
620
653
|
rule: args.parsedRule.rule,
|
|
621
654
|
scope: args.candidate.scope,
|
|
622
655
|
tool: args.parsedRule.tool,
|
|
@@ -639,7 +672,7 @@ async function applyRedemption(args) {
|
|
|
639
672
|
};
|
|
640
673
|
}
|
|
641
674
|
if (!("conflict" in res)) {
|
|
642
|
-
return { status: "redeemed", rule: args.parsedRule.rule, scope: args.candidate.scope, dot: args.dot, rev: res.rev, alreadyRedeemed: args.replay };
|
|
675
|
+
return { status: "redeemed", behavior: args.parsedRule.behavior, rule: args.parsedRule.rule, scope: args.candidate.scope, dot: args.dot, rev: res.rev, alreadyRedeemed: args.replay };
|
|
643
676
|
}
|
|
644
677
|
}
|
|
645
678
|
return { status: "refused", reason: `optimistic-concurrency retries exhausted after ${REDEEM_MAX_ATTEMPTS} attempts` };
|
|
@@ -654,7 +687,6 @@ export async function prepareCcImport(opts) {
|
|
|
654
687
|
const skipped = [];
|
|
655
688
|
const translated = [];
|
|
656
689
|
const layers = [];
|
|
657
|
-
const denyAskBuckets = { deny: 0, ask: 0 };
|
|
658
690
|
for (const layer of opts.layers) {
|
|
659
691
|
let raw;
|
|
660
692
|
try {
|
|
@@ -700,66 +732,55 @@ export async function prepareCcImport(opts) {
|
|
|
700
732
|
});
|
|
701
733
|
continue;
|
|
702
734
|
}
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
735
|
+
const scope = layer.layer === "userSettings" ? { kind: "global" } : { kind: "project", root: layer.root };
|
|
736
|
+
for (const behavior of RULE_BEHAVIORS) {
|
|
737
|
+
const entries = permissions?.[behavior];
|
|
738
|
+
if (entries === undefined || entries === null)
|
|
739
|
+
continue;
|
|
740
|
+
if (!Array.isArray(entries)) {
|
|
708
741
|
skipped.push({
|
|
709
742
|
rule: layer.path,
|
|
710
|
-
reason: `settings ${
|
|
743
|
+
reason: `settings ${behavior} bucket is not an array — no entry in it could be read (it stays in the settings file either way)`,
|
|
711
744
|
});
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
const allow = permissions?.allow;
|
|
715
|
-
if (!Array.isArray(allow))
|
|
716
|
-
continue;
|
|
717
|
-
const scope = layer.layer === "userSettings" ? { kind: "global" } : { kind: "project", root: layer.root };
|
|
718
|
-
for (const entry of allow) {
|
|
719
|
-
if (typeof entry !== "string") {
|
|
720
|
-
skipped.push({ rule: String(entry), reason: "settings entry is not a string" });
|
|
721
745
|
continue;
|
|
722
746
|
}
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
747
|
+
for (const entry of entries) {
|
|
748
|
+
if (typeof entry !== "string") {
|
|
749
|
+
skipped.push({ rule: String(entry), reason: `settings ${behavior} entry is not a string` });
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
if (behavior === "allow" && entry.startsWith("Read(") && "reject" in parseRuleText(entry, behavior)) {
|
|
726
753
|
skipped.push({
|
|
727
754
|
rule: entry,
|
|
728
755
|
reason: "unsupported.form: only the Read(//abs-dir/**) directory form imports in v1 — this entry stays in the settings file, unimported",
|
|
729
756
|
});
|
|
730
757
|
continue;
|
|
731
758
|
}
|
|
732
|
-
if (
|
|
733
|
-
|
|
734
|
-
}
|
|
735
|
-
continue;
|
|
736
|
-
}
|
|
737
|
-
if (!entry.startsWith("Bash(")) {
|
|
738
|
-
const reason = /^[A-Za-z][A-Za-z0-9_]*\(.*\)$/.test(entry)
|
|
739
|
-
? "unsupported.tool: only Bash(...) command rules and Read(//abs-dir/**) directory rules import in v1 — this entry stays in the settings file, unimported"
|
|
740
|
-
: /^[A-Za-z][A-Za-z0-9_-]*$/.test(entry)
|
|
759
|
+
if (!/^[A-Za-z][A-Za-z0-9_]*\(.*\)$/.test(entry)) {
|
|
760
|
+
const reason = /^[A-Za-z][A-Za-z0-9_-]*$/.test(entry)
|
|
741
761
|
? "unsupported.form: a bare tool-name entry is a name-set item, not a command rule — it stays in the settings file, unimported"
|
|
742
762
|
: "unsupported.form: not a command rule spelling this version imports — it stays in the settings file, unimported";
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
763
|
+
skipped.push({ rule: entry, reason });
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
const parsed = parseRuleText(entry, behavior);
|
|
767
|
+
if ("reject" in parsed) {
|
|
768
|
+
skipped.push({ rule: entry, reason: `${parsed.reject.code}: ${parsed.reject.message}` });
|
|
769
|
+
continue;
|
|
770
|
+
}
|
|
771
|
+
if (candidates.some((c) => c.behavior === behavior && c.rule === parsed.rule.rule && sameScope(c.scope, scope)))
|
|
772
|
+
continue;
|
|
773
|
+
candidates.push({ behavior, rule: parsed.rule.rule, scope });
|
|
750
774
|
}
|
|
751
|
-
if (candidates.some((c) => c.rule === parsed.rule.rule && sameScope(c.scope, scope)))
|
|
752
|
-
continue;
|
|
753
|
-
candidates.push({ rule: parsed.rule.rule, scope });
|
|
754
775
|
}
|
|
755
776
|
}
|
|
756
|
-
const preview = { candidates, skipped, translated, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS
|
|
777
|
+
const preview = { candidates, skipped, translated, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS } };
|
|
757
778
|
if (candidates.length === 0)
|
|
758
779
|
return { preview };
|
|
759
780
|
const record = {
|
|
760
781
|
id: mintId(opts.deps, "rar"),
|
|
761
782
|
...recordIdentityOf(owner),
|
|
762
|
-
schema:
|
|
783
|
+
schema: 4,
|
|
763
784
|
kind: "import",
|
|
764
785
|
state: "pending",
|
|
765
786
|
rev: 0,
|
|
@@ -785,15 +806,15 @@ export async function prepareStarterBatch(opts) {
|
|
|
785
806
|
const owner = resolveCallerOwner(opts.principal, opts.owner, "prepareStarterBatch");
|
|
786
807
|
const candidates = [];
|
|
787
808
|
for (const text of STARTER_RULES) {
|
|
788
|
-
const parsed =
|
|
809
|
+
const parsed = parseRuleText(text, "allow");
|
|
789
810
|
if ("reject" in parsed)
|
|
790
811
|
throw new Error(`starter rule "${text}" is not a valid rule: ${parsed.reject.message}`);
|
|
791
|
-
candidates.push({ rule: parsed.rule.rule, scope: { kind: "global" } });
|
|
812
|
+
candidates.push({ behavior: "allow", rule: parsed.rule.rule, scope: { kind: "global" } });
|
|
792
813
|
}
|
|
793
814
|
const record = {
|
|
794
815
|
id: mintId(opts.deps, "rar"),
|
|
795
816
|
...recordIdentityOf(owner),
|
|
796
|
-
schema:
|
|
817
|
+
schema: 4,
|
|
797
818
|
kind: "starter",
|
|
798
819
|
state: "pending",
|
|
799
820
|
rev: 0,
|
|
@@ -833,18 +854,30 @@ export async function redeemRuleBatch(opts) {
|
|
|
833
854
|
}
|
|
834
855
|
const members = [];
|
|
835
856
|
let rev = 0;
|
|
836
|
-
|
|
857
|
+
const ordered = [...offerMembersOf(chosenOffer)].sort((a, b) => {
|
|
858
|
+
const ra = RULE_BEHAVIOR_PRECEDENCE[rec.candidates[a].behavior];
|
|
859
|
+
const rb = RULE_BEHAVIOR_PRECEDENCE[rec.candidates[b].behavior];
|
|
860
|
+
return ra !== rb ? ra - rb : a - b;
|
|
861
|
+
});
|
|
862
|
+
let tighteningFailed;
|
|
863
|
+
for (const candidateIndex of ordered) {
|
|
837
864
|
const candidate = rec.candidates[candidateIndex];
|
|
838
|
-
const row = { candidateIndex, rule: candidate.rule, scope: candidate.scope };
|
|
865
|
+
const row = { candidateIndex, behavior: candidate.behavior, rule: candidate.rule, scope: candidate.scope };
|
|
866
|
+
if (candidate.behavior === "allow" && tighteningFailed !== undefined) {
|
|
867
|
+
members.push({ ...row, status: "refused", reason: `withheld: candidate ${tighteningFailed} (a deny/ask member of this batch) did not land, and an allow must not stand wider than the list that was confirmed — redeem the batch again once the store accepts the tightening member` });
|
|
868
|
+
continue;
|
|
869
|
+
}
|
|
839
870
|
let alreadyThere = false;
|
|
840
871
|
{
|
|
841
872
|
try {
|
|
842
873
|
const snap = await effectiveOrThrow(store, candidate.scope.kind === "session" ? { sessionId: candidate.scope.sessionId } : undefined);
|
|
843
874
|
rev = snap.rev;
|
|
844
|
-
alreadyThere = snap.rules.some((r) => r.rule === candidate.rule && sameScope(r.scope, candidate.scope));
|
|
875
|
+
alreadyThere = snap.rules.some((r) => r.behavior === candidate.behavior && r.rule === candidate.rule && sameScope(r.scope, candidate.scope));
|
|
845
876
|
}
|
|
846
877
|
catch (err) {
|
|
847
878
|
members.push({ ...row, status: "refused", reason: `could not read the permission-rule store: ${errText(err)}` });
|
|
879
|
+
if (candidate.behavior !== "allow")
|
|
880
|
+
tighteningFailed ??= candidateIndex;
|
|
848
881
|
continue;
|
|
849
882
|
}
|
|
850
883
|
}
|
|
@@ -855,8 +888,25 @@ export async function redeemRuleBatch(opts) {
|
|
|
855
888
|
});
|
|
856
889
|
if (res.status === "refused") {
|
|
857
890
|
members.push({ ...row, status: "refused", reason: res.reason });
|
|
891
|
+
if (candidate.behavior !== "allow")
|
|
892
|
+
tighteningFailed ??= candidateIndex;
|
|
858
893
|
continue;
|
|
859
894
|
}
|
|
895
|
+
if (candidate.behavior !== "allow") {
|
|
896
|
+
let live = false;
|
|
897
|
+
try {
|
|
898
|
+
const snap = await effectiveOrThrow(store, candidate.scope.kind === "session" ? { sessionId: candidate.scope.sessionId } : undefined);
|
|
899
|
+
live = snap.rules.some((r) => r.behavior === candidate.behavior && r.rule === candidate.rule && sameScope(r.scope, candidate.scope) && r.adds.some((a) => a.dot.actor === res.dot.actor && a.dot.counter === res.dot.counter));
|
|
900
|
+
}
|
|
901
|
+
catch {
|
|
902
|
+
live = false;
|
|
903
|
+
}
|
|
904
|
+
if (!live) {
|
|
905
|
+
members.push({ ...row, status: "refused", reason: `this ${candidate.behavior} member's redeemed dot ${res.dot.actor}#${res.dot.counter} is not live in the store (removed since it was redeemed) — a removed rule is not re-landed by replaying its record; a new consent is required` });
|
|
906
|
+
tighteningFailed ??= candidateIndex;
|
|
907
|
+
continue;
|
|
908
|
+
}
|
|
909
|
+
}
|
|
860
910
|
rev = candidate.scope.kind === "session" ? Math.max(rev, res.rev) : res.rev;
|
|
861
911
|
members.push({ ...row, status: alreadyThere ? "deduped" : "persisted", alreadyRedeemed: res.alreadyRedeemed, dot: res.dot });
|
|
862
912
|
}
|