@synapsor/runner 1.6.2 → 1.6.3
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 +77 -2
- package/README.md +33 -19
- package/SECURITY.md +10 -2
- package/THREAT_MODEL.md +27 -4
- package/dist/authoring.mjs +53 -3
- package/dist/cli.d.ts +19 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/local-ui.d.ts +85 -1
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +36107 -22949
- package/dist/runtime.mjs +3581 -237
- package/dist/shadow.mjs +2423 -65
- package/docs/README.md +22 -2
- package/docs/agent-guided-setup.md +239 -0
- package/docs/approval-roles-and-operator-identity.md +353 -0
- package/docs/auto-boundary-and-scoped-explore.md +32 -6
- package/docs/capability-authoring.md +19 -0
- package/docs/current-scope.md +20 -1
- package/docs/dsl-json-parity.md +75 -0
- package/docs/dsl-reference.md +10 -0
- package/docs/fresh-developer-usability.md +155 -80
- package/docs/getting-started-own-database.md +5 -4
- package/docs/guarded-crud-writeback.md +19 -0
- package/docs/guided-onboarding.md +331 -0
- package/docs/human-attention-notifications.md +367 -0
- package/docs/limitations.md +19 -3
- package/docs/local-mode.md +16 -2
- package/docs/migrating-to-synapsor-spec.md +38 -0
- package/docs/production.md +39 -0
- package/docs/release-notes.md +66 -2
- package/docs/runner-config-reference.md +132 -0
- package/docs/running-a-runner-fleet.md +16 -1
- package/docs/security-boundary.md +24 -0
- package/docs/store-lifecycle.md +20 -1
- package/docs/supervised-automatic-apply.md +267 -0
- package/docs/troubleshooting-first-run.md +35 -0
- package/examples/fitflow-guided-onboarding/README.md +43 -0
- package/examples/fitflow-guided-onboarding/app/page.tsx +8 -0
- package/examples/fitflow-guided-onboarding/docker-compose.yml +16 -0
- package/examples/fitflow-guided-onboarding/package.json +18 -0
- package/examples/fitflow-guided-onboarding/prisma/schema.prisma +98 -0
- package/examples/fitflow-guided-onboarding/seed/postgres.sql +401 -0
- package/examples/operator-oidc/issuer.mjs +188 -0
- package/examples/support-plan-credit/README.md +6 -2
- package/package.json +5 -3
- package/schemas/schema-candidate-review.schema.json +42 -0
- package/schemas/synapsor.runner.schema.json +227 -4
package/dist/shadow.mjs
CHANGED
|
@@ -1469,6 +1469,9 @@ var ProposalStore = class {
|
|
|
1469
1469
|
shadow_study_cases: this.countTable("shadow_study_cases"),
|
|
1470
1470
|
shadow_outcomes: this.countTable("shadow_outcomes"),
|
|
1471
1471
|
worker_queue: this.countTable("worker_queue"),
|
|
1472
|
+
attention_events: this.countTable("attention_events"),
|
|
1473
|
+
attention_items: this.countTable("attention_items"),
|
|
1474
|
+
notification_deliveries: this.countTable("notification_deliveries"),
|
|
1472
1475
|
policy_recommendations: this.countTable("policy_recommendations"),
|
|
1473
1476
|
page_count: pageCount,
|
|
1474
1477
|
page_size: pageSize,
|
|
@@ -1808,15 +1811,95 @@ var ProposalStore = class {
|
|
|
1808
1811
|
updated_at TEXT NOT NULL
|
|
1809
1812
|
);
|
|
1810
1813
|
|
|
1814
|
+
CREATE TABLE IF NOT EXISTS attention_events (
|
|
1815
|
+
event_id TEXT PRIMARY KEY,
|
|
1816
|
+
schema_version TEXT NOT NULL,
|
|
1817
|
+
event_type TEXT NOT NULL,
|
|
1818
|
+
severity TEXT NOT NULL,
|
|
1819
|
+
occurred_at TEXT NOT NULL,
|
|
1820
|
+
environment TEXT NOT NULL,
|
|
1821
|
+
proposal_id TEXT,
|
|
1822
|
+
job_id TEXT,
|
|
1823
|
+
operation_id TEXT,
|
|
1824
|
+
correlation_id TEXT,
|
|
1825
|
+
capability TEXT,
|
|
1826
|
+
contract_digest TEXT,
|
|
1827
|
+
attention_key TEXT,
|
|
1828
|
+
attention_required INTEGER NOT NULL,
|
|
1829
|
+
immediate_default INTEGER NOT NULL,
|
|
1830
|
+
summary TEXT NOT NULL,
|
|
1831
|
+
approval_source TEXT,
|
|
1832
|
+
worker_state TEXT,
|
|
1833
|
+
failure_class TEXT,
|
|
1834
|
+
expires_at TEXT,
|
|
1835
|
+
workbench_path TEXT,
|
|
1836
|
+
details_json TEXT NOT NULL,
|
|
1837
|
+
payload_hash TEXT NOT NULL,
|
|
1838
|
+
created_at TEXT NOT NULL
|
|
1839
|
+
);
|
|
1840
|
+
|
|
1841
|
+
CREATE TABLE IF NOT EXISTS attention_items (
|
|
1842
|
+
attention_id TEXT PRIMARY KEY,
|
|
1843
|
+
attention_key TEXT NOT NULL UNIQUE,
|
|
1844
|
+
status TEXT NOT NULL,
|
|
1845
|
+
severity TEXT NOT NULL,
|
|
1846
|
+
environment TEXT NOT NULL,
|
|
1847
|
+
event_type TEXT NOT NULL,
|
|
1848
|
+
capability TEXT,
|
|
1849
|
+
contract_digest TEXT,
|
|
1850
|
+
title TEXT NOT NULL,
|
|
1851
|
+
occurrence_count INTEGER NOT NULL,
|
|
1852
|
+
first_event_id TEXT NOT NULL,
|
|
1853
|
+
latest_event_id TEXT NOT NULL,
|
|
1854
|
+
first_seen_at TEXT NOT NULL,
|
|
1855
|
+
last_seen_at TEXT NOT NULL,
|
|
1856
|
+
acknowledged_by TEXT,
|
|
1857
|
+
acknowledged_at TEXT,
|
|
1858
|
+
acknowledgement_identity_json TEXT,
|
|
1859
|
+
acknowledgement_decision_hash TEXT,
|
|
1860
|
+
acknowledgement_signature TEXT,
|
|
1861
|
+
acknowledgement_integrity_hash TEXT,
|
|
1862
|
+
resolved_at TEXT,
|
|
1863
|
+
expires_at TEXT,
|
|
1864
|
+
FOREIGN KEY (first_event_id) REFERENCES attention_events(event_id),
|
|
1865
|
+
FOREIGN KEY (latest_event_id) REFERENCES attention_events(event_id)
|
|
1866
|
+
);
|
|
1867
|
+
|
|
1868
|
+
CREATE TABLE IF NOT EXISTS notification_deliveries (
|
|
1869
|
+
delivery_id TEXT PRIMARY KEY,
|
|
1870
|
+
sink_id TEXT NOT NULL,
|
|
1871
|
+
event_id TEXT NOT NULL,
|
|
1872
|
+
attention_id TEXT,
|
|
1873
|
+
status TEXT NOT NULL,
|
|
1874
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
1875
|
+
max_attempts INTEGER NOT NULL,
|
|
1876
|
+
next_attempt_at TEXT NOT NULL,
|
|
1877
|
+
lease_owner TEXT,
|
|
1878
|
+
lease_id TEXT,
|
|
1879
|
+
lease_expires_at TEXT,
|
|
1880
|
+
last_error_code TEXT,
|
|
1881
|
+
external_reference TEXT,
|
|
1882
|
+
delivered_at TEXT,
|
|
1883
|
+
created_at TEXT NOT NULL,
|
|
1884
|
+
updated_at TEXT NOT NULL,
|
|
1885
|
+
UNIQUE(sink_id, event_id),
|
|
1886
|
+
FOREIGN KEY (event_id) REFERENCES attention_events(event_id),
|
|
1887
|
+
FOREIGN KEY (attention_id) REFERENCES attention_items(attention_id)
|
|
1888
|
+
);
|
|
1889
|
+
|
|
1811
1890
|
CREATE TABLE IF NOT EXISTS worker_queue (
|
|
1812
1891
|
proposal_id TEXT PRIMARY KEY,
|
|
1813
1892
|
status TEXT NOT NULL,
|
|
1893
|
+
execution_mode TEXT DEFAULT 'legacy',
|
|
1894
|
+
contract_digest TEXT,
|
|
1814
1895
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
1815
1896
|
max_attempts INTEGER NOT NULL,
|
|
1816
1897
|
next_attempt_at TEXT NOT NULL,
|
|
1817
1898
|
lease_owner TEXT,
|
|
1899
|
+
lease_id TEXT,
|
|
1818
1900
|
lease_expires_at TEXT,
|
|
1819
1901
|
last_error_code TEXT,
|
|
1902
|
+
terminal_outcome TEXT,
|
|
1820
1903
|
created_at TEXT NOT NULL,
|
|
1821
1904
|
updated_at TEXT NOT NULL,
|
|
1822
1905
|
FOREIGN KEY (proposal_id) REFERENCES proposals(proposal_id)
|
|
@@ -1838,6 +1921,12 @@ var ProposalStore = class {
|
|
|
1838
1921
|
CREATE INDEX IF NOT EXISTS idx_cloud_outbox_due ON cloud_outbox(status, next_attempt_at, sequence, created_at);
|
|
1839
1922
|
CREATE INDEX IF NOT EXISTS idx_cloud_outbox_proposal ON cloud_outbox(proposal_id, sequence, created_at);
|
|
1840
1923
|
CREATE INDEX IF NOT EXISTS idx_cloud_governance_proposal ON cloud_governance_events(proposal_id, created_at);
|
|
1924
|
+
CREATE INDEX IF NOT EXISTS idx_attention_events_type_time ON attention_events(event_type, occurred_at, event_id);
|
|
1925
|
+
CREATE INDEX IF NOT EXISTS idx_attention_events_proposal ON attention_events(proposal_id, occurred_at, event_id);
|
|
1926
|
+
CREATE INDEX IF NOT EXISTS idx_attention_events_capability ON attention_events(capability, occurred_at, event_id);
|
|
1927
|
+
CREATE INDEX IF NOT EXISTS idx_attention_items_status ON attention_items(status, severity, last_seen_at, attention_id);
|
|
1928
|
+
CREATE INDEX IF NOT EXISTS idx_notification_deliveries_due ON notification_deliveries(status, next_attempt_at, created_at, delivery_id);
|
|
1929
|
+
CREATE INDEX IF NOT EXISTS idx_notification_deliveries_sink ON notification_deliveries(sink_id, status, updated_at, delivery_id);
|
|
1841
1930
|
|
|
1842
1931
|
INSERT OR IGNORE INTO proposal_store_schema(version, applied_at)
|
|
1843
1932
|
VALUES (1, datetime('now'));
|
|
@@ -1869,6 +1958,14 @@ var ProposalStore = class {
|
|
|
1869
1958
|
this.ensureColumn("approvals", "signature", "TEXT");
|
|
1870
1959
|
this.ensureColumn("approvals", "integrity_hash", "TEXT");
|
|
1871
1960
|
this.ensureColumn("approvals", "freshness_proof_digest", "TEXT");
|
|
1961
|
+
this.ensureColumn("attention_items", "acknowledgement_identity_json", "TEXT");
|
|
1962
|
+
this.ensureColumn("attention_items", "acknowledgement_decision_hash", "TEXT");
|
|
1963
|
+
this.ensureColumn("attention_items", "acknowledgement_signature", "TEXT");
|
|
1964
|
+
this.ensureColumn("attention_items", "acknowledgement_integrity_hash", "TEXT");
|
|
1965
|
+
this.ensureColumn("worker_queue", "execution_mode", "TEXT DEFAULT 'legacy'");
|
|
1966
|
+
this.ensureColumn("worker_queue", "contract_digest", "TEXT");
|
|
1967
|
+
this.ensureColumn("worker_queue", "lease_id", "TEXT");
|
|
1968
|
+
this.ensureColumn("worker_queue", "terminal_outcome", "TEXT");
|
|
1872
1969
|
}
|
|
1873
1970
|
ensureSearchIndexes() {
|
|
1874
1971
|
this.db.exec(`
|
|
@@ -1912,6 +2009,7 @@ var ProposalStore = class {
|
|
|
1912
2009
|
|
|
1913
2010
|
CREATE INDEX IF NOT EXISTS idx_proposal_events_kind_created ON proposal_events(kind, created_at);
|
|
1914
2011
|
CREATE INDEX IF NOT EXISTS idx_worker_queue_claim ON worker_queue(status, next_attempt_at, lease_expires_at, created_at);
|
|
2012
|
+
CREATE INDEX IF NOT EXISTS idx_worker_queue_supervised_claim ON worker_queue(execution_mode, contract_digest, status, next_attempt_at, lease_expires_at, created_at);
|
|
1915
2013
|
`);
|
|
1916
2014
|
}
|
|
1917
2015
|
ensureColumn(table, column, definition) {
|
|
@@ -2291,6 +2389,7 @@ var ProposalStore = class {
|
|
|
2291
2389
|
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
2292
2390
|
const window = utcDayWindow(now);
|
|
2293
2391
|
const trippedLimits = [];
|
|
2392
|
+
const nearLimits = [];
|
|
2294
2393
|
let quorumDeferred = false;
|
|
2295
2394
|
this.transaction(() => {
|
|
2296
2395
|
const proposal = this.requireProposal(proposalId);
|
|
@@ -2343,6 +2442,17 @@ var ProposalStore = class {
|
|
|
2343
2442
|
window_end: window.end,
|
|
2344
2443
|
reason: `${scope} daily auto-approval count ${projected2} exceeds ${limit.max}`
|
|
2345
2444
|
});
|
|
2445
|
+
} else if (projected2 / limit.max >= 0.8) {
|
|
2446
|
+
nearLimits.push({
|
|
2447
|
+
...limit,
|
|
2448
|
+
scope,
|
|
2449
|
+
observed: rows.length,
|
|
2450
|
+
proposed: 1,
|
|
2451
|
+
projected: projected2,
|
|
2452
|
+
window_start: window.start,
|
|
2453
|
+
window_end: window.end,
|
|
2454
|
+
reason: `${scope} daily auto-approval count ${projected2} is approaching ${limit.max}`
|
|
2455
|
+
});
|
|
2346
2456
|
}
|
|
2347
2457
|
continue;
|
|
2348
2458
|
}
|
|
@@ -2377,6 +2487,17 @@ var ProposalStore = class {
|
|
|
2377
2487
|
window_end: window.end,
|
|
2378
2488
|
reason: invalidHistory || !field || typeof proposed !== "number" || !Number.isSafeInteger(proposed) ? `${scope} daily auto-approval total could not be verified safely${field ? ` for ${field}` : ""}` : `${scope} daily auto-approval total ${projected} for ${field} exceeds ${limit.max}`
|
|
2379
2489
|
});
|
|
2490
|
+
} else if (projected / limit.max >= 0.8) {
|
|
2491
|
+
nearLimits.push({
|
|
2492
|
+
...limit,
|
|
2493
|
+
scope,
|
|
2494
|
+
observed,
|
|
2495
|
+
proposed: proposedNumber,
|
|
2496
|
+
projected,
|
|
2497
|
+
window_start: window.start,
|
|
2498
|
+
window_end: window.end,
|
|
2499
|
+
reason: `${scope} daily auto-approval total is approaching its reviewed maximum`
|
|
2500
|
+
});
|
|
2380
2501
|
}
|
|
2381
2502
|
}
|
|
2382
2503
|
if (trippedLimits.length > 0) {
|
|
@@ -2413,6 +2534,19 @@ var ProposalStore = class {
|
|
|
2413
2534
|
aggregate_limits: options.limits ?? [],
|
|
2414
2535
|
freshness_proof_digest: options.freshness_proof_digest ?? null
|
|
2415
2536
|
});
|
|
2537
|
+
for (const near of nearLimits) {
|
|
2538
|
+
this.appendEvent(proposalId, "policy_limit_near", actor, {
|
|
2539
|
+
policy: options.policy,
|
|
2540
|
+
kind: near.kind,
|
|
2541
|
+
scope: near.scope,
|
|
2542
|
+
observed: near.observed,
|
|
2543
|
+
proposed: near.proposed,
|
|
2544
|
+
projected: near.projected,
|
|
2545
|
+
max: near.max,
|
|
2546
|
+
window_start: near.window_start,
|
|
2547
|
+
window_end: near.window_end
|
|
2548
|
+
});
|
|
2549
|
+
}
|
|
2416
2550
|
});
|
|
2417
2551
|
return {
|
|
2418
2552
|
proposal: this.requireProposal(proposalId),
|
|
@@ -2722,6 +2856,30 @@ var ProposalStore = class {
|
|
|
2722
2856
|
decision_hash: input.identity?.decision_hash
|
|
2723
2857
|
});
|
|
2724
2858
|
this.recordExecutionReceiptRows(receipt, proposal);
|
|
2859
|
+
const workerItem = this.workerQueueItem(intent.proposal_id);
|
|
2860
|
+
if (workerItem?.status === "reconciliation_required") {
|
|
2861
|
+
const queueStatus = receipt.status === "failed" ? "dead_letter" : "completed";
|
|
2862
|
+
const terminalOutcome = receipt.status === "failed" ? "dead_letter" : receipt.status;
|
|
2863
|
+
this.db.prepare(`
|
|
2864
|
+
UPDATE worker_queue
|
|
2865
|
+
SET status = ?, lease_owner = NULL, lease_id = NULL,
|
|
2866
|
+
lease_expires_at = NULL, last_error_code = ?,
|
|
2867
|
+
terminal_outcome = ?, updated_at = ?
|
|
2868
|
+
WHERE proposal_id = ? AND status = 'reconciliation_required'
|
|
2869
|
+
`).run(
|
|
2870
|
+
queueStatus,
|
|
2871
|
+
receipt.status === "failed" ? "RECONCILED_FAILED" : null,
|
|
2872
|
+
terminalOutcome,
|
|
2873
|
+
receipt.executed_at,
|
|
2874
|
+
intent.proposal_id
|
|
2875
|
+
);
|
|
2876
|
+
this.appendEvent(intent.proposal_id, "writeback_worker_reconciled", input.actor, {
|
|
2877
|
+
intent_id: intent.intent_id,
|
|
2878
|
+
outcome: receipt.status,
|
|
2879
|
+
queue_status: queueStatus,
|
|
2880
|
+
source_database_changed: false
|
|
2881
|
+
});
|
|
2882
|
+
}
|
|
2725
2883
|
});
|
|
2726
2884
|
return this.requireWritebackIntent(intent.intent_id);
|
|
2727
2885
|
}
|
|
@@ -3050,6 +3208,78 @@ var ProposalStore = class {
|
|
|
3050
3208
|
const rows = this.db.prepare(query.sql).all(...query.params);
|
|
3051
3209
|
return rows.map(rowToEvent).filter((event) => event !== void 0);
|
|
3052
3210
|
}
|
|
3211
|
+
enqueueWorkerProposal(options) {
|
|
3212
|
+
const proposal = this.requireProposal(options.proposal_id);
|
|
3213
|
+
if (proposal.state !== "approved" && proposal.state !== "pending_worker") {
|
|
3214
|
+
throw new ProposalStoreError(
|
|
3215
|
+
"WORKER_PROPOSAL_NOT_APPROVED",
|
|
3216
|
+
`proposal ${proposal.proposal_id} is ${proposal.state}, not approved for worker execution`
|
|
3217
|
+
);
|
|
3218
|
+
}
|
|
3219
|
+
const executionMode = options.execution_mode ?? "legacy";
|
|
3220
|
+
const contractDigest = options.contract_digest;
|
|
3221
|
+
if (executionMode === "supervised_worker" && !contractDigest) {
|
|
3222
|
+
throw new ProposalStoreError(
|
|
3223
|
+
"SUPERVISED_WORKER_DIGEST_REQUIRED",
|
|
3224
|
+
`supervised worker queue item ${proposal.proposal_id} requires an exact contract digest`
|
|
3225
|
+
);
|
|
3226
|
+
}
|
|
3227
|
+
if (contractDigest && !/^sha256:[a-f0-9]{64}$/.test(contractDigest)) {
|
|
3228
|
+
throw new ProposalStoreError("WORKER_CONTRACT_DIGEST_INVALID", "worker queue contract digest must be a full lowercase sha256 digest");
|
|
3229
|
+
}
|
|
3230
|
+
const maxAttempts = Math.max(1, Math.min(options.max_attempts ?? 5, 100));
|
|
3231
|
+
const queueLimit = Math.max(1, Math.min(options.queue_limit ?? 1e4, 1e5));
|
|
3232
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3233
|
+
this.transaction(() => {
|
|
3234
|
+
const existing = this.workerQueueItem(proposal.proposal_id);
|
|
3235
|
+
if (existing) {
|
|
3236
|
+
if (existing.execution_mode !== executionMode || existing.contract_digest !== contractDigest) {
|
|
3237
|
+
throw new ProposalStoreError(
|
|
3238
|
+
"WORKER_QUEUE_AUTHORITY_MISMATCH",
|
|
3239
|
+
`worker queue item ${proposal.proposal_id} already exists under different execution authority`
|
|
3240
|
+
);
|
|
3241
|
+
}
|
|
3242
|
+
return;
|
|
3243
|
+
}
|
|
3244
|
+
const active = this.db.prepare(`
|
|
3245
|
+
SELECT COUNT(*) AS count
|
|
3246
|
+
FROM worker_queue q
|
|
3247
|
+
JOIN proposals p ON p.proposal_id = q.proposal_id
|
|
3248
|
+
WHERE q.execution_mode = ?
|
|
3249
|
+
AND (? IS NULL OR q.contract_digest = ?)
|
|
3250
|
+
AND p.action = ?
|
|
3251
|
+
AND q.status IN ('queued', 'leased', 'retry_wait', 'blocked', 'reconciliation_required')
|
|
3252
|
+
`).get(executionMode, contractDigest ?? null, contractDigest ?? null, proposal.action);
|
|
3253
|
+
if (isRecord(active) && Number(active.count ?? 0) >= queueLimit) {
|
|
3254
|
+
throw new ProposalStoreError(
|
|
3255
|
+
"WORKER_QUEUE_LIMIT_EXCEEDED",
|
|
3256
|
+
`worker queue limit ${queueLimit} reached for ${proposal.action}`
|
|
3257
|
+
);
|
|
3258
|
+
}
|
|
3259
|
+
this.db.prepare(`
|
|
3260
|
+
INSERT INTO worker_queue (
|
|
3261
|
+
proposal_id, status, execution_mode, contract_digest, attempts,
|
|
3262
|
+
max_attempts, next_attempt_at, lease_owner, lease_id,
|
|
3263
|
+
lease_expires_at, last_error_code, terminal_outcome, created_at,
|
|
3264
|
+
updated_at
|
|
3265
|
+
) VALUES (?, 'queued', ?, ?, 0, ?, ?, NULL, NULL, NULL, NULL, NULL, ?, ?)
|
|
3266
|
+
`).run(
|
|
3267
|
+
proposal.proposal_id,
|
|
3268
|
+
executionMode,
|
|
3269
|
+
contractDigest ?? null,
|
|
3270
|
+
maxAttempts,
|
|
3271
|
+
now,
|
|
3272
|
+
now,
|
|
3273
|
+
now
|
|
3274
|
+
);
|
|
3275
|
+
this.appendEvent(proposal.proposal_id, "writeback_worker_queued", "runner", {
|
|
3276
|
+
execution_mode: executionMode,
|
|
3277
|
+
contract_digest: contractDigest ?? null,
|
|
3278
|
+
max_attempts: maxAttempts
|
|
3279
|
+
});
|
|
3280
|
+
});
|
|
3281
|
+
return this.requireWorkerQueueItem(proposal.proposal_id);
|
|
3282
|
+
}
|
|
3053
3283
|
enqueueApprovedForWorker(options = {}) {
|
|
3054
3284
|
const maxAttempts = Math.max(1, Math.min(options.maxAttempts ?? 5, 100));
|
|
3055
3285
|
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -3061,9 +3291,11 @@ var ProposalStore = class {
|
|
|
3061
3291
|
for (const proposal of proposals) {
|
|
3062
3292
|
this.db.prepare(`
|
|
3063
3293
|
INSERT OR IGNORE INTO worker_queue (
|
|
3064
|
-
proposal_id, status,
|
|
3065
|
-
|
|
3066
|
-
|
|
3294
|
+
proposal_id, status, execution_mode, contract_digest, attempts,
|
|
3295
|
+
max_attempts, next_attempt_at, lease_owner, lease_id,
|
|
3296
|
+
lease_expires_at, last_error_code, terminal_outcome, created_at,
|
|
3297
|
+
updated_at
|
|
3298
|
+
) VALUES (?, 'queued', 'legacy', NULL, 0, ?, ?, NULL, NULL, NULL, NULL, NULL, ?, ?)
|
|
3067
3299
|
`).run(proposal.proposal_id, maxAttempts, now, now, now);
|
|
3068
3300
|
}
|
|
3069
3301
|
});
|
|
@@ -3075,6 +3307,58 @@ var ProposalStore = class {
|
|
|
3075
3307
|
const leaseExpiresAt = new Date(Date.parse(now) + leaseSeconds * 1e3).toISOString();
|
|
3076
3308
|
let claimed;
|
|
3077
3309
|
this.transaction(() => {
|
|
3310
|
+
if (options.maxConcurrent !== void 0) {
|
|
3311
|
+
const maximum = Math.max(1, Math.min(options.maxConcurrent, 32));
|
|
3312
|
+
const active = this.db.prepare(`
|
|
3313
|
+
SELECT COUNT(*) AS count
|
|
3314
|
+
FROM worker_queue q
|
|
3315
|
+
JOIN proposals p ON p.proposal_id = q.proposal_id
|
|
3316
|
+
WHERE q.status = 'leased'
|
|
3317
|
+
AND q.lease_expires_at > ?
|
|
3318
|
+
AND (? IS NULL OR q.execution_mode = ?)
|
|
3319
|
+
AND (? IS NULL OR p.action = ?)
|
|
3320
|
+
AND (? IS NULL OR p.tenant_id = ?)
|
|
3321
|
+
AND (? IS NULL OR q.contract_digest = ?)
|
|
3322
|
+
`).get(
|
|
3323
|
+
now,
|
|
3324
|
+
options.executionMode ?? null,
|
|
3325
|
+
options.executionMode ?? null,
|
|
3326
|
+
options.capability ?? null,
|
|
3327
|
+
options.capability ?? null,
|
|
3328
|
+
options.tenant ?? null,
|
|
3329
|
+
options.tenant ?? null,
|
|
3330
|
+
options.contractDigest ?? null,
|
|
3331
|
+
options.contractDigest ?? null
|
|
3332
|
+
);
|
|
3333
|
+
if (isRecord(active) && Number(active.count ?? 0) >= maximum) return;
|
|
3334
|
+
}
|
|
3335
|
+
if (options.rateLimit) {
|
|
3336
|
+
const executions = Math.max(1, Math.min(options.rateLimit.executions, 1e5));
|
|
3337
|
+
const windowSeconds = Math.max(1, Math.min(options.rateLimit.windowSeconds, 86400));
|
|
3338
|
+
const windowStart = new Date(Date.parse(now) - windowSeconds * 1e3).toISOString();
|
|
3339
|
+
const recent = this.db.prepare(`
|
|
3340
|
+
SELECT COUNT(*) AS count
|
|
3341
|
+
FROM worker_queue q
|
|
3342
|
+
JOIN proposals p ON p.proposal_id = q.proposal_id
|
|
3343
|
+
WHERE q.status = 'completed'
|
|
3344
|
+
AND q.updated_at >= ?
|
|
3345
|
+
AND (? IS NULL OR q.execution_mode = ?)
|
|
3346
|
+
AND (? IS NULL OR p.action = ?)
|
|
3347
|
+
AND (? IS NULL OR p.tenant_id = ?)
|
|
3348
|
+
AND (? IS NULL OR q.contract_digest = ?)
|
|
3349
|
+
`).get(
|
|
3350
|
+
windowStart,
|
|
3351
|
+
options.executionMode ?? null,
|
|
3352
|
+
options.executionMode ?? null,
|
|
3353
|
+
options.capability ?? null,
|
|
3354
|
+
options.capability ?? null,
|
|
3355
|
+
options.tenant ?? null,
|
|
3356
|
+
options.tenant ?? null,
|
|
3357
|
+
options.contractDigest ?? null,
|
|
3358
|
+
options.contractDigest ?? null
|
|
3359
|
+
);
|
|
3360
|
+
if (isRecord(recent) && Number(recent.count ?? 0) >= executions) return;
|
|
3361
|
+
}
|
|
3078
3362
|
const raw = this.db.prepare(`
|
|
3079
3363
|
SELECT q.*
|
|
3080
3364
|
FROM worker_queue q
|
|
@@ -3084,58 +3368,297 @@ var ProposalStore = class {
|
|
|
3084
3368
|
OR (q.status = 'leased' AND q.lease_expires_at <= ?)
|
|
3085
3369
|
)
|
|
3086
3370
|
AND p.state IN ('approved', 'pending_worker', 'failed')
|
|
3371
|
+
AND (? IS NULL OR q.execution_mode = ?)
|
|
3372
|
+
AND (? IS NULL OR p.action = ?)
|
|
3373
|
+
AND (? IS NULL OR p.tenant_id = ?)
|
|
3374
|
+
AND (? IS NULL OR q.contract_digest = ?)
|
|
3087
3375
|
ORDER BY q.next_attempt_at ASC, q.created_at ASC
|
|
3088
3376
|
LIMIT 1
|
|
3089
|
-
`).get(
|
|
3377
|
+
`).get(
|
|
3378
|
+
now,
|
|
3379
|
+
now,
|
|
3380
|
+
options.executionMode ?? null,
|
|
3381
|
+
options.executionMode ?? null,
|
|
3382
|
+
options.capability ?? null,
|
|
3383
|
+
options.capability ?? null,
|
|
3384
|
+
options.tenant ?? null,
|
|
3385
|
+
options.tenant ?? null,
|
|
3386
|
+
options.contractDigest ?? null,
|
|
3387
|
+
options.contractDigest ?? null
|
|
3388
|
+
);
|
|
3090
3389
|
const item = rowToWorkerQueueItem(raw);
|
|
3091
3390
|
if (!item) return;
|
|
3391
|
+
const proposal = this.requireProposal(item.proposal_id);
|
|
3392
|
+
if (options.proposalTtlSeconds !== void 0) {
|
|
3393
|
+
const ttlSeconds = Math.max(60, Math.min(options.proposalTtlSeconds, 2592e3));
|
|
3394
|
+
const expiresAt = Date.parse(proposal.created_at) + ttlSeconds * 1e3;
|
|
3395
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.parse(now)) {
|
|
3396
|
+
this.blockQueuedWorkerItem(item, options.workerId, "SUPERVISED_WORKER_PROPOSAL_EXPIRED", now);
|
|
3397
|
+
return;
|
|
3398
|
+
}
|
|
3399
|
+
}
|
|
3400
|
+
if (options.policyExecution) {
|
|
3401
|
+
const trips = this.workerPolicyExecutionLimitTrips({
|
|
3402
|
+
proposal,
|
|
3403
|
+
policy: options.policyExecution.policy,
|
|
3404
|
+
limits: options.policyExecution.limits,
|
|
3405
|
+
now
|
|
3406
|
+
});
|
|
3407
|
+
if (trips.length > 0) {
|
|
3408
|
+
this.blockQueuedWorkerItem(
|
|
3409
|
+
item,
|
|
3410
|
+
options.workerId,
|
|
3411
|
+
"SUPERVISED_WORKER_POLICY_LIMIT_EXCEEDED",
|
|
3412
|
+
now,
|
|
3413
|
+
{ policy: options.policyExecution.policy, tripped_limits: trips }
|
|
3414
|
+
);
|
|
3415
|
+
return;
|
|
3416
|
+
}
|
|
3417
|
+
}
|
|
3418
|
+
const leaseId = workerLeaseId(item.proposal_id, options.workerId, item.attempts + 1, now);
|
|
3092
3419
|
this.db.prepare(`
|
|
3093
3420
|
UPDATE worker_queue
|
|
3094
3421
|
SET status = 'leased', attempts = attempts + 1, lease_owner = ?,
|
|
3095
|
-
lease_expires_at = ?,
|
|
3422
|
+
lease_id = ?, lease_expires_at = ?, last_error_code = NULL,
|
|
3423
|
+
terminal_outcome = NULL, updated_at = ?
|
|
3096
3424
|
WHERE proposal_id = ?
|
|
3097
|
-
`).run(options.workerId, leaseExpiresAt, now, item.proposal_id);
|
|
3098
|
-
const proposal = this.requireProposal(item.proposal_id);
|
|
3425
|
+
`).run(options.workerId, leaseId, leaseExpiresAt, now, item.proposal_id);
|
|
3099
3426
|
if (proposal.state === "failed") {
|
|
3100
3427
|
this.db.prepare("UPDATE proposals SET state = 'pending_worker', updated_at = ? WHERE proposal_id = ?").run(now, item.proposal_id);
|
|
3101
3428
|
}
|
|
3102
3429
|
this.appendEvent(item.proposal_id, "writeback_worker_claimed", options.workerId, {
|
|
3103
3430
|
attempt: item.attempts + 1,
|
|
3104
3431
|
max_attempts: item.max_attempts,
|
|
3432
|
+
execution_mode: item.execution_mode,
|
|
3433
|
+
contract_digest: item.contract_digest ?? null,
|
|
3434
|
+
lease_id: leaseId,
|
|
3105
3435
|
lease_expires_at: leaseExpiresAt
|
|
3106
3436
|
});
|
|
3107
3437
|
claimed = this.workerQueueItem(item.proposal_id);
|
|
3108
3438
|
});
|
|
3109
3439
|
return claimed;
|
|
3110
3440
|
}
|
|
3111
|
-
|
|
3441
|
+
assertWorkerPolicyExecutionLimits(input) {
|
|
3442
|
+
const proposal = this.requireProposal(input.proposalId);
|
|
3443
|
+
const trips = this.workerPolicyExecutionLimitTrips({
|
|
3444
|
+
proposal,
|
|
3445
|
+
policy: input.policy,
|
|
3446
|
+
limits: input.limits,
|
|
3447
|
+
now: input.now ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
3448
|
+
});
|
|
3449
|
+
if (trips.length > 0) {
|
|
3450
|
+
throw new ProposalStoreError(
|
|
3451
|
+
"SUPERVISED_WORKER_POLICY_LIMIT_EXCEEDED",
|
|
3452
|
+
`execution-time policy limit no longer permits proposal ${proposal.proposal_id}`
|
|
3453
|
+
);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
blockQueuedWorkerItem(item, actor, errorCode, now, payload = {}) {
|
|
3457
|
+
this.db.prepare(`
|
|
3458
|
+
UPDATE worker_queue
|
|
3459
|
+
SET status = 'blocked', lease_owner = NULL, lease_id = NULL,
|
|
3460
|
+
lease_expires_at = NULL, last_error_code = ?,
|
|
3461
|
+
terminal_outcome = NULL, updated_at = ?
|
|
3462
|
+
WHERE proposal_id = ?
|
|
3463
|
+
`).run(errorCode, now, item.proposal_id);
|
|
3464
|
+
this.appendEvent(item.proposal_id, "writeback_worker_blocked", actor, {
|
|
3465
|
+
error_code: errorCode,
|
|
3466
|
+
execution_mode: item.execution_mode,
|
|
3467
|
+
contract_digest: item.contract_digest ?? null,
|
|
3468
|
+
...payload
|
|
3469
|
+
});
|
|
3470
|
+
}
|
|
3471
|
+
workerPolicyExecutionLimitTrips(input) {
|
|
3472
|
+
if (input.limits.length === 0) return [];
|
|
3473
|
+
const actor = `policy:${input.policy}`;
|
|
3474
|
+
const candidateApproval = this.db.prepare(`
|
|
3475
|
+
SELECT approval_id
|
|
3476
|
+
FROM approvals
|
|
3477
|
+
WHERE proposal_id = ?
|
|
3478
|
+
AND approver = ?
|
|
3479
|
+
AND status = 'approved'
|
|
3480
|
+
AND proposal_hash = ?
|
|
3481
|
+
AND proposal_version = ?
|
|
3482
|
+
LIMIT 1
|
|
3483
|
+
`).get(
|
|
3484
|
+
input.proposal.proposal_id,
|
|
3485
|
+
actor,
|
|
3486
|
+
input.proposal.proposal_hash,
|
|
3487
|
+
input.proposal.proposal_version
|
|
3488
|
+
);
|
|
3489
|
+
if (!isRecord(candidateApproval)) return [];
|
|
3490
|
+
const window = utcDayWindow(input.now);
|
|
3491
|
+
const rows = this.db.prepare(`
|
|
3492
|
+
SELECT DISTINCT
|
|
3493
|
+
p.proposal_id,
|
|
3494
|
+
p.business_object,
|
|
3495
|
+
p.object_id,
|
|
3496
|
+
p.change_set_json
|
|
3497
|
+
FROM worker_queue q
|
|
3498
|
+
JOIN proposals p ON p.proposal_id = q.proposal_id
|
|
3499
|
+
JOIN approvals a ON a.proposal_id = p.proposal_id
|
|
3500
|
+
WHERE a.approver = ?
|
|
3501
|
+
AND a.status = 'approved'
|
|
3502
|
+
AND p.tenant_id = ?
|
|
3503
|
+
AND (
|
|
3504
|
+
(q.status = 'leased' AND q.lease_expires_at > ?)
|
|
3505
|
+
OR (
|
|
3506
|
+
q.status = 'completed'
|
|
3507
|
+
AND q.terminal_outcome IN ('applied', 'already_applied')
|
|
3508
|
+
AND q.updated_at >= ?
|
|
3509
|
+
AND q.updated_at < ?
|
|
3510
|
+
)
|
|
3511
|
+
OR (
|
|
3512
|
+
q.status = 'reconciliation_required'
|
|
3513
|
+
AND q.updated_at >= ?
|
|
3514
|
+
AND q.updated_at < ?
|
|
3515
|
+
)
|
|
3516
|
+
OR (
|
|
3517
|
+
p.state = 'applied'
|
|
3518
|
+
AND p.updated_at >= ?
|
|
3519
|
+
AND p.updated_at < ?
|
|
3520
|
+
)
|
|
3521
|
+
)
|
|
3522
|
+
`).all(
|
|
3523
|
+
actor,
|
|
3524
|
+
input.proposal.tenant_id,
|
|
3525
|
+
input.now,
|
|
3526
|
+
window.start,
|
|
3527
|
+
window.end,
|
|
3528
|
+
window.start,
|
|
3529
|
+
window.end,
|
|
3530
|
+
window.start,
|
|
3531
|
+
window.end
|
|
3532
|
+
);
|
|
3533
|
+
const active = /* @__PURE__ */ new Map();
|
|
3534
|
+
let invalidHistory = false;
|
|
3535
|
+
for (const row of rows) {
|
|
3536
|
+
if (!isRecord(row)) continue;
|
|
3537
|
+
try {
|
|
3538
|
+
const proposalId = String(row.proposal_id);
|
|
3539
|
+
active.set(proposalId, {
|
|
3540
|
+
proposal_id: proposalId,
|
|
3541
|
+
business_object: String(row.business_object),
|
|
3542
|
+
object_id: String(row.object_id),
|
|
3543
|
+
change_set: parseChangeSet(JSON.parse(String(row.change_set_json)))
|
|
3544
|
+
});
|
|
3545
|
+
} catch {
|
|
3546
|
+
invalidHistory = true;
|
|
3547
|
+
active.set(String(row.proposal_id), {
|
|
3548
|
+
proposal_id: String(row.proposal_id),
|
|
3549
|
+
business_object: String(row.business_object),
|
|
3550
|
+
object_id: String(row.object_id),
|
|
3551
|
+
change_set: input.proposal.change_set
|
|
3552
|
+
});
|
|
3553
|
+
}
|
|
3554
|
+
}
|
|
3555
|
+
active.set(input.proposal.proposal_id, {
|
|
3556
|
+
proposal_id: input.proposal.proposal_id,
|
|
3557
|
+
business_object: input.proposal.business_object,
|
|
3558
|
+
object_id: input.proposal.object_id,
|
|
3559
|
+
change_set: input.proposal.change_set
|
|
3560
|
+
});
|
|
3561
|
+
const trips = [];
|
|
3562
|
+
for (const limit of input.limits) {
|
|
3563
|
+
const scope = limit.scope ?? "tenant_policy";
|
|
3564
|
+
const scoped = [...active.values()].filter((proposal) => scope !== "tenant_policy_object" || proposal.business_object === input.proposal.business_object && proposal.object_id === input.proposal.object_id);
|
|
3565
|
+
if (limit.kind === "count") {
|
|
3566
|
+
const projected2 = scoped.length;
|
|
3567
|
+
if (projected2 > limit.max) {
|
|
3568
|
+
trips.push({
|
|
3569
|
+
...limit,
|
|
3570
|
+
scope,
|
|
3571
|
+
observed: Math.max(0, projected2 - 1),
|
|
3572
|
+
proposed: 1,
|
|
3573
|
+
projected: projected2,
|
|
3574
|
+
window_start: window.start,
|
|
3575
|
+
window_end: window.end,
|
|
3576
|
+
reason: `${scope} execution count ${projected2} exceeds ${limit.max}`
|
|
3577
|
+
});
|
|
3578
|
+
}
|
|
3579
|
+
continue;
|
|
3580
|
+
}
|
|
3581
|
+
const field = limit.field;
|
|
3582
|
+
let projected = 0;
|
|
3583
|
+
let proposed = 0;
|
|
3584
|
+
let invalid = !field || invalidHistory;
|
|
3585
|
+
for (const proposal of scoped) {
|
|
3586
|
+
const value = field ? proposal.change_set.patch[field] : void 0;
|
|
3587
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
|
3588
|
+
invalid = true;
|
|
3589
|
+
continue;
|
|
3590
|
+
}
|
|
3591
|
+
projected += value;
|
|
3592
|
+
if (proposal.proposal_id === input.proposal.proposal_id) proposed = value;
|
|
3593
|
+
}
|
|
3594
|
+
if (invalid || projected > limit.max) {
|
|
3595
|
+
trips.push({
|
|
3596
|
+
...limit,
|
|
3597
|
+
scope,
|
|
3598
|
+
observed: projected - proposed,
|
|
3599
|
+
proposed,
|
|
3600
|
+
projected,
|
|
3601
|
+
window_start: window.start,
|
|
3602
|
+
window_end: window.end,
|
|
3603
|
+
reason: invalid ? `${scope} execution total could not be verified safely${field ? ` for ${field}` : ""}` : `${scope} execution total ${projected} for ${field} exceeds ${limit.max}`
|
|
3604
|
+
});
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
return trips;
|
|
3608
|
+
}
|
|
3609
|
+
assertActiveWorkerLease(options) {
|
|
3610
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3611
|
+
const item = this.assertWorkerLease(options.proposalId, options.workerId, options.leaseId);
|
|
3612
|
+
if (!item.lease_expires_at || Date.parse(item.lease_expires_at) <= Date.parse(now)) {
|
|
3613
|
+
throw new ProposalStoreError("WORKER_LEASE_EXPIRED", `worker lease ${options.leaseId} for ${options.proposalId} has expired`);
|
|
3614
|
+
}
|
|
3615
|
+
return item;
|
|
3616
|
+
}
|
|
3617
|
+
renewWorkerLease(options) {
|
|
3618
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3619
|
+
const leaseSeconds = Math.max(15, Math.min(options.leaseSeconds ?? 60, 3600));
|
|
3620
|
+
const leaseExpiresAt = new Date(Date.parse(now) + leaseSeconds * 1e3).toISOString();
|
|
3621
|
+
this.transaction(() => {
|
|
3622
|
+
this.assertActiveWorkerLease({ ...options, now });
|
|
3623
|
+
this.db.prepare(`
|
|
3624
|
+
UPDATE worker_queue
|
|
3625
|
+
SET lease_expires_at = ?, updated_at = ?
|
|
3626
|
+
WHERE proposal_id = ? AND status = 'leased' AND lease_owner = ? AND lease_id = ?
|
|
3627
|
+
`).run(leaseExpiresAt, now, options.proposalId, options.workerId, options.leaseId);
|
|
3628
|
+
});
|
|
3629
|
+
return this.requireWorkerQueueItem(options.proposalId);
|
|
3630
|
+
}
|
|
3631
|
+
completeWorkerItem(proposalId, workerId, outcome, now = (/* @__PURE__ */ new Date()).toISOString(), leaseId) {
|
|
3112
3632
|
this.transaction(() => {
|
|
3113
|
-
this.assertWorkerLease(proposalId, workerId);
|
|
3633
|
+
this.assertWorkerLease(proposalId, workerId, leaseId);
|
|
3114
3634
|
this.db.prepare(`
|
|
3115
3635
|
UPDATE worker_queue
|
|
3116
|
-
SET status = 'completed', lease_owner = NULL,
|
|
3117
|
-
|
|
3636
|
+
SET status = 'completed', lease_owner = NULL, lease_id = NULL,
|
|
3637
|
+
lease_expires_at = NULL, last_error_code = NULL,
|
|
3638
|
+
terminal_outcome = ?, updated_at = ?
|
|
3118
3639
|
WHERE proposal_id = ?
|
|
3119
|
-
`).run(now, proposalId);
|
|
3120
|
-
this.appendEvent(proposalId, "writeback_worker_completed", workerId, { outcome });
|
|
3640
|
+
`).run(outcome, now, proposalId);
|
|
3641
|
+
this.appendEvent(proposalId, "writeback_worker_completed", workerId, { outcome, lease_id: leaseId ?? null });
|
|
3121
3642
|
});
|
|
3122
3643
|
return this.requireWorkerQueueItem(proposalId);
|
|
3123
3644
|
}
|
|
3124
3645
|
retryWorkerItem(options) {
|
|
3125
3646
|
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3126
3647
|
this.transaction(() => {
|
|
3127
|
-
const item = this.assertWorkerLease(options.proposalId, options.workerId);
|
|
3648
|
+
const item = this.assertWorkerLease(options.proposalId, options.workerId, options.leaseId);
|
|
3128
3649
|
const deadLetter = item.attempts >= item.max_attempts;
|
|
3129
3650
|
this.db.prepare(`
|
|
3130
3651
|
UPDATE worker_queue
|
|
3131
|
-
SET status = ?, next_attempt_at = ?, lease_owner = NULL,
|
|
3132
|
-
lease_expires_at = NULL, last_error_code = ?,
|
|
3652
|
+
SET status = ?, next_attempt_at = ?, lease_owner = NULL, lease_id = NULL,
|
|
3653
|
+
lease_expires_at = NULL, last_error_code = ?, terminal_outcome = NULL,
|
|
3654
|
+
updated_at = ?
|
|
3133
3655
|
WHERE proposal_id = ?
|
|
3134
3656
|
`).run(deadLetter ? "dead_letter" : "retry_wait", options.retryAt, options.errorCode, now, options.proposalId);
|
|
3135
3657
|
this.appendEvent(options.proposalId, deadLetter ? "writeback_dead_lettered" : "writeback_retry_scheduled", options.workerId, {
|
|
3136
3658
|
attempt: item.attempts,
|
|
3137
3659
|
max_attempts: item.max_attempts,
|
|
3138
3660
|
error_code: options.errorCode,
|
|
3661
|
+
lease_id: options.leaseId ?? null,
|
|
3139
3662
|
...deadLetter ? {} : { retry_at: options.retryAt }
|
|
3140
3663
|
});
|
|
3141
3664
|
});
|
|
@@ -3144,17 +3667,95 @@ var ProposalStore = class {
|
|
|
3144
3667
|
deadLetterWorkerItem(options) {
|
|
3145
3668
|
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3146
3669
|
this.transaction(() => {
|
|
3147
|
-
const item = this.assertWorkerLease(options.proposalId, options.workerId);
|
|
3670
|
+
const item = this.assertWorkerLease(options.proposalId, options.workerId, options.leaseId);
|
|
3148
3671
|
this.db.prepare(`
|
|
3149
3672
|
UPDATE worker_queue
|
|
3150
|
-
SET status = 'dead_letter', lease_owner = NULL,
|
|
3151
|
-
|
|
3673
|
+
SET status = 'dead_letter', lease_owner = NULL, lease_id = NULL,
|
|
3674
|
+
lease_expires_at = NULL, last_error_code = ?,
|
|
3675
|
+
terminal_outcome = 'dead_letter', updated_at = ?
|
|
3152
3676
|
WHERE proposal_id = ?
|
|
3153
3677
|
`).run(options.errorCode, now, options.proposalId);
|
|
3154
3678
|
this.appendEvent(options.proposalId, "writeback_dead_lettered", options.workerId, {
|
|
3155
3679
|
attempt: item.attempts,
|
|
3156
3680
|
max_attempts: item.max_attempts,
|
|
3157
|
-
error_code: options.errorCode
|
|
3681
|
+
error_code: options.errorCode,
|
|
3682
|
+
lease_id: options.leaseId ?? null
|
|
3683
|
+
});
|
|
3684
|
+
});
|
|
3685
|
+
return this.requireWorkerQueueItem(options.proposalId);
|
|
3686
|
+
}
|
|
3687
|
+
blockWorkerItem(options) {
|
|
3688
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3689
|
+
this.transaction(() => {
|
|
3690
|
+
this.assertWorkerLease(options.proposalId, options.workerId, options.leaseId);
|
|
3691
|
+
this.db.prepare(`
|
|
3692
|
+
UPDATE worker_queue
|
|
3693
|
+
SET status = 'blocked', lease_owner = NULL, lease_id = NULL,
|
|
3694
|
+
lease_expires_at = NULL, last_error_code = ?,
|
|
3695
|
+
terminal_outcome = 'blocked', updated_at = ?
|
|
3696
|
+
WHERE proposal_id = ?
|
|
3697
|
+
`).run(options.errorCode, now, options.proposalId);
|
|
3698
|
+
this.appendEvent(options.proposalId, "writeback_worker_blocked", options.workerId, {
|
|
3699
|
+
error_code: options.errorCode,
|
|
3700
|
+
lease_id: options.leaseId
|
|
3701
|
+
});
|
|
3702
|
+
});
|
|
3703
|
+
return this.requireWorkerQueueItem(options.proposalId);
|
|
3704
|
+
}
|
|
3705
|
+
requireWorkerReconciliation(options) {
|
|
3706
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3707
|
+
this.transaction(() => {
|
|
3708
|
+
this.assertWorkerLease(options.proposalId, options.workerId, options.leaseId);
|
|
3709
|
+
this.db.prepare(`
|
|
3710
|
+
UPDATE worker_queue
|
|
3711
|
+
SET status = 'reconciliation_required', lease_owner = NULL,
|
|
3712
|
+
lease_id = NULL, lease_expires_at = NULL, last_error_code = ?,
|
|
3713
|
+
terminal_outcome = 'reconciliation_required', updated_at = ?
|
|
3714
|
+
WHERE proposal_id = ?
|
|
3715
|
+
`).run(options.errorCode, now, options.proposalId);
|
|
3716
|
+
this.appendEvent(options.proposalId, "writeback_reconciliation_required", options.workerId, {
|
|
3717
|
+
safe_error_code: options.errorCode,
|
|
3718
|
+
lease_id: options.leaseId
|
|
3719
|
+
});
|
|
3720
|
+
});
|
|
3721
|
+
return this.requireWorkerQueueItem(options.proposalId);
|
|
3722
|
+
}
|
|
3723
|
+
cancelWorkerItem(options) {
|
|
3724
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3725
|
+
const proposal = this.requireProposal(options.proposalId);
|
|
3726
|
+
assertOperatorDecision(
|
|
3727
|
+
proposal,
|
|
3728
|
+
"worker_cancel",
|
|
3729
|
+
options.actor,
|
|
3730
|
+
options.identity,
|
|
3731
|
+
options.require_verified_identity === true
|
|
3732
|
+
);
|
|
3733
|
+
this.transaction(() => {
|
|
3734
|
+
const item = this.requireWorkerQueueItem(options.proposalId);
|
|
3735
|
+
if (item.status !== "queued" && item.status !== "retry_wait") {
|
|
3736
|
+
throw new ProposalStoreError(
|
|
3737
|
+
"WORKER_ITEM_NOT_CANCELLABLE",
|
|
3738
|
+
`worker queue item ${options.proposalId} is ${item.status}, not safely cancellable before lease`
|
|
3739
|
+
);
|
|
3740
|
+
}
|
|
3741
|
+
this.db.prepare(`
|
|
3742
|
+
UPDATE worker_queue
|
|
3743
|
+
SET status = 'cancelled', lease_owner = NULL, lease_id = NULL,
|
|
3744
|
+
lease_expires_at = NULL, terminal_outcome = 'cancelled',
|
|
3745
|
+
updated_at = ?
|
|
3746
|
+
WHERE proposal_id = ?
|
|
3747
|
+
`).run(now, options.proposalId);
|
|
3748
|
+
if (proposal.state === "approved" || proposal.state === "pending_worker") {
|
|
3749
|
+
this.db.prepare(`
|
|
3750
|
+
UPDATE proposals
|
|
3751
|
+
SET state = 'canceled', updated_at = ?
|
|
3752
|
+
WHERE proposal_id = ?
|
|
3753
|
+
`).run(now, options.proposalId);
|
|
3754
|
+
}
|
|
3755
|
+
this.appendEvent(options.proposalId, "writeback_canceled", options.actor, {
|
|
3756
|
+
execution_mode: item.execution_mode,
|
|
3757
|
+
contract_digest: item.contract_digest ?? null,
|
|
3758
|
+
identity: options.identity ? publicIdentitySummary(options.identity) : null
|
|
3158
3759
|
});
|
|
3159
3760
|
});
|
|
3160
3761
|
return this.requireWorkerQueueItem(options.proposalId);
|
|
@@ -3183,7 +3784,8 @@ var ProposalStore = class {
|
|
|
3183
3784
|
this.db.prepare(`
|
|
3184
3785
|
UPDATE worker_queue
|
|
3185
3786
|
SET status = 'queued', attempts = 0, max_attempts = ?, next_attempt_at = ?,
|
|
3186
|
-
lease_owner = NULL,
|
|
3787
|
+
lease_owner = NULL, lease_id = NULL, lease_expires_at = NULL,
|
|
3788
|
+
last_error_code = NULL, terminal_outcome = NULL, updated_at = ?
|
|
3187
3789
|
WHERE proposal_id = ?
|
|
3188
3790
|
`).run(retryBudget, now, now, options.proposalId);
|
|
3189
3791
|
if (proposal.state === "failed") {
|
|
@@ -3208,7 +3810,8 @@ var ProposalStore = class {
|
|
|
3208
3810
|
}
|
|
3209
3811
|
this.db.prepare(`
|
|
3210
3812
|
UPDATE worker_queue
|
|
3211
|
-
SET status = 'discarded', lease_owner = NULL,
|
|
3813
|
+
SET status = 'discarded', lease_owner = NULL, lease_id = NULL,
|
|
3814
|
+
lease_expires_at = NULL, terminal_outcome = 'discarded', updated_at = ?
|
|
3212
3815
|
WHERE proposal_id = ?
|
|
3213
3816
|
`).run(now, options.proposalId);
|
|
3214
3817
|
this.appendEvent(options.proposalId, "writeback_dead_letter_discarded", options.identity.subject, {
|
|
@@ -3226,9 +3829,9 @@ var ProposalStore = class {
|
|
|
3226
3829
|
if (!item) throw new ProposalStoreError("WORKER_ITEM_NOT_FOUND", `worker queue item not found for ${proposalId}`);
|
|
3227
3830
|
return item;
|
|
3228
3831
|
}
|
|
3229
|
-
assertWorkerLease(proposalId, workerId) {
|
|
3832
|
+
assertWorkerLease(proposalId, workerId, leaseId) {
|
|
3230
3833
|
const item = this.requireWorkerQueueItem(proposalId);
|
|
3231
|
-
if (item.status !== "leased" || item.lease_owner !== workerId) {
|
|
3834
|
+
if (item.status !== "leased" || item.lease_owner !== workerId || leaseId !== void 0 && item.lease_id !== leaseId) {
|
|
3232
3835
|
throw new ProposalStoreError("WORKER_LEASE_MISMATCH", `worker ${workerId} does not hold the lease for ${proposalId}`);
|
|
3233
3836
|
}
|
|
3234
3837
|
return item;
|
|
@@ -3453,43 +4056,679 @@ var ProposalStore = class {
|
|
|
3453
4056
|
clauses.push(`${column} = ?`);
|
|
3454
4057
|
params.push(value);
|
|
3455
4058
|
}
|
|
3456
|
-
const sql = `SELECT * FROM policy_recommendations${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY created_at DESC, recommendation_id DESC`;
|
|
3457
|
-
return this.db.prepare(sql).all(...params).map(rowToPolicyRecommendation).filter((item) => item !== void 0);
|
|
4059
|
+
const sql = `SELECT * FROM policy_recommendations${clauses.length ? ` WHERE ${clauses.join(" AND ")}` : ""} ORDER BY created_at DESC, recommendation_id DESC`;
|
|
4060
|
+
return this.db.prepare(sql).all(...params).map(rowToPolicyRecommendation).filter((item) => item !== void 0);
|
|
4061
|
+
}
|
|
4062
|
+
decidePolicyRecommendation(recommendationId, input) {
|
|
4063
|
+
const recommendation = this.requirePolicyRecommendation(recommendationId);
|
|
4064
|
+
if (recommendation.status !== "pending_review") throw new ProposalStoreError("POLICY_RECOMMENDATION_NOT_PENDING", `policy recommendation ${recommendationId} is ${recommendation.status}`);
|
|
4065
|
+
assertPolicyRecommendationIdentity(recommendation, input);
|
|
4066
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4067
|
+
const unsigned = {
|
|
4068
|
+
...policyRecommendationUnsigned(recommendation),
|
|
4069
|
+
status: input.action === "approve" ? "approved" : "rejected",
|
|
4070
|
+
decision: { actor: input.actor, action: input.action, reason: input.reason, identity: input.identity, decided_at: now },
|
|
4071
|
+
updated_at: now
|
|
4072
|
+
};
|
|
4073
|
+
const updated = { ...unsigned, integrity_hash: canonicalJsonDigest(unsigned) };
|
|
4074
|
+
this.db.prepare("UPDATE policy_recommendations SET status = ?, payload_json = ?, integrity_hash = ?, updated_at = ? WHERE recommendation_id = ?").run(updated.status, JSON.stringify(unsigned), updated.integrity_hash, now, recommendationId);
|
|
4075
|
+
return updated;
|
|
4076
|
+
}
|
|
4077
|
+
markPolicyRecommendationExported(recommendationId, input) {
|
|
4078
|
+
const recommendation = this.requirePolicyRecommendation(recommendationId);
|
|
4079
|
+
if (recommendation.status !== "approved") throw new ProposalStoreError("POLICY_RECOMMENDATION_NOT_APPROVED", `policy recommendation ${recommendationId} is ${recommendation.status}`);
|
|
4080
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(input.artifact_digest)) throw new ProposalStoreError("POLICY_ARTIFACT_DIGEST_INVALID", "policy recommendation export requires a canonical SHA-256 artifact digest");
|
|
4081
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4082
|
+
const unsigned = {
|
|
4083
|
+
...policyRecommendationUnsigned(recommendation),
|
|
4084
|
+
status: "exported",
|
|
4085
|
+
export: { actor: input.actor, artifact_digest: input.artifact_digest, exported_at: now },
|
|
4086
|
+
updated_at: now
|
|
4087
|
+
};
|
|
4088
|
+
const updated = { ...unsigned, integrity_hash: canonicalJsonDigest(unsigned) };
|
|
4089
|
+
this.db.prepare("UPDATE policy_recommendations SET status = ?, payload_json = ?, integrity_hash = ?, updated_at = ? WHERE recommendation_id = ?").run(updated.status, JSON.stringify(unsigned), updated.integrity_hash, now, recommendationId);
|
|
4090
|
+
return updated;
|
|
4091
|
+
}
|
|
4092
|
+
requirePolicyRecommendation(recommendationId) {
|
|
4093
|
+
const recommendation = this.getPolicyRecommendation(recommendationId);
|
|
4094
|
+
if (!recommendation) throw new ProposalStoreError("POLICY_RECOMMENDATION_NOT_FOUND", `policy recommendation not found: ${recommendationId}`);
|
|
4095
|
+
return recommendation;
|
|
4096
|
+
}
|
|
4097
|
+
recordAttentionEvent(input) {
|
|
4098
|
+
let event;
|
|
4099
|
+
const record = () => {
|
|
4100
|
+
event = this.recordAttentionEventInternal(input);
|
|
4101
|
+
};
|
|
4102
|
+
if (this.db.isTransaction) record();
|
|
4103
|
+
else this.transaction(record);
|
|
4104
|
+
if (!event) throw new ProposalStoreError("ATTENTION_EVENT_CREATE_FAILED", "attention event was not persisted");
|
|
4105
|
+
return event;
|
|
4106
|
+
}
|
|
4107
|
+
listAttentionEvents(filters = {}) {
|
|
4108
|
+
const clauses = [];
|
|
4109
|
+
const params = [];
|
|
4110
|
+
if (filters.event_type) {
|
|
4111
|
+
clauses.push("event_type = ?");
|
|
4112
|
+
params.push(filters.event_type);
|
|
4113
|
+
}
|
|
4114
|
+
if (filters.severity) {
|
|
4115
|
+
clauses.push("severity = ?");
|
|
4116
|
+
params.push(filters.severity);
|
|
4117
|
+
}
|
|
4118
|
+
if (filters.proposal_id) {
|
|
4119
|
+
clauses.push("proposal_id = ?");
|
|
4120
|
+
params.push(filters.proposal_id);
|
|
4121
|
+
}
|
|
4122
|
+
if (filters.capability) {
|
|
4123
|
+
clauses.push("capability = ?");
|
|
4124
|
+
params.push(filters.capability);
|
|
4125
|
+
}
|
|
4126
|
+
if (filters.tenant) {
|
|
4127
|
+
clauses.push("EXISTS (SELECT 1 FROM proposals p WHERE p.proposal_id = attention_events.proposal_id AND p.tenant_id = ?)");
|
|
4128
|
+
params.push(filters.tenant);
|
|
4129
|
+
}
|
|
4130
|
+
if (filters.principal) {
|
|
4131
|
+
clauses.push("EXISTS (SELECT 1 FROM proposals p WHERE p.proposal_id = attention_events.proposal_id AND p.principal = ?)");
|
|
4132
|
+
params.push(filters.principal);
|
|
4133
|
+
}
|
|
4134
|
+
if (filters.from) {
|
|
4135
|
+
clauses.push("occurred_at >= ?");
|
|
4136
|
+
params.push(filters.from);
|
|
4137
|
+
}
|
|
4138
|
+
const limit = Math.max(1, Math.min(filters.limit ?? 200, 1e3));
|
|
4139
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
4140
|
+
return this.db.prepare(`
|
|
4141
|
+
SELECT *
|
|
4142
|
+
FROM attention_events
|
|
4143
|
+
${where}
|
|
4144
|
+
ORDER BY occurred_at DESC, event_id DESC
|
|
4145
|
+
LIMIT ?
|
|
4146
|
+
`).all(...params, limit).map(rowToAttentionEvent).filter((event) => event !== void 0);
|
|
4147
|
+
}
|
|
4148
|
+
getAttentionEvent(eventId) {
|
|
4149
|
+
return rowToAttentionEvent(
|
|
4150
|
+
this.db.prepare("SELECT * FROM attention_events WHERE event_id = ?").get(eventId)
|
|
4151
|
+
);
|
|
4152
|
+
}
|
|
4153
|
+
listAttentionItems(filters = {}) {
|
|
4154
|
+
const clauses = [];
|
|
4155
|
+
const params = [];
|
|
4156
|
+
if (filters.status) {
|
|
4157
|
+
clauses.push("status = ?");
|
|
4158
|
+
params.push(filters.status);
|
|
4159
|
+
}
|
|
4160
|
+
if (filters.severity) {
|
|
4161
|
+
clauses.push("severity = ?");
|
|
4162
|
+
params.push(filters.severity);
|
|
4163
|
+
}
|
|
4164
|
+
if (filters.capability) {
|
|
4165
|
+
clauses.push("capability = ?");
|
|
4166
|
+
params.push(filters.capability);
|
|
4167
|
+
}
|
|
4168
|
+
if (filters.tenant) {
|
|
4169
|
+
clauses.push(`EXISTS (
|
|
4170
|
+
SELECT 1
|
|
4171
|
+
FROM attention_events ae
|
|
4172
|
+
JOIN proposals p ON p.proposal_id = ae.proposal_id
|
|
4173
|
+
WHERE ae.attention_key = attention_items.attention_key
|
|
4174
|
+
AND p.tenant_id = ?
|
|
4175
|
+
)`);
|
|
4176
|
+
params.push(filters.tenant);
|
|
4177
|
+
}
|
|
4178
|
+
if (filters.principal) {
|
|
4179
|
+
clauses.push(`EXISTS (
|
|
4180
|
+
SELECT 1
|
|
4181
|
+
FROM attention_events ae
|
|
4182
|
+
JOIN proposals p ON p.proposal_id = ae.proposal_id
|
|
4183
|
+
WHERE ae.attention_key = attention_items.attention_key
|
|
4184
|
+
AND p.principal = ?
|
|
4185
|
+
)`);
|
|
4186
|
+
params.push(filters.principal);
|
|
4187
|
+
}
|
|
4188
|
+
const limit = Math.max(1, Math.min(filters.limit ?? 200, 1e3));
|
|
4189
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
4190
|
+
return this.db.prepare(`
|
|
4191
|
+
SELECT *
|
|
4192
|
+
FROM attention_items
|
|
4193
|
+
${where}
|
|
4194
|
+
ORDER BY
|
|
4195
|
+
CASE severity WHEN 'critical' THEN 0 WHEN 'warning' THEN 1 ELSE 2 END,
|
|
4196
|
+
last_seen_at DESC,
|
|
4197
|
+
attention_id DESC
|
|
4198
|
+
LIMIT ?
|
|
4199
|
+
`).all(...params, limit).map(rowToAttentionItem).filter((item) => item !== void 0);
|
|
4200
|
+
}
|
|
4201
|
+
getAttentionItem(attentionId) {
|
|
4202
|
+
return rowToAttentionItem(this.db.prepare("SELECT * FROM attention_items WHERE attention_id = ?").get(attentionId));
|
|
4203
|
+
}
|
|
4204
|
+
acknowledgeAttention(input) {
|
|
4205
|
+
const actor = boundedSafeLabel(input.actor, "attention acknowledgement actor", 256);
|
|
4206
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4207
|
+
this.transaction(() => {
|
|
4208
|
+
const item = this.requireAttentionItem(input.attention_id);
|
|
4209
|
+
if (item.status !== "open") {
|
|
4210
|
+
throw new ProposalStoreError("ATTENTION_ITEM_NOT_ACKNOWLEDGEABLE", `attention item ${item.attention_id} is ${item.status}`);
|
|
4211
|
+
}
|
|
4212
|
+
assertAttentionOperatorDecision(
|
|
4213
|
+
item,
|
|
4214
|
+
actor,
|
|
4215
|
+
input.identity,
|
|
4216
|
+
input.require_verified_identity === true
|
|
4217
|
+
);
|
|
4218
|
+
this.db.prepare(`
|
|
4219
|
+
UPDATE attention_items
|
|
4220
|
+
SET status = 'acknowledged', acknowledged_by = ?, acknowledged_at = ?,
|
|
4221
|
+
acknowledgement_identity_json = ?,
|
|
4222
|
+
acknowledgement_decision_hash = ?,
|
|
4223
|
+
acknowledgement_signature = ?,
|
|
4224
|
+
acknowledgement_integrity_hash = ?,
|
|
4225
|
+
last_seen_at = MAX(last_seen_at, ?)
|
|
4226
|
+
WHERE attention_id = ?
|
|
4227
|
+
`).run(
|
|
4228
|
+
actor,
|
|
4229
|
+
now,
|
|
4230
|
+
input.identity ? JSON.stringify(input.identity) : null,
|
|
4231
|
+
input.identity?.decision_hash ?? null,
|
|
4232
|
+
input.identity?.signature ?? null,
|
|
4233
|
+
input.identity?.integrity_hash ?? null,
|
|
4234
|
+
now,
|
|
4235
|
+
item.attention_id
|
|
4236
|
+
);
|
|
4237
|
+
});
|
|
4238
|
+
return this.requireAttentionItem(input.attention_id);
|
|
4239
|
+
}
|
|
4240
|
+
resolveAttention(input) {
|
|
4241
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4242
|
+
this.transaction(() => {
|
|
4243
|
+
const item = this.requireAttentionItem(input.attention_id);
|
|
4244
|
+
if (item.status === "expired") {
|
|
4245
|
+
throw new ProposalStoreError("ATTENTION_ITEM_NOT_RESOLVABLE", `attention item ${item.attention_id} is expired`);
|
|
4246
|
+
}
|
|
4247
|
+
this.db.prepare(`
|
|
4248
|
+
UPDATE attention_items
|
|
4249
|
+
SET status = 'resolved', resolved_at = ?, last_seen_at = MAX(last_seen_at, ?)
|
|
4250
|
+
WHERE attention_id = ?
|
|
4251
|
+
`).run(now, now, item.attention_id);
|
|
4252
|
+
});
|
|
4253
|
+
return this.requireAttentionItem(input.attention_id);
|
|
4254
|
+
}
|
|
4255
|
+
enqueueNotificationDelivery(input) {
|
|
4256
|
+
const sinkId = boundedSinkId(input.sink_id);
|
|
4257
|
+
const event = this.requireAttentionEvent(input.event_id);
|
|
4258
|
+
const attention = input.attention_id ? this.requireAttentionItem(input.attention_id) : void 0;
|
|
4259
|
+
if (attention && event.attention_key !== attention.attention_key) {
|
|
4260
|
+
throw new ProposalStoreError(
|
|
4261
|
+
"NOTIFICATION_ATTENTION_MISMATCH",
|
|
4262
|
+
`attention event ${event.event_id} does not belong to attention item ${attention.attention_id}`
|
|
4263
|
+
);
|
|
4264
|
+
}
|
|
4265
|
+
const maxAttempts = Math.max(1, Math.min(input.max_attempts ?? 5, 100));
|
|
4266
|
+
const status = input.status ?? "pending";
|
|
4267
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4268
|
+
if (!Number.isFinite(Date.parse(now))) {
|
|
4269
|
+
throw new ProposalStoreError("NOTIFICATION_TIME_INVALID", "notification delivery time must be an ISO timestamp");
|
|
4270
|
+
}
|
|
4271
|
+
const nextAttemptAt = input.next_attempt_at ?? now;
|
|
4272
|
+
if (!Number.isFinite(Date.parse(nextAttemptAt))) {
|
|
4273
|
+
throw new ProposalStoreError(
|
|
4274
|
+
"NOTIFICATION_TIME_INVALID",
|
|
4275
|
+
"notification delivery next-attempt time must be an ISO timestamp"
|
|
4276
|
+
);
|
|
4277
|
+
}
|
|
4278
|
+
const deliveryId = notificationDeliveryId(sinkId, event.event_id);
|
|
4279
|
+
this.transaction(() => {
|
|
4280
|
+
const existing = this.getNotificationDelivery(deliveryId);
|
|
4281
|
+
if (existing) {
|
|
4282
|
+
if (existing.sink_id !== sinkId || existing.event_id !== event.event_id || existing.attention_id !== attention?.attention_id) {
|
|
4283
|
+
throw new ProposalStoreError(
|
|
4284
|
+
"NOTIFICATION_DELIVERY_IDEMPOTENCY_MISMATCH",
|
|
4285
|
+
`notification delivery ${deliveryId} already exists with different immutable routing`
|
|
4286
|
+
);
|
|
4287
|
+
}
|
|
4288
|
+
return;
|
|
4289
|
+
}
|
|
4290
|
+
this.db.prepare(`
|
|
4291
|
+
INSERT INTO notification_deliveries (
|
|
4292
|
+
delivery_id, sink_id, event_id, attention_id, status, attempts,
|
|
4293
|
+
max_attempts, next_attempt_at, lease_owner, lease_id,
|
|
4294
|
+
lease_expires_at, last_error_code, external_reference, delivered_at,
|
|
4295
|
+
created_at, updated_at
|
|
4296
|
+
) VALUES (?, ?, ?, ?, ?, 0, ?, ?, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?)
|
|
4297
|
+
`).run(
|
|
4298
|
+
deliveryId,
|
|
4299
|
+
sinkId,
|
|
4300
|
+
event.event_id,
|
|
4301
|
+
attention?.attention_id ?? null,
|
|
4302
|
+
status,
|
|
4303
|
+
maxAttempts,
|
|
4304
|
+
nextAttemptAt,
|
|
4305
|
+
now,
|
|
4306
|
+
now
|
|
4307
|
+
);
|
|
4308
|
+
});
|
|
4309
|
+
return this.requireNotificationDelivery(deliveryId);
|
|
4310
|
+
}
|
|
4311
|
+
includeNotificationDeliveriesInDigest(input) {
|
|
4312
|
+
const sinkId = boundedSinkId(input.sink_id);
|
|
4313
|
+
const deliveryIds = [...new Set(input.delivery_ids)];
|
|
4314
|
+
if (deliveryIds.length === 0 || deliveryIds.length > 1e3) {
|
|
4315
|
+
throw new ProposalStoreError(
|
|
4316
|
+
"NOTIFICATION_DIGEST_MEMBERS_INVALID",
|
|
4317
|
+
"a notification digest must contain from 1 through 1000 delivery ids"
|
|
4318
|
+
);
|
|
4319
|
+
}
|
|
4320
|
+
const digestEvent = this.requireAttentionEvent(input.digest_event_id);
|
|
4321
|
+
if (digestEvent.event_type !== "notification.digest") {
|
|
4322
|
+
throw new ProposalStoreError(
|
|
4323
|
+
"NOTIFICATION_DIGEST_EVENT_INVALID",
|
|
4324
|
+
"notification digest membership requires a notification.digest event"
|
|
4325
|
+
);
|
|
4326
|
+
}
|
|
4327
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4328
|
+
if (!Number.isFinite(Date.parse(now))) {
|
|
4329
|
+
throw new ProposalStoreError("NOTIFICATION_TIME_INVALID", "notification digest time must be an ISO timestamp");
|
|
4330
|
+
}
|
|
4331
|
+
let included = 0;
|
|
4332
|
+
this.transaction(() => {
|
|
4333
|
+
for (const deliveryId of deliveryIds) {
|
|
4334
|
+
const item = this.requireNotificationDelivery(deliveryId);
|
|
4335
|
+
const digestReference = `digest:${digestEvent.event_id}`;
|
|
4336
|
+
if (item.status === "suppressed" && item.sink_id === sinkId && item.external_reference === digestReference) {
|
|
4337
|
+
continue;
|
|
4338
|
+
}
|
|
4339
|
+
if (item.sink_id !== sinkId || item.status !== "batched") {
|
|
4340
|
+
throw new ProposalStoreError(
|
|
4341
|
+
"NOTIFICATION_DIGEST_MEMBER_STATE_INVALID",
|
|
4342
|
+
`notification delivery ${deliveryId} is not a batched member of sink ${sinkId}`
|
|
4343
|
+
);
|
|
4344
|
+
}
|
|
4345
|
+
this.db.prepare(`
|
|
4346
|
+
UPDATE notification_deliveries
|
|
4347
|
+
SET status = 'suppressed', external_reference = ?, updated_at = ?
|
|
4348
|
+
WHERE delivery_id = ?
|
|
4349
|
+
`).run(digestReference, now, deliveryId);
|
|
4350
|
+
included += 1;
|
|
4351
|
+
}
|
|
4352
|
+
});
|
|
4353
|
+
return included;
|
|
4354
|
+
}
|
|
4355
|
+
claimNotificationDeliveries(input) {
|
|
4356
|
+
const owner = boundedSafeLabel(input.owner, "notification dispatcher identity", 128);
|
|
4357
|
+
const sinkId = input.sink_id ? boundedSinkId(input.sink_id) : void 0;
|
|
4358
|
+
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
4359
|
+
const leaseSeconds = Math.max(15, Math.min(input.lease_seconds ?? 60, 3600));
|
|
4360
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4361
|
+
if (!Number.isFinite(Date.parse(now))) {
|
|
4362
|
+
throw new ProposalStoreError("NOTIFICATION_TIME_INVALID", "notification claim time must be an ISO timestamp");
|
|
4363
|
+
}
|
|
4364
|
+
const leaseExpiresAt = new Date(Date.parse(now) + leaseSeconds * 1e3).toISOString();
|
|
4365
|
+
const claimed = [];
|
|
4366
|
+
this.transaction(() => {
|
|
4367
|
+
const rows = this.db.prepare(`
|
|
4368
|
+
SELECT *
|
|
4369
|
+
FROM notification_deliveries
|
|
4370
|
+
WHERE (
|
|
4371
|
+
(status IN ('pending', 'retry_wait') AND next_attempt_at <= ?)
|
|
4372
|
+
OR (status = 'leased' AND lease_expires_at <= ?)
|
|
4373
|
+
)
|
|
4374
|
+
AND (? IS NULL OR sink_id = ?)
|
|
4375
|
+
ORDER BY next_attempt_at ASC, created_at ASC, delivery_id ASC
|
|
4376
|
+
LIMIT ?
|
|
4377
|
+
`).all(now, now, sinkId ?? null, sinkId ?? null, limit);
|
|
4378
|
+
for (const row of rows) {
|
|
4379
|
+
const item = rowToNotificationDelivery(row);
|
|
4380
|
+
if (!item) continue;
|
|
4381
|
+
const leaseId = notificationLeaseId(item.delivery_id, owner, item.attempts + 1, now);
|
|
4382
|
+
this.db.prepare(`
|
|
4383
|
+
UPDATE notification_deliveries
|
|
4384
|
+
SET status = 'leased', attempts = attempts + 1, lease_owner = ?,
|
|
4385
|
+
lease_id = ?, lease_expires_at = ?, last_error_code = NULL,
|
|
4386
|
+
updated_at = ?
|
|
4387
|
+
WHERE delivery_id = ?
|
|
4388
|
+
`).run(owner, leaseId, leaseExpiresAt, now, item.delivery_id);
|
|
4389
|
+
claimed.push(this.requireNotificationDelivery(item.delivery_id));
|
|
4390
|
+
}
|
|
4391
|
+
});
|
|
4392
|
+
return claimed;
|
|
4393
|
+
}
|
|
4394
|
+
completeNotificationDelivery(input) {
|
|
4395
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4396
|
+
const externalReference = input.external_reference ? boundedSafeLabel(input.external_reference, "notification external reference", 256) : void 0;
|
|
4397
|
+
this.transaction(() => {
|
|
4398
|
+
this.assertNotificationLease(input.delivery_id, input.owner, input.lease_id, now);
|
|
4399
|
+
this.db.prepare(`
|
|
4400
|
+
UPDATE notification_deliveries
|
|
4401
|
+
SET status = 'delivered', lease_owner = NULL, lease_id = NULL,
|
|
4402
|
+
lease_expires_at = NULL, last_error_code = NULL,
|
|
4403
|
+
external_reference = COALESCE(?, external_reference),
|
|
4404
|
+
delivered_at = ?, updated_at = ?
|
|
4405
|
+
WHERE delivery_id = ?
|
|
4406
|
+
`).run(externalReference ?? null, now, now, input.delivery_id);
|
|
4407
|
+
});
|
|
4408
|
+
return this.requireNotificationDelivery(input.delivery_id);
|
|
4409
|
+
}
|
|
4410
|
+
failNotificationDelivery(input) {
|
|
4411
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4412
|
+
const errorCode = boundedSafeErrorCode(input.error_code);
|
|
4413
|
+
this.transaction(() => {
|
|
4414
|
+
const item = this.assertNotificationLease(input.delivery_id, input.owner, input.lease_id, now);
|
|
4415
|
+
const retryable = input.retryable && item.attempts < item.max_attempts;
|
|
4416
|
+
const retryAt = retryable ? input.retry_at ?? new Date(Date.parse(now) + 1e3).toISOString() : now;
|
|
4417
|
+
if (!Number.isFinite(Date.parse(retryAt))) {
|
|
4418
|
+
throw new ProposalStoreError("NOTIFICATION_RETRY_TIME_INVALID", "notification retry time must be an ISO timestamp");
|
|
4419
|
+
}
|
|
4420
|
+
this.db.prepare(`
|
|
4421
|
+
UPDATE notification_deliveries
|
|
4422
|
+
SET status = ?, next_attempt_at = ?, lease_owner = NULL, lease_id = NULL,
|
|
4423
|
+
lease_expires_at = NULL, last_error_code = ?, updated_at = ?
|
|
4424
|
+
WHERE delivery_id = ?
|
|
4425
|
+
`).run(retryable ? "retry_wait" : "dead_letter", retryAt, errorCode, now, input.delivery_id);
|
|
4426
|
+
});
|
|
4427
|
+
return this.requireNotificationDelivery(input.delivery_id);
|
|
4428
|
+
}
|
|
4429
|
+
listNotificationDeliveries(filters = {}) {
|
|
4430
|
+
const clauses = [];
|
|
4431
|
+
const params = [];
|
|
4432
|
+
for (const [column, value] of [
|
|
4433
|
+
["status", filters.status],
|
|
4434
|
+
["sink_id", filters.sink_id],
|
|
4435
|
+
["event_id", filters.event_id],
|
|
4436
|
+
["attention_id", filters.attention_id]
|
|
4437
|
+
]) {
|
|
4438
|
+
if (!value) continue;
|
|
4439
|
+
clauses.push(`${column} = ?`);
|
|
4440
|
+
params.push(value);
|
|
4441
|
+
}
|
|
4442
|
+
const limit = Math.max(1, Math.min(filters.limit ?? 200, 1e3));
|
|
4443
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
4444
|
+
return this.db.prepare(`
|
|
4445
|
+
SELECT *
|
|
4446
|
+
FROM notification_deliveries
|
|
4447
|
+
${where}
|
|
4448
|
+
ORDER BY created_at DESC, delivery_id DESC
|
|
4449
|
+
LIMIT ?
|
|
4450
|
+
`).all(...params, limit).map(rowToNotificationDelivery).filter((delivery) => delivery !== void 0);
|
|
4451
|
+
}
|
|
4452
|
+
getNotificationDelivery(deliveryId) {
|
|
4453
|
+
return rowToNotificationDelivery(
|
|
4454
|
+
this.db.prepare("SELECT * FROM notification_deliveries WHERE delivery_id = ?").get(deliveryId)
|
|
4455
|
+
);
|
|
4456
|
+
}
|
|
4457
|
+
requeueNotificationDelivery(input) {
|
|
4458
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4459
|
+
const reason = boundedSafeLabel(input.reason, "notification replay reason", 256);
|
|
4460
|
+
this.transaction(() => {
|
|
4461
|
+
const item = this.requireNotificationDelivery(input.delivery_id);
|
|
4462
|
+
if (!["dead_letter", "suppressed", "batched"].includes(item.status)) {
|
|
4463
|
+
throw new ProposalStoreError(
|
|
4464
|
+
"NOTIFICATION_DELIVERY_NOT_REQUEUEABLE",
|
|
4465
|
+
`notification delivery ${item.delivery_id} is ${item.status}`
|
|
4466
|
+
);
|
|
4467
|
+
}
|
|
4468
|
+
assertNotificationReplayOperatorDecision(item, input.identity, reason);
|
|
4469
|
+
const event = this.requireAttentionEvent(item.event_id);
|
|
4470
|
+
this.db.prepare(`
|
|
4471
|
+
UPDATE notification_deliveries
|
|
4472
|
+
SET status = 'pending', attempts = 0, next_attempt_at = ?,
|
|
4473
|
+
lease_owner = NULL, lease_id = NULL, lease_expires_at = NULL,
|
|
4474
|
+
last_error_code = NULL, delivered_at = NULL, updated_at = ?
|
|
4475
|
+
WHERE delivery_id = ?
|
|
4476
|
+
`).run(now, now, item.delivery_id);
|
|
4477
|
+
this.recordAttentionEventInternal({
|
|
4478
|
+
event_type: "notification.replayed",
|
|
4479
|
+
severity: "informational",
|
|
4480
|
+
environment: event.environment,
|
|
4481
|
+
...event.proposal_id ? { proposal_id: event.proposal_id } : {},
|
|
4482
|
+
...event.job_id ? { job_id: event.job_id } : {},
|
|
4483
|
+
...event.operation_id ? { operation_id: event.operation_id } : {},
|
|
4484
|
+
...event.correlation_id ? { correlation_id: event.correlation_id } : {},
|
|
4485
|
+
...event.capability ? { capability: event.capability } : {},
|
|
4486
|
+
...event.contract_digest ? { contract_digest: event.contract_digest } : {},
|
|
4487
|
+
attention_required: false,
|
|
4488
|
+
immediate_default: false,
|
|
4489
|
+
summary: `Notification delivery ${item.delivery_id} requeued by a verified operator`,
|
|
4490
|
+
...event.workbench_path ? { workbench_path: event.workbench_path } : {},
|
|
4491
|
+
details: {
|
|
4492
|
+
delivery_id: item.delivery_id,
|
|
4493
|
+
sink_id: item.sink_id,
|
|
4494
|
+
replayed_event_id: item.event_id,
|
|
4495
|
+
operator_subject: input.identity.subject,
|
|
4496
|
+
identity_provider: input.identity.provider,
|
|
4497
|
+
operator_decision_hash: input.identity.decision_hash,
|
|
4498
|
+
reason,
|
|
4499
|
+
approval_replayed: false,
|
|
4500
|
+
mutation_replayed: false,
|
|
4501
|
+
source_database_changed: false
|
|
4502
|
+
},
|
|
4503
|
+
source_event_key: `notification-replay:${item.delivery_id}:${input.identity.decision_hash}`,
|
|
4504
|
+
now
|
|
4505
|
+
});
|
|
4506
|
+
});
|
|
4507
|
+
return this.requireNotificationDelivery(input.delivery_id);
|
|
4508
|
+
}
|
|
4509
|
+
recordAttentionEventInternal(input) {
|
|
4510
|
+
assertAttentionEventInput(input);
|
|
4511
|
+
const occurredAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4512
|
+
const environment = boundedSafeLabel(input.environment, "attention environment", 64);
|
|
4513
|
+
const capability = input.capability ? boundedSafeLabel(input.capability, "attention capability", 256) : void 0;
|
|
4514
|
+
const contractDigest = input.contract_digest;
|
|
4515
|
+
const attentionRequired = input.attention_required ?? defaultAttentionEventTypes.has(input.event_type);
|
|
4516
|
+
const immediateDefault = input.immediate_default ?? defaultImmediateAttentionEventTypes.has(input.event_type);
|
|
4517
|
+
const details = Object.fromEntries(
|
|
4518
|
+
Object.entries(input.details ?? {}).sort(([left], [right]) => left.localeCompare(right))
|
|
4519
|
+
);
|
|
4520
|
+
const sourceEventKey = input.source_event_key ? boundedSafeLabel(input.source_event_key, "attention source event key", 512) : canonicalJsonDigest({
|
|
4521
|
+
event_type: input.event_type,
|
|
4522
|
+
occurred_at: occurredAt,
|
|
4523
|
+
proposal_id: input.proposal_id ?? null,
|
|
4524
|
+
job_id: input.job_id ?? null,
|
|
4525
|
+
operation_id: input.operation_id ?? null,
|
|
4526
|
+
capability: capability ?? null,
|
|
4527
|
+
details
|
|
4528
|
+
});
|
|
4529
|
+
const eventId = attentionEventId(sourceEventKey, input.event_type);
|
|
4530
|
+
const attentionKey = attentionRequired ? boundedSafeLabel(
|
|
4531
|
+
input.attention_key ?? defaultAttentionKey({
|
|
4532
|
+
environment,
|
|
4533
|
+
event_type: input.event_type,
|
|
4534
|
+
capability,
|
|
4535
|
+
contract_digest: contractDigest,
|
|
4536
|
+
details
|
|
4537
|
+
}),
|
|
4538
|
+
"attention coalescing key",
|
|
4539
|
+
512
|
|
4540
|
+
) : void 0;
|
|
4541
|
+
const unsigned = {
|
|
4542
|
+
schema_version: "synapsor.attention-event.v1",
|
|
4543
|
+
event_id: eventId,
|
|
4544
|
+
event_type: input.event_type,
|
|
4545
|
+
severity: input.severity,
|
|
4546
|
+
occurred_at: occurredAt,
|
|
4547
|
+
environment,
|
|
4548
|
+
...input.proposal_id ? { proposal_id: boundedSafeLabel(input.proposal_id, "attention proposal id", 256) } : {},
|
|
4549
|
+
...input.job_id ? { job_id: boundedSafeLabel(input.job_id, "attention job id", 256) } : {},
|
|
4550
|
+
...input.operation_id ? { operation_id: boundedSafeLabel(input.operation_id, "attention operation id", 256) } : {},
|
|
4551
|
+
...input.correlation_id ? { correlation_id: boundedSafeLabel(input.correlation_id, "attention correlation id", 256) } : {},
|
|
4552
|
+
...capability ? { capability } : {},
|
|
4553
|
+
...contractDigest ? { contract_digest: contractDigest } : {},
|
|
4554
|
+
...attentionKey ? { attention_key: attentionKey } : {},
|
|
4555
|
+
attention_required: attentionRequired,
|
|
4556
|
+
immediate_default: immediateDefault,
|
|
4557
|
+
summary: boundedSafeLabel(input.summary ?? attentionEventTitle(input.event_type), "attention summary", 512),
|
|
4558
|
+
...input.approval_source ? { approval_source: input.approval_source } : {},
|
|
4559
|
+
...input.worker_state ? { worker_state: boundedSafeLabel(input.worker_state, "attention worker state", 128) } : {},
|
|
4560
|
+
...input.failure_class ? { failure_class: boundedSafeLabel(input.failure_class, "attention failure class", 128) } : {},
|
|
4561
|
+
...input.expires_at ? { expires_at: input.expires_at } : {},
|
|
4562
|
+
...input.workbench_path ? { workbench_path: boundedWorkbenchPath(input.workbench_path) } : {},
|
|
4563
|
+
details
|
|
4564
|
+
};
|
|
4565
|
+
assertNoSecretMaterial(unsigned, `attention_event.${eventId}`);
|
|
4566
|
+
const event = {
|
|
4567
|
+
...unsigned,
|
|
4568
|
+
payload_hash: canonicalJsonDigest(unsigned)
|
|
4569
|
+
};
|
|
4570
|
+
const existing = rowToAttentionEvent(this.db.prepare("SELECT * FROM attention_events WHERE event_id = ?").get(eventId));
|
|
4571
|
+
if (existing) {
|
|
4572
|
+
if (existing.payload_hash !== event.payload_hash) {
|
|
4573
|
+
throw new ProposalStoreError(
|
|
4574
|
+
"ATTENTION_EVENT_IDEMPOTENCY_MISMATCH",
|
|
4575
|
+
`attention event ${eventId} already exists with different immutable content`
|
|
4576
|
+
);
|
|
4577
|
+
}
|
|
4578
|
+
return existing;
|
|
4579
|
+
}
|
|
4580
|
+
this.db.prepare(`
|
|
4581
|
+
INSERT INTO attention_events (
|
|
4582
|
+
event_id, schema_version, event_type, severity, occurred_at, environment,
|
|
4583
|
+
proposal_id, job_id, operation_id, correlation_id, capability,
|
|
4584
|
+
contract_digest, attention_key, attention_required, immediate_default,
|
|
4585
|
+
summary, approval_source, worker_state, failure_class, expires_at,
|
|
4586
|
+
workbench_path, details_json, payload_hash, created_at
|
|
4587
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
4588
|
+
`).run(
|
|
4589
|
+
event.event_id,
|
|
4590
|
+
event.schema_version,
|
|
4591
|
+
event.event_type,
|
|
4592
|
+
event.severity,
|
|
4593
|
+
event.occurred_at,
|
|
4594
|
+
event.environment,
|
|
4595
|
+
event.proposal_id ?? null,
|
|
4596
|
+
event.job_id ?? null,
|
|
4597
|
+
event.operation_id ?? null,
|
|
4598
|
+
event.correlation_id ?? null,
|
|
4599
|
+
event.capability ?? null,
|
|
4600
|
+
event.contract_digest ?? null,
|
|
4601
|
+
event.attention_key ?? null,
|
|
4602
|
+
event.attention_required ? 1 : 0,
|
|
4603
|
+
event.immediate_default ? 1 : 0,
|
|
4604
|
+
event.summary,
|
|
4605
|
+
event.approval_source ?? null,
|
|
4606
|
+
event.worker_state ?? null,
|
|
4607
|
+
event.failure_class ?? null,
|
|
4608
|
+
event.expires_at ?? null,
|
|
4609
|
+
event.workbench_path ?? null,
|
|
4610
|
+
JSON.stringify(event.details),
|
|
4611
|
+
event.payload_hash,
|
|
4612
|
+
event.occurred_at
|
|
4613
|
+
);
|
|
4614
|
+
if (event.attention_required && event.attention_key) this.projectAttentionItem(event);
|
|
4615
|
+
if (event.proposal_id && event.event_type === "proposal.expired") {
|
|
4616
|
+
this.closeProposalExpiryAttention(event, "expired");
|
|
4617
|
+
} else if (event.proposal_id && (event.event_type === "proposal.applied" || event.event_type === "proposal.cancelled" || event.event_type === "proposal.refused")) {
|
|
4618
|
+
this.closeProposalExpiryAttention(event, "resolved");
|
|
4619
|
+
}
|
|
4620
|
+
return event;
|
|
4621
|
+
}
|
|
4622
|
+
closeProposalExpiryAttention(event, status) {
|
|
4623
|
+
if (!event.proposal_id) return;
|
|
4624
|
+
this.db.prepare(`
|
|
4625
|
+
UPDATE attention_items
|
|
4626
|
+
SET status = ?,
|
|
4627
|
+
event_type = ?,
|
|
4628
|
+
title = ?,
|
|
4629
|
+
latest_event_id = ?,
|
|
4630
|
+
resolved_at = CASE WHEN ? = 'resolved' THEN ? ELSE NULL END,
|
|
4631
|
+
last_seen_at = MAX(last_seen_at, ?)
|
|
4632
|
+
WHERE status IN ('open', 'acknowledged')
|
|
4633
|
+
AND attention_key IN (
|
|
4634
|
+
SELECT DISTINCT attention_key
|
|
4635
|
+
FROM attention_events
|
|
4636
|
+
WHERE proposal_id = ?
|
|
4637
|
+
AND event_type = 'proposal.expiring'
|
|
4638
|
+
AND attention_key IS NOT NULL
|
|
4639
|
+
)
|
|
4640
|
+
`).run(
|
|
4641
|
+
status,
|
|
4642
|
+
event.event_type,
|
|
4643
|
+
event.summary,
|
|
4644
|
+
event.event_id,
|
|
4645
|
+
status,
|
|
4646
|
+
event.occurred_at,
|
|
4647
|
+
event.occurred_at,
|
|
4648
|
+
event.proposal_id
|
|
4649
|
+
);
|
|
4650
|
+
}
|
|
4651
|
+
projectAttentionItem(event) {
|
|
4652
|
+
const attentionKey = event.attention_key;
|
|
4653
|
+
if (!attentionKey) return;
|
|
4654
|
+
const existing = rowToAttentionItem(this.db.prepare("SELECT * FROM attention_items WHERE attention_key = ?").get(attentionKey));
|
|
4655
|
+
if (!existing) {
|
|
4656
|
+
this.db.prepare(`
|
|
4657
|
+
INSERT INTO attention_items (
|
|
4658
|
+
attention_id, attention_key, status, severity, environment, event_type,
|
|
4659
|
+
capability, contract_digest, title, occurrence_count, first_event_id,
|
|
4660
|
+
latest_event_id, first_seen_at, last_seen_at, acknowledged_by,
|
|
4661
|
+
acknowledged_at, resolved_at, expires_at
|
|
4662
|
+
) VALUES (?, ?, 'open', ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, NULL, NULL, NULL, ?)
|
|
4663
|
+
`).run(
|
|
4664
|
+
attentionItemId(attentionKey),
|
|
4665
|
+
attentionKey,
|
|
4666
|
+
event.severity,
|
|
4667
|
+
event.environment,
|
|
4668
|
+
event.event_type,
|
|
4669
|
+
event.capability ?? null,
|
|
4670
|
+
event.contract_digest ?? null,
|
|
4671
|
+
event.summary,
|
|
4672
|
+
event.event_id,
|
|
4673
|
+
event.event_id,
|
|
4674
|
+
event.occurred_at,
|
|
4675
|
+
event.occurred_at,
|
|
4676
|
+
event.expires_at ?? null
|
|
4677
|
+
);
|
|
4678
|
+
return;
|
|
4679
|
+
}
|
|
4680
|
+
const severity = attentionSeverityRank(event.severity) > attentionSeverityRank(existing.severity) ? event.severity : existing.severity;
|
|
4681
|
+
this.db.prepare(`
|
|
4682
|
+
UPDATE attention_items
|
|
4683
|
+
SET status = 'open', severity = ?, event_type = ?, title = ?,
|
|
4684
|
+
occurrence_count = occurrence_count + 1, latest_event_id = ?,
|
|
4685
|
+
last_seen_at = ?, acknowledged_by = NULL, acknowledged_at = NULL,
|
|
4686
|
+
acknowledgement_identity_json = NULL,
|
|
4687
|
+
acknowledgement_decision_hash = NULL,
|
|
4688
|
+
acknowledgement_signature = NULL,
|
|
4689
|
+
acknowledgement_integrity_hash = NULL,
|
|
4690
|
+
resolved_at = NULL,
|
|
4691
|
+
expires_at = COALESCE(?, expires_at)
|
|
4692
|
+
WHERE attention_id = ?
|
|
4693
|
+
`).run(
|
|
4694
|
+
severity,
|
|
4695
|
+
event.event_type,
|
|
4696
|
+
event.summary,
|
|
4697
|
+
event.event_id,
|
|
4698
|
+
event.occurred_at,
|
|
4699
|
+
event.expires_at ?? null,
|
|
4700
|
+
existing.attention_id
|
|
4701
|
+
);
|
|
3458
4702
|
}
|
|
3459
|
-
|
|
3460
|
-
const
|
|
3461
|
-
if (
|
|
3462
|
-
|
|
3463
|
-
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3464
|
-
const unsigned = {
|
|
3465
|
-
...policyRecommendationUnsigned(recommendation),
|
|
3466
|
-
status: input.action === "approve" ? "approved" : "rejected",
|
|
3467
|
-
decision: { actor: input.actor, action: input.action, reason: input.reason, identity: input.identity, decided_at: now },
|
|
3468
|
-
updated_at: now
|
|
3469
|
-
};
|
|
3470
|
-
const updated = { ...unsigned, integrity_hash: canonicalJsonDigest(unsigned) };
|
|
3471
|
-
this.db.prepare("UPDATE policy_recommendations SET status = ?, payload_json = ?, integrity_hash = ?, updated_at = ? WHERE recommendation_id = ?").run(updated.status, JSON.stringify(unsigned), updated.integrity_hash, now, recommendationId);
|
|
3472
|
-
return updated;
|
|
4703
|
+
requireAttentionItem(attentionId) {
|
|
4704
|
+
const item = this.getAttentionItem(attentionId);
|
|
4705
|
+
if (!item) throw new ProposalStoreError("ATTENTION_ITEM_NOT_FOUND", `attention item not found: ${attentionId}`);
|
|
4706
|
+
return item;
|
|
3473
4707
|
}
|
|
3474
|
-
|
|
3475
|
-
const
|
|
3476
|
-
if (
|
|
3477
|
-
|
|
3478
|
-
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
3479
|
-
const unsigned = {
|
|
3480
|
-
...policyRecommendationUnsigned(recommendation),
|
|
3481
|
-
status: "exported",
|
|
3482
|
-
export: { actor: input.actor, artifact_digest: input.artifact_digest, exported_at: now },
|
|
3483
|
-
updated_at: now
|
|
3484
|
-
};
|
|
3485
|
-
const updated = { ...unsigned, integrity_hash: canonicalJsonDigest(unsigned) };
|
|
3486
|
-
this.db.prepare("UPDATE policy_recommendations SET status = ?, payload_json = ?, integrity_hash = ?, updated_at = ? WHERE recommendation_id = ?").run(updated.status, JSON.stringify(unsigned), updated.integrity_hash, now, recommendationId);
|
|
3487
|
-
return updated;
|
|
4708
|
+
requireAttentionEvent(eventId) {
|
|
4709
|
+
const event = rowToAttentionEvent(this.db.prepare("SELECT * FROM attention_events WHERE event_id = ?").get(eventId));
|
|
4710
|
+
if (!event) throw new ProposalStoreError("ATTENTION_EVENT_NOT_FOUND", `attention event not found: ${eventId}`);
|
|
4711
|
+
return event;
|
|
3488
4712
|
}
|
|
3489
|
-
|
|
3490
|
-
const
|
|
3491
|
-
if (!
|
|
3492
|
-
|
|
4713
|
+
requireNotificationDelivery(deliveryId) {
|
|
4714
|
+
const delivery = this.getNotificationDelivery(deliveryId);
|
|
4715
|
+
if (!delivery) {
|
|
4716
|
+
throw new ProposalStoreError("NOTIFICATION_DELIVERY_NOT_FOUND", `notification delivery not found: ${deliveryId}`);
|
|
4717
|
+
}
|
|
4718
|
+
return delivery;
|
|
4719
|
+
}
|
|
4720
|
+
assertNotificationLease(deliveryId, owner, leaseId, now) {
|
|
4721
|
+
const item = this.requireNotificationDelivery(deliveryId);
|
|
4722
|
+
if (item.status !== "leased" || item.lease_owner !== owner || item.lease_id !== leaseId) {
|
|
4723
|
+
throw new ProposalStoreError(
|
|
4724
|
+
"NOTIFICATION_LEASE_MISMATCH",
|
|
4725
|
+
`dispatcher ${owner} does not hold lease ${leaseId} for ${deliveryId}`
|
|
4726
|
+
);
|
|
4727
|
+
}
|
|
4728
|
+
if (!item.lease_expires_at || Date.parse(item.lease_expires_at) <= Date.parse(now)) {
|
|
4729
|
+
throw new ProposalStoreError("NOTIFICATION_LEASE_EXPIRED", `notification lease ${leaseId} has expired`);
|
|
4730
|
+
}
|
|
4731
|
+
return item;
|
|
3493
4732
|
}
|
|
3494
4733
|
enqueueCloudOutbox(input) {
|
|
3495
4734
|
const eventId = input.event_id.trim();
|
|
@@ -3666,6 +4905,92 @@ var ProposalStore = class {
|
|
|
3666
4905
|
if (!item) throw new ProposalStoreError("CLOUD_OUTBOX_EVENT_NOT_FOUND", `cloud outbox event not found: ${eventId}`);
|
|
3667
4906
|
return item;
|
|
3668
4907
|
}
|
|
4908
|
+
workerControlState() {
|
|
4909
|
+
const raw = this.getRunnerState("supervised_worker_control");
|
|
4910
|
+
if (!raw) return defaultWorkerControlState();
|
|
4911
|
+
return parseWorkerControlState(raw);
|
|
4912
|
+
}
|
|
4913
|
+
updateWorkerControl(input) {
|
|
4914
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
4915
|
+
const current = this.workerControlState();
|
|
4916
|
+
assertWorkerControlTarget(input);
|
|
4917
|
+
assertWorkerControlOperatorDecision(
|
|
4918
|
+
current,
|
|
4919
|
+
input,
|
|
4920
|
+
input.actor,
|
|
4921
|
+
input.identity,
|
|
4922
|
+
input.require_verified_identity === true
|
|
4923
|
+
);
|
|
4924
|
+
let mode = current.mode;
|
|
4925
|
+
let controls = [...current.capability_controls];
|
|
4926
|
+
if (input.action === "pause") mode = "paused";
|
|
4927
|
+
else if (input.action === "resume") mode = "active";
|
|
4928
|
+
else if (input.action === "drain") mode = "draining";
|
|
4929
|
+
else {
|
|
4930
|
+
const capability = input.capability;
|
|
4931
|
+
const contractDigest = input.contract_digest;
|
|
4932
|
+
const keyMatches = (entry) => entry.capability === capability && entry.contract_digest === contractDigest;
|
|
4933
|
+
const existing = controls.find(keyMatches);
|
|
4934
|
+
if (existing?.status === "revoked" && input.action === "capability_enable") {
|
|
4935
|
+
throw new ProposalStoreError(
|
|
4936
|
+
"WORKER_DIGEST_REVOKED",
|
|
4937
|
+
`revoked supervised-worker digest ${contractDigest} cannot be re-enabled`
|
|
4938
|
+
);
|
|
4939
|
+
}
|
|
4940
|
+
const status = input.action === "capability_enable" ? "enabled" : input.action === "capability_disable" ? "disabled" : "revoked";
|
|
4941
|
+
controls = [
|
|
4942
|
+
...controls.filter((entry) => !keyMatches(entry)),
|
|
4943
|
+
{
|
|
4944
|
+
capability,
|
|
4945
|
+
contract_digest: contractDigest,
|
|
4946
|
+
status,
|
|
4947
|
+
updated_at: now,
|
|
4948
|
+
actor: input.actor
|
|
4949
|
+
}
|
|
4950
|
+
].sort((left, right) => left.capability.localeCompare(right.capability) || left.contract_digest.localeCompare(right.contract_digest));
|
|
4951
|
+
}
|
|
4952
|
+
const unsigned = {
|
|
4953
|
+
schema_version: "synapsor.worker-control.v1",
|
|
4954
|
+
mode,
|
|
4955
|
+
revision: current.revision + 1,
|
|
4956
|
+
capability_controls: controls,
|
|
4957
|
+
...input.identity ? { last_decision: input.identity } : {},
|
|
4958
|
+
updated_at: now
|
|
4959
|
+
};
|
|
4960
|
+
const updated = {
|
|
4961
|
+
...unsigned,
|
|
4962
|
+
integrity_hash: canonicalJsonDigest(unsigned)
|
|
4963
|
+
};
|
|
4964
|
+
assertNoSecretMaterial(updated, "runner_state.supervised_worker_control");
|
|
4965
|
+
this.transaction(() => {
|
|
4966
|
+
this.db.prepare(`
|
|
4967
|
+
INSERT INTO runner_state (key, value_json, updated_at)
|
|
4968
|
+
VALUES ('supervised_worker_control', ?, ?)
|
|
4969
|
+
ON CONFLICT(key) DO UPDATE SET value_json = excluded.value_json, updated_at = excluded.updated_at
|
|
4970
|
+
`).run(JSON.stringify(updated), now);
|
|
4971
|
+
const global = input.action === "pause" || input.action === "resume" || input.action === "drain";
|
|
4972
|
+
const eventType = input.action === "resume" || input.action === "capability_enable" ? "capability.activated" : global ? "worker.paused" : "capability.revoked";
|
|
4973
|
+
this.recordAttentionEventInternal({
|
|
4974
|
+
event_type: eventType,
|
|
4975
|
+
severity: "informational",
|
|
4976
|
+
environment: input.environment ? boundedSafeLabel(input.environment, "worker control environment", 64) : "unknown",
|
|
4977
|
+
...input.capability ? { capability: input.capability } : {},
|
|
4978
|
+
...input.contract_digest ? { contract_digest: input.contract_digest } : {},
|
|
4979
|
+
attention_required: false,
|
|
4980
|
+
immediate_default: false,
|
|
4981
|
+
summary: workerControlSummary(input),
|
|
4982
|
+
worker_state: mode,
|
|
4983
|
+
details: {
|
|
4984
|
+
control_action: input.action,
|
|
4985
|
+
control_revision: updated.revision,
|
|
4986
|
+
source_database_changed: false
|
|
4987
|
+
},
|
|
4988
|
+
source_event_key: `worker-control:${updated.revision}:${updated.integrity_hash}`,
|
|
4989
|
+
now
|
|
4990
|
+
});
|
|
4991
|
+
});
|
|
4992
|
+
return updated;
|
|
4993
|
+
}
|
|
3669
4994
|
setRunnerState(key, value) {
|
|
3670
4995
|
assertNoSecretMaterial(value, `runner_state.${key}`);
|
|
3671
4996
|
this.db.prepare(`
|
|
@@ -3697,6 +5022,9 @@ var ProposalStore = class {
|
|
|
3697
5022
|
{ table: "shadow_study_cases", kind: "shadow_study_case", key: "case_id", created: "created_at", proposal: "proposal_id", tenant: "tenant_id", capability: "capability" },
|
|
3698
5023
|
{ table: "shadow_outcomes", kind: "shadow_outcome", key: "outcome_id", created: "created_at", proposal: "proposal_id", tenant: "tenant_id" },
|
|
3699
5024
|
{ table: "worker_queue", kind: "worker_queue_item", key: "proposal_id", created: "created_at", proposal: "proposal_id" },
|
|
5025
|
+
{ table: "attention_events", kind: "attention_event", key: "event_id", created: "created_at", proposal: "proposal_id", capability: "capability" },
|
|
5026
|
+
{ table: "attention_items", kind: "attention_item", key: "attention_id", created: "first_seen_at", capability: "capability" },
|
|
5027
|
+
{ table: "notification_deliveries", kind: "notification_delivery", key: "delivery_id", created: "created_at" },
|
|
3700
5028
|
{ table: "runner_state", kind: "runner_state", key: "key", created: "updated_at" },
|
|
3701
5029
|
{ table: "policy_recommendations", kind: "policy_recommendation", key: "recommendation_id", created: "created_at", tenant: "tenant_id", capability: "capability" },
|
|
3702
5030
|
{ table: "cloud_outbox", kind: "cloud_outbox_event", key: "event_id", created: "created_at", proposal: "proposal_id" },
|
|
@@ -4269,10 +5597,52 @@ var ProposalStore = class {
|
|
|
4269
5597
|
});
|
|
4270
5598
|
}
|
|
4271
5599
|
appendEvent(proposalId, kind, actor, payload) {
|
|
5600
|
+
const append = () => {
|
|
5601
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
5602
|
+
const result = this.db.prepare(`
|
|
5603
|
+
INSERT INTO proposal_events (proposal_id, kind, actor, payload_json, created_at)
|
|
5604
|
+
VALUES (?, ?, ?, ?, ?)
|
|
5605
|
+
`).run(proposalId, kind, actor, JSON.stringify(payload), createdAt);
|
|
5606
|
+
const proposal = this.requireProposal(proposalId);
|
|
5607
|
+
for (const event of attentionEventsForProposalEvent({
|
|
5608
|
+
proposal,
|
|
5609
|
+
proposal_event_id: String(result.lastInsertRowid),
|
|
5610
|
+
kind,
|
|
5611
|
+
actor,
|
|
5612
|
+
payload,
|
|
5613
|
+
environment: this.attentionEnvironment(),
|
|
5614
|
+
occurred_at: createdAt
|
|
5615
|
+
})) {
|
|
5616
|
+
this.recordAttentionEventInternal(event);
|
|
5617
|
+
}
|
|
5618
|
+
if (proposal.state !== "pending_review") {
|
|
5619
|
+
this.resolveSatisfiedProposalReviewAttention(proposal, createdAt);
|
|
5620
|
+
}
|
|
5621
|
+
};
|
|
5622
|
+
if (this.db.isTransaction) append();
|
|
5623
|
+
else this.transaction(append);
|
|
5624
|
+
}
|
|
5625
|
+
attentionEnvironment() {
|
|
5626
|
+
const context = this.getRunnerState("attention_context");
|
|
5627
|
+
const environment = context?.environment;
|
|
5628
|
+
return typeof environment === "string" && /^(development|staging|production|unknown)$/.test(environment) ? environment : "unknown";
|
|
5629
|
+
}
|
|
5630
|
+
resolveSatisfiedProposalReviewAttention(proposal, now) {
|
|
5631
|
+
const attentionKey = proposalReviewAttentionKey(proposal, this.attentionEnvironment());
|
|
5632
|
+
const pending = this.db.prepare(`
|
|
5633
|
+
SELECT COUNT(DISTINCT p.proposal_id) AS count
|
|
5634
|
+
FROM attention_events ae
|
|
5635
|
+
JOIN proposals p ON p.proposal_id = ae.proposal_id
|
|
5636
|
+
WHERE ae.attention_key = ?
|
|
5637
|
+
AND p.state = 'pending_review'
|
|
5638
|
+
`).get(attentionKey);
|
|
5639
|
+
if (isRecord(pending) && Number(pending.count ?? 0) > 0) return;
|
|
4272
5640
|
this.db.prepare(`
|
|
4273
|
-
|
|
4274
|
-
|
|
4275
|
-
|
|
5641
|
+
UPDATE attention_items
|
|
5642
|
+
SET status = 'resolved', resolved_at = ?, last_seen_at = MAX(last_seen_at, ?)
|
|
5643
|
+
WHERE attention_key = ?
|
|
5644
|
+
AND status IN ('open', 'acknowledged')
|
|
5645
|
+
`).run(now, now, attentionKey);
|
|
4276
5646
|
}
|
|
4277
5647
|
queryAudit(proposalId) {
|
|
4278
5648
|
const rows = this.db.prepare("SELECT * FROM query_audit WHERE proposal_id = ? ORDER BY audit_id ASC").all(proposalId);
|
|
@@ -5012,23 +6382,858 @@ function assertPolicyRecommendationIdentity(recommendation, input) {
|
|
|
5012
6382
|
const canonicalCore = JSON.parse(JSON.stringify(core));
|
|
5013
6383
|
if (proof.integrity_hash !== canonicalJsonDigest(canonicalCore)) throw new ProposalStoreError("POLICY_RECOMMENDATION_IDENTITY_TAMPERED", "verified operator identity proof failed its integrity check");
|
|
5014
6384
|
}
|
|
6385
|
+
function attentionEventsForProposalEvent(input) {
|
|
6386
|
+
const proposal = input.proposal;
|
|
6387
|
+
const contractDigest = proposal.change_set.contract?.digest;
|
|
6388
|
+
const scopeDigest = canonicalJsonDigest({
|
|
6389
|
+
tenant_id: proposal.tenant_id,
|
|
6390
|
+
principal: proposal.principal ?? null
|
|
6391
|
+
});
|
|
6392
|
+
const sourceKey = (eventType) => `proposal:${proposal.proposal_id}:event:${input.proposal_event_id}:${eventType}`;
|
|
6393
|
+
const base = (eventType, severity) => ({
|
|
6394
|
+
event_type: eventType,
|
|
6395
|
+
severity,
|
|
6396
|
+
environment: input.environment,
|
|
6397
|
+
proposal_id: proposal.proposal_id,
|
|
6398
|
+
capability: proposal.capability ?? proposal.action,
|
|
6399
|
+
...contractDigest ? { contract_digest: contractDigest } : {},
|
|
6400
|
+
source_event_key: sourceKey(eventType),
|
|
6401
|
+
now: input.occurred_at
|
|
6402
|
+
});
|
|
6403
|
+
const safeCode = safeAttentionPayloadString(input.payload, "safe_error_code") ?? safeAttentionPayloadString(input.payload, "error_code") ?? safeAttentionPayloadString(input.payload, "reason_code");
|
|
6404
|
+
const workerDetails = safeAttentionDetails(input.payload, [
|
|
6405
|
+
"attempt",
|
|
6406
|
+
"max_attempts",
|
|
6407
|
+
"execution_mode",
|
|
6408
|
+
"outcome"
|
|
6409
|
+
]);
|
|
6410
|
+
const requiredRole = typeof proposal.change_set.approval.required_role === "string" ? proposal.change_set.approval.required_role : "reviewer";
|
|
6411
|
+
const reviewAttention = (summary) => {
|
|
6412
|
+
const attentionKey = proposalReviewAttentionKey(proposal, input.environment);
|
|
6413
|
+
return {
|
|
6414
|
+
...base("proposal.review_required", "warning"),
|
|
6415
|
+
attention_required: true,
|
|
6416
|
+
immediate_default: true,
|
|
6417
|
+
attention_key: attentionKey,
|
|
6418
|
+
summary: summary ?? `${proposal.capability ?? proposal.action} proposals need ${requiredRole} review`,
|
|
6419
|
+
workbench_path: `/attention/${attentionItemId(attentionKey)}`,
|
|
6420
|
+
details: {
|
|
6421
|
+
required_role: requiredRole,
|
|
6422
|
+
...safeCode ? { failure_class: safeCode } : {}
|
|
6423
|
+
},
|
|
6424
|
+
...safeCode ? { failure_class: safeCode } : {}
|
|
6425
|
+
};
|
|
6426
|
+
};
|
|
6427
|
+
const criticalWorkerAttention = (eventType, summary) => {
|
|
6428
|
+
const failureClass = safeCode ?? (eventType === "worker.dead_lettered" ? "RETRY_BUDGET_EXHAUSTED" : eventType === "worker.unknown_outcome" ? "OUTCOME_UNKNOWN" : "RECONCILIATION_REQUIRED");
|
|
6429
|
+
const attentionKey = [
|
|
6430
|
+
input.environment,
|
|
6431
|
+
eventType,
|
|
6432
|
+
proposal.capability ?? proposal.action,
|
|
6433
|
+
contractDigest ?? "no_digest",
|
|
6434
|
+
failureClass,
|
|
6435
|
+
scopeDigest
|
|
6436
|
+
].join(":");
|
|
6437
|
+
return {
|
|
6438
|
+
...base(eventType, "critical"),
|
|
6439
|
+
attention_required: true,
|
|
6440
|
+
immediate_default: true,
|
|
6441
|
+
attention_key: attentionKey,
|
|
6442
|
+
summary,
|
|
6443
|
+
workbench_path: `/attention/${attentionItemId(attentionKey)}`,
|
|
6444
|
+
worker_state: eventType.slice("worker.".length),
|
|
6445
|
+
failure_class: failureClass,
|
|
6446
|
+
details: {
|
|
6447
|
+
...workerDetails,
|
|
6448
|
+
failure_class: failureClass
|
|
6449
|
+
}
|
|
6450
|
+
};
|
|
6451
|
+
};
|
|
6452
|
+
if (input.kind === "proposal_created") {
|
|
6453
|
+
const events = [{
|
|
6454
|
+
...base("proposal.created", "informational"),
|
|
6455
|
+
attention_required: false,
|
|
6456
|
+
immediate_default: false,
|
|
6457
|
+
details: { source_database_changed: false }
|
|
6458
|
+
}];
|
|
6459
|
+
const approvalMode = proposal.change_set.approval.mode;
|
|
6460
|
+
if (proposal.state === "pending_review" && approvalMode !== "policy") events.push(reviewAttention());
|
|
6461
|
+
return events;
|
|
6462
|
+
}
|
|
6463
|
+
if (input.kind === "proposal_approved") {
|
|
6464
|
+
const policyApproved = input.actor.startsWith("policy:");
|
|
6465
|
+
return [{
|
|
6466
|
+
...base(policyApproved ? "proposal.auto_approved" : "proposal.approved", "informational"),
|
|
6467
|
+
attention_required: false,
|
|
6468
|
+
immediate_default: false,
|
|
6469
|
+
approval_source: policyApproved ? "policy_auto" : "human",
|
|
6470
|
+
details: safeAttentionDetails(input.payload, ["approvals", "required_approvals", "remaining_approvals"])
|
|
6471
|
+
}];
|
|
6472
|
+
}
|
|
6473
|
+
if (input.kind === "policy_auto_approval_deferred" || input.kind === "proposal_approval_blocked_freshness") {
|
|
6474
|
+
return [reviewAttention(
|
|
6475
|
+
input.kind === "proposal_approval_blocked_freshness" ? `${proposal.capability ?? proposal.action} needs review because its source freshness check failed` : `${proposal.capability ?? proposal.action} exceeded automatic-approval policy and needs human review`
|
|
6476
|
+
)];
|
|
6477
|
+
}
|
|
6478
|
+
if (input.kind === "proposal_rejected" || input.kind === "proposal_failed") {
|
|
6479
|
+
return [{
|
|
6480
|
+
...base("proposal.refused", "warning"),
|
|
6481
|
+
attention_required: false,
|
|
6482
|
+
immediate_default: false,
|
|
6483
|
+
...safeCode ? { failure_class: safeCode, details: { failure_class: safeCode } } : {}
|
|
6484
|
+
}];
|
|
6485
|
+
}
|
|
6486
|
+
if (input.kind === "proposal_canceled" || input.kind === "writeback_canceled") {
|
|
6487
|
+
return [{
|
|
6488
|
+
...base("proposal.cancelled", "informational"),
|
|
6489
|
+
attention_required: false,
|
|
6490
|
+
immediate_default: false
|
|
6491
|
+
}];
|
|
6492
|
+
}
|
|
6493
|
+
if (input.kind === "proposal_conflict" || input.kind === "writeback_conflict" || input.kind === "writeback_intent_conflict") {
|
|
6494
|
+
return [{
|
|
6495
|
+
...base("proposal.conflict", "warning"),
|
|
6496
|
+
attention_required: false,
|
|
6497
|
+
immediate_default: false,
|
|
6498
|
+
...safeCode ? { failure_class: safeCode, details: { failure_class: safeCode } } : {}
|
|
6499
|
+
}];
|
|
6500
|
+
}
|
|
6501
|
+
if (input.kind === "proposal_pending_worker" || input.kind === "writeback_worker_queued") {
|
|
6502
|
+
return [{
|
|
6503
|
+
...base("proposal.queued", "informational"),
|
|
6504
|
+
attention_required: false,
|
|
6505
|
+
immediate_default: false,
|
|
6506
|
+
worker_state: "queued",
|
|
6507
|
+
details: safeAttentionDetails(input.payload, ["execution_mode", "max_attempts"])
|
|
6508
|
+
}];
|
|
6509
|
+
}
|
|
6510
|
+
if (input.kind === "writeback_retry_scheduled") {
|
|
6511
|
+
return [{
|
|
6512
|
+
...base("worker.retry_scheduled", "warning"),
|
|
6513
|
+
attention_required: false,
|
|
6514
|
+
immediate_default: false,
|
|
6515
|
+
worker_state: "retry_wait",
|
|
6516
|
+
...safeCode ? { failure_class: safeCode } : {},
|
|
6517
|
+
details: {
|
|
6518
|
+
...workerDetails,
|
|
6519
|
+
...safeCode ? { failure_class: safeCode } : {}
|
|
6520
|
+
}
|
|
6521
|
+
}];
|
|
6522
|
+
}
|
|
6523
|
+
if (input.kind === "writeback_dead_lettered") {
|
|
6524
|
+
return [criticalWorkerAttention("worker.dead_lettered")];
|
|
6525
|
+
}
|
|
6526
|
+
if (input.kind === "writeback_worker_blocked") {
|
|
6527
|
+
const failureClass = safeCode ?? "WORKER_POLICY_BLOCKED";
|
|
6528
|
+
if (/PROPOSAL_.*EXPIRED|PROPOSAL_EXPIRED/i.test(failureClass)) {
|
|
6529
|
+
return [{
|
|
6530
|
+
...base("proposal.expired", "warning"),
|
|
6531
|
+
attention_required: false,
|
|
6532
|
+
immediate_default: false,
|
|
6533
|
+
worker_state: "blocked",
|
|
6534
|
+
failure_class: failureClass,
|
|
6535
|
+
details: {
|
|
6536
|
+
...workerDetails,
|
|
6537
|
+
failure_class: failureClass
|
|
6538
|
+
}
|
|
6539
|
+
}];
|
|
6540
|
+
}
|
|
6541
|
+
const digestStale = /(?:CONTRACT|DIGEST|GENERATION_LOCK|SCHEMA|POLICY)_.*(?:STALE|MISMATCH|CHANGED)|ACTIVE_CONTRACT_DIGEST/i.test(failureClass);
|
|
6542
|
+
const limitExceeded = /(?:LIMIT|RATE|QUEUE|BUDGET)_.*(?:EXCEEDED|STALE|MISMATCH)|POLICY_LIMIT/i.test(failureClass);
|
|
6543
|
+
const eventType = digestStale ? "contract.digest_stale" : limitExceeded ? "policy.limit_exceeded" : "proposal.refused";
|
|
6544
|
+
const severity = digestStale || limitExceeded ? "critical" : "warning";
|
|
6545
|
+
const attentionRequired = digestStale || limitExceeded;
|
|
6546
|
+
const attentionKey = [
|
|
6547
|
+
input.environment,
|
|
6548
|
+
eventType,
|
|
6549
|
+
proposal.capability ?? proposal.action,
|
|
6550
|
+
contractDigest ?? "no_digest",
|
|
6551
|
+
failureClass,
|
|
6552
|
+
scopeDigest
|
|
6553
|
+
].join(":");
|
|
6554
|
+
return [{
|
|
6555
|
+
...base(eventType, severity),
|
|
6556
|
+
attention_required: attentionRequired,
|
|
6557
|
+
immediate_default: attentionRequired,
|
|
6558
|
+
...attentionRequired ? {
|
|
6559
|
+
attention_key: attentionKey,
|
|
6560
|
+
workbench_path: `/attention/${attentionItemId(attentionKey)}`
|
|
6561
|
+
} : {},
|
|
6562
|
+
worker_state: "blocked",
|
|
6563
|
+
failure_class: failureClass,
|
|
6564
|
+
details: {
|
|
6565
|
+
...workerDetails,
|
|
6566
|
+
failure_class: failureClass
|
|
6567
|
+
}
|
|
6568
|
+
}];
|
|
6569
|
+
}
|
|
6570
|
+
if (input.kind === "policy_limit_near") {
|
|
6571
|
+
return [{
|
|
6572
|
+
...base("policy.limit_near", "warning"),
|
|
6573
|
+
attention_required: false,
|
|
6574
|
+
immediate_default: false,
|
|
6575
|
+
summary: `${proposal.capability ?? proposal.action} is approaching a reviewed policy limit`,
|
|
6576
|
+
details: safeAttentionDetails(input.payload, [
|
|
6577
|
+
"kind",
|
|
6578
|
+
"scope",
|
|
6579
|
+
"observed",
|
|
6580
|
+
"proposed",
|
|
6581
|
+
"projected",
|
|
6582
|
+
"max",
|
|
6583
|
+
"window_start",
|
|
6584
|
+
"window_end"
|
|
6585
|
+
])
|
|
6586
|
+
}];
|
|
6587
|
+
}
|
|
6588
|
+
if (input.kind === "writeback_reconciliation_required" || input.kind === "writeback_intent_reconciliation_required") {
|
|
6589
|
+
return [criticalWorkerAttention("worker.reconciliation_required")];
|
|
6590
|
+
}
|
|
6591
|
+
if ((input.kind === "writeback_failed" || input.kind === "writeback_intent_failed") && (safeCode === "OUTCOME_UNKNOWN" || safeCode === "RECONCILIATION_REQUIRED")) {
|
|
6592
|
+
return [criticalWorkerAttention("worker.unknown_outcome")];
|
|
6593
|
+
}
|
|
6594
|
+
if (input.kind === "writeback_applied" || input.kind === "writeback_already_applied" || input.kind === "writeback_intent_applied" || input.kind === "writeback_intent_already_applied") {
|
|
6595
|
+
return [{
|
|
6596
|
+
...base("proposal.applied", "informational"),
|
|
6597
|
+
attention_required: false,
|
|
6598
|
+
immediate_default: false,
|
|
6599
|
+
worker_state: "completed",
|
|
6600
|
+
details: safeAttentionDetails(input.payload, ["rows_affected", "source_database_mutated"])
|
|
6601
|
+
}];
|
|
6602
|
+
}
|
|
6603
|
+
if (input.kind === "writeback_worker_completed") {
|
|
6604
|
+
const outcome = safeAttentionPayloadString(input.payload, "outcome");
|
|
6605
|
+
if (outcome === "conflict") {
|
|
6606
|
+
return [{
|
|
6607
|
+
...base("proposal.conflict", "warning"),
|
|
6608
|
+
attention_required: false,
|
|
6609
|
+
immediate_default: false,
|
|
6610
|
+
worker_state: "completed",
|
|
6611
|
+
details: { outcome }
|
|
6612
|
+
}];
|
|
6613
|
+
}
|
|
6614
|
+
if (outcome === "applied" || outcome === "already_applied") {
|
|
6615
|
+
return [{
|
|
6616
|
+
...base("proposal.applied", "informational"),
|
|
6617
|
+
attention_required: false,
|
|
6618
|
+
immediate_default: false,
|
|
6619
|
+
worker_state: "completed",
|
|
6620
|
+
details: { outcome }
|
|
6621
|
+
}];
|
|
6622
|
+
}
|
|
6623
|
+
}
|
|
6624
|
+
if (input.kind === "proposal_reconciliation_required") {
|
|
6625
|
+
return [criticalWorkerAttention("worker.reconciliation_required")];
|
|
6626
|
+
}
|
|
6627
|
+
return [];
|
|
6628
|
+
}
|
|
6629
|
+
function proposalReviewAttentionKey(proposal, environment) {
|
|
6630
|
+
const contractDigest = proposal.change_set.contract?.digest;
|
|
6631
|
+
const requiredRole = typeof proposal.change_set.approval.required_role === "string" ? proposal.change_set.approval.required_role : "reviewer";
|
|
6632
|
+
const scopeDigest = canonicalJsonDigest({
|
|
6633
|
+
tenant_id: proposal.tenant_id,
|
|
6634
|
+
principal: proposal.principal ?? null
|
|
6635
|
+
});
|
|
6636
|
+
return [
|
|
6637
|
+
environment,
|
|
6638
|
+
"proposal.review_required",
|
|
6639
|
+
proposal.capability ?? proposal.action,
|
|
6640
|
+
contractDigest ?? "no_digest",
|
|
6641
|
+
requiredRole,
|
|
6642
|
+
scopeDigest
|
|
6643
|
+
].join(":");
|
|
6644
|
+
}
|
|
6645
|
+
function safeAttentionPayloadString(payload, key) {
|
|
6646
|
+
const value = payload[key];
|
|
6647
|
+
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
6648
|
+
return value.trim().slice(0, 128);
|
|
6649
|
+
}
|
|
6650
|
+
function safeAttentionDetails(payload, keys) {
|
|
6651
|
+
const details = {};
|
|
6652
|
+
for (const key of keys) {
|
|
6653
|
+
const value = payload[key];
|
|
6654
|
+
if (value === null || typeof value === "boolean") details[key] = value;
|
|
6655
|
+
else if (typeof value === "number" && Number.isFinite(value)) details[key] = value;
|
|
6656
|
+
else if (typeof value === "string" && value.trim()) details[key] = value.trim().slice(0, 128);
|
|
6657
|
+
}
|
|
6658
|
+
return details;
|
|
6659
|
+
}
|
|
6660
|
+
var attentionEventTypes = /* @__PURE__ */ new Set([
|
|
6661
|
+
"proposal.created",
|
|
6662
|
+
"proposal.review_required",
|
|
6663
|
+
"proposal.auto_approved",
|
|
6664
|
+
"proposal.approved",
|
|
6665
|
+
"proposal.queued",
|
|
6666
|
+
"proposal.expiring",
|
|
6667
|
+
"proposal.expired",
|
|
6668
|
+
"proposal.cancelled",
|
|
6669
|
+
"proposal.applied",
|
|
6670
|
+
"proposal.conflict",
|
|
6671
|
+
"proposal.refused",
|
|
6672
|
+
"worker.started",
|
|
6673
|
+
"worker.paused",
|
|
6674
|
+
"worker.unhealthy",
|
|
6675
|
+
"worker.recovered",
|
|
6676
|
+
"worker.queue_backlog",
|
|
6677
|
+
"worker.retry_scheduled",
|
|
6678
|
+
"worker.dead_lettered",
|
|
6679
|
+
"worker.unknown_outcome",
|
|
6680
|
+
"worker.reconciliation_required",
|
|
6681
|
+
"capability.review_required",
|
|
6682
|
+
"capability.activated",
|
|
6683
|
+
"capability.revoked",
|
|
6684
|
+
"contract.digest_stale",
|
|
6685
|
+
"schema.drift_detected",
|
|
6686
|
+
"credential.posture_changed",
|
|
6687
|
+
"policy.limit_near",
|
|
6688
|
+
"policy.limit_exceeded",
|
|
6689
|
+
"sensitive_override_activated",
|
|
6690
|
+
"notification.replayed",
|
|
6691
|
+
"notification.digest"
|
|
6692
|
+
]);
|
|
6693
|
+
var defaultAttentionEventTypes = /* @__PURE__ */ new Set([
|
|
6694
|
+
"proposal.review_required",
|
|
6695
|
+
"proposal.expiring",
|
|
6696
|
+
"proposal.expired",
|
|
6697
|
+
"worker.unhealthy",
|
|
6698
|
+
"worker.queue_backlog",
|
|
6699
|
+
"worker.dead_lettered",
|
|
6700
|
+
"worker.unknown_outcome",
|
|
6701
|
+
"worker.reconciliation_required",
|
|
6702
|
+
"capability.review_required",
|
|
6703
|
+
"contract.digest_stale",
|
|
6704
|
+
"schema.drift_detected",
|
|
6705
|
+
"credential.posture_changed",
|
|
6706
|
+
"policy.limit_exceeded",
|
|
6707
|
+
"sensitive_override_activated"
|
|
6708
|
+
]);
|
|
6709
|
+
var defaultImmediateAttentionEventTypes = /* @__PURE__ */ new Set([
|
|
6710
|
+
"proposal.review_required",
|
|
6711
|
+
"proposal.expiring",
|
|
6712
|
+
"worker.unhealthy",
|
|
6713
|
+
"worker.queue_backlog",
|
|
6714
|
+
"worker.dead_lettered",
|
|
6715
|
+
"worker.unknown_outcome",
|
|
6716
|
+
"worker.reconciliation_required",
|
|
6717
|
+
"contract.digest_stale",
|
|
6718
|
+
"schema.drift_detected",
|
|
6719
|
+
"credential.posture_changed",
|
|
6720
|
+
"policy.limit_exceeded"
|
|
6721
|
+
]);
|
|
6722
|
+
function attentionEventTitle(eventType) {
|
|
6723
|
+
const titles = {
|
|
6724
|
+
"proposal.created": "Proposal created",
|
|
6725
|
+
"proposal.review_required": "Proposal needs human review",
|
|
6726
|
+
"proposal.auto_approved": "Proposal approved by reviewed policy",
|
|
6727
|
+
"proposal.approved": "Proposal approved",
|
|
6728
|
+
"proposal.queued": "Proposal queued for trusted execution",
|
|
6729
|
+
"proposal.expiring": "Approved proposal is approaching expiry",
|
|
6730
|
+
"proposal.expired": "Approved proposal expired without execution",
|
|
6731
|
+
"proposal.cancelled": "Proposal cancelled",
|
|
6732
|
+
"proposal.applied": "Proposal applied",
|
|
6733
|
+
"proposal.conflict": "Guarded writeback conflict",
|
|
6734
|
+
"proposal.refused": "Proposal refused",
|
|
6735
|
+
"worker.started": "Trusted worker started",
|
|
6736
|
+
"worker.paused": "Trusted worker paused",
|
|
6737
|
+
"worker.unhealthy": "Trusted worker needs attention",
|
|
6738
|
+
"worker.recovered": "Trusted worker supervision recovered",
|
|
6739
|
+
"worker.queue_backlog": "Trusted worker queue backlog",
|
|
6740
|
+
"worker.retry_scheduled": "Trusted worker retry scheduled",
|
|
6741
|
+
"worker.dead_lettered": "Trusted worker job dead-lettered",
|
|
6742
|
+
"worker.unknown_outcome": "Database transaction outcome is unknown",
|
|
6743
|
+
"worker.reconciliation_required": "Operator reconciliation is required",
|
|
6744
|
+
"capability.review_required": "Capability needs human review",
|
|
6745
|
+
"capability.activated": "Capability activated",
|
|
6746
|
+
"capability.revoked": "Capability revoked",
|
|
6747
|
+
"contract.digest_stale": "Active contract or policy authority is stale",
|
|
6748
|
+
"schema.drift_detected": "Schema drift blocks active authority",
|
|
6749
|
+
"credential.posture_changed": "Credential posture changed",
|
|
6750
|
+
"policy.limit_near": "Policy limit is approaching",
|
|
6751
|
+
"policy.limit_exceeded": "Policy limit exceeded",
|
|
6752
|
+
"sensitive_override_activated": "Sensitive-field override activated",
|
|
6753
|
+
"notification.replayed": "Notification delivery requeued by a verified operator",
|
|
6754
|
+
"notification.digest": "Runner activity digest"
|
|
6755
|
+
};
|
|
6756
|
+
return titles[eventType];
|
|
6757
|
+
}
|
|
6758
|
+
function assertAttentionEventInput(input) {
|
|
6759
|
+
if (!attentionEventTypes.has(input.event_type)) {
|
|
6760
|
+
throw new ProposalStoreError("ATTENTION_EVENT_TYPE_INVALID", `unsupported attention event type: ${String(input.event_type)}`);
|
|
6761
|
+
}
|
|
6762
|
+
if (!["informational", "warning", "critical"].includes(input.severity)) {
|
|
6763
|
+
throw new ProposalStoreError("ATTENTION_EVENT_SEVERITY_INVALID", `unsupported attention severity: ${String(input.severity)}`);
|
|
6764
|
+
}
|
|
6765
|
+
if (input.contract_digest && !/^sha256:[a-f0-9]{64}$/.test(input.contract_digest)) {
|
|
6766
|
+
throw new ProposalStoreError("ATTENTION_EVENT_DIGEST_INVALID", "attention event contract digest must be a lowercase sha256 digest");
|
|
6767
|
+
}
|
|
6768
|
+
for (const [label, value] of [["now", input.now], ["expires_at", input.expires_at]]) {
|
|
6769
|
+
if (value !== void 0 && !Number.isFinite(Date.parse(value))) {
|
|
6770
|
+
throw new ProposalStoreError("ATTENTION_EVENT_TIME_INVALID", `attention event ${label} must be an ISO timestamp`);
|
|
6771
|
+
}
|
|
6772
|
+
}
|
|
6773
|
+
const entries = Object.entries(input.details ?? {});
|
|
6774
|
+
if (entries.length > 32) {
|
|
6775
|
+
throw new ProposalStoreError("ATTENTION_EVENT_DETAILS_TOO_LARGE", "attention event details may contain at most 32 safe fields");
|
|
6776
|
+
}
|
|
6777
|
+
for (const [key, value] of entries) {
|
|
6778
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(key)) {
|
|
6779
|
+
throw new ProposalStoreError("ATTENTION_EVENT_DETAIL_KEY_INVALID", `attention event detail key is invalid: ${key}`);
|
|
6780
|
+
}
|
|
6781
|
+
if (typeof value === "string") boundedSafeLabel(value, `attention detail ${key}`, 256);
|
|
6782
|
+
else if (typeof value === "number" && !Number.isFinite(value)) {
|
|
6783
|
+
throw new ProposalStoreError("ATTENTION_EVENT_DETAIL_VALUE_INVALID", `attention event detail ${key} must be finite`);
|
|
6784
|
+
} else if (value !== null && typeof value !== "number" && typeof value !== "boolean") {
|
|
6785
|
+
throw new ProposalStoreError("ATTENTION_EVENT_DETAIL_VALUE_INVALID", `attention event detail ${key} must be scalar`);
|
|
6786
|
+
}
|
|
6787
|
+
}
|
|
6788
|
+
assertNoSecretMaterial(input.details ?? {}, "attention_event.details");
|
|
6789
|
+
}
|
|
6790
|
+
function boundedSafeLabel(value, label, maximum) {
|
|
6791
|
+
const trimmed = value.trim();
|
|
6792
|
+
if (!trimmed || trimmed.length > maximum || /[\u0000-\u001f\u007f]/.test(trimmed)) {
|
|
6793
|
+
throw new ProposalStoreError("ATTENTION_EVENT_FIELD_INVALID", `${label} must be a non-empty bounded single-line value`);
|
|
6794
|
+
}
|
|
6795
|
+
assertNoSecretMaterial({ value: trimmed }, label);
|
|
6796
|
+
return trimmed;
|
|
6797
|
+
}
|
|
6798
|
+
function boundedWorkbenchPath(value) {
|
|
6799
|
+
const path = boundedSafeLabel(value, "attention Workbench path", 512);
|
|
6800
|
+
if (!path.startsWith("/") || path.includes("?") || path.includes("#") || path.includes("://") || path.includes("..")) {
|
|
6801
|
+
throw new ProposalStoreError(
|
|
6802
|
+
"ATTENTION_WORKBENCH_PATH_INVALID",
|
|
6803
|
+
"attention Workbench path must be a local absolute path without query, fragment, traversal, or authority"
|
|
6804
|
+
);
|
|
6805
|
+
}
|
|
6806
|
+
return path;
|
|
6807
|
+
}
|
|
6808
|
+
function defaultAttentionKey(input) {
|
|
6809
|
+
const failureClass = typeof input.details.failure_class === "string" ? input.details.failure_class : typeof input.details.reason_code === "string" ? input.details.reason_code : "none";
|
|
6810
|
+
return [
|
|
6811
|
+
input.environment,
|
|
6812
|
+
input.event_type,
|
|
6813
|
+
input.capability ?? "global",
|
|
6814
|
+
input.contract_digest ?? "no_digest",
|
|
6815
|
+
failureClass
|
|
6816
|
+
].join(":");
|
|
6817
|
+
}
|
|
6818
|
+
function attentionEventId(sourceEventKey, eventType) {
|
|
6819
|
+
const digest = canonicalJsonDigest({ source_event_key: sourceEventKey, event_type: eventType });
|
|
6820
|
+
return `aev_${digest.slice("sha256:".length)}`;
|
|
6821
|
+
}
|
|
6822
|
+
function attentionItemId(attentionKey) {
|
|
6823
|
+
const digest = canonicalJsonDigest({ attention_key: attentionKey });
|
|
6824
|
+
return `attn_${digest.slice("sha256:".length, "sha256:".length + 32)}`;
|
|
6825
|
+
}
|
|
6826
|
+
function boundedSinkId(value) {
|
|
6827
|
+
const sinkId = boundedSafeLabel(value, "notification sink id", 128);
|
|
6828
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(sinkId)) {
|
|
6829
|
+
throw new ProposalStoreError(
|
|
6830
|
+
"NOTIFICATION_SINK_ID_INVALID",
|
|
6831
|
+
"notification sink id must use letters, numbers, underscore, period, or hyphen"
|
|
6832
|
+
);
|
|
6833
|
+
}
|
|
6834
|
+
return sinkId;
|
|
6835
|
+
}
|
|
6836
|
+
function boundedSafeErrorCode(value) {
|
|
6837
|
+
const errorCode = boundedSafeLabel(value, "notification error code", 128);
|
|
6838
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(errorCode)) {
|
|
6839
|
+
throw new ProposalStoreError(
|
|
6840
|
+
"NOTIFICATION_ERROR_CODE_INVALID",
|
|
6841
|
+
"notification error code must be an uppercase safe code"
|
|
6842
|
+
);
|
|
6843
|
+
}
|
|
6844
|
+
return errorCode;
|
|
6845
|
+
}
|
|
6846
|
+
function notificationDeliveryId(sinkId, eventId) {
|
|
6847
|
+
const digest = canonicalJsonDigest({ sink_id: sinkId, event_id: eventId });
|
|
6848
|
+
return `ndl_${digest.slice("sha256:".length, "sha256:".length + 32)}`;
|
|
6849
|
+
}
|
|
6850
|
+
function notificationLeaseId(deliveryId, owner, attempt, now) {
|
|
6851
|
+
const digest = canonicalJsonDigest({
|
|
6852
|
+
delivery_id: deliveryId,
|
|
6853
|
+
owner,
|
|
6854
|
+
attempt,
|
|
6855
|
+
now
|
|
6856
|
+
});
|
|
6857
|
+
return `nlease_${digest.slice("sha256:".length, "sha256:".length + 32)}`;
|
|
6858
|
+
}
|
|
6859
|
+
function attentionSeverityRank(severity) {
|
|
6860
|
+
if (severity === "critical") return 3;
|
|
6861
|
+
if (severity === "warning") return 2;
|
|
6862
|
+
return 1;
|
|
6863
|
+
}
|
|
6864
|
+
function workerControlDecisionSubject(state, target) {
|
|
6865
|
+
assertWorkerControlTarget(target);
|
|
6866
|
+
const targetDigest = canonicalJsonDigest({
|
|
6867
|
+
schema_version: "synapsor.worker-control-decision-subject.v1",
|
|
6868
|
+
current_revision: state.revision,
|
|
6869
|
+
current_integrity_hash: state.integrity_hash,
|
|
6870
|
+
action: target.action,
|
|
6871
|
+
capability: target.capability ?? null,
|
|
6872
|
+
contract_digest: target.contract_digest ?? null
|
|
6873
|
+
});
|
|
6874
|
+
return {
|
|
6875
|
+
proposal_id: `worker_control_${targetDigest.slice("sha256:".length, "sha256:".length + 32)}`,
|
|
6876
|
+
proposal_version: state.revision + 1,
|
|
6877
|
+
proposal_hash: targetDigest
|
|
6878
|
+
};
|
|
6879
|
+
}
|
|
6880
|
+
function defaultWorkerControlState() {
|
|
6881
|
+
const unsigned = {
|
|
6882
|
+
schema_version: "synapsor.worker-control.v1",
|
|
6883
|
+
mode: "active",
|
|
6884
|
+
revision: 0,
|
|
6885
|
+
capability_controls: [],
|
|
6886
|
+
updated_at: "1970-01-01T00:00:00.000Z"
|
|
6887
|
+
};
|
|
6888
|
+
return { ...unsigned, integrity_hash: canonicalJsonDigest(unsigned) };
|
|
6889
|
+
}
|
|
6890
|
+
function parseWorkerControlState(value) {
|
|
6891
|
+
if (!isRecord(value) || value.schema_version !== "synapsor.worker-control.v1" || !["active", "paused", "draining"].includes(String(value.mode)) || !Number.isSafeInteger(value.revision) || Number(value.revision) < 0 || !Array.isArray(value.capability_controls) || typeof value.updated_at !== "string" || typeof value.integrity_hash !== "string") {
|
|
6892
|
+
throw new ProposalStoreError("WORKER_CONTROL_STATE_INVALID", "stored supervised-worker control state is invalid");
|
|
6893
|
+
}
|
|
6894
|
+
const controls = value.capability_controls.map((entry) => {
|
|
6895
|
+
if (!isRecord(entry) || typeof entry.capability !== "string" || !/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(entry.capability) || typeof entry.contract_digest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(entry.contract_digest) || !["enabled", "disabled", "revoked"].includes(String(entry.status)) || typeof entry.updated_at !== "string" || typeof entry.actor !== "string") {
|
|
6896
|
+
throw new ProposalStoreError("WORKER_CONTROL_STATE_INVALID", "stored supervised-worker capability control is invalid");
|
|
6897
|
+
}
|
|
6898
|
+
return {
|
|
6899
|
+
capability: entry.capability,
|
|
6900
|
+
contract_digest: entry.contract_digest,
|
|
6901
|
+
status: entry.status,
|
|
6902
|
+
updated_at: entry.updated_at,
|
|
6903
|
+
actor: entry.actor
|
|
6904
|
+
};
|
|
6905
|
+
});
|
|
6906
|
+
const lastDecision = isRecord(value.last_decision) ? value.last_decision : void 0;
|
|
6907
|
+
const unsigned = {
|
|
6908
|
+
schema_version: "synapsor.worker-control.v1",
|
|
6909
|
+
mode: value.mode,
|
|
6910
|
+
revision: Number(value.revision),
|
|
6911
|
+
capability_controls: controls,
|
|
6912
|
+
...lastDecision ? { last_decision: lastDecision } : {},
|
|
6913
|
+
updated_at: value.updated_at
|
|
6914
|
+
};
|
|
6915
|
+
const integrityHash = value.integrity_hash;
|
|
6916
|
+
if (integrityHash !== canonicalJsonDigest(unsigned)) {
|
|
6917
|
+
throw new ProposalStoreError("WORKER_CONTROL_STATE_TAMPERED", "stored supervised-worker control state failed integrity validation");
|
|
6918
|
+
}
|
|
6919
|
+
return { ...unsigned, integrity_hash: integrityHash };
|
|
6920
|
+
}
|
|
6921
|
+
function assertWorkerControlTarget(target) {
|
|
6922
|
+
const capabilityAction = target.action === "capability_enable" || target.action === "capability_disable" || target.action === "digest_revoke";
|
|
6923
|
+
if (capabilityAction) {
|
|
6924
|
+
if (!target.capability || !/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(target.capability)) {
|
|
6925
|
+
throw new ProposalStoreError("WORKER_CONTROL_CAPABILITY_REQUIRED", `${target.action} requires a fixed capability name`);
|
|
6926
|
+
}
|
|
6927
|
+
if (!target.contract_digest || !/^sha256:[a-f0-9]{64}$/.test(target.contract_digest)) {
|
|
6928
|
+
throw new ProposalStoreError("WORKER_CONTROL_DIGEST_REQUIRED", `${target.action} requires an exact sha256 contract digest`);
|
|
6929
|
+
}
|
|
6930
|
+
} else if (target.capability || target.contract_digest) {
|
|
6931
|
+
throw new ProposalStoreError("WORKER_CONTROL_TARGET_INVALID", `${target.action} is a global control and cannot carry capability authority`);
|
|
6932
|
+
}
|
|
6933
|
+
}
|
|
6934
|
+
function workerControlOperatorAction(action) {
|
|
6935
|
+
if (action === "pause") return "worker_pause";
|
|
6936
|
+
if (action === "resume") return "worker_resume";
|
|
6937
|
+
if (action === "drain") return "worker_drain";
|
|
6938
|
+
if (action === "capability_enable") return "worker_capability_enable";
|
|
6939
|
+
if (action === "capability_disable") return "worker_capability_disable";
|
|
6940
|
+
return "worker_digest_revoke";
|
|
6941
|
+
}
|
|
6942
|
+
function assertWorkerControlOperatorDecision(state, target, actor, identity, requireVerified) {
|
|
6943
|
+
if (requireVerified && (!identity || !identity.verified || identity.provider === "dev_env")) {
|
|
6944
|
+
throw new ProposalStoreError(
|
|
6945
|
+
"VERIFIED_OPERATOR_IDENTITY_REQUIRED",
|
|
6946
|
+
`verified operator identity is required to ${target.action} supervised execution`
|
|
6947
|
+
);
|
|
6948
|
+
}
|
|
6949
|
+
if (!identity) return;
|
|
6950
|
+
if (identity.subject !== actor || identity.decision.subject !== actor) {
|
|
6951
|
+
throw new ProposalStoreError("OPERATOR_IDENTITY_MISMATCH", "worker-control identity does not match its actor");
|
|
6952
|
+
}
|
|
6953
|
+
const subject = workerControlDecisionSubject(state, target);
|
|
6954
|
+
if (identity.decision.action !== workerControlOperatorAction(target.action) || identity.decision.proposal_id !== subject.proposal_id || identity.decision.proposal_version !== subject.proposal_version || identity.decision.proposal_hash !== subject.proposal_hash) {
|
|
6955
|
+
throw new ProposalStoreError("OPERATOR_DECISION_MISMATCH", "operator proof is not bound to this exact worker-control revision");
|
|
6956
|
+
}
|
|
6957
|
+
if (identity.decision_hash !== canonicalJsonDigest(identity.decision)) {
|
|
6958
|
+
throw new ProposalStoreError("OPERATOR_IDENTITY_TAMPERED", "worker-control decision hash failed integrity validation");
|
|
6959
|
+
}
|
|
6960
|
+
const { integrity_hash: _integrityHash, ...identityCore } = identity;
|
|
6961
|
+
if (identity.integrity_hash !== canonicalJsonDigest(identityCore)) {
|
|
6962
|
+
throw new ProposalStoreError("OPERATOR_IDENTITY_TAMPERED", "worker-control identity proof failed integrity validation");
|
|
6963
|
+
}
|
|
6964
|
+
}
|
|
6965
|
+
function workerControlSummary(target) {
|
|
6966
|
+
if (target.action === "pause") return "Supervised execution paused by an operator";
|
|
6967
|
+
if (target.action === "resume") return "Supervised execution resumed by an operator";
|
|
6968
|
+
if (target.action === "drain") return "Supervised execution is draining without new leases";
|
|
6969
|
+
if (target.action === "capability_enable") return `${target.capability} supervised execution enabled for the exact reviewed digest`;
|
|
6970
|
+
if (target.action === "capability_disable") return `${target.capability} supervised execution disabled for the exact reviewed digest`;
|
|
6971
|
+
return `${target.capability} supervised execution digest revoked`;
|
|
6972
|
+
}
|
|
6973
|
+
function attentionDecisionSubject(item) {
|
|
6974
|
+
return {
|
|
6975
|
+
proposal_id: item.attention_id,
|
|
6976
|
+
proposal_version: item.occurrence_count,
|
|
6977
|
+
proposal_hash: canonicalJsonDigest({
|
|
6978
|
+
schema_version: "synapsor.attention-decision-subject.v1",
|
|
6979
|
+
attention_id: item.attention_id,
|
|
6980
|
+
attention_key: item.attention_key,
|
|
6981
|
+
status: item.status,
|
|
6982
|
+
severity: item.severity,
|
|
6983
|
+
environment: item.environment,
|
|
6984
|
+
event_type: item.event_type,
|
|
6985
|
+
capability: item.capability ?? null,
|
|
6986
|
+
contract_digest: item.contract_digest ?? null,
|
|
6987
|
+
occurrence_count: item.occurrence_count,
|
|
6988
|
+
latest_event_id: item.latest_event_id,
|
|
6989
|
+
last_seen_at: item.last_seen_at
|
|
6990
|
+
})
|
|
6991
|
+
};
|
|
6992
|
+
}
|
|
6993
|
+
function notificationReplayDecisionSubject(delivery) {
|
|
6994
|
+
return {
|
|
6995
|
+
proposal_id: delivery.delivery_id,
|
|
6996
|
+
proposal_version: Math.max(1, delivery.attempts),
|
|
6997
|
+
proposal_hash: canonicalJsonDigest({
|
|
6998
|
+
schema_version: "synapsor.notification-replay-decision-subject.v1",
|
|
6999
|
+
delivery_id: delivery.delivery_id,
|
|
7000
|
+
sink_id: delivery.sink_id,
|
|
7001
|
+
event_id: delivery.event_id,
|
|
7002
|
+
attention_id: delivery.attention_id ?? null,
|
|
7003
|
+
status: delivery.status,
|
|
7004
|
+
attempts: delivery.attempts,
|
|
7005
|
+
max_attempts: delivery.max_attempts,
|
|
7006
|
+
last_error_code: delivery.last_error_code ?? null,
|
|
7007
|
+
updated_at: delivery.updated_at
|
|
7008
|
+
})
|
|
7009
|
+
};
|
|
7010
|
+
}
|
|
7011
|
+
function assertNotificationReplayOperatorDecision(delivery, identity, reason) {
|
|
7012
|
+
if (!identity.verified || identity.provider === "dev_env") {
|
|
7013
|
+
throw new ProposalStoreError(
|
|
7014
|
+
"VERIFIED_OPERATOR_IDENTITY_REQUIRED",
|
|
7015
|
+
`verified operator identity is required to replay notification delivery ${delivery.delivery_id}`
|
|
7016
|
+
);
|
|
7017
|
+
}
|
|
7018
|
+
if (identity.subject !== identity.decision.subject) {
|
|
7019
|
+
throw new ProposalStoreError(
|
|
7020
|
+
"OPERATOR_IDENTITY_MISMATCH",
|
|
7021
|
+
"notification replay identity does not match its signed decision subject"
|
|
7022
|
+
);
|
|
7023
|
+
}
|
|
7024
|
+
const subject = notificationReplayDecisionSubject(delivery);
|
|
7025
|
+
if (identity.decision.action !== "notification_replay" || identity.decision.proposal_id !== subject.proposal_id || identity.decision.proposal_version !== subject.proposal_version || identity.decision.proposal_hash !== subject.proposal_hash || identity.decision.reason !== reason) {
|
|
7026
|
+
throw new ProposalStoreError(
|
|
7027
|
+
"OPERATOR_DECISION_MISMATCH",
|
|
7028
|
+
"operator proof is not bound to this exact notification delivery revision and replay reason"
|
|
7029
|
+
);
|
|
7030
|
+
}
|
|
7031
|
+
if (identity.decision_hash !== canonicalJsonDigest(identity.decision)) {
|
|
7032
|
+
throw new ProposalStoreError(
|
|
7033
|
+
"OPERATOR_IDENTITY_TAMPERED",
|
|
7034
|
+
"notification replay decision hash failed its integrity check"
|
|
7035
|
+
);
|
|
7036
|
+
}
|
|
7037
|
+
const { integrity_hash: _integrityHash, ...core } = identity;
|
|
7038
|
+
const canonicalCore = JSON.parse(JSON.stringify(core));
|
|
7039
|
+
if (identity.integrity_hash !== canonicalJsonDigest(canonicalCore)) {
|
|
7040
|
+
throw new ProposalStoreError(
|
|
7041
|
+
"OPERATOR_IDENTITY_TAMPERED",
|
|
7042
|
+
"notification replay identity proof failed its integrity check"
|
|
7043
|
+
);
|
|
7044
|
+
}
|
|
7045
|
+
}
|
|
7046
|
+
function assertAttentionOperatorDecision(item, actor, identity, requireVerified) {
|
|
7047
|
+
if (requireVerified && (!identity || !identity.verified || identity.provider === "dev_env")) {
|
|
7048
|
+
throw new ProposalStoreError(
|
|
7049
|
+
"VERIFIED_OPERATOR_IDENTITY_REQUIRED",
|
|
7050
|
+
`verified operator identity is required to acknowledge attention item ${item.attention_id}`
|
|
7051
|
+
);
|
|
7052
|
+
}
|
|
7053
|
+
if (!identity) return;
|
|
7054
|
+
if (identity.subject !== actor || identity.decision.subject !== actor) {
|
|
7055
|
+
throw new ProposalStoreError(
|
|
7056
|
+
"OPERATOR_IDENTITY_MISMATCH",
|
|
7057
|
+
`operator identity ${identity.subject} does not match attention acknowledgement actor ${actor}`
|
|
7058
|
+
);
|
|
7059
|
+
}
|
|
7060
|
+
const subject = attentionDecisionSubject(item);
|
|
7061
|
+
if (identity.decision.action !== "attention_acknowledge" || identity.decision.proposal_id !== subject.proposal_id || identity.decision.proposal_version !== subject.proposal_version || identity.decision.proposal_hash !== subject.proposal_hash) {
|
|
7062
|
+
throw new ProposalStoreError(
|
|
7063
|
+
"OPERATOR_DECISION_MISMATCH",
|
|
7064
|
+
"operator proof is not bound to this exact attention item version"
|
|
7065
|
+
);
|
|
7066
|
+
}
|
|
7067
|
+
if (identity.decision_hash !== canonicalJsonDigest(identity.decision)) {
|
|
7068
|
+
throw new ProposalStoreError(
|
|
7069
|
+
"OPERATOR_IDENTITY_TAMPERED",
|
|
7070
|
+
"attention acknowledgement decision hash failed its integrity check"
|
|
7071
|
+
);
|
|
7072
|
+
}
|
|
7073
|
+
const { integrity_hash: _integrityHash, ...core } = identity;
|
|
7074
|
+
const canonicalCore = JSON.parse(JSON.stringify(core));
|
|
7075
|
+
if (identity.integrity_hash !== canonicalJsonDigest(canonicalCore)) {
|
|
7076
|
+
throw new ProposalStoreError(
|
|
7077
|
+
"OPERATOR_IDENTITY_TAMPERED",
|
|
7078
|
+
"attention acknowledgement identity proof failed its integrity check"
|
|
7079
|
+
);
|
|
7080
|
+
}
|
|
7081
|
+
}
|
|
7082
|
+
function rowToAttentionEvent(row) {
|
|
7083
|
+
if (!isRecord(row)) return void 0;
|
|
7084
|
+
const eventType = String(row.event_type);
|
|
7085
|
+
const severity = String(row.severity);
|
|
7086
|
+
if (!attentionEventTypes.has(eventType) || !["informational", "warning", "critical"].includes(severity)) return void 0;
|
|
7087
|
+
let details;
|
|
7088
|
+
try {
|
|
7089
|
+
const parsed = JSON.parse(String(row.details_json));
|
|
7090
|
+
if (!isRecord(parsed)) return void 0;
|
|
7091
|
+
details = {};
|
|
7092
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
7093
|
+
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return void 0;
|
|
7094
|
+
details[key] = value;
|
|
7095
|
+
}
|
|
7096
|
+
} catch {
|
|
7097
|
+
return void 0;
|
|
7098
|
+
}
|
|
7099
|
+
const event = {
|
|
7100
|
+
schema_version: "synapsor.attention-event.v1",
|
|
7101
|
+
event_id: String(row.event_id),
|
|
7102
|
+
event_type: eventType,
|
|
7103
|
+
severity,
|
|
7104
|
+
occurred_at: String(row.occurred_at),
|
|
7105
|
+
environment: String(row.environment),
|
|
7106
|
+
...row.proposal_id == null ? {} : { proposal_id: String(row.proposal_id) },
|
|
7107
|
+
...row.job_id == null ? {} : { job_id: String(row.job_id) },
|
|
7108
|
+
...row.operation_id == null ? {} : { operation_id: String(row.operation_id) },
|
|
7109
|
+
...row.correlation_id == null ? {} : { correlation_id: String(row.correlation_id) },
|
|
7110
|
+
...row.capability == null ? {} : { capability: String(row.capability) },
|
|
7111
|
+
...row.contract_digest == null ? {} : { contract_digest: String(row.contract_digest) },
|
|
7112
|
+
...row.attention_key == null ? {} : { attention_key: String(row.attention_key) },
|
|
7113
|
+
attention_required: Number(row.attention_required) === 1,
|
|
7114
|
+
immediate_default: Number(row.immediate_default) === 1,
|
|
7115
|
+
summary: String(row.summary),
|
|
7116
|
+
...row.approval_source === "human" || row.approval_source === "policy_auto" ? { approval_source: row.approval_source } : {},
|
|
7117
|
+
...row.worker_state == null ? {} : { worker_state: String(row.worker_state) },
|
|
7118
|
+
...row.failure_class == null ? {} : { failure_class: String(row.failure_class) },
|
|
7119
|
+
...row.expires_at == null ? {} : { expires_at: String(row.expires_at) },
|
|
7120
|
+
...row.workbench_path == null ? {} : { workbench_path: String(row.workbench_path) },
|
|
7121
|
+
details,
|
|
7122
|
+
payload_hash: String(row.payload_hash)
|
|
7123
|
+
};
|
|
7124
|
+
const { payload_hash: _payloadHash, ...unsigned } = event;
|
|
7125
|
+
if (event.payload_hash !== canonicalJsonDigest(unsigned)) {
|
|
7126
|
+
throw new ProposalStoreError("ATTENTION_EVENT_CORRUPT", `attention event ${event.event_id} failed its integrity check`);
|
|
7127
|
+
}
|
|
7128
|
+
return event;
|
|
7129
|
+
}
|
|
7130
|
+
function rowToAttentionItem(row) {
|
|
7131
|
+
if (!isRecord(row)) return void 0;
|
|
7132
|
+
const status = String(row.status);
|
|
7133
|
+
const severity = String(row.severity);
|
|
7134
|
+
const eventType = String(row.event_type);
|
|
7135
|
+
if (!["open", "acknowledged", "resolved", "expired"].includes(status)) return void 0;
|
|
7136
|
+
if (!["informational", "warning", "critical"].includes(severity) || !attentionEventTypes.has(eventType)) return void 0;
|
|
7137
|
+
let acknowledgementIdentity;
|
|
7138
|
+
if (row.acknowledgement_identity_json != null) {
|
|
7139
|
+
try {
|
|
7140
|
+
const parsed = JSON.parse(String(row.acknowledgement_identity_json));
|
|
7141
|
+
if (!isRecord(parsed) || !isRecord(parsed.decision)) return void 0;
|
|
7142
|
+
acknowledgementIdentity = parsed;
|
|
7143
|
+
} catch {
|
|
7144
|
+
return void 0;
|
|
7145
|
+
}
|
|
7146
|
+
}
|
|
7147
|
+
return {
|
|
7148
|
+
attention_id: String(row.attention_id),
|
|
7149
|
+
attention_key: String(row.attention_key),
|
|
7150
|
+
status,
|
|
7151
|
+
severity,
|
|
7152
|
+
environment: String(row.environment),
|
|
7153
|
+
event_type: eventType,
|
|
7154
|
+
capability: row.capability == null ? void 0 : String(row.capability),
|
|
7155
|
+
contract_digest: row.contract_digest == null ? void 0 : String(row.contract_digest),
|
|
7156
|
+
title: String(row.title),
|
|
7157
|
+
occurrence_count: Number(row.occurrence_count),
|
|
7158
|
+
first_event_id: String(row.first_event_id),
|
|
7159
|
+
latest_event_id: String(row.latest_event_id),
|
|
7160
|
+
first_seen_at: String(row.first_seen_at),
|
|
7161
|
+
last_seen_at: String(row.last_seen_at),
|
|
7162
|
+
acknowledged_by: row.acknowledged_by == null ? void 0 : String(row.acknowledged_by),
|
|
7163
|
+
acknowledged_at: row.acknowledged_at == null ? void 0 : String(row.acknowledged_at),
|
|
7164
|
+
acknowledgement_identity: acknowledgementIdentity,
|
|
7165
|
+
resolved_at: row.resolved_at == null ? void 0 : String(row.resolved_at),
|
|
7166
|
+
expires_at: row.expires_at == null ? void 0 : String(row.expires_at)
|
|
7167
|
+
};
|
|
7168
|
+
}
|
|
7169
|
+
function rowToNotificationDelivery(row) {
|
|
7170
|
+
if (!isRecord(row)) return void 0;
|
|
7171
|
+
const status = String(row.status);
|
|
7172
|
+
if (![
|
|
7173
|
+
"pending",
|
|
7174
|
+
"leased",
|
|
7175
|
+
"delivered",
|
|
7176
|
+
"retry_wait",
|
|
7177
|
+
"dead_letter",
|
|
7178
|
+
"suppressed",
|
|
7179
|
+
"batched"
|
|
7180
|
+
].includes(status)) return void 0;
|
|
7181
|
+
return {
|
|
7182
|
+
delivery_id: String(row.delivery_id),
|
|
7183
|
+
sink_id: String(row.sink_id),
|
|
7184
|
+
event_id: String(row.event_id),
|
|
7185
|
+
...row.attention_id == null ? {} : { attention_id: String(row.attention_id) },
|
|
7186
|
+
status,
|
|
7187
|
+
attempts: Number(row.attempts),
|
|
7188
|
+
max_attempts: Number(row.max_attempts),
|
|
7189
|
+
next_attempt_at: String(row.next_attempt_at),
|
|
7190
|
+
...row.lease_owner == null ? {} : { lease_owner: String(row.lease_owner) },
|
|
7191
|
+
...row.lease_id == null ? {} : { lease_id: String(row.lease_id) },
|
|
7192
|
+
...row.lease_expires_at == null ? {} : { lease_expires_at: String(row.lease_expires_at) },
|
|
7193
|
+
...row.last_error_code == null ? {} : { last_error_code: String(row.last_error_code) },
|
|
7194
|
+
...row.external_reference == null ? {} : { external_reference: String(row.external_reference) },
|
|
7195
|
+
...row.delivered_at == null ? {} : { delivered_at: String(row.delivered_at) },
|
|
7196
|
+
created_at: String(row.created_at),
|
|
7197
|
+
updated_at: String(row.updated_at)
|
|
7198
|
+
};
|
|
7199
|
+
}
|
|
5015
7200
|
function rowToWorkerQueueItem(row) {
|
|
5016
7201
|
if (!isRecord(row)) return void 0;
|
|
5017
7202
|
const status = String(row.status);
|
|
5018
|
-
if (![
|
|
7203
|
+
if (![
|
|
7204
|
+
"queued",
|
|
7205
|
+
"leased",
|
|
7206
|
+
"retry_wait",
|
|
7207
|
+
"completed",
|
|
7208
|
+
"dead_letter",
|
|
7209
|
+
"discarded",
|
|
7210
|
+
"cancelled",
|
|
7211
|
+
"blocked",
|
|
7212
|
+
"reconciliation_required"
|
|
7213
|
+
].includes(status)) return void 0;
|
|
7214
|
+
const executionMode = row.execution_mode == null ? "legacy" : String(row.execution_mode);
|
|
7215
|
+
if (executionMode !== "legacy" && executionMode !== "supervised_worker") return void 0;
|
|
5019
7216
|
return {
|
|
5020
7217
|
proposal_id: String(row.proposal_id),
|
|
5021
7218
|
status,
|
|
7219
|
+
execution_mode: executionMode,
|
|
7220
|
+
contract_digest: row.contract_digest == null ? void 0 : String(row.contract_digest),
|
|
5022
7221
|
attempts: Number(row.attempts),
|
|
5023
7222
|
max_attempts: Number(row.max_attempts),
|
|
5024
7223
|
next_attempt_at: String(row.next_attempt_at),
|
|
5025
7224
|
lease_owner: row.lease_owner == null ? void 0 : String(row.lease_owner),
|
|
7225
|
+
lease_id: row.lease_id == null ? void 0 : String(row.lease_id),
|
|
5026
7226
|
lease_expires_at: row.lease_expires_at == null ? void 0 : String(row.lease_expires_at),
|
|
5027
7227
|
last_error_code: row.last_error_code == null ? void 0 : String(row.last_error_code),
|
|
7228
|
+
terminal_outcome: row.terminal_outcome == null ? void 0 : String(row.terminal_outcome),
|
|
5028
7229
|
created_at: String(row.created_at),
|
|
5029
7230
|
updated_at: String(row.updated_at)
|
|
5030
7231
|
};
|
|
5031
7232
|
}
|
|
7233
|
+
function workerLeaseId(proposalId, workerId, attempt, now) {
|
|
7234
|
+
const prefixLength = "sha256:".length;
|
|
7235
|
+
return `wlease_${canonicalJsonDigest({ proposal_id: proposalId, worker_id: workerId, attempt, now }).slice(prefixLength, prefixLength + 32)}`;
|
|
7236
|
+
}
|
|
5032
7237
|
function rowToCloudOutboxItem(row) {
|
|
5033
7238
|
if (!isRecord(row)) return void 0;
|
|
5034
7239
|
const kind = String(row.kind);
|
|
@@ -5176,7 +7381,17 @@ function rowToWritebackIntent(row) {
|
|
|
5176
7381
|
};
|
|
5177
7382
|
}
|
|
5178
7383
|
function isStoredWritebackOperation(operation) {
|
|
5179
|
-
return [
|
|
7384
|
+
return [
|
|
7385
|
+
"single_row_update",
|
|
7386
|
+
"single_row_insert",
|
|
7387
|
+
"single_row_delete",
|
|
7388
|
+
"set_update",
|
|
7389
|
+
"set_delete",
|
|
7390
|
+
"batch_insert",
|
|
7391
|
+
"restore_update",
|
|
7392
|
+
"remove_insert",
|
|
7393
|
+
"restore_insert"
|
|
7394
|
+
].includes(operation);
|
|
5180
7395
|
}
|
|
5181
7396
|
function rowToQueryAudit(row) {
|
|
5182
7397
|
if (!isRecord(row)) return void 0;
|
|
@@ -5287,7 +7502,9 @@ var sharedLedgerJsonColumns = /* @__PURE__ */ new Set([
|
|
|
5287
7502
|
"patch_json",
|
|
5288
7503
|
"selected_capabilities_json",
|
|
5289
7504
|
"proposed_effect_json",
|
|
5290
|
-
"actual_effect_json"
|
|
7505
|
+
"actual_effect_json",
|
|
7506
|
+
"details_json",
|
|
7507
|
+
"acknowledgement_identity_json"
|
|
5291
7508
|
]);
|
|
5292
7509
|
var sharedLedgerKindToTable = {
|
|
5293
7510
|
proposal: "proposals",
|
|
@@ -5306,6 +7523,9 @@ var sharedLedgerKindToTable = {
|
|
|
5306
7523
|
shadow_study_case: "shadow_study_cases",
|
|
5307
7524
|
shadow_outcome: "shadow_outcomes",
|
|
5308
7525
|
worker_queue_item: "worker_queue",
|
|
7526
|
+
attention_event: "attention_events",
|
|
7527
|
+
attention_item: "attention_items",
|
|
7528
|
+
notification_delivery: "notification_deliveries",
|
|
5309
7529
|
runner_state: "runner_state",
|
|
5310
7530
|
policy_recommendation: "policy_recommendations",
|
|
5311
7531
|
cloud_outbox_event: "cloud_outbox",
|
|
@@ -5348,7 +7568,142 @@ var sharedLedgerRestoreSpecs = {
|
|
|
5348
7568
|
shadow_studies: restoreSpec("study_id", ["study_id", "name", "description", "selected_capabilities_json", "starts_at", "ends_at", "status", "created_at", "updated_at"], ["study_id", "name", "selected_capabilities_json", "status", "created_at", "updated_at"]),
|
|
5349
7569
|
shadow_study_cases: restoreSpec("case_id", ["case_id", "study_id", "request_id", "proposal_id", "tenant_id", "principal", "capability", "business_object", "object_id", "evidence_bundle_id", "proposed_effect_json", "agent_result", "decision_reason", "risk_score", "amount_value", "created_at"], ["case_id", "study_id", "request_id", "tenant_id", "capability", "business_object", "object_id", "agent_result", "created_at"]),
|
|
5350
7570
|
shadow_outcomes: restoreSpec("outcome_id", ["outcome_id", "study_id", "request_id", "proposal_id", "tenant_id", "business_object", "object_id", "actor", "disposition", "actual_effect_json", "occurred_at", "source", "reference", "reason", "created_at"], ["outcome_id", "study_id", "request_id", "tenant_id", "business_object", "object_id", "actor", "disposition", "occurred_at", "source", "created_at"]),
|
|
5351
|
-
worker_queue: restoreSpec(
|
|
7571
|
+
worker_queue: restoreSpec(
|
|
7572
|
+
"proposal_id",
|
|
7573
|
+
[
|
|
7574
|
+
"proposal_id",
|
|
7575
|
+
"status",
|
|
7576
|
+
"execution_mode",
|
|
7577
|
+
"contract_digest",
|
|
7578
|
+
"attempts",
|
|
7579
|
+
"max_attempts",
|
|
7580
|
+
"next_attempt_at",
|
|
7581
|
+
"lease_owner",
|
|
7582
|
+
"lease_id",
|
|
7583
|
+
"lease_expires_at",
|
|
7584
|
+
"last_error_code",
|
|
7585
|
+
"terminal_outcome",
|
|
7586
|
+
"created_at",
|
|
7587
|
+
"updated_at"
|
|
7588
|
+
],
|
|
7589
|
+
["proposal_id", "status", "attempts", "max_attempts", "next_attempt_at", "created_at", "updated_at"]
|
|
7590
|
+
),
|
|
7591
|
+
attention_events: restoreSpec(
|
|
7592
|
+
"event_id",
|
|
7593
|
+
[
|
|
7594
|
+
"event_id",
|
|
7595
|
+
"schema_version",
|
|
7596
|
+
"event_type",
|
|
7597
|
+
"severity",
|
|
7598
|
+
"occurred_at",
|
|
7599
|
+
"environment",
|
|
7600
|
+
"proposal_id",
|
|
7601
|
+
"job_id",
|
|
7602
|
+
"operation_id",
|
|
7603
|
+
"correlation_id",
|
|
7604
|
+
"capability",
|
|
7605
|
+
"contract_digest",
|
|
7606
|
+
"attention_key",
|
|
7607
|
+
"attention_required",
|
|
7608
|
+
"immediate_default",
|
|
7609
|
+
"summary",
|
|
7610
|
+
"approval_source",
|
|
7611
|
+
"worker_state",
|
|
7612
|
+
"failure_class",
|
|
7613
|
+
"expires_at",
|
|
7614
|
+
"workbench_path",
|
|
7615
|
+
"details_json",
|
|
7616
|
+
"payload_hash",
|
|
7617
|
+
"created_at"
|
|
7618
|
+
],
|
|
7619
|
+
[
|
|
7620
|
+
"event_id",
|
|
7621
|
+
"schema_version",
|
|
7622
|
+
"event_type",
|
|
7623
|
+
"severity",
|
|
7624
|
+
"occurred_at",
|
|
7625
|
+
"environment",
|
|
7626
|
+
"attention_required",
|
|
7627
|
+
"immediate_default",
|
|
7628
|
+
"summary",
|
|
7629
|
+
"details_json",
|
|
7630
|
+
"payload_hash",
|
|
7631
|
+
"created_at"
|
|
7632
|
+
]
|
|
7633
|
+
),
|
|
7634
|
+
attention_items: restoreSpec(
|
|
7635
|
+
"attention_id",
|
|
7636
|
+
[
|
|
7637
|
+
"attention_id",
|
|
7638
|
+
"attention_key",
|
|
7639
|
+
"status",
|
|
7640
|
+
"severity",
|
|
7641
|
+
"environment",
|
|
7642
|
+
"event_type",
|
|
7643
|
+
"capability",
|
|
7644
|
+
"contract_digest",
|
|
7645
|
+
"title",
|
|
7646
|
+
"occurrence_count",
|
|
7647
|
+
"first_event_id",
|
|
7648
|
+
"latest_event_id",
|
|
7649
|
+
"first_seen_at",
|
|
7650
|
+
"last_seen_at",
|
|
7651
|
+
"acknowledged_by",
|
|
7652
|
+
"acknowledged_at",
|
|
7653
|
+
"acknowledgement_identity_json",
|
|
7654
|
+
"acknowledgement_decision_hash",
|
|
7655
|
+
"acknowledgement_signature",
|
|
7656
|
+
"acknowledgement_integrity_hash",
|
|
7657
|
+
"resolved_at",
|
|
7658
|
+
"expires_at"
|
|
7659
|
+
],
|
|
7660
|
+
[
|
|
7661
|
+
"attention_id",
|
|
7662
|
+
"attention_key",
|
|
7663
|
+
"status",
|
|
7664
|
+
"severity",
|
|
7665
|
+
"environment",
|
|
7666
|
+
"event_type",
|
|
7667
|
+
"title",
|
|
7668
|
+
"occurrence_count",
|
|
7669
|
+
"first_event_id",
|
|
7670
|
+
"latest_event_id",
|
|
7671
|
+
"first_seen_at",
|
|
7672
|
+
"last_seen_at"
|
|
7673
|
+
]
|
|
7674
|
+
),
|
|
7675
|
+
notification_deliveries: restoreSpec(
|
|
7676
|
+
"delivery_id",
|
|
7677
|
+
[
|
|
7678
|
+
"delivery_id",
|
|
7679
|
+
"sink_id",
|
|
7680
|
+
"event_id",
|
|
7681
|
+
"attention_id",
|
|
7682
|
+
"status",
|
|
7683
|
+
"attempts",
|
|
7684
|
+
"max_attempts",
|
|
7685
|
+
"next_attempt_at",
|
|
7686
|
+
"lease_owner",
|
|
7687
|
+
"lease_id",
|
|
7688
|
+
"lease_expires_at",
|
|
7689
|
+
"last_error_code",
|
|
7690
|
+
"external_reference",
|
|
7691
|
+
"delivered_at",
|
|
7692
|
+
"created_at",
|
|
7693
|
+
"updated_at"
|
|
7694
|
+
],
|
|
7695
|
+
[
|
|
7696
|
+
"delivery_id",
|
|
7697
|
+
"sink_id",
|
|
7698
|
+
"event_id",
|
|
7699
|
+
"status",
|
|
7700
|
+
"attempts",
|
|
7701
|
+
"max_attempts",
|
|
7702
|
+
"next_attempt_at",
|
|
7703
|
+
"created_at",
|
|
7704
|
+
"updated_at"
|
|
7705
|
+
]
|
|
7706
|
+
),
|
|
5352
7707
|
runner_state: restoreSpec("key", ["key", "value_json", "updated_at"], ["key", "value_json", "updated_at"]),
|
|
5353
7708
|
policy_recommendations: restoreSpec("recommendation_id", ["recommendation_id", "tenant_id", "capability", "policy", "base_contract_digest", "status", "payload_json", "integrity_hash", "created_at", "updated_at"], ["recommendation_id", "tenant_id", "capability", "policy", "base_contract_digest", "status", "payload_json", "integrity_hash", "created_at", "updated_at"]),
|
|
5354
7709
|
cloud_outbox: restoreSpec("event_id", ["event_id", "proposal_id", "sequence", "kind", "status", "payload_hash", "payload_json", "attempts", "max_attempts", "next_attempt_at", "lease_owner", "lease_expires_at", "last_error_code", "sent_at", "acknowledged_at", "created_at", "updated_at"], ["event_id", "sequence", "kind", "status", "payload_hash", "payload_json", "attempts", "max_attempts", "next_attempt_at", "created_at", "updated_at"]),
|
|
@@ -5397,6 +7752,9 @@ function sharedLedgerRestoreRank(entry) {
|
|
|
5397
7752
|
"shadow_human_actions",
|
|
5398
7753
|
"shadow_outcomes",
|
|
5399
7754
|
"worker_queue",
|
|
7755
|
+
"attention_events",
|
|
7756
|
+
"attention_items",
|
|
7757
|
+
"notification_deliveries",
|
|
5400
7758
|
"runner_state",
|
|
5401
7759
|
"policy_recommendations",
|
|
5402
7760
|
"cloud_outbox",
|