@synapsor/runner 1.6.3 → 1.6.4

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 (97) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +72 -8
  3. package/CONTRIBUTING.md +10 -3
  4. package/README.md +61 -93
  5. package/SECURITY.md +7 -3
  6. package/THREAT_MODEL.md +37 -0
  7. package/dist/authoring.mjs +98 -13
  8. package/dist/cli.d.ts +12 -1
  9. package/dist/cli.d.ts.map +1 -1
  10. package/dist/local-ui.d.ts +60 -0
  11. package/dist/local-ui.d.ts.map +1 -1
  12. package/dist/runner.mjs +7321 -2000
  13. package/dist/runtime.mjs +528 -51
  14. package/docs/README.md +14 -0
  15. package/docs/agent-guided-setup.md +7 -7
  16. package/docs/aggregate-reads.md +14 -5
  17. package/docs/app-owned-executors.md +5 -6
  18. package/docs/auto-boundary-and-scoped-explore.md +58 -6
  19. package/docs/capability-authoring.md +2 -2
  20. package/docs/client-recipes.md +1 -1
  21. package/docs/cloud-mode.md +7 -7
  22. package/docs/conformance.md +6 -3
  23. package/docs/current-scope.md +13 -2
  24. package/docs/cursor-plugin.md +3 -3
  25. package/docs/database-enforced-scope.md +6 -0
  26. package/docs/dsl-reference.md +12 -1
  27. package/docs/fresh-developer-usability.md +6 -6
  28. package/docs/getting-started-own-database.md +41 -41
  29. package/docs/guided-onboarding.md +98 -30
  30. package/docs/limitations.md +34 -14
  31. package/docs/local-mode.md +20 -20
  32. package/docs/mcp-audit.md +14 -14
  33. package/docs/mcp-client-setup.md +11 -14
  34. package/docs/mcp-clients.md +1 -1
  35. package/docs/migrating-to-synapsor-spec.md +5 -1
  36. package/docs/openai-agents-sdk.md +2 -2
  37. package/docs/proposal-evidence-freshness.md +7 -7
  38. package/docs/recipes.md +6 -6
  39. package/docs/release-notes.md +86 -10
  40. package/docs/release-policy.md +20 -7
  41. package/docs/reversible-change-sets.md +1 -1
  42. package/docs/reviewed-database-views.md +132 -0
  43. package/docs/reviewed-relationships.md +245 -0
  44. package/docs/rfcs/006-reviewed-reversible-change-sets.md +1 -1
  45. package/docs/runner-bundles.md +7 -7
  46. package/docs/schema-api-candidates.md +1 -1
  47. package/docs/security-boundary.md +30 -0
  48. package/docs/troubleshooting-first-run.md +52 -12
  49. package/docs/use-your-own-database.md +6 -4
  50. package/docs/workbench-ask.md +279 -0
  51. package/docs/writeback-executors.md +7 -7
  52. package/examples/auto-boundary-churn/README.md +1 -1
  53. package/examples/community-solar-clean-room/README.md +7 -0
  54. package/examples/community-solar-clean-room/docker-compose.yml +16 -0
  55. package/examples/community-solar-clean-room/package.json +15 -0
  56. package/examples/community-solar-clean-room/prisma/schema.prisma +171 -0
  57. package/examples/community-solar-clean-room/seed/postgres.sql +571 -0
  58. package/examples/fitflow-guided-onboarding/README.md +2 -2
  59. package/examples/openai-agents-http/Makefile +1 -1
  60. package/examples/openai-agents-http/README.md +1 -1
  61. package/examples/openai-agents-stdio/Makefile +1 -1
  62. package/examples/openai-agents-stdio/README.md +2 -2
  63. package/examples/openai-agents-stdio/agent.py +1 -4
  64. package/examples/reference-support-billing-app/README.md +16 -16
  65. package/examples/retail-clean-room/README.md +7 -0
  66. package/examples/retail-clean-room/docker-compose.yml +16 -0
  67. package/examples/retail-clean-room/package.json +9 -0
  68. package/examples/retail-clean-room/prisma/schema.prisma +239 -0
  69. package/examples/retail-clean-room/seed/postgres.sql +781 -0
  70. package/examples/retail-clean-room/view-recipe/average-retained-revenue.synapsor.sql +21 -0
  71. package/examples/retail-clean-room/view-recipe/synapsor.runner.json +25 -0
  72. package/examples/support-plan-credit/mcp-client-examples/claude-code.sh +1 -4
  73. package/examples/support-plan-credit/mcp-client-examples/claude-desktop.json +1 -4
  74. package/examples/support-plan-credit/mcp-client-examples/codex.config.toml +1 -4
  75. package/examples/support-plan-credit/mcp-client-examples/cursor-global.mcp.json +1 -4
  76. package/examples/support-plan-credit/mcp-client-examples/cursor-project.mcp.json +1 -4
  77. package/examples/support-plan-credit/mcp-client-examples/generic-stdio.json +1 -4
  78. package/examples/support-plan-credit/mcp-client-examples/generic-stdio.mjs +1 -1
  79. package/examples/support-plan-credit/mcp-client-examples/google-adk.py +1 -1
  80. package/examples/support-plan-credit/mcp-client-examples/langchain.mjs +1 -1
  81. package/examples/support-plan-credit/mcp-client-examples/llamaindex.py +1 -1
  82. package/examples/support-plan-credit/mcp-client-examples/openai-agents-stdio.ts +1 -1
  83. package/examples/support-plan-credit/mcp-client-examples/vscode.mcp.json +1 -4
  84. package/fixtures/compatibility/published-1.6.3/manifest.json +76 -0
  85. package/fixtures/compatibility/published-1.6.3/sources/packages/dsl/examples/aggregate-read.synapsor.sql +21 -0
  86. package/fixtures/compatibility/published-1.6.3/sources/packages/dsl/examples/billing-late-fee.synapsor.sql +56 -0
  87. package/fixtures/compatibility/published-1.6.3/sources/packages/dsl/examples/bounded-set-multi-term.synapsor.sql +30 -0
  88. package/fixtures/compatibility/published-1.6.3/sources/packages/dsl/examples/principal-row-scope.synapsor.sql +23 -0
  89. package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/aggregate-read/contract.json +119 -0
  90. package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/approval-quorum/contract.json +44 -0
  91. package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/bounded-set-threats/contract.json +115 -0
  92. package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/principal-row-scope/contract.json +78 -0
  93. package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/proposal-capability/contract.json +101 -0
  94. package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/conformance/reversible-change-sets/contract.json +98 -0
  95. package/fixtures/compatibility/published-1.6.3/sources/packages/spec/fixtures/valid/basic-read.contract.json +60 -0
  96. package/llms.txt +10 -5
  97. package/package.json +8 -2
