@stigmer/server 3.22.1-dev.20260922095755 → 3.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/dist/authorization/authorizer.d.ts +8 -4
  2. package/dist/authorization/authorizer.d.ts.map +1 -1
  3. package/dist/authorization/authorizer.js +9 -5
  4. package/dist/authorization/authorizer.js.map +1 -1
  5. package/dist/authorization/derived-tuples.d.ts.map +1 -1
  6. package/dist/authorization/derived-tuples.js +25 -10
  7. package/dist/authorization/derived-tuples.js.map +1 -1
  8. package/dist/authorization/evaluator.d.ts +8 -7
  9. package/dist/authorization/evaluator.d.ts.map +1 -1
  10. package/dist/authorization/evaluator.js +20 -4
  11. package/dist/authorization/evaluator.js.map +1 -1
  12. package/dist/authorization/list-read-scope.d.ts.map +1 -1
  13. package/dist/authorization/list-read-scope.js +9 -5
  14. package/dist/authorization/list-read-scope.js.map +1 -1
  15. package/dist/authorization/model/agent.d.ts.map +1 -1
  16. package/dist/authorization/model/agent.js +5 -4
  17. package/dist/authorization/model/agent.js.map +1 -1
  18. package/dist/authorization/model/agent_instance.d.ts.map +1 -1
  19. package/dist/authorization/model/agent_instance.js +2 -2
  20. package/dist/authorization/model/agent_instance.js.map +1 -1
  21. package/dist/authorization/model/mcp_server.d.ts.map +1 -1
  22. package/dist/authorization/model/mcp_server.js +2 -2
  23. package/dist/authorization/model/mcp_server.js.map +1 -1
  24. package/dist/authorization/model/plugin.d.ts.map +1 -1
  25. package/dist/authorization/model/plugin.js +4 -3
  26. package/dist/authorization/model/plugin.js.map +1 -1
  27. package/dist/authorization/model/rewrite.d.ts +10 -8
  28. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  29. package/dist/authorization/model/rewrite.js +3 -0
  30. package/dist/authorization/model/rewrite.js.map +1 -1
  31. package/dist/authorization/model/skill.d.ts.map +1 -1
  32. package/dist/authorization/model/skill.js +2 -2
  33. package/dist/authorization/model/skill.js.map +1 -1
  34. package/dist/authorization/model/workflow.d.ts.map +1 -1
  35. package/dist/authorization/model/workflow.js +2 -2
  36. package/dist/authorization/model/workflow.js.map +1 -1
  37. package/dist/authorization/model/workflow_instance.d.ts.map +1 -1
  38. package/dist/authorization/model/workflow_instance.js +2 -2
  39. package/dist/authorization/model/workflow_instance.js.map +1 -1
  40. package/dist/authorization/organization-directory.d.ts.map +1 -1
  41. package/dist/authorization/organization-directory.js +3 -2
  42. package/dist/authorization/organization-directory.js.map +1 -1
  43. package/dist/authorization/store-test-kit.d.ts +6 -0
  44. package/dist/authorization/store-test-kit.d.ts.map +1 -1
  45. package/dist/authorization/store-test-kit.js +62 -16
  46. package/dist/authorization/store-test-kit.js.map +1 -1
  47. package/dist/authorization/tuples.d.ts +32 -13
  48. package/dist/authorization/tuples.d.ts.map +1 -1
  49. package/dist/authorization/tuples.js +11 -3
  50. package/dist/authorization/tuples.js.map +1 -1
  51. package/dist/domain/agent/controller.d.ts.map +1 -1
  52. package/dist/domain/agent/controller.js +4 -4
  53. package/dist/domain/agent/controller.js.map +1 -1
  54. package/dist/domain/agentchannel/controller.d.ts.map +1 -1
  55. package/dist/domain/agentchannel/controller.js +1 -3
  56. package/dist/domain/agentchannel/controller.js.map +1 -1
  57. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  58. package/dist/domain/agentexecution/controller.js +1 -3
  59. package/dist/domain/agentexecution/controller.js.map +1 -1
  60. package/dist/domain/agentinstance/controller.d.ts.map +1 -1
  61. package/dist/domain/agentinstance/controller.js +4 -3
  62. package/dist/domain/agentinstance/controller.js.map +1 -1
  63. package/dist/domain/agentshare/constants.d.ts +11 -6
  64. package/dist/domain/agentshare/constants.d.ts.map +1 -1
  65. package/dist/domain/agentshare/constants.js +16 -8
  66. package/dist/domain/agentshare/constants.js.map +1 -1
  67. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  68. package/dist/domain/agentshare/controller.js +1 -3
  69. package/dist/domain/agentshare/controller.js.map +1 -1
  70. package/dist/domain/agentshare/steps.d.ts +25 -18
  71. package/dist/domain/agentshare/steps.d.ts.map +1 -1
  72. package/dist/domain/agentshare/steps.js +142 -41
  73. package/dist/domain/agentshare/steps.js.map +1 -1
  74. package/dist/domain/environment/controller.d.ts.map +1 -1
  75. package/dist/domain/environment/controller.js +3 -2
  76. package/dist/domain/environment/controller.js.map +1 -1
  77. package/dist/domain/executioncontext/controller.d.ts.map +1 -1
  78. package/dist/domain/executioncontext/controller.js +1 -2
  79. package/dist/domain/executioncontext/controller.js.map +1 -1
  80. package/dist/domain/mcpserver/controller.d.ts.map +1 -1
  81. package/dist/domain/mcpserver/controller.js +4 -3
  82. package/dist/domain/mcpserver/controller.js.map +1 -1
  83. package/dist/domain/plugin/controller.d.ts.map +1 -1
  84. package/dist/domain/plugin/controller.js +3 -1
  85. package/dist/domain/plugin/controller.js.map +1 -1
  86. package/dist/domain/plugin/push.d.ts +5 -7
  87. package/dist/domain/plugin/push.d.ts.map +1 -1
  88. package/dist/domain/plugin/push.js +14 -8
  89. package/dist/domain/plugin/push.js.map +1 -1
  90. package/dist/domain/schedule/controller.d.ts.map +1 -1
  91. package/dist/domain/schedule/controller.js +1 -3
  92. package/dist/domain/schedule/controller.js.map +1 -1
  93. package/dist/domain/session/controller.d.ts.map +1 -1
  94. package/dist/domain/session/controller.js +2 -3
  95. package/dist/domain/session/controller.js.map +1 -1
  96. package/dist/domain/skill/controller.d.ts.map +1 -1
  97. package/dist/domain/skill/controller.js +2 -0
  98. package/dist/domain/skill/controller.js.map +1 -1
  99. package/dist/domain/workflow/controller.d.ts.map +1 -1
  100. package/dist/domain/workflow/controller.js +6 -14
  101. package/dist/domain/workflow/controller.js.map +1 -1
  102. package/dist/domain/workflow/registry/data/task-kind-registry.json +1 -1
  103. package/dist/domain/workflow/validation/task-config-constraints.d.ts +1 -10
  104. package/dist/domain/workflow/validation/task-config-constraints.d.ts.map +1 -1
  105. package/dist/domain/workflow/validation/task-config-constraints.js +2 -3
  106. package/dist/domain/workflow/validation/task-config-constraints.js.map +1 -1
  107. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  108. package/dist/domain/workflowexecution/controller.js +2 -4
  109. package/dist/domain/workflowexecution/controller.js.map +1 -1
  110. package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
  111. package/dist/domain/workflowinstance/controller.js +4 -3
  112. package/dist/domain/workflowinstance/controller.js.map +1 -1
  113. package/dist/extensions/authorization-queries.d.ts +5 -6
  114. package/dist/extensions/authorization-queries.d.ts.map +1 -1
  115. package/dist/extensions/list-read-scope.d.ts +1 -1
  116. package/dist/extensions/resource-authorization.d.ts +2 -3
  117. package/dist/extensions/resource-authorization.d.ts.map +1 -1
  118. package/dist/index.d.ts +3 -1
  119. package/dist/index.d.ts.map +1 -1
  120. package/dist/index.js +13 -0
  121. package/dist/index.js.map +1 -1
  122. package/dist/pipeline/apiresource-meta.d.ts +3 -12
  123. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  124. package/dist/pipeline/apiresource-meta.js +10 -21
  125. package/dist/pipeline/apiresource-meta.js.map +1 -1
  126. package/dist/pipeline/steps/authorization-tuples.d.ts +3 -9
  127. package/dist/pipeline/steps/authorization-tuples.d.ts.map +1 -1
  128. package/dist/pipeline/steps/authorization-tuples.js +11 -9
  129. package/dist/pipeline/steps/authorization-tuples.js.map +1 -1
  130. package/dist/pipeline/steps/references.d.ts +20 -178
  131. package/dist/pipeline/steps/references.d.ts.map +1 -1
  132. package/dist/pipeline/steps/references.js +43 -344
  133. package/dist/pipeline/steps/references.js.map +1 -1
  134. package/dist/pipeline/steps/visibility-gates.d.ts +27 -0
  135. package/dist/pipeline/steps/visibility-gates.d.ts.map +1 -0
  136. package/dist/pipeline/steps/visibility-gates.js +139 -0
  137. package/dist/pipeline/steps/visibility-gates.js.map +1 -0
  138. package/dist/query/search/controller.js +1 -0
  139. package/dist/query/search/controller.js.map +1 -1
  140. package/dist/query/search/criteria.d.ts +5 -1
  141. package/dist/query/search/criteria.d.ts.map +1 -1
  142. package/dist/query/search/criteria.js +13 -3
  143. package/dist/query/search/criteria.js.map +1 -1
  144. package/dist/query/search/handler.d.ts +4 -5
  145. package/dist/query/search/handler.d.ts.map +1 -1
  146. package/dist/query/search/handler.js +6 -7
  147. package/dist/query/search/handler.js.map +1 -1
  148. package/dist/query/search/query-store.d.ts.map +1 -1
  149. package/dist/query/search/query-store.js +2 -0
  150. package/dist/query/search/query-store.js.map +1 -1
  151. package/dist/store/interface.d.ts +6 -3
  152. package/dist/store/interface.d.ts.map +1 -1
  153. package/dist/store/interface.js.map +1 -1
  154. package/dist/store/postgres/migrations.d.ts +1 -3
  155. package/dist/store/postgres/migrations.d.ts.map +1 -1
  156. package/dist/store/postgres/migrations.js +1 -35
  157. package/dist/store/postgres/migrations.js.map +1 -1
  158. package/dist/store/postgres/store.d.ts.map +1 -1
  159. package/dist/store/postgres/store.js +9 -4
  160. package/dist/store/postgres/store.js.map +1 -1
  161. package/dist/store/sqlite/migrations.d.ts +1 -3
  162. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  163. package/dist/store/sqlite/migrations.js +1 -36
  164. package/dist/store/sqlite/migrations.js.map +1 -1
  165. package/dist/store/sqlite/store.d.ts.map +1 -1
  166. package/dist/store/sqlite/store.js +8 -4
  167. package/dist/store/sqlite/store.js.map +1 -1
  168. package/package.json +6 -6
  169. package/src/authorization/README.md +26 -24
  170. package/src/authorization/__tests__/authorizer.test.ts +9 -10
  171. package/src/authorization/__tests__/derived-tuples.test.ts +11 -4
  172. package/src/authorization/__tests__/evaluator.test.ts +73 -14
  173. package/src/authorization/__tests__/facts.test.ts +2 -2
  174. package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +185 -0
  175. package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +98 -6
  176. package/src/authorization/__tests__/fixtures/fga/cross-org-share-creation.fga.yaml +98 -0
  177. package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +61 -4
  178. package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +143 -0
  179. package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +21 -1
  180. package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +25 -1
  181. package/src/authorization/__tests__/fixtures/fga/public-visibility-setter.fga.yaml +52 -0
  182. package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +3 -1
  183. package/src/authorization/__tests__/list-read-scope.test.ts +17 -18
  184. package/src/authorization/__tests__/store-tests.test.ts +8 -1
  185. package/src/authorization/__tests__/tuples.test.ts +11 -12
  186. package/src/authorization/authorizer.ts +9 -4
  187. package/src/authorization/derived-tuples.ts +31 -14
  188. package/src/authorization/evaluator.ts +44 -8
  189. package/src/authorization/list-read-scope.ts +11 -5
  190. package/src/authorization/model/agent.ts +5 -2
  191. package/src/authorization/model/agent_instance.ts +2 -0
  192. package/src/authorization/model/mcp_server.ts +2 -0
  193. package/src/authorization/model/plugin.ts +4 -1
  194. package/src/authorization/model/rewrite.ts +13 -8
  195. package/src/authorization/model/skill.ts +2 -0
  196. package/src/authorization/model/workflow.ts +2 -0
  197. package/src/authorization/model/workflow_instance.ts +2 -0
  198. package/src/authorization/organization-directory.ts +3 -1
  199. package/src/authorization/store-test-kit.ts +75 -16
  200. package/src/authorization/tuples.ts +39 -16
  201. package/src/domain/agent/__tests__/agent.test.ts +4 -11
  202. package/src/domain/agent/controller.ts +7 -7
  203. package/src/domain/agentchannel/__tests__/agentchannel.test.ts +48 -223
  204. package/src/domain/agentchannel/controller.ts +1 -6
  205. package/src/domain/agentexecution/controller.ts +1 -6
  206. package/src/domain/agentinstance/controller.ts +12 -6
  207. package/src/domain/agentshare/__tests__/agentshare.test.ts +172 -196
  208. package/src/domain/agentshare/__tests__/create-authorization.test.ts +31 -16
  209. package/src/domain/agentshare/constants.ts +26 -14
  210. package/src/domain/agentshare/controller.ts +1 -6
  211. package/src/domain/agentshare/steps.ts +194 -41
  212. package/src/domain/environment/controller.ts +8 -5
  213. package/src/domain/executioncontext/controller.ts +1 -5
  214. package/src/domain/mcpserver/__tests__/mcpserver.test.ts +28 -115
  215. package/src/domain/mcpserver/__tests__/oauth-handshake.test.ts +18 -53
  216. package/src/domain/mcpserver/controller.ts +12 -6
  217. package/src/domain/plugin/controller.ts +8 -1
  218. package/src/domain/plugin/push.ts +16 -8
  219. package/src/domain/schedule/controller.ts +1 -6
  220. package/src/domain/session/controller.ts +4 -12
  221. package/src/domain/skill/controller.ts +7 -0
  222. package/src/domain/workflow/controller.ts +10 -28
  223. package/src/domain/workflow/registry/data/task-kind-registry.json +1 -1
  224. package/src/domain/workflow/validation/task-config-constraints.ts +3 -7
  225. package/src/domain/workflowexecution/controller.ts +2 -7
  226. package/src/domain/workflowinstance/controller.ts +12 -6
  227. package/src/extensions/__tests__/built-in-authorization-composed.test.ts +19 -32
  228. package/src/extensions/__tests__/extension-composition.test.ts +4 -4
  229. package/src/extensions/__tests__/list-read-scope-composed.test.ts +6 -9
  230. package/src/extensions/authorization-queries.ts +5 -6
  231. package/src/extensions/list-read-scope.ts +1 -1
  232. package/src/extensions/resource-authorization.ts +5 -3
  233. package/src/index.ts +14 -0
  234. package/src/pipeline/apiresource-meta.ts +10 -21
  235. package/src/pipeline/steps/__tests__/authorization-tuples.test.ts +31 -18
  236. package/src/pipeline/steps/__tests__/visibility-gates.test.ts +187 -0
  237. package/src/pipeline/steps/authorization-tuples.ts +11 -9
  238. package/src/pipeline/steps/references.ts +70 -578
  239. package/src/pipeline/steps/visibility-gates.ts +174 -0
  240. package/src/query/search/__tests__/criteria.test.ts +4 -0
  241. package/src/query/search/__tests__/query-store.test.ts +40 -11
  242. package/src/query/search/controller.ts +1 -0
  243. package/src/query/search/criteria.ts +14 -0
  244. package/src/query/search/handler.ts +7 -6
  245. package/src/query/search/query-store.ts +2 -0
  246. package/src/store/__tests__/store-contract.ts +51 -6
  247. package/src/store/interface.ts +6 -3
  248. package/src/store/postgres/__tests__/migrations.test.ts +6 -187
  249. package/src/store/postgres/__tests__/store-contract.test.ts +2 -0
  250. package/src/store/postgres/migrations.ts +1 -48
  251. package/src/store/postgres/store.ts +11 -4
  252. package/src/store/sqlite/__tests__/migrations.test.ts +5 -191
  253. package/src/store/sqlite/migrations.ts +1 -45
  254. package/src/store/sqlite/store.ts +10 -4
  255. package/dist/domain/workflow/agent-call-references.d.ts +0 -54
  256. package/dist/domain/workflow/agent-call-references.d.ts.map +0 -1
  257. package/dist/domain/workflow/agent-call-references.js +0 -97
  258. package/dist/domain/workflow/agent-call-references.js.map +0 -1
  259. package/dist/store/public-visibility-retired.d.ts +0 -67
  260. package/dist/store/public-visibility-retired.d.ts.map +0 -1
  261. package/dist/store/public-visibility-retired.js +0 -55
  262. package/dist/store/public-visibility-retired.js.map +0 -1
  263. package/src/domain/workflow/__tests__/agent-call-references.test.ts +0 -222
  264. package/src/domain/workflow/agent-call-references.ts +0 -157
  265. package/src/pipeline/__tests__/supports-visibility.test.ts +0 -112
  266. package/src/pipeline/steps/__tests__/references.test.ts +0 -703
  267. package/src/store/__tests__/public-visibility-retired.test.ts +0 -127
  268. package/src/store/public-visibility-retired.ts +0 -119
