@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/runner.mjs
CHANGED
|
@@ -25064,6 +25064,8 @@ var protocolVersions = {
|
|
|
25064
25064
|
runnerProposal: "synapsor.runner-proposal.v1",
|
|
25065
25065
|
runnerActivity: "synapsor.runner-activity.v1",
|
|
25066
25066
|
principalScope: "synapsor.principal-scope.v1",
|
|
25067
|
+
freshnessAuthority: "synapsor.freshness-authority.v1",
|
|
25068
|
+
freshnessProof: "synapsor.freshness-proof.v1",
|
|
25067
25069
|
legacyWritebackJob: "1.0",
|
|
25068
25070
|
normalizedWritebackJobV2: "2.0",
|
|
25069
25071
|
normalizedWritebackJobV3: "3.0",
|
|
@@ -25111,6 +25113,101 @@ var principalScopeGuardSchema = z.object({
|
|
|
25111
25113
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope fingerprint does not match its trusted value", path: ["value_fingerprint"] });
|
|
25112
25114
|
}
|
|
25113
25115
|
});
|
|
25116
|
+
var freshnessDependencyUnsignedSchema = z.object({
|
|
25117
|
+
id: safeIdentifier,
|
|
25118
|
+
capability: z.string().regex(/^[A-Za-z_][A-Za-z0-9_.-]*\.[A-Za-z_][A-Za-z0-9_.-]*$/, "expected reviewed capability name"),
|
|
25119
|
+
source_id: z.string().min(1).max(160),
|
|
25120
|
+
engine: writebackEngineSchema,
|
|
25121
|
+
target: z.object({
|
|
25122
|
+
schema: safeIdentifier,
|
|
25123
|
+
table: safeIdentifier,
|
|
25124
|
+
primary_key: columnValueSchema,
|
|
25125
|
+
tenant_column: safeIdentifier,
|
|
25126
|
+
principal_column: safeIdentifier.optional()
|
|
25127
|
+
}),
|
|
25128
|
+
expected_version: columnValueSchema,
|
|
25129
|
+
evidence: z.object({
|
|
25130
|
+
bundle_id: z.string().min(1).max(200),
|
|
25131
|
+
query_fingerprint: sha256
|
|
25132
|
+
})
|
|
25133
|
+
});
|
|
25134
|
+
var freshnessDependencyV1Schema = freshnessDependencyUnsignedSchema.extend({
|
|
25135
|
+
descriptor_digest: sha256
|
|
25136
|
+
}).superRefine((dependency, ctx) => {
|
|
25137
|
+
const { descriptor_digest: _digest, ...unsigned } = dependency;
|
|
25138
|
+
if (canonicalJsonDigest(unsigned) !== dependency.descriptor_digest) {
|
|
25139
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency digest mismatch", path: ["descriptor_digest"] });
|
|
25140
|
+
}
|
|
25141
|
+
});
|
|
25142
|
+
var freshnessAuthorityUnsignedSchema = z.object({
|
|
25143
|
+
schema_version: z.literal(protocolVersions.freshnessAuthority),
|
|
25144
|
+
required: z.literal(true),
|
|
25145
|
+
target: z.object({
|
|
25146
|
+
mode: z.enum(["exact_guard", "frozen_set", "not_applicable"]),
|
|
25147
|
+
member_count: z.number().int().min(0).max(100)
|
|
25148
|
+
}),
|
|
25149
|
+
dependencies: z.array(freshnessDependencyV1Schema).max(16)
|
|
25150
|
+
});
|
|
25151
|
+
var freshnessAuthorityV1Schema = freshnessAuthorityUnsignedSchema.extend({
|
|
25152
|
+
dependency_set_digest: sha256
|
|
25153
|
+
}).superRefine((authority, ctx) => {
|
|
25154
|
+
const identities = authority.dependencies.map((dependency) => dependency.id);
|
|
25155
|
+
if (new Set(identities).size !== identities.length) {
|
|
25156
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency ids must be unique", path: ["dependencies"] });
|
|
25157
|
+
}
|
|
25158
|
+
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));
|
|
25159
|
+
if (JSON.stringify(sorted) !== JSON.stringify(authority.dependencies)) {
|
|
25160
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependencies must use deterministic lock order", path: ["dependencies"] });
|
|
25161
|
+
}
|
|
25162
|
+
const { dependency_set_digest: _digest, ...unsigned } = authority;
|
|
25163
|
+
if (canonicalJsonDigest(unsigned) !== authority.dependency_set_digest) {
|
|
25164
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness dependency-set digest mismatch", path: ["dependency_set_digest"] });
|
|
25165
|
+
}
|
|
25166
|
+
});
|
|
25167
|
+
var freshnessCheckStatusSchema = z.enum(["fresh", "stale", "unavailable", "invalid", "unsupported"]);
|
|
25168
|
+
var freshnessItemResultSchema = z.object({
|
|
25169
|
+
id: z.string().min(1).max(160),
|
|
25170
|
+
kind: z.enum(["target", "supporting"]),
|
|
25171
|
+
status: z.enum(["fresh", "stale", "not_applicable", "unavailable", "invalid", "unsupported"]),
|
|
25172
|
+
safe_code: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
|
|
25173
|
+
expected_version_digest: sha256.optional(),
|
|
25174
|
+
observed_version_digest: sha256.optional()
|
|
25175
|
+
});
|
|
25176
|
+
var freshnessProofUnsignedSchema = z.object({
|
|
25177
|
+
schema_version: z.literal(protocolVersions.freshnessProof),
|
|
25178
|
+
proposal_id: z.string().min(1),
|
|
25179
|
+
proposal_hash: sha256,
|
|
25180
|
+
proposal_version: z.number().int().positive(),
|
|
25181
|
+
dependency_set_digest: sha256,
|
|
25182
|
+
checked_at: z.string().datetime(),
|
|
25183
|
+
valid_until: z.string().datetime(),
|
|
25184
|
+
source_adapters: z.array(z.object({
|
|
25185
|
+
source_id: z.string().min(1).max(160),
|
|
25186
|
+
engine: writebackEngineSchema
|
|
25187
|
+
})).min(1).max(8),
|
|
25188
|
+
result: freshnessCheckStatusSchema,
|
|
25189
|
+
safe_code: z.string().regex(/^[A-Z][A-Z0-9_]*$/),
|
|
25190
|
+
target_count: z.number().int().min(0).max(100),
|
|
25191
|
+
supporting_count: z.number().int().min(0).max(16),
|
|
25192
|
+
checks: z.array(freshnessItemResultSchema).max(116)
|
|
25193
|
+
});
|
|
25194
|
+
var freshnessProofV1Schema = freshnessProofUnsignedSchema.extend({
|
|
25195
|
+
proof_digest: sha256
|
|
25196
|
+
}).superRefine((proof, ctx) => {
|
|
25197
|
+
if (Date.parse(proof.valid_until) < Date.parse(proof.checked_at)) {
|
|
25198
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness proof validity ends before it was checked", path: ["valid_until"] });
|
|
25199
|
+
}
|
|
25200
|
+
if (proof.checks.filter((check) => check.kind === "target").length !== proof.target_count) {
|
|
25201
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness target count mismatch", path: ["target_count"] });
|
|
25202
|
+
}
|
|
25203
|
+
if (proof.checks.filter((check) => check.kind === "supporting").length !== proof.supporting_count) {
|
|
25204
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness supporting count mismatch", path: ["supporting_count"] });
|
|
25205
|
+
}
|
|
25206
|
+
const { proof_digest: _digest, ...unsigned } = proof;
|
|
25207
|
+
if (canonicalJsonDigest(unsigned) !== proof.proof_digest) {
|
|
25208
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "freshness proof digest mismatch", path: ["proof_digest"] });
|
|
25209
|
+
}
|
|
25210
|
+
});
|
|
25114
25211
|
var resolvedDeduplicationComponentSchema = z.object({
|
|
25115
25212
|
column: safeIdentifier,
|
|
25116
25213
|
value: scalar,
|
|
@@ -25240,6 +25337,7 @@ var changeSetV1Schema = z.object({
|
|
|
25240
25337
|
allowed_columns: z.array(z.string().min(1)).min(1),
|
|
25241
25338
|
expected_version: columnValueSchema
|
|
25242
25339
|
}),
|
|
25340
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
25243
25341
|
evidence: z.object({
|
|
25244
25342
|
bundle_id: z.string().min(1),
|
|
25245
25343
|
query_fingerprint: sha256,
|
|
@@ -25260,6 +25358,7 @@ var changeSetV1Schema = z.object({
|
|
|
25260
25358
|
}),
|
|
25261
25359
|
created_at: z.string().min(1)
|
|
25262
25360
|
}).superRefine((changeSet, ctx) => {
|
|
25361
|
+
validateFreshnessBinding(changeSet.freshness, changeSet.source.source_id, sourceEngine(changeSet.source.kind), "exact_guard", 1, ctx);
|
|
25263
25362
|
const allowed = new Set(changeSet.guards.allowed_columns);
|
|
25264
25363
|
for (const column of Object.keys(changeSet.patch)) {
|
|
25265
25364
|
if (!allowed.has(column)) {
|
|
@@ -25327,6 +25426,7 @@ var changeSetV2Schema = z.object({
|
|
|
25327
25426
|
version_advance: versionAdvanceSchema.optional(),
|
|
25328
25427
|
deduplication: z.object({ components: z.array(resolvedDeduplicationComponentSchema).min(1).max(8) }).optional()
|
|
25329
25428
|
}),
|
|
25429
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
25330
25430
|
reversibility: reversibilityRequestSchema.optional(),
|
|
25331
25431
|
evidence: z.object({
|
|
25332
25432
|
bundle_id: z.string().min(1),
|
|
@@ -25346,6 +25446,14 @@ var changeSetV2Schema = z.object({
|
|
|
25346
25446
|
integrity: z.object({ proposal_hash: sha256 }),
|
|
25347
25447
|
created_at: z.string().min(1)
|
|
25348
25448
|
}).superRefine((changeSet, ctx) => {
|
|
25449
|
+
validateFreshnessBinding(
|
|
25450
|
+
changeSet.freshness,
|
|
25451
|
+
changeSet.source.source_id,
|
|
25452
|
+
sourceEngine(changeSet.source.kind),
|
|
25453
|
+
changeSet.operation === "single_row_insert" ? "not_applicable" : "exact_guard",
|
|
25454
|
+
changeSet.operation === "single_row_insert" ? 0 : 1,
|
|
25455
|
+
ctx
|
|
25456
|
+
);
|
|
25349
25457
|
const allowed = new Set(changeSet.guards.allowed_columns);
|
|
25350
25458
|
for (const column of Object.keys(changeSet.patch)) {
|
|
25351
25459
|
if (!allowed.has(column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `patch column not allowed: ${column}`, path: ["patch", column] });
|
|
@@ -25412,6 +25520,7 @@ var changeSetV3Schema = z.object({
|
|
|
25412
25520
|
expected_version: columnValueSchema.optional(),
|
|
25413
25521
|
version_advance: versionAdvanceSchema.optional()
|
|
25414
25522
|
}),
|
|
25523
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
25415
25524
|
frozen_set: frozenSetSchema,
|
|
25416
25525
|
reversibility: reversibilityRequestSchema.optional(),
|
|
25417
25526
|
evidence: z.object({ bundle_id: z.string().min(1), query_fingerprint: sha256, items: z.array(z.unknown()).max(100) }).passthrough(),
|
|
@@ -25426,6 +25535,14 @@ var changeSetV3Schema = z.object({
|
|
|
25426
25535
|
integrity: z.object({ proposal_hash: sha256 }),
|
|
25427
25536
|
created_at: z.string().min(1)
|
|
25428
25537
|
}).superRefine((changeSet, ctx) => {
|
|
25538
|
+
validateFreshnessBinding(
|
|
25539
|
+
changeSet.freshness,
|
|
25540
|
+
changeSet.source.source_id,
|
|
25541
|
+
sourceEngine(changeSet.source.kind),
|
|
25542
|
+
changeSet.operation === "batch_insert" ? "not_applicable" : "frozen_set",
|
|
25543
|
+
changeSet.operation === "batch_insert" ? 0 : changeSet.frozen_set.row_count,
|
|
25544
|
+
ctx
|
|
25545
|
+
);
|
|
25429
25546
|
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"] });
|
|
25430
25547
|
const allowed = new Set(changeSet.guards.allowed_columns);
|
|
25431
25548
|
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] });
|
|
@@ -25521,6 +25638,7 @@ var writebackJobV1Schema = z.object({
|
|
|
25521
25638
|
allowed_columns: z.array(safeIdentifier).min(1),
|
|
25522
25639
|
patch: scalarMap,
|
|
25523
25640
|
conflict_guard: publicConflictGuardSchema,
|
|
25641
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
25524
25642
|
idempotency_key: z.string().min(1),
|
|
25525
25643
|
lease: z.object({
|
|
25526
25644
|
lease_id: z.string().min(1),
|
|
@@ -25528,6 +25646,7 @@ var writebackJobV1Schema = z.object({
|
|
|
25528
25646
|
expires_at: z.string().min(1)
|
|
25529
25647
|
})
|
|
25530
25648
|
}).superRefine((job, ctx) => {
|
|
25649
|
+
validateFreshnessBinding(job.freshness, job.runner_scope.source_id, job.engine, "exact_guard", 1, ctx);
|
|
25531
25650
|
validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.tenant_guard.column, ctx);
|
|
25532
25651
|
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"] });
|
|
25533
25652
|
});
|
|
@@ -25549,6 +25668,7 @@ var normalizedWritebackJobV1Schema = writebackJobV1Schema.transform((job) => ({
|
|
|
25549
25668
|
allowed_columns: job.allowed_columns,
|
|
25550
25669
|
patch: job.patch,
|
|
25551
25670
|
conflict_guard: normalizeConflictGuard(job.conflict_guard),
|
|
25671
|
+
...job.freshness ? { freshness: job.freshness } : {},
|
|
25552
25672
|
idempotency_key: job.idempotency_key,
|
|
25553
25673
|
lease_expires_at: job.lease.expires_at,
|
|
25554
25674
|
attempt_count: job.lease.attempt
|
|
@@ -25582,10 +25702,12 @@ var legacyWritebackJobSchema = z.object({
|
|
|
25582
25702
|
z.object({ kind: z.literal("row_hash"), expected_hash: z.string().min(1) }),
|
|
25583
25703
|
z.object({ kind: z.literal("none") })
|
|
25584
25704
|
]),
|
|
25705
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
25585
25706
|
idempotency_key: z.string().min(1),
|
|
25586
25707
|
lease_expires_at: z.union([z.string(), z.number()]),
|
|
25587
25708
|
attempt_count: z.number().int().nonnegative().optional()
|
|
25588
25709
|
}).superRefine((job, ctx) => {
|
|
25710
|
+
validateFreshnessBinding(job.freshness, job.source_id, job.engine, "exact_guard", 1, ctx);
|
|
25589
25711
|
validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.target.tenant_guard.column, ctx);
|
|
25590
25712
|
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"] });
|
|
25591
25713
|
});
|
|
@@ -25612,6 +25734,7 @@ var normalizedWritebackJobV2InputSchema = z.object({
|
|
|
25612
25734
|
z.object({ kind: z.literal("row_hash"), expected_hash: z.string().min(1) }),
|
|
25613
25735
|
z.object({ kind: z.literal("none") })
|
|
25614
25736
|
]),
|
|
25737
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
25615
25738
|
version_advance: versionAdvanceSchema.optional(),
|
|
25616
25739
|
deduplication: z.object({ components: z.array(resolvedDeduplicationComponentSchema).min(1).max(8) }).optional(),
|
|
25617
25740
|
idempotency_key: z.string().min(1),
|
|
@@ -25619,6 +25742,14 @@ var normalizedWritebackJobV2InputSchema = z.object({
|
|
|
25619
25742
|
attempt_count: z.number().int().positive(),
|
|
25620
25743
|
inverse_capture: inverseDescriptorV1Schema.optional()
|
|
25621
25744
|
}).superRefine((job, ctx) => {
|
|
25745
|
+
validateFreshnessBinding(
|
|
25746
|
+
job.freshness,
|
|
25747
|
+
job.source_id,
|
|
25748
|
+
job.engine,
|
|
25749
|
+
job.operation === "single_row_insert" ? "not_applicable" : "exact_guard",
|
|
25750
|
+
job.operation === "single_row_insert" ? 0 : 1,
|
|
25751
|
+
ctx
|
|
25752
|
+
);
|
|
25622
25753
|
if (job.operation !== "single_row_insert" && job.target.primary_key.value === void 0) {
|
|
25623
25754
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "UPDATE and DELETE require a primary-key value", path: ["target", "primary_key", "value"] });
|
|
25624
25755
|
}
|
|
@@ -25684,6 +25815,7 @@ var writebackJobV2Schema = z.object({
|
|
|
25684
25815
|
principal_scope: principalScopeGuardSchema.optional(),
|
|
25685
25816
|
allowed_columns: z.array(safeIdentifier),
|
|
25686
25817
|
mutation: writebackMutationV2Schema,
|
|
25818
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
25687
25819
|
idempotency_key: z.string().min(1),
|
|
25688
25820
|
inverse_capture: inverseDescriptorV1Schema.optional(),
|
|
25689
25821
|
lease: z.object({
|
|
@@ -25692,6 +25824,14 @@ var writebackJobV2Schema = z.object({
|
|
|
25692
25824
|
expires_at: z.string().min(1)
|
|
25693
25825
|
})
|
|
25694
25826
|
}).superRefine((job, ctx) => {
|
|
25827
|
+
validateFreshnessBinding(
|
|
25828
|
+
job.freshness,
|
|
25829
|
+
job.runner_scope.source_id,
|
|
25830
|
+
job.engine,
|
|
25831
|
+
job.mutation.kind === "single_row_insert" ? "not_applicable" : "exact_guard",
|
|
25832
|
+
job.mutation.kind === "single_row_insert" ? 0 : 1,
|
|
25833
|
+
ctx
|
|
25834
|
+
);
|
|
25695
25835
|
const mutation = job.mutation;
|
|
25696
25836
|
if (mutation.kind !== "single_row_insert" && job.target.primary_key.value === void 0) {
|
|
25697
25837
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "UPDATE and DELETE require a primary-key value", path: ["target", "primary_key", "value"] });
|
|
@@ -25730,6 +25870,7 @@ var writebackJobV2Schema = z.object({
|
|
|
25730
25870
|
conflict_guard: job.mutation.kind === "single_row_insert" ? { kind: "none" } : normalizeConflictGuard(job.mutation.conflict_guard),
|
|
25731
25871
|
...job.mutation.kind === "single_row_update" && job.mutation.version_advance ? { version_advance: job.mutation.version_advance } : {},
|
|
25732
25872
|
...job.mutation.kind === "single_row_insert" ? { deduplication: job.mutation.deduplication } : {},
|
|
25873
|
+
...job.freshness ? { freshness: job.freshness } : {},
|
|
25733
25874
|
idempotency_key: job.idempotency_key,
|
|
25734
25875
|
...job.inverse_capture ? { inverse_capture: job.inverse_capture } : {},
|
|
25735
25876
|
lease_expires_at: job.lease.expires_at,
|
|
@@ -25748,6 +25889,7 @@ var normalizedWritebackJobV3InputSchema = z.object({
|
|
|
25748
25889
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
25749
25890
|
patch: boundedScalarRecord,
|
|
25750
25891
|
conflict_guard: z.object({ kind: z.literal("none") }).default({ kind: "none" }),
|
|
25892
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
25751
25893
|
version_advance: versionAdvanceSchema.optional(),
|
|
25752
25894
|
frozen_set: frozenSetSchema,
|
|
25753
25895
|
idempotency_key: z.string().min(1),
|
|
@@ -25755,6 +25897,14 @@ var normalizedWritebackJobV3InputSchema = z.object({
|
|
|
25755
25897
|
attempt_count: z.number().int().positive(),
|
|
25756
25898
|
inverse_capture: inverseDescriptorV1Schema.optional()
|
|
25757
25899
|
}).superRefine((job, ctx) => {
|
|
25900
|
+
validateFreshnessBinding(
|
|
25901
|
+
job.freshness,
|
|
25902
|
+
job.source_id,
|
|
25903
|
+
job.engine,
|
|
25904
|
+
job.operation === "batch_insert" ? "not_applicable" : "frozen_set",
|
|
25905
|
+
job.operation === "batch_insert" ? 0 : job.frozen_set.row_count,
|
|
25906
|
+
ctx
|
|
25907
|
+
);
|
|
25758
25908
|
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"] });
|
|
25759
25909
|
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"] });
|
|
25760
25910
|
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"] });
|
|
@@ -25776,9 +25926,19 @@ var writebackJobV3Schema = z.object({
|
|
|
25776
25926
|
patch: boundedScalarRecord,
|
|
25777
25927
|
version_advance: versionAdvanceSchema.optional(),
|
|
25778
25928
|
frozen_set: frozenSetSchema,
|
|
25929
|
+
freshness: freshnessAuthorityV1Schema.optional(),
|
|
25779
25930
|
idempotency_key: z.string().min(1),
|
|
25780
25931
|
inverse_capture: inverseDescriptorV1Schema.optional(),
|
|
25781
25932
|
lease: z.object({ lease_id: z.string().min(1), attempt: z.number().int().positive(), expires_at: z.string().min(1) })
|
|
25933
|
+
}).superRefine((job, ctx) => {
|
|
25934
|
+
validateFreshnessBinding(
|
|
25935
|
+
job.freshness,
|
|
25936
|
+
job.runner_scope.source_id,
|
|
25937
|
+
job.engine,
|
|
25938
|
+
job.operation === "batch_insert" ? "not_applicable" : "frozen_set",
|
|
25939
|
+
job.operation === "batch_insert" ? 0 : job.frozen_set.row_count,
|
|
25940
|
+
ctx
|
|
25941
|
+
);
|
|
25782
25942
|
}).transform((job) => ({
|
|
25783
25943
|
protocol_version: protocolVersions.normalizedWritebackJobV3,
|
|
25784
25944
|
job_id: job.writeback_job_id,
|
|
@@ -25793,6 +25953,7 @@ var writebackJobV3Schema = z.object({
|
|
|
25793
25953
|
conflict_guard: { kind: "none" },
|
|
25794
25954
|
...job.version_advance ? { version_advance: job.version_advance } : {},
|
|
25795
25955
|
frozen_set: job.frozen_set,
|
|
25956
|
+
...job.freshness ? { freshness: job.freshness } : {},
|
|
25796
25957
|
idempotency_key: job.idempotency_key,
|
|
25797
25958
|
...job.inverse_capture ? { inverse_capture: job.inverse_capture } : {},
|
|
25798
25959
|
lease_expires_at: job.lease.expires_at,
|
|
@@ -26146,6 +26307,38 @@ var runnerActivityV1Schema = z.object({
|
|
|
26146
26307
|
detail: z.record(z.unknown()).optional(),
|
|
26147
26308
|
occurred_at: z.string().min(1).max(80).optional()
|
|
26148
26309
|
}).strict();
|
|
26310
|
+
function sourceEngine(kind) {
|
|
26311
|
+
if (kind === "external_postgres") return "postgres";
|
|
26312
|
+
if (kind === "external_mysql") return "mysql";
|
|
26313
|
+
return void 0;
|
|
26314
|
+
}
|
|
26315
|
+
function validateFreshnessBinding(freshness, sourceId, engine, expectedTargetMode, expectedMemberCount, ctx) {
|
|
26316
|
+
if (!freshness) return;
|
|
26317
|
+
if (!engine) {
|
|
26318
|
+
ctx.addIssue({
|
|
26319
|
+
code: z.ZodIssueCode.custom,
|
|
26320
|
+
message: "freshness authority requires an external PostgreSQL or MySQL source",
|
|
26321
|
+
path: ["freshness"]
|
|
26322
|
+
});
|
|
26323
|
+
return;
|
|
26324
|
+
}
|
|
26325
|
+
if (freshness.target.mode !== expectedTargetMode || freshness.target.member_count !== expectedMemberCount) {
|
|
26326
|
+
ctx.addIssue({
|
|
26327
|
+
code: z.ZodIssueCode.custom,
|
|
26328
|
+
message: `freshness target authority must be ${expectedTargetMode} with ${expectedMemberCount} reviewed member(s)`,
|
|
26329
|
+
path: ["freshness", "target"]
|
|
26330
|
+
});
|
|
26331
|
+
}
|
|
26332
|
+
for (const [index, dependency] of freshness.dependencies.entries()) {
|
|
26333
|
+
if (dependency.source_id !== sourceId || dependency.engine !== engine) {
|
|
26334
|
+
ctx.addIssue({
|
|
26335
|
+
code: z.ZodIssueCode.custom,
|
|
26336
|
+
message: "freshness dependencies must use the proposal/writeback source and engine",
|
|
26337
|
+
path: ["freshness", "dependencies", index, "source_id"]
|
|
26338
|
+
});
|
|
26339
|
+
}
|
|
26340
|
+
}
|
|
26341
|
+
}
|
|
26149
26342
|
function validateAllowedPatchColumns(allowedColumns, patchColumns, primaryKeyColumn, tenantGuardColumn, ctx) {
|
|
26150
26343
|
const allow = new Set(allowedColumns);
|
|
26151
26344
|
for (const column of patchColumns) {
|
|
@@ -26206,6 +26399,12 @@ function normalizeConflictGuard(guard) {
|
|
|
26206
26399
|
function parseChangeSet(input) {
|
|
26207
26400
|
return z.union([changeSetV1Schema, changeSetV2Schema, changeSetV3Schema, compensationChangeSetV1Schema]).parse(input);
|
|
26208
26401
|
}
|
|
26402
|
+
function parseFreshnessAuthority(input) {
|
|
26403
|
+
return freshnessAuthorityV1Schema.parse(input);
|
|
26404
|
+
}
|
|
26405
|
+
function parseFreshnessProof(input) {
|
|
26406
|
+
return freshnessProofV1Schema.parse(input);
|
|
26407
|
+
}
|
|
26209
26408
|
function parseWritebackJob(input) {
|
|
26210
26409
|
return writebackJobSchema.parse(input);
|
|
26211
26410
|
}
|
|
@@ -26576,10 +26775,12 @@ function sleep(ms) {
|
|
|
26576
26775
|
|
|
26577
26776
|
// packages/config/src/index.ts
|
|
26578
26777
|
init_dist();
|
|
26579
|
-
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"]);
|
|
26778
|
+
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"]);
|
|
26580
26779
|
var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path", "shared_postgres"]);
|
|
26581
26780
|
var SHARED_POSTGRES_STORAGE_KEYS = /* @__PURE__ */ new Set(["mode", "url_env", "schema", "lock_timeout_ms", "max_entries"]);
|
|
26582
26781
|
var APPROVALS_KEYS = /* @__PURE__ */ new Set(["disable_auto_approval"]);
|
|
26782
|
+
var PROPOSAL_FRESHNESS_KEYS = /* @__PURE__ */ new Set(["approval", "dependencies"]);
|
|
26783
|
+
var FRESHNESS_DEPENDENCY_KEYS = /* @__PURE__ */ new Set(["id", "capability", "identity_from_arg", "version_column"]);
|
|
26583
26784
|
var METRICS_KEYS = /* @__PURE__ */ new Set(["enabled", "token_env"]);
|
|
26584
26785
|
var GRADUATED_TRUST_KEYS = /* @__PURE__ */ new Set(["enabled", "kill_switch", "workspace_id", "project_id", "criteria"]);
|
|
26585
26786
|
var GRADUATED_TRUST_CRITERION_KEYS = /* @__PURE__ */ new Set([
|
|
@@ -26824,6 +27025,7 @@ function validateRunnerCapabilityConfig(input) {
|
|
|
26824
27025
|
validateMetrics(input.metrics, strict, errors);
|
|
26825
27026
|
validateGraduatedTrust(input.graduated_trust, strict, errors);
|
|
26826
27027
|
validateCapabilities2(input.capabilities, input.sources, input.contexts, input.executors, input.mode, strict, errors, warnings, hasContracts);
|
|
27028
|
+
validateProposalFreshness(input.proposal_freshness, input.capabilities, strict, errors);
|
|
26827
27029
|
validateEffectiveContextCompatibility(input.trusted_context, input.contexts, input.capabilities, errors);
|
|
26828
27030
|
validateApprovalPolicyReferences(input.capabilities, input.policies, errors);
|
|
26829
27031
|
validateWritebackReadiness(input.sources, input.capabilities, input.mode, errors, warnings);
|
|
@@ -27911,6 +28113,125 @@ function validateCapabilities2(value, sources, contexts, executors, mode, strict
|
|
|
27911
28113
|
capabilityNames.set(capability.name, index);
|
|
27912
28114
|
});
|
|
27913
28115
|
}
|
|
28116
|
+
function validateProposalFreshness(value, capabilitiesValue, strict, errors) {
|
|
28117
|
+
if (value === void 0) return;
|
|
28118
|
+
if (!isRecord2(value)) {
|
|
28119
|
+
errors.push({
|
|
28120
|
+
path: "$.proposal_freshness",
|
|
28121
|
+
code: "PROPOSAL_FRESHNESS_NOT_OBJECT",
|
|
28122
|
+
message: "proposal_freshness must map reviewed proposal capability names to freshness policy."
|
|
28123
|
+
});
|
|
28124
|
+
return;
|
|
28125
|
+
}
|
|
28126
|
+
const capabilities = Array.isArray(capabilitiesValue) ? capabilitiesValue.filter(isRecord2) : [];
|
|
28127
|
+
const byName = new Map(capabilities.flatMap((capability) => isQualifiedName2(capability.name) ? [[String(capability.name), capability]] : []));
|
|
28128
|
+
for (const [proposalName, rawPolicy] of Object.entries(value)) {
|
|
28129
|
+
const path26 = `$.proposal_freshness.${proposalName}`;
|
|
28130
|
+
if (!isQualifiedName2(proposalName)) {
|
|
28131
|
+
errors.push({ path: path26, code: "INVALID_FRESHNESS_CAPABILITY_NAME", message: "Freshness policy keys must be existing qualified proposal capability names." });
|
|
28132
|
+
continue;
|
|
28133
|
+
}
|
|
28134
|
+
if (!isRecord2(rawPolicy)) {
|
|
28135
|
+
errors.push({ path: path26, code: "FRESHNESS_POLICY_NOT_OBJECT", message: "Freshness policy must be an object." });
|
|
28136
|
+
continue;
|
|
28137
|
+
}
|
|
28138
|
+
if (strict) checkUnknownKeys2(rawPolicy, PROPOSAL_FRESHNESS_KEYS, path26, errors);
|
|
28139
|
+
if (rawPolicy.approval !== "required") {
|
|
28140
|
+
errors.push({ path: `${path26}.approval`, code: "FRESHNESS_APPROVAL_REQUIRED", message: "Freshness-enabled proposals must set approval to required." });
|
|
28141
|
+
}
|
|
28142
|
+
const proposal = byName.get(proposalName);
|
|
28143
|
+
if (!proposal) {
|
|
28144
|
+
errors.push({ path: path26, code: "FRESHNESS_PROPOSAL_UNKNOWN", message: `Freshness policy references unknown proposal capability ${proposalName}.` });
|
|
28145
|
+
continue;
|
|
28146
|
+
}
|
|
28147
|
+
if (proposal.kind !== "proposal") {
|
|
28148
|
+
errors.push({ path: path26, code: "FRESHNESS_PROPOSAL_REQUIRED", message: `${proposalName} must be a proposal capability.` });
|
|
28149
|
+
}
|
|
28150
|
+
const writeback2 = isRecord2(proposal.writeback) ? proposal.writeback : void 0;
|
|
28151
|
+
if (writeback2?.mode !== "direct_sql") {
|
|
28152
|
+
errors.push({
|
|
28153
|
+
path: path26,
|
|
28154
|
+
code: "FRESHNESS_DIRECT_SQL_REQUIRED",
|
|
28155
|
+
message: "Strict proposal freshness currently requires same-database direct_sql writeback; app-owned and cross-source handlers must enforce their own transaction preconditions."
|
|
28156
|
+
});
|
|
28157
|
+
}
|
|
28158
|
+
const operation = isRecord2(proposal.operation) ? proposal.operation : void 0;
|
|
28159
|
+
if ((operation?.kind ?? "update") !== "insert") {
|
|
28160
|
+
const guard = isRecord2(proposal.conflict_guard) ? proposal.conflict_guard : void 0;
|
|
28161
|
+
if (!isSafeIdentifier2(guard?.column)) {
|
|
28162
|
+
errors.push({ path: path26, code: "FRESHNESS_EXACT_TARGET_GUARD_REQUIRED", message: "Freshness-required UPDATE/DELETE needs an exact conflict_guard.column; weak row hashes are not sufficient." });
|
|
28163
|
+
}
|
|
28164
|
+
}
|
|
28165
|
+
if (rawPolicy.dependencies !== void 0 && !Array.isArray(rawPolicy.dependencies)) {
|
|
28166
|
+
errors.push({ path: `${path26}.dependencies`, code: "FRESHNESS_DEPENDENCIES_NOT_ARRAY", message: "dependencies must be an array of reviewed single-row read references." });
|
|
28167
|
+
continue;
|
|
28168
|
+
}
|
|
28169
|
+
const dependencies = Array.isArray(rawPolicy.dependencies) ? rawPolicy.dependencies : [];
|
|
28170
|
+
if (dependencies.length > 16) {
|
|
28171
|
+
errors.push({ path: `${path26}.dependencies`, code: "FRESHNESS_DEPENDENCY_LIMIT", message: "A proposal may declare at most 16 supporting freshness dependencies." });
|
|
28172
|
+
}
|
|
28173
|
+
const ids = /* @__PURE__ */ new Set();
|
|
28174
|
+
for (const [index, rawDependency] of dependencies.entries()) {
|
|
28175
|
+
const dependencyPath = `${path26}.dependencies[${index}]`;
|
|
28176
|
+
if (!isRecord2(rawDependency)) {
|
|
28177
|
+
errors.push({ path: dependencyPath, code: "FRESHNESS_DEPENDENCY_NOT_OBJECT", message: "Freshness dependency must be an object." });
|
|
28178
|
+
continue;
|
|
28179
|
+
}
|
|
28180
|
+
if (strict) checkUnknownKeys2(rawDependency, FRESHNESS_DEPENDENCY_KEYS, dependencyPath, errors);
|
|
28181
|
+
if (!isSafeIdentifier2(rawDependency.id)) {
|
|
28182
|
+
errors.push({ path: `${dependencyPath}.id`, code: "INVALID_FRESHNESS_DEPENDENCY_ID", message: "Dependency id must be a fixed safe identifier." });
|
|
28183
|
+
} else if (ids.has(String(rawDependency.id))) {
|
|
28184
|
+
errors.push({ path: `${dependencyPath}.id`, code: "DUPLICATE_FRESHNESS_DEPENDENCY_ID", message: "Dependency ids must be unique within a proposal." });
|
|
28185
|
+
} else {
|
|
28186
|
+
ids.add(String(rawDependency.id));
|
|
28187
|
+
}
|
|
28188
|
+
if (!isQualifiedName2(rawDependency.capability)) {
|
|
28189
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "INVALID_FRESHNESS_DEPENDENCY_CAPABILITY", message: "Dependency capability must be a reviewed qualified capability name." });
|
|
28190
|
+
continue;
|
|
28191
|
+
}
|
|
28192
|
+
const dependency = byName.get(String(rawDependency.capability));
|
|
28193
|
+
if (!dependency) {
|
|
28194
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_UNKNOWN", message: `Unknown supporting capability ${String(rawDependency.capability)}.` });
|
|
28195
|
+
continue;
|
|
28196
|
+
}
|
|
28197
|
+
if (dependency.kind !== "read" || dependency.protected_read !== void 0 || dependency.aggregate !== void 0) {
|
|
28198
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_SINGLE_ROW_READ_REQUIRED", message: "Supporting freshness dependencies must reference reviewed single-row read capabilities." });
|
|
28199
|
+
}
|
|
28200
|
+
if (dependency.name === proposalName) {
|
|
28201
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_CYCLE", message: "A proposal cannot depend on itself." });
|
|
28202
|
+
}
|
|
28203
|
+
if (dependency.source !== proposal.source) {
|
|
28204
|
+
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." });
|
|
28205
|
+
}
|
|
28206
|
+
if ((dependency.context ?? null) !== (proposal.context ?? null)) {
|
|
28207
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_CONTEXT_MISMATCH", message: "Supporting freshness dependencies must use the same reviewed trusted context as the proposal." });
|
|
28208
|
+
}
|
|
28209
|
+
const target2 = isRecord2(dependency.target) ? dependency.target : void 0;
|
|
28210
|
+
if (!isSafeIdentifier2(target2?.tenant_key) && target2?.single_tenant_dev !== true) {
|
|
28211
|
+
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." });
|
|
28212
|
+
}
|
|
28213
|
+
const proposalTarget = isRecord2(proposal.target) ? proposal.target : void 0;
|
|
28214
|
+
if (isSafeIdentifier2(target2?.principal_scope_key) && !isSafeIdentifier2(proposalTarget?.principal_scope_key)) {
|
|
28215
|
+
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." });
|
|
28216
|
+
}
|
|
28217
|
+
if (!isSafeIdentifier2(rawDependency.identity_from_arg) || !isRecord2(proposal.args) || !(String(rawDependency.identity_from_arg) in proposal.args)) {
|
|
28218
|
+
errors.push({ path: `${dependencyPath}.identity_from_arg`, code: "FRESHNESS_IDENTITY_ARG_UNKNOWN", message: "identity_from_arg must name an existing scalar proposal argument." });
|
|
28219
|
+
} else {
|
|
28220
|
+
const proposalArg = isRecord2(proposal.args[String(rawDependency.identity_from_arg)]) ? proposal.args[String(rawDependency.identity_from_arg)] : void 0;
|
|
28221
|
+
if (!proposalArg || proposalArg.type === "object_array") {
|
|
28222
|
+
errors.push({ path: `${dependencyPath}.identity_from_arg`, code: "FRESHNESS_IDENTITY_ARG_NOT_SCALAR", message: "identity_from_arg must reference one bounded scalar proposal argument." });
|
|
28223
|
+
}
|
|
28224
|
+
}
|
|
28225
|
+
const lookup = isRecord2(dependency.lookup) ? dependency.lookup : void 0;
|
|
28226
|
+
if (!isSafeIdentifier2(lookup?.id_from_arg) || !isRecord2(dependency.args) || !(String(lookup?.id_from_arg) in dependency.args)) {
|
|
28227
|
+
errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_LOOKUP_INVALID", message: "Supporting capability must have one fixed reviewed lookup argument." });
|
|
28228
|
+
}
|
|
28229
|
+
if (!isSafeIdentifier2(rawDependency.version_column)) {
|
|
28230
|
+
errors.push({ path: `${dependencyPath}.version_column`, code: "FRESHNESS_VERSION_COLUMN_REQUIRED", message: "version_column must be an exact fixed safe identifier." });
|
|
28231
|
+
}
|
|
28232
|
+
}
|
|
28233
|
+
}
|
|
28234
|
+
}
|
|
27914
28235
|
function validateCapability(value, index, sourceNames, contextNames, executorNames, strict, errors, warnings) {
|
|
27915
28236
|
const path26 = `$.capabilities[${index}]`;
|
|
27916
28237
|
if (!isRecord2(value)) {
|
|
@@ -30421,6 +30742,14 @@ async function sourceTransaction(client, job, config, fn, hooks = {}) {
|
|
|
30421
30742
|
scope: config.databaseScope,
|
|
30422
30743
|
operations: ["SELECT", rlsOperationForJob(job)]
|
|
30423
30744
|
});
|
|
30745
|
+
for (const dependency of freshnessDependencies(job)) {
|
|
30746
|
+
await assertPostgresRlsTarget(client, {
|
|
30747
|
+
schema: dependency.target.schema,
|
|
30748
|
+
table: dependency.target.table,
|
|
30749
|
+
scope: config.databaseScope,
|
|
30750
|
+
operations: ["SELECT"]
|
|
30751
|
+
});
|
|
30752
|
+
}
|
|
30424
30753
|
await bindPostgresRlsScope(client, config.databaseScope);
|
|
30425
30754
|
}
|
|
30426
30755
|
await hooks.afterBegin?.();
|
|
@@ -30540,6 +30869,15 @@ function validateOperation(job) {
|
|
|
30540
30869
|
}
|
|
30541
30870
|
async function mutatePostgres(job, client) {
|
|
30542
30871
|
if (job.protocol_version === "4.0") return await mutatePostgresCompensation(job, client);
|
|
30872
|
+
const freshnessConflict = await lockPostgresFreshnessDependencies(job, client);
|
|
30873
|
+
if (freshnessConflict) {
|
|
30874
|
+
return {
|
|
30875
|
+
status: "conflict",
|
|
30876
|
+
affectedRows: 0,
|
|
30877
|
+
code: freshnessConflict,
|
|
30878
|
+
targetIdentity: identityForJob(job)
|
|
30879
|
+
};
|
|
30880
|
+
}
|
|
30543
30881
|
if (job.protocol_version === "3.0") return await mutatePostgresSet(job, client);
|
|
30544
30882
|
const operation = operationOf(job);
|
|
30545
30883
|
if (operation === "single_row_insert") return await insertPostgres(job, client);
|
|
@@ -30577,6 +30915,38 @@ async function mutatePostgres(job, client) {
|
|
|
30577
30915
|
verifyVersionAdvanced(job, resultVersion);
|
|
30578
30916
|
return { status: "applied", affectedRows: 1, targetIdentity: identityForJob(job), resultVersion, beforeDigest, afterDigest: digest({ identity: identityForJob(job), patch: job.patch, version: resultVersion }) };
|
|
30579
30917
|
}
|
|
30918
|
+
function freshnessDependencies(job) {
|
|
30919
|
+
if (!("freshness" in job) || !job.freshness) return [];
|
|
30920
|
+
return job.freshness.dependencies;
|
|
30921
|
+
}
|
|
30922
|
+
async function lockPostgresFreshnessDependencies(job, client) {
|
|
30923
|
+
for (const dependency of freshnessDependencies(job)) {
|
|
30924
|
+
const values = [
|
|
30925
|
+
dependency.target.primary_key.value,
|
|
30926
|
+
job.target.tenant_guard.value
|
|
30927
|
+
];
|
|
30928
|
+
const where = [
|
|
30929
|
+
`${quotePostgresIdentifier(dependency.target.primary_key.column)} = $1`,
|
|
30930
|
+
`${quotePostgresIdentifier(dependency.target.tenant_column)} = $2`
|
|
30931
|
+
];
|
|
30932
|
+
if (dependency.target.principal_column) {
|
|
30933
|
+
const scope = principalScope(job);
|
|
30934
|
+
if (!scope) return "FRESHNESS_DEPENDENCY_SCOPE_INVALID";
|
|
30935
|
+
values.push(scope.value);
|
|
30936
|
+
where.push(`${quotePostgresIdentifier(dependency.target.principal_column)} = $${values.length}`);
|
|
30937
|
+
}
|
|
30938
|
+
const result = await client.query(
|
|
30939
|
+
`SELECT ${quotePostgresIdentifier(dependency.expected_version.column)}::text AS "__synapsor_freshness_version" FROM ${quotePostgresIdentifier(dependency.target.schema)}.${quotePostgresIdentifier(dependency.target.table)} WHERE ${where.join(" AND ")} FOR UPDATE`,
|
|
30940
|
+
values
|
|
30941
|
+
);
|
|
30942
|
+
if (result.rowCount !== 1) return "FRESHNESS_DEPENDENCY_STALE";
|
|
30943
|
+
if (!versionValuesMatch(
|
|
30944
|
+
result.rows[0]?.__synapsor_freshness_version,
|
|
30945
|
+
dependency.expected_version.value
|
|
30946
|
+
)) return "FRESHNESS_DEPENDENCY_STALE";
|
|
30947
|
+
}
|
|
30948
|
+
return void 0;
|
|
30949
|
+
}
|
|
30580
30950
|
function compensationProjection(job) {
|
|
30581
30951
|
const columns = /* @__PURE__ */ new Set([job.target.primary_key.column, job.target.tenant_guard.column]);
|
|
30582
30952
|
const scope = principalScope(job);
|
|
@@ -31176,6 +31546,15 @@ var PostgresProposalRuntimeStore = class {
|
|
|
31176
31546
|
async createProposal(input) {
|
|
31177
31547
|
return await this.withWrite("createProposal", (store) => store.createProposal(input));
|
|
31178
31548
|
}
|
|
31549
|
+
async recordFreshnessProof(input) {
|
|
31550
|
+
return await this.withWrite("recordFreshnessProof", (store) => store.recordFreshnessProof(input));
|
|
31551
|
+
}
|
|
31552
|
+
async latestFreshnessProof(proposalId) {
|
|
31553
|
+
return await this.withRead((store) => store.latestFreshnessProof(proposalId));
|
|
31554
|
+
}
|
|
31555
|
+
async recordFreshnessApprovalBlocked(proposalId, input) {
|
|
31556
|
+
await this.withWrite("recordFreshnessApprovalBlocked", (store) => store.recordFreshnessApprovalBlocked(proposalId, input));
|
|
31557
|
+
}
|
|
31179
31558
|
async approveProposalByPolicy(proposalId, options) {
|
|
31180
31559
|
return await this.withWrite("approveProposalByPolicy", (store) => store.approveProposalByPolicy(proposalId, options));
|
|
31181
31560
|
}
|
|
@@ -31624,6 +32003,7 @@ var ProposalStore = class {
|
|
|
31624
32003
|
decision_hash TEXT,
|
|
31625
32004
|
signature TEXT,
|
|
31626
32005
|
integrity_hash TEXT,
|
|
32006
|
+
freshness_proof_digest TEXT,
|
|
31627
32007
|
created_at TEXT NOT NULL,
|
|
31628
32008
|
FOREIGN KEY (proposal_id) REFERENCES proposals(proposal_id)
|
|
31629
32009
|
);
|
|
@@ -31897,6 +32277,7 @@ var ProposalStore = class {
|
|
|
31897
32277
|
this.ensureColumn("approvals", "decision_hash", "TEXT");
|
|
31898
32278
|
this.ensureColumn("approvals", "signature", "TEXT");
|
|
31899
32279
|
this.ensureColumn("approvals", "integrity_hash", "TEXT");
|
|
32280
|
+
this.ensureColumn("approvals", "freshness_proof_digest", "TEXT");
|
|
31900
32281
|
}
|
|
31901
32282
|
ensureSearchIndexes() {
|
|
31902
32283
|
this.db.exec(`
|
|
@@ -32187,6 +32568,75 @@ var ProposalStore = class {
|
|
|
32187
32568
|
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);
|
|
32188
32569
|
return isRecord4(row) && row.proposal_id != null ? String(row.proposal_id) : void 0;
|
|
32189
32570
|
}
|
|
32571
|
+
recordFreshnessProof(input) {
|
|
32572
|
+
const proof = parseFreshnessProof(input);
|
|
32573
|
+
const proposal = this.requireProposal(proof.proposal_id);
|
|
32574
|
+
assertProposalIdentity(proposal, proof.proposal_hash, proof.proposal_version);
|
|
32575
|
+
const authority = proposalFreshnessAuthority(proposal);
|
|
32576
|
+
if (!authority) {
|
|
32577
|
+
throw new ProposalStoreError(
|
|
32578
|
+
"FRESHNESS_NOT_REQUIRED",
|
|
32579
|
+
`proposal ${proof.proposal_id} has no reviewed freshness authority`
|
|
32580
|
+
);
|
|
32581
|
+
}
|
|
32582
|
+
if (proof.dependency_set_digest !== authority.dependency_set_digest) {
|
|
32583
|
+
throw new ProposalStoreError(
|
|
32584
|
+
"FRESHNESS_PROOF_AUTHORITY_MISMATCH",
|
|
32585
|
+
`freshness proof does not match proposal ${proof.proposal_id}`
|
|
32586
|
+
);
|
|
32587
|
+
}
|
|
32588
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
32589
|
+
this.transaction(() => {
|
|
32590
|
+
this.appendEvent(proof.proposal_id, "proposal_freshness_checked", "runner", { proof });
|
|
32591
|
+
if (proof.result === "stale") {
|
|
32592
|
+
const current = this.requireProposal(proof.proposal_id);
|
|
32593
|
+
if (current.state === "pending_review" || current.state === "approved" || current.state === "pending_worker") {
|
|
32594
|
+
this.db.prepare("UPDATE proposals SET state = ?, updated_at = ? WHERE proposal_id = ?").run("conflict", now, proof.proposal_id);
|
|
32595
|
+
this.appendEvent(proof.proposal_id, "proposal_conflict", "runner", {
|
|
32596
|
+
reason: "freshness_stale",
|
|
32597
|
+
safe_code: proof.safe_code,
|
|
32598
|
+
proof_digest: proof.proof_digest
|
|
32599
|
+
});
|
|
32600
|
+
}
|
|
32601
|
+
}
|
|
32602
|
+
});
|
|
32603
|
+
return proof;
|
|
32604
|
+
}
|
|
32605
|
+
latestFreshnessProof(proposalId) {
|
|
32606
|
+
this.requireProposal(proposalId);
|
|
32607
|
+
const row = this.db.prepare(`
|
|
32608
|
+
SELECT payload_json
|
|
32609
|
+
FROM proposal_events
|
|
32610
|
+
WHERE proposal_id = ? AND kind = 'proposal_freshness_checked'
|
|
32611
|
+
ORDER BY event_id DESC
|
|
32612
|
+
LIMIT 1
|
|
32613
|
+
`).get(proposalId);
|
|
32614
|
+
if (!isRecord4(row)) return void 0;
|
|
32615
|
+
try {
|
|
32616
|
+
const payload = JSON.parse(String(row.payload_json));
|
|
32617
|
+
return parseFreshnessProof(payload.proof);
|
|
32618
|
+
} catch {
|
|
32619
|
+
throw new ProposalStoreError(
|
|
32620
|
+
"FRESHNESS_PROOF_TAMPERED",
|
|
32621
|
+
`stored freshness proof for proposal ${proposalId} failed integrity validation`
|
|
32622
|
+
);
|
|
32623
|
+
}
|
|
32624
|
+
}
|
|
32625
|
+
recordFreshnessApprovalBlocked(proposalId, input) {
|
|
32626
|
+
this.requireProposal(proposalId);
|
|
32627
|
+
const proof = this.latestFreshnessProof(proposalId);
|
|
32628
|
+
if (!proof || proof.proof_digest !== input.proof_digest || proof.result === "fresh") {
|
|
32629
|
+
throw new ProposalStoreError(
|
|
32630
|
+
"FRESHNESS_BLOCK_RECORD_INVALID",
|
|
32631
|
+
`freshness block for proposal ${proposalId} does not match its latest non-fresh proof`
|
|
32632
|
+
);
|
|
32633
|
+
}
|
|
32634
|
+
this.appendEvent(proposalId, "proposal_approval_blocked_freshness", input.actor, {
|
|
32635
|
+
proof_digest: proof.proof_digest,
|
|
32636
|
+
safe_code: input.safe_code,
|
|
32637
|
+
result: proof.result
|
|
32638
|
+
});
|
|
32639
|
+
}
|
|
32190
32640
|
approveProposal(proposalId, options) {
|
|
32191
32641
|
const proposal = this.requireProposal(proposalId);
|
|
32192
32642
|
assertWritebackAllowed(proposal, "approved");
|
|
@@ -32207,11 +32657,12 @@ var ProposalStore = class {
|
|
|
32207
32657
|
if (isRecord4(existing)) {
|
|
32208
32658
|
throw new ProposalStoreError("APPROVER_ALREADY_COUNTED", `operator ${options.approver} already recorded a decision for proposal ${proposalId}`);
|
|
32209
32659
|
}
|
|
32660
|
+
this.assertApprovalFreshness(current, options.freshness_proof_digest, now);
|
|
32210
32661
|
this.db.prepare(`
|
|
32211
32662
|
INSERT INTO approvals (
|
|
32212
32663
|
proposal_id, proposal_version, proposal_hash, approver, status, reason,
|
|
32213
|
-
identity_json, decision_hash, signature, integrity_hash, created_at
|
|
32214
|
-
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
32664
|
+
identity_json, decision_hash, signature, integrity_hash, freshness_proof_digest, created_at
|
|
32665
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
32215
32666
|
`).run(
|
|
32216
32667
|
proposalId,
|
|
32217
32668
|
options.proposal_version,
|
|
@@ -32223,6 +32674,7 @@ var ProposalStore = class {
|
|
|
32223
32674
|
options.identity?.decision_hash ?? null,
|
|
32224
32675
|
options.identity?.signature ?? null,
|
|
32225
32676
|
options.identity?.integrity_hash ?? null,
|
|
32677
|
+
options.freshness_proof_digest ?? null,
|
|
32226
32678
|
now
|
|
32227
32679
|
);
|
|
32228
32680
|
const progress = this.approvalProgress(proposalId);
|
|
@@ -32235,6 +32687,7 @@ var ProposalStore = class {
|
|
|
32235
32687
|
proposal_version: options.proposal_version,
|
|
32236
32688
|
reason: options.reason ?? null,
|
|
32237
32689
|
identity: publicIdentitySummary(options.identity),
|
|
32690
|
+
freshness_proof_digest: options.freshness_proof_digest ?? null,
|
|
32238
32691
|
approvals: progress.approved,
|
|
32239
32692
|
required_approvals: progress.required,
|
|
32240
32693
|
remaining_approvals: progress.remaining
|
|
@@ -32343,17 +32796,31 @@ var ProposalStore = class {
|
|
|
32343
32796
|
});
|
|
32344
32797
|
return;
|
|
32345
32798
|
}
|
|
32799
|
+
this.assertApprovalFreshness(proposal, options.freshness_proof_digest, now);
|
|
32346
32800
|
this.db.prepare("UPDATE proposals SET state = ?, updated_at = ? WHERE proposal_id = ?").run("approved", now, proposalId);
|
|
32347
32801
|
this.db.prepare(`
|
|
32348
|
-
INSERT INTO approvals (
|
|
32349
|
-
|
|
32350
|
-
|
|
32802
|
+
INSERT INTO approvals (
|
|
32803
|
+
proposal_id, proposal_version, proposal_hash, approver, status, reason,
|
|
32804
|
+
freshness_proof_digest, created_at
|
|
32805
|
+
)
|
|
32806
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
32807
|
+
`).run(
|
|
32808
|
+
proposalId,
|
|
32809
|
+
options.proposal_version,
|
|
32810
|
+
options.proposal_hash,
|
|
32811
|
+
actor,
|
|
32812
|
+
"approved",
|
|
32813
|
+
options.reason,
|
|
32814
|
+
options.freshness_proof_digest ?? null,
|
|
32815
|
+
now
|
|
32816
|
+
);
|
|
32351
32817
|
this.appendEvent(proposalId, "proposal_approved", actor, {
|
|
32352
32818
|
proposal_hash: options.proposal_hash,
|
|
32353
32819
|
proposal_version: options.proposal_version,
|
|
32354
32820
|
reason: options.reason,
|
|
32355
32821
|
policy: options.policy,
|
|
32356
|
-
aggregate_limits: options.limits ?? []
|
|
32822
|
+
aggregate_limits: options.limits ?? [],
|
|
32823
|
+
freshness_proof_digest: options.freshness_proof_digest ?? null
|
|
32357
32824
|
});
|
|
32358
32825
|
});
|
|
32359
32826
|
return {
|
|
@@ -32701,7 +33168,8 @@ var ProposalStore = class {
|
|
|
32701
33168
|
writeback_job_id: receipt.writeback_job_id,
|
|
32702
33169
|
rows_affected: receipt.rows_affected,
|
|
32703
33170
|
source_database_mutated: receipt.source_database_mutated,
|
|
32704
|
-
receipt_hash: receipt.receipt_hash
|
|
33171
|
+
receipt_hash: receipt.receipt_hash,
|
|
33172
|
+
safe_error_code: "safe_error_code" in receipt ? receipt.safe_error_code ?? null : null
|
|
32705
33173
|
});
|
|
32706
33174
|
}
|
|
32707
33175
|
recordHandlerWritebackJob(input) {
|
|
@@ -32785,6 +33253,7 @@ var ProposalStore = class {
|
|
|
32785
33253
|
tenant_guard: changeSet.guards.tenant,
|
|
32786
33254
|
...changeSet.guards.principal_scope ? { principal_scope: changeSet.guards.principal_scope } : {},
|
|
32787
33255
|
allowed_columns: changeSet.guards.allowed_columns,
|
|
33256
|
+
..."freshness" in changeSet && changeSet.freshness ? { freshness: parseFreshnessAuthority(changeSet.freshness) } : {},
|
|
32788
33257
|
idempotency_key: `${proposal.proposal_id}:${proposal.object_id}`,
|
|
32789
33258
|
lease
|
|
32790
33259
|
};
|
|
@@ -33238,7 +33707,21 @@ var ProposalStore = class {
|
|
|
33238
33707
|
const key = `${tenantId}\0${capability}`;
|
|
33239
33708
|
let row = rows.get(key);
|
|
33240
33709
|
if (!row) {
|
|
33241
|
-
row = {
|
|
33710
|
+
row = {
|
|
33711
|
+
tenant_id: tenantId,
|
|
33712
|
+
capability,
|
|
33713
|
+
worker_retries: 0,
|
|
33714
|
+
dead_letters: 0,
|
|
33715
|
+
auto_approval_limit_trips: 0,
|
|
33716
|
+
freshness_checks: 0,
|
|
33717
|
+
freshness_fresh: 0,
|
|
33718
|
+
freshness_stale_target: 0,
|
|
33719
|
+
freshness_stale_supporting: 0,
|
|
33720
|
+
freshness_unavailable: 0,
|
|
33721
|
+
freshness_unsupported: 0,
|
|
33722
|
+
freshness_approval_blocked: 0,
|
|
33723
|
+
freshness_apply_blocked: 0
|
|
33724
|
+
};
|
|
33242
33725
|
rows.set(key, row);
|
|
33243
33726
|
}
|
|
33244
33727
|
return row;
|
|
@@ -33246,7 +33729,14 @@ var ProposalStore = class {
|
|
|
33246
33729
|
const events2 = this.db.prepare(`
|
|
33247
33730
|
SELECT p.tenant_id, p.action, e.kind, e.payload_json
|
|
33248
33731
|
FROM proposal_events e JOIN proposals p ON p.proposal_id = e.proposal_id
|
|
33249
|
-
WHERE e.kind IN (
|
|
33732
|
+
WHERE e.kind IN (
|
|
33733
|
+
'writeback_retry_scheduled',
|
|
33734
|
+
'writeback_dead_lettered',
|
|
33735
|
+
'policy_auto_approval_deferred',
|
|
33736
|
+
'proposal_freshness_checked',
|
|
33737
|
+
'proposal_approval_blocked_freshness',
|
|
33738
|
+
'writeback_conflict'
|
|
33739
|
+
)
|
|
33250
33740
|
AND (? IS NULL OR p.tenant_id = ?)
|
|
33251
33741
|
AND (? IS NULL OR p.action = ?)
|
|
33252
33742
|
`).all(filters.tenant ?? null, filters.tenant ?? null, filters.capability ?? null, filters.capability ?? null);
|
|
@@ -33255,13 +33745,29 @@ var ProposalStore = class {
|
|
|
33255
33745
|
const row = ensure(String(raw.tenant_id), String(raw.action));
|
|
33256
33746
|
if (raw.kind === "writeback_retry_scheduled") row.worker_retries += 1;
|
|
33257
33747
|
if (raw.kind === "writeback_dead_lettered") row.dead_letters += 1;
|
|
33258
|
-
|
|
33259
|
-
|
|
33260
|
-
|
|
33261
|
-
|
|
33262
|
-
|
|
33748
|
+
let payload = {};
|
|
33749
|
+
try {
|
|
33750
|
+
const parsed = JSON.parse(String(raw.payload_json));
|
|
33751
|
+
if (isRecord4(parsed)) payload = parsed;
|
|
33752
|
+
} catch {
|
|
33753
|
+
}
|
|
33754
|
+
if (raw.kind === "proposal_freshness_checked") {
|
|
33755
|
+
row.freshness_checks += 1;
|
|
33756
|
+
const proof = isRecord4(payload.proof) ? payload.proof : {};
|
|
33757
|
+
if (proof.result === "fresh") row.freshness_fresh += 1;
|
|
33758
|
+
if (proof.result === "unavailable") row.freshness_unavailable += 1;
|
|
33759
|
+
if (proof.result === "unsupported") row.freshness_unsupported += 1;
|
|
33760
|
+
if (proof.result === "stale") {
|
|
33761
|
+
const checks = Array.isArray(proof.checks) ? proof.checks.filter(isRecord4) : [];
|
|
33762
|
+
if (checks.some((check) => check.kind === "target" && check.status === "stale")) row.freshness_stale_target += 1;
|
|
33763
|
+
if (checks.some((check) => check.kind === "supporting" && check.status === "stale")) row.freshness_stale_supporting += 1;
|
|
33263
33764
|
}
|
|
33264
33765
|
}
|
|
33766
|
+
if (raw.kind === "proposal_approval_blocked_freshness") row.freshness_approval_blocked += 1;
|
|
33767
|
+
if (raw.kind === "writeback_conflict" && /^FRESHNESS_/.test(String(payload.safe_error_code ?? ""))) row.freshness_apply_blocked += 1;
|
|
33768
|
+
if (raw.kind === "policy_auto_approval_deferred") {
|
|
33769
|
+
if (Array.isArray(payload.tripped_limits) && payload.tripped_limits.length > 0) row.auto_approval_limit_trips += 1;
|
|
33770
|
+
}
|
|
33265
33771
|
}
|
|
33266
33772
|
return [...rows.values()].sort((left, right) => left.tenant_id.localeCompare(right.tenant_id) || left.capability.localeCompare(right.capability));
|
|
33267
33773
|
}
|
|
@@ -34109,6 +34615,61 @@ var ProposalStore = class {
|
|
|
34109
34615
|
}
|
|
34110
34616
|
return proposal;
|
|
34111
34617
|
}
|
|
34618
|
+
assertApprovalFreshness(proposal, proofDigest, now) {
|
|
34619
|
+
const authority = proposalFreshnessAuthority(proposal);
|
|
34620
|
+
if (!authority) {
|
|
34621
|
+
if (proofDigest !== void 0) {
|
|
34622
|
+
throw new ProposalStoreError(
|
|
34623
|
+
"FRESHNESS_PROOF_UNEXPECTED",
|
|
34624
|
+
`proposal ${proposal.proposal_id} does not require a freshness proof`
|
|
34625
|
+
);
|
|
34626
|
+
}
|
|
34627
|
+
return;
|
|
34628
|
+
}
|
|
34629
|
+
if (!proofDigest) {
|
|
34630
|
+
throw new ProposalStoreError(
|
|
34631
|
+
"FRESHNESS_PROOF_REQUIRED",
|
|
34632
|
+
`proposal ${proposal.proposal_id} requires a fresh live proof before approval`
|
|
34633
|
+
);
|
|
34634
|
+
}
|
|
34635
|
+
const proof = this.latestFreshnessProof(proposal.proposal_id);
|
|
34636
|
+
if (!proof || proof.proof_digest !== proofDigest) {
|
|
34637
|
+
throw new ProposalStoreError(
|
|
34638
|
+
"FRESHNESS_PROOF_MISSING",
|
|
34639
|
+
`the latest freshness proof for proposal ${proposal.proposal_id} was not supplied`
|
|
34640
|
+
);
|
|
34641
|
+
}
|
|
34642
|
+
if (proof.proposal_hash !== proposal.proposal_hash || proof.proposal_version !== proposal.proposal_version || proof.dependency_set_digest !== authority.dependency_set_digest) {
|
|
34643
|
+
throw new ProposalStoreError(
|
|
34644
|
+
"FRESHNESS_PROOF_AUTHORITY_MISMATCH",
|
|
34645
|
+
`freshness proof does not match proposal ${proposal.proposal_id}`
|
|
34646
|
+
);
|
|
34647
|
+
}
|
|
34648
|
+
if (proof.result !== "fresh") {
|
|
34649
|
+
throw new ProposalStoreError(
|
|
34650
|
+
proof.result === "stale" ? "FRESHNESS_STALE" : "FRESHNESS_NOT_VERIFIED",
|
|
34651
|
+
`proposal ${proposal.proposal_id} freshness result is ${proof.result}`
|
|
34652
|
+
);
|
|
34653
|
+
}
|
|
34654
|
+
if (Date.parse(proof.valid_until) < Date.parse(now)) {
|
|
34655
|
+
throw new ProposalStoreError(
|
|
34656
|
+
"FRESHNESS_PROOF_EXPIRED",
|
|
34657
|
+
`freshness proof for proposal ${proposal.proposal_id} has expired`
|
|
34658
|
+
);
|
|
34659
|
+
}
|
|
34660
|
+
const used = this.db.prepare(`
|
|
34661
|
+
SELECT approval_id
|
|
34662
|
+
FROM approvals
|
|
34663
|
+
WHERE proposal_id = ? AND freshness_proof_digest = ?
|
|
34664
|
+
LIMIT 1
|
|
34665
|
+
`).get(proposal.proposal_id, proofDigest);
|
|
34666
|
+
if (isRecord4(used)) {
|
|
34667
|
+
throw new ProposalStoreError(
|
|
34668
|
+
"FRESHNESS_PROOF_ALREADY_USED",
|
|
34669
|
+
`freshness proof for proposal ${proposal.proposal_id} already authorized a reviewer decision`
|
|
34670
|
+
);
|
|
34671
|
+
}
|
|
34672
|
+
}
|
|
34112
34673
|
setState(proposalId, state, actor, payload) {
|
|
34113
34674
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
34114
34675
|
this.transaction(() => {
|
|
@@ -34269,6 +34830,17 @@ function stateFromChangeSet(changeSet) {
|
|
|
34269
34830
|
if (changeSet.approval.status === "canceled") return "canceled";
|
|
34270
34831
|
return "pending_review";
|
|
34271
34832
|
}
|
|
34833
|
+
function proposalFreshnessAuthority(proposal) {
|
|
34834
|
+
if (!("freshness" in proposal.change_set) || proposal.change_set.freshness === void 0) return void 0;
|
|
34835
|
+
try {
|
|
34836
|
+
return parseFreshnessAuthority(proposal.change_set.freshness);
|
|
34837
|
+
} catch {
|
|
34838
|
+
throw new ProposalStoreError(
|
|
34839
|
+
"FRESHNESS_AUTHORITY_TAMPERED",
|
|
34840
|
+
`stored freshness authority for proposal ${proposal.proposal_id} failed integrity validation`
|
|
34841
|
+
);
|
|
34842
|
+
}
|
|
34843
|
+
}
|
|
34272
34844
|
function requiredApprovalCount(proposal) {
|
|
34273
34845
|
const configured = proposal.change_set.approval.required_approvals;
|
|
34274
34846
|
return typeof configured === "number" && Number.isSafeInteger(configured) && configured >= 1 ? configured : 1;
|
|
@@ -34795,6 +35367,7 @@ function rowToApproval(row) {
|
|
|
34795
35367
|
decision_hash: row.decision_hash == null ? void 0 : String(row.decision_hash),
|
|
34796
35368
|
signature: row.signature == null ? void 0 : String(row.signature),
|
|
34797
35369
|
integrity_hash: row.integrity_hash == null ? void 0 : String(row.integrity_hash),
|
|
35370
|
+
freshness_proof_digest: row.freshness_proof_digest == null ? void 0 : String(row.freshness_proof_digest),
|
|
34798
35371
|
created_at: String(row.created_at)
|
|
34799
35372
|
};
|
|
34800
35373
|
}
|
|
@@ -35218,7 +35791,7 @@ var sharedLedgerRestoreSpecs = {
|
|
|
35218
35791
|
"updated_at"
|
|
35219
35792
|
], ["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"]),
|
|
35220
35793
|
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"]),
|
|
35221
|
-
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"]),
|
|
35794
|
+
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"]),
|
|
35222
35795
|
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"]),
|
|
35223
35796
|
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"]),
|
|
35224
35797
|
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"]),
|
|
@@ -41327,6 +41900,18 @@ async function callConfiguredTool(input) {
|
|
|
41327
41900
|
at: createdAt
|
|
41328
41901
|
});
|
|
41329
41902
|
const queryFingerprint = queryFingerprintFor(capability, context);
|
|
41903
|
+
const freshnessCapture = capability.kind === "proposal" ? await captureProposalFreshnessAuthority({
|
|
41904
|
+
config: input.config,
|
|
41905
|
+
capability,
|
|
41906
|
+
args: input.args,
|
|
41907
|
+
context,
|
|
41908
|
+
source,
|
|
41909
|
+
readRow: input.readRow,
|
|
41910
|
+
env: input.env,
|
|
41911
|
+
proposalId,
|
|
41912
|
+
createdAt,
|
|
41913
|
+
targetMemberCount: operation === "insert" ? 0 : setOperation ? currentRows.length : 1
|
|
41914
|
+
}) : void 0;
|
|
41330
41915
|
const changeSet = capability.kind === "proposal" ? buildChangeSet({
|
|
41331
41916
|
config: input.config,
|
|
41332
41917
|
capability,
|
|
@@ -41344,7 +41929,8 @@ async function callConfiguredTool(input) {
|
|
|
41344
41929
|
resolvedDeduplication,
|
|
41345
41930
|
evidenceBundleId,
|
|
41346
41931
|
queryFingerprint,
|
|
41347
|
-
objectId
|
|
41932
|
+
objectId,
|
|
41933
|
+
freshness: freshnessCapture?.authority
|
|
41348
41934
|
}) : void 0;
|
|
41349
41935
|
await input.store.recordEvidenceBundle({
|
|
41350
41936
|
evidence_bundle_id: evidenceBundleId,
|
|
@@ -41389,6 +41975,48 @@ async function callConfiguredTool(input) {
|
|
|
41389
41975
|
}
|
|
41390
41976
|
});
|
|
41391
41977
|
}
|
|
41978
|
+
for (const supporting of freshnessCapture?.evidence ?? []) {
|
|
41979
|
+
await input.store.recordEvidenceBundle({
|
|
41980
|
+
evidence_bundle_id: supporting.bundle_id,
|
|
41981
|
+
tenant_id: context.tenant_id,
|
|
41982
|
+
capability: supporting.capability.name,
|
|
41983
|
+
source_id: supporting.capability.source,
|
|
41984
|
+
source_table: `${supporting.capability.target.schema}.${supporting.capability.target.table}`,
|
|
41985
|
+
business_object: supporting.capability.target.table,
|
|
41986
|
+
object_id: String(supporting.primary_key.value),
|
|
41987
|
+
query_fingerprint: supporting.query_fingerprint,
|
|
41988
|
+
payload: {
|
|
41989
|
+
kind: "freshness_dependency",
|
|
41990
|
+
dependency_id: supporting.dependency_id,
|
|
41991
|
+
capability: supporting.capability.name,
|
|
41992
|
+
source_id: supporting.capability.source,
|
|
41993
|
+
target: `${supporting.capability.target.schema}.${supporting.capability.target.table}`,
|
|
41994
|
+
parameters_redacted: true,
|
|
41995
|
+
source_database_changed: false
|
|
41996
|
+
},
|
|
41997
|
+
items: []
|
|
41998
|
+
});
|
|
41999
|
+
await input.store.recordQueryAudit({
|
|
42000
|
+
evidence_bundle_id: supporting.bundle_id,
|
|
42001
|
+
capability: supporting.capability.name,
|
|
42002
|
+
source_id: supporting.capability.source,
|
|
42003
|
+
query_fingerprint: supporting.query_fingerprint,
|
|
42004
|
+
table_name: `${supporting.capability.target.schema}.${supporting.capability.target.table}`,
|
|
42005
|
+
business_object: supporting.capability.target.table,
|
|
42006
|
+
object_id: String(supporting.primary_key.value),
|
|
42007
|
+
primary_key_value: String(supporting.primary_key.value),
|
|
42008
|
+
row_count: 1,
|
|
42009
|
+
payload: {
|
|
42010
|
+
kind: "freshness_dependency",
|
|
42011
|
+
dependency_id: supporting.dependency_id,
|
|
42012
|
+
capability: supporting.capability.name,
|
|
42013
|
+
columns: [supporting.version_column],
|
|
42014
|
+
parameters_redacted: true,
|
|
42015
|
+
tenant_bound: Boolean(supporting.capability.target.tenant_key),
|
|
42016
|
+
principal_bound: Boolean(supporting.capability.target.principal_scope_key)
|
|
42017
|
+
}
|
|
42018
|
+
});
|
|
42019
|
+
}
|
|
41392
42020
|
if (capability.kind === "read") {
|
|
41393
42021
|
return {
|
|
41394
42022
|
status: "ok",
|
|
@@ -41438,7 +42066,9 @@ async function callConfiguredTool(input) {
|
|
|
41438
42066
|
capability,
|
|
41439
42067
|
store: input.store,
|
|
41440
42068
|
proposal,
|
|
41441
|
-
patch: changeSet.patch
|
|
42069
|
+
patch: changeSet.patch,
|
|
42070
|
+
env: input.env,
|
|
42071
|
+
readRow: input.readRow
|
|
41442
42072
|
});
|
|
41443
42073
|
await input.store.recordEvidenceBundle({
|
|
41444
42074
|
evidence_bundle_id: evidenceBundleId,
|
|
@@ -41486,7 +42116,7 @@ async function callConfiguredTool(input) {
|
|
|
41486
42116
|
env: input.env
|
|
41487
42117
|
});
|
|
41488
42118
|
return {
|
|
41489
|
-
status: input.config.governance?.mode === "cloud_linked" ? "pending_cloud_sync" : input.config.mode === "shadow" ? "shadow_proposal_created" : approvalResult.proposal.state === "approved" ? "approved" : "review_required",
|
|
42119
|
+
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",
|
|
41490
42120
|
action: capability.name,
|
|
41491
42121
|
proposal_id: approvalResult.proposal.proposal_id,
|
|
41492
42122
|
proposal_version: approvalResult.proposal.proposal_version,
|
|
@@ -41511,6 +42141,7 @@ async function callConfiguredTool(input) {
|
|
|
41511
42141
|
} : {}
|
|
41512
42142
|
},
|
|
41513
42143
|
approval_required: approvalResult.proposal.state === "pending_review",
|
|
42144
|
+
...approvalResult.freshness ? { freshness: approvalResult.freshness } : {},
|
|
41514
42145
|
governance: input.config.governance?.mode === "cloud_linked" ? { authority: "synapsor_cloud", state: "pending_cloud_sync", evidence_residency: "metadata_only" } : { authority: "local" },
|
|
41515
42146
|
writeback: changeSet.writeback,
|
|
41516
42147
|
source_database_changed: false,
|
|
@@ -41993,18 +42624,52 @@ async function maybeAutoApproveProposal(input) {
|
|
|
41993
42624
|
if (!policy) throw new McpRuntimeError("APPROVAL_POLICY_UNRESOLVED", `capability ${input.capability.name} references missing approval policy ${policyName}.`);
|
|
41994
42625
|
const evaluation = evaluateApprovalPolicy(input.capability, policy, input.patch);
|
|
41995
42626
|
if (!evaluation.qualifies) return { proposal: input.proposal, approved: false, policy: policyName };
|
|
42627
|
+
let freshnessProofDigest;
|
|
42628
|
+
const freshness = await evaluateProposalFreshness({
|
|
42629
|
+
config: input.config,
|
|
42630
|
+
proposal: input.proposal,
|
|
42631
|
+
env: input.env,
|
|
42632
|
+
readRow: input.readRow
|
|
42633
|
+
});
|
|
42634
|
+
if (freshness.required) {
|
|
42635
|
+
await input.store.recordFreshnessProof(freshness.proof);
|
|
42636
|
+
if (freshness.status !== "fresh") {
|
|
42637
|
+
await input.store.recordFreshnessApprovalBlocked(input.proposal.proposal_id, {
|
|
42638
|
+
proof_digest: freshness.proof.proof_digest,
|
|
42639
|
+
safe_code: freshness.safe_code,
|
|
42640
|
+
actor: `policy:${policyName}`
|
|
42641
|
+
});
|
|
42642
|
+
return {
|
|
42643
|
+
proposal: await input.store.getProposal(input.proposal.proposal_id) ?? input.proposal,
|
|
42644
|
+
approved: false,
|
|
42645
|
+
policy: policyName,
|
|
42646
|
+
freshness: {
|
|
42647
|
+
status: freshness.status,
|
|
42648
|
+
safe_code: freshness.safe_code
|
|
42649
|
+
}
|
|
42650
|
+
};
|
|
42651
|
+
}
|
|
42652
|
+
freshnessProofDigest = freshness.proof.proof_digest;
|
|
42653
|
+
}
|
|
41996
42654
|
const decision = await input.store.approveProposalByPolicy(input.proposal.proposal_id, {
|
|
41997
42655
|
policy: policyName,
|
|
41998
42656
|
proposal_hash: input.proposal.proposal_hash,
|
|
41999
42657
|
proposal_version: input.proposal.proposal_version,
|
|
42000
42658
|
reason: `auto-approved by policy ${policyName}: ${evaluation.reason}`,
|
|
42001
|
-
limits: policy.limits
|
|
42659
|
+
limits: policy.limits,
|
|
42660
|
+
freshness_proof_digest: freshnessProofDigest
|
|
42002
42661
|
});
|
|
42003
42662
|
return {
|
|
42004
42663
|
proposal: decision.proposal,
|
|
42005
42664
|
approved: decision.approved,
|
|
42006
42665
|
policy: policyName,
|
|
42007
|
-
tripped_limits: decision.tripped_limits
|
|
42666
|
+
tripped_limits: decision.tripped_limits,
|
|
42667
|
+
...freshness.required ? {
|
|
42668
|
+
freshness: {
|
|
42669
|
+
status: freshness.status,
|
|
42670
|
+
safe_code: freshness.safe_code
|
|
42671
|
+
}
|
|
42672
|
+
} : {}
|
|
42008
42673
|
};
|
|
42009
42674
|
}
|
|
42010
42675
|
function approvalPolicyByName(config, policyName) {
|
|
@@ -42272,6 +42937,372 @@ function errorMessage(error) {
|
|
|
42272
42937
|
if (isRecord6(error) && typeof error.message === "string") return error.message;
|
|
42273
42938
|
return typeof error === "string" ? error : "";
|
|
42274
42939
|
}
|
|
42940
|
+
async function captureProposalFreshnessAuthority(input) {
|
|
42941
|
+
const policy = input.config.proposal_freshness?.[input.capability.name];
|
|
42942
|
+
if (!policy) return void 0;
|
|
42943
|
+
const operation = input.capability.operation?.kind ?? "update";
|
|
42944
|
+
const targetMode = operation === "insert" ? "not_applicable" : isSetCapability(input.capability) ? "frozen_set" : "exact_guard";
|
|
42945
|
+
const captured = [];
|
|
42946
|
+
for (const configured of policy.dependencies ?? []) {
|
|
42947
|
+
const capability = (input.config.capabilities ?? []).find((item) => item.name === configured.capability);
|
|
42948
|
+
if (!capability || capability.kind !== "read") {
|
|
42949
|
+
throw new McpRuntimeError("FRESHNESS_DEPENDENCY_INVALID", `Reviewed freshness dependency ${configured.id} is unavailable.`);
|
|
42950
|
+
}
|
|
42951
|
+
const identity = scalar3(input.args[configured.identity_from_arg]);
|
|
42952
|
+
if (identity === null) {
|
|
42953
|
+
throw new McpRuntimeError("FRESHNESS_DEPENDENCY_IDENTITY_MISSING", `Freshness dependency ${configured.id} requires its reviewed scalar identity.`);
|
|
42954
|
+
}
|
|
42955
|
+
const readCapability = {
|
|
42956
|
+
...capability,
|
|
42957
|
+
conflict_guard: { column: configured.version_column }
|
|
42958
|
+
};
|
|
42959
|
+
const current = await input.readRow({
|
|
42960
|
+
sourceName: capability.source,
|
|
42961
|
+
source: input.source,
|
|
42962
|
+
capability: readCapability,
|
|
42963
|
+
args: { [capability.lookup.id_from_arg]: identity },
|
|
42964
|
+
context: input.context,
|
|
42965
|
+
env: input.env,
|
|
42966
|
+
transaction_mode: "read_only"
|
|
42967
|
+
});
|
|
42968
|
+
if (current.rowCount !== 1) {
|
|
42969
|
+
throw new McpRuntimeError("FRESHNESS_DEPENDENCY_UNRESOLVED", `Freshness dependency ${configured.id} did not resolve to one authorized row.`);
|
|
42970
|
+
}
|
|
42971
|
+
const primaryValue = scalar3(current.row[capability.target.primary_key]);
|
|
42972
|
+
const versionValue = scalar3(current.row[configured.version_column]);
|
|
42973
|
+
if (primaryValue === null || versionValue === null) {
|
|
42974
|
+
throw new McpRuntimeError("FRESHNESS_DEPENDENCY_VERSION_MISSING", `Freshness dependency ${configured.id} lacks an exact primary-key or version value.`);
|
|
42975
|
+
}
|
|
42976
|
+
const bundleId = stableId("evf", {
|
|
42977
|
+
proposal_id: input.proposalId,
|
|
42978
|
+
dependency_id: configured.id,
|
|
42979
|
+
primary_key: primaryValue,
|
|
42980
|
+
created_at: input.createdAt
|
|
42981
|
+
});
|
|
42982
|
+
const queryFingerprint = queryFingerprintFor(readCapability, input.context);
|
|
42983
|
+
const unsigned2 = {
|
|
42984
|
+
id: configured.id,
|
|
42985
|
+
capability: configured.capability,
|
|
42986
|
+
source_id: capability.source,
|
|
42987
|
+
engine: input.source.engine,
|
|
42988
|
+
target: {
|
|
42989
|
+
schema: capability.target.schema,
|
|
42990
|
+
table: capability.target.table,
|
|
42991
|
+
primary_key: { column: capability.target.primary_key, value: primaryValue },
|
|
42992
|
+
tenant_column: capability.target.tenant_key ?? "__single_tenant_dev",
|
|
42993
|
+
...capability.target.principal_scope_key ? { principal_column: capability.target.principal_scope_key } : {}
|
|
42994
|
+
},
|
|
42995
|
+
expected_version: { column: configured.version_column, value: conflictGuardScalar(versionValue) },
|
|
42996
|
+
evidence: {
|
|
42997
|
+
bundle_id: bundleId,
|
|
42998
|
+
query_fingerprint: queryFingerprint
|
|
42999
|
+
}
|
|
43000
|
+
};
|
|
43001
|
+
const descriptor = {
|
|
43002
|
+
...unsigned2,
|
|
43003
|
+
descriptor_digest: canonicalJsonDigest(unsigned2)
|
|
43004
|
+
};
|
|
43005
|
+
captured.push({
|
|
43006
|
+
descriptor,
|
|
43007
|
+
evidence: {
|
|
43008
|
+
dependency_id: configured.id,
|
|
43009
|
+
bundle_id: bundleId,
|
|
43010
|
+
query_fingerprint: queryFingerprint,
|
|
43011
|
+
capability,
|
|
43012
|
+
primary_key: descriptor.target.primary_key,
|
|
43013
|
+
version_column: configured.version_column
|
|
43014
|
+
}
|
|
43015
|
+
});
|
|
43016
|
+
}
|
|
43017
|
+
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));
|
|
43018
|
+
const unsigned = {
|
|
43019
|
+
schema_version: protocolVersions.freshnessAuthority,
|
|
43020
|
+
required: true,
|
|
43021
|
+
target: { mode: targetMode, member_count: input.targetMemberCount },
|
|
43022
|
+
dependencies: captured.map((item) => item.descriptor)
|
|
43023
|
+
};
|
|
43024
|
+
const authority = parseFreshnessAuthority({
|
|
43025
|
+
...unsigned,
|
|
43026
|
+
dependency_set_digest: canonicalJsonDigest(unsigned)
|
|
43027
|
+
});
|
|
43028
|
+
return {
|
|
43029
|
+
authority,
|
|
43030
|
+
evidence: captured.map((item) => item.evidence)
|
|
43031
|
+
};
|
|
43032
|
+
}
|
|
43033
|
+
async function evaluateProposalFreshness(input) {
|
|
43034
|
+
const config = resolveRuntimeConfig(input.config);
|
|
43035
|
+
assertValidRunnerCapabilityConfig(config);
|
|
43036
|
+
const authority = freshnessAuthorityFromChangeSet(input.proposal.change_set);
|
|
43037
|
+
if (!authority) {
|
|
43038
|
+
return {
|
|
43039
|
+
required: false,
|
|
43040
|
+
status: "not_required",
|
|
43041
|
+
safe_code: "FRESHNESS_NOT_REQUIRED",
|
|
43042
|
+
target_count: 0,
|
|
43043
|
+
supporting_count: 0
|
|
43044
|
+
};
|
|
43045
|
+
}
|
|
43046
|
+
const env = input.env ?? process.env;
|
|
43047
|
+
const resources = createMcpRuntimeSharedResources(config, env, input.readRow, input.clock, input.credentialResolver);
|
|
43048
|
+
const checkedAtMs = (input.clock ?? Date.now)();
|
|
43049
|
+
const checks = [];
|
|
43050
|
+
let forcedResult;
|
|
43051
|
+
let forcedCode;
|
|
43052
|
+
try {
|
|
43053
|
+
const capability = (config.capabilities ?? []).find((item) => item.name === input.proposal.action);
|
|
43054
|
+
const source = capability ? config.sources?.[capability.source] : void 0;
|
|
43055
|
+
if (!capability || capability.kind !== "proposal" || !source) {
|
|
43056
|
+
forcedResult = "invalid";
|
|
43057
|
+
forcedCode = "FRESHNESS_PROPOSAL_AUTHORITY_INVALID";
|
|
43058
|
+
} else if (!proposalAuthorityMatchesCapability(input.proposal, capability)) {
|
|
43059
|
+
forcedResult = "invalid";
|
|
43060
|
+
forcedCode = "FRESHNESS_PROPOSAL_AUTHORITY_MISMATCH";
|
|
43061
|
+
} else {
|
|
43062
|
+
const context = proposalTrustedContext(config, capability, input.proposal);
|
|
43063
|
+
const targetChecks = await evaluateTargetFreshness({
|
|
43064
|
+
proposal: input.proposal,
|
|
43065
|
+
authority,
|
|
43066
|
+
capability,
|
|
43067
|
+
source,
|
|
43068
|
+
context,
|
|
43069
|
+
env,
|
|
43070
|
+
readRow: resources.readRow
|
|
43071
|
+
});
|
|
43072
|
+
checks.push(...targetChecks);
|
|
43073
|
+
for (const dependency of authority.dependencies) {
|
|
43074
|
+
const validationCode = validateResolvedFreshnessDependency(config, capability, dependency);
|
|
43075
|
+
if (validationCode) {
|
|
43076
|
+
checks.push({
|
|
43077
|
+
id: dependency.id,
|
|
43078
|
+
kind: "supporting",
|
|
43079
|
+
status: "invalid",
|
|
43080
|
+
safe_code: validationCode
|
|
43081
|
+
});
|
|
43082
|
+
continue;
|
|
43083
|
+
}
|
|
43084
|
+
const supporting = (config.capabilities ?? []).find((item) => item.name === dependency.capability);
|
|
43085
|
+
checks.push(await evaluateSupportingFreshness({
|
|
43086
|
+
dependency,
|
|
43087
|
+
capability: supporting,
|
|
43088
|
+
source,
|
|
43089
|
+
context,
|
|
43090
|
+
env,
|
|
43091
|
+
readRow: resources.readRow
|
|
43092
|
+
}));
|
|
43093
|
+
}
|
|
43094
|
+
}
|
|
43095
|
+
} catch (error) {
|
|
43096
|
+
const classified = classifyFreshnessReadError(error);
|
|
43097
|
+
forcedResult = classified.result;
|
|
43098
|
+
forcedCode = classified.safe_code;
|
|
43099
|
+
} finally {
|
|
43100
|
+
await resources.close();
|
|
43101
|
+
}
|
|
43102
|
+
const status = forcedResult ?? freshnessResultForChecks(checks);
|
|
43103
|
+
const safeCode2 = forcedCode ?? freshnessSafeCode(status, checks);
|
|
43104
|
+
const targetCount = checks.filter((check) => check.kind === "target").length;
|
|
43105
|
+
const supportingCount = checks.filter((check) => check.kind === "supporting").length;
|
|
43106
|
+
const adapters2 = freshnessSourceAdapters(input.proposal, authority);
|
|
43107
|
+
const unsigned = {
|
|
43108
|
+
schema_version: protocolVersions.freshnessProof,
|
|
43109
|
+
proposal_id: input.proposal.proposal_id,
|
|
43110
|
+
proposal_hash: input.proposal.proposal_hash,
|
|
43111
|
+
proposal_version: input.proposal.proposal_version,
|
|
43112
|
+
dependency_set_digest: authority.dependency_set_digest,
|
|
43113
|
+
checked_at: new Date(checkedAtMs).toISOString(),
|
|
43114
|
+
valid_until: new Date(checkedAtMs + Math.max(1e3, Math.min(input.proofValidityMs ?? 3e4, 3e5))).toISOString(),
|
|
43115
|
+
source_adapters: adapters2,
|
|
43116
|
+
result: status,
|
|
43117
|
+
safe_code: safeCode2,
|
|
43118
|
+
target_count: targetCount,
|
|
43119
|
+
supporting_count: supportingCount,
|
|
43120
|
+
checks
|
|
43121
|
+
};
|
|
43122
|
+
const proof = parseFreshnessProof({
|
|
43123
|
+
...unsigned,
|
|
43124
|
+
proof_digest: canonicalJsonDigest(unsigned)
|
|
43125
|
+
});
|
|
43126
|
+
return {
|
|
43127
|
+
required: true,
|
|
43128
|
+
status,
|
|
43129
|
+
safe_code: safeCode2,
|
|
43130
|
+
target_count: targetCount,
|
|
43131
|
+
supporting_count: supportingCount,
|
|
43132
|
+
proof
|
|
43133
|
+
};
|
|
43134
|
+
}
|
|
43135
|
+
function freshnessAuthorityFromChangeSet(changeSet) {
|
|
43136
|
+
if (!("freshness" in changeSet) || changeSet.freshness === void 0) return void 0;
|
|
43137
|
+
return parseFreshnessAuthority(changeSet.freshness);
|
|
43138
|
+
}
|
|
43139
|
+
function proposalAuthorityMatchesCapability(proposal, capability) {
|
|
43140
|
+
const changeSet = proposal.change_set;
|
|
43141
|
+
if (proposal.proposal_hash !== changeSet.integrity.proposal_hash || proposal.proposal_version !== changeSet.proposal_version) return false;
|
|
43142
|
+
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;
|
|
43143
|
+
if (changeSet.contract && (!capability.contract_provenance || changeSet.contract.digest !== capability.contract_provenance.digest || changeSet.contract.version !== capability.contract_provenance.version)) return false;
|
|
43144
|
+
return true;
|
|
43145
|
+
}
|
|
43146
|
+
function proposalTrustedContext(config, capability, proposal) {
|
|
43147
|
+
const contextConfig = (capability.context ? config.contexts?.[capability.context] : void 0) ?? config.trusted_context;
|
|
43148
|
+
const provider = contextConfig?.provider ?? (proposal.change_set.principal.source === "cloud_session" ? "cloud_session" : "environment");
|
|
43149
|
+
return {
|
|
43150
|
+
tenant_id: proposal.tenant_id,
|
|
43151
|
+
principal: proposal.principal ?? proposal.change_set.principal.id,
|
|
43152
|
+
provenance: provider
|
|
43153
|
+
};
|
|
43154
|
+
}
|
|
43155
|
+
async function evaluateTargetFreshness(input) {
|
|
43156
|
+
if (input.authority.target.mode === "not_applicable") return [];
|
|
43157
|
+
if (input.authority.target.mode === "frozen_set") {
|
|
43158
|
+
const changeSet2 = input.proposal.change_set;
|
|
43159
|
+
if (changeSet2.schema_version !== protocolVersions.changeSetV3) {
|
|
43160
|
+
return [{ id: "target", kind: "target", status: "invalid", safe_code: "FRESHNESS_TARGET_SET_INVALID" }];
|
|
43161
|
+
}
|
|
43162
|
+
const current2 = await input.readRow({
|
|
43163
|
+
sourceName: input.capability.source,
|
|
43164
|
+
source: input.source,
|
|
43165
|
+
capability: input.capability,
|
|
43166
|
+
args: {},
|
|
43167
|
+
context: input.context,
|
|
43168
|
+
env: input.env,
|
|
43169
|
+
transaction_mode: "read_only"
|
|
43170
|
+
});
|
|
43171
|
+
const rows = current2.rows ?? (current2.rowCount === 1 ? [current2.row] : []);
|
|
43172
|
+
const byIdentity = new Map(rows.map((row) => [JSON.stringify(scalar3(row[input.capability.target.primary_key])), row]));
|
|
43173
|
+
return changeSet2.frozen_set.members.map((member) => {
|
|
43174
|
+
const expected2 = member.expected_version;
|
|
43175
|
+
const row = byIdentity.get(JSON.stringify(member.primary_key.value));
|
|
43176
|
+
const observed2 = expected2 && row ? conflictGuardScalar(scalar3(row[expected2.column])) : void 0;
|
|
43177
|
+
const fresh2 = Boolean(expected2 && row && versionsEqual(observed2, expected2.value));
|
|
43178
|
+
return {
|
|
43179
|
+
id: `target:${canonicalJsonDigest(member.primary_key).slice(7, 23)}`,
|
|
43180
|
+
kind: "target",
|
|
43181
|
+
status: fresh2 ? "fresh" : "stale",
|
|
43182
|
+
safe_code: fresh2 ? "FRESHNESS_TARGET_FRESH" : "FRESHNESS_TARGET_STALE",
|
|
43183
|
+
...expected2 ? { expected_version_digest: versionMetadataDigest(expected2.column, expected2.value) } : {},
|
|
43184
|
+
...expected2 && observed2 !== void 0 ? { observed_version_digest: versionMetadataDigest(expected2.column, observed2) } : {}
|
|
43185
|
+
};
|
|
43186
|
+
});
|
|
43187
|
+
}
|
|
43188
|
+
const changeSet = input.proposal.change_set;
|
|
43189
|
+
if (changeSet.schema_version === protocolVersions.changeSetV3 || changeSet.schema_version === protocolVersions.compensationChangeSet) {
|
|
43190
|
+
return [{ id: "target", kind: "target", status: "invalid", safe_code: "FRESHNESS_TARGET_AUTHORITY_INVALID" }];
|
|
43191
|
+
}
|
|
43192
|
+
const expected = changeSet.guards.expected_version;
|
|
43193
|
+
if (!expected || expected.column === "__row_hash" || changeSet.source.primary_key.value === void 0) {
|
|
43194
|
+
return [{ id: "target", kind: "target", status: "invalid", safe_code: "FRESHNESS_EXACT_TARGET_GUARD_REQUIRED" }];
|
|
43195
|
+
}
|
|
43196
|
+
const readCapability = {
|
|
43197
|
+
...input.capability,
|
|
43198
|
+
conflict_guard: { column: expected.column }
|
|
43199
|
+
};
|
|
43200
|
+
const current = await input.readRow({
|
|
43201
|
+
sourceName: input.capability.source,
|
|
43202
|
+
source: input.source,
|
|
43203
|
+
capability: readCapability,
|
|
43204
|
+
args: { [input.capability.lookup.id_from_arg]: changeSet.source.primary_key.value },
|
|
43205
|
+
context: input.context,
|
|
43206
|
+
env: input.env,
|
|
43207
|
+
transaction_mode: "read_only"
|
|
43208
|
+
});
|
|
43209
|
+
const observed = current.rowCount === 1 ? conflictGuardScalar(scalar3(current.row[expected.column])) : void 0;
|
|
43210
|
+
const fresh = current.rowCount === 1 && observed !== void 0 && versionsEqual(observed, expected.value);
|
|
43211
|
+
return [{
|
|
43212
|
+
id: "target",
|
|
43213
|
+
kind: "target",
|
|
43214
|
+
status: fresh ? "fresh" : "stale",
|
|
43215
|
+
safe_code: fresh ? "FRESHNESS_TARGET_FRESH" : "FRESHNESS_TARGET_STALE",
|
|
43216
|
+
expected_version_digest: versionMetadataDigest(expected.column, expected.value),
|
|
43217
|
+
...observed !== void 0 ? { observed_version_digest: versionMetadataDigest(expected.column, observed) } : {}
|
|
43218
|
+
}];
|
|
43219
|
+
}
|
|
43220
|
+
function validateResolvedFreshnessDependency(config, proposalCapability, dependency) {
|
|
43221
|
+
const policy = config.proposal_freshness?.[proposalCapability.name];
|
|
43222
|
+
const declared = policy?.dependencies?.find((item) => item.id === dependency.id);
|
|
43223
|
+
const capability = (config.capabilities ?? []).find((item) => item.name === dependency.capability);
|
|
43224
|
+
const source = config.sources?.[dependency.source_id];
|
|
43225
|
+
if (!declared || declared.capability !== dependency.capability || declared.version_column !== dependency.expected_version.column) return "FRESHNESS_DEPENDENCY_AUTHORITY_MISMATCH";
|
|
43226
|
+
if (!capability || capability.kind !== "read" || !source) return "FRESHNESS_DEPENDENCY_AUTHORITY_INVALID";
|
|
43227
|
+
if (capability.source !== proposalCapability.source || dependency.source_id !== capability.source || dependency.engine !== source.engine) return "FRESHNESS_CROSS_SOURCE_UNSUPPORTED";
|
|
43228
|
+
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";
|
|
43229
|
+
return void 0;
|
|
43230
|
+
}
|
|
43231
|
+
async function evaluateSupportingFreshness(input) {
|
|
43232
|
+
const readCapability = {
|
|
43233
|
+
...input.capability,
|
|
43234
|
+
conflict_guard: { column: input.dependency.expected_version.column }
|
|
43235
|
+
};
|
|
43236
|
+
try {
|
|
43237
|
+
const current = await input.readRow({
|
|
43238
|
+
sourceName: input.dependency.source_id,
|
|
43239
|
+
source: input.source,
|
|
43240
|
+
capability: readCapability,
|
|
43241
|
+
args: { [input.capability.lookup.id_from_arg]: input.dependency.target.primary_key.value },
|
|
43242
|
+
context: input.context,
|
|
43243
|
+
env: input.env,
|
|
43244
|
+
transaction_mode: "read_only"
|
|
43245
|
+
});
|
|
43246
|
+
const observed = current.rowCount === 1 ? conflictGuardScalar(scalar3(current.row[input.dependency.expected_version.column])) : void 0;
|
|
43247
|
+
const fresh = current.rowCount === 1 && observed !== void 0 && versionsEqual(observed, input.dependency.expected_version.value);
|
|
43248
|
+
return {
|
|
43249
|
+
id: input.dependency.id,
|
|
43250
|
+
kind: "supporting",
|
|
43251
|
+
status: fresh ? "fresh" : "stale",
|
|
43252
|
+
safe_code: fresh ? "FRESHNESS_DEPENDENCY_FRESH" : "FRESHNESS_DEPENDENCY_STALE",
|
|
43253
|
+
expected_version_digest: versionMetadataDigest(input.dependency.expected_version.column, input.dependency.expected_version.value),
|
|
43254
|
+
...observed !== void 0 ? { observed_version_digest: versionMetadataDigest(input.dependency.expected_version.column, observed) } : {}
|
|
43255
|
+
};
|
|
43256
|
+
} catch (error) {
|
|
43257
|
+
const classified = classifyFreshnessReadError(error);
|
|
43258
|
+
return {
|
|
43259
|
+
id: input.dependency.id,
|
|
43260
|
+
kind: "supporting",
|
|
43261
|
+
status: classified.result,
|
|
43262
|
+
safe_code: classified.safe_code
|
|
43263
|
+
};
|
|
43264
|
+
}
|
|
43265
|
+
}
|
|
43266
|
+
function freshnessResultForChecks(checks) {
|
|
43267
|
+
if (checks.some((check) => check.status === "invalid")) return "invalid";
|
|
43268
|
+
if (checks.some((check) => check.status === "unsupported")) return "unsupported";
|
|
43269
|
+
if (checks.some((check) => check.status === "unavailable")) return "unavailable";
|
|
43270
|
+
if (checks.some((check) => check.status === "stale")) return "stale";
|
|
43271
|
+
return "fresh";
|
|
43272
|
+
}
|
|
43273
|
+
function freshnessSafeCode(status, checks) {
|
|
43274
|
+
if (status === "fresh") return "FRESHNESS_FRESH";
|
|
43275
|
+
if (status === "stale" && checks.some((check) => check.kind === "target" && check.status === "stale")) return "FRESHNESS_TARGET_STALE";
|
|
43276
|
+
if (status === "stale") return "FRESHNESS_DEPENDENCY_STALE";
|
|
43277
|
+
if (status === "unavailable") return "FRESHNESS_TEMPORARILY_UNAVAILABLE";
|
|
43278
|
+
if (status === "unsupported") return "FRESHNESS_TOPOLOGY_UNSUPPORTED";
|
|
43279
|
+
return "FRESHNESS_AUTHORITY_INVALID";
|
|
43280
|
+
}
|
|
43281
|
+
function classifyFreshnessReadError(error) {
|
|
43282
|
+
const code = error instanceof McpRuntimeError ? error.code : errorStringProperty(error, "code") ?? "";
|
|
43283
|
+
if (/(TIMEOUT|POOL|CONNECTION|ECONN|ETIMEDOUT|TOO_MANY|UNAVAILABLE|SATURAT)/i.test(`${code} ${errorMessage(error)}`)) {
|
|
43284
|
+
return { result: "unavailable", safe_code: "FRESHNESS_TEMPORARILY_UNAVAILABLE" };
|
|
43285
|
+
}
|
|
43286
|
+
if (/(UNSUPPORTED|CROSS_SOURCE)/i.test(code)) {
|
|
43287
|
+
return { result: "unsupported", safe_code: "FRESHNESS_TOPOLOGY_UNSUPPORTED" };
|
|
43288
|
+
}
|
|
43289
|
+
return { result: "invalid", safe_code: "FRESHNESS_CHECK_FAILED" };
|
|
43290
|
+
}
|
|
43291
|
+
function freshnessSourceAdapters(proposal, authority) {
|
|
43292
|
+
const entries = /* @__PURE__ */ new Map();
|
|
43293
|
+
const targetEngine = proposal.source_kind === "external_mysql" ? "mysql" : "postgres";
|
|
43294
|
+
entries.set(`${proposal.source_id}:${targetEngine}`, { source_id: proposal.source_id, engine: targetEngine });
|
|
43295
|
+
for (const dependency of authority.dependencies) {
|
|
43296
|
+
entries.set(`${dependency.source_id}:${dependency.engine}`, { source_id: dependency.source_id, engine: dependency.engine });
|
|
43297
|
+
}
|
|
43298
|
+
return [...entries.values()].sort((left, right) => left.source_id.localeCompare(right.source_id) || left.engine.localeCompare(right.engine));
|
|
43299
|
+
}
|
|
43300
|
+
function versionsEqual(left, right) {
|
|
43301
|
+
return canonicalJsonDigest({ value: conflictGuardScalar(scalar3(left)) }) === canonicalJsonDigest({ value: conflictGuardScalar(scalar3(right)) });
|
|
43302
|
+
}
|
|
43303
|
+
function versionMetadataDigest(column, value) {
|
|
43304
|
+
return canonicalJsonDigest({ column, value: conflictGuardScalar(scalar3(value)) });
|
|
43305
|
+
}
|
|
42275
43306
|
function effectivePrincipalScope(config, capability, context) {
|
|
42276
43307
|
const column = capability.target.principal_scope_key;
|
|
42277
43308
|
if (!column) return void 0;
|
|
@@ -42350,6 +43381,7 @@ function buildChangeSet(input) {
|
|
|
42350
43381
|
...input.capability.operation.version_advance ? { version_advance: input.capability.operation.version_advance } : {},
|
|
42351
43382
|
...input.resolvedDeduplication ? { deduplication: input.resolvedDeduplication } : {}
|
|
42352
43383
|
},
|
|
43384
|
+
...input.freshness ? { freshness: input.freshness } : {},
|
|
42353
43385
|
...input.capability.reversibility ? {
|
|
42354
43386
|
reversibility: {
|
|
42355
43387
|
mode: "reviewed_inverse",
|
|
@@ -42415,6 +43447,7 @@ function buildChangeSet(input) {
|
|
|
42415
43447
|
allowed_columns: input.capability.allowed_columns ?? Object.keys(patch),
|
|
42416
43448
|
expected_version: guard
|
|
42417
43449
|
},
|
|
43450
|
+
...input.freshness ? { freshness: input.freshness } : {},
|
|
42418
43451
|
evidence: {
|
|
42419
43452
|
bundle_id: input.evidenceBundleId,
|
|
42420
43453
|
query_fingerprint: input.queryFingerprint,
|
|
@@ -42548,6 +43581,7 @@ function buildBoundedSetChangeSet(input) {
|
|
|
42548
43581
|
allowed_columns: kind === "set_delete" ? [] : input.capability.allowed_columns ?? Object.keys(input.patch),
|
|
42549
43582
|
...kind === "set_update" && operation.version_advance ? { version_advance: operation.version_advance } : {}
|
|
42550
43583
|
},
|
|
43584
|
+
...input.freshness ? { freshness: input.freshness } : {},
|
|
42551
43585
|
frozen_set: frozenSet,
|
|
42552
43586
|
...input.capability.reversibility ? {
|
|
42553
43587
|
reversibility: {
|
|
@@ -42747,7 +43781,7 @@ var RuntimeDatabasePools = class {
|
|
|
42747
43781
|
counter.active += 1;
|
|
42748
43782
|
try {
|
|
42749
43783
|
const query = runtimeReadQuery(input.capability, "$", input.args, input.context);
|
|
42750
|
-
await client.query(input.capability.protected_read ? "BEGIN READ ONLY" : "BEGIN");
|
|
43784
|
+
await client.query(input.capability.protected_read || input.transaction_mode === "read_only" ? "BEGIN READ ONLY" : "BEGIN");
|
|
42751
43785
|
const timeoutMs = protectedStatementTimeout(input.capability, input.source.statement_timeout_ms);
|
|
42752
43786
|
if (timeoutMs) await client.query(`SET LOCAL statement_timeout = ${timeoutMs}`);
|
|
42753
43787
|
if (input.source.database_scope?.mode === "postgres_rls") {
|
|
@@ -42814,14 +43848,15 @@ var RuntimeDatabasePools = class {
|
|
|
42814
43848
|
const timeoutMs = protectedStatementTimeout(input.capability, input.source.statement_timeout_ms);
|
|
42815
43849
|
if (timeoutMs) await connection.query("SET SESSION max_execution_time = ?", [timeoutMs]).catch(() => void 0);
|
|
42816
43850
|
const query = runtimeReadQuery(input.capability, "?", input.args, input.context);
|
|
42817
|
-
|
|
43851
|
+
const readOnlyTransaction = Boolean(input.capability.protected_read || input.transaction_mode === "read_only");
|
|
43852
|
+
if (readOnlyTransaction) await connection.query("START TRANSACTION READ ONLY");
|
|
42818
43853
|
try {
|
|
42819
43854
|
const [rows] = await connection.execute(query.sql, query.values.map(scalar3));
|
|
42820
43855
|
const list = Array.isArray(rows) ? rows : [];
|
|
42821
|
-
if (
|
|
43856
|
+
if (readOnlyTransaction) await connection.query("COMMIT");
|
|
42822
43857
|
return { row: list[0] ?? {}, rows: list, rowCount: list.length };
|
|
42823
43858
|
} catch (error) {
|
|
42824
|
-
if (
|
|
43859
|
+
if (readOnlyTransaction) await connection.query("ROLLBACK").catch(() => void 0);
|
|
42825
43860
|
throw error;
|
|
42826
43861
|
}
|
|
42827
43862
|
} finally {
|
|
@@ -43975,6 +45010,15 @@ function validateOperation2(job) {
|
|
|
43975
45010
|
}
|
|
43976
45011
|
async function mutateMysql(job, connection) {
|
|
43977
45012
|
if (job.protocol_version === "4.0") return await mutateMysqlCompensation(job, connection);
|
|
45013
|
+
const freshnessConflict = await lockMysqlFreshnessDependencies(job, connection);
|
|
45014
|
+
if (freshnessConflict) {
|
|
45015
|
+
return {
|
|
45016
|
+
status: "conflict",
|
|
45017
|
+
affectedRows: 0,
|
|
45018
|
+
code: freshnessConflict,
|
|
45019
|
+
targetIdentity: identityForJob2(job)
|
|
45020
|
+
};
|
|
45021
|
+
}
|
|
43978
45022
|
if (job.protocol_version === "3.0") return await mutateMysqlSet(job, connection);
|
|
43979
45023
|
const operation = operationOf2(job);
|
|
43980
45024
|
if (operation === "single_row_insert") return await insertMysql(job, connection);
|
|
@@ -44035,6 +45079,38 @@ EXISTS (SELECT 1 FROM information_schema.USER_PRIVILEGES WHERE REPLACE(GRANTEE,
|
|
|
44035
45079
|
}
|
|
44036
45080
|
return { status: "applied", affectedRows: 1, targetIdentity: identityForJob2(job), resultVersion, beforeDigest, afterDigest: digest2({ identity: identityForJob2(job), patch: job.patch, version: resultVersion }) };
|
|
44037
45081
|
}
|
|
45082
|
+
function freshnessDependencies2(job) {
|
|
45083
|
+
if (!("freshness" in job) || !job.freshness) return [];
|
|
45084
|
+
return job.freshness.dependencies;
|
|
45085
|
+
}
|
|
45086
|
+
async function lockMysqlFreshnessDependencies(job, connection) {
|
|
45087
|
+
for (const dependency of freshnessDependencies2(job)) {
|
|
45088
|
+
const where = [
|
|
45089
|
+
`${quoteMysqlIdentifier(dependency.target.primary_key.column)} = ?`,
|
|
45090
|
+
`${quoteMysqlIdentifier(dependency.target.tenant_column)} = ?`
|
|
45091
|
+
];
|
|
45092
|
+
const values = [
|
|
45093
|
+
dependency.target.primary_key.value,
|
|
45094
|
+
job.target.tenant_guard.value
|
|
45095
|
+
];
|
|
45096
|
+
if (dependency.target.principal_column) {
|
|
45097
|
+
const scope = principalScope2(job);
|
|
45098
|
+
if (!scope) return "FRESHNESS_DEPENDENCY_SCOPE_INVALID";
|
|
45099
|
+
where.push(`${quoteMysqlIdentifier(dependency.target.principal_column)} = ?`);
|
|
45100
|
+
values.push(scope.value);
|
|
45101
|
+
}
|
|
45102
|
+
const [rows] = await connection.query(
|
|
45103
|
+
`SELECT ${quoteMysqlIdentifier(dependency.expected_version.column)} AS __synapsor_freshness_version FROM ${quoteMysqlIdentifier(dependency.target.schema)}.${quoteMysqlIdentifier(dependency.target.table)} WHERE ${where.join(" AND ")} FOR UPDATE`,
|
|
45104
|
+
values
|
|
45105
|
+
);
|
|
45106
|
+
if (rows.length !== 1) return "FRESHNESS_DEPENDENCY_STALE";
|
|
45107
|
+
if (!versionValuesMatch2(
|
|
45108
|
+
rows[0]?.__synapsor_freshness_version,
|
|
45109
|
+
dependency.expected_version.value
|
|
45110
|
+
)) return "FRESHNESS_DEPENDENCY_STALE";
|
|
45111
|
+
}
|
|
45112
|
+
return void 0;
|
|
45113
|
+
}
|
|
44038
45114
|
function compensationProjection2(job) {
|
|
44039
45115
|
const columns = /* @__PURE__ */ new Set([job.target.primary_key.column, job.target.tenant_guard.column]);
|
|
44040
45116
|
const scope = principalScope2(job);
|
|
@@ -45826,7 +46902,7 @@ import path3 from "node:path";
|
|
|
45826
46902
|
// apps/runner/package.json
|
|
45827
46903
|
var package_default = {
|
|
45828
46904
|
name: "@synapsor/runner",
|
|
45829
|
-
version: "1.6.
|
|
46905
|
+
version: "1.6.1",
|
|
45830
46906
|
description: "Stop giving AI agents execute_sql; expose reviewed Postgres/MySQL MCP actions with proposals, approval, writeback, and replay.",
|
|
45831
46907
|
license: "Apache-2.0",
|
|
45832
46908
|
type: "module",
|
|
@@ -50859,10 +51935,11 @@ async function startLocalUiServer(options = {}) {
|
|
|
50859
51935
|
const projectRoot = path14.resolve(options.projectRoot ?? path14.dirname(configPath));
|
|
50860
51936
|
const boundaryRoot = options.boundaryRoot ? path14.resolve(options.boundaryRoot) : void 0;
|
|
50861
51937
|
const safeActionPreview = options.safeActionPreview ?? executeSafeActionPreview;
|
|
51938
|
+
const freshnessEvaluator = options.freshnessEvaluator ?? ((proposal) => evaluateWorkbenchFreshness(configPath, proposal));
|
|
50862
51939
|
const bootstrapState = { consumed: false };
|
|
50863
51940
|
const server = createServer2(async (request, response) => {
|
|
50864
51941
|
try {
|
|
50865
|
-
await handleRequest({ request, response, configPath, storePath, projectRoot, boundaryRoot, storeAccess, safeActionPreview, token, csrfToken, tour: options.tour === true, bootstrapState });
|
|
51942
|
+
await handleRequest({ request, response, configPath, storePath, projectRoot, boundaryRoot, storeAccess, safeActionPreview, freshnessEvaluator, token, csrfToken, tour: options.tour === true, bootstrapState });
|
|
50866
51943
|
} catch (error) {
|
|
50867
51944
|
sendJson(response, 500, {
|
|
50868
51945
|
ok: false,
|
|
@@ -50896,7 +51973,7 @@ async function startLocalUiServer(options = {}) {
|
|
|
50896
51973
|
};
|
|
50897
51974
|
}
|
|
50898
51975
|
async function handleRequest(input) {
|
|
50899
|
-
const { request, response, configPath, storePath, projectRoot, boundaryRoot, storeAccess, safeActionPreview, token, csrfToken, tour, bootstrapState } = input;
|
|
51976
|
+
const { request, response, configPath, storePath, projectRoot, boundaryRoot, storeAccess, safeActionPreview, freshnessEvaluator, token, csrfToken, tour, bootstrapState } = input;
|
|
50900
51977
|
const url = new URL(request.url ?? "/", `http://${request.headers.host ?? "127.0.0.1"}`);
|
|
50901
51978
|
if (request.method === "GET" && url.pathname === "/" && url.searchParams.has("token")) {
|
|
50902
51979
|
if (url.searchParams.get("token") !== token || bootstrapState.consumed) {
|
|
@@ -51207,11 +52284,32 @@ async function handleRequest(input) {
|
|
|
51207
52284
|
data_pr: buildDataPr(proposal, reviewView, receipts2.at(-1)),
|
|
51208
52285
|
events: store.events(proposalId),
|
|
51209
52286
|
receipts: receipts2,
|
|
51210
|
-
evidence: store.getEvidenceBundle(proposal.change_set.evidence.bundle_id)
|
|
52287
|
+
evidence: store.getEvidenceBundle(proposal.change_set.evidence.bundle_id),
|
|
52288
|
+
freshness: storedFreshnessSummary(proposal, store.latestFreshnessProof(proposalId))
|
|
51211
52289
|
});
|
|
51212
52290
|
});
|
|
51213
52291
|
return;
|
|
51214
52292
|
}
|
|
52293
|
+
const freshnessMatch = url.pathname.match(/^\/api\/proposals\/([^/]+)\/check-freshness$/);
|
|
52294
|
+
if (request.method === "POST" && freshnessMatch) {
|
|
52295
|
+
if (!hasValidCsrf(request, csrfToken)) {
|
|
52296
|
+
sendJson(response, 403, { ok: false, error: "CSRF token required for proposal freshness checks" });
|
|
52297
|
+
return;
|
|
52298
|
+
}
|
|
52299
|
+
const proposalId = decodeURIComponent(freshnessMatch[1] ?? "");
|
|
52300
|
+
const proposal = await storeAccess("read", "proposal-freshness-read", (store) => requireProposal(store, proposalId));
|
|
52301
|
+
const freshness = await freshnessEvaluator(proposal);
|
|
52302
|
+
if (freshness.required) {
|
|
52303
|
+
await storeAccess("write", "proposal-freshness-record", (store) => {
|
|
52304
|
+
store.recordFreshnessProof(freshness.proof);
|
|
52305
|
+
});
|
|
52306
|
+
}
|
|
52307
|
+
sendJson(response, freshnessHttpStatus(freshness), {
|
|
52308
|
+
ok: freshness.status === "fresh" || freshness.status === "not_required",
|
|
52309
|
+
freshness: workbenchFreshnessSummary(freshness)
|
|
52310
|
+
});
|
|
52311
|
+
return;
|
|
52312
|
+
}
|
|
51215
52313
|
const approveMatch = url.pathname.match(/^\/api\/proposals\/([^/]+)\/approve$/);
|
|
51216
52314
|
if (request.method === "POST" && approveMatch) {
|
|
51217
52315
|
if (!hasValidCsrf(request, csrfToken)) {
|
|
@@ -51229,15 +52327,45 @@ async function handleRequest(input) {
|
|
|
51229
52327
|
const proposalId = decodeURIComponent(approveMatch[1] ?? "");
|
|
51230
52328
|
const body = await readJsonBody(request);
|
|
51231
52329
|
if (body.confirm !== "approve") throw new Error("approval requires confirm=approve");
|
|
52330
|
+
const proposalForCheck = await storeAccess("read", "proposal-approve-freshness-read", (store) => requireProposal(store, proposalId));
|
|
52331
|
+
const freshness = await freshnessEvaluator(proposalForCheck);
|
|
52332
|
+
if (freshness.required) {
|
|
52333
|
+
await storeAccess("write", "proposal-approve-freshness-record", (store) => {
|
|
52334
|
+
store.recordFreshnessProof(freshness.proof);
|
|
52335
|
+
});
|
|
52336
|
+
}
|
|
52337
|
+
if (freshness.status !== "fresh" && freshness.status !== "not_required") {
|
|
52338
|
+
if (freshness.required) {
|
|
52339
|
+
await storeAccess("write", "proposal-approve-freshness-blocked", (store) => {
|
|
52340
|
+
store.recordFreshnessApprovalBlocked(proposalId, {
|
|
52341
|
+
proof_digest: freshness.proof.proof_digest,
|
|
52342
|
+
safe_code: freshness.safe_code,
|
|
52343
|
+
actor: stringOrDefault(body.actor, "local_reviewer")
|
|
52344
|
+
});
|
|
52345
|
+
});
|
|
52346
|
+
}
|
|
52347
|
+
sendJson(response, freshnessHttpStatus(freshness), {
|
|
52348
|
+
ok: false,
|
|
52349
|
+
error: freshness.status === "stale" ? "Proposal or supporting evidence is stale. Create a new source read and proposal." : "Freshness could not be verified. No approval was recorded.",
|
|
52350
|
+
freshness: workbenchFreshnessSummary(freshness)
|
|
52351
|
+
});
|
|
52352
|
+
return;
|
|
52353
|
+
}
|
|
51232
52354
|
await storeAccess("write", "proposal-approve", (store) => {
|
|
51233
52355
|
const proposal = requireProposal(store, proposalId);
|
|
51234
52356
|
const updated = store.approveProposal(proposalId, {
|
|
51235
52357
|
approver: stringOrDefault(body.actor, "local_reviewer"),
|
|
51236
52358
|
proposal_hash: proposal.proposal_hash,
|
|
51237
52359
|
proposal_version: proposal.proposal_version,
|
|
51238
|
-
reason: typeof body.reason === "string" && body.reason.trim() ? body.reason.trim() : void 0
|
|
52360
|
+
reason: typeof body.reason === "string" && body.reason.trim() ? body.reason.trim() : void 0,
|
|
52361
|
+
freshness_proof_digest: freshness.required ? freshness.proof.proof_digest : void 0
|
|
52362
|
+
});
|
|
52363
|
+
sendJson(response, 200, {
|
|
52364
|
+
ok: true,
|
|
52365
|
+
proposal: updated,
|
|
52366
|
+
approval_progress: store.approvalProgress(proposalId),
|
|
52367
|
+
freshness: workbenchFreshnessSummary(freshness)
|
|
51239
52368
|
});
|
|
51240
|
-
sendJson(response, 200, { ok: true, proposal: updated, approval_progress: store.approvalProgress(proposalId) });
|
|
51241
52369
|
});
|
|
51242
52370
|
return;
|
|
51243
52371
|
}
|
|
@@ -51282,6 +52410,65 @@ async function handleRequest(input) {
|
|
|
51282
52410
|
}
|
|
51283
52411
|
sendJson(response, 404, { ok: false, error: "not found" });
|
|
51284
52412
|
}
|
|
52413
|
+
async function evaluateWorkbenchFreshness(configPath, proposal) {
|
|
52414
|
+
const required = "freshness" in proposal.change_set && proposal.change_set.freshness !== void 0;
|
|
52415
|
+
if (!required) {
|
|
52416
|
+
return {
|
|
52417
|
+
required: false,
|
|
52418
|
+
status: "not_required",
|
|
52419
|
+
safe_code: "FRESHNESS_NOT_REQUIRED",
|
|
52420
|
+
target_count: 0,
|
|
52421
|
+
supporting_count: 0
|
|
52422
|
+
};
|
|
52423
|
+
}
|
|
52424
|
+
const config = await loadRuntimeConfigFromFile(configPath);
|
|
52425
|
+
return evaluateProposalFreshness({ config, proposal, env: process.env });
|
|
52426
|
+
}
|
|
52427
|
+
function workbenchFreshnessSummary(result) {
|
|
52428
|
+
if (!result.required) {
|
|
52429
|
+
return {
|
|
52430
|
+
required: false,
|
|
52431
|
+
status: "not_required",
|
|
52432
|
+
safe_code: result.safe_code,
|
|
52433
|
+
target_count: 0,
|
|
52434
|
+
supporting_count: 0
|
|
52435
|
+
};
|
|
52436
|
+
}
|
|
52437
|
+
return {
|
|
52438
|
+
required: true,
|
|
52439
|
+
status: result.status,
|
|
52440
|
+
safe_code: result.safe_code,
|
|
52441
|
+
checked_at: result.proof.checked_at,
|
|
52442
|
+
valid_until: result.proof.valid_until,
|
|
52443
|
+
proof_digest: result.proof.proof_digest,
|
|
52444
|
+
target_count: result.target_count,
|
|
52445
|
+
supporting_count: result.supporting_count,
|
|
52446
|
+
checks: result.proof.checks
|
|
52447
|
+
};
|
|
52448
|
+
}
|
|
52449
|
+
function storedFreshnessSummary(proposal, proof) {
|
|
52450
|
+
const required = "freshness" in proposal.change_set && proposal.change_set.freshness !== void 0;
|
|
52451
|
+
if (!required) return { required: false, status: "not_required", safe_code: "FRESHNESS_NOT_REQUIRED" };
|
|
52452
|
+
if (!proof) return { required: true, status: "not_checked", safe_code: "FRESHNESS_PROOF_MISSING" };
|
|
52453
|
+
const expired = proof.result === "fresh" && Date.parse(proof.valid_until) < Date.now();
|
|
52454
|
+
return {
|
|
52455
|
+
required: true,
|
|
52456
|
+
status: expired ? "unavailable" : proof.result,
|
|
52457
|
+
safe_code: expired ? "FRESHNESS_PROOF_EXPIRED" : proof.safe_code,
|
|
52458
|
+
checked_at: proof.checked_at,
|
|
52459
|
+
valid_until: proof.valid_until,
|
|
52460
|
+
proof_digest: proof.proof_digest,
|
|
52461
|
+
target_count: proof.target_count,
|
|
52462
|
+
supporting_count: proof.supporting_count,
|
|
52463
|
+
checks: proof.checks
|
|
52464
|
+
};
|
|
52465
|
+
}
|
|
52466
|
+
function freshnessHttpStatus(result) {
|
|
52467
|
+
if (result.status === "fresh" || result.status === "not_required") return 200;
|
|
52468
|
+
if (result.status === "stale") return 409;
|
|
52469
|
+
if (result.status === "unavailable") return 503;
|
|
52470
|
+
return 422;
|
|
52471
|
+
}
|
|
51285
52472
|
async function executeSafeActionPreview(input) {
|
|
51286
52473
|
const prepared = await prepareSafeActionPreview({ projectRoot: input.projectRoot, configPath: input.configPath });
|
|
51287
52474
|
const previewConfigPath = path14.resolve(input.projectRoot, prepared.config_path);
|
|
@@ -52301,6 +53488,7 @@ function buildStory(payload) {
|
|
|
52301
53488
|
const principalId = (cs.principal && cs.principal.id) || "the agent";
|
|
52302
53489
|
const requiredRole = (cs.approval && cs.approval.required_role) || "a reviewer";
|
|
52303
53490
|
const approvalProgress = payload.approval_progress || { approved: 0, required: 1, remaining: 1, complete: false };
|
|
53491
|
+
const freshness = payload.freshness || { required: false, status: "not_required" };
|
|
52304
53492
|
const story = el("div", { class: "story" });
|
|
52305
53493
|
|
|
52306
53494
|
// 1. Agent requested a change
|
|
@@ -52346,8 +53534,17 @@ function buildStory(payload) {
|
|
|
52346
53534
|
el("div", { class: "kv" }, [
|
|
52347
53535
|
el("dt", { text: "Approval progress" }), el("dd", { text: approvalProgress.approved + "/" + approvalProgress.required }),
|
|
52348
53536
|
el("dt", { text: "Policy result" }), el("dd", { text: (rv.policy_and_risk && rv.policy_and_risk.decision) || stateVal }),
|
|
53537
|
+
el("dt", { text: "Live freshness" }), el("dd", { text: String(freshness.status || "not checked").replaceAll("_", " ") }),
|
|
53538
|
+
el("dt", { text: "Freshness checks" }), el("dd", { text: String(freshness.target_count || 0) + " target / " + String(freshness.supporting_count || 0) + " supporting" }),
|
|
52349
53539
|
]),
|
|
52350
53540
|
];
|
|
53541
|
+
if (freshness.required) {
|
|
53542
|
+
approveBody.push(el("p", { text: freshness.status === "fresh"
|
|
53543
|
+
? "The live preflight passed. Approval still does not guarantee freshness through apply; the trusted apply path checks again."
|
|
53544
|
+
: freshness.status === "stale"
|
|
53545
|
+
? "The target or supporting evidence drifted. This proposal cannot be refreshed; create a new source read and proposal."
|
|
53546
|
+
: "A live source preflight is required before this proposal can be approved." }));
|
|
53547
|
+
}
|
|
52351
53548
|
const approvedEv = find("proposal_approved");
|
|
52352
53549
|
const rejectedEv = find("proposal_rejected");
|
|
52353
53550
|
if (stateVal === "pending_review") {
|
|
@@ -52466,10 +53663,42 @@ async function loadDetail(proposalId) {
|
|
|
52466
53663
|
actor.setAttribute("aria-label", "Reviewer identity");
|
|
52467
53664
|
reason.setAttribute("aria-label", "Reason for approval or rejection");
|
|
52468
53665
|
const actions = el("div", { class: "actions" });
|
|
53666
|
+
const freshness = payload.freshness || { required: false, status: "not_required" };
|
|
53667
|
+
const freshnessStatus = el("div", { class: "status-line", text: freshness.required
|
|
53668
|
+
? "Freshness: " + String(freshness.status || "not checked").replaceAll("_", " ") + "."
|
|
53669
|
+
: "Freshness: not required for this legacy proposal." });
|
|
53670
|
+
const check = freshness.required
|
|
53671
|
+
? el("button", { class: "secondary", text: "Check live freshness", onclick: async () => {
|
|
53672
|
+
check.disabled = true;
|
|
53673
|
+
try {
|
|
53674
|
+
await api("/api/proposals/" + encodeURIComponent(proposalId) + "/check-freshness", {
|
|
53675
|
+
method: "POST",
|
|
53676
|
+
headers: { "x-synapsor-csrf": csrfToken },
|
|
53677
|
+
body: JSON.stringify({}),
|
|
53678
|
+
});
|
|
53679
|
+
} catch (error) {
|
|
53680
|
+
freshnessStatus.textContent = error.message;
|
|
53681
|
+
} finally {
|
|
53682
|
+
await loadProposals();
|
|
53683
|
+
await loadDetail(proposalId);
|
|
53684
|
+
}
|
|
53685
|
+
} })
|
|
53686
|
+
: null;
|
|
52469
53687
|
const approve = el("button", { text: "Approve outside MCP", onclick: async () => { await api("/api/proposals/" + encodeURIComponent(proposalId) + "/approve", { method: "POST", headers: { "x-synapsor-csrf": csrfToken }, body: JSON.stringify({ actor: actor.value, reason: reason.value, confirm: "approve" }) }); await loadProposals(); await loadDetail(proposalId); } });
|
|
53688
|
+
approve.disabled = freshness.required && freshness.status !== "fresh";
|
|
53689
|
+
approve.title = freshness.required
|
|
53690
|
+
? "A fresh live check is required. Approval performs another check immediately before recording the decision."
|
|
53691
|
+
: "Record this human decision outside MCP.";
|
|
52470
53692
|
const reject = el("button", { class: "danger", text: "Reject", onclick: async () => { await api("/api/proposals/" + encodeURIComponent(proposalId) + "/reject", { method: "POST", headers: { "x-synapsor-csrf": csrfToken }, body: JSON.stringify({ actor: actor.value, reason: reason.value || "rejected from local UI", confirm: "reject" }) }); await loadProposals(); await loadDetail(proposalId); } });
|
|
53693
|
+
if (check) actions.append(check);
|
|
52471
53694
|
actions.append(approve, reject);
|
|
52472
|
-
reviewPane.append(
|
|
53695
|
+
reviewPane.append(
|
|
53696
|
+
el("div", { class: "callout", text: "You are the approval authority here \u2014 the model cannot reach these controls." }),
|
|
53697
|
+
freshnessStatus,
|
|
53698
|
+
actor,
|
|
53699
|
+
reason,
|
|
53700
|
+
actions,
|
|
53701
|
+
);
|
|
52473
53702
|
} else if (proposal.state === "approved" || proposal.state === "pending_worker") {
|
|
52474
53703
|
const command = trustedApplyCommand(proposalId);
|
|
52475
53704
|
const commandBox = el("div", { class: "mono", text: command, style: "display:block;margin-top:8px" });
|
|
@@ -52495,6 +53724,7 @@ async function loadDetail(proposalId) {
|
|
|
52495
53724
|
el("h3", { text: "events", style: "margin:6px 0 2px;font-size:13px;color:var(--muted)" }), pre(payload.events),
|
|
52496
53725
|
el("h3", { text: "receipts", style: "margin:6px 0 2px;font-size:13px;color:var(--muted)" }), pre(payload.receipts),
|
|
52497
53726
|
el("h3", { text: "evidence", style: "margin:6px 0 2px;font-size:13px;color:var(--muted)" }), pre(payload.evidence),
|
|
53727
|
+
el("h3", { text: "freshness", style: "margin:6px 0 2px;font-size:13px;color:var(--muted)" }), pre(payload.freshness),
|
|
52498
53728
|
);
|
|
52499
53729
|
root.append(reviewPane, jsonPane);
|
|
52500
53730
|
}
|
|
@@ -53159,7 +54389,16 @@ async function createComplianceReport(input) {
|
|
|
53159
54389
|
capability: proposal.capability ?? proposal.action,
|
|
53160
54390
|
principal: approval.approver,
|
|
53161
54391
|
object: `${proposal.business_object}:${proposal.object_id}`,
|
|
53162
|
-
details: sanitize({
|
|
54392
|
+
details: sanitize({
|
|
54393
|
+
status: approval.status,
|
|
54394
|
+
reason: approval.reason,
|
|
54395
|
+
identity_provider: approval.identity?.provider,
|
|
54396
|
+
verified: approval.identity?.verified,
|
|
54397
|
+
subject: approval.identity?.subject,
|
|
54398
|
+
decision_hash: approval.decision_hash,
|
|
54399
|
+
integrity_hash: approval.integrity_hash,
|
|
54400
|
+
freshness_proof_digest: approval.freshness_proof_digest
|
|
54401
|
+
})
|
|
53163
54402
|
});
|
|
53164
54403
|
for (const event of store.events(proposal.proposal_id)) entries.push({
|
|
53165
54404
|
timestamp: event.created_at,
|
|
@@ -53169,7 +54408,7 @@ async function createComplianceReport(input) {
|
|
|
53169
54408
|
capability: proposal.capability ?? proposal.action,
|
|
53170
54409
|
principal: event.actor,
|
|
53171
54410
|
object: `${proposal.business_object}:${proposal.object_id}`,
|
|
53172
|
-
details:
|
|
54411
|
+
details: complianceEventDetails(event)
|
|
53173
54412
|
});
|
|
53174
54413
|
for (const intent of store.listWritebackIntents({ proposal_id: proposal.proposal_id, limit: 1e6 })) entries.push({
|
|
53175
54414
|
timestamp: intent.created_at,
|
|
@@ -53346,6 +54585,7 @@ async function verifyComplianceReport(report, publicKeyPath) {
|
|
|
53346
54585
|
}
|
|
53347
54586
|
function proposalEntry(proposal) {
|
|
53348
54587
|
const change = proposal.change_set;
|
|
54588
|
+
const freshness = "freshness" in change && change.freshness ? parseFreshnessAuthority(change.freshness) : void 0;
|
|
53349
54589
|
return {
|
|
53350
54590
|
timestamp: proposal.created_at,
|
|
53351
54591
|
category: "proposal",
|
|
@@ -53365,10 +54605,64 @@ function proposalEntry(proposal) {
|
|
|
53365
54605
|
after: change.after,
|
|
53366
54606
|
allowed_columns: change.guards.allowed_columns,
|
|
53367
54607
|
source_database_mutated: proposal.source_database_mutated,
|
|
53368
|
-
proposal_hash: proposal.proposal_hash
|
|
54608
|
+
proposal_hash: proposal.proposal_hash,
|
|
54609
|
+
freshness_required: Boolean(freshness),
|
|
54610
|
+
freshness_authority: freshness ? {
|
|
54611
|
+
schema_version: freshness.schema_version,
|
|
54612
|
+
target_mode: freshness.target.mode,
|
|
54613
|
+
target_member_count: freshness.target.member_count,
|
|
54614
|
+
supporting_dependency_count: freshness.dependencies.length,
|
|
54615
|
+
dependency_ids: freshness.dependencies.map((dependency) => dependency.id),
|
|
54616
|
+
dependency_set_digest: freshness.dependency_set_digest
|
|
54617
|
+
} : void 0
|
|
53369
54618
|
})
|
|
53370
54619
|
};
|
|
53371
54620
|
}
|
|
54621
|
+
function complianceEventDetails(event) {
|
|
54622
|
+
const base = { kind: event.kind, actor: event.actor, payload_included: false };
|
|
54623
|
+
if (event.kind === "proposal_freshness_checked") {
|
|
54624
|
+
try {
|
|
54625
|
+
const proof = parseFreshnessProof(event.payload.proof);
|
|
54626
|
+
return sanitize({
|
|
54627
|
+
...base,
|
|
54628
|
+
freshness: {
|
|
54629
|
+
schema_version: proof.schema_version,
|
|
54630
|
+
result: proof.result,
|
|
54631
|
+
safe_code: proof.safe_code,
|
|
54632
|
+
checked_at: proof.checked_at,
|
|
54633
|
+
valid_until: proof.valid_until,
|
|
54634
|
+
proof_digest: proof.proof_digest,
|
|
54635
|
+
dependency_set_digest: proof.dependency_set_digest,
|
|
54636
|
+
target_count: proof.target_count,
|
|
54637
|
+
supporting_count: proof.supporting_count,
|
|
54638
|
+
checks: proof.checks
|
|
54639
|
+
}
|
|
54640
|
+
});
|
|
54641
|
+
} catch {
|
|
54642
|
+
return sanitize({ ...base, freshness: { status: "invalid_stored_proof" } });
|
|
54643
|
+
}
|
|
54644
|
+
}
|
|
54645
|
+
if (event.kind === "proposal_approval_blocked_freshness") {
|
|
54646
|
+
return sanitize({
|
|
54647
|
+
...base,
|
|
54648
|
+
freshness: {
|
|
54649
|
+
result: event.payload.result,
|
|
54650
|
+
safe_code: event.payload.safe_code,
|
|
54651
|
+
proof_digest: event.payload.proof_digest
|
|
54652
|
+
}
|
|
54653
|
+
});
|
|
54654
|
+
}
|
|
54655
|
+
if (event.kind === "writeback_conflict" && /^FRESHNESS_/.test(String(event.payload.safe_error_code ?? ""))) {
|
|
54656
|
+
return sanitize({
|
|
54657
|
+
...base,
|
|
54658
|
+
freshness: {
|
|
54659
|
+
safe_code: event.payload.safe_error_code,
|
|
54660
|
+
apply_revalidation_blocked: true
|
|
54661
|
+
}
|
|
54662
|
+
});
|
|
54663
|
+
}
|
|
54664
|
+
return sanitize(base);
|
|
54665
|
+
}
|
|
53372
54666
|
function queryAuditEntry(audit2) {
|
|
53373
54667
|
const proposalId = string(audit2.proposal_id);
|
|
53374
54668
|
const objectType = string(audit2.business_object) ?? string(audit2.table_name);
|
|
@@ -56571,6 +57865,7 @@ function buildLifecycleViewUnchecked(store, proposal, selection, commandName) {
|
|
|
56571
57865
|
const outbox = stableBy(store.listCloudOutbox({ proposal_id: proposalId, limit: 1e3 }), (item) => `${item.created_at}:${item.event_id}`);
|
|
56572
57866
|
const governance = stableBy(store.listCloudGovernanceEvents(proposalId), (item) => `${item.created_at}:${item.event_id}`);
|
|
56573
57867
|
const replay2 = store.getStoredReplayForProposal(proposalId);
|
|
57868
|
+
const latestFreshness = store.latestFreshnessProof(proposalId);
|
|
56574
57869
|
assertLifecycleLinks(proposal, evidence2, audit2, jobs, intents, receipts2, worker, governance, replay2);
|
|
56575
57870
|
const changeSet = asRecord4(proposal.change_set);
|
|
56576
57871
|
if (!changeSet) {
|
|
@@ -56589,6 +57884,9 @@ function buildLifecycleViewUnchecked(store, proposal, selection, commandName) {
|
|
|
56589
57884
|
const latestReceipt = receipts2.at(-1);
|
|
56590
57885
|
const lineage = compensationLineage(changeSet);
|
|
56591
57886
|
const inverseReceipts = receipts2.map((item) => asRecord4(item.receipt)?.inverse).filter((item) => item !== void 0).map((item) => safeDomainValue(item));
|
|
57887
|
+
const freshnessAuthority = asRecord4(changeSet.freshness);
|
|
57888
|
+
const freshnessTarget = asRecord4(freshnessAuthority?.target);
|
|
57889
|
+
const freshnessDependencies3 = Array.isArray(freshnessAuthority?.dependencies) ? freshnessAuthority.dependencies : [];
|
|
56592
57890
|
return {
|
|
56593
57891
|
schema_version: lifecycleViewSchemaVersion,
|
|
56594
57892
|
selection,
|
|
@@ -56635,6 +57933,21 @@ function buildLifecycleViewUnchecked(store, proposal, selection, commandName) {
|
|
|
56635
57933
|
decisions: approvals.map(publicApproval),
|
|
56636
57934
|
tripped_policy_limits: policyLimitTrips(events2)
|
|
56637
57935
|
},
|
|
57936
|
+
freshness: {
|
|
57937
|
+
required: freshnessAuthority !== void 0,
|
|
57938
|
+
target_count: latestFreshness?.target_count ?? numberValue(freshnessTarget?.member_count) ?? 0,
|
|
57939
|
+
supporting_count: latestFreshness?.supporting_count ?? freshnessDependencies3.length,
|
|
57940
|
+
latest_status: latestFreshness?.result ?? (freshnessAuthority ? "not_checked" : "not_required"),
|
|
57941
|
+
latest_checked_at: latestFreshness?.checked_at ?? null,
|
|
57942
|
+
latest_proof_digest: latestFreshness?.proof_digest ?? null,
|
|
57943
|
+
latest_safe_code: latestFreshness?.safe_code ?? null,
|
|
57944
|
+
approval_proofs: approvals.map((item) => ({
|
|
57945
|
+
approval_id: item.approval_id,
|
|
57946
|
+
approver: item.approver,
|
|
57947
|
+
proof_digest: item.freshness_proof_digest ?? null
|
|
57948
|
+
})),
|
|
57949
|
+
next_action: latestFreshness?.result === "stale" ? "Create a new source read and proposal." : latestFreshness?.result === "unavailable" ? "Retry the live freshness check when the source is available." : freshnessAuthority && !latestFreshness ? "Run proposals check-freshness before approval." : "Apply still revalidates target and supporting dependencies."
|
|
57950
|
+
},
|
|
56638
57951
|
evidence: {
|
|
56639
57952
|
bundles: evidence2.map(publicEvidence),
|
|
56640
57953
|
count: evidence2.length
|
|
@@ -56690,6 +58003,7 @@ function formatLifecycleFirstLook(view) {
|
|
|
56690
58003
|
`Object: ${view.proposal.scope.business_object}:${view.proposal.scope.object_id}`,
|
|
56691
58004
|
`Trusted scope: tenant=${view.proposal.scope.tenant_id} principal=${view.proposal.scope.principal}`,
|
|
56692
58005
|
`Approval: ${view.approval.status} via ${view.approval.source} (${view.approval.progress.approved}/${view.approval.progress.required})`,
|
|
58006
|
+
`Freshness: ${view.freshness.latest_status}; target=${view.freshness.target_count} supporting=${view.freshness.supporting_count}; proof=${view.freshness.latest_proof_digest ?? "none"}`,
|
|
56693
58007
|
`Evidence: ${view.evidence.count} bundle${view.evidence.count === 1 ? "" : "s"}; query audit: ${view.query_audit.count}`,
|
|
56694
58008
|
`Writeback: ${view.writeback.jobs.length ? `${view.writeback.jobs.length} job(s)` : "not created"}; ${view.writeback.intents.length ? `${view.writeback.intents.length} intent(s)` : "no intent"}`,
|
|
56695
58009
|
`Latest outcome: ${latest ? `${String(latest.status)} rows=${String(latest.rows_affected)} source_changed=${String(latest.source_database_mutated)}` : "not applied"}`,
|
|
@@ -56709,6 +58023,7 @@ function formatLifecycleDetails(view) {
|
|
|
56709
58023
|
const sections = [
|
|
56710
58024
|
["Proposal", view.proposal],
|
|
56711
58025
|
["Approval", view.approval],
|
|
58026
|
+
["Freshness", view.freshness],
|
|
56712
58027
|
["Evidence", view.evidence],
|
|
56713
58028
|
["Query audit", view.query_audit],
|
|
56714
58029
|
["Writeback", view.writeback],
|
|
@@ -56827,6 +58142,7 @@ function publicApproval(approval) {
|
|
|
56827
58142
|
decision_hash: identity.decision_hash,
|
|
56828
58143
|
integrity_hash: identity.integrity_hash
|
|
56829
58144
|
} : null,
|
|
58145
|
+
freshness_proof_digest: approval.freshness_proof_digest ?? null,
|
|
56830
58146
|
created_at: approval.created_at
|
|
56831
58147
|
};
|
|
56832
58148
|
}
|
|
@@ -57208,7 +58524,7 @@ function createScopedExploreMcpServer(runtime) {
|
|
|
57208
58524
|
}).strict().optional()
|
|
57209
58525
|
}).strict();
|
|
57210
58526
|
const server = new McpServer2(
|
|
57211
|
-
{ name: "synapsor-runner-authoring", version: "1.6.
|
|
58527
|
+
{ name: "synapsor-runner-authoring", version: "1.6.1" },
|
|
57212
58528
|
{ capabilities: { tools: {} } }
|
|
57213
58529
|
);
|
|
57214
58530
|
server.registerTool(SCOPED_EXPLORE_DESCRIBE_TOOL, {
|
|
@@ -61926,6 +63242,29 @@ async function directSqlWritebackDoctorChecks(config, sourceName, source, writeU
|
|
|
61926
63242
|
message: `Rollback-only writer probe failed for configured target ${capability.target.schema}.${capability.target.table} (${safeDatabaseProbeError(error)}). Verify writer SELECT/${capabilityOperation(capability).toUpperCase()} on the target table and configured columns.`
|
|
61927
63243
|
});
|
|
61928
63244
|
}
|
|
63245
|
+
for (const dependency of proposalFreshnessDependencyCapabilities(config, capability)) {
|
|
63246
|
+
try {
|
|
63247
|
+
await rollbackOnlyFreshnessDependencyProbe(
|
|
63248
|
+
source.engine,
|
|
63249
|
+
writeUrl,
|
|
63250
|
+
dependency.capability,
|
|
63251
|
+
dependency.versionColumn
|
|
63252
|
+
);
|
|
63253
|
+
checks.push({
|
|
63254
|
+
name: `capability:${capability.name}:freshness-dependency:${dependency.id}:lock-probe`,
|
|
63255
|
+
ok: true,
|
|
63256
|
+
level: "pass",
|
|
63257
|
+
message: `Rollback-only writer probe verified locking-read authority for freshness dependency ${dependency.capability.target.schema}.${dependency.capability.target.table} without reading or mutating business rows.`
|
|
63258
|
+
});
|
|
63259
|
+
} catch (error) {
|
|
63260
|
+
checks.push({
|
|
63261
|
+
name: `capability:${capability.name}:freshness-dependency:${dependency.id}:lock-probe`,
|
|
63262
|
+
ok: false,
|
|
63263
|
+
level: "fail",
|
|
63264
|
+
message: `Rollback-only locking-read probe failed for freshness dependency ${dependency.capability.target.schema}.${dependency.capability.target.table} (${safeDatabaseProbeError(error)}). Verify writer SELECT plus row-lock authority on the dependency relation; a narrow UPDATE grant on its version column is sufficient for the supported PostgreSQL/MySQL fixtures.`
|
|
63265
|
+
});
|
|
63266
|
+
}
|
|
63267
|
+
}
|
|
61929
63268
|
}
|
|
61930
63269
|
return checks;
|
|
61931
63270
|
}
|
|
@@ -61935,6 +63274,22 @@ function directSqlProposalCapabilities(config, sourceName) {
|
|
|
61935
63274
|
return capabilityWritebackMode2(capability) === "direct_sql";
|
|
61936
63275
|
});
|
|
61937
63276
|
}
|
|
63277
|
+
function proposalFreshnessDependencyCapabilities(config, proposal) {
|
|
63278
|
+
const policy = config.proposal_freshness?.[proposal.name];
|
|
63279
|
+
if (!policy) return [];
|
|
63280
|
+
const capabilities = new Map((config.capabilities ?? []).map((capability) => [capability.name, capability]));
|
|
63281
|
+
return (policy.dependencies ?? []).map((dependency) => {
|
|
63282
|
+
const capability = capabilities.get(dependency.capability);
|
|
63283
|
+
if (!capability) {
|
|
63284
|
+
throw new Error(`reviewed freshness dependency capability is missing: ${dependency.capability}`);
|
|
63285
|
+
}
|
|
63286
|
+
return {
|
|
63287
|
+
id: dependency.id,
|
|
63288
|
+
capability,
|
|
63289
|
+
versionColumn: dependency.version_column
|
|
63290
|
+
};
|
|
63291
|
+
});
|
|
63292
|
+
}
|
|
61938
63293
|
async function rollbackOnlyTargetProbe(engine, databaseUrl, capability) {
|
|
61939
63294
|
if (engine === "postgres") {
|
|
61940
63295
|
await rollbackOnlyPostgresTargetProbe(databaseUrl, capability);
|
|
@@ -61942,6 +63297,45 @@ async function rollbackOnlyTargetProbe(engine, databaseUrl, capability) {
|
|
|
61942
63297
|
}
|
|
61943
63298
|
await rollbackOnlyMysqlTargetProbe(databaseUrl, capability);
|
|
61944
63299
|
}
|
|
63300
|
+
async function rollbackOnlyFreshnessDependencyProbe(engine, databaseUrl, capability, versionColumn) {
|
|
63301
|
+
if (engine === "postgres") {
|
|
63302
|
+
const pg = await dynamicImportModule("pg");
|
|
63303
|
+
const pool = new pg.Pool({ connectionString: databaseUrl });
|
|
63304
|
+
const client = await pool.connect();
|
|
63305
|
+
try {
|
|
63306
|
+
await client.query("BEGIN");
|
|
63307
|
+
try {
|
|
63308
|
+
const table = `${quotePostgresIdentifier3(capability.target.schema)}.${quotePostgresIdentifier3(capability.target.table)}`;
|
|
63309
|
+
const columns = freshnessDependencyProbeColumns(capability, versionColumn).map(quotePostgresIdentifier3).join(", ");
|
|
63310
|
+
await client.query(`SELECT ${columns} FROM ${table} WHERE false FOR UPDATE`);
|
|
63311
|
+
await client.query("ROLLBACK");
|
|
63312
|
+
} catch (error) {
|
|
63313
|
+
await client.query("ROLLBACK").catch(() => void 0);
|
|
63314
|
+
throw error;
|
|
63315
|
+
}
|
|
63316
|
+
} finally {
|
|
63317
|
+
client.release();
|
|
63318
|
+
await pool.end();
|
|
63319
|
+
}
|
|
63320
|
+
return;
|
|
63321
|
+
}
|
|
63322
|
+
const mysql5 = await dynamicImportModule("mysql2/promise");
|
|
63323
|
+
const connection = await mysql5.createConnection({ uri: databaseUrl, dateStrings: true });
|
|
63324
|
+
try {
|
|
63325
|
+
await connection.beginTransaction();
|
|
63326
|
+
try {
|
|
63327
|
+
const table = `${quoteMysqlIdentifier2(capability.target.schema)}.${quoteMysqlIdentifier2(capability.target.table)}`;
|
|
63328
|
+
const columns = freshnessDependencyProbeColumns(capability, versionColumn).map(quoteMysqlIdentifier2).join(", ");
|
|
63329
|
+
await connection.query(`SELECT ${columns} FROM ${table} WHERE 1 = 0 FOR UPDATE`);
|
|
63330
|
+
await connection.rollback();
|
|
63331
|
+
} catch (error) {
|
|
63332
|
+
await connection.rollback().catch(() => void 0);
|
|
63333
|
+
throw error;
|
|
63334
|
+
}
|
|
63335
|
+
} finally {
|
|
63336
|
+
await connection.end();
|
|
63337
|
+
}
|
|
63338
|
+
}
|
|
61945
63339
|
async function rollbackOnlyPostgresTargetProbe(databaseUrl, capability) {
|
|
61946
63340
|
const pg = await dynamicImportModule("pg");
|
|
61947
63341
|
const pool = new pg.Pool({ connectionString: databaseUrl });
|
|
@@ -62024,6 +63418,14 @@ function proposalReadProbeColumns(capability) {
|
|
|
62024
63418
|
}
|
|
62025
63419
|
return [...columns];
|
|
62026
63420
|
}
|
|
63421
|
+
function freshnessDependencyProbeColumns(capability, versionColumn) {
|
|
63422
|
+
return [...new Set([
|
|
63423
|
+
capability.target.primary_key,
|
|
63424
|
+
capability.target.tenant_key,
|
|
63425
|
+
capability.target.principal_scope_key,
|
|
63426
|
+
versionColumn
|
|
63427
|
+
].filter((column) => Boolean(column)))];
|
|
63428
|
+
}
|
|
62027
63429
|
function proposalWriteProbeColumns(capability) {
|
|
62028
63430
|
const columns = /* @__PURE__ */ new Set();
|
|
62029
63431
|
for (const column of capability.allowed_columns ?? []) columns.add(column);
|
|
@@ -63226,6 +64628,11 @@ async function verifyLocalWritebackAuthority(job, configPath, storePath, options
|
|
|
63226
64628
|
if (proposal.proposal_hash !== job.approval_id) {
|
|
63227
64629
|
throw new Error("writeback approval/proposal digest does not match local proposal");
|
|
63228
64630
|
}
|
|
64631
|
+
const proposalFreshness = "freshness" in proposal.change_set && proposal.change_set.freshness ? parseFreshnessAuthority(proposal.change_set.freshness) : void 0;
|
|
64632
|
+
const jobFreshness = "freshness" in job && job.freshness ? parseFreshnessAuthority(job.freshness) : void 0;
|
|
64633
|
+
if (Boolean(proposalFreshness) !== Boolean(jobFreshness) || proposalFreshness && jobFreshness && proposalFreshness.dependency_set_digest !== jobFreshness.dependency_set_digest) {
|
|
64634
|
+
throw new Error("writeback freshness authority does not match the immutable local proposal");
|
|
64635
|
+
}
|
|
63229
64636
|
const proposalContract = proposal.change_set.contract;
|
|
63230
64637
|
const reviewedContract = matching.contract_provenance;
|
|
63231
64638
|
if (reviewedContract) {
|
|
@@ -63259,12 +64666,42 @@ async function verifyLocalWritebackAuthority(job, configPath, storePath, options
|
|
|
63259
64666
|
} else if (proposalPrincipalScope) {
|
|
63260
64667
|
throw new Error("local proposal carries principal scope outside the reviewed capability");
|
|
63261
64668
|
}
|
|
63262
|
-
if (!options.cloudApproved)
|
|
64669
|
+
if (!options.cloudApproved) {
|
|
64670
|
+
verifyStoredFreshnessApprovalAuthority(store, proposal);
|
|
64671
|
+
await verifyStoredApprovalAuthority(config, configPath, store, proposal, matching);
|
|
64672
|
+
}
|
|
63263
64673
|
} finally {
|
|
63264
64674
|
store.close();
|
|
63265
64675
|
}
|
|
63266
64676
|
}
|
|
63267
64677
|
}
|
|
64678
|
+
function verifyStoredFreshnessApprovalAuthority(store, proposal) {
|
|
64679
|
+
if (!("freshness" in proposal.change_set) || !proposal.change_set.freshness) return;
|
|
64680
|
+
const authority = parseFreshnessAuthority(proposal.change_set.freshness);
|
|
64681
|
+
const approvals = store.approvals(proposal.proposal_id).filter((approval) => approval.status === "approved" && approval.proposal_hash === proposal.proposal_hash && approval.proposal_version === proposal.proposal_version);
|
|
64682
|
+
const required = proposal.change_set.approval.required_approvals ?? 1;
|
|
64683
|
+
if (new Set(approvals.map((approval) => approval.approver)).size < required) {
|
|
64684
|
+
throw new Error(`proposal ${proposal.proposal_id} does not have its required freshness-bound approvals`);
|
|
64685
|
+
}
|
|
64686
|
+
const proofEvents = store.events(proposal.proposal_id).filter((event) => event.kind === "proposal_freshness_checked");
|
|
64687
|
+
for (const approval of approvals) {
|
|
64688
|
+
if (!approval.freshness_proof_digest) {
|
|
64689
|
+
throw new Error(`proposal ${proposal.proposal_id} has an approval without a freshness proof`);
|
|
64690
|
+
}
|
|
64691
|
+
const event = proofEvents.find((candidate) => {
|
|
64692
|
+
try {
|
|
64693
|
+
return parseFreshnessProof(candidate.payload.proof).proof_digest === approval.freshness_proof_digest;
|
|
64694
|
+
} catch {
|
|
64695
|
+
return false;
|
|
64696
|
+
}
|
|
64697
|
+
});
|
|
64698
|
+
if (!event) throw new Error(`proposal ${proposal.proposal_id} freshness proof is missing or failed integrity validation`);
|
|
64699
|
+
const proof = parseFreshnessProof(event.payload.proof);
|
|
64700
|
+
if (proof.result !== "fresh" || proof.proposal_hash !== proposal.proposal_hash || proof.proposal_version !== proposal.proposal_version || proof.dependency_set_digest !== authority.dependency_set_digest) {
|
|
64701
|
+
throw new Error(`proposal ${proposal.proposal_id} approval freshness proof does not match immutable authority`);
|
|
64702
|
+
}
|
|
64703
|
+
}
|
|
64704
|
+
}
|
|
63268
64705
|
async function verifyStoredApprovalAuthority(config, configPath, store, proposal, capability) {
|
|
63269
64706
|
if (!config.operator_identity || config.operator_identity.provider === "dev_env") return;
|
|
63270
64707
|
const approval = [...store.approvals(proposal.proposal_id)].reverse().find((item) => item.status === "approved" && item.proposal_hash === proposal.proposal_hash && item.proposal_version === proposal.proposal_version);
|
|
@@ -67082,6 +68519,7 @@ async function proposals(args) {
|
|
|
67082
68519
|
const [subcommand, ...rest] = args;
|
|
67083
68520
|
if (subcommand === "list") return proposalsList(rest);
|
|
67084
68521
|
if (subcommand === "show") return proposalsShow(rest);
|
|
68522
|
+
if (subcommand === "check-freshness") return proposalsCheckFreshness(rest);
|
|
67085
68523
|
if (subcommand === "approve") return proposalsApprove(rest);
|
|
67086
68524
|
if (subcommand === "reject") return proposalsReject(rest);
|
|
67087
68525
|
if (subcommand === "writeback-job") return proposalsWritebackJob(rest);
|
|
@@ -68043,13 +69481,21 @@ async function proposalsShow(args) {
|
|
|
68043
69481
|
if (!proposal) throw new Error(`proposal not found: ${resolvedProposalId}`);
|
|
68044
69482
|
const evidence2 = store.getEvidenceBundle(proposal.change_set.evidence.bundle_id);
|
|
68045
69483
|
const approvalProgress = store.approvalProgress(resolvedProposalId);
|
|
68046
|
-
const
|
|
69484
|
+
const latestFreshness = store.latestFreshnessProof(resolvedProposalId);
|
|
69485
|
+
const freshness = {
|
|
69486
|
+
required: "freshness" in proposal.change_set && proposal.change_set.freshness !== void 0,
|
|
69487
|
+
status: latestFreshness?.result ?? ("freshness" in proposal.change_set && proposal.change_set.freshness !== void 0 ? "not_checked" : "not_required"),
|
|
69488
|
+
proof: latestFreshness ?? null
|
|
69489
|
+
};
|
|
69490
|
+
const payload = { proposal, approval_progress: approvalProgress, freshness, events: store.events(resolvedProposalId), receipts: store.receipts(resolvedProposalId), evidence: evidence2 };
|
|
68047
69491
|
if (args.includes("--json")) {
|
|
68048
69492
|
process2.stdout.write(`${JSON.stringify(payload, null, 2)}
|
|
68049
69493
|
`);
|
|
68050
69494
|
} else if (showDetails(args)) {
|
|
68051
69495
|
process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
|
|
68052
69496
|
process2.stdout.write(`Approval progress: ${approvalProgress.approved}/${approvalProgress.required}${approvalProgress.rejected ? " (rejected)" : ""}
|
|
69497
|
+
`);
|
|
69498
|
+
process2.stdout.write(`Freshness: ${freshness.status}${latestFreshness ? ` checked=${latestFreshness.checked_at} proof=${latestFreshness.proof_digest}` : ""}
|
|
68053
69499
|
`);
|
|
68054
69500
|
process2.stdout.write(formatProposalEventDetail(payload.events));
|
|
68055
69501
|
if (args.includes("--debug")) process2.stdout.write(formatProposalDebug(proposal, optionalArg(args, "--store")));
|
|
@@ -68061,6 +69507,119 @@ async function proposalsShow(args) {
|
|
|
68061
69507
|
store.close();
|
|
68062
69508
|
}
|
|
68063
69509
|
}
|
|
69510
|
+
var freshnessExitCodes = {
|
|
69511
|
+
fresh: 0,
|
|
69512
|
+
not_required: 0,
|
|
69513
|
+
stale: 3,
|
|
69514
|
+
unavailable: 4,
|
|
69515
|
+
invalid: 5,
|
|
69516
|
+
unsupported: 6
|
|
69517
|
+
};
|
|
69518
|
+
async function proposalsCheckFreshness(args) {
|
|
69519
|
+
const proposalId = positional(args, 0);
|
|
69520
|
+
if (!proposalId) throw new Error("proposals check-freshness requires <proposal_id|latest>");
|
|
69521
|
+
const storePath = localStorePath(args);
|
|
69522
|
+
const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
|
|
69523
|
+
const config = await optionalRuntimeConfig(configPath);
|
|
69524
|
+
if (config && runtimeStoreBridgeRequired(args, config)) {
|
|
69525
|
+
return withSharedPostgresRuntimeStoreBridge(
|
|
69526
|
+
args,
|
|
69527
|
+
config,
|
|
69528
|
+
`proposals check-freshness ${proposalId}`,
|
|
69529
|
+
(bridgeStorePath) => proposalsCheckFreshness(argsWithRuntimeStoreBridge(args, bridgeStorePath))
|
|
69530
|
+
);
|
|
69531
|
+
}
|
|
69532
|
+
assertNoRuntimeStoreForLocalMutation(config, "proposals check-freshness", args);
|
|
69533
|
+
if (sharedPostgresLedgerMirrorRequested(args, config)) {
|
|
69534
|
+
return withSharedPostgresLedgerMirror(
|
|
69535
|
+
args,
|
|
69536
|
+
storePath,
|
|
69537
|
+
`proposals check-freshness ${proposalId}`,
|
|
69538
|
+
() => proposalsCheckFreshness(withoutSharedPostgresLedgerMirror(args)),
|
|
69539
|
+
config
|
|
69540
|
+
);
|
|
69541
|
+
}
|
|
69542
|
+
const store = await openLocalStore(args);
|
|
69543
|
+
try {
|
|
69544
|
+
const resolvedProposalId = resolveProposalIdFromStore(proposalId, store);
|
|
69545
|
+
const proposal = requireLocalProposal(store, resolvedProposalId);
|
|
69546
|
+
const freshness = await evaluateAndRecordProposalFreshness({ proposal, config, configPath, store });
|
|
69547
|
+
operationalLog(freshness.status === "fresh" || freshness.status === "not_required" ? "info" : "warn", "proposal_freshness_check", {
|
|
69548
|
+
proposal_id: proposal.proposal_id,
|
|
69549
|
+
capability: proposal.action,
|
|
69550
|
+
tenant: proposal.tenant_id,
|
|
69551
|
+
status: freshness.status,
|
|
69552
|
+
error_code: freshness.safe_code,
|
|
69553
|
+
target_count: freshness.target_count,
|
|
69554
|
+
supporting_count: freshness.supporting_count,
|
|
69555
|
+
...freshness.required ? { freshness_proof_digest: freshness.proof.proof_digest } : {},
|
|
69556
|
+
source_database_changed: false
|
|
69557
|
+
});
|
|
69558
|
+
process2.stdout.write(args.includes("--json") ? `${JSON.stringify(freshnessJson(freshness), null, 2)}
|
|
69559
|
+
` : formatFreshnessResult(freshness, args.includes("--details")));
|
|
69560
|
+
return freshnessExitCodes[freshness.status];
|
|
69561
|
+
} finally {
|
|
69562
|
+
store.close();
|
|
69563
|
+
}
|
|
69564
|
+
}
|
|
69565
|
+
async function evaluateAndRecordProposalFreshness(input) {
|
|
69566
|
+
const required = "freshness" in input.proposal.change_set && input.proposal.change_set.freshness !== void 0;
|
|
69567
|
+
if (!required) {
|
|
69568
|
+
return {
|
|
69569
|
+
required: false,
|
|
69570
|
+
status: "not_required",
|
|
69571
|
+
safe_code: "FRESHNESS_NOT_REQUIRED",
|
|
69572
|
+
target_count: 0,
|
|
69573
|
+
supporting_count: 0
|
|
69574
|
+
};
|
|
69575
|
+
}
|
|
69576
|
+
if (!input.config) {
|
|
69577
|
+
throw new Error(`freshness-required proposal needs an existing --config file; not found: ${path25.resolve(input.configPath)}`);
|
|
69578
|
+
}
|
|
69579
|
+
const result = await evaluateProposalFreshness({
|
|
69580
|
+
config: input.config,
|
|
69581
|
+
proposal: input.proposal,
|
|
69582
|
+
env: process2.env
|
|
69583
|
+
});
|
|
69584
|
+
if (result.required) input.store.recordFreshnessProof(result.proof);
|
|
69585
|
+
return result;
|
|
69586
|
+
}
|
|
69587
|
+
function freshnessJson(result) {
|
|
69588
|
+
return {
|
|
69589
|
+
schema_version: "synapsor.proposal-freshness-result.v1",
|
|
69590
|
+
required: result.required,
|
|
69591
|
+
status: result.status,
|
|
69592
|
+
safe_code: result.safe_code,
|
|
69593
|
+
target_count: result.target_count,
|
|
69594
|
+
supporting_count: result.supporting_count,
|
|
69595
|
+
...result.required ? { proof: result.proof } : {},
|
|
69596
|
+
source_database_changed: false
|
|
69597
|
+
};
|
|
69598
|
+
}
|
|
69599
|
+
function formatFreshnessResult(result, details) {
|
|
69600
|
+
if (!result.required) return "Freshness: not required (legacy target guard remains enforced at apply).\n";
|
|
69601
|
+
const lines = [
|
|
69602
|
+
`Freshness: ${result.status}`,
|
|
69603
|
+
`code: ${result.safe_code}`,
|
|
69604
|
+
`target checks: ${result.target_count}`,
|
|
69605
|
+
`supporting checks: ${result.supporting_count}`,
|
|
69606
|
+
`checked at: ${result.proof.checked_at}`,
|
|
69607
|
+
`proof: ${result.proof.proof_digest}`
|
|
69608
|
+
];
|
|
69609
|
+
if (result.status === "stale") lines.push("next: create a new source read and proposal; this proposal cannot be refreshed or approved");
|
|
69610
|
+
else if (result.status === "unavailable") lines.push("next: retry the live check after the source is available; no approval was recorded");
|
|
69611
|
+
else if (result.status === "invalid" || result.status === "unsupported") lines.push("next: fix the reviewed freshness configuration; no approval was recorded");
|
|
69612
|
+
else lines.push("note: approval-time freshness does not replace the final apply-time revalidation");
|
|
69613
|
+
if (details) {
|
|
69614
|
+
for (const check of result.proof.checks) {
|
|
69615
|
+
lines.push(
|
|
69616
|
+
`- ${check.kind}:${check.id} ${check.status} (${check.safe_code})${check.expected_version_digest ? ` expected=${check.expected_version_digest}` : ""}${check.observed_version_digest ? ` observed=${check.observed_version_digest}` : ""}`
|
|
69617
|
+
);
|
|
69618
|
+
}
|
|
69619
|
+
}
|
|
69620
|
+
return `${lines.join("\n")}
|
|
69621
|
+
`;
|
|
69622
|
+
}
|
|
68064
69623
|
async function proposalsApprove(args) {
|
|
68065
69624
|
const proposalId = positional(args, 0);
|
|
68066
69625
|
if (!proposalId) throw new Error("proposals approve requires <proposal_id>");
|
|
@@ -68083,6 +69642,20 @@ async function proposalsApprove(args) {
|
|
|
68083
69642
|
const evidence2 = store.getEvidenceBundle(proposal.change_set.evidence.bundle_id);
|
|
68084
69643
|
process2.stdout.write(formatProposalDetail(proposal, evidence2?.items.length));
|
|
68085
69644
|
}
|
|
69645
|
+
const freshness = await evaluateAndRecordProposalFreshness({ proposal, config, configPath, store });
|
|
69646
|
+
if (!args.includes("--json")) process2.stdout.write(formatFreshnessResult(freshness, args.includes("--details")));
|
|
69647
|
+
if (freshness.status !== "fresh" && freshness.status !== "not_required") {
|
|
69648
|
+
if (freshness.required) {
|
|
69649
|
+
store.recordFreshnessApprovalBlocked(resolvedProposalId, {
|
|
69650
|
+
proof_digest: freshness.proof.proof_digest,
|
|
69651
|
+
safe_code: freshness.safe_code,
|
|
69652
|
+
actor: "operator"
|
|
69653
|
+
});
|
|
69654
|
+
}
|
|
69655
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(freshnessJson(freshness), null, 2)}
|
|
69656
|
+
`);
|
|
69657
|
+
return freshnessExitCodes[freshness.status];
|
|
69658
|
+
}
|
|
68086
69659
|
await confirmDangerousAction(args, `Approve proposal ${resolvedProposalId} for guarded writeback?`);
|
|
68087
69660
|
const identity = await operatorIdentityForDecision({ args, config, configPath, proposal, action: "approve", reason: optionalArg(args, "--reason") });
|
|
68088
69661
|
const updated = store.approveProposal(resolvedProposalId, {
|
|
@@ -68091,7 +69664,8 @@ async function proposalsApprove(args) {
|
|
|
68091
69664
|
proposal_version: proposal.proposal_version,
|
|
68092
69665
|
reason: optionalArg(args, "--reason") ?? void 0,
|
|
68093
69666
|
identity,
|
|
68094
|
-
require_verified_identity: Boolean(config?.operator_identity && config.operator_identity.provider !== "dev_env")
|
|
69667
|
+
require_verified_identity: Boolean(config?.operator_identity && config.operator_identity.provider !== "dev_env"),
|
|
69668
|
+
freshness_proof_digest: freshness.required ? freshness.proof.proof_digest : void 0
|
|
68095
69669
|
});
|
|
68096
69670
|
operationalLog("info", "operator_decision", {
|
|
68097
69671
|
action: "approve",
|
|
@@ -68102,10 +69676,16 @@ async function proposalsApprove(args) {
|
|
|
68102
69676
|
identity_provider: identity.provider,
|
|
68103
69677
|
identity_verified: identity.verified,
|
|
68104
69678
|
required_role: proposal.change_set.approval.required_role,
|
|
68105
|
-
approval_progress: `${store.approvalProgress(resolvedProposalId).approved}/${store.approvalProgress(resolvedProposalId).required}
|
|
69679
|
+
approval_progress: `${store.approvalProgress(resolvedProposalId).approved}/${store.approvalProgress(resolvedProposalId).required}`,
|
|
69680
|
+
freshness_status: freshness.status,
|
|
69681
|
+
...freshness.required ? { freshness_proof_digest: freshness.proof.proof_digest } : {}
|
|
68106
69682
|
});
|
|
68107
69683
|
const progress = store.approvalProgress(resolvedProposalId);
|
|
68108
|
-
const approvalResult = {
|
|
69684
|
+
const approvalResult = {
|
|
69685
|
+
...updated,
|
|
69686
|
+
approval_progress: progress,
|
|
69687
|
+
freshness: freshnessJson(freshness)
|
|
69688
|
+
};
|
|
68109
69689
|
process2.stdout.write(args.includes("--json") ? `${JSON.stringify(approvalResult, null, 2)}
|
|
68110
69690
|
` : progress.complete ? `approved ${updated.proposal_id} (${progress.approved}/${progress.required})
|
|
68111
69691
|
` : `approval recorded for ${updated.proposal_id} (${progress.approved}/${progress.required}); awaiting ${progress.remaining} more verified reviewer${progress.remaining === 1 ? "" : "s"}
|
|
@@ -71978,11 +73558,13 @@ Without --config, doctor is the legacy Cloud worker check and requires SYNAPSOR_
|
|
|
71978
73558
|
${cmd} proposals list [--tenant acme] [--capability billing.propose_late_fee_waiver] [--object invoice:INV-3001] [--status applied]
|
|
71979
73559
|
${cmd} proposals show latest
|
|
71980
73560
|
${cmd} proposals show latest --details
|
|
73561
|
+
${cmd} proposals check-freshness latest --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
73562
|
+
${cmd} proposals check-freshness latest --details --json --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
71981
73563
|
${cmd} proposals approve latest --yes
|
|
71982
73564
|
${cmd} proposals reject latest --reason "..."
|
|
71983
73565
|
${cmd} proposals approve latest --yes --shared-ledger-mirror --shared-ledger-url-env SYNAPSOR_LEDGER_DATABASE_URL
|
|
71984
73566
|
|
|
71985
|
-
Review decisions happen outside the model-facing MCP tool surface. Human output is concise by default; use --details for reviewer metadata or --json for complete records.
|
|
73567
|
+
Review decisions happen outside the model-facing MCP tool surface. A freshness-required approval performs the same live check automatically and binds the approval to its immutable proof. Freshness at approval improves review quality; apply rechecks again before mutation. Human output is concise by default; use --details for reviewer metadata or --json for complete records.
|
|
71986
73568
|
`,
|
|
71987
73569
|
lifecycle: `Usage:
|
|
71988
73570
|
${cmd} lifecycle [--store ./.synapsor/local.db]
|