@synapsor/runner 1.6.1 → 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 +98 -3
- 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 +33542 -20382
- 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/cursor-plugin.md +2 -2
- 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 +87 -4
- 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/app-owned-writeback/command-handler.mjs +0 -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/mcp-postgres-billing-app-handler/scripts/run-demo.sh +0 -0
- package/examples/operator-oidc/issuer.mjs +188 -0
- package/examples/reference-support-billing-app/scripts/run-demo.sh +0 -0
- package/examples/support-billing-agent/scripts/run-demo.sh +0 -0
- package/examples/support-billing-agent/scripts/run-evaluation.sh +0 -0
- package/examples/support-plan-credit/README.md +6 -2
- package/package.json +11 -9
- package/schemas/schema-candidate-review.schema.json +42 -0
- package/schemas/synapsor.runner.schema.json +227 -4
package/dist/runtime.mjs
CHANGED
|
@@ -158,7 +158,7 @@ var SUBJECT_KEYS = /* @__PURE__ */ new Set(["resource", "schema", "table", "prim
|
|
|
158
158
|
var ARG_KEYS = /* @__PURE__ */ new Set(["type", "description", "required", "max_length", "minimum", "maximum", "enum", "max_items", "fields"]);
|
|
159
159
|
var LOOKUP_KEYS = /* @__PURE__ */ new Set(["id_from_arg"]);
|
|
160
160
|
var EVIDENCE_KEYS = /* @__PURE__ */ new Set(["required", "sources", "query_audit", "handle_prefix"]);
|
|
161
|
-
var PROPOSAL_KEYS = /* @__PURE__ */ new Set(["action", "operation", "allowed_fields", "patch", "numeric_bounds", "transition_guards", "reversibility", "conflict_guard", "approval", "writeback"]);
|
|
161
|
+
var PROPOSAL_KEYS = /* @__PURE__ */ new Set(["action", "operation", "allowed_fields", "patch", "numeric_bounds", "transition_guards", "reversibility", "conflict_guard", "approval", "execution", "writeback"]);
|
|
162
162
|
var OPERATION_KEYS = /* @__PURE__ */ new Set(["kind", "cardinality", "selection", "max_rows", "aggregate_bounds", "batch", "deduplication", "version_advance"]);
|
|
163
163
|
var SELECTION_KEYS = /* @__PURE__ */ new Set(["all"]);
|
|
164
164
|
var PREDICATE_TERM_KEYS = /* @__PURE__ */ new Set(["column", "operator", "value"]);
|
|
@@ -173,6 +173,7 @@ var NUMERIC_BOUND_KEYS = /* @__PURE__ */ new Set(["minimum", "maximum"]);
|
|
|
173
173
|
var TRANSITION_GUARD_KEYS = /* @__PURE__ */ new Set(["from_column", "allowed"]);
|
|
174
174
|
var CONFLICT_GUARD_KEYS = /* @__PURE__ */ new Set(["column", "weak_guard_ack"]);
|
|
175
175
|
var APPROVAL_KEYS = /* @__PURE__ */ new Set(["mode", "required_role", "required_approvals", "policy"]);
|
|
176
|
+
var EXECUTION_KEYS = /* @__PURE__ */ new Set(["supervised_worker"]);
|
|
176
177
|
var WRITEBACK_KEYS = /* @__PURE__ */ new Set(["mode", "executor", "idempotency_key"]);
|
|
177
178
|
var REVERSIBILITY_KEYS = /* @__PURE__ */ new Set(["mode"]);
|
|
178
179
|
var AGGREGATE_READ_KEYS = /* @__PURE__ */ new Set(["function", "count_mode", "column", "selection", "minimum_group_size"]);
|
|
@@ -394,8 +395,8 @@ function validateContexts(value, errors, warnings) {
|
|
|
394
395
|
}
|
|
395
396
|
function validateCapabilities(value, contextNames, resourceNames, errors, warnings) {
|
|
396
397
|
const names = /* @__PURE__ */ new Set();
|
|
397
|
-
if (!Array.isArray(value)
|
|
398
|
-
errors.push({ path: "$.capabilities", code: "CAPABILITIES_REQUIRED", message: "
|
|
398
|
+
if (!Array.isArray(value)) {
|
|
399
|
+
errors.push({ path: "$.capabilities", code: "CAPABILITIES_REQUIRED", message: "capabilities must be an array; an empty array represents zero authority." });
|
|
399
400
|
return names;
|
|
400
401
|
}
|
|
401
402
|
value.forEach((capability, index) => {
|
|
@@ -1122,6 +1123,55 @@ function validateProposalAction(value, subject, path3, errors) {
|
|
|
1122
1123
|
});
|
|
1123
1124
|
}
|
|
1124
1125
|
}
|
|
1126
|
+
if (value.execution !== void 0) {
|
|
1127
|
+
validateNestedObject(value.execution, EXECUTION_KEYS, `${path3}.execution`, errors);
|
|
1128
|
+
if (isRecord(value.execution) && value.execution.supervised_worker !== "allowed") {
|
|
1129
|
+
errors.push({
|
|
1130
|
+
path: `${path3}.execution.supervised_worker`,
|
|
1131
|
+
code: "INVALID_SUPERVISED_WORKER_PERMISSION",
|
|
1132
|
+
message: "execution.supervised_worker must be allowed when the optional permission is present."
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
if (isRecord(value.execution) && value.execution.supervised_worker === "allowed") {
|
|
1136
|
+
const cardinality = isRecord(value.operation) ? value.operation.cardinality ?? "single" : "single";
|
|
1137
|
+
const writebackMode = isRecord(value.writeback) ? value.writeback.mode : "direct_sql";
|
|
1138
|
+
if (writebackMode !== "direct_sql") {
|
|
1139
|
+
errors.push({
|
|
1140
|
+
path: `${path3}.writeback.mode`,
|
|
1141
|
+
code: "SUPERVISED_WORKER_DIRECT_SQL_REQUIRED",
|
|
1142
|
+
message: "supervised-worker execution is initially limited to Runner-owned direct_sql writeback."
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
if (cardinality !== "single") {
|
|
1146
|
+
errors.push({
|
|
1147
|
+
path: `${path3}.operation.cardinality`,
|
|
1148
|
+
code: "SUPERVISED_WORKER_SINGLE_ROW_REQUIRED",
|
|
1149
|
+
message: "supervised-worker execution is initially limited to deterministic single-row writes."
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
if (deleteOperation) {
|
|
1153
|
+
errors.push({
|
|
1154
|
+
path: `${path3}.operation.kind`,
|
|
1155
|
+
code: "SUPERVISED_WORKER_DELETE_FORBIDDEN",
|
|
1156
|
+
message: "hard DELETE is not eligible for supervised-worker execution in this release."
|
|
1157
|
+
});
|
|
1158
|
+
}
|
|
1159
|
+
if (value.reversibility !== void 0) {
|
|
1160
|
+
errors.push({
|
|
1161
|
+
path: `${path3}.reversibility`,
|
|
1162
|
+
code: "SUPERVISED_WORKER_REVERSIBILITY_FORBIDDEN",
|
|
1163
|
+
message: "reviewed compensation actions require manual execution in this release."
|
|
1164
|
+
});
|
|
1165
|
+
}
|
|
1166
|
+
if (operation === "update" && (!isRecord(value.conflict_guard) || !isSafeIdentifier(value.conflict_guard.column) || value.conflict_guard.weak_guard_ack === true)) {
|
|
1167
|
+
errors.push({
|
|
1168
|
+
path: `${path3}.conflict_guard.column`,
|
|
1169
|
+
code: "SUPERVISED_WORKER_EXACT_CONFLICT_GUARD_REQUIRED",
|
|
1170
|
+
message: "supervised-worker UPDATE requires an exact source version/conflict column; a weak row hash is not eligible."
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
1125
1175
|
if (value.writeback !== void 0) {
|
|
1126
1176
|
validateNestedObject(value.writeback, WRITEBACK_KEYS, `${path3}.writeback`, errors);
|
|
1127
1177
|
if (isRecord(value.writeback) && !["direct_sql", "app_handler", "cloud_worker", "none"].includes(String(value.writeback.mode)))
|
|
@@ -1467,7 +1517,7 @@ function validatePolicies(value, errors) {
|
|
|
1467
1517
|
if (!Array.isArray(policy.rules)) {
|
|
1468
1518
|
errors.push({ path: `${path3}.rules`, code: "POLICY_RULES_NOT_ARRAY", message: "policy.rules must be an array." });
|
|
1469
1519
|
} else if (policy.kind === "approval") {
|
|
1470
|
-
policy.rules.forEach((
|
|
1520
|
+
policy.rules.forEach((rule2, ruleIndex) => validateApprovalPolicyRuleShape(rule2, `${path3}.rules[${ruleIndex}]`, errors));
|
|
1471
1521
|
}
|
|
1472
1522
|
}
|
|
1473
1523
|
if (policy.limits !== void 0) {
|
|
@@ -1507,16 +1557,16 @@ function validateApprovalPolicyLimitShape(limit, path3, errors) {
|
|
|
1507
1557
|
errors.push({ path: `${path3}.field`, code: "APPROVAL_POLICY_COUNT_FIELD_FORBIDDEN", message: "count approval limits must not declare a field." });
|
|
1508
1558
|
}
|
|
1509
1559
|
}
|
|
1510
|
-
function validateApprovalPolicyRuleShape(
|
|
1511
|
-
if (!isRecord(
|
|
1560
|
+
function validateApprovalPolicyRuleShape(rule2, path3, errors) {
|
|
1561
|
+
if (!isRecord(rule2)) {
|
|
1512
1562
|
errors.push({ path: path3, code: "APPROVAL_POLICY_RULE_NOT_OBJECT", message: "approval policy rules must be objects." });
|
|
1513
1563
|
return;
|
|
1514
1564
|
}
|
|
1515
1565
|
const keys = /* @__PURE__ */ new Set(["field", "max"]);
|
|
1516
|
-
checkUnknownKeys(
|
|
1517
|
-
if (!isSafeIdentifier(
|
|
1566
|
+
checkUnknownKeys(rule2, keys, path3, errors);
|
|
1567
|
+
if (!isSafeIdentifier(rule2.field))
|
|
1518
1568
|
errors.push({ path: `${path3}.field`, code: "INVALID_APPROVAL_POLICY_FIELD", message: "approval policy rule field must be a safe identifier." });
|
|
1519
|
-
if (!Number.isInteger(
|
|
1569
|
+
if (!Number.isInteger(rule2.max) || Number(rule2.max) < 0)
|
|
1520
1570
|
errors.push({ path: `${path3}.max`, code: "INVALID_APPROVAL_POLICY_MAX", message: "approval policy rule max must be a non-negative integer." });
|
|
1521
1571
|
}
|
|
1522
1572
|
function validateCapabilityApprovalPolicies(capabilities, policies, policyByName, errors) {
|
|
@@ -1563,11 +1613,11 @@ function validateApprovalPolicyRulesAgainstCapability(policy, policyIndex, capab
|
|
|
1563
1613
|
if (!proposal)
|
|
1564
1614
|
return;
|
|
1565
1615
|
if (Array.isArray(policy.rules))
|
|
1566
|
-
policy.rules.forEach((
|
|
1567
|
-
if (!isRecord(
|
|
1616
|
+
policy.rules.forEach((rule2, ruleIndex) => {
|
|
1617
|
+
if (!isRecord(rule2))
|
|
1568
1618
|
return;
|
|
1569
|
-
const field =
|
|
1570
|
-
const max =
|
|
1619
|
+
const field = rule2.field;
|
|
1620
|
+
const max = rule2.max;
|
|
1571
1621
|
const rulePath = `$.policies[${policyIndex}].rules[${ruleIndex}]`;
|
|
1572
1622
|
if (!isSafeIdentifier(field) || !Number.isInteger(max))
|
|
1573
1623
|
return;
|
|
@@ -1787,7 +1837,7 @@ function sortJson(value) {
|
|
|
1787
1837
|
}
|
|
1788
1838
|
|
|
1789
1839
|
// packages/config/src/index.ts
|
|
1790
|
-
var TOP_LEVEL_KEYS2 = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "proposal_freshness", "operator_identity", "session_auth", "http_security", "rate_limits", "metrics", "graduated_trust", "cloud", "governance", "generated_authority", "strict", "result_format"]);
|
|
1840
|
+
var TOP_LEVEL_KEYS2 = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "proposal_freshness", "operator_identity", "session_auth", "http_security", "rate_limits", "metrics", "graduated_trust", "cloud", "governance", "generated_authority", "supervised_worker", "notifications", "strict", "result_format"]);
|
|
1791
1841
|
var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path", "shared_postgres"]);
|
|
1792
1842
|
var SHARED_POSTGRES_STORAGE_KEYS = /* @__PURE__ */ new Set(["mode", "url_env", "schema", "lock_timeout_ms", "max_entries"]);
|
|
1793
1843
|
var APPROVALS_KEYS = /* @__PURE__ */ new Set(["disable_auto_approval"]);
|
|
@@ -1876,6 +1926,95 @@ var RATE_LIMIT_RULE_KEYS = /* @__PURE__ */ new Set(["requests", "window_seconds"
|
|
|
1876
1926
|
var CLOUD_KEYS = /* @__PURE__ */ new Set(["base_url_env", "runner_token_env", "runner_id", "runner_version", "project_id", "adapter_id", "source_id", "engines", "capabilities", "session"]);
|
|
1877
1927
|
var GOVERNANCE_KEYS = /* @__PURE__ */ new Set(["mode", "connection_file", "evidence_residency", "queue_when_unavailable", "sync_interval_ms", "max_attempts", "outbox_retention_days"]);
|
|
1878
1928
|
var GENERATED_AUTHORITY_KEYS = /* @__PURE__ */ new Set(["generation_lock_path", "enforcement"]);
|
|
1929
|
+
var SUPERVISED_WORKER_KEYS = /* @__PURE__ */ new Set(["enabled", "profile", "capabilities"]);
|
|
1930
|
+
var SUPERVISED_WORKER_CAPABILITY_KEYS = /* @__PURE__ */ new Set([
|
|
1931
|
+
"capability",
|
|
1932
|
+
"contract_digest",
|
|
1933
|
+
"mode",
|
|
1934
|
+
"concurrency",
|
|
1935
|
+
"queue_limit",
|
|
1936
|
+
"lease_seconds",
|
|
1937
|
+
"max_attempts",
|
|
1938
|
+
"proposal_ttl_seconds",
|
|
1939
|
+
"rate_limit",
|
|
1940
|
+
"write_url_env",
|
|
1941
|
+
"require_least_privilege_writer",
|
|
1942
|
+
"writer_posture_fingerprint",
|
|
1943
|
+
"worker_identity",
|
|
1944
|
+
"control_role",
|
|
1945
|
+
"required_attention_sinks"
|
|
1946
|
+
]);
|
|
1947
|
+
var SUPERVISED_WORKER_RATE_LIMIT_KEYS = /* @__PURE__ */ new Set(["executions", "window_seconds"]);
|
|
1948
|
+
var NOTIFICATION_KEYS = /* @__PURE__ */ new Set(["enabled", "workbench_url_env", "sinks"]);
|
|
1949
|
+
var NOTIFICATION_SINK_KEYS = /* @__PURE__ */ new Set([
|
|
1950
|
+
"id",
|
|
1951
|
+
"type",
|
|
1952
|
+
"enabled",
|
|
1953
|
+
"url_env",
|
|
1954
|
+
"signing_secret_env",
|
|
1955
|
+
"destination",
|
|
1956
|
+
"minimum_severity",
|
|
1957
|
+
"events",
|
|
1958
|
+
"capabilities",
|
|
1959
|
+
"environments",
|
|
1960
|
+
"delivery",
|
|
1961
|
+
"max_attempts",
|
|
1962
|
+
"timeout_ms",
|
|
1963
|
+
"max_response_bytes",
|
|
1964
|
+
"replay_window_seconds",
|
|
1965
|
+
"allow_private_destinations",
|
|
1966
|
+
"private_host_allowlist",
|
|
1967
|
+
"recovery_notifications",
|
|
1968
|
+
"budgets",
|
|
1969
|
+
"quiet_hours"
|
|
1970
|
+
]);
|
|
1971
|
+
var NOTIFICATION_BUDGET_KEYS = /* @__PURE__ */ new Set([
|
|
1972
|
+
"per_minute",
|
|
1973
|
+
"per_hour",
|
|
1974
|
+
"immediate_informational_per_hour",
|
|
1975
|
+
"aggregation_window_seconds",
|
|
1976
|
+
"cooldown_seconds",
|
|
1977
|
+
"max_unresolved_reminders",
|
|
1978
|
+
"digest_cadence_minutes",
|
|
1979
|
+
"escalation_delay_seconds",
|
|
1980
|
+
"retry_attempt_threshold",
|
|
1981
|
+
"degraded_duration_seconds",
|
|
1982
|
+
"queue_depth_threshold",
|
|
1983
|
+
"queue_age_seconds"
|
|
1984
|
+
]);
|
|
1985
|
+
var NOTIFICATION_QUIET_HOURS_KEYS = /* @__PURE__ */ new Set(["start_utc_hour", "end_utc_hour"]);
|
|
1986
|
+
var NOTIFICATION_EVENT_TYPES = /* @__PURE__ */ new Set([
|
|
1987
|
+
"proposal.created",
|
|
1988
|
+
"proposal.review_required",
|
|
1989
|
+
"proposal.auto_approved",
|
|
1990
|
+
"proposal.approved",
|
|
1991
|
+
"proposal.queued",
|
|
1992
|
+
"proposal.expiring",
|
|
1993
|
+
"proposal.expired",
|
|
1994
|
+
"proposal.cancelled",
|
|
1995
|
+
"proposal.applied",
|
|
1996
|
+
"proposal.conflict",
|
|
1997
|
+
"proposal.refused",
|
|
1998
|
+
"worker.started",
|
|
1999
|
+
"worker.paused",
|
|
2000
|
+
"worker.unhealthy",
|
|
2001
|
+
"worker.recovered",
|
|
2002
|
+
"worker.queue_backlog",
|
|
2003
|
+
"worker.retry_scheduled",
|
|
2004
|
+
"worker.dead_lettered",
|
|
2005
|
+
"worker.unknown_outcome",
|
|
2006
|
+
"worker.reconciliation_required",
|
|
2007
|
+
"capability.review_required",
|
|
2008
|
+
"capability.activated",
|
|
2009
|
+
"capability.revoked",
|
|
2010
|
+
"contract.digest_stale",
|
|
2011
|
+
"schema.drift_detected",
|
|
2012
|
+
"credential.posture_changed",
|
|
2013
|
+
"policy.limit_near",
|
|
2014
|
+
"policy.limit_exceeded",
|
|
2015
|
+
"sensitive_override_activated",
|
|
2016
|
+
"notification.replayed"
|
|
2017
|
+
]);
|
|
1879
2018
|
var SOURCE_KEYS = /* @__PURE__ */ new Set([
|
|
1880
2019
|
"engine",
|
|
1881
2020
|
"read_url_env",
|
|
@@ -1917,6 +2056,7 @@ var CAPABILITY_KEYS2 = /* @__PURE__ */ new Set([
|
|
|
1917
2056
|
"reversibility",
|
|
1918
2057
|
"conflict_guard",
|
|
1919
2058
|
"approval",
|
|
2059
|
+
"execution",
|
|
1920
2060
|
"writeback",
|
|
1921
2061
|
"operation",
|
|
1922
2062
|
"single_tenant_dev_ack",
|
|
@@ -1935,6 +2075,7 @@ var TRANSITION_GUARD_KEYS2 = /* @__PURE__ */ new Set(["from_column", "allowed"])
|
|
|
1935
2075
|
var REVERSIBILITY_KEYS2 = /* @__PURE__ */ new Set(["mode"]);
|
|
1936
2076
|
var CONFLICT_GUARD_KEYS2 = /* @__PURE__ */ new Set(["column", "weak_guard_ack"]);
|
|
1937
2077
|
var APPROVAL_KEYS2 = /* @__PURE__ */ new Set(["mode", "required_role", "required_approvals", "policy"]);
|
|
2078
|
+
var EXECUTION_KEYS2 = /* @__PURE__ */ new Set(["supervised_worker"]);
|
|
1938
2079
|
var POLICY_KEYS2 = /* @__PURE__ */ new Set(["name", "kind", "mode", "rules", "limits"]);
|
|
1939
2080
|
var APPROVAL_POLICY_RULE_KEYS = /* @__PURE__ */ new Set(["field", "max"]);
|
|
1940
2081
|
var APPROVAL_POLICY_LIMIT_KEYS2 = /* @__PURE__ */ new Set(["kind", "max", "period", "field", "scope"]);
|
|
@@ -2036,7 +2177,20 @@ function validateRunnerCapabilityConfig(input) {
|
|
|
2036
2177
|
validateRateLimits(input.rate_limits, strict, errors);
|
|
2037
2178
|
validateMetrics(input.metrics, strict, errors);
|
|
2038
2179
|
validateGraduatedTrust(input.graduated_trust, strict, errors);
|
|
2039
|
-
validateCapabilities2(
|
|
2180
|
+
validateCapabilities2(
|
|
2181
|
+
input.capabilities,
|
|
2182
|
+
input.sources,
|
|
2183
|
+
input.contexts,
|
|
2184
|
+
input.executors,
|
|
2185
|
+
input.mode,
|
|
2186
|
+
strict,
|
|
2187
|
+
errors,
|
|
2188
|
+
warnings,
|
|
2189
|
+
hasContracts,
|
|
2190
|
+
input.generated_authority
|
|
2191
|
+
);
|
|
2192
|
+
validateNotifications(input.notifications, strict, errors);
|
|
2193
|
+
validateSupervisedWorker(input.supervised_worker, input, strict, errors);
|
|
2040
2194
|
validateProposalFreshness(input.proposal_freshness, input.capabilities, strict, errors);
|
|
2041
2195
|
validateEffectiveContextCompatibility(input.trusted_context, input.contexts, input.capabilities, errors);
|
|
2042
2196
|
validateApprovalPolicyReferences(input.capabilities, input.policies, errors);
|
|
@@ -2058,6 +2212,314 @@ function validateGeneratedAuthority(value, strict, errors) {
|
|
|
2058
2212
|
errors.push({ path: "$.generated_authority.enforcement", code: "INVALID_GENERATION_LOCK_ENFORCEMENT", message: "generated authority must use required lock enforcement." });
|
|
2059
2213
|
}
|
|
2060
2214
|
}
|
|
2215
|
+
function validateNotifications(value, strict, errors) {
|
|
2216
|
+
if (value === void 0) return;
|
|
2217
|
+
if (!isRecord2(value)) {
|
|
2218
|
+
errors.push({ path: "$.notifications", code: "NOTIFICATIONS_NOT_OBJECT", message: "notifications must be an operator-owned configuration object." });
|
|
2219
|
+
return;
|
|
2220
|
+
}
|
|
2221
|
+
if (strict) checkUnknownKeys2(value, NOTIFICATION_KEYS, "$.notifications", errors);
|
|
2222
|
+
if (typeof value.enabled !== "boolean") {
|
|
2223
|
+
errors.push({ path: "$.notifications.enabled", code: "NOTIFICATIONS_ENABLED_REQUIRED", message: "notifications.enabled must be an explicit boolean." });
|
|
2224
|
+
}
|
|
2225
|
+
if (value.workbench_url_env !== void 0 && !isEnvName(value.workbench_url_env)) {
|
|
2226
|
+
errors.push({ path: "$.notifications.workbench_url_env", code: "INVALID_WORKBENCH_URL_ENV", message: "workbench_url_env must name an environment variable, not contain a URL." });
|
|
2227
|
+
}
|
|
2228
|
+
if (!Array.isArray(value.sinks)) {
|
|
2229
|
+
errors.push({ path: "$.notifications.sinks", code: "NOTIFICATION_SINKS_REQUIRED", message: "notifications.sinks must be an explicit array." });
|
|
2230
|
+
return;
|
|
2231
|
+
}
|
|
2232
|
+
if (value.sinks.length > 32) {
|
|
2233
|
+
errors.push({ path: "$.notifications.sinks", code: "TOO_MANY_NOTIFICATION_SINKS", message: "notifications supports at most 32 operator-owned sinks." });
|
|
2234
|
+
}
|
|
2235
|
+
if (value.enabled === true && value.sinks.filter(isRecord2).every((sink) => sink.enabled === false)) {
|
|
2236
|
+
errors.push({ path: "$.notifications.sinks", code: "ENABLED_NOTIFICATION_SINK_REQUIRED", message: "enabled notifications require at least one sink that is not disabled." });
|
|
2237
|
+
}
|
|
2238
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2239
|
+
for (const [index, rawSink] of value.sinks.entries()) {
|
|
2240
|
+
const path3 = `$.notifications.sinks[${index}]`;
|
|
2241
|
+
if (!isRecord2(rawSink)) {
|
|
2242
|
+
errors.push({ path: path3, code: "NOTIFICATION_SINK_NOT_OBJECT", message: "each notification sink must be an object." });
|
|
2243
|
+
continue;
|
|
2244
|
+
}
|
|
2245
|
+
if (strict) checkUnknownKeys2(rawSink, NOTIFICATION_SINK_KEYS, path3, errors);
|
|
2246
|
+
if (!isSafeName(rawSink.id)) {
|
|
2247
|
+
errors.push({ path: `${path3}.id`, code: "INVALID_NOTIFICATION_SINK_ID", message: "notification sink id must be a fixed safe name." });
|
|
2248
|
+
} else if (seen.has(rawSink.id)) {
|
|
2249
|
+
errors.push({ path: `${path3}.id`, code: "DUPLICATE_NOTIFICATION_SINK_ID", message: "notification sink ids must be unique." });
|
|
2250
|
+
} else seen.add(rawSink.id);
|
|
2251
|
+
if (rawSink.type !== "webhook" && rawSink.type !== "jsonl") {
|
|
2252
|
+
errors.push({ path: `${path3}.type`, code: "INVALID_NOTIFICATION_SINK_TYPE", message: "notification sink type must be webhook or jsonl." });
|
|
2253
|
+
}
|
|
2254
|
+
if (rawSink.enabled !== void 0 && typeof rawSink.enabled !== "boolean") {
|
|
2255
|
+
errors.push({ path: `${path3}.enabled`, code: "INVALID_NOTIFICATION_SINK_ENABLED", message: "notification sink enabled must be boolean." });
|
|
2256
|
+
}
|
|
2257
|
+
if (rawSink.type === "webhook") {
|
|
2258
|
+
if (!isEnvName(rawSink.url_env)) {
|
|
2259
|
+
errors.push({ path: `${path3}.url_env`, code: "NOTIFICATION_WEBHOOK_URL_ENV_REQUIRED", message: "webhook url_env must name an environment variable." });
|
|
2260
|
+
}
|
|
2261
|
+
if (!isEnvName(rawSink.signing_secret_env)) {
|
|
2262
|
+
errors.push({ path: `${path3}.signing_secret_env`, code: "NOTIFICATION_SIGNING_SECRET_ENV_REQUIRED", message: "webhook signing_secret_env must name an environment variable." });
|
|
2263
|
+
}
|
|
2264
|
+
if (rawSink.destination !== void 0) {
|
|
2265
|
+
errors.push({ path: `${path3}.destination`, code: "WEBHOOK_DESTINATION_UNSUPPORTED", message: "webhook destinations come only from url_env." });
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
if (rawSink.type === "jsonl") {
|
|
2269
|
+
if (rawSink.destination !== "stdout") {
|
|
2270
|
+
errors.push({ path: `${path3}.destination`, code: "JSONL_STDOUT_REQUIRED", message: "the built-in JSONL development sink writes only to stdout." });
|
|
2271
|
+
}
|
|
2272
|
+
if (rawSink.url_env !== void 0 || rawSink.signing_secret_env !== void 0) {
|
|
2273
|
+
errors.push({ path: path3, code: "JSONL_WEBHOOK_FIELDS_FORBIDDEN", message: "JSONL sinks do not accept webhook URL or signing-secret fields." });
|
|
2274
|
+
}
|
|
2275
|
+
}
|
|
2276
|
+
if (rawSink.minimum_severity !== void 0 && !["informational", "warning", "critical"].includes(String(rawSink.minimum_severity))) {
|
|
2277
|
+
errors.push({ path: `${path3}.minimum_severity`, code: "INVALID_NOTIFICATION_SEVERITY", message: "minimum_severity must be informational, warning, or critical." });
|
|
2278
|
+
}
|
|
2279
|
+
if (rawSink.delivery !== void 0 && !["immediate", "digest", "all"].includes(String(rawSink.delivery))) {
|
|
2280
|
+
errors.push({ path: `${path3}.delivery`, code: "INVALID_NOTIFICATION_DELIVERY_MODE", message: "delivery must be immediate, digest, or all." });
|
|
2281
|
+
}
|
|
2282
|
+
if (rawSink.events !== void 0) {
|
|
2283
|
+
if (!Array.isArray(rawSink.events) || rawSink.events.length > 64 || rawSink.events.some((event) => typeof event !== "string" || !NOTIFICATION_EVENT_TYPES.has(event))) {
|
|
2284
|
+
errors.push({ path: `${path3}.events`, code: "INVALID_NOTIFICATION_EVENT_FILTER", message: "events must contain only supported fixed attention event names." });
|
|
2285
|
+
}
|
|
2286
|
+
}
|
|
2287
|
+
if (rawSink.capabilities !== void 0 && (!Array.isArray(rawSink.capabilities) || rawSink.capabilities.length > 256 || rawSink.capabilities.some((item) => !isQualifiedName2(item)))) {
|
|
2288
|
+
errors.push({ path: `${path3}.capabilities`, code: "INVALID_NOTIFICATION_CAPABILITY_FILTER", message: "capabilities must contain fixed qualified capability names." });
|
|
2289
|
+
}
|
|
2290
|
+
if (rawSink.environments !== void 0 && (!Array.isArray(rawSink.environments) || rawSink.environments.length > 4 || rawSink.environments.some((item) => !["development", "staging", "production", "unknown"].includes(String(item))))) {
|
|
2291
|
+
errors.push({ path: `${path3}.environments`, code: "INVALID_NOTIFICATION_ENVIRONMENT_FILTER", message: "environments must contain development, staging, production, or unknown." });
|
|
2292
|
+
}
|
|
2293
|
+
for (const [key, minimum, maximum] of [
|
|
2294
|
+
["max_attempts", 1, 100],
|
|
2295
|
+
["timeout_ms", 100, 3e4],
|
|
2296
|
+
["max_response_bytes", 0, 65536],
|
|
2297
|
+
["replay_window_seconds", 30, 3600]
|
|
2298
|
+
]) {
|
|
2299
|
+
if (rawSink[key] !== void 0 && (!Number.isSafeInteger(rawSink[key]) || Number(rawSink[key]) < minimum || Number(rawSink[key]) > maximum)) {
|
|
2300
|
+
errors.push({ path: `${path3}.${key}`, code: "INVALID_NOTIFICATION_BOUND", message: `${key} must be an integer from ${minimum} through ${maximum}.` });
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
if (rawSink.allow_private_destinations !== void 0 && typeof rawSink.allow_private_destinations !== "boolean") {
|
|
2304
|
+
errors.push({ path: `${path3}.allow_private_destinations`, code: "INVALID_PRIVATE_DESTINATION_POLICY", message: "allow_private_destinations must be boolean." });
|
|
2305
|
+
}
|
|
2306
|
+
if (rawSink.recovery_notifications !== void 0 && typeof rawSink.recovery_notifications !== "boolean") {
|
|
2307
|
+
errors.push({ path: `${path3}.recovery_notifications`, code: "INVALID_RECOVERY_NOTIFICATION_POLICY", message: "recovery_notifications must be boolean." });
|
|
2308
|
+
}
|
|
2309
|
+
if (rawSink.private_host_allowlist !== void 0) {
|
|
2310
|
+
if (!Array.isArray(rawSink.private_host_allowlist) || rawSink.private_host_allowlist.length > 32 || rawSink.private_host_allowlist.some((host) => !isSafeNotificationHost(host))) {
|
|
2311
|
+
errors.push({ path: `${path3}.private_host_allowlist`, code: "INVALID_PRIVATE_HOST_ALLOWLIST", message: "private_host_allowlist must contain at most 32 fixed DNS hostnames or IP literals." });
|
|
2312
|
+
}
|
|
2313
|
+
if (rawSink.allow_private_destinations !== true && Array.isArray(rawSink.private_host_allowlist) && rawSink.private_host_allowlist.length > 0) {
|
|
2314
|
+
errors.push({ path: `${path3}.allow_private_destinations`, code: "PRIVATE_DESTINATION_OPT_IN_REQUIRED", message: "private host exceptions require allow_private_destinations=true." });
|
|
2315
|
+
}
|
|
2316
|
+
}
|
|
2317
|
+
validateNotificationBudgets(rawSink.budgets, `${path3}.budgets`, strict, errors);
|
|
2318
|
+
validateNotificationQuietHours(rawSink.quiet_hours, `${path3}.quiet_hours`, strict, errors);
|
|
2319
|
+
}
|
|
2320
|
+
}
|
|
2321
|
+
function validateNotificationBudgets(value, path3, strict, errors) {
|
|
2322
|
+
if (value === void 0) return;
|
|
2323
|
+
if (!isRecord2(value)) {
|
|
2324
|
+
errors.push({ path: path3, code: "NOTIFICATION_BUDGETS_NOT_OBJECT", message: "notification budgets must be an object." });
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
if (strict) checkUnknownKeys2(value, NOTIFICATION_BUDGET_KEYS, path3, errors);
|
|
2328
|
+
for (const [key, minimum, maximum] of [
|
|
2329
|
+
["per_minute", 1, 1e4],
|
|
2330
|
+
["per_hour", 1, 1e5],
|
|
2331
|
+
["immediate_informational_per_hour", 0, 1e4],
|
|
2332
|
+
["aggregation_window_seconds", 1, 86400],
|
|
2333
|
+
["cooldown_seconds", 0, 604800],
|
|
2334
|
+
["max_unresolved_reminders", 0, 100],
|
|
2335
|
+
["digest_cadence_minutes", 1, 10080],
|
|
2336
|
+
["escalation_delay_seconds", 0, 604800],
|
|
2337
|
+
["retry_attempt_threshold", 1, 100],
|
|
2338
|
+
["degraded_duration_seconds", 1, 86400],
|
|
2339
|
+
["queue_depth_threshold", 1, 1e6],
|
|
2340
|
+
["queue_age_seconds", 1, 604800]
|
|
2341
|
+
]) {
|
|
2342
|
+
if (value[key] !== void 0 && (!Number.isSafeInteger(value[key]) || Number(value[key]) < minimum || Number(value[key]) > maximum)) {
|
|
2343
|
+
errors.push({ path: `${path3}.${key}`, code: "INVALID_NOTIFICATION_BUDGET", message: `${key} must be an integer from ${minimum} through ${maximum}.` });
|
|
2344
|
+
}
|
|
2345
|
+
}
|
|
2346
|
+
}
|
|
2347
|
+
function validateNotificationQuietHours(value, path3, strict, errors) {
|
|
2348
|
+
if (value === void 0) return;
|
|
2349
|
+
if (!isRecord2(value)) {
|
|
2350
|
+
errors.push({ path: path3, code: "NOTIFICATION_QUIET_HOURS_NOT_OBJECT", message: "quiet_hours must be an object." });
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
if (strict) checkUnknownKeys2(value, NOTIFICATION_QUIET_HOURS_KEYS, path3, errors);
|
|
2354
|
+
for (const key of ["start_utc_hour", "end_utc_hour"]) {
|
|
2355
|
+
if (!Number.isSafeInteger(value[key]) || Number(value[key]) < 0 || Number(value[key]) > 23) {
|
|
2356
|
+
errors.push({ path: `${path3}.${key}`, code: "INVALID_NOTIFICATION_QUIET_HOUR", message: `${key} must be an integer from 0 through 23.` });
|
|
2357
|
+
}
|
|
2358
|
+
}
|
|
2359
|
+
}
|
|
2360
|
+
function validateSupervisedWorker(value, config, strict, errors) {
|
|
2361
|
+
if (value === void 0) return;
|
|
2362
|
+
if (!isRecord2(value)) {
|
|
2363
|
+
errors.push({ path: "$.supervised_worker", code: "SUPERVISED_WORKER_NOT_OBJECT", message: "supervised_worker must be an explicit deployment policy object." });
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
if (strict) checkUnknownKeys2(value, SUPERVISED_WORKER_KEYS, "$.supervised_worker", errors);
|
|
2367
|
+
if (typeof value.enabled !== "boolean") {
|
|
2368
|
+
errors.push({ path: "$.supervised_worker.enabled", code: "SUPERVISED_WORKER_ENABLED_REQUIRED", message: "supervised_worker.enabled must be an explicit boolean." });
|
|
2369
|
+
}
|
|
2370
|
+
if (value.profile !== "development" && value.profile !== "staging" && value.profile !== "production") {
|
|
2371
|
+
errors.push({ path: "$.supervised_worker.profile", code: "SUPERVISED_WORKER_PROFILE_REQUIRED", message: "supervised_worker.profile must be development, staging, or production." });
|
|
2372
|
+
}
|
|
2373
|
+
if (!Array.isArray(value.capabilities)) {
|
|
2374
|
+
errors.push({ path: "$.supervised_worker.capabilities", code: "SUPERVISED_WORKER_CAPABILITIES_REQUIRED", message: "supervised_worker.capabilities must be an explicit exact-digest allowlist." });
|
|
2375
|
+
return;
|
|
2376
|
+
}
|
|
2377
|
+
if (value.enabled === true && value.capabilities.length === 0) {
|
|
2378
|
+
errors.push({ path: "$.supervised_worker.capabilities", code: "SUPERVISED_WORKER_ALLOWLIST_REQUIRED", message: "enabled supervised-worker execution requires at least one exact capability/digest allowlist entry." });
|
|
2379
|
+
}
|
|
2380
|
+
if (config.mode !== "review" && value.enabled === true) {
|
|
2381
|
+
errors.push({ path: "$.mode", code: "SUPERVISED_WORKER_REVIEW_MODE_REQUIRED", message: "supervised-worker execution requires Runner review mode." });
|
|
2382
|
+
}
|
|
2383
|
+
if (isRecord2(config.governance) && config.governance.mode === "cloud_linked" && value.enabled === true) {
|
|
2384
|
+
errors.push({ path: "$.governance.mode", code: "SUPERVISED_WORKER_LOCAL_AUTHORITY_REQUIRED", message: "Cloud-linked governance does not permit the local supervised worker to become execution authority." });
|
|
2385
|
+
}
|
|
2386
|
+
if (value.profile === "production" && value.enabled === true) {
|
|
2387
|
+
const operator = isRecord2(config.operator_identity) ? config.operator_identity : void 0;
|
|
2388
|
+
if (!operator || operator.provider !== "signed_key" && operator.provider !== "jwt_oidc") {
|
|
2389
|
+
errors.push({ path: "$.operator_identity.provider", code: "SUPERVISED_WORKER_VERIFIED_OPERATOR_REQUIRED", message: "production supervised-worker controls require signed_key or jwt_oidc operator identity." });
|
|
2390
|
+
}
|
|
2391
|
+
}
|
|
2392
|
+
const runtimeCapabilities = Array.isArray(config.capabilities) ? config.capabilities.filter(isRecord2) : [];
|
|
2393
|
+
const byName = new Map(runtimeCapabilities.flatMap((capability) => isQualifiedName2(capability.name) ? [[String(capability.name), capability]] : []));
|
|
2394
|
+
const sources = isRecord2(config.sources) ? config.sources : {};
|
|
2395
|
+
const notifications = isRecord2(config.notifications) ? config.notifications : void 0;
|
|
2396
|
+
const notificationSinks = new Map(
|
|
2397
|
+
Array.isArray(notifications?.sinks) ? notifications.sinks.filter(isRecord2).filter((sink) => isSafeName(sink.id)).map((sink) => [String(sink.id), sink]) : []
|
|
2398
|
+
);
|
|
2399
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2400
|
+
for (const [index, rawEntry] of value.capabilities.entries()) {
|
|
2401
|
+
const path3 = `$.supervised_worker.capabilities[${index}]`;
|
|
2402
|
+
if (!isRecord2(rawEntry)) {
|
|
2403
|
+
errors.push({ path: path3, code: "SUPERVISED_WORKER_CAPABILITY_NOT_OBJECT", message: "each supervised-worker allowlist entry must be an object." });
|
|
2404
|
+
continue;
|
|
2405
|
+
}
|
|
2406
|
+
if (strict) checkUnknownKeys2(rawEntry, SUPERVISED_WORKER_CAPABILITY_KEYS, path3, errors);
|
|
2407
|
+
if (!isQualifiedName2(rawEntry.capability)) {
|
|
2408
|
+
errors.push({ path: `${path3}.capability`, code: "INVALID_SUPERVISED_WORKER_CAPABILITY", message: "capability must be a fixed qualified capability name." });
|
|
2409
|
+
}
|
|
2410
|
+
if (typeof rawEntry.contract_digest !== "string" || !/^sha256:[a-f0-9]{64}$/.test(rawEntry.contract_digest)) {
|
|
2411
|
+
errors.push({ path: `${path3}.contract_digest`, code: "INVALID_SUPERVISED_WORKER_DIGEST", message: "contract_digest must be the exact canonical sha256 digest." });
|
|
2412
|
+
}
|
|
2413
|
+
if (rawEntry.mode !== "supervised_worker") {
|
|
2414
|
+
errors.push({ path: `${path3}.mode`, code: "INVALID_SUPERVISED_WORKER_MODE", message: "mode must be supervised_worker." });
|
|
2415
|
+
}
|
|
2416
|
+
for (const [key2, minimum, maximum] of [
|
|
2417
|
+
["concurrency", 1, 32],
|
|
2418
|
+
["queue_limit", 1, 1e4],
|
|
2419
|
+
["lease_seconds", 15, 3600],
|
|
2420
|
+
["max_attempts", 1, 100],
|
|
2421
|
+
["proposal_ttl_seconds", 60, 2592e3]
|
|
2422
|
+
]) {
|
|
2423
|
+
if (!Number.isSafeInteger(rawEntry[key2]) || Number(rawEntry[key2]) < minimum || Number(rawEntry[key2]) > maximum) {
|
|
2424
|
+
errors.push({ path: `${path3}.${key2}`, code: "INVALID_SUPERVISED_WORKER_BOUND", message: `${key2} must be an integer from ${minimum} through ${maximum}.` });
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
if (!isEnvName(rawEntry.write_url_env)) {
|
|
2428
|
+
errors.push({ path: `${path3}.write_url_env`, code: "SUPERVISED_WORKER_WRITE_ENV_REQUIRED", message: "write_url_env must name the separately configured writer credential environment variable." });
|
|
2429
|
+
}
|
|
2430
|
+
if (rawEntry.require_least_privilege_writer !== void 0 && typeof rawEntry.require_least_privilege_writer !== "boolean") {
|
|
2431
|
+
errors.push({ path: `${path3}.require_least_privilege_writer`, code: "INVALID_SUPERVISED_WORKER_POSTURE_MODE", message: "require_least_privilege_writer must be an explicit boolean when present." });
|
|
2432
|
+
}
|
|
2433
|
+
if (rawEntry.writer_posture_fingerprint !== void 0 && (typeof rawEntry.writer_posture_fingerprint !== "string" || !/^sha256:[a-f0-9]{64}$/.test(rawEntry.writer_posture_fingerprint))) {
|
|
2434
|
+
errors.push({ path: `${path3}.writer_posture_fingerprint`, code: "INVALID_SUPERVISED_WORKER_POSTURE_FINGERPRINT", message: "writer_posture_fingerprint must be the reviewed non-secret sha256 posture digest." });
|
|
2435
|
+
}
|
|
2436
|
+
if (rawEntry.require_least_privilege_writer === true && rawEntry.writer_posture_fingerprint === void 0) {
|
|
2437
|
+
errors.push({ path: `${path3}.writer_posture_fingerprint`, code: "SUPERVISED_WORKER_POSTURE_FINGERPRINT_REQUIRED", message: "hardened writer posture requires an exact reviewed posture fingerprint." });
|
|
2438
|
+
}
|
|
2439
|
+
if (value.enabled === true && value.profile === "production" && (rawEntry.require_least_privilege_writer !== true || rawEntry.writer_posture_fingerprint === void 0)) {
|
|
2440
|
+
errors.push({ path: path3, code: "SUPERVISED_WORKER_PRODUCTION_POSTURE_REQUIRED", message: "production supervised execution requires live least-privilege writer verification bound to an exact reviewed posture fingerprint." });
|
|
2441
|
+
}
|
|
2442
|
+
if (rawEntry.worker_identity !== void 0 && !isSafeName(rawEntry.worker_identity)) {
|
|
2443
|
+
errors.push({ path: `${path3}.worker_identity`, code: "INVALID_SUPERVISED_WORKER_IDENTITY", message: "worker_identity must be a fixed safe operator identity." });
|
|
2444
|
+
}
|
|
2445
|
+
if (rawEntry.control_role !== void 0 && !isSafeName(rawEntry.control_role)) {
|
|
2446
|
+
errors.push({ path: `${path3}.control_role`, code: "INVALID_SUPERVISED_WORKER_CONTROL_ROLE", message: "control_role must be a fixed safe operator role." });
|
|
2447
|
+
}
|
|
2448
|
+
if (rawEntry.required_attention_sinks !== void 0) {
|
|
2449
|
+
if (!Array.isArray(rawEntry.required_attention_sinks) || rawEntry.required_attention_sinks.length < 1 || rawEntry.required_attention_sinks.length > 8 || rawEntry.required_attention_sinks.some((sink) => !isSafeName(sink))) {
|
|
2450
|
+
errors.push({ path: `${path3}.required_attention_sinks`, code: "INVALID_REQUIRED_ATTENTION_SINKS", message: "required_attention_sinks must contain 1 through 8 fixed sink ids." });
|
|
2451
|
+
} else {
|
|
2452
|
+
if (notifications?.enabled !== true) {
|
|
2453
|
+
errors.push({ path: "$.notifications.enabled", code: "HEALTH_GATE_NOTIFICATIONS_REQUIRED", message: "required attention sinks need notifications.enabled=true." });
|
|
2454
|
+
}
|
|
2455
|
+
for (const [sinkIndex, sinkId] of rawEntry.required_attention_sinks.entries()) {
|
|
2456
|
+
const sink = notificationSinks.get(String(sinkId));
|
|
2457
|
+
if (!sink || sink.enabled === false) {
|
|
2458
|
+
errors.push({ path: `${path3}.required_attention_sinks[${sinkIndex}]`, code: "REQUIRED_ATTENTION_SINK_UNAVAILABLE", message: `required attention sink ${String(sinkId)} must name an enabled configured sink.` });
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
}
|
|
2463
|
+
if (!isRecord2(rawEntry.rate_limit)) {
|
|
2464
|
+
errors.push({ path: `${path3}.rate_limit`, code: "SUPERVISED_WORKER_RATE_LIMIT_REQUIRED", message: "rate_limit must define a bounded execution window." });
|
|
2465
|
+
} else {
|
|
2466
|
+
if (strict) checkUnknownKeys2(rawEntry.rate_limit, SUPERVISED_WORKER_RATE_LIMIT_KEYS, `${path3}.rate_limit`, errors);
|
|
2467
|
+
if (!Number.isSafeInteger(rawEntry.rate_limit.executions) || Number(rawEntry.rate_limit.executions) < 1 || Number(rawEntry.rate_limit.executions) > 1e5) {
|
|
2468
|
+
errors.push({ path: `${path3}.rate_limit.executions`, code: "INVALID_SUPERVISED_WORKER_RATE", message: "rate_limit.executions must be an integer from 1 through 100000." });
|
|
2469
|
+
}
|
|
2470
|
+
if (!Number.isSafeInteger(rawEntry.rate_limit.window_seconds) || Number(rawEntry.rate_limit.window_seconds) < 1 || Number(rawEntry.rate_limit.window_seconds) > 86400) {
|
|
2471
|
+
errors.push({ path: `${path3}.rate_limit.window_seconds`, code: "INVALID_SUPERVISED_WORKER_WINDOW", message: "rate_limit.window_seconds must be an integer from 1 through 86400." });
|
|
2472
|
+
}
|
|
2473
|
+
}
|
|
2474
|
+
const key = `${String(rawEntry.capability)}:${String(rawEntry.contract_digest)}`;
|
|
2475
|
+
if (seen.has(key)) {
|
|
2476
|
+
errors.push({ path: path3, code: "DUPLICATE_SUPERVISED_WORKER_ALLOWLIST", message: "each capability/digest may appear only once." });
|
|
2477
|
+
}
|
|
2478
|
+
seen.add(key);
|
|
2479
|
+
const capability = byName.get(String(rawEntry.capability));
|
|
2480
|
+
if (!capability) {
|
|
2481
|
+
if (runtimeCapabilities.length > 0 || !Array.isArray(config.contracts)) {
|
|
2482
|
+
errors.push({ path: `${path3}.capability`, code: "SUPERVISED_WORKER_CAPABILITY_UNKNOWN", message: "allowlist capability must resolve from the active Runner contract catalog." });
|
|
2483
|
+
}
|
|
2484
|
+
continue;
|
|
2485
|
+
}
|
|
2486
|
+
const provenance = isRecord2(capability.contract_provenance) ? capability.contract_provenance : void 0;
|
|
2487
|
+
if (!provenance || provenance.digest !== rawEntry.contract_digest) {
|
|
2488
|
+
errors.push({ path: `${path3}.contract_digest`, code: "SUPERVISED_WORKER_DIGEST_MISMATCH", message: "allowlist digest must exactly match the active capability contract digest." });
|
|
2489
|
+
}
|
|
2490
|
+
if (!isRecord2(capability.execution) || capability.execution.supervised_worker !== "allowed") {
|
|
2491
|
+
errors.push({ path: `${path3}.capability`, code: "SUPERVISED_WORKER_CONTRACT_PERMISSION_REQUIRED", message: "the active capability contract does not permit supervised-worker execution." });
|
|
2492
|
+
}
|
|
2493
|
+
const source = isNonEmptyString2(capability.source) && isRecord2(sources[capability.source]) ? sources[capability.source] : void 0;
|
|
2494
|
+
if (!source || source.write_url_env !== rawEntry.write_url_env || source.read_only === true) {
|
|
2495
|
+
errors.push({ path: `${path3}.write_url_env`, code: "SUPERVISED_WORKER_WRITER_MISMATCH", message: "allowlist write_url_env must match the selected writable source without embedding a credential." });
|
|
2496
|
+
}
|
|
2497
|
+
if (!source || !isRecord2(source.receipts)) {
|
|
2498
|
+
errors.push({ path: `$.sources.${String(capability.source)}.receipts`, code: "SUPERVISED_WORKER_RECEIPT_AUTHORITY_REQUIRED", message: "supervised-worker execution requires explicit source_db or runner_ledger receipt authority." });
|
|
2499
|
+
}
|
|
2500
|
+
if (rawEntry.require_least_privilege_writer === true && source && isRecord2(source.receipts) && source.receipts.authority === "source_db" && (source.receipts.provisioning !== "precreated" || !isSafeIdentifier2(source.receipts.schema) || !isSafeIdentifier2(source.receipts.table))) {
|
|
2501
|
+
errors.push({
|
|
2502
|
+
path: `$.sources.${String(capability.source)}.receipts`,
|
|
2503
|
+
code: "SUPERVISED_WORKER_PRECREATED_RECEIPT_REQUIRED",
|
|
2504
|
+
message: "hardened supervised execution with source_db receipts requires an explicitly named precreated receipt table; the runtime writer must not retain schema-creation authority."
|
|
2505
|
+
});
|
|
2506
|
+
}
|
|
2507
|
+
if (!isRecord2(capability.target) || !isSafeIdentifier2(capability.target.tenant_key)) {
|
|
2508
|
+
errors.push({ path: `${path3}.capability`, code: "SUPERVISED_WORKER_TENANT_SCOPE_REQUIRED", message: "supervised-worker execution requires a trusted tenant-scoped target." });
|
|
2509
|
+
}
|
|
2510
|
+
const operation = isRecord2(capability.operation) ? capability.operation : { kind: "update" };
|
|
2511
|
+
const operationKind = operation.kind ?? "update";
|
|
2512
|
+
if (operation.cardinality === "set" || operationKind === "delete" || capability.reversibility !== void 0 || !isRecord2(capability.writeback) || capability.writeback.mode !== "direct_sql") {
|
|
2513
|
+
errors.push({ path: `${path3}.capability`, code: "SUPERVISED_WORKER_SHAPE_INELIGIBLE", message: "the first supervised-worker release permits only native single-row direct_sql UPDATE/INSERT without compensation." });
|
|
2514
|
+
}
|
|
2515
|
+
if (operationKind === "update" && (!isRecord2(capability.conflict_guard) || !isSafeIdentifier2(capability.conflict_guard.column) || capability.conflict_guard.weak_guard_ack === true)) {
|
|
2516
|
+
errors.push({ path: `${path3}.capability`, code: "SUPERVISED_WORKER_EXACT_GUARD_REQUIRED", message: "supervised-worker UPDATE requires an exact version/conflict guard." });
|
|
2517
|
+
}
|
|
2518
|
+
if (operationKind === "insert" && !isRecord2(operation.deduplication)) {
|
|
2519
|
+
errors.push({ path: `${path3}.capability`, code: "SUPERVISED_WORKER_INSERT_DEDUP_REQUIRED", message: "supervised-worker INSERT requires reviewed deterministic source deduplication." });
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
}
|
|
2061
2523
|
function validateHttpSecurity(value, sessionAuth, trustedContext, contexts, strict, errors, warnings) {
|
|
2062
2524
|
if (value === void 0) return;
|
|
2063
2525
|
if (!isRecord2(value)) {
|
|
@@ -2314,9 +2776,9 @@ function validateRateLimits(value, strict, errors) {
|
|
|
2314
2776
|
if (!isRecord2(value.capabilities)) {
|
|
2315
2777
|
errors.push({ path: "$.rate_limits.capabilities", code: "RATE_LIMIT_CAPABILITIES_NOT_OBJECT", message: "capabilities must map reviewed capability names to rate-limit rules." });
|
|
2316
2778
|
} else {
|
|
2317
|
-
for (const [name,
|
|
2779
|
+
for (const [name, rule2] of Object.entries(value.capabilities)) {
|
|
2318
2780
|
if (!isQualifiedName2(name)) errors.push({ path: `$.rate_limits.capabilities.${name}`, code: "INVALID_RATE_LIMIT_CAPABILITY", message: "rate-limit capability keys must be qualified capability names." });
|
|
2319
|
-
validateRateLimitRule(
|
|
2781
|
+
validateRateLimitRule(rule2, `$.rate_limits.capabilities.${name}`, strict, errors);
|
|
2320
2782
|
}
|
|
2321
2783
|
}
|
|
2322
2784
|
}
|
|
@@ -2896,7 +3358,7 @@ function validatePolicies2(value, strict, errors) {
|
|
|
2896
3358
|
if (!Array.isArray(policy.rules)) {
|
|
2897
3359
|
errors.push({ path: `${path3}.rules`, code: "POLICY_RULES_NOT_ARRAY", message: "policy.rules must be an array." });
|
|
2898
3360
|
} else if (policy.kind === "approval") {
|
|
2899
|
-
policy.rules.forEach((
|
|
3361
|
+
policy.rules.forEach((rule2, ruleIndex) => validateApprovalPolicyRule(rule2, `${path3}.rules[${ruleIndex}]`, strict, errors));
|
|
2900
3362
|
}
|
|
2901
3363
|
}
|
|
2902
3364
|
if (policy.limits !== void 0) {
|
|
@@ -3087,10 +3549,20 @@ function validateExecutorAuth(value, path3, strict, errors) {
|
|
|
3087
3549
|
errors.push({ path: `${path3}.token_env`, code: "EXECUTOR_TOKEN_ENV_REQUIRED", message: "bearer_env auth requires token_env." });
|
|
3088
3550
|
}
|
|
3089
3551
|
}
|
|
3090
|
-
function validateCapabilities2(value, sources, contexts, executors, mode, strict, errors, warnings, hasContracts = false) {
|
|
3552
|
+
function validateCapabilities2(value, sources, contexts, executors, mode, strict, errors, warnings, hasContracts = false, generatedAuthority) {
|
|
3091
3553
|
if (mode === "cloud" && value === void 0) return;
|
|
3092
3554
|
if (hasContracts && value === void 0) return;
|
|
3093
3555
|
if (hasContracts && Array.isArray(value) && value.length === 0) return;
|
|
3556
|
+
const emptyReadOnlyShell = mode === "read_only" && Array.isArray(value) && value.length === 0;
|
|
3557
|
+
if (emptyReadOnlyShell) {
|
|
3558
|
+
const lockBound = isRecord2(generatedAuthority) && generatedAuthority.enforcement === "required";
|
|
3559
|
+
warnings.push({
|
|
3560
|
+
path: "$.capabilities",
|
|
3561
|
+
code: lockBound ? "AUTHORING_PROJECT_HAS_NO_ACTIVE_CAPABILITIES" : "READ_ONLY_CONFIG_HAS_NO_ACTIVE_CAPABILITIES",
|
|
3562
|
+
message: lockBound ? "This generated authoring project exposes no named runtime capabilities until an exact reviewed digest is activated." : "This read-only configuration exposes no model-facing capabilities."
|
|
3563
|
+
});
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3094
3566
|
if (!Array.isArray(value) || value.length === 0) {
|
|
3095
3567
|
errors.push({ path: "$.capabilities", code: "CAPABILITIES_REQUIRED", message: "At least one capability is required." });
|
|
3096
3568
|
return;
|
|
@@ -3622,6 +4094,16 @@ function validateProposalCapability(capability, path3, strict, errors) {
|
|
|
3622
4094
|
}
|
|
3623
4095
|
}
|
|
3624
4096
|
}
|
|
4097
|
+
if (capability.execution !== void 0) {
|
|
4098
|
+
if (!isRecord2(capability.execution)) {
|
|
4099
|
+
errors.push({ path: `${path3}.execution`, code: "EXECUTION_NOT_OBJECT", message: "execution must be an explicit reviewed permission object." });
|
|
4100
|
+
} else {
|
|
4101
|
+
if (strict) checkUnknownKeys2(capability.execution, EXECUTION_KEYS2, `${path3}.execution`, errors);
|
|
4102
|
+
if (capability.execution.supervised_worker !== "allowed") {
|
|
4103
|
+
errors.push({ path: `${path3}.execution.supervised_worker`, code: "INVALID_SUPERVISED_WORKER_PERMISSION", message: "execution.supervised_worker must be allowed when present." });
|
|
4104
|
+
}
|
|
4105
|
+
}
|
|
4106
|
+
}
|
|
3625
4107
|
if (operation === "delete" && capabilityWritebackMode(capability) === "direct_sql") {
|
|
3626
4108
|
const approval = isRecord2(capability.approval) ? capability.approval : void 0;
|
|
3627
4109
|
if (!approval || approval.mode !== "human" && approval.mode !== "operator") {
|
|
@@ -3896,6 +4378,10 @@ function isSafeIdentifier2(value) {
|
|
|
3896
4378
|
function isSafeName(value) {
|
|
3897
4379
|
return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_.-]*$/.test(value);
|
|
3898
4380
|
}
|
|
4381
|
+
function isSafeNotificationHost(value) {
|
|
4382
|
+
if (typeof value !== "string" || value.length < 1 || value.length > 253 || /[\s/@?#]/.test(value)) return false;
|
|
4383
|
+
return /^[A-Za-z0-9.-]+$/.test(value) || /^[0-9A-Fa-f:]+$/.test(value);
|
|
4384
|
+
}
|
|
3899
4385
|
function isQualifiedName2(value) {
|
|
3900
4386
|
return typeof value === "string" && /^[A-Za-z_][A-Za-z0-9_]*\.[A-Za-z_][A-Za-z0-9_]*$/.test(value);
|
|
3901
4387
|
}
|
|
@@ -5979,6 +6465,90 @@ var PostgresProposalRuntimeStore = class {
|
|
|
5979
6465
|
async requireWritebackReconciliation(intentId, reason) {
|
|
5980
6466
|
await this.withWrite("requireWritebackReconciliation", (store) => store.requireWritebackReconciliation(intentId, reason));
|
|
5981
6467
|
}
|
|
6468
|
+
async enqueueWorkerProposal(input) {
|
|
6469
|
+
return await this.withWrite("enqueueWorkerProposal", (store) => store.enqueueWorkerProposal(input));
|
|
6470
|
+
}
|
|
6471
|
+
async claimWorkerItem(input) {
|
|
6472
|
+
return await this.withWrite("claimWorkerItem", (store) => store.claimWorkerItem(input));
|
|
6473
|
+
}
|
|
6474
|
+
async assertActiveWorkerLease(input) {
|
|
6475
|
+
return await this.withRead((store) => store.assertActiveWorkerLease(input));
|
|
6476
|
+
}
|
|
6477
|
+
async renewWorkerLease(input) {
|
|
6478
|
+
return await this.withWrite("renewWorkerLease", (store) => store.renewWorkerLease(input));
|
|
6479
|
+
}
|
|
6480
|
+
async completeWorkerItem(proposalId, workerId, outcome, now, leaseId) {
|
|
6481
|
+
return await this.withWrite("completeWorkerItem", (store) => store.completeWorkerItem(proposalId, workerId, outcome, now, leaseId));
|
|
6482
|
+
}
|
|
6483
|
+
async blockWorkerItem(input) {
|
|
6484
|
+
return await this.withWrite("blockWorkerItem", (store) => store.blockWorkerItem(input));
|
|
6485
|
+
}
|
|
6486
|
+
async requireWorkerReconciliation(input) {
|
|
6487
|
+
return await this.withWrite("requireWorkerReconciliation", (store) => store.requireWorkerReconciliation(input));
|
|
6488
|
+
}
|
|
6489
|
+
async workerControlState() {
|
|
6490
|
+
return await this.withRead((store) => store.workerControlState());
|
|
6491
|
+
}
|
|
6492
|
+
async updateWorkerControl(input) {
|
|
6493
|
+
return await this.withWrite("updateWorkerControl", (store) => store.updateWorkerControl(input));
|
|
6494
|
+
}
|
|
6495
|
+
async cancelWorkerItem(input) {
|
|
6496
|
+
return await this.withWrite("cancelWorkerItem", (store) => store.cancelWorkerItem(input));
|
|
6497
|
+
}
|
|
6498
|
+
async listWorkerQueue(status) {
|
|
6499
|
+
return await this.withRead((store) => store.listWorkerQueue(status));
|
|
6500
|
+
}
|
|
6501
|
+
async getWorkerQueueItem(proposalId) {
|
|
6502
|
+
return await this.withRead((store) => store.getWorkerQueueItem(proposalId));
|
|
6503
|
+
}
|
|
6504
|
+
async recordAttentionEvent(input) {
|
|
6505
|
+
return await this.withWrite("recordAttentionEvent", (store) => store.recordAttentionEvent(input));
|
|
6506
|
+
}
|
|
6507
|
+
async listAttentionEvents(filters = {}) {
|
|
6508
|
+
return await this.withRead((store) => store.listAttentionEvents(filters));
|
|
6509
|
+
}
|
|
6510
|
+
async getAttentionEvent(eventId) {
|
|
6511
|
+
return await this.withRead((store) => store.getAttentionEvent(eventId));
|
|
6512
|
+
}
|
|
6513
|
+
async listAttentionItems(filters = {}) {
|
|
6514
|
+
return await this.withRead((store) => store.listAttentionItems(filters));
|
|
6515
|
+
}
|
|
6516
|
+
async getAttentionItem(attentionId) {
|
|
6517
|
+
return await this.withRead((store) => store.getAttentionItem(attentionId));
|
|
6518
|
+
}
|
|
6519
|
+
async acknowledgeAttention(input) {
|
|
6520
|
+
return await this.withWrite("acknowledgeAttention", (store) => store.acknowledgeAttention(input));
|
|
6521
|
+
}
|
|
6522
|
+
async resolveAttention(input) {
|
|
6523
|
+
return await this.withWrite("resolveAttention", (store) => store.resolveAttention(input));
|
|
6524
|
+
}
|
|
6525
|
+
async enqueueNotificationDelivery(input) {
|
|
6526
|
+
return await this.withWrite("enqueueNotificationDelivery", (store) => store.enqueueNotificationDelivery(input));
|
|
6527
|
+
}
|
|
6528
|
+
async includeNotificationDeliveriesInDigest(input) {
|
|
6529
|
+
return await this.withWrite(
|
|
6530
|
+
"includeNotificationDeliveriesInDigest",
|
|
6531
|
+
(store) => store.includeNotificationDeliveriesInDigest(input)
|
|
6532
|
+
);
|
|
6533
|
+
}
|
|
6534
|
+
async claimNotificationDeliveries(input) {
|
|
6535
|
+
return await this.withWrite("claimNotificationDeliveries", (store) => store.claimNotificationDeliveries(input));
|
|
6536
|
+
}
|
|
6537
|
+
async completeNotificationDelivery(input) {
|
|
6538
|
+
return await this.withWrite("completeNotificationDelivery", (store) => store.completeNotificationDelivery(input));
|
|
6539
|
+
}
|
|
6540
|
+
async failNotificationDelivery(input) {
|
|
6541
|
+
return await this.withWrite("failNotificationDelivery", (store) => store.failNotificationDelivery(input));
|
|
6542
|
+
}
|
|
6543
|
+
async listNotificationDeliveries(filters = {}) {
|
|
6544
|
+
return await this.withRead((store) => store.listNotificationDeliveries(filters));
|
|
6545
|
+
}
|
|
6546
|
+
async getNotificationDelivery(deliveryId) {
|
|
6547
|
+
return await this.withRead((store) => store.getNotificationDelivery(deliveryId));
|
|
6548
|
+
}
|
|
6549
|
+
async requeueNotificationDelivery(input) {
|
|
6550
|
+
return await this.withWrite("requeueNotificationDelivery", (store) => store.requeueNotificationDelivery(input));
|
|
6551
|
+
}
|
|
5982
6552
|
async enqueueCloudOutbox(input) {
|
|
5983
6553
|
return await this.withWrite("enqueueCloudOutbox", (store) => store.enqueueCloudOutbox(input));
|
|
5984
6554
|
}
|
|
@@ -6236,6 +6806,9 @@ var ProposalStore = class {
|
|
|
6236
6806
|
shadow_study_cases: this.countTable("shadow_study_cases"),
|
|
6237
6807
|
shadow_outcomes: this.countTable("shadow_outcomes"),
|
|
6238
6808
|
worker_queue: this.countTable("worker_queue"),
|
|
6809
|
+
attention_events: this.countTable("attention_events"),
|
|
6810
|
+
attention_items: this.countTable("attention_items"),
|
|
6811
|
+
notification_deliveries: this.countTable("notification_deliveries"),
|
|
6239
6812
|
policy_recommendations: this.countTable("policy_recommendations"),
|
|
6240
6813
|
page_count: pageCount,
|
|
6241
6814
|
page_size: pageSize,
|
|
@@ -6575,15 +7148,95 @@ var ProposalStore = class {
|
|
|
6575
7148
|
updated_at TEXT NOT NULL
|
|
6576
7149
|
);
|
|
6577
7150
|
|
|
7151
|
+
CREATE TABLE IF NOT EXISTS attention_events (
|
|
7152
|
+
event_id TEXT PRIMARY KEY,
|
|
7153
|
+
schema_version TEXT NOT NULL,
|
|
7154
|
+
event_type TEXT NOT NULL,
|
|
7155
|
+
severity TEXT NOT NULL,
|
|
7156
|
+
occurred_at TEXT NOT NULL,
|
|
7157
|
+
environment TEXT NOT NULL,
|
|
7158
|
+
proposal_id TEXT,
|
|
7159
|
+
job_id TEXT,
|
|
7160
|
+
operation_id TEXT,
|
|
7161
|
+
correlation_id TEXT,
|
|
7162
|
+
capability TEXT,
|
|
7163
|
+
contract_digest TEXT,
|
|
7164
|
+
attention_key TEXT,
|
|
7165
|
+
attention_required INTEGER NOT NULL,
|
|
7166
|
+
immediate_default INTEGER NOT NULL,
|
|
7167
|
+
summary TEXT NOT NULL,
|
|
7168
|
+
approval_source TEXT,
|
|
7169
|
+
worker_state TEXT,
|
|
7170
|
+
failure_class TEXT,
|
|
7171
|
+
expires_at TEXT,
|
|
7172
|
+
workbench_path TEXT,
|
|
7173
|
+
details_json TEXT NOT NULL,
|
|
7174
|
+
payload_hash TEXT NOT NULL,
|
|
7175
|
+
created_at TEXT NOT NULL
|
|
7176
|
+
);
|
|
7177
|
+
|
|
7178
|
+
CREATE TABLE IF NOT EXISTS attention_items (
|
|
7179
|
+
attention_id TEXT PRIMARY KEY,
|
|
7180
|
+
attention_key TEXT NOT NULL UNIQUE,
|
|
7181
|
+
status TEXT NOT NULL,
|
|
7182
|
+
severity TEXT NOT NULL,
|
|
7183
|
+
environment TEXT NOT NULL,
|
|
7184
|
+
event_type TEXT NOT NULL,
|
|
7185
|
+
capability TEXT,
|
|
7186
|
+
contract_digest TEXT,
|
|
7187
|
+
title TEXT NOT NULL,
|
|
7188
|
+
occurrence_count INTEGER NOT NULL,
|
|
7189
|
+
first_event_id TEXT NOT NULL,
|
|
7190
|
+
latest_event_id TEXT NOT NULL,
|
|
7191
|
+
first_seen_at TEXT NOT NULL,
|
|
7192
|
+
last_seen_at TEXT NOT NULL,
|
|
7193
|
+
acknowledged_by TEXT,
|
|
7194
|
+
acknowledged_at TEXT,
|
|
7195
|
+
acknowledgement_identity_json TEXT,
|
|
7196
|
+
acknowledgement_decision_hash TEXT,
|
|
7197
|
+
acknowledgement_signature TEXT,
|
|
7198
|
+
acknowledgement_integrity_hash TEXT,
|
|
7199
|
+
resolved_at TEXT,
|
|
7200
|
+
expires_at TEXT,
|
|
7201
|
+
FOREIGN KEY (first_event_id) REFERENCES attention_events(event_id),
|
|
7202
|
+
FOREIGN KEY (latest_event_id) REFERENCES attention_events(event_id)
|
|
7203
|
+
);
|
|
7204
|
+
|
|
7205
|
+
CREATE TABLE IF NOT EXISTS notification_deliveries (
|
|
7206
|
+
delivery_id TEXT PRIMARY KEY,
|
|
7207
|
+
sink_id TEXT NOT NULL,
|
|
7208
|
+
event_id TEXT NOT NULL,
|
|
7209
|
+
attention_id TEXT,
|
|
7210
|
+
status TEXT NOT NULL,
|
|
7211
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
7212
|
+
max_attempts INTEGER NOT NULL,
|
|
7213
|
+
next_attempt_at TEXT NOT NULL,
|
|
7214
|
+
lease_owner TEXT,
|
|
7215
|
+
lease_id TEXT,
|
|
7216
|
+
lease_expires_at TEXT,
|
|
7217
|
+
last_error_code TEXT,
|
|
7218
|
+
external_reference TEXT,
|
|
7219
|
+
delivered_at TEXT,
|
|
7220
|
+
created_at TEXT NOT NULL,
|
|
7221
|
+
updated_at TEXT NOT NULL,
|
|
7222
|
+
UNIQUE(sink_id, event_id),
|
|
7223
|
+
FOREIGN KEY (event_id) REFERENCES attention_events(event_id),
|
|
7224
|
+
FOREIGN KEY (attention_id) REFERENCES attention_items(attention_id)
|
|
7225
|
+
);
|
|
7226
|
+
|
|
6578
7227
|
CREATE TABLE IF NOT EXISTS worker_queue (
|
|
6579
7228
|
proposal_id TEXT PRIMARY KEY,
|
|
6580
7229
|
status TEXT NOT NULL,
|
|
7230
|
+
execution_mode TEXT DEFAULT 'legacy',
|
|
7231
|
+
contract_digest TEXT,
|
|
6581
7232
|
attempts INTEGER NOT NULL DEFAULT 0,
|
|
6582
7233
|
max_attempts INTEGER NOT NULL,
|
|
6583
7234
|
next_attempt_at TEXT NOT NULL,
|
|
6584
7235
|
lease_owner TEXT,
|
|
7236
|
+
lease_id TEXT,
|
|
6585
7237
|
lease_expires_at TEXT,
|
|
6586
7238
|
last_error_code TEXT,
|
|
7239
|
+
terminal_outcome TEXT,
|
|
6587
7240
|
created_at TEXT NOT NULL,
|
|
6588
7241
|
updated_at TEXT NOT NULL,
|
|
6589
7242
|
FOREIGN KEY (proposal_id) REFERENCES proposals(proposal_id)
|
|
@@ -6605,6 +7258,12 @@ var ProposalStore = class {
|
|
|
6605
7258
|
CREATE INDEX IF NOT EXISTS idx_cloud_outbox_due ON cloud_outbox(status, next_attempt_at, sequence, created_at);
|
|
6606
7259
|
CREATE INDEX IF NOT EXISTS idx_cloud_outbox_proposal ON cloud_outbox(proposal_id, sequence, created_at);
|
|
6607
7260
|
CREATE INDEX IF NOT EXISTS idx_cloud_governance_proposal ON cloud_governance_events(proposal_id, created_at);
|
|
7261
|
+
CREATE INDEX IF NOT EXISTS idx_attention_events_type_time ON attention_events(event_type, occurred_at, event_id);
|
|
7262
|
+
CREATE INDEX IF NOT EXISTS idx_attention_events_proposal ON attention_events(proposal_id, occurred_at, event_id);
|
|
7263
|
+
CREATE INDEX IF NOT EXISTS idx_attention_events_capability ON attention_events(capability, occurred_at, event_id);
|
|
7264
|
+
CREATE INDEX IF NOT EXISTS idx_attention_items_status ON attention_items(status, severity, last_seen_at, attention_id);
|
|
7265
|
+
CREATE INDEX IF NOT EXISTS idx_notification_deliveries_due ON notification_deliveries(status, next_attempt_at, created_at, delivery_id);
|
|
7266
|
+
CREATE INDEX IF NOT EXISTS idx_notification_deliveries_sink ON notification_deliveries(sink_id, status, updated_at, delivery_id);
|
|
6608
7267
|
|
|
6609
7268
|
INSERT OR IGNORE INTO proposal_store_schema(version, applied_at)
|
|
6610
7269
|
VALUES (1, datetime('now'));
|
|
@@ -6636,6 +7295,14 @@ var ProposalStore = class {
|
|
|
6636
7295
|
this.ensureColumn("approvals", "signature", "TEXT");
|
|
6637
7296
|
this.ensureColumn("approvals", "integrity_hash", "TEXT");
|
|
6638
7297
|
this.ensureColumn("approvals", "freshness_proof_digest", "TEXT");
|
|
7298
|
+
this.ensureColumn("attention_items", "acknowledgement_identity_json", "TEXT");
|
|
7299
|
+
this.ensureColumn("attention_items", "acknowledgement_decision_hash", "TEXT");
|
|
7300
|
+
this.ensureColumn("attention_items", "acknowledgement_signature", "TEXT");
|
|
7301
|
+
this.ensureColumn("attention_items", "acknowledgement_integrity_hash", "TEXT");
|
|
7302
|
+
this.ensureColumn("worker_queue", "execution_mode", "TEXT DEFAULT 'legacy'");
|
|
7303
|
+
this.ensureColumn("worker_queue", "contract_digest", "TEXT");
|
|
7304
|
+
this.ensureColumn("worker_queue", "lease_id", "TEXT");
|
|
7305
|
+
this.ensureColumn("worker_queue", "terminal_outcome", "TEXT");
|
|
6639
7306
|
}
|
|
6640
7307
|
ensureSearchIndexes() {
|
|
6641
7308
|
this.db.exec(`
|
|
@@ -6679,6 +7346,7 @@ var ProposalStore = class {
|
|
|
6679
7346
|
|
|
6680
7347
|
CREATE INDEX IF NOT EXISTS idx_proposal_events_kind_created ON proposal_events(kind, created_at);
|
|
6681
7348
|
CREATE INDEX IF NOT EXISTS idx_worker_queue_claim ON worker_queue(status, next_attempt_at, lease_expires_at, created_at);
|
|
7349
|
+
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);
|
|
6682
7350
|
`);
|
|
6683
7351
|
}
|
|
6684
7352
|
ensureColumn(table, column, definition) {
|
|
@@ -7058,6 +7726,7 @@ var ProposalStore = class {
|
|
|
7058
7726
|
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
7059
7727
|
const window2 = utcDayWindow(now);
|
|
7060
7728
|
const trippedLimits = [];
|
|
7729
|
+
const nearLimits = [];
|
|
7061
7730
|
let quorumDeferred = false;
|
|
7062
7731
|
this.transaction(() => {
|
|
7063
7732
|
const proposal = this.requireProposal(proposalId);
|
|
@@ -7110,6 +7779,17 @@ var ProposalStore = class {
|
|
|
7110
7779
|
window_end: window2.end,
|
|
7111
7780
|
reason: `${scope} daily auto-approval count ${projected2} exceeds ${limit.max}`
|
|
7112
7781
|
});
|
|
7782
|
+
} else if (projected2 / limit.max >= 0.8) {
|
|
7783
|
+
nearLimits.push({
|
|
7784
|
+
...limit,
|
|
7785
|
+
scope,
|
|
7786
|
+
observed: rows.length,
|
|
7787
|
+
proposed: 1,
|
|
7788
|
+
projected: projected2,
|
|
7789
|
+
window_start: window2.start,
|
|
7790
|
+
window_end: window2.end,
|
|
7791
|
+
reason: `${scope} daily auto-approval count ${projected2} is approaching ${limit.max}`
|
|
7792
|
+
});
|
|
7113
7793
|
}
|
|
7114
7794
|
continue;
|
|
7115
7795
|
}
|
|
@@ -7144,6 +7824,17 @@ var ProposalStore = class {
|
|
|
7144
7824
|
window_end: window2.end,
|
|
7145
7825
|
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}`
|
|
7146
7826
|
});
|
|
7827
|
+
} else if (projected / limit.max >= 0.8) {
|
|
7828
|
+
nearLimits.push({
|
|
7829
|
+
...limit,
|
|
7830
|
+
scope,
|
|
7831
|
+
observed,
|
|
7832
|
+
proposed: proposedNumber,
|
|
7833
|
+
projected,
|
|
7834
|
+
window_start: window2.start,
|
|
7835
|
+
window_end: window2.end,
|
|
7836
|
+
reason: `${scope} daily auto-approval total is approaching its reviewed maximum`
|
|
7837
|
+
});
|
|
7147
7838
|
}
|
|
7148
7839
|
}
|
|
7149
7840
|
if (trippedLimits.length > 0) {
|
|
@@ -7180,6 +7871,19 @@ var ProposalStore = class {
|
|
|
7180
7871
|
aggregate_limits: options.limits ?? [],
|
|
7181
7872
|
freshness_proof_digest: options.freshness_proof_digest ?? null
|
|
7182
7873
|
});
|
|
7874
|
+
for (const near of nearLimits) {
|
|
7875
|
+
this.appendEvent(proposalId, "policy_limit_near", actor, {
|
|
7876
|
+
policy: options.policy,
|
|
7877
|
+
kind: near.kind,
|
|
7878
|
+
scope: near.scope,
|
|
7879
|
+
observed: near.observed,
|
|
7880
|
+
proposed: near.proposed,
|
|
7881
|
+
projected: near.projected,
|
|
7882
|
+
max: near.max,
|
|
7883
|
+
window_start: near.window_start,
|
|
7884
|
+
window_end: near.window_end
|
|
7885
|
+
});
|
|
7886
|
+
}
|
|
7183
7887
|
});
|
|
7184
7888
|
return {
|
|
7185
7889
|
proposal: this.requireProposal(proposalId),
|
|
@@ -7489,6 +8193,30 @@ var ProposalStore = class {
|
|
|
7489
8193
|
decision_hash: input.identity?.decision_hash
|
|
7490
8194
|
});
|
|
7491
8195
|
this.recordExecutionReceiptRows(receipt, proposal);
|
|
8196
|
+
const workerItem = this.workerQueueItem(intent.proposal_id);
|
|
8197
|
+
if (workerItem?.status === "reconciliation_required") {
|
|
8198
|
+
const queueStatus = receipt.status === "failed" ? "dead_letter" : "completed";
|
|
8199
|
+
const terminalOutcome = receipt.status === "failed" ? "dead_letter" : receipt.status;
|
|
8200
|
+
this.db.prepare(`
|
|
8201
|
+
UPDATE worker_queue
|
|
8202
|
+
SET status = ?, lease_owner = NULL, lease_id = NULL,
|
|
8203
|
+
lease_expires_at = NULL, last_error_code = ?,
|
|
8204
|
+
terminal_outcome = ?, updated_at = ?
|
|
8205
|
+
WHERE proposal_id = ? AND status = 'reconciliation_required'
|
|
8206
|
+
`).run(
|
|
8207
|
+
queueStatus,
|
|
8208
|
+
receipt.status === "failed" ? "RECONCILED_FAILED" : null,
|
|
8209
|
+
terminalOutcome,
|
|
8210
|
+
receipt.executed_at,
|
|
8211
|
+
intent.proposal_id
|
|
8212
|
+
);
|
|
8213
|
+
this.appendEvent(intent.proposal_id, "writeback_worker_reconciled", input.actor, {
|
|
8214
|
+
intent_id: intent.intent_id,
|
|
8215
|
+
outcome: receipt.status,
|
|
8216
|
+
queue_status: queueStatus,
|
|
8217
|
+
source_database_changed: false
|
|
8218
|
+
});
|
|
8219
|
+
}
|
|
7492
8220
|
});
|
|
7493
8221
|
return this.requireWritebackIntent(intent.intent_id);
|
|
7494
8222
|
}
|
|
@@ -7817,6 +8545,78 @@ var ProposalStore = class {
|
|
|
7817
8545
|
const rows = this.db.prepare(query.sql).all(...query.params);
|
|
7818
8546
|
return rows.map(rowToEvent).filter((event) => event !== void 0);
|
|
7819
8547
|
}
|
|
8548
|
+
enqueueWorkerProposal(options) {
|
|
8549
|
+
const proposal = this.requireProposal(options.proposal_id);
|
|
8550
|
+
if (proposal.state !== "approved" && proposal.state !== "pending_worker") {
|
|
8551
|
+
throw new ProposalStoreError(
|
|
8552
|
+
"WORKER_PROPOSAL_NOT_APPROVED",
|
|
8553
|
+
`proposal ${proposal.proposal_id} is ${proposal.state}, not approved for worker execution`
|
|
8554
|
+
);
|
|
8555
|
+
}
|
|
8556
|
+
const executionMode = options.execution_mode ?? "legacy";
|
|
8557
|
+
const contractDigest = options.contract_digest;
|
|
8558
|
+
if (executionMode === "supervised_worker" && !contractDigest) {
|
|
8559
|
+
throw new ProposalStoreError(
|
|
8560
|
+
"SUPERVISED_WORKER_DIGEST_REQUIRED",
|
|
8561
|
+
`supervised worker queue item ${proposal.proposal_id} requires an exact contract digest`
|
|
8562
|
+
);
|
|
8563
|
+
}
|
|
8564
|
+
if (contractDigest && !/^sha256:[a-f0-9]{64}$/.test(contractDigest)) {
|
|
8565
|
+
throw new ProposalStoreError("WORKER_CONTRACT_DIGEST_INVALID", "worker queue contract digest must be a full lowercase sha256 digest");
|
|
8566
|
+
}
|
|
8567
|
+
const maxAttempts = Math.max(1, Math.min(options.max_attempts ?? 5, 100));
|
|
8568
|
+
const queueLimit = Math.max(1, Math.min(options.queue_limit ?? 1e4, 1e5));
|
|
8569
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
8570
|
+
this.transaction(() => {
|
|
8571
|
+
const existing = this.workerQueueItem(proposal.proposal_id);
|
|
8572
|
+
if (existing) {
|
|
8573
|
+
if (existing.execution_mode !== executionMode || existing.contract_digest !== contractDigest) {
|
|
8574
|
+
throw new ProposalStoreError(
|
|
8575
|
+
"WORKER_QUEUE_AUTHORITY_MISMATCH",
|
|
8576
|
+
`worker queue item ${proposal.proposal_id} already exists under different execution authority`
|
|
8577
|
+
);
|
|
8578
|
+
}
|
|
8579
|
+
return;
|
|
8580
|
+
}
|
|
8581
|
+
const active = this.db.prepare(`
|
|
8582
|
+
SELECT COUNT(*) AS count
|
|
8583
|
+
FROM worker_queue q
|
|
8584
|
+
JOIN proposals p ON p.proposal_id = q.proposal_id
|
|
8585
|
+
WHERE q.execution_mode = ?
|
|
8586
|
+
AND (? IS NULL OR q.contract_digest = ?)
|
|
8587
|
+
AND p.action = ?
|
|
8588
|
+
AND q.status IN ('queued', 'leased', 'retry_wait', 'blocked', 'reconciliation_required')
|
|
8589
|
+
`).get(executionMode, contractDigest ?? null, contractDigest ?? null, proposal.action);
|
|
8590
|
+
if (isRecord3(active) && Number(active.count ?? 0) >= queueLimit) {
|
|
8591
|
+
throw new ProposalStoreError(
|
|
8592
|
+
"WORKER_QUEUE_LIMIT_EXCEEDED",
|
|
8593
|
+
`worker queue limit ${queueLimit} reached for ${proposal.action}`
|
|
8594
|
+
);
|
|
8595
|
+
}
|
|
8596
|
+
this.db.prepare(`
|
|
8597
|
+
INSERT INTO worker_queue (
|
|
8598
|
+
proposal_id, status, execution_mode, contract_digest, attempts,
|
|
8599
|
+
max_attempts, next_attempt_at, lease_owner, lease_id,
|
|
8600
|
+
lease_expires_at, last_error_code, terminal_outcome, created_at,
|
|
8601
|
+
updated_at
|
|
8602
|
+
) VALUES (?, 'queued', ?, ?, 0, ?, ?, NULL, NULL, NULL, NULL, NULL, ?, ?)
|
|
8603
|
+
`).run(
|
|
8604
|
+
proposal.proposal_id,
|
|
8605
|
+
executionMode,
|
|
8606
|
+
contractDigest ?? null,
|
|
8607
|
+
maxAttempts,
|
|
8608
|
+
now,
|
|
8609
|
+
now,
|
|
8610
|
+
now
|
|
8611
|
+
);
|
|
8612
|
+
this.appendEvent(proposal.proposal_id, "writeback_worker_queued", "runner", {
|
|
8613
|
+
execution_mode: executionMode,
|
|
8614
|
+
contract_digest: contractDigest ?? null,
|
|
8615
|
+
max_attempts: maxAttempts
|
|
8616
|
+
});
|
|
8617
|
+
});
|
|
8618
|
+
return this.requireWorkerQueueItem(proposal.proposal_id);
|
|
8619
|
+
}
|
|
7820
8620
|
enqueueApprovedForWorker(options = {}) {
|
|
7821
8621
|
const maxAttempts = Math.max(1, Math.min(options.maxAttempts ?? 5, 100));
|
|
7822
8622
|
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
@@ -7828,9 +8628,11 @@ var ProposalStore = class {
|
|
|
7828
8628
|
for (const proposal of proposals) {
|
|
7829
8629
|
this.db.prepare(`
|
|
7830
8630
|
INSERT OR IGNORE INTO worker_queue (
|
|
7831
|
-
proposal_id, status,
|
|
7832
|
-
|
|
7833
|
-
|
|
8631
|
+
proposal_id, status, execution_mode, contract_digest, attempts,
|
|
8632
|
+
max_attempts, next_attempt_at, lease_owner, lease_id,
|
|
8633
|
+
lease_expires_at, last_error_code, terminal_outcome, created_at,
|
|
8634
|
+
updated_at
|
|
8635
|
+
) VALUES (?, 'queued', 'legacy', NULL, 0, ?, ?, NULL, NULL, NULL, NULL, NULL, ?, ?)
|
|
7834
8636
|
`).run(proposal.proposal_id, maxAttempts, now, now, now);
|
|
7835
8637
|
}
|
|
7836
8638
|
});
|
|
@@ -7842,67 +8644,358 @@ var ProposalStore = class {
|
|
|
7842
8644
|
const leaseExpiresAt = new Date(Date.parse(now) + leaseSeconds * 1e3).toISOString();
|
|
7843
8645
|
let claimed;
|
|
7844
8646
|
this.transaction(() => {
|
|
7845
|
-
|
|
7846
|
-
|
|
7847
|
-
|
|
8647
|
+
if (options.maxConcurrent !== void 0) {
|
|
8648
|
+
const maximum = Math.max(1, Math.min(options.maxConcurrent, 32));
|
|
8649
|
+
const active = this.db.prepare(`
|
|
8650
|
+
SELECT COUNT(*) AS count
|
|
8651
|
+
FROM worker_queue q
|
|
8652
|
+
JOIN proposals p ON p.proposal_id = q.proposal_id
|
|
8653
|
+
WHERE q.status = 'leased'
|
|
8654
|
+
AND q.lease_expires_at > ?
|
|
8655
|
+
AND (? IS NULL OR q.execution_mode = ?)
|
|
8656
|
+
AND (? IS NULL OR p.action = ?)
|
|
8657
|
+
AND (? IS NULL OR p.tenant_id = ?)
|
|
8658
|
+
AND (? IS NULL OR q.contract_digest = ?)
|
|
8659
|
+
`).get(
|
|
8660
|
+
now,
|
|
8661
|
+
options.executionMode ?? null,
|
|
8662
|
+
options.executionMode ?? null,
|
|
8663
|
+
options.capability ?? null,
|
|
8664
|
+
options.capability ?? null,
|
|
8665
|
+
options.tenant ?? null,
|
|
8666
|
+
options.tenant ?? null,
|
|
8667
|
+
options.contractDigest ?? null,
|
|
8668
|
+
options.contractDigest ?? null
|
|
8669
|
+
);
|
|
8670
|
+
if (isRecord3(active) && Number(active.count ?? 0) >= maximum) return;
|
|
8671
|
+
}
|
|
8672
|
+
if (options.rateLimit) {
|
|
8673
|
+
const executions = Math.max(1, Math.min(options.rateLimit.executions, 1e5));
|
|
8674
|
+
const windowSeconds = Math.max(1, Math.min(options.rateLimit.windowSeconds, 86400));
|
|
8675
|
+
const windowStart = new Date(Date.parse(now) - windowSeconds * 1e3).toISOString();
|
|
8676
|
+
const recent = this.db.prepare(`
|
|
8677
|
+
SELECT COUNT(*) AS count
|
|
8678
|
+
FROM worker_queue q
|
|
8679
|
+
JOIN proposals p ON p.proposal_id = q.proposal_id
|
|
8680
|
+
WHERE q.status = 'completed'
|
|
8681
|
+
AND q.updated_at >= ?
|
|
8682
|
+
AND (? IS NULL OR q.execution_mode = ?)
|
|
8683
|
+
AND (? IS NULL OR p.action = ?)
|
|
8684
|
+
AND (? IS NULL OR p.tenant_id = ?)
|
|
8685
|
+
AND (? IS NULL OR q.contract_digest = ?)
|
|
8686
|
+
`).get(
|
|
8687
|
+
windowStart,
|
|
8688
|
+
options.executionMode ?? null,
|
|
8689
|
+
options.executionMode ?? null,
|
|
8690
|
+
options.capability ?? null,
|
|
8691
|
+
options.capability ?? null,
|
|
8692
|
+
options.tenant ?? null,
|
|
8693
|
+
options.tenant ?? null,
|
|
8694
|
+
options.contractDigest ?? null,
|
|
8695
|
+
options.contractDigest ?? null
|
|
8696
|
+
);
|
|
8697
|
+
if (isRecord3(recent) && Number(recent.count ?? 0) >= executions) return;
|
|
8698
|
+
}
|
|
8699
|
+
const raw = this.db.prepare(`
|
|
8700
|
+
SELECT q.*
|
|
8701
|
+
FROM worker_queue q
|
|
7848
8702
|
JOIN proposals p ON p.proposal_id = q.proposal_id
|
|
7849
8703
|
WHERE (
|
|
7850
8704
|
(q.status IN ('queued', 'retry_wait') AND q.next_attempt_at <= ?)
|
|
7851
8705
|
OR (q.status = 'leased' AND q.lease_expires_at <= ?)
|
|
7852
8706
|
)
|
|
7853
8707
|
AND p.state IN ('approved', 'pending_worker', 'failed')
|
|
8708
|
+
AND (? IS NULL OR q.execution_mode = ?)
|
|
8709
|
+
AND (? IS NULL OR p.action = ?)
|
|
8710
|
+
AND (? IS NULL OR p.tenant_id = ?)
|
|
8711
|
+
AND (? IS NULL OR q.contract_digest = ?)
|
|
7854
8712
|
ORDER BY q.next_attempt_at ASC, q.created_at ASC
|
|
7855
8713
|
LIMIT 1
|
|
7856
|
-
`).get(
|
|
8714
|
+
`).get(
|
|
8715
|
+
now,
|
|
8716
|
+
now,
|
|
8717
|
+
options.executionMode ?? null,
|
|
8718
|
+
options.executionMode ?? null,
|
|
8719
|
+
options.capability ?? null,
|
|
8720
|
+
options.capability ?? null,
|
|
8721
|
+
options.tenant ?? null,
|
|
8722
|
+
options.tenant ?? null,
|
|
8723
|
+
options.contractDigest ?? null,
|
|
8724
|
+
options.contractDigest ?? null
|
|
8725
|
+
);
|
|
7857
8726
|
const item = rowToWorkerQueueItem(raw);
|
|
7858
8727
|
if (!item) return;
|
|
8728
|
+
const proposal = this.requireProposal(item.proposal_id);
|
|
8729
|
+
if (options.proposalTtlSeconds !== void 0) {
|
|
8730
|
+
const ttlSeconds = Math.max(60, Math.min(options.proposalTtlSeconds, 2592e3));
|
|
8731
|
+
const expiresAt = Date.parse(proposal.created_at) + ttlSeconds * 1e3;
|
|
8732
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.parse(now)) {
|
|
8733
|
+
this.blockQueuedWorkerItem(item, options.workerId, "SUPERVISED_WORKER_PROPOSAL_EXPIRED", now);
|
|
8734
|
+
return;
|
|
8735
|
+
}
|
|
8736
|
+
}
|
|
8737
|
+
if (options.policyExecution) {
|
|
8738
|
+
const trips = this.workerPolicyExecutionLimitTrips({
|
|
8739
|
+
proposal,
|
|
8740
|
+
policy: options.policyExecution.policy,
|
|
8741
|
+
limits: options.policyExecution.limits,
|
|
8742
|
+
now
|
|
8743
|
+
});
|
|
8744
|
+
if (trips.length > 0) {
|
|
8745
|
+
this.blockQueuedWorkerItem(
|
|
8746
|
+
item,
|
|
8747
|
+
options.workerId,
|
|
8748
|
+
"SUPERVISED_WORKER_POLICY_LIMIT_EXCEEDED",
|
|
8749
|
+
now,
|
|
8750
|
+
{ policy: options.policyExecution.policy, tripped_limits: trips }
|
|
8751
|
+
);
|
|
8752
|
+
return;
|
|
8753
|
+
}
|
|
8754
|
+
}
|
|
8755
|
+
const leaseId = workerLeaseId(item.proposal_id, options.workerId, item.attempts + 1, now);
|
|
7859
8756
|
this.db.prepare(`
|
|
7860
8757
|
UPDATE worker_queue
|
|
7861
8758
|
SET status = 'leased', attempts = attempts + 1, lease_owner = ?,
|
|
7862
|
-
lease_expires_at = ?,
|
|
8759
|
+
lease_id = ?, lease_expires_at = ?, last_error_code = NULL,
|
|
8760
|
+
terminal_outcome = NULL, updated_at = ?
|
|
7863
8761
|
WHERE proposal_id = ?
|
|
7864
|
-
`).run(options.workerId, leaseExpiresAt, now, item.proposal_id);
|
|
7865
|
-
const proposal = this.requireProposal(item.proposal_id);
|
|
8762
|
+
`).run(options.workerId, leaseId, leaseExpiresAt, now, item.proposal_id);
|
|
7866
8763
|
if (proposal.state === "failed") {
|
|
7867
8764
|
this.db.prepare("UPDATE proposals SET state = 'pending_worker', updated_at = ? WHERE proposal_id = ?").run(now, item.proposal_id);
|
|
7868
8765
|
}
|
|
7869
8766
|
this.appendEvent(item.proposal_id, "writeback_worker_claimed", options.workerId, {
|
|
7870
8767
|
attempt: item.attempts + 1,
|
|
7871
8768
|
max_attempts: item.max_attempts,
|
|
8769
|
+
execution_mode: item.execution_mode,
|
|
8770
|
+
contract_digest: item.contract_digest ?? null,
|
|
8771
|
+
lease_id: leaseId,
|
|
7872
8772
|
lease_expires_at: leaseExpiresAt
|
|
7873
8773
|
});
|
|
7874
8774
|
claimed = this.workerQueueItem(item.proposal_id);
|
|
7875
8775
|
});
|
|
7876
8776
|
return claimed;
|
|
7877
8777
|
}
|
|
7878
|
-
|
|
8778
|
+
assertWorkerPolicyExecutionLimits(input) {
|
|
8779
|
+
const proposal = this.requireProposal(input.proposalId);
|
|
8780
|
+
const trips = this.workerPolicyExecutionLimitTrips({
|
|
8781
|
+
proposal,
|
|
8782
|
+
policy: input.policy,
|
|
8783
|
+
limits: input.limits,
|
|
8784
|
+
now: input.now ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
8785
|
+
});
|
|
8786
|
+
if (trips.length > 0) {
|
|
8787
|
+
throw new ProposalStoreError(
|
|
8788
|
+
"SUPERVISED_WORKER_POLICY_LIMIT_EXCEEDED",
|
|
8789
|
+
`execution-time policy limit no longer permits proposal ${proposal.proposal_id}`
|
|
8790
|
+
);
|
|
8791
|
+
}
|
|
8792
|
+
}
|
|
8793
|
+
blockQueuedWorkerItem(item, actor, errorCode, now, payload = {}) {
|
|
8794
|
+
this.db.prepare(`
|
|
8795
|
+
UPDATE worker_queue
|
|
8796
|
+
SET status = 'blocked', lease_owner = NULL, lease_id = NULL,
|
|
8797
|
+
lease_expires_at = NULL, last_error_code = ?,
|
|
8798
|
+
terminal_outcome = NULL, updated_at = ?
|
|
8799
|
+
WHERE proposal_id = ?
|
|
8800
|
+
`).run(errorCode, now, item.proposal_id);
|
|
8801
|
+
this.appendEvent(item.proposal_id, "writeback_worker_blocked", actor, {
|
|
8802
|
+
error_code: errorCode,
|
|
8803
|
+
execution_mode: item.execution_mode,
|
|
8804
|
+
contract_digest: item.contract_digest ?? null,
|
|
8805
|
+
...payload
|
|
8806
|
+
});
|
|
8807
|
+
}
|
|
8808
|
+
workerPolicyExecutionLimitTrips(input) {
|
|
8809
|
+
if (input.limits.length === 0) return [];
|
|
8810
|
+
const actor = `policy:${input.policy}`;
|
|
8811
|
+
const candidateApproval = this.db.prepare(`
|
|
8812
|
+
SELECT approval_id
|
|
8813
|
+
FROM approvals
|
|
8814
|
+
WHERE proposal_id = ?
|
|
8815
|
+
AND approver = ?
|
|
8816
|
+
AND status = 'approved'
|
|
8817
|
+
AND proposal_hash = ?
|
|
8818
|
+
AND proposal_version = ?
|
|
8819
|
+
LIMIT 1
|
|
8820
|
+
`).get(
|
|
8821
|
+
input.proposal.proposal_id,
|
|
8822
|
+
actor,
|
|
8823
|
+
input.proposal.proposal_hash,
|
|
8824
|
+
input.proposal.proposal_version
|
|
8825
|
+
);
|
|
8826
|
+
if (!isRecord3(candidateApproval)) return [];
|
|
8827
|
+
const window2 = utcDayWindow(input.now);
|
|
8828
|
+
const rows = this.db.prepare(`
|
|
8829
|
+
SELECT DISTINCT
|
|
8830
|
+
p.proposal_id,
|
|
8831
|
+
p.business_object,
|
|
8832
|
+
p.object_id,
|
|
8833
|
+
p.change_set_json
|
|
8834
|
+
FROM worker_queue q
|
|
8835
|
+
JOIN proposals p ON p.proposal_id = q.proposal_id
|
|
8836
|
+
JOIN approvals a ON a.proposal_id = p.proposal_id
|
|
8837
|
+
WHERE a.approver = ?
|
|
8838
|
+
AND a.status = 'approved'
|
|
8839
|
+
AND p.tenant_id = ?
|
|
8840
|
+
AND (
|
|
8841
|
+
(q.status = 'leased' AND q.lease_expires_at > ?)
|
|
8842
|
+
OR (
|
|
8843
|
+
q.status = 'completed'
|
|
8844
|
+
AND q.terminal_outcome IN ('applied', 'already_applied')
|
|
8845
|
+
AND q.updated_at >= ?
|
|
8846
|
+
AND q.updated_at < ?
|
|
8847
|
+
)
|
|
8848
|
+
OR (
|
|
8849
|
+
q.status = 'reconciliation_required'
|
|
8850
|
+
AND q.updated_at >= ?
|
|
8851
|
+
AND q.updated_at < ?
|
|
8852
|
+
)
|
|
8853
|
+
OR (
|
|
8854
|
+
p.state = 'applied'
|
|
8855
|
+
AND p.updated_at >= ?
|
|
8856
|
+
AND p.updated_at < ?
|
|
8857
|
+
)
|
|
8858
|
+
)
|
|
8859
|
+
`).all(
|
|
8860
|
+
actor,
|
|
8861
|
+
input.proposal.tenant_id,
|
|
8862
|
+
input.now,
|
|
8863
|
+
window2.start,
|
|
8864
|
+
window2.end,
|
|
8865
|
+
window2.start,
|
|
8866
|
+
window2.end,
|
|
8867
|
+
window2.start,
|
|
8868
|
+
window2.end
|
|
8869
|
+
);
|
|
8870
|
+
const active = /* @__PURE__ */ new Map();
|
|
8871
|
+
let invalidHistory = false;
|
|
8872
|
+
for (const row of rows) {
|
|
8873
|
+
if (!isRecord3(row)) continue;
|
|
8874
|
+
try {
|
|
8875
|
+
const proposalId = String(row.proposal_id);
|
|
8876
|
+
active.set(proposalId, {
|
|
8877
|
+
proposal_id: proposalId,
|
|
8878
|
+
business_object: String(row.business_object),
|
|
8879
|
+
object_id: String(row.object_id),
|
|
8880
|
+
change_set: parseChangeSet(JSON.parse(String(row.change_set_json)))
|
|
8881
|
+
});
|
|
8882
|
+
} catch {
|
|
8883
|
+
invalidHistory = true;
|
|
8884
|
+
active.set(String(row.proposal_id), {
|
|
8885
|
+
proposal_id: String(row.proposal_id),
|
|
8886
|
+
business_object: String(row.business_object),
|
|
8887
|
+
object_id: String(row.object_id),
|
|
8888
|
+
change_set: input.proposal.change_set
|
|
8889
|
+
});
|
|
8890
|
+
}
|
|
8891
|
+
}
|
|
8892
|
+
active.set(input.proposal.proposal_id, {
|
|
8893
|
+
proposal_id: input.proposal.proposal_id,
|
|
8894
|
+
business_object: input.proposal.business_object,
|
|
8895
|
+
object_id: input.proposal.object_id,
|
|
8896
|
+
change_set: input.proposal.change_set
|
|
8897
|
+
});
|
|
8898
|
+
const trips = [];
|
|
8899
|
+
for (const limit of input.limits) {
|
|
8900
|
+
const scope = limit.scope ?? "tenant_policy";
|
|
8901
|
+
const scoped = [...active.values()].filter((proposal) => scope !== "tenant_policy_object" || proposal.business_object === input.proposal.business_object && proposal.object_id === input.proposal.object_id);
|
|
8902
|
+
if (limit.kind === "count") {
|
|
8903
|
+
const projected2 = scoped.length;
|
|
8904
|
+
if (projected2 > limit.max) {
|
|
8905
|
+
trips.push({
|
|
8906
|
+
...limit,
|
|
8907
|
+
scope,
|
|
8908
|
+
observed: Math.max(0, projected2 - 1),
|
|
8909
|
+
proposed: 1,
|
|
8910
|
+
projected: projected2,
|
|
8911
|
+
window_start: window2.start,
|
|
8912
|
+
window_end: window2.end,
|
|
8913
|
+
reason: `${scope} execution count ${projected2} exceeds ${limit.max}`
|
|
8914
|
+
});
|
|
8915
|
+
}
|
|
8916
|
+
continue;
|
|
8917
|
+
}
|
|
8918
|
+
const field = limit.field;
|
|
8919
|
+
let projected = 0;
|
|
8920
|
+
let proposed = 0;
|
|
8921
|
+
let invalid = !field || invalidHistory;
|
|
8922
|
+
for (const proposal of scoped) {
|
|
8923
|
+
const value = field ? proposal.change_set.patch[field] : void 0;
|
|
8924
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value)) {
|
|
8925
|
+
invalid = true;
|
|
8926
|
+
continue;
|
|
8927
|
+
}
|
|
8928
|
+
projected += value;
|
|
8929
|
+
if (proposal.proposal_id === input.proposal.proposal_id) proposed = value;
|
|
8930
|
+
}
|
|
8931
|
+
if (invalid || projected > limit.max) {
|
|
8932
|
+
trips.push({
|
|
8933
|
+
...limit,
|
|
8934
|
+
scope,
|
|
8935
|
+
observed: projected - proposed,
|
|
8936
|
+
proposed,
|
|
8937
|
+
projected,
|
|
8938
|
+
window_start: window2.start,
|
|
8939
|
+
window_end: window2.end,
|
|
8940
|
+
reason: invalid ? `${scope} execution total could not be verified safely${field ? ` for ${field}` : ""}` : `${scope} execution total ${projected} for ${field} exceeds ${limit.max}`
|
|
8941
|
+
});
|
|
8942
|
+
}
|
|
8943
|
+
}
|
|
8944
|
+
return trips;
|
|
8945
|
+
}
|
|
8946
|
+
assertActiveWorkerLease(options) {
|
|
8947
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
8948
|
+
const item = this.assertWorkerLease(options.proposalId, options.workerId, options.leaseId);
|
|
8949
|
+
if (!item.lease_expires_at || Date.parse(item.lease_expires_at) <= Date.parse(now)) {
|
|
8950
|
+
throw new ProposalStoreError("WORKER_LEASE_EXPIRED", `worker lease ${options.leaseId} for ${options.proposalId} has expired`);
|
|
8951
|
+
}
|
|
8952
|
+
return item;
|
|
8953
|
+
}
|
|
8954
|
+
renewWorkerLease(options) {
|
|
8955
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
8956
|
+
const leaseSeconds = Math.max(15, Math.min(options.leaseSeconds ?? 60, 3600));
|
|
8957
|
+
const leaseExpiresAt = new Date(Date.parse(now) + leaseSeconds * 1e3).toISOString();
|
|
8958
|
+
this.transaction(() => {
|
|
8959
|
+
this.assertActiveWorkerLease({ ...options, now });
|
|
8960
|
+
this.db.prepare(`
|
|
8961
|
+
UPDATE worker_queue
|
|
8962
|
+
SET lease_expires_at = ?, updated_at = ?
|
|
8963
|
+
WHERE proposal_id = ? AND status = 'leased' AND lease_owner = ? AND lease_id = ?
|
|
8964
|
+
`).run(leaseExpiresAt, now, options.proposalId, options.workerId, options.leaseId);
|
|
8965
|
+
});
|
|
8966
|
+
return this.requireWorkerQueueItem(options.proposalId);
|
|
8967
|
+
}
|
|
8968
|
+
completeWorkerItem(proposalId, workerId, outcome, now = (/* @__PURE__ */ new Date()).toISOString(), leaseId) {
|
|
7879
8969
|
this.transaction(() => {
|
|
7880
|
-
this.assertWorkerLease(proposalId, workerId);
|
|
8970
|
+
this.assertWorkerLease(proposalId, workerId, leaseId);
|
|
7881
8971
|
this.db.prepare(`
|
|
7882
8972
|
UPDATE worker_queue
|
|
7883
|
-
SET status = 'completed', lease_owner = NULL,
|
|
7884
|
-
|
|
8973
|
+
SET status = 'completed', lease_owner = NULL, lease_id = NULL,
|
|
8974
|
+
lease_expires_at = NULL, last_error_code = NULL,
|
|
8975
|
+
terminal_outcome = ?, updated_at = ?
|
|
7885
8976
|
WHERE proposal_id = ?
|
|
7886
|
-
`).run(now, proposalId);
|
|
7887
|
-
this.appendEvent(proposalId, "writeback_worker_completed", workerId, { outcome });
|
|
8977
|
+
`).run(outcome, now, proposalId);
|
|
8978
|
+
this.appendEvent(proposalId, "writeback_worker_completed", workerId, { outcome, lease_id: leaseId ?? null });
|
|
7888
8979
|
});
|
|
7889
8980
|
return this.requireWorkerQueueItem(proposalId);
|
|
7890
8981
|
}
|
|
7891
8982
|
retryWorkerItem(options) {
|
|
7892
8983
|
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
7893
8984
|
this.transaction(() => {
|
|
7894
|
-
const item = this.assertWorkerLease(options.proposalId, options.workerId);
|
|
8985
|
+
const item = this.assertWorkerLease(options.proposalId, options.workerId, options.leaseId);
|
|
7895
8986
|
const deadLetter = item.attempts >= item.max_attempts;
|
|
7896
8987
|
this.db.prepare(`
|
|
7897
8988
|
UPDATE worker_queue
|
|
7898
|
-
SET status = ?, next_attempt_at = ?, lease_owner = NULL,
|
|
7899
|
-
lease_expires_at = NULL, last_error_code = ?,
|
|
8989
|
+
SET status = ?, next_attempt_at = ?, lease_owner = NULL, lease_id = NULL,
|
|
8990
|
+
lease_expires_at = NULL, last_error_code = ?, terminal_outcome = NULL,
|
|
8991
|
+
updated_at = ?
|
|
7900
8992
|
WHERE proposal_id = ?
|
|
7901
8993
|
`).run(deadLetter ? "dead_letter" : "retry_wait", options.retryAt, options.errorCode, now, options.proposalId);
|
|
7902
8994
|
this.appendEvent(options.proposalId, deadLetter ? "writeback_dead_lettered" : "writeback_retry_scheduled", options.workerId, {
|
|
7903
8995
|
attempt: item.attempts,
|
|
7904
8996
|
max_attempts: item.max_attempts,
|
|
7905
8997
|
error_code: options.errorCode,
|
|
8998
|
+
lease_id: options.leaseId ?? null,
|
|
7906
8999
|
...deadLetter ? {} : { retry_at: options.retryAt }
|
|
7907
9000
|
});
|
|
7908
9001
|
});
|
|
@@ -7911,17 +9004,95 @@ var ProposalStore = class {
|
|
|
7911
9004
|
deadLetterWorkerItem(options) {
|
|
7912
9005
|
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
7913
9006
|
this.transaction(() => {
|
|
7914
|
-
const item = this.assertWorkerLease(options.proposalId, options.workerId);
|
|
9007
|
+
const item = this.assertWorkerLease(options.proposalId, options.workerId, options.leaseId);
|
|
7915
9008
|
this.db.prepare(`
|
|
7916
9009
|
UPDATE worker_queue
|
|
7917
|
-
SET status = 'dead_letter', lease_owner = NULL,
|
|
7918
|
-
|
|
9010
|
+
SET status = 'dead_letter', lease_owner = NULL, lease_id = NULL,
|
|
9011
|
+
lease_expires_at = NULL, last_error_code = ?,
|
|
9012
|
+
terminal_outcome = 'dead_letter', updated_at = ?
|
|
7919
9013
|
WHERE proposal_id = ?
|
|
7920
9014
|
`).run(options.errorCode, now, options.proposalId);
|
|
7921
9015
|
this.appendEvent(options.proposalId, "writeback_dead_lettered", options.workerId, {
|
|
7922
9016
|
attempt: item.attempts,
|
|
7923
9017
|
max_attempts: item.max_attempts,
|
|
7924
|
-
error_code: options.errorCode
|
|
9018
|
+
error_code: options.errorCode,
|
|
9019
|
+
lease_id: options.leaseId ?? null
|
|
9020
|
+
});
|
|
9021
|
+
});
|
|
9022
|
+
return this.requireWorkerQueueItem(options.proposalId);
|
|
9023
|
+
}
|
|
9024
|
+
blockWorkerItem(options) {
|
|
9025
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9026
|
+
this.transaction(() => {
|
|
9027
|
+
this.assertWorkerLease(options.proposalId, options.workerId, options.leaseId);
|
|
9028
|
+
this.db.prepare(`
|
|
9029
|
+
UPDATE worker_queue
|
|
9030
|
+
SET status = 'blocked', lease_owner = NULL, lease_id = NULL,
|
|
9031
|
+
lease_expires_at = NULL, last_error_code = ?,
|
|
9032
|
+
terminal_outcome = 'blocked', updated_at = ?
|
|
9033
|
+
WHERE proposal_id = ?
|
|
9034
|
+
`).run(options.errorCode, now, options.proposalId);
|
|
9035
|
+
this.appendEvent(options.proposalId, "writeback_worker_blocked", options.workerId, {
|
|
9036
|
+
error_code: options.errorCode,
|
|
9037
|
+
lease_id: options.leaseId
|
|
9038
|
+
});
|
|
9039
|
+
});
|
|
9040
|
+
return this.requireWorkerQueueItem(options.proposalId);
|
|
9041
|
+
}
|
|
9042
|
+
requireWorkerReconciliation(options) {
|
|
9043
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9044
|
+
this.transaction(() => {
|
|
9045
|
+
this.assertWorkerLease(options.proposalId, options.workerId, options.leaseId);
|
|
9046
|
+
this.db.prepare(`
|
|
9047
|
+
UPDATE worker_queue
|
|
9048
|
+
SET status = 'reconciliation_required', lease_owner = NULL,
|
|
9049
|
+
lease_id = NULL, lease_expires_at = NULL, last_error_code = ?,
|
|
9050
|
+
terminal_outcome = 'reconciliation_required', updated_at = ?
|
|
9051
|
+
WHERE proposal_id = ?
|
|
9052
|
+
`).run(options.errorCode, now, options.proposalId);
|
|
9053
|
+
this.appendEvent(options.proposalId, "writeback_reconciliation_required", options.workerId, {
|
|
9054
|
+
safe_error_code: options.errorCode,
|
|
9055
|
+
lease_id: options.leaseId
|
|
9056
|
+
});
|
|
9057
|
+
});
|
|
9058
|
+
return this.requireWorkerQueueItem(options.proposalId);
|
|
9059
|
+
}
|
|
9060
|
+
cancelWorkerItem(options) {
|
|
9061
|
+
const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9062
|
+
const proposal = this.requireProposal(options.proposalId);
|
|
9063
|
+
assertOperatorDecision(
|
|
9064
|
+
proposal,
|
|
9065
|
+
"worker_cancel",
|
|
9066
|
+
options.actor,
|
|
9067
|
+
options.identity,
|
|
9068
|
+
options.require_verified_identity === true
|
|
9069
|
+
);
|
|
9070
|
+
this.transaction(() => {
|
|
9071
|
+
const item = this.requireWorkerQueueItem(options.proposalId);
|
|
9072
|
+
if (item.status !== "queued" && item.status !== "retry_wait") {
|
|
9073
|
+
throw new ProposalStoreError(
|
|
9074
|
+
"WORKER_ITEM_NOT_CANCELLABLE",
|
|
9075
|
+
`worker queue item ${options.proposalId} is ${item.status}, not safely cancellable before lease`
|
|
9076
|
+
);
|
|
9077
|
+
}
|
|
9078
|
+
this.db.prepare(`
|
|
9079
|
+
UPDATE worker_queue
|
|
9080
|
+
SET status = 'cancelled', lease_owner = NULL, lease_id = NULL,
|
|
9081
|
+
lease_expires_at = NULL, terminal_outcome = 'cancelled',
|
|
9082
|
+
updated_at = ?
|
|
9083
|
+
WHERE proposal_id = ?
|
|
9084
|
+
`).run(now, options.proposalId);
|
|
9085
|
+
if (proposal.state === "approved" || proposal.state === "pending_worker") {
|
|
9086
|
+
this.db.prepare(`
|
|
9087
|
+
UPDATE proposals
|
|
9088
|
+
SET state = 'canceled', updated_at = ?
|
|
9089
|
+
WHERE proposal_id = ?
|
|
9090
|
+
`).run(now, options.proposalId);
|
|
9091
|
+
}
|
|
9092
|
+
this.appendEvent(options.proposalId, "writeback_canceled", options.actor, {
|
|
9093
|
+
execution_mode: item.execution_mode,
|
|
9094
|
+
contract_digest: item.contract_digest ?? null,
|
|
9095
|
+
identity: options.identity ? publicIdentitySummary(options.identity) : null
|
|
7925
9096
|
});
|
|
7926
9097
|
});
|
|
7927
9098
|
return this.requireWorkerQueueItem(options.proposalId);
|
|
@@ -7950,7 +9121,8 @@ var ProposalStore = class {
|
|
|
7950
9121
|
this.db.prepare(`
|
|
7951
9122
|
UPDATE worker_queue
|
|
7952
9123
|
SET status = 'queued', attempts = 0, max_attempts = ?, next_attempt_at = ?,
|
|
7953
|
-
lease_owner = NULL,
|
|
9124
|
+
lease_owner = NULL, lease_id = NULL, lease_expires_at = NULL,
|
|
9125
|
+
last_error_code = NULL, terminal_outcome = NULL, updated_at = ?
|
|
7954
9126
|
WHERE proposal_id = ?
|
|
7955
9127
|
`).run(retryBudget, now, now, options.proposalId);
|
|
7956
9128
|
if (proposal.state === "failed") {
|
|
@@ -7975,7 +9147,8 @@ var ProposalStore = class {
|
|
|
7975
9147
|
}
|
|
7976
9148
|
this.db.prepare(`
|
|
7977
9149
|
UPDATE worker_queue
|
|
7978
|
-
SET status = 'discarded', lease_owner = NULL,
|
|
9150
|
+
SET status = 'discarded', lease_owner = NULL, lease_id = NULL,
|
|
9151
|
+
lease_expires_at = NULL, terminal_outcome = 'discarded', updated_at = ?
|
|
7979
9152
|
WHERE proposal_id = ?
|
|
7980
9153
|
`).run(now, options.proposalId);
|
|
7981
9154
|
this.appendEvent(options.proposalId, "writeback_dead_letter_discarded", options.identity.subject, {
|
|
@@ -7993,9 +9166,9 @@ var ProposalStore = class {
|
|
|
7993
9166
|
if (!item) throw new ProposalStoreError("WORKER_ITEM_NOT_FOUND", `worker queue item not found for ${proposalId}`);
|
|
7994
9167
|
return item;
|
|
7995
9168
|
}
|
|
7996
|
-
assertWorkerLease(proposalId, workerId) {
|
|
9169
|
+
assertWorkerLease(proposalId, workerId, leaseId) {
|
|
7997
9170
|
const item = this.requireWorkerQueueItem(proposalId);
|
|
7998
|
-
if (item.status !== "leased" || item.lease_owner !== workerId) {
|
|
9171
|
+
if (item.status !== "leased" || item.lease_owner !== workerId || leaseId !== void 0 && item.lease_id !== leaseId) {
|
|
7999
9172
|
throw new ProposalStoreError("WORKER_LEASE_MISMATCH", `worker ${workerId} does not hold the lease for ${proposalId}`);
|
|
8000
9173
|
}
|
|
8001
9174
|
return item;
|
|
@@ -8258,6 +9431,642 @@ var ProposalStore = class {
|
|
|
8258
9431
|
if (!recommendation) throw new ProposalStoreError("POLICY_RECOMMENDATION_NOT_FOUND", `policy recommendation not found: ${recommendationId}`);
|
|
8259
9432
|
return recommendation;
|
|
8260
9433
|
}
|
|
9434
|
+
recordAttentionEvent(input) {
|
|
9435
|
+
let event;
|
|
9436
|
+
const record = () => {
|
|
9437
|
+
event = this.recordAttentionEventInternal(input);
|
|
9438
|
+
};
|
|
9439
|
+
if (this.db.isTransaction) record();
|
|
9440
|
+
else this.transaction(record);
|
|
9441
|
+
if (!event) throw new ProposalStoreError("ATTENTION_EVENT_CREATE_FAILED", "attention event was not persisted");
|
|
9442
|
+
return event;
|
|
9443
|
+
}
|
|
9444
|
+
listAttentionEvents(filters = {}) {
|
|
9445
|
+
const clauses = [];
|
|
9446
|
+
const params = [];
|
|
9447
|
+
if (filters.event_type) {
|
|
9448
|
+
clauses.push("event_type = ?");
|
|
9449
|
+
params.push(filters.event_type);
|
|
9450
|
+
}
|
|
9451
|
+
if (filters.severity) {
|
|
9452
|
+
clauses.push("severity = ?");
|
|
9453
|
+
params.push(filters.severity);
|
|
9454
|
+
}
|
|
9455
|
+
if (filters.proposal_id) {
|
|
9456
|
+
clauses.push("proposal_id = ?");
|
|
9457
|
+
params.push(filters.proposal_id);
|
|
9458
|
+
}
|
|
9459
|
+
if (filters.capability) {
|
|
9460
|
+
clauses.push("capability = ?");
|
|
9461
|
+
params.push(filters.capability);
|
|
9462
|
+
}
|
|
9463
|
+
if (filters.tenant) {
|
|
9464
|
+
clauses.push("EXISTS (SELECT 1 FROM proposals p WHERE p.proposal_id = attention_events.proposal_id AND p.tenant_id = ?)");
|
|
9465
|
+
params.push(filters.tenant);
|
|
9466
|
+
}
|
|
9467
|
+
if (filters.principal) {
|
|
9468
|
+
clauses.push("EXISTS (SELECT 1 FROM proposals p WHERE p.proposal_id = attention_events.proposal_id AND p.principal = ?)");
|
|
9469
|
+
params.push(filters.principal);
|
|
9470
|
+
}
|
|
9471
|
+
if (filters.from) {
|
|
9472
|
+
clauses.push("occurred_at >= ?");
|
|
9473
|
+
params.push(filters.from);
|
|
9474
|
+
}
|
|
9475
|
+
const limit = Math.max(1, Math.min(filters.limit ?? 200, 1e3));
|
|
9476
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
9477
|
+
return this.db.prepare(`
|
|
9478
|
+
SELECT *
|
|
9479
|
+
FROM attention_events
|
|
9480
|
+
${where}
|
|
9481
|
+
ORDER BY occurred_at DESC, event_id DESC
|
|
9482
|
+
LIMIT ?
|
|
9483
|
+
`).all(...params, limit).map(rowToAttentionEvent).filter((event) => event !== void 0);
|
|
9484
|
+
}
|
|
9485
|
+
getAttentionEvent(eventId) {
|
|
9486
|
+
return rowToAttentionEvent(
|
|
9487
|
+
this.db.prepare("SELECT * FROM attention_events WHERE event_id = ?").get(eventId)
|
|
9488
|
+
);
|
|
9489
|
+
}
|
|
9490
|
+
listAttentionItems(filters = {}) {
|
|
9491
|
+
const clauses = [];
|
|
9492
|
+
const params = [];
|
|
9493
|
+
if (filters.status) {
|
|
9494
|
+
clauses.push("status = ?");
|
|
9495
|
+
params.push(filters.status);
|
|
9496
|
+
}
|
|
9497
|
+
if (filters.severity) {
|
|
9498
|
+
clauses.push("severity = ?");
|
|
9499
|
+
params.push(filters.severity);
|
|
9500
|
+
}
|
|
9501
|
+
if (filters.capability) {
|
|
9502
|
+
clauses.push("capability = ?");
|
|
9503
|
+
params.push(filters.capability);
|
|
9504
|
+
}
|
|
9505
|
+
if (filters.tenant) {
|
|
9506
|
+
clauses.push(`EXISTS (
|
|
9507
|
+
SELECT 1
|
|
9508
|
+
FROM attention_events ae
|
|
9509
|
+
JOIN proposals p ON p.proposal_id = ae.proposal_id
|
|
9510
|
+
WHERE ae.attention_key = attention_items.attention_key
|
|
9511
|
+
AND p.tenant_id = ?
|
|
9512
|
+
)`);
|
|
9513
|
+
params.push(filters.tenant);
|
|
9514
|
+
}
|
|
9515
|
+
if (filters.principal) {
|
|
9516
|
+
clauses.push(`EXISTS (
|
|
9517
|
+
SELECT 1
|
|
9518
|
+
FROM attention_events ae
|
|
9519
|
+
JOIN proposals p ON p.proposal_id = ae.proposal_id
|
|
9520
|
+
WHERE ae.attention_key = attention_items.attention_key
|
|
9521
|
+
AND p.principal = ?
|
|
9522
|
+
)`);
|
|
9523
|
+
params.push(filters.principal);
|
|
9524
|
+
}
|
|
9525
|
+
const limit = Math.max(1, Math.min(filters.limit ?? 200, 1e3));
|
|
9526
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
9527
|
+
return this.db.prepare(`
|
|
9528
|
+
SELECT *
|
|
9529
|
+
FROM attention_items
|
|
9530
|
+
${where}
|
|
9531
|
+
ORDER BY
|
|
9532
|
+
CASE severity WHEN 'critical' THEN 0 WHEN 'warning' THEN 1 ELSE 2 END,
|
|
9533
|
+
last_seen_at DESC,
|
|
9534
|
+
attention_id DESC
|
|
9535
|
+
LIMIT ?
|
|
9536
|
+
`).all(...params, limit).map(rowToAttentionItem).filter((item) => item !== void 0);
|
|
9537
|
+
}
|
|
9538
|
+
getAttentionItem(attentionId) {
|
|
9539
|
+
return rowToAttentionItem(this.db.prepare("SELECT * FROM attention_items WHERE attention_id = ?").get(attentionId));
|
|
9540
|
+
}
|
|
9541
|
+
acknowledgeAttention(input) {
|
|
9542
|
+
const actor = boundedSafeLabel(input.actor, "attention acknowledgement actor", 256);
|
|
9543
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9544
|
+
this.transaction(() => {
|
|
9545
|
+
const item = this.requireAttentionItem(input.attention_id);
|
|
9546
|
+
if (item.status !== "open") {
|
|
9547
|
+
throw new ProposalStoreError("ATTENTION_ITEM_NOT_ACKNOWLEDGEABLE", `attention item ${item.attention_id} is ${item.status}`);
|
|
9548
|
+
}
|
|
9549
|
+
assertAttentionOperatorDecision(
|
|
9550
|
+
item,
|
|
9551
|
+
actor,
|
|
9552
|
+
input.identity,
|
|
9553
|
+
input.require_verified_identity === true
|
|
9554
|
+
);
|
|
9555
|
+
this.db.prepare(`
|
|
9556
|
+
UPDATE attention_items
|
|
9557
|
+
SET status = 'acknowledged', acknowledged_by = ?, acknowledged_at = ?,
|
|
9558
|
+
acknowledgement_identity_json = ?,
|
|
9559
|
+
acknowledgement_decision_hash = ?,
|
|
9560
|
+
acknowledgement_signature = ?,
|
|
9561
|
+
acknowledgement_integrity_hash = ?,
|
|
9562
|
+
last_seen_at = MAX(last_seen_at, ?)
|
|
9563
|
+
WHERE attention_id = ?
|
|
9564
|
+
`).run(
|
|
9565
|
+
actor,
|
|
9566
|
+
now,
|
|
9567
|
+
input.identity ? JSON.stringify(input.identity) : null,
|
|
9568
|
+
input.identity?.decision_hash ?? null,
|
|
9569
|
+
input.identity?.signature ?? null,
|
|
9570
|
+
input.identity?.integrity_hash ?? null,
|
|
9571
|
+
now,
|
|
9572
|
+
item.attention_id
|
|
9573
|
+
);
|
|
9574
|
+
});
|
|
9575
|
+
return this.requireAttentionItem(input.attention_id);
|
|
9576
|
+
}
|
|
9577
|
+
resolveAttention(input) {
|
|
9578
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9579
|
+
this.transaction(() => {
|
|
9580
|
+
const item = this.requireAttentionItem(input.attention_id);
|
|
9581
|
+
if (item.status === "expired") {
|
|
9582
|
+
throw new ProposalStoreError("ATTENTION_ITEM_NOT_RESOLVABLE", `attention item ${item.attention_id} is expired`);
|
|
9583
|
+
}
|
|
9584
|
+
this.db.prepare(`
|
|
9585
|
+
UPDATE attention_items
|
|
9586
|
+
SET status = 'resolved', resolved_at = ?, last_seen_at = MAX(last_seen_at, ?)
|
|
9587
|
+
WHERE attention_id = ?
|
|
9588
|
+
`).run(now, now, item.attention_id);
|
|
9589
|
+
});
|
|
9590
|
+
return this.requireAttentionItem(input.attention_id);
|
|
9591
|
+
}
|
|
9592
|
+
enqueueNotificationDelivery(input) {
|
|
9593
|
+
const sinkId = boundedSinkId(input.sink_id);
|
|
9594
|
+
const event = this.requireAttentionEvent(input.event_id);
|
|
9595
|
+
const attention = input.attention_id ? this.requireAttentionItem(input.attention_id) : void 0;
|
|
9596
|
+
if (attention && event.attention_key !== attention.attention_key) {
|
|
9597
|
+
throw new ProposalStoreError(
|
|
9598
|
+
"NOTIFICATION_ATTENTION_MISMATCH",
|
|
9599
|
+
`attention event ${event.event_id} does not belong to attention item ${attention.attention_id}`
|
|
9600
|
+
);
|
|
9601
|
+
}
|
|
9602
|
+
const maxAttempts = Math.max(1, Math.min(input.max_attempts ?? 5, 100));
|
|
9603
|
+
const status = input.status ?? "pending";
|
|
9604
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9605
|
+
if (!Number.isFinite(Date.parse(now))) {
|
|
9606
|
+
throw new ProposalStoreError("NOTIFICATION_TIME_INVALID", "notification delivery time must be an ISO timestamp");
|
|
9607
|
+
}
|
|
9608
|
+
const nextAttemptAt = input.next_attempt_at ?? now;
|
|
9609
|
+
if (!Number.isFinite(Date.parse(nextAttemptAt))) {
|
|
9610
|
+
throw new ProposalStoreError(
|
|
9611
|
+
"NOTIFICATION_TIME_INVALID",
|
|
9612
|
+
"notification delivery next-attempt time must be an ISO timestamp"
|
|
9613
|
+
);
|
|
9614
|
+
}
|
|
9615
|
+
const deliveryId = notificationDeliveryId(sinkId, event.event_id);
|
|
9616
|
+
this.transaction(() => {
|
|
9617
|
+
const existing = this.getNotificationDelivery(deliveryId);
|
|
9618
|
+
if (existing) {
|
|
9619
|
+
if (existing.sink_id !== sinkId || existing.event_id !== event.event_id || existing.attention_id !== attention?.attention_id) {
|
|
9620
|
+
throw new ProposalStoreError(
|
|
9621
|
+
"NOTIFICATION_DELIVERY_IDEMPOTENCY_MISMATCH",
|
|
9622
|
+
`notification delivery ${deliveryId} already exists with different immutable routing`
|
|
9623
|
+
);
|
|
9624
|
+
}
|
|
9625
|
+
return;
|
|
9626
|
+
}
|
|
9627
|
+
this.db.prepare(`
|
|
9628
|
+
INSERT INTO notification_deliveries (
|
|
9629
|
+
delivery_id, sink_id, event_id, attention_id, status, attempts,
|
|
9630
|
+
max_attempts, next_attempt_at, lease_owner, lease_id,
|
|
9631
|
+
lease_expires_at, last_error_code, external_reference, delivered_at,
|
|
9632
|
+
created_at, updated_at
|
|
9633
|
+
) VALUES (?, ?, ?, ?, ?, 0, ?, ?, NULL, NULL, NULL, NULL, NULL, NULL, ?, ?)
|
|
9634
|
+
`).run(
|
|
9635
|
+
deliveryId,
|
|
9636
|
+
sinkId,
|
|
9637
|
+
event.event_id,
|
|
9638
|
+
attention?.attention_id ?? null,
|
|
9639
|
+
status,
|
|
9640
|
+
maxAttempts,
|
|
9641
|
+
nextAttemptAt,
|
|
9642
|
+
now,
|
|
9643
|
+
now
|
|
9644
|
+
);
|
|
9645
|
+
});
|
|
9646
|
+
return this.requireNotificationDelivery(deliveryId);
|
|
9647
|
+
}
|
|
9648
|
+
includeNotificationDeliveriesInDigest(input) {
|
|
9649
|
+
const sinkId = boundedSinkId(input.sink_id);
|
|
9650
|
+
const deliveryIds = [...new Set(input.delivery_ids)];
|
|
9651
|
+
if (deliveryIds.length === 0 || deliveryIds.length > 1e3) {
|
|
9652
|
+
throw new ProposalStoreError(
|
|
9653
|
+
"NOTIFICATION_DIGEST_MEMBERS_INVALID",
|
|
9654
|
+
"a notification digest must contain from 1 through 1000 delivery ids"
|
|
9655
|
+
);
|
|
9656
|
+
}
|
|
9657
|
+
const digestEvent = this.requireAttentionEvent(input.digest_event_id);
|
|
9658
|
+
if (digestEvent.event_type !== "notification.digest") {
|
|
9659
|
+
throw new ProposalStoreError(
|
|
9660
|
+
"NOTIFICATION_DIGEST_EVENT_INVALID",
|
|
9661
|
+
"notification digest membership requires a notification.digest event"
|
|
9662
|
+
);
|
|
9663
|
+
}
|
|
9664
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9665
|
+
if (!Number.isFinite(Date.parse(now))) {
|
|
9666
|
+
throw new ProposalStoreError("NOTIFICATION_TIME_INVALID", "notification digest time must be an ISO timestamp");
|
|
9667
|
+
}
|
|
9668
|
+
let included = 0;
|
|
9669
|
+
this.transaction(() => {
|
|
9670
|
+
for (const deliveryId of deliveryIds) {
|
|
9671
|
+
const item = this.requireNotificationDelivery(deliveryId);
|
|
9672
|
+
const digestReference = `digest:${digestEvent.event_id}`;
|
|
9673
|
+
if (item.status === "suppressed" && item.sink_id === sinkId && item.external_reference === digestReference) {
|
|
9674
|
+
continue;
|
|
9675
|
+
}
|
|
9676
|
+
if (item.sink_id !== sinkId || item.status !== "batched") {
|
|
9677
|
+
throw new ProposalStoreError(
|
|
9678
|
+
"NOTIFICATION_DIGEST_MEMBER_STATE_INVALID",
|
|
9679
|
+
`notification delivery ${deliveryId} is not a batched member of sink ${sinkId}`
|
|
9680
|
+
);
|
|
9681
|
+
}
|
|
9682
|
+
this.db.prepare(`
|
|
9683
|
+
UPDATE notification_deliveries
|
|
9684
|
+
SET status = 'suppressed', external_reference = ?, updated_at = ?
|
|
9685
|
+
WHERE delivery_id = ?
|
|
9686
|
+
`).run(digestReference, now, deliveryId);
|
|
9687
|
+
included += 1;
|
|
9688
|
+
}
|
|
9689
|
+
});
|
|
9690
|
+
return included;
|
|
9691
|
+
}
|
|
9692
|
+
claimNotificationDeliveries(input) {
|
|
9693
|
+
const owner = boundedSafeLabel(input.owner, "notification dispatcher identity", 128);
|
|
9694
|
+
const sinkId = input.sink_id ? boundedSinkId(input.sink_id) : void 0;
|
|
9695
|
+
const limit = Math.max(1, Math.min(input.limit ?? 20, 100));
|
|
9696
|
+
const leaseSeconds = Math.max(15, Math.min(input.lease_seconds ?? 60, 3600));
|
|
9697
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9698
|
+
if (!Number.isFinite(Date.parse(now))) {
|
|
9699
|
+
throw new ProposalStoreError("NOTIFICATION_TIME_INVALID", "notification claim time must be an ISO timestamp");
|
|
9700
|
+
}
|
|
9701
|
+
const leaseExpiresAt = new Date(Date.parse(now) + leaseSeconds * 1e3).toISOString();
|
|
9702
|
+
const claimed = [];
|
|
9703
|
+
this.transaction(() => {
|
|
9704
|
+
const rows = this.db.prepare(`
|
|
9705
|
+
SELECT *
|
|
9706
|
+
FROM notification_deliveries
|
|
9707
|
+
WHERE (
|
|
9708
|
+
(status IN ('pending', 'retry_wait') AND next_attempt_at <= ?)
|
|
9709
|
+
OR (status = 'leased' AND lease_expires_at <= ?)
|
|
9710
|
+
)
|
|
9711
|
+
AND (? IS NULL OR sink_id = ?)
|
|
9712
|
+
ORDER BY next_attempt_at ASC, created_at ASC, delivery_id ASC
|
|
9713
|
+
LIMIT ?
|
|
9714
|
+
`).all(now, now, sinkId ?? null, sinkId ?? null, limit);
|
|
9715
|
+
for (const row of rows) {
|
|
9716
|
+
const item = rowToNotificationDelivery(row);
|
|
9717
|
+
if (!item) continue;
|
|
9718
|
+
const leaseId = notificationLeaseId(item.delivery_id, owner, item.attempts + 1, now);
|
|
9719
|
+
this.db.prepare(`
|
|
9720
|
+
UPDATE notification_deliveries
|
|
9721
|
+
SET status = 'leased', attempts = attempts + 1, lease_owner = ?,
|
|
9722
|
+
lease_id = ?, lease_expires_at = ?, last_error_code = NULL,
|
|
9723
|
+
updated_at = ?
|
|
9724
|
+
WHERE delivery_id = ?
|
|
9725
|
+
`).run(owner, leaseId, leaseExpiresAt, now, item.delivery_id);
|
|
9726
|
+
claimed.push(this.requireNotificationDelivery(item.delivery_id));
|
|
9727
|
+
}
|
|
9728
|
+
});
|
|
9729
|
+
return claimed;
|
|
9730
|
+
}
|
|
9731
|
+
completeNotificationDelivery(input) {
|
|
9732
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9733
|
+
const externalReference = input.external_reference ? boundedSafeLabel(input.external_reference, "notification external reference", 256) : void 0;
|
|
9734
|
+
this.transaction(() => {
|
|
9735
|
+
this.assertNotificationLease(input.delivery_id, input.owner, input.lease_id, now);
|
|
9736
|
+
this.db.prepare(`
|
|
9737
|
+
UPDATE notification_deliveries
|
|
9738
|
+
SET status = 'delivered', lease_owner = NULL, lease_id = NULL,
|
|
9739
|
+
lease_expires_at = NULL, last_error_code = NULL,
|
|
9740
|
+
external_reference = COALESCE(?, external_reference),
|
|
9741
|
+
delivered_at = ?, updated_at = ?
|
|
9742
|
+
WHERE delivery_id = ?
|
|
9743
|
+
`).run(externalReference ?? null, now, now, input.delivery_id);
|
|
9744
|
+
});
|
|
9745
|
+
return this.requireNotificationDelivery(input.delivery_id);
|
|
9746
|
+
}
|
|
9747
|
+
failNotificationDelivery(input) {
|
|
9748
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9749
|
+
const errorCode = boundedSafeErrorCode(input.error_code);
|
|
9750
|
+
this.transaction(() => {
|
|
9751
|
+
const item = this.assertNotificationLease(input.delivery_id, input.owner, input.lease_id, now);
|
|
9752
|
+
const retryable = input.retryable && item.attempts < item.max_attempts;
|
|
9753
|
+
const retryAt = retryable ? input.retry_at ?? new Date(Date.parse(now) + 1e3).toISOString() : now;
|
|
9754
|
+
if (!Number.isFinite(Date.parse(retryAt))) {
|
|
9755
|
+
throw new ProposalStoreError("NOTIFICATION_RETRY_TIME_INVALID", "notification retry time must be an ISO timestamp");
|
|
9756
|
+
}
|
|
9757
|
+
this.db.prepare(`
|
|
9758
|
+
UPDATE notification_deliveries
|
|
9759
|
+
SET status = ?, next_attempt_at = ?, lease_owner = NULL, lease_id = NULL,
|
|
9760
|
+
lease_expires_at = NULL, last_error_code = ?, updated_at = ?
|
|
9761
|
+
WHERE delivery_id = ?
|
|
9762
|
+
`).run(retryable ? "retry_wait" : "dead_letter", retryAt, errorCode, now, input.delivery_id);
|
|
9763
|
+
});
|
|
9764
|
+
return this.requireNotificationDelivery(input.delivery_id);
|
|
9765
|
+
}
|
|
9766
|
+
listNotificationDeliveries(filters = {}) {
|
|
9767
|
+
const clauses = [];
|
|
9768
|
+
const params = [];
|
|
9769
|
+
for (const [column, value] of [
|
|
9770
|
+
["status", filters.status],
|
|
9771
|
+
["sink_id", filters.sink_id],
|
|
9772
|
+
["event_id", filters.event_id],
|
|
9773
|
+
["attention_id", filters.attention_id]
|
|
9774
|
+
]) {
|
|
9775
|
+
if (!value) continue;
|
|
9776
|
+
clauses.push(`${column} = ?`);
|
|
9777
|
+
params.push(value);
|
|
9778
|
+
}
|
|
9779
|
+
const limit = Math.max(1, Math.min(filters.limit ?? 200, 1e3));
|
|
9780
|
+
const where = clauses.length > 0 ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
9781
|
+
return this.db.prepare(`
|
|
9782
|
+
SELECT *
|
|
9783
|
+
FROM notification_deliveries
|
|
9784
|
+
${where}
|
|
9785
|
+
ORDER BY created_at DESC, delivery_id DESC
|
|
9786
|
+
LIMIT ?
|
|
9787
|
+
`).all(...params, limit).map(rowToNotificationDelivery).filter((delivery) => delivery !== void 0);
|
|
9788
|
+
}
|
|
9789
|
+
getNotificationDelivery(deliveryId) {
|
|
9790
|
+
return rowToNotificationDelivery(
|
|
9791
|
+
this.db.prepare("SELECT * FROM notification_deliveries WHERE delivery_id = ?").get(deliveryId)
|
|
9792
|
+
);
|
|
9793
|
+
}
|
|
9794
|
+
requeueNotificationDelivery(input) {
|
|
9795
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9796
|
+
const reason = boundedSafeLabel(input.reason, "notification replay reason", 256);
|
|
9797
|
+
this.transaction(() => {
|
|
9798
|
+
const item = this.requireNotificationDelivery(input.delivery_id);
|
|
9799
|
+
if (!["dead_letter", "suppressed", "batched"].includes(item.status)) {
|
|
9800
|
+
throw new ProposalStoreError(
|
|
9801
|
+
"NOTIFICATION_DELIVERY_NOT_REQUEUEABLE",
|
|
9802
|
+
`notification delivery ${item.delivery_id} is ${item.status}`
|
|
9803
|
+
);
|
|
9804
|
+
}
|
|
9805
|
+
assertNotificationReplayOperatorDecision(item, input.identity, reason);
|
|
9806
|
+
const event = this.requireAttentionEvent(item.event_id);
|
|
9807
|
+
this.db.prepare(`
|
|
9808
|
+
UPDATE notification_deliveries
|
|
9809
|
+
SET status = 'pending', attempts = 0, next_attempt_at = ?,
|
|
9810
|
+
lease_owner = NULL, lease_id = NULL, lease_expires_at = NULL,
|
|
9811
|
+
last_error_code = NULL, delivered_at = NULL, updated_at = ?
|
|
9812
|
+
WHERE delivery_id = ?
|
|
9813
|
+
`).run(now, now, item.delivery_id);
|
|
9814
|
+
this.recordAttentionEventInternal({
|
|
9815
|
+
event_type: "notification.replayed",
|
|
9816
|
+
severity: "informational",
|
|
9817
|
+
environment: event.environment,
|
|
9818
|
+
...event.proposal_id ? { proposal_id: event.proposal_id } : {},
|
|
9819
|
+
...event.job_id ? { job_id: event.job_id } : {},
|
|
9820
|
+
...event.operation_id ? { operation_id: event.operation_id } : {},
|
|
9821
|
+
...event.correlation_id ? { correlation_id: event.correlation_id } : {},
|
|
9822
|
+
...event.capability ? { capability: event.capability } : {},
|
|
9823
|
+
...event.contract_digest ? { contract_digest: event.contract_digest } : {},
|
|
9824
|
+
attention_required: false,
|
|
9825
|
+
immediate_default: false,
|
|
9826
|
+
summary: `Notification delivery ${item.delivery_id} requeued by a verified operator`,
|
|
9827
|
+
...event.workbench_path ? { workbench_path: event.workbench_path } : {},
|
|
9828
|
+
details: {
|
|
9829
|
+
delivery_id: item.delivery_id,
|
|
9830
|
+
sink_id: item.sink_id,
|
|
9831
|
+
replayed_event_id: item.event_id,
|
|
9832
|
+
operator_subject: input.identity.subject,
|
|
9833
|
+
identity_provider: input.identity.provider,
|
|
9834
|
+
operator_decision_hash: input.identity.decision_hash,
|
|
9835
|
+
reason,
|
|
9836
|
+
approval_replayed: false,
|
|
9837
|
+
mutation_replayed: false,
|
|
9838
|
+
source_database_changed: false
|
|
9839
|
+
},
|
|
9840
|
+
source_event_key: `notification-replay:${item.delivery_id}:${input.identity.decision_hash}`,
|
|
9841
|
+
now
|
|
9842
|
+
});
|
|
9843
|
+
});
|
|
9844
|
+
return this.requireNotificationDelivery(input.delivery_id);
|
|
9845
|
+
}
|
|
9846
|
+
recordAttentionEventInternal(input) {
|
|
9847
|
+
assertAttentionEventInput(input);
|
|
9848
|
+
const occurredAt = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
9849
|
+
const environment = boundedSafeLabel(input.environment, "attention environment", 64);
|
|
9850
|
+
const capability = input.capability ? boundedSafeLabel(input.capability, "attention capability", 256) : void 0;
|
|
9851
|
+
const contractDigest = input.contract_digest;
|
|
9852
|
+
const attentionRequired = input.attention_required ?? defaultAttentionEventTypes.has(input.event_type);
|
|
9853
|
+
const immediateDefault = input.immediate_default ?? defaultImmediateAttentionEventTypes.has(input.event_type);
|
|
9854
|
+
const details = Object.fromEntries(
|
|
9855
|
+
Object.entries(input.details ?? {}).sort(([left], [right]) => left.localeCompare(right))
|
|
9856
|
+
);
|
|
9857
|
+
const sourceEventKey = input.source_event_key ? boundedSafeLabel(input.source_event_key, "attention source event key", 512) : canonicalJsonDigest({
|
|
9858
|
+
event_type: input.event_type,
|
|
9859
|
+
occurred_at: occurredAt,
|
|
9860
|
+
proposal_id: input.proposal_id ?? null,
|
|
9861
|
+
job_id: input.job_id ?? null,
|
|
9862
|
+
operation_id: input.operation_id ?? null,
|
|
9863
|
+
capability: capability ?? null,
|
|
9864
|
+
details
|
|
9865
|
+
});
|
|
9866
|
+
const eventId = attentionEventId(sourceEventKey, input.event_type);
|
|
9867
|
+
const attentionKey = attentionRequired ? boundedSafeLabel(
|
|
9868
|
+
input.attention_key ?? defaultAttentionKey({
|
|
9869
|
+
environment,
|
|
9870
|
+
event_type: input.event_type,
|
|
9871
|
+
capability,
|
|
9872
|
+
contract_digest: contractDigest,
|
|
9873
|
+
details
|
|
9874
|
+
}),
|
|
9875
|
+
"attention coalescing key",
|
|
9876
|
+
512
|
|
9877
|
+
) : void 0;
|
|
9878
|
+
const unsigned = {
|
|
9879
|
+
schema_version: "synapsor.attention-event.v1",
|
|
9880
|
+
event_id: eventId,
|
|
9881
|
+
event_type: input.event_type,
|
|
9882
|
+
severity: input.severity,
|
|
9883
|
+
occurred_at: occurredAt,
|
|
9884
|
+
environment,
|
|
9885
|
+
...input.proposal_id ? { proposal_id: boundedSafeLabel(input.proposal_id, "attention proposal id", 256) } : {},
|
|
9886
|
+
...input.job_id ? { job_id: boundedSafeLabel(input.job_id, "attention job id", 256) } : {},
|
|
9887
|
+
...input.operation_id ? { operation_id: boundedSafeLabel(input.operation_id, "attention operation id", 256) } : {},
|
|
9888
|
+
...input.correlation_id ? { correlation_id: boundedSafeLabel(input.correlation_id, "attention correlation id", 256) } : {},
|
|
9889
|
+
...capability ? { capability } : {},
|
|
9890
|
+
...contractDigest ? { contract_digest: contractDigest } : {},
|
|
9891
|
+
...attentionKey ? { attention_key: attentionKey } : {},
|
|
9892
|
+
attention_required: attentionRequired,
|
|
9893
|
+
immediate_default: immediateDefault,
|
|
9894
|
+
summary: boundedSafeLabel(input.summary ?? attentionEventTitle(input.event_type), "attention summary", 512),
|
|
9895
|
+
...input.approval_source ? { approval_source: input.approval_source } : {},
|
|
9896
|
+
...input.worker_state ? { worker_state: boundedSafeLabel(input.worker_state, "attention worker state", 128) } : {},
|
|
9897
|
+
...input.failure_class ? { failure_class: boundedSafeLabel(input.failure_class, "attention failure class", 128) } : {},
|
|
9898
|
+
...input.expires_at ? { expires_at: input.expires_at } : {},
|
|
9899
|
+
...input.workbench_path ? { workbench_path: boundedWorkbenchPath(input.workbench_path) } : {},
|
|
9900
|
+
details
|
|
9901
|
+
};
|
|
9902
|
+
assertNoSecretMaterial(unsigned, `attention_event.${eventId}`);
|
|
9903
|
+
const event = {
|
|
9904
|
+
...unsigned,
|
|
9905
|
+
payload_hash: canonicalJsonDigest(unsigned)
|
|
9906
|
+
};
|
|
9907
|
+
const existing = rowToAttentionEvent(this.db.prepare("SELECT * FROM attention_events WHERE event_id = ?").get(eventId));
|
|
9908
|
+
if (existing) {
|
|
9909
|
+
if (existing.payload_hash !== event.payload_hash) {
|
|
9910
|
+
throw new ProposalStoreError(
|
|
9911
|
+
"ATTENTION_EVENT_IDEMPOTENCY_MISMATCH",
|
|
9912
|
+
`attention event ${eventId} already exists with different immutable content`
|
|
9913
|
+
);
|
|
9914
|
+
}
|
|
9915
|
+
return existing;
|
|
9916
|
+
}
|
|
9917
|
+
this.db.prepare(`
|
|
9918
|
+
INSERT INTO attention_events (
|
|
9919
|
+
event_id, schema_version, event_type, severity, occurred_at, environment,
|
|
9920
|
+
proposal_id, job_id, operation_id, correlation_id, capability,
|
|
9921
|
+
contract_digest, attention_key, attention_required, immediate_default,
|
|
9922
|
+
summary, approval_source, worker_state, failure_class, expires_at,
|
|
9923
|
+
workbench_path, details_json, payload_hash, created_at
|
|
9924
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
9925
|
+
`).run(
|
|
9926
|
+
event.event_id,
|
|
9927
|
+
event.schema_version,
|
|
9928
|
+
event.event_type,
|
|
9929
|
+
event.severity,
|
|
9930
|
+
event.occurred_at,
|
|
9931
|
+
event.environment,
|
|
9932
|
+
event.proposal_id ?? null,
|
|
9933
|
+
event.job_id ?? null,
|
|
9934
|
+
event.operation_id ?? null,
|
|
9935
|
+
event.correlation_id ?? null,
|
|
9936
|
+
event.capability ?? null,
|
|
9937
|
+
event.contract_digest ?? null,
|
|
9938
|
+
event.attention_key ?? null,
|
|
9939
|
+
event.attention_required ? 1 : 0,
|
|
9940
|
+
event.immediate_default ? 1 : 0,
|
|
9941
|
+
event.summary,
|
|
9942
|
+
event.approval_source ?? null,
|
|
9943
|
+
event.worker_state ?? null,
|
|
9944
|
+
event.failure_class ?? null,
|
|
9945
|
+
event.expires_at ?? null,
|
|
9946
|
+
event.workbench_path ?? null,
|
|
9947
|
+
JSON.stringify(event.details),
|
|
9948
|
+
event.payload_hash,
|
|
9949
|
+
event.occurred_at
|
|
9950
|
+
);
|
|
9951
|
+
if (event.attention_required && event.attention_key) this.projectAttentionItem(event);
|
|
9952
|
+
if (event.proposal_id && event.event_type === "proposal.expired") {
|
|
9953
|
+
this.closeProposalExpiryAttention(event, "expired");
|
|
9954
|
+
} else if (event.proposal_id && (event.event_type === "proposal.applied" || event.event_type === "proposal.cancelled" || event.event_type === "proposal.refused")) {
|
|
9955
|
+
this.closeProposalExpiryAttention(event, "resolved");
|
|
9956
|
+
}
|
|
9957
|
+
return event;
|
|
9958
|
+
}
|
|
9959
|
+
closeProposalExpiryAttention(event, status) {
|
|
9960
|
+
if (!event.proposal_id) return;
|
|
9961
|
+
this.db.prepare(`
|
|
9962
|
+
UPDATE attention_items
|
|
9963
|
+
SET status = ?,
|
|
9964
|
+
event_type = ?,
|
|
9965
|
+
title = ?,
|
|
9966
|
+
latest_event_id = ?,
|
|
9967
|
+
resolved_at = CASE WHEN ? = 'resolved' THEN ? ELSE NULL END,
|
|
9968
|
+
last_seen_at = MAX(last_seen_at, ?)
|
|
9969
|
+
WHERE status IN ('open', 'acknowledged')
|
|
9970
|
+
AND attention_key IN (
|
|
9971
|
+
SELECT DISTINCT attention_key
|
|
9972
|
+
FROM attention_events
|
|
9973
|
+
WHERE proposal_id = ?
|
|
9974
|
+
AND event_type = 'proposal.expiring'
|
|
9975
|
+
AND attention_key IS NOT NULL
|
|
9976
|
+
)
|
|
9977
|
+
`).run(
|
|
9978
|
+
status,
|
|
9979
|
+
event.event_type,
|
|
9980
|
+
event.summary,
|
|
9981
|
+
event.event_id,
|
|
9982
|
+
status,
|
|
9983
|
+
event.occurred_at,
|
|
9984
|
+
event.occurred_at,
|
|
9985
|
+
event.proposal_id
|
|
9986
|
+
);
|
|
9987
|
+
}
|
|
9988
|
+
projectAttentionItem(event) {
|
|
9989
|
+
const attentionKey = event.attention_key;
|
|
9990
|
+
if (!attentionKey) return;
|
|
9991
|
+
const existing = rowToAttentionItem(this.db.prepare("SELECT * FROM attention_items WHERE attention_key = ?").get(attentionKey));
|
|
9992
|
+
if (!existing) {
|
|
9993
|
+
this.db.prepare(`
|
|
9994
|
+
INSERT INTO attention_items (
|
|
9995
|
+
attention_id, attention_key, status, severity, environment, event_type,
|
|
9996
|
+
capability, contract_digest, title, occurrence_count, first_event_id,
|
|
9997
|
+
latest_event_id, first_seen_at, last_seen_at, acknowledged_by,
|
|
9998
|
+
acknowledged_at, resolved_at, expires_at
|
|
9999
|
+
) VALUES (?, ?, 'open', ?, ?, ?, ?, ?, ?, 1, ?, ?, ?, ?, NULL, NULL, NULL, ?)
|
|
10000
|
+
`).run(
|
|
10001
|
+
attentionItemId(attentionKey),
|
|
10002
|
+
attentionKey,
|
|
10003
|
+
event.severity,
|
|
10004
|
+
event.environment,
|
|
10005
|
+
event.event_type,
|
|
10006
|
+
event.capability ?? null,
|
|
10007
|
+
event.contract_digest ?? null,
|
|
10008
|
+
event.summary,
|
|
10009
|
+
event.event_id,
|
|
10010
|
+
event.event_id,
|
|
10011
|
+
event.occurred_at,
|
|
10012
|
+
event.occurred_at,
|
|
10013
|
+
event.expires_at ?? null
|
|
10014
|
+
);
|
|
10015
|
+
return;
|
|
10016
|
+
}
|
|
10017
|
+
const severity = attentionSeverityRank(event.severity) > attentionSeverityRank(existing.severity) ? event.severity : existing.severity;
|
|
10018
|
+
this.db.prepare(`
|
|
10019
|
+
UPDATE attention_items
|
|
10020
|
+
SET status = 'open', severity = ?, event_type = ?, title = ?,
|
|
10021
|
+
occurrence_count = occurrence_count + 1, latest_event_id = ?,
|
|
10022
|
+
last_seen_at = ?, acknowledged_by = NULL, acknowledged_at = NULL,
|
|
10023
|
+
acknowledgement_identity_json = NULL,
|
|
10024
|
+
acknowledgement_decision_hash = NULL,
|
|
10025
|
+
acknowledgement_signature = NULL,
|
|
10026
|
+
acknowledgement_integrity_hash = NULL,
|
|
10027
|
+
resolved_at = NULL,
|
|
10028
|
+
expires_at = COALESCE(?, expires_at)
|
|
10029
|
+
WHERE attention_id = ?
|
|
10030
|
+
`).run(
|
|
10031
|
+
severity,
|
|
10032
|
+
event.event_type,
|
|
10033
|
+
event.summary,
|
|
10034
|
+
event.event_id,
|
|
10035
|
+
event.occurred_at,
|
|
10036
|
+
event.expires_at ?? null,
|
|
10037
|
+
existing.attention_id
|
|
10038
|
+
);
|
|
10039
|
+
}
|
|
10040
|
+
requireAttentionItem(attentionId) {
|
|
10041
|
+
const item = this.getAttentionItem(attentionId);
|
|
10042
|
+
if (!item) throw new ProposalStoreError("ATTENTION_ITEM_NOT_FOUND", `attention item not found: ${attentionId}`);
|
|
10043
|
+
return item;
|
|
10044
|
+
}
|
|
10045
|
+
requireAttentionEvent(eventId) {
|
|
10046
|
+
const event = rowToAttentionEvent(this.db.prepare("SELECT * FROM attention_events WHERE event_id = ?").get(eventId));
|
|
10047
|
+
if (!event) throw new ProposalStoreError("ATTENTION_EVENT_NOT_FOUND", `attention event not found: ${eventId}`);
|
|
10048
|
+
return event;
|
|
10049
|
+
}
|
|
10050
|
+
requireNotificationDelivery(deliveryId) {
|
|
10051
|
+
const delivery = this.getNotificationDelivery(deliveryId);
|
|
10052
|
+
if (!delivery) {
|
|
10053
|
+
throw new ProposalStoreError("NOTIFICATION_DELIVERY_NOT_FOUND", `notification delivery not found: ${deliveryId}`);
|
|
10054
|
+
}
|
|
10055
|
+
return delivery;
|
|
10056
|
+
}
|
|
10057
|
+
assertNotificationLease(deliveryId, owner, leaseId, now) {
|
|
10058
|
+
const item = this.requireNotificationDelivery(deliveryId);
|
|
10059
|
+
if (item.status !== "leased" || item.lease_owner !== owner || item.lease_id !== leaseId) {
|
|
10060
|
+
throw new ProposalStoreError(
|
|
10061
|
+
"NOTIFICATION_LEASE_MISMATCH",
|
|
10062
|
+
`dispatcher ${owner} does not hold lease ${leaseId} for ${deliveryId}`
|
|
10063
|
+
);
|
|
10064
|
+
}
|
|
10065
|
+
if (!item.lease_expires_at || Date.parse(item.lease_expires_at) <= Date.parse(now)) {
|
|
10066
|
+
throw new ProposalStoreError("NOTIFICATION_LEASE_EXPIRED", `notification lease ${leaseId} has expired`);
|
|
10067
|
+
}
|
|
10068
|
+
return item;
|
|
10069
|
+
}
|
|
8261
10070
|
enqueueCloudOutbox(input) {
|
|
8262
10071
|
const eventId = input.event_id.trim();
|
|
8263
10072
|
if (!eventId) throw new ProposalStoreError("CLOUD_OUTBOX_EVENT_ID_REQUIRED", "cloud outbox event_id is required");
|
|
@@ -8433,6 +10242,92 @@ var ProposalStore = class {
|
|
|
8433
10242
|
if (!item) throw new ProposalStoreError("CLOUD_OUTBOX_EVENT_NOT_FOUND", `cloud outbox event not found: ${eventId}`);
|
|
8434
10243
|
return item;
|
|
8435
10244
|
}
|
|
10245
|
+
workerControlState() {
|
|
10246
|
+
const raw = this.getRunnerState("supervised_worker_control");
|
|
10247
|
+
if (!raw) return defaultWorkerControlState();
|
|
10248
|
+
return parseWorkerControlState(raw);
|
|
10249
|
+
}
|
|
10250
|
+
updateWorkerControl(input) {
|
|
10251
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
10252
|
+
const current = this.workerControlState();
|
|
10253
|
+
assertWorkerControlTarget(input);
|
|
10254
|
+
assertWorkerControlOperatorDecision(
|
|
10255
|
+
current,
|
|
10256
|
+
input,
|
|
10257
|
+
input.actor,
|
|
10258
|
+
input.identity,
|
|
10259
|
+
input.require_verified_identity === true
|
|
10260
|
+
);
|
|
10261
|
+
let mode = current.mode;
|
|
10262
|
+
let controls = [...current.capability_controls];
|
|
10263
|
+
if (input.action === "pause") mode = "paused";
|
|
10264
|
+
else if (input.action === "resume") mode = "active";
|
|
10265
|
+
else if (input.action === "drain") mode = "draining";
|
|
10266
|
+
else {
|
|
10267
|
+
const capability = input.capability;
|
|
10268
|
+
const contractDigest = input.contract_digest;
|
|
10269
|
+
const keyMatches = (entry) => entry.capability === capability && entry.contract_digest === contractDigest;
|
|
10270
|
+
const existing = controls.find(keyMatches);
|
|
10271
|
+
if (existing?.status === "revoked" && input.action === "capability_enable") {
|
|
10272
|
+
throw new ProposalStoreError(
|
|
10273
|
+
"WORKER_DIGEST_REVOKED",
|
|
10274
|
+
`revoked supervised-worker digest ${contractDigest} cannot be re-enabled`
|
|
10275
|
+
);
|
|
10276
|
+
}
|
|
10277
|
+
const status = input.action === "capability_enable" ? "enabled" : input.action === "capability_disable" ? "disabled" : "revoked";
|
|
10278
|
+
controls = [
|
|
10279
|
+
...controls.filter((entry) => !keyMatches(entry)),
|
|
10280
|
+
{
|
|
10281
|
+
capability,
|
|
10282
|
+
contract_digest: contractDigest,
|
|
10283
|
+
status,
|
|
10284
|
+
updated_at: now,
|
|
10285
|
+
actor: input.actor
|
|
10286
|
+
}
|
|
10287
|
+
].sort((left, right) => left.capability.localeCompare(right.capability) || left.contract_digest.localeCompare(right.contract_digest));
|
|
10288
|
+
}
|
|
10289
|
+
const unsigned = {
|
|
10290
|
+
schema_version: "synapsor.worker-control.v1",
|
|
10291
|
+
mode,
|
|
10292
|
+
revision: current.revision + 1,
|
|
10293
|
+
capability_controls: controls,
|
|
10294
|
+
...input.identity ? { last_decision: input.identity } : {},
|
|
10295
|
+
updated_at: now
|
|
10296
|
+
};
|
|
10297
|
+
const updated = {
|
|
10298
|
+
...unsigned,
|
|
10299
|
+
integrity_hash: canonicalJsonDigest(unsigned)
|
|
10300
|
+
};
|
|
10301
|
+
assertNoSecretMaterial(updated, "runner_state.supervised_worker_control");
|
|
10302
|
+
this.transaction(() => {
|
|
10303
|
+
this.db.prepare(`
|
|
10304
|
+
INSERT INTO runner_state (key, value_json, updated_at)
|
|
10305
|
+
VALUES ('supervised_worker_control', ?, ?)
|
|
10306
|
+
ON CONFLICT(key) DO UPDATE SET value_json = excluded.value_json, updated_at = excluded.updated_at
|
|
10307
|
+
`).run(JSON.stringify(updated), now);
|
|
10308
|
+
const global = input.action === "pause" || input.action === "resume" || input.action === "drain";
|
|
10309
|
+
const eventType = input.action === "resume" || input.action === "capability_enable" ? "capability.activated" : global ? "worker.paused" : "capability.revoked";
|
|
10310
|
+
this.recordAttentionEventInternal({
|
|
10311
|
+
event_type: eventType,
|
|
10312
|
+
severity: "informational",
|
|
10313
|
+
environment: input.environment ? boundedSafeLabel(input.environment, "worker control environment", 64) : "unknown",
|
|
10314
|
+
...input.capability ? { capability: input.capability } : {},
|
|
10315
|
+
...input.contract_digest ? { contract_digest: input.contract_digest } : {},
|
|
10316
|
+
attention_required: false,
|
|
10317
|
+
immediate_default: false,
|
|
10318
|
+
summary: workerControlSummary(input),
|
|
10319
|
+
worker_state: mode,
|
|
10320
|
+
details: {
|
|
10321
|
+
control_action: input.action,
|
|
10322
|
+
control_revision: updated.revision,
|
|
10323
|
+
source_database_changed: false
|
|
10324
|
+
},
|
|
10325
|
+
source_event_key: `worker-control:${updated.revision}:${updated.integrity_hash}`,
|
|
10326
|
+
now
|
|
10327
|
+
});
|
|
10328
|
+
});
|
|
10329
|
+
return updated;
|
|
10330
|
+
}
|
|
8436
10331
|
setRunnerState(key, value) {
|
|
8437
10332
|
assertNoSecretMaterial(value, `runner_state.${key}`);
|
|
8438
10333
|
this.db.prepare(`
|
|
@@ -8464,6 +10359,9 @@ var ProposalStore = class {
|
|
|
8464
10359
|
{ table: "shadow_study_cases", kind: "shadow_study_case", key: "case_id", created: "created_at", proposal: "proposal_id", tenant: "tenant_id", capability: "capability" },
|
|
8465
10360
|
{ table: "shadow_outcomes", kind: "shadow_outcome", key: "outcome_id", created: "created_at", proposal: "proposal_id", tenant: "tenant_id" },
|
|
8466
10361
|
{ table: "worker_queue", kind: "worker_queue_item", key: "proposal_id", created: "created_at", proposal: "proposal_id" },
|
|
10362
|
+
{ table: "attention_events", kind: "attention_event", key: "event_id", created: "created_at", proposal: "proposal_id", capability: "capability" },
|
|
10363
|
+
{ table: "attention_items", kind: "attention_item", key: "attention_id", created: "first_seen_at", capability: "capability" },
|
|
10364
|
+
{ table: "notification_deliveries", kind: "notification_delivery", key: "delivery_id", created: "created_at" },
|
|
8467
10365
|
{ table: "runner_state", kind: "runner_state", key: "key", created: "updated_at" },
|
|
8468
10366
|
{ table: "policy_recommendations", kind: "policy_recommendation", key: "recommendation_id", created: "created_at", tenant: "tenant_id", capability: "capability" },
|
|
8469
10367
|
{ table: "cloud_outbox", kind: "cloud_outbox_event", key: "event_id", created: "created_at", proposal: "proposal_id" },
|
|
@@ -9036,10 +10934,52 @@ var ProposalStore = class {
|
|
|
9036
10934
|
});
|
|
9037
10935
|
}
|
|
9038
10936
|
appendEvent(proposalId, kind, actor, payload) {
|
|
10937
|
+
const append = () => {
|
|
10938
|
+
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
10939
|
+
const result = this.db.prepare(`
|
|
10940
|
+
INSERT INTO proposal_events (proposal_id, kind, actor, payload_json, created_at)
|
|
10941
|
+
VALUES (?, ?, ?, ?, ?)
|
|
10942
|
+
`).run(proposalId, kind, actor, JSON.stringify(payload), createdAt);
|
|
10943
|
+
const proposal = this.requireProposal(proposalId);
|
|
10944
|
+
for (const event of attentionEventsForProposalEvent({
|
|
10945
|
+
proposal,
|
|
10946
|
+
proposal_event_id: String(result.lastInsertRowid),
|
|
10947
|
+
kind,
|
|
10948
|
+
actor,
|
|
10949
|
+
payload,
|
|
10950
|
+
environment: this.attentionEnvironment(),
|
|
10951
|
+
occurred_at: createdAt
|
|
10952
|
+
})) {
|
|
10953
|
+
this.recordAttentionEventInternal(event);
|
|
10954
|
+
}
|
|
10955
|
+
if (proposal.state !== "pending_review") {
|
|
10956
|
+
this.resolveSatisfiedProposalReviewAttention(proposal, createdAt);
|
|
10957
|
+
}
|
|
10958
|
+
};
|
|
10959
|
+
if (this.db.isTransaction) append();
|
|
10960
|
+
else this.transaction(append);
|
|
10961
|
+
}
|
|
10962
|
+
attentionEnvironment() {
|
|
10963
|
+
const context = this.getRunnerState("attention_context");
|
|
10964
|
+
const environment = context?.environment;
|
|
10965
|
+
return typeof environment === "string" && /^(development|staging|production|unknown)$/.test(environment) ? environment : "unknown";
|
|
10966
|
+
}
|
|
10967
|
+
resolveSatisfiedProposalReviewAttention(proposal, now) {
|
|
10968
|
+
const attentionKey = proposalReviewAttentionKey(proposal, this.attentionEnvironment());
|
|
10969
|
+
const pending = this.db.prepare(`
|
|
10970
|
+
SELECT COUNT(DISTINCT p.proposal_id) AS count
|
|
10971
|
+
FROM attention_events ae
|
|
10972
|
+
JOIN proposals p ON p.proposal_id = ae.proposal_id
|
|
10973
|
+
WHERE ae.attention_key = ?
|
|
10974
|
+
AND p.state = 'pending_review'
|
|
10975
|
+
`).get(attentionKey);
|
|
10976
|
+
if (isRecord3(pending) && Number(pending.count ?? 0) > 0) return;
|
|
9039
10977
|
this.db.prepare(`
|
|
9040
|
-
|
|
9041
|
-
|
|
9042
|
-
|
|
10978
|
+
UPDATE attention_items
|
|
10979
|
+
SET status = 'resolved', resolved_at = ?, last_seen_at = MAX(last_seen_at, ?)
|
|
10980
|
+
WHERE attention_key = ?
|
|
10981
|
+
AND status IN ('open', 'acknowledged')
|
|
10982
|
+
`).run(now, now, attentionKey);
|
|
9043
10983
|
}
|
|
9044
10984
|
queryAudit(proposalId) {
|
|
9045
10985
|
const rows = this.db.prepare("SELECT * FROM query_audit WHERE proposal_id = ? ORDER BY audit_id ASC").all(proposalId);
|
|
@@ -9628,174 +11568,1009 @@ function assertProposalIdentity(proposal, hash, version) {
|
|
|
9628
11568
|
if (proposal.proposal_hash !== hash) {
|
|
9629
11569
|
throw new ProposalStoreError("PROPOSAL_HASH_MISMATCH", `proposal ${proposal.proposal_id} hash mismatch`);
|
|
9630
11570
|
}
|
|
9631
|
-
if (proposal.proposal_version !== version) {
|
|
9632
|
-
throw new ProposalStoreError("PROPOSAL_VERSION_MISMATCH", `proposal ${proposal.proposal_id} version mismatch`);
|
|
11571
|
+
if (proposal.proposal_version !== version) {
|
|
11572
|
+
throw new ProposalStoreError("PROPOSAL_VERSION_MISMATCH", `proposal ${proposal.proposal_id} version mismatch`);
|
|
11573
|
+
}
|
|
11574
|
+
}
|
|
11575
|
+
function assertWritebackAllowed(proposal, operation) {
|
|
11576
|
+
if (proposal.change_set.mode === "shadow") {
|
|
11577
|
+
throw new ProposalStoreError(
|
|
11578
|
+
"SHADOW_WRITEBACK_DISABLED",
|
|
11579
|
+
`shadow proposal ${proposal.proposal_id} cannot be ${operation}; shadow mode stores proposals, evidence, query audit, and replay only and never mutates the source database`
|
|
11580
|
+
);
|
|
11581
|
+
}
|
|
11582
|
+
if (proposal.change_set.mode === "read_only") {
|
|
11583
|
+
throw new ProposalStoreError(
|
|
11584
|
+
"READ_ONLY_WRITEBACK_DISABLED",
|
|
11585
|
+
`read-only proposal ${proposal.proposal_id} cannot be ${operation}; read-only mode does not allow proposal writeback`
|
|
11586
|
+
);
|
|
11587
|
+
}
|
|
11588
|
+
}
|
|
11589
|
+
var secretKeyPattern = /(^|[_-])(password|passwd|secret|token|api[_-]?key|access[_-]?key|private[_-]?key|cookie|credential|connection[_-]?string|database[_-]?url|read[_-]?url|write[_-]?url)($|[_-])/i;
|
|
11590
|
+
var secretValuePattern = /(postgres(?:ql)?:\/\/|mysql:\/\/|Bearer\s+[A-Za-z0-9._~+/=-]+|syn_wbr_[A-Za-z0-9._~+/=-]+|-----BEGIN [A-Z ]*PRIVATE KEY-----)/i;
|
|
11591
|
+
function assertNoSecretMaterial(value, path3) {
|
|
11592
|
+
if (Array.isArray(value)) {
|
|
11593
|
+
value.forEach((item, index) => assertNoSecretMaterial(item, `${path3}[${index}]`));
|
|
11594
|
+
return;
|
|
11595
|
+
}
|
|
11596
|
+
if (isRecord3(value)) {
|
|
11597
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
11598
|
+
const entryPath = `${path3}.${key}`;
|
|
11599
|
+
if (secretKeyPattern.test(key)) {
|
|
11600
|
+
throw new ProposalStoreError(
|
|
11601
|
+
"SECRET_MATERIAL_REJECTED",
|
|
11602
|
+
`refusing to persist secret-like field ${entryPath}; remove it from reviewed visible/evidence/query-audit data`
|
|
11603
|
+
);
|
|
11604
|
+
}
|
|
11605
|
+
assertNoSecretMaterial(entry, entryPath);
|
|
11606
|
+
}
|
|
11607
|
+
return;
|
|
11608
|
+
}
|
|
11609
|
+
if (typeof value === "string" && secretValuePattern.test(value)) {
|
|
11610
|
+
throw new ProposalStoreError(
|
|
11611
|
+
"SECRET_MATERIAL_REJECTED",
|
|
11612
|
+
`refusing to persist secret-like value at ${path3}; remove it from reviewed visible/evidence/query-audit data`
|
|
11613
|
+
);
|
|
11614
|
+
}
|
|
11615
|
+
}
|
|
11616
|
+
function rowToProposal(row) {
|
|
11617
|
+
if (!isRecord3(row)) return void 0;
|
|
11618
|
+
return {
|
|
11619
|
+
proposal_id: String(row.proposal_id),
|
|
11620
|
+
proposal_version: Number(row.proposal_version),
|
|
11621
|
+
proposal_hash: String(row.proposal_hash),
|
|
11622
|
+
action: String(row.action),
|
|
11623
|
+
state: String(row.state),
|
|
11624
|
+
tenant_id: String(row.tenant_id),
|
|
11625
|
+
principal: row.principal == null ? void 0 : String(row.principal),
|
|
11626
|
+
capability: row.capability == null ? void 0 : String(row.capability),
|
|
11627
|
+
interaction_id: row.interaction_id == null ? void 0 : String(row.interaction_id),
|
|
11628
|
+
tool_call_id: row.tool_call_id == null ? void 0 : String(row.tool_call_id),
|
|
11629
|
+
business_object: String(row.business_object),
|
|
11630
|
+
object_id: String(row.object_id),
|
|
11631
|
+
source_kind: String(row.source_kind),
|
|
11632
|
+
source_id: String(row.source_id),
|
|
11633
|
+
source_schema: String(row.source_schema),
|
|
11634
|
+
source_table: String(row.source_table),
|
|
11635
|
+
source_database_mutated: Number(row.source_database_mutated) === 1,
|
|
11636
|
+
change_set: parseChangeSet(JSON.parse(String(row.change_set_json))),
|
|
11637
|
+
created_at: String(row.created_at),
|
|
11638
|
+
updated_at: String(row.updated_at)
|
|
11639
|
+
};
|
|
11640
|
+
}
|
|
11641
|
+
function rowToEvent(row) {
|
|
11642
|
+
if (!isRecord3(row)) return void 0;
|
|
11643
|
+
return {
|
|
11644
|
+
event_id: Number(row.event_id),
|
|
11645
|
+
proposal_id: String(row.proposal_id),
|
|
11646
|
+
kind: String(row.kind),
|
|
11647
|
+
actor: String(row.actor),
|
|
11648
|
+
payload: JSON.parse(String(row.payload_json)),
|
|
11649
|
+
created_at: String(row.created_at)
|
|
11650
|
+
};
|
|
11651
|
+
}
|
|
11652
|
+
function rowToApproval(row) {
|
|
11653
|
+
if (!isRecord3(row)) return void 0;
|
|
11654
|
+
const status = String(row.status);
|
|
11655
|
+
if (status !== "approved" && status !== "rejected") return void 0;
|
|
11656
|
+
return {
|
|
11657
|
+
approval_id: Number(row.approval_id),
|
|
11658
|
+
proposal_id: String(row.proposal_id),
|
|
11659
|
+
proposal_version: Number(row.proposal_version),
|
|
11660
|
+
proposal_hash: String(row.proposal_hash),
|
|
11661
|
+
approver: String(row.approver),
|
|
11662
|
+
status,
|
|
11663
|
+
reason: row.reason == null ? void 0 : String(row.reason),
|
|
11664
|
+
identity: row.identity_json == null ? void 0 : JSON.parse(String(row.identity_json)),
|
|
11665
|
+
decision_hash: row.decision_hash == null ? void 0 : String(row.decision_hash),
|
|
11666
|
+
signature: row.signature == null ? void 0 : String(row.signature),
|
|
11667
|
+
integrity_hash: row.integrity_hash == null ? void 0 : String(row.integrity_hash),
|
|
11668
|
+
freshness_proof_digest: row.freshness_proof_digest == null ? void 0 : String(row.freshness_proof_digest),
|
|
11669
|
+
created_at: String(row.created_at)
|
|
11670
|
+
};
|
|
11671
|
+
}
|
|
11672
|
+
function rowToPolicyRecommendation(row) {
|
|
11673
|
+
if (!isRecord3(row)) return void 0;
|
|
11674
|
+
let unsigned;
|
|
11675
|
+
try {
|
|
11676
|
+
unsigned = JSON.parse(String(row.payload_json));
|
|
11677
|
+
} catch {
|
|
11678
|
+
throw new ProposalStoreError("POLICY_RECOMMENDATION_TAMPERED", "policy recommendation payload is not valid JSON");
|
|
11679
|
+
}
|
|
11680
|
+
const recommendation = { ...unsigned, integrity_hash: String(row.integrity_hash) };
|
|
11681
|
+
assertPolicyRecommendationShape(recommendation);
|
|
11682
|
+
if (recommendation.recommendation_id !== String(row.recommendation_id) || recommendation.tenant_id !== String(row.tenant_id) || recommendation.capability !== String(row.capability) || recommendation.policy !== String(row.policy) || recommendation.base_contract_digest !== String(row.base_contract_digest) || recommendation.status !== String(row.status)) throw new ProposalStoreError("POLICY_RECOMMENDATION_TAMPERED", `policy recommendation ${String(row.recommendation_id)} index fields do not match its signed payload`);
|
|
11683
|
+
const expected = canonicalJsonDigest(policyRecommendationUnsigned(recommendation));
|
|
11684
|
+
if (recommendation.integrity_hash !== expected) throw new ProposalStoreError("POLICY_RECOMMENDATION_TAMPERED", `policy recommendation ${recommendation.recommendation_id} failed its integrity check`);
|
|
11685
|
+
return recommendation;
|
|
11686
|
+
}
|
|
11687
|
+
function policyRecommendationUnsigned(recommendation) {
|
|
11688
|
+
const { integrity_hash: _integrityHash, ...unsigned } = recommendation;
|
|
11689
|
+
return unsigned;
|
|
11690
|
+
}
|
|
11691
|
+
function assertPolicyRecommendationShape(recommendation) {
|
|
11692
|
+
if (recommendation.schema_version !== "synapsor.policy-recommendation.v1") throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "unsupported policy recommendation schema version");
|
|
11693
|
+
if (!/^ptr_[a-f0-9]{20}$/.test(recommendation.recommendation_id)) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation id is invalid");
|
|
11694
|
+
for (const [name, value] of [["tenant", recommendation.tenant_id], ["capability", recommendation.capability], ["policy", recommendation.policy], ["field", recommendation.field]]) {
|
|
11695
|
+
if (!value || value.length > 256) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", `policy recommendation ${name} is invalid`);
|
|
11696
|
+
}
|
|
11697
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(recommendation.base_contract_digest)) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation base digest is invalid");
|
|
11698
|
+
if (!/^sha256:[a-f0-9]{64}$/.test(recommendation.integrity_hash)) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation integrity digest is invalid");
|
|
11699
|
+
if (!Number.isFinite(recommendation.current_threshold) || !Number.isFinite(recommendation.proposed_threshold) || !Number.isFinite(recommendation.maximum_increment) || !Number.isFinite(recommendation.absolute_ceiling)) {
|
|
11700
|
+
throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation thresholds must be finite numbers");
|
|
11701
|
+
}
|
|
11702
|
+
if (recommendation.proposed_threshold <= recommendation.current_threshold || recommendation.proposed_threshold - recommendation.current_threshold > recommendation.maximum_increment || recommendation.proposed_threshold > recommendation.absolute_ceiling) {
|
|
11703
|
+
throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation exceeds its reviewed increment or ceiling");
|
|
11704
|
+
}
|
|
11705
|
+
if (!Array.isArray(recommendation.evidence_proposal_ids) || recommendation.evidence_proposal_ids.length === 0 || recommendation.evidence_proposal_ids.length > 1e4) {
|
|
11706
|
+
throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation requires bounded proposal evidence");
|
|
11707
|
+
}
|
|
11708
|
+
if (!isRecord3(recommendation.metrics) || !isRecord3(recommendation.criteria)) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation criteria and metrics are required");
|
|
11709
|
+
if (!["pending_review", "approved", "rejected", "exported"].includes(recommendation.status)) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation status is invalid");
|
|
11710
|
+
}
|
|
11711
|
+
function assertPolicyRecommendationIdentity(recommendation, input) {
|
|
11712
|
+
const proof = input.identity;
|
|
11713
|
+
if (!proof.verified || proof.provider === "dev_env") throw new ProposalStoreError("POLICY_RECOMMENDATION_VERIFIED_IDENTITY_REQUIRED", "policy recommendation decisions require a cryptographically verified operator identity");
|
|
11714
|
+
if (proof.subject !== input.actor || proof.decision.subject !== input.actor) throw new ProposalStoreError("POLICY_RECOMMENDATION_IDENTITY_MISMATCH", "policy recommendation actor does not match the verified identity");
|
|
11715
|
+
if (proof.decision.action !== input.action || proof.decision.proposal_id !== recommendation.recommendation_id || proof.decision.proposal_hash !== recommendation.integrity_hash || proof.decision.proposal_version !== 1) {
|
|
11716
|
+
throw new ProposalStoreError("POLICY_RECOMMENDATION_IDENTITY_MISMATCH", "verified operator decision is not bound to this policy recommendation version");
|
|
11717
|
+
}
|
|
11718
|
+
const { integrity_hash: _integrityHash, ...core } = proof;
|
|
11719
|
+
const canonicalCore = JSON.parse(JSON.stringify(core));
|
|
11720
|
+
if (proof.integrity_hash !== canonicalJsonDigest(canonicalCore)) throw new ProposalStoreError("POLICY_RECOMMENDATION_IDENTITY_TAMPERED", "verified operator identity proof failed its integrity check");
|
|
11721
|
+
}
|
|
11722
|
+
function attentionEventsForProposalEvent(input) {
|
|
11723
|
+
const proposal = input.proposal;
|
|
11724
|
+
const contractDigest = proposal.change_set.contract?.digest;
|
|
11725
|
+
const scopeDigest = canonicalJsonDigest({
|
|
11726
|
+
tenant_id: proposal.tenant_id,
|
|
11727
|
+
principal: proposal.principal ?? null
|
|
11728
|
+
});
|
|
11729
|
+
const sourceKey = (eventType) => `proposal:${proposal.proposal_id}:event:${input.proposal_event_id}:${eventType}`;
|
|
11730
|
+
const base = (eventType, severity) => ({
|
|
11731
|
+
event_type: eventType,
|
|
11732
|
+
severity,
|
|
11733
|
+
environment: input.environment,
|
|
11734
|
+
proposal_id: proposal.proposal_id,
|
|
11735
|
+
capability: proposal.capability ?? proposal.action,
|
|
11736
|
+
...contractDigest ? { contract_digest: contractDigest } : {},
|
|
11737
|
+
source_event_key: sourceKey(eventType),
|
|
11738
|
+
now: input.occurred_at
|
|
11739
|
+
});
|
|
11740
|
+
const safeCode = safeAttentionPayloadString(input.payload, "safe_error_code") ?? safeAttentionPayloadString(input.payload, "error_code") ?? safeAttentionPayloadString(input.payload, "reason_code");
|
|
11741
|
+
const workerDetails = safeAttentionDetails(input.payload, [
|
|
11742
|
+
"attempt",
|
|
11743
|
+
"max_attempts",
|
|
11744
|
+
"execution_mode",
|
|
11745
|
+
"outcome"
|
|
11746
|
+
]);
|
|
11747
|
+
const requiredRole = typeof proposal.change_set.approval.required_role === "string" ? proposal.change_set.approval.required_role : "reviewer";
|
|
11748
|
+
const reviewAttention = (summary) => {
|
|
11749
|
+
const attentionKey = proposalReviewAttentionKey(proposal, input.environment);
|
|
11750
|
+
return {
|
|
11751
|
+
...base("proposal.review_required", "warning"),
|
|
11752
|
+
attention_required: true,
|
|
11753
|
+
immediate_default: true,
|
|
11754
|
+
attention_key: attentionKey,
|
|
11755
|
+
summary: summary ?? `${proposal.capability ?? proposal.action} proposals need ${requiredRole} review`,
|
|
11756
|
+
workbench_path: `/attention/${attentionItemId(attentionKey)}`,
|
|
11757
|
+
details: {
|
|
11758
|
+
required_role: requiredRole,
|
|
11759
|
+
...safeCode ? { failure_class: safeCode } : {}
|
|
11760
|
+
},
|
|
11761
|
+
...safeCode ? { failure_class: safeCode } : {}
|
|
11762
|
+
};
|
|
11763
|
+
};
|
|
11764
|
+
const criticalWorkerAttention = (eventType, summary) => {
|
|
11765
|
+
const failureClass = safeCode ?? (eventType === "worker.dead_lettered" ? "RETRY_BUDGET_EXHAUSTED" : eventType === "worker.unknown_outcome" ? "OUTCOME_UNKNOWN" : "RECONCILIATION_REQUIRED");
|
|
11766
|
+
const attentionKey = [
|
|
11767
|
+
input.environment,
|
|
11768
|
+
eventType,
|
|
11769
|
+
proposal.capability ?? proposal.action,
|
|
11770
|
+
contractDigest ?? "no_digest",
|
|
11771
|
+
failureClass,
|
|
11772
|
+
scopeDigest
|
|
11773
|
+
].join(":");
|
|
11774
|
+
return {
|
|
11775
|
+
...base(eventType, "critical"),
|
|
11776
|
+
attention_required: true,
|
|
11777
|
+
immediate_default: true,
|
|
11778
|
+
attention_key: attentionKey,
|
|
11779
|
+
summary,
|
|
11780
|
+
workbench_path: `/attention/${attentionItemId(attentionKey)}`,
|
|
11781
|
+
worker_state: eventType.slice("worker.".length),
|
|
11782
|
+
failure_class: failureClass,
|
|
11783
|
+
details: {
|
|
11784
|
+
...workerDetails,
|
|
11785
|
+
failure_class: failureClass
|
|
11786
|
+
}
|
|
11787
|
+
};
|
|
11788
|
+
};
|
|
11789
|
+
if (input.kind === "proposal_created") {
|
|
11790
|
+
const events = [{
|
|
11791
|
+
...base("proposal.created", "informational"),
|
|
11792
|
+
attention_required: false,
|
|
11793
|
+
immediate_default: false,
|
|
11794
|
+
details: { source_database_changed: false }
|
|
11795
|
+
}];
|
|
11796
|
+
const approvalMode = proposal.change_set.approval.mode;
|
|
11797
|
+
if (proposal.state === "pending_review" && approvalMode !== "policy") events.push(reviewAttention());
|
|
11798
|
+
return events;
|
|
11799
|
+
}
|
|
11800
|
+
if (input.kind === "proposal_approved") {
|
|
11801
|
+
const policyApproved = input.actor.startsWith("policy:");
|
|
11802
|
+
return [{
|
|
11803
|
+
...base(policyApproved ? "proposal.auto_approved" : "proposal.approved", "informational"),
|
|
11804
|
+
attention_required: false,
|
|
11805
|
+
immediate_default: false,
|
|
11806
|
+
approval_source: policyApproved ? "policy_auto" : "human",
|
|
11807
|
+
details: safeAttentionDetails(input.payload, ["approvals", "required_approvals", "remaining_approvals"])
|
|
11808
|
+
}];
|
|
11809
|
+
}
|
|
11810
|
+
if (input.kind === "policy_auto_approval_deferred" || input.kind === "proposal_approval_blocked_freshness") {
|
|
11811
|
+
return [reviewAttention(
|
|
11812
|
+
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`
|
|
11813
|
+
)];
|
|
11814
|
+
}
|
|
11815
|
+
if (input.kind === "proposal_rejected" || input.kind === "proposal_failed") {
|
|
11816
|
+
return [{
|
|
11817
|
+
...base("proposal.refused", "warning"),
|
|
11818
|
+
attention_required: false,
|
|
11819
|
+
immediate_default: false,
|
|
11820
|
+
...safeCode ? { failure_class: safeCode, details: { failure_class: safeCode } } : {}
|
|
11821
|
+
}];
|
|
11822
|
+
}
|
|
11823
|
+
if (input.kind === "proposal_canceled" || input.kind === "writeback_canceled") {
|
|
11824
|
+
return [{
|
|
11825
|
+
...base("proposal.cancelled", "informational"),
|
|
11826
|
+
attention_required: false,
|
|
11827
|
+
immediate_default: false
|
|
11828
|
+
}];
|
|
11829
|
+
}
|
|
11830
|
+
if (input.kind === "proposal_conflict" || input.kind === "writeback_conflict" || input.kind === "writeback_intent_conflict") {
|
|
11831
|
+
return [{
|
|
11832
|
+
...base("proposal.conflict", "warning"),
|
|
11833
|
+
attention_required: false,
|
|
11834
|
+
immediate_default: false,
|
|
11835
|
+
...safeCode ? { failure_class: safeCode, details: { failure_class: safeCode } } : {}
|
|
11836
|
+
}];
|
|
11837
|
+
}
|
|
11838
|
+
if (input.kind === "proposal_pending_worker" || input.kind === "writeback_worker_queued") {
|
|
11839
|
+
return [{
|
|
11840
|
+
...base("proposal.queued", "informational"),
|
|
11841
|
+
attention_required: false,
|
|
11842
|
+
immediate_default: false,
|
|
11843
|
+
worker_state: "queued",
|
|
11844
|
+
details: safeAttentionDetails(input.payload, ["execution_mode", "max_attempts"])
|
|
11845
|
+
}];
|
|
11846
|
+
}
|
|
11847
|
+
if (input.kind === "writeback_retry_scheduled") {
|
|
11848
|
+
return [{
|
|
11849
|
+
...base("worker.retry_scheduled", "warning"),
|
|
11850
|
+
attention_required: false,
|
|
11851
|
+
immediate_default: false,
|
|
11852
|
+
worker_state: "retry_wait",
|
|
11853
|
+
...safeCode ? { failure_class: safeCode } : {},
|
|
11854
|
+
details: {
|
|
11855
|
+
...workerDetails,
|
|
11856
|
+
...safeCode ? { failure_class: safeCode } : {}
|
|
11857
|
+
}
|
|
11858
|
+
}];
|
|
11859
|
+
}
|
|
11860
|
+
if (input.kind === "writeback_dead_lettered") {
|
|
11861
|
+
return [criticalWorkerAttention("worker.dead_lettered")];
|
|
11862
|
+
}
|
|
11863
|
+
if (input.kind === "writeback_worker_blocked") {
|
|
11864
|
+
const failureClass = safeCode ?? "WORKER_POLICY_BLOCKED";
|
|
11865
|
+
if (/PROPOSAL_.*EXPIRED|PROPOSAL_EXPIRED/i.test(failureClass)) {
|
|
11866
|
+
return [{
|
|
11867
|
+
...base("proposal.expired", "warning"),
|
|
11868
|
+
attention_required: false,
|
|
11869
|
+
immediate_default: false,
|
|
11870
|
+
worker_state: "blocked",
|
|
11871
|
+
failure_class: failureClass,
|
|
11872
|
+
details: {
|
|
11873
|
+
...workerDetails,
|
|
11874
|
+
failure_class: failureClass
|
|
11875
|
+
}
|
|
11876
|
+
}];
|
|
11877
|
+
}
|
|
11878
|
+
const digestStale = /(?:CONTRACT|DIGEST|GENERATION_LOCK|SCHEMA|POLICY)_.*(?:STALE|MISMATCH|CHANGED)|ACTIVE_CONTRACT_DIGEST/i.test(failureClass);
|
|
11879
|
+
const limitExceeded = /(?:LIMIT|RATE|QUEUE|BUDGET)_.*(?:EXCEEDED|STALE|MISMATCH)|POLICY_LIMIT/i.test(failureClass);
|
|
11880
|
+
const eventType = digestStale ? "contract.digest_stale" : limitExceeded ? "policy.limit_exceeded" : "proposal.refused";
|
|
11881
|
+
const severity = digestStale || limitExceeded ? "critical" : "warning";
|
|
11882
|
+
const attentionRequired = digestStale || limitExceeded;
|
|
11883
|
+
const attentionKey = [
|
|
11884
|
+
input.environment,
|
|
11885
|
+
eventType,
|
|
11886
|
+
proposal.capability ?? proposal.action,
|
|
11887
|
+
contractDigest ?? "no_digest",
|
|
11888
|
+
failureClass,
|
|
11889
|
+
scopeDigest
|
|
11890
|
+
].join(":");
|
|
11891
|
+
return [{
|
|
11892
|
+
...base(eventType, severity),
|
|
11893
|
+
attention_required: attentionRequired,
|
|
11894
|
+
immediate_default: attentionRequired,
|
|
11895
|
+
...attentionRequired ? {
|
|
11896
|
+
attention_key: attentionKey,
|
|
11897
|
+
workbench_path: `/attention/${attentionItemId(attentionKey)}`
|
|
11898
|
+
} : {},
|
|
11899
|
+
worker_state: "blocked",
|
|
11900
|
+
failure_class: failureClass,
|
|
11901
|
+
details: {
|
|
11902
|
+
...workerDetails,
|
|
11903
|
+
failure_class: failureClass
|
|
11904
|
+
}
|
|
11905
|
+
}];
|
|
11906
|
+
}
|
|
11907
|
+
if (input.kind === "policy_limit_near") {
|
|
11908
|
+
return [{
|
|
11909
|
+
...base("policy.limit_near", "warning"),
|
|
11910
|
+
attention_required: false,
|
|
11911
|
+
immediate_default: false,
|
|
11912
|
+
summary: `${proposal.capability ?? proposal.action} is approaching a reviewed policy limit`,
|
|
11913
|
+
details: safeAttentionDetails(input.payload, [
|
|
11914
|
+
"kind",
|
|
11915
|
+
"scope",
|
|
11916
|
+
"observed",
|
|
11917
|
+
"proposed",
|
|
11918
|
+
"projected",
|
|
11919
|
+
"max",
|
|
11920
|
+
"window_start",
|
|
11921
|
+
"window_end"
|
|
11922
|
+
])
|
|
11923
|
+
}];
|
|
11924
|
+
}
|
|
11925
|
+
if (input.kind === "writeback_reconciliation_required" || input.kind === "writeback_intent_reconciliation_required") {
|
|
11926
|
+
return [criticalWorkerAttention("worker.reconciliation_required")];
|
|
11927
|
+
}
|
|
11928
|
+
if ((input.kind === "writeback_failed" || input.kind === "writeback_intent_failed") && (safeCode === "OUTCOME_UNKNOWN" || safeCode === "RECONCILIATION_REQUIRED")) {
|
|
11929
|
+
return [criticalWorkerAttention("worker.unknown_outcome")];
|
|
11930
|
+
}
|
|
11931
|
+
if (input.kind === "writeback_applied" || input.kind === "writeback_already_applied" || input.kind === "writeback_intent_applied" || input.kind === "writeback_intent_already_applied") {
|
|
11932
|
+
return [{
|
|
11933
|
+
...base("proposal.applied", "informational"),
|
|
11934
|
+
attention_required: false,
|
|
11935
|
+
immediate_default: false,
|
|
11936
|
+
worker_state: "completed",
|
|
11937
|
+
details: safeAttentionDetails(input.payload, ["rows_affected", "source_database_mutated"])
|
|
11938
|
+
}];
|
|
11939
|
+
}
|
|
11940
|
+
if (input.kind === "writeback_worker_completed") {
|
|
11941
|
+
const outcome = safeAttentionPayloadString(input.payload, "outcome");
|
|
11942
|
+
if (outcome === "conflict") {
|
|
11943
|
+
return [{
|
|
11944
|
+
...base("proposal.conflict", "warning"),
|
|
11945
|
+
attention_required: false,
|
|
11946
|
+
immediate_default: false,
|
|
11947
|
+
worker_state: "completed",
|
|
11948
|
+
details: { outcome }
|
|
11949
|
+
}];
|
|
11950
|
+
}
|
|
11951
|
+
if (outcome === "applied" || outcome === "already_applied") {
|
|
11952
|
+
return [{
|
|
11953
|
+
...base("proposal.applied", "informational"),
|
|
11954
|
+
attention_required: false,
|
|
11955
|
+
immediate_default: false,
|
|
11956
|
+
worker_state: "completed",
|
|
11957
|
+
details: { outcome }
|
|
11958
|
+
}];
|
|
11959
|
+
}
|
|
11960
|
+
}
|
|
11961
|
+
if (input.kind === "proposal_reconciliation_required") {
|
|
11962
|
+
return [criticalWorkerAttention("worker.reconciliation_required")];
|
|
11963
|
+
}
|
|
11964
|
+
return [];
|
|
11965
|
+
}
|
|
11966
|
+
function proposalReviewAttentionKey(proposal, environment) {
|
|
11967
|
+
const contractDigest = proposal.change_set.contract?.digest;
|
|
11968
|
+
const requiredRole = typeof proposal.change_set.approval.required_role === "string" ? proposal.change_set.approval.required_role : "reviewer";
|
|
11969
|
+
const scopeDigest = canonicalJsonDigest({
|
|
11970
|
+
tenant_id: proposal.tenant_id,
|
|
11971
|
+
principal: proposal.principal ?? null
|
|
11972
|
+
});
|
|
11973
|
+
return [
|
|
11974
|
+
environment,
|
|
11975
|
+
"proposal.review_required",
|
|
11976
|
+
proposal.capability ?? proposal.action,
|
|
11977
|
+
contractDigest ?? "no_digest",
|
|
11978
|
+
requiredRole,
|
|
11979
|
+
scopeDigest
|
|
11980
|
+
].join(":");
|
|
11981
|
+
}
|
|
11982
|
+
function safeAttentionPayloadString(payload, key) {
|
|
11983
|
+
const value = payload[key];
|
|
11984
|
+
if (typeof value !== "string" || !value.trim()) return void 0;
|
|
11985
|
+
return value.trim().slice(0, 128);
|
|
11986
|
+
}
|
|
11987
|
+
function safeAttentionDetails(payload, keys) {
|
|
11988
|
+
const details = {};
|
|
11989
|
+
for (const key of keys) {
|
|
11990
|
+
const value = payload[key];
|
|
11991
|
+
if (value === null || typeof value === "boolean") details[key] = value;
|
|
11992
|
+
else if (typeof value === "number" && Number.isFinite(value)) details[key] = value;
|
|
11993
|
+
else if (typeof value === "string" && value.trim()) details[key] = value.trim().slice(0, 128);
|
|
11994
|
+
}
|
|
11995
|
+
return details;
|
|
11996
|
+
}
|
|
11997
|
+
var attentionEventTypes = /* @__PURE__ */ new Set([
|
|
11998
|
+
"proposal.created",
|
|
11999
|
+
"proposal.review_required",
|
|
12000
|
+
"proposal.auto_approved",
|
|
12001
|
+
"proposal.approved",
|
|
12002
|
+
"proposal.queued",
|
|
12003
|
+
"proposal.expiring",
|
|
12004
|
+
"proposal.expired",
|
|
12005
|
+
"proposal.cancelled",
|
|
12006
|
+
"proposal.applied",
|
|
12007
|
+
"proposal.conflict",
|
|
12008
|
+
"proposal.refused",
|
|
12009
|
+
"worker.started",
|
|
12010
|
+
"worker.paused",
|
|
12011
|
+
"worker.unhealthy",
|
|
12012
|
+
"worker.recovered",
|
|
12013
|
+
"worker.queue_backlog",
|
|
12014
|
+
"worker.retry_scheduled",
|
|
12015
|
+
"worker.dead_lettered",
|
|
12016
|
+
"worker.unknown_outcome",
|
|
12017
|
+
"worker.reconciliation_required",
|
|
12018
|
+
"capability.review_required",
|
|
12019
|
+
"capability.activated",
|
|
12020
|
+
"capability.revoked",
|
|
12021
|
+
"contract.digest_stale",
|
|
12022
|
+
"schema.drift_detected",
|
|
12023
|
+
"credential.posture_changed",
|
|
12024
|
+
"policy.limit_near",
|
|
12025
|
+
"policy.limit_exceeded",
|
|
12026
|
+
"sensitive_override_activated",
|
|
12027
|
+
"notification.replayed",
|
|
12028
|
+
"notification.digest"
|
|
12029
|
+
]);
|
|
12030
|
+
var defaultAttentionEventTypes = /* @__PURE__ */ new Set([
|
|
12031
|
+
"proposal.review_required",
|
|
12032
|
+
"proposal.expiring",
|
|
12033
|
+
"proposal.expired",
|
|
12034
|
+
"worker.unhealthy",
|
|
12035
|
+
"worker.queue_backlog",
|
|
12036
|
+
"worker.dead_lettered",
|
|
12037
|
+
"worker.unknown_outcome",
|
|
12038
|
+
"worker.reconciliation_required",
|
|
12039
|
+
"capability.review_required",
|
|
12040
|
+
"contract.digest_stale",
|
|
12041
|
+
"schema.drift_detected",
|
|
12042
|
+
"credential.posture_changed",
|
|
12043
|
+
"policy.limit_exceeded",
|
|
12044
|
+
"sensitive_override_activated"
|
|
12045
|
+
]);
|
|
12046
|
+
var defaultImmediateAttentionEventTypes = /* @__PURE__ */ new Set([
|
|
12047
|
+
"proposal.review_required",
|
|
12048
|
+
"proposal.expiring",
|
|
12049
|
+
"worker.unhealthy",
|
|
12050
|
+
"worker.queue_backlog",
|
|
12051
|
+
"worker.dead_lettered",
|
|
12052
|
+
"worker.unknown_outcome",
|
|
12053
|
+
"worker.reconciliation_required",
|
|
12054
|
+
"contract.digest_stale",
|
|
12055
|
+
"schema.drift_detected",
|
|
12056
|
+
"credential.posture_changed",
|
|
12057
|
+
"policy.limit_exceeded"
|
|
12058
|
+
]);
|
|
12059
|
+
function attentionEventTitle(eventType) {
|
|
12060
|
+
const titles = {
|
|
12061
|
+
"proposal.created": "Proposal created",
|
|
12062
|
+
"proposal.review_required": "Proposal needs human review",
|
|
12063
|
+
"proposal.auto_approved": "Proposal approved by reviewed policy",
|
|
12064
|
+
"proposal.approved": "Proposal approved",
|
|
12065
|
+
"proposal.queued": "Proposal queued for trusted execution",
|
|
12066
|
+
"proposal.expiring": "Approved proposal is approaching expiry",
|
|
12067
|
+
"proposal.expired": "Approved proposal expired without execution",
|
|
12068
|
+
"proposal.cancelled": "Proposal cancelled",
|
|
12069
|
+
"proposal.applied": "Proposal applied",
|
|
12070
|
+
"proposal.conflict": "Guarded writeback conflict",
|
|
12071
|
+
"proposal.refused": "Proposal refused",
|
|
12072
|
+
"worker.started": "Trusted worker started",
|
|
12073
|
+
"worker.paused": "Trusted worker paused",
|
|
12074
|
+
"worker.unhealthy": "Trusted worker needs attention",
|
|
12075
|
+
"worker.recovered": "Trusted worker supervision recovered",
|
|
12076
|
+
"worker.queue_backlog": "Trusted worker queue backlog",
|
|
12077
|
+
"worker.retry_scheduled": "Trusted worker retry scheduled",
|
|
12078
|
+
"worker.dead_lettered": "Trusted worker job dead-lettered",
|
|
12079
|
+
"worker.unknown_outcome": "Database transaction outcome is unknown",
|
|
12080
|
+
"worker.reconciliation_required": "Operator reconciliation is required",
|
|
12081
|
+
"capability.review_required": "Capability needs human review",
|
|
12082
|
+
"capability.activated": "Capability activated",
|
|
12083
|
+
"capability.revoked": "Capability revoked",
|
|
12084
|
+
"contract.digest_stale": "Active contract or policy authority is stale",
|
|
12085
|
+
"schema.drift_detected": "Schema drift blocks active authority",
|
|
12086
|
+
"credential.posture_changed": "Credential posture changed",
|
|
12087
|
+
"policy.limit_near": "Policy limit is approaching",
|
|
12088
|
+
"policy.limit_exceeded": "Policy limit exceeded",
|
|
12089
|
+
"sensitive_override_activated": "Sensitive-field override activated",
|
|
12090
|
+
"notification.replayed": "Notification delivery requeued by a verified operator",
|
|
12091
|
+
"notification.digest": "Runner activity digest"
|
|
12092
|
+
};
|
|
12093
|
+
return titles[eventType];
|
|
12094
|
+
}
|
|
12095
|
+
function assertAttentionEventInput(input) {
|
|
12096
|
+
if (!attentionEventTypes.has(input.event_type)) {
|
|
12097
|
+
throw new ProposalStoreError("ATTENTION_EVENT_TYPE_INVALID", `unsupported attention event type: ${String(input.event_type)}`);
|
|
12098
|
+
}
|
|
12099
|
+
if (!["informational", "warning", "critical"].includes(input.severity)) {
|
|
12100
|
+
throw new ProposalStoreError("ATTENTION_EVENT_SEVERITY_INVALID", `unsupported attention severity: ${String(input.severity)}`);
|
|
12101
|
+
}
|
|
12102
|
+
if (input.contract_digest && !/^sha256:[a-f0-9]{64}$/.test(input.contract_digest)) {
|
|
12103
|
+
throw new ProposalStoreError("ATTENTION_EVENT_DIGEST_INVALID", "attention event contract digest must be a lowercase sha256 digest");
|
|
12104
|
+
}
|
|
12105
|
+
for (const [label, value] of [["now", input.now], ["expires_at", input.expires_at]]) {
|
|
12106
|
+
if (value !== void 0 && !Number.isFinite(Date.parse(value))) {
|
|
12107
|
+
throw new ProposalStoreError("ATTENTION_EVENT_TIME_INVALID", `attention event ${label} must be an ISO timestamp`);
|
|
12108
|
+
}
|
|
12109
|
+
}
|
|
12110
|
+
const entries = Object.entries(input.details ?? {});
|
|
12111
|
+
if (entries.length > 32) {
|
|
12112
|
+
throw new ProposalStoreError("ATTENTION_EVENT_DETAILS_TOO_LARGE", "attention event details may contain at most 32 safe fields");
|
|
12113
|
+
}
|
|
12114
|
+
for (const [key, value] of entries) {
|
|
12115
|
+
if (!/^[a-z][a-z0-9_]{0,63}$/.test(key)) {
|
|
12116
|
+
throw new ProposalStoreError("ATTENTION_EVENT_DETAIL_KEY_INVALID", `attention event detail key is invalid: ${key}`);
|
|
12117
|
+
}
|
|
12118
|
+
if (typeof value === "string") boundedSafeLabel(value, `attention detail ${key}`, 256);
|
|
12119
|
+
else if (typeof value === "number" && !Number.isFinite(value)) {
|
|
12120
|
+
throw new ProposalStoreError("ATTENTION_EVENT_DETAIL_VALUE_INVALID", `attention event detail ${key} must be finite`);
|
|
12121
|
+
} else if (value !== null && typeof value !== "number" && typeof value !== "boolean") {
|
|
12122
|
+
throw new ProposalStoreError("ATTENTION_EVENT_DETAIL_VALUE_INVALID", `attention event detail ${key} must be scalar`);
|
|
12123
|
+
}
|
|
12124
|
+
}
|
|
12125
|
+
assertNoSecretMaterial(input.details ?? {}, "attention_event.details");
|
|
12126
|
+
}
|
|
12127
|
+
function boundedSafeLabel(value, label, maximum) {
|
|
12128
|
+
const trimmed = value.trim();
|
|
12129
|
+
if (!trimmed || trimmed.length > maximum || /[\u0000-\u001f\u007f]/.test(trimmed)) {
|
|
12130
|
+
throw new ProposalStoreError("ATTENTION_EVENT_FIELD_INVALID", `${label} must be a non-empty bounded single-line value`);
|
|
12131
|
+
}
|
|
12132
|
+
assertNoSecretMaterial({ value: trimmed }, label);
|
|
12133
|
+
return trimmed;
|
|
12134
|
+
}
|
|
12135
|
+
function boundedWorkbenchPath(value) {
|
|
12136
|
+
const path3 = boundedSafeLabel(value, "attention Workbench path", 512);
|
|
12137
|
+
if (!path3.startsWith("/") || path3.includes("?") || path3.includes("#") || path3.includes("://") || path3.includes("..")) {
|
|
12138
|
+
throw new ProposalStoreError(
|
|
12139
|
+
"ATTENTION_WORKBENCH_PATH_INVALID",
|
|
12140
|
+
"attention Workbench path must be a local absolute path without query, fragment, traversal, or authority"
|
|
12141
|
+
);
|
|
12142
|
+
}
|
|
12143
|
+
return path3;
|
|
12144
|
+
}
|
|
12145
|
+
function defaultAttentionKey(input) {
|
|
12146
|
+
const failureClass = typeof input.details.failure_class === "string" ? input.details.failure_class : typeof input.details.reason_code === "string" ? input.details.reason_code : "none";
|
|
12147
|
+
return [
|
|
12148
|
+
input.environment,
|
|
12149
|
+
input.event_type,
|
|
12150
|
+
input.capability ?? "global",
|
|
12151
|
+
input.contract_digest ?? "no_digest",
|
|
12152
|
+
failureClass
|
|
12153
|
+
].join(":");
|
|
12154
|
+
}
|
|
12155
|
+
function attentionEventId(sourceEventKey, eventType) {
|
|
12156
|
+
const digest = canonicalJsonDigest({ source_event_key: sourceEventKey, event_type: eventType });
|
|
12157
|
+
return `aev_${digest.slice("sha256:".length)}`;
|
|
12158
|
+
}
|
|
12159
|
+
function attentionItemId(attentionKey) {
|
|
12160
|
+
const digest = canonicalJsonDigest({ attention_key: attentionKey });
|
|
12161
|
+
return `attn_${digest.slice("sha256:".length, "sha256:".length + 32)}`;
|
|
12162
|
+
}
|
|
12163
|
+
function boundedSinkId(value) {
|
|
12164
|
+
const sinkId = boundedSafeLabel(value, "notification sink id", 128);
|
|
12165
|
+
if (!/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(sinkId)) {
|
|
12166
|
+
throw new ProposalStoreError(
|
|
12167
|
+
"NOTIFICATION_SINK_ID_INVALID",
|
|
12168
|
+
"notification sink id must use letters, numbers, underscore, period, or hyphen"
|
|
12169
|
+
);
|
|
12170
|
+
}
|
|
12171
|
+
return sinkId;
|
|
12172
|
+
}
|
|
12173
|
+
function boundedSafeErrorCode(value) {
|
|
12174
|
+
const errorCode = boundedSafeLabel(value, "notification error code", 128);
|
|
12175
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(errorCode)) {
|
|
12176
|
+
throw new ProposalStoreError(
|
|
12177
|
+
"NOTIFICATION_ERROR_CODE_INVALID",
|
|
12178
|
+
"notification error code must be an uppercase safe code"
|
|
12179
|
+
);
|
|
12180
|
+
}
|
|
12181
|
+
return errorCode;
|
|
12182
|
+
}
|
|
12183
|
+
function notificationDeliveryId(sinkId, eventId) {
|
|
12184
|
+
const digest = canonicalJsonDigest({ sink_id: sinkId, event_id: eventId });
|
|
12185
|
+
return `ndl_${digest.slice("sha256:".length, "sha256:".length + 32)}`;
|
|
12186
|
+
}
|
|
12187
|
+
function notificationLeaseId(deliveryId, owner, attempt, now) {
|
|
12188
|
+
const digest = canonicalJsonDigest({
|
|
12189
|
+
delivery_id: deliveryId,
|
|
12190
|
+
owner,
|
|
12191
|
+
attempt,
|
|
12192
|
+
now
|
|
12193
|
+
});
|
|
12194
|
+
return `nlease_${digest.slice("sha256:".length, "sha256:".length + 32)}`;
|
|
12195
|
+
}
|
|
12196
|
+
function attentionSeverityRank(severity) {
|
|
12197
|
+
if (severity === "critical") return 3;
|
|
12198
|
+
if (severity === "warning") return 2;
|
|
12199
|
+
return 1;
|
|
12200
|
+
}
|
|
12201
|
+
function workerControlDecisionSubject(state, target) {
|
|
12202
|
+
assertWorkerControlTarget(target);
|
|
12203
|
+
const targetDigest = canonicalJsonDigest({
|
|
12204
|
+
schema_version: "synapsor.worker-control-decision-subject.v1",
|
|
12205
|
+
current_revision: state.revision,
|
|
12206
|
+
current_integrity_hash: state.integrity_hash,
|
|
12207
|
+
action: target.action,
|
|
12208
|
+
capability: target.capability ?? null,
|
|
12209
|
+
contract_digest: target.contract_digest ?? null
|
|
12210
|
+
});
|
|
12211
|
+
return {
|
|
12212
|
+
proposal_id: `worker_control_${targetDigest.slice("sha256:".length, "sha256:".length + 32)}`,
|
|
12213
|
+
proposal_version: state.revision + 1,
|
|
12214
|
+
proposal_hash: targetDigest
|
|
12215
|
+
};
|
|
12216
|
+
}
|
|
12217
|
+
function defaultWorkerControlState() {
|
|
12218
|
+
const unsigned = {
|
|
12219
|
+
schema_version: "synapsor.worker-control.v1",
|
|
12220
|
+
mode: "active",
|
|
12221
|
+
revision: 0,
|
|
12222
|
+
capability_controls: [],
|
|
12223
|
+
updated_at: "1970-01-01T00:00:00.000Z"
|
|
12224
|
+
};
|
|
12225
|
+
return { ...unsigned, integrity_hash: canonicalJsonDigest(unsigned) };
|
|
12226
|
+
}
|
|
12227
|
+
function parseWorkerControlState(value) {
|
|
12228
|
+
if (!isRecord3(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") {
|
|
12229
|
+
throw new ProposalStoreError("WORKER_CONTROL_STATE_INVALID", "stored supervised-worker control state is invalid");
|
|
12230
|
+
}
|
|
12231
|
+
const controls = value.capability_controls.map((entry) => {
|
|
12232
|
+
if (!isRecord3(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") {
|
|
12233
|
+
throw new ProposalStoreError("WORKER_CONTROL_STATE_INVALID", "stored supervised-worker capability control is invalid");
|
|
12234
|
+
}
|
|
12235
|
+
return {
|
|
12236
|
+
capability: entry.capability,
|
|
12237
|
+
contract_digest: entry.contract_digest,
|
|
12238
|
+
status: entry.status,
|
|
12239
|
+
updated_at: entry.updated_at,
|
|
12240
|
+
actor: entry.actor
|
|
12241
|
+
};
|
|
12242
|
+
});
|
|
12243
|
+
const lastDecision = isRecord3(value.last_decision) ? value.last_decision : void 0;
|
|
12244
|
+
const unsigned = {
|
|
12245
|
+
schema_version: "synapsor.worker-control.v1",
|
|
12246
|
+
mode: value.mode,
|
|
12247
|
+
revision: Number(value.revision),
|
|
12248
|
+
capability_controls: controls,
|
|
12249
|
+
...lastDecision ? { last_decision: lastDecision } : {},
|
|
12250
|
+
updated_at: value.updated_at
|
|
12251
|
+
};
|
|
12252
|
+
const integrityHash = value.integrity_hash;
|
|
12253
|
+
if (integrityHash !== canonicalJsonDigest(unsigned)) {
|
|
12254
|
+
throw new ProposalStoreError("WORKER_CONTROL_STATE_TAMPERED", "stored supervised-worker control state failed integrity validation");
|
|
12255
|
+
}
|
|
12256
|
+
return { ...unsigned, integrity_hash: integrityHash };
|
|
12257
|
+
}
|
|
12258
|
+
function assertWorkerControlTarget(target) {
|
|
12259
|
+
const capabilityAction = target.action === "capability_enable" || target.action === "capability_disable" || target.action === "digest_revoke";
|
|
12260
|
+
if (capabilityAction) {
|
|
12261
|
+
if (!target.capability || !/^[A-Za-z_][A-Za-z0-9_.-]*$/.test(target.capability)) {
|
|
12262
|
+
throw new ProposalStoreError("WORKER_CONTROL_CAPABILITY_REQUIRED", `${target.action} requires a fixed capability name`);
|
|
12263
|
+
}
|
|
12264
|
+
if (!target.contract_digest || !/^sha256:[a-f0-9]{64}$/.test(target.contract_digest)) {
|
|
12265
|
+
throw new ProposalStoreError("WORKER_CONTROL_DIGEST_REQUIRED", `${target.action} requires an exact sha256 contract digest`);
|
|
12266
|
+
}
|
|
12267
|
+
} else if (target.capability || target.contract_digest) {
|
|
12268
|
+
throw new ProposalStoreError("WORKER_CONTROL_TARGET_INVALID", `${target.action} is a global control and cannot carry capability authority`);
|
|
12269
|
+
}
|
|
12270
|
+
}
|
|
12271
|
+
function workerControlOperatorAction(action) {
|
|
12272
|
+
if (action === "pause") return "worker_pause";
|
|
12273
|
+
if (action === "resume") return "worker_resume";
|
|
12274
|
+
if (action === "drain") return "worker_drain";
|
|
12275
|
+
if (action === "capability_enable") return "worker_capability_enable";
|
|
12276
|
+
if (action === "capability_disable") return "worker_capability_disable";
|
|
12277
|
+
return "worker_digest_revoke";
|
|
12278
|
+
}
|
|
12279
|
+
function assertWorkerControlOperatorDecision(state, target, actor, identity, requireVerified) {
|
|
12280
|
+
if (requireVerified && (!identity || !identity.verified || identity.provider === "dev_env")) {
|
|
12281
|
+
throw new ProposalStoreError(
|
|
12282
|
+
"VERIFIED_OPERATOR_IDENTITY_REQUIRED",
|
|
12283
|
+
`verified operator identity is required to ${target.action} supervised execution`
|
|
12284
|
+
);
|
|
12285
|
+
}
|
|
12286
|
+
if (!identity) return;
|
|
12287
|
+
if (identity.subject !== actor || identity.decision.subject !== actor) {
|
|
12288
|
+
throw new ProposalStoreError("OPERATOR_IDENTITY_MISMATCH", "worker-control identity does not match its actor");
|
|
12289
|
+
}
|
|
12290
|
+
const subject = workerControlDecisionSubject(state, target);
|
|
12291
|
+
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) {
|
|
12292
|
+
throw new ProposalStoreError("OPERATOR_DECISION_MISMATCH", "operator proof is not bound to this exact worker-control revision");
|
|
12293
|
+
}
|
|
12294
|
+
if (identity.decision_hash !== canonicalJsonDigest(identity.decision)) {
|
|
12295
|
+
throw new ProposalStoreError("OPERATOR_IDENTITY_TAMPERED", "worker-control decision hash failed integrity validation");
|
|
12296
|
+
}
|
|
12297
|
+
const { integrity_hash: _integrityHash, ...identityCore } = identity;
|
|
12298
|
+
if (identity.integrity_hash !== canonicalJsonDigest(identityCore)) {
|
|
12299
|
+
throw new ProposalStoreError("OPERATOR_IDENTITY_TAMPERED", "worker-control identity proof failed integrity validation");
|
|
12300
|
+
}
|
|
12301
|
+
}
|
|
12302
|
+
function workerControlSummary(target) {
|
|
12303
|
+
if (target.action === "pause") return "Supervised execution paused by an operator";
|
|
12304
|
+
if (target.action === "resume") return "Supervised execution resumed by an operator";
|
|
12305
|
+
if (target.action === "drain") return "Supervised execution is draining without new leases";
|
|
12306
|
+
if (target.action === "capability_enable") return `${target.capability} supervised execution enabled for the exact reviewed digest`;
|
|
12307
|
+
if (target.action === "capability_disable") return `${target.capability} supervised execution disabled for the exact reviewed digest`;
|
|
12308
|
+
return `${target.capability} supervised execution digest revoked`;
|
|
12309
|
+
}
|
|
12310
|
+
function attentionDecisionSubject(item) {
|
|
12311
|
+
return {
|
|
12312
|
+
proposal_id: item.attention_id,
|
|
12313
|
+
proposal_version: item.occurrence_count,
|
|
12314
|
+
proposal_hash: canonicalJsonDigest({
|
|
12315
|
+
schema_version: "synapsor.attention-decision-subject.v1",
|
|
12316
|
+
attention_id: item.attention_id,
|
|
12317
|
+
attention_key: item.attention_key,
|
|
12318
|
+
status: item.status,
|
|
12319
|
+
severity: item.severity,
|
|
12320
|
+
environment: item.environment,
|
|
12321
|
+
event_type: item.event_type,
|
|
12322
|
+
capability: item.capability ?? null,
|
|
12323
|
+
contract_digest: item.contract_digest ?? null,
|
|
12324
|
+
occurrence_count: item.occurrence_count,
|
|
12325
|
+
latest_event_id: item.latest_event_id,
|
|
12326
|
+
last_seen_at: item.last_seen_at
|
|
12327
|
+
})
|
|
12328
|
+
};
|
|
12329
|
+
}
|
|
12330
|
+
function notificationReplayDecisionSubject(delivery) {
|
|
12331
|
+
return {
|
|
12332
|
+
proposal_id: delivery.delivery_id,
|
|
12333
|
+
proposal_version: Math.max(1, delivery.attempts),
|
|
12334
|
+
proposal_hash: canonicalJsonDigest({
|
|
12335
|
+
schema_version: "synapsor.notification-replay-decision-subject.v1",
|
|
12336
|
+
delivery_id: delivery.delivery_id,
|
|
12337
|
+
sink_id: delivery.sink_id,
|
|
12338
|
+
event_id: delivery.event_id,
|
|
12339
|
+
attention_id: delivery.attention_id ?? null,
|
|
12340
|
+
status: delivery.status,
|
|
12341
|
+
attempts: delivery.attempts,
|
|
12342
|
+
max_attempts: delivery.max_attempts,
|
|
12343
|
+
last_error_code: delivery.last_error_code ?? null,
|
|
12344
|
+
updated_at: delivery.updated_at
|
|
12345
|
+
})
|
|
12346
|
+
};
|
|
12347
|
+
}
|
|
12348
|
+
function assertNotificationReplayOperatorDecision(delivery, identity, reason) {
|
|
12349
|
+
if (!identity.verified || identity.provider === "dev_env") {
|
|
12350
|
+
throw new ProposalStoreError(
|
|
12351
|
+
"VERIFIED_OPERATOR_IDENTITY_REQUIRED",
|
|
12352
|
+
`verified operator identity is required to replay notification delivery ${delivery.delivery_id}`
|
|
12353
|
+
);
|
|
12354
|
+
}
|
|
12355
|
+
if (identity.subject !== identity.decision.subject) {
|
|
12356
|
+
throw new ProposalStoreError(
|
|
12357
|
+
"OPERATOR_IDENTITY_MISMATCH",
|
|
12358
|
+
"notification replay identity does not match its signed decision subject"
|
|
12359
|
+
);
|
|
12360
|
+
}
|
|
12361
|
+
const subject = notificationReplayDecisionSubject(delivery);
|
|
12362
|
+
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) {
|
|
12363
|
+
throw new ProposalStoreError(
|
|
12364
|
+
"OPERATOR_DECISION_MISMATCH",
|
|
12365
|
+
"operator proof is not bound to this exact notification delivery revision and replay reason"
|
|
12366
|
+
);
|
|
9633
12367
|
}
|
|
9634
|
-
|
|
9635
|
-
function assertWritebackAllowed(proposal, operation) {
|
|
9636
|
-
if (proposal.change_set.mode === "shadow") {
|
|
12368
|
+
if (identity.decision_hash !== canonicalJsonDigest(identity.decision)) {
|
|
9637
12369
|
throw new ProposalStoreError(
|
|
9638
|
-
"
|
|
9639
|
-
|
|
12370
|
+
"OPERATOR_IDENTITY_TAMPERED",
|
|
12371
|
+
"notification replay decision hash failed its integrity check"
|
|
9640
12372
|
);
|
|
9641
12373
|
}
|
|
9642
|
-
|
|
12374
|
+
const { integrity_hash: _integrityHash, ...core } = identity;
|
|
12375
|
+
const canonicalCore = JSON.parse(JSON.stringify(core));
|
|
12376
|
+
if (identity.integrity_hash !== canonicalJsonDigest(canonicalCore)) {
|
|
9643
12377
|
throw new ProposalStoreError(
|
|
9644
|
-
"
|
|
9645
|
-
|
|
12378
|
+
"OPERATOR_IDENTITY_TAMPERED",
|
|
12379
|
+
"notification replay identity proof failed its integrity check"
|
|
9646
12380
|
);
|
|
9647
12381
|
}
|
|
9648
12382
|
}
|
|
9649
|
-
|
|
9650
|
-
|
|
9651
|
-
|
|
9652
|
-
|
|
9653
|
-
|
|
9654
|
-
|
|
12383
|
+
function assertAttentionOperatorDecision(item, actor, identity, requireVerified) {
|
|
12384
|
+
if (requireVerified && (!identity || !identity.verified || identity.provider === "dev_env")) {
|
|
12385
|
+
throw new ProposalStoreError(
|
|
12386
|
+
"VERIFIED_OPERATOR_IDENTITY_REQUIRED",
|
|
12387
|
+
`verified operator identity is required to acknowledge attention item ${item.attention_id}`
|
|
12388
|
+
);
|
|
9655
12389
|
}
|
|
9656
|
-
if (
|
|
9657
|
-
|
|
9658
|
-
|
|
9659
|
-
|
|
9660
|
-
|
|
9661
|
-
|
|
9662
|
-
`refusing to persist secret-like field ${entryPath}; remove it from reviewed visible/evidence/query-audit data`
|
|
9663
|
-
);
|
|
9664
|
-
}
|
|
9665
|
-
assertNoSecretMaterial(entry, entryPath);
|
|
9666
|
-
}
|
|
9667
|
-
return;
|
|
12390
|
+
if (!identity) return;
|
|
12391
|
+
if (identity.subject !== actor || identity.decision.subject !== actor) {
|
|
12392
|
+
throw new ProposalStoreError(
|
|
12393
|
+
"OPERATOR_IDENTITY_MISMATCH",
|
|
12394
|
+
`operator identity ${identity.subject} does not match attention acknowledgement actor ${actor}`
|
|
12395
|
+
);
|
|
9668
12396
|
}
|
|
9669
|
-
|
|
12397
|
+
const subject = attentionDecisionSubject(item);
|
|
12398
|
+
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) {
|
|
9670
12399
|
throw new ProposalStoreError(
|
|
9671
|
-
"
|
|
9672
|
-
|
|
12400
|
+
"OPERATOR_DECISION_MISMATCH",
|
|
12401
|
+
"operator proof is not bound to this exact attention item version"
|
|
12402
|
+
);
|
|
12403
|
+
}
|
|
12404
|
+
if (identity.decision_hash !== canonicalJsonDigest(identity.decision)) {
|
|
12405
|
+
throw new ProposalStoreError(
|
|
12406
|
+
"OPERATOR_IDENTITY_TAMPERED",
|
|
12407
|
+
"attention acknowledgement decision hash failed its integrity check"
|
|
12408
|
+
);
|
|
12409
|
+
}
|
|
12410
|
+
const { integrity_hash: _integrityHash, ...core } = identity;
|
|
12411
|
+
const canonicalCore = JSON.parse(JSON.stringify(core));
|
|
12412
|
+
if (identity.integrity_hash !== canonicalJsonDigest(canonicalCore)) {
|
|
12413
|
+
throw new ProposalStoreError(
|
|
12414
|
+
"OPERATOR_IDENTITY_TAMPERED",
|
|
12415
|
+
"attention acknowledgement identity proof failed its integrity check"
|
|
9673
12416
|
);
|
|
9674
12417
|
}
|
|
9675
12418
|
}
|
|
9676
|
-
function
|
|
12419
|
+
function rowToAttentionEvent(row) {
|
|
9677
12420
|
if (!isRecord3(row)) return void 0;
|
|
9678
|
-
|
|
9679
|
-
|
|
9680
|
-
|
|
9681
|
-
|
|
9682
|
-
|
|
9683
|
-
|
|
9684
|
-
|
|
9685
|
-
|
|
9686
|
-
|
|
9687
|
-
|
|
9688
|
-
|
|
9689
|
-
|
|
9690
|
-
|
|
9691
|
-
|
|
9692
|
-
|
|
9693
|
-
|
|
9694
|
-
|
|
9695
|
-
|
|
9696
|
-
|
|
9697
|
-
|
|
9698
|
-
|
|
12421
|
+
const eventType = String(row.event_type);
|
|
12422
|
+
const severity = String(row.severity);
|
|
12423
|
+
if (!attentionEventTypes.has(eventType) || !["informational", "warning", "critical"].includes(severity)) return void 0;
|
|
12424
|
+
let details;
|
|
12425
|
+
try {
|
|
12426
|
+
const parsed = JSON.parse(String(row.details_json));
|
|
12427
|
+
if (!isRecord3(parsed)) return void 0;
|
|
12428
|
+
details = {};
|
|
12429
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
12430
|
+
if (value !== null && typeof value !== "string" && typeof value !== "number" && typeof value !== "boolean") return void 0;
|
|
12431
|
+
details[key] = value;
|
|
12432
|
+
}
|
|
12433
|
+
} catch {
|
|
12434
|
+
return void 0;
|
|
12435
|
+
}
|
|
12436
|
+
const event = {
|
|
12437
|
+
schema_version: "synapsor.attention-event.v1",
|
|
12438
|
+
event_id: String(row.event_id),
|
|
12439
|
+
event_type: eventType,
|
|
12440
|
+
severity,
|
|
12441
|
+
occurred_at: String(row.occurred_at),
|
|
12442
|
+
environment: String(row.environment),
|
|
12443
|
+
...row.proposal_id == null ? {} : { proposal_id: String(row.proposal_id) },
|
|
12444
|
+
...row.job_id == null ? {} : { job_id: String(row.job_id) },
|
|
12445
|
+
...row.operation_id == null ? {} : { operation_id: String(row.operation_id) },
|
|
12446
|
+
...row.correlation_id == null ? {} : { correlation_id: String(row.correlation_id) },
|
|
12447
|
+
...row.capability == null ? {} : { capability: String(row.capability) },
|
|
12448
|
+
...row.contract_digest == null ? {} : { contract_digest: String(row.contract_digest) },
|
|
12449
|
+
...row.attention_key == null ? {} : { attention_key: String(row.attention_key) },
|
|
12450
|
+
attention_required: Number(row.attention_required) === 1,
|
|
12451
|
+
immediate_default: Number(row.immediate_default) === 1,
|
|
12452
|
+
summary: String(row.summary),
|
|
12453
|
+
...row.approval_source === "human" || row.approval_source === "policy_auto" ? { approval_source: row.approval_source } : {},
|
|
12454
|
+
...row.worker_state == null ? {} : { worker_state: String(row.worker_state) },
|
|
12455
|
+
...row.failure_class == null ? {} : { failure_class: String(row.failure_class) },
|
|
12456
|
+
...row.expires_at == null ? {} : { expires_at: String(row.expires_at) },
|
|
12457
|
+
...row.workbench_path == null ? {} : { workbench_path: String(row.workbench_path) },
|
|
12458
|
+
details,
|
|
12459
|
+
payload_hash: String(row.payload_hash)
|
|
9699
12460
|
};
|
|
12461
|
+
const { payload_hash: _payloadHash, ...unsigned } = event;
|
|
12462
|
+
if (event.payload_hash !== canonicalJsonDigest(unsigned)) {
|
|
12463
|
+
throw new ProposalStoreError("ATTENTION_EVENT_CORRUPT", `attention event ${event.event_id} failed its integrity check`);
|
|
12464
|
+
}
|
|
12465
|
+
return event;
|
|
9700
12466
|
}
|
|
9701
|
-
function
|
|
12467
|
+
function rowToAttentionItem(row) {
|
|
9702
12468
|
if (!isRecord3(row)) return void 0;
|
|
12469
|
+
const status = String(row.status);
|
|
12470
|
+
const severity = String(row.severity);
|
|
12471
|
+
const eventType = String(row.event_type);
|
|
12472
|
+
if (!["open", "acknowledged", "resolved", "expired"].includes(status)) return void 0;
|
|
12473
|
+
if (!["informational", "warning", "critical"].includes(severity) || !attentionEventTypes.has(eventType)) return void 0;
|
|
12474
|
+
let acknowledgementIdentity;
|
|
12475
|
+
if (row.acknowledgement_identity_json != null) {
|
|
12476
|
+
try {
|
|
12477
|
+
const parsed = JSON.parse(String(row.acknowledgement_identity_json));
|
|
12478
|
+
if (!isRecord3(parsed) || !isRecord3(parsed.decision)) return void 0;
|
|
12479
|
+
acknowledgementIdentity = parsed;
|
|
12480
|
+
} catch {
|
|
12481
|
+
return void 0;
|
|
12482
|
+
}
|
|
12483
|
+
}
|
|
9703
12484
|
return {
|
|
9704
|
-
|
|
9705
|
-
|
|
9706
|
-
|
|
9707
|
-
|
|
9708
|
-
|
|
9709
|
-
|
|
12485
|
+
attention_id: String(row.attention_id),
|
|
12486
|
+
attention_key: String(row.attention_key),
|
|
12487
|
+
status,
|
|
12488
|
+
severity,
|
|
12489
|
+
environment: String(row.environment),
|
|
12490
|
+
event_type: eventType,
|
|
12491
|
+
capability: row.capability == null ? void 0 : String(row.capability),
|
|
12492
|
+
contract_digest: row.contract_digest == null ? void 0 : String(row.contract_digest),
|
|
12493
|
+
title: String(row.title),
|
|
12494
|
+
occurrence_count: Number(row.occurrence_count),
|
|
12495
|
+
first_event_id: String(row.first_event_id),
|
|
12496
|
+
latest_event_id: String(row.latest_event_id),
|
|
12497
|
+
first_seen_at: String(row.first_seen_at),
|
|
12498
|
+
last_seen_at: String(row.last_seen_at),
|
|
12499
|
+
acknowledged_by: row.acknowledged_by == null ? void 0 : String(row.acknowledged_by),
|
|
12500
|
+
acknowledged_at: row.acknowledged_at == null ? void 0 : String(row.acknowledged_at),
|
|
12501
|
+
acknowledgement_identity: acknowledgementIdentity,
|
|
12502
|
+
resolved_at: row.resolved_at == null ? void 0 : String(row.resolved_at),
|
|
12503
|
+
expires_at: row.expires_at == null ? void 0 : String(row.expires_at)
|
|
9710
12504
|
};
|
|
9711
12505
|
}
|
|
9712
|
-
function
|
|
12506
|
+
function rowToNotificationDelivery(row) {
|
|
9713
12507
|
if (!isRecord3(row)) return void 0;
|
|
9714
12508
|
const status = String(row.status);
|
|
9715
|
-
if (
|
|
12509
|
+
if (![
|
|
12510
|
+
"pending",
|
|
12511
|
+
"leased",
|
|
12512
|
+
"delivered",
|
|
12513
|
+
"retry_wait",
|
|
12514
|
+
"dead_letter",
|
|
12515
|
+
"suppressed",
|
|
12516
|
+
"batched"
|
|
12517
|
+
].includes(status)) return void 0;
|
|
9716
12518
|
return {
|
|
9717
|
-
|
|
9718
|
-
|
|
9719
|
-
|
|
9720
|
-
|
|
9721
|
-
approver: String(row.approver),
|
|
12519
|
+
delivery_id: String(row.delivery_id),
|
|
12520
|
+
sink_id: String(row.sink_id),
|
|
12521
|
+
event_id: String(row.event_id),
|
|
12522
|
+
...row.attention_id == null ? {} : { attention_id: String(row.attention_id) },
|
|
9722
12523
|
status,
|
|
9723
|
-
|
|
9724
|
-
|
|
9725
|
-
|
|
9726
|
-
|
|
9727
|
-
|
|
9728
|
-
|
|
9729
|
-
|
|
12524
|
+
attempts: Number(row.attempts),
|
|
12525
|
+
max_attempts: Number(row.max_attempts),
|
|
12526
|
+
next_attempt_at: String(row.next_attempt_at),
|
|
12527
|
+
...row.lease_owner == null ? {} : { lease_owner: String(row.lease_owner) },
|
|
12528
|
+
...row.lease_id == null ? {} : { lease_id: String(row.lease_id) },
|
|
12529
|
+
...row.lease_expires_at == null ? {} : { lease_expires_at: String(row.lease_expires_at) },
|
|
12530
|
+
...row.last_error_code == null ? {} : { last_error_code: String(row.last_error_code) },
|
|
12531
|
+
...row.external_reference == null ? {} : { external_reference: String(row.external_reference) },
|
|
12532
|
+
...row.delivered_at == null ? {} : { delivered_at: String(row.delivered_at) },
|
|
12533
|
+
created_at: String(row.created_at),
|
|
12534
|
+
updated_at: String(row.updated_at)
|
|
9730
12535
|
};
|
|
9731
12536
|
}
|
|
9732
|
-
function rowToPolicyRecommendation(row) {
|
|
9733
|
-
if (!isRecord3(row)) return void 0;
|
|
9734
|
-
let unsigned;
|
|
9735
|
-
try {
|
|
9736
|
-
unsigned = JSON.parse(String(row.payload_json));
|
|
9737
|
-
} catch {
|
|
9738
|
-
throw new ProposalStoreError("POLICY_RECOMMENDATION_TAMPERED", "policy recommendation payload is not valid JSON");
|
|
9739
|
-
}
|
|
9740
|
-
const recommendation = { ...unsigned, integrity_hash: String(row.integrity_hash) };
|
|
9741
|
-
assertPolicyRecommendationShape(recommendation);
|
|
9742
|
-
if (recommendation.recommendation_id !== String(row.recommendation_id) || recommendation.tenant_id !== String(row.tenant_id) || recommendation.capability !== String(row.capability) || recommendation.policy !== String(row.policy) || recommendation.base_contract_digest !== String(row.base_contract_digest) || recommendation.status !== String(row.status)) throw new ProposalStoreError("POLICY_RECOMMENDATION_TAMPERED", `policy recommendation ${String(row.recommendation_id)} index fields do not match its signed payload`);
|
|
9743
|
-
const expected = canonicalJsonDigest(policyRecommendationUnsigned(recommendation));
|
|
9744
|
-
if (recommendation.integrity_hash !== expected) throw new ProposalStoreError("POLICY_RECOMMENDATION_TAMPERED", `policy recommendation ${recommendation.recommendation_id} failed its integrity check`);
|
|
9745
|
-
return recommendation;
|
|
9746
|
-
}
|
|
9747
|
-
function policyRecommendationUnsigned(recommendation) {
|
|
9748
|
-
const { integrity_hash: _integrityHash, ...unsigned } = recommendation;
|
|
9749
|
-
return unsigned;
|
|
9750
|
-
}
|
|
9751
|
-
function assertPolicyRecommendationShape(recommendation) {
|
|
9752
|
-
if (recommendation.schema_version !== "synapsor.policy-recommendation.v1") throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "unsupported policy recommendation schema version");
|
|
9753
|
-
if (!/^ptr_[a-f0-9]{20}$/.test(recommendation.recommendation_id)) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation id is invalid");
|
|
9754
|
-
for (const [name, value] of [["tenant", recommendation.tenant_id], ["capability", recommendation.capability], ["policy", recommendation.policy], ["field", recommendation.field]]) {
|
|
9755
|
-
if (!value || value.length > 256) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", `policy recommendation ${name} is invalid`);
|
|
9756
|
-
}
|
|
9757
|
-
if (!/^sha256:[a-f0-9]{64}$/.test(recommendation.base_contract_digest)) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation base digest is invalid");
|
|
9758
|
-
if (!/^sha256:[a-f0-9]{64}$/.test(recommendation.integrity_hash)) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation integrity digest is invalid");
|
|
9759
|
-
if (!Number.isFinite(recommendation.current_threshold) || !Number.isFinite(recommendation.proposed_threshold) || !Number.isFinite(recommendation.maximum_increment) || !Number.isFinite(recommendation.absolute_ceiling)) {
|
|
9760
|
-
throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation thresholds must be finite numbers");
|
|
9761
|
-
}
|
|
9762
|
-
if (recommendation.proposed_threshold <= recommendation.current_threshold || recommendation.proposed_threshold - recommendation.current_threshold > recommendation.maximum_increment || recommendation.proposed_threshold > recommendation.absolute_ceiling) {
|
|
9763
|
-
throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation exceeds its reviewed increment or ceiling");
|
|
9764
|
-
}
|
|
9765
|
-
if (!Array.isArray(recommendation.evidence_proposal_ids) || recommendation.evidence_proposal_ids.length === 0 || recommendation.evidence_proposal_ids.length > 1e4) {
|
|
9766
|
-
throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation requires bounded proposal evidence");
|
|
9767
|
-
}
|
|
9768
|
-
if (!isRecord3(recommendation.metrics) || !isRecord3(recommendation.criteria)) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation criteria and metrics are required");
|
|
9769
|
-
if (!["pending_review", "approved", "rejected", "exported"].includes(recommendation.status)) throw new ProposalStoreError("POLICY_RECOMMENDATION_INVALID", "policy recommendation status is invalid");
|
|
9770
|
-
}
|
|
9771
|
-
function assertPolicyRecommendationIdentity(recommendation, input) {
|
|
9772
|
-
const proof = input.identity;
|
|
9773
|
-
if (!proof.verified || proof.provider === "dev_env") throw new ProposalStoreError("POLICY_RECOMMENDATION_VERIFIED_IDENTITY_REQUIRED", "policy recommendation decisions require a cryptographically verified operator identity");
|
|
9774
|
-
if (proof.subject !== input.actor || proof.decision.subject !== input.actor) throw new ProposalStoreError("POLICY_RECOMMENDATION_IDENTITY_MISMATCH", "policy recommendation actor does not match the verified identity");
|
|
9775
|
-
if (proof.decision.action !== input.action || proof.decision.proposal_id !== recommendation.recommendation_id || proof.decision.proposal_hash !== recommendation.integrity_hash || proof.decision.proposal_version !== 1) {
|
|
9776
|
-
throw new ProposalStoreError("POLICY_RECOMMENDATION_IDENTITY_MISMATCH", "verified operator decision is not bound to this policy recommendation version");
|
|
9777
|
-
}
|
|
9778
|
-
const { integrity_hash: _integrityHash, ...core } = proof;
|
|
9779
|
-
const canonicalCore = JSON.parse(JSON.stringify(core));
|
|
9780
|
-
if (proof.integrity_hash !== canonicalJsonDigest(canonicalCore)) throw new ProposalStoreError("POLICY_RECOMMENDATION_IDENTITY_TAMPERED", "verified operator identity proof failed its integrity check");
|
|
9781
|
-
}
|
|
9782
12537
|
function rowToWorkerQueueItem(row) {
|
|
9783
12538
|
if (!isRecord3(row)) return void 0;
|
|
9784
12539
|
const status = String(row.status);
|
|
9785
|
-
if (![
|
|
12540
|
+
if (![
|
|
12541
|
+
"queued",
|
|
12542
|
+
"leased",
|
|
12543
|
+
"retry_wait",
|
|
12544
|
+
"completed",
|
|
12545
|
+
"dead_letter",
|
|
12546
|
+
"discarded",
|
|
12547
|
+
"cancelled",
|
|
12548
|
+
"blocked",
|
|
12549
|
+
"reconciliation_required"
|
|
12550
|
+
].includes(status)) return void 0;
|
|
12551
|
+
const executionMode = row.execution_mode == null ? "legacy" : String(row.execution_mode);
|
|
12552
|
+
if (executionMode !== "legacy" && executionMode !== "supervised_worker") return void 0;
|
|
9786
12553
|
return {
|
|
9787
12554
|
proposal_id: String(row.proposal_id),
|
|
9788
12555
|
status,
|
|
12556
|
+
execution_mode: executionMode,
|
|
12557
|
+
contract_digest: row.contract_digest == null ? void 0 : String(row.contract_digest),
|
|
9789
12558
|
attempts: Number(row.attempts),
|
|
9790
12559
|
max_attempts: Number(row.max_attempts),
|
|
9791
12560
|
next_attempt_at: String(row.next_attempt_at),
|
|
9792
12561
|
lease_owner: row.lease_owner == null ? void 0 : String(row.lease_owner),
|
|
12562
|
+
lease_id: row.lease_id == null ? void 0 : String(row.lease_id),
|
|
9793
12563
|
lease_expires_at: row.lease_expires_at == null ? void 0 : String(row.lease_expires_at),
|
|
9794
12564
|
last_error_code: row.last_error_code == null ? void 0 : String(row.last_error_code),
|
|
12565
|
+
terminal_outcome: row.terminal_outcome == null ? void 0 : String(row.terminal_outcome),
|
|
9795
12566
|
created_at: String(row.created_at),
|
|
9796
12567
|
updated_at: String(row.updated_at)
|
|
9797
12568
|
};
|
|
9798
12569
|
}
|
|
12570
|
+
function workerLeaseId(proposalId, workerId, attempt, now) {
|
|
12571
|
+
const prefixLength = "sha256:".length;
|
|
12572
|
+
return `wlease_${canonicalJsonDigest({ proposal_id: proposalId, worker_id: workerId, attempt, now }).slice(prefixLength, prefixLength + 32)}`;
|
|
12573
|
+
}
|
|
9799
12574
|
function rowToCloudOutboxItem(row) {
|
|
9800
12575
|
if (!isRecord3(row)) return void 0;
|
|
9801
12576
|
const kind = String(row.kind);
|
|
@@ -9943,7 +12718,17 @@ function rowToWritebackIntent(row) {
|
|
|
9943
12718
|
};
|
|
9944
12719
|
}
|
|
9945
12720
|
function isStoredWritebackOperation(operation) {
|
|
9946
|
-
return [
|
|
12721
|
+
return [
|
|
12722
|
+
"single_row_update",
|
|
12723
|
+
"single_row_insert",
|
|
12724
|
+
"single_row_delete",
|
|
12725
|
+
"set_update",
|
|
12726
|
+
"set_delete",
|
|
12727
|
+
"batch_insert",
|
|
12728
|
+
"restore_update",
|
|
12729
|
+
"remove_insert",
|
|
12730
|
+
"restore_insert"
|
|
12731
|
+
].includes(operation);
|
|
9947
12732
|
}
|
|
9948
12733
|
function rowToQueryAudit(row) {
|
|
9949
12734
|
if (!isRecord3(row)) return void 0;
|
|
@@ -10054,7 +12839,9 @@ var sharedLedgerJsonColumns = /* @__PURE__ */ new Set([
|
|
|
10054
12839
|
"patch_json",
|
|
10055
12840
|
"selected_capabilities_json",
|
|
10056
12841
|
"proposed_effect_json",
|
|
10057
|
-
"actual_effect_json"
|
|
12842
|
+
"actual_effect_json",
|
|
12843
|
+
"details_json",
|
|
12844
|
+
"acknowledgement_identity_json"
|
|
10058
12845
|
]);
|
|
10059
12846
|
var sharedLedgerKindToTable = {
|
|
10060
12847
|
proposal: "proposals",
|
|
@@ -10073,6 +12860,9 @@ var sharedLedgerKindToTable = {
|
|
|
10073
12860
|
shadow_study_case: "shadow_study_cases",
|
|
10074
12861
|
shadow_outcome: "shadow_outcomes",
|
|
10075
12862
|
worker_queue_item: "worker_queue",
|
|
12863
|
+
attention_event: "attention_events",
|
|
12864
|
+
attention_item: "attention_items",
|
|
12865
|
+
notification_delivery: "notification_deliveries",
|
|
10076
12866
|
runner_state: "runner_state",
|
|
10077
12867
|
policy_recommendation: "policy_recommendations",
|
|
10078
12868
|
cloud_outbox_event: "cloud_outbox",
|
|
@@ -10115,7 +12905,142 @@ var sharedLedgerRestoreSpecs = {
|
|
|
10115
12905
|
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"]),
|
|
10116
12906
|
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"]),
|
|
10117
12907
|
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"]),
|
|
10118
|
-
worker_queue: restoreSpec(
|
|
12908
|
+
worker_queue: restoreSpec(
|
|
12909
|
+
"proposal_id",
|
|
12910
|
+
[
|
|
12911
|
+
"proposal_id",
|
|
12912
|
+
"status",
|
|
12913
|
+
"execution_mode",
|
|
12914
|
+
"contract_digest",
|
|
12915
|
+
"attempts",
|
|
12916
|
+
"max_attempts",
|
|
12917
|
+
"next_attempt_at",
|
|
12918
|
+
"lease_owner",
|
|
12919
|
+
"lease_id",
|
|
12920
|
+
"lease_expires_at",
|
|
12921
|
+
"last_error_code",
|
|
12922
|
+
"terminal_outcome",
|
|
12923
|
+
"created_at",
|
|
12924
|
+
"updated_at"
|
|
12925
|
+
],
|
|
12926
|
+
["proposal_id", "status", "attempts", "max_attempts", "next_attempt_at", "created_at", "updated_at"]
|
|
12927
|
+
),
|
|
12928
|
+
attention_events: restoreSpec(
|
|
12929
|
+
"event_id",
|
|
12930
|
+
[
|
|
12931
|
+
"event_id",
|
|
12932
|
+
"schema_version",
|
|
12933
|
+
"event_type",
|
|
12934
|
+
"severity",
|
|
12935
|
+
"occurred_at",
|
|
12936
|
+
"environment",
|
|
12937
|
+
"proposal_id",
|
|
12938
|
+
"job_id",
|
|
12939
|
+
"operation_id",
|
|
12940
|
+
"correlation_id",
|
|
12941
|
+
"capability",
|
|
12942
|
+
"contract_digest",
|
|
12943
|
+
"attention_key",
|
|
12944
|
+
"attention_required",
|
|
12945
|
+
"immediate_default",
|
|
12946
|
+
"summary",
|
|
12947
|
+
"approval_source",
|
|
12948
|
+
"worker_state",
|
|
12949
|
+
"failure_class",
|
|
12950
|
+
"expires_at",
|
|
12951
|
+
"workbench_path",
|
|
12952
|
+
"details_json",
|
|
12953
|
+
"payload_hash",
|
|
12954
|
+
"created_at"
|
|
12955
|
+
],
|
|
12956
|
+
[
|
|
12957
|
+
"event_id",
|
|
12958
|
+
"schema_version",
|
|
12959
|
+
"event_type",
|
|
12960
|
+
"severity",
|
|
12961
|
+
"occurred_at",
|
|
12962
|
+
"environment",
|
|
12963
|
+
"attention_required",
|
|
12964
|
+
"immediate_default",
|
|
12965
|
+
"summary",
|
|
12966
|
+
"details_json",
|
|
12967
|
+
"payload_hash",
|
|
12968
|
+
"created_at"
|
|
12969
|
+
]
|
|
12970
|
+
),
|
|
12971
|
+
attention_items: restoreSpec(
|
|
12972
|
+
"attention_id",
|
|
12973
|
+
[
|
|
12974
|
+
"attention_id",
|
|
12975
|
+
"attention_key",
|
|
12976
|
+
"status",
|
|
12977
|
+
"severity",
|
|
12978
|
+
"environment",
|
|
12979
|
+
"event_type",
|
|
12980
|
+
"capability",
|
|
12981
|
+
"contract_digest",
|
|
12982
|
+
"title",
|
|
12983
|
+
"occurrence_count",
|
|
12984
|
+
"first_event_id",
|
|
12985
|
+
"latest_event_id",
|
|
12986
|
+
"first_seen_at",
|
|
12987
|
+
"last_seen_at",
|
|
12988
|
+
"acknowledged_by",
|
|
12989
|
+
"acknowledged_at",
|
|
12990
|
+
"acknowledgement_identity_json",
|
|
12991
|
+
"acknowledgement_decision_hash",
|
|
12992
|
+
"acknowledgement_signature",
|
|
12993
|
+
"acknowledgement_integrity_hash",
|
|
12994
|
+
"resolved_at",
|
|
12995
|
+
"expires_at"
|
|
12996
|
+
],
|
|
12997
|
+
[
|
|
12998
|
+
"attention_id",
|
|
12999
|
+
"attention_key",
|
|
13000
|
+
"status",
|
|
13001
|
+
"severity",
|
|
13002
|
+
"environment",
|
|
13003
|
+
"event_type",
|
|
13004
|
+
"title",
|
|
13005
|
+
"occurrence_count",
|
|
13006
|
+
"first_event_id",
|
|
13007
|
+
"latest_event_id",
|
|
13008
|
+
"first_seen_at",
|
|
13009
|
+
"last_seen_at"
|
|
13010
|
+
]
|
|
13011
|
+
),
|
|
13012
|
+
notification_deliveries: restoreSpec(
|
|
13013
|
+
"delivery_id",
|
|
13014
|
+
[
|
|
13015
|
+
"delivery_id",
|
|
13016
|
+
"sink_id",
|
|
13017
|
+
"event_id",
|
|
13018
|
+
"attention_id",
|
|
13019
|
+
"status",
|
|
13020
|
+
"attempts",
|
|
13021
|
+
"max_attempts",
|
|
13022
|
+
"next_attempt_at",
|
|
13023
|
+
"lease_owner",
|
|
13024
|
+
"lease_id",
|
|
13025
|
+
"lease_expires_at",
|
|
13026
|
+
"last_error_code",
|
|
13027
|
+
"external_reference",
|
|
13028
|
+
"delivered_at",
|
|
13029
|
+
"created_at",
|
|
13030
|
+
"updated_at"
|
|
13031
|
+
],
|
|
13032
|
+
[
|
|
13033
|
+
"delivery_id",
|
|
13034
|
+
"sink_id",
|
|
13035
|
+
"event_id",
|
|
13036
|
+
"status",
|
|
13037
|
+
"attempts",
|
|
13038
|
+
"max_attempts",
|
|
13039
|
+
"next_attempt_at",
|
|
13040
|
+
"created_at",
|
|
13041
|
+
"updated_at"
|
|
13042
|
+
]
|
|
13043
|
+
),
|
|
10119
13044
|
runner_state: restoreSpec("key", ["key", "value_json", "updated_at"], ["key", "value_json", "updated_at"]),
|
|
10120
13045
|
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"]),
|
|
10121
13046
|
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"]),
|
|
@@ -10164,6 +13089,9 @@ function sharedLedgerRestoreRank(entry) {
|
|
|
10164
13089
|
"shadow_human_actions",
|
|
10165
13090
|
"shadow_outcomes",
|
|
10166
13091
|
"worker_queue",
|
|
13092
|
+
"attention_events",
|
|
13093
|
+
"attention_items",
|
|
13094
|
+
"notification_deliveries",
|
|
10167
13095
|
"runner_state",
|
|
10168
13096
|
"policy_recommendations",
|
|
10169
13097
|
"cloud_outbox",
|
|
@@ -10478,6 +13406,214 @@ function isRecord3(value) {
|
|
|
10478
13406
|
// packages/schema-inspector/src/index.ts
|
|
10479
13407
|
import mysql from "mysql2/promise";
|
|
10480
13408
|
import { Pool as Pool2 } from "pg";
|
|
13409
|
+
|
|
13410
|
+
// packages/schema-inspector/src/sensitivity.ts
|
|
13411
|
+
var HIGH_CONFIDENCE_RULES = [
|
|
13412
|
+
rule(
|
|
13413
|
+
"credential_or_secret",
|
|
13414
|
+
"The field name or description indicates credentials, authentication material, or a secret.",
|
|
13415
|
+
[
|
|
13416
|
+
"password",
|
|
13417
|
+
"passwordhash",
|
|
13418
|
+
"passwd",
|
|
13419
|
+
"passphrase",
|
|
13420
|
+
"secret",
|
|
13421
|
+
"token",
|
|
13422
|
+
"apikey",
|
|
13423
|
+
"accesskey",
|
|
13424
|
+
"privatekey",
|
|
13425
|
+
"credential",
|
|
13426
|
+
"oauth",
|
|
13427
|
+
"cookie",
|
|
13428
|
+
"sessiontoken"
|
|
13429
|
+
]
|
|
13430
|
+
),
|
|
13431
|
+
rule(
|
|
13432
|
+
"payment_or_bank_detail",
|
|
13433
|
+
"The field name or description indicates a payment instrument or bank detail.",
|
|
13434
|
+
[
|
|
13435
|
+
"paymentmethod",
|
|
13436
|
+
"paymentinstrument",
|
|
13437
|
+
"paymentdetails",
|
|
13438
|
+
"cardnumber",
|
|
13439
|
+
"creditcard",
|
|
13440
|
+
"debitcard",
|
|
13441
|
+
"cardtoken",
|
|
13442
|
+
"cvv",
|
|
13443
|
+
"cvc",
|
|
13444
|
+
"bankaccount",
|
|
13445
|
+
"accountnumber",
|
|
13446
|
+
"routingnumber",
|
|
13447
|
+
"iban",
|
|
13448
|
+
"swiftcode"
|
|
13449
|
+
]
|
|
13450
|
+
),
|
|
13451
|
+
rule(
|
|
13452
|
+
"government_identifier",
|
|
13453
|
+
"The field name or description indicates a government-issued or tax identifier.",
|
|
13454
|
+
[
|
|
13455
|
+
"ssn",
|
|
13456
|
+
"socialsecurity",
|
|
13457
|
+
"socialsecuritynumber",
|
|
13458
|
+
"taxid",
|
|
13459
|
+
"nationalid",
|
|
13460
|
+
"passport",
|
|
13461
|
+
"passportnumber",
|
|
13462
|
+
"driverslicense",
|
|
13463
|
+
"driverlicense"
|
|
13464
|
+
]
|
|
13465
|
+
),
|
|
13466
|
+
rule(
|
|
13467
|
+
"birth_information",
|
|
13468
|
+
"The field name or description indicates a date of birth or birth information.",
|
|
13469
|
+
["dateofbirth", "birthdate", "dob"]
|
|
13470
|
+
),
|
|
13471
|
+
rule(
|
|
13472
|
+
"medical_or_health_information",
|
|
13473
|
+
"The field name or description indicates medical or health information.",
|
|
13474
|
+
[
|
|
13475
|
+
"medical",
|
|
13476
|
+
"health",
|
|
13477
|
+
"diagnosis",
|
|
13478
|
+
"treatment",
|
|
13479
|
+
"medication",
|
|
13480
|
+
"prescription",
|
|
13481
|
+
"allergy",
|
|
13482
|
+
"clinical",
|
|
13483
|
+
"patientnote",
|
|
13484
|
+
"waivernote"
|
|
13485
|
+
]
|
|
13486
|
+
),
|
|
13487
|
+
rule(
|
|
13488
|
+
"direct_contact_or_address",
|
|
13489
|
+
"The field name or description indicates direct contact or address information.",
|
|
13490
|
+
[
|
|
13491
|
+
"email",
|
|
13492
|
+
"emailaddress",
|
|
13493
|
+
"phone",
|
|
13494
|
+
"phonenumber",
|
|
13495
|
+
"telephone",
|
|
13496
|
+
"streetaddress",
|
|
13497
|
+
"homeaddress",
|
|
13498
|
+
"mailingaddress",
|
|
13499
|
+
"postalcode",
|
|
13500
|
+
"zipcode"
|
|
13501
|
+
]
|
|
13502
|
+
),
|
|
13503
|
+
rule(
|
|
13504
|
+
"biometric_or_precise_location",
|
|
13505
|
+
"The field name or description indicates biometric or precise-location information.",
|
|
13506
|
+
[
|
|
13507
|
+
"biometric",
|
|
13508
|
+
"fingerprint",
|
|
13509
|
+
"faceprint",
|
|
13510
|
+
"voiceprint",
|
|
13511
|
+
"geolocation",
|
|
13512
|
+
"gpscoordinate",
|
|
13513
|
+
"latitude",
|
|
13514
|
+
"longitude",
|
|
13515
|
+
"preciselocation"
|
|
13516
|
+
]
|
|
13517
|
+
),
|
|
13518
|
+
rule(
|
|
13519
|
+
"private_or_risk_information",
|
|
13520
|
+
"The field name or description indicates private notes, internal risk data, or a risk score.",
|
|
13521
|
+
["privatenote", "privatedata", "internalrisk", "riskscore"]
|
|
13522
|
+
)
|
|
13523
|
+
];
|
|
13524
|
+
var UNRESOLVED_NAME_TOKENS = /* @__PURE__ */ new Set([
|
|
13525
|
+
"note",
|
|
13526
|
+
"notes",
|
|
13527
|
+
"comment",
|
|
13528
|
+
"comments",
|
|
13529
|
+
"description",
|
|
13530
|
+
"memo",
|
|
13531
|
+
"message",
|
|
13532
|
+
"payload",
|
|
13533
|
+
"body",
|
|
13534
|
+
"content",
|
|
13535
|
+
"details",
|
|
13536
|
+
"remarks",
|
|
13537
|
+
"metadata",
|
|
13538
|
+
"freeform"
|
|
13539
|
+
]);
|
|
13540
|
+
var UNRESOLVED_DATA_TYPES = /* @__PURE__ */ new Set(["json", "jsonb", "xml", "object", "array"]);
|
|
13541
|
+
function classifySensitivity(input) {
|
|
13542
|
+
const evidence = normalizeEvidence(input);
|
|
13543
|
+
const matched = HIGH_CONFIDENCE_RULES.filter((candidate) => candidate.matches(evidence));
|
|
13544
|
+
if (input.writeOnly) {
|
|
13545
|
+
matched.push({
|
|
13546
|
+
code: "write_only_input",
|
|
13547
|
+
reason: "The source marks this field as write-only, so it must not become model-visible.",
|
|
13548
|
+
matches: () => true
|
|
13549
|
+
});
|
|
13550
|
+
}
|
|
13551
|
+
if (matched.length > 0) {
|
|
13552
|
+
return classification(
|
|
13553
|
+
"high_confidence_sensitive",
|
|
13554
|
+
matched.map((item) => item.code),
|
|
13555
|
+
matched.map((item) => item.reason),
|
|
13556
|
+
input.source
|
|
13557
|
+
);
|
|
13558
|
+
}
|
|
13559
|
+
const unresolved = [];
|
|
13560
|
+
if ([...evidence.tokens].some((token) => UNRESOLVED_NAME_TOKENS.has(token))) {
|
|
13561
|
+
unresolved.push({
|
|
13562
|
+
code: "unconstrained_free_text_name",
|
|
13563
|
+
reason: "The field name indicates unconstrained notes, comments, descriptions, or payload content."
|
|
13564
|
+
});
|
|
13565
|
+
}
|
|
13566
|
+
if (UNRESOLVED_DATA_TYPES.has(evidence.dataType)) {
|
|
13567
|
+
unresolved.push({
|
|
13568
|
+
code: "unstructured_data_type",
|
|
13569
|
+
reason: `The ${evidence.dataType} data type can contain unreviewed nested or free-form content.`
|
|
13570
|
+
});
|
|
13571
|
+
}
|
|
13572
|
+
if (unresolved.length > 0) {
|
|
13573
|
+
return classification(
|
|
13574
|
+
"unresolved_free_text",
|
|
13575
|
+
unresolved.map((item) => item.code),
|
|
13576
|
+
unresolved.map((item) => item.reason),
|
|
13577
|
+
input.source
|
|
13578
|
+
);
|
|
13579
|
+
}
|
|
13580
|
+
return classification(
|
|
13581
|
+
"structurally_low_risk",
|
|
13582
|
+
["no_sensitive_structural_signal"],
|
|
13583
|
+
["No deterministic sensitive or unconstrained free-text signal was found in the available structural metadata."],
|
|
13584
|
+
input.source
|
|
13585
|
+
);
|
|
13586
|
+
}
|
|
13587
|
+
function classification(state, reasonCodes, reasons, evidenceSource) {
|
|
13588
|
+
return {
|
|
13589
|
+
state,
|
|
13590
|
+
reason_codes: [...new Set(reasonCodes)].sort(),
|
|
13591
|
+
reasons: [...new Set(reasons)].sort(),
|
|
13592
|
+
evidence_source: evidenceSource
|
|
13593
|
+
};
|
|
13594
|
+
}
|
|
13595
|
+
function rule(code, reason, patterns) {
|
|
13596
|
+
return {
|
|
13597
|
+
code,
|
|
13598
|
+
reason,
|
|
13599
|
+
matches: (value) => patterns.some((pattern) => value.compact.includes(pattern) || value.descriptionCompact.includes(pattern))
|
|
13600
|
+
};
|
|
13601
|
+
}
|
|
13602
|
+
function normalizeEvidence(input) {
|
|
13603
|
+
const identifier = splitIdentifier(input.name);
|
|
13604
|
+
const description = splitIdentifier(input.description ?? "");
|
|
13605
|
+
return {
|
|
13606
|
+
compact: identifier.replaceAll("_", ""),
|
|
13607
|
+
tokens: new Set(identifier.split("_").filter(Boolean)),
|
|
13608
|
+
descriptionCompact: description.replaceAll("_", ""),
|
|
13609
|
+
dataType: String(input.dataType ?? "").trim().toLowerCase()
|
|
13610
|
+
};
|
|
13611
|
+
}
|
|
13612
|
+
function splitIdentifier(value) {
|
|
13613
|
+
return value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2").toLowerCase().replace(/[^a-z0-9]+/g, "_").replace(/^_+|_+$/g, "");
|
|
13614
|
+
}
|
|
13615
|
+
|
|
13616
|
+
// packages/schema-inspector/src/index.ts
|
|
10481
13617
|
function schemaFingerprintForInspection(inspection) {
|
|
10482
13618
|
return canonicalJsonDigest({
|
|
10483
13619
|
engine: inspection.engine,
|
|
@@ -10536,30 +13672,6 @@ function rolePostureFingerprint(inspection) {
|
|
|
10536
13672
|
var TENANT_COLUMNS = /* @__PURE__ */ new Set(["tenant_id", "account_id", "organization_id", "org_id", "workspace_id", "customer_id"]);
|
|
10537
13673
|
var CONFLICT_COLUMNS = /* @__PURE__ */ new Set(["updated_at", "modified_at", "row_version", "version", "lock_version", "etag"]);
|
|
10538
13674
|
var IMMUTABLE_COLUMNS = /* @__PURE__ */ new Set(["id", "uuid", "created_at", "created_by"]);
|
|
10539
|
-
var SENSITIVE_PATTERNS = [
|
|
10540
|
-
/password/i,
|
|
10541
|
-
/password_hash/i,
|
|
10542
|
-
/secret/i,
|
|
10543
|
-
/token/i,
|
|
10544
|
-
/api[_-]?key/i,
|
|
10545
|
-
/access[_-]?key/i,
|
|
10546
|
-
/private[_-]?key/i,
|
|
10547
|
-
/session/i,
|
|
10548
|
-
/cookie/i,
|
|
10549
|
-
/\bssn\b/i,
|
|
10550
|
-
/social[_-]?security/i,
|
|
10551
|
-
/credit[_-]?card/i,
|
|
10552
|
-
/card[_-]?number/i,
|
|
10553
|
-
/\bcvv\b/i,
|
|
10554
|
-
/refresh[_-]?token/i,
|
|
10555
|
-
/oauth/i,
|
|
10556
|
-
/(?:^|[_-])email(?:$|[_-])/i,
|
|
10557
|
-
/(?:^|[_-])phone(?:$|[_-])/i,
|
|
10558
|
-
/(?:^|[_-])(?:street_)?address(?:$|[_-])/i,
|
|
10559
|
-
/(?:^|[_-])(?:date_of_birth|birth_date|dob)(?:$|[_-])/i,
|
|
10560
|
-
/(?:^|[_-])risk[_-]?score(?:$|[_-])/i,
|
|
10561
|
-
/(?:^|[_-])private[_-]?(?:note|notes|data)(?:$|[_-])/i
|
|
10562
|
-
];
|
|
10563
13675
|
var LARGE_OR_BINARY_TYPES = /* @__PURE__ */ new Set([
|
|
10564
13676
|
"bytea",
|
|
10565
13677
|
"blob",
|
|
@@ -10877,20 +13989,26 @@ async function inspectMysql(options) {
|
|
|
10877
13989
|
[schemaParam, schemaParam]
|
|
10878
13990
|
);
|
|
10879
13991
|
const [grantRows] = await connection.query("SHOW GRANTS FOR CURRENT_USER");
|
|
10880
|
-
const
|
|
13992
|
+
const mysqlGrants = mysqlGrantPosture(
|
|
13993
|
+
grantRows.map((row) => String(Object.values(row)[0] ?? "")),
|
|
13994
|
+
tableRows.map((row) => ({
|
|
13995
|
+
schema: String(row.schema),
|
|
13996
|
+
table: String(row.name)
|
|
13997
|
+
}))
|
|
13998
|
+
);
|
|
10881
13999
|
const relationRolePosture = tableRows.map((row) => ({
|
|
10882
14000
|
schema: String(row.schema),
|
|
10883
14001
|
table_name: String(row.name),
|
|
10884
14002
|
owner: "",
|
|
10885
14003
|
current_role_is_owner: false,
|
|
10886
14004
|
current_role_can_assume_owner: false,
|
|
10887
|
-
can_select:
|
|
10888
|
-
can_insert:
|
|
10889
|
-
can_update:
|
|
10890
|
-
can_delete:
|
|
10891
|
-
can_truncate:
|
|
10892
|
-
can_references:
|
|
10893
|
-
can_trigger:
|
|
14005
|
+
can_select: mysqlGrants.relations[`${String(row.schema)}.${String(row.name)}`]?.select ?? false,
|
|
14006
|
+
can_insert: mysqlGrants.relations[`${String(row.schema)}.${String(row.name)}`]?.insert ?? false,
|
|
14007
|
+
can_update: mysqlGrants.relations[`${String(row.schema)}.${String(row.name)}`]?.update ?? false,
|
|
14008
|
+
can_delete: mysqlGrants.relations[`${String(row.schema)}.${String(row.name)}`]?.delete ?? false,
|
|
14009
|
+
can_truncate: mysqlGrants.relations[`${String(row.schema)}.${String(row.name)}`]?.truncate ?? false,
|
|
14010
|
+
can_references: mysqlGrants.relations[`${String(row.schema)}.${String(row.name)}`]?.references ?? false,
|
|
14011
|
+
can_trigger: mysqlGrants.relations[`${String(row.schema)}.${String(row.name)}`]?.trigger ?? false,
|
|
10894
14012
|
row_security_forced: false
|
|
10895
14013
|
}));
|
|
10896
14014
|
await connection.query("COMMIT").catch(() => void 0);
|
|
@@ -10899,8 +14017,8 @@ async function inspectMysql(options) {
|
|
|
10899
14017
|
server_version: String(versionRows[0]?.version ?? "unknown"),
|
|
10900
14018
|
current_user: String(versionRows[0]?.current_user ?? "unknown"),
|
|
10901
14019
|
role: {
|
|
10902
|
-
verified:
|
|
10903
|
-
superuser: "unsupported",
|
|
14020
|
+
verified: mysqlGrants.verified,
|
|
14021
|
+
superuser: mysqlGrants.elevated ? true : "unsupported",
|
|
10904
14022
|
bypass_rls: "unsupported"
|
|
10905
14023
|
},
|
|
10906
14024
|
schemas: schemaRows.map((row) => String(row.schema_name)),
|
|
@@ -11028,6 +14146,12 @@ function normalizeColumn(column, primaryKey) {
|
|
|
11028
14146
|
const name = String(column.name);
|
|
11029
14147
|
const lower = name.toLowerCase();
|
|
11030
14148
|
const type = String(column.udt_name || column.data_type || "unknown").toLowerCase();
|
|
14149
|
+
const sensitivity = classifySensitivity({
|
|
14150
|
+
name,
|
|
14151
|
+
dataType: String(column.data_type || column.udt_name || "unknown"),
|
|
14152
|
+
description: column.comment ?? void 0,
|
|
14153
|
+
source: "database"
|
|
14154
|
+
});
|
|
11031
14155
|
return {
|
|
11032
14156
|
name,
|
|
11033
14157
|
data_type: String(column.data_type || column.udt_name || "unknown"),
|
|
@@ -11041,7 +14165,8 @@ function normalizeColumn(column, primaryKey) {
|
|
|
11041
14165
|
suggestions: {
|
|
11042
14166
|
tenant: TENANT_COLUMNS.has(lower),
|
|
11043
14167
|
conflict: CONFLICT_COLUMNS.has(lower),
|
|
11044
|
-
sensitive:
|
|
14168
|
+
sensitive: sensitivity.state !== "structurally_low_risk",
|
|
14169
|
+
sensitivity,
|
|
11045
14170
|
immutable: primaryKey.includes(name) || TENANT_COLUMNS.has(lower) || IMMUTABLE_COLUMNS.has(lower),
|
|
11046
14171
|
large_or_binary: LARGE_OR_BINARY_TYPES.has(type) || /blob|binary|bytea|vector/i.test(type)
|
|
11047
14172
|
}
|
|
@@ -11073,13 +14198,139 @@ function relationIsWriteCapable(table) {
|
|
|
11073
14198
|
if (!privileges) return true;
|
|
11074
14199
|
return privileges.insert || privileges.update || privileges.delete || privileges.truncate || privileges.trigger;
|
|
11075
14200
|
}
|
|
11076
|
-
function
|
|
11077
|
-
const
|
|
11078
|
-
|
|
11079
|
-
|
|
11080
|
-
|
|
11081
|
-
|
|
11082
|
-
|
|
14201
|
+
function mysqlGrantPosture(grants, relations) {
|
|
14202
|
+
const parsed = [];
|
|
14203
|
+
let verified = true;
|
|
14204
|
+
let elevated = false;
|
|
14205
|
+
for (const raw of grants) {
|
|
14206
|
+
const grant = raw.trim();
|
|
14207
|
+
const match = grant.match(/^GRANT\s+(.+?)\s+ON\s+(.+?)\s+TO\s+/i);
|
|
14208
|
+
if (!match) {
|
|
14209
|
+
if (/^GRANT\s+/i.test(grant)) verified = false;
|
|
14210
|
+
continue;
|
|
14211
|
+
}
|
|
14212
|
+
const target = parseMysqlGrantTarget(match[2]);
|
|
14213
|
+
const privileges = parseMysqlGrantPrivileges(match[1]);
|
|
14214
|
+
if (!target || !privileges.verified) {
|
|
14215
|
+
verified = false;
|
|
14216
|
+
continue;
|
|
14217
|
+
}
|
|
14218
|
+
elevated ||= privileges.elevated || /\bWITH\s+GRANT\s+OPTION\b/i.test(grant);
|
|
14219
|
+
parsed.push({
|
|
14220
|
+
...target,
|
|
14221
|
+
privileges: privileges.relation
|
|
14222
|
+
});
|
|
14223
|
+
}
|
|
14224
|
+
const relationMap = {};
|
|
14225
|
+
for (const relation of relations) {
|
|
14226
|
+
const posture = {
|
|
14227
|
+
select: false,
|
|
14228
|
+
insert: false,
|
|
14229
|
+
update: false,
|
|
14230
|
+
delete: false,
|
|
14231
|
+
truncate: false,
|
|
14232
|
+
references: false,
|
|
14233
|
+
trigger: false
|
|
14234
|
+
};
|
|
14235
|
+
for (const grant of parsed) {
|
|
14236
|
+
if ((grant.schema === "*" || grant.schema === relation.schema) && (grant.table === "*" || grant.table === relation.table)) {
|
|
14237
|
+
for (const privilege of Object.keys(posture)) {
|
|
14238
|
+
posture[privilege] ||= grant.privileges[privilege];
|
|
14239
|
+
}
|
|
14240
|
+
}
|
|
14241
|
+
}
|
|
14242
|
+
relationMap[`${relation.schema}.${relation.table}`] = posture;
|
|
14243
|
+
}
|
|
14244
|
+
return { verified, elevated, relations: relationMap };
|
|
14245
|
+
}
|
|
14246
|
+
function parseMysqlGrantTarget(value) {
|
|
14247
|
+
const identifier = String.raw`(?:\*|` + "`(?:``|[^`])+`" + String.raw`|[A-Za-z0-9_$-]+)`;
|
|
14248
|
+
const match = value.trim().match(new RegExp(`^(${identifier})\\s*\\.\\s*(${identifier})$`));
|
|
14249
|
+
if (!match) return void 0;
|
|
14250
|
+
return {
|
|
14251
|
+
schema: unquoteMysqlGrantIdentifier(match[1]),
|
|
14252
|
+
table: unquoteMysqlGrantIdentifier(match[2])
|
|
14253
|
+
};
|
|
14254
|
+
}
|
|
14255
|
+
function unquoteMysqlGrantIdentifier(value) {
|
|
14256
|
+
if (value === "*") return value;
|
|
14257
|
+
return value.startsWith("`") && value.endsWith("`") ? value.slice(1, -1).replaceAll("``", "`") : value;
|
|
14258
|
+
}
|
|
14259
|
+
function parseMysqlGrantPrivileges(value) {
|
|
14260
|
+
const relation = {
|
|
14261
|
+
select: false,
|
|
14262
|
+
insert: false,
|
|
14263
|
+
update: false,
|
|
14264
|
+
delete: false,
|
|
14265
|
+
truncate: false,
|
|
14266
|
+
references: false,
|
|
14267
|
+
trigger: false
|
|
14268
|
+
};
|
|
14269
|
+
const normalized = value.replace(/\([^)]*\)/g, "").split(",").map((entry) => entry.trim().replace(/\s+/g, " ").toUpperCase()).filter(Boolean);
|
|
14270
|
+
const allowed = /* @__PURE__ */ new Set([
|
|
14271
|
+
"USAGE",
|
|
14272
|
+
"SELECT",
|
|
14273
|
+
"INSERT",
|
|
14274
|
+
"UPDATE",
|
|
14275
|
+
"DELETE",
|
|
14276
|
+
"REFERENCES",
|
|
14277
|
+
"TRIGGER",
|
|
14278
|
+
"ALL",
|
|
14279
|
+
"ALL PRIVILEGES"
|
|
14280
|
+
]);
|
|
14281
|
+
const elevatedPrivileges = /* @__PURE__ */ new Set([
|
|
14282
|
+
"ALTER",
|
|
14283
|
+
"ALTER ROUTINE",
|
|
14284
|
+
"CREATE",
|
|
14285
|
+
"CREATE ROLE",
|
|
14286
|
+
"CREATE ROUTINE",
|
|
14287
|
+
"CREATE TABLESPACE",
|
|
14288
|
+
"CREATE TEMPORARY TABLES",
|
|
14289
|
+
"CREATE USER",
|
|
14290
|
+
"CREATE VIEW",
|
|
14291
|
+
"DROP",
|
|
14292
|
+
"EVENT",
|
|
14293
|
+
"EXECUTE",
|
|
14294
|
+
"FILE",
|
|
14295
|
+
"GRANT OPTION",
|
|
14296
|
+
"INDEX",
|
|
14297
|
+
"LOCK TABLES",
|
|
14298
|
+
"PROCESS",
|
|
14299
|
+
"RELOAD",
|
|
14300
|
+
"REPLICATION CLIENT",
|
|
14301
|
+
"REPLICATION SLAVE",
|
|
14302
|
+
"SHOW DATABASES",
|
|
14303
|
+
"SHOW VIEW",
|
|
14304
|
+
"SHUTDOWN",
|
|
14305
|
+
"SUPER",
|
|
14306
|
+
"SYSTEM_USER"
|
|
14307
|
+
]);
|
|
14308
|
+
let verified = true;
|
|
14309
|
+
let elevated = false;
|
|
14310
|
+
for (const privilege of normalized) {
|
|
14311
|
+
const all = privilege === "ALL" || privilege === "ALL PRIVILEGES";
|
|
14312
|
+
if (all) {
|
|
14313
|
+
relation.select = true;
|
|
14314
|
+
relation.insert = true;
|
|
14315
|
+
relation.update = true;
|
|
14316
|
+
relation.delete = true;
|
|
14317
|
+
relation.truncate = true;
|
|
14318
|
+
relation.references = true;
|
|
14319
|
+
relation.trigger = true;
|
|
14320
|
+
elevated = true;
|
|
14321
|
+
continue;
|
|
14322
|
+
}
|
|
14323
|
+
if (privilege === "SELECT") relation.select = true;
|
|
14324
|
+
else if (privilege === "INSERT") relation.insert = true;
|
|
14325
|
+
else if (privilege === "UPDATE") relation.update = true;
|
|
14326
|
+
else if (privilege === "DELETE") relation.delete = true;
|
|
14327
|
+
else if (privilege === "REFERENCES") relation.references = true;
|
|
14328
|
+
else if (privilege === "TRIGGER") relation.trigger = true;
|
|
14329
|
+
else if (privilege === "DROP") relation.truncate = true;
|
|
14330
|
+
if (elevatedPrivileges.has(privilege)) elevated = true;
|
|
14331
|
+
else if (!allowed.has(privilege)) verified = false;
|
|
14332
|
+
}
|
|
14333
|
+
return { verified, elevated, relation };
|
|
11083
14334
|
}
|
|
11084
14335
|
function constraintColumns(rows, kind) {
|
|
11085
14336
|
const grouped = groupBy(rows.filter((row) => row.constraint_type === kind), (row) => row.constraint_name);
|
|
@@ -13204,6 +16455,51 @@ function asRecord(value) {
|
|
|
13204
16455
|
}
|
|
13205
16456
|
|
|
13206
16457
|
// packages/mcp-server/src/index.ts
|
|
16458
|
+
function resolveSupervisedWorkerEligibility(config, capability, options = {}) {
|
|
16459
|
+
const reasons = [];
|
|
16460
|
+
const deployment = config.supervised_worker;
|
|
16461
|
+
const digest = capability.contract_provenance?.digest;
|
|
16462
|
+
if (!deployment?.enabled) reasons.push("deployment_disabled");
|
|
16463
|
+
if (config.mode !== "review") reasons.push("review_mode_required");
|
|
16464
|
+
if (config.governance?.mode === "cloud_linked") reasons.push("cloud_linked_local_execution_forbidden");
|
|
16465
|
+
if (capability.kind !== "proposal") reasons.push("proposal_capability_required");
|
|
16466
|
+
if (capability.execution?.supervised_worker !== "allowed") reasons.push("contract_permission_missing");
|
|
16467
|
+
if (!digest) reasons.push("active_contract_digest_missing");
|
|
16468
|
+
const policy = digest ? deployment?.capabilities.find((entry) => entry.capability === capability.name && entry.contract_digest === digest && entry.mode === "supervised_worker") : void 0;
|
|
16469
|
+
if (!policy) reasons.push("deployment_allowlist_mismatch");
|
|
16470
|
+
if (options.phase !== "queue" && policy?.worker_identity && policy.worker_identity !== options.workerIdentity) {
|
|
16471
|
+
reasons.push("worker_identity_mismatch");
|
|
16472
|
+
}
|
|
16473
|
+
const source = config.sources?.[capability.source];
|
|
16474
|
+
if (!source || source.read_only === true || !source.write_url_env) {
|
|
16475
|
+
reasons.push("writable_source_unavailable");
|
|
16476
|
+
} else {
|
|
16477
|
+
if (policy && policy.write_url_env !== source.write_url_env) reasons.push("writer_reference_mismatch");
|
|
16478
|
+
if (!source.receipts) reasons.push("receipt_authority_missing");
|
|
16479
|
+
}
|
|
16480
|
+
if (!capability.target.tenant_key) reasons.push("trusted_tenant_scope_missing");
|
|
16481
|
+
if (capability.writeback?.mode !== "direct_sql") reasons.push("direct_sql_required");
|
|
16482
|
+
if ((capability.operation?.cardinality ?? "single") !== "single") reasons.push("single_row_required");
|
|
16483
|
+
const operation = capability.operation?.kind ?? "update";
|
|
16484
|
+
if (operation === "delete") reasons.push("delete_ineligible");
|
|
16485
|
+
if (capability.reversibility) reasons.push("reversibility_ineligible");
|
|
16486
|
+
if (operation === "update" && (!capability.conflict_guard?.column || capability.conflict_guard.weak_guard_ack === true)) {
|
|
16487
|
+
reasons.push("exact_conflict_guard_required");
|
|
16488
|
+
}
|
|
16489
|
+
if (operation === "insert" && !capability.operation?.deduplication?.components.length) {
|
|
16490
|
+
reasons.push("insert_deduplication_required");
|
|
16491
|
+
}
|
|
16492
|
+
const uniqueReasons = [...new Set(reasons)];
|
|
16493
|
+
return {
|
|
16494
|
+
eligible: uniqueReasons.length === 0,
|
|
16495
|
+
code: uniqueReasons.length === 0 ? "SUPERVISED_WORKER_ELIGIBLE" : uniqueReasons[0].toUpperCase(),
|
|
16496
|
+
reasons: uniqueReasons,
|
|
16497
|
+
capability: capability.name,
|
|
16498
|
+
...digest ? { contract_digest: digest } : {},
|
|
16499
|
+
...deployment ? { profile: deployment.profile } : {},
|
|
16500
|
+
...policy ? { policy } : {}
|
|
16501
|
+
};
|
|
16502
|
+
}
|
|
13207
16503
|
function describeIsolationAssurance(config) {
|
|
13208
16504
|
return Object.entries(config.sources ?? {}).sort(([left], [right]) => left.localeCompare(right)).map(([sourceName, source]) => {
|
|
13209
16505
|
const providers = trustedContextProvidersForSource(config, sourceName);
|
|
@@ -13744,8 +17040,8 @@ var CloudLinkedSynchronizer = class {
|
|
|
13744
17040
|
acknowledged += 1;
|
|
13745
17041
|
} catch (error) {
|
|
13746
17042
|
failed += 1;
|
|
13747
|
-
const
|
|
13748
|
-
await this.store.failCloudOutbox({ event_id: item.event_id, owner: this.owner, ...
|
|
17043
|
+
const classification2 = classifyCloudSyncFailure(error);
|
|
17044
|
+
await this.store.failCloudOutbox({ event_id: item.event_id, owner: this.owner, ...classification2 });
|
|
13749
17045
|
}
|
|
13750
17046
|
}
|
|
13751
17047
|
await this.reconcileOnce().catch((error) => {
|
|
@@ -13979,6 +17275,7 @@ function runtimeCapabilityFromSpec(capability, resources, config, provenance) {
|
|
|
13979
17275
|
runtime.operation = capability.proposal.operation;
|
|
13980
17276
|
runtime.conflict_guard = capability.proposal.conflict_guard;
|
|
13981
17277
|
runtime.approval = capability.proposal.approval;
|
|
17278
|
+
runtime.execution = capability.proposal.execution;
|
|
13982
17279
|
runtime.writeback = {
|
|
13983
17280
|
mode: capability.proposal.writeback?.mode ?? "direct_sql",
|
|
13984
17281
|
...capability.proposal.writeback?.executor ? { executor: capability.proposal.writeback.executor } : {}
|
|
@@ -14024,7 +17321,7 @@ function createMcpRuntime(config, options = {}) {
|
|
|
14024
17321
|
return {
|
|
14025
17322
|
config,
|
|
14026
17323
|
store,
|
|
14027
|
-
listTools: () => config.mode === "cloud" ? cloudTools : listedLocalCapabilities(config).map((capability) => toolMetadata(capability)),
|
|
17324
|
+
listTools: () => config.mode === "cloud" ? cloudTools : listedLocalCapabilities(config).map((capability) => toolMetadata(capability, config)),
|
|
14028
17325
|
callTool: async (name, args) => {
|
|
14029
17326
|
const capability = config.mode === "cloud" ? void 0 : localCapabilities(config).find((item) => item.name === name);
|
|
14030
17327
|
try {
|
|
@@ -14133,6 +17430,8 @@ async function preflightPostgresDatabaseScope(inputConfig, env = process.env, cr
|
|
|
14133
17430
|
}
|
|
14134
17431
|
}
|
|
14135
17432
|
}
|
|
17433
|
+
var SUPPORTED_GENERATED_AUTHORITY_COMPILER_VERSIONS = /* @__PURE__ */ new Set(["1.6.0", "1.6.3"]);
|
|
17434
|
+
var SUPPORTED_GENERATED_AUTHORITY_SPEC_VERSIONS = /* @__PURE__ */ new Set(["1.5.0", "1.5.1", "1.6.0"]);
|
|
14136
17435
|
async function preflightGeneratedAuthority(inputConfig, env = process.env, inspect = inspectDatabase) {
|
|
14137
17436
|
const config = resolveRuntimeConfig(inputConfig);
|
|
14138
17437
|
const protectedCapabilities = localCapabilities(config).filter((capability) => capability.protected_read);
|
|
@@ -14198,7 +17497,7 @@ async function preflightGeneratedAuthority(inputConfig, env = process.env, inspe
|
|
|
14198
17497
|
}
|
|
14199
17498
|
function assertGeneratedAuthorityLockShape(value) {
|
|
14200
17499
|
const digest = /^sha256:[a-f0-9]{64}$/;
|
|
14201
|
-
if (!value || value.schema_version !== "synapsor.generation-lock.v1" || value.compiler_version
|
|
17500
|
+
if (!value || value.schema_version !== "synapsor.generation-lock.v1" || !SUPPORTED_GENERATED_AUTHORITY_COMPILER_VERSIONS.has(value.compiler_version) || !SUPPORTED_GENERATED_AUTHORITY_SPEC_VERSIONS.has(value.spec_version) || value.engine !== "postgres" && value.engine !== "mysql" || !/^[A-Z_][A-Z0-9_]*$/.test(value.source_env) || !digest.test(value.schema_fingerprint) || !digest.test(value.role_posture_fingerprint) || !digest.test(value.evidence_fingerprint) || !digest.test(value.generated_contract_digest) || !digest.test(value.reviewed_overrides_digest) || !Array.isArray(value.protected_authority) || value.protected_authority.some((item) => typeof item !== "string")) {
|
|
14202
17501
|
throw new McpRuntimeError(
|
|
14203
17502
|
"GENERATION_LOCK_INVALID",
|
|
14204
17503
|
"The generated-authority lock is malformed or belongs to an unsupported compiler/spec version."
|
|
@@ -14291,7 +17590,7 @@ function createSynapsorMcpServer(runtime, options = {}) {
|
|
|
14291
17590
|
for (const exposedName of exposedNames.get(capability.name) ?? [capability.name]) {
|
|
14292
17591
|
const toolConfig = {
|
|
14293
17592
|
title: capability.name,
|
|
14294
|
-
description: capabilityDescription(capability, exposedName),
|
|
17593
|
+
description: capabilityDescription(capability, runtime.config, exposedName),
|
|
14295
17594
|
inputSchema: zodInputShape(capability),
|
|
14296
17595
|
annotations: {
|
|
14297
17596
|
readOnlyHint: capability.kind === "read" || capability.kind === "aggregate_read",
|
|
@@ -15955,6 +19254,31 @@ async function callConfiguredTool(input) {
|
|
|
15955
19254
|
}
|
|
15956
19255
|
});
|
|
15957
19256
|
}
|
|
19257
|
+
let supervisedWorker;
|
|
19258
|
+
if (input.config.governance?.mode !== "cloud_linked" && approvalResult.proposal.state === "approved") {
|
|
19259
|
+
const eligibility = resolveSupervisedWorkerEligibility(input.config, capability, { phase: "queue" });
|
|
19260
|
+
if (eligibility.eligible && eligibility.policy && eligibility.contract_digest) {
|
|
19261
|
+
if (!input.store.enqueueWorkerProposal) {
|
|
19262
|
+
throw new McpRuntimeError(
|
|
19263
|
+
"SUPERVISED_WORKER_LEDGER_REQUIRED",
|
|
19264
|
+
"Supervised execution requires a durable worker queue in the authoritative proposal store."
|
|
19265
|
+
);
|
|
19266
|
+
}
|
|
19267
|
+
await input.store.enqueueWorkerProposal({
|
|
19268
|
+
proposal_id: approvalResult.proposal.proposal_id,
|
|
19269
|
+
execution_mode: "supervised_worker",
|
|
19270
|
+
contract_digest: eligibility.contract_digest,
|
|
19271
|
+
max_attempts: eligibility.policy.max_attempts,
|
|
19272
|
+
queue_limit: eligibility.policy.queue_limit
|
|
19273
|
+
});
|
|
19274
|
+
supervisedWorker = {
|
|
19275
|
+
status: "queued",
|
|
19276
|
+
mode: "supervised_worker",
|
|
19277
|
+
capability: capability.name,
|
|
19278
|
+
contract_digest: eligibility.contract_digest
|
|
19279
|
+
};
|
|
19280
|
+
}
|
|
19281
|
+
}
|
|
15958
19282
|
await enqueueCloudLinkedProposal({
|
|
15959
19283
|
config: input.config,
|
|
15960
19284
|
store: input.store,
|
|
@@ -15964,7 +19288,7 @@ async function callConfiguredTool(input) {
|
|
|
15964
19288
|
env: input.env
|
|
15965
19289
|
});
|
|
15966
19290
|
return {
|
|
15967
|
-
status: input.config.governance?.mode === "cloud_linked" ? "pending_cloud_sync" : input.config.mode === "shadow" ? "shadow_proposal_created" : approvalResult.freshness?.status === "stale" ? "freshness_conflict" : approvalResult.proposal.state === "approved" ? "approved" : "review_required",
|
|
19291
|
+
status: input.config.governance?.mode === "cloud_linked" ? "pending_cloud_sync" : input.config.mode === "shadow" ? "shadow_proposal_created" : approvalResult.freshness?.status === "stale" ? "freshness_conflict" : supervisedWorker ? "queued_for_trusted_execution" : approvalResult.proposal.state === "approved" ? "approved" : "review_required",
|
|
15968
19292
|
action: capability.name,
|
|
15969
19293
|
proposal_id: approvalResult.proposal.proposal_id,
|
|
15970
19294
|
proposal_version: approvalResult.proposal.proposal_version,
|
|
@@ -15992,6 +19316,13 @@ async function callConfiguredTool(input) {
|
|
|
15992
19316
|
...approvalResult.freshness ? { freshness: approvalResult.freshness } : {},
|
|
15993
19317
|
governance: input.config.governance?.mode === "cloud_linked" ? { authority: "synapsor_cloud", state: "pending_cloud_sync", evidence_residency: "metadata_only" } : { authority: "local" },
|
|
15994
19318
|
writeback: changeSet.writeback,
|
|
19319
|
+
...supervisedWorker ? {
|
|
19320
|
+
execution: {
|
|
19321
|
+
...supervisedWorker,
|
|
19322
|
+
approval_source: "policy_auto",
|
|
19323
|
+
model_can_approve_or_apply: false
|
|
19324
|
+
}
|
|
19325
|
+
} : {},
|
|
15995
19326
|
source_database_changed: false,
|
|
15996
19327
|
source_database_mutated: false
|
|
15997
19328
|
};
|
|
@@ -16525,9 +19856,9 @@ function approvalPolicyByName(config, policyName) {
|
|
|
16525
19856
|
}
|
|
16526
19857
|
function evaluateApprovalPolicy(capability, policy, patch) {
|
|
16527
19858
|
const ruleByField = /* @__PURE__ */ new Map();
|
|
16528
|
-
for (const
|
|
16529
|
-
const field = typeof
|
|
16530
|
-
const max = typeof
|
|
19859
|
+
for (const rule2 of policy.rules ?? []) {
|
|
19860
|
+
const field = typeof rule2.field === "string" ? rule2.field : void 0;
|
|
19861
|
+
const max = typeof rule2.max === "number" && Number.isInteger(rule2.max) ? rule2.max : void 0;
|
|
16531
19862
|
if (field && max !== void 0) ruleByField.set(field, max);
|
|
16532
19863
|
}
|
|
16533
19864
|
const numericFields = Object.keys(patch).filter((field) => isNumericRuntimeProposalField(capability, field));
|
|
@@ -17517,10 +20848,10 @@ var RuntimeRateLimiter = class {
|
|
|
17517
20848
|
sharedSchema;
|
|
17518
20849
|
migration;
|
|
17519
20850
|
async consume(context, capability) {
|
|
17520
|
-
const
|
|
17521
|
-
if (!
|
|
20851
|
+
const rule2 = this.config.rate_limits?.capabilities?.[capability] ?? this.config.rate_limits?.default;
|
|
20852
|
+
if (!rule2) return;
|
|
17522
20853
|
const now = this.clock();
|
|
17523
|
-
const windowMs =
|
|
20854
|
+
const windowMs = rule2.window_seconds * 1e3;
|
|
17524
20855
|
const windowStart = Math.floor(now / windowMs) * windowMs;
|
|
17525
20856
|
let count;
|
|
17526
20857
|
if (this.sharedPool && this.sharedSchema) {
|
|
@@ -17536,7 +20867,7 @@ var RuntimeRateLimiter = class {
|
|
|
17536
20867
|
[bucketKey, windowStart]
|
|
17537
20868
|
);
|
|
17538
20869
|
count = Number(result.rows[0]?.request_count ?? 0);
|
|
17539
|
-
if (count >
|
|
20870
|
+
if (count > rule2.requests) {
|
|
17540
20871
|
await this.sharedPool.query(
|
|
17541
20872
|
`UPDATE ${table} SET rejected_count = rejected_count + 1, updated_at = now() WHERE bucket_key = $1 AND window_start = $2`,
|
|
17542
20873
|
[bucketKey, windowStart]
|
|
@@ -17550,7 +20881,7 @@ var RuntimeRateLimiter = class {
|
|
|
17550
20881
|
this.local.set(key, bucket);
|
|
17551
20882
|
count = bucket.count;
|
|
17552
20883
|
}
|
|
17553
|
-
if (count <=
|
|
20884
|
+
if (count <= rule2.requests) return;
|
|
17554
20885
|
const retryAfterMs = Math.max(1, windowStart + windowMs - now);
|
|
17555
20886
|
const metricKey = `${context.tenant_id}\0${capability}`;
|
|
17556
20887
|
const metric = this.rejected.get(metricKey) ?? { tenant: context.tenant_id, capability, rejected: 0 };
|
|
@@ -18100,11 +21431,11 @@ function zodScalarArg(spec) {
|
|
|
18100
21431
|
if (spec.required === false) schema = schema.optional();
|
|
18101
21432
|
return schema;
|
|
18102
21433
|
}
|
|
18103
|
-
function toolMetadata(capability) {
|
|
21434
|
+
function toolMetadata(capability, config) {
|
|
18104
21435
|
return {
|
|
18105
21436
|
name: capability.name,
|
|
18106
21437
|
title: capability.name,
|
|
18107
|
-
description: capabilityDescription(capability),
|
|
21438
|
+
description: capabilityDescription(capability, config),
|
|
18108
21439
|
kind: capability.kind,
|
|
18109
21440
|
input_schema: Object.fromEntries(Object.entries(capability.args).map(([name, spec]) => [name, {
|
|
18110
21441
|
type: spec.type === "object_array" ? "array" : spec.type,
|
|
@@ -18127,7 +21458,7 @@ function toolMetadata(capability) {
|
|
|
18127
21458
|
}
|
|
18128
21459
|
};
|
|
18129
21460
|
}
|
|
18130
|
-
function capabilityDescription(capability, exposedName) {
|
|
21461
|
+
function capabilityDescription(capability, config, exposedName) {
|
|
18131
21462
|
const lines = [];
|
|
18132
21463
|
if (exposedName && exposedName !== capability.name) {
|
|
18133
21464
|
lines.push(`Canonical Synapsor capability: ${capability.name}.`);
|
|
@@ -18137,7 +21468,20 @@ function capabilityDescription(capability, exposedName) {
|
|
|
18137
21468
|
} else if (capability.kind === "read") {
|
|
18138
21469
|
lines.push(`Read ${capability.target.schema}.${capability.target.table} through a reviewed Synapsor capability with trusted tenant context and evidence.`);
|
|
18139
21470
|
} else {
|
|
18140
|
-
|
|
21471
|
+
const supervised = config ? resolveSupervisedWorkerEligibility(config, capability, { phase: "queue" }) : void 0;
|
|
21472
|
+
if (supervised?.eligible) {
|
|
21473
|
+
if (capability.approval?.mode === "policy") {
|
|
21474
|
+
lines.push(
|
|
21475
|
+
`Create an evidence-backed Synapsor proposal for ${capability.target.schema}.${capability.target.table}. If it satisfies the reviewed automatic-approval policy, a separately trusted Runner worker may later apply it without a per-request human click. The model cannot approve, apply, start the worker, or change that policy.`
|
|
21476
|
+
);
|
|
21477
|
+
} else {
|
|
21478
|
+
lines.push(
|
|
21479
|
+
`Create an evidence-backed Synapsor proposal for ${capability.target.schema}.${capability.target.table}. After the required human approval, a separately trusted Runner worker may later apply it. The model cannot approve, apply, start the worker, or change that execution policy.`
|
|
21480
|
+
);
|
|
21481
|
+
}
|
|
21482
|
+
} else {
|
|
21483
|
+
lines.push(`Create an evidence-backed Synapsor proposal for ${capability.target.schema}.${capability.target.table}; the source database is not mutated by this tool.`);
|
|
21484
|
+
}
|
|
18141
21485
|
}
|
|
18142
21486
|
if (capability.returns_hint) {
|
|
18143
21487
|
lines.push(capability.returns_hint);
|