@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
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, suggestRulesForCommand, } from "./permission-rule-model.js";
|
|
2
|
+
import { hasUnrenderableCharacters, parseAllowRuleText, ruleAdmitsCommand, segmentCoverageOf, suggestRulesForCommand, } from "./permission-rule-model.js";
|
|
3
3
|
import { errText, sameRuleOwner, sameScope, writerOf } from "./permission-rule-store.js";
|
|
4
4
|
export class InMemoryRuleApprovalRecordStore {
|
|
5
5
|
rows = new Map();
|
|
6
6
|
async get(id) {
|
|
7
7
|
const r = this.rows.get(id);
|
|
8
|
-
|
|
8
|
+
if (r === undefined)
|
|
9
|
+
return undefined;
|
|
10
|
+
if (r.schema !== 2) {
|
|
11
|
+
return {
|
|
12
|
+
staleSchema: true,
|
|
13
|
+
id: r.id,
|
|
14
|
+
...(typeof r.principal === "string" ? { principal: r.principal } : {}),
|
|
15
|
+
...(r.owner !== undefined ? { owner: structuredClone(r.owner) } : {}),
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
return structuredClone(r);
|
|
9
19
|
}
|
|
10
20
|
async create(record) {
|
|
11
21
|
if (this.rows.has(record.id))
|
|
@@ -80,6 +90,90 @@ function storeForOwner(provider, owner) {
|
|
|
80
90
|
}
|
|
81
91
|
return store;
|
|
82
92
|
}
|
|
93
|
+
function isStale(rec) {
|
|
94
|
+
return rec.staleSchema === true;
|
|
95
|
+
}
|
|
96
|
+
const STALE_RECORD_MESSAGE = "this approval record predates the current record form, and the decision it carried is void — re-trigger the command to draw a fresh card";
|
|
97
|
+
function offerMembersOf(offer) {
|
|
98
|
+
return offer.kind === "single" ? [offer.candidate] : offer.candidates;
|
|
99
|
+
}
|
|
100
|
+
function approvalRecordDamageOf(rec) {
|
|
101
|
+
if (!Array.isArray(rec.candidates))
|
|
102
|
+
return "the record carries no candidate list";
|
|
103
|
+
if (!Array.isArray(rec.offers) || rec.offers.length === 0)
|
|
104
|
+
return "the record carries no offers";
|
|
105
|
+
const claimed = new Set();
|
|
106
|
+
for (const [at, offer] of rec.offers.entries()) {
|
|
107
|
+
if (typeof offer !== "object" || offer === null)
|
|
108
|
+
return `offer ${at} is not an offer object`;
|
|
109
|
+
if (offer.kind !== "single" && offer.kind !== "batch")
|
|
110
|
+
return `offer ${at} has an unknown kind`;
|
|
111
|
+
if (offer.kind === "batch" && (!Array.isArray(offer.candidates) || offer.candidates.length === 0)) {
|
|
112
|
+
return `batch offer ${at} has no members`;
|
|
113
|
+
}
|
|
114
|
+
const members = offerMembersOf(offer);
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (const member of members) {
|
|
117
|
+
if (!Number.isInteger(member) || member < 0 || rec.candidates[member] === undefined) {
|
|
118
|
+
return `offer ${at} references candidate ${member}, which the record does not carry`;
|
|
119
|
+
}
|
|
120
|
+
const cand = rec.candidates[member];
|
|
121
|
+
if (typeof cand !== "object" || cand === null || typeof cand.rule !== "string") {
|
|
122
|
+
return `offer ${at} references candidate ${member}, which is not a candidate row`;
|
|
123
|
+
}
|
|
124
|
+
if (seen.has(member))
|
|
125
|
+
return `batch offer ${at} references candidate ${member} twice`;
|
|
126
|
+
seen.add(member);
|
|
127
|
+
if (claimed.has(member))
|
|
128
|
+
return `candidate ${member} belongs to more than one offer`;
|
|
129
|
+
claimed.add(member);
|
|
130
|
+
}
|
|
131
|
+
if (offer.kind === "batch") {
|
|
132
|
+
if (rec.kind === "card") {
|
|
133
|
+
if (!Array.isArray(offer.segments) || offer.segments.length !== offer.candidates.length || !offer.segments.every((s) => typeof s === "string")) {
|
|
134
|
+
return `card batch offer ${at} must carry exactly one source segment per member`;
|
|
135
|
+
}
|
|
136
|
+
if (!Number.isInteger(offer.uncoveredSegments) || offer.uncoveredSegments < 0) {
|
|
137
|
+
return `card batch offer ${at} must carry a non-negative uncovered-segment count`;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
else if (offer.segments !== undefined || offer.uncoveredSegments !== undefined) {
|
|
141
|
+
return `${rec.kind} batch offer ${at} must not carry segment metadata — there is no compound command behind it`;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return undefined;
|
|
146
|
+
}
|
|
147
|
+
export function ruleOffersOfRecord(rec) {
|
|
148
|
+
const tripleOf = (index) => {
|
|
149
|
+
const text = rec.candidates[index]?.rule;
|
|
150
|
+
if (text === undefined)
|
|
151
|
+
throw new Error(`approval-record offer references candidate ${index}, which the record does not carry`);
|
|
152
|
+
const parsed = parseAllowRuleText(text);
|
|
153
|
+
if ("reject" in parsed) {
|
|
154
|
+
throw new Error(`approval-record candidate ${index} does not parse as a rule (${parsed.reject.code}) — refusing to render it as one`);
|
|
155
|
+
}
|
|
156
|
+
return { rule: parsed.rule.rule, match: parsed.rule.match, command: parsed.rule.command };
|
|
157
|
+
};
|
|
158
|
+
return rec.offers.map((offer) => {
|
|
159
|
+
if (offer.kind === "single")
|
|
160
|
+
return { kind: "single", ...tripleOf(offer.candidate) };
|
|
161
|
+
const segments = offer.segments;
|
|
162
|
+
if (segments === undefined || offer.uncoveredSegments === undefined) {
|
|
163
|
+
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");
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
kind: "batch",
|
|
167
|
+
rules: offer.candidates.map((candidate, i) => {
|
|
168
|
+
const segment = segments[i];
|
|
169
|
+
if (segment === undefined)
|
|
170
|
+
throw new Error(`batch offer member ${i} has no source segment recorded`);
|
|
171
|
+
return { ...tripleOf(candidate), segment };
|
|
172
|
+
}),
|
|
173
|
+
uncoveredSegments: offer.uncoveredSegments,
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
}
|
|
83
177
|
export function mintRuleTicket(recordId, candidateIndex) {
|
|
84
178
|
return `rt.${candidateIndex}.${recordId}`;
|
|
85
179
|
}
|
|
@@ -99,30 +193,72 @@ export async function prepareCardApproval(opts) {
|
|
|
99
193
|
if (opts.toolName !== CARD_RULE_TOOL)
|
|
100
194
|
return undefined;
|
|
101
195
|
const scope = opts.scope ?? { kind: "global" };
|
|
102
|
-
|
|
196
|
+
let coverage;
|
|
197
|
+
try {
|
|
198
|
+
const listed = await storeForOwner(opts.deps.provider, owner).list();
|
|
199
|
+
coverage = segmentCoverageOf(opts.command, { persisted: listed.rules }, { tool: CARD_RULE_TOOL, cwd: opts.cwd });
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
coverage = undefined;
|
|
203
|
+
}
|
|
204
|
+
const suggested = suggestRulesForCommand(opts.command, coverage !== undefined ? { coverage } : undefined);
|
|
205
|
+
const candidates = [];
|
|
206
|
+
const claimedBy = new Map();
|
|
207
|
+
const claim = (text) => {
|
|
208
|
+
if (claimedBy.has(text))
|
|
209
|
+
throw new Error(`engine defect: rule "${text}" was minted into two offers on one card`);
|
|
210
|
+
const index = candidates.length;
|
|
211
|
+
candidates.push({ rule: text, scope });
|
|
212
|
+
claimedBy.set(text, index);
|
|
213
|
+
return index;
|
|
214
|
+
};
|
|
215
|
+
const offers = suggested.map((offer) => offer.kind === "single"
|
|
216
|
+
? { kind: "single", candidate: claim(offer.rule) }
|
|
217
|
+
: {
|
|
218
|
+
kind: "batch",
|
|
219
|
+
candidates: offer.rules.map((r) => claim(r.rule)),
|
|
220
|
+
segments: offer.rules.map((r) => r.segment),
|
|
221
|
+
uncoveredSegments: offer.uncoveredSegments,
|
|
222
|
+
});
|
|
103
223
|
if (candidates.length === 0)
|
|
104
224
|
return undefined;
|
|
105
225
|
const record = {
|
|
106
226
|
id: mintId(opts.deps, "rar"),
|
|
107
227
|
...recordIdentityOf(owner),
|
|
228
|
+
schema: 2,
|
|
108
229
|
kind: "card",
|
|
109
230
|
state: "pending",
|
|
110
231
|
rev: 0,
|
|
111
232
|
candidates,
|
|
233
|
+
offers,
|
|
112
234
|
command: opts.command,
|
|
113
235
|
createdAt: nowIso(opts.deps),
|
|
114
236
|
...(opts.toolCallId !== undefined ? { toolCallId: opts.toolCallId } : {}),
|
|
115
237
|
...(opts.boundInputHash !== undefined ? { boundInputHash: opts.boundInputHash } : {}),
|
|
116
238
|
};
|
|
117
239
|
await opts.deps.approvals.create(record);
|
|
118
|
-
return {
|
|
240
|
+
return {
|
|
241
|
+
approvalId: record.id,
|
|
242
|
+
offers: ruleOffersOfRecord(record),
|
|
243
|
+
tickets: candidates.map((_, i) => mintRuleTicket(record.id, i)),
|
|
244
|
+
candidates,
|
|
245
|
+
};
|
|
119
246
|
}
|
|
120
247
|
const CARD_RULE_TOOL = "Bash";
|
|
121
248
|
export async function confirmRuleApproval(opts) {
|
|
122
249
|
const caller = resolveCallerOwner(opts.principal, opts.owner, "confirmRuleApproval");
|
|
123
|
-
const no = (reason) => ({
|
|
124
|
-
|
|
125
|
-
|
|
250
|
+
const no = (reason, detail) => ({
|
|
251
|
+
ok: false,
|
|
252
|
+
reason,
|
|
253
|
+
...(detail !== undefined ? { detail } : {}),
|
|
254
|
+
});
|
|
255
|
+
if (opts.editedCandidate !== undefined && opts.selectedOffer !== undefined) {
|
|
256
|
+
const e = new Error("confirmRuleApproval takes selectedOffer OR editedCandidate, never both — one confirmation carries one choice");
|
|
257
|
+
e.code = "config.invalid_argument";
|
|
258
|
+
throw e;
|
|
259
|
+
}
|
|
260
|
+
if ("selectedCandidate" in opts) {
|
|
261
|
+
const e = new Error("confirmRuleApproval no longer takes selectedCandidate — selection is by OFFER index (selectedOffer) against the prepared record's offers; re-fetch the card and choose an offer");
|
|
126
262
|
e.code = "config.invalid_argument";
|
|
127
263
|
throw e;
|
|
128
264
|
}
|
|
@@ -130,28 +266,44 @@ export async function confirmRuleApproval(opts) {
|
|
|
130
266
|
const recOwner = rec === undefined ? undefined : ownerOfRecord(rec);
|
|
131
267
|
if (rec === undefined || recOwner === undefined || !sameRuleOwner(recOwner, caller))
|
|
132
268
|
return no("record_not_found");
|
|
269
|
+
if (isStale(rec))
|
|
270
|
+
return no("record_schema_stale", { message: STALE_RECORD_MESSAGE });
|
|
271
|
+
const damage = approvalRecordDamageOf(rec);
|
|
272
|
+
if (damage !== undefined)
|
|
273
|
+
return no("record_malformed", { message: damage });
|
|
133
274
|
if (opts.editedCandidate !== undefined)
|
|
134
275
|
return await confirmEditedCandidate(rec, opts.editedCandidate, opts.deps);
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
276
|
+
const chosen = opts.selectedOffer === undefined ? (rec.offers.length === 1 ? 0 : undefined) : opts.selectedOffer;
|
|
277
|
+
if (chosen === undefined)
|
|
278
|
+
return no("selection_missing");
|
|
279
|
+
if (!Number.isInteger(chosen) || rec.offers[chosen] === undefined)
|
|
280
|
+
return no("selection_invalid");
|
|
281
|
+
if (rec.state === "approved" || rec.state === "redeemed") {
|
|
282
|
+
const stored = rec.selectedOffer;
|
|
283
|
+
if (stored === undefined || !Number.isInteger(stored) || rec.offers[stored] === undefined) {
|
|
284
|
+
return no("record_malformed", { message: "the settled record does not name a valid chosen offer" });
|
|
143
285
|
}
|
|
144
|
-
|
|
145
|
-
return won ? { ok: true } : no("conflict");
|
|
286
|
+
return stored === chosen ? { ok: true } : no("selection_mismatch");
|
|
146
287
|
}
|
|
147
|
-
if (opts.selectedCandidate !== undefined)
|
|
148
|
-
return no("batch_takes_no_selection");
|
|
149
|
-
if (rec.state === "approved")
|
|
150
|
-
return { ok: true };
|
|
151
288
|
if (rec.state !== "pending")
|
|
152
289
|
return no("not_pending");
|
|
153
|
-
|
|
154
|
-
|
|
290
|
+
if (rec.selectedOffer !== undefined)
|
|
291
|
+
return no("record_malformed", { message: "a pending record must not already carry a chosen offer" });
|
|
292
|
+
const won = await opts.deps.approvals.cas(rec.id, rec.rev, { ...rec, rev: rec.rev + 1, state: "approved", selectedOffer: chosen });
|
|
293
|
+
if (won)
|
|
294
|
+
return { ok: true };
|
|
295
|
+
const again = await opts.deps.approvals.get(rec.id);
|
|
296
|
+
if (again === undefined)
|
|
297
|
+
return no("record_not_found");
|
|
298
|
+
if (isStale(again))
|
|
299
|
+
return no("record_schema_stale", { message: STALE_RECORD_MESSAGE });
|
|
300
|
+
if (again.state === "pending")
|
|
301
|
+
return no("conflict");
|
|
302
|
+
const settled = again.selectedOffer;
|
|
303
|
+
if (settled === undefined || !Number.isInteger(settled) || again.offers[settled] === undefined) {
|
|
304
|
+
return no("record_malformed", { message: "the settled record does not name a valid chosen offer" });
|
|
305
|
+
}
|
|
306
|
+
return settled === chosen ? { ok: true } : no("selection_mismatch");
|
|
155
307
|
}
|
|
156
308
|
function normalizeEditedSpelling(text) {
|
|
157
309
|
const m = /^([A-Za-z][A-Za-z0-9_]*)\((.+) \*\)$/.exec(text);
|
|
@@ -251,13 +403,14 @@ async function confirmEditedCandidate(rec, edit, deps) {
|
|
|
251
403
|
rev: rec.rev + 1,
|
|
252
404
|
state: "approved",
|
|
253
405
|
candidates: [...rec.candidates, { rule: checked.canonicalRule, scope }],
|
|
254
|
-
|
|
406
|
+
offers: [...rec.offers, { kind: "single", candidate: index }],
|
|
407
|
+
selectedOffer: rec.offers.length,
|
|
255
408
|
edited: { index, text: edit.text, at: nowIso(deps) },
|
|
256
409
|
};
|
|
257
410
|
const won = await deps.approvals.cas(rec.id, rec.rev, next);
|
|
258
411
|
if (!won) {
|
|
259
412
|
const again = await deps.approvals.get(rec.id);
|
|
260
|
-
if (again === undefined)
|
|
413
|
+
if (again === undefined || isStale(again))
|
|
261
414
|
return no("record_not_found");
|
|
262
415
|
if (again.state === "pending")
|
|
263
416
|
return no("conflict");
|
|
@@ -276,12 +429,22 @@ export async function redeemRuleTicket(opts) {
|
|
|
276
429
|
const recOwner = ownerOfRecord(rec);
|
|
277
430
|
if (recOwner === undefined || !sameRuleOwner(recOwner, caller))
|
|
278
431
|
return { status: "refused", reason: "the ticket belongs to a different principal" };
|
|
432
|
+
if (isStale(rec))
|
|
433
|
+
return { status: "refused", reason: `record_schema_stale: ${STALE_RECORD_MESSAGE}` };
|
|
279
434
|
if (rec.state === "pending")
|
|
280
435
|
return { status: "refused", reason: "the approval record has not been confirmed" };
|
|
436
|
+
const damage = approvalRecordDamageOf(rec);
|
|
437
|
+
if (damage !== undefined)
|
|
438
|
+
return { status: "refused", reason: `the approval record is malformed: ${damage}` };
|
|
281
439
|
const candidate = rec.candidates[parsed.index];
|
|
282
440
|
if (candidate === undefined)
|
|
283
441
|
return { status: "refused", reason: "the ticket names no candidate on this record" };
|
|
284
|
-
|
|
442
|
+
const chosen = rec.selectedOffer;
|
|
443
|
+
const chosenOffer = chosen !== undefined && Number.isInteger(chosen) ? rec.offers[chosen] : undefined;
|
|
444
|
+
if (chosenOffer === undefined) {
|
|
445
|
+
return { status: "refused", reason: "the approval record is malformed: it is settled but names no valid chosen offer" };
|
|
446
|
+
}
|
|
447
|
+
if (!offerMembersOf(chosenOffer).includes(parsed.index)) {
|
|
285
448
|
return { status: "refused", reason: "the ticket names an option the person did not choose" };
|
|
286
449
|
}
|
|
287
450
|
const parsedRule = parseAllowRuleText(candidate.rule);
|
|
@@ -309,7 +472,7 @@ export async function redeemRuleTicket(opts) {
|
|
|
309
472
|
const ok = await opts.deps.approvals.cas(rec.id, rec.rev, next);
|
|
310
473
|
if (!ok) {
|
|
311
474
|
const again = await opts.deps.approvals.get(rec.id);
|
|
312
|
-
const theirs = again
|
|
475
|
+
const theirs = again === undefined || isStale(again) ? undefined : again.redeemedDots?.[parsed.index];
|
|
313
476
|
if (theirs === undefined)
|
|
314
477
|
return { status: "refused", reason: "the approval record changed state concurrently" };
|
|
315
478
|
return await applyRedemption({ deps: opts.deps, owner: caller, candidate, parsedRule: parsedRule.rule, dot: theirs, recordId: rec.id, kind: rec.kind, replay: true });
|
|
@@ -457,17 +620,22 @@ export async function prepareCcImport(opts) {
|
|
|
457
620
|
candidates.push({ rule: parsed.rule.rule, scope });
|
|
458
621
|
}
|
|
459
622
|
}
|
|
623
|
+
const preview = { candidates, skipped, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } };
|
|
624
|
+
if (candidates.length === 0)
|
|
625
|
+
return { preview };
|
|
460
626
|
const record = {
|
|
461
627
|
id: mintId(opts.deps, "rar"),
|
|
462
628
|
...recordIdentityOf(owner),
|
|
629
|
+
schema: 2,
|
|
463
630
|
kind: "import",
|
|
464
631
|
state: "pending",
|
|
465
632
|
rev: 0,
|
|
466
633
|
candidates,
|
|
634
|
+
offers: [{ kind: "batch", candidates: candidates.map((_, i) => i) }],
|
|
467
635
|
createdAt: nowIso(opts.deps),
|
|
468
636
|
};
|
|
469
637
|
await opts.deps.approvals.create(record);
|
|
470
|
-
return { preview
|
|
638
|
+
return { preview, approvalId: record.id };
|
|
471
639
|
}
|
|
472
640
|
export const STARTER_RULES = [
|
|
473
641
|
"Bash(ls)",
|
|
@@ -492,10 +660,12 @@ export async function prepareStarterBatch(opts) {
|
|
|
492
660
|
const record = {
|
|
493
661
|
id: mintId(opts.deps, "rar"),
|
|
494
662
|
...recordIdentityOf(owner),
|
|
663
|
+
schema: 2,
|
|
495
664
|
kind: "starter",
|
|
496
665
|
state: "pending",
|
|
497
666
|
rev: 0,
|
|
498
667
|
candidates,
|
|
668
|
+
offers: [{ kind: "batch", candidates: candidates.map((_, i) => i) }],
|
|
499
669
|
createdAt: nowIso(opts.deps),
|
|
500
670
|
};
|
|
501
671
|
await opts.deps.approvals.create(record);
|
|
@@ -509,14 +679,18 @@ export async function redeemRuleBatch(opts) {
|
|
|
509
679
|
const recOwner = ownerOfRecord(rec);
|
|
510
680
|
if (recOwner === undefined || !sameRuleOwner(recOwner, caller))
|
|
511
681
|
return { status: "refused", reason: "the record belongs to a different principal" };
|
|
512
|
-
if (rec
|
|
513
|
-
return { status: "refused", reason:
|
|
682
|
+
if (isStale(rec))
|
|
683
|
+
return { status: "refused", reason: `record_schema_stale: ${STALE_RECORD_MESSAGE}` };
|
|
514
684
|
if (rec.state === "pending")
|
|
515
685
|
return { status: "refused", reason: "the approval record has not been confirmed" };
|
|
516
|
-
const
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
686
|
+
const damage = approvalRecordDamageOf(rec);
|
|
687
|
+
if (damage !== undefined)
|
|
688
|
+
return { status: "refused", reason: `the approval record is malformed: ${damage}` };
|
|
689
|
+
const chosen = rec.selectedOffer;
|
|
690
|
+
const chosenOffer = chosen !== undefined && Number.isInteger(chosen) ? rec.offers[chosen] : undefined;
|
|
691
|
+
if (chosenOffer === undefined) {
|
|
692
|
+
return { status: "refused", reason: "the approval record is malformed: it is settled but names no valid chosen offer" };
|
|
693
|
+
}
|
|
520
694
|
let store;
|
|
521
695
|
try {
|
|
522
696
|
store = storeForOwner(opts.deps.provider, caller);
|
|
@@ -524,8 +698,11 @@ export async function redeemRuleBatch(opts) {
|
|
|
524
698
|
catch (err) {
|
|
525
699
|
return { status: "refused", reason: errText(err) };
|
|
526
700
|
}
|
|
527
|
-
|
|
528
|
-
|
|
701
|
+
const members = [];
|
|
702
|
+
let rev = 0;
|
|
703
|
+
for (const candidateIndex of offerMembersOf(chosenOffer)) {
|
|
704
|
+
const candidate = rec.candidates[candidateIndex];
|
|
705
|
+
const row = { candidateIndex, rule: candidate.rule, scope: candidate.scope };
|
|
529
706
|
let alreadyThere = false;
|
|
530
707
|
try {
|
|
531
708
|
const snap = await store.list();
|
|
@@ -533,22 +710,20 @@ export async function redeemRuleBatch(opts) {
|
|
|
533
710
|
alreadyThere = snap.rules.some((r) => r.rule === candidate.rule && sameScope(r.scope, candidate.scope));
|
|
534
711
|
}
|
|
535
712
|
catch (err) {
|
|
536
|
-
|
|
713
|
+
members.push({ ...row, status: "refused", reason: `could not read the permission-rule store: ${errText(err)}` });
|
|
714
|
+
continue;
|
|
537
715
|
}
|
|
538
716
|
const res = await redeemRuleTicket({
|
|
539
|
-
ticket: mintRuleTicket(rec.id,
|
|
717
|
+
ticket: mintRuleTicket(rec.id, candidateIndex),
|
|
540
718
|
...(caller.kind === "principal" ? { principal: caller.principal } : { owner: caller }),
|
|
541
719
|
deps: opts.deps,
|
|
542
720
|
});
|
|
543
721
|
if (res.status === "refused") {
|
|
544
|
-
|
|
722
|
+
members.push({ ...row, status: "refused", reason: res.reason });
|
|
545
723
|
continue;
|
|
546
724
|
}
|
|
547
725
|
rev = res.rev;
|
|
548
|
-
|
|
549
|
-
deduped.push(candidate);
|
|
550
|
-
else
|
|
551
|
-
persisted.push(candidate);
|
|
726
|
+
members.push({ ...row, status: alreadyThere ? "deduped" : "persisted", alreadyRedeemed: res.alreadyRedeemed, dot: res.dot });
|
|
552
727
|
}
|
|
553
|
-
return {
|
|
728
|
+
return { members, rev };
|
|
554
729
|
}
|