@synapsor/runner 1.6.0 → 1.6.1
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 +32 -4
- package/README.md +16 -2
- package/dist/cli.d.ts.map +1 -1
- package/dist/local-ui.d.ts +4 -1
- package/dist/local-ui.d.ts.map +1 -1
- package/dist/runner.mjs +1624 -42
- package/dist/runtime.mjs +1011 -25
- package/dist/shadow.mjs +408 -15
- package/docs/app-owned-executors.md +8 -0
- package/docs/capability-authoring.md +45 -0
- package/docs/current-scope.md +5 -0
- package/docs/cursor-plugin.md +2 -2
- package/docs/limitations.md +15 -2
- package/docs/local-mode.md +22 -3
- package/docs/production.md +19 -0
- package/docs/proposal-evidence-freshness.md +334 -0
- package/docs/release-notes.md +30 -4
- package/docs/runner-config-reference.md +46 -0
- package/docs/security-boundary.md +17 -0
- package/docs/store-lifecycle.md +8 -0
- package/docs/troubleshooting-first-run.md +76 -0
- package/docs/writeback-executors.md +8 -0
- package/examples/app-owned-writeback/command-handler.mjs +0 -0
- package/examples/mcp-postgres-billing-app-handler/scripts/run-demo.sh +0 -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/fixtures/compatibility/published-1.6.0/manifest.json +76 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/aggregate-read.synapsor.sql +21 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/billing-late-fee.synapsor.sql +56 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/bounded-set-multi-term.synapsor.sql +30 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/principal-row-scope.synapsor.sql +23 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/aggregate-read/contract.json +119 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/approval-quorum/contract.json +44 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/bounded-set-threats/contract.json +115 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/principal-row-scope/contract.json +78 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/proposal-capability/contract.json +101 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/reversible-change-sets/contract.json +98 -0
- package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/valid/basic-read.contract.json +60 -0
- package/fixtures/protocol/MANIFEST.json +37 -7
- package/fixtures/protocol/change-set.freshness-update.v2.json +58 -0
- package/fixtures/protocol/freshness-authority.invoice.v1.json +35 -0
- package/fixtures/protocol/freshness-proof.fresh.v1.json +38 -0
- package/fixtures/protocol/writeback-job.freshness-update.v2.json +50 -0
- package/llms.txt +1 -0
- package/package.json +9 -8
- package/schemas/change-set.v1.schema.json +1 -0
- package/schemas/change-set.v2.schema.json +1 -0
- package/schemas/change-set.v3.schema.json +1 -0
- package/schemas/freshness-authority.v1.schema.json +89 -0
- package/schemas/freshness-proof.v1.schema.json +73 -0
- package/schemas/synapsor.runner.schema.json +32 -0
- package/schemas/writeback-job.v1.schema.json +1 -0
- package/schemas/writeback-job.v2.schema.json +1 -0
- package/schemas/writeback-job.v3.schema.json +1 -0
package/dist/runtime.mjs
CHANGED
|
@@ -1787,10 +1787,12 @@ function sortJson(value) {
|
|
|
1787
1787
|
}
|
|
1788
1788
|
|
|
1789
1789
|
// 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", "operator_identity", "session_auth", "http_security", "rate_limits", "metrics", "graduated_trust", "cloud", "governance", "generated_authority", "strict", "result_format"]);
|
|
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"]);
|
|
1791
1791
|
var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path", "shared_postgres"]);
|
|
1792
1792
|
var SHARED_POSTGRES_STORAGE_KEYS = /* @__PURE__ */ new Set(["mode", "url_env", "schema", "lock_timeout_ms", "max_entries"]);
|
|
1793
1793
|
var APPROVALS_KEYS = /* @__PURE__ */ new Set(["disable_auto_approval"]);
|
|
1794
|
+
var PROPOSAL_FRESHNESS_KEYS = /* @__PURE__ */ new Set(["approval", "dependencies"]);
|
|
1795
|
+
var FRESHNESS_DEPENDENCY_KEYS = /* @__PURE__ */ new Set(["id", "capability", "identity_from_arg", "version_column"]);
|
|
1794
1796
|
var METRICS_KEYS = /* @__PURE__ */ new Set(["enabled", "token_env"]);
|
|
1795
1797
|
var GRADUATED_TRUST_KEYS = /* @__PURE__ */ new Set(["enabled", "kill_switch", "workspace_id", "project_id", "criteria"]);
|
|
1796
1798
|
var GRADUATED_TRUST_CRITERION_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -2035,6 +2037,7 @@ function validateRunnerCapabilityConfig(input) {
|
|
|
2035
2037
|
validateMetrics(input.metrics, strict, errors);
|
|
2036
2038
|
validateGraduatedTrust(input.graduated_trust, strict, errors);
|
|
2037
2039
|
validateCapabilities2(input.capabilities, input.sources, input.contexts, input.executors, input.mode, strict, errors, warnings, hasContracts);
|
|
2040
|
+
validateProposalFreshness(input.proposal_freshness, input.capabilities, strict, errors);
|
|
2038
2041
|
validateEffectiveContextCompatibility(input.trusted_context, input.contexts, input.capabilities, errors);
|
|
2039
2042
|
validateApprovalPolicyReferences(input.capabilities, input.policies, errors);
|
|
2040
2043
|
validateWritebackReadiness(input.sources, input.capabilities, input.mode, errors, warnings);
|
|
@@ -3122,6 +3125,125 @@ function validateCapabilities2(value, sources, contexts, executors, mode, strict
|
|
|
3122
3125
|
capabilityNames.set(capability.name, index);
|
|
3123
3126
|
});
|
|
3124
3127
|
}
|
|
3128
|
+
function validateProposalFreshness(value, capabilitiesValue, strict, errors) {
|
|
3129
|
+
if (value === void 0) return;
|
|
3130
|
+
if (!isRecord2(value)) {
|
|
3131
|
+
errors.push({
|
|
3132
|
+
path: "$.proposal_freshness",
|
|
3133
|
+
code: "PROPOSAL_FRESHNESS_NOT_OBJECT",
|
|
3134
|
+
message: "proposal_freshness must map reviewed proposal capability names to freshness policy."
|
|
3135
|
+
});
|
|
3136
|
+
return;
|
|
3137
|
+
}
|
|
3138
|
+
const capabilities = Array.isArray(capabilitiesValue) ? capabilitiesValue.filter(isRecord2) : [];
|
|
3139
|
+
const byName = new Map(capabilities.flatMap((capability) => isQualifiedName2(capability.name) ? [[String(capability.name), capability]] : []));
|
|
3140
|
+
for (const [proposalName, rawPolicy] of Object.entries(value)) {
|
|
3141
|
+
const path3 = `$.proposal_freshness.${proposalName}`;
|
|
3142
|
+
if (!isQualifiedName2(proposalName)) {
|
|
3143
|
+
errors.push({ path: path3, code: "INVALID_FRESHNESS_CAPABILITY_NAME", message: "Freshness policy keys must be existing qualified proposal capability names." });
|
|
3144
|
+
continue;
|
|
3145
|
+
}
|
|
3146
|
+
if (!isRecord2(rawPolicy)) {
|
|
3147
|
+
errors.push({ path: path3, code: "FRESHNESS_POLICY_NOT_OBJECT", message: "Freshness policy must be an object." });
|
|
3148
|
+
continue;
|
|
3149
|
+
}
|
|
3150
|
+
if (strict) checkUnknownKeys2(rawPolicy, PROPOSAL_FRESHNESS_KEYS, path3, errors);
|
|
3151
|
+
if (rawPolicy.approval !== "required") {
|
|
3152
|
+
errors.push({ path: `${path3}.approval`, code: "FRESHNESS_APPROVAL_REQUIRED", message: "Freshness-enabled proposals must set approval to required." });
|
|
3153
|
+
}
|
|
3154
|
+
const proposal = byName.get(proposalName);
|
|
3155
|
+
if (!proposal) {
|
|
3156
|
+
errors.push({ path: path3, code: "FRESHNESS_PROPOSAL_UNKNOWN", message: `Freshness policy references unknown proposal capability ${proposalName}.` });
|
|
3157
|
+
continue;
|
|
3158
|
+
}
|
|
3159
|
+
if (proposal.kind !== "proposal") {
|
|
3160
|
+
errors.push({ path: path3, code: "FRESHNESS_PROPOSAL_REQUIRED", message: `${proposalName} must be a proposal capability.` });
|
|
3161
|
+
}
|
|
3162
|
+
const writeback = isRecord2(proposal.writeback) ? proposal.writeback : void 0;
|
|
3163
|
+
if (writeback?.mode !== "direct_sql") {
|
|
3164
|
+
errors.push({
|
|
3165
|
+
path: path3,
|
|
3166
|
+
code: "FRESHNESS_DIRECT_SQL_REQUIRED",
|
|
3167
|
+
message: "Strict proposal freshness currently requires same-database direct_sql writeback; app-owned and cross-source handlers must enforce their own transaction preconditions."
|
|
3168
|
+
});
|
|
3169
|
+
}
|
|
3170
|
+
const operation = isRecord2(proposal.operation) ? proposal.operation : void 0;
|
|
3171
|
+
if ((operation?.kind ?? "update") !== "insert") {
|
|
3172
|
+
const guard = isRecord2(proposal.conflict_guard) ? proposal.conflict_guard : void 0;
|
|
3173
|
+
if (!isSafeIdentifier2(guard?.column)) {
|
|
3174
|
+
errors.push({ path: path3, code: "FRESHNESS_EXACT_TARGET_GUARD_REQUIRED", message: "Freshness-required UPDATE/DELETE needs an exact conflict_guard.column; weak row hashes are not sufficient." });
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
if (rawPolicy.dependencies !== void 0 && !Array.isArray(rawPolicy.dependencies)) {
|
|
3178
|
+
errors.push({ path: `${path3}.dependencies`, code: "FRESHNESS_DEPENDENCIES_NOT_ARRAY", message: "dependencies must be an array of reviewed single-row read references." });
|
|
3179
|
+
continue;
|
|
3180
|
+
}
|
|
3181
|
+
const dependencies = Array.isArray(rawPolicy.dependencies) ? rawPolicy.dependencies : [];
|
|
3182
|
+
if (dependencies.length > 16) {
|
|
3183
|
+
errors.push({ path: `${path3}.dependencies`, code: "FRESHNESS_DEPENDENCY_LIMIT", message: "A proposal may declare at most 16 supporting freshness dependencies." });
|
|
3184
|
+
}
|
|
3185
|
+
const ids = /* @__PURE__ */ new Set();
|
|
3186
|
+
for (const [index, rawDependency] of dependencies.entries()) {
|
|
3187
|
+
const dependencyPath = `${path3}.dependencies[${index}]`;
|
|
3188
|
+
if (!isRecord2(rawDependency)) {
|
|
3189
|
+
errors.push({ path: dependencyPath, code: "FRESHNESS_DEPENDENCY_NOT_OBJECT", message: "Freshness dependency must be an object." });
|
|
3190
|
+
continue;
|
|
3191
|
+
}
|
|
3192
|
+
if (strict) checkUnknownKeys2(rawDependency, FRESHNESS_DEPENDENCY_KEYS, dependencyPath, errors);
|
|
3193
|
+
if (!isSafeIdentifier2(rawDependency.id)) {
|
|
3194
|
+
errors.push({ path: `${dependencyPath}.id`, code: "INVALID_FRESHNESS_DEPENDENCY_ID", message: "Dependency id must be a fixed safe identifier." });
|
|
3195
|
+
} else if (ids.has(String(rawDependency.id))) {
|
|
3196
|
+
errors.push({ path: `${dependencyPath}.id`, code: "DUPLICATE_FRESHNESS_DEPENDENCY_ID", message: "Dependency ids must be unique within a proposal." });
|
|
3197
|
+
} else {
|
|
3198
|
+
ids.add(String(rawDependency.id));
|
|
3199
|
+
}
|
|
3200
|
+
if (!isQualifiedName2(rawDependency.capability)) {
|
|
3201
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "INVALID_FRESHNESS_DEPENDENCY_CAPABILITY", message: "Dependency capability must be a reviewed qualified capability name." });
|
|
3202
|
+
continue;
|
|
3203
|
+
}
|
|
3204
|
+
const dependency = byName.get(String(rawDependency.capability));
|
|
3205
|
+
if (!dependency) {
|
|
3206
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_UNKNOWN", message: `Unknown supporting capability ${String(rawDependency.capability)}.` });
|
|
3207
|
+
continue;
|
|
3208
|
+
}
|
|
3209
|
+
if (dependency.kind !== "read" || dependency.protected_read !== void 0 || dependency.aggregate !== void 0) {
|
|
3210
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_SINGLE_ROW_READ_REQUIRED", message: "Supporting freshness dependencies must reference reviewed single-row read capabilities." });
|
|
3211
|
+
}
|
|
3212
|
+
if (dependency.name === proposalName) {
|
|
3213
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_CYCLE", message: "A proposal cannot depend on itself." });
|
|
3214
|
+
}
|
|
3215
|
+
if (dependency.source !== proposal.source) {
|
|
3216
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_CROSS_SOURCE_UNSUPPORTED", message: "Strict atomic freshness requires the proposal and supporting dependency to use the same source." });
|
|
3217
|
+
}
|
|
3218
|
+
if ((dependency.context ?? null) !== (proposal.context ?? null)) {
|
|
3219
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_CONTEXT_MISMATCH", message: "Supporting freshness dependencies must use the same reviewed trusted context as the proposal." });
|
|
3220
|
+
}
|
|
3221
|
+
const target = isRecord2(dependency.target) ? dependency.target : void 0;
|
|
3222
|
+
if (!isSafeIdentifier2(target?.tenant_key) && target?.single_tenant_dev !== true) {
|
|
3223
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_TENANT_SCOPE_REQUIRED", message: "Supporting dependencies require a trusted tenant key or an existing explicit single-tenant-dev boundary." });
|
|
3224
|
+
}
|
|
3225
|
+
const proposalTarget = isRecord2(proposal.target) ? proposal.target : void 0;
|
|
3226
|
+
if (isSafeIdentifier2(target?.principal_scope_key) && !isSafeIdentifier2(proposalTarget?.principal_scope_key)) {
|
|
3227
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_PRINCIPAL_SCOPE_UNAVAILABLE", message: "A principal-scoped dependency cannot introduce a trusted principal value absent from the proposal authority." });
|
|
3228
|
+
}
|
|
3229
|
+
if (!isSafeIdentifier2(rawDependency.identity_from_arg) || !isRecord2(proposal.args) || !(String(rawDependency.identity_from_arg) in proposal.args)) {
|
|
3230
|
+
errors.push({ path: `${dependencyPath}.identity_from_arg`, code: "FRESHNESS_IDENTITY_ARG_UNKNOWN", message: "identity_from_arg must name an existing scalar proposal argument." });
|
|
3231
|
+
} else {
|
|
3232
|
+
const proposalArg = isRecord2(proposal.args[String(rawDependency.identity_from_arg)]) ? proposal.args[String(rawDependency.identity_from_arg)] : void 0;
|
|
3233
|
+
if (!proposalArg || proposalArg.type === "object_array") {
|
|
3234
|
+
errors.push({ path: `${dependencyPath}.identity_from_arg`, code: "FRESHNESS_IDENTITY_ARG_NOT_SCALAR", message: "identity_from_arg must reference one bounded scalar proposal argument." });
|
|
3235
|
+
}
|
|
3236
|
+
}
|
|
3237
|
+
const lookup = isRecord2(dependency.lookup) ? dependency.lookup : void 0;
|
|
3238
|
+
if (!isSafeIdentifier2(lookup?.id_from_arg) || !isRecord2(dependency.args) || !(String(lookup?.id_from_arg) in dependency.args)) {
|
|
3239
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_LOOKUP_INVALID", message: "Supporting capability must have one fixed reviewed lookup argument." });
|
|
3240
|
+
}
|
|
3241
|
+
if (!isSafeIdentifier2(rawDependency.version_column)) {
|
|
3242
|
+
errors.push({ path: `${dependencyPath}.version_column`, code: "FRESHNESS_VERSION_COLUMN_REQUIRED", message: "version_column must be an exact fixed safe identifier." });
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
3245
|
+
}
|
|
3246
|
+
}
|
|
3125
3247
|
function validateCapability(value, index, sourceNames, contextNames, executorNames, strict, errors, warnings) {
|
|
3126
3248
|
const path3 = `$.capabilities[${index}]`;
|
|
3127
3249
|
if (!isRecord2(value)) {
|
|
@@ -3907,6 +4029,8 @@ var protocolVersions = {
|
|
|
3907
4029
|
runnerProposal: "synapsor.runner-proposal.v1",
|
|
3908
4030
|
runnerActivity: "synapsor.runner-activity.v1",
|
|
3909
4031
|
principalScope: "synapsor.principal-scope.v1",
|
|
4032
|
+
freshnessAuthority: "synapsor.freshness-authority.v1",
|
|
4033
|
+
freshnessProof: "synapsor.freshness-proof.v1",
|
|
3910
4034
|
legacyWritebackJob: "1.0",
|
|
3911
4035
|
normalizedWritebackJobV2: "2.0",
|
|
3912
4036
|
normalizedWritebackJobV3: "3.0",
|
|
@@ -3954,6 +4078,101 @@ var principalScopeGuardSchema = z.object({
|
|
|
3954
4078
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope fingerprint does not match its trusted value", path: ["value_fingerprint"] });
|
|
3955
4079
|
}
|
|
3956
4080
|
});
|
|
4081
|
+
var freshnessDependencyUnsignedSchema = z.object({
|
|
4082
|
+
id: safeIdentifier,
|
|
4083
|
+
capability: z.string().regex(/^[A-Za-z_][A-Za-z0-9_.-]*\.[A-Za-z_][A-Za-z0-9_.-]*$/, "expected reviewed capability name"),
|
|
4084
|
+
source_id: z.string().min(1).max(160),
|
|
4085
|
+
engine: writebackEngineSchema,
|
|
4086
|
+
target: z.object({
|
|
4087
|
+
schema: safeIdentifier,
|
|
4088
|
+
table: safeIdentifier,
|
|
4089
|
+
primary_key: columnValueSchema,
|
|
4090
|
+
tenant_column: safeIdentifier,
|
|
4091
|
+
principal_column: safeIdentifier.optional()
|
|
4092
|
+
}),
|
|
4093
|
+
expected_version: columnValueSchema,
|
|
4094
|
+
evidence: z.object({
|
|
4095
|
+
bundle_id: z.string().min(1).max(200),
|
|
4096
|
+
query_fingerprint: sha256
|
|
4097
|
+
})
|
|
4098
|
+
});
|
|
4099
|
+
var freshnessDependencyV1Schema = freshnessDependencyUnsignedSchema.extend({
|
|
4100
|
+
descriptor_digest: sha256
|
|
4101
|
+
}).superRefine((dependency, ctx) => {
|
|
4102
|
+
const { descriptor_digest: _digest, ...unsigned } = dependency;
|
|
4103
|
+
if (canonicalJsonDigest(unsigned) !== dependency.descriptor_digest) {
|
|
4104
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency digest mismatch", path: ["descriptor_digest"] });
|
|
4105
|
+
}
|
|
4106
|
+
});
|
|
4107
|
+
var freshnessAuthorityUnsignedSchema = z.object({
|
|
4108
|
+
schema_version: z.literal(protocolVersions.freshnessAuthority),
|
|
4109
|
+
required: z.literal(true),
|
|
4110
|
+
target: z.object({
|
|
4111
|
+
mode: z.enum(["exact_guard", "frozen_set", "not_applicable"]),
|
|
4112
|
+
member_count: z.number().int().min(0).max(100)
|
|
4113
|
+
}),
|
|
4114
|
+
dependencies: z.array(freshnessDependencyV1Schema).max(16)
|
|
4115
|
+
});
|
|
4116
|
+
var freshnessAuthorityV1Schema = freshnessAuthorityUnsignedSchema.extend({
|
|
4117
|
+
dependency_set_digest: sha256
|
|
4118
|
+
}).superRefine((authority, ctx) => {
|
|
4119
|
+
const identities = authority.dependencies.map((dependency) => dependency.id);
|
|
4120
|
+
if (new Set(identities).size !== identities.length) {
|
|
4121
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency ids must be unique", path: ["dependencies"] });
|
|
4122
|
+
}
|
|
4123
|
+
const sorted = [...authority.dependencies].sort((left, right) => left.source_id.localeCompare(right.source_id) || left.target.schema.localeCompare(right.target.schema) || left.target.table.localeCompare(right.target.table) || JSON.stringify(left.target.primary_key.value).localeCompare(JSON.stringify(right.target.primary_key.value)) || left.id.localeCompare(right.id));
|
|
4124
|
+
if (JSON.stringify(sorted) !== JSON.stringify(authority.dependencies)) {
|
|
4125
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependencies must use deterministic lock order", path: ["dependencies"] });
|
|
4126
|
+
}
|
|
4127
|
+
const { dependency_set_digest: _digest, ...unsigned } = authority;
|
|
4128
|
+
if (canonicalJsonDigest(unsigned) !== authority.dependency_set_digest) {
|
|
4129
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency-set digest mismatch", path: ["dependency_set_digest"] });
|
|
4130
|
+
}
|
|
4131
|
+
});
|
|
4132
|
+
var freshnessCheckStatusSchema = z.enum(["fresh", "stale", "unavailable", "invalid", "unsupported"]);
|
|
4133
|
+
var freshnessItemResultSchema = z.object({
|
|
4134
|
+
id: z.string().min(1).max(160),
|
|
4135
|
+
kind: z.enum(["target", "supporting"]),
|
|
4136
|
+
status: z.enum(["fresh", "stale", "not_applicable", "unavailable", "invalid", "unsupported"]),
|
|
4137
|
+
safe_code: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
|
|
4138
|
+
expected_version_digest: sha256.optional(),
|
|
4139
|
+
observed_version_digest: sha256.optional()
|
|
4140
|
+
});
|
|
4141
|
+
var freshnessProofUnsignedSchema = z.object({
|
|
4142
|
+
schema_version: z.literal(protocolVersions.freshnessProof),
|
|
4143
|
+
proposal_id: z.string().min(1),
|
|
4144
|
+
proposal_hash: sha256,
|
|
4145
|
+
proposal_version: z.number().int().positive(),
|
|
4146
|
+
dependency_set_digest: sha256,
|
|
4147
|
+
checked_at: z.string().datetime(),
|
|
4148
|
+
valid_until: z.string().datetime(),
|
|
4149
|
+
source_adapters: z.array(z.object({
|
|
4150
|
+
source_id: z.string().min(1).max(160),
|
|
4151
|
+
engine: writebackEngineSchema
|
|
4152
|
+
})).min(1).max(8),
|
|
4153
|
+
result: freshnessCheckStatusSchema,
|
|
4154
|
+
safe_code: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
|
|
4155
|
+
target_count: z.number().int().min(0).max(100),
|
|
4156
|
+
supporting_count: z.number().int().min(0).max(16),
|
|
4157
|
+
checks: z.array(freshnessItemResultSchema).max(116)
|
|
4158
|
+
});
|
|
4159
|
+
var freshnessProofV1Schema = freshnessProofUnsignedSchema.extend({
|
|
4160
|
+
proof_digest: sha256
|
|
4161
|
+
}).superRefine((proof, ctx) => {
|
|
4162
|
+
if (Date.parse(proof.valid_until) < Date.parse(proof.checked_at)) {
|
|
4163
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness proof validity ends before it was checked", path: ["valid_until"] });
|
|
4164
|
+
}
|
|
4165
|
+
if (proof.checks.filter((check) => check.kind === "target").length !== proof.target_count) {
|
|
4166
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness target count mismatch", path: ["target_count"] });
|
|
4167
|
+
}
|
|
4168
|
+
if (proof.checks.filter((check) => check.kind === "supporting").length !== proof.supporting_count) {
|
|
4169
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness supporting count mismatch", path: ["supporting_count"] });
|
|
4170
|
+
}
|
|
4171
|
+
const { proof_digest: _digest, ...unsigned } = proof;
|
|
4172
|
+
if (canonicalJsonDigest(unsigned) !== proof.proof_digest) {
|
|
4173
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness proof digest mismatch", path: ["proof_digest"] });
|
|
4174
|
+
}
|
|
4175
|
+
});
|
|
3957
4176
|
var resolvedDeduplicationComponentSchema = z.object({
|
|
3958
4177
|
column: safeIdentifier,
|
|
3959
4178
|
value: scalar,
|
|
@@ -4083,6 +4302,7 @@ var changeSetV1Schema = z.object({
|
|
|
4083
4302
|
allowed_columns: z.array(z.string().min(1)).min(1),
|
|
4084
4303
|
expected_version: columnValueSchema
|
|
4085
4304
|
}),
|
|
4305
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
4086
4306
|
evidence: z.object({
|
|
4087
4307
|
bundle_id: z.string().min(1),
|
|
4088
4308
|
query_fingerprint: sha256,
|
|
@@ -4103,6 +4323,7 @@ var changeSetV1Schema = z.object({
|
|
|
4103
4323
|
}),
|
|
4104
4324
|
created_at: z.string().min(1)
|
|
4105
4325
|
}).superRefine((changeSet, ctx) => {
|
|
4326
|
+
validateFreshnessBinding(changeSet.freshness, changeSet.source.source_id, sourceEngine(changeSet.source.kind), "exact_guard", 1, ctx);
|
|
4106
4327
|
const allowed = new Set(changeSet.guards.allowed_columns);
|
|
4107
4328
|
for (const column of Object.keys(changeSet.patch)) {
|
|
4108
4329
|
if (!allowed.has(column)) {
|
|
@@ -4170,6 +4391,7 @@ var changeSetV2Schema = z.object({
|
|
|
4170
4391
|
version_advance: versionAdvanceSchema.optional(),
|
|
4171
4392
|
deduplication: z.object({ components: z.array(resolvedDeduplicationComponentSchema).min(1).max(8) }).optional()
|
|
4172
4393
|
}),
|
|
4394
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
4173
4395
|
reversibility: reversibilityRequestSchema.optional(),
|
|
4174
4396
|
evidence: z.object({
|
|
4175
4397
|
bundle_id: z.string().min(1),
|
|
@@ -4189,6 +4411,14 @@ var changeSetV2Schema = z.object({
|
|
|
4189
4411
|
integrity: z.object({ proposal_hash: sha256 }),
|
|
4190
4412
|
created_at: z.string().min(1)
|
|
4191
4413
|
}).superRefine((changeSet, ctx) => {
|
|
4414
|
+
validateFreshnessBinding(
|
|
4415
|
+
changeSet.freshness,
|
|
4416
|
+
changeSet.source.source_id,
|
|
4417
|
+
sourceEngine(changeSet.source.kind),
|
|
4418
|
+
changeSet.operation === "single_row_insert" ? "not_applicable" : "exact_guard",
|
|
4419
|
+
changeSet.operation === "single_row_insert" ? 0 : 1,
|
|
4420
|
+
ctx
|
|
4421
|
+
);
|
|
4192
4422
|
const allowed = new Set(changeSet.guards.allowed_columns);
|
|
4193
4423
|
for (const column of Object.keys(changeSet.patch)) {
|
|
4194
4424
|
if (!allowed.has(column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `patch column not allowed: ${column}`, path: ["patch", column] });
|
|
@@ -4255,6 +4485,7 @@ var changeSetV3Schema = z.object({
|
|
|
4255
4485
|
expected_version: columnValueSchema.optional(),
|
|
4256
4486
|
version_advance: versionAdvanceSchema.optional()
|
|
4257
4487
|
}),
|
|
4488
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
4258
4489
|
frozen_set: frozenSetSchema,
|
|
4259
4490
|
reversibility: reversibilityRequestSchema.optional(),
|
|
4260
4491
|
evidence: z.object({ bundle_id: z.string().min(1), query_fingerprint: sha256, items: z.array(z.unknown()).max(100) }).passthrough(),
|
|
@@ -4269,6 +4500,14 @@ var changeSetV3Schema = z.object({
|
|
|
4269
4500
|
integrity: z.object({ proposal_hash: sha256 }),
|
|
4270
4501
|
created_at: z.string().min(1)
|
|
4271
4502
|
}).superRefine((changeSet, ctx) => {
|
|
4503
|
+
validateFreshnessBinding(
|
|
4504
|
+
changeSet.freshness,
|
|
4505
|
+
changeSet.source.source_id,
|
|
4506
|
+
sourceEngine(changeSet.source.kind),
|
|
4507
|
+
changeSet.operation === "batch_insert" ? "not_applicable" : "frozen_set",
|
|
4508
|
+
changeSet.operation === "batch_insert" ? 0 : changeSet.frozen_set.row_count,
|
|
4509
|
+
ctx
|
|
4510
|
+
);
|
|
4272
4511
|
if (changeSet.source.primary_key.value !== void 0 || changeSet.guards.expected_version) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "set guards live on frozen members, not the top-level envelope", path: ["frozen_set"] });
|
|
4273
4512
|
const allowed = new Set(changeSet.guards.allowed_columns);
|
|
4274
4513
|
for (const column of Object.keys(changeSet.patch)) if (!allowed.has(column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `patch column not allowed: ${column}`, path: ["patch", column] });
|
|
@@ -4364,6 +4603,7 @@ var writebackJobV1Schema = z.object({
|
|
|
4364
4603
|
allowed_columns: z.array(safeIdentifier).min(1),
|
|
4365
4604
|
patch: scalarMap,
|
|
4366
4605
|
conflict_guard: publicConflictGuardSchema,
|
|
4606
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
4367
4607
|
idempotency_key: z.string().min(1),
|
|
4368
4608
|
lease: z.object({
|
|
4369
4609
|
lease_id: z.string().min(1),
|
|
@@ -4371,6 +4611,7 @@ var writebackJobV1Schema = z.object({
|
|
|
4371
4611
|
expires_at: z.string().min(1)
|
|
4372
4612
|
})
|
|
4373
4613
|
}).superRefine((job, ctx) => {
|
|
4614
|
+
validateFreshnessBinding(job.freshness, job.runner_scope.source_id, job.engine, "exact_guard", 1, ctx);
|
|
4374
4615
|
validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.tenant_guard.column, ctx);
|
|
4375
4616
|
if (job.principal_scope && job.allowed_columns.includes(job.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["allowed_columns"] });
|
|
4376
4617
|
});
|
|
@@ -4392,6 +4633,7 @@ var normalizedWritebackJobV1Schema = writebackJobV1Schema.transform((job) => ({
|
|
|
4392
4633
|
allowed_columns: job.allowed_columns,
|
|
4393
4634
|
patch: job.patch,
|
|
4394
4635
|
conflict_guard: normalizeConflictGuard(job.conflict_guard),
|
|
4636
|
+
...job.freshness ? { freshness: job.freshness } : {},
|
|
4395
4637
|
idempotency_key: job.idempotency_key,
|
|
4396
4638
|
lease_expires_at: job.lease.expires_at,
|
|
4397
4639
|
attempt_count: job.lease.attempt
|
|
@@ -4425,10 +4667,12 @@ var legacyWritebackJobSchema = z.object({
|
|
|
4425
4667
|
z.object({ kind: z.literal("row_hash"), expected_hash: z.string().min(1) }),
|
|
4426
4668
|
z.object({ kind: z.literal("none") })
|
|
4427
4669
|
]),
|
|
4670
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
4428
4671
|
idempotency_key: z.string().min(1),
|
|
4429
4672
|
lease_expires_at: z.union([z.string(), z.number()]),
|
|
4430
4673
|
attempt_count: z.number().int().nonnegative().optional()
|
|
4431
4674
|
}).superRefine((job, ctx) => {
|
|
4675
|
+
validateFreshnessBinding(job.freshness, job.source_id, job.engine, "exact_guard", 1, ctx);
|
|
4432
4676
|
validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.target.tenant_guard.column, ctx);
|
|
4433
4677
|
if (job.target.principal_scope && job.allowed_columns.includes(job.target.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["allowed_columns"] });
|
|
4434
4678
|
});
|
|
@@ -4455,6 +4699,7 @@ var normalizedWritebackJobV2InputSchema = z.object({
|
|
|
4455
4699
|
z.object({ kind: z.literal("row_hash"), expected_hash: z.string().min(1) }),
|
|
4456
4700
|
z.object({ kind: z.literal("none") })
|
|
4457
4701
|
]),
|
|
4702
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
4458
4703
|
version_advance: versionAdvanceSchema.optional(),
|
|
4459
4704
|
deduplication: z.object({ components: z.array(resolvedDeduplicationComponentSchema).min(1).max(8) }).optional(),
|
|
4460
4705
|
idempotency_key: z.string().min(1),
|
|
@@ -4462,6 +4707,14 @@ var normalizedWritebackJobV2InputSchema = z.object({
|
|
|
4462
4707
|
attempt_count: z.number().int().positive(),
|
|
4463
4708
|
inverse_capture: inverseDescriptorV1Schema.optional()
|
|
4464
4709
|
}).superRefine((job, ctx) => {
|
|
4710
|
+
validateFreshnessBinding(
|
|
4711
|
+
job.freshness,
|
|
4712
|
+
job.source_id,
|
|
4713
|
+
job.engine,
|
|
4714
|
+
job.operation === "single_row_insert" ? "not_applicable" : "exact_guard",
|
|
4715
|
+
job.operation === "single_row_insert" ? 0 : 1,
|
|
4716
|
+
ctx
|
|
4717
|
+
);
|
|
4465
4718
|
if (job.operation !== "single_row_insert" && job.target.primary_key.value === void 0) {
|
|
4466
4719
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "UPDATE and DELETE require a primary-key value", path: ["target", "primary_key", "value"] });
|
|
4467
4720
|
}
|
|
@@ -4527,6 +4780,7 @@ var writebackJobV2Schema = z.object({
|
|
|
4527
4780
|
principal_scope: principalScopeGuardSchema.optional(),
|
|
4528
4781
|
allowed_columns: z.array(safeIdentifier),
|
|
4529
4782
|
mutation: writebackMutationV2Schema,
|
|
4783
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
4530
4784
|
idempotency_key: z.string().min(1),
|
|
4531
4785
|
inverse_capture: inverseDescriptorV1Schema.optional(),
|
|
4532
4786
|
lease: z.object({
|
|
@@ -4535,6 +4789,14 @@ var writebackJobV2Schema = z.object({
|
|
|
4535
4789
|
expires_at: z.string().min(1)
|
|
4536
4790
|
})
|
|
4537
4791
|
}).superRefine((job, ctx) => {
|
|
4792
|
+
validateFreshnessBinding(
|
|
4793
|
+
job.freshness,
|
|
4794
|
+
job.runner_scope.source_id,
|
|
4795
|
+
job.engine,
|
|
4796
|
+
job.mutation.kind === "single_row_insert" ? "not_applicable" : "exact_guard",
|
|
4797
|
+
job.mutation.kind === "single_row_insert" ? 0 : 1,
|
|
4798
|
+
ctx
|
|
4799
|
+
);
|
|
4538
4800
|
const mutation = job.mutation;
|
|
4539
4801
|
if (mutation.kind !== "single_row_insert" && job.target.primary_key.value === void 0) {
|
|
4540
4802
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "UPDATE and DELETE require a primary-key value", path: ["target", "primary_key", "value"] });
|
|
@@ -4573,6 +4835,7 @@ var writebackJobV2Schema = z.object({
|
|
|
4573
4835
|
conflict_guard: job.mutation.kind === "single_row_insert" ? { kind: "none" } : normalizeConflictGuard(job.mutation.conflict_guard),
|
|
4574
4836
|
...job.mutation.kind === "single_row_update" && job.mutation.version_advance ? { version_advance: job.mutation.version_advance } : {},
|
|
4575
4837
|
...job.mutation.kind === "single_row_insert" ? { deduplication: job.mutation.deduplication } : {},
|
|
4838
|
+
...job.freshness ? { freshness: job.freshness } : {},
|
|
4576
4839
|
idempotency_key: job.idempotency_key,
|
|
4577
4840
|
...job.inverse_capture ? { inverse_capture: job.inverse_capture } : {},
|
|
4578
4841
|
lease_expires_at: job.lease.expires_at,
|
|
@@ -4591,6 +4854,7 @@ var normalizedWritebackJobV3InputSchema = z.object({
|
|
|
4591
4854
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
4592
4855
|
patch: boundedScalarRecord,
|
|
4593
4856
|
conflict_guard: z.object({ kind: z.literal("none") }).default({ kind: "none" }),
|
|
4857
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
4594
4858
|
version_advance: versionAdvanceSchema.optional(),
|
|
4595
4859
|
frozen_set: frozenSetSchema,
|
|
4596
4860
|
idempotency_key: z.string().min(1),
|
|
@@ -4598,6 +4862,14 @@ var normalizedWritebackJobV3InputSchema = z.object({
|
|
|
4598
4862
|
attempt_count: z.number().int().positive(),
|
|
4599
4863
|
inverse_capture: inverseDescriptorV1Schema.optional()
|
|
4600
4864
|
}).superRefine((job, ctx) => {
|
|
4865
|
+
validateFreshnessBinding(
|
|
4866
|
+
job.freshness,
|
|
4867
|
+
job.source_id,
|
|
4868
|
+
job.engine,
|
|
4869
|
+
job.operation === "batch_insert" ? "not_applicable" : "frozen_set",
|
|
4870
|
+
job.operation === "batch_insert" ? 0 : job.frozen_set.row_count,
|
|
4871
|
+
ctx
|
|
4872
|
+
);
|
|
4601
4873
|
if (job.target.principal_scope && job.allowed_columns.includes(job.target.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["allowed_columns"] });
|
|
4602
4874
|
if (job.operation === "set_delete" && (job.allowed_columns.length || Object.keys(job.patch).length || job.version_advance)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "set DELETE cannot carry patch authority", path: ["patch"] });
|
|
4603
4875
|
if (job.operation === "set_update" && (!Object.keys(job.patch).length || !job.version_advance)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "set UPDATE requires patch and version advance", path: ["patch"] });
|
|
@@ -4619,9 +4891,19 @@ var writebackJobV3Schema = z.object({
|
|
|
4619
4891
|
patch: boundedScalarRecord,
|
|
4620
4892
|
version_advance: versionAdvanceSchema.optional(),
|
|
4621
4893
|
frozen_set: frozenSetSchema,
|
|
4894
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
4622
4895
|
idempotency_key: z.string().min(1),
|
|
4623
4896
|
inverse_capture: inverseDescriptorV1Schema.optional(),
|
|
4624
4897
|
lease: z.object({ lease_id: z.string().min(1), attempt: z.number().int().positive(), expires_at: z.string().min(1) })
|
|
4898
|
+
}).superRefine((job, ctx) => {
|
|
4899
|
+
validateFreshnessBinding(
|
|
4900
|
+
job.freshness,
|
|
4901
|
+
job.runner_scope.source_id,
|
|
4902
|
+
job.engine,
|
|
4903
|
+
job.operation === "batch_insert" ? "not_applicable" : "frozen_set",
|
|
4904
|
+
job.operation === "batch_insert" ? 0 : job.frozen_set.row_count,
|
|
4905
|
+
ctx
|
|
4906
|
+
);
|
|
4625
4907
|
}).transform((job) => ({
|
|
4626
4908
|
protocol_version: protocolVersions.normalizedWritebackJobV3,
|
|
4627
4909
|
job_id: job.writeback_job_id,
|
|
@@ -4636,6 +4918,7 @@ var writebackJobV3Schema = z.object({
|
|
|
4636
4918
|
conflict_guard: { kind: "none" },
|
|
4637
4919
|
...job.version_advance ? { version_advance: job.version_advance } : {},
|
|
4638
4920
|
frozen_set: job.frozen_set,
|
|
4921
|
+
...job.freshness ? { freshness: job.freshness } : {},
|
|
4639
4922
|
idempotency_key: job.idempotency_key,
|
|
4640
4923
|
...job.inverse_capture ? { inverse_capture: job.inverse_capture } : {},
|
|
4641
4924
|
lease_expires_at: job.lease.expires_at,
|
|
@@ -4989,6 +5272,38 @@ var runnerActivityV1Schema = z.object({
|
|
|
4989
5272
|
detail: z.record(z.unknown()).optional(),
|
|
4990
5273
|
occurred_at: z.string().min(1).max(80).optional()
|
|
4991
5274
|
}).strict();
|
|
5275
|
+
function sourceEngine(kind) {
|
|
5276
|
+
if (kind === "external_postgres") return "postgres";
|
|
5277
|
+
if (kind === "external_mysql") return "mysql";
|
|
5278
|
+
return void 0;
|
|
5279
|
+
}
|
|
5280
|
+
function validateFreshnessBinding(freshness, sourceId, engine, expectedTargetMode, expectedMemberCount, ctx) {
|
|
5281
|
+
if (!freshness) return;
|
|
5282
|
+
if (!engine) {
|
|
5283
|
+
ctx.addIssue({
|
|
5284
|
+
code: z.ZodIssueCode.custom,
|
|
5285
|
+
message: "freshness authority requires an external PostgreSQL or MySQL source",
|
|
5286
|
+
path: ["freshness"]
|
|
5287
|
+
});
|
|
5288
|
+
return;
|
|
5289
|
+
}
|
|
5290
|
+
if (freshness.target.mode !== expectedTargetMode || freshness.target.member_count !== expectedMemberCount) {
|
|
5291
|
+
ctx.addIssue({
|
|
5292
|
+
code: z.ZodIssueCode.custom,
|
|
5293
|
+
message: `freshness target authority must be ${expectedTargetMode} with ${expectedMemberCount} reviewed member(s)`,
|
|
5294
|
+
path: ["freshness", "target"]
|
|
5295
|
+
});
|
|
5296
|
+
}
|
|
5297
|
+
for (const [index, dependency] of freshness.dependencies.entries()) {
|
|
5298
|
+
if (dependency.source_id !== sourceId || dependency.engine !== engine) {
|
|
5299
|
+
ctx.addIssue({
|
|
5300
|
+
code: z.ZodIssueCode.custom,
|
|
5301
|
+
message: "freshness dependencies must use the proposal/writeback source and engine",
|
|
5302
|
+
path: ["freshness", "dependencies", index, "source_id"]
|
|
5303
|
+
});
|
|
5304
|
+
}
|
|
5305
|
+
}
|
|
5306
|
+
}
|
|
4992
5307
|
function validateAllowedPatchColumns(allowedColumns, patchColumns, primaryKeyColumn, tenantGuardColumn, ctx) {
|
|
4993
5308
|
const allow = new Set(allowedColumns);
|
|
4994
5309
|
for (const column of patchColumns) {
|
|
@@ -5049,6 +5364,12 @@ function normalizeConflictGuard(guard) {
|
|
|
5049
5364
|
function parseChangeSet(input) {
|
|
5050
5365
|
return z.union([changeSetV1Schema, changeSetV2Schema, changeSetV3Schema, compensationChangeSetV1Schema]).parse(input);
|
|
5051
5366
|
}
|
|
5367
|
+
function parseFreshnessAuthority(input) {
|
|
5368
|
+
return freshnessAuthorityV1Schema.parse(input);
|
|
5369
|
+
}
|
|
5370
|
+
function parseFreshnessProof(input) {
|
|
5371
|
+
return freshnessProofV1Schema.parse(input);
|
|
5372
|
+
}
|
|
5052
5373
|
function parseWritebackJob(input) {
|
|
5053
5374
|
return writebackJobSchema.parse(input);
|
|
5054
5375
|
}
|
|
@@ -5583,6 +5904,15 @@ var PostgresProposalRuntimeStore = class {
|
|
|
5583
5904
|
async createProposal(input) {
|
|
5584
5905
|
return await this.withWrite("createProposal", (store) => store.createProposal(input));
|
|
5585
5906
|
}
|
|
5907
|
+
async recordFreshnessProof(input) {
|
|
5908
|
+
return await this.withWrite("recordFreshnessProof", (store) => store.recordFreshnessProof(input));
|
|
5909
|
+
}
|
|
5910
|
+
async latestFreshnessProof(proposalId) {
|
|
5911
|
+
return await this.withRead((store) => store.latestFreshnessProof(proposalId));
|
|
5912
|
+
}
|
|
5913
|
+
async recordFreshnessApprovalBlocked(proposalId, input) {
|
|
5914
|
+
await this.withWrite("recordFreshnessApprovalBlocked", (store) => store.recordFreshnessApprovalBlocked(proposalId, input));
|
|
5915
|
+
}
|
|
5586
5916
|
async approveProposalByPolicy(proposalId, options) {
|
|
5587
5917
|
return await this.withWrite("approveProposalByPolicy", (store) => store.approveProposalByPolicy(proposalId, options));
|
|
5588
5918
|
}
|
|
@@ -6031,6 +6361,7 @@ var ProposalStore = class {
|
|
|
6031
6361
|
decision_hash TEXT,
|
|
6032
6362
|
signature TEXT,
|
|
6033
6363
|
integrity_hash TEXT,
|
|
6364
|
+
freshness_proof_digest TEXT,
|
|
6034
6365
|
created_at TEXT NOT NULL,
|
|
6035
6366
|
FOREIGN KEY (proposal_id) REFERENCES proposals(proposal_id)
|
|
6036
6367
|
);
|
|
@@ -6304,6 +6635,7 @@ var ProposalStore = class {
|
|
|
6304
6635
|
this.ensureColumn("approvals", "decision_hash", "TEXT");
|
|
6305
6636
|
this.ensureColumn("approvals", "signature", "TEXT");
|
|
6306
6637
|
this.ensureColumn("approvals", "integrity_hash", "TEXT");
|
|
6638
|
+
this.ensureColumn("approvals", "freshness_proof_digest", "TEXT");
|
|
6307
6639
|
}
|
|
6308
6640
|
ensureSearchIndexes() {
|
|
6309
6641
|
this.db.exec(`
|
|
@@ -6594,6 +6926,75 @@ var ProposalStore = class {
|
|
|
6594
6926
|
const row = this.db.prepare("SELECT proposal_id FROM query_audit WHERE evidence_bundle_id = ? AND proposal_id IS NOT NULL ORDER BY created_at DESC LIMIT 1").get(evidenceBundleId);
|
|
6595
6927
|
return isRecord3(row) && row.proposal_id != null ? String(row.proposal_id) : void 0;
|
|
6596
6928
|
}
|
|
6929
|
+
recordFreshnessProof(input) {
|
|
6930
|
+
const proof = parseFreshnessProof(input);
|
|
6931
|
+
const proposal = this.requireProposal(proof.proposal_id);
|
|
6932
|
+
assertProposalIdentity(proposal, proof.proposal_hash, proof.proposal_version);
|
|
6933
|
+
const authority = proposalFreshnessAuthority(proposal);
|
|
6934
|
+
if (!authority) {
|
|
6935
|
+
throw new ProposalStoreError(
|
|
6936
|
+
"FRESHNESS_NOT_REQUIRED",
|
|
6937
|
+
`proposal ${proof.proposal_id} has no reviewed freshness authority`
|
|
6938
|
+
);
|
|
6939
|
+
}
|
|
6940
|
+
if (proof.dependency_set_digest !== authority.dependency_set_digest) {
|
|
6941
|
+
throw new ProposalStoreError(
|
|
6942
|
+
"FRESHNESS_PROOF_AUTHORITY_MISMATCH",
|
|
6943
|
+
`freshness proof does not match proposal ${proof.proposal_id}`
|
|
6944
|
+
);
|
|
6945
|
+
}
|
|
6946
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
6947
|
+
this.transaction(() => {
|
|
6948
|
+
this.appendEvent(proof.proposal_id, "proposal_freshness_checked", "runner", { proof });
|
|
6949
|
+
if (proof.result === "stale") {
|
|
6950
|
+
const current = this.requireProposal(proof.proposal_id);
|
|
6951
|
+
if (current.state === "pending_review" || current.state === "approved" || current.state === "pending_worker") {
|
|
6952
|
+
this.db.prepare("UPDATE proposals SET state = ?, updated_at = ? WHERE proposal_id = ?").run("conflict", now, proof.proposal_id);
|
|
6953
|
+
this.appendEvent(proof.proposal_id, "proposal_conflict", "runner", {
|
|
6954
|
+
reason: "freshness_stale",
|
|
6955
|
+
safe_code: proof.safe_code,
|
|
6956
|
+
proof_digest: proof.proof_digest
|
|
6957
|
+
});
|
|
6958
|
+
}
|
|
6959
|
+
}
|
|
6960
|
+
});
|
|
6961
|
+
return proof;
|
|
6962
|
+
}
|
|
6963
|
+
latestFreshnessProof(proposalId) {
|
|
6964
|
+
this.requireProposal(proposalId);
|
|
6965
|
+
const row = this.db.prepare(`
|
|
6966
|
+
SELECT payload_json
|
|
6967
|
+
FROM proposal_events
|
|
6968
|
+
WHERE proposal_id = ? AND kind = 'proposal_freshness_checked'
|
|
6969
|
+
ORDER BY event_id DESC
|
|
6970
|
+
LIMIT 1
|
|
6971
|
+
`).get(proposalId);
|
|
6972
|
+
if (!isRecord3(row)) return void 0;
|
|
6973
|
+
try {
|
|
6974
|
+
const payload = JSON.parse(String(row.payload_json));
|
|
6975
|
+
return parseFreshnessProof(payload.proof);
|
|
6976
|
+
} catch {
|
|
6977
|
+
throw new ProposalStoreError(
|
|
6978
|
+
"FRESHNESS_PROOF_TAMPERED",
|
|
6979
|
+
`stored freshness proof for proposal ${proposalId} failed integrity validation`
|
|
6980
|
+
);
|
|
6981
|
+
}
|
|
6982
|
+
}
|
|
6983
|
+
recordFreshnessApprovalBlocked(proposalId, input) {
|
|
6984
|
+
this.requireProposal(proposalId);
|
|
6985
|
+
const proof = this.latestFreshnessProof(proposalId);
|
|
6986
|
+
if (!proof || proof.proof_digest !== input.proof_digest || proof.result === "fresh") {
|
|
6987
|
+
throw new ProposalStoreError(
|
|
6988
|
+
"FRESHNESS_BLOCK_RECORD_INVALID",
|
|
6989
|
+
`freshness block for proposal ${proposalId} does not match its latest non-fresh proof`
|
|
6990
|
+
);
|
|
6991
|
+
}
|
|
6992
|
+
this.appendEvent(proposalId, "proposal_approval_blocked_freshness", input.actor, {
|
|
6993
|
+
proof_digest: proof.proof_digest,
|
|
6994
|
+
safe_code: input.safe_code,
|
|
6995
|
+
result: proof.result
|
|
6996
|
+
});
|
|
6997
|
+
}
|
|
6597
6998
|
approveProposal(proposalId, options) {
|
|
6598
6999
|
const proposal = this.requireProposal(proposalId);
|
|
6599
7000
|
assertWritebackAllowed(proposal, "approved");
|
|
@@ -6614,11 +7015,12 @@ var ProposalStore = class {
|
|
|
6614
7015
|
if (isRecord3(existing)) {
|
|
6615
7016
|
throw new ProposalStoreError("APPROVER_ALREADY_COUNTED", `operator ${options.approver} already recorded a decision for proposal ${proposalId}`);
|
|
6616
7017
|
}
|
|
7018
|
+
this.assertApprovalFreshness(current, options.freshness_proof_digest, now);
|
|
6617
7019
|
this.db.prepare(`
|
|
6618
7020
|
INSERT INTO approvals (
|
|
6619
7021
|
proposal_id, proposal_version, proposal_hash, approver, status, reason,
|
|
6620
|
-
identity_json, decision_hash, signature, integrity_hash, created_at
|
|
6621
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7022
|
+
identity_json, decision_hash, signature, integrity_hash, freshness_proof_digest, created_at
|
|
7023
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
6622
7024
|
`).run(
|
|
6623
7025
|
proposalId,
|
|
6624
7026
|
options.proposal_version,
|
|
@@ -6630,6 +7032,7 @@ var ProposalStore = class {
|
|
|
6630
7032
|
options.identity?.decision_hash ?? null,
|
|
6631
7033
|
options.identity?.signature ?? null,
|
|
6632
7034
|
options.identity?.integrity_hash ?? null,
|
|
7035
|
+
options.freshness_proof_digest ?? null,
|
|
6633
7036
|
now
|
|
6634
7037
|
);
|
|
6635
7038
|
const progress = this.approvalProgress(proposalId);
|
|
@@ -6642,6 +7045,7 @@ var ProposalStore = class {
|
|
|
6642
7045
|
proposal_version: options.proposal_version,
|
|
6643
7046
|
reason: options.reason ?? null,
|
|
6644
7047
|
identity: publicIdentitySummary(options.identity),
|
|
7048
|
+
freshness_proof_digest: options.freshness_proof_digest ?? null,
|
|
6645
7049
|
approvals: progress.approved,
|
|
6646
7050
|
required_approvals: progress.required,
|
|
6647
7051
|
remaining_approvals: progress.remaining
|
|
@@ -6750,17 +7154,31 @@ var ProposalStore = class {
|
|
|
6750
7154
|
});
|
|
6751
7155
|
return;
|
|
6752
7156
|
}
|
|
7157
|
+
this.assertApprovalFreshness(proposal, options.freshness_proof_digest, now);
|
|
6753
7158
|
this.db.prepare("UPDATE proposals SET state = ?, updated_at = ? WHERE proposal_id = ?").run("approved", now, proposalId);
|
|
6754
7159
|
this.db.prepare(`
|
|
6755
|
-
INSERT INTO approvals (
|
|
6756
|
-
|
|
6757
|
-
|
|
7160
|
+
INSERT INTO approvals (
|
|
7161
|
+
proposal_id, proposal_version, proposal_hash, approver, status, reason,
|
|
7162
|
+
freshness_proof_digest, created_at
|
|
7163
|
+
)
|
|
7164
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
7165
|
+
`).run(
|
|
7166
|
+
proposalId,
|
|
7167
|
+
options.proposal_version,
|
|
7168
|
+
options.proposal_hash,
|
|
7169
|
+
actor,
|
|
7170
|
+
"approved",
|
|
7171
|
+
options.reason,
|
|
7172
|
+
options.freshness_proof_digest ?? null,
|
|
7173
|
+
now
|
|
7174
|
+
);
|
|
6758
7175
|
this.appendEvent(proposalId, "proposal_approved", actor, {
|
|
6759
7176
|
proposal_hash: options.proposal_hash,
|
|
6760
7177
|
proposal_version: options.proposal_version,
|
|
6761
7178
|
reason: options.reason,
|
|
6762
7179
|
policy: options.policy,
|
|
6763
|
-
aggregate_limits: options.limits ?? []
|
|
7180
|
+
aggregate_limits: options.limits ?? [],
|
|
7181
|
+
freshness_proof_digest: options.freshness_proof_digest ?? null
|
|
6764
7182
|
});
|
|
6765
7183
|
});
|
|
6766
7184
|
return {
|
|
@@ -7108,7 +7526,8 @@ var ProposalStore = class {
|
|
|
7108
7526
|
writeback_job_id: receipt.writeback_job_id,
|
|
7109
7527
|
rows_affected: receipt.rows_affected,
|
|
7110
7528
|
source_database_mutated: receipt.source_database_mutated,
|
|
7111
|
-
receipt_hash: receipt.receipt_hash
|
|
7529
|
+
receipt_hash: receipt.receipt_hash,
|
|
7530
|
+
safe_error_code: "safe_error_code" in receipt ? receipt.safe_error_code ?? null : null
|
|
7112
7531
|
});
|
|
7113
7532
|
}
|
|
7114
7533
|
recordHandlerWritebackJob(input) {
|
|
@@ -7192,6 +7611,7 @@ var ProposalStore = class {
|
|
|
7192
7611
|
tenant_guard: changeSet.guards.tenant,
|
|
7193
7612
|
...changeSet.guards.principal_scope ? { principal_scope: changeSet.guards.principal_scope } : {},
|
|
7194
7613
|
allowed_columns: changeSet.guards.allowed_columns,
|
|
7614
|
+
..."freshness" in changeSet && changeSet.freshness ? { freshness: parseFreshnessAuthority(changeSet.freshness) } : {},
|
|
7195
7615
|
idempotency_key: `${proposal.proposal_id}:${proposal.object_id}`,
|
|
7196
7616
|
lease
|
|
7197
7617
|
};
|
|
@@ -7645,7 +8065,21 @@ var ProposalStore = class {
|
|
|
7645
8065
|
const key = `${tenantId}\0${capability}`;
|
|
7646
8066
|
let row = rows.get(key);
|
|
7647
8067
|
if (!row) {
|
|
7648
|
-
row = {
|
|
8068
|
+
row = {
|
|
8069
|
+
tenant_id: tenantId,
|
|
8070
|
+
capability,
|
|
8071
|
+
worker_retries: 0,
|
|
8072
|
+
dead_letters: 0,
|
|
8073
|
+
auto_approval_limit_trips: 0,
|
|
8074
|
+
freshness_checks: 0,
|
|
8075
|
+
freshness_fresh: 0,
|
|
8076
|
+
freshness_stale_target: 0,
|
|
8077
|
+
freshness_stale_supporting: 0,
|
|
8078
|
+
freshness_unavailable: 0,
|
|
8079
|
+
freshness_unsupported: 0,
|
|
8080
|
+
freshness_approval_blocked: 0,
|
|
8081
|
+
freshness_apply_blocked: 0
|
|
8082
|
+
};
|
|
7649
8083
|
rows.set(key, row);
|
|
7650
8084
|
}
|
|
7651
8085
|
return row;
|
|
@@ -7653,7 +8087,14 @@ var ProposalStore = class {
|
|
|
7653
8087
|
const events = this.db.prepare(`
|
|
7654
8088
|
SELECT p.tenant_id, p.action, e.kind, e.payload_json
|
|
7655
8089
|
FROM proposal_events e JOIN proposals p ON p.proposal_id = e.proposal_id
|
|
7656
|
-
WHERE e.kind IN (
|
|
8090
|
+
WHERE e.kind IN (
|
|
8091
|
+
'writeback_retry_scheduled',
|
|
8092
|
+
'writeback_dead_lettered',
|
|
8093
|
+
'policy_auto_approval_deferred',
|
|
8094
|
+
'proposal_freshness_checked',
|
|
8095
|
+
'proposal_approval_blocked_freshness',
|
|
8096
|
+
'writeback_conflict'
|
|
8097
|
+
)
|
|
7657
8098
|
AND (? IS NULL OR p.tenant_id = ?)
|
|
7658
8099
|
AND (? IS NULL OR p.action = ?)
|
|
7659
8100
|
`).all(filters.tenant ?? null, filters.tenant ?? null, filters.capability ?? null, filters.capability ?? null);
|
|
@@ -7662,13 +8103,29 @@ var ProposalStore = class {
|
|
|
7662
8103
|
const row = ensure(String(raw.tenant_id), String(raw.action));
|
|
7663
8104
|
if (raw.kind === "writeback_retry_scheduled") row.worker_retries += 1;
|
|
7664
8105
|
if (raw.kind === "writeback_dead_lettered") row.dead_letters += 1;
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
8106
|
+
let payload = {};
|
|
8107
|
+
try {
|
|
8108
|
+
const parsed = JSON.parse(String(raw.payload_json));
|
|
8109
|
+
if (isRecord3(parsed)) payload = parsed;
|
|
8110
|
+
} catch {
|
|
8111
|
+
}
|
|
8112
|
+
if (raw.kind === "proposal_freshness_checked") {
|
|
8113
|
+
row.freshness_checks += 1;
|
|
8114
|
+
const proof = isRecord3(payload.proof) ? payload.proof : {};
|
|
8115
|
+
if (proof.result === "fresh") row.freshness_fresh += 1;
|
|
8116
|
+
if (proof.result === "unavailable") row.freshness_unavailable += 1;
|
|
8117
|
+
if (proof.result === "unsupported") row.freshness_unsupported += 1;
|
|
8118
|
+
if (proof.result === "stale") {
|
|
8119
|
+
const checks = Array.isArray(proof.checks) ? proof.checks.filter(isRecord3) : [];
|
|
8120
|
+
if (checks.some((check) => check.kind === "target" && check.status === "stale")) row.freshness_stale_target += 1;
|
|
8121
|
+
if (checks.some((check) => check.kind === "supporting" && check.status === "stale")) row.freshness_stale_supporting += 1;
|
|
7670
8122
|
}
|
|
7671
8123
|
}
|
|
8124
|
+
if (raw.kind === "proposal_approval_blocked_freshness") row.freshness_approval_blocked += 1;
|
|
8125
|
+
if (raw.kind === "writeback_conflict" && /^FRESHNESS_/.test(String(payload.safe_error_code ?? ""))) row.freshness_apply_blocked += 1;
|
|
8126
|
+
if (raw.kind === "policy_auto_approval_deferred") {
|
|
8127
|
+
if (Array.isArray(payload.tripped_limits) && payload.tripped_limits.length > 0) row.auto_approval_limit_trips += 1;
|
|
8128
|
+
}
|
|
7672
8129
|
}
|
|
7673
8130
|
return [...rows.values()].sort((left, right) => left.tenant_id.localeCompare(right.tenant_id) || left.capability.localeCompare(right.capability));
|
|
7674
8131
|
}
|
|
@@ -8516,6 +8973,61 @@ var ProposalStore = class {
|
|
|
8516
8973
|
}
|
|
8517
8974
|
return proposal;
|
|
8518
8975
|
}
|
|
8976
|
+
assertApprovalFreshness(proposal, proofDigest, now) {
|
|
8977
|
+
const authority = proposalFreshnessAuthority(proposal);
|
|
8978
|
+
if (!authority) {
|
|
8979
|
+
if (proofDigest !== void 0) {
|
|
8980
|
+
throw new ProposalStoreError(
|
|
8981
|
+
"FRESHNESS_PROOF_UNEXPECTED",
|
|
8982
|
+
`proposal ${proposal.proposal_id} does not require a freshness proof`
|
|
8983
|
+
);
|
|
8984
|
+
}
|
|
8985
|
+
return;
|
|
8986
|
+
}
|
|
8987
|
+
if (!proofDigest) {
|
|
8988
|
+
throw new ProposalStoreError(
|
|
8989
|
+
"FRESHNESS_PROOF_REQUIRED",
|
|
8990
|
+
`proposal ${proposal.proposal_id} requires a fresh live proof before approval`
|
|
8991
|
+
);
|
|
8992
|
+
}
|
|
8993
|
+
const proof = this.latestFreshnessProof(proposal.proposal_id);
|
|
8994
|
+
if (!proof || proof.proof_digest !== proofDigest) {
|
|
8995
|
+
throw new ProposalStoreError(
|
|
8996
|
+
"FRESHNESS_PROOF_MISSING",
|
|
8997
|
+
`the latest freshness proof for proposal ${proposal.proposal_id} was not supplied`
|
|
8998
|
+
);
|
|
8999
|
+
}
|
|
9000
|
+
if (proof.proposal_hash !== proposal.proposal_hash || proof.proposal_version !== proposal.proposal_version || proof.dependency_set_digest !== authority.dependency_set_digest) {
|
|
9001
|
+
throw new ProposalStoreError(
|
|
9002
|
+
"FRESHNESS_PROOF_AUTHORITY_MISMATCH",
|
|
9003
|
+
`freshness proof does not match proposal ${proposal.proposal_id}`
|
|
9004
|
+
);
|
|
9005
|
+
}
|
|
9006
|
+
if (proof.result !== "fresh") {
|
|
9007
|
+
throw new ProposalStoreError(
|
|
9008
|
+
proof.result === "stale" ? "FRESHNESS_STALE" : "FRESHNESS_NOT_VERIFIED",
|
|
9009
|
+
`proposal ${proposal.proposal_id} freshness result is ${proof.result}`
|
|
9010
|
+
);
|
|
9011
|
+
}
|
|
9012
|
+
if (Date.parse(proof.valid_until) < Date.parse(now)) {
|
|
9013
|
+
throw new ProposalStoreError(
|
|
9014
|
+
"FRESHNESS_PROOF_EXPIRED",
|
|
9015
|
+
`freshness proof for proposal ${proposal.proposal_id} has expired`
|
|
9016
|
+
);
|
|
9017
|
+
}
|
|
9018
|
+
const used = this.db.prepare(`
|
|
9019
|
+
SELECT approval_id
|
|
9020
|
+
FROM approvals
|
|
9021
|
+
WHERE proposal_id = ? AND freshness_proof_digest = ?
|
|
9022
|
+
LIMIT 1
|
|
9023
|
+
`).get(proposal.proposal_id, proofDigest);
|
|
9024
|
+
if (isRecord3(used)) {
|
|
9025
|
+
throw new ProposalStoreError(
|
|
9026
|
+
"FRESHNESS_PROOF_ALREADY_USED",
|
|
9027
|
+
`freshness proof for proposal ${proposal.proposal_id} already authorized a reviewer decision`
|
|
9028
|
+
);
|
|
9029
|
+
}
|
|
9030
|
+
}
|
|
8519
9031
|
setState(proposalId, state, actor, payload) {
|
|
8520
9032
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8521
9033
|
this.transaction(() => {
|
|
@@ -8676,6 +9188,17 @@ function stateFromChangeSet(changeSet) {
|
|
|
8676
9188
|
if (changeSet.approval.status === "canceled") return "canceled";
|
|
8677
9189
|
return "pending_review";
|
|
8678
9190
|
}
|
|
9191
|
+
function proposalFreshnessAuthority(proposal) {
|
|
9192
|
+
if (!("freshness" in proposal.change_set) || proposal.change_set.freshness === void 0) return void 0;
|
|
9193
|
+
try {
|
|
9194
|
+
return parseFreshnessAuthority(proposal.change_set.freshness);
|
|
9195
|
+
} catch {
|
|
9196
|
+
throw new ProposalStoreError(
|
|
9197
|
+
"FRESHNESS_AUTHORITY_TAMPERED",
|
|
9198
|
+
`stored freshness authority for proposal ${proposal.proposal_id} failed integrity validation`
|
|
9199
|
+
);
|
|
9200
|
+
}
|
|
9201
|
+
}
|
|
8679
9202
|
function requiredApprovalCount(proposal) {
|
|
8680
9203
|
const configured = proposal.change_set.approval.required_approvals;
|
|
8681
9204
|
return typeof configured === "number" && Number.isSafeInteger(configured) && configured >= 1 ? configured : 1;
|
|
@@ -9202,6 +9725,7 @@ function rowToApproval(row) {
|
|
|
9202
9725
|
decision_hash: row.decision_hash == null ? void 0 : String(row.decision_hash),
|
|
9203
9726
|
signature: row.signature == null ? void 0 : String(row.signature),
|
|
9204
9727
|
integrity_hash: row.integrity_hash == null ? void 0 : String(row.integrity_hash),
|
|
9728
|
+
freshness_proof_digest: row.freshness_proof_digest == null ? void 0 : String(row.freshness_proof_digest),
|
|
9205
9729
|
created_at: String(row.created_at)
|
|
9206
9730
|
};
|
|
9207
9731
|
}
|
|
@@ -9578,7 +10102,7 @@ var sharedLedgerRestoreSpecs = {
|
|
|
9578
10102
|
"updated_at"
|
|
9579
10103
|
], ["proposal_id", "proposal_version", "proposal_hash", "action", "state", "tenant_id", "business_object", "object_id", "source_kind", "source_id", "source_schema", "source_table", "source_database_mutated", "change_set_json", "created_at", "updated_at"]),
|
|
9580
10104
|
proposal_events: restoreSpec("event_id", ["event_id", "proposal_id", "kind", "actor", "payload_json", "created_at"], ["event_id", "proposal_id", "kind", "actor", "payload_json", "created_at"]),
|
|
9581
|
-
approvals: restoreSpec("approval_id", ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "reason", "identity_json", "decision_hash", "signature", "integrity_hash", "created_at"], ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "created_at"]),
|
|
10105
|
+
approvals: restoreSpec("approval_id", ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "reason", "identity_json", "decision_hash", "signature", "integrity_hash", "freshness_proof_digest", "created_at"], ["approval_id", "proposal_id", "proposal_version", "proposal_hash", "approver", "status", "created_at"]),
|
|
9582
10106
|
writeback_jobs: restoreSpec("writeback_job_id", ["writeback_job_id", "proposal_id", "proposal_hash", "status", "job_json", "created_at", "updated_at"], ["writeback_job_id", "proposal_id", "proposal_hash", "status", "job_json", "created_at", "updated_at"]),
|
|
9583
10107
|
writeback_intents: restoreSpec("intent_id", ["intent_id", "idempotency_key", "writeback_job_id", "proposal_id", "proposal_hash", "runner_id", "operation", "status", "intent_json", "result_json", "reconciliation_reason", "created_at", "updated_at"], ["intent_id", "idempotency_key", "writeback_job_id", "proposal_id", "proposal_hash", "runner_id", "operation", "status", "intent_json", "created_at", "updated_at"]),
|
|
9584
10108
|
idempotency_receipts: restoreSpec("idempotency_key", ["idempotency_key", "writeback_job_id", "proposal_id", "receipt_status", "receipt_json", "created_at"], ["idempotency_key", "writeback_job_id", "proposal_id", "receipt_status", "receipt_json", "created_at"]),
|
|
@@ -15224,6 +15748,18 @@ async function callConfiguredTool(input) {
|
|
|
15224
15748
|
at: createdAt
|
|
15225
15749
|
});
|
|
15226
15750
|
const queryFingerprint = queryFingerprintFor(capability, context);
|
|
15751
|
+
const freshnessCapture = capability.kind === "proposal" ? await captureProposalFreshnessAuthority({
|
|
15752
|
+
config: input.config,
|
|
15753
|
+
capability,
|
|
15754
|
+
args: input.args,
|
|
15755
|
+
context,
|
|
15756
|
+
source,
|
|
15757
|
+
readRow: input.readRow,
|
|
15758
|
+
env: input.env,
|
|
15759
|
+
proposalId,
|
|
15760
|
+
createdAt,
|
|
15761
|
+
targetMemberCount: operation === "insert" ? 0 : setOperation ? currentRows.length : 1
|
|
15762
|
+
}) : void 0;
|
|
15227
15763
|
const changeSet = capability.kind === "proposal" ? buildChangeSet({
|
|
15228
15764
|
config: input.config,
|
|
15229
15765
|
capability,
|
|
@@ -15241,7 +15777,8 @@ async function callConfiguredTool(input) {
|
|
|
15241
15777
|
resolvedDeduplication,
|
|
15242
15778
|
evidenceBundleId,
|
|
15243
15779
|
queryFingerprint,
|
|
15244
|
-
objectId
|
|
15780
|
+
objectId,
|
|
15781
|
+
freshness: freshnessCapture?.authority
|
|
15245
15782
|
}) : void 0;
|
|
15246
15783
|
await input.store.recordEvidenceBundle({
|
|
15247
15784
|
evidence_bundle_id: evidenceBundleId,
|
|
@@ -15286,6 +15823,48 @@ async function callConfiguredTool(input) {
|
|
|
15286
15823
|
}
|
|
15287
15824
|
});
|
|
15288
15825
|
}
|
|
15826
|
+
for (const supporting of freshnessCapture?.evidence ?? []) {
|
|
15827
|
+
await input.store.recordEvidenceBundle({
|
|
15828
|
+
evidence_bundle_id: supporting.bundle_id,
|
|
15829
|
+
tenant_id: context.tenant_id,
|
|
15830
|
+
capability: supporting.capability.name,
|
|
15831
|
+
source_id: supporting.capability.source,
|
|
15832
|
+
source_table: `${supporting.capability.target.schema}.${supporting.capability.target.table}`,
|
|
15833
|
+
business_object: supporting.capability.target.table,
|
|
15834
|
+
object_id: String(supporting.primary_key.value),
|
|
15835
|
+
query_fingerprint: supporting.query_fingerprint,
|
|
15836
|
+
payload: {
|
|
15837
|
+
kind: "freshness_dependency",
|
|
15838
|
+
dependency_id: supporting.dependency_id,
|
|
15839
|
+
capability: supporting.capability.name,
|
|
15840
|
+
source_id: supporting.capability.source,
|
|
15841
|
+
target: `${supporting.capability.target.schema}.${supporting.capability.target.table}`,
|
|
15842
|
+
parameters_redacted: true,
|
|
15843
|
+
source_database_changed: false
|
|
15844
|
+
},
|
|
15845
|
+
items: []
|
|
15846
|
+
});
|
|
15847
|
+
await input.store.recordQueryAudit({
|
|
15848
|
+
evidence_bundle_id: supporting.bundle_id,
|
|
15849
|
+
capability: supporting.capability.name,
|
|
15850
|
+
source_id: supporting.capability.source,
|
|
15851
|
+
query_fingerprint: supporting.query_fingerprint,
|
|
15852
|
+
table_name: `${supporting.capability.target.schema}.${supporting.capability.target.table}`,
|
|
15853
|
+
business_object: supporting.capability.target.table,
|
|
15854
|
+
object_id: String(supporting.primary_key.value),
|
|
15855
|
+
primary_key_value: String(supporting.primary_key.value),
|
|
15856
|
+
row_count: 1,
|
|
15857
|
+
payload: {
|
|
15858
|
+
kind: "freshness_dependency",
|
|
15859
|
+
dependency_id: supporting.dependency_id,
|
|
15860
|
+
capability: supporting.capability.name,
|
|
15861
|
+
columns: [supporting.version_column],
|
|
15862
|
+
parameters_redacted: true,
|
|
15863
|
+
tenant_bound: Boolean(supporting.capability.target.tenant_key),
|
|
15864
|
+
principal_bound: Boolean(supporting.capability.target.principal_scope_key)
|
|
15865
|
+
}
|
|
15866
|
+
});
|
|
15867
|
+
}
|
|
15289
15868
|
if (capability.kind === "read") {
|
|
15290
15869
|
return {
|
|
15291
15870
|
status: "ok",
|
|
@@ -15335,7 +15914,9 @@ async function callConfiguredTool(input) {
|
|
|
15335
15914
|
capability,
|
|
15336
15915
|
store: input.store,
|
|
15337
15916
|
proposal,
|
|
15338
|
-
patch: changeSet.patch
|
|
15917
|
+
patch: changeSet.patch,
|
|
15918
|
+
env: input.env,
|
|
15919
|
+
readRow: input.readRow
|
|
15339
15920
|
});
|
|
15340
15921
|
await input.store.recordEvidenceBundle({
|
|
15341
15922
|
evidence_bundle_id: evidenceBundleId,
|
|
@@ -15383,7 +15964,7 @@ async function callConfiguredTool(input) {
|
|
|
15383
15964
|
env: input.env
|
|
15384
15965
|
});
|
|
15385
15966
|
return {
|
|
15386
|
-
status: input.config.governance?.mode === "cloud_linked" ? "pending_cloud_sync" : input.config.mode === "shadow" ? "shadow_proposal_created" : approvalResult.proposal.state === "approved" ? "approved" : "review_required",
|
|
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",
|
|
15387
15968
|
action: capability.name,
|
|
15388
15969
|
proposal_id: approvalResult.proposal.proposal_id,
|
|
15389
15970
|
proposal_version: approvalResult.proposal.proposal_version,
|
|
@@ -15408,6 +15989,7 @@ async function callConfiguredTool(input) {
|
|
|
15408
15989
|
} : {}
|
|
15409
15990
|
},
|
|
15410
15991
|
approval_required: approvalResult.proposal.state === "pending_review",
|
|
15992
|
+
...approvalResult.freshness ? { freshness: approvalResult.freshness } : {},
|
|
15411
15993
|
governance: input.config.governance?.mode === "cloud_linked" ? { authority: "synapsor_cloud", state: "pending_cloud_sync", evidence_residency: "metadata_only" } : { authority: "local" },
|
|
15412
15994
|
writeback: changeSet.writeback,
|
|
15413
15995
|
source_database_changed: false,
|
|
@@ -15890,18 +16472,52 @@ async function maybeAutoApproveProposal(input) {
|
|
|
15890
16472
|
if (!policy) throw new McpRuntimeError("APPROVAL_POLICY_UNRESOLVED", `capability ${input.capability.name} references missing approval policy ${policyName}.`);
|
|
15891
16473
|
const evaluation = evaluateApprovalPolicy(input.capability, policy, input.patch);
|
|
15892
16474
|
if (!evaluation.qualifies) return { proposal: input.proposal, approved: false, policy: policyName };
|
|
16475
|
+
let freshnessProofDigest;
|
|
16476
|
+
const freshness = await evaluateProposalFreshness({
|
|
16477
|
+
config: input.config,
|
|
16478
|
+
proposal: input.proposal,
|
|
16479
|
+
env: input.env,
|
|
16480
|
+
readRow: input.readRow
|
|
16481
|
+
});
|
|
16482
|
+
if (freshness.required) {
|
|
16483
|
+
await input.store.recordFreshnessProof(freshness.proof);
|
|
16484
|
+
if (freshness.status !== "fresh") {
|
|
16485
|
+
await input.store.recordFreshnessApprovalBlocked(input.proposal.proposal_id, {
|
|
16486
|
+
proof_digest: freshness.proof.proof_digest,
|
|
16487
|
+
safe_code: freshness.safe_code,
|
|
16488
|
+
actor: `policy:${policyName}`
|
|
16489
|
+
});
|
|
16490
|
+
return {
|
|
16491
|
+
proposal: await input.store.getProposal(input.proposal.proposal_id) ?? input.proposal,
|
|
16492
|
+
approved: false,
|
|
16493
|
+
policy: policyName,
|
|
16494
|
+
freshness: {
|
|
16495
|
+
status: freshness.status,
|
|
16496
|
+
safe_code: freshness.safe_code
|
|
16497
|
+
}
|
|
16498
|
+
};
|
|
16499
|
+
}
|
|
16500
|
+
freshnessProofDigest = freshness.proof.proof_digest;
|
|
16501
|
+
}
|
|
15893
16502
|
const decision = await input.store.approveProposalByPolicy(input.proposal.proposal_id, {
|
|
15894
16503
|
policy: policyName,
|
|
15895
16504
|
proposal_hash: input.proposal.proposal_hash,
|
|
15896
16505
|
proposal_version: input.proposal.proposal_version,
|
|
15897
16506
|
reason: `auto-approved by policy ${policyName}: ${evaluation.reason}`,
|
|
15898
|
-
limits: policy.limits
|
|
16507
|
+
limits: policy.limits,
|
|
16508
|
+
freshness_proof_digest: freshnessProofDigest
|
|
15899
16509
|
});
|
|
15900
16510
|
return {
|
|
15901
16511
|
proposal: decision.proposal,
|
|
15902
16512
|
approved: decision.approved,
|
|
15903
16513
|
policy: policyName,
|
|
15904
|
-
tripped_limits: decision.tripped_limits
|
|
16514
|
+
tripped_limits: decision.tripped_limits,
|
|
16515
|
+
...freshness.required ? {
|
|
16516
|
+
freshness: {
|
|
16517
|
+
status: freshness.status,
|
|
16518
|
+
safe_code: freshness.safe_code
|
|
16519
|
+
}
|
|
16520
|
+
} : {}
|
|
15905
16521
|
};
|
|
15906
16522
|
}
|
|
15907
16523
|
function approvalPolicyByName(config, policyName) {
|
|
@@ -16169,6 +16785,372 @@ function errorMessage(error) {
|
|
|
16169
16785
|
if (isRecord5(error) && typeof error.message === "string") return error.message;
|
|
16170
16786
|
return typeof error === "string" ? error : "";
|
|
16171
16787
|
}
|
|
16788
|
+
async function captureProposalFreshnessAuthority(input) {
|
|
16789
|
+
const policy = input.config.proposal_freshness?.[input.capability.name];
|
|
16790
|
+
if (!policy) return void 0;
|
|
16791
|
+
const operation = input.capability.operation?.kind ?? "update";
|
|
16792
|
+
const targetMode = operation === "insert" ? "not_applicable" : isSetCapability(input.capability) ? "frozen_set" : "exact_guard";
|
|
16793
|
+
const captured = [];
|
|
16794
|
+
for (const configured of policy.dependencies ?? []) {
|
|
16795
|
+
const capability = (input.config.capabilities ?? []).find((item) => item.name === configured.capability);
|
|
16796
|
+
if (!capability || capability.kind !== "read") {
|
|
16797
|
+
throw new McpRuntimeError("FRESHNESS_DEPENDENCY_INVALID", `Reviewed freshness dependency ${configured.id} is unavailable.`);
|
|
16798
|
+
}
|
|
16799
|
+
const identity = scalar2(input.args[configured.identity_from_arg]);
|
|
16800
|
+
if (identity === null) {
|
|
16801
|
+
throw new McpRuntimeError("FRESHNESS_DEPENDENCY_IDENTITY_MISSING", `Freshness dependency ${configured.id} requires its reviewed scalar identity.`);
|
|
16802
|
+
}
|
|
16803
|
+
const readCapability = {
|
|
16804
|
+
...capability,
|
|
16805
|
+
conflict_guard: { column: configured.version_column }
|
|
16806
|
+
};
|
|
16807
|
+
const current = await input.readRow({
|
|
16808
|
+
sourceName: capability.source,
|
|
16809
|
+
source: input.source,
|
|
16810
|
+
capability: readCapability,
|
|
16811
|
+
args: { [capability.lookup.id_from_arg]: identity },
|
|
16812
|
+
context: input.context,
|
|
16813
|
+
env: input.env,
|
|
16814
|
+
transaction_mode: "read_only"
|
|
16815
|
+
});
|
|
16816
|
+
if (current.rowCount !== 1) {
|
|
16817
|
+
throw new McpRuntimeError("FRESHNESS_DEPENDENCY_UNRESOLVED", `Freshness dependency ${configured.id} did not resolve to one authorized row.`);
|
|
16818
|
+
}
|
|
16819
|
+
const primaryValue = scalar2(current.row[capability.target.primary_key]);
|
|
16820
|
+
const versionValue = scalar2(current.row[configured.version_column]);
|
|
16821
|
+
if (primaryValue === null || versionValue === null) {
|
|
16822
|
+
throw new McpRuntimeError("FRESHNESS_DEPENDENCY_VERSION_MISSING", `Freshness dependency ${configured.id} lacks an exact primary-key or version value.`);
|
|
16823
|
+
}
|
|
16824
|
+
const bundleId = stableId("evf", {
|
|
16825
|
+
proposal_id: input.proposalId,
|
|
16826
|
+
dependency_id: configured.id,
|
|
16827
|
+
primary_key: primaryValue,
|
|
16828
|
+
created_at: input.createdAt
|
|
16829
|
+
});
|
|
16830
|
+
const queryFingerprint = queryFingerprintFor(readCapability, input.context);
|
|
16831
|
+
const unsigned2 = {
|
|
16832
|
+
id: configured.id,
|
|
16833
|
+
capability: configured.capability,
|
|
16834
|
+
source_id: capability.source,
|
|
16835
|
+
engine: input.source.engine,
|
|
16836
|
+
target: {
|
|
16837
|
+
schema: capability.target.schema,
|
|
16838
|
+
table: capability.target.table,
|
|
16839
|
+
primary_key: { column: capability.target.primary_key, value: primaryValue },
|
|
16840
|
+
tenant_column: capability.target.tenant_key ?? "__single_tenant_dev",
|
|
16841
|
+
...capability.target.principal_scope_key ? { principal_column: capability.target.principal_scope_key } : {}
|
|
16842
|
+
},
|
|
16843
|
+
expected_version: { column: configured.version_column, value: conflictGuardScalar(versionValue) },
|
|
16844
|
+
evidence: {
|
|
16845
|
+
bundle_id: bundleId,
|
|
16846
|
+
query_fingerprint: queryFingerprint
|
|
16847
|
+
}
|
|
16848
|
+
};
|
|
16849
|
+
const descriptor = {
|
|
16850
|
+
...unsigned2,
|
|
16851
|
+
descriptor_digest: canonicalJsonDigest(unsigned2)
|
|
16852
|
+
};
|
|
16853
|
+
captured.push({
|
|
16854
|
+
descriptor,
|
|
16855
|
+
evidence: {
|
|
16856
|
+
dependency_id: configured.id,
|
|
16857
|
+
bundle_id: bundleId,
|
|
16858
|
+
query_fingerprint: queryFingerprint,
|
|
16859
|
+
capability,
|
|
16860
|
+
primary_key: descriptor.target.primary_key,
|
|
16861
|
+
version_column: configured.version_column
|
|
16862
|
+
}
|
|
16863
|
+
});
|
|
16864
|
+
}
|
|
16865
|
+
captured.sort((left, right) => left.descriptor.source_id.localeCompare(right.descriptor.source_id) || left.descriptor.target.schema.localeCompare(right.descriptor.target.schema) || left.descriptor.target.table.localeCompare(right.descriptor.target.table) || JSON.stringify(left.descriptor.target.primary_key.value).localeCompare(JSON.stringify(right.descriptor.target.primary_key.value)) || left.descriptor.id.localeCompare(right.descriptor.id));
|
|
16866
|
+
const unsigned = {
|
|
16867
|
+
schema_version: protocolVersions.freshnessAuthority,
|
|
16868
|
+
required: true,
|
|
16869
|
+
target: { mode: targetMode, member_count: input.targetMemberCount },
|
|
16870
|
+
dependencies: captured.map((item) => item.descriptor)
|
|
16871
|
+
};
|
|
16872
|
+
const authority = parseFreshnessAuthority({
|
|
16873
|
+
...unsigned,
|
|
16874
|
+
dependency_set_digest: canonicalJsonDigest(unsigned)
|
|
16875
|
+
});
|
|
16876
|
+
return {
|
|
16877
|
+
authority,
|
|
16878
|
+
evidence: captured.map((item) => item.evidence)
|
|
16879
|
+
};
|
|
16880
|
+
}
|
|
16881
|
+
async function evaluateProposalFreshness(input) {
|
|
16882
|
+
const config = resolveRuntimeConfig(input.config);
|
|
16883
|
+
assertValidRunnerCapabilityConfig(config);
|
|
16884
|
+
const authority = freshnessAuthorityFromChangeSet(input.proposal.change_set);
|
|
16885
|
+
if (!authority) {
|
|
16886
|
+
return {
|
|
16887
|
+
required: false,
|
|
16888
|
+
status: "not_required",
|
|
16889
|
+
safe_code: "FRESHNESS_NOT_REQUIRED",
|
|
16890
|
+
target_count: 0,
|
|
16891
|
+
supporting_count: 0
|
|
16892
|
+
};
|
|
16893
|
+
}
|
|
16894
|
+
const env = input.env ?? process.env;
|
|
16895
|
+
const resources = createMcpRuntimeSharedResources(config, env, input.readRow, input.clock, input.credentialResolver);
|
|
16896
|
+
const checkedAtMs = (input.clock ?? Date.now)();
|
|
16897
|
+
const checks = [];
|
|
16898
|
+
let forcedResult;
|
|
16899
|
+
let forcedCode;
|
|
16900
|
+
try {
|
|
16901
|
+
const capability = (config.capabilities ?? []).find((item) => item.name === input.proposal.action);
|
|
16902
|
+
const source = capability ? config.sources?.[capability.source] : void 0;
|
|
16903
|
+
if (!capability || capability.kind !== "proposal" || !source) {
|
|
16904
|
+
forcedResult = "invalid";
|
|
16905
|
+
forcedCode = "FRESHNESS_PROPOSAL_AUTHORITY_INVALID";
|
|
16906
|
+
} else if (!proposalAuthorityMatchesCapability(input.proposal, capability)) {
|
|
16907
|
+
forcedResult = "invalid";
|
|
16908
|
+
forcedCode = "FRESHNESS_PROPOSAL_AUTHORITY_MISMATCH";
|
|
16909
|
+
} else {
|
|
16910
|
+
const context = proposalTrustedContext(config, capability, input.proposal);
|
|
16911
|
+
const targetChecks = await evaluateTargetFreshness({
|
|
16912
|
+
proposal: input.proposal,
|
|
16913
|
+
authority,
|
|
16914
|
+
capability,
|
|
16915
|
+
source,
|
|
16916
|
+
context,
|
|
16917
|
+
env,
|
|
16918
|
+
readRow: resources.readRow
|
|
16919
|
+
});
|
|
16920
|
+
checks.push(...targetChecks);
|
|
16921
|
+
for (const dependency of authority.dependencies) {
|
|
16922
|
+
const validationCode = validateResolvedFreshnessDependency(config, capability, dependency);
|
|
16923
|
+
if (validationCode) {
|
|
16924
|
+
checks.push({
|
|
16925
|
+
id: dependency.id,
|
|
16926
|
+
kind: "supporting",
|
|
16927
|
+
status: "invalid",
|
|
16928
|
+
safe_code: validationCode
|
|
16929
|
+
});
|
|
16930
|
+
continue;
|
|
16931
|
+
}
|
|
16932
|
+
const supporting = (config.capabilities ?? []).find((item) => item.name === dependency.capability);
|
|
16933
|
+
checks.push(await evaluateSupportingFreshness({
|
|
16934
|
+
dependency,
|
|
16935
|
+
capability: supporting,
|
|
16936
|
+
source,
|
|
16937
|
+
context,
|
|
16938
|
+
env,
|
|
16939
|
+
readRow: resources.readRow
|
|
16940
|
+
}));
|
|
16941
|
+
}
|
|
16942
|
+
}
|
|
16943
|
+
} catch (error) {
|
|
16944
|
+
const classified = classifyFreshnessReadError(error);
|
|
16945
|
+
forcedResult = classified.result;
|
|
16946
|
+
forcedCode = classified.safe_code;
|
|
16947
|
+
} finally {
|
|
16948
|
+
await resources.close();
|
|
16949
|
+
}
|
|
16950
|
+
const status = forcedResult ?? freshnessResultForChecks(checks);
|
|
16951
|
+
const safeCode = forcedCode ?? freshnessSafeCode(status, checks);
|
|
16952
|
+
const targetCount = checks.filter((check) => check.kind === "target").length;
|
|
16953
|
+
const supportingCount = checks.filter((check) => check.kind === "supporting").length;
|
|
16954
|
+
const adapters = freshnessSourceAdapters(input.proposal, authority);
|
|
16955
|
+
const unsigned = {
|
|
16956
|
+
schema_version: protocolVersions.freshnessProof,
|
|
16957
|
+
proposal_id: input.proposal.proposal_id,
|
|
16958
|
+
proposal_hash: input.proposal.proposal_hash,
|
|
16959
|
+
proposal_version: input.proposal.proposal_version,
|
|
16960
|
+
dependency_set_digest: authority.dependency_set_digest,
|
|
16961
|
+
checked_at: new Date(checkedAtMs).toISOString(),
|
|
16962
|
+
valid_until: new Date(checkedAtMs + Math.max(1e3, Math.min(input.proofValidityMs ?? 3e4, 3e5))).toISOString(),
|
|
16963
|
+
source_adapters: adapters,
|
|
16964
|
+
result: status,
|
|
16965
|
+
safe_code: safeCode,
|
|
16966
|
+
target_count: targetCount,
|
|
16967
|
+
supporting_count: supportingCount,
|
|
16968
|
+
checks
|
|
16969
|
+
};
|
|
16970
|
+
const proof = parseFreshnessProof({
|
|
16971
|
+
...unsigned,
|
|
16972
|
+
proof_digest: canonicalJsonDigest(unsigned)
|
|
16973
|
+
});
|
|
16974
|
+
return {
|
|
16975
|
+
required: true,
|
|
16976
|
+
status,
|
|
16977
|
+
safe_code: safeCode,
|
|
16978
|
+
target_count: targetCount,
|
|
16979
|
+
supporting_count: supportingCount,
|
|
16980
|
+
proof
|
|
16981
|
+
};
|
|
16982
|
+
}
|
|
16983
|
+
function freshnessAuthorityFromChangeSet(changeSet) {
|
|
16984
|
+
if (!("freshness" in changeSet) || changeSet.freshness === void 0) return void 0;
|
|
16985
|
+
return parseFreshnessAuthority(changeSet.freshness);
|
|
16986
|
+
}
|
|
16987
|
+
function proposalAuthorityMatchesCapability(proposal, capability) {
|
|
16988
|
+
const changeSet = proposal.change_set;
|
|
16989
|
+
if (proposal.proposal_hash !== changeSet.integrity.proposal_hash || proposal.proposal_version !== changeSet.proposal_version) return false;
|
|
16990
|
+
if (changeSet.source.source_id !== capability.source || changeSet.source.schema !== capability.target.schema || changeSet.source.table !== capability.target.table || changeSet.source.primary_key.column !== capability.target.primary_key) return false;
|
|
16991
|
+
if (changeSet.contract && (!capability.contract_provenance || changeSet.contract.digest !== capability.contract_provenance.digest || changeSet.contract.version !== capability.contract_provenance.version)) return false;
|
|
16992
|
+
return true;
|
|
16993
|
+
}
|
|
16994
|
+
function proposalTrustedContext(config, capability, proposal) {
|
|
16995
|
+
const contextConfig = (capability.context ? config.contexts?.[capability.context] : void 0) ?? config.trusted_context;
|
|
16996
|
+
const provider = contextConfig?.provider ?? (proposal.change_set.principal.source === "cloud_session" ? "cloud_session" : "environment");
|
|
16997
|
+
return {
|
|
16998
|
+
tenant_id: proposal.tenant_id,
|
|
16999
|
+
principal: proposal.principal ?? proposal.change_set.principal.id,
|
|
17000
|
+
provenance: provider
|
|
17001
|
+
};
|
|
17002
|
+
}
|
|
17003
|
+
async function evaluateTargetFreshness(input) {
|
|
17004
|
+
if (input.authority.target.mode === "not_applicable") return [];
|
|
17005
|
+
if (input.authority.target.mode === "frozen_set") {
|
|
17006
|
+
const changeSet2 = input.proposal.change_set;
|
|
17007
|
+
if (changeSet2.schema_version !== protocolVersions.changeSetV3) {
|
|
17008
|
+
return [{ id: "target", kind: "target", status: "invalid", safe_code: "FRESHNESS_TARGET_SET_INVALID" }];
|
|
17009
|
+
}
|
|
17010
|
+
const current2 = await input.readRow({
|
|
17011
|
+
sourceName: input.capability.source,
|
|
17012
|
+
source: input.source,
|
|
17013
|
+
capability: input.capability,
|
|
17014
|
+
args: {},
|
|
17015
|
+
context: input.context,
|
|
17016
|
+
env: input.env,
|
|
17017
|
+
transaction_mode: "read_only"
|
|
17018
|
+
});
|
|
17019
|
+
const rows = current2.rows ?? (current2.rowCount === 1 ? [current2.row] : []);
|
|
17020
|
+
const byIdentity = new Map(rows.map((row) => [JSON.stringify(scalar2(row[input.capability.target.primary_key])), row]));
|
|
17021
|
+
return changeSet2.frozen_set.members.map((member) => {
|
|
17022
|
+
const expected2 = member.expected_version;
|
|
17023
|
+
const row = byIdentity.get(JSON.stringify(member.primary_key.value));
|
|
17024
|
+
const observed2 = expected2 && row ? conflictGuardScalar(scalar2(row[expected2.column])) : void 0;
|
|
17025
|
+
const fresh2 = Boolean(expected2 && row && versionsEqual(observed2, expected2.value));
|
|
17026
|
+
return {
|
|
17027
|
+
id: `target:${canonicalJsonDigest(member.primary_key).slice(7, 23)}`,
|
|
17028
|
+
kind: "target",
|
|
17029
|
+
status: fresh2 ? "fresh" : "stale",
|
|
17030
|
+
safe_code: fresh2 ? "FRESHNESS_TARGET_FRESH" : "FRESHNESS_TARGET_STALE",
|
|
17031
|
+
...expected2 ? { expected_version_digest: versionMetadataDigest(expected2.column, expected2.value) } : {},
|
|
17032
|
+
...expected2 && observed2 !== void 0 ? { observed_version_digest: versionMetadataDigest(expected2.column, observed2) } : {}
|
|
17033
|
+
};
|
|
17034
|
+
});
|
|
17035
|
+
}
|
|
17036
|
+
const changeSet = input.proposal.change_set;
|
|
17037
|
+
if (changeSet.schema_version === protocolVersions.changeSetV3 || changeSet.schema_version === protocolVersions.compensationChangeSet) {
|
|
17038
|
+
return [{ id: "target", kind: "target", status: "invalid", safe_code: "FRESHNESS_TARGET_AUTHORITY_INVALID" }];
|
|
17039
|
+
}
|
|
17040
|
+
const expected = changeSet.guards.expected_version;
|
|
17041
|
+
if (!expected || expected.column === "__row_hash" || changeSet.source.primary_key.value === void 0) {
|
|
17042
|
+
return [{ id: "target", kind: "target", status: "invalid", safe_code: "FRESHNESS_EXACT_TARGET_GUARD_REQUIRED" }];
|
|
17043
|
+
}
|
|
17044
|
+
const readCapability = {
|
|
17045
|
+
...input.capability,
|
|
17046
|
+
conflict_guard: { column: expected.column }
|
|
17047
|
+
};
|
|
17048
|
+
const current = await input.readRow({
|
|
17049
|
+
sourceName: input.capability.source,
|
|
17050
|
+
source: input.source,
|
|
17051
|
+
capability: readCapability,
|
|
17052
|
+
args: { [input.capability.lookup.id_from_arg]: changeSet.source.primary_key.value },
|
|
17053
|
+
context: input.context,
|
|
17054
|
+
env: input.env,
|
|
17055
|
+
transaction_mode: "read_only"
|
|
17056
|
+
});
|
|
17057
|
+
const observed = current.rowCount === 1 ? conflictGuardScalar(scalar2(current.row[expected.column])) : void 0;
|
|
17058
|
+
const fresh = current.rowCount === 1 && observed !== void 0 && versionsEqual(observed, expected.value);
|
|
17059
|
+
return [{
|
|
17060
|
+
id: "target",
|
|
17061
|
+
kind: "target",
|
|
17062
|
+
status: fresh ? "fresh" : "stale",
|
|
17063
|
+
safe_code: fresh ? "FRESHNESS_TARGET_FRESH" : "FRESHNESS_TARGET_STALE",
|
|
17064
|
+
expected_version_digest: versionMetadataDigest(expected.column, expected.value),
|
|
17065
|
+
...observed !== void 0 ? { observed_version_digest: versionMetadataDigest(expected.column, observed) } : {}
|
|
17066
|
+
}];
|
|
17067
|
+
}
|
|
17068
|
+
function validateResolvedFreshnessDependency(config, proposalCapability, dependency) {
|
|
17069
|
+
const policy = config.proposal_freshness?.[proposalCapability.name];
|
|
17070
|
+
const declared = policy?.dependencies?.find((item) => item.id === dependency.id);
|
|
17071
|
+
const capability = (config.capabilities ?? []).find((item) => item.name === dependency.capability);
|
|
17072
|
+
const source = config.sources?.[dependency.source_id];
|
|
17073
|
+
if (!declared || declared.capability !== dependency.capability || declared.version_column !== dependency.expected_version.column) return "FRESHNESS_DEPENDENCY_AUTHORITY_MISMATCH";
|
|
17074
|
+
if (!capability || capability.kind !== "read" || !source) return "FRESHNESS_DEPENDENCY_AUTHORITY_INVALID";
|
|
17075
|
+
if (capability.source !== proposalCapability.source || dependency.source_id !== capability.source || dependency.engine !== source.engine) return "FRESHNESS_CROSS_SOURCE_UNSUPPORTED";
|
|
17076
|
+
if (capability.target.schema !== dependency.target.schema || capability.target.table !== dependency.target.table || capability.target.primary_key !== dependency.target.primary_key.column || (capability.target.tenant_key ?? "__single_tenant_dev") !== dependency.target.tenant_column || capability.target.principal_scope_key !== dependency.target.principal_column) return "FRESHNESS_DEPENDENCY_TARGET_MISMATCH";
|
|
17077
|
+
return void 0;
|
|
17078
|
+
}
|
|
17079
|
+
async function evaluateSupportingFreshness(input) {
|
|
17080
|
+
const readCapability = {
|
|
17081
|
+
...input.capability,
|
|
17082
|
+
conflict_guard: { column: input.dependency.expected_version.column }
|
|
17083
|
+
};
|
|
17084
|
+
try {
|
|
17085
|
+
const current = await input.readRow({
|
|
17086
|
+
sourceName: input.dependency.source_id,
|
|
17087
|
+
source: input.source,
|
|
17088
|
+
capability: readCapability,
|
|
17089
|
+
args: { [input.capability.lookup.id_from_arg]: input.dependency.target.primary_key.value },
|
|
17090
|
+
context: input.context,
|
|
17091
|
+
env: input.env,
|
|
17092
|
+
transaction_mode: "read_only"
|
|
17093
|
+
});
|
|
17094
|
+
const observed = current.rowCount === 1 ? conflictGuardScalar(scalar2(current.row[input.dependency.expected_version.column])) : void 0;
|
|
17095
|
+
const fresh = current.rowCount === 1 && observed !== void 0 && versionsEqual(observed, input.dependency.expected_version.value);
|
|
17096
|
+
return {
|
|
17097
|
+
id: input.dependency.id,
|
|
17098
|
+
kind: "supporting",
|
|
17099
|
+
status: fresh ? "fresh" : "stale",
|
|
17100
|
+
safe_code: fresh ? "FRESHNESS_DEPENDENCY_FRESH" : "FRESHNESS_DEPENDENCY_STALE",
|
|
17101
|
+
expected_version_digest: versionMetadataDigest(input.dependency.expected_version.column, input.dependency.expected_version.value),
|
|
17102
|
+
...observed !== void 0 ? { observed_version_digest: versionMetadataDigest(input.dependency.expected_version.column, observed) } : {}
|
|
17103
|
+
};
|
|
17104
|
+
} catch (error) {
|
|
17105
|
+
const classified = classifyFreshnessReadError(error);
|
|
17106
|
+
return {
|
|
17107
|
+
id: input.dependency.id,
|
|
17108
|
+
kind: "supporting",
|
|
17109
|
+
status: classified.result,
|
|
17110
|
+
safe_code: classified.safe_code
|
|
17111
|
+
};
|
|
17112
|
+
}
|
|
17113
|
+
}
|
|
17114
|
+
function freshnessResultForChecks(checks) {
|
|
17115
|
+
if (checks.some((check) => check.status === "invalid")) return "invalid";
|
|
17116
|
+
if (checks.some((check) => check.status === "unsupported")) return "unsupported";
|
|
17117
|
+
if (checks.some((check) => check.status === "unavailable")) return "unavailable";
|
|
17118
|
+
if (checks.some((check) => check.status === "stale")) return "stale";
|
|
17119
|
+
return "fresh";
|
|
17120
|
+
}
|
|
17121
|
+
function freshnessSafeCode(status, checks) {
|
|
17122
|
+
if (status === "fresh") return "FRESHNESS_FRESH";
|
|
17123
|
+
if (status === "stale" && checks.some((check) => check.kind === "target" && check.status === "stale")) return "FRESHNESS_TARGET_STALE";
|
|
17124
|
+
if (status === "stale") return "FRESHNESS_DEPENDENCY_STALE";
|
|
17125
|
+
if (status === "unavailable") return "FRESHNESS_TEMPORARILY_UNAVAILABLE";
|
|
17126
|
+
if (status === "unsupported") return "FRESHNESS_TOPOLOGY_UNSUPPORTED";
|
|
17127
|
+
return "FRESHNESS_AUTHORITY_INVALID";
|
|
17128
|
+
}
|
|
17129
|
+
function classifyFreshnessReadError(error) {
|
|
17130
|
+
const code = error instanceof McpRuntimeError ? error.code : errorStringProperty(error, "code") ?? "";
|
|
17131
|
+
if (/(TIMEOUT|POOL|CONNECTION|ECONN|ETIMEDOUT|TOO_MANY|UNAVAILABLE|SATURAT)/i.test(`${code} ${errorMessage(error)}`)) {
|
|
17132
|
+
return { result: "unavailable", safe_code: "FRESHNESS_TEMPORARILY_UNAVAILABLE" };
|
|
17133
|
+
}
|
|
17134
|
+
if (/(UNSUPPORTED|CROSS_SOURCE)/i.test(code)) {
|
|
17135
|
+
return { result: "unsupported", safe_code: "FRESHNESS_TOPOLOGY_UNSUPPORTED" };
|
|
17136
|
+
}
|
|
17137
|
+
return { result: "invalid", safe_code: "FRESHNESS_CHECK_FAILED" };
|
|
17138
|
+
}
|
|
17139
|
+
function freshnessSourceAdapters(proposal, authority) {
|
|
17140
|
+
const entries = /* @__PURE__ */ new Map();
|
|
17141
|
+
const targetEngine = proposal.source_kind === "external_mysql" ? "mysql" : "postgres";
|
|
17142
|
+
entries.set(`${proposal.source_id}:${targetEngine}`, { source_id: proposal.source_id, engine: targetEngine });
|
|
17143
|
+
for (const dependency of authority.dependencies) {
|
|
17144
|
+
entries.set(`${dependency.source_id}:${dependency.engine}`, { source_id: dependency.source_id, engine: dependency.engine });
|
|
17145
|
+
}
|
|
17146
|
+
return [...entries.values()].sort((left, right) => left.source_id.localeCompare(right.source_id) || left.engine.localeCompare(right.engine));
|
|
17147
|
+
}
|
|
17148
|
+
function versionsEqual(left, right) {
|
|
17149
|
+
return canonicalJsonDigest({ value: conflictGuardScalar(scalar2(left)) }) === canonicalJsonDigest({ value: conflictGuardScalar(scalar2(right)) });
|
|
17150
|
+
}
|
|
17151
|
+
function versionMetadataDigest(column, value) {
|
|
17152
|
+
return canonicalJsonDigest({ column, value: conflictGuardScalar(scalar2(value)) });
|
|
17153
|
+
}
|
|
16172
17154
|
function effectivePrincipalScope(config, capability, context) {
|
|
16173
17155
|
const column = capability.target.principal_scope_key;
|
|
16174
17156
|
if (!column) return void 0;
|
|
@@ -16247,6 +17229,7 @@ function buildChangeSet(input) {
|
|
|
16247
17229
|
...input.capability.operation.version_advance ? { version_advance: input.capability.operation.version_advance } : {},
|
|
16248
17230
|
...input.resolvedDeduplication ? { deduplication: input.resolvedDeduplication } : {}
|
|
16249
17231
|
},
|
|
17232
|
+
...input.freshness ? { freshness: input.freshness } : {},
|
|
16250
17233
|
...input.capability.reversibility ? {
|
|
16251
17234
|
reversibility: {
|
|
16252
17235
|
mode: "reviewed_inverse",
|
|
@@ -16312,6 +17295,7 @@ function buildChangeSet(input) {
|
|
|
16312
17295
|
allowed_columns: input.capability.allowed_columns ?? Object.keys(patch),
|
|
16313
17296
|
expected_version: guard
|
|
16314
17297
|
},
|
|
17298
|
+
...input.freshness ? { freshness: input.freshness } : {},
|
|
16315
17299
|
evidence: {
|
|
16316
17300
|
bundle_id: input.evidenceBundleId,
|
|
16317
17301
|
query_fingerprint: input.queryFingerprint,
|
|
@@ -16445,6 +17429,7 @@ function buildBoundedSetChangeSet(input) {
|
|
|
16445
17429
|
allowed_columns: kind === "set_delete" ? [] : input.capability.allowed_columns ?? Object.keys(input.patch),
|
|
16446
17430
|
...kind === "set_update" && operation.version_advance ? { version_advance: operation.version_advance } : {}
|
|
16447
17431
|
},
|
|
17432
|
+
...input.freshness ? { freshness: input.freshness } : {},
|
|
16448
17433
|
frozen_set: frozenSet,
|
|
16449
17434
|
...input.capability.reversibility ? {
|
|
16450
17435
|
reversibility: {
|
|
@@ -16644,7 +17629,7 @@ var RuntimeDatabasePools = class {
|
|
|
16644
17629
|
counter.active += 1;
|
|
16645
17630
|
try {
|
|
16646
17631
|
const query = runtimeReadQuery(input.capability, "$", input.args, input.context);
|
|
16647
|
-
await client.query(input.capability.protected_read ? "BEGIN READ ONLY" : "BEGIN");
|
|
17632
|
+
await client.query(input.capability.protected_read || input.transaction_mode === "read_only" ? "BEGIN READ ONLY" : "BEGIN");
|
|
16648
17633
|
const timeoutMs = protectedStatementTimeout(input.capability, input.source.statement_timeout_ms);
|
|
16649
17634
|
if (timeoutMs) await client.query(`SET LOCAL statement_timeout = ${timeoutMs}`);
|
|
16650
17635
|
if (input.source.database_scope?.mode === "postgres_rls") {
|
|
@@ -16711,14 +17696,15 @@ var RuntimeDatabasePools = class {
|
|
|
16711
17696
|
const timeoutMs = protectedStatementTimeout(input.capability, input.source.statement_timeout_ms);
|
|
16712
17697
|
if (timeoutMs) await connection.query("SET SESSION max_execution_time = ?", [timeoutMs]).catch(() => void 0);
|
|
16713
17698
|
const query = runtimeReadQuery(input.capability, "?", input.args, input.context);
|
|
16714
|
-
|
|
17699
|
+
const readOnlyTransaction = Boolean(input.capability.protected_read || input.transaction_mode === "read_only");
|
|
17700
|
+
if (readOnlyTransaction) await connection.query("START TRANSACTION READ ONLY");
|
|
16715
17701
|
try {
|
|
16716
17702
|
const [rows] = await connection.execute(query.sql, query.values.map(scalar2));
|
|
16717
17703
|
const list = Array.isArray(rows) ? rows : [];
|
|
16718
|
-
if (
|
|
17704
|
+
if (readOnlyTransaction) await connection.query("COMMIT");
|
|
16719
17705
|
return { row: list[0] ?? {}, rows: list, rowCount: list.length };
|
|
16720
17706
|
} catch (error) {
|
|
16721
|
-
if (
|
|
17707
|
+
if (readOnlyTransaction) await connection.query("ROLLBACK").catch(() => void 0);
|
|
16722
17708
|
throw error;
|
|
16723
17709
|
}
|
|
16724
17710
|
} finally {
|