@synapsor/runner 1.4.121 → 1.4.122
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 +26 -0
- package/README.md +23 -13
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/runner.mjs +2533 -323
- package/docs/README.md +1 -0
- package/docs/capability-authoring.md +43 -1
- package/docs/cloud-cli.md +248 -0
- package/docs/cloud-mode.md +88 -6
- package/docs/cloud-push.md +22 -5
- package/docs/conformance.md +6 -0
- package/docs/contract-testing.md +30 -1
- package/docs/dsl-reference.md +11 -0
- package/docs/hosted-cloud-linked-verification.md +97 -0
- package/docs/oss-vs-cloud.md +13 -6
- package/docs/release-notes.md +30 -0
- package/docs/rfcs/007-trusted-principal-row-scope.md +75 -0
- package/docs/runner-bundles.md +24 -0
- package/docs/running-a-runner-fleet.md +8 -0
- package/docs/security-boundary.md +15 -1
- package/examples/runner-fleet/seed/mysql.sql +5 -1
- package/examples/runner-fleet/seed/postgres.sql +5 -1
- package/examples/support-plan-credit/README.md +8 -6
- package/fixtures/dsl/principal-row-scope.synapsor.sql +23 -0
- package/fixtures/protocol/MANIFEST.json +10 -10
- package/package.json +1 -1
- package/schemas/change-set.v1.schema.json +14 -0
- package/schemas/change-set.v2.schema.json +14 -0
- package/schemas/change-set.v3.schema.json +2 -0
- package/schemas/compensation-change-set.v1.schema.json +3 -2
- package/schemas/inverse-descriptor.v1.schema.json +2 -0
- package/schemas/synapsor.contract-tests.schema.json +11 -1
- package/schemas/writeback-job.v1.schema.json +14 -0
- package/schemas/writeback-job.v2.schema.json +14 -0
- package/schemas/writeback-job.v3.schema.json +2 -0
- package/schemas/writeback-job.v4.schema.json +2 -2
package/dist/runner.mjs
CHANGED
|
@@ -22091,6 +22091,10 @@ var protocolVersions = {
|
|
|
22091
22091
|
executionReceiptV3: "synapsor.execution-receipt.v3",
|
|
22092
22092
|
executionReceiptV4: "synapsor.execution-receipt.v4",
|
|
22093
22093
|
runnerRegistration: "synapsor.runner-registration.v1",
|
|
22094
|
+
runnerControl: "synapsor.runner-control.v1",
|
|
22095
|
+
runnerProposal: "synapsor.runner-proposal.v1",
|
|
22096
|
+
runnerActivity: "synapsor.runner-activity.v1",
|
|
22097
|
+
principalScope: "synapsor.principal-scope.v1",
|
|
22094
22098
|
legacyWritebackJob: "1.0",
|
|
22095
22099
|
normalizedWritebackJobV2: "2.0",
|
|
22096
22100
|
normalizedWritebackJobV3: "3.0",
|
|
@@ -22116,6 +22120,28 @@ var columnValueSchema = z.object({
|
|
|
22116
22120
|
column: safeIdentifier,
|
|
22117
22121
|
value: scalar
|
|
22118
22122
|
});
|
|
22123
|
+
var trustedScopeProviderSchema = z.enum(["environment", "http_claims", "cloud_session", "static_dev"]);
|
|
22124
|
+
function principalScopeFingerprint(input) {
|
|
22125
|
+
return canonicalJsonDigest({
|
|
22126
|
+
schema_version: protocolVersions.principalScope,
|
|
22127
|
+
column: input.column,
|
|
22128
|
+
binding: input.binding,
|
|
22129
|
+
provider: input.provider,
|
|
22130
|
+
value: input.value
|
|
22131
|
+
});
|
|
22132
|
+
}
|
|
22133
|
+
var principalScopeGuardSchema = z.object({
|
|
22134
|
+
schema_version: z.literal(protocolVersions.principalScope),
|
|
22135
|
+
column: safeIdentifier,
|
|
22136
|
+
binding: safeIdentifier,
|
|
22137
|
+
provider: trustedScopeProviderSchema,
|
|
22138
|
+
value_fingerprint: sha256,
|
|
22139
|
+
value: scalar.optional()
|
|
22140
|
+
}).superRefine((scope, ctx) => {
|
|
22141
|
+
if (scope.value !== void 0 && principalScopeFingerprint(scope) !== scope.value_fingerprint) {
|
|
22142
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope fingerprint does not match its trusted value", path: ["value_fingerprint"] });
|
|
22143
|
+
}
|
|
22144
|
+
});
|
|
22119
22145
|
var resolvedDeduplicationComponentSchema = z.object({
|
|
22120
22146
|
column: safeIdentifier,
|
|
22121
22147
|
value: scalar,
|
|
@@ -22182,6 +22208,7 @@ var inverseDescriptorV1Schema = z.object({
|
|
|
22182
22208
|
primary_key_column: safeIdentifier
|
|
22183
22209
|
}),
|
|
22184
22210
|
tenant_guard: columnValueSchema,
|
|
22211
|
+
principal_scope: principalScopeGuardSchema.optional(),
|
|
22185
22212
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
22186
22213
|
members: z.array(inverseMemberSchema).min(1).max(100),
|
|
22187
22214
|
max_rows: z.number().int().min(1).max(100),
|
|
@@ -22240,6 +22267,7 @@ var changeSetV1Schema = z.object({
|
|
|
22240
22267
|
after: scalarMap,
|
|
22241
22268
|
guards: z.object({
|
|
22242
22269
|
tenant: columnValueSchema,
|
|
22270
|
+
principal_scope: principalScopeGuardSchema.optional(),
|
|
22243
22271
|
allowed_columns: z.array(z.string().min(1)).min(1),
|
|
22244
22272
|
expected_version: columnValueSchema
|
|
22245
22273
|
}),
|
|
@@ -22287,6 +22315,13 @@ var changeSetV1Schema = z.object({
|
|
|
22287
22315
|
path: ["guards", "allowed_columns"]
|
|
22288
22316
|
});
|
|
22289
22317
|
}
|
|
22318
|
+
if (changeSet.guards.principal_scope && allowed.has(changeSet.guards.principal_scope.column)) {
|
|
22319
|
+
ctx.addIssue({
|
|
22320
|
+
code: z.ZodIssueCode.custom,
|
|
22321
|
+
message: "principal scope column must not be patch-allowlisted",
|
|
22322
|
+
path: ["guards", "allowed_columns"]
|
|
22323
|
+
});
|
|
22324
|
+
}
|
|
22290
22325
|
});
|
|
22291
22326
|
var changeSetV2Schema = z.object({
|
|
22292
22327
|
schema_version: z.literal(protocolVersions.changeSetV2),
|
|
@@ -22317,6 +22352,7 @@ var changeSetV2Schema = z.object({
|
|
|
22317
22352
|
after: boundedScalarRecord,
|
|
22318
22353
|
guards: z.object({
|
|
22319
22354
|
tenant: columnValueSchema,
|
|
22355
|
+
principal_scope: principalScopeGuardSchema.optional(),
|
|
22320
22356
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
22321
22357
|
expected_version: columnValueSchema.optional(),
|
|
22322
22358
|
version_advance: versionAdvanceSchema.optional(),
|
|
@@ -22347,6 +22383,7 @@ var changeSetV2Schema = z.object({
|
|
|
22347
22383
|
}
|
|
22348
22384
|
if (allowed.has(changeSet.source.primary_key.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "primary key column must not be patch-allowlisted", path: ["guards", "allowed_columns"] });
|
|
22349
22385
|
if (allowed.has(changeSet.guards.tenant.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "tenant guard column must not be patch-allowlisted", path: ["guards", "allowed_columns"] });
|
|
22386
|
+
if (changeSet.guards.principal_scope && allowed.has(changeSet.guards.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["guards", "allowed_columns"] });
|
|
22350
22387
|
if (changeSet.operation === "single_row_insert") {
|
|
22351
22388
|
if (Object.keys(changeSet.before).length !== 0) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "INSERT before must be empty", path: ["before"] });
|
|
22352
22389
|
if (!changeSet.guards.deduplication) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "INSERT requires source-enforced deduplication", path: ["guards", "deduplication"] });
|
|
@@ -22401,6 +22438,7 @@ var changeSetV3Schema = z.object({
|
|
|
22401
22438
|
after: boundedScalarRecord,
|
|
22402
22439
|
guards: z.object({
|
|
22403
22440
|
tenant: columnValueSchema,
|
|
22441
|
+
principal_scope: principalScopeGuardSchema.optional(),
|
|
22404
22442
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
22405
22443
|
expected_version: columnValueSchema.optional(),
|
|
22406
22444
|
version_advance: versionAdvanceSchema.optional()
|
|
@@ -22422,6 +22460,7 @@ var changeSetV3Schema = z.object({
|
|
|
22422
22460
|
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"] });
|
|
22423
22461
|
const allowed = new Set(changeSet.guards.allowed_columns);
|
|
22424
22462
|
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] });
|
|
22463
|
+
if (changeSet.guards.principal_scope && allowed.has(changeSet.guards.principal_scope.column)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "principal scope column must not be patch-allowlisted", path: ["guards", "allowed_columns"] });
|
|
22425
22464
|
for (const [index, member] of changeSet.frozen_set.members.entries()) {
|
|
22426
22465
|
if (member.primary_key.column !== changeSet.source.primary_key.column) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "member primary key column must match source", path: ["frozen_set", "members", index, "primary_key", "column"] });
|
|
22427
22466
|
if (changeSet.operation === "set_update") {
|
|
@@ -22461,7 +22500,7 @@ var compensationChangeSetV1Schema = z.object({
|
|
|
22461
22500
|
descriptor: inverseDescriptorV1Schema,
|
|
22462
22501
|
forward_receipt_hash: sha256
|
|
22463
22502
|
}),
|
|
22464
|
-
guards: z.object({ tenant: columnValueSchema, allowed_columns: z.array(safeIdentifier).max(256) }),
|
|
22503
|
+
guards: z.object({ tenant: columnValueSchema, principal_scope: principalScopeGuardSchema.optional(), allowed_columns: z.array(safeIdentifier).max(256) }),
|
|
22465
22504
|
evidence: z.object({ bundle_id: z.string().min(1), query_fingerprint: sha256, items: z.array(z.unknown()).max(100) }).passthrough(),
|
|
22466
22505
|
approval: z.object({
|
|
22467
22506
|
status: z.enum(["pending", "approved", "rejected", "canceled"]),
|
|
@@ -22478,6 +22517,7 @@ var compensationChangeSetV1Schema = z.object({
|
|
|
22478
22517
|
if (descriptor.availability !== "available") ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation proposal requires an available inverse", path: ["compensation", "descriptor", "availability"] });
|
|
22479
22518
|
if (descriptor.target.source_id !== changeSet.source.source_id || descriptor.target.schema !== changeSet.source.schema || descriptor.target.table !== changeSet.source.table || descriptor.target.primary_key_column !== changeSet.source.primary_key.column) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation descriptor target must match proposal source", path: ["compensation", "descriptor", "target"] });
|
|
22480
22519
|
if (descriptor.tenant_guard.column !== changeSet.guards.tenant.column || descriptor.tenant_guard.value !== changeSet.guards.tenant.value || descriptor.tenant_guard.value !== changeSet.scope.tenant_id) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation tenant authority must match trusted proposal scope", path: ["guards", "tenant"] });
|
|
22520
|
+
if (JSON.stringify(descriptor.principal_scope) !== JSON.stringify(changeSet.guards.principal_scope)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation principal authority must match inverse descriptor", path: ["guards", "principal_scope"] });
|
|
22481
22521
|
if (JSON.stringify(descriptor.allowed_columns) !== JSON.stringify(changeSet.guards.allowed_columns)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation allowlist must match inverse descriptor", path: ["guards", "allowed_columns"] });
|
|
22482
22522
|
});
|
|
22483
22523
|
var publicConflictGuardSchema = z.discriminatedUnion("kind", [
|
|
@@ -22485,6 +22525,11 @@ var publicConflictGuardSchema = z.discriminatedUnion("kind", [
|
|
|
22485
22525
|
z.object({ kind: z.literal("row_hash"), expected_hash: z.string().min(1) }),
|
|
22486
22526
|
z.object({ kind: z.literal("none") })
|
|
22487
22527
|
]);
|
|
22528
|
+
var leasedContractRefSchema = z.object({
|
|
22529
|
+
contract_id: z.string().min(1),
|
|
22530
|
+
contract_version_id: z.string().min(1),
|
|
22531
|
+
digest: sha256
|
|
22532
|
+
});
|
|
22488
22533
|
var writebackJobV1Schema = z.object({
|
|
22489
22534
|
schema_version: z.literal(protocolVersions.writebackJob),
|
|
22490
22535
|
writeback_job_id: z.string().min(1),
|
|
@@ -22503,6 +22548,7 @@ var writebackJobV1Schema = z.object({
|
|
|
22503
22548
|
primary_key: columnValueSchema
|
|
22504
22549
|
}),
|
|
22505
22550
|
tenant_guard: columnValueSchema,
|
|
22551
|
+
principal_scope: principalScopeGuardSchema.optional(),
|
|
22506
22552
|
allowed_columns: z.array(safeIdentifier).min(1),
|
|
22507
22553
|
patch: scalarMap,
|
|
22508
22554
|
conflict_guard: publicConflictGuardSchema,
|
|
@@ -22514,6 +22560,7 @@ var writebackJobV1Schema = z.object({
|
|
|
22514
22560
|
})
|
|
22515
22561
|
}).superRefine((job, ctx) => {
|
|
22516
22562
|
validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.tenant_guard.column, ctx);
|
|
22563
|
+
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"] });
|
|
22517
22564
|
});
|
|
22518
22565
|
var normalizedWritebackJobV1Schema = writebackJobV1Schema.transform((job) => ({
|
|
22519
22566
|
protocol_version: protocolVersions.legacyWritebackJob,
|
|
@@ -22527,7 +22574,8 @@ var normalizedWritebackJobV1Schema = writebackJobV1Schema.transform((job) => ({
|
|
|
22527
22574
|
schema: job.target.schema,
|
|
22528
22575
|
table: job.target.table,
|
|
22529
22576
|
primary_key: job.target.primary_key,
|
|
22530
|
-
tenant_guard: job.tenant_guard
|
|
22577
|
+
tenant_guard: job.tenant_guard,
|
|
22578
|
+
...job.principal_scope ? { principal_scope: job.principal_scope } : {}
|
|
22531
22579
|
},
|
|
22532
22580
|
allowed_columns: job.allowed_columns,
|
|
22533
22581
|
patch: job.patch,
|
|
@@ -22541,6 +22589,7 @@ var legacyWritebackJobSchema = z.object({
|
|
|
22541
22589
|
job_id: z.string().min(1),
|
|
22542
22590
|
proposal_id: z.string().min(1),
|
|
22543
22591
|
approval_id: z.string().min(1),
|
|
22592
|
+
contract: leasedContractRefSchema.optional(),
|
|
22544
22593
|
source_id: z.string().min(1),
|
|
22545
22594
|
engine: writebackEngineSchema,
|
|
22546
22595
|
operation: z.literal("single_row_update").optional(),
|
|
@@ -22554,7 +22603,8 @@ var legacyWritebackJobSchema = z.object({
|
|
|
22554
22603
|
tenant_guard: z.object({
|
|
22555
22604
|
column: safeIdentifier,
|
|
22556
22605
|
value: scalar
|
|
22557
|
-
})
|
|
22606
|
+
}),
|
|
22607
|
+
principal_scope: principalScopeGuardSchema.optional()
|
|
22558
22608
|
}),
|
|
22559
22609
|
allowed_columns: z.array(safeIdentifier).min(1),
|
|
22560
22610
|
patch: scalarMap,
|
|
@@ -22568,12 +22618,14 @@ var legacyWritebackJobSchema = z.object({
|
|
|
22568
22618
|
attempt_count: z.number().int().nonnegative().optional()
|
|
22569
22619
|
}).superRefine((job, ctx) => {
|
|
22570
22620
|
validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.target.tenant_guard.column, ctx);
|
|
22621
|
+
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"] });
|
|
22571
22622
|
});
|
|
22572
22623
|
var normalizedWritebackJobV2InputSchema = z.object({
|
|
22573
22624
|
protocol_version: z.literal(protocolVersions.normalizedWritebackJobV2),
|
|
22574
22625
|
job_id: z.string().min(1),
|
|
22575
22626
|
proposal_id: z.string().min(1),
|
|
22576
22627
|
approval_id: sha256,
|
|
22628
|
+
contract: leasedContractRefSchema.optional(),
|
|
22577
22629
|
source_id: z.string().min(1),
|
|
22578
22630
|
engine: writebackEngineSchema,
|
|
22579
22631
|
operation: z.enum(["single_row_update", "single_row_insert", "single_row_delete"]),
|
|
@@ -22581,7 +22633,8 @@ var normalizedWritebackJobV2InputSchema = z.object({
|
|
|
22581
22633
|
schema: safeIdentifier,
|
|
22582
22634
|
table: safeIdentifier,
|
|
22583
22635
|
primary_key: z.object({ column: safeIdentifier, value: scalar.optional() }),
|
|
22584
|
-
tenant_guard: columnValueSchema
|
|
22636
|
+
tenant_guard: columnValueSchema,
|
|
22637
|
+
principal_scope: principalScopeGuardSchema.optional()
|
|
22585
22638
|
}),
|
|
22586
22639
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
22587
22640
|
patch: boundedScalarRecord,
|
|
@@ -22602,6 +22655,7 @@ var normalizedWritebackJobV2InputSchema = z.object({
|
|
|
22602
22655
|
}
|
|
22603
22656
|
if (job.operation === "single_row_update" || job.operation === "single_row_insert") {
|
|
22604
22657
|
validateAllowedPatchColumns(job.allowed_columns, Object.keys(job.patch), job.target.primary_key.column, job.target.tenant_guard.column, ctx);
|
|
22658
|
+
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"] });
|
|
22605
22659
|
if (Object.keys(job.patch).length === 0) ctx.addIssue({ code: z.ZodIssueCode.custom, message: `${job.operation} patch must not be empty`, path: ["patch"] });
|
|
22606
22660
|
} else if (job.allowed_columns.length !== 0 || Object.keys(job.patch).length !== 0) {
|
|
22607
22661
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "DELETE must not allow or carry write columns", path: ["allowed_columns"] });
|
|
@@ -22658,6 +22712,7 @@ var writebackJobV2Schema = z.object({
|
|
|
22658
22712
|
primary_key: z.object({ column: safeIdentifier, value: scalar.optional() })
|
|
22659
22713
|
}),
|
|
22660
22714
|
tenant_guard: columnValueSchema,
|
|
22715
|
+
principal_scope: principalScopeGuardSchema.optional(),
|
|
22661
22716
|
allowed_columns: z.array(safeIdentifier),
|
|
22662
22717
|
mutation: writebackMutationV2Schema,
|
|
22663
22718
|
idempotency_key: z.string().min(1),
|
|
@@ -22674,6 +22729,7 @@ var writebackJobV2Schema = z.object({
|
|
|
22674
22729
|
}
|
|
22675
22730
|
if (mutation.kind === "single_row_update" || mutation.kind === "single_row_insert") {
|
|
22676
22731
|
validateAllowedPatchColumns(job.allowed_columns, Object.keys(mutation.values), job.target.primary_key.column, job.tenant_guard.column, ctx);
|
|
22732
|
+
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"] });
|
|
22677
22733
|
} else if (job.allowed_columns.length !== 0) {
|
|
22678
22734
|
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "DELETE must not allow write columns", path: ["allowed_columns"] });
|
|
22679
22735
|
}
|
|
@@ -22697,7 +22753,8 @@ var writebackJobV2Schema = z.object({
|
|
|
22697
22753
|
schema: job.target.schema,
|
|
22698
22754
|
table: job.target.table,
|
|
22699
22755
|
primary_key: job.target.primary_key,
|
|
22700
|
-
tenant_guard: job.tenant_guard
|
|
22756
|
+
tenant_guard: job.tenant_guard,
|
|
22757
|
+
...job.principal_scope ? { principal_scope: job.principal_scope } : {}
|
|
22701
22758
|
},
|
|
22702
22759
|
allowed_columns: job.allowed_columns,
|
|
22703
22760
|
patch: job.mutation.kind === "single_row_delete" ? {} : job.mutation.values,
|
|
@@ -22714,10 +22771,11 @@ var normalizedWritebackJobV3InputSchema = z.object({
|
|
|
22714
22771
|
job_id: z.string().min(1),
|
|
22715
22772
|
proposal_id: z.string().min(1),
|
|
22716
22773
|
approval_id: sha256,
|
|
22774
|
+
contract: leasedContractRefSchema.optional(),
|
|
22717
22775
|
source_id: z.string().min(1),
|
|
22718
22776
|
engine: writebackEngineSchema,
|
|
22719
22777
|
operation: setOperationSchema,
|
|
22720
|
-
target: z.object({ schema: safeIdentifier, table: safeIdentifier, primary_key: z.object({ column: safeIdentifier, value: scalar.optional() }), tenant_guard: columnValueSchema }),
|
|
22778
|
+
target: z.object({ schema: safeIdentifier, table: safeIdentifier, primary_key: z.object({ column: safeIdentifier, value: scalar.optional() }), tenant_guard: columnValueSchema, principal_scope: principalScopeGuardSchema.optional() }),
|
|
22721
22779
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
22722
22780
|
patch: boundedScalarRecord,
|
|
22723
22781
|
conflict_guard: z.object({ kind: z.literal("none") }).default({ kind: "none" }),
|
|
@@ -22728,6 +22786,7 @@ var normalizedWritebackJobV3InputSchema = z.object({
|
|
|
22728
22786
|
attempt_count: z.number().int().positive(),
|
|
22729
22787
|
inverse_capture: inverseDescriptorV1Schema.optional()
|
|
22730
22788
|
}).superRefine((job, ctx) => {
|
|
22789
|
+
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"] });
|
|
22731
22790
|
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"] });
|
|
22732
22791
|
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"] });
|
|
22733
22792
|
if (job.operation === "batch_insert" && !job.frozen_set.members.every((member) => member.deduplication)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "batch INSERT requires per-item source deduplication", path: ["frozen_set", "members"] });
|
|
@@ -22743,6 +22802,7 @@ var writebackJobV3Schema = z.object({
|
|
|
22743
22802
|
operation: setOperationSchema,
|
|
22744
22803
|
target: z.object({ schema: safeIdentifier, table: safeIdentifier, primary_key: z.object({ column: safeIdentifier, value: scalar.optional() }) }),
|
|
22745
22804
|
tenant_guard: columnValueSchema,
|
|
22805
|
+
principal_scope: principalScopeGuardSchema.optional(),
|
|
22746
22806
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
22747
22807
|
patch: boundedScalarRecord,
|
|
22748
22808
|
version_advance: versionAdvanceSchema.optional(),
|
|
@@ -22758,7 +22818,7 @@ var writebackJobV3Schema = z.object({
|
|
|
22758
22818
|
source_id: job.runner_scope.source_id,
|
|
22759
22819
|
engine: job.engine,
|
|
22760
22820
|
operation: job.operation,
|
|
22761
|
-
target: { ...job.target, primary_key: { ...job.target.primary_key, value: void 0 }, tenant_guard: job.tenant_guard },
|
|
22821
|
+
target: { ...job.target, primary_key: { ...job.target.primary_key, value: void 0 }, tenant_guard: job.tenant_guard, ...job.principal_scope ? { principal_scope: job.principal_scope } : {} },
|
|
22762
22822
|
allowed_columns: job.allowed_columns,
|
|
22763
22823
|
patch: job.patch,
|
|
22764
22824
|
conflict_guard: { kind: "none" },
|
|
@@ -22780,6 +22840,7 @@ var writebackJobV4Schema = z.object({
|
|
|
22780
22840
|
operation: reversalOperationSchema,
|
|
22781
22841
|
target: z.object({ schema: safeIdentifier, table: safeIdentifier, primary_key: z.object({ column: safeIdentifier, value: scalar.optional() }) }),
|
|
22782
22842
|
tenant_guard: columnValueSchema,
|
|
22843
|
+
principal_scope: principalScopeGuardSchema.optional(),
|
|
22783
22844
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
22784
22845
|
patch: z.object({}).default({}),
|
|
22785
22846
|
compensation: inverseDescriptorV1Schema,
|
|
@@ -22790,6 +22851,7 @@ var writebackJobV4Schema = z.object({
|
|
|
22790
22851
|
if (job.compensation.availability !== "available") ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation job requires an available inverse", path: ["compensation", "availability"] });
|
|
22791
22852
|
if (job.compensation.target.source_id !== job.runner_scope.source_id) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation source must match runner scope", path: ["compensation", "target", "source_id"] });
|
|
22792
22853
|
if (job.operation !== job.compensation.operation || job.target.schema !== job.compensation.target.schema || job.target.table !== job.compensation.target.table || job.target.primary_key.column !== job.compensation.target.primary_key_column || job.tenant_guard.column !== job.compensation.tenant_guard.column || job.tenant_guard.value !== job.compensation.tenant_guard.value) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation public authority must match descriptor", path: ["compensation"] });
|
|
22854
|
+
if (JSON.stringify(job.principal_scope) !== JSON.stringify(job.compensation.principal_scope)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation principal authority must match descriptor", path: ["compensation", "principal_scope"] });
|
|
22793
22855
|
}).transform((job) => ({
|
|
22794
22856
|
protocol_version: protocolVersions.normalizedWritebackJobV4,
|
|
22795
22857
|
job_id: job.writeback_job_id,
|
|
@@ -22800,7 +22862,8 @@ var writebackJobV4Schema = z.object({
|
|
|
22800
22862
|
operation: job.operation,
|
|
22801
22863
|
target: {
|
|
22802
22864
|
...job.target,
|
|
22803
|
-
tenant_guard: job.tenant_guard
|
|
22865
|
+
tenant_guard: job.tenant_guard,
|
|
22866
|
+
...job.principal_scope ? { principal_scope: job.principal_scope } : {}
|
|
22804
22867
|
},
|
|
22805
22868
|
allowed_columns: job.allowed_columns,
|
|
22806
22869
|
patch: job.patch,
|
|
@@ -22816,10 +22879,11 @@ var normalizedWritebackJobV4InputSchema = z.object({
|
|
|
22816
22879
|
job_id: z.string().min(1),
|
|
22817
22880
|
proposal_id: z.string().min(1),
|
|
22818
22881
|
approval_id: sha256,
|
|
22882
|
+
contract: leasedContractRefSchema.optional(),
|
|
22819
22883
|
source_id: z.string().min(1),
|
|
22820
22884
|
engine: writebackEngineSchema,
|
|
22821
22885
|
operation: reversalOperationSchema,
|
|
22822
|
-
target: z.object({ schema: safeIdentifier, table: safeIdentifier, primary_key: z.object({ column: safeIdentifier, value: scalar.optional() }), tenant_guard: columnValueSchema }),
|
|
22886
|
+
target: z.object({ schema: safeIdentifier, table: safeIdentifier, primary_key: z.object({ column: safeIdentifier, value: scalar.optional() }), tenant_guard: columnValueSchema, principal_scope: principalScopeGuardSchema.optional() }),
|
|
22823
22887
|
allowed_columns: z.array(safeIdentifier).max(256),
|
|
22824
22888
|
patch: z.object({}).default({}),
|
|
22825
22889
|
conflict_guard: z.object({ kind: z.literal("none") }).default({ kind: "none" }),
|
|
@@ -22832,6 +22896,7 @@ var normalizedWritebackJobV4InputSchema = z.object({
|
|
|
22832
22896
|
if (job.operation !== job.compensation.operation) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation operation mismatch", path: ["operation"] });
|
|
22833
22897
|
if (job.source_id !== job.compensation.target.source_id || job.target.schema !== job.compensation.target.schema || job.target.table !== job.compensation.target.table || job.target.primary_key.column !== job.compensation.target.primary_key_column) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation target mismatch", path: ["target"] });
|
|
22834
22898
|
if (job.target.tenant_guard.column !== job.compensation.tenant_guard.column || job.target.tenant_guard.value !== job.compensation.tenant_guard.value) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation tenant mismatch", path: ["target", "tenant_guard"] });
|
|
22899
|
+
if (JSON.stringify(job.target.principal_scope) !== JSON.stringify(job.compensation.principal_scope)) ctx.addIssue({ code: z.ZodIssueCode.custom, message: "compensation principal mismatch", path: ["target", "principal_scope"] });
|
|
22835
22900
|
});
|
|
22836
22901
|
var writebackJobSchema = z.union([legacyWritebackJobSchema, normalizedWritebackJobV4InputSchema, normalizedWritebackJobV3InputSchema, normalizedWritebackJobV2InputSchema, normalizedWritebackJobV1Schema, writebackJobV4Schema, writebackJobV3Schema, writebackJobV2Schema]);
|
|
22837
22902
|
var executionReceiptV1Schema = z.object({
|
|
@@ -23050,6 +23115,7 @@ var normalizedWritebackResultV4Schema = z.object({
|
|
|
23050
23115
|
var writebackResultSchema = z.union([legacyWritebackResultSchema, normalizedExecutionReceiptV1Schema, normalizedWritebackResultV4Schema, normalizedWritebackResultV3Schema, normalizedWritebackResultV2Schema]);
|
|
23051
23116
|
var runnerRegistrationV1Schema = z.object({
|
|
23052
23117
|
schema_version: z.literal(protocolVersions.runnerRegistration),
|
|
23118
|
+
protocol_version: z.literal(protocolVersions.runnerControl).optional(),
|
|
23053
23119
|
runner_id: z.string().min(1),
|
|
23054
23120
|
runner_version: z.string().min(1),
|
|
23055
23121
|
engines: z.array(writebackEngineSchema).min(1),
|
|
@@ -23058,8 +23124,59 @@ var runnerRegistrationV1Schema = z.object({
|
|
|
23058
23124
|
project_id: z.string().min(1),
|
|
23059
23125
|
source_ids: z.array(z.string().min(1)).min(1)
|
|
23060
23126
|
}),
|
|
23127
|
+
contracts: z.array(z.object({
|
|
23128
|
+
contract_id: z.string().min(1),
|
|
23129
|
+
contract_version_id: z.string().min(1),
|
|
23130
|
+
digest: sha256
|
|
23131
|
+
})).max(100).optional(),
|
|
23061
23132
|
registered_at: z.string().min(1)
|
|
23062
23133
|
});
|
|
23134
|
+
var runnerProposalV1Schema = z.object({
|
|
23135
|
+
schema_version: z.literal(protocolVersions.runnerProposal),
|
|
23136
|
+
runner_id: z.string().min(1),
|
|
23137
|
+
source_id: z.string().min(1),
|
|
23138
|
+
mapping_id: z.string().min(1).optional(),
|
|
23139
|
+
contract: z.object({
|
|
23140
|
+
contract_id: z.string().min(1),
|
|
23141
|
+
contract_version_id: z.string().min(1),
|
|
23142
|
+
digest: sha256
|
|
23143
|
+
}),
|
|
23144
|
+
change_set: z.union([changeSetV1Schema, changeSetV2Schema, changeSetV3Schema, compensationChangeSetV1Schema]),
|
|
23145
|
+
evidence_metadata: z.record(z.unknown()).optional(),
|
|
23146
|
+
query_audit: z.record(z.unknown()).optional()
|
|
23147
|
+
}).superRefine((proposal, ctx) => {
|
|
23148
|
+
if (proposal.change_set.source_database_mutated) {
|
|
23149
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Runner proposals must describe an unchanged source database", path: ["change_set", "source_database_mutated"] });
|
|
23150
|
+
}
|
|
23151
|
+
if (proposal.change_set.guards.principal_scope?.value !== void 0) {
|
|
23152
|
+
ctx.addIssue({ code: z.ZodIssueCode.custom, message: "Runner Cloud proposals must use fingerprint-only principal scope metadata", path: ["change_set", "guards", "principal_scope", "value"] });
|
|
23153
|
+
}
|
|
23154
|
+
});
|
|
23155
|
+
var runnerActivityV1Schema = z.object({
|
|
23156
|
+
schema_version: z.literal(protocolVersions.runnerActivity),
|
|
23157
|
+
event_id: z.string().min(1).max(120).regex(/^[A-Za-z0-9_.:-]+$/),
|
|
23158
|
+
event_type: z.enum(["evidence.recorded", "query_audit.recorded", "replay.recorded", "worker.diagnostic"]),
|
|
23159
|
+
runner_id: z.string().min(1).max(120),
|
|
23160
|
+
source_id: z.string().min(1).max(120),
|
|
23161
|
+
proposal_id: z.string().min(1).max(120).optional(),
|
|
23162
|
+
job_id: z.string().min(1).max(120).optional(),
|
|
23163
|
+
contract_id: z.string().min(1).max(120).optional(),
|
|
23164
|
+
contract_version_id: z.string().min(1).max(120).optional(),
|
|
23165
|
+
contract_digest: sha256.optional(),
|
|
23166
|
+
capability: z.string().min(1).max(160).optional(),
|
|
23167
|
+
workflow: z.string().min(1).max(160).optional(),
|
|
23168
|
+
tenant_id: z.string().min(1).max(160).optional(),
|
|
23169
|
+
principal: z.string().min(1).max(200).optional(),
|
|
23170
|
+
business_object: z.string().min(1).max(160).optional(),
|
|
23171
|
+
object_id: z.string().min(1).max(200).optional(),
|
|
23172
|
+
status: z.string().min(1).max(80).optional(),
|
|
23173
|
+
evidence_ids: z.array(z.string().min(1).max(160)).max(100).optional(),
|
|
23174
|
+
query_audit_ids: z.array(z.string().min(1).max(160)).max(100).optional(),
|
|
23175
|
+
receipt_id: z.string().min(1).max(160).optional(),
|
|
23176
|
+
replay_id: z.string().min(1).max(160).optional(),
|
|
23177
|
+
detail: z.record(z.unknown()).optional(),
|
|
23178
|
+
occurred_at: z.string().min(1).max(80).optional()
|
|
23179
|
+
}).strict();
|
|
23063
23180
|
function validateAllowedPatchColumns(allowedColumns, patchColumns, primaryKeyColumn, tenantGuardColumn, ctx) {
|
|
23064
23181
|
const allow = new Set(allowedColumns);
|
|
23065
23182
|
for (const column of patchColumns) {
|
|
@@ -23132,26 +23249,189 @@ function parseWritebackResult(input) {
|
|
|
23132
23249
|
function parseRunnerRegistration(input) {
|
|
23133
23250
|
return runnerRegistrationV1Schema.parse(input);
|
|
23134
23251
|
}
|
|
23252
|
+
function parseRunnerProposal(input) {
|
|
23253
|
+
return runnerProposalV1Schema.parse(input);
|
|
23254
|
+
}
|
|
23255
|
+
function parseRunnerActivity(input) {
|
|
23256
|
+
return runnerActivityV1Schema.parse(input);
|
|
23257
|
+
}
|
|
23135
23258
|
|
|
23136
23259
|
// packages/control-plane-client/src/index.ts
|
|
23260
|
+
var CloudControlError = class extends Error {
|
|
23261
|
+
error_code;
|
|
23262
|
+
retryable;
|
|
23263
|
+
retry_after_ms;
|
|
23264
|
+
request_id;
|
|
23265
|
+
status;
|
|
23266
|
+
details;
|
|
23267
|
+
constructor(shape) {
|
|
23268
|
+
super(shape.message);
|
|
23269
|
+
this.name = "CloudControlError";
|
|
23270
|
+
this.error_code = shape.error_code;
|
|
23271
|
+
this.retryable = shape.retryable;
|
|
23272
|
+
this.retry_after_ms = shape.retry_after_ms;
|
|
23273
|
+
this.request_id = shape.request_id;
|
|
23274
|
+
this.status = shape.status;
|
|
23275
|
+
this.details = shape.details;
|
|
23276
|
+
}
|
|
23277
|
+
toJSON() {
|
|
23278
|
+
return {
|
|
23279
|
+
error_code: this.error_code,
|
|
23280
|
+
message: this.message,
|
|
23281
|
+
retryable: this.retryable,
|
|
23282
|
+
...this.retry_after_ms === void 0 ? {} : { retry_after_ms: this.retry_after_ms },
|
|
23283
|
+
...this.request_id ? { request_id: this.request_id } : {},
|
|
23284
|
+
status: this.status,
|
|
23285
|
+
...this.details ? { details: this.details } : {}
|
|
23286
|
+
};
|
|
23287
|
+
}
|
|
23288
|
+
};
|
|
23289
|
+
var CloudControlClient = class {
|
|
23290
|
+
baseUrl;
|
|
23291
|
+
credential;
|
|
23292
|
+
credentialKind;
|
|
23293
|
+
userAgent;
|
|
23294
|
+
maxRetries;
|
|
23295
|
+
timeoutMs;
|
|
23296
|
+
constructor(config) {
|
|
23297
|
+
this.baseUrl = normalizeBaseUrl(config.baseUrl);
|
|
23298
|
+
this.credential = requireValue(config.credential, "Cloud credential");
|
|
23299
|
+
this.credentialKind = config.credentialKind ?? "service";
|
|
23300
|
+
this.userAgent = config.userAgent ?? "synapsor-cloud-client";
|
|
23301
|
+
this.maxRetries = Math.max(0, Math.min(5, config.maxRetries ?? 2));
|
|
23302
|
+
this.timeoutMs = Math.max(1e3, Math.min(12e4, config.timeoutMs ?? 15e3));
|
|
23303
|
+
}
|
|
23304
|
+
async get(path9) {
|
|
23305
|
+
return this.request(path9, { method: "GET" });
|
|
23306
|
+
}
|
|
23307
|
+
async post(path9, body, options = {}) {
|
|
23308
|
+
return this.request(path9, { ...options, method: "POST", body });
|
|
23309
|
+
}
|
|
23310
|
+
async request(path9, options = {}) {
|
|
23311
|
+
const method = options.method ?? "GET";
|
|
23312
|
+
const retryPermitted = options.retry !== false && (method === "GET" || Boolean(options.idempotencyKey));
|
|
23313
|
+
let attempt = 0;
|
|
23314
|
+
while (true) {
|
|
23315
|
+
try {
|
|
23316
|
+
return await this.requestOnce(path9, options);
|
|
23317
|
+
} catch (error) {
|
|
23318
|
+
const retryable = error instanceof CloudControlError && error.retryable;
|
|
23319
|
+
if (!retryPermitted || !retryable || attempt >= this.maxRetries) throw error;
|
|
23320
|
+
const delay = error.retry_after_ms ?? Math.min(5e3, 200 * 2 ** attempt);
|
|
23321
|
+
attempt += 1;
|
|
23322
|
+
await sleep(delay);
|
|
23323
|
+
}
|
|
23324
|
+
}
|
|
23325
|
+
}
|
|
23326
|
+
async pushContract(input) {
|
|
23327
|
+
const localDigest = canonicalJsonDigest(input.contract);
|
|
23328
|
+
const payload = {
|
|
23329
|
+
schema_version: "synapsor.cloud-contract-push.v0.1",
|
|
23330
|
+
contract: input.contract,
|
|
23331
|
+
name: input.name,
|
|
23332
|
+
description: input.description,
|
|
23333
|
+
source: input.source,
|
|
23334
|
+
source_versions: input.sourceVersions ?? {},
|
|
23335
|
+
activate: input.activate === true,
|
|
23336
|
+
idempotency_key: input.idempotencyKey,
|
|
23337
|
+
local_digest: localDigest
|
|
23338
|
+
};
|
|
23339
|
+
const response = await this.post(
|
|
23340
|
+
`/v1/control/projects/${encodeURIComponent(input.projectId)}/agent-contracts`,
|
|
23341
|
+
payload,
|
|
23342
|
+
{ idempotencyKey: input.idempotencyKey ?? localDigest }
|
|
23343
|
+
);
|
|
23344
|
+
const remoteDigest = firstString(response.digest, normalizeRecord(response.version)?.digest);
|
|
23345
|
+
if (!remoteDigest) {
|
|
23346
|
+
throw new CloudControlError({
|
|
23347
|
+
error_code: "contract_digest_missing",
|
|
23348
|
+
message: "Cloud did not return the canonical contract digest.",
|
|
23349
|
+
retryable: false,
|
|
23350
|
+
status: 502
|
|
23351
|
+
});
|
|
23352
|
+
}
|
|
23353
|
+
if (remoteDigest !== localDigest) {
|
|
23354
|
+
throw new CloudControlError({
|
|
23355
|
+
error_code: "contract_digest_mismatch",
|
|
23356
|
+
message: `Cloud contract digest ${remoteDigest} does not match local digest ${localDigest}.`,
|
|
23357
|
+
retryable: false,
|
|
23358
|
+
status: 409,
|
|
23359
|
+
details: { local_digest: localDigest, remote_digest: remoteDigest }
|
|
23360
|
+
});
|
|
23361
|
+
}
|
|
23362
|
+
return { ...response, local_digest: localDigest };
|
|
23363
|
+
}
|
|
23364
|
+
async requestOnce(path9, options) {
|
|
23365
|
+
const endpoint = new URL(path9, `${this.baseUrl}/`);
|
|
23366
|
+
if (endpoint.origin !== new URL(this.baseUrl).origin) {
|
|
23367
|
+
throw new CloudControlError({ error_code: "invalid_api_path", message: "Cloud API path must remain on the configured origin.", retryable: false, status: 0 });
|
|
23368
|
+
}
|
|
23369
|
+
const controller = new AbortController();
|
|
23370
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
23371
|
+
let response;
|
|
23372
|
+
try {
|
|
23373
|
+
response = await fetch(endpoint.toString(), {
|
|
23374
|
+
method: options.method ?? "GET",
|
|
23375
|
+
headers: {
|
|
23376
|
+
accept: "application/json",
|
|
23377
|
+
authorization: `Bearer ${this.credential}`,
|
|
23378
|
+
"user-agent": this.userAgent,
|
|
23379
|
+
"x-synapsor-credential-kind": this.credentialKind,
|
|
23380
|
+
...options.body === void 0 ? {} : { "content-type": "application/json" },
|
|
23381
|
+
...options.idempotencyKey ? { "idempotency-key": options.idempotencyKey } : {},
|
|
23382
|
+
...options.headers
|
|
23383
|
+
},
|
|
23384
|
+
...options.body === void 0 ? {} : { body: JSON.stringify(options.body) },
|
|
23385
|
+
signal: controller.signal
|
|
23386
|
+
});
|
|
23387
|
+
} catch (error) {
|
|
23388
|
+
const timeout = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError");
|
|
23389
|
+
throw new CloudControlError({
|
|
23390
|
+
error_code: timeout ? "request_timeout" : "network_unavailable",
|
|
23391
|
+
message: timeout ? "Cloud request timed out." : "Cloud request could not reach the configured API.",
|
|
23392
|
+
retryable: true,
|
|
23393
|
+
retry_after_ms: 500,
|
|
23394
|
+
status: 0
|
|
23395
|
+
});
|
|
23396
|
+
} finally {
|
|
23397
|
+
clearTimeout(timer);
|
|
23398
|
+
}
|
|
23399
|
+
const payload = await response.json().catch(() => ({}));
|
|
23400
|
+
if (!response.ok || payload.ok === false) throw cloudControlError(response, payload);
|
|
23401
|
+
return payload;
|
|
23402
|
+
}
|
|
23403
|
+
};
|
|
23137
23404
|
var ControlPlaneClient = class {
|
|
23138
23405
|
baseUrl;
|
|
23139
23406
|
runnerToken;
|
|
23140
23407
|
sourceId;
|
|
23408
|
+
runnerId;
|
|
23409
|
+
transport;
|
|
23141
23410
|
constructor(config) {
|
|
23142
23411
|
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
23143
23412
|
this.runnerToken = config.runnerToken;
|
|
23144
23413
|
this.sourceId = config.sourceId;
|
|
23414
|
+
this.runnerId = config.runnerId;
|
|
23415
|
+
this.transport = new CloudControlClient({
|
|
23416
|
+
baseUrl: config.baseUrl,
|
|
23417
|
+
credential: config.runnerToken,
|
|
23418
|
+
credentialKind: "runner",
|
|
23419
|
+
userAgent: "synapsor-runner-control-plane-client"
|
|
23420
|
+
});
|
|
23145
23421
|
}
|
|
23146
23422
|
async claim(options = {}) {
|
|
23147
23423
|
const body = {
|
|
23148
23424
|
source_id: options.sourceId || this.sourceId,
|
|
23425
|
+
runner_id: options.runnerId || this.runnerId,
|
|
23149
23426
|
limit: options.limit ?? 1,
|
|
23150
23427
|
lease_seconds: options.leaseSeconds
|
|
23151
23428
|
};
|
|
23152
23429
|
const response = await this.post("/v1/writeback/jobs/claim", body);
|
|
23153
23430
|
const jobs = Array.isArray(response.jobs) ? response.jobs : [];
|
|
23154
|
-
return jobs.map((job) =>
|
|
23431
|
+
return jobs.map((job) => {
|
|
23432
|
+
const parsed = parseWritebackJob(job);
|
|
23433
|
+
return Object.assign(parsed, { cloud_lease: cloudLease(job) });
|
|
23434
|
+
});
|
|
23155
23435
|
}
|
|
23156
23436
|
async register(payload) {
|
|
23157
23437
|
const registration = parseRunnerRegistration(payload);
|
|
@@ -23160,18 +23440,36 @@ var ControlPlaneClient = class {
|
|
|
23160
23440
|
async runnerHeartbeat(payload) {
|
|
23161
23441
|
return this.post("/v1/runner/heartbeat", payload);
|
|
23162
23442
|
}
|
|
23163
|
-
async
|
|
23164
|
-
|
|
23443
|
+
async submitProposal(payload) {
|
|
23444
|
+
return this.post("/v1/runner/proposals", parseRunnerProposal(payload));
|
|
23445
|
+
}
|
|
23446
|
+
async submitActivity(payload) {
|
|
23447
|
+
return this.post("/v1/runner/activity", parseRunnerActivity(payload));
|
|
23448
|
+
}
|
|
23449
|
+
async proposalStatus(proposalId) {
|
|
23450
|
+
return this.transport.get(`/v1/runner/proposals/${encodeURIComponent(requireValue(proposalId, "proposal_id"))}`);
|
|
23165
23451
|
}
|
|
23166
|
-
async
|
|
23167
|
-
|
|
23452
|
+
async heartbeat(jobId, leaseId, runnerId = this.runnerId, leaseSeconds = 60) {
|
|
23453
|
+
await this.renewLease(jobId, leaseId, runnerId, leaseSeconds);
|
|
23168
23454
|
}
|
|
23169
|
-
async
|
|
23455
|
+
async renewLease(jobId, leaseId, runnerId = this.runnerId, leaseSeconds = 60) {
|
|
23456
|
+
return this.post(`/v1/writeback/jobs/${encodeURIComponent(jobId)}/heartbeat`, {
|
|
23457
|
+
runner_id: requireValue(runnerId, "runner_id"),
|
|
23458
|
+
lease_id: requireValue(leaseId, "lease_id"),
|
|
23459
|
+
lease_seconds: leaseSeconds
|
|
23460
|
+
});
|
|
23461
|
+
}
|
|
23462
|
+
async result(result, leaseId) {
|
|
23170
23463
|
const parsed = parseWritebackResult(result);
|
|
23171
|
-
|
|
23464
|
+
const cloudSafeResult = { ...parsed };
|
|
23465
|
+
delete cloudSafeResult.inverse;
|
|
23466
|
+
return this.post(`/v1/writeback/jobs/${encodeURIComponent(parsed.job_id)}/result`, {
|
|
23467
|
+
...cloudSafeResult,
|
|
23468
|
+
lease_id: requireValue(leaseId, "lease_id")
|
|
23469
|
+
});
|
|
23172
23470
|
}
|
|
23173
|
-
async submitReceipt(result) {
|
|
23174
|
-
|
|
23471
|
+
async submitReceipt(result, leaseId) {
|
|
23472
|
+
return this.result(result, leaseId);
|
|
23175
23473
|
}
|
|
23176
23474
|
async adapterTools(adapterId, options = {}) {
|
|
23177
23475
|
const response = await this.post("/v1/agent/adapters/tools", {
|
|
@@ -23220,38 +23518,26 @@ var ControlPlaneClient = class {
|
|
|
23220
23518
|
return { ok: health.ok, status: health.status, authenticated: false, details: { fallback: "health_endpoint_only" } };
|
|
23221
23519
|
}
|
|
23222
23520
|
async post(path9, body) {
|
|
23223
|
-
const
|
|
23224
|
-
|
|
23225
|
-
headers: {
|
|
23226
|
-
"Authorization": `Bearer ${this.runnerToken}`,
|
|
23227
|
-
"Content-Type": "application/json"
|
|
23228
|
-
},
|
|
23229
|
-
body: JSON.stringify(body)
|
|
23230
|
-
});
|
|
23231
|
-
const payload = await response.json().catch(() => ({}));
|
|
23232
|
-
if (!response.ok || !payload?.ok) {
|
|
23233
|
-
const code = typeof payload?.error === "string" ? payload.error : `http_${response.status}`;
|
|
23234
|
-
throw new Error(`control plane request failed: ${code}`);
|
|
23235
|
-
}
|
|
23236
|
-
return payload;
|
|
23237
|
-
}
|
|
23238
|
-
async fetchWithRetry(path9, init2) {
|
|
23239
|
-
let lastError;
|
|
23240
|
-
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
23241
|
-
try {
|
|
23242
|
-
const response = await fetch(`${this.baseUrl}${path9}`, init2);
|
|
23243
|
-
if (![408, 429, 500, 502, 503, 504].includes(response.status)) return response;
|
|
23244
|
-
if (attempt === 2) return response;
|
|
23245
|
-
lastError = new Error(`retryable_http_${response.status}`);
|
|
23246
|
-
} catch (error) {
|
|
23247
|
-
lastError = error;
|
|
23248
|
-
if (attempt === 2) throw error;
|
|
23249
|
-
}
|
|
23250
|
-
await sleep(100 * 2 ** attempt + Math.floor(Math.random() * 25));
|
|
23251
|
-
}
|
|
23252
|
-
throw lastError instanceof Error ? lastError : new Error(String(lastError));
|
|
23521
|
+
const normalizedBody = JSON.parse(JSON.stringify(body));
|
|
23522
|
+
return this.transport.post(path9, normalizedBody, { idempotencyKey: canonicalJsonDigest({ path: path9, body: normalizedBody }) });
|
|
23253
23523
|
}
|
|
23254
23524
|
};
|
|
23525
|
+
function cloudLease(input) {
|
|
23526
|
+
const record = normalizeRecord(input);
|
|
23527
|
+
const publicLease = normalizeRecord(record?.lease);
|
|
23528
|
+
const leaseId = typeof publicLease?.lease_id === "string" ? publicLease.lease_id : typeof record?.lease_id === "string" ? record.lease_id : "";
|
|
23529
|
+
const expiresAt = publicLease?.expires_at ?? record?.lease_expires_at;
|
|
23530
|
+
const attempt = Number(publicLease?.attempt ?? record?.attempt_count ?? 0);
|
|
23531
|
+
if (!leaseId || typeof expiresAt !== "string" && typeof expiresAt !== "number" || !Number.isSafeInteger(attempt) || attempt < 1) {
|
|
23532
|
+
throw new Error("control plane returned a writeback job without a valid lease");
|
|
23533
|
+
}
|
|
23534
|
+
return { leaseId, expiresAt, attempt };
|
|
23535
|
+
}
|
|
23536
|
+
function requireValue(value, field) {
|
|
23537
|
+
const normalized = String(value ?? "").trim();
|
|
23538
|
+
if (!normalized) throw new Error(`${field} is required for Cloud lease ownership`);
|
|
23539
|
+
return normalized;
|
|
23540
|
+
}
|
|
23255
23541
|
function normalizeTool(value) {
|
|
23256
23542
|
const record = normalizeRecord(value);
|
|
23257
23543
|
if (!record || typeof record.name !== "string" || record.name.length === 0) return void 0;
|
|
@@ -23268,12 +23554,59 @@ function normalizeTool(value) {
|
|
|
23268
23554
|
function normalizeRecord(value) {
|
|
23269
23555
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
|
|
23270
23556
|
}
|
|
23557
|
+
function normalizeBaseUrl(value) {
|
|
23558
|
+
const normalized = String(value || "").trim().replace(/\/+$/, "");
|
|
23559
|
+
let parsed;
|
|
23560
|
+
try {
|
|
23561
|
+
parsed = new URL(normalized);
|
|
23562
|
+
} catch {
|
|
23563
|
+
throw new CloudControlError({ error_code: "invalid_api_url", message: "Cloud API URL is invalid.", retryable: false, status: 0 });
|
|
23564
|
+
}
|
|
23565
|
+
if (!/^https?:$/.test(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
23566
|
+
throw new CloudControlError({ error_code: "invalid_api_url", message: "Cloud API URL must be an HTTP(S) origin without credentials, query, or fragment.", retryable: false, status: 0 });
|
|
23567
|
+
}
|
|
23568
|
+
return normalized;
|
|
23569
|
+
}
|
|
23570
|
+
function firstString(...values) {
|
|
23571
|
+
return values.find((value) => typeof value === "string" && value.length > 0) ?? "";
|
|
23572
|
+
}
|
|
23573
|
+
function cloudControlError(response, payload) {
|
|
23574
|
+
const errorCode = firstString(payload.error_code, payload.error, `http_${response.status}`);
|
|
23575
|
+
const requestId = firstString(payload.request_id, response.headers.get("x-request-id"));
|
|
23576
|
+
const retryAfterHeader = response.headers.get("retry-after");
|
|
23577
|
+
const payloadDelay = Number(payload.retry_after_ms);
|
|
23578
|
+
const headerSeconds = retryAfterHeader === null ? Number.NaN : Number(retryAfterHeader);
|
|
23579
|
+
const retryAfterMs = Number.isFinite(payloadDelay) && payloadDelay >= 0 ? payloadDelay : Number.isFinite(headerSeconds) && headerSeconds >= 0 ? Math.round(headerSeconds * 1e3) : void 0;
|
|
23580
|
+
const retryable = payload.retryable === true || [408, 425, 429, 502, 503, 504].includes(response.status);
|
|
23581
|
+
const message2 = firstString(payload.message, payload.error_description, humanCloudErrorMessage(errorCode, response.status));
|
|
23582
|
+
const details = normalizeRecord(payload.details) ?? (Array.isArray(payload.errors) ? { errors: payload.errors } : void 0);
|
|
23583
|
+
return new CloudControlError({
|
|
23584
|
+
error_code: errorCode,
|
|
23585
|
+
message: message2,
|
|
23586
|
+
retryable,
|
|
23587
|
+
...retryAfterMs === void 0 ? {} : { retry_after_ms: retryAfterMs },
|
|
23588
|
+
...requestId ? { request_id: requestId } : {},
|
|
23589
|
+
status: response.status,
|
|
23590
|
+
...details ? { details } : {}
|
|
23591
|
+
});
|
|
23592
|
+
}
|
|
23593
|
+
function humanCloudErrorMessage(code, status) {
|
|
23594
|
+
if (code === "payment_required") return "This project does not currently have the required Cloud entitlement.";
|
|
23595
|
+
if (code === "feature_not_entitled") return "This Cloud feature is not enabled for the selected project.";
|
|
23596
|
+
if (status === 401) return "Cloud authentication failed or the credential expired.";
|
|
23597
|
+
if (status === 403) return "The authenticated principal is not authorized for this operation.";
|
|
23598
|
+
if (status === 404) return "The requested Cloud resource was not found in the selected scope.";
|
|
23599
|
+
if (status === 409) return "Cloud rejected the request because the current state conflicts with it.";
|
|
23600
|
+
if (status === 429) return "Cloud rate-limited the request; retry after the supplied delay.";
|
|
23601
|
+
if (status >= 500) return "Cloud is temporarily unable to complete the request.";
|
|
23602
|
+
return `Cloud request failed (${code}).`;
|
|
23603
|
+
}
|
|
23271
23604
|
function sleep(ms) {
|
|
23272
23605
|
return new Promise((resolve2) => setTimeout(resolve2, ms));
|
|
23273
23606
|
}
|
|
23274
23607
|
|
|
23275
23608
|
// packages/config/src/index.ts
|
|
23276
|
-
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "operator_identity", "session_auth", "rate_limits", "metrics", "graduated_trust", "cloud", "strict", "result_format"]);
|
|
23609
|
+
var TOP_LEVEL_KEYS = /* @__PURE__ */ new Set(["version", "mode", "storage", "sources", "trusted_context", "contexts", "executors", "capabilities", "contracts", "policies", "approvals", "operator_identity", "session_auth", "rate_limits", "metrics", "graduated_trust", "cloud", "governance", "strict", "result_format"]);
|
|
23277
23610
|
var STORAGE_KEYS = /* @__PURE__ */ new Set(["sqlite_path", "shared_postgres"]);
|
|
23278
23611
|
var SHARED_POSTGRES_STORAGE_KEYS = /* @__PURE__ */ new Set(["mode", "url_env", "schema", "lock_timeout_ms", "max_entries"]);
|
|
23279
23612
|
var APPROVALS_KEYS = /* @__PURE__ */ new Set(["disable_auto_approval"]);
|
|
@@ -23338,6 +23671,7 @@ var SESSION_AUTH_KEYS = /* @__PURE__ */ new Set([
|
|
|
23338
23671
|
var RATE_LIMITS_KEYS = /* @__PURE__ */ new Set(["enabled", "default", "capabilities"]);
|
|
23339
23672
|
var RATE_LIMIT_RULE_KEYS = /* @__PURE__ */ new Set(["requests", "window_seconds"]);
|
|
23340
23673
|
var CLOUD_KEYS = /* @__PURE__ */ new Set(["base_url_env", "runner_token_env", "runner_id", "runner_version", "project_id", "adapter_id", "source_id", "engines", "capabilities", "session"]);
|
|
23674
|
+
var GOVERNANCE_KEYS = /* @__PURE__ */ new Set(["mode", "connection_file", "evidence_residency", "queue_when_unavailable", "sync_interval_ms", "max_attempts", "outbox_retention_days"]);
|
|
23341
23675
|
var SOURCE_KEYS = /* @__PURE__ */ new Set([
|
|
23342
23676
|
"engine",
|
|
23343
23677
|
"read_url_env",
|
|
@@ -23350,7 +23684,7 @@ var SOURCE_KEYS = /* @__PURE__ */ new Set([
|
|
|
23350
23684
|
]);
|
|
23351
23685
|
var SOURCE_POOL_KEYS = /* @__PURE__ */ new Set(["max_connections", "connection_timeout_ms", "idle_timeout_ms", "queue_timeout_ms", "queue_limit"]);
|
|
23352
23686
|
var SOURCE_RECEIPT_KEYS = /* @__PURE__ */ new Set(["authority", "provisioning", "schema", "table"]);
|
|
23353
|
-
var TRUSTED_CONTEXT_KEYS = /* @__PURE__ */ new Set(["provider", "values"]);
|
|
23687
|
+
var TRUSTED_CONTEXT_KEYS = /* @__PURE__ */ new Set(["provider", "values", "tenant_binding", "principal_binding"]);
|
|
23354
23688
|
var CONTEXT_KEYS = TRUSTED_CONTEXT_KEYS;
|
|
23355
23689
|
var EXECUTOR_KEYS = /* @__PURE__ */ new Set(["type", "url_env", "method", "auth", "signing_secret_env", "timeout_ms", "command_env"]);
|
|
23356
23690
|
var EXECUTOR_AUTH_KEYS = /* @__PURE__ */ new Set(["type", "token_env"]);
|
|
@@ -23382,7 +23716,7 @@ var CAPABILITY_KEYS = /* @__PURE__ */ new Set([
|
|
|
23382
23716
|
"contract_provenance"
|
|
23383
23717
|
]);
|
|
23384
23718
|
var CONTRACT_PROVENANCE_KEYS = /* @__PURE__ */ new Set(["digest", "version"]);
|
|
23385
|
-
var TARGET_KEYS = /* @__PURE__ */ new Set(["schema", "table", "primary_key", "tenant_key", "single_tenant_dev"]);
|
|
23719
|
+
var TARGET_KEYS = /* @__PURE__ */ new Set(["schema", "table", "primary_key", "tenant_key", "principal_scope_key", "single_tenant_dev"]);
|
|
23386
23720
|
var LOOKUP_KEYS = /* @__PURE__ */ new Set(["id_from_arg"]);
|
|
23387
23721
|
var ARG_KEYS = /* @__PURE__ */ new Set(["type", "description", "required", "max_length", "minimum", "maximum", "enum", "max_items", "fields"]);
|
|
23388
23722
|
var PATCH_BINDING_KEYS = /* @__PURE__ */ new Set(["fixed", "from_arg", "from_item"]);
|
|
@@ -23478,6 +23812,7 @@ function validateRunnerCapabilityConfig(input) {
|
|
|
23478
23812
|
validateApprovals(input.approvals, strict, errors);
|
|
23479
23813
|
const hasContracts = validateContracts(input.contracts, errors);
|
|
23480
23814
|
validateCloud(input.cloud, input.mode, strict, errors);
|
|
23815
|
+
validateGovernance(input.governance, strict, errors);
|
|
23481
23816
|
validateSources(input.sources, input.mode, strict, errors, warnings);
|
|
23482
23817
|
validateReceiptTopology(input.sources, input.storage, errors);
|
|
23483
23818
|
validateContexts(input.contexts, strict, errors, warnings);
|
|
@@ -23496,6 +23831,34 @@ function validateRunnerCapabilityConfig(input) {
|
|
|
23496
23831
|
scanForForbiddenFields(input, "$", errors);
|
|
23497
23832
|
return { ok: errors.length === 0, errors, warnings };
|
|
23498
23833
|
}
|
|
23834
|
+
function validateGovernance(value, strict, errors) {
|
|
23835
|
+
if (value === void 0) return;
|
|
23836
|
+
if (!isRecord(value)) {
|
|
23837
|
+
errors.push({ path: "$.governance", code: "GOVERNANCE_NOT_OBJECT", message: "governance must be an object." });
|
|
23838
|
+
return;
|
|
23839
|
+
}
|
|
23840
|
+
if (strict) checkUnknownKeys(value, GOVERNANCE_KEYS, "$.governance", errors);
|
|
23841
|
+
if (value.mode !== "local_only" && value.mode !== "cloud_linked") {
|
|
23842
|
+
errors.push({ path: "$.governance.mode", code: "INVALID_GOVERNANCE_MODE", message: "governance.mode must be local_only or cloud_linked." });
|
|
23843
|
+
}
|
|
23844
|
+
if (value.mode === "cloud_linked" && !isNonEmptyString(value.connection_file)) {
|
|
23845
|
+
errors.push({ path: "$.governance.connection_file", code: "CLOUD_LINKED_CONNECTION_REQUIRED", message: "cloud_linked governance requires a reviewed Cloud connection file." });
|
|
23846
|
+
}
|
|
23847
|
+
if (value.connection_file !== void 0 && !isNonEmptyString(value.connection_file)) {
|
|
23848
|
+
errors.push({ path: "$.governance.connection_file", code: "INVALID_GOVERNANCE_CONNECTION_FILE", message: "governance.connection_file must be a non-empty path." });
|
|
23849
|
+
}
|
|
23850
|
+
if (value.evidence_residency !== void 0 && value.evidence_residency !== "metadata_only") {
|
|
23851
|
+
errors.push({ path: "$.governance.evidence_residency", code: "UNSUPPORTED_EVIDENCE_RESIDENCY", message: "Only metadata_only Cloud evidence residency is supported; full evidence remains local." });
|
|
23852
|
+
}
|
|
23853
|
+
if (value.queue_when_unavailable !== void 0 && typeof value.queue_when_unavailable !== "boolean") {
|
|
23854
|
+
errors.push({ path: "$.governance.queue_when_unavailable", code: "INVALID_GOVERNANCE_QUEUE_POLICY", message: "queue_when_unavailable must be true or false." });
|
|
23855
|
+
}
|
|
23856
|
+
for (const [key, min, max] of [["sync_interval_ms", 250, 3e5], ["max_attempts", 1, 100], ["outbox_retention_days", 1, 3650]]) {
|
|
23857
|
+
if (value[key] !== void 0 && (!Number.isSafeInteger(value[key]) || Number(value[key]) < min || Number(value[key]) > max)) {
|
|
23858
|
+
errors.push({ path: `$.governance.${key}`, code: "INVALID_GOVERNANCE_LIMIT", message: `${key} must be an integer from ${min} through ${max}.` });
|
|
23859
|
+
}
|
|
23860
|
+
}
|
|
23861
|
+
}
|
|
23499
23862
|
function validateMetrics(value, strict, errors) {
|
|
23500
23863
|
if (value === void 0) return;
|
|
23501
23864
|
if (!isRecord(value)) {
|
|
@@ -24472,6 +24835,7 @@ function validateTarget(value, path9, strict, errors, warnings) {
|
|
|
24472
24835
|
}
|
|
24473
24836
|
}
|
|
24474
24837
|
const hasTenantKey = isSafeIdentifier(value.tenant_key);
|
|
24838
|
+
const hasPrincipalScopeKey = isSafeIdentifier(value.principal_scope_key);
|
|
24475
24839
|
const singleTenant = value.single_tenant_dev === true;
|
|
24476
24840
|
if (!hasTenantKey && !singleTenant) {
|
|
24477
24841
|
errors.push({
|
|
@@ -24480,6 +24844,12 @@ function validateTarget(value, path9, strict, errors, warnings) {
|
|
|
24480
24844
|
message: "tenant_key is required unless target.single_tenant_dev is explicitly true for a local dev example."
|
|
24481
24845
|
});
|
|
24482
24846
|
}
|
|
24847
|
+
if (value.principal_scope_key !== void 0 && !hasPrincipalScopeKey) {
|
|
24848
|
+
errors.push({ path: `${path9}.principal_scope_key`, code: "INVALID_PRINCIPAL_SCOPE_KEY", message: "principal_scope_key must be a fixed safe identifier." });
|
|
24849
|
+
}
|
|
24850
|
+
if (hasPrincipalScopeKey && !hasTenantKey) {
|
|
24851
|
+
errors.push({ path: `${path9}.principal_scope_key`, code: "PRINCIPAL_SCOPE_TENANT_REQUIRED", message: "principal_scope_key can only narrow a target that also declares tenant_key." });
|
|
24852
|
+
}
|
|
24483
24853
|
if (singleTenant) {
|
|
24484
24854
|
warnings.push({
|
|
24485
24855
|
path: `${path9}.single_tenant_dev`,
|
|
@@ -25334,6 +25704,7 @@ function assertCompensationJobIntegrity(job) {
|
|
|
25334
25704
|
if (descriptor.operation !== job.operation) throw new Error("COMPENSATION_OPERATION_MISMATCH");
|
|
25335
25705
|
if (descriptor.target.source_id !== job.source_id || descriptor.target.schema !== job.target.schema || descriptor.target.table !== job.target.table || descriptor.target.primary_key_column !== job.target.primary_key.column) throw new Error("COMPENSATION_TARGET_MISMATCH");
|
|
25336
25706
|
if (descriptor.tenant_guard.column !== job.target.tenant_guard.column || descriptor.tenant_guard.value !== job.target.tenant_guard.value) throw new Error("COMPENSATION_TENANT_MISMATCH");
|
|
25707
|
+
if (JSON.stringify(descriptor.principal_scope) !== JSON.stringify(job.target.principal_scope)) throw new Error("COMPENSATION_PRINCIPAL_SCOPE_MISMATCH");
|
|
25337
25708
|
if (descriptor.members.length < 1 || descriptor.members.length > descriptor.max_rows || descriptor.max_rows > 100) throw new Error("COMPENSATION_ROW_CAP_EXCEEDED");
|
|
25338
25709
|
const identities = descriptor.members.map((member) => JSON.stringify(member.primary_key.value));
|
|
25339
25710
|
if (new Set(identities).size !== identities.length) throw new Error("COMPENSATION_IDENTITY_NOT_UNIQUE");
|
|
@@ -25474,6 +25845,7 @@ function assertFrozenSetJobIntegrity(job) {
|
|
|
25474
25845
|
if (!job.version_advance || job.version_advance.strategy !== "integer_increment" || !member.expected_version || member.expected_version.column !== job.version_advance.column) throw new Error("SET_VERSION_GUARD_REQUIRED");
|
|
25475
25846
|
if (typeof member.expected_version.value !== "number" || member.before[job.version_advance.column] !== member.expected_version.value) throw new Error("SET_VERSION_GUARD_MISMATCH");
|
|
25476
25847
|
if (member.before[job.target.tenant_guard.column] !== job.target.tenant_guard.value) throw new Error("SET_TENANT_GUARD_MISMATCH");
|
|
25848
|
+
if (job.target.principal_scope && member.before[job.target.principal_scope.column] !== job.target.principal_scope.value) throw new Error("SET_PRINCIPAL_SCOPE_MISMATCH");
|
|
25477
25849
|
const expectedAfter = { ...member.before, ...job.patch, [job.version_advance.column]: member.expected_version.value + 1 };
|
|
25478
25850
|
if (!recordsEqual(member.after, expectedAfter)) throw new Error("SET_AFTER_STATE_MISMATCH");
|
|
25479
25851
|
if (!reviewedDigestMatches(member.before_digest, { primary_key: member.primary_key.value, before: member.before })) throw new Error("SET_BEFORE_DIGEST_MISMATCH");
|
|
@@ -25481,6 +25853,7 @@ function assertFrozenSetJobIntegrity(job) {
|
|
|
25481
25853
|
} else if (job.operation === "set_delete") {
|
|
25482
25854
|
if (!member.expected_version || member.before[member.expected_version.column] !== member.expected_version.value) throw new Error("SET_VERSION_GUARD_MISMATCH");
|
|
25483
25855
|
if (member.before[job.target.tenant_guard.column] !== job.target.tenant_guard.value) throw new Error("SET_TENANT_GUARD_MISMATCH");
|
|
25856
|
+
if (job.target.principal_scope && member.before[job.target.principal_scope.column] !== job.target.principal_scope.value) throw new Error("SET_PRINCIPAL_SCOPE_MISMATCH");
|
|
25484
25857
|
if (Object.keys(member.after).length !== 0) throw new Error("SET_DELETE_PATCH_FORBIDDEN");
|
|
25485
25858
|
if (!reviewedDigestMatches(member.before_digest, { primary_key: member.primary_key.value, before: member.before })) throw new Error("SET_BEFORE_DIGEST_MISMATCH");
|
|
25486
25859
|
if (!reviewedDigestMatches(member.tombstone_digest, { primary_key: member.primary_key.value, expected_version: member.expected_version })) throw new Error("SET_TOMBSTONE_DIGEST_MISMATCH");
|
|
@@ -25490,6 +25863,7 @@ function assertFrozenSetJobIntegrity(job) {
|
|
|
25490
25863
|
const tenant = components.find((component) => component.column === job.target.tenant_guard.column);
|
|
25491
25864
|
if (!primary || primary.value !== member.primary_key.value || !tenant || tenant.source !== "trusted_tenant" || tenant.value !== job.target.tenant_guard.value) throw new Error("BATCH_DEDUP_REQUIRED");
|
|
25492
25865
|
if (member.after[job.target.primary_key.column] !== member.primary_key.value || member.after[job.target.tenant_guard.column] !== job.target.tenant_guard.value) throw new Error("BATCH_IDENTITY_MISMATCH");
|
|
25866
|
+
if (job.target.principal_scope && member.after[job.target.principal_scope.column] !== job.target.principal_scope.value) throw new Error("BATCH_PRINCIPAL_SCOPE_MISMATCH");
|
|
25493
25867
|
if (!reviewedDigestMatches(member.after_digest, { primary_key: member.primary_key.value, after: member.after })) throw new Error("SET_AFTER_DIGEST_MISMATCH");
|
|
25494
25868
|
}
|
|
25495
25869
|
}
|
|
@@ -25548,7 +25922,7 @@ function loadConfig(env = process.env) {
|
|
|
25548
25922
|
runnerToken: requireEnv(env, "SYNAPSOR_RUNNER_TOKEN"),
|
|
25549
25923
|
runnerId: env.SYNAPSOR_RUNNER_ID || "synapsor-runner-local",
|
|
25550
25924
|
sourceId: requireEnv(env, "SYNAPSOR_SOURCE_ID"),
|
|
25551
|
-
databaseUrl: env.SYNAPSOR_DATABASE_URL || "",
|
|
25925
|
+
databaseUrl: env.SYNAPSOR_DATABASE_WRITE_URL || env.SYNAPSOR_DATABASE_URL || "",
|
|
25552
25926
|
engine,
|
|
25553
25927
|
pollIntervalMs: Number(env.SYNAPSOR_POLL_INTERVAL_MS || "5000"),
|
|
25554
25928
|
statementTimeoutMs: optionalPositiveInteger(env.SYNAPSOR_WRITEBACK_TIMEOUT_MS, "SYNAPSOR_WRITEBACK_TIMEOUT_MS"),
|
|
@@ -25592,7 +25966,7 @@ function createLogger(config = { logLevel: "info" }) {
|
|
|
25592
25966
|
}
|
|
25593
25967
|
}
|
|
25594
25968
|
async function doctorChecks(config, adapter) {
|
|
25595
|
-
const client = new ControlPlaneClient({ baseUrl: config.controlPlaneUrl, runnerToken: config.runnerToken, sourceId: config.sourceId });
|
|
25969
|
+
const client = new ControlPlaneClient({ baseUrl: config.controlPlaneUrl, runnerToken: config.runnerToken, sourceId: config.sourceId, runnerId: config.runnerId });
|
|
25596
25970
|
const [controlPlane, database] = await Promise.all([
|
|
25597
25971
|
client.doctor(),
|
|
25598
25972
|
adapter.doctor(config)
|
|
@@ -25608,38 +25982,86 @@ async function doctorChecks(config, adapter) {
|
|
|
25608
25982
|
database
|
|
25609
25983
|
};
|
|
25610
25984
|
}
|
|
25611
|
-
|
|
25985
|
+
function failedWritebackResult(job, runnerId, errorCode) {
|
|
25986
|
+
const common = {
|
|
25987
|
+
protocol_version: job.protocol_version,
|
|
25988
|
+
job_id: job.job_id,
|
|
25989
|
+
runner_id: runnerId,
|
|
25990
|
+
status: "failed",
|
|
25991
|
+
affected_rows: 0,
|
|
25992
|
+
completed_at: (/* @__PURE__ */ new Date()).toISOString(),
|
|
25993
|
+
error_code: errorCode
|
|
25994
|
+
};
|
|
25995
|
+
let result;
|
|
25996
|
+
if (job.protocol_version === protocolVersions.legacyWritebackJob) {
|
|
25997
|
+
result = common;
|
|
25998
|
+
} else if (job.protocol_version === protocolVersions.normalizedWritebackJobV2) {
|
|
25999
|
+
const primaryKey = job.target.primary_key;
|
|
26000
|
+
result = {
|
|
26001
|
+
...common,
|
|
26002
|
+
operation: job.operation,
|
|
26003
|
+
receipt_authority: "runner_ledger",
|
|
26004
|
+
target_identity: [{ column: primaryKey.column, value: primaryKey.value ?? job.patch[primaryKey.column] ?? null }]
|
|
26005
|
+
};
|
|
26006
|
+
} else if (job.protocol_version === protocolVersions.normalizedWritebackJobV3) {
|
|
26007
|
+
result = {
|
|
26008
|
+
...common,
|
|
26009
|
+
operation: job.operation,
|
|
26010
|
+
receipt_authority: "runner_ledger",
|
|
26011
|
+
target_identities: job.frozen_set.members.map((member) => member.primary_key),
|
|
26012
|
+
set_digest: job.frozen_set.set_digest,
|
|
26013
|
+
member_effects: []
|
|
26014
|
+
};
|
|
26015
|
+
} else {
|
|
26016
|
+
result = {
|
|
26017
|
+
...common,
|
|
26018
|
+
operation: job.operation,
|
|
26019
|
+
receipt_authority: "runner_ledger",
|
|
26020
|
+
target_identities: job.compensation.members.map((member) => member.primary_key),
|
|
26021
|
+
member_effects: []
|
|
26022
|
+
};
|
|
26023
|
+
}
|
|
26024
|
+
return { ...result, result_hash: canonicalJsonDigest(result) };
|
|
26025
|
+
}
|
|
26026
|
+
async function runOnce(config, adapters2, verifyAuthority, reportResult) {
|
|
25612
26027
|
const logger = createLogger(config);
|
|
25613
|
-
const client = new ControlPlaneClient({ baseUrl: config.controlPlaneUrl, runnerToken: config.runnerToken, sourceId: config.sourceId });
|
|
25614
|
-
const jobs = await client.claim({ sourceId: config.sourceId, limit: 1 });
|
|
26028
|
+
const client = new ControlPlaneClient({ baseUrl: config.controlPlaneUrl, runnerToken: config.runnerToken, sourceId: config.sourceId, runnerId: config.runnerId });
|
|
26029
|
+
const jobs = await client.claim({ sourceId: config.sourceId, runnerId: config.runnerId, limit: 1 });
|
|
25615
26030
|
if (jobs.length === 0) {
|
|
25616
26031
|
logger.info("no approved writeback jobs available", { source_id: config.sourceId });
|
|
25617
26032
|
return 0;
|
|
25618
26033
|
}
|
|
25619
26034
|
let completed = 0;
|
|
26035
|
+
const report = async (job, result) => {
|
|
26036
|
+
if (reportResult) await reportResult({ job, result, leaseId: job.cloud_lease.leaseId });
|
|
26037
|
+
else await client.result(result, job.cloud_lease.leaseId);
|
|
26038
|
+
};
|
|
25620
26039
|
for (const job of jobs) {
|
|
25621
26040
|
if (job.engine !== config.engine) {
|
|
25622
|
-
await
|
|
25623
|
-
protocol_version: "1.0",
|
|
25624
|
-
job_id: job.job_id,
|
|
25625
|
-
runner_id: config.runnerId,
|
|
25626
|
-
status: "failed",
|
|
25627
|
-
error_code: "DATABASE_UNAVAILABLE"
|
|
25628
|
-
});
|
|
26041
|
+
await report(job, failedWritebackResult(job, config.runnerId, "DATABASE_UNAVAILABLE"));
|
|
25629
26042
|
continue;
|
|
25630
26043
|
}
|
|
25631
|
-
|
|
26044
|
+
if (verifyAuthority) {
|
|
26045
|
+
try {
|
|
26046
|
+
await verifyAuthority(job);
|
|
26047
|
+
} catch {
|
|
26048
|
+
logger.warn("Cloud-approved job rejected by local reviewed authority", { job_id: job.job_id, error_code: "LOCAL_AUTHORITY_REJECTED" });
|
|
26049
|
+
await report(job, failedWritebackResult(job, config.runnerId, "LOCAL_AUTHORITY_REJECTED"));
|
|
26050
|
+
continue;
|
|
26051
|
+
}
|
|
26052
|
+
}
|
|
26053
|
+
await client.heartbeat(job.job_id, job.cloud_lease.leaseId, config.runnerId);
|
|
25632
26054
|
const result = await adapters2[config.engine].apply(job, config);
|
|
25633
|
-
await
|
|
26055
|
+
await report(job, result);
|
|
25634
26056
|
completed += 1;
|
|
25635
26057
|
}
|
|
25636
26058
|
return completed;
|
|
25637
26059
|
}
|
|
25638
|
-
async function startPolling(config, adapters2, signal) {
|
|
26060
|
+
async function startPolling(config, adapters2, signal, verifyAuthority, reportResult) {
|
|
25639
26061
|
const logger = createLogger(config);
|
|
25640
26062
|
while (!signal?.aborted) {
|
|
25641
26063
|
try {
|
|
25642
|
-
await runOnce(config, adapters2);
|
|
26064
|
+
await runOnce(config, adapters2, verifyAuthority, reportResult);
|
|
25643
26065
|
} catch (error) {
|
|
25644
26066
|
logger.error("runner loop failed", { error: error instanceof Error ? error.message : String(error) });
|
|
25645
26067
|
}
|
|
@@ -25679,6 +26101,12 @@ function quotePostgresIdentifier(identifier) {
|
|
|
25679
26101
|
function operationOf(job) {
|
|
25680
26102
|
return job.operation ?? "single_row_update";
|
|
25681
26103
|
}
|
|
26104
|
+
function principalScope(job) {
|
|
26105
|
+
const scope = job.target.principal_scope;
|
|
26106
|
+
if (!scope) return void 0;
|
|
26107
|
+
if (scope.value === void 0) throw new Error("PRINCIPAL_SCOPE_VALUE_REQUIRED");
|
|
26108
|
+
return { column: scope.column, value: scope.value };
|
|
26109
|
+
}
|
|
25682
26110
|
function receiptAuthority(config) {
|
|
25683
26111
|
return config.receipts?.authority ?? "source_db";
|
|
25684
26112
|
}
|
|
@@ -25710,6 +26138,11 @@ function buildPostgresUpdate(job) {
|
|
|
25710
26138
|
`${quotePostgresIdentifier(job.target.primary_key.column)} = ${pkParam}`,
|
|
25711
26139
|
`${quotePostgresIdentifier(job.target.tenant_guard.column)} = ${tenantParam}`
|
|
25712
26140
|
];
|
|
26141
|
+
const scope = principalScope(job);
|
|
26142
|
+
if (scope) {
|
|
26143
|
+
values.push(scope.value);
|
|
26144
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26145
|
+
}
|
|
25713
26146
|
if (job.conflict_guard.kind === "version_column") {
|
|
25714
26147
|
values.push(job.conflict_guard.expected_value);
|
|
25715
26148
|
where.push(`${quotePostgresIdentifier(job.conflict_guard.column)} = $${values.length}`);
|
|
@@ -25742,13 +26175,20 @@ function buildPostgresDelete(job) {
|
|
|
25742
26175
|
if (job.target.primary_key.value === void 0 || job.conflict_guard.kind !== "version_column") {
|
|
25743
26176
|
throw new Error("postgres delete requires primary-key and exact version guards");
|
|
25744
26177
|
}
|
|
25745
|
-
|
|
25746
|
-
|
|
25747
|
-
|
|
25748
|
-
|
|
25749
|
-
|
|
25750
|
-
|
|
25751
|
-
|
|
26178
|
+
const values = [job.target.primary_key.value, job.target.tenant_guard.value];
|
|
26179
|
+
const where = [
|
|
26180
|
+
`${quotePostgresIdentifier(job.target.primary_key.column)} = $1`,
|
|
26181
|
+
`${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2`
|
|
26182
|
+
];
|
|
26183
|
+
const scope = principalScope(job);
|
|
26184
|
+
if (scope) {
|
|
26185
|
+
values.push(scope.value);
|
|
26186
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26187
|
+
}
|
|
26188
|
+
values.push(job.conflict_guard.expected_value);
|
|
26189
|
+
where.push(`${quotePostgresIdentifier(job.conflict_guard.column)} = $${values.length}`);
|
|
26190
|
+
return { sql: `DELETE FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)}
|
|
26191
|
+
WHERE ${where.join("\n AND ")}`, values };
|
|
25752
26192
|
}
|
|
25753
26193
|
function validatePatch(job, engine) {
|
|
25754
26194
|
const patchColumns = Object.keys(job.patch || {});
|
|
@@ -25756,6 +26196,8 @@ function validatePatch(job, engine) {
|
|
|
25756
26196
|
const allowed = new Set(job.allowed_columns);
|
|
25757
26197
|
if (allowed.has(job.target.primary_key.column)) throw new Error(`${engine} primary key column must not be patch-allowlisted`);
|
|
25758
26198
|
if (allowed.has(job.target.tenant_guard.column)) throw new Error(`${engine} tenant guard column must not be patch-allowlisted`);
|
|
26199
|
+
const scope = principalScope(job);
|
|
26200
|
+
if (scope && allowed.has(scope.column)) throw new Error(`${engine} principal scope column must not be patch-allowlisted`);
|
|
25759
26201
|
for (const column of patchColumns) if (!allowed.has(column)) throw new Error(`${engine} patch column not allowlisted: ${column}`);
|
|
25760
26202
|
}
|
|
25761
26203
|
function insertValues(job) {
|
|
@@ -25770,6 +26212,11 @@ function insertValues(job) {
|
|
|
25770
26212
|
if (component.source === "proposal_id") proposalIdentity = true;
|
|
25771
26213
|
}
|
|
25772
26214
|
if (!trustedTenant || !proposalIdentity) throw new Error("INSERT_DEDUP_REQUIRED");
|
|
26215
|
+
const scope = principalScope(job);
|
|
26216
|
+
if (scope) {
|
|
26217
|
+
if (Object.prototype.hasOwnProperty.call(values, scope.column)) throw new Error("PRINCIPAL_SCOPE_COLUMN_COLLISION");
|
|
26218
|
+
values[scope.column] = scope.value;
|
|
26219
|
+
}
|
|
25773
26220
|
return values;
|
|
25774
26221
|
}
|
|
25775
26222
|
function identityForJob(job, insertedPrimaryKey) {
|
|
@@ -25794,6 +26241,8 @@ function digest(value) {
|
|
|
25794
26241
|
}
|
|
25795
26242
|
function reconciliationProjection(job) {
|
|
25796
26243
|
const columns = /* @__PURE__ */ new Set([job.target.primary_key.column, job.target.tenant_guard.column, ...job.allowed_columns]);
|
|
26244
|
+
const scope = principalScope(job);
|
|
26245
|
+
if (scope) columns.add(scope.column);
|
|
25797
26246
|
if (job.protocol_version === "3.0") {
|
|
25798
26247
|
for (const member of job.frozen_set.members) {
|
|
25799
26248
|
for (const column of Object.keys(member.before)) columns.add(column);
|
|
@@ -25815,41 +26264,70 @@ function reconciliationProjection(job) {
|
|
|
25815
26264
|
function buildPostgresReconciliationRead(job) {
|
|
25816
26265
|
const projection = reconciliationProjection(job).map(quotePostgresIdentifier).join(", ");
|
|
25817
26266
|
if (job.protocol_version === "4.0") {
|
|
25818
|
-
const
|
|
26267
|
+
const values2 = [job.target.tenant_guard.value];
|
|
26268
|
+
const where2 = [`${quotePostgresIdentifier(job.target.tenant_guard.column)} = $1`];
|
|
26269
|
+
const scope2 = principalScope(job);
|
|
26270
|
+
if (scope2) {
|
|
26271
|
+
values2.push(scope2.value);
|
|
26272
|
+
where2.push(`${quotePostgresIdentifier(scope2.column)} = $${values2.length}`);
|
|
26273
|
+
}
|
|
26274
|
+
const firstIdentity = values2.length + 1;
|
|
26275
|
+
values2.push(...job.compensation.members.map((member) => member.primary_key.value));
|
|
25819
26276
|
return {
|
|
25820
26277
|
sql: `SELECT ${projection} FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)}
|
|
25821
|
-
WHERE ${
|
|
25822
|
-
AND ${quotePostgresIdentifier(job.target.primary_key.column)} IN (${job.compensation.members.map((_, index) => `$${
|
|
26278
|
+
WHERE ${where2.join("\n AND ")}
|
|
26279
|
+
AND ${quotePostgresIdentifier(job.target.primary_key.column)} IN (${job.compensation.members.map((_, index) => `$${firstIdentity + index}`).join(", ")})
|
|
25823
26280
|
ORDER BY ${quotePostgresIdentifier(job.target.primary_key.column)} ASC`,
|
|
25824
|
-
values
|
|
26281
|
+
values: values2
|
|
25825
26282
|
};
|
|
25826
26283
|
}
|
|
25827
26284
|
if (job.protocol_version === "3.0") {
|
|
25828
|
-
const
|
|
25829
|
-
const
|
|
26285
|
+
const values2 = [job.target.tenant_guard.value];
|
|
26286
|
+
const where2 = [`${quotePostgresIdentifier(job.target.tenant_guard.column)} = $1`];
|
|
26287
|
+
const scope2 = principalScope(job);
|
|
26288
|
+
if (scope2) {
|
|
26289
|
+
values2.push(scope2.value);
|
|
26290
|
+
where2.push(`${quotePostgresIdentifier(scope2.column)} = $${values2.length}`);
|
|
26291
|
+
}
|
|
26292
|
+
const firstIdentity = values2.length + 1;
|
|
26293
|
+
values2.push(...job.frozen_set.members.map((member) => member.primary_key.value));
|
|
26294
|
+
const identities = job.frozen_set.members.map((_, index) => `$${firstIdentity + index}`).join(", ");
|
|
25830
26295
|
return {
|
|
25831
26296
|
sql: `SELECT ${projection} FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)}
|
|
25832
|
-
WHERE ${
|
|
26297
|
+
WHERE ${where2.join("\n AND ")}
|
|
25833
26298
|
AND ${quotePostgresIdentifier(job.target.primary_key.column)} IN (${identities})
|
|
25834
26299
|
ORDER BY ${quotePostgresIdentifier(job.target.primary_key.column)} ASC`,
|
|
25835
|
-
values
|
|
26300
|
+
values: values2
|
|
25836
26301
|
};
|
|
25837
26302
|
}
|
|
25838
26303
|
if (operationOf(job) === "single_row_insert" && job.protocol_version === "2.0" && job.deduplication) {
|
|
26304
|
+
const values2 = job.deduplication.components.map((component) => component.value);
|
|
26305
|
+
const where2 = job.deduplication.components.map((component, index) => `${quotePostgresIdentifier(component.column)} = $${index + 1}`);
|
|
26306
|
+
const scope2 = principalScope(job);
|
|
26307
|
+
if (scope2) {
|
|
26308
|
+
values2.push(scope2.value);
|
|
26309
|
+
where2.push(`${quotePostgresIdentifier(scope2.column)} = $${values2.length}`);
|
|
26310
|
+
}
|
|
25839
26311
|
return {
|
|
25840
26312
|
sql: `SELECT ${projection} FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)}
|
|
25841
|
-
WHERE ${
|
|
26313
|
+
WHERE ${where2.join(" AND ")}
|
|
25842
26314
|
LIMIT 2`,
|
|
25843
|
-
values:
|
|
26315
|
+
values: values2
|
|
25844
26316
|
};
|
|
25845
26317
|
}
|
|
25846
26318
|
if (job.target.primary_key.value === void 0) throw new Error("RECONCILIATION_TARGET_IDENTITY_REQUIRED");
|
|
26319
|
+
const values = [job.target.primary_key.value, job.target.tenant_guard.value];
|
|
26320
|
+
const where = [`${quotePostgresIdentifier(job.target.primary_key.column)} = $1`, `${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2`];
|
|
26321
|
+
const scope = principalScope(job);
|
|
26322
|
+
if (scope) {
|
|
26323
|
+
values.push(scope.value);
|
|
26324
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26325
|
+
}
|
|
25847
26326
|
return {
|
|
25848
26327
|
sql: `SELECT ${projection} FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)}
|
|
25849
|
-
WHERE ${
|
|
25850
|
-
AND ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2
|
|
26328
|
+
WHERE ${where.join("\n AND ")}
|
|
25851
26329
|
LIMIT 2`,
|
|
25852
|
-
values
|
|
26330
|
+
values
|
|
25853
26331
|
};
|
|
25854
26332
|
}
|
|
25855
26333
|
async function inspectPostgresWritebackSource(job, databaseUrl) {
|
|
@@ -26109,6 +26587,8 @@ async function mutatePostgres(job, client) {
|
|
|
26109
26587
|
}
|
|
26110
26588
|
function compensationProjection(job) {
|
|
26111
26589
|
const columns = /* @__PURE__ */ new Set([job.target.primary_key.column, job.target.tenant_guard.column]);
|
|
26590
|
+
const scope = principalScope(job);
|
|
26591
|
+
if (scope) columns.add(scope.column);
|
|
26112
26592
|
for (const member of job.compensation.members) {
|
|
26113
26593
|
for (const column of Object.keys(member.expected_state)) columns.add(column);
|
|
26114
26594
|
for (const column of Object.keys(member.restore_values ?? {})) columns.add(column);
|
|
@@ -26117,9 +26597,18 @@ function compensationProjection(job) {
|
|
|
26117
26597
|
return [...columns].sort();
|
|
26118
26598
|
}
|
|
26119
26599
|
async function lockPostgresCompensationMembers(job, client) {
|
|
26600
|
+
const values = [job.target.tenant_guard.value];
|
|
26601
|
+
const where = [`${quotePostgresIdentifier(job.target.tenant_guard.column)} = $1`];
|
|
26602
|
+
const scope = principalScope(job);
|
|
26603
|
+
if (scope) {
|
|
26604
|
+
values.push(scope.value);
|
|
26605
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26606
|
+
}
|
|
26607
|
+
const firstIdentity = values.length + 1;
|
|
26608
|
+
values.push(...job.compensation.members.map((member) => member.primary_key.value));
|
|
26120
26609
|
const result = await client.query(
|
|
26121
|
-
`SELECT ${compensationProjection(job).map(quotePostgresIdentifier).join(", ")} FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} WHERE ${
|
|
26122
|
-
|
|
26610
|
+
`SELECT ${compensationProjection(job).map(quotePostgresIdentifier).join(", ")} FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} WHERE ${where.join(" AND ")} AND ${quotePostgresIdentifier(job.target.primary_key.column)} IN (${job.compensation.members.map((_, index) => `$${firstIdentity + index}`).join(", ")}) ORDER BY ${quotePostgresIdentifier(job.target.primary_key.column)} ASC FOR UPDATE`,
|
|
26611
|
+
values
|
|
26123
26612
|
);
|
|
26124
26613
|
return result.rows;
|
|
26125
26614
|
}
|
|
@@ -26163,6 +26652,8 @@ async function mutatePostgresCompensation(job, client) {
|
|
|
26163
26652
|
if (job.operation === "restore_insert") {
|
|
26164
26653
|
for (const member of job.compensation.members) {
|
|
26165
26654
|
const values = { ...member.restore_values, [job.target.primary_key.column]: member.primary_key.value, [job.target.tenant_guard.column]: job.target.tenant_guard.value };
|
|
26655
|
+
const scope = principalScope(job);
|
|
26656
|
+
if (scope) values[scope.column] = scope.value;
|
|
26166
26657
|
const columns = Object.keys(values).sort();
|
|
26167
26658
|
const inserted = await client.query(
|
|
26168
26659
|
`INSERT INTO ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} (${columns.map(quotePostgresIdentifier).join(", ")}) VALUES (${columns.map((_, index) => `$${index + 1}`).join(", ")})`,
|
|
@@ -26178,6 +26669,11 @@ async function mutatePostgresCompensation(job, client) {
|
|
|
26178
26669
|
`${quotePostgresIdentifier(job.target.tenant_guard.column)} = $1`,
|
|
26179
26670
|
`${quotePostgresIdentifier(job.target.primary_key.column)} = $2`
|
|
26180
26671
|
];
|
|
26672
|
+
const scope = principalScope(job);
|
|
26673
|
+
if (scope) {
|
|
26674
|
+
values.push(scope.value);
|
|
26675
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26676
|
+
}
|
|
26181
26677
|
for (const [column, value] of Object.entries(member.expected_state)) {
|
|
26182
26678
|
values.push(value);
|
|
26183
26679
|
where.push(`${quotePostgresIdentifier(column)} IS NOT DISTINCT FROM $${values.length}`);
|
|
@@ -26200,6 +26696,11 @@ async function mutatePostgresCompensation(job, client) {
|
|
|
26200
26696
|
const where = [`${quotePostgresIdentifier(job.target.tenant_guard.column)} = $${values.length}`];
|
|
26201
26697
|
values.push(member.primary_key.value);
|
|
26202
26698
|
where.push(`${quotePostgresIdentifier(job.target.primary_key.column)} = $${values.length}`);
|
|
26699
|
+
const scope = principalScope(job);
|
|
26700
|
+
if (scope) {
|
|
26701
|
+
values.push(scope.value);
|
|
26702
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26703
|
+
}
|
|
26203
26704
|
for (const [column, value] of Object.entries(member.expected_state)) {
|
|
26204
26705
|
values.push(value);
|
|
26205
26706
|
where.push(`${quotePostgresIdentifier(column)} IS NOT DISTINCT FROM $${values.length}`);
|
|
@@ -26254,18 +26755,35 @@ async function mutatePostgresSet(job, client) {
|
|
|
26254
26755
|
return `${quotePostgresIdentifier(column)} = $${values.length}`;
|
|
26255
26756
|
});
|
|
26256
26757
|
assignments.push(`${quotePostgresIdentifier(job.version_advance.column)} = ${quotePostgresIdentifier(job.version_advance.column)} + 1`);
|
|
26257
|
-
values.push(member.primary_key.value, job.target.tenant_guard.value
|
|
26758
|
+
values.push(member.primary_key.value, job.target.tenant_guard.value);
|
|
26759
|
+
const where = [
|
|
26760
|
+
`${quotePostgresIdentifier(job.target.primary_key.column)} = $${values.length - 1}`,
|
|
26761
|
+
`${quotePostgresIdentifier(job.target.tenant_guard.column)} = $${values.length}`
|
|
26762
|
+
];
|
|
26763
|
+
const scope = principalScope(job);
|
|
26764
|
+
if (scope) {
|
|
26765
|
+
values.push(scope.value);
|
|
26766
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26767
|
+
}
|
|
26768
|
+
values.push(expected.value);
|
|
26769
|
+
where.push(`${quotePostgresIdentifier(expected.column)} = $${values.length}`);
|
|
26258
26770
|
const updated = await client.query(
|
|
26259
|
-
`UPDATE ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} SET ${assignments.join(", ")} WHERE ${
|
|
26771
|
+
`UPDATE ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} SET ${assignments.join(", ")} WHERE ${where.join(" AND ")}`,
|
|
26260
26772
|
values
|
|
26261
26773
|
);
|
|
26262
26774
|
if (updated.rowCount !== 1) throw new Error("SET_ATOMICITY_VIOLATION");
|
|
26263
26775
|
memberEffects.push({ primary_key: member.primary_key, before_digest: member.before_digest, after_digest: member.after_digest });
|
|
26264
26776
|
} else {
|
|
26265
|
-
const
|
|
26266
|
-
|
|
26267
|
-
|
|
26268
|
-
)
|
|
26777
|
+
const values = [member.primary_key.value, job.target.tenant_guard.value];
|
|
26778
|
+
const where = [`${quotePostgresIdentifier(job.target.primary_key.column)} = $1`, `${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2`];
|
|
26779
|
+
const scope = principalScope(job);
|
|
26780
|
+
if (scope) {
|
|
26781
|
+
values.push(scope.value);
|
|
26782
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26783
|
+
}
|
|
26784
|
+
values.push(expected.value);
|
|
26785
|
+
where.push(`${quotePostgresIdentifier(expected.column)} = $${values.length}`);
|
|
26786
|
+
const deleted = await client.query(`DELETE FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} WHERE ${where.join(" AND ")}`, values);
|
|
26269
26787
|
if (deleted.rowCount !== 1) throw new Error("SET_ATOMICITY_VIOLATION");
|
|
26270
26788
|
memberEffects.push({ primary_key: member.primary_key, before_digest: member.before_digest, tombstone_digest: member.tombstone_digest });
|
|
26271
26789
|
}
|
|
@@ -26275,10 +26793,19 @@ async function mutatePostgresSet(job, client) {
|
|
|
26275
26793
|
async function lockPostgresFrozenMembers(job, client) {
|
|
26276
26794
|
const columns = /* @__PURE__ */ new Set([job.target.primary_key.column]);
|
|
26277
26795
|
for (const member of job.frozen_set.members) for (const column of Object.keys(member.before)) columns.add(column);
|
|
26278
|
-
const values = [job.target.tenant_guard.value
|
|
26279
|
-
const
|
|
26796
|
+
const values = [job.target.tenant_guard.value];
|
|
26797
|
+
const where = [`${quotePostgresIdentifier(job.target.tenant_guard.column)} = $1`];
|
|
26798
|
+
const scope = principalScope(job);
|
|
26799
|
+
if (scope) {
|
|
26800
|
+
columns.add(scope.column);
|
|
26801
|
+
values.push(scope.value);
|
|
26802
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26803
|
+
}
|
|
26804
|
+
const firstIdentity = values.length + 1;
|
|
26805
|
+
values.push(...job.frozen_set.members.map((member) => member.primary_key.value));
|
|
26806
|
+
const placeholders = job.frozen_set.members.map((_, index) => `$${firstIdentity + index}`);
|
|
26280
26807
|
const result = await client.query(
|
|
26281
|
-
`SELECT ${[...columns].map(quotePostgresIdentifier).join(", ")} FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} WHERE ${
|
|
26808
|
+
`SELECT ${[...columns].map(quotePostgresIdentifier).join(", ")} FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} WHERE ${where.join(" AND ")} AND ${quotePostgresIdentifier(job.target.primary_key.column)} IN (${placeholders.join(", ")}) ORDER BY ${quotePostgresIdentifier(job.target.primary_key.column)} ASC FOR UPDATE`,
|
|
26282
26809
|
values
|
|
26283
26810
|
);
|
|
26284
26811
|
if (result.rowCount !== job.frozen_set.row_count) return false;
|
|
@@ -26300,16 +26827,23 @@ async function postgresDeleteSafetyCode(job, client) {
|
|
|
26300
26827
|
function validateBatchInsertMember(job, member) {
|
|
26301
26828
|
const dedupColumns = new Set(member.deduplication?.components.map((component) => component.column));
|
|
26302
26829
|
for (const column of Object.keys(member.after)) {
|
|
26303
|
-
if (!job.allowed_columns.includes(column) && !dedupColumns.has(column)) throw new Error("BATCH_COLUMN_NOT_ALLOWED");
|
|
26830
|
+
if (!job.allowed_columns.includes(column) && !dedupColumns.has(column) && column !== job.target.principal_scope?.column) throw new Error("BATCH_COLUMN_NOT_ALLOWED");
|
|
26304
26831
|
}
|
|
26305
26832
|
if (!dedupColumns.has(job.target.primary_key.column) || !dedupColumns.has(job.target.tenant_guard.column)) throw new Error("BATCH_DEDUP_REQUIRED");
|
|
26306
26833
|
}
|
|
26307
26834
|
async function insertPostgresBatch(job, client) {
|
|
26308
26835
|
for (const member of job.frozen_set.members) {
|
|
26309
26836
|
const components = member.deduplication.components;
|
|
26837
|
+
const values = components.map((component) => component.value);
|
|
26838
|
+
const where = components.map((component, index) => `${quotePostgresIdentifier(component.column)} = $${index + 1}`);
|
|
26839
|
+
const scope = principalScope(job);
|
|
26840
|
+
if (scope) {
|
|
26841
|
+
values.push(scope.value);
|
|
26842
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26843
|
+
}
|
|
26310
26844
|
const existing = await client.query(
|
|
26311
|
-
`SELECT 1 FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} WHERE ${
|
|
26312
|
-
|
|
26845
|
+
`SELECT 1 FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} WHERE ${where.join(" AND ")} LIMIT 1 FOR UPDATE`,
|
|
26846
|
+
values
|
|
26313
26847
|
);
|
|
26314
26848
|
if (existing.rowCount) return { status: "conflict", affectedRows: 0, code: "INSERT_DEDUP_CONFLICT", targetIdentity: identityForJob(job) };
|
|
26315
26849
|
}
|
|
@@ -26333,12 +26867,18 @@ async function lockTargetRow(job, client) {
|
|
|
26333
26867
|
...job.conflict_guard.kind === "version_column" ? [`${quotePostgresIdentifier(job.conflict_guard.column)}::text AS "__synapsor_conflict_value"`] : [],
|
|
26334
26868
|
...inverseColumns.filter((column) => job.conflict_guard.kind !== "version_column" || column !== job.conflict_guard.column).map(quotePostgresIdentifier)
|
|
26335
26869
|
].join(", ");
|
|
26870
|
+
const values = [job.target.primary_key.value, job.target.tenant_guard.value];
|
|
26871
|
+
const where = [`${quotePostgresIdentifier(job.target.primary_key.column)} = $1`, `${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2`];
|
|
26872
|
+
const scope = principalScope(job);
|
|
26873
|
+
if (scope) {
|
|
26874
|
+
values.push(scope.value);
|
|
26875
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${values.length}`);
|
|
26876
|
+
}
|
|
26336
26877
|
const result = await client.query(
|
|
26337
26878
|
`SELECT ${projection} FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)}
|
|
26338
|
-
WHERE ${
|
|
26339
|
-
AND ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2
|
|
26879
|
+
WHERE ${where.join("\n AND ")}
|
|
26340
26880
|
FOR UPDATE`,
|
|
26341
|
-
|
|
26881
|
+
values
|
|
26342
26882
|
);
|
|
26343
26883
|
return result.rowCount ? result.rows[0] : void 0;
|
|
26344
26884
|
}
|
|
@@ -26346,8 +26886,14 @@ async function insertPostgres(job, client) {
|
|
|
26346
26886
|
if (job.protocol_version !== "2.0" || !job.deduplication) throw new Error("INSERT_DEDUP_REQUIRED");
|
|
26347
26887
|
const components = job.deduplication.components;
|
|
26348
26888
|
const whereValues = components.map((component) => component.value);
|
|
26889
|
+
const where = components.map((component, index) => `${quotePostgresIdentifier(component.column)} = $${index + 1}`);
|
|
26890
|
+
const scope = principalScope(job);
|
|
26891
|
+
if (scope) {
|
|
26892
|
+
whereValues.push(scope.value);
|
|
26893
|
+
where.push(`${quotePostgresIdentifier(scope.column)} = $${whereValues.length}`);
|
|
26894
|
+
}
|
|
26349
26895
|
const existing = await client.query(
|
|
26350
|
-
`SELECT ${quotePostgresIdentifier(job.target.primary_key.column)}::text AS "__synapsor_primary_key" FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} WHERE ${
|
|
26896
|
+
`SELECT ${quotePostgresIdentifier(job.target.primary_key.column)}::text AS "__synapsor_primary_key" FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} WHERE ${where.join(" AND ")}`,
|
|
26351
26897
|
whereValues
|
|
26352
26898
|
);
|
|
26353
26899
|
if (existing.rowCount) return { status: "conflict", affectedRows: 0, code: "INSERT_DEDUP_CONFLICT", targetIdentity: identityForJob(job, existing.rows[0]?.__synapsor_primary_key) };
|
|
@@ -26395,15 +26941,18 @@ function resultFromOutcome(job, config, outcome, overrideCode, overrideHash) {
|
|
|
26395
26941
|
const operation = operationOf(job);
|
|
26396
26942
|
const hash = overrideHash ?? resultHash(job, outcome.status, outcome.resultVersion);
|
|
26397
26943
|
if (job.protocol_version === "4.0") {
|
|
26398
|
-
return { protocol_version: "4.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identities: identityForJob(job), member_effects: outcome.memberEffects ?? [], ...outcome.status === "applied" || outcome.status === "already_applied" ? { inverse: outcome.inverse ?? compensationInverseFromJob(job) } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26944
|
+
return wireResult({ protocol_version: "4.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identities: identityForJob(job), member_effects: outcome.memberEffects ?? [], ...outcome.status === "applied" || outcome.status === "already_applied" ? { inverse: outcome.inverse ?? compensationInverseFromJob(job) } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
26399
26945
|
}
|
|
26400
26946
|
if (job.protocol_version === "3.0") {
|
|
26401
|
-
return { protocol_version: "3.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identities: identityForJob(job), set_digest: job.frozen_set.set_digest, member_effects: outcome.memberEffects ?? [], ...job.inverse_capture && (outcome.status === "applied" || outcome.status === "already_applied") ? { inverse: job.inverse_capture } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26947
|
+
return wireResult({ protocol_version: "3.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identities: identityForJob(job), set_digest: job.frozen_set.set_digest, member_effects: outcome.memberEffects ?? [], ...job.inverse_capture && (outcome.status === "applied" || outcome.status === "already_applied") ? { inverse: job.inverse_capture } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
26402
26948
|
}
|
|
26403
26949
|
if (job.protocol_version !== "2.0") {
|
|
26404
|
-
return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: outcome.status, affected_rows: outcome.affectedRows, result_version: outcome.resultVersion == null ? void 0 : String(outcome.resultVersion), result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26950
|
+
return wireResult({ protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: outcome.status, affected_rows: outcome.affectedRows, result_version: outcome.resultVersion == null ? void 0 : String(outcome.resultVersion), result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
26405
26951
|
}
|
|
26406
|
-
return { protocol_version: "2.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identity: outcome.targetIdentity, result_version: outcome.resultVersion, before_digest: outcome.beforeDigest, after_digest: outcome.afterDigest, tombstone_digest: outcome.tombstoneDigest, ...job.inverse_capture && (outcome.status === "applied" || outcome.status === "already_applied") ? { inverse: job.inverse_capture } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
26952
|
+
return wireResult({ protocol_version: "2.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identity: outcome.targetIdentity, result_version: outcome.resultVersion, before_digest: outcome.beforeDigest, after_digest: outcome.afterDigest, tombstone_digest: outcome.tombstoneDigest, ...job.inverse_capture && (outcome.status === "applied" || outcome.status === "already_applied") ? { inverse: job.inverse_capture } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
26953
|
+
}
|
|
26954
|
+
function wireResult(result) {
|
|
26955
|
+
return Object.fromEntries(Object.entries(result).filter(([, value]) => value !== void 0));
|
|
26407
26956
|
}
|
|
26408
26957
|
function failedResult(job, config, code) {
|
|
26409
26958
|
return resultFromOutcome(job, config, { status: "failed", affectedRows: 0, code, targetIdentity: identityForJob(job) });
|
|
@@ -26465,6 +27014,7 @@ var postgresAdapter = {
|
|
|
26465
27014
|
import { DatabaseSync } from "node:sqlite";
|
|
26466
27015
|
import { chmodSync, mkdirSync } from "node:fs";
|
|
26467
27016
|
import { dirname, resolve } from "node:path";
|
|
27017
|
+
var SQLITE_BUSY_TIMEOUT_MS = 5e3;
|
|
26468
27018
|
var PostgresWritebackIntentStore = class {
|
|
26469
27019
|
pool;
|
|
26470
27020
|
schema;
|
|
@@ -26675,6 +27225,12 @@ var PostgresProposalRuntimeStore = class {
|
|
|
26675
27225
|
async getEvidenceBundle(evidenceBundleId) {
|
|
26676
27226
|
return await this.withRead((store) => store.getEvidenceBundle(evidenceBundleId));
|
|
26677
27227
|
}
|
|
27228
|
+
async listEvidenceBundles(filters = {}) {
|
|
27229
|
+
return await this.withRead((store) => store.listEvidenceBundles(filters));
|
|
27230
|
+
}
|
|
27231
|
+
async listQueryAudit(filters = {}) {
|
|
27232
|
+
return await this.withRead((store) => store.listQueryAudit(filters));
|
|
27233
|
+
}
|
|
26678
27234
|
async replay(proposalId) {
|
|
26679
27235
|
return await this.withWrite("replay", (store) => store.replay(proposalId));
|
|
26680
27236
|
}
|
|
@@ -26690,6 +27246,33 @@ var PostgresProposalRuntimeStore = class {
|
|
|
26690
27246
|
async requireWritebackReconciliation(intentId, reason) {
|
|
26691
27247
|
await this.withWrite("requireWritebackReconciliation", (store) => store.requireWritebackReconciliation(intentId, reason));
|
|
26692
27248
|
}
|
|
27249
|
+
async enqueueCloudOutbox(input) {
|
|
27250
|
+
return await this.withWrite("enqueueCloudOutbox", (store) => store.enqueueCloudOutbox(input));
|
|
27251
|
+
}
|
|
27252
|
+
async claimCloudOutbox(input) {
|
|
27253
|
+
return await this.withWrite("claimCloudOutbox", (store) => store.claimCloudOutbox(input));
|
|
27254
|
+
}
|
|
27255
|
+
async acknowledgeCloudOutbox(eventId, owner, now) {
|
|
27256
|
+
return await this.withWrite("acknowledgeCloudOutbox", (store) => store.acknowledgeCloudOutbox(eventId, owner, now));
|
|
27257
|
+
}
|
|
27258
|
+
async failCloudOutbox(input) {
|
|
27259
|
+
return await this.withWrite("failCloudOutbox", (store) => store.failCloudOutbox(input));
|
|
27260
|
+
}
|
|
27261
|
+
async requeueCloudOutbox(eventId, now) {
|
|
27262
|
+
return await this.withWrite("requeueCloudOutbox", (store) => store.requeueCloudOutbox(eventId, now));
|
|
27263
|
+
}
|
|
27264
|
+
async listCloudOutbox(filters = {}) {
|
|
27265
|
+
return await this.withRead((store) => store.listCloudOutbox(filters));
|
|
27266
|
+
}
|
|
27267
|
+
async compactCloudOutbox(input) {
|
|
27268
|
+
return await this.withWrite("compactCloudOutbox", (store) => store.compactCloudOutbox(input));
|
|
27269
|
+
}
|
|
27270
|
+
async recordCloudGovernanceEvent(input) {
|
|
27271
|
+
return await this.withWrite("recordCloudGovernanceEvent", (store) => store.recordCloudGovernanceEvent(input));
|
|
27272
|
+
}
|
|
27273
|
+
async listCloudGovernanceEvents(proposalId) {
|
|
27274
|
+
return await this.withRead((store) => store.listCloudGovernanceEvents(proposalId));
|
|
27275
|
+
}
|
|
26693
27276
|
async withRead(callback) {
|
|
26694
27277
|
const store = await this.transientStoreFromPostgres(this.pool);
|
|
26695
27278
|
try {
|
|
@@ -26885,6 +27468,7 @@ var ProposalStore = class {
|
|
|
26885
27468
|
mkdirSync(dirname(resolve(path9)), { recursive: true, mode: 448 });
|
|
26886
27469
|
}
|
|
26887
27470
|
this.db = new DatabaseSync(path9);
|
|
27471
|
+
this.db.exec(`PRAGMA busy_timeout = ${SQLITE_BUSY_TIMEOUT_MS}`);
|
|
26888
27472
|
if (path9 !== ":memory:" && process.platform !== "win32") {
|
|
26889
27473
|
try {
|
|
26890
27474
|
chmodSync(path9, 384);
|
|
@@ -26928,7 +27512,13 @@ var ProposalStore = class {
|
|
|
26928
27512
|
pruneBefore(cutoffIso, options = {}) {
|
|
26929
27513
|
const dryRun = options.dryRun !== false;
|
|
26930
27514
|
const proposalIds = this.stringColumn(
|
|
26931
|
-
|
|
27515
|
+
`SELECT proposal_id FROM proposals
|
|
27516
|
+
WHERE created_at < ? AND state IN ('applied', 'conflict', 'rejected', 'canceled')
|
|
27517
|
+
AND NOT EXISTS (
|
|
27518
|
+
SELECT 1 FROM cloud_outbox
|
|
27519
|
+
WHERE cloud_outbox.proposal_id = proposals.proposal_id
|
|
27520
|
+
AND cloud_outbox.status <> 'acknowledged'
|
|
27521
|
+
)`,
|
|
26932
27522
|
[cutoffIso],
|
|
26933
27523
|
"proposal_id"
|
|
26934
27524
|
);
|
|
@@ -26942,6 +27532,8 @@ var ProposalStore = class {
|
|
|
26942
27532
|
const evidenceWhere = inWhere("evidence_bundle_id", evidenceIds);
|
|
26943
27533
|
this.transaction(() => {
|
|
26944
27534
|
if (proposalWhere) {
|
|
27535
|
+
run("cloud_outbox", `${proposalWhere.sql} AND status = 'acknowledged'`, proposalWhere.params);
|
|
27536
|
+
run("cloud_governance_events", proposalWhere.sql, proposalWhere.params);
|
|
26945
27537
|
run("idempotency_receipts", proposalWhere.sql, proposalWhere.params);
|
|
26946
27538
|
run("writeback_receipts", proposalWhere.sql, proposalWhere.params);
|
|
26947
27539
|
run("writeback_jobs", proposalWhere.sql, proposalWhere.params);
|
|
@@ -26952,7 +27544,7 @@ var ProposalStore = class {
|
|
|
26952
27544
|
run("worker_queue", proposalWhere.sql, proposalWhere.params);
|
|
26953
27545
|
run("replay_records", proposalWhere.sql, proposalWhere.params);
|
|
26954
27546
|
} else {
|
|
26955
|
-
for (const table of ["idempotency_receipts", "writeback_receipts", "writeback_jobs", "writeback_intents", "approvals", "proposal_events", "shadow_human_actions", "worker_queue", "replay_records"]) {
|
|
27547
|
+
for (const table of ["cloud_outbox", "cloud_governance_events", "idempotency_receipts", "writeback_receipts", "writeback_jobs", "writeback_intents", "approvals", "proposal_events", "shadow_human_actions", "worker_queue", "replay_records"]) {
|
|
26956
27548
|
deleted[table] = 0;
|
|
26957
27549
|
}
|
|
26958
27550
|
}
|
|
@@ -27143,6 +27735,40 @@ var ProposalStore = class {
|
|
|
27143
27735
|
updated_at TEXT NOT NULL
|
|
27144
27736
|
);
|
|
27145
27737
|
|
|
27738
|
+
CREATE TABLE IF NOT EXISTS cloud_outbox (
|
|
27739
|
+
event_id TEXT PRIMARY KEY,
|
|
27740
|
+
proposal_id TEXT,
|
|
27741
|
+
sequence INTEGER NOT NULL,
|
|
27742
|
+
kind TEXT NOT NULL,
|
|
27743
|
+
status TEXT NOT NULL,
|
|
27744
|
+
payload_hash TEXT NOT NULL,
|
|
27745
|
+
payload_json TEXT NOT NULL,
|
|
27746
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
27747
|
+
max_attempts INTEGER NOT NULL,
|
|
27748
|
+
next_attempt_at TEXT NOT NULL,
|
|
27749
|
+
lease_owner TEXT,
|
|
27750
|
+
lease_expires_at TEXT,
|
|
27751
|
+
last_error_code TEXT,
|
|
27752
|
+
sent_at TEXT,
|
|
27753
|
+
acknowledged_at TEXT,
|
|
27754
|
+
created_at TEXT NOT NULL,
|
|
27755
|
+
updated_at TEXT NOT NULL,
|
|
27756
|
+
FOREIGN KEY (proposal_id) REFERENCES proposals(proposal_id)
|
|
27757
|
+
);
|
|
27758
|
+
|
|
27759
|
+
CREATE TABLE IF NOT EXISTS cloud_governance_events (
|
|
27760
|
+
event_id TEXT PRIMARY KEY,
|
|
27761
|
+
proposal_id TEXT NOT NULL,
|
|
27762
|
+
cloud_proposal_id TEXT,
|
|
27763
|
+
kind TEXT NOT NULL,
|
|
27764
|
+
state TEXT NOT NULL,
|
|
27765
|
+
authority TEXT NOT NULL,
|
|
27766
|
+
payload_json TEXT NOT NULL,
|
|
27767
|
+
integrity_hash TEXT NOT NULL,
|
|
27768
|
+
created_at TEXT NOT NULL,
|
|
27769
|
+
FOREIGN KEY (proposal_id) REFERENCES proposals(proposal_id)
|
|
27770
|
+
);
|
|
27771
|
+
|
|
27146
27772
|
CREATE TABLE IF NOT EXISTS policy_recommendations (
|
|
27147
27773
|
recommendation_id TEXT PRIMARY KEY,
|
|
27148
27774
|
tenant_id TEXT NOT NULL,
|
|
@@ -27178,6 +27804,9 @@ var ProposalStore = class {
|
|
|
27178
27804
|
CREATE INDEX IF NOT EXISTS idx_replay_records_proposal_id ON replay_records(proposal_id);
|
|
27179
27805
|
CREATE INDEX IF NOT EXISTS idx_shadow_human_actions_proposal_id ON shadow_human_actions(proposal_id);
|
|
27180
27806
|
CREATE INDEX IF NOT EXISTS idx_policy_recommendations_scope ON policy_recommendations(tenant_id, capability, policy, status, created_at);
|
|
27807
|
+
CREATE INDEX IF NOT EXISTS idx_cloud_outbox_due ON cloud_outbox(status, next_attempt_at, sequence, created_at);
|
|
27808
|
+
CREATE INDEX IF NOT EXISTS idx_cloud_outbox_proposal ON cloud_outbox(proposal_id, sequence, created_at);
|
|
27809
|
+
CREATE INDEX IF NOT EXISTS idx_cloud_governance_proposal ON cloud_governance_events(proposal_id, created_at);
|
|
27181
27810
|
|
|
27182
27811
|
INSERT OR IGNORE INTO proposal_store_schema(version, applied_at)
|
|
27183
27812
|
VALUES (1, datetime('now'));
|
|
@@ -28060,6 +28689,7 @@ var ProposalStore = class {
|
|
|
28060
28689
|
runner_scope: { project_id: options.project_id ?? "local", source_id: proposal.source_id },
|
|
28061
28690
|
engine,
|
|
28062
28691
|
tenant_guard: changeSet.guards.tenant,
|
|
28692
|
+
...changeSet.guards.principal_scope ? { principal_scope: changeSet.guards.principal_scope } : {},
|
|
28063
28693
|
allowed_columns: changeSet.guards.allowed_columns,
|
|
28064
28694
|
idempotency_key: `${proposal.proposal_id}:${proposal.object_id}`,
|
|
28065
28695
|
lease
|
|
@@ -28105,6 +28735,7 @@ var ProposalStore = class {
|
|
|
28105
28735
|
}
|
|
28106
28736
|
},
|
|
28107
28737
|
tenant_guard: changeSet.guards.tenant,
|
|
28738
|
+
...changeSet.guards.principal_scope ? { principal_scope: changeSet.guards.principal_scope } : {},
|
|
28108
28739
|
allowed_columns: changeSet.guards.allowed_columns,
|
|
28109
28740
|
patch: {},
|
|
28110
28741
|
compensation: changeSet.compensation.descriptor,
|
|
@@ -28669,6 +29300,181 @@ var ProposalStore = class {
|
|
|
28669
29300
|
if (!recommendation) throw new ProposalStoreError("POLICY_RECOMMENDATION_NOT_FOUND", `policy recommendation not found: ${recommendationId}`);
|
|
28670
29301
|
return recommendation;
|
|
28671
29302
|
}
|
|
29303
|
+
enqueueCloudOutbox(input) {
|
|
29304
|
+
const eventId = input.event_id.trim();
|
|
29305
|
+
if (!eventId) throw new ProposalStoreError("CLOUD_OUTBOX_EVENT_ID_REQUIRED", "cloud outbox event_id is required");
|
|
29306
|
+
if (!["proposal", "activity", "result"].includes(input.kind)) throw new ProposalStoreError("CLOUD_OUTBOX_KIND_INVALID", `unsupported Cloud outbox kind: ${input.kind}`);
|
|
29307
|
+
if (input.proposal_id) this.requireProposal(input.proposal_id);
|
|
29308
|
+
assertNoSecretMaterial(input.payload, `cloud_outbox.${eventId}`);
|
|
29309
|
+
const payloadHash = canonicalJsonDigest(input.payload);
|
|
29310
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
29311
|
+
const sequence = Math.max(0, Math.trunc(input.sequence ?? 0));
|
|
29312
|
+
const maxAttempts = Math.max(1, Math.min(100, Math.trunc(input.max_attempts ?? 12)));
|
|
29313
|
+
this.db.prepare(`
|
|
29314
|
+
INSERT OR IGNORE INTO cloud_outbox (
|
|
29315
|
+
event_id, proposal_id, sequence, kind, status, payload_hash, payload_json,
|
|
29316
|
+
attempts, max_attempts, next_attempt_at, created_at, updated_at
|
|
29317
|
+
) VALUES (?, ?, ?, ?, 'pending', ?, ?, 0, ?, ?, ?, ?)
|
|
29318
|
+
`).run(eventId, input.proposal_id ?? null, sequence, input.kind, payloadHash, JSON.stringify(input.payload), maxAttempts, now, now, now);
|
|
29319
|
+
const item = this.requireCloudOutboxItem(eventId);
|
|
29320
|
+
if (item.payload_hash !== payloadHash || item.kind !== input.kind || item.proposal_id !== input.proposal_id) {
|
|
29321
|
+
throw new ProposalStoreError("CLOUD_OUTBOX_IDEMPOTENCY_MISMATCH", `cloud outbox event ${eventId} was already recorded with different immutable content`);
|
|
29322
|
+
}
|
|
29323
|
+
return item;
|
|
29324
|
+
}
|
|
29325
|
+
claimCloudOutbox(input) {
|
|
29326
|
+
const owner = input.owner.trim();
|
|
29327
|
+
if (!owner) throw new ProposalStoreError("CLOUD_OUTBOX_OWNER_REQUIRED", "cloud outbox lease owner is required");
|
|
29328
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
29329
|
+
const leaseExpiresAt = new Date(Date.parse(now) + Math.max(1e3, Math.min(3e5, input.lease_ms ?? 3e4))).toISOString();
|
|
29330
|
+
const limit = Math.max(1, Math.min(100, Math.trunc(input.limit ?? 10)));
|
|
29331
|
+
const claimed = [];
|
|
29332
|
+
this.transaction(() => {
|
|
29333
|
+
this.db.prepare(`
|
|
29334
|
+
UPDATE cloud_outbox
|
|
29335
|
+
SET status = 'pending', lease_owner = NULL, lease_expires_at = NULL, updated_at = ?
|
|
29336
|
+
WHERE status = 'leased' AND lease_expires_at <= ?
|
|
29337
|
+
`).run(now, now);
|
|
29338
|
+
const rows = this.db.prepare(`
|
|
29339
|
+
SELECT candidate.event_id
|
|
29340
|
+
FROM cloud_outbox candidate
|
|
29341
|
+
WHERE candidate.status = 'pending'
|
|
29342
|
+
AND candidate.next_attempt_at <= ?
|
|
29343
|
+
AND NOT EXISTS (
|
|
29344
|
+
SELECT 1 FROM cloud_outbox earlier
|
|
29345
|
+
WHERE earlier.proposal_id = candidate.proposal_id
|
|
29346
|
+
AND earlier.sequence < candidate.sequence
|
|
29347
|
+
AND earlier.status NOT IN ('acknowledged')
|
|
29348
|
+
)
|
|
29349
|
+
ORDER BY candidate.sequence ASC, candidate.created_at ASC, candidate.event_id ASC
|
|
29350
|
+
LIMIT ?
|
|
29351
|
+
`).all(now, limit);
|
|
29352
|
+
for (const row of rows) {
|
|
29353
|
+
if (!isRecord3(row) || typeof row.event_id !== "string") continue;
|
|
29354
|
+
const result = this.db.prepare(`
|
|
29355
|
+
UPDATE cloud_outbox
|
|
29356
|
+
SET status = 'leased', lease_owner = ?, lease_expires_at = ?, attempts = attempts + 1,
|
|
29357
|
+
sent_at = COALESCE(sent_at, ?), updated_at = ?
|
|
29358
|
+
WHERE event_id = ? AND status = 'pending'
|
|
29359
|
+
`).run(owner, leaseExpiresAt, now, now, row.event_id);
|
|
29360
|
+
if (Number(result.changes) === 1) claimed.push(row.event_id);
|
|
29361
|
+
}
|
|
29362
|
+
});
|
|
29363
|
+
return claimed.map((eventId) => this.requireCloudOutboxItem(eventId));
|
|
29364
|
+
}
|
|
29365
|
+
acknowledgeCloudOutbox(eventId, owner, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
29366
|
+
const result = this.db.prepare(`
|
|
29367
|
+
UPDATE cloud_outbox
|
|
29368
|
+
SET status = 'acknowledged', lease_owner = NULL, lease_expires_at = NULL,
|
|
29369
|
+
acknowledged_at = ?, last_error_code = NULL, updated_at = ?
|
|
29370
|
+
WHERE event_id = ? AND status = 'leased' AND lease_owner = ?
|
|
29371
|
+
`).run(now, now, eventId, owner);
|
|
29372
|
+
if (Number(result.changes) !== 1) throw new ProposalStoreError("CLOUD_OUTBOX_LEASE_MISMATCH", `cloud outbox event ${eventId} is not leased by ${owner}`);
|
|
29373
|
+
return this.requireCloudOutboxItem(eventId);
|
|
29374
|
+
}
|
|
29375
|
+
failCloudOutbox(input) {
|
|
29376
|
+
const now = input.now ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
29377
|
+
const current = this.requireCloudOutboxItem(input.event_id);
|
|
29378
|
+
if (current.status !== "leased" || current.lease_owner !== input.owner) {
|
|
29379
|
+
throw new ProposalStoreError("CLOUD_OUTBOX_LEASE_MISMATCH", `cloud outbox event ${input.event_id} is not leased by ${input.owner}`);
|
|
29380
|
+
}
|
|
29381
|
+
const exhausted = current.attempts >= current.max_attempts;
|
|
29382
|
+
const status = input.reconciliation ? "reconciliation_required" : input.retryable && !exhausted ? "pending" : "dead_letter";
|
|
29383
|
+
const fallbackDelay = Math.min(3e5, 500 * 2 ** Math.min(current.attempts, 9));
|
|
29384
|
+
const delayMs = Math.max(0, Math.min(36e5, input.retry_after_ms ?? fallbackDelay));
|
|
29385
|
+
const nextAttemptAt = new Date(Date.parse(now) + delayMs).toISOString();
|
|
29386
|
+
this.db.prepare(`
|
|
29387
|
+
UPDATE cloud_outbox
|
|
29388
|
+
SET status = ?, lease_owner = NULL, lease_expires_at = NULL, last_error_code = ?,
|
|
29389
|
+
next_attempt_at = ?, updated_at = ?
|
|
29390
|
+
WHERE event_id = ?
|
|
29391
|
+
`).run(status, input.error_code, nextAttemptAt, now, input.event_id);
|
|
29392
|
+
return this.requireCloudOutboxItem(input.event_id);
|
|
29393
|
+
}
|
|
29394
|
+
requeueCloudOutbox(eventId, now = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
29395
|
+
const current = this.requireCloudOutboxItem(eventId);
|
|
29396
|
+
if (!["dead_letter", "reconciliation_required"].includes(current.status)) {
|
|
29397
|
+
throw new ProposalStoreError("CLOUD_OUTBOX_NOT_REQUEUEABLE", `cloud outbox event ${eventId} is ${current.status}, not dead_letter or reconciliation_required`);
|
|
29398
|
+
}
|
|
29399
|
+
this.db.prepare(`
|
|
29400
|
+
UPDATE cloud_outbox
|
|
29401
|
+
SET status = 'pending', attempts = 0, next_attempt_at = ?, lease_owner = NULL,
|
|
29402
|
+
lease_expires_at = NULL, last_error_code = NULL, updated_at = ?
|
|
29403
|
+
WHERE event_id = ?
|
|
29404
|
+
`).run(now, now, eventId);
|
|
29405
|
+
return this.requireCloudOutboxItem(eventId);
|
|
29406
|
+
}
|
|
29407
|
+
listCloudOutbox(filters = {}) {
|
|
29408
|
+
const conditions = [];
|
|
29409
|
+
const values = [];
|
|
29410
|
+
if (filters.status) {
|
|
29411
|
+
conditions.push("status = ?");
|
|
29412
|
+
values.push(filters.status);
|
|
29413
|
+
}
|
|
29414
|
+
if (filters.proposal_id) {
|
|
29415
|
+
conditions.push("proposal_id = ?");
|
|
29416
|
+
values.push(filters.proposal_id);
|
|
29417
|
+
}
|
|
29418
|
+
const limit = Math.max(1, Math.min(1e4, Math.trunc(filters.limit ?? 100)));
|
|
29419
|
+
const where = conditions.length ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
29420
|
+
return this.db.prepare(`SELECT * FROM cloud_outbox ${where} ORDER BY sequence ASC, created_at ASC, event_id ASC LIMIT ?`).all(...values, limit).map(rowToCloudOutboxItem).filter((item) => item !== void 0);
|
|
29421
|
+
}
|
|
29422
|
+
compactCloudOutbox(input) {
|
|
29423
|
+
const result = this.db.prepare("DELETE FROM cloud_outbox WHERE status = 'acknowledged' AND acknowledged_at < ?").run(input.acknowledged_before);
|
|
29424
|
+
return Number(result.changes);
|
|
29425
|
+
}
|
|
29426
|
+
recordCloudGovernanceEvent(input) {
|
|
29427
|
+
this.requireProposal(input.proposal_id);
|
|
29428
|
+
assertNoSecretMaterial(input.payload, `cloud_governance_event.${input.event_id}`);
|
|
29429
|
+
const createdAt = input.created_at ?? (/* @__PURE__ */ new Date()).toISOString();
|
|
29430
|
+
const unsigned = {
|
|
29431
|
+
event_id: input.event_id,
|
|
29432
|
+
proposal_id: input.proposal_id,
|
|
29433
|
+
...input.cloud_proposal_id ? { cloud_proposal_id: input.cloud_proposal_id } : {},
|
|
29434
|
+
kind: input.kind,
|
|
29435
|
+
state: input.state,
|
|
29436
|
+
authority: "synapsor_cloud",
|
|
29437
|
+
payload: input.payload,
|
|
29438
|
+
created_at: createdAt
|
|
29439
|
+
};
|
|
29440
|
+
const event = { ...unsigned, integrity_hash: canonicalJsonDigest(unsigned) };
|
|
29441
|
+
this.db.prepare(`
|
|
29442
|
+
INSERT OR IGNORE INTO cloud_governance_events (
|
|
29443
|
+
event_id, proposal_id, cloud_proposal_id, kind, state, authority, payload_json, integrity_hash, created_at
|
|
29444
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
29445
|
+
`).run(event.event_id, event.proposal_id, event.cloud_proposal_id ?? null, event.kind, event.state, event.authority, JSON.stringify(event.payload), event.integrity_hash, event.created_at);
|
|
29446
|
+
const localState = localStateFromCloudGovernance(event.state);
|
|
29447
|
+
if (localState) {
|
|
29448
|
+
const sourceMutated = localState === "applied" ? 1 : 0;
|
|
29449
|
+
const projected = this.db.prepare(`
|
|
29450
|
+
UPDATE proposals
|
|
29451
|
+
SET state = ?, source_database_mutated = CASE WHEN ? = 1 THEN 1 ELSE source_database_mutated END, updated_at = ?
|
|
29452
|
+
WHERE proposal_id = ? AND state IN ('pending_review', 'approved', 'pending_worker')
|
|
29453
|
+
`).run(localState, sourceMutated, event.created_at, event.proposal_id);
|
|
29454
|
+
if (Number(projected.changes) === 1) {
|
|
29455
|
+
this.appendEvent(event.proposal_id, `proposal_cloud_${localState}`, "synapsor_cloud", {
|
|
29456
|
+
cloud_event_id: event.event_id,
|
|
29457
|
+
cloud_proposal_id: event.cloud_proposal_id ?? event.proposal_id,
|
|
29458
|
+
cloud_state: event.state,
|
|
29459
|
+
authority: event.authority
|
|
29460
|
+
});
|
|
29461
|
+
}
|
|
29462
|
+
}
|
|
29463
|
+
const stored = this.listCloudGovernanceEvents(input.proposal_id).find((item) => item.event_id === input.event_id);
|
|
29464
|
+
if (!stored || stored.integrity_hash !== event.integrity_hash) {
|
|
29465
|
+
throw new ProposalStoreError("CLOUD_GOVERNANCE_EVENT_MISMATCH", `Cloud governance event ${input.event_id} conflicts with an existing immutable event`);
|
|
29466
|
+
}
|
|
29467
|
+
return stored;
|
|
29468
|
+
}
|
|
29469
|
+
listCloudGovernanceEvents(proposalId) {
|
|
29470
|
+
const rows = proposalId ? this.db.prepare("SELECT * FROM cloud_governance_events WHERE proposal_id = ? ORDER BY created_at ASC, event_id ASC").all(proposalId) : this.db.prepare("SELECT * FROM cloud_governance_events ORDER BY created_at ASC, event_id ASC").all();
|
|
29471
|
+
return rows.map(rowToCloudGovernanceEvent).filter((item) => item !== void 0);
|
|
29472
|
+
}
|
|
29473
|
+
requireCloudOutboxItem(eventId) {
|
|
29474
|
+
const item = rowToCloudOutboxItem(this.db.prepare("SELECT * FROM cloud_outbox WHERE event_id = ?").get(eventId));
|
|
29475
|
+
if (!item) throw new ProposalStoreError("CLOUD_OUTBOX_EVENT_NOT_FOUND", `cloud outbox event not found: ${eventId}`);
|
|
29476
|
+
return item;
|
|
29477
|
+
}
|
|
28672
29478
|
setRunnerState(key, value) {
|
|
28673
29479
|
assertNoSecretMaterial(value, `runner_state.${key}`);
|
|
28674
29480
|
this.db.prepare(`
|
|
@@ -28698,7 +29504,9 @@ var ProposalStore = class {
|
|
|
28698
29504
|
{ table: "shadow_human_actions", kind: "shadow_human_action", key: "action_id", created: "created_at", proposal: "proposal_id" },
|
|
28699
29505
|
{ table: "worker_queue", kind: "worker_queue_item", key: "proposal_id", created: "created_at", proposal: "proposal_id" },
|
|
28700
29506
|
{ table: "runner_state", kind: "runner_state", key: "key", created: "updated_at" },
|
|
28701
|
-
{ table: "policy_recommendations", kind: "policy_recommendation", key: "recommendation_id", created: "created_at", tenant: "tenant_id", capability: "capability" }
|
|
29507
|
+
{ table: "policy_recommendations", kind: "policy_recommendation", key: "recommendation_id", created: "created_at", tenant: "tenant_id", capability: "capability" },
|
|
29508
|
+
{ table: "cloud_outbox", kind: "cloud_outbox_event", key: "event_id", created: "created_at", proposal: "proposal_id" },
|
|
29509
|
+
{ table: "cloud_governance_events", kind: "cloud_governance_event", key: "event_id", created: "created_at", proposal: "proposal_id" }
|
|
28702
29510
|
];
|
|
28703
29511
|
const entries = [];
|
|
28704
29512
|
for (const spec of specs) {
|
|
@@ -29151,6 +29959,15 @@ function stateFromReceipt(receipt) {
|
|
|
29151
29959
|
if (receipt.status === "reconciliation_required") return "reconciliation_required";
|
|
29152
29960
|
return "failed";
|
|
29153
29961
|
}
|
|
29962
|
+
function localStateFromCloudGovernance(state) {
|
|
29963
|
+
if (state === "applied" || state === "already_applied") return "applied";
|
|
29964
|
+
if (state === "rejected") return "rejected";
|
|
29965
|
+
if (state === "canceled") return "canceled";
|
|
29966
|
+
if (state === "conflict") return "conflict";
|
|
29967
|
+
if (state === "failed") return "failed";
|
|
29968
|
+
if (state === "indeterminate" || state === "reconciliation_required") return "reconciliation_required";
|
|
29969
|
+
return void 0;
|
|
29970
|
+
}
|
|
29154
29971
|
function receiptToWritebackResult(receipt) {
|
|
29155
29972
|
if (receipt.schema_version === protocolVersions.executionReceiptV4) {
|
|
29156
29973
|
return parseWritebackResult({
|
|
@@ -29223,6 +30040,7 @@ function inverseCaptureFromChangeSet(changeSet, writebackJobId) {
|
|
|
29223
30040
|
primary_key_column: changeSet.source.primary_key.column
|
|
29224
30041
|
},
|
|
29225
30042
|
tenant_guard: changeSet.guards.tenant,
|
|
30043
|
+
...changeSet.guards.principal_scope ? { principal_scope: changeSet.guards.principal_scope } : {},
|
|
29226
30044
|
allowed_columns: changeSet.guards.allowed_columns,
|
|
29227
30045
|
lineage: changeSet.reversibility.lineage
|
|
29228
30046
|
};
|
|
@@ -29248,7 +30066,7 @@ function inverseCaptureFromChangeSet(changeSet, writebackJobId) {
|
|
|
29248
30066
|
operation: "remove_insert",
|
|
29249
30067
|
members: [{
|
|
29250
30068
|
primary_key: { column: changeSet.source.primary_key.column, value: primaryValue },
|
|
29251
|
-
expected_state: selectReviewedState(changeSet.after, [changeSet.source.primary_key.column, changeSet.guards.tenant.column, ...changeSet.guards.allowed_columns])
|
|
30069
|
+
expected_state: selectReviewedState(changeSet.after, [changeSet.source.primary_key.column, changeSet.guards.tenant.column, ...changeSet.guards.principal_scope ? [changeSet.guards.principal_scope.column] : [], ...changeSet.guards.allowed_columns])
|
|
29252
30070
|
}],
|
|
29253
30071
|
max_rows: 1,
|
|
29254
30072
|
aggregate_bounds: []
|
|
@@ -29528,6 +30346,45 @@ function rowToWorkerQueueItem(row) {
|
|
|
29528
30346
|
updated_at: String(row.updated_at)
|
|
29529
30347
|
};
|
|
29530
30348
|
}
|
|
30349
|
+
function rowToCloudOutboxItem(row) {
|
|
30350
|
+
if (!isRecord3(row)) return void 0;
|
|
30351
|
+
const kind = String(row.kind);
|
|
30352
|
+
const status = String(row.status);
|
|
30353
|
+
if (!["proposal", "activity", "result"].includes(kind) || !["pending", "leased", "acknowledged", "dead_letter", "reconciliation_required"].includes(status)) return void 0;
|
|
30354
|
+
return {
|
|
30355
|
+
event_id: String(row.event_id),
|
|
30356
|
+
proposal_id: row.proposal_id == null ? void 0 : String(row.proposal_id),
|
|
30357
|
+
sequence: Number(row.sequence),
|
|
30358
|
+
kind,
|
|
30359
|
+
status,
|
|
30360
|
+
payload_hash: String(row.payload_hash),
|
|
30361
|
+
payload: JSON.parse(String(row.payload_json)),
|
|
30362
|
+
attempts: Number(row.attempts),
|
|
30363
|
+
max_attempts: Number(row.max_attempts),
|
|
30364
|
+
next_attempt_at: String(row.next_attempt_at),
|
|
30365
|
+
lease_owner: row.lease_owner == null ? void 0 : String(row.lease_owner),
|
|
30366
|
+
lease_expires_at: row.lease_expires_at == null ? void 0 : String(row.lease_expires_at),
|
|
30367
|
+
last_error_code: row.last_error_code == null ? void 0 : String(row.last_error_code),
|
|
30368
|
+
sent_at: row.sent_at == null ? void 0 : String(row.sent_at),
|
|
30369
|
+
acknowledged_at: row.acknowledged_at == null ? void 0 : String(row.acknowledged_at),
|
|
30370
|
+
created_at: String(row.created_at),
|
|
30371
|
+
updated_at: String(row.updated_at)
|
|
30372
|
+
};
|
|
30373
|
+
}
|
|
30374
|
+
function rowToCloudGovernanceEvent(row) {
|
|
30375
|
+
if (!isRecord3(row) || String(row.authority) !== "synapsor_cloud") return void 0;
|
|
30376
|
+
return {
|
|
30377
|
+
event_id: String(row.event_id),
|
|
30378
|
+
proposal_id: String(row.proposal_id),
|
|
30379
|
+
cloud_proposal_id: row.cloud_proposal_id == null ? void 0 : String(row.cloud_proposal_id),
|
|
30380
|
+
kind: String(row.kind),
|
|
30381
|
+
state: String(row.state),
|
|
30382
|
+
authority: "synapsor_cloud",
|
|
30383
|
+
payload: JSON.parse(String(row.payload_json)),
|
|
30384
|
+
integrity_hash: String(row.integrity_hash),
|
|
30385
|
+
created_at: String(row.created_at)
|
|
30386
|
+
};
|
|
30387
|
+
}
|
|
29531
30388
|
function rowToReceipt(row) {
|
|
29532
30389
|
if (!isRecord3(row)) return void 0;
|
|
29533
30390
|
return {
|
|
@@ -29687,7 +30544,9 @@ var sharedLedgerKindToTable = {
|
|
|
29687
30544
|
shadow_human_action: "shadow_human_actions",
|
|
29688
30545
|
worker_queue_item: "worker_queue",
|
|
29689
30546
|
runner_state: "runner_state",
|
|
29690
|
-
policy_recommendation: "policy_recommendations"
|
|
30547
|
+
policy_recommendation: "policy_recommendations",
|
|
30548
|
+
cloud_outbox_event: "cloud_outbox",
|
|
30549
|
+
cloud_governance_event: "cloud_governance_events"
|
|
29691
30550
|
};
|
|
29692
30551
|
var sharedLedgerRestoreSpecs = {
|
|
29693
30552
|
proposals: restoreSpec("proposal_id", [
|
|
@@ -29725,7 +30584,9 @@ var sharedLedgerRestoreSpecs = {
|
|
|
29725
30584
|
shadow_human_actions: restoreSpec("action_id", ["action_id", "proposal_id", "actor", "patch_json", "notes", "created_at"], ["action_id", "proposal_id", "actor", "patch_json", "created_at"]),
|
|
29726
30585
|
worker_queue: restoreSpec("proposal_id", ["proposal_id", "status", "attempts", "max_attempts", "next_attempt_at", "lease_owner", "lease_expires_at", "last_error_code", "created_at", "updated_at"], ["proposal_id", "status", "attempts", "max_attempts", "next_attempt_at", "created_at", "updated_at"]),
|
|
29727
30586
|
runner_state: restoreSpec("key", ["key", "value_json", "updated_at"], ["key", "value_json", "updated_at"]),
|
|
29728
|
-
policy_recommendations: restoreSpec("recommendation_id", ["recommendation_id", "tenant_id", "capability", "policy", "base_contract_digest", "status", "payload_json", "integrity_hash", "created_at", "updated_at"], ["recommendation_id", "tenant_id", "capability", "policy", "base_contract_digest", "status", "payload_json", "integrity_hash", "created_at", "updated_at"])
|
|
30587
|
+
policy_recommendations: restoreSpec("recommendation_id", ["recommendation_id", "tenant_id", "capability", "policy", "base_contract_digest", "status", "payload_json", "integrity_hash", "created_at", "updated_at"], ["recommendation_id", "tenant_id", "capability", "policy", "base_contract_digest", "status", "payload_json", "integrity_hash", "created_at", "updated_at"]),
|
|
30588
|
+
cloud_outbox: restoreSpec("event_id", ["event_id", "proposal_id", "sequence", "kind", "status", "payload_hash", "payload_json", "attempts", "max_attempts", "next_attempt_at", "lease_owner", "lease_expires_at", "last_error_code", "sent_at", "acknowledged_at", "created_at", "updated_at"], ["event_id", "sequence", "kind", "status", "payload_hash", "payload_json", "attempts", "max_attempts", "next_attempt_at", "created_at", "updated_at"]),
|
|
30589
|
+
cloud_governance_events: restoreSpec("event_id", ["event_id", "proposal_id", "cloud_proposal_id", "kind", "state", "authority", "payload_json", "integrity_hash", "created_at"], ["event_id", "proposal_id", "kind", "state", "authority", "payload_json", "integrity_hash", "created_at"])
|
|
29729
30590
|
};
|
|
29730
30591
|
function sharedLedgerPayload(table, row) {
|
|
29731
30592
|
const payload = { table };
|
|
@@ -29768,7 +30629,9 @@ function sharedLedgerRestoreRank(entry) {
|
|
|
29768
30629
|
"shadow_human_actions",
|
|
29769
30630
|
"worker_queue",
|
|
29770
30631
|
"runner_state",
|
|
29771
|
-
"policy_recommendations"
|
|
30632
|
+
"policy_recommendations",
|
|
30633
|
+
"cloud_outbox",
|
|
30634
|
+
"cloud_governance_events"
|
|
29772
30635
|
];
|
|
29773
30636
|
const table = sharedLedgerTableForEntry(entry);
|
|
29774
30637
|
const index = table ? order.indexOf(table) : -1;
|
|
@@ -29858,7 +30721,7 @@ var RESOURCE_KEYS = /* @__PURE__ */ new Set(["name", "engine", "schema", "table"
|
|
|
29858
30721
|
var CONTEXT_KEYS2 = /* @__PURE__ */ new Set(["name", "description", "bindings", "tenant_binding", "principal_binding"]);
|
|
29859
30722
|
var BINDING_KEYS = /* @__PURE__ */ new Set(["name", "source", "key", "required"]);
|
|
29860
30723
|
var CAPABILITY_KEYS2 = /* @__PURE__ */ new Set(["name", "description", "returns_hint", "kind", "context", "source", "subject", "args", "lookup", "visible_fields", "kept_out_fields", "evidence", "max_rows", "proposal", "aggregate"]);
|
|
29861
|
-
var SUBJECT_KEYS = /* @__PURE__ */ new Set(["resource", "schema", "table", "primary_key", "tenant_key", "conflict_key", "single_tenant_dev"]);
|
|
30724
|
+
var SUBJECT_KEYS = /* @__PURE__ */ new Set(["resource", "schema", "table", "primary_key", "tenant_key", "principal_scope_key", "conflict_key", "single_tenant_dev"]);
|
|
29862
30725
|
var ARG_KEYS2 = /* @__PURE__ */ new Set(["type", "description", "required", "max_length", "minimum", "maximum", "enum", "max_items", "fields"]);
|
|
29863
30726
|
var LOOKUP_KEYS2 = /* @__PURE__ */ new Set(["id_from_arg"]);
|
|
29864
30727
|
var EVIDENCE_KEYS = /* @__PURE__ */ new Set(["required", "sources", "query_audit", "handle_prefix"]);
|
|
@@ -29927,6 +30790,7 @@ function validateContract(input) {
|
|
|
29927
30790
|
const capabilities = Array.isArray(input.capabilities) ? input.capabilities : [];
|
|
29928
30791
|
const policies = Array.isArray(input.policies) ? input.policies : [];
|
|
29929
30792
|
const capabilityNames = validateCapabilities2(input.capabilities, contextNames, resourceNames, errors, warnings);
|
|
30793
|
+
validatePrincipalScopes(input.capabilities, input.contexts, input.resources, errors);
|
|
29930
30794
|
validateWorkflows(input.workflows, contextNames, capabilityNames, errors);
|
|
29931
30795
|
const policyByName = validatePolicies2(input.policies, errors);
|
|
29932
30796
|
validateCapabilityApprovalPolicies(capabilities, policies, policyByName, errors);
|
|
@@ -30154,9 +31018,66 @@ function validateSubject(value, path9, resourceNames, errors, warnings) {
|
|
|
30154
31018
|
errors.push({ path: `${path9}.tenant_key`, code: "TENANT_KEY_REQUIRED", message: "tenant_key is required unless single_tenant_dev is explicitly true." });
|
|
30155
31019
|
}
|
|
30156
31020
|
}
|
|
31021
|
+
if (value.principal_scope_key !== void 0 && !isSafeIdentifier2(value.principal_scope_key)) {
|
|
31022
|
+
errors.push({ path: `${path9}.principal_scope_key`, code: "INVALID_PRINCIPAL_SCOPE_KEY", message: "principal_scope_key must be a fixed safe identifier." });
|
|
31023
|
+
}
|
|
31024
|
+
if (value.principal_scope_key !== void 0 && value.resource === void 0 && !isSafeIdentifier2(value.tenant_key)) {
|
|
31025
|
+
errors.push({ path: `${path9}.principal_scope_key`, code: "PRINCIPAL_SCOPE_TENANT_REQUIRED", message: "principal_scope_key can only narrow a capability that also declares tenant_key." });
|
|
31026
|
+
}
|
|
31027
|
+
if (value.principal_scope_key !== void 0 && value.single_tenant_dev === true) {
|
|
31028
|
+
errors.push({ path: `${path9}.principal_scope_key`, code: "PRINCIPAL_SCOPE_TENANT_REQUIRED", message: "principal_scope_key cannot be used with single_tenant_dev; declare a reviewed tenant_key." });
|
|
31029
|
+
}
|
|
30157
31030
|
if (value.single_tenant_dev === true)
|
|
30158
31031
|
warnings.push({ path: `${path9}.single_tenant_dev`, code: "SINGLE_TENANT_DEV", message: "single_tenant_dev is only for local demos and must not be used for shared tenant data." });
|
|
30159
31032
|
}
|
|
31033
|
+
function validatePrincipalScopes(capabilities, contexts, resources, errors) {
|
|
31034
|
+
if (!Array.isArray(capabilities) || !Array.isArray(contexts))
|
|
31035
|
+
return;
|
|
31036
|
+
const contextByName = /* @__PURE__ */ new Map();
|
|
31037
|
+
const resourceByName = /* @__PURE__ */ new Map();
|
|
31038
|
+
for (const context of contexts) {
|
|
31039
|
+
if (isRecord4(context) && isNonEmptyString2(context.name))
|
|
31040
|
+
contextByName.set(context.name, context);
|
|
31041
|
+
}
|
|
31042
|
+
if (Array.isArray(resources)) {
|
|
31043
|
+
for (const resource of resources) {
|
|
31044
|
+
if (isRecord4(resource) && isNonEmptyString2(resource.name))
|
|
31045
|
+
resourceByName.set(resource.name, resource);
|
|
31046
|
+
}
|
|
31047
|
+
}
|
|
31048
|
+
capabilities.forEach((capability, index) => {
|
|
31049
|
+
if (!isRecord4(capability) || !isRecord4(capability.subject) || capability.subject.principal_scope_key === void 0)
|
|
31050
|
+
return;
|
|
31051
|
+
const path9 = `$.capabilities[${index}]`;
|
|
31052
|
+
const resource = typeof capability.subject.resource === "string" ? resourceByName.get(capability.subject.resource) : void 0;
|
|
31053
|
+
const tenantKey = capability.subject.tenant_key ?? resource?.tenant_key;
|
|
31054
|
+
const singleTenantDev = capability.subject.single_tenant_dev ?? resource?.single_tenant_dev;
|
|
31055
|
+
if (!isSafeIdentifier2(tenantKey) || singleTenantDev === true) {
|
|
31056
|
+
errors.push({ path: `${path9}.subject.principal_scope_key`, code: "PRINCIPAL_SCOPE_TENANT_REQUIRED", message: "principal_scope_key can only narrow a capability with a reviewed tenant_key." });
|
|
31057
|
+
}
|
|
31058
|
+
const context = contextByName.get(String(capability.context));
|
|
31059
|
+
if (!context || !isSafeIdentifier2(context.principal_binding)) {
|
|
31060
|
+
errors.push({ path: `${path9}.context`, code: "PRINCIPAL_SCOPE_BINDING_REQUIRED", message: "principal-scoped capabilities require a context with principal_binding." });
|
|
31061
|
+
return;
|
|
31062
|
+
}
|
|
31063
|
+
const binding = Array.isArray(context.bindings) ? context.bindings.find((item) => isRecord4(item) && item.name === context.principal_binding) : void 0;
|
|
31064
|
+
if (!isRecord4(binding) || binding.required !== true) {
|
|
31065
|
+
errors.push({ path: `${path9}.context`, code: "PRINCIPAL_SCOPE_BINDING_REQUIRED", message: "principal_binding must reference a required trusted context binding." });
|
|
31066
|
+
}
|
|
31067
|
+
const scopeKey = capability.subject.principal_scope_key;
|
|
31068
|
+
if (isRecord4(capability.args) && Object.prototype.hasOwnProperty.call(capability.args, String(scopeKey))) {
|
|
31069
|
+
errors.push({ path: `${path9}.args.${String(scopeKey)}`, code: "MODEL_CONTROLLED_PRINCIPAL_SCOPE", message: "the reviewed principal scope column cannot also be a model-facing argument." });
|
|
31070
|
+
}
|
|
31071
|
+
if (isRecord4(capability.proposal)) {
|
|
31072
|
+
if (Array.isArray(capability.proposal.allowed_fields) && capability.proposal.allowed_fields.includes(scopeKey)) {
|
|
31073
|
+
errors.push({ path: `${path9}.proposal.allowed_fields`, code: "PRINCIPAL_SCOPE_WRITE_FORBIDDEN", message: "the reviewed principal scope column cannot be model-writeable." });
|
|
31074
|
+
}
|
|
31075
|
+
if (isRecord4(capability.proposal.patch) && Object.prototype.hasOwnProperty.call(capability.proposal.patch, String(scopeKey))) {
|
|
31076
|
+
errors.push({ path: `${path9}.proposal.patch.${String(scopeKey)}`, code: "PRINCIPAL_SCOPE_WRITE_FORBIDDEN", message: "the reviewed principal scope column is forced from trusted context and cannot be patched by the model." });
|
|
31077
|
+
}
|
|
31078
|
+
}
|
|
31079
|
+
});
|
|
31080
|
+
}
|
|
30160
31081
|
function validateArgs2(value, path9, errors, allowEmpty = false) {
|
|
30161
31082
|
if (!isRecord4(value) || !allowEmpty && Object.keys(value).length === 0) {
|
|
30162
31083
|
errors.push({ path: path9, code: "ARGS_REQUIRED", message: "args must define at least one model-facing business argument." });
|
|
@@ -32778,7 +33699,10 @@ function loadRuntimeConfigFromFile(configPath = process.env.SYNAPSOR_MCP_CONFIG
|
|
|
32778
33699
|
return config;
|
|
32779
33700
|
}
|
|
32780
33701
|
function resolveRuntimeConfig(config, baseDir = process.cwd()) {
|
|
32781
|
-
|
|
33702
|
+
const governance = config.governance?.connection_file ? { ...config.governance, connection_file: path2.resolve(baseDir, config.governance.connection_file) } : config.governance;
|
|
33703
|
+
if (!Array.isArray(config.contracts) || config.contracts.length === 0) {
|
|
33704
|
+
return governance === config.governance ? config : { ...config, governance };
|
|
33705
|
+
}
|
|
32782
33706
|
const seenCapabilities = /* @__PURE__ */ new Map();
|
|
32783
33707
|
const seenPolicies = /* @__PURE__ */ new Map();
|
|
32784
33708
|
for (const [index, capability] of (config.capabilities ?? []).entries()) {
|
|
@@ -32789,6 +33713,7 @@ function resolveRuntimeConfig(config, baseDir = process.cwd()) {
|
|
|
32789
33713
|
}
|
|
32790
33714
|
const resolved = {
|
|
32791
33715
|
...config,
|
|
33716
|
+
...governance ? { governance } : {},
|
|
32792
33717
|
contexts: { ...config.contexts ?? {} },
|
|
32793
33718
|
capabilities: [...config.capabilities ?? []],
|
|
32794
33719
|
policies: [...config.policies ?? []]
|
|
@@ -32801,6 +33726,490 @@ function resolveRuntimeConfig(config, baseDir = process.cwd()) {
|
|
|
32801
33726
|
delete resolved.contracts;
|
|
32802
33727
|
return resolved;
|
|
32803
33728
|
}
|
|
33729
|
+
function loadCloudLinkedConnection(config, env = process.env) {
|
|
33730
|
+
if (config.governance?.mode !== "cloud_linked") {
|
|
33731
|
+
throw new McpRuntimeError("CLOUD_LINKED_MODE_REQUIRED", "This operation requires governance.mode cloud_linked.");
|
|
33732
|
+
}
|
|
33733
|
+
const connectionPath = config.governance.connection_file;
|
|
33734
|
+
if (!connectionPath) throw new McpRuntimeError("CLOUD_CONNECTION_REQUIRED", "Cloud-linked governance requires governance.connection_file.");
|
|
33735
|
+
let parsed;
|
|
33736
|
+
try {
|
|
33737
|
+
parsed = JSON.parse(fs2.readFileSync(connectionPath, "utf8"));
|
|
33738
|
+
} catch (error) {
|
|
33739
|
+
throw new McpRuntimeError("CLOUD_CONNECTION_INVALID", `Unable to read the reviewed Cloud connection file: ${error instanceof Error ? error.message : String(error)}`);
|
|
33740
|
+
}
|
|
33741
|
+
const root = isRecord5(parsed) ? parsed : {};
|
|
33742
|
+
const cloud2 = isRecord5(root.cloud) ? root.cloud : void 0;
|
|
33743
|
+
if (!cloud2) throw new McpRuntimeError("CLOUD_CONNECTION_INVALID", "Cloud connection file must contain a cloud object.");
|
|
33744
|
+
const baseUrlEnv = nonEmptyString(cloud2.base_url_env) ?? "SYNAPSOR_CLOUD_BASE_URL";
|
|
33745
|
+
const runnerTokenEnv = nonEmptyString(cloud2.runner_token_env) ?? "SYNAPSOR_RUNNER_TOKEN";
|
|
33746
|
+
const baseUrl = envValue(env, baseUrlEnv) ?? nonEmptyString(cloud2.base_url);
|
|
33747
|
+
const runnerToken = envValue(env, runnerTokenEnv);
|
|
33748
|
+
const sourceId = nonEmptyString(cloud2.source_id);
|
|
33749
|
+
const runnerSourceId = nonEmptyString(cloud2.runner_source_id) ?? sourceId;
|
|
33750
|
+
const projectId = nonEmptyString(cloud2.project_id);
|
|
33751
|
+
const contractId = nonEmptyString(cloud2.contract_id);
|
|
33752
|
+
const contractVersionId = nonEmptyString(cloud2.contract_version_id);
|
|
33753
|
+
const digest3 = nonEmptyString(cloud2.contract_digest);
|
|
33754
|
+
const missing = [
|
|
33755
|
+
!baseUrl ? baseUrlEnv : "",
|
|
33756
|
+
!runnerToken ? runnerTokenEnv : "",
|
|
33757
|
+
!projectId ? "cloud.project_id" : "",
|
|
33758
|
+
!sourceId ? "cloud.source_id" : "",
|
|
33759
|
+
!contractId ? "cloud.contract_id" : "",
|
|
33760
|
+
!contractVersionId ? "cloud.contract_version_id" : "",
|
|
33761
|
+
!digest3 ? "cloud.contract_digest" : ""
|
|
33762
|
+
].filter(Boolean);
|
|
33763
|
+
if (missing.length) throw new McpRuntimeError("CLOUD_CONNECTION_INCOMPLETE", `Cloud-linked connection is missing: ${missing.join(", ")}.`);
|
|
33764
|
+
if (!/^sha256:[0-9a-f]{64}$/i.test(digest3)) throw new McpRuntimeError("CLOUD_CONTRACT_DIGEST_INVALID", "cloud.contract_digest must be a full sha256 digest.");
|
|
33765
|
+
let normalizedBaseUrl;
|
|
33766
|
+
try {
|
|
33767
|
+
const url = new URL(baseUrl);
|
|
33768
|
+
if (!/^https?:$/.test(url.protocol) || url.username || url.password || url.search || url.hash) throw new Error("unsafe URL components");
|
|
33769
|
+
normalizedBaseUrl = url.toString().replace(/\/$/, "");
|
|
33770
|
+
} catch {
|
|
33771
|
+
throw new McpRuntimeError("CLOUD_BASE_URL_INVALID", `${baseUrlEnv} must contain an HTTP(S) origin without credentials, query, or fragment.`);
|
|
33772
|
+
}
|
|
33773
|
+
return {
|
|
33774
|
+
protocol_version: nonEmptyString(cloud2.protocol_version) ?? protocolVersions.runnerProposal,
|
|
33775
|
+
base_url: normalizedBaseUrl,
|
|
33776
|
+
runner_token_env: runnerTokenEnv,
|
|
33777
|
+
runner_token: runnerToken,
|
|
33778
|
+
runner_id: nonEmptyString(cloud2.runner_id) ?? envValue(env, "SYNAPSOR_RUNNER_ID") ?? "synapsor_runner_local",
|
|
33779
|
+
runner_version: nonEmptyString(cloud2.runner_version) ?? envValue(env, "npm_package_version") ?? "unknown",
|
|
33780
|
+
project_id: projectId,
|
|
33781
|
+
source_id: sourceId,
|
|
33782
|
+
runner_source_id: runnerSourceId,
|
|
33783
|
+
...nonEmptyString(cloud2.mapping_id) ? { mapping_id: nonEmptyString(cloud2.mapping_id) } : {},
|
|
33784
|
+
contract_id: contractId,
|
|
33785
|
+
contract_version_id: contractVersionId,
|
|
33786
|
+
contract_digest: digest3.toLowerCase()
|
|
33787
|
+
};
|
|
33788
|
+
}
|
|
33789
|
+
async function enqueueCloudLinkedProposal(input) {
|
|
33790
|
+
if (input.config.governance?.mode !== "cloud_linked") return void 0;
|
|
33791
|
+
if (!input.store.enqueueCloudOutbox) throw new McpRuntimeError("CLOUD_OUTBOX_UNAVAILABLE", "The configured runtime store does not implement the durable Cloud outbox.");
|
|
33792
|
+
const connection = loadCloudLinkedConnection(input.config, input.env ?? process.env);
|
|
33793
|
+
if (input.proposal.source_id !== connection.runner_source_id) {
|
|
33794
|
+
throw new McpRuntimeError("CLOUD_SOURCE_MAPPING_MISMATCH", `Proposal source ${input.proposal.source_id} is not the reviewed local source ${connection.runner_source_id}.`);
|
|
33795
|
+
}
|
|
33796
|
+
const evidence2 = input.store.listEvidenceBundles ? await input.store.listEvidenceBundles({ proposal: input.proposal.proposal_id, limit: 100 }) : [];
|
|
33797
|
+
const queryAudit2 = input.store.listQueryAudit ? await input.store.listQueryAudit({ proposal: input.proposal.proposal_id, limit: 100 }) : [];
|
|
33798
|
+
const sanitizedChangeSet = cloudSafeChangeSet(input.proposal.change_set);
|
|
33799
|
+
const proposalPayload = {
|
|
33800
|
+
schema_version: protocolVersions.runnerProposal,
|
|
33801
|
+
runner_id: connection.runner_id,
|
|
33802
|
+
source_id: connection.source_id,
|
|
33803
|
+
...connection.mapping_id ? { mapping_id: connection.mapping_id } : {},
|
|
33804
|
+
contract: {
|
|
33805
|
+
contract_id: connection.contract_id,
|
|
33806
|
+
contract_version_id: connection.contract_version_id,
|
|
33807
|
+
digest: connection.contract_digest
|
|
33808
|
+
},
|
|
33809
|
+
change_set: sanitizedChangeSet,
|
|
33810
|
+
evidence_metadata: {
|
|
33811
|
+
bundle_ids: evidence2.length ? evidence2.map((item) => item.evidence_bundle_id) : [input.evidenceBundleId],
|
|
33812
|
+
count: evidence2.length || 1,
|
|
33813
|
+
query_fingerprints: [...new Set([input.queryFingerprint, ...evidence2.map((item) => item.query_fingerprint)].filter((value) => Boolean(value)))],
|
|
33814
|
+
payload_uploaded: false
|
|
33815
|
+
},
|
|
33816
|
+
query_audit: {
|
|
33817
|
+
audit_ids: queryAudit2.map((item) => item.audit_id).filter((value) => value !== void 0),
|
|
33818
|
+
count: queryAudit2.length,
|
|
33819
|
+
query_fingerprints: [...new Set(queryAudit2.map((item) => typeof item.query_fingerprint === "string" ? item.query_fingerprint : void 0).filter((value) => Boolean(value)))],
|
|
33820
|
+
tables: [...new Set(queryAudit2.map((item) => typeof item.table_name === "string" ? item.table_name : void 0).filter((value) => Boolean(value)))],
|
|
33821
|
+
payload_uploaded: false
|
|
33822
|
+
}
|
|
33823
|
+
};
|
|
33824
|
+
const maxAttempts = input.config.governance.max_attempts ?? 12;
|
|
33825
|
+
const proposalItem = await input.store.enqueueCloudOutbox({
|
|
33826
|
+
event_id: `cloud-proposal:${input.proposal.proposal_id}`,
|
|
33827
|
+
proposal_id: input.proposal.proposal_id,
|
|
33828
|
+
sequence: 0,
|
|
33829
|
+
kind: "proposal",
|
|
33830
|
+
payload: proposalPayload,
|
|
33831
|
+
max_attempts: maxAttempts
|
|
33832
|
+
});
|
|
33833
|
+
const principalScope3 = input.proposal.change_set.guards.principal_scope;
|
|
33834
|
+
const common = {
|
|
33835
|
+
schema_version: protocolVersions.runnerActivity,
|
|
33836
|
+
runner_id: connection.runner_id,
|
|
33837
|
+
source_id: connection.source_id,
|
|
33838
|
+
proposal_id: input.proposal.proposal_id,
|
|
33839
|
+
capability: input.proposal.action,
|
|
33840
|
+
tenant_id: input.proposal.tenant_id,
|
|
33841
|
+
principal: principalScope3?.value_fingerprint ?? input.proposal.principal,
|
|
33842
|
+
business_object: input.proposal.business_object,
|
|
33843
|
+
object_id: input.proposal.object_id,
|
|
33844
|
+
status: "pending_cloud_sync"
|
|
33845
|
+
};
|
|
33846
|
+
for (const [index, bundle] of evidence2.entries()) {
|
|
33847
|
+
const activity2 = {
|
|
33848
|
+
...common,
|
|
33849
|
+
event_id: `evidence:${input.proposal.proposal_id}:${bundle.evidence_bundle_id}`,
|
|
33850
|
+
event_type: "evidence.recorded",
|
|
33851
|
+
evidence_ids: [bundle.evidence_bundle_id],
|
|
33852
|
+
detail: { residency: "metadata_only", stored_locally: true, payload_uploaded: false },
|
|
33853
|
+
occurred_at: bundle.created_at
|
|
33854
|
+
};
|
|
33855
|
+
await input.store.enqueueCloudOutbox({ event_id: `cloud-activity:${activity2.event_id}`, proposal_id: input.proposal.proposal_id, sequence: 10 + index, kind: "activity", payload: activity2, max_attempts: maxAttempts });
|
|
33856
|
+
}
|
|
33857
|
+
for (const [index, audit2] of queryAudit2.entries()) {
|
|
33858
|
+
const auditId = String(audit2.audit_id);
|
|
33859
|
+
const activity2 = {
|
|
33860
|
+
...common,
|
|
33861
|
+
event_id: `query-audit:${input.proposal.proposal_id}:${auditId}`,
|
|
33862
|
+
event_type: "query_audit.recorded",
|
|
33863
|
+
query_audit_ids: [auditId],
|
|
33864
|
+
...typeof audit2.evidence_bundle_id === "string" ? { evidence_ids: [audit2.evidence_bundle_id] } : {},
|
|
33865
|
+
detail: { residency: "metadata_only", stored_locally: true, payload_uploaded: false },
|
|
33866
|
+
occurred_at: typeof audit2.created_at === "string" ? audit2.created_at : void 0
|
|
33867
|
+
};
|
|
33868
|
+
await input.store.enqueueCloudOutbox({ event_id: `cloud-activity:${activity2.event_id}`, proposal_id: input.proposal.proposal_id, sequence: 20 + index, kind: "activity", payload: activity2, max_attempts: maxAttempts });
|
|
33869
|
+
}
|
|
33870
|
+
await input.store.recordCloudGovernanceEvent?.({
|
|
33871
|
+
event_id: `cloud-governance:pending:${input.proposal.proposal_id}`,
|
|
33872
|
+
proposal_id: input.proposal.proposal_id,
|
|
33873
|
+
kind: "proposal.pending_cloud_sync",
|
|
33874
|
+
state: "pending_cloud_sync",
|
|
33875
|
+
payload: { evidence_residency: "metadata_only", contract_digest: connection.contract_digest, project_id: connection.project_id, source_id: connection.source_id }
|
|
33876
|
+
});
|
|
33877
|
+
return proposalItem;
|
|
33878
|
+
}
|
|
33879
|
+
async function enqueueCloudLinkedResult(input) {
|
|
33880
|
+
if (input.config.governance?.mode !== "cloud_linked") return void 0;
|
|
33881
|
+
if (!input.store.enqueueCloudOutbox) throw new McpRuntimeError("CLOUD_OUTBOX_UNAVAILABLE", "The configured runtime store does not implement the durable Cloud outbox.");
|
|
33882
|
+
if (input.result.job_id !== `wbj_${input.proposalId}` && !input.result.job_id.endsWith(input.proposalId)) {
|
|
33883
|
+
throw new McpRuntimeError("CLOUD_RESULT_PROPOSAL_MISMATCH", "Cloud result job identity does not match the local proposal.");
|
|
33884
|
+
}
|
|
33885
|
+
const proposal = await input.store.getProposal(input.proposalId);
|
|
33886
|
+
const localAuthorityRejected = input.result.status === "failed" && input.result.affected_rows === 0 && input.result.error_code === "LOCAL_AUTHORITY_REJECTED";
|
|
33887
|
+
if (!proposal && !localAuthorityRejected) {
|
|
33888
|
+
throw new McpRuntimeError("CLOUD_RESULT_LOCAL_PROPOSAL_REQUIRED", `Cloud result ${input.result.job_id} has no matching local proposal.`);
|
|
33889
|
+
}
|
|
33890
|
+
const payload = {
|
|
33891
|
+
schema_version: "synapsor.cloud-result-outbox.v1",
|
|
33892
|
+
lease_id: input.leaseId,
|
|
33893
|
+
result: input.result
|
|
33894
|
+
};
|
|
33895
|
+
return input.store.enqueueCloudOutbox({
|
|
33896
|
+
event_id: `cloud-result:${input.result.job_id}:${input.result.result_hash}`,
|
|
33897
|
+
...proposal ? { proposal_id: input.proposalId } : {},
|
|
33898
|
+
sequence: 1e3,
|
|
33899
|
+
kind: "result",
|
|
33900
|
+
payload,
|
|
33901
|
+
max_attempts: input.config.governance.max_attempts ?? 12
|
|
33902
|
+
});
|
|
33903
|
+
}
|
|
33904
|
+
async function assertCloudLinkedProposalAvailability(config, env) {
|
|
33905
|
+
if (config.governance?.mode !== "cloud_linked" || config.governance.queue_when_unavailable !== false) return;
|
|
33906
|
+
const connection = loadCloudLinkedConnection(config, env);
|
|
33907
|
+
const client = new ControlPlaneClient({
|
|
33908
|
+
baseUrl: connection.base_url,
|
|
33909
|
+
runnerToken: connection.runner_token,
|
|
33910
|
+
sourceId: connection.source_id,
|
|
33911
|
+
runnerId: connection.runner_id
|
|
33912
|
+
});
|
|
33913
|
+
let result;
|
|
33914
|
+
try {
|
|
33915
|
+
result = await client.doctor();
|
|
33916
|
+
} catch (error) {
|
|
33917
|
+
throw new McpRuntimeError(
|
|
33918
|
+
"CLOUD_TEMPORARILY_UNAVAILABLE",
|
|
33919
|
+
"Synapsor Cloud is temporarily unavailable and this Runner is configured not to queue proposals.",
|
|
33920
|
+
{ retry_after_ms: 1e3, cause_code: safeRuntimeErrorCode(error) }
|
|
33921
|
+
);
|
|
33922
|
+
}
|
|
33923
|
+
if (result.ok && result.authenticated) return;
|
|
33924
|
+
const errorCode = nonEmptyString(result.details?.error) ?? nonEmptyString(result.details?.error_code);
|
|
33925
|
+
if (result.status === 401) {
|
|
33926
|
+
throw new McpRuntimeError("CLOUD_RUNNER_AUTHENTICATION_FAILED", "The reviewed Synapsor Cloud Runner credential is not authenticated.");
|
|
33927
|
+
}
|
|
33928
|
+
if (result.status === 403) {
|
|
33929
|
+
throw new McpRuntimeError("CLOUD_RUNNER_AUTHORIZATION_FAILED", "The reviewed Synapsor Cloud Runner identity is not authorized for this source.");
|
|
33930
|
+
}
|
|
33931
|
+
if ([409, 412, 422].includes(result.status)) {
|
|
33932
|
+
throw new McpRuntimeError("CLOUD_CONNECTION_CONFLICT", "The reviewed local Cloud connection no longer matches the active Cloud contract or source.", {
|
|
33933
|
+
...errorCode ? { cloud_error_code: errorCode } : {}
|
|
33934
|
+
});
|
|
33935
|
+
}
|
|
33936
|
+
if (result.status === 429) {
|
|
33937
|
+
throw new McpRuntimeError("CLOUD_RATE_LIMITED", "Synapsor Cloud is rate limiting proposal submissions.", { retry_after_ms: 1e3 });
|
|
33938
|
+
}
|
|
33939
|
+
throw new McpRuntimeError(
|
|
33940
|
+
"CLOUD_TEMPORARILY_UNAVAILABLE",
|
|
33941
|
+
"Synapsor Cloud is temporarily unavailable and this Runner is configured not to queue proposals.",
|
|
33942
|
+
{ retry_after_ms: 1e3, cloud_status: result.status, ...errorCode ? { cloud_error_code: errorCode } : {} }
|
|
33943
|
+
);
|
|
33944
|
+
}
|
|
33945
|
+
function cloudSafeChangeSet(changeSet) {
|
|
33946
|
+
const sanitized = JSON.parse(JSON.stringify(changeSet));
|
|
33947
|
+
sanitized.evidence.items = [];
|
|
33948
|
+
const principalScope3 = sanitized.guards.principal_scope;
|
|
33949
|
+
if (principalScope3) {
|
|
33950
|
+
stripCloudPrincipalColumn(sanitized, principalScope3.column);
|
|
33951
|
+
sanitized.principal.id = principalScope3.value_fingerprint;
|
|
33952
|
+
delete principalScope3.value;
|
|
33953
|
+
}
|
|
33954
|
+
return sanitized;
|
|
33955
|
+
}
|
|
33956
|
+
function stripCloudPrincipalColumn(changeSet, column) {
|
|
33957
|
+
const strip = (value) => {
|
|
33958
|
+
if (isRecord5(value)) delete value[column];
|
|
33959
|
+
};
|
|
33960
|
+
strip(changeSet.before);
|
|
33961
|
+
strip(changeSet.after);
|
|
33962
|
+
if ("frozen_set" in changeSet && isRecord5(changeSet.frozen_set) && Array.isArray(changeSet.frozen_set.members)) {
|
|
33963
|
+
for (const member of changeSet.frozen_set.members) {
|
|
33964
|
+
if (!isRecord5(member)) continue;
|
|
33965
|
+
strip(member.before);
|
|
33966
|
+
strip(member.after);
|
|
33967
|
+
}
|
|
33968
|
+
}
|
|
33969
|
+
if (changeSet.schema_version === protocolVersions.compensationChangeSet) {
|
|
33970
|
+
for (const member of changeSet.compensation.descriptor.members) {
|
|
33971
|
+
strip(member.expected_state);
|
|
33972
|
+
strip(member.restore_values);
|
|
33973
|
+
}
|
|
33974
|
+
}
|
|
33975
|
+
}
|
|
33976
|
+
var CloudLinkedSynchronizer = class {
|
|
33977
|
+
constructor(config, store, env = process.env) {
|
|
33978
|
+
this.config = config;
|
|
33979
|
+
this.store = store;
|
|
33980
|
+
this.connection = loadCloudLinkedConnection(config, env);
|
|
33981
|
+
this.client = new ControlPlaneClient({ baseUrl: this.connection.base_url, runnerToken: this.connection.runner_token, sourceId: this.connection.source_id, runnerId: this.connection.runner_id });
|
|
33982
|
+
this.owner = `${this.connection.runner_id}:${process.pid}:${crypto5.randomBytes(4).toString("hex")}`;
|
|
33983
|
+
if (!store.claimCloudOutbox || !store.acknowledgeCloudOutbox || !store.failCloudOutbox || !store.listCloudOutbox) {
|
|
33984
|
+
throw new McpRuntimeError("CLOUD_OUTBOX_UNAVAILABLE", "Cloud-linked governance requires durable outbox support in the runtime store.");
|
|
33985
|
+
}
|
|
33986
|
+
}
|
|
33987
|
+
config;
|
|
33988
|
+
store;
|
|
33989
|
+
connection;
|
|
33990
|
+
client;
|
|
33991
|
+
owner;
|
|
33992
|
+
timer;
|
|
33993
|
+
stopped = false;
|
|
33994
|
+
activeDrain;
|
|
33995
|
+
lastReconciledAt;
|
|
33996
|
+
lastReconciliationErrorCode;
|
|
33997
|
+
lastCompactedAt;
|
|
33998
|
+
lastCompactedCount = 0;
|
|
33999
|
+
start() {
|
|
34000
|
+
if (this.timer || this.stopped) return;
|
|
34001
|
+
const tick = async () => {
|
|
34002
|
+
if (this.stopped) return;
|
|
34003
|
+
await this.drainOnce().catch(() => void 0);
|
|
34004
|
+
if (!this.stopped) {
|
|
34005
|
+
this.timer = setTimeout(tick, this.config.governance?.sync_interval_ms ?? 2e3);
|
|
34006
|
+
this.timer.unref?.();
|
|
34007
|
+
}
|
|
34008
|
+
};
|
|
34009
|
+
void tick();
|
|
34010
|
+
}
|
|
34011
|
+
async stop() {
|
|
34012
|
+
this.stopped = true;
|
|
34013
|
+
if (this.timer) clearTimeout(this.timer);
|
|
34014
|
+
await this.activeDrain?.catch(() => void 0);
|
|
34015
|
+
}
|
|
34016
|
+
async drainOnce() {
|
|
34017
|
+
if (this.activeDrain) return this.activeDrain;
|
|
34018
|
+
const drain = this.performDrainOnce();
|
|
34019
|
+
this.activeDrain = drain;
|
|
34020
|
+
try {
|
|
34021
|
+
return await drain;
|
|
34022
|
+
} finally {
|
|
34023
|
+
if (this.activeDrain === drain) this.activeDrain = void 0;
|
|
34024
|
+
}
|
|
34025
|
+
}
|
|
34026
|
+
async synchronizeBeforeProposal() {
|
|
34027
|
+
while (this.activeDrain) await this.activeDrain;
|
|
34028
|
+
await this.drainOnce();
|
|
34029
|
+
}
|
|
34030
|
+
async flushEvent(eventId, timeoutMs = 3e4) {
|
|
34031
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
34032
|
+
while (true) {
|
|
34033
|
+
const current = (await this.store.listCloudOutbox({ limit: 1e4 })).find((item) => item.event_id === eventId);
|
|
34034
|
+
if (!current) throw new McpRuntimeError("CLOUD_OUTBOX_EVENT_NOT_FOUND", `Cloud outbox event ${eventId} was not found.`);
|
|
34035
|
+
if (current.status === "acknowledged") return current;
|
|
34036
|
+
if (current.status === "dead_letter" || current.status === "reconciliation_required") {
|
|
34037
|
+
throw new McpRuntimeError(
|
|
34038
|
+
current.last_error_code ?? "CLOUD_OUTBOX_DELIVERY_FAILED",
|
|
34039
|
+
`Cloud outbox event ${eventId} requires operator attention (${current.status}).`
|
|
34040
|
+
);
|
|
34041
|
+
}
|
|
34042
|
+
await this.drainOnce();
|
|
34043
|
+
const refreshed = (await this.store.listCloudOutbox({ limit: 1e4 })).find((item) => item.event_id === eventId);
|
|
34044
|
+
if (!refreshed) throw new McpRuntimeError("CLOUD_OUTBOX_EVENT_NOT_FOUND", `Cloud outbox event ${eventId} was not found.`);
|
|
34045
|
+
if (refreshed.status === "acknowledged") return refreshed;
|
|
34046
|
+
if (refreshed.status === "dead_letter" || refreshed.status === "reconciliation_required") {
|
|
34047
|
+
throw new McpRuntimeError(
|
|
34048
|
+
refreshed.last_error_code ?? "CLOUD_OUTBOX_DELIVERY_FAILED",
|
|
34049
|
+
`Cloud outbox event ${eventId} requires operator attention (${refreshed.status}).`
|
|
34050
|
+
);
|
|
34051
|
+
}
|
|
34052
|
+
if (Date.now() >= deadline) return refreshed;
|
|
34053
|
+
await new Promise((resolve2) => setTimeout(resolve2, Math.min(250, Math.max(1, deadline - Date.now()))));
|
|
34054
|
+
}
|
|
34055
|
+
}
|
|
34056
|
+
async performDrainOnce() {
|
|
34057
|
+
let acknowledged = 0;
|
|
34058
|
+
let failed = 0;
|
|
34059
|
+
const items = await this.store.claimCloudOutbox({ owner: this.owner, limit: 10, lease_ms: 3e4 });
|
|
34060
|
+
for (const item of items) {
|
|
34061
|
+
try {
|
|
34062
|
+
const response = await this.deliver(item);
|
|
34063
|
+
if (item.proposal_id && item.kind === "proposal") {
|
|
34064
|
+
const cloudProposalId = nonEmptyString(response.proposal_id) ?? nonEmptyString(response.id) ?? item.proposal_id;
|
|
34065
|
+
const requestId = nonEmptyString(response.request_id);
|
|
34066
|
+
await this.store.recordCloudGovernanceEvent?.({
|
|
34067
|
+
event_id: `cloud-governance:ack:${item.event_id}`,
|
|
34068
|
+
proposal_id: item.proposal_id,
|
|
34069
|
+
cloud_proposal_id: cloudProposalId,
|
|
34070
|
+
kind: "proposal.cloud_acknowledged",
|
|
34071
|
+
state: nonEmptyString(response.status) ?? "pending_review",
|
|
34072
|
+
payload: { ...requestId ? { request_id: requestId } : {}, evidence_residency: "metadata_only", payload_hash: item.payload_hash }
|
|
34073
|
+
});
|
|
34074
|
+
}
|
|
34075
|
+
await this.store.acknowledgeCloudOutbox(item.event_id, this.owner);
|
|
34076
|
+
acknowledged += 1;
|
|
34077
|
+
} catch (error) {
|
|
34078
|
+
failed += 1;
|
|
34079
|
+
const classification = classifyCloudSyncFailure(error);
|
|
34080
|
+
await this.store.failCloudOutbox({ event_id: item.event_id, owner: this.owner, ...classification });
|
|
34081
|
+
}
|
|
34082
|
+
}
|
|
34083
|
+
await this.reconcileOnce().catch((error) => {
|
|
34084
|
+
this.lastReconciliationErrorCode = classifyCloudSyncFailure(error).error_code;
|
|
34085
|
+
});
|
|
34086
|
+
await this.compactAcknowledged().catch(() => void 0);
|
|
34087
|
+
return { claimed: items.length, acknowledged, failed };
|
|
34088
|
+
}
|
|
34089
|
+
async status() {
|
|
34090
|
+
const items = await this.store.listCloudOutbox({ limit: 1e4 });
|
|
34091
|
+
const count = (status) => items.filter((item) => item.status === status).length;
|
|
34092
|
+
const pending = items.filter((item) => item.status === "pending");
|
|
34093
|
+
const acknowledged = items.filter((item) => item.status === "acknowledged" && item.acknowledged_at);
|
|
34094
|
+
return {
|
|
34095
|
+
authority_mode: "cloud_linked",
|
|
34096
|
+
evidence_residency: "metadata_only",
|
|
34097
|
+
pending: count("pending"),
|
|
34098
|
+
leased: count("leased"),
|
|
34099
|
+
acknowledged: count("acknowledged"),
|
|
34100
|
+
dead_letter: count("dead_letter"),
|
|
34101
|
+
reconciliation_required: count("reconciliation_required"),
|
|
34102
|
+
...pending[0] ? { oldest_pending_at: pending[0].created_at } : {},
|
|
34103
|
+
...acknowledged.length ? { last_acknowledged_at: acknowledged.map((item) => item.acknowledged_at).sort().at(-1) } : {},
|
|
34104
|
+
...this.lastReconciledAt ? { last_reconciled_at: this.lastReconciledAt } : {},
|
|
34105
|
+
...this.lastReconciliationErrorCode ? { last_reconciliation_error_code: this.lastReconciliationErrorCode } : {},
|
|
34106
|
+
...this.lastCompactedAt ? { last_compacted_at: this.lastCompactedAt, last_compacted_count: this.lastCompactedCount } : {}
|
|
34107
|
+
};
|
|
34108
|
+
}
|
|
34109
|
+
async compactAcknowledged() {
|
|
34110
|
+
if (!this.store.compactCloudOutbox) return;
|
|
34111
|
+
const now = Date.now();
|
|
34112
|
+
if (this.lastCompactedAt && now - Date.parse(this.lastCompactedAt) < 60 * 60 * 1e3) return;
|
|
34113
|
+
const retentionDays = this.config.governance?.outbox_retention_days ?? 30;
|
|
34114
|
+
const cutoff = new Date(now - retentionDays * 24 * 60 * 60 * 1e3).toISOString();
|
|
34115
|
+
this.lastCompactedCount = await this.store.compactCloudOutbox({ acknowledged_before: cutoff });
|
|
34116
|
+
this.lastCompactedAt = new Date(now).toISOString();
|
|
34117
|
+
}
|
|
34118
|
+
async reconcileOnce() {
|
|
34119
|
+
if (!this.store.listCloudGovernanceEvents || !this.store.recordCloudGovernanceEvent) return { inspected: 0, recorded: 0 };
|
|
34120
|
+
const acknowledged = (await this.store.listCloudOutbox({ status: "acknowledged", limit: 1e4 })).filter((item) => item.kind === "proposal" && item.proposal_id);
|
|
34121
|
+
let recorded = 0;
|
|
34122
|
+
for (const item of acknowledged.slice(-100)) {
|
|
34123
|
+
const proposalId = item.proposal_id;
|
|
34124
|
+
const events2 = await this.store.listCloudGovernanceEvents(proposalId);
|
|
34125
|
+
const latest = events2.at(-1);
|
|
34126
|
+
if (latest && ["applied", "failed", "conflict", "indeterminate", "canceled", "rejected"].includes(latest.state)) continue;
|
|
34127
|
+
const response = await this.client.proposalStatus(proposalId);
|
|
34128
|
+
const state = nonEmptyString(response.status) ?? "unknown";
|
|
34129
|
+
const payload = cloudGovernanceStatusPayload(response);
|
|
34130
|
+
const identity = canonicalJsonDigest({ proposal_id: proposalId, state, payload });
|
|
34131
|
+
const eventId = `cloud-governance:state:${proposalId}:${identity.slice("sha256:".length, "sha256:".length + 20)}`;
|
|
34132
|
+
const existed = events2.some((event) => event.event_id === eventId);
|
|
34133
|
+
await this.store.recordCloudGovernanceEvent({
|
|
34134
|
+
event_id: eventId,
|
|
34135
|
+
proposal_id: proposalId,
|
|
34136
|
+
cloud_proposal_id: nonEmptyString(response.proposal_id) ?? proposalId,
|
|
34137
|
+
kind: `proposal.cloud_${state}`,
|
|
34138
|
+
state,
|
|
34139
|
+
payload
|
|
34140
|
+
});
|
|
34141
|
+
if (!existed) recorded += 1;
|
|
34142
|
+
}
|
|
34143
|
+
this.lastReconciledAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
34144
|
+
this.lastReconciliationErrorCode = void 0;
|
|
34145
|
+
return { inspected: acknowledged.length, recorded };
|
|
34146
|
+
}
|
|
34147
|
+
async deliver(item) {
|
|
34148
|
+
if (item.kind === "proposal") return this.client.submitProposal(item.payload);
|
|
34149
|
+
if (item.kind === "activity") return this.client.submitActivity(item.payload);
|
|
34150
|
+
if (item.kind === "result") {
|
|
34151
|
+
const result = isRecord5(item.payload.result) ? item.payload.result : void 0;
|
|
34152
|
+
const leaseId = nonEmptyString(item.payload.lease_id);
|
|
34153
|
+
if (!result || !leaseId) throw new McpRuntimeError("CLOUD_RESULT_OUTBOX_INVALID", "Cloud result outbox entry is missing a result or lease identity.");
|
|
34154
|
+
return this.client.result(result, leaseId);
|
|
34155
|
+
}
|
|
34156
|
+
throw new McpRuntimeError("CLOUD_OUTBOX_KIND_UNSUPPORTED", `Unsupported Cloud outbox kind: ${item.kind}`);
|
|
34157
|
+
}
|
|
34158
|
+
};
|
|
34159
|
+
function cloudGovernanceStatusPayload(response) {
|
|
34160
|
+
const decision = isRecord5(response.decision) ? response.decision : void 0;
|
|
34161
|
+
const job = isRecord5(response.job) ? response.job : void 0;
|
|
34162
|
+
const result = isRecord5(response.result) ? response.result : void 0;
|
|
34163
|
+
const actor = nonEmptyString(decision?.actor);
|
|
34164
|
+
return JSON.parse(JSON.stringify({
|
|
34165
|
+
contract_id: nonEmptyString(response.contract_id),
|
|
34166
|
+
contract_version_id: nonEmptyString(response.contract_version_id),
|
|
34167
|
+
contract_digest: nonEmptyString(response.contract_digest),
|
|
34168
|
+
source_id: nonEmptyString(response.source_id),
|
|
34169
|
+
terminal: response.terminal === true,
|
|
34170
|
+
evidence_residency: "metadata_only",
|
|
34171
|
+
decision: decision ? {
|
|
34172
|
+
status: nonEmptyString(decision.status),
|
|
34173
|
+
authority: "synapsor_cloud",
|
|
34174
|
+
actor_fingerprint: actor ? canonicalJsonDigest({ actor }) : void 0,
|
|
34175
|
+
decided_at: nonEmptyString(decision.decided_at)
|
|
34176
|
+
} : void 0,
|
|
34177
|
+
job: job ? {
|
|
34178
|
+
job_id: nonEmptyString(job.job_id),
|
|
34179
|
+
status: nonEmptyString(job.status),
|
|
34180
|
+
attempt_count: typeof job.attempt_count === "number" ? job.attempt_count : void 0,
|
|
34181
|
+
leased_runner_id: nonEmptyString(job.leased_runner_id),
|
|
34182
|
+
lease_expires_at: job.lease_expires_at
|
|
34183
|
+
} : void 0,
|
|
34184
|
+
result: result ? {
|
|
34185
|
+
status: nonEmptyString(result.status),
|
|
34186
|
+
source_database_mutated: result.source_database_mutated === true,
|
|
34187
|
+
affected_rows: typeof result.affected_rows === "number" ? result.affected_rows : void 0,
|
|
34188
|
+
receipt_id: nonEmptyString(result.receipt_id),
|
|
34189
|
+
result_hash: nonEmptyString(result.result_hash),
|
|
34190
|
+
error_code: nonEmptyString(result.error_code)
|
|
34191
|
+
} : void 0,
|
|
34192
|
+
updated_at: response.updated_at
|
|
34193
|
+
}));
|
|
34194
|
+
}
|
|
34195
|
+
function classifyCloudSyncFailure(error) {
|
|
34196
|
+
if (error instanceof CloudControlError) {
|
|
34197
|
+
const reconciliation = [409, 412, 422].includes(error.status) || ["contract_digest_mismatch", "proposal_hash_mismatch", "cloud_state_conflict"].includes(error.error_code);
|
|
34198
|
+
return {
|
|
34199
|
+
error_code: error.error_code,
|
|
34200
|
+
retryable: error.retryable && !reconciliation,
|
|
34201
|
+
...error.retry_after_ms === void 0 ? {} : { retry_after_ms: error.retry_after_ms },
|
|
34202
|
+
...reconciliation ? { reconciliation: true } : {}
|
|
34203
|
+
};
|
|
34204
|
+
}
|
|
34205
|
+
if (error instanceof McpRuntimeError) return { error_code: error.code, retryable: false };
|
|
34206
|
+
return { error_code: "cloud_sync_internal", retryable: false };
|
|
34207
|
+
}
|
|
34208
|
+
function nonEmptyString(value) {
|
|
34209
|
+
if (typeof value !== "string") return void 0;
|
|
34210
|
+
const normalized = value.trim();
|
|
34211
|
+
return normalized || void 0;
|
|
34212
|
+
}
|
|
32804
34213
|
function rememberCapabilityName(seen, name, origin) {
|
|
32805
34214
|
const previous = seen.get(name);
|
|
32806
34215
|
if (previous) {
|
|
@@ -32848,6 +34257,8 @@ function runtimeContextFromSpec(context) {
|
|
|
32848
34257
|
const provider = context.bindings.some((binding) => binding.source === "environment") ? "environment" : context.bindings.some((binding) => binding.source === "cloud_session") ? "cloud_session" : context.bindings.some((binding) => binding.source === "http_claim") ? "http_claims" : context.bindings.some((binding) => binding.source === "static_dev") ? "static_dev" : "environment";
|
|
32849
34258
|
return {
|
|
32850
34259
|
provider,
|
|
34260
|
+
tenant_binding: context.tenant_binding,
|
|
34261
|
+
principal_binding: context.principal_binding,
|
|
32851
34262
|
values: {
|
|
32852
34263
|
...tenantBinding ? { tenant_id_env: tenantBinding.key, tenant_id_key: tenantBinding.key } : {},
|
|
32853
34264
|
...principalBinding ? { principal_env: principalBinding.key, principal_key: principalBinding.key } : {}
|
|
@@ -32862,6 +34273,7 @@ function runtimeCapabilityFromSpec(capability, resources, config, provenance) {
|
|
|
32862
34273
|
table: subjectResource?.table ?? capability.subject.table ?? "",
|
|
32863
34274
|
primary_key: subjectResource?.primary_key ?? capability.subject.primary_key ?? "",
|
|
32864
34275
|
tenant_key: subjectResource?.tenant_key ?? capability.subject.tenant_key,
|
|
34276
|
+
principal_scope_key: capability.subject.principal_scope_key,
|
|
32865
34277
|
single_tenant_dev: subjectResource?.single_tenant_dev ?? capability.subject.single_tenant_dev
|
|
32866
34278
|
};
|
|
32867
34279
|
const runtime = {
|
|
@@ -32920,6 +34332,9 @@ function createMcpRuntime(config, options = {}) {
|
|
|
32920
34332
|
const cloudTools = options.cloudTools ?? [];
|
|
32921
34333
|
const resultFormat = options.resultFormat ?? config.result_format ?? 1;
|
|
32922
34334
|
const trustedContext = options.trustedContext;
|
|
34335
|
+
if (config.governance?.mode === "cloud_linked") loadCloudLinkedConnection(config, env);
|
|
34336
|
+
const cloudSynchronizer = ownsStore && config.governance?.mode === "cloud_linked" ? new CloudLinkedSynchronizer(config, store, env) : void 0;
|
|
34337
|
+
cloudSynchronizer?.start();
|
|
32923
34338
|
const assertLocalStoreAvailable = ownsStore && sharedPostgres?.mode !== "runtime_store";
|
|
32924
34339
|
const assertStoreAvailable = () => {
|
|
32925
34340
|
if (assertLocalStoreAvailable) assertPersistentStoreAvailable(storePath);
|
|
@@ -32931,6 +34346,7 @@ function createMcpRuntime(config, options = {}) {
|
|
|
32931
34346
|
callTool: async (name, args) => {
|
|
32932
34347
|
const capability = config.mode === "cloud" ? void 0 : localCapabilities(config).find((item) => item.name === name);
|
|
32933
34348
|
try {
|
|
34349
|
+
if (capability?.kind === "proposal") await cloudSynchronizer?.synchronizeBeforeProposal();
|
|
32934
34350
|
if (capability) {
|
|
32935
34351
|
const context = resolveTrustedContext(config, env, capability, trustedContext);
|
|
32936
34352
|
await resources.consumeRateLimit(context, capability.name);
|
|
@@ -32953,7 +34369,9 @@ function createMcpRuntime(config, options = {}) {
|
|
|
32953
34369
|
},
|
|
32954
34370
|
poolMetrics: () => resources.poolMetrics(),
|
|
32955
34371
|
rateLimitMetrics: () => resources.rateLimitMetrics(),
|
|
34372
|
+
cloudSyncStatus: async () => cloudSynchronizer ? cloudSynchronizer.status() : { authority_mode: "local_only", evidence_residency: "metadata_only", pending: 0, leased: 0, acknowledged: 0, dead_letter: 0, reconciliation_required: 0 },
|
|
32956
34373
|
close: async () => {
|
|
34374
|
+
await cloudSynchronizer?.stop();
|
|
32957
34375
|
if (ownsResources) await resources.close();
|
|
32958
34376
|
if (!options.store) await store.close();
|
|
32959
34377
|
}
|
|
@@ -33232,6 +34650,8 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
33232
34650
|
const cloudTools = config.mode === "cloud" ? await fetchCloudToolMetadata(config, env) : void 0;
|
|
33233
34651
|
const sharedStorePath = options.storePath ?? config.storage?.sqlite_path ?? "./.synapsor/local.db";
|
|
33234
34652
|
const sharedStore = createDefaultRuntimeStore(config, env, sharedStorePath);
|
|
34653
|
+
const cloudSynchronizer = config.governance?.mode === "cloud_linked" ? new CloudLinkedSynchronizer(config, sharedStore, env) : void 0;
|
|
34654
|
+
cloudSynchronizer?.start();
|
|
33235
34655
|
const sharedResources = createMcpRuntimeSharedResources(config, env, options.readRow);
|
|
33236
34656
|
const sessions = /* @__PURE__ */ new Map();
|
|
33237
34657
|
const openSessions = /* @__PURE__ */ new Set();
|
|
@@ -33274,6 +34694,7 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
33274
34694
|
});
|
|
33275
34695
|
});
|
|
33276
34696
|
} catch (error) {
|
|
34697
|
+
await cloudSynchronizer?.stop();
|
|
33277
34698
|
await closeStreamableSessions(openSessions);
|
|
33278
34699
|
await sharedResources.close();
|
|
33279
34700
|
await sharedStore.close();
|
|
@@ -33301,7 +34722,7 @@ async function startStreamableHttpMcpServer(options = {}) {
|
|
|
33301
34722
|
host: actualHost,
|
|
33302
34723
|
port: actualPort,
|
|
33303
34724
|
url,
|
|
33304
|
-
close: () => closeStreamableHttpServer(server, openSessions, sharedResources, sharedStore)
|
|
34725
|
+
close: () => closeStreamableHttpServer(server, openSessions, sharedResources, sharedStore, cloudSynchronizer)
|
|
33305
34726
|
};
|
|
33306
34727
|
}
|
|
33307
34728
|
async function handleStreamableHttpMcpRequest(input) {
|
|
@@ -33876,13 +35297,14 @@ async function closeHttpServer(server, runtime) {
|
|
|
33876
35297
|
});
|
|
33877
35298
|
}).finally(() => runtime.close());
|
|
33878
35299
|
}
|
|
33879
|
-
async function closeStreamableHttpServer(server, sessions, sharedResources, sharedStore) {
|
|
35300
|
+
async function closeStreamableHttpServer(server, sessions, sharedResources, sharedStore, cloudSynchronizer) {
|
|
33880
35301
|
await new Promise((resolve2, reject) => {
|
|
33881
35302
|
server.close((error) => {
|
|
33882
35303
|
if (error) reject(error);
|
|
33883
35304
|
else resolve2();
|
|
33884
35305
|
});
|
|
33885
35306
|
}).finally(async () => {
|
|
35307
|
+
await cloudSynchronizer?.stop();
|
|
33886
35308
|
await closeStreamableSessions(sessions);
|
|
33887
35309
|
await sharedResources.close();
|
|
33888
35310
|
await sharedStore.close();
|
|
@@ -34015,6 +35437,9 @@ async function callConfiguredTool(input) {
|
|
|
34015
35437
|
assertProposalWritebackResolvable(input.config, capability);
|
|
34016
35438
|
assertApprovalPolicyResolvable(input.config, capability);
|
|
34017
35439
|
}
|
|
35440
|
+
if (capability.kind === "proposal") {
|
|
35441
|
+
await assertCloudLinkedProposalAvailability(input.config, input.env);
|
|
35442
|
+
}
|
|
34018
35443
|
const source = input.config.sources?.[capability.source];
|
|
34019
35444
|
if (!source) throw new McpRuntimeError("SOURCE_NOT_FOUND", `Unknown source: ${capability.source}`);
|
|
34020
35445
|
const context = resolveTrustedContext(input.config, input.env, capability, input.trustedContext);
|
|
@@ -34045,6 +35470,8 @@ async function callConfiguredTool(input) {
|
|
|
34045
35470
|
const patch = capability.kind !== "proposal" || operation === "delete" || batchInsert ? {} : buildPatch(capability, input.args);
|
|
34046
35471
|
const itemPatches = batchInsert ? batchItems.map((item) => buildItemPatch(capability, item, input.args)) : [];
|
|
34047
35472
|
const before = scalarRecord(current.row);
|
|
35473
|
+
const principalScope3 = effectivePrincipalScope(input.config, capability, context);
|
|
35474
|
+
const principalScopeMetadata = principalScope3 ? withoutPrincipalScopeValue(principalScope3) : void 0;
|
|
34048
35475
|
if (capability.kind === "proposal") {
|
|
34049
35476
|
if (setOperation && !batchInsert) currentRows.forEach((row) => enforcePatchGuards(capability, scalarRecord(row), patch));
|
|
34050
35477
|
else if (batchInsert) itemPatches.forEach((itemPatch) => enforcePatchGuards(capability, {}, itemPatch));
|
|
@@ -34055,12 +35482,14 @@ async function callConfiguredTool(input) {
|
|
|
34055
35482
|
action: capability.name,
|
|
34056
35483
|
operation,
|
|
34057
35484
|
tenant: context.tenant_id,
|
|
35485
|
+
principal_scope: principalScope3?.value_fingerprint,
|
|
34058
35486
|
before: setOperation ? currentRows.map(scalarRecord) : before,
|
|
34059
35487
|
patch: batchInsert ? itemPatches : patch,
|
|
34060
35488
|
created_at: createdAt
|
|
34061
35489
|
} : {
|
|
34062
35490
|
action: capability.name,
|
|
34063
35491
|
tenant: context.tenant_id,
|
|
35492
|
+
principal_scope: principalScope3?.value_fingerprint,
|
|
34064
35493
|
object: String(current.row[capability.target.primary_key] ?? input.args[capability.lookup.id_from_arg]),
|
|
34065
35494
|
before,
|
|
34066
35495
|
patch,
|
|
@@ -34071,12 +35500,14 @@ async function callConfiguredTool(input) {
|
|
|
34071
35500
|
const objectId = setOperation ? stableId("set", {
|
|
34072
35501
|
capability: capability.name,
|
|
34073
35502
|
tenant: context.tenant_id,
|
|
35503
|
+
principal_scope: principalScope3?.value_fingerprint,
|
|
34074
35504
|
identities: batchInsert ? batchItems : currentRows.map((row) => row[capability.target.primary_key])
|
|
34075
35505
|
}) : capability.kind === "proposal" && operation === "insert" ? String(primaryDedup?.value ?? proposalId) : String(current.row[capability.target.primary_key] ?? input.args[capability.lookup.id_from_arg]);
|
|
34076
35506
|
const evidenceBundleId = stableId("ev", {
|
|
34077
35507
|
capability: capability.name,
|
|
34078
35508
|
source: capability.source,
|
|
34079
35509
|
tenant: context.tenant_id,
|
|
35510
|
+
principal_scope: principalScope3?.value_fingerprint,
|
|
34080
35511
|
row: capability.kind === "proposal" && operation === "insert" ? void 0 : setOperation ? currentRows : current.row,
|
|
34081
35512
|
patch: capability.kind === "proposal" && operation === "insert" ? batchInsert ? itemPatches : patch : void 0,
|
|
34082
35513
|
at: createdAt
|
|
@@ -34111,7 +35542,8 @@ async function callConfiguredTool(input) {
|
|
|
34111
35542
|
principal: context.principal,
|
|
34112
35543
|
tenant_id: context.tenant_id,
|
|
34113
35544
|
source_database_changed: false,
|
|
34114
|
-
binding_provenance: context.provenance
|
|
35545
|
+
binding_provenance: context.provenance,
|
|
35546
|
+
...principalScopeMetadata ? { principal_scope: principalScopeMetadata } : {}
|
|
34115
35547
|
},
|
|
34116
35548
|
items: setOperation ? boundedSetEvidenceItems(capability, context, operation, currentRows, itemPatches, batchItems) : [{
|
|
34117
35549
|
kind: capability.kind === "proposal" && operation === "insert" ? "reviewed_insert_intent" : "external_row",
|
|
@@ -34119,7 +35551,8 @@ async function callConfiguredTool(input) {
|
|
|
34119
35551
|
table: `${capability.target.schema}.${capability.target.table}`,
|
|
34120
35552
|
primary_key: { column: capability.target.primary_key, value: objectId },
|
|
34121
35553
|
tenant: capability.target.tenant_key ? { column: capability.target.tenant_key, value: context.tenant_id } : void 0,
|
|
34122
|
-
|
|
35554
|
+
...principalScopeMetadata ? { principal_scope: principalScopeMetadata } : {},
|
|
35555
|
+
visible_row: capability.kind === "proposal" && operation === "insert" ? patch : visibleScalarRecord(capability, current.row),
|
|
34123
35556
|
...resolvedDeduplication ? { deduplication: resolvedDeduplication } : {}
|
|
34124
35557
|
}]
|
|
34125
35558
|
});
|
|
@@ -34134,6 +35567,8 @@ async function callConfiguredTool(input) {
|
|
|
34134
35567
|
capability: capability.name,
|
|
34135
35568
|
columns: capability.visible_columns,
|
|
34136
35569
|
tenant_bound: Boolean(capability.target.tenant_key),
|
|
35570
|
+
principal_bound: Boolean(principalScope3),
|
|
35571
|
+
...principalScopeMetadata ? { principal_scope: principalScopeMetadata } : {},
|
|
34137
35572
|
statement_template: selectTemplate(capability),
|
|
34138
35573
|
parameters_redacted: true
|
|
34139
35574
|
}
|
|
@@ -34148,7 +35583,7 @@ async function callConfiguredTool(input) {
|
|
|
34148
35583
|
type: capability.target.table,
|
|
34149
35584
|
id: objectId
|
|
34150
35585
|
},
|
|
34151
|
-
data:
|
|
35586
|
+
data: visibleScalarRecord(capability, current.row),
|
|
34152
35587
|
trusted_context: {
|
|
34153
35588
|
tenant_id: context.tenant_id,
|
|
34154
35589
|
principal: context.principal,
|
|
@@ -34183,7 +35618,7 @@ async function callConfiguredTool(input) {
|
|
|
34183
35618
|
}
|
|
34184
35619
|
throw error;
|
|
34185
35620
|
}
|
|
34186
|
-
const approvalResult = await maybeAutoApproveProposal({
|
|
35621
|
+
const approvalResult = input.config.governance?.mode === "cloud_linked" ? { proposal, approved: false } : await maybeAutoApproveProposal({
|
|
34187
35622
|
config: input.config,
|
|
34188
35623
|
capability,
|
|
34189
35624
|
store: input.store,
|
|
@@ -34198,7 +35633,8 @@ async function callConfiguredTool(input) {
|
|
|
34198
35633
|
capability: capability.name,
|
|
34199
35634
|
proposal_id: proposal.proposal_id,
|
|
34200
35635
|
source_database_changed: false,
|
|
34201
|
-
approval_status: approvalResult.proposal.state === "approved" ? "approved" : changeSet.approval.status
|
|
35636
|
+
approval_status: approvalResult.proposal.state === "approved" ? "approved" : changeSet.approval.status,
|
|
35637
|
+
...principalScopeMetadata ? { principal_scope: principalScopeMetadata } : {}
|
|
34202
35638
|
},
|
|
34203
35639
|
items: [
|
|
34204
35640
|
{
|
|
@@ -34220,12 +35656,22 @@ async function callConfiguredTool(input) {
|
|
|
34220
35656
|
payload: {
|
|
34221
35657
|
capability: capability.name,
|
|
34222
35658
|
statement_template: selectTemplate(capability),
|
|
34223
|
-
parameters_redacted: true
|
|
35659
|
+
parameters_redacted: true,
|
|
35660
|
+
principal_bound: Boolean(principalScope3),
|
|
35661
|
+
...principalScopeMetadata ? { principal_scope: principalScopeMetadata } : {}
|
|
34224
35662
|
}
|
|
34225
35663
|
});
|
|
34226
35664
|
}
|
|
35665
|
+
await enqueueCloudLinkedProposal({
|
|
35666
|
+
config: input.config,
|
|
35667
|
+
store: input.store,
|
|
35668
|
+
proposal: approvalResult.proposal,
|
|
35669
|
+
evidenceBundleId,
|
|
35670
|
+
queryFingerprint,
|
|
35671
|
+
env: input.env
|
|
35672
|
+
});
|
|
34227
35673
|
return {
|
|
34228
|
-
status: input.config.mode === "shadow" ? "shadow_proposal_created" : approvalResult.proposal.state === "approved" ? "approved" : "review_required",
|
|
35674
|
+
status: input.config.governance?.mode === "cloud_linked" ? "pending_cloud_sync" : input.config.mode === "shadow" ? "shadow_proposal_created" : approvalResult.proposal.state === "approved" ? "approved" : "review_required",
|
|
34229
35675
|
action: capability.name,
|
|
34230
35676
|
proposal_id: approvalResult.proposal.proposal_id,
|
|
34231
35677
|
proposal_version: approvalResult.proposal.proposal_version,
|
|
@@ -34250,6 +35696,7 @@ async function callConfiguredTool(input) {
|
|
|
34250
35696
|
} : {}
|
|
34251
35697
|
},
|
|
34252
35698
|
approval_required: approvalResult.proposal.state === "pending_review",
|
|
35699
|
+
governance: input.config.governance?.mode === "cloud_linked" ? { authority: "synapsor_cloud", state: "pending_cloud_sync", evidence_residency: "metadata_only" } : { authority: "local" },
|
|
34253
35700
|
writeback: changeSet.writeback,
|
|
34254
35701
|
source_database_changed: false,
|
|
34255
35702
|
source_database_mutated: false
|
|
@@ -34264,7 +35711,12 @@ async function recordAggregateRead(input) {
|
|
|
34264
35711
|
const suppressed = groupSize < aggregate.minimum_group_size;
|
|
34265
35712
|
const value = suppressed ? null : finiteAggregateNumber(input.current.row.aggregate_value, "AGGREGATE_VALUE_INVALID");
|
|
34266
35713
|
const createdAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
34267
|
-
const
|
|
35714
|
+
const aggregatePrincipalScope = input.capability.target.principal_scope_key ? {
|
|
35715
|
+
schema_version: protocolVersions.principalScope,
|
|
35716
|
+
column: input.capability.target.principal_scope_key,
|
|
35717
|
+
value_fingerprint: canonicalJsonDigest({ principal: input.context.principal })
|
|
35718
|
+
} : void 0;
|
|
35719
|
+
const evidenceBundleId = stableId("ev", { capability: input.capability.name, tenant: input.context.tenant_id, principal_scope: aggregatePrincipalScope?.value_fingerprint, aggregate, suppressed, value, at: createdAt });
|
|
34268
35720
|
const queryFingerprint = queryFingerprintFor(input.capability, input.context);
|
|
34269
35721
|
await input.store.recordEvidenceBundle({
|
|
34270
35722
|
evidence_bundle_id: evidenceBundleId,
|
|
@@ -34282,6 +35734,7 @@ async function recordAggregateRead(input) {
|
|
|
34282
35734
|
principal: input.context.principal,
|
|
34283
35735
|
tenant_id: input.context.tenant_id,
|
|
34284
35736
|
binding_provenance: input.context.provenance,
|
|
35737
|
+
...aggregatePrincipalScope ? { principal_scope: aggregatePrincipalScope } : {},
|
|
34285
35738
|
aggregate: aggregate.function,
|
|
34286
35739
|
aggregate_column: aggregate.column ?? null,
|
|
34287
35740
|
count_mode: aggregate.count_mode ?? null,
|
|
@@ -34313,6 +35766,8 @@ async function recordAggregateRead(input) {
|
|
|
34313
35766
|
count_mode: aggregate.count_mode ?? null,
|
|
34314
35767
|
fixed_selection: aggregate.selection ?? null,
|
|
34315
35768
|
tenant_bound: Boolean(input.capability.target.tenant_key),
|
|
35769
|
+
principal_bound: Boolean(aggregatePrincipalScope),
|
|
35770
|
+
...aggregatePrincipalScope ? { principal_scope: aggregatePrincipalScope } : {},
|
|
34316
35771
|
minimum_group_size: aggregate.minimum_group_size,
|
|
34317
35772
|
suppressed,
|
|
34318
35773
|
source_member_count_recorded: false,
|
|
@@ -34573,6 +36028,16 @@ function safeToolError(error) {
|
|
|
34573
36028
|
const retryAfter = error instanceof McpRuntimeError && typeof error.details?.retry_after_ms === "number" ? Math.max(1, Math.round(error.details.retry_after_ms)) : void 0;
|
|
34574
36029
|
return { code: "RATE_LIMITED", message: "The trusted tenant request limit was reached. Retry after the current window.", retryable: true, ...retryAfter ? { retry_after_ms: retryAfter } : {} };
|
|
34575
36030
|
}
|
|
36031
|
+
if (runtimeCode === "CLOUD_RATE_LIMITED") {
|
|
36032
|
+
const retryAfter = error instanceof McpRuntimeError && typeof error.details?.retry_after_ms === "number" ? Math.max(1, Math.round(error.details.retry_after_ms)) : DEFAULT_INFRA_RETRY_AFTER_MS;
|
|
36033
|
+
return { code: "RATE_LIMITED", message: "Synapsor Cloud is rate limiting proposal submissions. Retry after the current window.", retryable: true, retry_after_ms: retryAfter };
|
|
36034
|
+
}
|
|
36035
|
+
if (runtimeCode === "CLOUD_TEMPORARILY_UNAVAILABLE") {
|
|
36036
|
+
return temporarilyUnavailableError("Synapsor Cloud is temporarily unavailable. Retry later or enable reviewed durable proposal queueing.", error);
|
|
36037
|
+
}
|
|
36038
|
+
if (runtimeCode && ["CLOUD_RUNNER_AUTHENTICATION_FAILED", "CLOUD_RUNNER_AUTHORIZATION_FAILED", "CLOUD_CONNECTION_CONFLICT"].includes(runtimeCode)) {
|
|
36039
|
+
return { code: "POLICY_VIOLATION", message: "The reviewed Synapsor Cloud authority rejected this Runner connection.", retryable: false };
|
|
36040
|
+
}
|
|
34576
36041
|
if (runtimeCode && (runtimeCode.startsWith("ARGUMENT_") || runtimeCode === "LOOKUP_ARG_MISSING" || runtimeCode === "MODEL_PREDICATE_REJECTED" || runtimeCode === "MODEL_CANNOT_OVERRIDE_BINDING" || runtimeCode === "TRUSTED_BINDING_MISSING" || runtimeCode === "TRUSTED_CONTEXT_MISSING")) {
|
|
34577
36042
|
return { code: "INVALID_ARGUMENT", message: "The tool input or trusted context binding is invalid.", retryable: false };
|
|
34578
36043
|
}
|
|
@@ -34733,9 +36198,24 @@ function errorMessage(error) {
|
|
|
34733
36198
|
if (isRecord5(error) && typeof error.message === "string") return error.message;
|
|
34734
36199
|
return typeof error === "string" ? error : "";
|
|
34735
36200
|
}
|
|
36201
|
+
function effectivePrincipalScope(config, capability, context) {
|
|
36202
|
+
const column = capability.target.principal_scope_key;
|
|
36203
|
+
if (!column) return void 0;
|
|
36204
|
+
const contextConfig = (capability.context ? config.contexts?.[capability.context] : void 0) ?? config.trusted_context;
|
|
36205
|
+
if (!contextConfig) throw new McpRuntimeError("TRUSTED_CONTEXT_MISSING", `Principal-scoped capability ${capability.name} has no trusted context.`);
|
|
36206
|
+
const binding = contextConfig.principal_binding ?? "principal";
|
|
36207
|
+
const value = scalar3(context.principal);
|
|
36208
|
+
const material = { column, binding, provider: contextConfig.provider, value };
|
|
36209
|
+
return {
|
|
36210
|
+
schema_version: protocolVersions.principalScope,
|
|
36211
|
+
...material,
|
|
36212
|
+
value_fingerprint: principalScopeFingerprint(material)
|
|
36213
|
+
};
|
|
36214
|
+
}
|
|
34736
36215
|
function buildChangeSet(input) {
|
|
34737
36216
|
const patch = input.patch;
|
|
34738
36217
|
const before = scalarRecord(input.currentRow);
|
|
36218
|
+
const principalScope3 = effectivePrincipalScope(input.config, input.capability, input.context);
|
|
34739
36219
|
if (isSetCapability(input.capability)) return buildBoundedSetChangeSet(input);
|
|
34740
36220
|
enforcePatchGuards(input.capability, before, patch);
|
|
34741
36221
|
const operation = input.capability.operation?.kind ?? "update";
|
|
@@ -34749,6 +36229,7 @@ function buildChangeSet(input) {
|
|
|
34749
36229
|
if (operation === "insert" && input.resolvedDeduplication) {
|
|
34750
36230
|
for (const component of input.resolvedDeduplication.components) after[component.column] = component.value;
|
|
34751
36231
|
}
|
|
36232
|
+
if (operation === "insert" && principalScope3) after[principalScope3.column] = principalScope3.value;
|
|
34752
36233
|
const writebackMode = capabilityWritebackMode2(input.capability);
|
|
34753
36234
|
const changeSetWritebackMode = writebackMode === "none" ? "read_only" : "trusted_worker_required";
|
|
34754
36235
|
const writebackExecutor = writebackMode === "none" ? "none" : writebackMode === "cloud_worker" ? "cloud_worker" : writebackMode === "direct_sql" ? "sql_update" : capabilityWritebackExecutor(input.capability);
|
|
@@ -34789,6 +36270,7 @@ function buildChangeSet(input) {
|
|
|
34789
36270
|
after,
|
|
34790
36271
|
guards: {
|
|
34791
36272
|
tenant: { column: input.capability.target.tenant_key ?? "__single_tenant_dev", value: input.capability.target.tenant_key ? input.context.tenant_id : "single_tenant_dev" },
|
|
36273
|
+
...principalScope3 ? { principal_scope: principalScope3 } : {},
|
|
34792
36274
|
allowed_columns: input.capability.allowed_columns ?? Object.keys(patch),
|
|
34793
36275
|
...guard ? { expected_version: guard } : {},
|
|
34794
36276
|
...input.capability.operation.version_advance ? { version_advance: input.capability.operation.version_advance } : {},
|
|
@@ -34855,6 +36337,7 @@ function buildChangeSet(input) {
|
|
|
34855
36337
|
after,
|
|
34856
36338
|
guards: {
|
|
34857
36339
|
tenant: { column: input.capability.target.tenant_key ?? "__single_tenant_dev", value: input.capability.target.tenant_key ? input.context.tenant_id : "single_tenant_dev" },
|
|
36340
|
+
...principalScope3 ? { principal_scope: principalScope3 } : {},
|
|
34858
36341
|
allowed_columns: input.capability.allowed_columns ?? Object.keys(patch),
|
|
34859
36342
|
expected_version: guard
|
|
34860
36343
|
},
|
|
@@ -34884,6 +36367,7 @@ function buildChangeSet(input) {
|
|
|
34884
36367
|
};
|
|
34885
36368
|
}
|
|
34886
36369
|
function buildBoundedSetChangeSet(input) {
|
|
36370
|
+
const principalScope3 = effectivePrincipalScope(input.config, input.capability, input.context);
|
|
34887
36371
|
const operation = input.capability.operation;
|
|
34888
36372
|
if (!operation || operation.cardinality !== "set" || !operation.max_rows || !operation.aggregate_bounds?.length) {
|
|
34889
36373
|
throw new McpRuntimeError("SET_GUARDS_REQUIRED", `Bounded set capability ${input.capability.name} is missing reviewed set guards.`);
|
|
@@ -34901,6 +36385,7 @@ function buildBoundedSetChangeSet(input) {
|
|
|
34901
36385
|
if (Object.prototype.hasOwnProperty.call(after, component.column)) throw new McpRuntimeError("BATCH_DEDUP_COLUMN_COLLISION", `Batch deduplication column ${component.column} collides with a patch column.`);
|
|
34902
36386
|
after[component.column] = component.value;
|
|
34903
36387
|
}
|
|
36388
|
+
if (principalScope3) after[principalScope3.column] = principalScope3.value;
|
|
34904
36389
|
return {
|
|
34905
36390
|
primary_key: { column: input.capability.target.primary_key, value: primary.value },
|
|
34906
36391
|
before: {},
|
|
@@ -34985,6 +36470,7 @@ function buildBoundedSetChangeSet(input) {
|
|
|
34985
36470
|
after: { row_count: kind === "set_delete" ? 0 : members.length },
|
|
34986
36471
|
guards: {
|
|
34987
36472
|
tenant: { column: input.capability.target.tenant_key ?? "__single_tenant_dev", value: input.capability.target.tenant_key ? input.context.tenant_id : "single_tenant_dev" },
|
|
36473
|
+
...principalScope3 ? { principal_scope: principalScope3 } : {},
|
|
34988
36474
|
allowed_columns: kind === "set_delete" ? [] : input.capability.allowed_columns ?? Object.keys(input.patch),
|
|
34989
36475
|
...kind === "set_update" && operation.version_advance ? { version_advance: operation.version_advance } : {}
|
|
34990
36476
|
},
|
|
@@ -35257,6 +36743,7 @@ function buildSelect(capability, placeholderStyle) {
|
|
|
35257
36743
|
const fixedTerms = aggregate.selection?.all ?? [];
|
|
35258
36744
|
const where2 = fixedTerms.map((term, index) => `${quoteIdentifier(term.column, placeholderStyle)} = ${placeholderStyle === "$" ? `$${index + 1}` : "?"}`);
|
|
35259
36745
|
if (capability.target.tenant_key) where2.push(`${quoteIdentifier(capability.target.tenant_key, placeholderStyle)} = ${placeholderStyle === "$" ? `$${fixedTerms.length + 1}` : "?"}`);
|
|
36746
|
+
if (capability.target.principal_scope_key) where2.push(`${quoteIdentifier(capability.target.principal_scope_key, placeholderStyle)} = ${placeholderStyle === "$" ? `$${fixedTerms.length + 2}` : "?"}`);
|
|
35260
36747
|
const expression = aggregate.function === "count" && aggregate.count_mode === "rows" ? "COUNT(*)" : `${aggregate.function.toUpperCase()}(${quoteIdentifier(aggregate.column ?? "", placeholderStyle)})`;
|
|
35261
36748
|
return { sql: `SELECT ${expression} AS aggregate_value, COUNT(*) AS group_size FROM ${quoteIdentifier(capability.target.schema, placeholderStyle)}.${quoteIdentifier(capability.target.table, placeholderStyle)}${where2.length ? ` WHERE ${where2.join(" AND ")}` : ""}` };
|
|
35262
36749
|
}
|
|
@@ -35268,45 +36755,60 @@ function buildSelect(capability, placeholderStyle) {
|
|
|
35268
36755
|
const tenantIndex = fixedTerms.length + 1;
|
|
35269
36756
|
where2.push(`${quoteIdentifier(capability.target.tenant_key, placeholderStyle)} = ${placeholderStyle === "$" ? `$${tenantIndex}` : "?"}`);
|
|
35270
36757
|
}
|
|
36758
|
+
if (capability.target.principal_scope_key) {
|
|
36759
|
+
const principalIndex = fixedTerms.length + 2;
|
|
36760
|
+
where2.push(`${quoteIdentifier(capability.target.principal_scope_key, placeholderStyle)} = ${placeholderStyle === "$" ? `$${principalIndex}` : "?"}`);
|
|
36761
|
+
}
|
|
35271
36762
|
const maxRows = capability.operation?.max_rows ?? 0;
|
|
35272
36763
|
return {
|
|
35273
36764
|
sql: `SELECT ${columns} FROM ${quoteIdentifier(capability.target.schema, placeholderStyle)}.${quoteIdentifier(capability.target.table, placeholderStyle)} WHERE ${where2.join(" AND ")} ORDER BY ${quoteIdentifier(capability.target.primary_key, placeholderStyle)} ASC LIMIT ${maxRows + 1}`
|
|
35274
36765
|
};
|
|
35275
36766
|
}
|
|
35276
|
-
const placeholders = placeholderStyle === "$" ? ["$1", "$2"] : ["?", "?"];
|
|
36767
|
+
const placeholders = placeholderStyle === "$" ? ["$1", "$2", "$3"] : ["?", "?", "?"];
|
|
35277
36768
|
const where = [
|
|
35278
36769
|
`${quoteIdentifier(capability.target.primary_key, placeholderStyle)} = ${placeholders[0]}`
|
|
35279
36770
|
];
|
|
35280
36771
|
if (capability.target.tenant_key) {
|
|
35281
36772
|
where.push(`${quoteIdentifier(capability.target.tenant_key, placeholderStyle)} = ${placeholders[1]}`);
|
|
35282
36773
|
}
|
|
36774
|
+
if (capability.target.principal_scope_key) {
|
|
36775
|
+
where.push(`${quoteIdentifier(capability.target.principal_scope_key, placeholderStyle)} = ${placeholders[2]}`);
|
|
36776
|
+
}
|
|
35283
36777
|
const sql = `SELECT ${columns} FROM ${quoteIdentifier(capability.target.schema, placeholderStyle)}.${quoteIdentifier(capability.target.table, placeholderStyle)} WHERE ${where.join(" AND ")} LIMIT ${Math.max(1, capability.max_rows ?? 1)}`;
|
|
35284
36778
|
return { sql };
|
|
35285
36779
|
}
|
|
35286
36780
|
function queryValues(capability, args, context) {
|
|
35287
36781
|
if (capability.kind === "aggregate_read") return [
|
|
35288
36782
|
...(capability.aggregate?.selection?.all ?? []).map((term) => term.value),
|
|
35289
|
-
...capability.target.tenant_key ? [context.tenant_id] : []
|
|
36783
|
+
...capability.target.tenant_key ? [context.tenant_id] : [],
|
|
36784
|
+
...capability.target.principal_scope_key ? [context.principal] : []
|
|
35290
36785
|
];
|
|
35291
36786
|
if (isSetSelectionCapability(capability)) {
|
|
35292
36787
|
return [
|
|
35293
36788
|
...(capability.operation?.selection?.all ?? []).map((term) => term.value),
|
|
35294
|
-
...capability.target.tenant_key ? [context.tenant_id] : []
|
|
36789
|
+
...capability.target.tenant_key ? [context.tenant_id] : [],
|
|
36790
|
+
...capability.target.principal_scope_key ? [context.principal] : []
|
|
35295
36791
|
];
|
|
35296
36792
|
}
|
|
35297
36793
|
const pkValue = args[capability.lookup.id_from_arg];
|
|
35298
36794
|
if (pkValue === void 0) throw new McpRuntimeError("LOOKUP_ARG_MISSING", `${capability.lookup.id_from_arg} is required.`);
|
|
35299
|
-
return
|
|
36795
|
+
return [
|
|
36796
|
+
pkValue,
|
|
36797
|
+
...capability.target.tenant_key ? [context.tenant_id] : [],
|
|
36798
|
+
...capability.target.principal_scope_key ? [context.principal] : []
|
|
36799
|
+
];
|
|
35300
36800
|
}
|
|
35301
36801
|
function readColumns(capability) {
|
|
35302
36802
|
if (capability.kind === "aggregate_read") return [
|
|
35303
36803
|
...capability.aggregate?.column ? [capability.aggregate.column] : [],
|
|
35304
36804
|
...(capability.aggregate?.selection?.all ?? []).map((term) => term.column),
|
|
35305
|
-
...capability.target.tenant_key ? [capability.target.tenant_key] : []
|
|
36805
|
+
...capability.target.tenant_key ? [capability.target.tenant_key] : [],
|
|
36806
|
+
...capability.target.principal_scope_key ? [capability.target.principal_scope_key] : []
|
|
35306
36807
|
];
|
|
35307
36808
|
const columns = new Set(capability.visible_columns);
|
|
35308
36809
|
columns.add(capability.target.primary_key);
|
|
35309
36810
|
if (capability.target.tenant_key) columns.add(capability.target.tenant_key);
|
|
36811
|
+
if (capability.target.principal_scope_key) columns.add(capability.target.principal_scope_key);
|
|
35310
36812
|
if (capability.conflict_guard?.column) columns.add(capability.conflict_guard.column);
|
|
35311
36813
|
for (const term of capability.operation?.selection?.all ?? []) columns.add(term.column);
|
|
35312
36814
|
for (const bound of capability.operation?.aggregate_bounds ?? []) columns.add(bound.column);
|
|
@@ -35518,7 +37020,7 @@ function boundedSetEvidenceItems(capability, context, operation, currentRows, it
|
|
|
35518
37020
|
table: `${capability.target.schema}.${capability.target.table}`,
|
|
35519
37021
|
primary_key: { column: capability.target.primary_key, value: scalar3(row[capability.target.primary_key]) },
|
|
35520
37022
|
tenant: capability.target.tenant_key ? { column: capability.target.tenant_key, value: context.tenant_id } : void 0,
|
|
35521
|
-
visible_row:
|
|
37023
|
+
visible_row: visibleScalarRecord(capability, row)
|
|
35522
37024
|
}));
|
|
35523
37025
|
}
|
|
35524
37026
|
function resolveDeduplication(capability, proposalId, context) {
|
|
@@ -35649,6 +37151,10 @@ async function resourceResult(uri, reader) {
|
|
|
35649
37151
|
};
|
|
35650
37152
|
}
|
|
35651
37153
|
function queryFingerprintFor(capability, context) {
|
|
37154
|
+
const principalScope3 = capability.target.principal_scope_key ? {
|
|
37155
|
+
column: capability.target.principal_scope_key,
|
|
37156
|
+
value_fingerprint: canonicalJsonDigest({ principal: context.principal })
|
|
37157
|
+
} : void 0;
|
|
35652
37158
|
return hashJson({
|
|
35653
37159
|
source: capability.source,
|
|
35654
37160
|
target: capability.target,
|
|
@@ -35657,28 +37163,42 @@ function queryFingerprintFor(capability, context) {
|
|
|
35657
37163
|
aggregate: capability.aggregate,
|
|
35658
37164
|
columns: readColumns(capability),
|
|
35659
37165
|
tenant_bound: Boolean(capability.target.tenant_key),
|
|
35660
|
-
tenant: context.tenant_id
|
|
37166
|
+
tenant: context.tenant_id,
|
|
37167
|
+
...principalScope3 ? { principal_scope: principalScope3 } : {}
|
|
35661
37168
|
});
|
|
35662
37169
|
}
|
|
35663
37170
|
function selectTemplate(capability) {
|
|
35664
37171
|
if (capability.kind === "aggregate_read") {
|
|
35665
37172
|
const aggregate = capability.aggregate;
|
|
35666
37173
|
const expression = aggregate?.function === "count" && aggregate.count_mode === "rows" ? "COUNT(*)" : `${aggregate?.function?.toUpperCase() ?? "AGGREGATE"}(${aggregate?.column ?? "<fixed column>"})`;
|
|
35667
|
-
const
|
|
35668
|
-
if (capability.target.tenant_key)
|
|
35669
|
-
|
|
37174
|
+
const terms2 = (aggregate?.selection?.all ?? []).map((term) => `${term.column} = <fixed>`);
|
|
37175
|
+
if (capability.target.tenant_key) terms2.push(`${capability.target.tenant_key} = <trusted tenant>`);
|
|
37176
|
+
if (capability.target.principal_scope_key) terms2.push(`${capability.target.principal_scope_key} = <trusted principal>`);
|
|
37177
|
+
return `SELECT ${expression}, COUNT(*) AS group_size FROM ${capability.target.schema}.${capability.target.table}${terms2.length ? ` WHERE ${terms2.join(" AND ")}` : ""}`;
|
|
35670
37178
|
}
|
|
35671
37179
|
if (isSetSelectionCapability(capability)) {
|
|
35672
|
-
const
|
|
35673
|
-
if (capability.target.tenant_key)
|
|
35674
|
-
|
|
35675
|
-
|
|
35676
|
-
|
|
37180
|
+
const terms2 = (capability.operation?.selection?.all ?? []).map((term) => `${term.column} = <fixed>`);
|
|
37181
|
+
if (capability.target.tenant_key) terms2.push(`${capability.target.tenant_key} = <trusted tenant>`);
|
|
37182
|
+
if (capability.target.principal_scope_key) terms2.push(`${capability.target.principal_scope_key} = <trusted principal>`);
|
|
37183
|
+
return `SELECT ${readColumns(capability).join(", ")} FROM ${capability.target.schema}.${capability.target.table} WHERE ${terms2.join(" AND ")} ORDER BY ${capability.target.primary_key} ASC LIMIT ${(capability.operation?.max_rows ?? 0) + 1}`;
|
|
37184
|
+
}
|
|
37185
|
+
const terms = [`${capability.target.primary_key} = ?`];
|
|
37186
|
+
if (capability.target.tenant_key) terms.push(`${capability.target.tenant_key} = ?`);
|
|
37187
|
+
if (capability.target.principal_scope_key) terms.push(`${capability.target.principal_scope_key} = ?`);
|
|
37188
|
+
const where = terms.join(" AND ");
|
|
35677
37189
|
return `SELECT ${readColumns(capability).join(", ")} FROM ${capability.target.schema}.${capability.target.table} WHERE ${where} LIMIT ${capability.max_rows ?? 1}`;
|
|
35678
37190
|
}
|
|
35679
37191
|
function scalarRecord(row) {
|
|
35680
37192
|
return Object.fromEntries(Object.entries(row).map(([key, value]) => [key, scalar3(value)]));
|
|
35681
37193
|
}
|
|
37194
|
+
function visibleScalarRecord(capability, row) {
|
|
37195
|
+
const visible = new Set(capability.visible_columns);
|
|
37196
|
+
return Object.fromEntries(Object.entries(row).filter(([column]) => visible.has(column)).map(([key, value]) => [key, scalar3(value)]));
|
|
37197
|
+
}
|
|
37198
|
+
function withoutPrincipalScopeValue(scope) {
|
|
37199
|
+
const { value: _value, ...metadata } = scope;
|
|
37200
|
+
return metadata;
|
|
37201
|
+
}
|
|
35682
37202
|
function scalar3(value) {
|
|
35683
37203
|
if (value === null || value === void 0) return null;
|
|
35684
37204
|
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
|
|
@@ -35743,6 +37263,12 @@ function quoteMysqlIdentifier(identifier) {
|
|
|
35743
37263
|
function operationOf2(job) {
|
|
35744
37264
|
return job.operation ?? "single_row_update";
|
|
35745
37265
|
}
|
|
37266
|
+
function principalScope2(job) {
|
|
37267
|
+
const scope = job.target.principal_scope;
|
|
37268
|
+
if (!scope) return void 0;
|
|
37269
|
+
if (scope.value === void 0) throw new Error("PRINCIPAL_SCOPE_VALUE_REQUIRED");
|
|
37270
|
+
return { column: scope.column, value: scope.value };
|
|
37271
|
+
}
|
|
35746
37272
|
function receiptAuthority2(config) {
|
|
35747
37273
|
return config.receipts?.authority ?? "source_db";
|
|
35748
37274
|
}
|
|
@@ -35771,6 +37297,11 @@ function buildMysqlUpdate(job) {
|
|
|
35771
37297
|
`${quoteMysqlIdentifier(job.target.primary_key.column)} = ?`,
|
|
35772
37298
|
`${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`
|
|
35773
37299
|
];
|
|
37300
|
+
const scope = principalScope2(job);
|
|
37301
|
+
if (scope) {
|
|
37302
|
+
values.push(scope.value);
|
|
37303
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
37304
|
+
}
|
|
35774
37305
|
if (job.conflict_guard.kind === "version_column") {
|
|
35775
37306
|
values.push(job.conflict_guard.expected_value);
|
|
35776
37307
|
where.push(`${quoteMysqlIdentifier(job.conflict_guard.column)} = ?`);
|
|
@@ -35793,13 +37324,17 @@ VALUES (${columns.map(() => "?").join(", ")})`,
|
|
|
35793
37324
|
function buildMysqlDelete(job) {
|
|
35794
37325
|
if (operationOf2(job) !== "single_row_delete") throw new Error("mysql delete builder requires single_row_delete");
|
|
35795
37326
|
if (job.target.primary_key.value === void 0 || job.conflict_guard.kind !== "version_column") throw new Error("mysql delete requires primary-key and exact version guards");
|
|
35796
|
-
|
|
35797
|
-
|
|
35798
|
-
|
|
35799
|
-
|
|
35800
|
-
|
|
35801
|
-
|
|
35802
|
-
}
|
|
37327
|
+
const values = [job.target.primary_key.value, job.target.tenant_guard.value];
|
|
37328
|
+
const where = [`${quoteMysqlIdentifier(job.target.primary_key.column)} = ?`, `${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`];
|
|
37329
|
+
const scope = principalScope2(job);
|
|
37330
|
+
if (scope) {
|
|
37331
|
+
values.push(scope.value);
|
|
37332
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
37333
|
+
}
|
|
37334
|
+
values.push(job.conflict_guard.expected_value);
|
|
37335
|
+
where.push(`${quoteMysqlIdentifier(job.conflict_guard.column)} = ?`);
|
|
37336
|
+
return { sql: `DELETE FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)}
|
|
37337
|
+
WHERE ${where.join("\n AND ")}`, values };
|
|
35803
37338
|
}
|
|
35804
37339
|
function validatePatch2(job, engine) {
|
|
35805
37340
|
const patchColumns = Object.keys(job.patch || {});
|
|
@@ -35807,6 +37342,8 @@ function validatePatch2(job, engine) {
|
|
|
35807
37342
|
const allowed = new Set(job.allowed_columns);
|
|
35808
37343
|
if (allowed.has(job.target.primary_key.column)) throw new Error(`${engine} primary key column must not be patch-allowlisted`);
|
|
35809
37344
|
if (allowed.has(job.target.tenant_guard.column)) throw new Error(`${engine} tenant guard column must not be patch-allowlisted`);
|
|
37345
|
+
const scope = principalScope2(job);
|
|
37346
|
+
if (scope && allowed.has(scope.column)) throw new Error(`${engine} principal scope column must not be patch-allowlisted`);
|
|
35810
37347
|
for (const column of patchColumns) if (!allowed.has(column)) throw new Error(`${engine} patch column not allowlisted: ${column}`);
|
|
35811
37348
|
}
|
|
35812
37349
|
function insertValues2(job) {
|
|
@@ -35821,6 +37358,11 @@ function insertValues2(job) {
|
|
|
35821
37358
|
if (component.source === "proposal_id") proposalIdentity = true;
|
|
35822
37359
|
}
|
|
35823
37360
|
if (!trustedTenant || !proposalIdentity) throw new Error("INSERT_DEDUP_REQUIRED");
|
|
37361
|
+
const scope = principalScope2(job);
|
|
37362
|
+
if (scope) {
|
|
37363
|
+
if (Object.prototype.hasOwnProperty.call(values, scope.column)) throw new Error("PRINCIPAL_SCOPE_COLUMN_COLLISION");
|
|
37364
|
+
values[scope.column] = scope.value;
|
|
37365
|
+
}
|
|
35824
37366
|
return values;
|
|
35825
37367
|
}
|
|
35826
37368
|
function identityForJob2(job, insertedPrimaryKey) {
|
|
@@ -35846,6 +37388,8 @@ function resultHash2(job, status, version) {
|
|
|
35846
37388
|
}
|
|
35847
37389
|
function reconciliationProjection2(job) {
|
|
35848
37390
|
const columns = /* @__PURE__ */ new Set([job.target.primary_key.column, job.target.tenant_guard.column, ...job.allowed_columns]);
|
|
37391
|
+
const scope = principalScope2(job);
|
|
37392
|
+
if (scope) columns.add(scope.column);
|
|
35849
37393
|
if (job.protocol_version === "3.0") {
|
|
35850
37394
|
for (const member of job.frozen_set.members) {
|
|
35851
37395
|
for (const column of Object.keys(member.before)) columns.add(column);
|
|
@@ -35867,39 +37411,68 @@ function reconciliationProjection2(job) {
|
|
|
35867
37411
|
function buildMysqlReconciliationRead(job) {
|
|
35868
37412
|
const projection = reconciliationProjection2(job).map(quoteMysqlIdentifier).join(", ");
|
|
35869
37413
|
if (job.protocol_version === "4.0") {
|
|
37414
|
+
const scope2 = principalScope2(job);
|
|
37415
|
+
const where2 = [`${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`];
|
|
37416
|
+
const values2 = [job.target.tenant_guard.value];
|
|
37417
|
+
if (scope2) {
|
|
37418
|
+
where2.push(`${quoteMysqlIdentifier(scope2.column)} = ?`);
|
|
37419
|
+
values2.push(scope2.value);
|
|
37420
|
+
}
|
|
37421
|
+
values2.push(...job.compensation.members.map((member) => member.primary_key.value));
|
|
35870
37422
|
return {
|
|
35871
37423
|
sql: `SELECT ${projection} FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)}
|
|
35872
|
-
WHERE ${
|
|
37424
|
+
WHERE ${where2.join("\n AND ")}
|
|
35873
37425
|
AND ${quoteMysqlIdentifier(job.target.primary_key.column)} IN (${job.compensation.members.map(() => "?").join(", ")})
|
|
35874
37426
|
ORDER BY ${quoteMysqlIdentifier(job.target.primary_key.column)} ASC`,
|
|
35875
|
-
values:
|
|
37427
|
+
values: values2
|
|
35876
37428
|
};
|
|
35877
37429
|
}
|
|
35878
37430
|
if (job.protocol_version === "3.0") {
|
|
35879
37431
|
const identities = job.frozen_set.members.map(() => "?").join(", ");
|
|
37432
|
+
const scope2 = principalScope2(job);
|
|
37433
|
+
const where2 = [`${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`];
|
|
37434
|
+
const values2 = [job.target.tenant_guard.value];
|
|
37435
|
+
if (scope2) {
|
|
37436
|
+
where2.push(`${quoteMysqlIdentifier(scope2.column)} = ?`);
|
|
37437
|
+
values2.push(scope2.value);
|
|
37438
|
+
}
|
|
37439
|
+
values2.push(...job.frozen_set.members.map((member) => member.primary_key.value));
|
|
35880
37440
|
return {
|
|
35881
37441
|
sql: `SELECT ${projection} FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)}
|
|
35882
|
-
WHERE ${
|
|
37442
|
+
WHERE ${where2.join("\n AND ")}
|
|
35883
37443
|
AND ${quoteMysqlIdentifier(job.target.primary_key.column)} IN (${identities})
|
|
35884
37444
|
ORDER BY ${quoteMysqlIdentifier(job.target.primary_key.column)} ASC`,
|
|
35885
|
-
values:
|
|
37445
|
+
values: values2
|
|
35886
37446
|
};
|
|
35887
37447
|
}
|
|
35888
37448
|
if (operationOf2(job) === "single_row_insert" && job.protocol_version === "2.0" && job.deduplication) {
|
|
37449
|
+
const scope2 = principalScope2(job);
|
|
37450
|
+
const where2 = job.deduplication.components.map((component) => `${quoteMysqlIdentifier(component.column)} = ?`);
|
|
37451
|
+
const values2 = job.deduplication.components.map((component) => component.value);
|
|
37452
|
+
if (scope2) {
|
|
37453
|
+
where2.push(`${quoteMysqlIdentifier(scope2.column)} = ?`);
|
|
37454
|
+
values2.push(scope2.value);
|
|
37455
|
+
}
|
|
35889
37456
|
return {
|
|
35890
37457
|
sql: `SELECT ${projection} FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)}
|
|
35891
|
-
WHERE ${
|
|
37458
|
+
WHERE ${where2.join(" AND ")}
|
|
35892
37459
|
LIMIT 2`,
|
|
35893
|
-
values:
|
|
37460
|
+
values: values2
|
|
35894
37461
|
};
|
|
35895
37462
|
}
|
|
35896
37463
|
if (job.target.primary_key.value === void 0) throw new Error("RECONCILIATION_TARGET_IDENTITY_REQUIRED");
|
|
37464
|
+
const scope = principalScope2(job);
|
|
37465
|
+
const where = [`${quoteMysqlIdentifier(job.target.primary_key.column)} = ?`, `${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`];
|
|
37466
|
+
const values = [job.target.primary_key.value, job.target.tenant_guard.value];
|
|
37467
|
+
if (scope) {
|
|
37468
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
37469
|
+
values.push(scope.value);
|
|
37470
|
+
}
|
|
35897
37471
|
return {
|
|
35898
37472
|
sql: `SELECT ${projection} FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)}
|
|
35899
|
-
WHERE ${
|
|
35900
|
-
AND ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?
|
|
37473
|
+
WHERE ${where.join("\n AND ")}
|
|
35901
37474
|
LIMIT 2`,
|
|
35902
|
-
values
|
|
37475
|
+
values
|
|
35903
37476
|
};
|
|
35904
37477
|
}
|
|
35905
37478
|
async function inspectMysqlWritebackSource(job, databaseUrl) {
|
|
@@ -36162,7 +37735,14 @@ EXISTS (SELECT 1 FROM information_schema.USER_PRIVILEGES WHERE REPLACE(GRANTEE,
|
|
|
36162
37735
|
if (count !== 1) throw new Error(count === 0 ? "VERSION_CONFLICT" : "MULTI_ROW_WRITE_BLOCKED");
|
|
36163
37736
|
let resultVersion;
|
|
36164
37737
|
if (job.protocol_version === "2.0" && job.version_advance) {
|
|
36165
|
-
const
|
|
37738
|
+
const where = [`${quoteMysqlIdentifier(job.target.primary_key.column)} = ?`, `${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`];
|
|
37739
|
+
const values = [job.target.primary_key.value, job.target.tenant_guard.value];
|
|
37740
|
+
const scope = principalScope2(job);
|
|
37741
|
+
if (scope) {
|
|
37742
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
37743
|
+
values.push(scope.value);
|
|
37744
|
+
}
|
|
37745
|
+
const [rows] = await connection.query(`SELECT ${quoteMysqlIdentifier(job.version_advance.column)} AS __synapsor_result_version FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${where.join(" AND ")} FOR UPDATE`, values);
|
|
36166
37746
|
resultVersion = rows[0]?.__synapsor_result_version == null ? void 0 : scalar4(rows[0].__synapsor_result_version);
|
|
36167
37747
|
verifyVersionAdvanced2(job, resultVersion);
|
|
36168
37748
|
}
|
|
@@ -36170,6 +37750,8 @@ EXISTS (SELECT 1 FROM information_schema.USER_PRIVILEGES WHERE REPLACE(GRANTEE,
|
|
|
36170
37750
|
}
|
|
36171
37751
|
function compensationProjection2(job) {
|
|
36172
37752
|
const columns = /* @__PURE__ */ new Set([job.target.primary_key.column, job.target.tenant_guard.column]);
|
|
37753
|
+
const scope = principalScope2(job);
|
|
37754
|
+
if (scope) columns.add(scope.column);
|
|
36173
37755
|
for (const member of job.compensation.members) {
|
|
36174
37756
|
for (const column of Object.keys(member.expected_state)) columns.add(column);
|
|
36175
37757
|
for (const column of Object.keys(member.restore_values ?? {})) columns.add(column);
|
|
@@ -36178,10 +37760,15 @@ function compensationProjection2(job) {
|
|
|
36178
37760
|
return [...columns].sort();
|
|
36179
37761
|
}
|
|
36180
37762
|
async function lockMysqlCompensationMembers(job, connection) {
|
|
36181
|
-
const [
|
|
36182
|
-
|
|
36183
|
-
|
|
36184
|
-
)
|
|
37763
|
+
const where = [`${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`];
|
|
37764
|
+
const values = [job.target.tenant_guard.value];
|
|
37765
|
+
const scope = principalScope2(job);
|
|
37766
|
+
if (scope) {
|
|
37767
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
37768
|
+
values.push(scope.value);
|
|
37769
|
+
}
|
|
37770
|
+
values.push(...job.compensation.members.map((member) => member.primary_key.value));
|
|
37771
|
+
const [rows] = await connection.query(`SELECT ${compensationProjection2(job).map(quoteMysqlIdentifier).join(", ")} FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${where.join(" AND ")} AND ${quoteMysqlIdentifier(job.target.primary_key.column)} IN (${job.compensation.members.map(() => "?").join(", ")}) ORDER BY ${quoteMysqlIdentifier(job.target.primary_key.column)} ASC FOR UPDATE`, values);
|
|
36185
37772
|
return rows;
|
|
36186
37773
|
}
|
|
36187
37774
|
function rowsByCompensationIdentity2(job, rows) {
|
|
@@ -36232,6 +37819,8 @@ async function mutateMysqlCompensation(job, connection) {
|
|
|
36232
37819
|
if (job.operation === "restore_insert") {
|
|
36233
37820
|
for (const member of job.compensation.members) {
|
|
36234
37821
|
const values = { ...member.restore_values, [job.target.primary_key.column]: member.primary_key.value, [job.target.tenant_guard.column]: job.target.tenant_guard.value };
|
|
37822
|
+
const scope = principalScope2(job);
|
|
37823
|
+
if (scope) values[scope.column] = scope.value;
|
|
36235
37824
|
const columns = Object.keys(values).sort();
|
|
36236
37825
|
const [inserted] = await connection.query(`INSERT INTO ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} (${columns.map(quoteMysqlIdentifier).join(", ")}) VALUES (${columns.map(() => "?").join(", ")})`, columns.map((column) => values[column]));
|
|
36237
37826
|
if (affectedRows(inserted) !== 1) throw new Error("COMPENSATION_ATOMICITY_VIOLATION");
|
|
@@ -36241,6 +37830,11 @@ async function mutateMysqlCompensation(job, connection) {
|
|
|
36241
37830
|
for (const member of job.compensation.members) {
|
|
36242
37831
|
const where = [`${quoteMysqlIdentifier(job.target.tenant_guard.column)} <=> ?`, `${quoteMysqlIdentifier(job.target.primary_key.column)} <=> ?`];
|
|
36243
37832
|
const values = [job.target.tenant_guard.value, member.primary_key.value];
|
|
37833
|
+
const scope = principalScope2(job);
|
|
37834
|
+
if (scope) {
|
|
37835
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} <=> ?`);
|
|
37836
|
+
values.push(scope.value);
|
|
37837
|
+
}
|
|
36244
37838
|
for (const [column, value] of Object.entries(member.expected_state)) {
|
|
36245
37839
|
where.push(`${quoteMysqlIdentifier(column)} <=> ?`);
|
|
36246
37840
|
values.push(value);
|
|
@@ -36258,6 +37852,11 @@ async function mutateMysqlCompensation(job, connection) {
|
|
|
36258
37852
|
assignments.push(`${quoteMysqlIdentifier(version.column)} = ${quoteMysqlIdentifier(version.column)} + 1`);
|
|
36259
37853
|
const where = [`${quoteMysqlIdentifier(job.target.tenant_guard.column)} <=> ?`, `${quoteMysqlIdentifier(job.target.primary_key.column)} <=> ?`];
|
|
36260
37854
|
const values = [...columns.map((column) => restoreValues[column]), job.target.tenant_guard.value, member.primary_key.value];
|
|
37855
|
+
const scope = principalScope2(job);
|
|
37856
|
+
if (scope) {
|
|
37857
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} <=> ?`);
|
|
37858
|
+
values.push(scope.value);
|
|
37859
|
+
}
|
|
36261
37860
|
for (const [column, value] of Object.entries(member.expected_state)) {
|
|
36262
37861
|
where.push(`${quoteMysqlIdentifier(column)} <=> ?`);
|
|
36263
37862
|
values.push(value);
|
|
@@ -36302,17 +37901,29 @@ async function mutateMysqlSet(job, connection) {
|
|
|
36302
37901
|
if (job.operation === "set_update") {
|
|
36303
37902
|
const assignments = Object.keys(job.patch).map((column) => `${quoteMysqlIdentifier(column)} = ?`);
|
|
36304
37903
|
assignments.push(`${quoteMysqlIdentifier(job.version_advance.column)} = ${quoteMysqlIdentifier(job.version_advance.column)} + 1`);
|
|
36305
|
-
const [
|
|
36306
|
-
|
|
36307
|
-
|
|
36308
|
-
)
|
|
37904
|
+
const where = [`${quoteMysqlIdentifier(job.target.primary_key.column)} = ?`, `${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`];
|
|
37905
|
+
const values = [...Object.values(job.patch), member.primary_key.value, job.target.tenant_guard.value];
|
|
37906
|
+
const scope = principalScope2(job);
|
|
37907
|
+
if (scope) {
|
|
37908
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
37909
|
+
values.push(scope.value);
|
|
37910
|
+
}
|
|
37911
|
+
where.push(`${quoteMysqlIdentifier(expected.column)} = ?`);
|
|
37912
|
+
values.push(expected.value);
|
|
37913
|
+
const [updated] = await connection.query(`UPDATE ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} SET ${assignments.join(", ")} WHERE ${where.join(" AND ")}`, values);
|
|
36309
37914
|
if (affectedRows(updated) !== 1) throw new Error("SET_ATOMICITY_VIOLATION");
|
|
36310
37915
|
memberEffects.push({ primary_key: member.primary_key, before_digest: member.before_digest, after_digest: member.after_digest });
|
|
36311
37916
|
} else {
|
|
36312
|
-
const [
|
|
36313
|
-
|
|
36314
|
-
|
|
36315
|
-
)
|
|
37917
|
+
const where = [`${quoteMysqlIdentifier(job.target.primary_key.column)} = ?`, `${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`];
|
|
37918
|
+
const values = [member.primary_key.value, job.target.tenant_guard.value];
|
|
37919
|
+
const scope = principalScope2(job);
|
|
37920
|
+
if (scope) {
|
|
37921
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
37922
|
+
values.push(scope.value);
|
|
37923
|
+
}
|
|
37924
|
+
where.push(`${quoteMysqlIdentifier(expected.column)} = ?`);
|
|
37925
|
+
values.push(expected.value);
|
|
37926
|
+
const [deleted] = await connection.query(`DELETE FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${where.join(" AND ")}`, values);
|
|
36316
37927
|
if (affectedRows(deleted) !== 1) throw new Error("SET_ATOMICITY_VIOLATION");
|
|
36317
37928
|
memberEffects.push({ primary_key: member.primary_key, before_digest: member.before_digest, tombstone_digest: member.tombstone_digest });
|
|
36318
37929
|
}
|
|
@@ -36322,10 +37933,16 @@ async function mutateMysqlSet(job, connection) {
|
|
|
36322
37933
|
async function lockMysqlFrozenMembers(job, connection) {
|
|
36323
37934
|
const columns = /* @__PURE__ */ new Set([job.target.primary_key.column]);
|
|
36324
37935
|
for (const member of job.frozen_set.members) for (const column of Object.keys(member.before)) columns.add(column);
|
|
36325
|
-
const [
|
|
36326
|
-
|
|
36327
|
-
|
|
36328
|
-
)
|
|
37936
|
+
const where = [`${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`];
|
|
37937
|
+
const values = [job.target.tenant_guard.value];
|
|
37938
|
+
const scope = principalScope2(job);
|
|
37939
|
+
if (scope) {
|
|
37940
|
+
columns.add(scope.column);
|
|
37941
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
37942
|
+
values.push(scope.value);
|
|
37943
|
+
}
|
|
37944
|
+
values.push(...job.frozen_set.members.map((member) => member.primary_key.value));
|
|
37945
|
+
const [rows] = await connection.query(`SELECT ${[...columns].map(quoteMysqlIdentifier).join(", ")} FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${where.join(" AND ")} AND ${quoteMysqlIdentifier(job.target.primary_key.column)} IN (${job.frozen_set.members.map(() => "?").join(", ")}) ORDER BY ${quoteMysqlIdentifier(job.target.primary_key.column)} ASC FOR UPDATE`, values);
|
|
36329
37946
|
if (rows.length !== job.frozen_set.row_count) return false;
|
|
36330
37947
|
const byIdentity = new Map(rows.map((row) => [JSON.stringify(scalar4(row[job.target.primary_key.column])), row]));
|
|
36331
37948
|
return job.frozen_set.members.every((member) => {
|
|
@@ -36350,16 +37967,20 @@ EXISTS (SELECT 1 FROM information_schema.USER_PRIVILEGES WHERE REPLACE(GRANTEE,
|
|
|
36350
37967
|
}
|
|
36351
37968
|
function validateBatchInsertMember2(job, member) {
|
|
36352
37969
|
const dedupColumns = new Set(member.deduplication?.components.map((component) => component.column));
|
|
36353
|
-
for (const column of Object.keys(member.after)) if (!job.allowed_columns.includes(column) && !dedupColumns.has(column)) throw new Error("BATCH_COLUMN_NOT_ALLOWED");
|
|
37970
|
+
for (const column of Object.keys(member.after)) if (!job.allowed_columns.includes(column) && !dedupColumns.has(column) && column !== job.target.principal_scope?.column) throw new Error("BATCH_COLUMN_NOT_ALLOWED");
|
|
36354
37971
|
if (!dedupColumns.has(job.target.primary_key.column) || !dedupColumns.has(job.target.tenant_guard.column)) throw new Error("BATCH_DEDUP_REQUIRED");
|
|
36355
37972
|
}
|
|
36356
37973
|
async function insertMysqlBatch(job, connection) {
|
|
36357
37974
|
for (const member of job.frozen_set.members) {
|
|
36358
37975
|
const components = member.deduplication.components;
|
|
36359
|
-
const
|
|
36360
|
-
|
|
36361
|
-
|
|
36362
|
-
)
|
|
37976
|
+
const where = components.map((component) => `${quoteMysqlIdentifier(component.column)} = ?`);
|
|
37977
|
+
const values = components.map((component) => component.value);
|
|
37978
|
+
const scope = principalScope2(job);
|
|
37979
|
+
if (scope) {
|
|
37980
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
37981
|
+
values.push(scope.value);
|
|
37982
|
+
}
|
|
37983
|
+
const [existing] = await connection.query(`SELECT 1 AS found FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${where.join(" AND ")} LIMIT 1 FOR UPDATE`, values);
|
|
36363
37984
|
if (existing[0]) return { status: "conflict", affectedRows: 0, code: "INSERT_DEDUP_CONFLICT", targetIdentity: identityForJob2(job) };
|
|
36364
37985
|
}
|
|
36365
37986
|
const memberEffects = [];
|
|
@@ -36382,16 +38003,29 @@ async function lockTargetRow2(job, connection) {
|
|
|
36382
38003
|
...job.conflict_guard.kind === "version_column" ? [`${quoteMysqlIdentifier(job.conflict_guard.column)} AS __synapsor_conflict_value`] : [],
|
|
36383
38004
|
...inverseColumns.filter((column) => job.conflict_guard.kind !== "version_column" || column !== job.conflict_guard.column).map(quoteMysqlIdentifier)
|
|
36384
38005
|
].join(", ");
|
|
38006
|
+
const where = [`${quoteMysqlIdentifier(job.target.primary_key.column)} = ?`, `${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?`];
|
|
38007
|
+
const values = [job.target.primary_key.value, job.target.tenant_guard.value];
|
|
38008
|
+
const scope = principalScope2(job);
|
|
38009
|
+
if (scope) {
|
|
38010
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
38011
|
+
values.push(scope.value);
|
|
38012
|
+
}
|
|
36385
38013
|
const [rows] = await connection.query(`SELECT ${projection} FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)}
|
|
36386
|
-
WHERE ${
|
|
36387
|
-
|
|
36388
|
-
FOR UPDATE`, [job.target.primary_key.value, job.target.tenant_guard.value]);
|
|
38014
|
+
WHERE ${where.join("\n AND ")}
|
|
38015
|
+
FOR UPDATE`, values);
|
|
36389
38016
|
return rows[0];
|
|
36390
38017
|
}
|
|
36391
38018
|
async function insertMysql(job, connection) {
|
|
36392
38019
|
if (job.protocol_version !== "2.0" || !job.deduplication) throw new Error("INSERT_DEDUP_REQUIRED");
|
|
36393
38020
|
const components = job.deduplication.components;
|
|
36394
|
-
const
|
|
38021
|
+
const where = components.map((component) => `${quoteMysqlIdentifier(component.column)} = ?`);
|
|
38022
|
+
const values = components.map((component) => component.value);
|
|
38023
|
+
const scope = principalScope2(job);
|
|
38024
|
+
if (scope) {
|
|
38025
|
+
where.push(`${quoteMysqlIdentifier(scope.column)} = ?`);
|
|
38026
|
+
values.push(scope.value);
|
|
38027
|
+
}
|
|
38028
|
+
const [existing] = await connection.query(`SELECT ${quoteMysqlIdentifier(job.target.primary_key.column)} AS __synapsor_primary_key FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${where.join(" AND ")}`, values);
|
|
36395
38029
|
if (existing[0]) return { status: "conflict", affectedRows: 0, code: "INSERT_DEDUP_CONFLICT", targetIdentity: identityForJob2(job, existing[0].__synapsor_primary_key) };
|
|
36396
38030
|
const insertion = buildMysqlInsert(job);
|
|
36397
38031
|
const [inserted] = await connection.query(insertion.sql, insertion.values);
|
|
@@ -36431,10 +38065,13 @@ function resultFromExistingReceipt2(job, config, status, hash) {
|
|
|
36431
38065
|
}
|
|
36432
38066
|
function resultFromOutcome2(job, config, outcome, overrideCode, overrideHash) {
|
|
36433
38067
|
const hash = overrideHash ?? resultHash2(job, outcome.status, outcome.resultVersion);
|
|
36434
|
-
if (job.protocol_version === "4.0") return { protocol_version: "4.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority2(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identities: identityForJob2(job), member_effects: outcome.memberEffects ?? [], ...outcome.status === "applied" || outcome.status === "already_applied" ? { inverse: outcome.inverse ?? compensationInverseFromJob(job) } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
36435
|
-
if (job.protocol_version === "3.0") return { protocol_version: "3.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority2(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identities: identityForJob2(job), set_digest: job.frozen_set.set_digest, member_effects: outcome.memberEffects ?? [], ...job.inverse_capture && (outcome.status === "applied" || outcome.status === "already_applied") ? { inverse: job.inverse_capture } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
36436
|
-
if (job.protocol_version !== "2.0") return { protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: outcome.status, affected_rows: outcome.affectedRows, result_version: outcome.resultVersion == null ? void 0 : String(outcome.resultVersion), result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
36437
|
-
return { protocol_version: "2.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority2(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identity: outcome.targetIdentity, result_version: outcome.resultVersion, before_digest: outcome.beforeDigest, after_digest: outcome.afterDigest, tombstone_digest: outcome.tombstoneDigest, ...job.inverse_capture && (outcome.status === "applied" || outcome.status === "already_applied") ? { inverse: job.inverse_capture } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() };
|
|
38068
|
+
if (job.protocol_version === "4.0") return wireResult2({ protocol_version: "4.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority2(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identities: identityForJob2(job), member_effects: outcome.memberEffects ?? [], ...outcome.status === "applied" || outcome.status === "already_applied" ? { inverse: outcome.inverse ?? compensationInverseFromJob(job) } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
38069
|
+
if (job.protocol_version === "3.0") return wireResult2({ protocol_version: "3.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority2(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identities: identityForJob2(job), set_digest: job.frozen_set.set_digest, member_effects: outcome.memberEffects ?? [], ...job.inverse_capture && (outcome.status === "applied" || outcome.status === "already_applied") ? { inverse: job.inverse_capture } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
38070
|
+
if (job.protocol_version !== "2.0") return wireResult2({ protocol_version: "1.0", job_id: job.job_id, runner_id: config.runnerId, status: outcome.status, affected_rows: outcome.affectedRows, result_version: outcome.resultVersion == null ? void 0 : String(outcome.resultVersion), result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
38071
|
+
return wireResult2({ protocol_version: "2.0", job_id: job.job_id, runner_id: config.runnerId, operation: job.operation, receipt_authority: receiptAuthority2(config), status: outcome.status, affected_rows: outcome.affectedRows, target_identity: outcome.targetIdentity, result_version: outcome.resultVersion, before_digest: outcome.beforeDigest, after_digest: outcome.afterDigest, tombstone_digest: outcome.tombstoneDigest, ...job.inverse_capture && (outcome.status === "applied" || outcome.status === "already_applied") ? { inverse: job.inverse_capture } : {}, result_hash: hash, error_code: outcome.code ?? overrideCode, completed_at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
38072
|
+
}
|
|
38073
|
+
function wireResult2(result) {
|
|
38074
|
+
return Object.fromEntries(Object.entries(result).filter(([, value]) => value !== void 0));
|
|
36438
38075
|
}
|
|
36439
38076
|
function failedResult2(job, config, code) {
|
|
36440
38077
|
return resultFromOutcome2(job, config, { status: "failed", affectedRows: 0, code, targetIdentity: identityForJob2(job) });
|
|
@@ -37383,6 +39020,7 @@ function parseAgentDsl(source) {
|
|
|
37383
39020
|
}
|
|
37384
39021
|
function compileAgentDslWithWarnings(source) {
|
|
37385
39022
|
const ast = parseAgentDsl(source);
|
|
39023
|
+
validatePrincipalScopeContexts(ast);
|
|
37386
39024
|
const contexts = ast.contexts.map((context) => ({
|
|
37387
39025
|
name: context.name,
|
|
37388
39026
|
bindings: context.bindings,
|
|
@@ -37403,6 +39041,7 @@ function compileAgentDslWithWarnings(source) {
|
|
|
37403
39041
|
table: capability.table,
|
|
37404
39042
|
primary_key: capability.primaryKey,
|
|
37405
39043
|
...capability.tenantKey ? { tenant_key: capability.tenantKey } : {},
|
|
39044
|
+
...capability.principalScopeKey ? { principal_scope_key: capability.principalScopeKey } : {},
|
|
37406
39045
|
...capability.conflictKey ? { conflict_key: capability.conflictKey } : {}
|
|
37407
39046
|
},
|
|
37408
39047
|
args: specArgsFromDsl(capability.args),
|
|
@@ -37478,6 +39117,25 @@ function compileAgentDslWithWarnings(source) {
|
|
|
37478
39117
|
assertValidContract(contract);
|
|
37479
39118
|
return { contract: normalizeContract(contract), warnings: collectDslWarnings(ast) };
|
|
37480
39119
|
}
|
|
39120
|
+
function validatePrincipalScopeContexts(ast) {
|
|
39121
|
+
const contexts = new Map(ast.contexts.map((context) => [context.name, context]));
|
|
39122
|
+
for (const capability of ast.capabilities) {
|
|
39123
|
+
if (!capability.principalScopeKey)
|
|
39124
|
+
continue;
|
|
39125
|
+
const context = contexts.get(capability.context);
|
|
39126
|
+
const principalBinding = context?.principalBinding;
|
|
39127
|
+
const binding = principalBinding ? context?.bindings.find((item) => item.name === principalBinding) : void 0;
|
|
39128
|
+
if (!context || !principalBinding || !binding || binding.required !== true) {
|
|
39129
|
+
throw dslError(capability.line ?? 1, 1, "PRINCIPAL_SCOPE_BINDING_REQUIRED", `${capability.name} PRINCIPAL SCOPE KEY requires a context with a required PRINCIPAL BINDING`);
|
|
39130
|
+
}
|
|
39131
|
+
if (Object.prototype.hasOwnProperty.call(capability.args, capability.principalScopeKey)) {
|
|
39132
|
+
throw dslError(capability.line ?? 1, 1, "MODEL_CONTROLLED_PRINCIPAL_SCOPE", `${capability.principalScopeKey} cannot be both a trusted principal scope and a model-facing ARG`);
|
|
39133
|
+
}
|
|
39134
|
+
if (capability.proposal?.allowedFields.includes(capability.principalScopeKey) || capability.proposal?.patch[capability.principalScopeKey]) {
|
|
39135
|
+
throw dslError(capability.line ?? 1, 1, "PRINCIPAL_SCOPE_WRITE_FORBIDDEN", `${capability.principalScopeKey} is forced from trusted context and cannot be model-writeable`);
|
|
39136
|
+
}
|
|
39137
|
+
}
|
|
39138
|
+
}
|
|
37481
39139
|
function validateAgentDsl(source) {
|
|
37482
39140
|
try {
|
|
37483
39141
|
const result = compileAgentDslWithWarnings(source);
|
|
@@ -37616,6 +39274,13 @@ function parseCapabilityBlock(block) {
|
|
|
37616
39274
|
capability.tenantKey = tenant[1];
|
|
37617
39275
|
continue;
|
|
37618
39276
|
}
|
|
39277
|
+
const principalScope3 = item.text.match(/^PRINCIPAL\s+SCOPE\s+KEY\s+([A-Za-z_][A-Za-z0-9_]*)$/i);
|
|
39278
|
+
if (principalScope3?.[1]) {
|
|
39279
|
+
if (capability.principalScopeKey)
|
|
39280
|
+
throw dslError(item.line, 1, "DUPLICATE_PRINCIPAL_SCOPE", `${block.name} declares PRINCIPAL SCOPE KEY more than once`);
|
|
39281
|
+
capability.principalScopeKey = principalScope3[1];
|
|
39282
|
+
continue;
|
|
39283
|
+
}
|
|
37619
39284
|
const conflict = item.text.match(/^CONFLICT\s+GUARD\s+([A-Za-z_][A-Za-z0-9_]*)$/i);
|
|
37620
39285
|
if (conflict?.[1]) {
|
|
37621
39286
|
capability.conflictKey = conflict[1];
|
|
@@ -38549,6 +40214,10 @@ async function handleRequest(input) {
|
|
|
38549
40214
|
sendJson(response, 403, { ok: false, error: "CSRF token required for proposal review actions" });
|
|
38550
40215
|
return;
|
|
38551
40216
|
}
|
|
40217
|
+
if (await cloudLinkedGovernance(configPath)) {
|
|
40218
|
+
sendJson(response, 403, { ok: false, error: "Cloud-linked proposals must be reviewed in Synapsor Cloud; local approval is disabled." });
|
|
40219
|
+
return;
|
|
40220
|
+
}
|
|
38552
40221
|
if (await signedIdentityRequired(configPath)) {
|
|
38553
40222
|
sendJson(response, 403, { ok: false, error: "This Runner requires a signed operator identity. Approve with the CLI using --identity and --identity-key." });
|
|
38554
40223
|
return;
|
|
@@ -38574,6 +40243,10 @@ async function handleRequest(input) {
|
|
|
38574
40243
|
sendJson(response, 403, { ok: false, error: "CSRF token required for proposal review actions" });
|
|
38575
40244
|
return;
|
|
38576
40245
|
}
|
|
40246
|
+
if (await cloudLinkedGovernance(configPath)) {
|
|
40247
|
+
sendJson(response, 403, { ok: false, error: "Cloud-linked proposals must be reviewed in Synapsor Cloud; local rejection is disabled." });
|
|
40248
|
+
return;
|
|
40249
|
+
}
|
|
38577
40250
|
if (await signedIdentityRequired(configPath)) {
|
|
38578
40251
|
sendJson(response, 403, { ok: false, error: "This Runner requires a signed operator identity. Reject with the CLI using --identity and --identity-key." });
|
|
38579
40252
|
return;
|
|
@@ -38615,6 +40288,10 @@ async function signedIdentityRequired(configPath) {
|
|
|
38615
40288
|
const config = await readRunnerConfig(configPath);
|
|
38616
40289
|
return isRecord6(config.operator_identity) && ["signed_key", "jwt_oidc"].includes(String(config.operator_identity.provider));
|
|
38617
40290
|
}
|
|
40291
|
+
async function cloudLinkedGovernance(configPath) {
|
|
40292
|
+
const config = await readRunnerConfig(configPath);
|
|
40293
|
+
return isRecord6(config.governance) && config.governance.mode === "cloud_linked";
|
|
40294
|
+
}
|
|
38618
40295
|
function buildSummary(config, configPath, storePath) {
|
|
38619
40296
|
const validation = validateRunnerCapabilityConfig(config);
|
|
38620
40297
|
const sources = Object.fromEntries(Object.entries(asRecord(config.sources)).map(([name, source]) => {
|
|
@@ -39778,6 +41455,17 @@ function lintContract(contract, options = {}) {
|
|
|
39778
41455
|
add({ code: "KEPT_OUT_REVIEW_NOT_RECORDED", severity: "warning", path: `${base}.kept_out_fields`, message: `${capability.name} does not record an explicit kept-out field review; lint cannot infer which columns are sensitive.` });
|
|
39779
41456
|
}
|
|
39780
41457
|
if (capability.evidence?.required !== true) add({ code: "EVIDENCE_NOT_REQUIRED", severity: "warning", path: `${base}.evidence.required`, message: `${capability.name} does not require evidence.` });
|
|
41458
|
+
if (!capability.subject.principal_scope_key) {
|
|
41459
|
+
const possibleOwnerFields = [.../* @__PURE__ */ new Set([...capability.visible_fields, ...capability.kept_out_fields ?? []])].filter((field) => /^(?:assigned_to|assignee_id|owner_id|principal_id|user_id|case_manager_id)$/i.test(field));
|
|
41460
|
+
if (possibleOwnerFields.length > 0) {
|
|
41461
|
+
add({
|
|
41462
|
+
code: "PRINCIPAL_SCOPE_REVIEW_RECOMMENDED",
|
|
41463
|
+
severity: "info",
|
|
41464
|
+
path: `${base}.subject.principal_scope_key`,
|
|
41465
|
+
message: `${capability.name} includes ${possibleOwnerFields.join(", ")}, which may represent row ownership. Review whether PRINCIPAL SCOPE KEY is appropriate; this is only a naming heuristic and is not data classification.`
|
|
41466
|
+
});
|
|
41467
|
+
}
|
|
41468
|
+
}
|
|
39781
41469
|
for (const [argName, arg] of Object.entries(capability.args)) lintArgument(add, `${base}.args.${argName}`, argName, arg);
|
|
39782
41470
|
if (options.runnerConfig && capability.source && !configSources.has(capability.source)) {
|
|
39783
41471
|
add({ code: "RUNNER_SOURCE_UNRESOLVED", severity: "error", path: `${base}.source`, message: `${capability.name} references source ${capability.source}, which is absent from runner config.` });
|
|
@@ -39836,6 +41524,14 @@ function explainContext(context) {
|
|
|
39836
41524
|
function explainCapability(capability, context) {
|
|
39837
41525
|
const proposal = capability.proposal;
|
|
39838
41526
|
const selection = (proposal?.operation?.selection ?? capability.aggregate?.selection)?.all.map((term) => `${term.column} = ${scalarText(term.value)}`);
|
|
41527
|
+
const principalBinding = context?.bindings.find((binding) => binding.name === context.principal_binding);
|
|
41528
|
+
const tenantColumn = capability.subject.tenant_key;
|
|
41529
|
+
const principalColumn = capability.subject.principal_scope_key;
|
|
41530
|
+
const effectivePredicate = [
|
|
41531
|
+
...tenantColumn ? [`${tenantColumn} = <trusted tenant>`] : [],
|
|
41532
|
+
...principalColumn ? [`${principalColumn} = <trusted principal>`] : [],
|
|
41533
|
+
...(proposal?.operation?.selection ?? capability.aggregate?.selection)?.all.map((term) => `${term.column} ${term.operator} <reviewed value>`) ?? []
|
|
41534
|
+
].join(" AND ") || "no row predicate declared";
|
|
39839
41535
|
return {
|
|
39840
41536
|
name: capability.name,
|
|
39841
41537
|
...capability.description ? { description: capability.description } : {},
|
|
@@ -39846,8 +41542,16 @@ function explainCapability(capability, context) {
|
|
|
39846
41542
|
target: capability.subject.resource ?? ([capability.subject.schema, capability.subject.table].filter(Boolean).join(".") || "unresolved target"),
|
|
39847
41543
|
trusted_scope: [
|
|
39848
41544
|
...context?.tenant_binding ? [`tenant from context binding ${context.tenant_binding}`] : capability.subject.single_tenant_dev ? ["single-tenant development scope"] : ["no tenant binding declared"],
|
|
39849
|
-
...context?.principal_binding ? [`principal from context binding ${context.principal_binding}`] : ["no principal binding declared"]
|
|
41545
|
+
...principalColumn && context?.principal_binding ? [`principal row lock ${principalColumn} from required ${principalBinding?.source ?? "trusted"} binding ${context.principal_binding}`] : context?.principal_binding ? [`principal identity from context binding ${context.principal_binding}; no principal row lock declared`] : ["no principal binding declared"]
|
|
39850
41546
|
],
|
|
41547
|
+
...tenantColumn || principalColumn ? { row_scope: {
|
|
41548
|
+
...tenantColumn ? { tenant_column: tenantColumn } : {},
|
|
41549
|
+
...context?.tenant_binding ? { tenant_binding: context.tenant_binding } : {},
|
|
41550
|
+
...principalColumn ? { principal_column: principalColumn } : {},
|
|
41551
|
+
...context?.principal_binding ? { principal_binding: context.principal_binding } : {},
|
|
41552
|
+
...principalBinding ? { principal_provider: principalBinding.source, principal_required: principalBinding.required === true } : {},
|
|
41553
|
+
effective_predicate: effectivePredicate
|
|
41554
|
+
} } : {},
|
|
39851
41555
|
arguments: Object.entries(capability.args).map(([name, arg]) => explainArgument(name, arg)),
|
|
39852
41556
|
...capability.lookup ? { lookup: `${capability.subject.primary_key ?? "primary key"} from argument ${capability.lookup.id_from_arg}` } : {},
|
|
39853
41557
|
...selection?.length ? { fixed_selection: selection } : {},
|
|
@@ -39925,6 +41629,11 @@ function formatExplanationMarkdown(explanation) {
|
|
|
39925
41629
|
lines.push(`### ${capability.name}`, "", capability.description ?? "No capability description was provided.", "");
|
|
39926
41630
|
lines.push(`- Kind: ${capability.kind}`, `- Target: \`${capability.target}\``, `- Context: \`${capability.context}\``);
|
|
39927
41631
|
for (const scope of capability.trusted_scope) lines.push(`- Trusted scope: ${scope}`);
|
|
41632
|
+
if (capability.row_scope) {
|
|
41633
|
+
if (capability.row_scope.tenant_column) lines.push(`- Tenant row lock: \`${capability.row_scope.tenant_column}\` from binding \`${capability.row_scope.tenant_binding ?? "not declared"}\``);
|
|
41634
|
+
if (capability.row_scope.principal_column) lines.push(`- Principal row lock: \`${capability.row_scope.principal_column}\` from ${capability.row_scope.principal_required ? "required " : ""}${capability.row_scope.principal_provider ?? "trusted"} binding \`${capability.row_scope.principal_binding ?? "not declared"}\``);
|
|
41635
|
+
lines.push(`- Effective row predicate: \`${capability.row_scope.effective_predicate}\``);
|
|
41636
|
+
}
|
|
39928
41637
|
lines.push(`- Visible fields: ${capability.visible_fields.map((field) => `\`${field}\``).join(", ") || "none"}`);
|
|
39929
41638
|
lines.push(`- Kept out: ${capability.kept_out_fields.map((field) => `\`${field}\``).join(", ") || "no explicit list"}`);
|
|
39930
41639
|
lines.push(`- Evidence: ${capability.evidence}`);
|
|
@@ -39977,6 +41686,7 @@ import { isDeepStrictEqual } from "node:util";
|
|
|
39977
41686
|
var kinds = /* @__PURE__ */ new Set([
|
|
39978
41687
|
"tool_allow",
|
|
39979
41688
|
"tool_deny",
|
|
41689
|
+
"cross_principal_deny",
|
|
39980
41690
|
"hide_fields",
|
|
39981
41691
|
"argument_constraint",
|
|
39982
41692
|
"transition_guard",
|
|
@@ -40119,6 +41829,53 @@ async function runStaticAssertion(test, config) {
|
|
|
40119
41829
|
async function runLiveAssertion(test, config, env, storePath) {
|
|
40120
41830
|
const trusted = test.trusted_context;
|
|
40121
41831
|
if (!trusted) throw new ContractAssertionFailure("TRUSTED_CONTEXT_REQUIRED", `${test.kind} requires trusted_context in test setup`);
|
|
41832
|
+
if (test.kind === "cross_principal_deny") {
|
|
41833
|
+
const other = test.other_trusted_context;
|
|
41834
|
+
if (!other) throw new ContractAssertionFailure("OTHER_TRUSTED_CONTEXT_REQUIRED", "cross_principal_deny requires other_trusted_context");
|
|
41835
|
+
if (other.tenant_id !== trusted.tenant_id) throw new ContractAssertionFailure("CROSS_PRINCIPAL_TENANT_MISMATCH", "cross_principal_deny contexts must use the same tenant");
|
|
41836
|
+
if (other.principal === trusted.principal) throw new ContractAssertionFailure("CROSS_PRINCIPAL_IDENTITY_MATCH", "cross_principal_deny contexts must use different principals");
|
|
41837
|
+
const ownerRuntime = createMcpRuntime(config, {
|
|
41838
|
+
env,
|
|
41839
|
+
storePath,
|
|
41840
|
+
resultFormat: 2,
|
|
41841
|
+
trustedContext: { tenant_id: trusted.tenant_id, principal: trusted.principal, provenance: trusted.provenance ?? "static_dev" }
|
|
41842
|
+
});
|
|
41843
|
+
const deniedRuntime = createMcpRuntime(config, {
|
|
41844
|
+
env,
|
|
41845
|
+
storePath,
|
|
41846
|
+
resultFormat: 2,
|
|
41847
|
+
trustedContext: { tenant_id: other.tenant_id, principal: other.principal, provenance: other.provenance ?? "static_dev" }
|
|
41848
|
+
});
|
|
41849
|
+
try {
|
|
41850
|
+
const allowed = await ownerRuntime.callTool(test.capability, test.args ?? {});
|
|
41851
|
+
if (allowed.ok !== true) throw new ContractAssertionFailure("OWNER_ACCESS_NOT_PROVEN", "the owning principal could not access the reviewed row");
|
|
41852
|
+
const denied = await deniedRuntime.callTool(test.capability, test.args ?? {});
|
|
41853
|
+
const code = isRecord8(denied.error) && typeof denied.error.code === "string" ? denied.error.code : void 0;
|
|
41854
|
+
const expected = test.expected_code ?? "NOT_FOUND_IN_TENANT";
|
|
41855
|
+
if (denied.ok === true || code !== expected) throw new ContractAssertionFailure("CROSS_PRINCIPAL_DENIAL_MISMATCH", `expected generic ${expected}, got ${code ?? "success"}`);
|
|
41856
|
+
const serialized = JSON.stringify(denied);
|
|
41857
|
+
const deniedEvidenceHandle = isRecord8(denied.evidence) && typeof denied.evidence.bundle_id === "string" ? denied.evidence.bundle_id : void 0;
|
|
41858
|
+
const deniedProposalHandle = isRecord8(denied.proposal) && typeof denied.proposal.id === "string" ? denied.proposal.id : void 0;
|
|
41859
|
+
if (deniedEvidenceHandle || deniedProposalHandle || /\b(?:ev|wrp|receipt|replay)_[A-Za-z0-9_.-]+\b/.test(serialized)) {
|
|
41860
|
+
throw new ContractAssertionFailure("CROSS_PRINCIPAL_HANDLE_LEAK", "denied result exposed a local resource handle");
|
|
41861
|
+
}
|
|
41862
|
+
const evidenceId = isRecord8(allowed.evidence) && typeof allowed.evidence.bundle_id === "string" ? allowed.evidence.bundle_id : void 0;
|
|
41863
|
+
if (!evidenceId) throw new ContractAssertionFailure("OWNER_EVIDENCE_NOT_PROVEN", "the owning principal call did not create an evidence handle");
|
|
41864
|
+
try {
|
|
41865
|
+
await deniedRuntime.readResource(`synapsor://evidence/${evidenceId}`);
|
|
41866
|
+
throw new ContractAssertionFailure("CROSS_PRINCIPAL_HANDLE_LEAK", "another principal could read the owner's evidence handle");
|
|
41867
|
+
} catch (error) {
|
|
41868
|
+
if (error instanceof ContractAssertionFailure) throw error;
|
|
41869
|
+
if (!isRecord8(error) || error.code !== "RESOURCE_NOT_FOUND") {
|
|
41870
|
+
throw new ContractAssertionFailure("CROSS_PRINCIPAL_HANDLE_DENIAL_MISMATCH", "another principal's evidence handle did not fail with generic RESOURCE_NOT_FOUND");
|
|
41871
|
+
}
|
|
41872
|
+
}
|
|
41873
|
+
return;
|
|
41874
|
+
} finally {
|
|
41875
|
+
await ownerRuntime.close();
|
|
41876
|
+
await deniedRuntime.close();
|
|
41877
|
+
}
|
|
41878
|
+
}
|
|
40122
41879
|
const runtime = createMcpRuntime(config, {
|
|
40123
41880
|
env,
|
|
40124
41881
|
storePath,
|
|
@@ -40161,7 +41918,7 @@ async function runLiveAssertion(test, config, env, storePath) {
|
|
|
40161
41918
|
}
|
|
40162
41919
|
function validateAssertion(raw, index, ids) {
|
|
40163
41920
|
if (!isRecord8(raw)) throw new Error(`CONTRACT_TEST_INVALID: tests[${index}] must be an object`);
|
|
40164
|
-
rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["id", "kind", "capability", "args", "trusted_context", "expected_code", "fields", "argument", "expected"]), `$.tests[${index}]`);
|
|
41921
|
+
rejectUnknownKeys(raw, /* @__PURE__ */ new Set(["id", "kind", "capability", "args", "trusted_context", "other_trusted_context", "expected_code", "fields", "argument", "expected"]), `$.tests[${index}]`);
|
|
40165
41922
|
const id = requiredString(raw.id, `tests[${index}].id`);
|
|
40166
41923
|
if (ids.has(id)) throw new Error(`CONTRACT_TEST_ID_DUPLICATE: ${id}`);
|
|
40167
41924
|
ids.add(id);
|
|
@@ -40174,6 +41931,7 @@ function validateAssertion(raw, index, ids) {
|
|
|
40174
41931
|
capability,
|
|
40175
41932
|
...isRecord8(raw.args) ? { args: raw.args } : {},
|
|
40176
41933
|
...isRecord8(raw.trusted_context) ? { trusted_context: trustedContextFromManifest(raw.trusted_context, id) } : {},
|
|
41934
|
+
...isRecord8(raw.other_trusted_context) ? { other_trusted_context: trustedContextFromManifest(raw.other_trusted_context, `${id}.other_trusted_context`) } : {},
|
|
40177
41935
|
...typeof raw.expected_code === "string" ? { expected_code: raw.expected_code } : {},
|
|
40178
41936
|
...Array.isArray(raw.fields) ? { fields: raw.fields.map(String) } : {},
|
|
40179
41937
|
...typeof raw.argument === "string" ? { argument: raw.argument } : {},
|
|
@@ -40203,7 +41961,7 @@ function liveEngine(config) {
|
|
|
40203
41961
|
return engines.size === 1 ? [...engines][0] : void 0;
|
|
40204
41962
|
}
|
|
40205
41963
|
function requiresLive(test) {
|
|
40206
|
-
return ["tool_allow", "tool_deny", "source_unchanged_before_approval"].includes(test.kind);
|
|
41964
|
+
return ["tool_allow", "tool_deny", "cross_principal_deny", "source_unchanged_before_approval"].includes(test.kind);
|
|
40207
41965
|
}
|
|
40208
41966
|
function trustedContextFromManifest(record, id) {
|
|
40209
41967
|
const provenance = record.provenance;
|
|
@@ -40995,7 +42753,7 @@ function rate(value) {
|
|
|
40995
42753
|
// apps/runner/package.json
|
|
40996
42754
|
var package_default = {
|
|
40997
42755
|
name: "@synapsor/runner",
|
|
40998
|
-
version: "1.4.
|
|
42756
|
+
version: "1.4.122",
|
|
40999
42757
|
description: "Stop giving AI agents execute_sql; expose reviewed Postgres/MySQL MCP actions with proposals, approval, writeback, and replay.",
|
|
41000
42758
|
license: "Apache-2.0",
|
|
41001
42759
|
type: "module",
|
|
@@ -41088,7 +42846,7 @@ var package_default = {
|
|
|
41088
42846
|
// packages/dsl/package.json
|
|
41089
42847
|
var package_default2 = {
|
|
41090
42848
|
name: "@synapsor/dsl",
|
|
41091
|
-
version: "1.4.
|
|
42849
|
+
version: "1.4.3",
|
|
41092
42850
|
description: "SQL-like Synapsor authoring frontend for canonical @synapsor/spec contracts.",
|
|
41093
42851
|
license: "Apache-2.0",
|
|
41094
42852
|
type: "module",
|
|
@@ -41109,7 +42867,7 @@ var package_default2 = {
|
|
|
41109
42867
|
test: "vitest run"
|
|
41110
42868
|
},
|
|
41111
42869
|
dependencies: {
|
|
41112
|
-
"@synapsor/spec": "workspace:^1.4.
|
|
42870
|
+
"@synapsor/spec": "workspace:^1.4.2"
|
|
41113
42871
|
},
|
|
41114
42872
|
devDependencies: {
|
|
41115
42873
|
typescript: "^5.7.0",
|
|
@@ -41142,7 +42900,7 @@ var package_default2 = {
|
|
|
41142
42900
|
// packages/spec/package.json
|
|
41143
42901
|
var package_default3 = {
|
|
41144
42902
|
name: "@synapsor/spec",
|
|
41145
|
-
version: "1.4.
|
|
42903
|
+
version: "1.4.2",
|
|
41146
42904
|
description: "Canonical Synapsor contract schemas, types, validation, and conformance fixtures.",
|
|
41147
42905
|
license: "Apache-2.0",
|
|
41148
42906
|
type: "module",
|
|
@@ -44001,7 +45759,13 @@ async function localToolNames(config, checks) {
|
|
|
44001
45759
|
}
|
|
44002
45760
|
}
|
|
44003
45761
|
function formatLocalDoctorReport(report) {
|
|
44004
|
-
const lines = [
|
|
45762
|
+
const lines = [
|
|
45763
|
+
`Synapsor Runner doctor: ${report.ok ? "ok" : "failed"}`,
|
|
45764
|
+
`Config: ${report.config_path}`,
|
|
45765
|
+
`Mode: ${report.mode}`,
|
|
45766
|
+
`Governance authority: ${report.governance.authority_mode}`,
|
|
45767
|
+
`Evidence residency: ${report.governance.evidence_residency}`
|
|
45768
|
+
];
|
|
44005
45769
|
if (report.tools.length) {
|
|
44006
45770
|
lines.push("Exposed MCP tools:");
|
|
44007
45771
|
for (const tool of report.tools) lines.push(` - ${tool}`);
|
|
@@ -44023,6 +45787,9 @@ function formatLocalDoctorMarkdown(report) {
|
|
|
44023
45787
|
`- Node version: ${process2.versions.node}`,
|
|
44024
45788
|
`- Config: ${report.config_path}`,
|
|
44025
45789
|
`- Mode: ${report.mode}`,
|
|
45790
|
+
`- Governance authority: ${report.governance.authority_mode}`,
|
|
45791
|
+
`- Evidence residency: ${report.governance.evidence_residency}`,
|
|
45792
|
+
`- Queue proposals while Cloud is unavailable: ${report.governance.queue_when_unavailable ? "yes" : "no"}`,
|
|
44026
45793
|
`- Status: ${report.ok ? "ok" : "needs attention"}`,
|
|
44027
45794
|
"",
|
|
44028
45795
|
"## Semantic Tools",
|
|
@@ -44358,6 +46125,7 @@ async function localDoctor(args) {
|
|
|
44358
46125
|
}
|
|
44359
46126
|
checks.push(...await sharedPostgresLedgerDoctorChecks(parsed));
|
|
44360
46127
|
checks.push(...graduatedTrustDoctorChecks(parsed));
|
|
46128
|
+
const governance = await cloudLinkedGovernanceDoctorStatus(parsed, args, checks);
|
|
44361
46129
|
const contextsToCheck = trustedContextsForDoctor(parsed);
|
|
44362
46130
|
for (const [contextName, contextValues] of contextsToCheck) {
|
|
44363
46131
|
const tenantEnv = String(contextValues.tenant_id_env ?? "SYNAPSOR_TENANT_ID");
|
|
@@ -44482,6 +46250,7 @@ async function localDoctor(args) {
|
|
|
44482
46250
|
config_path: configPath,
|
|
44483
46251
|
checks,
|
|
44484
46252
|
tools: tools2,
|
|
46253
|
+
governance,
|
|
44485
46254
|
store_stats: await localDoctorStoreStats(optionalArg(args, "--store") ?? parsed.storage?.sqlite_path)
|
|
44486
46255
|
};
|
|
44487
46256
|
if (args.includes("--report")) {
|
|
@@ -44498,6 +46267,58 @@ async function localDoctor(args) {
|
|
|
44498
46267
|
}
|
|
44499
46268
|
return report.ok ? 0 : 1;
|
|
44500
46269
|
}
|
|
46270
|
+
async function cloudLinkedGovernanceDoctorStatus(config, args, checks) {
|
|
46271
|
+
if (config.governance?.mode !== "cloud_linked") {
|
|
46272
|
+
checks.push({ name: "governance:authority", ok: true, level: "pass", message: "Governance authority is local-only; no Synapsor Cloud account is required." });
|
|
46273
|
+
return { authority_mode: "local_only", evidence_residency: "metadata_only", queue_when_unavailable: false };
|
|
46274
|
+
}
|
|
46275
|
+
const storePath = optionalArg(args, "--store") ?? config.storage?.sqlite_path ?? "./.synapsor/local.db";
|
|
46276
|
+
let store;
|
|
46277
|
+
let synchronizer;
|
|
46278
|
+
try {
|
|
46279
|
+
store = createDefaultRuntimeStore(config, process2.env, storePath);
|
|
46280
|
+
synchronizer = new CloudLinkedSynchronizer(config, store, process2.env);
|
|
46281
|
+
const status = await synchronizer.status();
|
|
46282
|
+
checks.push({
|
|
46283
|
+
name: "governance:authority",
|
|
46284
|
+
ok: true,
|
|
46285
|
+
level: "pass",
|
|
46286
|
+
message: `Governance authority is Synapsor Cloud; local store ${storePath} is an operational spool/mirror and is never uploaded.`
|
|
46287
|
+
});
|
|
46288
|
+
checks.push({
|
|
46289
|
+
name: "governance:evidence-residency",
|
|
46290
|
+
ok: true,
|
|
46291
|
+
level: "pass",
|
|
46292
|
+
message: "Evidence residency is metadata_only; source rows, SQL details, kept-out fields, credentials, and replay payloads remain local."
|
|
46293
|
+
});
|
|
46294
|
+
const unhealthy = status.dead_letter > 0 || status.reconciliation_required > 0;
|
|
46295
|
+
const lagging = status.pending > 0 || status.leased > 0;
|
|
46296
|
+
checks.push({
|
|
46297
|
+
name: "governance:outbox",
|
|
46298
|
+
ok: !unhealthy,
|
|
46299
|
+
level: unhealthy ? "fail" : lagging ? "warn" : "pass",
|
|
46300
|
+
message: unhealthy ? `Cloud outbox needs operator attention: ${status.dead_letter} dead-letter and ${status.reconciliation_required} reconciliation-required event(s). Run ${cliCommandName()} cloud outbox inspect latest.` : lagging ? `Cloud outbox has ${status.pending} pending and ${status.leased} leased event(s); source writes remain blocked until Cloud governance completes.` : "Cloud outbox has no pending, leased, dead-letter, or reconciliation-required events."
|
|
46301
|
+
});
|
|
46302
|
+
return { ...status, queue_when_unavailable: config.governance.queue_when_unavailable !== false };
|
|
46303
|
+
} catch (error) {
|
|
46304
|
+
const errorCode = error instanceof Error && "code" in error ? String(error.code ?? "CLOUD_LINKED_DOCTOR_FAILED") : "CLOUD_LINKED_DOCTOR_FAILED";
|
|
46305
|
+
checks.push({
|
|
46306
|
+
name: "governance:cloud-connection",
|
|
46307
|
+
ok: false,
|
|
46308
|
+
level: "fail",
|
|
46309
|
+
message: `Cloud-linked governance configuration could not be opened (${errorCode}). Check the reviewed connection file and Runner credential environment; no local approval fallback is allowed.`
|
|
46310
|
+
});
|
|
46311
|
+
return {
|
|
46312
|
+
authority_mode: "cloud_linked",
|
|
46313
|
+
evidence_residency: "metadata_only",
|
|
46314
|
+
queue_when_unavailable: config.governance.queue_when_unavailable !== false,
|
|
46315
|
+
connection_error_code: errorCode
|
|
46316
|
+
};
|
|
46317
|
+
} finally {
|
|
46318
|
+
await synchronizer?.stop();
|
|
46319
|
+
await store?.close();
|
|
46320
|
+
}
|
|
46321
|
+
}
|
|
44501
46322
|
function graduatedTrustDoctorChecks(config) {
|
|
44502
46323
|
const trust = config.graduated_trust;
|
|
44503
46324
|
if (trust?.enabled !== true) {
|
|
@@ -44828,6 +46649,11 @@ async function revert(args) {
|
|
|
44828
46649
|
if (capability.reversibility?.mode !== "reviewed_inverse") throw new Error(`REVERSIBILITY_NOT_REVIEWED: capability ${capability.name} does not declare reviewed inverse authority`);
|
|
44829
46650
|
const trusted = trustedCliContext(config, capability, process2.env);
|
|
44830
46651
|
if (trusted.tenant_id !== forward.tenant_id) throw new Error("REVERSAL_TENANT_MISMATCH: current trusted tenant does not own the forward proposal");
|
|
46652
|
+
const forwardPrincipalScope = forward.change_set.guards.principal_scope;
|
|
46653
|
+
if (capability.target.principal_scope_key) {
|
|
46654
|
+
if (!forwardPrincipalScope?.value || forwardPrincipalScope.column !== capability.target.principal_scope_key) throw new Error("REVERSAL_PRINCIPAL_SCOPE_MISSING: forward proposal does not preserve reviewed principal authority");
|
|
46655
|
+
if (String(forwardPrincipalScope.value) !== trusted.principal) throw new Error("REVERSAL_PRINCIPAL_MISMATCH: current trusted principal does not own the forward proposal");
|
|
46656
|
+
}
|
|
44831
46657
|
const identity = await operatorIdentityForDecision({ args, config, configPath, proposal: forward, action: "revert", reason: optionalArg(args, "--reason") });
|
|
44832
46658
|
const receipt = [...store.receipts(forward.proposal_id)].reverse().find((item) => item.status === "applied" || item.status === "already_applied");
|
|
44833
46659
|
if (!receipt) {
|
|
@@ -44923,7 +46749,7 @@ function createCompensationProposal(input) {
|
|
|
44923
46749
|
patch,
|
|
44924
46750
|
after,
|
|
44925
46751
|
compensation: { descriptor: input.inverse, forward_receipt_hash: input.receiptHash },
|
|
44926
|
-
guards: { tenant: input.inverse.tenant_guard, allowed_columns: input.inverse.allowed_columns },
|
|
46752
|
+
guards: { tenant: input.inverse.tenant_guard, ...input.inverse.principal_scope ? { principal_scope: input.inverse.principal_scope } : {}, allowed_columns: input.inverse.allowed_columns },
|
|
44927
46753
|
evidence: { bundle_id: evidenceId, query_fingerprint: queryFingerprint, items: evidenceItems },
|
|
44928
46754
|
approval: {
|
|
44929
46755
|
status: "pending",
|
|
@@ -44964,6 +46790,7 @@ async function apply(args) {
|
|
|
44964
46790
|
const dryRun = args.includes("--dry-run") || process2.env.SYNAPSOR_DRY_RUN === "true";
|
|
44965
46791
|
const configPath = optionalArg(args, "--config") ?? (await fileExists("synapsor.runner.json") ? "synapsor.runner.json" : void 0);
|
|
44966
46792
|
const runtimeConfig = configPath ? await optionalRuntimeConfig(configPath) : void 0;
|
|
46793
|
+
if (!dryRun) assertLocalGovernanceMutationAllowed(runtimeConfig, "apply --job");
|
|
44967
46794
|
if (runtimeConfig && runtimeStoreBridgeRequired(args, runtimeConfig)) {
|
|
44968
46795
|
return withSharedPostgresRuntimeStoreBridge(args, runtimeConfig, "apply --job", (bridgeStorePath) => apply(argsWithRuntimeStoreBridge(args, bridgeStorePath)));
|
|
44969
46796
|
}
|
|
@@ -45063,6 +46890,7 @@ async function applyAllApproved(args) {
|
|
|
45063
46890
|
const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
|
|
45064
46891
|
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
|
|
45065
46892
|
const config = await optionalRuntimeConfig(configPath);
|
|
46893
|
+
assertLocalGovernanceMutationAllowed(config, "apply --all-approved");
|
|
45066
46894
|
if (config && runtimeStoreBridgeRequired(args, config)) {
|
|
45067
46895
|
return withSharedPostgresRuntimeStoreBridge(args, config, "apply --all-approved", (bridgeStorePath) => applyAllApproved(argsWithRuntimeStoreBridge(args, bridgeStorePath)));
|
|
45068
46896
|
}
|
|
@@ -45171,6 +46999,7 @@ async function applyProposal(args, proposalId) {
|
|
|
45171
46999
|
const runnerId = optionalArg(args, "--runner") ?? process2.env.SYNAPSOR_RUNNER_ID ?? "local_runner";
|
|
45172
47000
|
const workerAttempt = Number(optionalArg(args, "--worker-attempt") ?? "1");
|
|
45173
47001
|
const config = await readRuntimeConfig(configPath);
|
|
47002
|
+
assertLocalGovernanceMutationAllowed(config, "apply");
|
|
45174
47003
|
if (config && runtimeStoreBridgeRequired(args, config)) {
|
|
45175
47004
|
return withSharedPostgresRuntimeStoreBridge(args, config, `apply ${proposalId}`, (bridgeStorePath) => applyProposal(argsWithRuntimeStoreBridge(args, bridgeStorePath), proposalId));
|
|
45176
47005
|
}
|
|
@@ -45206,6 +47035,20 @@ async function applyProposal(args, proposalId) {
|
|
|
45206
47035
|
identity_verified: identity.verified,
|
|
45207
47036
|
required_role: config.operator_identity?.apply_roles?.join(",") || void 0
|
|
45208
47037
|
});
|
|
47038
|
+
const proposalScope = proposal.change_set.guards.principal_scope;
|
|
47039
|
+
if (capability.target.principal_scope_key) {
|
|
47040
|
+
if (!proposalScope || proposalScope.column !== capability.target.principal_scope_key || proposalScope.value === void 0) {
|
|
47041
|
+
throw new Error(`proposal ${proposal.proposal_id} is missing its reviewed principal scope`);
|
|
47042
|
+
}
|
|
47043
|
+
if (proposalScope.provider === "environment" || proposalScope.provider === "static_dev") {
|
|
47044
|
+
const current = trustedCliContext(config, capability, process2.env);
|
|
47045
|
+
if (current.tenant_id !== proposal.tenant_id || current.principal !== String(proposalScope.value)) {
|
|
47046
|
+
throw new Error("current trusted tenant/principal cannot apply this proposal");
|
|
47047
|
+
}
|
|
47048
|
+
}
|
|
47049
|
+
} else if (proposalScope) {
|
|
47050
|
+
throw new Error(`proposal ${proposal.proposal_id} carries unreviewed principal scope`);
|
|
47051
|
+
}
|
|
45209
47052
|
const executorName = proposalExecutorName(proposal, capability);
|
|
45210
47053
|
if (executorName === "none" || executorName === "cloud_worker") {
|
|
45211
47054
|
throw new Error(`proposal ${resolvedProposalId} is not locally applyable; capability ${capability.name} declares ${executorName === "none" ? "no local writeback" : "cloud-worker writeback"}.`);
|
|
@@ -45471,6 +47314,7 @@ function prepareHandlerProposal(store, proposal, runnerId, workerAttempt = 1) {
|
|
|
45471
47314
|
primary_key: changeSet.source.primary_key
|
|
45472
47315
|
},
|
|
45473
47316
|
tenant_guard: changeSet.guards.tenant,
|
|
47317
|
+
...changeSet.guards.principal_scope ? { principal_scope: changeSet.guards.principal_scope } : {},
|
|
45474
47318
|
allowed_columns: changeSet.guards.allowed_columns,
|
|
45475
47319
|
before: changeSet.before,
|
|
45476
47320
|
patch: changeSet.patch,
|
|
@@ -45761,7 +47605,7 @@ async function runCommandHandler(command, args, request, timeoutMs) {
|
|
|
45761
47605
|
child.stdin.end();
|
|
45762
47606
|
});
|
|
45763
47607
|
}
|
|
45764
|
-
async function verifyLocalWritebackAuthority(job, configPath, storePath) {
|
|
47608
|
+
async function verifyLocalWritebackAuthority(job, configPath, storePath, options = {}) {
|
|
45765
47609
|
const config = await readRuntimeConfig(configPath);
|
|
45766
47610
|
const validation = validateRunnerCapabilityConfig(config);
|
|
45767
47611
|
if (!validation.ok) {
|
|
@@ -45785,6 +47629,22 @@ async function verifyLocalWritebackAuthority(job, configPath, storePath) {
|
|
|
45785
47629
|
if (!matching) {
|
|
45786
47630
|
throw new Error("writeback job does not match any reviewed proposal capability in local config");
|
|
45787
47631
|
}
|
|
47632
|
+
const reviewedPrincipalColumn = matching.target.principal_scope_key;
|
|
47633
|
+
const jobPrincipalScope = job.target.principal_scope;
|
|
47634
|
+
if (reviewedPrincipalColumn) {
|
|
47635
|
+
if (!jobPrincipalScope || jobPrincipalScope.column !== reviewedPrincipalColumn) {
|
|
47636
|
+
throw new Error("writeback job is missing or changes the reviewed principal scope");
|
|
47637
|
+
}
|
|
47638
|
+
} else if (jobPrincipalScope) {
|
|
47639
|
+
throw new Error("writeback job adds principal scope not present in the reviewed capability");
|
|
47640
|
+
}
|
|
47641
|
+
if (options.cloudApproved) {
|
|
47642
|
+
const leasedContract = "contract" in job ? job.contract : void 0;
|
|
47643
|
+
if (!leasedContract?.digest) throw new Error("Cloud writeback job is missing its immutable contract digest");
|
|
47644
|
+
if (!matching.contract_provenance || matching.contract_provenance.digest !== leasedContract.digest) {
|
|
47645
|
+
throw new Error("Cloud writeback job contract digest does not match the reviewed local contract");
|
|
47646
|
+
}
|
|
47647
|
+
}
|
|
45788
47648
|
const reviewedAllowed = new Set(matching.allowed_columns ?? []);
|
|
45789
47649
|
for (const column of job.allowed_columns) {
|
|
45790
47650
|
if (!reviewedAllowed.has(column)) {
|
|
@@ -45804,13 +47664,35 @@ async function verifyLocalWritebackAuthority(job, configPath, storePath) {
|
|
|
45804
47664
|
try {
|
|
45805
47665
|
const proposal = store.getProposal(job.proposal_id);
|
|
45806
47666
|
if (!proposal) throw new Error(`local proposal not found for writeback job: ${job.proposal_id}`);
|
|
45807
|
-
|
|
45808
|
-
|
|
47667
|
+
const allowedStates = options.cloudApproved ? /* @__PURE__ */ new Set(["pending_review", "approved", "pending_worker", "applied"]) : /* @__PURE__ */ new Set(["approved", "pending_worker", "applied"]);
|
|
47668
|
+
if (!allowedStates.has(proposal.state)) {
|
|
47669
|
+
throw new Error(`local proposal ${job.proposal_id} is ${proposal.state}, not eligible for ${options.cloudApproved ? "Cloud-approved" : "local-approved"} writeback`);
|
|
45809
47670
|
}
|
|
45810
47671
|
if (proposal.proposal_hash !== job.approval_id) {
|
|
45811
47672
|
throw new Error("writeback approval/proposal digest does not match local proposal");
|
|
45812
47673
|
}
|
|
45813
|
-
|
|
47674
|
+
const proposalPrincipalScope = proposal.change_set.guards.principal_scope;
|
|
47675
|
+
if (reviewedPrincipalColumn) {
|
|
47676
|
+
if (!proposalPrincipalScope || proposalPrincipalScope.column !== reviewedPrincipalColumn || proposalPrincipalScope.value === void 0) {
|
|
47677
|
+
throw new Error("local proposal is missing its immutable principal scope");
|
|
47678
|
+
}
|
|
47679
|
+
if (!jobPrincipalScope || jobPrincipalScope.value_fingerprint !== proposalPrincipalScope.value_fingerprint || jobPrincipalScope.binding !== proposalPrincipalScope.binding || jobPrincipalScope.provider !== proposalPrincipalScope.provider) {
|
|
47680
|
+
throw new Error("writeback principal scope does not match the immutable local proposal");
|
|
47681
|
+
}
|
|
47682
|
+
if (jobPrincipalScope.value !== void 0 && jobPrincipalScope.value !== proposalPrincipalScope.value) {
|
|
47683
|
+
throw new Error("writeback principal scope value does not match the immutable local proposal");
|
|
47684
|
+
}
|
|
47685
|
+
jobPrincipalScope.value = proposalPrincipalScope.value;
|
|
47686
|
+
if (!options.cloudApproved && (proposalPrincipalScope.provider === "environment" || proposalPrincipalScope.provider === "static_dev")) {
|
|
47687
|
+
const current = trustedCliContext(config, matching, process2.env);
|
|
47688
|
+
if (current.tenant_id !== proposal.tenant_id || current.principal !== String(proposalPrincipalScope.value)) {
|
|
47689
|
+
throw new Error("current trusted tenant/principal cannot apply this proposal");
|
|
47690
|
+
}
|
|
47691
|
+
}
|
|
47692
|
+
} else if (proposalPrincipalScope) {
|
|
47693
|
+
throw new Error("local proposal carries principal scope outside the reviewed capability");
|
|
47694
|
+
}
|
|
47695
|
+
if (!options.cloudApproved) await verifyStoredApprovalAuthority(config, configPath, store, proposal, matching);
|
|
45814
47696
|
} finally {
|
|
45815
47697
|
store.close();
|
|
45816
47698
|
}
|
|
@@ -45855,6 +47737,7 @@ function capabilityMatchesJob(capability, job) {
|
|
|
45855
47737
|
if (capability.target.table !== job.target.table) return false;
|
|
45856
47738
|
if (capability.target.primary_key !== job.target.primary_key.column) return false;
|
|
45857
47739
|
if (!capability.target.tenant_key || capability.target.tenant_key !== job.target.tenant_guard.column) return false;
|
|
47740
|
+
if ((capability.target.principal_scope_key ?? void 0) !== (job.target.principal_scope?.column ?? void 0)) return false;
|
|
45858
47741
|
const reviewedOperation = capability.operation?.kind ?? "update";
|
|
45859
47742
|
if (job.protocol_version === protocolVersions.normalizedWritebackJobV4) {
|
|
45860
47743
|
if (capability.reversibility?.mode !== "reviewed_inverse") return false;
|
|
@@ -45900,18 +47783,89 @@ function capabilityMatchesJob(capability, job) {
|
|
|
45900
47783
|
return Object.keys(job.patch).every((column) => reviewedAllowed.has(column));
|
|
45901
47784
|
}
|
|
45902
47785
|
async function start(args = []) {
|
|
45903
|
-
if (args.
|
|
45904
|
-
if (args.
|
|
47786
|
+
if (args.includes("--from-env") || args.includes("--schema") || args.includes("--mode") || args.includes("--engine")) {
|
|
47787
|
+
if (args.length > 0) {
|
|
45905
47788
|
return onboard(["db", ...args]);
|
|
45906
47789
|
}
|
|
45907
|
-
throw new Error(`start accepts own-database onboarding flags such as --from-env DATABASE_URL, or no flags for the legacy polling worker. Unknown start arguments: ${args.join(" ")}`);
|
|
45908
47790
|
}
|
|
47791
|
+
const workerOptions = /* @__PURE__ */ new Map();
|
|
47792
|
+
const once = args.includes("--once");
|
|
47793
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
47794
|
+
const flag = args[index];
|
|
47795
|
+
if (flag === "--once") continue;
|
|
47796
|
+
if (flag !== "--config" && flag !== "--store") {
|
|
47797
|
+
throw new Error(`start accepts own-database onboarding flags or Cloud worker flags --config, --store, and --once. Unknown argument: ${flag}`);
|
|
47798
|
+
}
|
|
47799
|
+
const value = args[index + 1];
|
|
47800
|
+
if (!value || value.startsWith("--")) throw new Error(`${flag} requires a value`);
|
|
47801
|
+
workerOptions.set(flag, value);
|
|
47802
|
+
index += 1;
|
|
47803
|
+
}
|
|
47804
|
+
const configPath = workerOptions.get("--config");
|
|
47805
|
+
const storePath = workerOptions.get("--store");
|
|
47806
|
+
if (Boolean(configPath) !== Boolean(storePath)) throw new Error("Cloud worker mode requires both --config and --store");
|
|
47807
|
+
if (once && (!configPath || !storePath)) throw new Error("Cloud worker --once requires both --config and --store so local reviewed authority is rechecked");
|
|
45909
47808
|
const config = loadConfig();
|
|
47809
|
+
const cloudLinkedWorker = configPath && storePath ? createCloudLinkedWorkerSync(configPath, storePath) : void 0;
|
|
47810
|
+
if (once) {
|
|
47811
|
+
const reviewedConfigPath = configPath;
|
|
47812
|
+
const reviewedStorePath = storePath;
|
|
47813
|
+
if (!reviewedConfigPath || !reviewedStorePath) {
|
|
47814
|
+
throw new Error("Cloud worker --once requires both --config and --store so local reviewed authority is rechecked");
|
|
47815
|
+
}
|
|
47816
|
+
try {
|
|
47817
|
+
await cloudLinkedWorker?.synchronizer.drainOnce();
|
|
47818
|
+
const completed = await runOnce(
|
|
47819
|
+
config,
|
|
47820
|
+
adapters,
|
|
47821
|
+
(job) => verifyLocalWritebackAuthority(job, reviewedConfigPath, reviewedStorePath, { cloudApproved: true }),
|
|
47822
|
+
cloudLinkedWorker?.reportResult
|
|
47823
|
+
);
|
|
47824
|
+
process2.stdout.write(`Cloud worker completed ${completed} job(s).
|
|
47825
|
+
`);
|
|
47826
|
+
return 0;
|
|
47827
|
+
} finally {
|
|
47828
|
+
await closeCloudLinkedWorkerSync(cloudLinkedWorker);
|
|
47829
|
+
}
|
|
47830
|
+
}
|
|
45910
47831
|
const controller = new AbortController();
|
|
45911
47832
|
process2.on("SIGINT", () => controller.abort());
|
|
45912
47833
|
process2.on("SIGTERM", () => controller.abort());
|
|
45913
|
-
|
|
45914
|
-
|
|
47834
|
+
cloudLinkedWorker?.synchronizer.start();
|
|
47835
|
+
try {
|
|
47836
|
+
await startPolling(
|
|
47837
|
+
config,
|
|
47838
|
+
adapters,
|
|
47839
|
+
controller.signal,
|
|
47840
|
+
configPath && storePath ? (job) => verifyLocalWritebackAuthority(job, configPath, storePath, { cloudApproved: true }) : void 0,
|
|
47841
|
+
cloudLinkedWorker?.reportResult
|
|
47842
|
+
);
|
|
47843
|
+
return 0;
|
|
47844
|
+
} finally {
|
|
47845
|
+
await closeCloudLinkedWorkerSync(cloudLinkedWorker);
|
|
47846
|
+
}
|
|
47847
|
+
}
|
|
47848
|
+
function createCloudLinkedWorkerSync(configPath, storePath) {
|
|
47849
|
+
const runtimeConfig = loadRuntimeConfigFromFile(configPath);
|
|
47850
|
+
if (runtimeConfig.governance?.mode !== "cloud_linked") return void 0;
|
|
47851
|
+
const store = createDefaultRuntimeStore(runtimeConfig, process2.env, storePath);
|
|
47852
|
+
const synchronizer = new CloudLinkedSynchronizer(runtimeConfig, store, process2.env);
|
|
47853
|
+
const reportResult = async ({ job, result, leaseId }) => {
|
|
47854
|
+
const outboxItem = await enqueueCloudLinkedResult({
|
|
47855
|
+
config: runtimeConfig,
|
|
47856
|
+
store,
|
|
47857
|
+
proposalId: job.proposal_id,
|
|
47858
|
+
result,
|
|
47859
|
+
leaseId
|
|
47860
|
+
});
|
|
47861
|
+
if (outboxItem) await synchronizer.flushEvent(outboxItem.event_id);
|
|
47862
|
+
};
|
|
47863
|
+
return { runtimeConfig, store, synchronizer, reportResult };
|
|
47864
|
+
}
|
|
47865
|
+
async function closeCloudLinkedWorkerSync(sync) {
|
|
47866
|
+
if (!sync) return;
|
|
47867
|
+
await sync.synchronizer.stop();
|
|
47868
|
+
await sync.store.close();
|
|
45915
47869
|
}
|
|
45916
47870
|
async function up(args = []) {
|
|
45917
47871
|
const allowed = /* @__PURE__ */ new Set([
|
|
@@ -46135,10 +48089,295 @@ async function runnerCommand(args) {
|
|
|
46135
48089
|
async function cloud(args) {
|
|
46136
48090
|
const [subcommand, ...rest] = args;
|
|
46137
48091
|
if (subcommand === "connect") return cloudConnect(rest);
|
|
48092
|
+
if (subcommand === "sync") return cloudSync(rest);
|
|
48093
|
+
if (subcommand === "sync-activity") return cloudSyncActivity(rest);
|
|
46138
48094
|
if (subcommand === "push") return cloudPush(rest);
|
|
48095
|
+
if (subcommand === "outbox") return cloudOutbox(rest);
|
|
46139
48096
|
usage();
|
|
46140
48097
|
return 2;
|
|
46141
48098
|
}
|
|
48099
|
+
async function cloudOutbox(args) {
|
|
48100
|
+
const [action = "status", ...rest] = args;
|
|
48101
|
+
const configPath = optionalArg(rest, "--config") ?? "synapsor.runner.json";
|
|
48102
|
+
const storePath = optionalArg(rest, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
|
|
48103
|
+
const runtimeConfig = loadRuntimeConfigFromFile(configPath);
|
|
48104
|
+
if (runtimeConfig.governance?.mode !== "cloud_linked") throw new Error("cloud outbox requires governance.mode cloud_linked");
|
|
48105
|
+
const store = createDefaultRuntimeStore(runtimeConfig, process2.env, storePath);
|
|
48106
|
+
const synchronizer = new CloudLinkedSynchronizer(runtimeConfig, store, process2.env);
|
|
48107
|
+
try {
|
|
48108
|
+
if (action === "status") {
|
|
48109
|
+
const status = await synchronizer.status();
|
|
48110
|
+
if (rest.includes("--json")) process2.stdout.write(`${JSON.stringify({ ok: true, ...status }, null, 2)}
|
|
48111
|
+
`);
|
|
48112
|
+
else process2.stdout.write([
|
|
48113
|
+
"Synapsor Cloud outbox",
|
|
48114
|
+
`Authority: ${status.authority_mode}`,
|
|
48115
|
+
`Evidence residency: ${status.evidence_residency}`,
|
|
48116
|
+
`Pending: ${status.pending}`,
|
|
48117
|
+
`Leased: ${status.leased}`,
|
|
48118
|
+
`Acknowledged: ${status.acknowledged}`,
|
|
48119
|
+
`Dead letter: ${status.dead_letter}`,
|
|
48120
|
+
`Reconciliation required: ${status.reconciliation_required}`,
|
|
48121
|
+
status.last_reconciliation_error_code ? `Last reconciliation error: ${status.last_reconciliation_error_code}` : ""
|
|
48122
|
+
].filter(Boolean).join("\n") + "\n");
|
|
48123
|
+
return 0;
|
|
48124
|
+
}
|
|
48125
|
+
if (action === "inspect") {
|
|
48126
|
+
const requested = firstPositional(rest);
|
|
48127
|
+
const entries = await store.listCloudOutbox?.({ limit: 1e4 }) ?? [];
|
|
48128
|
+
const selected = requested === "latest" || !requested ? entries.at(-1) : entries.find((entry) => entry.event_id === requested);
|
|
48129
|
+
if (!selected) throw new Error(`cloud outbox event not found: ${requested || "latest"}`);
|
|
48130
|
+
const governance = selected.proposal_id ? await store.listCloudGovernanceEvents?.(selected.proposal_id) ?? [] : [];
|
|
48131
|
+
const safe = { ok: true, outbox: selected, governance };
|
|
48132
|
+
if (rest.includes("--json")) process2.stdout.write(`${JSON.stringify(safe, null, 2)}
|
|
48133
|
+
`);
|
|
48134
|
+
else process2.stdout.write(`Cloud outbox event: ${selected.event_id}
|
|
48135
|
+
Status: ${selected.status}
|
|
48136
|
+
Kind: ${selected.kind}
|
|
48137
|
+
Attempts: ${selected.attempts}/${selected.max_attempts}
|
|
48138
|
+
Last error: ${selected.last_error_code ?? "none"}
|
|
48139
|
+
Governance events: ${governance.length}
|
|
48140
|
+
`);
|
|
48141
|
+
return 0;
|
|
48142
|
+
}
|
|
48143
|
+
if (action === "reconcile") {
|
|
48144
|
+
if (!rest.includes("--yes")) throw new Error("cloud outbox reconcile requires --yes after inspecting local and Cloud state");
|
|
48145
|
+
const drained = await synchronizer.drainOnce();
|
|
48146
|
+
const reconciled = await synchronizer.reconcileOnce();
|
|
48147
|
+
const status = await synchronizer.status();
|
|
48148
|
+
const result = { ok: true, drained, reconciled, status };
|
|
48149
|
+
if (rest.includes("--json")) process2.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
48150
|
+
`);
|
|
48151
|
+
else process2.stdout.write(`Cloud outbox reconciliation complete.
|
|
48152
|
+
Acknowledged: ${drained.acknowledged}
|
|
48153
|
+
Failed: ${drained.failed}
|
|
48154
|
+
Governance updates: ${reconciled.recorded}
|
|
48155
|
+
`);
|
|
48156
|
+
return status.dead_letter || status.reconciliation_required ? 1 : 0;
|
|
48157
|
+
}
|
|
48158
|
+
if (action === "retry") {
|
|
48159
|
+
const eventId = firstPositional(rest);
|
|
48160
|
+
if (!eventId) throw new Error("cloud outbox retry requires <event-id>");
|
|
48161
|
+
if (!rest.includes("--yes")) throw new Error("cloud outbox retry requires --yes after resolving the reported permanent cause");
|
|
48162
|
+
if (!store.requeueCloudOutbox) throw new Error("configured runtime store does not support Cloud outbox repair");
|
|
48163
|
+
const requeued = await store.requeueCloudOutbox(eventId);
|
|
48164
|
+
const drained = await synchronizer.drainOnce();
|
|
48165
|
+
const result = { ok: true, requeued, drained, status: await synchronizer.status() };
|
|
48166
|
+
if (rest.includes("--json")) process2.stdout.write(`${JSON.stringify(result, null, 2)}
|
|
48167
|
+
`);
|
|
48168
|
+
else process2.stdout.write(`Requeued ${eventId}.
|
|
48169
|
+
Acknowledged: ${drained.acknowledged}
|
|
48170
|
+
Failed: ${drained.failed}
|
|
48171
|
+
`);
|
|
48172
|
+
return drained.failed ? 1 : 0;
|
|
48173
|
+
}
|
|
48174
|
+
throw new Error("cloud outbox supports status, inspect, reconcile, and retry");
|
|
48175
|
+
} finally {
|
|
48176
|
+
await synchronizer.stop();
|
|
48177
|
+
await store.close();
|
|
48178
|
+
}
|
|
48179
|
+
}
|
|
48180
|
+
async function loadCloudConnection(configPath) {
|
|
48181
|
+
const parsed = JSON.parse(await fs8.readFile(configPath, "utf8"));
|
|
48182
|
+
if (!parsed.cloud) throw new Error(`cloud config missing in ${configPath}`);
|
|
48183
|
+
const baseUrlEnv = parsed.cloud.base_url_env || "SYNAPSOR_CLOUD_BASE_URL";
|
|
48184
|
+
const tokenEnv = parsed.cloud.runner_token_env || "SYNAPSOR_RUNNER_TOKEN";
|
|
48185
|
+
const baseUrl = envValue2(process2.env, baseUrlEnv) || String(parsed.cloud.base_url || "").trim();
|
|
48186
|
+
const runnerToken = envValue2(process2.env, tokenEnv);
|
|
48187
|
+
const missing = [baseUrl ? "" : baseUrlEnv, runnerToken ? "" : tokenEnv].filter(Boolean);
|
|
48188
|
+
if (missing.length > 0) throw new Error(`missing environment variables: ${missing.join(", ")}`);
|
|
48189
|
+
const sourceId = String(parsed.cloud.source_id || process2.env.SYNAPSOR_SOURCE_ID || "").trim();
|
|
48190
|
+
if (!sourceId || sourceId === "src_replace_me") throw new Error("cloud.source_id is required and must match the scoped Cloud Runner token source");
|
|
48191
|
+
return {
|
|
48192
|
+
file: parsed.cloud,
|
|
48193
|
+
baseUrl,
|
|
48194
|
+
runnerToken,
|
|
48195
|
+
sourceId,
|
|
48196
|
+
runnerId: String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim(),
|
|
48197
|
+
runnerVersion: String(parsed.cloud.runner_version || process2.env.npm_package_version || package_default.version).trim()
|
|
48198
|
+
};
|
|
48199
|
+
}
|
|
48200
|
+
function stripPrincipalScopeFromCloudRows(changeSet, column) {
|
|
48201
|
+
const stripRecord = (value) => {
|
|
48202
|
+
if (isRecord9(value)) delete value[column];
|
|
48203
|
+
};
|
|
48204
|
+
stripRecord(changeSet.before);
|
|
48205
|
+
stripRecord(changeSet.after);
|
|
48206
|
+
if ("frozen_set" in changeSet && isRecord9(changeSet.frozen_set) && Array.isArray(changeSet.frozen_set.members)) {
|
|
48207
|
+
for (const member of changeSet.frozen_set.members) {
|
|
48208
|
+
if (!isRecord9(member)) continue;
|
|
48209
|
+
stripRecord(member.before);
|
|
48210
|
+
stripRecord(member.after);
|
|
48211
|
+
}
|
|
48212
|
+
}
|
|
48213
|
+
if (changeSet.schema_version === protocolVersions.compensationChangeSet) {
|
|
48214
|
+
for (const member of changeSet.compensation.descriptor.members) {
|
|
48215
|
+
stripRecord(member.expected_state);
|
|
48216
|
+
stripRecord(member.restore_values);
|
|
48217
|
+
}
|
|
48218
|
+
}
|
|
48219
|
+
}
|
|
48220
|
+
async function cloudSync(args) {
|
|
48221
|
+
const configPath = optionalArg(args, "--config") ?? "synapsor.cloud.json";
|
|
48222
|
+
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
|
|
48223
|
+
const requested = firstPositional(args);
|
|
48224
|
+
const connection = await loadCloudConnection(configPath);
|
|
48225
|
+
const contractId = String(connection.file.contract_id || "").trim();
|
|
48226
|
+
const contractVersionId = String(connection.file.contract_version_id || "").trim();
|
|
48227
|
+
const contractDigest = String(connection.file.contract_digest || "").trim();
|
|
48228
|
+
const runnerSourceId = String(connection.file.runner_source_id || connection.sourceId).trim();
|
|
48229
|
+
if (!contractId || !contractVersionId || !/^sha256:[0-9a-f]{16,}$/i.test(contractDigest)) {
|
|
48230
|
+
throw new Error("cloud sync requires contract_id, contract_version_id, and contract_digest in synapsor.cloud.json");
|
|
48231
|
+
}
|
|
48232
|
+
const client = new ControlPlaneClient({
|
|
48233
|
+
baseUrl: connection.baseUrl,
|
|
48234
|
+
runnerToken: connection.runnerToken,
|
|
48235
|
+
sourceId: connection.sourceId,
|
|
48236
|
+
runnerId: connection.runnerId
|
|
48237
|
+
});
|
|
48238
|
+
const store = new ProposalStore(storePath);
|
|
48239
|
+
try {
|
|
48240
|
+
const candidates = store.listProposals({ state: "pending_review", source: runnerSourceId, limit: 100 });
|
|
48241
|
+
const selected = requested ? [requested === "latest" ? candidates[0] : store.getProposal(requested)].filter((value) => value !== void 0) : candidates;
|
|
48242
|
+
if (requested && selected.length === 0) throw new Error(`local pending proposal not found: ${requested}`);
|
|
48243
|
+
let synced = 0;
|
|
48244
|
+
for (const proposal of selected) {
|
|
48245
|
+
if (proposal.state !== "pending_review") throw new Error(`proposal ${proposal.proposal_id} is ${proposal.state}; only pending_review proposals can enter Cloud approval`);
|
|
48246
|
+
if (proposal.source_id !== runnerSourceId) {
|
|
48247
|
+
throw new Error(`proposal ${proposal.proposal_id} uses local source ${proposal.source_id}; Cloud source ${connection.sourceId} is mapped to reviewed local source ${runnerSourceId}`);
|
|
48248
|
+
}
|
|
48249
|
+
const evidence2 = store.listEvidenceBundles({ proposal: proposal.proposal_id, limit: 100 });
|
|
48250
|
+
const queryAudit2 = store.listQueryAudit({ proposal: proposal.proposal_id, limit: 100 });
|
|
48251
|
+
const sanitizedChangeSet = JSON.parse(JSON.stringify(proposal.change_set));
|
|
48252
|
+
sanitizedChangeSet.evidence.items = [];
|
|
48253
|
+
if (sanitizedChangeSet.guards.principal_scope) {
|
|
48254
|
+
stripPrincipalScopeFromCloudRows(sanitizedChangeSet, sanitizedChangeSet.guards.principal_scope.column);
|
|
48255
|
+
sanitizedChangeSet.principal.id = sanitizedChangeSet.guards.principal_scope.value_fingerprint;
|
|
48256
|
+
delete sanitizedChangeSet.guards.principal_scope.value;
|
|
48257
|
+
}
|
|
48258
|
+
const payload = {
|
|
48259
|
+
schema_version: protocolVersions.runnerProposal,
|
|
48260
|
+
runner_id: connection.runnerId,
|
|
48261
|
+
source_id: connection.sourceId,
|
|
48262
|
+
...connection.file.mapping_id ? { mapping_id: connection.file.mapping_id } : {},
|
|
48263
|
+
contract: {
|
|
48264
|
+
contract_id: contractId,
|
|
48265
|
+
contract_version_id: contractVersionId,
|
|
48266
|
+
digest: contractDigest
|
|
48267
|
+
},
|
|
48268
|
+
change_set: sanitizedChangeSet,
|
|
48269
|
+
evidence_metadata: {
|
|
48270
|
+
bundle_ids: evidence2.map((item) => item.evidence_bundle_id),
|
|
48271
|
+
count: evidence2.length,
|
|
48272
|
+
query_fingerprints: [...new Set(evidence2.map((item) => item.query_fingerprint).filter(Boolean))],
|
|
48273
|
+
payload_uploaded: false
|
|
48274
|
+
},
|
|
48275
|
+
query_audit: {
|
|
48276
|
+
audit_ids: queryAudit2.map((item) => item.audit_id).filter((value) => value !== void 0),
|
|
48277
|
+
count: queryAudit2.length,
|
|
48278
|
+
query_fingerprints: [...new Set(queryAudit2.map((item) => item.query_fingerprint).filter(Boolean))],
|
|
48279
|
+
tables: [...new Set(queryAudit2.map((item) => item.table_name).filter(Boolean))],
|
|
48280
|
+
payload_uploaded: false
|
|
48281
|
+
}
|
|
48282
|
+
};
|
|
48283
|
+
await client.submitProposal(payload);
|
|
48284
|
+
synced += 1;
|
|
48285
|
+
}
|
|
48286
|
+
if (args.includes("--json")) {
|
|
48287
|
+
process2.stdout.write(`${JSON.stringify({ ok: true, synced, source_id: connection.sourceId, contract_version_id: contractVersionId }, null, 2)}
|
|
48288
|
+
`);
|
|
48289
|
+
} else {
|
|
48290
|
+
process2.stdout.write(`Synced ${synced} pending proposal${synced === 1 ? "" : "s"} to Cloud for ${connection.sourceId}.
|
|
48291
|
+
`);
|
|
48292
|
+
process2.stdout.write("Only proposal diffs and bounded evidence/query-audit metadata were sent; database credentials and source rows stayed local.\n");
|
|
48293
|
+
}
|
|
48294
|
+
return 0;
|
|
48295
|
+
} finally {
|
|
48296
|
+
store.close();
|
|
48297
|
+
}
|
|
48298
|
+
}
|
|
48299
|
+
async function cloudSyncActivity(args) {
|
|
48300
|
+
const configPath = optionalArg(args, "--config") ?? "synapsor.cloud.json";
|
|
48301
|
+
const storePath = optionalArg(args, "--store") ?? process2.env.SYNAPSOR_LOCAL_STORE ?? "./.synapsor/local.db";
|
|
48302
|
+
const requested = firstPositional(args) ?? "latest";
|
|
48303
|
+
const connection = await loadCloudConnection(configPath);
|
|
48304
|
+
const runnerSourceId = String(connection.file.runner_source_id || connection.sourceId).trim();
|
|
48305
|
+
const client = new ControlPlaneClient({
|
|
48306
|
+
baseUrl: connection.baseUrl,
|
|
48307
|
+
runnerToken: connection.runnerToken,
|
|
48308
|
+
sourceId: connection.sourceId,
|
|
48309
|
+
runnerId: connection.runnerId
|
|
48310
|
+
});
|
|
48311
|
+
const store = new ProposalStore(storePath);
|
|
48312
|
+
try {
|
|
48313
|
+
const proposal = requested === "latest" ? store.listProposals({ source: runnerSourceId, limit: 1 })[0] : store.getProposal(requested);
|
|
48314
|
+
if (!proposal) throw new Error(`local proposal not found: ${requested}`);
|
|
48315
|
+
if (proposal.source_id !== runnerSourceId) {
|
|
48316
|
+
throw new Error(`proposal ${proposal.proposal_id} uses local source ${proposal.source_id}; Cloud source ${connection.sourceId} is mapped to reviewed local source ${runnerSourceId}`);
|
|
48317
|
+
}
|
|
48318
|
+
const evidence2 = store.listEvidenceBundles({ proposal: proposal.proposal_id, limit: 100 });
|
|
48319
|
+
const queryAudit2 = store.listQueryAudit({ proposal: proposal.proposal_id, limit: 100 });
|
|
48320
|
+
const replay2 = store.replay(proposal.proposal_id);
|
|
48321
|
+
const principalScope3 = proposal.change_set.guards.principal_scope;
|
|
48322
|
+
const common = {
|
|
48323
|
+
schema_version: protocolVersions.runnerActivity,
|
|
48324
|
+
runner_id: connection.runnerId,
|
|
48325
|
+
source_id: connection.sourceId,
|
|
48326
|
+
proposal_id: proposal.proposal_id,
|
|
48327
|
+
capability: proposal.action,
|
|
48328
|
+
tenant_id: proposal.tenant_id,
|
|
48329
|
+
principal: principalScope3?.value_fingerprint ?? proposal.principal,
|
|
48330
|
+
business_object: proposal.business_object,
|
|
48331
|
+
object_id: proposal.object_id,
|
|
48332
|
+
status: proposal.state
|
|
48333
|
+
};
|
|
48334
|
+
const events2 = [
|
|
48335
|
+
...evidence2.map((item) => ({
|
|
48336
|
+
...common,
|
|
48337
|
+
event_id: `evidence:${item.evidence_bundle_id}`,
|
|
48338
|
+
event_type: "evidence.recorded",
|
|
48339
|
+
evidence_ids: [item.evidence_bundle_id],
|
|
48340
|
+
detail: { stored_locally: true, payload_uploaded: false },
|
|
48341
|
+
occurred_at: item.created_at
|
|
48342
|
+
})),
|
|
48343
|
+
...queryAudit2.map((item) => ({
|
|
48344
|
+
...common,
|
|
48345
|
+
event_id: `query-audit:${String(item.audit_id)}`,
|
|
48346
|
+
event_type: "query_audit.recorded",
|
|
48347
|
+
query_audit_ids: [String(item.audit_id)],
|
|
48348
|
+
...typeof item.evidence_bundle_id === "string" ? { evidence_ids: [item.evidence_bundle_id] } : {},
|
|
48349
|
+
detail: { stored_locally: true, payload_uploaded: false },
|
|
48350
|
+
occurred_at: typeof item.created_at === "string" ? item.created_at : void 0
|
|
48351
|
+
})),
|
|
48352
|
+
{
|
|
48353
|
+
...common,
|
|
48354
|
+
event_id: `replay:${replay2.replay_id}`,
|
|
48355
|
+
event_type: "replay.recorded",
|
|
48356
|
+
replay_id: replay2.replay_id,
|
|
48357
|
+
detail: { stored_locally: true, payload_uploaded: false }
|
|
48358
|
+
}
|
|
48359
|
+
];
|
|
48360
|
+
for (const event of events2) await client.submitActivity(event);
|
|
48361
|
+
const output = {
|
|
48362
|
+
ok: true,
|
|
48363
|
+
synced: events2.length,
|
|
48364
|
+
proposal_id: proposal.proposal_id,
|
|
48365
|
+
evidence_references: evidence2.length,
|
|
48366
|
+
query_audit_references: queryAudit2.length,
|
|
48367
|
+
replay_id: replay2.replay_id
|
|
48368
|
+
};
|
|
48369
|
+
if (args.includes("--json")) process2.stdout.write(`${JSON.stringify(output, null, 2)}
|
|
48370
|
+
`);
|
|
48371
|
+
else {
|
|
48372
|
+
process2.stdout.write(`Synced ${events2.length} local activity reference${events2.length === 1 ? "" : "s"} to Cloud for ${proposal.proposal_id}.
|
|
48373
|
+
`);
|
|
48374
|
+
process2.stdout.write("Evidence contents, source rows, database credentials, and local replay payloads stayed local.\n");
|
|
48375
|
+
}
|
|
48376
|
+
return 0;
|
|
48377
|
+
} finally {
|
|
48378
|
+
store.close();
|
|
48379
|
+
}
|
|
48380
|
+
}
|
|
46142
48381
|
async function cloudPush(args) {
|
|
46143
48382
|
const target2 = firstPositional(args);
|
|
46144
48383
|
if (!target2) throw new Error("cloud push requires <synapsor.contract.json>");
|
|
@@ -46192,20 +48431,46 @@ async function cloudPush(args) {
|
|
|
46192
48431
|
return 0;
|
|
46193
48432
|
}
|
|
46194
48433
|
const apiUrl = (optionalArg(args, "--api-url") ?? process2.env.SYNAPSOR_CLOUD_BASE_URL ?? "").trim();
|
|
46195
|
-
|
|
48434
|
+
if (args.includes("--token")) {
|
|
48435
|
+
throw new Error("cloud push does not accept secrets through --token. Set SYNAPSOR_API_KEY for automation or SYNAPSOR_CLOUD_ACCESS_TOKEN for an authenticated human session.");
|
|
48436
|
+
}
|
|
48437
|
+
const apiKey = (process2.env.SYNAPSOR_API_KEY ?? "").trim();
|
|
48438
|
+
const humanAccessToken = (process2.env.SYNAPSOR_CLOUD_ACCESS_TOKEN ?? "").trim();
|
|
48439
|
+
const credential = apiKey || humanAccessToken;
|
|
46196
48440
|
if (!workspace) {
|
|
46197
48441
|
throw new Error("cloud push upload requires --workspace <project_id> or SYNAPSOR_CLOUD_WORKSPACE/SYNAPSOR_WORKSPACE_ID/SYNAPSOR_PROJECT_ID.");
|
|
46198
48442
|
}
|
|
46199
|
-
if (!apiUrl || !
|
|
46200
|
-
throw new Error("cloud push upload requires --api-url plus
|
|
48443
|
+
if (!apiUrl || !credential) {
|
|
48444
|
+
throw new Error("cloud push upload requires --api-url/SYNAPSOR_CLOUD_BASE_URL plus SYNAPSOR_API_KEY or SYNAPSOR_CLOUD_ACCESS_TOKEN. Use --dry-run for local validation without a network call.");
|
|
48445
|
+
}
|
|
48446
|
+
let response;
|
|
48447
|
+
try {
|
|
48448
|
+
response = await new CloudControlClient({
|
|
48449
|
+
baseUrl: apiUrl,
|
|
48450
|
+
credential,
|
|
48451
|
+
credentialKind: apiKey ? "service" : "human",
|
|
48452
|
+
userAgent: "synapsor-runner-cloud-push"
|
|
48453
|
+
}).pushContract({
|
|
48454
|
+
projectId: workspace,
|
|
48455
|
+
contract,
|
|
48456
|
+
name,
|
|
48457
|
+
description,
|
|
48458
|
+
source: "runner",
|
|
48459
|
+
sourceVersions: payload.source_versions,
|
|
48460
|
+
activate: args.includes("--activate"),
|
|
48461
|
+
idempotencyKey
|
|
48462
|
+
});
|
|
48463
|
+
} catch (error) {
|
|
48464
|
+
if (error instanceof CloudControlError) {
|
|
48465
|
+
const request = error.request_id ? ` Request: ${error.request_id}.` : "";
|
|
48466
|
+
const issues = Array.isArray(error.details?.errors) ? error.details.errors.slice(0, 3).map((issue) => isRecord9(issue) ? `${String(issue.path || "$")} ${String(issue.code || "validation_error")}: ${String(issue.message || "")}` : String(issue)).join("; ") : "";
|
|
48467
|
+
if (error.status === 422 && issues) {
|
|
48468
|
+
throw new Error(`Cloud rejected the contract: ${issues}.${request}`);
|
|
48469
|
+
}
|
|
48470
|
+
throw new Error(`cloud push upload failed: ${error.message} (${error.error_code}).${issues ? ` ${issues}` : ""}${request}`);
|
|
48471
|
+
}
|
|
48472
|
+
throw error;
|
|
46201
48473
|
}
|
|
46202
|
-
const response = await postCloudContractPush({
|
|
46203
|
-
apiUrl,
|
|
46204
|
-
token,
|
|
46205
|
-
workspace,
|
|
46206
|
-
payload,
|
|
46207
|
-
idempotencyKey
|
|
46208
|
-
});
|
|
46209
48474
|
if (json) {
|
|
46210
48475
|
process2.stdout.write(`${JSON.stringify(response, null, 2)}
|
|
46211
48476
|
`);
|
|
@@ -46245,133 +48510,51 @@ function contractSummary(contract) {
|
|
|
46245
48510
|
kept_out_fields: keptOutFields.size
|
|
46246
48511
|
};
|
|
46247
48512
|
}
|
|
46248
|
-
async function postCloudContractPush(input) {
|
|
46249
|
-
let endpoint;
|
|
46250
|
-
try {
|
|
46251
|
-
endpoint = new URL(`/v1/control/projects/${encodeURIComponent(input.workspace)}/agent-contracts`, input.apiUrl.endsWith("/") ? input.apiUrl : `${input.apiUrl}/`);
|
|
46252
|
-
} catch {
|
|
46253
|
-
throw new Error("cloud push upload failed: --api-url/SYNAPSOR_CLOUD_BASE_URL is not a valid URL.");
|
|
46254
|
-
}
|
|
46255
|
-
const headers = {
|
|
46256
|
-
accept: "application/json",
|
|
46257
|
-
authorization: `Bearer ${input.token}`,
|
|
46258
|
-
"content-type": "application/json",
|
|
46259
|
-
"user-agent": "synapsor-runner-cloud-push"
|
|
46260
|
-
};
|
|
46261
|
-
if (input.idempotencyKey) headers["idempotency-key"] = input.idempotencyKey;
|
|
46262
|
-
const controller = new AbortController();
|
|
46263
|
-
const timeout = setTimeout(() => controller.abort(), 15e3);
|
|
46264
|
-
let response;
|
|
46265
|
-
try {
|
|
46266
|
-
response = await fetch(endpoint, {
|
|
46267
|
-
method: "POST",
|
|
46268
|
-
headers,
|
|
46269
|
-
body: JSON.stringify(input.payload),
|
|
46270
|
-
signal: controller.signal
|
|
46271
|
-
});
|
|
46272
|
-
} catch (error) {
|
|
46273
|
-
const reason = error instanceof Error && (error.name === "AbortError" || error.name === "TimeoutError") ? "request timed out" : "network request failed";
|
|
46274
|
-
throw new Error(`cloud push upload failed: ${reason}. Check --api-url/SYNAPSOR_CLOUD_BASE_URL and network connectivity.`);
|
|
46275
|
-
} finally {
|
|
46276
|
-
clearTimeout(timeout);
|
|
46277
|
-
}
|
|
46278
|
-
const text = await response.text();
|
|
46279
|
-
const body = parseCloudResponseJson(text);
|
|
46280
|
-
if (!response.ok || body.ok === false) {
|
|
46281
|
-
throw new Error(cloudPushErrorMessage(response.status, body));
|
|
46282
|
-
}
|
|
46283
|
-
return body;
|
|
46284
|
-
}
|
|
46285
|
-
function parseCloudResponseJson(text) {
|
|
46286
|
-
if (!text.trim()) return {};
|
|
46287
|
-
try {
|
|
46288
|
-
const parsed = JSON.parse(text);
|
|
46289
|
-
return isRecord9(parsed) ? parsed : { value: parsed };
|
|
46290
|
-
} catch {
|
|
46291
|
-
return { raw: text.slice(0, 500) };
|
|
46292
|
-
}
|
|
46293
|
-
}
|
|
46294
|
-
function cloudPushErrorMessage(status, body) {
|
|
46295
|
-
const code = typeof body.error === "string" && body.error ? body.error : `http_${status}`;
|
|
46296
|
-
const detail = cloudValidationDetail(body);
|
|
46297
|
-
if (status === 401) return `cloud push upload failed: token is missing, invalid, or expired (${code}).`;
|
|
46298
|
-
if (status === 403) return `cloud push upload failed: token does not have permission to write this workspace (${code}).`;
|
|
46299
|
-
if (status === 404) return `cloud push upload failed: Cloud API URL or workspace was not found (${code}).`;
|
|
46300
|
-
if (status === 409) return `cloud push upload failed: registry conflict (${code}).${detail}`;
|
|
46301
|
-
if (status === 422) return `cloud push upload failed: Cloud rejected the contract (${code}).${detail}`;
|
|
46302
|
-
if (status >= 500) return `cloud push upload failed: Cloud returned HTTP ${status} (${code}).`;
|
|
46303
|
-
return `cloud push upload failed: Cloud returned HTTP ${status} (${code}).${detail}`;
|
|
46304
|
-
}
|
|
46305
|
-
function cloudValidationDetail(body) {
|
|
46306
|
-
const errors = Array.isArray(body.errors) ? body.errors : [];
|
|
46307
|
-
if (!errors.length) {
|
|
46308
|
-
const message2 = typeof body.message === "string" ? body.message : "";
|
|
46309
|
-
return message2 ? ` ${message2}` : "";
|
|
46310
|
-
}
|
|
46311
|
-
const rendered = errors.slice(0, 3).map((issue) => {
|
|
46312
|
-
if (!isRecord9(issue)) return String(issue);
|
|
46313
|
-
const path9 = typeof issue.path === "string" ? issue.path : "$";
|
|
46314
|
-
const code = typeof issue.code === "string" ? issue.code : "validation_error";
|
|
46315
|
-
const message2 = typeof issue.message === "string" ? issue.message : "";
|
|
46316
|
-
return `${path9} ${code}${message2 ? `: ${message2}` : ""}`;
|
|
46317
|
-
}).join("; ");
|
|
46318
|
-
return ` ${rendered}`;
|
|
46319
|
-
}
|
|
46320
48513
|
function cloudStringField(value, key) {
|
|
46321
48514
|
return isRecord9(value) && typeof value[key] === "string" ? value[key] : "";
|
|
46322
48515
|
}
|
|
46323
48516
|
async function cloudConnect(args) {
|
|
46324
48517
|
const configPath = optionalArg(args, "--config") ?? process2.env.SYNAPSOR_MCP_CONFIG ?? "synapsor.cloud.json";
|
|
46325
|
-
|
|
46326
|
-
|
|
46327
|
-
|
|
46328
|
-
|
|
46329
|
-
|
|
46330
|
-
}
|
|
46331
|
-
const baseUrlEnv = parsed.cloud.base_url_env || "SYNAPSOR_CLOUD_BASE_URL";
|
|
46332
|
-
const tokenEnv = parsed.cloud.runner_token_env || "SYNAPSOR_RUNNER_TOKEN";
|
|
46333
|
-
const baseUrl = envValue2(process2.env, baseUrlEnv);
|
|
46334
|
-
const runnerToken = envValue2(process2.env, tokenEnv);
|
|
46335
|
-
const missing = [baseUrl ? "" : baseUrlEnv, runnerToken ? "" : tokenEnv].filter(Boolean);
|
|
46336
|
-
if (missing.length > 0) {
|
|
46337
|
-
process2.stdout.write(`missing environment variables: ${missing.join(", ")}
|
|
48518
|
+
let connection;
|
|
48519
|
+
try {
|
|
48520
|
+
connection = await loadCloudConnection(configPath);
|
|
48521
|
+
} catch (error) {
|
|
48522
|
+
process2.stdout.write(`${error instanceof Error ? error.message : String(error)}
|
|
46338
48523
|
`);
|
|
46339
48524
|
return 1;
|
|
46340
48525
|
}
|
|
46341
|
-
|
|
46342
|
-
|
|
46343
|
-
|
|
46344
|
-
}
|
|
46345
|
-
const sourceId = String(parsed.cloud.source_id || process2.env.SYNAPSOR_SOURCE_ID || "").trim();
|
|
46346
|
-
if (!sourceId || sourceId === "src_replace_me") {
|
|
46347
|
-
process2.stdout.write("cloud.source_id is required before registering a runner. It must match the scoped Cloud runner token source.\n");
|
|
46348
|
-
return 1;
|
|
46349
|
-
}
|
|
46350
|
-
const runnerId = String(parsed.cloud.runner_id || process2.env.SYNAPSOR_RUNNER_ID || "synapsor_runner_local").trim();
|
|
46351
|
-
const runnerVersion = String(parsed.cloud.runner_version || process2.env.npm_package_version || "0.1.0").trim();
|
|
46352
|
-
const engines = normalizeEngines(parsed.cloud.engines);
|
|
46353
|
-
const capabilities = normalizeCapabilities(parsed.cloud.capabilities);
|
|
48526
|
+
const { baseUrl, runnerToken, sourceId, runnerId, runnerVersion } = connection;
|
|
48527
|
+
const engines = normalizeEngines(connection.file.engines);
|
|
48528
|
+
const capabilities = normalizeCapabilities(connection.file.capabilities);
|
|
46354
48529
|
const client = new ControlPlaneClient({
|
|
46355
48530
|
baseUrl,
|
|
46356
48531
|
runnerToken,
|
|
46357
|
-
sourceId
|
|
48532
|
+
sourceId,
|
|
48533
|
+
runnerId
|
|
46358
48534
|
});
|
|
46359
48535
|
const report = await client.doctor();
|
|
46360
|
-
if (!report.ok) {
|
|
46361
|
-
|
|
48536
|
+
if (!report.ok || !report.authenticated) {
|
|
48537
|
+
const reason = report.authenticated ? `status ${report.status}` : "the endpoint did not authenticate the Runner protocol; upgrade Cloud or use its supported API URL";
|
|
48538
|
+
process2.stdout.write(`cloud connection failed: ${reason}
|
|
46362
48539
|
`);
|
|
46363
48540
|
return 1;
|
|
46364
48541
|
}
|
|
46365
48542
|
const registration = {
|
|
46366
48543
|
schema_version: protocolVersions.runnerRegistration,
|
|
48544
|
+
protocol_version: protocolVersions.runnerControl,
|
|
46367
48545
|
runner_id: runnerId,
|
|
46368
48546
|
runner_version: runnerVersion,
|
|
46369
48547
|
engines,
|
|
46370
48548
|
capabilities,
|
|
46371
48549
|
scope: {
|
|
46372
|
-
project_id: String(
|
|
48550
|
+
project_id: String(connection.file.project_id || "token_scope"),
|
|
46373
48551
|
source_ids: [sourceId]
|
|
46374
48552
|
},
|
|
48553
|
+
contracts: connection.file.contract_id && connection.file.contract_version_id && connection.file.contract_digest ? [{
|
|
48554
|
+
contract_id: connection.file.contract_id,
|
|
48555
|
+
contract_version_id: connection.file.contract_version_id,
|
|
48556
|
+
digest: connection.file.contract_digest
|
|
48557
|
+
}] : void 0,
|
|
46375
48558
|
registered_at: (/* @__PURE__ */ new Date()).toISOString()
|
|
46376
48559
|
};
|
|
46377
48560
|
await client.register(registration);
|
|
@@ -48022,6 +50205,11 @@ Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <pa
|
|
|
48022
50205
|
const autoApprovalDisabled = parsed.approvals?.disable_auto_approval === true;
|
|
48023
50206
|
const approvalPolicies = approvalPolicySummaries(parsed);
|
|
48024
50207
|
const capabilityDetails = toolPreviewCapabilityDetails(parsed);
|
|
50208
|
+
const cloudSync2 = await runtime.cloudSyncStatus();
|
|
50209
|
+
const governance = {
|
|
50210
|
+
...cloudSync2,
|
|
50211
|
+
queue_when_unavailable: parsed.governance?.mode === "cloud_linked" && parsed.governance.queue_when_unavailable !== false
|
|
50212
|
+
};
|
|
48025
50213
|
const graduatedTrust = {
|
|
48026
50214
|
enabled: parsed.graduated_trust?.enabled === true,
|
|
48027
50215
|
kill_switch: parsed.graduated_trust?.kill_switch === true,
|
|
@@ -48041,7 +50229,7 @@ Run ${cliCommandName()} onboard db --from-env DATABASE_URL, or pass --config <pa
|
|
|
48041
50229
|
{ name: "write credentials absent", ok: !/(password|secret|bearer|private[_-]?key|token)/i.test(serialized), detail: "MCP tools do not include write credentials" }
|
|
48042
50230
|
];
|
|
48043
50231
|
const ok = checks.every((check) => check.ok);
|
|
48044
|
-
return { ok, configPath, storePath, aliasMode, names, exposures, autoApprovalDisabled, approvalPolicies, capabilityDetails, graduatedTrust, checks };
|
|
50232
|
+
return { ok, configPath, storePath, aliasMode, names, exposures, autoApprovalDisabled, approvalPolicies, capabilityDetails, governance, graduatedTrust, checks };
|
|
48045
50233
|
} finally {
|
|
48046
50234
|
await runtime.close();
|
|
48047
50235
|
}
|
|
@@ -48065,6 +50253,9 @@ function formatToolsPreview(input) {
|
|
|
48065
50253
|
`Config: ${input.configPath}`,
|
|
48066
50254
|
`Store: ${input.storePath}`,
|
|
48067
50255
|
`Alias mode: ${input.aliasMode}`,
|
|
50256
|
+
`Governance authority: ${input.governance.authority_mode}`,
|
|
50257
|
+
`Evidence residency: ${input.governance.evidence_residency}`,
|
|
50258
|
+
...input.governance.authority_mode === "cloud_linked" ? [`Queue proposals while Cloud is unavailable: ${input.governance.queue_when_unavailable ? "yes" : "no"}`] : [],
|
|
48068
50259
|
`auto-approval: ${input.autoApprovalDisabled ? "disabled" : "enabled"}`,
|
|
48069
50260
|
`graduated trust: ${input.graduatedTrust.enabled ? input.graduatedTrust.kill_switch ? "enabled, kill switch active" : `enabled (${input.graduatedTrust.criteria} reviewed criteria)` : "disabled"}; operator-only, never MCP-facing`,
|
|
48070
50261
|
...formatApprovalPolicyPreview(input.approvalPolicies),
|
|
@@ -48102,6 +50293,9 @@ function toolPreviewCapabilityDetails(config) {
|
|
|
48102
50293
|
cardinality,
|
|
48103
50294
|
target: `${capability.target.schema}.${capability.target.table}`,
|
|
48104
50295
|
tenant_source: capability.target.single_tenant_dev ? "explicit single-tenant development acknowledgement" : `${capability.target.tenant_key ?? "missing tenant key"} from trusted ${context?.provider ?? "context"}`,
|
|
50296
|
+
...capability.target.principal_scope_key ? {
|
|
50297
|
+
principal_source: `${capability.target.principal_scope_key} from required trusted ${context?.provider ?? "context"} binding ${context?.principal_binding ?? "principal"}`
|
|
50298
|
+
} : {},
|
|
48105
50299
|
writable_columns: capability.allowed_columns ?? [],
|
|
48106
50300
|
dedup_columns: capability.operation?.deduplication?.components.map((component) => component.column) ?? [],
|
|
48107
50301
|
fixed_selection: (capability.operation?.selection?.all ?? capability.aggregate?.selection?.all)?.map((term) => `${term.column} ${term.operator} ${formatScalar(term.value)}`) ?? [],
|
|
@@ -48123,6 +50317,7 @@ function formatToolPreviewCapabilityDetails(details) {
|
|
|
48123
50317
|
` - ${detail.name}: ${detail.kind}${detail.operation ? ` ${detail.cardinality === "set" ? "BOUNDED SET " : "SINGLE-ROW "}${detail.operation.toUpperCase()}` : ""}`,
|
|
48124
50318
|
detail.kind === "aggregate_read" ? ` target: ${detail.target}; output: one ${detail.aggregate} scalar; minimum group size: ${detail.minimum_group_size}` : ` target: ${detail.target}; max rows: ${detail.max_rows}`,
|
|
48125
50319
|
` tenant: ${detail.tenant_source}`,
|
|
50320
|
+
...detail.principal_source ? [` principal row lock: ${detail.principal_source} (AND tenant)`] : [],
|
|
48126
50321
|
...detail.kind === "aggregate_read" ? [
|
|
48127
50322
|
` fixed selection: ${detail.fixed_selection.join(" AND ") || "tenant scope only"}`,
|
|
48128
50323
|
" privacy: member rows and identities are never returned or stored as evidence items"
|
|
@@ -49318,6 +51513,7 @@ async function proposalsApprove(args) {
|
|
|
49318
51513
|
const storePath = localStorePath(args);
|
|
49319
51514
|
const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
|
|
49320
51515
|
const config = await optionalRuntimeConfig(configPath);
|
|
51516
|
+
assertLocalGovernanceMutationAllowed(config, "proposals approve");
|
|
49321
51517
|
if (config && runtimeStoreBridgeRequired(args, config)) {
|
|
49322
51518
|
return withSharedPostgresRuntimeStoreBridge(args, config, `proposals approve ${proposalId}`, (bridgeStorePath) => proposalsApprove(argsWithRuntimeStoreBridge(args, bridgeStorePath)));
|
|
49323
51519
|
}
|
|
@@ -49373,6 +51569,7 @@ async function proposalsReject(args) {
|
|
|
49373
51569
|
const storePath = localStorePath(args);
|
|
49374
51570
|
const configPath = optionalArg(args, "--config") ?? "synapsor.runner.json";
|
|
49375
51571
|
const config = await optionalRuntimeConfig(configPath);
|
|
51572
|
+
assertLocalGovernanceMutationAllowed(config, "proposals reject");
|
|
49376
51573
|
if (config && runtimeStoreBridgeRequired(args, config)) {
|
|
49377
51574
|
return withSharedPostgresRuntimeStoreBridge(args, config, `proposals reject ${proposalId}`, (bridgeStorePath) => proposalsReject(argsWithRuntimeStoreBridge(args, bridgeStorePath)));
|
|
49378
51575
|
}
|
|
@@ -50243,6 +52440,10 @@ function assertNoRuntimeStoreForLocalMutation(config, command, args = []) {
|
|
|
50243
52440
|
if (args.includes(runtimeStoreBridgeFlag)) return;
|
|
50244
52441
|
throw new Error(`${command} cannot run directly against the local SQLite path when storage.shared_postgres.mode=runtime_store. Use the built-in runtime-store bridge or switch to local SQLite/mirror mode.`);
|
|
50245
52442
|
}
|
|
52443
|
+
function assertLocalGovernanceMutationAllowed(config, command) {
|
|
52444
|
+
if (config?.governance?.mode !== "cloud_linked") return;
|
|
52445
|
+
throw new Error(`${command} is disabled for cloud_linked governance. Record human decisions through Synapsor Cloud; only a Cloud-approved leased job may reach the trusted Runner writeback path.`);
|
|
52446
|
+
}
|
|
50246
52447
|
function runtimeStoreBridgeRequired(args, config) {
|
|
50247
52448
|
return config?.storage?.shared_postgres?.mode === "runtime_store" && !args.includes(runtimeStoreBridgeFlag);
|
|
50248
52449
|
}
|
|
@@ -52641,9 +54842,14 @@ instead of being ignored.
|
|
|
52641
54842
|
`,
|
|
52642
54843
|
cloud: `Usage:
|
|
52643
54844
|
${cmd} cloud connect --config ./synapsor.cloud.json
|
|
54845
|
+
${cmd} cloud sync latest --config ./synapsor.cloud.json --store ./.synapsor/local.db
|
|
54846
|
+
${cmd} cloud sync-activity latest --config ./synapsor.cloud.json --store ./.synapsor/local.db
|
|
52644
54847
|
${cmd} cloud push ./synapsor.contract.json --dry-run [--workspace <id>] [--name <registry-name>]
|
|
52645
54848
|
|
|
52646
|
-
cloud
|
|
54849
|
+
cloud sync sends a pending proposal plus bounded evidence/query-audit metadata.
|
|
54850
|
+
cloud sync-activity sends stable local evidence, query-audit, and replay ids;
|
|
54851
|
+
record contents and database credentials stay local. cloud push validates and
|
|
54852
|
+
normalizes the contract locally, then prints the
|
|
52647
54853
|
payload summary. With --dry-run it makes no network request. Without --dry-run
|
|
52648
54854
|
it uploads to the authenticated Cloud registry and reports the stored contract,
|
|
52649
54855
|
version, digest, and registry URL returned by the server.
|
|
@@ -52674,6 +54880,7 @@ Options:
|
|
|
52674
54880
|
start: `Usage:
|
|
52675
54881
|
${cmd} start --from-env DATABASE_URL [--schema public] [--mode read_only|shadow|review]
|
|
52676
54882
|
${cmd} start --from-env DATABASE_URL --mode review --writeback http_handler --handler-url-env APP_WRITEBACK_URL [--handler-signing-secret-env APP_WRITEBACK_SIGNING_SECRET]
|
|
54883
|
+
${cmd} runner start --once --config ./synapsor.runner.json --store ./.synapsor/local.db
|
|
52677
54884
|
${cmd} start
|
|
52678
54885
|
|
|
52679
54886
|
With --from-env, run the guided own-database setup: inspect schema, choose one
|
|
@@ -52682,7 +54889,9 @@ call, and print MCP/UI next steps.
|
|
|
52682
54889
|
|
|
52683
54890
|
With no flags, start the legacy cloud-linked writeback polling worker from the
|
|
52684
54891
|
worker environment config. Prefer \`${cmd} runner start\` for that worker path
|
|
52685
|
-
so it is not confused with first-run onboarding.
|
|
54892
|
+
so it is not confused with first-run onboarding. Add \`--once\` with both
|
|
54893
|
+
\`--config\` and \`--store\` for a bounded claim/apply cycle that still rechecks
|
|
54894
|
+
the local reviewed contract and proposal before writeback.
|
|
52686
54895
|
`,
|
|
52687
54896
|
inspect: `Usage:
|
|
52688
54897
|
${cmd} inspect --from-env DATABASE_URL [--engine auto|postgres|mysql] [--schema public] [--json]
|
|
@@ -53115,7 +55324,8 @@ export {
|
|
|
53115
55324
|
reconciliationReceipt,
|
|
53116
55325
|
reconciliationSupportedOutcome,
|
|
53117
55326
|
resolveSqlWriteDatabaseUrl,
|
|
53118
|
-
runInitWizard
|
|
55327
|
+
runInitWizard,
|
|
55328
|
+
verifyLocalWritebackAuthority
|
|
53119
55329
|
};
|
|
53120
55330
|
/*! Bundled license information:
|
|
53121
55331
|
|