@sema-agent/core 5.56.0 → 5.58.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 +122 -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 +13 -2
- package/dist/agents/send-message-tool.js +34 -12
- package/dist/agents/subagent-steps.d.ts +1 -1
- package/dist/agents/subagent.d.ts +13 -13
- package/dist/agents/team.d.ts +15 -6
- package/dist/agents/team.js +1 -0
- 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/anthropic.js +15 -5
- package/dist/brain/circuit-breaker.js +2 -1
- package/dist/brain/degrading.d.ts +1 -1
- package/dist/brain/degrading.js +4 -1
- package/dist/brain/errors.d.ts +3 -3
- package/dist/brain/failover.js +16 -1
- package/dist/brain/open-responses.js +15 -5
- package/dist/brain/openai.js +16 -5
- package/dist/brain/reasoning.d.ts +2 -2
- package/dist/brain/repetition.d.ts +1 -1
- package/dist/brain/request-params.d.ts +30 -27
- package/dist/brain/request-params.js +1 -7
- package/dist/brain/route-adjudicator.d.ts +190 -0
- package/dist/brain/route-adjudicator.js +189 -0
- package/dist/brain/route-conformance.d.ts +55 -0
- package/dist/brain/route-conformance.js +136 -0
- package/dist/brain/routing.js +8 -3
- 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.js +2 -0
- 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/mcp.js +4 -4
- package/dist/core/memory-engine/consolidation-driver.d.ts +207 -0
- package/dist/core/memory-engine/consolidation-driver.js +378 -0
- package/dist/core/memory-engine/consolidation.d.ts +46 -2
- package/dist/core/memory-engine/consolidation.js +1 -0
- 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 +62 -8
- package/dist/core/memory-engine/engine.js +40 -4
- package/dist/core/memory-engine/file-backend.d.ts +1 -1
- package/dist/core/memory-engine/index.d.ts +4 -2
- package/dist/core/memory-engine/index.js +4 -2
- 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-recall.d.ts +1 -1
- package/dist/core/memory.d.ts +2 -2
- package/dist/core/permission-rule-consent.d.ts +230 -36
- package/dist/core/permission-rule-consent.js +258 -54
- package/dist/core/permission-rule-model.d.ts +295 -97
- package/dist/core/permission-rule-model.js +132 -41
- 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 +54 -18
- package/dist/core/runner/prepare-workspace-restore.d.ts +2 -2
- package/dist/core/runner/runtask.d.ts +15 -12
- package/dist/core/runner/runtask.js +48 -0
- 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 +10 -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 +14 -7
- 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 +55 -28
- 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 +155 -70
- 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/agent-harness.js +26 -1
- package/dist/engine/harness/messages.d.ts +1 -1
- package/dist/engine/harness/types.d.ts +15 -11
- package/dist/engine/llm/types.d.ts +79 -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 +8 -4
- package/dist/index.js +5 -2
- package/dist/internal/llm.d.ts +1 -1
- 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 +10 -10
- package/dist/prompts/default.js +2 -0
- 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 +6 -2
- package/dist/scenarios/scenario-registry.js +4 -2
- 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/index.js +8 -1
- 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/scheduler-tools.js +28 -6
- package/dist/tools/task-list.d.ts +2 -2
- package/dist/tools/web.d.ts +19 -4
- package/dist/tools/web.js +8 -2
- package/dist/tools/worktree.d.ts +5 -5
- package/dist/tools/worktree.js +2 -2
- package/package.json +1 -1
- package/test/export-surface.snapshot.json +74 -3
|
@@ -1,11 +1,21 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
|
-
import { 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);
|
|
@@ -163,6 +315,39 @@ function normalizeEditedSpelling(text) {
|
|
|
163
315
|
return text;
|
|
164
316
|
return `${head}(${body}:*)`;
|
|
165
317
|
}
|
|
318
|
+
function parseSubmittedSpelling(text) {
|
|
319
|
+
return parseAllowRuleText(normalizeEditedSpelling(text));
|
|
320
|
+
}
|
|
321
|
+
function checkEditedRuleText(text, command) {
|
|
322
|
+
const parsed = parseSubmittedSpelling(text);
|
|
323
|
+
if ("reject" in parsed)
|
|
324
|
+
return { ok: false, code: parsed.reject.code, message: parsed.reject.message };
|
|
325
|
+
if (!ruleAdmitsCommand(parsed.rule, command)) {
|
|
326
|
+
return {
|
|
327
|
+
ok: false,
|
|
328
|
+
message: `the edited rule "${parsed.rule.rule}" does not admit the command that was decided ("${command}") — a card's edit may widen how much the rule covers, never move it to a different grant`,
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
return { ok: true, canonicalRule: parsed.rule.rule };
|
|
332
|
+
}
|
|
333
|
+
export function precheckEditedRuleText(text, command) {
|
|
334
|
+
if (typeof text !== "string") {
|
|
335
|
+
const e = new Error(`precheckEditedRuleText takes the edited rule text as a string (got ${typeof text}) — a non-string is a caller bug, not a rule a person could have typed`);
|
|
336
|
+
e.code = "config.invalid_argument";
|
|
337
|
+
throw e;
|
|
338
|
+
}
|
|
339
|
+
if (typeof command !== "string" || command === "") {
|
|
340
|
+
const e = new Error("precheckEditedRuleText takes the adjudicated command the card was drawn for — coverage cannot be judged without it, and reporting the absence as a rejected rule would blame the person's text");
|
|
341
|
+
e.code = "config.invalid_argument";
|
|
342
|
+
throw e;
|
|
343
|
+
}
|
|
344
|
+
if (hasUnrenderableCharacters(command)) {
|
|
345
|
+
const e = new Error("precheckEditedRuleText takes a command this lane can read — one carrying control or format characters is not a command any card was drawn for, and the refusal line naming it would carry those bytes onto a display surface");
|
|
346
|
+
e.code = "config.invalid_argument";
|
|
347
|
+
throw e;
|
|
348
|
+
}
|
|
349
|
+
return checkEditedRuleText(text, command);
|
|
350
|
+
}
|
|
166
351
|
async function confirmEditedCandidate(rec, edit, deps) {
|
|
167
352
|
const no = (reason, detail) => ({
|
|
168
353
|
ok: false,
|
|
@@ -187,7 +372,7 @@ async function confirmEditedCandidate(rec, edit, deps) {
|
|
|
187
372
|
return no("selection_mismatch");
|
|
188
373
|
let hit = edit.text === rec.edited.text;
|
|
189
374
|
if (!hit) {
|
|
190
|
-
const reparsed =
|
|
375
|
+
const reparsed = parseSubmittedSpelling(edit.text);
|
|
191
376
|
hit = "rule" in reparsed && reparsed.rule.rule === canonical;
|
|
192
377
|
}
|
|
193
378
|
if (!hit)
|
|
@@ -208,33 +393,30 @@ async function confirmEditedCandidate(rec, edit, deps) {
|
|
|
208
393
|
if (rec.command === undefined) {
|
|
209
394
|
return no("edit_rejected", { message: "the record does not carry the adjudicated command (minted before card edits existed) — coverage cannot be verified, so the edit is refused" });
|
|
210
395
|
}
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
return no("edit_rejected", { code:
|
|
214
|
-
if (!ruleAdmitsCommand(parsed.rule, rec.command)) {
|
|
215
|
-
return no("edit_rejected", {
|
|
216
|
-
message: `the edited rule "${parsed.rule.rule}" does not admit the command that was decided ("${rec.command}") — a card's edit may widen how much the rule covers, never move it to a different grant`,
|
|
217
|
-
});
|
|
396
|
+
const checked = checkEditedRuleText(edit.text, rec.command);
|
|
397
|
+
if (!checked.ok) {
|
|
398
|
+
return no("edit_rejected", { ...(checked.code !== undefined ? { code: checked.code } : {}), message: checked.message });
|
|
218
399
|
}
|
|
219
400
|
const index = rec.candidates.length;
|
|
220
401
|
const next = {
|
|
221
402
|
...rec,
|
|
222
403
|
rev: rec.rev + 1,
|
|
223
404
|
state: "approved",
|
|
224
|
-
candidates: [...rec.candidates, { rule:
|
|
225
|
-
|
|
405
|
+
candidates: [...rec.candidates, { rule: checked.canonicalRule, scope }],
|
|
406
|
+
offers: [...rec.offers, { kind: "single", candidate: index }],
|
|
407
|
+
selectedOffer: rec.offers.length,
|
|
226
408
|
edited: { index, text: edit.text, at: nowIso(deps) },
|
|
227
409
|
};
|
|
228
410
|
const won = await deps.approvals.cas(rec.id, rec.rev, next);
|
|
229
411
|
if (!won) {
|
|
230
412
|
const again = await deps.approvals.get(rec.id);
|
|
231
|
-
if (again === undefined)
|
|
413
|
+
if (again === undefined || isStale(again))
|
|
232
414
|
return no("record_not_found");
|
|
233
415
|
if (again.state === "pending")
|
|
234
416
|
return no("conflict");
|
|
235
417
|
return await confirmEditedCandidate(again, edit, deps);
|
|
236
418
|
}
|
|
237
|
-
return { ok: true, mintedCandidate: { index, rule:
|
|
419
|
+
return { ok: true, mintedCandidate: { index, rule: checked.canonicalRule, ticket: mintRuleTicket(rec.id, index) } };
|
|
238
420
|
}
|
|
239
421
|
export async function redeemRuleTicket(opts) {
|
|
240
422
|
const caller = resolveCallerOwner(opts.principal, opts.owner, "redeemRuleTicket");
|
|
@@ -247,12 +429,22 @@ export async function redeemRuleTicket(opts) {
|
|
|
247
429
|
const recOwner = ownerOfRecord(rec);
|
|
248
430
|
if (recOwner === undefined || !sameRuleOwner(recOwner, caller))
|
|
249
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}` };
|
|
250
434
|
if (rec.state === "pending")
|
|
251
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}` };
|
|
252
439
|
const candidate = rec.candidates[parsed.index];
|
|
253
440
|
if (candidate === undefined)
|
|
254
441
|
return { status: "refused", reason: "the ticket names no candidate on this record" };
|
|
255
|
-
|
|
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)) {
|
|
256
448
|
return { status: "refused", reason: "the ticket names an option the person did not choose" };
|
|
257
449
|
}
|
|
258
450
|
const parsedRule = parseAllowRuleText(candidate.rule);
|
|
@@ -280,7 +472,7 @@ export async function redeemRuleTicket(opts) {
|
|
|
280
472
|
const ok = await opts.deps.approvals.cas(rec.id, rec.rev, next);
|
|
281
473
|
if (!ok) {
|
|
282
474
|
const again = await opts.deps.approvals.get(rec.id);
|
|
283
|
-
const theirs = again
|
|
475
|
+
const theirs = again === undefined || isStale(again) ? undefined : again.redeemedDots?.[parsed.index];
|
|
284
476
|
if (theirs === undefined)
|
|
285
477
|
return { status: "refused", reason: "the approval record changed state concurrently" };
|
|
286
478
|
return await applyRedemption({ deps: opts.deps, owner: caller, candidate, parsedRule: parsedRule.rule, dot: theirs, recordId: rec.id, kind: rec.kind, replay: true });
|
|
@@ -428,17 +620,22 @@ export async function prepareCcImport(opts) {
|
|
|
428
620
|
candidates.push({ rule: parsed.rule.rule, scope });
|
|
429
621
|
}
|
|
430
622
|
}
|
|
623
|
+
const preview = { candidates, skipped, layers, uncovered: { ...IMPORT_UNCOVERED_LAYERS, denyAskBuckets } };
|
|
624
|
+
if (candidates.length === 0)
|
|
625
|
+
return { preview };
|
|
431
626
|
const record = {
|
|
432
627
|
id: mintId(opts.deps, "rar"),
|
|
433
628
|
...recordIdentityOf(owner),
|
|
629
|
+
schema: 2,
|
|
434
630
|
kind: "import",
|
|
435
631
|
state: "pending",
|
|
436
632
|
rev: 0,
|
|
437
633
|
candidates,
|
|
634
|
+
offers: [{ kind: "batch", candidates: candidates.map((_, i) => i) }],
|
|
438
635
|
createdAt: nowIso(opts.deps),
|
|
439
636
|
};
|
|
440
637
|
await opts.deps.approvals.create(record);
|
|
441
|
-
return { preview
|
|
638
|
+
return { preview, approvalId: record.id };
|
|
442
639
|
}
|
|
443
640
|
export const STARTER_RULES = [
|
|
444
641
|
"Bash(ls)",
|
|
@@ -463,10 +660,12 @@ export async function prepareStarterBatch(opts) {
|
|
|
463
660
|
const record = {
|
|
464
661
|
id: mintId(opts.deps, "rar"),
|
|
465
662
|
...recordIdentityOf(owner),
|
|
663
|
+
schema: 2,
|
|
466
664
|
kind: "starter",
|
|
467
665
|
state: "pending",
|
|
468
666
|
rev: 0,
|
|
469
667
|
candidates,
|
|
668
|
+
offers: [{ kind: "batch", candidates: candidates.map((_, i) => i) }],
|
|
470
669
|
createdAt: nowIso(opts.deps),
|
|
471
670
|
};
|
|
472
671
|
await opts.deps.approvals.create(record);
|
|
@@ -480,14 +679,18 @@ export async function redeemRuleBatch(opts) {
|
|
|
480
679
|
const recOwner = ownerOfRecord(rec);
|
|
481
680
|
if (recOwner === undefined || !sameRuleOwner(recOwner, caller))
|
|
482
681
|
return { status: "refused", reason: "the record belongs to a different principal" };
|
|
483
|
-
if (rec
|
|
484
|
-
return { status: "refused", reason:
|
|
682
|
+
if (isStale(rec))
|
|
683
|
+
return { status: "refused", reason: `record_schema_stale: ${STALE_RECORD_MESSAGE}` };
|
|
485
684
|
if (rec.state === "pending")
|
|
486
685
|
return { status: "refused", reason: "the approval record has not been confirmed" };
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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
|
+
}
|
|
491
694
|
let store;
|
|
492
695
|
try {
|
|
493
696
|
store = storeForOwner(opts.deps.provider, caller);
|
|
@@ -495,8 +698,11 @@ export async function redeemRuleBatch(opts) {
|
|
|
495
698
|
catch (err) {
|
|
496
699
|
return { status: "refused", reason: errText(err) };
|
|
497
700
|
}
|
|
498
|
-
|
|
499
|
-
|
|
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 };
|
|
500
706
|
let alreadyThere = false;
|
|
501
707
|
try {
|
|
502
708
|
const snap = await store.list();
|
|
@@ -504,22 +710,20 @@ export async function redeemRuleBatch(opts) {
|
|
|
504
710
|
alreadyThere = snap.rules.some((r) => r.rule === candidate.rule && sameScope(r.scope, candidate.scope));
|
|
505
711
|
}
|
|
506
712
|
catch (err) {
|
|
507
|
-
|
|
713
|
+
members.push({ ...row, status: "refused", reason: `could not read the permission-rule store: ${errText(err)}` });
|
|
714
|
+
continue;
|
|
508
715
|
}
|
|
509
716
|
const res = await redeemRuleTicket({
|
|
510
|
-
ticket: mintRuleTicket(rec.id,
|
|
717
|
+
ticket: mintRuleTicket(rec.id, candidateIndex),
|
|
511
718
|
...(caller.kind === "principal" ? { principal: caller.principal } : { owner: caller }),
|
|
512
719
|
deps: opts.deps,
|
|
513
720
|
});
|
|
514
721
|
if (res.status === "refused") {
|
|
515
|
-
|
|
722
|
+
members.push({ ...row, status: "refused", reason: res.reason });
|
|
516
723
|
continue;
|
|
517
724
|
}
|
|
518
725
|
rev = res.rev;
|
|
519
|
-
|
|
520
|
-
deduped.push(candidate);
|
|
521
|
-
else
|
|
522
|
-
persisted.push(candidate);
|
|
726
|
+
members.push({ ...row, status: alreadyThere ? "deduped" : "persisted", alreadyRedeemed: res.alreadyRedeemed, dot: res.dot });
|
|
523
727
|
}
|
|
524
|
-
return {
|
|
728
|
+
return { members, rev };
|
|
525
729
|
}
|