package/dist/runtime.mjs CHANGED
@@ -184,6 +184,7 @@ var PROTECTED_READ_KEYS = /* @__PURE__ */ new Set([
184
184
  "generation_lock_fingerprint",
185
185
  "predicates",
186
186
  "relationship",
187
+ "relationships",
187
188
  "row_order_by",
188
189
  "aggregate",
189
190
  "limits"
@@ -202,6 +203,19 @@ var PROTECTED_RELATIONSHIP_KEYS = /* @__PURE__ */ new Set([
202
203
  "cardinality",
203
204
  "max_fan_out"
204
205
  ]);
206
+ var PROTECTED_RELATIONSHIP_PATH_KEYS = /* @__PURE__ */ new Set(["name", "links"]);
207
+ var PROTECTED_RELATIONSHIP_LINK_KEYS = /* @__PURE__ */ new Set([
208
+ "schema",
209
+ "table",
210
+ "primary_key",
211
+ "tenant_key",
212
+ "principal_scope_key",
213
+ "local_key",
214
+ "target_key",
215
+ "cardinality",
216
+ "max_fan_out",
217
+ "unmatched_rows"
218
+ ]);
205
219
  var PROTECTED_ROW_ORDER_KEYS = /* @__PURE__ */ new Set(["field", "direction"]);
206
220
  var PROTECTED_AGGREGATE_KEYS = /* @__PURE__ */ new Set([
207
221
  "counted_entity",
@@ -666,7 +680,18 @@ function validateProtectedRead(capability, path3, errors) {
666
680
  const keptOut = new Set(Array.isArray(capability.kept_out_fields) ? capability.kept_out_fields.filter((field) => typeof field === "string") : []);
667
681
  const subject = isRecord(capability.subject) ? capability.subject : {};
668
682
  const trustedScopeFields = new Set([subject.tenant_key, subject.principal_scope_key].filter((field) => isSafeIdentifier(field)));
669
- const relationshipName = validateProtectedRelationship(protectedRead.relationship, keptOut, trustedScopeFields, `${path3}.protected_read.relationship`, errors);
683
+ const relationshipNames = /* @__PURE__ */ new Set();
684
+ const legacyRelationshipName = validateProtectedRelationship(protectedRead.relationship, keptOut, trustedScopeFields, `${path3}.protected_read.relationship`, errors);
685
+ if (legacyRelationshipName)
686
+ relationshipNames.add(legacyRelationshipName);
687
+ if (protectedRead.relationship !== void 0 && protectedRead.relationships !== void 0) {
688
+ errors.push({
689
+ path: `${path3}.protected_read`,
690
+ code: "PROTECTED_RELATIONSHIP_FORMS_CONFLICT",
691
+ message: "protected_read must use either legacy relationship or reviewed relationships, not both."
692
+ });
693
+ }
694
+ validateProtectedRelationshipPaths(protectedRead.relationships, keptOut, trustedScopeFields, `${path3}.protected_read.relationships`, relationshipNames, errors);
670
695
  if (!isRecord(capability.evidence) || capability.evidence.required !== true || capability.evidence.query_audit !== true) {
671
696
  errors.push({
672
697
  path: `${path3}.evidence`,
@@ -685,7 +710,7 @@ function validateProtectedRead(capability, path3, errors) {
685
710
  return;
686
711
  }
687
712
  checkUnknownKeys(predicate, PROTECTED_PREDICATE_KEYS, predicatePath, errors);
688
- validateProtectedFieldReference(predicate.field, predicate.relationship, relationshipName, keptOut, trustedScopeFields, predicatePath, errors);
713
+ validateProtectedFieldReference(predicate.field, predicate.relationship, relationshipNames, keptOut, trustedScopeFields, predicatePath, errors);
689
714
  if (!["eq", "neq", "lt", "lte", "gt", "gte", "in"].includes(String(predicate.operator))) {
690
715
  errors.push({ path: `${predicatePath}.operator`, code: "INVALID_PROTECTED_PREDICATE_OPERATOR", message: "protected predicate operator must be eq, neq, lt, lte, gt, gte, or in." });
691
716
  }
@@ -745,7 +770,7 @@ function validateProtectedRead(capability, path3, errors) {
745
770
  validateProtectedAggregate(protectedRead.aggregate, {
746
771
  args,
747
772
  referencedArgs,
748
- relationshipName,
773
+ relationshipNames,
749
774
  keptOut,
750
775
  trustedScopeFields,
751
776
  maxGroups: isRecord(protectedRead.limits) ? protectedRead.limits.max_groups : void 0,
@@ -788,6 +813,65 @@ function validateProtectedRelationship(value, keptOut, trustedScopeFields, path3
788
813
  }
789
814
  return isSafeIdentifier(value.name) ? value.name : void 0;
790
815
  }
816
+ function validateProtectedRelationshipPaths(value, keptOut, trustedScopeFields, path3, relationshipNames, errors) {
817
+ if (value === void 0)
818
+ return;
819
+ if (!Array.isArray(value) || value.length < 1 || value.length > 3) {
820
+ errors.push({
821
+ path: path3,
822
+ code: "INVALID_PROTECTED_RELATIONSHIP_PATHS",
823
+ message: "protected relationships must contain 1 through 3 reviewed paths."
824
+ });
825
+ return;
826
+ }
827
+ value.forEach((candidate, pathIndex) => {
828
+ const candidatePath = `${path3}[${pathIndex}]`;
829
+ if (!isRecord(candidate)) {
830
+ errors.push({ path: candidatePath, code: "PROTECTED_RELATIONSHIP_PATH_NOT_OBJECT", message: "protected relationship path must be an object." });
831
+ return;
832
+ }
833
+ checkUnknownKeys(candidate, PROTECTED_RELATIONSHIP_PATH_KEYS, candidatePath, errors);
834
+ if (!isSafeIdentifier(candidate.name)) {
835
+ errors.push({ path: `${candidatePath}.name`, code: "INVALID_PROTECTED_RELATIONSHIP_IDENTIFIER", message: "relationship path name must be a fixed safe identifier." });
836
+ } else if (relationshipNames.has(candidate.name)) {
837
+ errors.push({ path: `${candidatePath}.name`, code: "DUPLICATE_PROTECTED_RELATIONSHIP", message: "protected relationship names must be unique." });
838
+ } else {
839
+ relationshipNames.add(candidate.name);
840
+ }
841
+ if (!Array.isArray(candidate.links) || candidate.links.length < 1 || candidate.links.length > 2) {
842
+ errors.push({ path: `${candidatePath}.links`, code: "INVALID_PROTECTED_RELATIONSHIP_LINKS", message: "a protected relationship path requires one or two reviewed many-to-one links." });
843
+ return;
844
+ }
845
+ candidate.links.forEach((link, linkIndex) => {
846
+ const linkPath = `${candidatePath}.links[${linkIndex}]`;
847
+ if (!isRecord(link)) {
848
+ errors.push({ path: linkPath, code: "PROTECTED_RELATIONSHIP_LINK_NOT_OBJECT", message: "protected relationship link must be an object." });
849
+ return;
850
+ }
851
+ checkUnknownKeys(link, PROTECTED_RELATIONSHIP_LINK_KEYS, linkPath, errors);
852
+ for (const key of ["schema", "table", "primary_key", "tenant_key", "local_key", "target_key"]) {
853
+ if (!isSafeIdentifier(link[key]))
854
+ errors.push({ path: `${linkPath}.${key}`, code: "INVALID_PROTECTED_RELATIONSHIP_IDENTIFIER", message: `${key} must be a fixed safe identifier.` });
855
+ }
856
+ if (link.principal_scope_key !== void 0 && !isSafeIdentifier(link.principal_scope_key)) {
857
+ errors.push({ path: `${linkPath}.principal_scope_key`, code: "INVALID_PROTECTED_RELATIONSHIP_IDENTIFIER", message: "principal_scope_key must be a fixed safe identifier." });
858
+ }
859
+ if (link.cardinality !== "many_to_one" || link.max_fan_out !== 1) {
860
+ errors.push({ path: linkPath, code: "PROTECTED_RELATIONSHIP_CARDINALITY_FORBIDDEN", message: "every protected relationship link must be reviewed many-to-one with max_fan_out 1." });
861
+ }
862
+ if (link.unmatched_rows !== "exclude" && link.unmatched_rows !== "keep_null") {
863
+ errors.push({ path: `${linkPath}.unmatched_rows`, code: "PROTECTED_RELATIONSHIP_NULL_SEMANTICS_REQUIRED", message: "every protected relationship link must freeze exclude or keep_null unmatched-row semantics." });
864
+ }
865
+ if (linkIndex === 0 && isSafeIdentifier(link.local_key) && (keptOut.has(link.local_key) || trustedScopeFields.has(link.local_key))) {
866
+ errors.push({
867
+ path: `${linkPath}.local_key`,
868
+ code: "PROTECTED_RELATIONSHIP_LOCAL_KEY_FORBIDDEN",
869
+ message: "the reviewed relationship cannot join through a kept-out or trusted-scope subject field."
870
+ });
871
+ }
872
+ });
873
+ });
874
+ }
791
875
  function validateProtectedAggregate(value, input) {
792
876
  const { path: path3, errors } = input;
793
877
  if (!isRecord(value)) {
@@ -811,14 +895,14 @@ function validateProtectedAggregate(value, input) {
811
895
  validateUniqueIdentifier(measure.name, measureNames, `${measurePath}.name`, "DUPLICATE_PROTECTED_MEASURE_NAME", errors);
812
896
  if (!["count", "count_distinct", "sum", "avg"].includes(String(measure.function)))
813
897
  errors.push({ path: `${measurePath}.function`, code: "INVALID_PROTECTED_MEASURE_FUNCTION", message: "measure function must be count, count_distinct, sum, or avg." });
814
- validateProtectedRelationshipReference(measure.relationship, input.relationshipName, `${measurePath}.relationship`, errors);
898
+ validateProtectedRelationshipReference(measure.relationship, input.relationshipNames, `${measurePath}.relationship`, errors);
815
899
  if (measure.function === "count") {
816
900
  if (measure.field !== void 0)
817
901
  errors.push({ path: `${measurePath}.field`, code: "PROTECTED_COUNT_FIELD_FORBIDDEN", message: "count measures count scoped subject rows and must not declare a field." });
818
902
  if (measure.relationship !== void 0)
819
903
  errors.push({ path: `${measurePath}.relationship`, code: "PROTECTED_COUNT_RELATIONSHIP_FORBIDDEN", message: "count measures count the scoped subject entity, not a joined relation." });
820
904
  } else {
821
- validateProtectedFieldReference(measure.field, measure.relationship, input.relationshipName, input.keptOut, input.trustedScopeFields, measurePath, errors);
905
+ validateProtectedFieldReference(measure.field, measure.relationship, input.relationshipNames, input.keptOut, input.trustedScopeFields, measurePath, errors);
822
906
  }
823
907
  });
824
908
  }
@@ -835,7 +919,7 @@ function validateProtectedAggregate(value, input) {
835
919
  }
836
920
  checkUnknownKeys(dimension, PROTECTED_DIMENSION_KEYS, dimensionPath, errors);
837
921
  validateUniqueIdentifier(dimension.name, dimensionNames, `${dimensionPath}.name`, "DUPLICATE_PROTECTED_DIMENSION_NAME", errors);
838
- validateProtectedFieldReference(dimension.field, dimension.relationship, input.relationshipName, input.keptOut, input.trustedScopeFields, dimensionPath, errors);
922
+ validateProtectedFieldReference(dimension.field, dimension.relationship, input.relationshipNames, input.keptOut, input.trustedScopeFields, dimensionPath, errors);
839
923
  });
840
924
  }
841
925
  }
@@ -849,7 +933,7 @@ function validateProtectedAggregate(value, input) {
849
933
  errors.push({ path: `${bucketPath}.name`, code: "INVALID_PROTECTED_TIME_BUCKET_NAME", message: "time bucket name must be a safe identifier." });
850
934
  if (value.time_bucket.bucket !== "day" && value.time_bucket.bucket !== "week" && value.time_bucket.bucket !== "month")
851
935
  errors.push({ path: `${bucketPath}.bucket`, code: "INVALID_PROTECTED_TIME_BUCKET", message: "time bucket must be day, week, or month." });
852
- validateProtectedFieldReference(value.time_bucket.field, value.time_bucket.relationship, input.relationshipName, input.keptOut, input.trustedScopeFields, bucketPath, errors);
936
+ validateProtectedFieldReference(value.time_bucket.field, value.time_bucket.relationship, input.relationshipNames, input.keptOut, input.trustedScopeFields, bucketPath, errors);
853
937
  }
854
938
  }
855
939
  if (value.comparison !== void 0) {
@@ -858,7 +942,7 @@ function validateProtectedAggregate(value, input) {
858
942
  errors.push({ path: comparisonPath, code: "PROTECTED_COMPARISON_NOT_OBJECT", message: "comparison must be an object." });
859
943
  } else {
860
944
  checkUnknownKeys(value.comparison, PROTECTED_COMPARISON_KEYS, comparisonPath, errors);
861
- validateProtectedFieldReference(value.comparison.field, value.comparison.relationship, input.relationshipName, input.keptOut, input.trustedScopeFields, comparisonPath, errors);
945
+ validateProtectedFieldReference(value.comparison.field, value.comparison.relationship, input.relationshipNames, input.keptOut, input.trustedScopeFields, comparisonPath, errors);
862
946
  if (!Array.isArray(value.comparison.ranges) || value.comparison.ranges.length < 1 || value.comparison.ranges.length > 2) {
863
947
  errors.push({ path: `${comparisonPath}.ranges`, code: "INVALID_PROTECTED_COMPARISON_RANGES", message: "comparison requires one or two bounded time ranges." });
864
948
  } else {
@@ -949,19 +1033,20 @@ function validateProtectedValue(value, args, referencedArgs, path3, errors) {
949
1033
  }
950
1034
  }
951
1035
  }
952
- function validateProtectedFieldReference(field, relationship, relationshipName, keptOut, trustedScopeFields, path3, errors) {
1036
+ function validateProtectedFieldReference(field, relationship, relationshipNames, keptOut, trustedScopeFields, path3, errors) {
953
1037
  if (!isSafeIdentifier(field)) {
954
1038
  errors.push({ path: `${path3}.field`, code: "INVALID_PROTECTED_FIELD", message: "protected field must be a fixed safe identifier." });
955
1039
  } else if (relationship === void 0 && (keptOut.has(field) || trustedScopeFields.has(field))) {
956
1040
  errors.push({ path: `${path3}.field`, code: "PROTECTED_FIELD_FORBIDDEN", message: "kept-out and trusted-scope fields cannot be selected, filtered, grouped, sorted, joined, aggregated, or counted distinctly." });
957
1041
  }
958
- validateProtectedRelationshipReference(relationship, relationshipName, `${path3}.relationship`, errors);
1042
+ validateProtectedRelationshipReference(relationship, relationshipNames, `${path3}.relationship`, errors);
959
1043
  }
960
- function validateProtectedRelationshipReference(value, relationshipName, path3, errors) {
1044
+ function validateProtectedRelationshipReference(value, relationshipNames, path3, errors) {
961
1045
  if (value === void 0)
962
1046
  return;
963
- if (!isSafeIdentifier(value) || value !== relationshipName)
964
- errors.push({ path: path3, code: "UNKNOWN_PROTECTED_RELATIONSHIP", message: "relationship must reference the capability's one reviewed many-to-one relationship." });
1047
+ if (!isSafeIdentifier(value) || !relationshipNames.has(value)) {
1048
+ errors.push({ path: path3, code: "UNKNOWN_PROTECTED_RELATIONSHIP", message: "relationship must reference one of the capability's reviewed many-to-one paths." });
1049
+ }
965
1050
  }
966
1051
  function validateUniqueIdentifier(value, seen, path3, code, errors) {
967
1052
  if (!isSafeIdentifier(value)) {
@@ -2191,7 +2276,7 @@ function validateRunnerCapabilityConfig(input) {
2191
2276
  );
2192
2277
  validateNotifications(input.notifications, strict, errors);
2193
2278
  validateSupervisedWorker(input.supervised_worker, input, strict, errors);
2194
- validateProposalFreshness(input.proposal_freshness, input.capabilities, strict, errors);
2279
+ validateProposalFreshness(input.proposal_freshness, input.capabilities, strict, errors, hasContracts);
2195
2280
  validateEffectiveContextCompatibility(input.trusted_context, input.contexts, input.capabilities, errors);
2196
2281
  validateApprovalPolicyReferences(input.capabilities, input.policies, errors);
2197
2282
  validateWritebackReadiness(input.sources, input.capabilities, input.mode, errors, warnings);
@@ -3597,7 +3682,7 @@ function validateCapabilities2(value, sources, contexts, executors, mode, strict
3597
3682
  capabilityNames.set(capability.name, index);
3598
3683
  });
3599
3684
  }
3600
- function validateProposalFreshness(value, capabilitiesValue, strict, errors) {
3685
+ function validateProposalFreshness(value, capabilitiesValue, strict, errors, hasContracts = false) {
3601
3686
  if (value === void 0) return;
3602
3687
  if (!isRecord2(value)) {
3603
3688
  errors.push({
@@ -3625,7 +3710,9 @@ function validateProposalFreshness(value, capabilitiesValue, strict, errors) {
3625
3710
  }
3626
3711
  const proposal = byName.get(proposalName);
3627
3712
  if (!proposal) {
3628
- errors.push({ path: path3, code: "FRESHNESS_PROPOSAL_UNKNOWN", message: `Freshness policy references unknown proposal capability ${proposalName}.` });
3713
+ if (!hasContracts) {
3714
+ errors.push({ path: path3, code: "FRESHNESS_PROPOSAL_UNKNOWN", message: `Freshness policy references unknown proposal capability ${proposalName}.` });
3715
+ }
3629
3716
  continue;
3630
3717
  }
3631
3718
  if (proposal.kind !== "proposal") {
@@ -3675,7 +3762,9 @@ function validateProposalFreshness(value, capabilitiesValue, strict, errors) {
3675
3762
  }
3676
3763
  const dependency = byName.get(String(rawDependency.capability));
3677
3764
  if (!dependency) {
3678
- errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_UNKNOWN", message: `Unknown supporting capability ${String(rawDependency.capability)}.` });
3765
+ if (!hasContracts) {
3766
+ errors.push({ path: `${dependencyPath}.capability`, code: "FRESHNESS_DEPENDENCY_UNKNOWN", message: `Unknown supporting capability ${String(rawDependency.capability)}.` });
3767
+ }
3679
3768
  continue;
3680
3769
  }
3681
3770
  if (dependency.kind !== "read" || dependency.protected_read !== void 0 || dependency.aggregate !== void 0) {
@@ -6246,10 +6335,16 @@ function quotePostgresIdentifier(identifier) {
6246
6335
  return `"${identifier}"`;
6247
6336
  }
6248
6337
  async function inspectPostgresRlsTarget(client, input) {
6338
+ return inspectPostgresRlsTargetInternal(client, input, /* @__PURE__ */ new Set());
6339
+ }
6340
+ async function inspectPostgresRlsTargetInternal(client, input, ancestors) {
6341
+ const targetKey = `${input.schema}.${input.table}`;
6249
6342
  const metadata = await client.query(
6250
6343
  `SELECT
6251
6344
  current_user AS role,
6252
6345
  c.oid::text AS table_oid,
6346
+ c.relkind AS relation_kind,
6347
+ c.reloptions AS relation_options,
6253
6348
  c.relrowsecurity AS row_security_enabled,
6254
6349
  c.relforcerowsecurity AS force_row_security,
6255
6350
  owner.rolname = current_user AS role_is_table_owner,
@@ -6259,19 +6354,26 @@ async function inspectPostgresRlsTarget(client, input) {
6259
6354
  JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
6260
6355
  JOIN pg_catalog.pg_roles owner ON owner.oid = c.relowner
6261
6356
  JOIN pg_catalog.pg_roles current_role_info ON current_role_info.rolname = current_user
6262
- WHERE n.nspname = $1 AND c.relname = $2 AND c.relkind IN ('r', 'p')
6357
+ WHERE n.nspname = $1 AND c.relname = $2 AND c.relkind IN ('r', 'p', 'v')
6263
6358
  LIMIT 1`,
6264
6359
  [input.schema, input.table]
6265
6360
  );
6266
6361
  const row = metadata.rows[0];
6362
+ const relationKind = row?.relation_kind === "v" ? "view" : row?.relation_kind === "p" ? "partitioned_table" : "table";
6363
+ const relationOptions = Array.isArray(row?.relation_options) ? row.relation_options.filter((value) => typeof value === "string") : [];
6267
6364
  const requiredOperations = [...new Set(input.operations)].sort();
6268
6365
  const report = {
6269
6366
  schema: input.schema,
6270
6367
  table: input.table,
6368
+ ...row ? { target_kind: relationKind } : {},
6271
6369
  role: typeof row?.role === "string" ? row.role : "unknown",
6272
6370
  table_exists: Boolean(row),
6273
6371
  row_security_enabled: row?.row_security_enabled === true,
6274
6372
  force_row_security: row?.force_row_security === true,
6373
+ ...relationKind === "view" && row ? {
6374
+ security_invoker: relationOptions.includes("security_invoker=true"),
6375
+ security_barrier: relationOptions.includes("security_barrier=true")
6376
+ } : {},
6275
6377
  role_is_superuser: row?.role_is_superuser === true,
6276
6378
  role_has_bypassrls: row?.role_has_bypassrls === true,
6277
6379
  role_is_table_owner: row?.role_is_table_owner === true,
@@ -6286,6 +6388,70 @@ async function inspectPostgresRlsTarget(client, input) {
6286
6388
  report.errors.push("POSTGRES_RLS_TARGET_NOT_FOUND");
6287
6389
  return report;
6288
6390
  }
6391
+ if (relationKind === "view") {
6392
+ if (ancestors.has(targetKey)) {
6393
+ report.errors.push("POSTGRES_RLS_VIEW_DEPENDENCY_CYCLE");
6394
+ return report;
6395
+ }
6396
+ if (requiredOperations.some((operation) => operation !== "SELECT")) {
6397
+ report.errors.push("POSTGRES_RLS_VIEW_READ_ONLY_REQUIRED");
6398
+ }
6399
+ if (!report.security_invoker) report.errors.push("POSTGRES_RLS_VIEW_SECURITY_INVOKER_REQUIRED");
6400
+ if (!report.security_barrier) report.errors.push("POSTGRES_RLS_VIEW_SECURITY_BARRIER_REQUIRED");
6401
+ if (report.role_is_superuser) report.errors.push("POSTGRES_RLS_ROLE_SUPERUSER");
6402
+ if (report.role_has_bypassrls) report.errors.push("POSTGRES_RLS_ROLE_BYPASSRLS");
6403
+ if (report.role_is_table_owner) report.errors.push("POSTGRES_RLS_ROLE_TABLE_OWNER");
6404
+ const dependencies = await client.query(
6405
+ `SELECT DISTINCT
6406
+ dependency_namespace.nspname AS schema_name,
6407
+ dependency_relation.relname AS relation_name,
6408
+ dependency_relation.relkind AS relation_kind
6409
+ FROM pg_catalog.pg_rewrite rewrite
6410
+ JOIN pg_catalog.pg_depend dependency
6411
+ ON dependency.classid = 'pg_rewrite'::regclass
6412
+ AND dependency.objid = rewrite.oid
6413
+ AND dependency.refclassid = 'pg_class'::regclass
6414
+ JOIN pg_catalog.pg_class dependency_relation
6415
+ ON dependency_relation.oid = dependency.refobjid
6416
+ JOIN pg_catalog.pg_namespace dependency_namespace
6417
+ ON dependency_namespace.oid = dependency_relation.relnamespace
6418
+ WHERE rewrite.ev_class = $1::oid
6419
+ AND dependency.deptype = 'n'
6420
+ AND dependency.refobjid <> rewrite.ev_class
6421
+ AND dependency_relation.relkind IN ('r', 'p', 'v')
6422
+ ORDER BY dependency_namespace.nspname, dependency_relation.relname`,
6423
+ [row.table_oid]
6424
+ );
6425
+ if (!dependencies.rows.length) report.errors.push("POSTGRES_RLS_VIEW_DEPENDENCIES_UNVERIFIED");
6426
+ const nextAncestors = new Set(ancestors);
6427
+ nextAncestors.add(targetKey);
6428
+ report.referenced_relations = [];
6429
+ for (const dependency of dependencies.rows) {
6430
+ if (typeof dependency.schema_name !== "string" || typeof dependency.relation_name !== "string") {
6431
+ report.errors.push("POSTGRES_RLS_VIEW_DEPENDENCIES_UNVERIFIED");
6432
+ continue;
6433
+ }
6434
+ const dependencyReport = await inspectPostgresRlsTargetInternal(client, {
6435
+ schema: dependency.schema_name,
6436
+ table: dependency.relation_name,
6437
+ scope: input.scope,
6438
+ operations: ["SELECT"]
6439
+ }, nextAncestors);
6440
+ report.referenced_relations.push({
6441
+ schema: dependencyReport.schema,
6442
+ table: dependencyReport.table,
6443
+ ...dependencyReport.target_kind ? { target_kind: dependencyReport.target_kind } : {},
6444
+ ok: dependencyReport.ok,
6445
+ errors: dependencyReport.errors
6446
+ });
6447
+ for (const error of dependencyReport.errors) {
6448
+ report.errors.push(`POSTGRES_RLS_VIEW_DEPENDENCY_FAILED:${dependencyReport.schema}.${dependencyReport.table}:${error}`);
6449
+ }
6450
+ }
6451
+ report.errors = [...new Set(report.errors)];
6452
+ report.ok = report.errors.length === 0;
6453
+ return report;
6454
+ }
6289
6455
  const policies = await client.query(
6290
6456
  `SELECT
6291
6457
  p.polname AS name,
@@ -13435,10 +13601,22 @@ var HIGH_CONFIDENCE_RULES = [
13435
13601
  "paymentmethod",
13436
13602
  "paymentinstrument",
13437
13603
  "paymentdetails",
13604
+ "paymentcard",
13605
+ "cardonfile",
13606
+ "cardholder",
13607
+ "cardholdername",
13608
+ "cardexpiry",
13609
+ "cardexpiration",
13438
13610
  "cardnumber",
13611
+ "fullpan",
13439
13612
  "creditcard",
13440
13613
  "debitcard",
13441
13614
  "cardtoken",
13615
+ "card",
13616
+ "cc",
13617
+ "ccnumber",
13618
+ "ccnum",
13619
+ "pan",
13442
13620
  "cvv",
13443
13621
  "cvc",
13444
13622
  "bankaccount",
@@ -13493,6 +13671,7 @@ var HIGH_CONFIDENCE_RULES = [
13493
13671
  "phone",
13494
13672
  "phonenumber",
13495
13673
  "telephone",
13674
+ "address",
13496
13675
  "streetaddress",
13497
13676
  "homeaddress",
13498
13677
  "mailingaddress",
@@ -13596,7 +13775,15 @@ function rule(code, reason, patterns) {
13596
13775
  return {
13597
13776
  code,
13598
13777
  reason,
13599
- matches: (value) => patterns.some((pattern) => value.compact.includes(pattern) || value.descriptionCompact.includes(pattern))
13778
+ matches: (value) => patterns.some((pattern) => {
13779
+ if (pattern === "pan" || pattern === "cc" || pattern === "card") {
13780
+ return value.compact === pattern || value.descriptionTokens.has(pattern);
13781
+ }
13782
+ if (pattern.length <= 4) {
13783
+ return value.compact === pattern || value.tokens.has(pattern) || value.descriptionTokens.has(pattern);
13784
+ }
13785
+ return value.compact.includes(pattern) || value.descriptionCompact.includes(pattern);
13786
+ })
13600
13787
  };
13601
13788
  }
13602
13789
  function normalizeEvidence(input) {
@@ -13606,6 +13793,7 @@ function normalizeEvidence(input) {
13606
13793
  compact: identifier.replaceAll("_", ""),
13607
13794
  tokens: new Set(identifier.split("_").filter(Boolean)),
13608
13795
  descriptionCompact: description.replaceAll("_", ""),
13796
+ descriptionTokens: new Set(description.split("_").filter(Boolean)),
13609
13797
  dataType: String(input.dataType ?? "").trim().toLowerCase()
13610
13798
  };
13611
13799
  }
@@ -13669,7 +13857,22 @@ function rolePostureFingerprint(inspection) {
13669
13857
  }))
13670
13858
  });
13671
13859
  }
13672
- var TENANT_COLUMNS = /* @__PURE__ */ new Set(["tenant_id", "account_id", "organization_id", "org_id", "workspace_id", "customer_id"]);
13860
+ var TENANT_COLUMNS = /* @__PURE__ */ new Set([
13861
+ "tenant_id",
13862
+ "account_id",
13863
+ "organization_id",
13864
+ "org_id",
13865
+ "workspace_id",
13866
+ "customer_id",
13867
+ "merchant_id",
13868
+ "store_id",
13869
+ "company_id",
13870
+ "team_id",
13871
+ "facility_id",
13872
+ "property_id",
13873
+ "clinic_id",
13874
+ "project_id"
13875
+ ]);
13673
13876
  var CONFLICT_COLUMNS = /* @__PURE__ */ new Set(["updated_at", "modified_at", "row_version", "version", "lock_version", "etag"]);
13674
13877
  var IMMUTABLE_COLUMNS = /* @__PURE__ */ new Set(["id", "uuid", "created_at", "created_by"]);
13675
13878
  var LARGE_OR_BINARY_TYPES = /* @__PURE__ */ new Set([
@@ -13906,7 +14109,7 @@ async function inspectMysql(options) {
13906
14109
  const [versionRows] = await connection.query("SELECT VERSION() AS version, CURRENT_USER() AS `current_user`");
13907
14110
  const schemaParam = options.schema ?? null;
13908
14111
  const [schemaRows] = await connection.query(
13909
- `SELECT schema_name FROM information_schema.schemata
14112
+ `SELECT schema_name AS schema_name FROM information_schema.schemata
13910
14113
  WHERE schema_name NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
13911
14114
  ORDER BY schema_name`
13912
14115
  );
@@ -13922,8 +14125,8 @@ async function inspectMysql(options) {
13922
14125
  );
13923
14126
  const [columnRows] = await connection.query(
13924
14127
  `SELECT table_schema AS \`schema\`, table_name AS table_name, column_name AS name,
13925
- data_type, column_type AS udt_name, is_nullable,
13926
- column_default, extra AS is_generated, extra AS is_identity,
14128
+ data_type AS data_type, column_type AS udt_name, is_nullable AS is_nullable,
14129
+ column_default AS column_default, extra AS is_generated, extra AS is_identity,
13927
14130
  ordinal_position AS ordinal_position, column_comment AS comment
13928
14131
  FROM information_schema.columns
13929
14132
  WHERE table_schema NOT IN ('information_schema', 'mysql', 'performance_schema', 'sys')
@@ -17430,8 +17633,8 @@ async function preflightPostgresDatabaseScope(inputConfig, env = process.env, cr
17430
17633
  }
17431
17634
  }
17432
17635
  }
17433
- var SUPPORTED_GENERATED_AUTHORITY_COMPILER_VERSIONS = /* @__PURE__ */ new Set(["1.6.0", "1.6.3"]);
17434
- var SUPPORTED_GENERATED_AUTHORITY_SPEC_VERSIONS = /* @__PURE__ */ new Set(["1.5.0", "1.5.1", "1.6.0"]);
17636
+ var SUPPORTED_GENERATED_AUTHORITY_COMPILER_VERSIONS = /* @__PURE__ */ new Set(["1.6.0", "1.6.3", "1.6.4"]);
17637
+ var SUPPORTED_GENERATED_AUTHORITY_SPEC_VERSIONS = /* @__PURE__ */ new Set(["1.5.0", "1.5.1", "1.6.0", "1.7.0"]);
17435
17638
  async function preflightGeneratedAuthority(inputConfig, env = process.env, inspect = inspectDatabase) {
17436
17639
  const config = resolveRuntimeConfig(inputConfig);
17437
17640
  const protectedCapabilities = localCapabilities(config).filter((capability) => capability.protected_read);
@@ -17472,20 +17675,37 @@ async function preflightGeneratedAuthority(inputConfig, env = process.env, inspe
17472
17675
  const inspection = await inspect({
17473
17676
  engine: lock.engine,
17474
17677
  databaseUrlEnv: lock.source_env,
17678
+ ...lock.inspected_schema ? { schema: lock.inspected_schema } : {},
17475
17679
  statementTimeoutMs: Math.min(...protectedCapabilities.map((capability) => capability.protected_read.limits.statement_timeout_ms)),
17476
17680
  env
17477
17681
  });
17478
- const schemaFingerprint = schemaFingerprintForInspection(inspection);
17479
- const postureFingerprint = rolePostureFingerprint(inspection);
17480
- const changes = [
17481
- ...schemaFingerprint !== lock.schema_fingerprint ? ["schema metadata"] : [],
17482
- ...postureFingerprint !== lock.role_posture_fingerprint ? ["database role, grants, ownership, or RLS posture"] : []
17483
- ];
17484
- if (changes.length > 0) {
17485
- throw new McpRuntimeError(
17486
- "GENERATED_AUTHORITY_DRIFT",
17487
- `Generated protected authority is stale because ${changes.join(" and ")} changed. Rescan, review the semantic diff, regenerate, and activate a new digest.`
17488
- );
17682
+ if (lock.authority_dependencies) {
17683
+ if (credentialPostureFingerprintForGeneratedAuthority(inspection) !== lock.authority_dependencies.credential_posture_fingerprint) {
17684
+ throw new McpRuntimeError(
17685
+ "GENERATED_AUTHORITY_DRIFT",
17686
+ "Generated protected authority is stale because the database credential posture changed. Rescan, review, and activate a new digest."
17687
+ );
17688
+ }
17689
+ for (const capability of protectedCapabilities) {
17690
+ assertProtectedCapabilityDependenciesCurrent(
17691
+ capability,
17692
+ lock.authority_dependencies,
17693
+ inspection
17694
+ );
17695
+ }
17696
+ } else {
17697
+ const schemaFingerprint = schemaFingerprintForInspection(inspection);
17698
+ const postureFingerprint = rolePostureFingerprint(inspection);
17699
+ const changes = [
17700
+ ...schemaFingerprint !== lock.schema_fingerprint ? ["schema metadata"] : [],
17701
+ ...postureFingerprint !== lock.role_posture_fingerprint ? ["database role, grants, ownership, or RLS posture"] : []
17702
+ ];
17703
+ if (changes.length > 0) {
17704
+ throw new McpRuntimeError(
17705
+ "GENERATED_AUTHORITY_DRIFT",
17706
+ `Generated protected authority is stale because ${changes.join(" and ")} changed. Rescan, review the semantic diff, regenerate, and activate a new digest.`
17707
+ );
17708
+ }
17489
17709
  }
17490
17710
  const role = inspection.role_posture;
17491
17711
  if (!role?.verified || !role.read_only || role.superuser !== false || role.bypass_rls !== false || role.writable_relations.length > 0 || role.owned_relations.length > 0) {
@@ -17497,13 +17717,194 @@ async function preflightGeneratedAuthority(inputConfig, env = process.env, inspe
17497
17717
  }
17498
17718
  function assertGeneratedAuthorityLockShape(value) {
17499
17719
  const digest = /^sha256:[a-f0-9]{64}$/;
17500
- if (!value || value.schema_version !== "synapsor.generation-lock.v1" || !SUPPORTED_GENERATED_AUTHORITY_COMPILER_VERSIONS.has(value.compiler_version) || !SUPPORTED_GENERATED_AUTHORITY_SPEC_VERSIONS.has(value.spec_version) || value.engine !== "postgres" && value.engine !== "mysql" || !/^[A-Z_][A-Z0-9_]*$/.test(value.source_env) || !digest.test(value.schema_fingerprint) || !digest.test(value.role_posture_fingerprint) || !digest.test(value.evidence_fingerprint) || !digest.test(value.generated_contract_digest) || !digest.test(value.reviewed_overrides_digest) || !Array.isArray(value.protected_authority) || value.protected_authority.some((item) => typeof item !== "string")) {
17720
+ if (!value || value.schema_version !== "synapsor.generation-lock.v1" || !SUPPORTED_GENERATED_AUTHORITY_COMPILER_VERSIONS.has(value.compiler_version) || !SUPPORTED_GENERATED_AUTHORITY_SPEC_VERSIONS.has(value.spec_version) || value.engine !== "postgres" && value.engine !== "mysql" || !/^[A-Z_][A-Z0-9_]*$/.test(value.source_env) || value.inspected_schema !== void 0 && (typeof value.inspected_schema !== "string" || !value.inspected_schema) || !digest.test(value.schema_fingerprint) || !digest.test(value.role_posture_fingerprint) || !digest.test(value.evidence_fingerprint) || !digest.test(value.generated_contract_digest) || !digest.test(value.reviewed_overrides_digest) || !Array.isArray(value.protected_authority) || value.protected_authority.some((item) => typeof item !== "string") || value.authority_dependencies !== void 0 && !generatedAuthorityDependenciesValid(value.authority_dependencies, digest)) {
17501
17721
  throw new McpRuntimeError(
17502
17722
  "GENERATION_LOCK_INVALID",
17503
17723
  "The generated-authority lock is malformed or belongs to an unsupported compiler/spec version."
17504
17724
  );
17505
17725
  }
17506
17726
  }
17727
+ function generatedAuthorityDependenciesValid(value, digest) {
17728
+ if (!value || value.schema_version !== "synapsor.authority-dependencies.v1" || !digest.test(value.credential_posture_fingerprint) || !isRecord5(value.resources) || !isRecord5(value.relationships)) {
17729
+ return false;
17730
+ }
17731
+ for (const dependency of Object.values(value.resources)) {
17732
+ if (!dependency || typeof dependency.schema !== "string" || typeof dependency.table !== "string" || !Array.isArray(dependency.fields) || dependency.fields.some((field) => typeof field !== "string") || !digest.test(dependency.fingerprint)) {
17733
+ return false;
17734
+ }
17735
+ }
17736
+ for (const dependency of Object.values(value.relationships)) {
17737
+ if (!dependency || typeof dependency.root_resource !== "string" || typeof dependency.relationship_id !== "string" || !Array.isArray(dependency.links) || dependency.links.length < 1 || dependency.links.length > 2 || !digest.test(dependency.proof_digest) || canonicalJsonDigest(dependency.links) !== dependency.proof_digest) {
17738
+ return false;
17739
+ }
17740
+ }
17741
+ return true;
17742
+ }
17743
+ function credentialPostureFingerprintForGeneratedAuthority(inspection) {
17744
+ const role = inspection.role_posture;
17745
+ return canonicalJsonDigest({
17746
+ engine: inspection.engine,
17747
+ current_user: inspection.current_user,
17748
+ role: role ? {
17749
+ verified: role.verified,
17750
+ superuser: role.superuser,
17751
+ bypass_rls: role.bypass_rls,
17752
+ read_only: role.read_only,
17753
+ writable_relations: [...role.writable_relations].sort(),
17754
+ owned_relations: [...role.owned_relations].sort()
17755
+ } : null
17756
+ });
17757
+ }
17758
+ function assertProtectedCapabilityDependenciesCurrent(capability, dependencies, inspection) {
17759
+ const root = `${capability.target.schema}.${capability.target.table}`;
17760
+ const resourceIds = /* @__PURE__ */ new Set([root]);
17761
+ const protectedRead = capability.protected_read;
17762
+ const paths = [
17763
+ ...protectedRead.relationship ? [{
17764
+ name: protectedRead.relationship.name,
17765
+ links: [{
17766
+ schema: protectedRead.relationship.schema,
17767
+ table: protectedRead.relationship.table,
17768
+ local_key: protectedRead.relationship.local_key,
17769
+ target_key: protectedRead.relationship.target_key,
17770
+ cardinality: protectedRead.relationship.cardinality,
17771
+ max_fan_out: protectedRead.relationship.max_fan_out
17772
+ }]
17773
+ }] : [],
17774
+ ...(protectedRead.relationships ?? []).map((path3) => ({
17775
+ name: path3.name,
17776
+ links: path3.links.map((link) => ({
17777
+ schema: link.schema,
17778
+ table: link.table,
17779
+ local_key: link.local_key,
17780
+ target_key: link.target_key,
17781
+ cardinality: link.cardinality,
17782
+ max_fan_out: link.max_fan_out
17783
+ }))
17784
+ }))
17785
+ ];
17786
+ for (const path3 of paths) {
17787
+ const dependency = dependencies.relationships[`${root}::${path3.name}`];
17788
+ if (!dependency || dependency.root_resource !== root || dependency.relationship_id !== path3.name || path3.links.length !== dependency.links.length) {
17789
+ throw generatedDependencyDrift(capability, `relationship ${path3.name} is not bound to its generation-lock proof`);
17790
+ }
17791
+ let source = root;
17792
+ for (const [index, link] of path3.links.entries()) {
17793
+ const expected = dependency.links[index];
17794
+ const target = `${link.schema}.${link.table}`;
17795
+ if (expected.source_resource !== source || expected.target_resource !== target || expected.source_columns.length !== 1 || expected.source_columns[0] !== link.local_key || expected.target_columns.length !== 1 || expected.target_columns[0] !== link.target_key || link.cardinality !== "many_to_one" || link.max_fan_out !== 1) {
17796
+ throw generatedDependencyDrift(capability, `relationship ${path3.name} no longer matches its reviewed structural path`);
17797
+ }
17798
+ resourceIds.add(source);
17799
+ resourceIds.add(target);
17800
+ source = target;
17801
+ }
17802
+ if (relationshipDependencyFingerprintForGeneratedAuthority(dependency, inspection) !== dependency.proof_digest) {
17803
+ throw generatedDependencyDrift(capability, `relationship ${path3.name} foreign-key or uniqueness proof changed`);
17804
+ }
17805
+ }
17806
+ for (const resourceId of resourceIds) {
17807
+ const dependency = dependencies.resources[resourceId];
17808
+ if (!dependency || resourceDependencyFingerprintForGeneratedAuthority(dependency, inspection) !== dependency.fingerprint) {
17809
+ throw generatedDependencyDrift(capability, `resource ${resourceId} authority-bearing schema or RLS metadata changed`);
17810
+ }
17811
+ }
17812
+ }
17813
+ function generatedDependencyDrift(capability, reason) {
17814
+ return new McpRuntimeError(
17815
+ "GENERATED_AUTHORITY_DRIFT",
17816
+ `Generated protected capability ${capability.name} is stale because ${reason}. Rescan, review, and activate a new digest.`
17817
+ );
17818
+ }
17819
+ function resourceDependencyFingerprintForGeneratedAuthority(dependency, inspection) {
17820
+ const table = inspection.tables.find((candidate) => candidate.schema === dependency.schema && candidate.name === dependency.table);
17821
+ if (!table) return void 0;
17822
+ const columns = new Map(table.columns.map((column) => [column.name, column]));
17823
+ const selectedColumns = dependency.fields.map((name) => {
17824
+ const column = columns.get(name);
17825
+ if (!column) return void 0;
17826
+ return {
17827
+ name: column.name,
17828
+ data_type: column.data_type,
17829
+ nullable: column.nullable,
17830
+ default: column.default ?? null,
17831
+ generated: column.generated,
17832
+ identity: column.identity ?? false,
17833
+ enum_values: [...column.enum_values ?? []].sort()
17834
+ };
17835
+ });
17836
+ if (selectedColumns.some((column) => !column)) return void 0;
17837
+ return canonicalJsonDigest({
17838
+ engine: inspection.engine,
17839
+ schema: table.schema,
17840
+ table: table.name,
17841
+ type: table.type,
17842
+ primary_key: [...table.primary_key],
17843
+ columns: selectedColumns,
17844
+ row_level_security: table.row_level_security ?? "unknown",
17845
+ row_level_security_policies: [...table.row_level_security_policies ?? []].map((policy) => ({
17846
+ name: policy.name,
17847
+ command: policy.command,
17848
+ permissive: policy.permissive,
17849
+ roles: [...policy.roles].sort(),
17850
+ using_expression: policy.using_expression ?? null,
17851
+ check_expression: policy.check_expression ?? null
17852
+ })).sort((left, right) => left.name.localeCompare(right.name)),
17853
+ role_posture: table.role_posture ? {
17854
+ owner: table.role_posture.owner,
17855
+ current_role_is_owner: table.role_posture.current_role_is_owner,
17856
+ current_role_can_assume_owner: table.role_posture.current_role_can_assume_owner,
17857
+ privileges: table.role_posture.privileges,
17858
+ row_security_forced: table.role_posture.row_security_forced,
17859
+ row_security_effective_for_current_role: table.role_posture.row_security_effective_for_current_role
17860
+ } : null
17861
+ });
17862
+ }
17863
+ function relationshipDependencyFingerprintForGeneratedAuthority(dependency, inspection) {
17864
+ const tables = new Map(inspection.tables.map((table) => [`${table.schema}.${table.name}`, table]));
17865
+ const links = [];
17866
+ for (const expected of dependency.links) {
17867
+ const source = tables.get(expected.source_resource);
17868
+ const target = tables.get(expected.target_resource);
17869
+ if (!source || !target) return void 0;
17870
+ const foreignKey = source.foreign_keys.find((candidate) => candidate.name === expected.constraint_name && candidate.referenced_schema === target.schema && candidate.referenced_table === target.name && sameGeneratedColumns(candidate.columns, expected.source_columns) && sameGeneratedColumns(candidate.referenced_columns, expected.target_columns));
17871
+ if (!foreignKey) return void 0;
17872
+ const uniqueness = generatedRelationshipTargetUniqueness(target, expected.target_columns);
17873
+ if (!uniqueness || uniqueness.kind !== expected.target_uniqueness.kind || uniqueness.name !== expected.target_uniqueness.name || !sameGeneratedColumns(uniqueness.columns, expected.target_uniqueness.columns)) {
17874
+ return void 0;
17875
+ }
17876
+ links.push({
17877
+ constraint_name: foreignKey.name,
17878
+ source_resource: expected.source_resource,
17879
+ target_resource: expected.target_resource,
17880
+ source_columns: [...foreignKey.columns],
17881
+ target_columns: [...foreignKey.referenced_columns],
17882
+ target_uniqueness: uniqueness,
17883
+ nullable: foreignKey.columns.some((name) => source.columns.find((column) => column.name === name)?.nullable !== false),
17884
+ cardinality: "many_to_one",
17885
+ max_fan_out: 1
17886
+ });
17887
+ }
17888
+ return canonicalJsonDigest(links);
17889
+ }
17890
+ function generatedRelationshipTargetUniqueness(target, columns) {
17891
+ const same = (candidate) => sameGeneratedColumns(candidate, columns);
17892
+ if (same(target.primary_key)) {
17893
+ const named = target.unique_constraints.find((constraint2) => same(constraint2.columns)) ?? target.indexes.find((index2) => index2.unique === true && same(index2.columns ?? []));
17894
+ return {
17895
+ kind: "primary_key",
17896
+ name: named?.name ?? `${target.schema}.${target.name}.primary_key`,
17897
+ columns: [...columns]
17898
+ };
17899
+ }
17900
+ const constraint = target.unique_constraints.find((candidate) => same(candidate.columns));
17901
+ if (constraint) return { kind: "unique_constraint", name: constraint.name, columns: [...columns] };
17902
+ const index = target.indexes.find((candidate) => candidate.unique === true && same(candidate.columns ?? []));
17903
+ return index ? { kind: "unique_index", name: index.name, columns: [...columns] } : void 0;
17904
+ }
17905
+ function sameGeneratedColumns(left, right) {
17906
+ return left.length === right.length && left.every((value, index) => value === right[index]);
17907
+ }
17507
17908
  function createDefaultRuntimeStore(config, env, storePath) {
17508
17909
  const sharedPostgres = config.storage?.shared_postgres;
17509
17910
  if (sharedPostgres?.mode === "runtime_store") {
@@ -20965,14 +21366,14 @@ var RuntimeDatabasePools = class {
20965
21366
  if (timeoutMs) await client.query(`SET LOCAL statement_timeout = ${timeoutMs}`);
20966
21367
  if (input.source.database_scope?.mode === "postgres_rls") {
20967
21368
  for (const target of protectedReadTargets(input.capability)) {
20968
- const preflightKey = `${poolKey}\0${target.schema}\0${target.table}\0SELECT`;
21369
+ const preflightKey = `${poolKey}\0${target.schema}\0${target.table}\0SELECT\0${target.principalScoped ? "tenant+principal" : "tenant"}`;
20969
21370
  if (!this.postgresRlsPreflight.has(preflightKey)) {
20970
21371
  await assertPostgresRlsTarget(client, {
20971
21372
  schema: target.schema,
20972
21373
  table: target.table,
20973
21374
  scope: {
20974
21375
  tenantSetting: input.source.database_scope.tenant_setting,
20975
- principalSetting: input.source.database_scope.principal_setting
21376
+ ...target.principalScoped && input.source.database_scope.principal_setting ? { principalSetting: input.source.database_scope.principal_setting } : {}
20976
21377
  },
20977
21378
  operations: ["SELECT"]
20978
21379
  });
@@ -21153,27 +21554,51 @@ function runtimeReadQuery(capability, placeholderStyle, args, context) {
21153
21554
  function buildProtectedReadQuery(capability, placeholderStyle, args, context) {
21154
21555
  const protectedRead = capability.protected_read;
21155
21556
  if (!protectedRead) throw new McpRuntimeError("PROTECTED_READ_REQUIRED", "Protected read authority is missing.");
21156
- const relationship = protectedRead.relationship;
21557
+ const relationships = protectedRelationshipPaths(protectedRead);
21157
21558
  const values = [];
21158
21559
  const bind = (value) => {
21159
21560
  values.push(value);
21160
21561
  return placeholderStyle === "$" ? `$${values.length}` : "?";
21161
21562
  };
21563
+ const relationshipAliases = /* @__PURE__ */ new Map();
21564
+ for (const [pathIndex, relationship] of relationships.entries()) {
21565
+ relationshipAliases.set(
21566
+ relationship.name,
21567
+ protectedRelationshipAlias(pathIndex, relationship.links.length - 1)
21568
+ );
21569
+ }
21162
21570
  const field = (name, relationshipName) => {
21163
- if (relationshipName !== void 0 && relationshipName !== relationship?.name) {
21571
+ const alias = relationshipName === void 0 ? "t0" : relationshipAliases.get(relationshipName);
21572
+ if (!alias) {
21164
21573
  throw new McpRuntimeError("PROTECTED_RELATIONSHIP_INVALID", "Protected field references an unreviewed relationship.");
21165
21574
  }
21166
- return `${relationshipName ? "t1" : "t0"}.${quoteIdentifier(name, placeholderStyle)}`;
21575
+ return `${alias}.${quoteIdentifier(name, placeholderStyle)}`;
21576
+ };
21577
+ const fromClause = () => {
21578
+ const joins = [];
21579
+ for (const [pathIndex, relationship] of relationships.entries()) {
21580
+ let sourceAlias = "t0";
21581
+ let preserveUnmatched = false;
21582
+ for (const [linkIndex, link] of relationship.links.entries()) {
21583
+ const alias = protectedRelationshipAlias(pathIndex, linkIndex);
21584
+ preserveUnmatched ||= link.unmatched_rows === "keep_null";
21585
+ const conditions = [
21586
+ `${sourceAlias}.${quoteIdentifier(link.local_key, placeholderStyle)} = ${alias}.${quoteIdentifier(link.target_key, placeholderStyle)}`,
21587
+ `${alias}.${quoteIdentifier(link.tenant_key, placeholderStyle)} = ${bind(context.tenant_id)}`,
21588
+ ...link.principal_scope_key ? [`${alias}.${quoteIdentifier(link.principal_scope_key, placeholderStyle)} = ${bind(context.principal)}`] : []
21589
+ ];
21590
+ joins.push(
21591
+ ` ${preserveUnmatched ? "LEFT JOIN" : "JOIN"} ${quoteIdentifier(link.schema, placeholderStyle)}.${quoteIdentifier(link.table, placeholderStyle)} ${alias} ON ${conditions.join(" AND ")}`
21592
+ );
21593
+ sourceAlias = alias;
21594
+ }
21595
+ }
21596
+ return `${quoteIdentifier(capability.target.schema, placeholderStyle)}.${quoteIdentifier(capability.target.table, placeholderStyle)} t0${joins.join("")}`;
21167
21597
  };
21168
- const join = relationship ? ` JOIN ${quoteIdentifier(relationship.schema, placeholderStyle)}.${quoteIdentifier(relationship.table, placeholderStyle)} t1 ON t0.${quoteIdentifier(relationship.local_key, placeholderStyle)} = t1.${quoteIdentifier(relationship.target_key, placeholderStyle)}` : "";
21169
21598
  const scopedWhere = () => {
21170
21599
  const where = [];
21171
21600
  if (capability.target.tenant_key) where.push(`t0.${quoteIdentifier(capability.target.tenant_key, placeholderStyle)} = ${bind(context.tenant_id)}`);
21172
21601
  if (capability.target.principal_scope_key) where.push(`t0.${quoteIdentifier(capability.target.principal_scope_key, placeholderStyle)} = ${bind(context.principal)}`);
21173
- if (relationship) {
21174
- where.push(`t1.${quoteIdentifier(relationship.tenant_key, placeholderStyle)} = ${bind(context.tenant_id)}`);
21175
- if (relationship.principal_scope_key) where.push(`t1.${quoteIdentifier(relationship.principal_scope_key, placeholderStyle)} = ${bind(context.principal)}`);
21176
- }
21177
21602
  for (const predicate of protectedRead.predicates ?? []) {
21178
21603
  const reference = field(predicate.field, predicate.relationship);
21179
21604
  if (predicate.operator === "in") {
@@ -21200,8 +21625,8 @@ function buildProtectedReadQuery(capability, placeholderStyle, args, context) {
21200
21625
  }
21201
21626
  return where;
21202
21627
  };
21203
- const from = `${quoteIdentifier(capability.target.schema, placeholderStyle)}.${quoteIdentifier(capability.target.table, placeholderStyle)} t0${join}`;
21204
21628
  if (protectedRead.mode === "rows") {
21629
+ const from = fromClause();
21205
21630
  const columns = capability.visible_columns.map((column) => `t0.${quoteIdentifier(column, placeholderStyle)} AS ${quoteIdentifier(column, placeholderStyle)}`);
21206
21631
  const where = scopedWhere();
21207
21632
  const order = protectedRead.row_order_by?.length ? ` ORDER BY ${protectedRead.row_order_by.map((item) => `${field(item.field)} ${item.direction.toUpperCase()}`).join(", ")}` : ` ORDER BY t0.${quoteIdentifier(capability.target.primary_key, placeholderStyle)} ASC`;
@@ -21213,6 +21638,7 @@ function buildProtectedReadQuery(capability, placeholderStyle, args, context) {
21213
21638
  const aggregate = protectedRead.aggregate;
21214
21639
  if (!aggregate) throw new McpRuntimeError("PROTECTED_AGGREGATE_REQUIRED", "Protected aggregate authority is missing.");
21215
21640
  const aggregateQuery = (range, period) => {
21641
+ const from = fromClause();
21216
21642
  const where = scopedWhere();
21217
21643
  if (range && aggregate.comparison) {
21218
21644
  const reference = field(aggregate.comparison.field, aggregate.comparison.relationship);
@@ -21273,10 +21699,61 @@ function protectedStatementTimeout(capability, sourceTimeout) {
21273
21699
  return sourceTimeout === void 0 ? protectedTimeout : Math.min(protectedTimeout, sourceTimeout);
21274
21700
  }
21275
21701
  function protectedReadTargets(capability) {
21276
- const targets = [{ schema: capability.target.schema, table: capability.target.table }];
21702
+ const targets = /* @__PURE__ */ new Map();
21703
+ const addTarget = (target) => {
21704
+ const key = `${target.schema}\0${target.table}`;
21705
+ const existing = targets.get(key);
21706
+ targets.set(key, {
21707
+ ...target,
21708
+ principalScoped: target.principalScoped || Boolean(existing?.principalScoped)
21709
+ });
21710
+ };
21711
+ addTarget({
21712
+ schema: capability.target.schema,
21713
+ table: capability.target.table,
21714
+ principalScoped: Boolean(capability.target.principal_scope_key)
21715
+ });
21277
21716
  const relationship = capability.protected_read?.relationship;
21278
- if (relationship) targets.push({ schema: relationship.schema, table: relationship.table });
21279
- return targets;
21717
+ if (relationship) {
21718
+ addTarget({
21719
+ schema: relationship.schema,
21720
+ table: relationship.table,
21721
+ principalScoped: Boolean(relationship.principal_scope_key)
21722
+ });
21723
+ }
21724
+ for (const path3 of capability.protected_read?.relationships ?? []) {
21725
+ for (const link of path3.links) {
21726
+ addTarget({
21727
+ schema: link.schema,
21728
+ table: link.table,
21729
+ principalScoped: Boolean(link.principal_scope_key)
21730
+ });
21731
+ }
21732
+ }
21733
+ return [...targets.values()];
21734
+ }
21735
+ function protectedRelationshipPaths(protectedRead) {
21736
+ if (protectedRead.relationships?.length) return protectedRead.relationships;
21737
+ const relationship = protectedRead.relationship;
21738
+ if (!relationship) return [];
21739
+ return [{
21740
+ name: relationship.name,
21741
+ links: [{
21742
+ schema: relationship.schema,
21743
+ table: relationship.table,
21744
+ primary_key: relationship.primary_key,
21745
+ tenant_key: relationship.tenant_key,
21746
+ ...relationship.principal_scope_key ? { principal_scope_key: relationship.principal_scope_key } : {},
21747
+ local_key: relationship.local_key,
21748
+ target_key: relationship.target_key,
21749
+ cardinality: "many_to_one",
21750
+ max_fan_out: 1,
21751
+ unmatched_rows: "exclude"
21752
+ }]
21753
+ }];
21754
+ }
21755
+ function protectedRelationshipAlias(pathIndex, linkIndex) {
21756
+ return `r${pathIndex + 1}_${linkIndex + 1}`;
21280
21757
  }
21281
21758
  function queryValues(capability, args, context) {
21282
21759
  if (capability.kind === "aggregate_read") return [