@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.
Files changed (36) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/README.md +23 -13
  3. package/dist/cli.d.ts +3 -0
  4. package/dist/cli.d.ts.map +1 -1
  5. package/dist/runner.mjs +2533 -323
  6. package/docs/README.md +1 -0
  7. package/docs/capability-authoring.md +43 -1
  8. package/docs/cloud-cli.md +248 -0
  9. package/docs/cloud-mode.md +88 -6
  10. package/docs/cloud-push.md +22 -5
  11. package/docs/conformance.md +6 -0
  12. package/docs/contract-testing.md +30 -1
  13. package/docs/dsl-reference.md +11 -0
  14. package/docs/hosted-cloud-linked-verification.md +97 -0
  15. package/docs/oss-vs-cloud.md +13 -6
  16. package/docs/release-notes.md +30 -0
  17. package/docs/rfcs/007-trusted-principal-row-scope.md +75 -0
  18. package/docs/runner-bundles.md +24 -0
  19. package/docs/running-a-runner-fleet.md +8 -0
  20. package/docs/security-boundary.md +15 -1
  21. package/examples/runner-fleet/seed/mysql.sql +5 -1
  22. package/examples/runner-fleet/seed/postgres.sql +5 -1
  23. package/examples/support-plan-credit/README.md +8 -6
  24. package/fixtures/dsl/principal-row-scope.synapsor.sql +23 -0
  25. package/fixtures/protocol/MANIFEST.json +10 -10
  26. package/package.json +1 -1
  27. package/schemas/change-set.v1.schema.json +14 -0
  28. package/schemas/change-set.v2.schema.json +14 -0
  29. package/schemas/change-set.v3.schema.json +2 -0
  30. package/schemas/compensation-change-set.v1.schema.json +3 -2
  31. package/schemas/inverse-descriptor.v1.schema.json +2 -0
  32. package/schemas/synapsor.contract-tests.schema.json +11 -1
  33. package/schemas/writeback-job.v1.schema.json +14 -0
  34. package/schemas/writeback-job.v2.schema.json +14 -0
  35. package/schemas/writeback-job.v3.schema.json +2 -0
  36. 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) => parseWritebackJob(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 heartbeat(jobId, leaseSeconds = 60) {
23164
- await this.post(`/v1/writeback/jobs/${encodeURIComponent(jobId)}/heartbeat`, { lease_seconds: leaseSeconds });
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 renewLease(jobId, leaseSeconds = 60) {
23167
- return this.post(`/v1/writeback/jobs/${encodeURIComponent(jobId)}/heartbeat`, { lease_seconds: leaseSeconds });
23452
+ async heartbeat(jobId, leaseId, runnerId = this.runnerId, leaseSeconds = 60) {
23453
+ await this.renewLease(jobId, leaseId, runnerId, leaseSeconds);
23168
23454
  }
23169
- async result(result) {
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
- await this.post(`/v1/writeback/jobs/${encodeURIComponent(parsed.job_id)}/result`, parsed);
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
- await this.result(result);
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 response = await this.fetchWithRetry(path9, {
23224
- method: "POST",
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
- async function runOnce(config, adapters2) {
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 client.result({
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
- await client.heartbeat(job.job_id);
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 client.result(result);
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
- return {
25746
- sql: `DELETE FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)}
25747
- WHERE ${quotePostgresIdentifier(job.target.primary_key.column)} = $1
25748
- AND ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2
25749
- AND ${quotePostgresIdentifier(job.conflict_guard.column)} = $3`,
25750
- values: [job.target.primary_key.value, job.target.tenant_guard.value, job.conflict_guard.expected_value]
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 values = [job.target.tenant_guard.value, ...job.compensation.members.map((member) => member.primary_key.value)];
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 ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $1
25822
- AND ${quotePostgresIdentifier(job.target.primary_key.column)} IN (${job.compensation.members.map((_, index) => `$${index + 2}`).join(", ")})
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 values = [job.target.tenant_guard.value, ...job.frozen_set.members.map((member) => member.primary_key.value)];
25829
- const identities = job.frozen_set.members.map((_, index) => `$${index + 2}`).join(", ");
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 ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $1
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 ${job.deduplication.components.map((component, index) => `${quotePostgresIdentifier(component.column)} = $${index + 1}`).join(" AND ")}
26313
+ WHERE ${where2.join(" AND ")}
25842
26314
  LIMIT 2`,
25843
- values: job.deduplication.components.map((component) => component.value)
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 ${quotePostgresIdentifier(job.target.primary_key.column)} = $1
25850
- AND ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2
26328
+ WHERE ${where.join("\n AND ")}
25851
26329
  LIMIT 2`,
25852
- values: [job.target.primary_key.value, job.target.tenant_guard.value]
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 ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $1 AND ${quotePostgresIdentifier(job.target.primary_key.column)} IN (${job.compensation.members.map((_, index) => `$${index + 2}`).join(", ")}) ORDER BY ${quotePostgresIdentifier(job.target.primary_key.column)} ASC FOR UPDATE`,
26122
- [job.target.tenant_guard.value, ...job.compensation.members.map((member) => member.primary_key.value)]
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, expected.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 ${quotePostgresIdentifier(job.target.primary_key.column)} = $${values.length - 2} AND ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $${values.length - 1} AND ${quotePostgresIdentifier(expected.column)} = $${values.length}`,
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 deleted = await client.query(
26266
- `DELETE FROM ${quotePostgresIdentifier(job.target.schema)}.${quotePostgresIdentifier(job.target.table)} WHERE ${quotePostgresIdentifier(job.target.primary_key.column)} = $1 AND ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2 AND ${quotePostgresIdentifier(expected.column)} = $3`,
26267
- [member.primary_key.value, job.target.tenant_guard.value, expected.value]
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, ...job.frozen_set.members.map((member) => member.primary_key.value)];
26279
- const placeholders = job.frozen_set.members.map((_, index) => `$${index + 2}`);
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 ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $1 AND ${quotePostgresIdentifier(job.target.primary_key.column)} IN (${placeholders.join(", ")}) ORDER BY ${quotePostgresIdentifier(job.target.primary_key.column)} ASC FOR UPDATE`,
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 ${components.map((component, index) => `${quotePostgresIdentifier(component.column)} = $${index + 1}`).join(" AND ")} LIMIT 1 FOR UPDATE`,
26312
- components.map((component) => component.value)
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 ${quotePostgresIdentifier(job.target.primary_key.column)} = $1
26339
- AND ${quotePostgresIdentifier(job.target.tenant_guard.column)} = $2
26879
+ WHERE ${where.join("\n AND ")}
26340
26880
  FOR UPDATE`,
26341
- [job.target.primary_key.value, job.target.tenant_guard.value]
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 ${components.map((component, index) => `${quotePostgresIdentifier(component.column)} = $${index + 1}`).join(" AND ")}`,
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
- "SELECT proposal_id FROM proposals WHERE created_at < ? AND state IN ('applied', 'conflict', 'rejected', 'canceled')",
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
- if (!Array.isArray(config.contracts) || config.contracts.length === 0) return config;
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
- visible_row: capability.kind === "proposal" && operation === "insert" ? patch : scalarRecord(current.row),
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: scalarRecord(current.row),
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 evidenceBundleId = stableId("ev", { capability: input.capability.name, tenant: input.context.tenant_id, aggregate, suppressed, value, at: createdAt });
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 capability.target.tenant_key ? [pkValue, context.tenant_id] : [pkValue];
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: scalarRecord(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 terms = (aggregate?.selection?.all ?? []).map((term) => `${term.column} = <fixed>`);
35668
- if (capability.target.tenant_key) terms.push(`${capability.target.tenant_key} = <trusted tenant>`);
35669
- return `SELECT ${expression}, COUNT(*) AS group_size FROM ${capability.target.schema}.${capability.target.table}${terms.length ? ` WHERE ${terms.join(" AND ")}` : ""}`;
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 terms = (capability.operation?.selection?.all ?? []).map((term) => `${term.column} = <fixed>`);
35673
- if (capability.target.tenant_key) terms.push(`${capability.target.tenant_key} = <trusted tenant>`);
35674
- return `SELECT ${readColumns(capability).join(", ")} FROM ${capability.target.schema}.${capability.target.table} WHERE ${terms.join(" AND ")} ORDER BY ${capability.target.primary_key} ASC LIMIT ${(capability.operation?.max_rows ?? 0) + 1}`;
35675
- }
35676
- const where = capability.target.tenant_key ? `${capability.target.primary_key} = ? AND ${capability.target.tenant_key} = ?` : `${capability.target.primary_key} = ?`;
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
- return {
35797
- sql: `DELETE FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)}
35798
- WHERE ${quoteMysqlIdentifier(job.target.primary_key.column)} = ?
35799
- AND ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?
35800
- AND ${quoteMysqlIdentifier(job.conflict_guard.column)} = ?`,
35801
- values: [job.target.primary_key.value, job.target.tenant_guard.value, job.conflict_guard.expected_value]
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 ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?
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: [job.target.tenant_guard.value, ...job.compensation.members.map((member) => member.primary_key.value)]
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 ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?
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: [job.target.tenant_guard.value, ...job.frozen_set.members.map((member) => member.primary_key.value)]
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 ${job.deduplication.components.map((component) => `${quoteMysqlIdentifier(component.column)} = ?`).join(" AND ")}
37458
+ WHERE ${where2.join(" AND ")}
35892
37459
  LIMIT 2`,
35893
- values: job.deduplication.components.map((component) => component.value)
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 ${quoteMysqlIdentifier(job.target.primary_key.column)} = ?
35900
- AND ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?
37473
+ WHERE ${where.join("\n AND ")}
35901
37474
  LIMIT 2`,
35902
- values: [job.target.primary_key.value, job.target.tenant_guard.value]
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 [rows] = await connection.query(`SELECT ${quoteMysqlIdentifier(job.version_advance.column)} AS __synapsor_result_version FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${quoteMysqlIdentifier(job.target.primary_key.column)} = ? AND ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ? FOR UPDATE`, [job.target.primary_key.value, job.target.tenant_guard.value]);
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 [rows] = await connection.query(
36182
- `SELECT ${compensationProjection2(job).map(quoteMysqlIdentifier).join(", ")} FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ? AND ${quoteMysqlIdentifier(job.target.primary_key.column)} IN (${job.compensation.members.map(() => "?").join(", ")}) ORDER BY ${quoteMysqlIdentifier(job.target.primary_key.column)} ASC FOR UPDATE`,
36183
- [job.target.tenant_guard.value, ...job.compensation.members.map((member) => member.primary_key.value)]
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 [updated] = await connection.query(
36306
- `UPDATE ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} SET ${assignments.join(", ")} WHERE ${quoteMysqlIdentifier(job.target.primary_key.column)} = ? AND ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ? AND ${quoteMysqlIdentifier(expected.column)} = ?`,
36307
- [...Object.values(job.patch), member.primary_key.value, job.target.tenant_guard.value, expected.value]
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 [deleted] = await connection.query(
36313
- `DELETE FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${quoteMysqlIdentifier(job.target.primary_key.column)} = ? AND ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ? AND ${quoteMysqlIdentifier(expected.column)} = ?`,
36314
- [member.primary_key.value, job.target.tenant_guard.value, expected.value]
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 [rows] = await connection.query(
36326
- `SELECT ${[...columns].map(quoteMysqlIdentifier).join(", ")} FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ? 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`,
36327
- [job.target.tenant_guard.value, ...job.frozen_set.members.map((member) => member.primary_key.value)]
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 [existing] = await connection.query(
36360
- `SELECT 1 AS found FROM ${quoteMysqlIdentifier(job.target.schema)}.${quoteMysqlIdentifier(job.target.table)} WHERE ${components.map((component) => `${quoteMysqlIdentifier(component.column)} = ?`).join(" AND ")} LIMIT 1 FOR UPDATE`,
36361
- components.map((component) => component.value)
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 ${quoteMysqlIdentifier(job.target.primary_key.column)} = ?
36387
- AND ${quoteMysqlIdentifier(job.target.tenant_guard.column)} = ?
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 [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 ${components.map((component) => `${quoteMysqlIdentifier(component.column)} = ?`).join(" AND ")}`, components.map((component) => component.value));
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.121",
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.2",
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.1"
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.1",
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 = [`Synapsor Runner doctor: ${report.ok ? "ok" : "failed"}`, `Config: ${report.config_path}`, `Mode: ${report.mode}`];
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
- if (proposal.state !== "approved" && proposal.state !== "pending_worker" && proposal.state !== "applied") {
45808
- throw new Error(`local proposal ${job.proposal_id} is ${proposal.state}, not approved for writeback`);
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
- await verifyStoredApprovalAuthority(config, configPath, store, proposal, matching);
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.length > 0) {
45904
- if (args.includes("--from-env") || args.includes("--schema") || args.includes("--mode") || args.includes("--engine")) {
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
- await startPolling(config, adapters, controller.signal);
45914
- return 0;
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
- const token = (optionalArg(args, "--token") ?? process2.env.SYNAPSOR_CLOUD_TOKEN ?? process2.env.SYNAPSOR_RUNNER_TOKEN ?? "").trim();
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 || !token) {
46200
- throw new Error("cloud push upload requires --api-url plus --token/SYNAPSOR_CLOUD_TOKEN. Use --dry-run for local validation without a network call.");
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
- const parsed = JSON.parse(await fs8.readFile(configPath, "utf8"));
46326
- if (!parsed.cloud) {
46327
- process2.stdout.write(`cloud config missing in ${configPath}
46328
- `);
46329
- return 1;
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
- if (!baseUrl || !runnerToken) {
46342
- process2.stdout.write("missing Cloud connection settings\n");
46343
- return 1;
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
- process2.stdout.write(`cloud connection failed: status ${report.status}
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(parsed.cloud.project_id || "token_scope"),
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 push validates and normalizes the contract locally, then prints the
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