@sema-agent/server 7.51.0 → 7.52.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/README.md +1 -1
- package/README.zh-CN.md +1 -1
- package/USAGE.md +83 -20
- package/dist/adoption/plan.d.ts +4 -4
- package/dist/adoption/plan.js +11 -0
- package/dist/approval-card.d.ts +26 -2
- package/dist/approval-card.js +9 -0
- package/dist/approval-hmac.d.ts +1 -1
- package/dist/approval-reconciler.d.ts +2 -2
- package/dist/approval-reconciler.js +1 -1
- package/dist/bench/l8/artifact.d.ts +1 -1
- package/dist/bench/s1/arms.d.ts +1 -1
- package/dist/bench/s1/live-deps.d.ts +1 -1
- package/dist/bench/s1/oracle.d.ts +1 -1
- package/dist/bench/s1/row.d.ts +1 -1
- package/dist/boot/budget-tracing.d.ts +1 -1
- package/dist/boot/budget-tracing.js +2 -5
- package/dist/boot/coordinators.js +3 -1
- package/dist/boot/deferred-sandbox-path-env.d.ts +1 -1
- package/dist/boot/engine-lease.d.ts +1 -1
- package/dist/boot/execution-env.d.ts +1 -1
- package/dist/boot/governance-seams.d.ts +2 -2
- package/dist/boot/leader.d.ts +1 -1
- package/dist/boot/memory-boundary.d.ts +2 -2
- package/dist/boot/memory-consolidation.d.ts +6 -6
- package/dist/boot/org-memory.d.ts +1 -1
- package/dist/boot/parked-revive-gate.d.ts +3 -3
- package/dist/boot/reapers.d.ts +2 -2
- package/dist/boot/reapers.js +17 -4
- package/dist/boot/resolve-spec.d.ts +12 -0
- package/dist/boot/resolve-spec.js +29 -7
- package/dist/boot/retention-lane.d.ts +2 -2
- package/dist/boot/runner-deps.d.ts +17 -7
- package/dist/boot/runner-deps.js +4 -2
- package/dist/boot/runtime-caps.d.ts +23 -0
- package/dist/boot/runtime-caps.js +24 -2
- package/dist/boot/session-faces.d.ts +1 -1
- package/dist/boot/session-faces.js +14 -3
- package/dist/boot/stores.js +31 -1
- package/dist/boot/task-list-lane.d.ts +2 -2
- package/dist/brain.js +51 -31
- package/dist/budget.d.ts +2 -2
- package/dist/capabilities/center-plugins.d.ts +1 -1
- package/dist/capabilities/collab-workflows.d.ts +1 -1
- package/dist/capabilities/hands-lane.d.ts +3 -3
- package/dist/capabilities/scenarios.d.ts +3 -3
- package/dist/capabilities/skills.d.ts +1 -1
- package/dist/config-center/hot-keys-registry.js +5 -0
- package/dist/config-center/http-client.d.ts +2 -2
- package/dist/config-center/read-face.d.ts +1 -1
- package/dist/config-center/restart-signal.d.ts +1 -1
- package/dist/config-types.d.ts +78 -18
- package/dist/config.js +45 -10
- package/dist/deployment-governance.d.ts +1 -1
- package/dist/device-enrollment.d.ts +2 -2
- package/dist/device-store.d.ts +3 -3
- package/dist/device-ws-protocol.d.ts +2 -2
- package/dist/env-facts.d.ts +1 -1
- package/dist/execution-lane-caps.d.ts +1 -1
- package/dist/fleet/fleet-bus.d.ts +1 -1
- package/dist/fleet/fleet-reconciler.d.ts +1 -1
- package/dist/fleet/fleet-terminal-window.d.ts +1 -1
- package/dist/http/active-run-conflict.d.ts +2 -2
- package/dist/http/principal-gate.d.ts +1 -1
- package/dist/http/route-ctx.d.ts +24 -4
- package/dist/http/routes/admin-memory-optout.d.ts +48 -0
- package/dist/http/routes/admin-memory-optout.js +75 -0
- package/dist/http/routes/agents-roster.d.ts +1 -1
- package/dist/http/routes/approvals-assistant.js +21 -21
- package/dist/http/routes/capabilities.js +20 -9
- package/dist/http/routes/diagnostics.d.ts +1 -1
- package/dist/http/routes/leader.d.ts +1 -1
- package/dist/http/routes/memory-bundle.d.ts +1 -1
- package/dist/http/routes/memory-bundle.js +3 -1
- package/dist/http/routes/memory-compliance.d.ts +4 -4
- package/dist/http/routes/memory-origin.d.ts +4 -4
- package/dist/http/routes/memory-origin.js +3 -1
- package/dist/http/routes/memory-policy.js +6 -2
- package/dist/http/routes/retention-ops.d.ts +1 -1
- package/dist/http/routes/rules.d.ts +5 -2
- package/dist/http/routes/rules.js +84 -5
- package/dist/http/routes/run-memory-capture.d.ts +34 -0
- package/dist/http/routes/run-memory-capture.js +97 -0
- package/dist/http/routes/runs.d.ts +1 -0
- package/dist/http/routes/runs.js +212 -6
- package/dist/http/routes/session-sync.d.ts +4 -6
- package/dist/http/routes/session-sync.js +60 -47
- package/dist/http/routes/sessions.js +61 -188
- package/dist/http/routes/shared-memory.d.ts +1 -1
- package/dist/http/routes/tasks.js +1 -1
- package/dist/http/routes/workflows.js +19 -0
- package/dist/http/server.d.ts +35 -13
- package/dist/http/server.js +105 -12
- package/dist/http/verify-rounds.d.ts +1 -1
- package/dist/http/wire-types.d.ts +41 -8
- package/dist/leader/endpoint.d.ts +1 -1
- package/dist/leader/leader.d.ts +1 -1
- package/dist/leader/repair-oracle.d.ts +2 -2
- package/dist/leader/repair-oracle.js +1 -1
- package/dist/main.js +13 -6
- package/dist/memory-bundle-engine.d.ts +1 -1
- package/dist/memory-operator-faces.d.ts +5 -5
- package/dist/memory-origin-fence.d.ts +50 -0
- package/dist/memory-origin-fence.js +15 -0
- package/dist/memory-posture.d.ts +13 -1
- package/dist/memory-posture.js +1 -0
- package/dist/memory-scope.d.ts +6 -1
- package/dist/memory-scope.js +3 -2
- package/dist/model-select.d.ts +2 -2
- package/dist/observability/fail-open.d.ts +17 -5
- package/dist/observability/fail-open.js +17 -5
- package/dist/orchestration/hardened-vm-runner.d.ts +2 -2
- package/dist/orchestration/workflow-completion-inbox.d.ts +2 -2
- package/dist/plugins/background-shell-support.d.ts +2 -2
- package/dist/plugins/blob-backend.d.ts +7 -2
- package/dist/plugins/blob-backend.js +22 -18
- package/dist/plugins/caching-session-store.d.ts +7 -3
- package/dist/plugins/checkpoint-store-sql.d.ts +2 -2
- package/dist/plugins/file-history-store-sql.d.ts +170 -0
- package/dist/plugins/file-history-store-sql.js +827 -0
- package/dist/plugins/file-run-store.d.ts +44 -5
- package/dist/plugins/file-run-store.js +85 -13
- package/dist/plugins/fork-routing-session-store.d.ts +1 -1
- package/dist/plugins/fork-routing-session-store.js +2 -2
- package/dist/plugins/leader-run-store-sql.d.ts +1 -1
- package/dist/plugins/local-session-store.d.ts +9 -1
- package/dist/plugins/memory-embedder-fingerprint.d.ts +1 -1
- package/dist/plugins/memory-optout-grant-store-sql.d.ts +139 -0
- package/dist/plugins/memory-optout-grant-store-sql.js +157 -0
- package/dist/plugins/memory-origin-law.d.ts +2 -2
- package/dist/plugins/memory-run-store.d.ts +4 -3
- package/dist/plugins/memory-run-store.js +2 -1
- package/dist/plugins/permission-rule-store-file.d.ts +16 -6
- package/dist/plugins/permission-rule-store-file.js +77 -13
- package/dist/plugins/permission-rule-store-sql.d.ts +156 -8
- package/dist/plugins/permission-rule-store-sql.js +123 -21
- package/dist/plugins/pg-pool.js +59 -0
- package/dist/plugins/pg-safe-json.d.ts +1 -1
- package/dist/plugins/remote-env-device.d.ts +1 -1
- package/dist/plugins/remote-env-k8s.d.ts +1 -1
- package/dist/plugins/retention-lane-store-sql.d.ts +3 -3
- package/dist/plugins/retention-store-sql.d.ts +7 -7
- package/dist/plugins/retention-store-sql.js +6 -0
- package/dist/plugins/run-store-sql.d.ts +9 -3
- package/dist/plugins/run-store-sql.js +2 -2
- package/dist/plugins/send-user-file.d.ts +1 -1
- package/dist/plugins/session-placement.d.ts +1 -1
- package/dist/plugins/sql-errors.d.ts +1 -1
- package/dist/plugins/store-backend.d.ts +59 -29
- package/dist/plugins/store-backend.js +75 -15
- package/dist/plugins/store-contracts.d.ts +5 -3
- package/dist/plugins/task-list-store-sql.d.ts +1 -1
- package/dist/plugins/tidb-pool.js +59 -0
- package/dist/plugins/web-search.d.ts +1 -1
- package/dist/prompts-domain-validate.js +1 -1
- package/dist/resource-suspend.d.ts +1 -1
- package/dist/resource-window.d.ts +3 -3
- package/dist/rules-consent.d.ts +231 -9
- package/dist/rules-consent.js +411 -13
- package/dist/run-local.js +1 -0
- package/dist/runs.d.ts +15 -1
- package/dist/runs.js +19 -0
- package/dist/runtime-caps-resolver.d.ts +36 -1
- package/dist/runtime-caps-resolver.js +25 -0
- package/dist/runtime-governance.d.ts +1 -1
- package/dist/security.d.ts +5 -5
- package/dist/security.js +1 -1
- package/dist/session-sync.d.ts +52 -37
- package/dist/session-sync.js +19 -16
- package/dist/shared-memory-scope-authorizer.d.ts +1 -1
- package/dist/sighup-idle.d.ts +1 -1
- package/dist/spec-fields.d.ts +22 -1
- package/dist/spec-fields.js +11 -0
- package/dist/store-live-probe.d.ts +2 -2
- package/dist/task-a2a.d.ts +1 -1
- package/dist/task-cwd.d.ts +1 -1
- package/dist/task-mcp.d.ts +3 -3
- package/dist/task-settings.d.ts +3 -3
- package/dist/task-workflow.d.ts +1 -1
- package/dist/text-bidi.d.ts +2 -2
- package/dist/tool-approval.d.ts +191 -9
- package/dist/tool-approval.js +248 -29
- package/dist/trace/core-keyset-guard.d.ts +1 -1
- package/dist/trace/engine-notice-wire.d.ts +6 -4
- package/dist/trace/engine-notice-wire.js +2 -0
- package/dist/trace/ledger-events.d.ts +1 -1
- package/dist/trace/ledger-sink.d.ts +2 -2
- package/dist/trace/project.d.ts +3 -2
- package/dist/trace/project.js +1 -0
- package/dist/turn-activity.d.ts +1 -1
- package/dist/wall-clock-jump-guard.d.ts +3 -3
- package/package.json +2 -2
- package/dist/http/tar.d.ts +0 -17
- package/dist/http/tar.js +0 -53
- package/dist/http/workspace-content.d.ts +0 -11
- package/dist/http/workspace-content.js +0 -20
- package/dist/plugins/file-snapshot-store-sql.d.ts +0 -170
- package/dist/plugins/file-snapshot-store-sql.js +0 -213
package/dist/rules-consent.js
CHANGED
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { z } from "zod";
|
|
2
3
|
import { recordFailOpen } from "./observability/fail-open.js";
|
|
3
4
|
import { createLogger } from "./observability/logger.js";
|
|
4
5
|
import { MAX_CWD_CHARS } from "./task-cwd.js";
|
|
6
|
+
import { CONTROL_AND_BIDI_CHARS, hasBidiControls } from "./text-bidi.js";
|
|
5
7
|
import { confirmRuleApproval, parseAllowRuleText, precheckEditedRuleText, prepareCardApproval, prepareCcImport, redeemRuleBatch, redeemRuleTicket, removePersistedRule, } from "@sema-agent/core";
|
|
6
8
|
import { buildRulePayloadHash } from "./plugins/permission-rule-store-sql.js";
|
|
7
9
|
export const RULE_IMPORT_TICKET_TTL_MS = 10 * 60_000;
|
|
8
10
|
export const RULE_IMPORT_RETRY_AFTER_SEC = 2;
|
|
9
11
|
export const MAX_IMPORT_CANDIDATES = 200;
|
|
12
|
+
export const MAX_LOCAL_IMPORT_ROWS = MAX_IMPORT_CANDIDATES;
|
|
13
|
+
export const MAX_LOCAL_IMPORT_BYTES = 256 * 1024;
|
|
14
|
+
export const LOCAL_IMPORT_CLAIM_LEASE_MS = 60_000;
|
|
15
|
+
const LOCAL_IMPORT_CLAIM_WAIT_MS = RULE_IMPORT_RETRY_AFTER_SEC * 1000;
|
|
16
|
+
const LOCAL_IMPORT_CLAIM_POLL_MS = 25;
|
|
17
|
+
const LocalImportOutcomeSchema = z
|
|
18
|
+
.object({
|
|
19
|
+
members: z.array(z
|
|
20
|
+
.object({
|
|
21
|
+
key: z.object({ rule: z.string().min(1), scope: z.string().min(1) }).strict(),
|
|
22
|
+
outcome: z.enum(["persisted", "no-op", "refused", "indeterminate"]),
|
|
23
|
+
reason: z.enum(["store_refused", "walk_failed"]).optional(),
|
|
24
|
+
detail: z.string().optional(),
|
|
25
|
+
})
|
|
26
|
+
.strict()),
|
|
27
|
+
rev: z.number().int().nonnegative().safe(),
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
10
30
|
const precheckLogger = createLogger();
|
|
11
31
|
export function precheckCardRuleText(text, command) {
|
|
12
32
|
try {
|
|
@@ -21,7 +41,14 @@ export function precheckCardRuleText(text, command) {
|
|
|
21
41
|
}
|
|
22
42
|
}
|
|
23
43
|
export function serializeRuleScope(scope) {
|
|
24
|
-
|
|
44
|
+
switch (scope.kind) {
|
|
45
|
+
case "global":
|
|
46
|
+
return "global";
|
|
47
|
+
case "project":
|
|
48
|
+
return `project:${scope.root}`;
|
|
49
|
+
case "session":
|
|
50
|
+
throw new Error("serializeRuleScope got a session scope — session consent never lands on the durable rules wire (design/382 §4.3 two-member durable face); a session row reaching this serializer is a defect upstream, not a spelling to invent");
|
|
51
|
+
}
|
|
25
52
|
}
|
|
26
53
|
export const MAX_RULE_SCOPE_CHARS = "project:".length + MAX_CWD_CHARS;
|
|
27
54
|
export function parseRuleScope(text) {
|
|
@@ -122,11 +149,87 @@ function anchorBatchMembers(members, displayed) {
|
|
|
122
149
|
anchored.sort((a, b) => a.memberIndex - b.memberIndex);
|
|
123
150
|
return { kind: "batch", rules: anchored.map((a) => a.rule), members: anchored };
|
|
124
151
|
}
|
|
152
|
+
function screenLocalImportRow(row) {
|
|
153
|
+
const scope = parseRuleScope(row.scope);
|
|
154
|
+
if (scope === undefined) {
|
|
155
|
+
return { reason: "scope_unreadable", detail: "scope must be 'global' or 'project:<root>' with a non-empty root" };
|
|
156
|
+
}
|
|
157
|
+
if (scope.kind === "project" && hasBidiControls(scope.root)) {
|
|
158
|
+
return {
|
|
159
|
+
reason: "scope_bidi_controls",
|
|
160
|
+
detail: "this project root carries bidirectional control characters — the directory a reader sees is not the directory the matcher would use",
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (scope.kind === "project" && CONTROL_AND_BIDI_DETECT_RE.test(scope.root)) {
|
|
164
|
+
return {
|
|
165
|
+
reason: "scope_control_chars",
|
|
166
|
+
detail: "this project root carries control bytes (C0/DEL) — it is not a path, one SQL dialect refuses to store it at all, and it breaks the separator assumption this lane's comparison keys rest on",
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const pre = precheckCardRuleText(row.rule, row.command);
|
|
170
|
+
if (pre === undefined) {
|
|
171
|
+
return { reason: "rule_uncheckable", detail: "the rule text gate could not be asked about this row (its command is absent or not a string) — the row itself is malformed" };
|
|
172
|
+
}
|
|
173
|
+
if (!pre.ok)
|
|
174
|
+
return { reason: "rule_rejected", detail: pre.message };
|
|
175
|
+
const canonical = parseAllowRuleText(pre.canonicalRule);
|
|
176
|
+
if ("reject" in canonical) {
|
|
177
|
+
return { reason: "rule_rejected", detail: `the canonical spelling the engine minted does not re-parse: ${canonical.reject.message}` };
|
|
178
|
+
}
|
|
179
|
+
const derived = canonical.rule;
|
|
180
|
+
if (derived.tool !== row.tool || derived.match !== row.match || derived.command !== row.command) {
|
|
181
|
+
return {
|
|
182
|
+
reason: "row_inconsistent",
|
|
183
|
+
detail: `this row's tool/match/command are derived facts of its rule text, and they disagree: the rule reads as ` +
|
|
184
|
+
`${derived.tool}/${derived.match}/${JSON.stringify(derived.command)} but the row says ${String(row.tool)}/${String(row.match)}/${JSON.stringify(row.command)}`,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
return { rule: derived.rule, scope };
|
|
188
|
+
}
|
|
189
|
+
const LOCAL_IMPORT_LAYER_PATH = "local-import:rules";
|
|
190
|
+
const CONTROL_AND_BIDI_DETECT_RE = new RegExp(`[${CONTROL_AND_BIDI_CHARS}]`);
|
|
191
|
+
const LOCAL_IMPORT_KEY_SEP = "\u0000";
|
|
192
|
+
function buildLocalImportLayers(candidates) {
|
|
193
|
+
const byRoot = new Map();
|
|
194
|
+
for (const c of candidates) {
|
|
195
|
+
const key = c.scope.kind === "global" ? "" : c.scope.root;
|
|
196
|
+
const bucket = byRoot.get(key);
|
|
197
|
+
if (bucket === undefined)
|
|
198
|
+
byRoot.set(key, [c.rule]);
|
|
199
|
+
else
|
|
200
|
+
bucket.push(c.rule);
|
|
201
|
+
}
|
|
202
|
+
return [...byRoot].map(([root, rules]) => ({
|
|
203
|
+
layer: root === "" ? "userSettings" : "projectSettings",
|
|
204
|
+
path: root === "" ? LOCAL_IMPORT_LAYER_PATH : `${LOCAL_IMPORT_LAYER_PATH}:${root}`,
|
|
205
|
+
root: root === "" ? "/" : root,
|
|
206
|
+
readFile: async () => JSON.stringify({ permissions: { allow: rules } }),
|
|
207
|
+
}));
|
|
208
|
+
}
|
|
209
|
+
function buildLocalImportMembers(members) {
|
|
210
|
+
return members.map((m) => {
|
|
211
|
+
const key = { rule: m.rule, scope: serializeRuleScope(m.scope) };
|
|
212
|
+
switch (m.status) {
|
|
213
|
+
case "persisted":
|
|
214
|
+
return { key, outcome: "persisted" };
|
|
215
|
+
case "deduped":
|
|
216
|
+
return { key, outcome: "no-op" };
|
|
217
|
+
case "refused":
|
|
218
|
+
return { key, outcome: "indeterminate", reason: "store_refused", detail: m.reason };
|
|
219
|
+
default: {
|
|
220
|
+
const unknown = m;
|
|
221
|
+
throw new Error(`unknown redeemed-batch member status: ${JSON.stringify(unknown)}`);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
}
|
|
125
226
|
export function createRuleConsentLane(stores, opts) {
|
|
126
227
|
const deps = { provider: stores.provider, approvals: stores.approvals, cardEdits: true };
|
|
127
228
|
const ticketTtlMs = opts?.ticketTtlMs ?? RULE_IMPORT_TICKET_TTL_MS;
|
|
128
229
|
const newTicketId = opts?.newTicketId ?? (() => randomUUID());
|
|
129
230
|
const monotonic = opts?.monotonic ?? (() => performance.now());
|
|
231
|
+
const sleep = opts?.sleep ?? (async (ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
232
|
+
const localImportLogger = createLogger();
|
|
130
233
|
const anchors = new Map();
|
|
131
234
|
const ANCHOR_CAP = 4096;
|
|
132
235
|
const pruneAnchors = (nowMono) => {
|
|
@@ -150,15 +253,24 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
150
253
|
};
|
|
151
254
|
return {
|
|
152
255
|
async persistCardRule(input) {
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
256
|
+
let prepared;
|
|
257
|
+
try {
|
|
258
|
+
prepared = await prepareCardApproval({
|
|
259
|
+
principal: input.principal,
|
|
260
|
+
toolName: input.toolName,
|
|
261
|
+
command: input.command,
|
|
262
|
+
...(input.toolCallId !== undefined ? { toolCallId: input.toolCallId } : {}),
|
|
263
|
+
...(input.boundInputHash !== undefined ? { boundInputHash: input.boundInputHash } : {}),
|
|
264
|
+
...(input.scope !== undefined ? { scope: input.scope } : {}),
|
|
265
|
+
deps,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch (err) {
|
|
269
|
+
if (err.code === "config.missing_scope") {
|
|
270
|
+
return { ok: false, reason: "scope-unresolved", detail: err.message };
|
|
271
|
+
}
|
|
272
|
+
throw err;
|
|
273
|
+
}
|
|
162
274
|
if (prepared === undefined) {
|
|
163
275
|
return { ok: false, reason: "no-candidates", detail: "the rule lane cannot speak for this command (compound / redirection / unsupported tool)" };
|
|
164
276
|
}
|
|
@@ -249,19 +361,53 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
249
361
|
return { ok: false, reason: "too-many-candidates", candidates: preview.candidates.length };
|
|
250
362
|
}
|
|
251
363
|
const mintedAtMono = monotonic();
|
|
252
|
-
const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, candidates: preview.candidates, ttlMs: ticketTtlMs });
|
|
364
|
+
const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, purpose: "cc-import", candidates: preview.candidates, ttlMs: ticketTtlMs });
|
|
253
365
|
pruneAnchors(mintedAtMono);
|
|
254
366
|
anchors.set(minted.ticketId, { mintedAtWall: minted.expiresAtMs - ticketTtlMs, mintedAtMono });
|
|
255
367
|
return { ok: true, preview, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
|
|
256
368
|
},
|
|
257
369
|
async redeemImport(principal, ticket) {
|
|
258
370
|
const clock = decisionClock(ticket);
|
|
259
|
-
const
|
|
371
|
+
const consumeOutcomeAfterThrow = async (err) => {
|
|
372
|
+
localImportLogger.warn("cc_import_claim_write_failed", {
|
|
373
|
+
err: err instanceof Error ? err.message : String(err),
|
|
374
|
+
note: "the consume write failed with an ambiguous outcome (it may have committed) — classifying by the ticket's real state instead of surfacing a 500",
|
|
375
|
+
});
|
|
376
|
+
let snapshot;
|
|
377
|
+
try {
|
|
378
|
+
snapshot = await stores.tickets.peek(ticket, principal, "cc-import");
|
|
379
|
+
}
|
|
380
|
+
catch (peekErr) {
|
|
381
|
+
localImportLogger.warn("cc_import_ticket_state_unreadable", {
|
|
382
|
+
err: peekErr instanceof Error ? peekErr.message : String(peekErr),
|
|
383
|
+
note: "the ticket's state could not be read while classifying a failed consume — answering an explicit ambiguity (503) rather than declaring a terminal outcome or asserting an in-flight redemption",
|
|
384
|
+
});
|
|
385
|
+
return { ok: false, reason: "state-unreadable", detail: "the ticket's state could not be read while classifying a failed consume" };
|
|
386
|
+
}
|
|
387
|
+
if (snapshot === undefined)
|
|
388
|
+
return { ok: false, reason: "ticket-unusable", detail: "unknown" };
|
|
389
|
+
if (snapshot.state === "minted") {
|
|
390
|
+
return { ok: false, reason: "state-unreadable", detail: "the consume write did not report its outcome and may still land — the ticket's fate cannot be answered yet" };
|
|
391
|
+
}
|
|
392
|
+
if (snapshot.state === "settled") {
|
|
393
|
+
localImportLogger.warn("cc_import_ticket_unexpectedly_settled", {
|
|
394
|
+
note: "a cc-import ticket reads SETTLED — structurally unreachable (settle is only ever called on the local lane and purpose binding keeps the lanes apart); answering same-shape 404, loudly",
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
return { ok: false, reason: "ticket-unusable", detail: "claim outstanding after a failed consume write" };
|
|
398
|
+
};
|
|
399
|
+
let consumed;
|
|
400
|
+
try {
|
|
401
|
+
consumed = await stores.tickets.consume(ticket, principal, "cc-import", clock);
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
return await consumeOutcomeAfterThrow(err);
|
|
405
|
+
}
|
|
260
406
|
if (!consumed.ok)
|
|
261
407
|
return { ok: false, reason: "ticket-unusable", detail: consumed.reason };
|
|
262
408
|
const releaseClaim = async () => {
|
|
263
409
|
try {
|
|
264
|
-
return await stores.tickets.release(ticket, principal, RULE_IMPORT_RETRY_AFTER_SEC * 1000, decisionClock(ticket));
|
|
410
|
+
return await stores.tickets.release(ticket, principal, consumed.claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000, decisionClock(ticket));
|
|
265
411
|
}
|
|
266
412
|
catch (err) {
|
|
267
413
|
recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
|
|
@@ -304,6 +450,258 @@ export function createRuleConsentLane(stores, opts) {
|
|
|
304
450
|
return await indeterminate(err instanceof Error ? err.message : String(err));
|
|
305
451
|
}
|
|
306
452
|
},
|
|
453
|
+
async prepareLocalImport(principal, rows) {
|
|
454
|
+
if (rows.length > MAX_LOCAL_IMPORT_ROWS)
|
|
455
|
+
return { ok: false, reason: "too-many-rows", rows: rows.length };
|
|
456
|
+
const bytes = JSON.stringify(rows).length;
|
|
457
|
+
if (bytes > MAX_LOCAL_IMPORT_BYTES)
|
|
458
|
+
return { ok: false, reason: "batch-too-large", bytes };
|
|
459
|
+
const members = [];
|
|
460
|
+
const admitted = [];
|
|
461
|
+
const seen = new Set();
|
|
462
|
+
for (const row of rows) {
|
|
463
|
+
const screened = screenLocalImportRow(row);
|
|
464
|
+
if ("reason" in screened) {
|
|
465
|
+
members.push({ key: { rule: row.rule, scope: row.scope }, admitted: false, reason: screened.reason, detail: screened.detail });
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
const key = { rule: screened.rule, scope: serializeRuleScope(screened.scope) };
|
|
469
|
+
const dedupKey = `${key.scope}${LOCAL_IMPORT_KEY_SEP}${key.rule}`;
|
|
470
|
+
if (seen.has(dedupKey)) {
|
|
471
|
+
members.push({ key, admitted: false, reason: "duplicate_row", detail: "this (rule, scope) appears earlier in the same batch — the engine would fold the two silently, so it is reported here instead" });
|
|
472
|
+
continue;
|
|
473
|
+
}
|
|
474
|
+
seen.add(dedupKey);
|
|
475
|
+
members.push({ key, admitted: true });
|
|
476
|
+
admitted.push(screened);
|
|
477
|
+
}
|
|
478
|
+
if (admitted.length === 0)
|
|
479
|
+
return { ok: true, preview: { members } };
|
|
480
|
+
const { preview, approvalId } = await prepareCcImport({ layers: buildLocalImportLayers(admitted), principal, deps });
|
|
481
|
+
const engineKeys = new Set(preview.candidates.map((c) => `${serializeRuleScope(c.scope)}${LOCAL_IMPORT_KEY_SEP}${c.rule}`));
|
|
482
|
+
for (let i = 0; i < members.length; i++) {
|
|
483
|
+
const m = members[i];
|
|
484
|
+
if (m === undefined || !m.admitted)
|
|
485
|
+
continue;
|
|
486
|
+
if (engineKeys.has(`${m.key.scope}${LOCAL_IMPORT_KEY_SEP}${m.key.rule}`))
|
|
487
|
+
continue;
|
|
488
|
+
const why = preview.skipped.find((s) => s.rule === m.key.rule)?.reason ?? "the engine's import gate did not take it, and said nothing about it";
|
|
489
|
+
localImportLogger.warn("local_import_row_admitted_but_not_importable", {
|
|
490
|
+
rule: m.key.rule,
|
|
491
|
+
scope: m.key.scope,
|
|
492
|
+
reason: why,
|
|
493
|
+
note: "this repository's row gate and core's import gate disagree about this rule — the row is reported refused rather than silently dropped",
|
|
494
|
+
});
|
|
495
|
+
members[i] = { key: m.key, admitted: false, reason: "not_importable", detail: why };
|
|
496
|
+
}
|
|
497
|
+
if (approvalId === undefined)
|
|
498
|
+
return { ok: true, preview: { members } };
|
|
499
|
+
const mintedAtMono = monotonic();
|
|
500
|
+
const minted = await stores.tickets.mint({ ticketId: newTicketId(), principal, approvalId, purpose: "local-import", candidates: preview.candidates, ttlMs: ticketTtlMs });
|
|
501
|
+
pruneAnchors(mintedAtMono);
|
|
502
|
+
anchors.set(minted.ticketId, { mintedAtWall: minted.expiresAtMs - ticketTtlMs, mintedAtMono });
|
|
503
|
+
return { ok: true, preview: { members }, ticket: minted.ticketId, expiresAtMs: minted.expiresAtMs };
|
|
504
|
+
},
|
|
505
|
+
async redeemLocalImport(principal, ticket) {
|
|
506
|
+
const readSettled = (raw) => {
|
|
507
|
+
const parsed = LocalImportOutcomeSchema.safeParse(raw);
|
|
508
|
+
if (!parsed.success) {
|
|
509
|
+
throw new Error(`the settled outcome recorded on this import ticket does not carry a readable shape: ${parsed.error.message}`);
|
|
510
|
+
}
|
|
511
|
+
return parsed.data;
|
|
512
|
+
};
|
|
513
|
+
const tryPeek = async () => {
|
|
514
|
+
try {
|
|
515
|
+
return await stores.tickets.peek(ticket, principal, "local-import");
|
|
516
|
+
}
|
|
517
|
+
catch (err) {
|
|
518
|
+
localImportLogger.warn("local_import_ticket_state_unreadable", {
|
|
519
|
+
err: err instanceof Error ? err.message : String(err),
|
|
520
|
+
note: "the ticket's state could not be read while classifying a failure — answering in-flight (retry with the same ticket) rather than declaring a terminal outcome",
|
|
521
|
+
});
|
|
522
|
+
return "unreadable";
|
|
523
|
+
}
|
|
524
|
+
};
|
|
525
|
+
const unreadableTicketState = () => ({
|
|
526
|
+
ok: false,
|
|
527
|
+
reason: "in-flight",
|
|
528
|
+
detail: "the ticket's state could not be read — retry with the same ticket",
|
|
529
|
+
});
|
|
530
|
+
const driveAndSettle = async (approvalId, payloadHash, claimId) => {
|
|
531
|
+
const tryRelease = async () => {
|
|
532
|
+
try {
|
|
533
|
+
return await stores.tickets.release(ticket, principal, claimId, RULE_IMPORT_RETRY_AFTER_SEC * 1000, decisionClock(ticket));
|
|
534
|
+
}
|
|
535
|
+
catch (err) {
|
|
536
|
+
recordFailOpen("server.rules.ticket-claim-release-failed", err instanceof Error ? err.message : String(err));
|
|
537
|
+
return false;
|
|
538
|
+
}
|
|
539
|
+
};
|
|
540
|
+
const verdict = async (reason, detail) => {
|
|
541
|
+
await tryRelease();
|
|
542
|
+
return { ok: false, reason, detail };
|
|
543
|
+
};
|
|
544
|
+
const transient = async (detail) => {
|
|
545
|
+
const released = await tryRelease();
|
|
546
|
+
if (released)
|
|
547
|
+
return { ok: false, reason: "record-unusable", detail, retryable: true };
|
|
548
|
+
const snapshot = await tryPeek();
|
|
549
|
+
if (snapshot === "unreadable")
|
|
550
|
+
return unreadableTicketState();
|
|
551
|
+
if (snapshot === undefined)
|
|
552
|
+
return { ok: false, reason: "record-unusable", detail };
|
|
553
|
+
if (snapshot.state === "settled")
|
|
554
|
+
return { ok: true, outcome: readSettled(snapshot.outcome), replayed: true };
|
|
555
|
+
return { ok: false, reason: "in-flight", detail: `${detail} — the claim could not be handed back, but the ticket is still recoverable; retry with the same ticket` };
|
|
556
|
+
};
|
|
557
|
+
let members;
|
|
558
|
+
let rev;
|
|
559
|
+
try {
|
|
560
|
+
const record = await stores.approvals.get(approvalId);
|
|
561
|
+
if (record === undefined)
|
|
562
|
+
return await verdict("record-unusable", "the approval record behind this ticket is gone");
|
|
563
|
+
if ("staleSchema" in record) {
|
|
564
|
+
return await verdict("record-unusable", "this approval record predates the schema:2 form — re-run the import to mint a fresh record");
|
|
565
|
+
}
|
|
566
|
+
if (buildRulePayloadHash(record.candidates) !== payloadHash) {
|
|
567
|
+
return await verdict("payload-mismatch", "the approval record's candidates changed after the ticket was minted");
|
|
568
|
+
}
|
|
569
|
+
if (record.state === "pending") {
|
|
570
|
+
const confirmed = await confirmRuleApproval({ approvalId, principal, deps });
|
|
571
|
+
if (!confirmed.ok) {
|
|
572
|
+
if (confirmed.reason === "conflict")
|
|
573
|
+
return await transient(confirmed.reason);
|
|
574
|
+
return await verdict("record-unusable", confirmed.reason);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
const result = await redeemRuleBatch({ approvalId, principal, deps });
|
|
578
|
+
if ("status" in result) {
|
|
579
|
+
return await transient(result.reason);
|
|
580
|
+
}
|
|
581
|
+
members = buildLocalImportMembers(result.members);
|
|
582
|
+
rev = result.rev;
|
|
583
|
+
}
|
|
584
|
+
catch (err) {
|
|
585
|
+
return await transient(err instanceof Error ? err.message : String(err));
|
|
586
|
+
}
|
|
587
|
+
const outcome = { members, rev };
|
|
588
|
+
let settled;
|
|
589
|
+
try {
|
|
590
|
+
settled = await stores.tickets.settle(ticket, principal, claimId, outcome);
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
localImportLogger.warn("local_import_settle_failed", {
|
|
594
|
+
err: err instanceof Error ? err.message : String(err),
|
|
595
|
+
note: "the redemption ran but its outcome could not be recorded — answering in-flight so the owner retries and reads the authoritative state, never 500 (which reads as 'nothing happened')",
|
|
596
|
+
});
|
|
597
|
+
const afterThrow = await tryPeek();
|
|
598
|
+
if (afterThrow !== "unreadable" && afterThrow?.state === "settled") {
|
|
599
|
+
return { ok: true, outcome: readSettled(afterThrow.outcome), replayed: true };
|
|
600
|
+
}
|
|
601
|
+
return { ok: false, reason: "in-flight", detail: "the redemption ran but its outcome could not be recorded — retry with the same ticket to read the authoritative state" };
|
|
602
|
+
}
|
|
603
|
+
if (settled)
|
|
604
|
+
return { ok: true, outcome, replayed: false };
|
|
605
|
+
const snapshot = await tryPeek();
|
|
606
|
+
if (snapshot === "unreadable")
|
|
607
|
+
return unreadableTicketState();
|
|
608
|
+
if (snapshot?.state === "settled") {
|
|
609
|
+
return { ok: true, outcome: readSettled(snapshot.outcome), replayed: true };
|
|
610
|
+
}
|
|
611
|
+
if (snapshot !== undefined) {
|
|
612
|
+
localImportLogger.warn("local_import_claim_superseded", {
|
|
613
|
+
ticketState: snapshot.state,
|
|
614
|
+
note: "this claim was superseded (lease takeover) before it could settle — the local outcome is NOT authoritative and is deliberately not returned; the client retries and reads the canonical settled outcome",
|
|
615
|
+
});
|
|
616
|
+
return { ok: false, reason: "in-flight", detail: "this redemption's claim was superseded before it settled — retry with the same ticket to read the authoritative outcome" };
|
|
617
|
+
}
|
|
618
|
+
localImportLogger.warn("local_import_outcome_not_recorded", {
|
|
619
|
+
ticketState: "gone",
|
|
620
|
+
note: "the redemption ran and its result is being returned truthfully, but the ticket row is gone so it could not be recorded — a lost response cannot be replayed for this one",
|
|
621
|
+
});
|
|
622
|
+
return { ok: true, outcome, replayed: false };
|
|
623
|
+
};
|
|
624
|
+
const claimOutcomeAfterThrow = async (err, what) => {
|
|
625
|
+
localImportLogger.warn("local_import_claim_write_failed", {
|
|
626
|
+
op: what,
|
|
627
|
+
err: err instanceof Error ? err.message : String(err),
|
|
628
|
+
note: "a claim-changing write failed with an ambiguous outcome (it may have committed) — classifying by the ticket's real state instead of surfacing a 500",
|
|
629
|
+
});
|
|
630
|
+
const snapshot = await tryPeek();
|
|
631
|
+
if (snapshot === "unreadable")
|
|
632
|
+
return unreadableTicketState();
|
|
633
|
+
if (snapshot === undefined)
|
|
634
|
+
return { ok: false, reason: "ticket-unusable", detail: "unknown" };
|
|
635
|
+
if (snapshot.state === "settled")
|
|
636
|
+
return { ok: true, outcome: readSettled(snapshot.outcome), replayed: true };
|
|
637
|
+
return { ok: false, reason: "in-flight", detail: `the ${what} write did not report its outcome — retry with the same ticket` };
|
|
638
|
+
};
|
|
639
|
+
let consumed;
|
|
640
|
+
try {
|
|
641
|
+
consumed = await stores.tickets.consume(ticket, principal, "local-import", decisionClock(ticket));
|
|
642
|
+
}
|
|
643
|
+
catch (err) {
|
|
644
|
+
return await claimOutcomeAfterThrow(err, "consume");
|
|
645
|
+
}
|
|
646
|
+
if (consumed.ok)
|
|
647
|
+
return await driveAndSettle(consumed.approvalId, consumed.payloadHash, consumed.claimId);
|
|
648
|
+
if (consumed.reason !== "consumed")
|
|
649
|
+
return { ok: false, reason: "ticket-unusable", detail: consumed.reason };
|
|
650
|
+
const deadline = monotonic() + LOCAL_IMPORT_CLAIM_WAIT_MS;
|
|
651
|
+
for (;;) {
|
|
652
|
+
const snapshot = await tryPeek();
|
|
653
|
+
if (snapshot === "unreadable")
|
|
654
|
+
return unreadableTicketState();
|
|
655
|
+
if (snapshot === undefined)
|
|
656
|
+
return { ok: false, reason: "ticket-unusable", detail: "unknown" };
|
|
657
|
+
switch (snapshot.state) {
|
|
658
|
+
case "settled":
|
|
659
|
+
return { ok: true, outcome: readSettled(snapshot.outcome), replayed: true };
|
|
660
|
+
case "minted": {
|
|
661
|
+
let again;
|
|
662
|
+
try {
|
|
663
|
+
again = await stores.tickets.consume(ticket, principal, "local-import", decisionClock(ticket));
|
|
664
|
+
}
|
|
665
|
+
catch (err) {
|
|
666
|
+
return await claimOutcomeAfterThrow(err, "consume");
|
|
667
|
+
}
|
|
668
|
+
if (again.ok)
|
|
669
|
+
return await driveAndSettle(again.approvalId, again.payloadHash, again.claimId);
|
|
670
|
+
if (again.reason !== "consumed")
|
|
671
|
+
return { ok: false, reason: "ticket-unusable", detail: again.reason };
|
|
672
|
+
break;
|
|
673
|
+
}
|
|
674
|
+
case "claimed": {
|
|
675
|
+
if (snapshot.claimAgeMs >= LOCAL_IMPORT_CLAIM_LEASE_MS) {
|
|
676
|
+
let seized;
|
|
677
|
+
try {
|
|
678
|
+
seized = await stores.tickets.reclaim(ticket, principal, "local-import", LOCAL_IMPORT_CLAIM_LEASE_MS);
|
|
679
|
+
}
|
|
680
|
+
catch (err) {
|
|
681
|
+
return await claimOutcomeAfterThrow(err, "reclaim");
|
|
682
|
+
}
|
|
683
|
+
if (seized.ok) {
|
|
684
|
+
localImportLogger.warn("local_import_stale_claim_reclaimed", {
|
|
685
|
+
claimAgeMs: snapshot.claimAgeMs,
|
|
686
|
+
leaseMs: LOCAL_IMPORT_CLAIM_LEASE_MS,
|
|
687
|
+
note: "a claim outlived its lease (the replica holding it is presumed gone) — re-driving the batch, which is member-level idempotent",
|
|
688
|
+
});
|
|
689
|
+
return await driveAndSettle(seized.approvalId, seized.payloadHash, seized.claimId);
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
default: {
|
|
695
|
+
const unknown = snapshot;
|
|
696
|
+
throw new Error(`unknown import-ticket state: ${JSON.stringify(unknown)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
if (monotonic() >= deadline) {
|
|
700
|
+
return { ok: false, reason: "in-flight", detail: "another redemption of this ticket is still in flight" };
|
|
701
|
+
}
|
|
702
|
+
await sleep(LOCAL_IMPORT_CLAIM_POLL_MS);
|
|
703
|
+
}
|
|
704
|
+
},
|
|
307
705
|
async listRules(bucket) {
|
|
308
706
|
const stored = await storeForBucket(stores.provider, bucket).list();
|
|
309
707
|
const rows = stored.rules.map((r) => ({ rule: r.rule, scope: serializeRuleScope(r.scope), tool: r.tool, match: r.match, command: r.command, adds: r.adds }));
|
package/dist/run-local.js
CHANGED
|
@@ -245,6 +245,7 @@ export async function runLocal(argv, deps = {}) {
|
|
|
245
245
|
try {
|
|
246
246
|
fileBackend = new FileStorageBackend({
|
|
247
247
|
root,
|
|
248
|
+
...(config.fileHistory !== undefined ? { fileHistory: config.fileHistory } : {}),
|
|
248
249
|
onCorruptRead: (info) => logger.warn("file_store_corrupt_read", {
|
|
249
250
|
path: info.path,
|
|
250
251
|
reason: info.reason,
|
package/dist/runs.d.ts
CHANGED
|
@@ -122,6 +122,20 @@ export declare function isSessionConflictResult(result: {
|
|
|
122
122
|
* the directory is unreachable / no resolver is wired → 503 (retry later; the sync leg forwards the
|
|
123
123
|
* result's `retryAfterMs` as body `retryAfterSec`, same family shape as usage.window_exhausted). Exact
|
|
124
124
|
* codes only, no memory.* family grab — an unknown sibling stays a 200-with-failed-body until cataloged.
|
|
125
|
+
*
|
|
126
|
+
* design/383 (core 7.0.0 片2/3, S-49 S-3): the session capture opt-out's four prepare-throw refusals ride the
|
|
127
|
+
* same pipe. `memory.capture_optout_denied` is TERMINAL (the principal's `allowMemoryOptOut` verdict is an
|
|
128
|
+
* explicit `false`, or the posture is `capture-required` / `governed`-with-absent-verdict — a retry cannot
|
|
129
|
+
* change it) → 403, the `memory.admission_denied` twin. `memory.capture_optout_unpersisted` is the
|
|
130
|
+
* declaration arm's "control-plane record could not be written" refusal — a store outage, retry may succeed
|
|
131
|
+
* → 503 (transient fail-closed, the `memory.admission_required` twin; no retryAfterMs on this one — core
|
|
132
|
+
* mints none, so the sync leg forwards no hint rather than inventing one). `config.memory_capture_spelling`
|
|
133
|
+
* is a TaskSpec authoring mistake (the only legal spelling is the string "off") → 400, the
|
|
134
|
+
* `config.memory_erasure_request` family shape. `config.memory_capture_unsupported` is the DEPLOYMENT SHAPE
|
|
135
|
+
* refusing the declaration (remote/per-run execution with no durable capture-record carrier: the record
|
|
136
|
+
* would not survive to a resume replica) → 409 — the request is well-formed, the deployment's state
|
|
137
|
+
* conflicts with it, the `config.refresh_unavailable` family shape ("change the deployment, not the request").
|
|
138
|
+
* Status choices are pinned by the `config.`/`memory.` family status sets in error-code-catalog-live.test.ts.
|
|
125
139
|
*/
|
|
126
140
|
export declare function resumeAtHttpStatus(result: {
|
|
127
141
|
status: string;
|
|
@@ -249,7 +263,7 @@ cascade?: CascadeConfig, instrumentDegenerate?: (result: TaskResult) => void, pl
|
|
|
249
263
|
record: (scope: string | undefined, objective: string, completed: boolean) => void;
|
|
250
264
|
}, persistThinking?: boolean, inflight?: Map<string, AbortController>, preemptable?: Map<string, AbortController>, steerable?: Map<string, TaskStream>, modelUsage?: ModelUsageTracker, elicitation?: ElicitationCoordinator,
|
|
251
265
|
/** E23: the VERIFIED principal (gatedPrincipal) that may answer this run's elicitations — matches the respond
|
|
252
|
-
* owner-gate's identity source (NOT the cost-attribution `principal`, which on a direct door is the
|
|
266
|
+
* owner-gate's identity source (NOT the cost-attribution `principal`, which on a direct door is the self-asserted
|
|
253
267
|
* header → can be null while a JWT principal exists). Falls back to the run owner when not supplied. */
|
|
254
268
|
elicitOwner?: string | null,
|
|
255
269
|
/** E18 resume-at: capture the turn's (firstTextEventId → leaf entryId) anchor at each turn_end. Built by the caller
|
package/dist/runs.js
CHANGED
|
@@ -73,6 +73,14 @@ export function resumeAtHttpStatus(result) {
|
|
|
73
73
|
return 403;
|
|
74
74
|
if (result.errorCode === "memory.admission_required")
|
|
75
75
|
return 503;
|
|
76
|
+
if (result.errorCode === "memory.capture_optout_denied")
|
|
77
|
+
return 403;
|
|
78
|
+
if (result.errorCode === "memory.capture_optout_unpersisted")
|
|
79
|
+
return 503;
|
|
80
|
+
if (result.errorCode === "config.memory_capture_spelling")
|
|
81
|
+
return 400;
|
|
82
|
+
if (result.errorCode === "config.memory_capture_unsupported")
|
|
83
|
+
return 409;
|
|
76
84
|
if (!(result.errorCode?.startsWith("resume_at.") ?? false))
|
|
77
85
|
return undefined;
|
|
78
86
|
return result.errorCode === "resume_at.not_found" ? 404 : 422;
|
|
@@ -176,6 +184,17 @@ export async function runInBackground(runner, spec, runStore, taskId, metrics, p
|
|
|
176
184
|
nowMonotonicMs: performance.now(),
|
|
177
185
|
});
|
|
178
186
|
const legDeadlineMonotonic = approvalLeg.legDeadlineMonotonic;
|
|
187
|
+
if (approval) {
|
|
188
|
+
const coordinator = approval.coordinator;
|
|
189
|
+
spec.onAsk = approvalLeg.windowZero
|
|
190
|
+
? () => Promise.resolve(coordinator.unattendedAskOutcome())
|
|
191
|
+
: coordinator.boundAsk({
|
|
192
|
+
owner: elicitOwner ?? owner,
|
|
193
|
+
taskId,
|
|
194
|
+
...(spec.sessionId ? { sessionId: spec.sessionId } : {}),
|
|
195
|
+
...(approvalLeg.active ? { legKey: "", ...(legDeadlineMonotonic !== undefined ? { legDeadlineMonotonic } : {}) } : {}),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
179
198
|
const heartbeat = setInterval(() => {
|
|
180
199
|
void runStore.heartbeat(taskId, owner).catch(() => { metrics?.inc("run_signal_poll_errors_total", { kind: "heartbeat" }); });
|
|
181
200
|
if (!cancelCtrl.signal.aborted) {
|
|
@@ -66,7 +66,7 @@ export interface EntitlementsResolverOpts {
|
|
|
66
66
|
* `allowObservers`(observer 开闸线,core 1.270.0/1.273.0):**极性与 allow* 相反** —
|
|
67
67
|
* core 是 `=== true` 显式 opt-in(默认 OFF,undefined ships dark)。registry-core 0.10.3 起该键已收编进
|
|
68
68
|
* `EntitlementRuntimeCaps` 类型(此前 0.10.2 走 unknown 宽读透传,现为类型直读,行为零变)。
|
|
69
|
-
* **缺键=不写键**(绝不把缺键补成显式 false —— core 是唯一默认源,补 false
|
|
69
|
+
* **缺键=不写键**(绝不把缺键补成显式 false —— core 是唯一默认源,补 false 语义上等价但会自造「center
|
|
70
70
|
* 说过话」的显式判决,污染 env-baseline 合成序,见 applyObserverEnvOptIn)。非 boolean 虽然骗不过 core
|
|
71
71
|
* 的 `=== true`(不会误 GRANT),但 PRESENT-非-boolean 与 allow* 同属同一 body 的契约破损信号 → 同款 THROW。
|
|
72
72
|
*
|
|
@@ -135,6 +135,41 @@ export declare function createPrincipalEntitlementsClient(opts: EntitlementsReso
|
|
|
135
135
|
export declare function createEntitlementsResolver(opts: EntitlementsResolverOpts): (principal: string | undefined) => Promise<RuntimeCaps | undefined>;
|
|
136
136
|
/** core's `RunnerDeps.runtimeCapsResolver` seam signature (types.d.ts): sync or async, undefined = no caps. */
|
|
137
137
|
export type EntitlementsResolverFn = (principal: string | undefined) => RuntimeCaps | undefined | Promise<RuntimeCaps | undefined>;
|
|
138
|
+
/**
|
|
139
|
+
* design/383 §3.1(S-49 S-1 / S-50 §1.2)—— `RuntimeCaps.allowMemoryOptOut` 的 **verdict 源**(本地腿)。
|
|
140
|
+
* 唯一实现 = `plugins/memory-optout-grant-store-sql.ts`(三层折叠:per-principal 行 → 部署缺省哨兵行 →
|
|
141
|
+
* 代码缺省 allow,所以健康的源**恒答显式 boolean**;`principal` 缺席=匿名/系统 run,只看缺省行)。
|
|
142
|
+
* 契约:答非 boolean 或 throw = **面故障**(见 {@link applyMemoryOptOutGrant} 的处置)。
|
|
143
|
+
*/
|
|
144
|
+
export interface MemoryOptOutVerdictSource {
|
|
145
|
+
resolveAllow(principal: string | undefined): Promise<boolean>;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* design/383 §3.1 **F2 例外臂**(server 阻断级,采;core 7.0.0 `RuntimeCaps.allowMemoryOptOut` JSDoc 的
|
|
149
|
+
* FAULT-TRANSIT CONTRACT 逐字):把本地 grant 腿的 verdict 合成进 caps resolver —— **与 allowWorkflows 家族
|
|
150
|
+
* 反向的例外臂,逐字注释**:
|
|
151
|
+
*
|
|
152
|
+
* · 本文件 `load()` 的 catch 臂对 center 硬失败铸 `{allowWorkflows:false, allowFork:false}`(fail-closed
|
|
153
|
+
* 降级:放大面上「故障 = 拒」是安全方向)。**本键禁止照抄**:隐私轴上「拒」= 把用户明说不要记的会话
|
|
154
|
+
* 记进去(不可逆),而「错误放行」只是少记一次会话(可恢复)。core 因此把 `open` 姿态的故障臂定成
|
|
155
|
+
* **放行 + 具名披露**,把 `governed` 姿态的故障臂定成**拒跑**;两臂都以「故障」为输入。把故障编成
|
|
156
|
+
* 显式 `false` = 把「故障」伪装成「显式 denied verdict」,遮蔽这两条分臂 —— 在 `open` 下恰是
|
|
157
|
+
* 极性反转(故障走了「显式拒」臂)。
|
|
158
|
+
* · 故障过境形写死:本键故障 = **键缺席**(对象上不写键;core 读作 `value:undefined`)。不选 throw
|
|
159
|
+
* 透传是因为 throw 会把**同一次**解析里 center 已答好的 allowWorkflows/allowFork 一并作废(core 的
|
|
160
|
+
* resolver-throw 降级臂),那是把一条腿的故障扩成整车的故障。缺席的代价如实记:`open` 下 core 读
|
|
161
|
+
* 「clean undefined」不铸 `faultDisclosure`(那句披露只给 throw/非 boolean),所以**本层**用 `onFault`
|
|
162
|
+
* 打一行 warn 补上披露;`governed` 下缺席 = verdict absent = 拒跑,方向正确。
|
|
163
|
+
* · 源答非 boolean 同判故障(契约:never truthiness-read)。
|
|
164
|
+
* · 无源 ⇒ 原 resolver **同一引用**透传(零行为差;多数部署路径)。
|
|
165
|
+
* · `principal` 缺席也问源:部署缺省行那一层对匿名 run 同样成立(governed 部署的匿名 run 也该被
|
|
166
|
+
* 一个显式 verdict 罩住,而不是因为「没 principal」结构性拿不到 verdict 而被拒跑)。
|
|
167
|
+
*
|
|
168
|
+
* 双向 G 钉在 `test/memory-capture-optout-lane.test.ts` ②:故障 ⇒ 键缺席;显式 false ⇒ 键=false。
|
|
169
|
+
*/
|
|
170
|
+
export declare function applyMemoryOptOutGrant(base: EntitlementsResolverFn | undefined, source: MemoryOptOutVerdictSource | undefined, opts: {
|
|
171
|
+
onFault: (err: Error, principal: string | undefined) => void;
|
|
172
|
+
}): EntitlementsResolverFn | undefined;
|
|
138
173
|
/**
|
|
139
174
|
* observer 开闸线的 env 半场:`EXPERIMENTAL_OBSERVER_AGENTS=true` 时把
|
|
140
175
|
* `RuntimeCaps.allowObservers: true` 作为**部署基线**合成进 caps resolver(core 1.270.0/1.273.0 的
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { fetchPrincipalCaps } from "./config-center/facade.js";
|
|
2
2
|
import { HttpError } from "./security.js";
|
|
3
3
|
import { createLogger } from "./observability/logger.js";
|
|
4
|
+
import { recordFailOpen } from "./observability/fail-open.js";
|
|
4
5
|
const defaultLogger = createLogger();
|
|
5
6
|
const executionLaneDefaultTrace = new Map();
|
|
6
7
|
export function resetExecutionLaneDefaultTraceForTest() {
|
|
@@ -156,6 +157,30 @@ export function createPrincipalEntitlementsClient(opts) {
|
|
|
156
157
|
export function createEntitlementsResolver(opts) {
|
|
157
158
|
return createPrincipalEntitlementsClient(opts).resolveRuntimeCaps;
|
|
158
159
|
}
|
|
160
|
+
export function applyMemoryOptOutGrant(base, source, opts) {
|
|
161
|
+
if (source === undefined)
|
|
162
|
+
return base;
|
|
163
|
+
return async (principal) => {
|
|
164
|
+
const caps = base !== undefined ? await base(principal) : undefined;
|
|
165
|
+
let allow;
|
|
166
|
+
try {
|
|
167
|
+
const v = await source.resolveAllow(principal);
|
|
168
|
+
if (typeof v !== "boolean")
|
|
169
|
+
throw new Error(`memory opt-out grant source answered a non-boolean (${typeof v}) — treated as a fault, never truthiness-read`);
|
|
170
|
+
allow = v;
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
try {
|
|
174
|
+
opts.onFault(err instanceof Error ? err : new Error(String(err)), principal);
|
|
175
|
+
}
|
|
176
|
+
catch (hookErr) {
|
|
177
|
+
recordFailOpen("server.memory-optout-grant.onfault-hook-threw", String(hookErr));
|
|
178
|
+
}
|
|
179
|
+
return caps;
|
|
180
|
+
}
|
|
181
|
+
return { ...(caps ?? {}), allowMemoryOptOut: allow };
|
|
182
|
+
};
|
|
183
|
+
}
|
|
159
184
|
export function applyObserverEnvOptIn(centerResolver, opts) {
|
|
160
185
|
if (!opts.experimentalObserverAgents)
|
|
161
186
|
return centerResolver;
|