@synapsor/runner 1.6.0 → 1.6.2

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.
Files changed (50) hide show
  1. package/CHANGELOG.md +52 -4
  2. package/README.md +16 -2
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/local-ui.d.ts +4 -1
  5. package/dist/local-ui.d.ts.map +1 -1
  6. package/dist/runner.mjs +1627 -43
  7. package/dist/runtime.mjs +1011 -25
  8. package/dist/shadow.mjs +408 -15
  9. package/docs/app-owned-executors.md +8 -0
  10. package/docs/capability-authoring.md +45 -0
  11. package/docs/current-scope.md +5 -0
  12. package/docs/cursor-plugin.md +2 -2
  13. package/docs/limitations.md +15 -2
  14. package/docs/local-mode.md +22 -3
  15. package/docs/production.md +19 -0
  16. package/docs/proposal-evidence-freshness.md +334 -0
  17. package/docs/release-notes.md +49 -4
  18. package/docs/runner-config-reference.md +46 -0
  19. package/docs/security-boundary.md +17 -0
  20. package/docs/store-lifecycle.md +8 -0
  21. package/docs/troubleshooting-first-run.md +76 -0
  22. package/docs/writeback-executors.md +8 -0
  23. package/fixtures/compatibility/published-1.6.0/manifest.json +76 -0
  24. package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/aggregate-read.synapsor.sql +21 -0
  25. package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/billing-late-fee.synapsor.sql +56 -0
  26. package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/bounded-set-multi-term.synapsor.sql +30 -0
  27. package/fixtures/compatibility/published-1.6.0/sources/packages/dsl/examples/principal-row-scope.synapsor.sql +23 -0
  28. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/aggregate-read/contract.json +119 -0
  29. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/approval-quorum/contract.json +44 -0
  30. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/bounded-set-threats/contract.json +115 -0
  31. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/principal-row-scope/contract.json +78 -0
  32. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/proposal-capability/contract.json +101 -0
  33. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/conformance/reversible-change-sets/contract.json +98 -0
  34. package/fixtures/compatibility/published-1.6.0/sources/packages/spec/fixtures/valid/basic-read.contract.json +60 -0
  35. package/fixtures/protocol/MANIFEST.json +37 -7
  36. package/fixtures/protocol/change-set.freshness-update.v2.json +58 -0
  37. package/fixtures/protocol/freshness-authority.invoice.v1.json +35 -0
  38. package/fixtures/protocol/freshness-proof.fresh.v1.json +38 -0
  39. package/fixtures/protocol/writeback-job.freshness-update.v2.json +50 -0
  40. package/llms.txt +1 -0
  41. package/package.json +4 -3
  42. package/schemas/change-set.v1.schema.json +1 -0
  43. package/schemas/change-set.v2.schema.json +1 -0
  44. package/schemas/change-set.v3.schema.json +1 -0
  45. package/schemas/freshness-authority.v1.schema.json +89 -0
  46. package/schemas/freshness-proof.v1.schema.json +73 -0
  47. package/schemas/synapsor.runner.schema.json +32 -0
  48. package/schemas/writeback-job.v1.schema.json +1 -0
  49. package/schemas/writeback-job.v2.schema.json +1 -0
  50. 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 (proposal_id, proposal_version, proposal_hash, approver, status, reason, created_at)
6756
- VALUES (?, ?, ?, ?, ?, ?, ?)
6757
- `).run(proposalId, options.proposal_version, options.proposal_hash, actor, "approved", options.reason, now);
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 = { tenant_id: tenantId, capability, worker_retries: 0, dead_letters: 0, auto_approval_limit_trips: 0 };
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 ('writeback_retry_scheduled', 'writeback_dead_lettered', 'policy_auto_approval_deferred')
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
- if (raw.kind === "policy_auto_approval_deferred") {
7666
- try {
7667
- const payload = JSON.parse(String(raw.payload_json));
7668
- if (Array.isArray(payload.tripped_limits) && payload.tripped_limits.length > 0) row.auto_approval_limit_trips += 1;
7669
- } catch {
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
- if (input.capability.protected_read) await connection.query("START TRANSACTION READ ONLY");
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 (input.capability.protected_read) await connection.query("COMMIT");
17704
+ if (readOnlyTransaction) await connection.query("COMMIT");
16719
17705
  return { row: list[0] ?? {}, rows: list, rowCount: list.length };
16720
17706
  } catch (error) {
16721
- if (input.capability.protected_read) await connection.query("ROLLBACK").catch(() => void 0);
17707
+ if (readOnlyTransaction) await connection.query("ROLLBACK").catch(() => void 0);
16722
17708
  throw error;
16723
17709
  }
16724
17710
  } finally {