@@ -78,6 +78,10 @@ import {
78
78
  } from "../../pipeline/steps/authorize-resolved-target.js";
79
79
  import { versionHistoryTarget } from "../../pipeline/steps/version-history.js";
80
80
  import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
81
+ import {
82
+ newAuthorizeVisibilityTransitionStep,
83
+ newGuardPublicVisibilityStep,
84
+ } from "../../pipeline/steps/visibility-gates.js";
81
85
  import { newBuildUpdateStateStep } from "../../pipeline/steps/build-update-state.js";
82
86
  import {
83
87
  setAuditFieldsForUpdate,
@@ -108,16 +112,7 @@ import {
108
112
  TARGET_RESOURCE_KEY,
109
113
  newLoadTargetStep,
110
114
  } from "../../pipeline/steps/load-target.js";
111
- import {
112
- collectSpecReferences,
113
- newGuardReferenceFloorOnEscalationStep,
114
- newNormalizeReferencesStep,
115
- newValidateReferencesStep,
116
- } from "../../pipeline/steps/references.js";
117
- import {
118
- collectAgentCallReferences,
119
- newValidateAgentCallReferencesStep,
120
- } from "./agent-call-references.js";
115
+ import { newNormalizeReferencesStep } from "../../pipeline/steps/references.js";
121
116
  import {
122
117
  newCleanupIamPoliciesStep,
123
118
  newCreateAuthorizationTuplesStep,
@@ -243,10 +238,9 @@ async function createWorkflow(
243
238
  .addStep(newResolveSlugStep())
244
239
  .addStep(newCheckDuplicateStep(deps.store))
245
240
  .addStep(newBuildNewStateStep())
241
+ .addStep(newGuardPublicVisibilityStep(deps.authorizer))
246
242
  .addStep(newGuardReservedLabelsStep(deps.authorizer))
247
243
  .addStep(newNormalizeReferencesStep())
248
- .addStep(newValidateReferencesStep(deps.store))
249
- .addStep(newValidateAgentCallReferencesStep(deps.store))
250
244
  .addStep(newPopulateServerlessValidationStep(deps.logger))
251
245
  .addStep(newComputeVersionHashStep(deps.logger))
252
246
  .addStep(newPopulateVersionHashStep(true))
@@ -311,8 +305,6 @@ async function update(
311
305
  .addStep(newBuildUpdateStateStep())
312
306
  .addStep(newGuardReservedLabelsStep(deps.authorizer))
313
307
  .addStep(newNormalizeReferencesStep())
314
- .addStep(newValidateReferencesStep(deps.store))
315
- .addStep(newValidateAgentCallReferencesStep(deps.store))
316
308
  .addStep(newPopulateServerlessValidationStepForUpdate(deps.logger))
317
309
  .addStep(newComputeVersionHashStep(deps.logger))
318
310
  .addStep(newCheckVersionChangedStep(deps.logger))
@@ -419,8 +411,8 @@ async function deleteWorkflow(
419
411
  // updateVisibility — update_visibility.go: a targeted metadata update (only
420
412
  // metadata.visibility changes; spec/status untouched). The level check runs
421
413
  // AFTER load, preserving the cross-edition error precedence: unknown id +
422
- // bad level = NOT_FOUND on both editions. The workflow conformance suite's
423
- // visibility block is the wire pin.
414
+ // bad level = NOT_FOUND on both editions. CW-9's suite block is the wire
415
+ // pin (clientPublicVisibilityWrites, true on local targets).
424
416
  // ---------------------------------------------------------------------------
425
417
 
426
418
  const UPDATE_VISIBILITY_WORKFLOW_KEY = "updateVisibilityWorkflow";
@@ -460,20 +452,10 @@ async function updateVisibility(
460
452
  newRecordVisibilityBeforeUpdateStep(UPDATE_VISIBILITY_WORKFLOW_KEY),
461
453
  )
462
454
  .addStep(newValidateVisibilityUpdateStep())
463
- // The reference floor's second door: a workflow may not be raised above
464
- // the agents its agent_call tasks run.
465
455
  .addStep(
466
- newGuardReferenceFloorOnEscalationStep(
467
- deps.store,
456
+ newAuthorizeVisibilityTransitionStep(
468
457
  UPDATE_VISIBILITY_WORKFLOW_KEY,
469
- [
470
- (row) => collectSpecReferences(WorkflowSchema, row),
471
- (row) =>
472
- collectAgentCallReferences(
473
- (row as Workflow).spec,
474
- (row as Workflow).metadata?.org ?? "",
475
- ),
476
- ],
458
+ deps.authorizer,
477
459
  ),
478
460
  )
479
461
  .addStep(newSetWorkflowVisibilityStep())
@@ -1525,7 +1525,7 @@
1525
1525
  "type": "string"
1526
1526
  },
1527
1527
  "org": {
1528
- "description": "Organization that owns the referenced resource. When non-empty: must be a valid org slug (lowercase alphanumeric with hyphens, starts with a letter, 1-63 characters). Example: \"stigmer\", \"acme-corp\". When empty: the reference is relative — the server resolves it to the parent resource's organization at write time. All stored and returned references always have org populated (absolute form). Use empty org for same-org references (the common case). An explicit other org is accepted only when that organization is a platform that shares the resource with yours (visibility_platform).",
1528
+ "description": "Organization that owns the referenced resource. When non-empty: must be a valid org slug (lowercase alphanumeric with hyphens, starts with a letter, 1-63 characters). Example: \"stigmer\", \"acme-corp\". When empty: the reference is relative — the server resolves it to the parent resource's organization at write time. All stored and returned references always have org populated (absolute form). Use empty org for same-org references (the common case). Use explicit org for cross-org references (e.g., marketplace resources).",
1529
1529
  "maxLength": 63,
1530
1530
  "pattern": "^$|^[a-z][a-z0-9-]*[a-z0-9]$",
1531
1531
  "type": "string"
@@ -105,10 +105,7 @@ function taskConstraintViolations(task: WorkflowTask): string[] {
105
105
  * task_config is a Struct again — that is what the walker's recursion is
106
106
  * for).
107
107
  */
108
- function configConstraintViolations(
109
- task: WorkflowTask,
110
- cfg: Message,
111
- ): string[] {
108
+ function configConstraintViolations(task: WorkflowTask, cfg: Message): string[] {
112
109
  const schema = taskConfigSchemaFor(task.kind);
113
110
  if (schema === undefined) {
114
111
  // Unknown kinds were already rejected by validateTaskKinds; an
@@ -132,10 +129,9 @@ function configConstraintViolations(
132
129
  * The WorkflowTask lists embedded in a control-flow task config, in
133
130
  * declaration order — the same recursion set the converter walks
134
131
  * (convertForTask/convertForkTask/convertTryTask) and the cloud Java
135
- * TaskConfigStrictParser mirrors. Exported for the write-time reference
136
- * collector (../agent-call-references.ts), which walks the same tree.
132
+ * TaskConfigStrictParser mirrors.
137
133
  */
138
- export function nestedTasks(cfg: Message): WorkflowTask[] {
134
+ function nestedTasks(cfg: Message): WorkflowTask[] {
139
135
  switch (cfg.$typeName) {
140
136
  case ForTaskConfigSchema.typeName:
141
137
  return (cfg as ForTaskConfig).do;
@@ -72,10 +72,7 @@ import {
72
72
  TARGET_RESOURCE_KEY,
73
73
  newLoadTargetStep,
74
74
  } from "../../pipeline/steps/load-target.js";
75
- import {
76
- newNormalizeReferencesStep,
77
- newValidateReferencesStep,
78
- } from "../../pipeline/steps/references.js";
75
+ import { newNormalizeReferencesStep } from "../../pipeline/steps/references.js";
79
76
  import {
80
77
  newCleanupIamPoliciesStep,
81
78
  newCreateAuthorizationTuplesStep,
@@ -302,8 +299,7 @@ async function createExecution(
302
299
  .addStep(newCheckDuplicateStep(deps.store))
303
300
  .addStep(newBuildNewStateStep())
304
301
  .addStep(newGuardReservedLabelsStep(deps.authorizer))
305
- .addStep(newNormalizeReferencesStep())
306
- .addStep(newValidateReferencesStep(deps.store));
302
+ .addStep(newNormalizeReferencesStep());
307
303
  // The ratified sandbox-acquisition gate slot (blueprint 03 §3a; C4):
308
304
  // the Java-verified capacity-gate position — after Authorize and every
309
305
  // resolution step (the default instance, like Java's, side-effects
@@ -393,7 +389,6 @@ async function update(
393
389
  .addStep(newBuildUpdateStateStep())
394
390
  .addStep(newGuardReservedLabelsStep(deps.authorizer))
395
391
  .addStep(newNormalizeReferencesStep())
396
- .addStep(newValidateReferencesStep(deps.store))
397
392
  .addStep(newPersistStep(deps.store))
398
393
  .addStep(
399
394
  newIndexSearchStep(
@@ -58,6 +58,10 @@ import {
58
58
  newAuthorizeResolvedTargetStep,
59
59
  } from "../../pipeline/steps/authorize-resolved-target.js";
60
60
  import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
61
+ import {
62
+ newAuthorizeVisibilityTransitionStep,
63
+ newGuardPublicVisibilityStep,
64
+ } from "../../pipeline/steps/visibility-gates.js";
61
65
  import {
62
66
  newBuildNewStateStep,
63
67
  setAuditFieldsForUpdate,
@@ -87,10 +91,7 @@ import {
87
91
  TARGET_RESOURCE_KEY,
88
92
  newLoadTargetStep,
89
93
  } from "../../pipeline/steps/load-target.js";
90
- import {
91
- newNormalizeReferencesStep,
92
- newValidateReferencesStep,
93
- } from "../../pipeline/steps/references.js";
94
+ import { newNormalizeReferencesStep } from "../../pipeline/steps/references.js";
94
95
  import {
95
96
  newCleanupIamPoliciesStep,
96
97
  newCreateAuthorizationTuplesStep,
@@ -197,9 +198,9 @@ async function createInstance(
197
198
  .addStep(newValidateSameOrgBusinessRuleStep(deps.logger))
198
199
  .addStep(newCheckDuplicateStep(deps.store))
199
200
  .addStep(newBuildNewStateStep())
201
+ .addStep(newGuardPublicVisibilityStep(deps.authorizer))
200
202
  .addStep(newGuardReservedLabelsStep(deps.authorizer))
201
203
  .addStep(newNormalizeReferencesStep())
202
- .addStep(newValidateReferencesStep(deps.store))
203
204
  .addStep(newPersistStep(deps.store))
204
205
  .addStep(
205
206
  newCreateAuthorizationTuplesStep(
@@ -248,7 +249,6 @@ async function update(
248
249
  .addStep(newBuildUpdateStateStep())
249
250
  .addStep(newGuardReservedLabelsStep(deps.authorizer))
250
251
  .addStep(newNormalizeReferencesStep())
251
- .addStep(newValidateReferencesStep(deps.store))
252
252
  .addStep(newPersistStep(deps.store))
253
253
  .addStep(
254
254
  newIndexSearchStep(
@@ -407,6 +407,12 @@ async function updateVisibility(
407
407
  ),
408
408
  )
409
409
  .addStep(newValidateVisibilityUpdateStep())
410
+ .addStep(
411
+ newAuthorizeVisibilityTransitionStep(
412
+ UPDATE_VISIBILITY_INSTANCE_KEY,
413
+ deps.authorizer,
414
+ ),
415
+ )
410
416
  .addStep(newSetInstanceVisibilityStep())
411
417
  .addStep(
412
418
  newPersistInstanceStep(
@@ -16,9 +16,8 @@
16
16
  * outsider on an EXISTING resource hears PERMISSION_DENIED, the
17
17
  * cloud's answer; `findMyOrganizations` lists what a person may view
18
18
  * and `find` refuses enumeration; `checkMyPermission` tells the truth;
19
- * the retired public level is refused for the founder too, as an
20
- * invalid level and not a permission question; an unprovisioned
21
- * subject writes nothing until provisioned. The LIST lanes are a member's
19
+ * nobody sets public visibility; an unprovisioned subject
20
+ * writes nothing until provisioned. The LIST lanes are a member's
22
21
  * lists: an outsider lists nothing of an organization they hold no
23
22
  * row in; environments and API keys are their owner's even to the
24
23
  * organization's owner; the library (a search) omits a private agent
@@ -27,9 +26,8 @@
27
26
  * provisioned.
28
27
  * - trusted-local: the permissive default stays (one caller,
29
28
  * nothing to separate; the laptop's rows are stamped `"system"`):
30
- * tokenless reads and organization enumeration keep working, and
31
- * the retired public level is refused here exactly as it is under
32
- * every other posture (the level is gone, not gated).
29
+ * tokenless reads, public visibility and organization enumeration all
30
+ * keep working.
33
31
  * - a unit's own Authorizer: nothing built in is installed; the unit's
34
32
  * answer stands even against the founder.
35
33
  *
@@ -82,6 +80,7 @@ import { loadConfig } from "../../boot/config.js";
82
80
  import { composeServer } from "../../boot/compose.js";
83
81
  import type { ComposedServer } from "../../boot/compose.js";
84
82
  import { metadataOf } from "../../pipeline/steps/shapes.js";
83
+ import { PUBLIC_VISIBILITY_DENY_MESSAGE } from "../../pipeline/steps/visibility-gates.js";
85
84
  import type { Store } from "../../store/interface.js";
86
85
  import type { Authorizer } from "../authorizer.js";
87
86
  import type { ListReadScope } from "../list-read-scope.js";
@@ -133,15 +132,6 @@ function organizationInput(slug: string) {
133
132
  };
134
133
  }
135
134
 
136
- /**
137
- * The one sentence for a level a kind does not support, as the agent chain
138
- * answers it (pipeline/steps/validate-visibility.ts builds it from the
139
- * kind's config); pinned as bytes because clients render it.
140
- */
141
- const PUBLIC_LEVEL_REFUSED_FOR_AGENT =
142
- "agent resources cannot be set to visibility_public. " +
143
- "Supported visibility levels: visibility_private, visibility_org, visibility_platform.";
144
-
145
135
  function agentInput(
146
136
  name: string,
147
137
  visibility: ApiResourceVisibility,
@@ -387,14 +377,14 @@ describe("built-in authorizer (composed server, OIDC with no unit Authorizer: th
387
377
  );
388
378
  });
389
379
 
390
- it("the retired public level is refused for the founder as an invalid level, before any permission question", async () => {
380
+ it("nobody sets PUBLIC visibility on an enforcing self-host — the platform gate denies the founder with the policy copy", async () => {
391
381
  const failure = await failureOf(
392
382
  createClient(AgentCommandController, asFounder()).create(
393
383
  agentInput("Public Agent", ApiResourceVisibility.visibility_public),
394
384
  ),
395
385
  );
396
- expect(failure.code).toBe(Code.InvalidArgument);
397
- expect(failure.rawMessage).toBe(PUBLIC_LEVEL_REFUSED_FOR_AGENT);
386
+ expect(failure.code).toBe(Code.PermissionDenied);
387
+ expect(failure.rawMessage).toBe(PUBLIC_VISIBILITY_DENY_MESSAGE);
398
388
  });
399
389
 
400
390
  describe("the list lanes (the built-in ListReadScope)", () => {
@@ -605,18 +595,20 @@ describe("built-in authorizer (composed server, trusted-local: the permissive de
605
595
  expect(read.metadata?.id).toBe(created.metadata!.id);
606
596
  });
607
597
 
608
- it("the laptop keeps organization enumeration and refuses the retired public level like every posture — the level is gone, not gated", async () => {
598
+ it("the laptop keeps public visibility and organization enumeration — no directory, no platform gate that bites", async () => {
609
599
  const anonymous = transportFor(port);
610
- const failure = await failureOf(
611
- createClient(AgentCommandController, anonymous).create(
612
- agentInput(
613
- "Public Laptop Agent",
614
- ApiResourceVisibility.visibility_public,
615
- ),
600
+ const publicAgent = await createClient(
601
+ AgentCommandController,
602
+ anonymous,
603
+ ).create(
604
+ agentInput(
605
+ "Public Laptop Agent",
606
+ ApiResourceVisibility.visibility_public,
616
607
  ),
617
608
  );
618
- expect(failure.code).toBe(Code.InvalidArgument);
619
- expect(failure.rawMessage).toBe(PUBLIC_LEVEL_REFUSED_FOR_AGENT);
609
+ expect(publicAgent.metadata?.visibility).toBe(
610
+ ApiResourceVisibility.visibility_public,
611
+ );
620
612
  const all = await createClient(OrganizationQueryController, anonymous).find(
621
613
  { org: ORG },
622
614
  );
@@ -625,11 +617,6 @@ describe("built-in authorizer (composed server, trusted-local: the permissive de
625
617
 
626
618
  it("the laptop's lists are the full scan — no scope composed, the rows stamped `system` included", async () => {
627
619
  const anonymous = transportFor(port);
628
- // A second agent beside the private one the earlier test created, so
629
- // the search below proves a scan and not a single-row coincidence.
630
- await createClient(AgentCommandController, anonymous).create(
631
- agentInput("Second Laptop Agent", ApiResourceVisibility.visibility_org),
632
- );
633
620
  const environments = createClient(EnvironmentCommandController, anonymous);
634
621
  await environments.create(environmentInput("laptop env one"));
635
622
  await environments.create(environmentInput("laptop env two"));
@@ -1074,7 +1074,7 @@ describe("extension composition (C2 tuple lifecycle + organization directory)",
1074
1074
  });
1075
1075
  });
1076
1076
 
1077
- it("updateVisibility fires the set-diff transition (org→private deletes the org shape)", async () => {
1077
+ it("updateVisibility fires the set-diff transition (org→public keeps the floor)", async () => {
1078
1078
  const command = createClient(AgentCommandController, portTransport);
1079
1079
  const agent = await command.create(
1080
1080
  create(AgentSchema, {
@@ -1091,12 +1091,12 @@ describe("extension composition (C2 tuple lifecycle + organization directory)",
1091
1091
  visibilityEvents.length = 0;
1092
1092
  await command.updateVisibility({
1093
1093
  resourceId: agent.metadata?.id ?? "",
1094
- visibility: ApiResourceVisibility.visibility_private,
1094
+ visibility: ApiResourceVisibility.visibility_public,
1095
1095
  });
1096
1096
  expect(visibilityEvents).toHaveLength(1);
1097
1097
  const event = visibilityEvents[0]!;
1098
- expect(event.shapesToCreate).toEqual([]);
1099
- expect(event.shapesToDelete).toEqual(["org-viewer"]);
1098
+ expect(event.shapesToCreate).toEqual(["public-viewer"]);
1099
+ expect(event.shapesToDelete).toEqual([]);
1100
1100
  });
1101
1101
 
1102
1102
  it("delete fires the cleanup event; a cleanup failure never fails the delete", async () => {
@@ -10,7 +10,7 @@
10
10
  * narrows, blank spans orgs — lane 6),
11
11
  * - activity.listRecentActivity (enumeration verb, two kinds — lane 22),
12
12
  * - search (enumeration verb feeding the engine allowlist, and the
13
- * proof that no request shape bypasses it — lane 21),
13
+ * crossOrgPublic bypass where FGA is never consulted — lane 21),
14
14
  * - the OUTAGE arm: a throwing scope answers the sanitized INTERNAL,
15
15
  * never an empty (or full!) list;
16
16
  * - the SERVER'S OWN reads over `inProcessTransport` (stigmer#1207):
@@ -277,23 +277,20 @@ describe("list read scope (composed server, fake scope)", () => {
277
277
  ]);
278
278
  });
279
279
 
280
- it("search narrows through the engine allowlist on every request shape — nothing bypasses the scope (lane 21)", async () => {
280
+ it("search narrows through the engine allowlist; crossOrgPublic bypasses the scope (lane 21)", async () => {
281
281
  allowed = new Set(["agt_mine"]);
282
282
  const search = createClient(SearchService, transport);
283
283
  const scoped = await search.search({ query: "scopedagent" });
284
284
  expect(scoped.entries.map((e) => e.id)).toEqual(["agt_mine"]);
285
285
 
286
- // The org-filtered shape asks the scope too: the one request shape
287
- // that once skipped it (the retired public level's cross-organization
288
- // discovery) no longer exists on the wire.
286
+ // The Java bypass: public-widened discovery never consults FGA.
289
287
  seenKinds.length = 0;
290
- const orgScoped = await search.search({
291
- kinds: [ApiResourceKind.agent],
288
+ await search.search({
292
289
  query: "scopedagent",
293
290
  org: "acme",
291
+ crossOrgPublic: true,
294
292
  });
295
- expect(orgScoped.entries.map((e) => e.id)).toEqual(["agt_mine"]);
296
- expect(seenKinds).toEqual([ApiResourceKind.agent]);
293
+ expect(seenKinds).toEqual([]);
297
294
  });
298
295
 
299
296
  it("a scope outage answers the sanitized INTERNAL — never an unscoped or empty success", async () => {
@@ -32,10 +32,9 @@
32
32
  * Semantics the controller and the conformance suite rely on (the Java
33
33
  * check-handler posture the cloud's client already implements):
34
34
  *
35
- * - `check` resolves usersets (a grant to `organization:acme#member`
36
- * answers true for a member), so "may I read this" is the model's
37
- * answer and not a lookup of who was named.
38
- * - the two listings never expand usersets, and `listPrincipalIds`
35
+ * - `check` answers a public grant (a wildcard principal resolves), so
36
+ * "may I read this public agent" is true for anyone.
37
+ * - the two listings never expand wildcards, and `listPrincipalIds`
39
38
  * answers DIRECT principals only — a listing is an inventory of who
40
39
  * was named, never a resolution of who could reach.
41
40
  * - contextual policies are evaluated as if held, and nothing is written.
@@ -75,7 +74,7 @@ export interface AuthorizationQueryEngine {
75
74
  ): Promise<boolean>;
76
75
  /**
77
76
  * The ids of `resourceKind` resources on which `principal` holds
78
- * `relation` (ListAuthorizedResourceIdsInput's field order). Usersets
77
+ * `relation` (ListAuthorizedResourceIdsInput's field order). Wildcards
79
78
  * are not expanded.
80
79
  */
81
80
  listResourceIds(
@@ -87,7 +86,7 @@ export interface AuthorizationQueryEngine {
87
86
  /**
88
87
  * The ids of `principalKind` principals holding `relation` on `resource`
89
88
  * (ListAuthorizedPrincipalIdsInput's field order). Direct principals
90
- * only; usersets are not expanded.
89
+ * only; wildcards are not expanded.
91
90
  */
92
91
  listPrincipalIds(
93
92
  resource: ApiResourceRef,
@@ -8,7 +8,7 @@
8
8
  * is yours). On a multi-tenant edition the same scans serve OTHER
9
9
  * tenants' rows, so the cloud baseline (the Java list handlers) narrows
10
10
  * every one of them to the caller's FGA-authorized id set (ListObjects
11
- * on can_view), with a second guest
11
+ * on can_view, public wildcards suppressed), with a second guest
12
12
  * cookie-label rule on the session and agent-execution lanes.
13
13
  *
14
14
  * This port is the ONE seam for that fork (DD-007 addendum 3: every
@@ -65,12 +65,14 @@ import type { CallerIdentity } from "./identity.js";
65
65
  * The visibility tuple shapes of the shared FGA model, named
66
66
  * edition-neutrally. The driver maps each to its tuple:
67
67
  * - org-viewer: <kind>:<id>#viewer@organization:<org>#viewer
68
+ * - public-viewer: <kind>:<id>#viewer@identity_account:* (conditional)
68
69
  * - platform-viewer: <kind>:<id>#platform_viewer@identity_provider:<idp>#platform_user
69
70
  * (fans out per IdP the org owns — the driver's lookup, not OSS's)
70
- * Every shape names a bounded set of readers; the model has no shape that
71
- * reaches every account.
72
71
  */
73
- export type VisibilityTupleShape = "org-viewer" | "platform-viewer";
72
+ export type VisibilityTupleShape =
73
+ | "org-viewer"
74
+ | "public-viewer"
75
+ | "platform-viewer";
74
76
 
75
77
  /**
76
78
  * One resolved structural link from the created resource to a parent
package/src/index.ts CHANGED
@@ -185,6 +185,7 @@ export {
185
185
  newModel,
186
186
  } from "./authorization/model/index.js";
187
187
  export type {
188
+ CheckContext,
188
189
  ObjectRef,
189
190
  Person,
190
191
  Subject,
@@ -308,6 +309,19 @@ export { newAuthorizeStep } from "./pipeline/steps/authorize.js";
308
309
  // composition never re-derives the set.
309
310
  export { isRunGateCheck } from "./pipeline/steps/authorize-run-target.js";
310
311
  export { newValidateProtoStep } from "./pipeline/steps/validation.js";
312
+ // The create-shaping steps, for an envelope kind a composition serves
313
+ // itself (a cloud_only kind has no OSS controller to run them): between
314
+ // ValidateProto and its own domain step the composition runs the SAME
315
+ // ResolveSlug and BuildNewState every OSS create runs, so how a slug is
316
+ // derived from a name, how an id is spelled, how status.audit is stamped
317
+ // for the caller and how visibility defaults live exactly once and move
318
+ // together. The RequestContext must carry the kind (its fourth
319
+ // constructor argument) for BuildNewState to pick the id prefix.
320
+ // CheckDuplicate is deliberately not here: it reads this server's Store,
321
+ // and a composition's own table answers that question in a step of the
322
+ // same name over its own repo.
323
+ export { newBuildNewStateStep } from "./pipeline/steps/defaults.js";
324
+ export { newResolveSlugStep } from "./pipeline/steps/slug.js";
311
325
 
312
326
  // The driver interfaces and the store-fault classes the ratified mapping
313
327
  // keys on (typed not-found → NotFound; anything else rethrows as an
@@ -266,18 +266,9 @@ function tierRank(tier: ResourceTier): number {
266
266
  }
267
267
 
268
268
  /**
269
- * Whether a kind may hold a visibility level — the one predicate behind the
270
- * ValidateVisibility doors on every create and updateVisibility chain.
271
- *
272
- * PRIVATE and UNSPECIFIED are always supported (no visibility grant is
273
- * written for them); ORG and PLATFORM require the matching supports_* flag
274
- * on the kind's VisibilityConfig, so kinds with no config are private-only.
275
- * PUBLIC is refused for every kind: the level is retired (it made a row
276
- * readable to every account on the server), no config may declare it, and
277
- * the answer is fixed here rather than read from a flag that no longer
278
- * exists. Every level is named so a level added to the enum must be taught
279
- * here before any row can hold it; the closing arm is unreachable by wire
280
- * input, because both doors validate the field `defined_only` first.
269
+ * Go SupportsVisibility: PRIVATE and UNSPECIFIED are always supported (no
270
+ * visibility grant); every other level requires the matching supports_*
271
+ * flag. Kinds with no VisibilityConfig are private-only.
281
272
  */
282
273
  export function supportsVisibility(
283
274
  kind: ApiResourceKind,
@@ -285,19 +276,14 @@ export function supportsVisibility(
285
276
  ): boolean {
286
277
  const config = getKindMeta(kind).authorization?.visibility;
287
278
  switch (visibility) {
288
- case ApiResourceVisibility.api_resource_visibility_unspecified:
289
- case ApiResourceVisibility.visibility_private:
290
- return true;
279
+ case ApiResourceVisibility.visibility_public:
280
+ return config?.supportsPublic === true;
291
281
  case ApiResourceVisibility.visibility_org:
292
282
  return config?.supportsOrg === true;
293
283
  case ApiResourceVisibility.visibility_platform:
294
284
  return config?.supportsPlatform === true;
295
- case ApiResourceVisibility.visibility_public:
296
- return false;
297
- default: {
298
- const exhaustive: never = visibility;
299
- throw new Error(`unknown visibility level: ${String(exhaustive)}`);
300
- }
285
+ default:
286
+ return true;
301
287
  }
302
288
  }
303
289
 
@@ -312,6 +298,9 @@ export function supportedVisibilityLevels(kind: ApiResourceKind): string {
312
298
  if (config?.supportsOrg === true) {
313
299
  levels += ", visibility_org";
314
300
  }
301
+ if (config?.supportsPublic === true) {
302
+ levels += ", visibility_public";
303
+ }
315
304
  if (config?.supportsPlatform === true) {
316
305
  levels += ", visibility_platform";
317
306
  }
@@ -41,10 +41,15 @@ const caller: CallerIdentity = {
41
41
  const V = ApiResourceVisibility;
42
42
 
43
43
  describe("visibilityShapesFor (the reconciler's level→shape policy)", () => {
44
- it("agent (blueprint with org floor): org / platform expansions", () => {
44
+ it("agent (blueprint with org floor): org / public / platform expansions", () => {
45
45
  expect([
46
46
  ...visibilityShapesFor(ApiResourceKind.agent, V.visibility_org),
47
47
  ]).toEqual(["org-viewer"]);
48
+ expect(
49
+ [
50
+ ...visibilityShapesFor(ApiResourceKind.agent, V.visibility_public),
51
+ ].sort(),
52
+ ).toEqual(["org-viewer", "public-viewer"]);
48
53
  expect(
49
54
  [
50
55
  ...visibilityShapesFor(ApiResourceKind.agent, V.visibility_platform),
@@ -55,27 +60,25 @@ describe("visibilityShapesFor (the reconciler's level→shape policy)", () => {
55
60
  ]).toEqual([]);
56
61
  });
57
62
 
58
- it("the retired public level expands to nothing for every kind — no wildcard shape exists", () => {
59
- for (const kind of [
60
- ApiResourceKind.agent,
61
- ApiResourceKind.workflow_instance,
62
- ApiResourceKind.plugin,
63
- ApiResourceKind.session,
64
- ]) {
65
- expect([...visibilityShapesFor(kind, V.visibility_public)]).toEqual([]);
66
- }
63
+ it("workflow_instance (no org floor): public expands to the wildcard shape alone", () => {
64
+ expect([
65
+ ...visibilityShapesFor(
66
+ ApiResourceKind.workflow_instance,
67
+ V.visibility_public,
68
+ ),
69
+ ]).toEqual(["public-viewer"]);
67
70
  });
68
71
 
69
72
  it("session (no visibility config): every level yields nothing, silently", () => {
70
73
  expect([
71
- ...visibilityShapesFor(ApiResourceKind.session, V.visibility_org),
74
+ ...visibilityShapesFor(ApiResourceKind.session, V.visibility_public),
72
75
  ]).toEqual([]);
73
76
  expect([
74
- ...visibilityShapesFor(ApiResourceKind.session, V.visibility_platform),
77
+ ...visibilityShapesFor(ApiResourceKind.session, V.visibility_org),
75
78
  ]).toEqual([]);
76
79
  });
77
80
 
78
- it("agent_instance supports org but never platform (tenant isolation)", () => {
81
+ it("agent_instance supports org+public but never platform (tenant isolation)", () => {
79
82
  expect([
80
83
  ...visibilityShapesFor(
81
84
  ApiResourceKind.agent_instance,
@@ -98,15 +101,15 @@ describe("diffVisibilityShapes (the reconciler's transition matrix, re-pinned)",
98
101
  expect(diff.shapesToDelete).toEqual([]);
99
102
  });
100
103
 
101
- it("unspecified→platform creates the platform shape + the org floor", () => {
104
+ it("unspecified→public creates public + the org floor", () => {
102
105
  const diff = diffVisibilityShapes(
103
106
  agent,
104
107
  V.api_resource_visibility_unspecified,
105
- V.visibility_platform,
108
+ V.visibility_public,
106
109
  );
107
110
  expect([...diff.shapesToCreate].sort()).toEqual([
108
111
  "org-viewer",
109
- "platform-viewer",
112
+ "public-viewer",
110
113
  ]);
111
114
  expect(diff.shapesToDelete).toEqual([]);
112
115
  });
@@ -121,13 +124,23 @@ describe("diffVisibilityShapes (the reconciler's transition matrix, re-pinned)",
121
124
  expect(diff.shapesToDelete).toEqual(["org-viewer"]);
122
125
  });
123
126
 
124
- it("platform→org deletes ONLY the platform shape — the shared org floor stays untouched", () => {
127
+ it("public→org deletes ONLY the wildcard — the shared org shape stays untouched", () => {
125
128
  const diff = diffVisibilityShapes(
126
129
  agent,
127
- V.visibility_platform,
130
+ V.visibility_public,
128
131
  V.visibility_org,
129
132
  );
130
133
  expect(diff.shapesToCreate).toEqual([]);
134
+ expect(diff.shapesToDelete).toEqual(["public-viewer"]);
135
+ });
136
+
137
+ it("platform→public swaps the family shape and never touches the org floor", () => {
138
+ const diff = diffVisibilityShapes(
139
+ agent,
140
+ V.visibility_platform,
141
+ V.visibility_public,
142
+ );
143
+ expect(diff.shapesToCreate).toEqual(["public-viewer"]);
131
144
  expect(diff.shapesToDelete).toEqual(["platform-viewer"]);
132
145
  });
133
146