@stigmer/server 3.23.0 → 3.23.1-dev.20260923075205

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 (267) hide show
  1. package/dist/authorization/authorizer.d.ts +4 -8
  2. package/dist/authorization/authorizer.d.ts.map +1 -1
  3. package/dist/authorization/authorizer.js +5 -9
  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 +10 -25
  7. package/dist/authorization/derived-tuples.js.map +1 -1
  8. package/dist/authorization/evaluator.d.ts +7 -8
  9. package/dist/authorization/evaluator.d.ts.map +1 -1
  10. package/dist/authorization/evaluator.js +4 -20
  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 +5 -9
  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 +4 -5
  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 +3 -4
  26. package/dist/authorization/model/plugin.js.map +1 -1
  27. package/dist/authorization/model/rewrite.d.ts +8 -10
  28. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  29. package/dist/authorization/model/rewrite.js +0 -3
  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 +2 -3
  42. package/dist/authorization/organization-directory.js.map +1 -1
  43. package/dist/authorization/store-test-kit.d.ts +0 -6
  44. package/dist/authorization/store-test-kit.d.ts.map +1 -1
  45. package/dist/authorization/store-test-kit.js +16 -62
  46. package/dist/authorization/store-test-kit.js.map +1 -1
  47. package/dist/authorization/tuples.d.ts +13 -32
  48. package/dist/authorization/tuples.d.ts.map +1 -1
  49. package/dist/authorization/tuples.js +3 -11
  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 +3 -1
  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 +3 -1
  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 +3 -4
  62. package/dist/domain/agentinstance/controller.js.map +1 -1
  63. package/dist/domain/agentshare/constants.d.ts +6 -11
  64. package/dist/domain/agentshare/constants.d.ts.map +1 -1
  65. package/dist/domain/agentshare/constants.js +8 -16
  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 +3 -1
  69. package/dist/domain/agentshare/controller.js.map +1 -1
  70. package/dist/domain/agentshare/steps.d.ts +18 -25
  71. package/dist/domain/agentshare/steps.d.ts.map +1 -1
  72. package/dist/domain/agentshare/steps.js +41 -142
  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 +2 -3
  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 +2 -1
  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 +3 -4
  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 +1 -3
  85. package/dist/domain/plugin/controller.js.map +1 -1
  86. package/dist/domain/plugin/push.d.ts +7 -5
  87. package/dist/domain/plugin/push.d.ts.map +1 -1
  88. package/dist/domain/plugin/push.js +8 -14
  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 +3 -1
  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 +3 -2
  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 +0 -2
  98. package/dist/domain/skill/controller.js.map +1 -1
  99. package/dist/domain/workflow/agent-call-references.d.ts +54 -0
  100. package/dist/domain/workflow/agent-call-references.d.ts.map +1 -0
  101. package/dist/domain/workflow/agent-call-references.js +97 -0
  102. package/dist/domain/workflow/agent-call-references.js.map +1 -0
  103. package/dist/domain/workflow/controller.d.ts.map +1 -1
  104. package/dist/domain/workflow/controller.js +14 -6
  105. package/dist/domain/workflow/controller.js.map +1 -1
  106. package/dist/domain/workflow/registry/data/task-kind-registry.json +1 -1
  107. package/dist/domain/workflow/validation/task-config-constraints.d.ts +10 -1
  108. package/dist/domain/workflow/validation/task-config-constraints.d.ts.map +1 -1
  109. package/dist/domain/workflow/validation/task-config-constraints.js +3 -2
  110. package/dist/domain/workflow/validation/task-config-constraints.js.map +1 -1
  111. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  112. package/dist/domain/workflowexecution/controller.js +4 -2
  113. package/dist/domain/workflowexecution/controller.js.map +1 -1
  114. package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
  115. package/dist/domain/workflowinstance/controller.js +3 -4
  116. package/dist/domain/workflowinstance/controller.js.map +1 -1
  117. package/dist/extensions/authorization-queries.d.ts +6 -5
  118. package/dist/extensions/authorization-queries.d.ts.map +1 -1
  119. package/dist/extensions/list-read-scope.d.ts +1 -1
  120. package/dist/extensions/resource-authorization.d.ts +3 -2
  121. package/dist/extensions/resource-authorization.d.ts.map +1 -1
  122. package/dist/index.d.ts +1 -1
  123. package/dist/index.d.ts.map +1 -1
  124. package/dist/index.js.map +1 -1
  125. package/dist/pipeline/apiresource-meta.d.ts +12 -3
  126. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  127. package/dist/pipeline/apiresource-meta.js +21 -10
  128. package/dist/pipeline/apiresource-meta.js.map +1 -1
  129. package/dist/pipeline/steps/authorization-tuples.d.ts +9 -3
  130. package/dist/pipeline/steps/authorization-tuples.d.ts.map +1 -1
  131. package/dist/pipeline/steps/authorization-tuples.js +9 -11
  132. package/dist/pipeline/steps/authorization-tuples.js.map +1 -1
  133. package/dist/pipeline/steps/references.d.ts +178 -20
  134. package/dist/pipeline/steps/references.d.ts.map +1 -1
  135. package/dist/pipeline/steps/references.js +344 -43
  136. package/dist/pipeline/steps/references.js.map +1 -1
  137. package/dist/query/search/controller.js +0 -1
  138. package/dist/query/search/controller.js.map +1 -1
  139. package/dist/query/search/criteria.d.ts +1 -5
  140. package/dist/query/search/criteria.d.ts.map +1 -1
  141. package/dist/query/search/criteria.js +3 -13
  142. package/dist/query/search/criteria.js.map +1 -1
  143. package/dist/query/search/handler.d.ts +5 -4
  144. package/dist/query/search/handler.d.ts.map +1 -1
  145. package/dist/query/search/handler.js +7 -6
  146. package/dist/query/search/handler.js.map +1 -1
  147. package/dist/query/search/query-store.d.ts.map +1 -1
  148. package/dist/query/search/query-store.js +0 -2
  149. package/dist/query/search/query-store.js.map +1 -1
  150. package/dist/store/interface.d.ts +3 -6
  151. package/dist/store/interface.d.ts.map +1 -1
  152. package/dist/store/interface.js.map +1 -1
  153. package/dist/store/postgres/migrations.d.ts +3 -1
  154. package/dist/store/postgres/migrations.d.ts.map +1 -1
  155. package/dist/store/postgres/migrations.js +35 -1
  156. package/dist/store/postgres/migrations.js.map +1 -1
  157. package/dist/store/postgres/store.d.ts.map +1 -1
  158. package/dist/store/postgres/store.js +4 -9
  159. package/dist/store/postgres/store.js.map +1 -1
  160. package/dist/store/public-visibility-retired.d.ts +67 -0
  161. package/dist/store/public-visibility-retired.d.ts.map +1 -0
  162. package/dist/store/public-visibility-retired.js +55 -0
  163. package/dist/store/public-visibility-retired.js.map +1 -0
  164. package/dist/store/sqlite/migrations.d.ts +3 -1
  165. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  166. package/dist/store/sqlite/migrations.js +36 -1
  167. package/dist/store/sqlite/migrations.js.map +1 -1
  168. package/dist/store/sqlite/store.d.ts.map +1 -1
  169. package/dist/store/sqlite/store.js +4 -8
  170. package/dist/store/sqlite/store.js.map +1 -1
  171. package/package.json +6 -6
  172. package/src/authorization/README.md +24 -26
  173. package/src/authorization/__tests__/authorizer.test.ts +10 -9
  174. package/src/authorization/__tests__/derived-tuples.test.ts +4 -11
  175. package/src/authorization/__tests__/evaluator.test.ts +14 -73
  176. package/src/authorization/__tests__/facts.test.ts +2 -2
  177. package/src/authorization/__tests__/fixtures/fga/agent-instance-creation.fga.yaml +19 -27
  178. package/src/authorization/__tests__/fixtures/fga/blueprint-private-visibility.fga.yaml +6 -98
  179. package/src/authorization/__tests__/fixtures/fga/default-instance-inheritance.fga.yaml +4 -61
  180. package/src/authorization/__tests__/fixtures/fga/mcp-server-authoring.fga.yaml +1 -15
  181. package/src/authorization/__tests__/fixtures/fga/org-admin-owner-inheritance.fga.yaml +1 -21
  182. package/src/authorization/__tests__/fixtures/fga/platform-visibility.fga.yaml +1 -25
  183. package/src/authorization/__tests__/fixtures/fga/workflow-execution-sharing.fga.yaml +1 -3
  184. package/src/authorization/__tests__/list-read-scope.test.ts +18 -17
  185. package/src/authorization/__tests__/store-tests.test.ts +2 -6
  186. package/src/authorization/__tests__/tuples.test.ts +12 -11
  187. package/src/authorization/authorizer.ts +4 -9
  188. package/src/authorization/derived-tuples.ts +14 -31
  189. package/src/authorization/evaluator.ts +8 -44
  190. package/src/authorization/list-read-scope.ts +5 -11
  191. package/src/authorization/model/agent.ts +2 -5
  192. package/src/authorization/model/agent_instance.ts +0 -2
  193. package/src/authorization/model/mcp_server.ts +0 -2
  194. package/src/authorization/model/plugin.ts +1 -4
  195. package/src/authorization/model/rewrite.ts +8 -13
  196. package/src/authorization/model/skill.ts +0 -2
  197. package/src/authorization/model/workflow.ts +0 -2
  198. package/src/authorization/model/workflow_instance.ts +0 -2
  199. package/src/authorization/organization-directory.ts +1 -3
  200. package/src/authorization/store-test-kit.ts +16 -75
  201. package/src/authorization/tuples.ts +16 -39
  202. package/src/domain/agent/__tests__/agent.test.ts +11 -4
  203. package/src/domain/agent/controller.ts +7 -7
  204. package/src/domain/agentchannel/__tests__/agentchannel.test.ts +223 -48
  205. package/src/domain/agentchannel/controller.ts +6 -1
  206. package/src/domain/agentexecution/controller.ts +6 -1
  207. package/src/domain/agentinstance/controller.ts +6 -12
  208. package/src/domain/agentshare/__tests__/agentshare.test.ts +196 -172
  209. package/src/domain/agentshare/__tests__/create-authorization.test.ts +16 -31
  210. package/src/domain/agentshare/constants.ts +14 -26
  211. package/src/domain/agentshare/controller.ts +6 -1
  212. package/src/domain/agentshare/steps.ts +41 -194
  213. package/src/domain/environment/controller.ts +5 -8
  214. package/src/domain/executioncontext/controller.ts +5 -1
  215. package/src/domain/mcpserver/__tests__/mcpserver.test.ts +115 -28
  216. package/src/domain/mcpserver/__tests__/oauth-handshake.test.ts +53 -18
  217. package/src/domain/mcpserver/controller.ts +6 -12
  218. package/src/domain/plugin/controller.ts +1 -8
  219. package/src/domain/plugin/push.ts +8 -16
  220. package/src/domain/schedule/controller.ts +6 -1
  221. package/src/domain/session/controller.ts +12 -4
  222. package/src/domain/skill/controller.ts +0 -7
  223. package/src/domain/workflow/__tests__/agent-call-references.test.ts +222 -0
  224. package/src/domain/workflow/agent-call-references.ts +157 -0
  225. package/src/domain/workflow/controller.ts +28 -10
  226. package/src/domain/workflow/registry/data/task-kind-registry.json +1 -1
  227. package/src/domain/workflow/validation/task-config-constraints.ts +7 -3
  228. package/src/domain/workflowexecution/controller.ts +7 -2
  229. package/src/domain/workflowinstance/controller.ts +6 -12
  230. package/src/extensions/__tests__/built-in-authorization-composed.test.ts +32 -19
  231. package/src/extensions/__tests__/extension-composition.test.ts +4 -4
  232. package/src/extensions/__tests__/list-read-scope-composed.test.ts +9 -6
  233. package/src/extensions/authorization-queries.ts +6 -5
  234. package/src/extensions/list-read-scope.ts +1 -1
  235. package/src/extensions/resource-authorization.ts +3 -5
  236. package/src/index.ts +0 -1
  237. package/src/pipeline/__tests__/supports-visibility.test.ts +112 -0
  238. package/src/pipeline/apiresource-meta.ts +21 -10
  239. package/src/pipeline/steps/__tests__/authorization-tuples.test.ts +18 -31
  240. package/src/pipeline/steps/__tests__/references.test.ts +703 -0
  241. package/src/pipeline/steps/authorization-tuples.ts +9 -11
  242. package/src/pipeline/steps/references.ts +578 -70
  243. package/src/query/search/__tests__/criteria.test.ts +0 -4
  244. package/src/query/search/__tests__/query-store.test.ts +11 -40
  245. package/src/query/search/controller.ts +0 -1
  246. package/src/query/search/criteria.ts +0 -14
  247. package/src/query/search/handler.ts +6 -7
  248. package/src/query/search/query-store.ts +0 -2
  249. package/src/store/__tests__/public-visibility-retired.test.ts +127 -0
  250. package/src/store/__tests__/store-contract.ts +6 -51
  251. package/src/store/interface.ts +3 -6
  252. package/src/store/postgres/__tests__/migrations.test.ts +187 -6
  253. package/src/store/postgres/__tests__/store-contract.test.ts +0 -2
  254. package/src/store/postgres/migrations.ts +48 -1
  255. package/src/store/postgres/store.ts +4 -11
  256. package/src/store/public-visibility-retired.ts +119 -0
  257. package/src/store/sqlite/__tests__/migrations.test.ts +191 -5
  258. package/src/store/sqlite/migrations.ts +45 -1
  259. package/src/store/sqlite/store.ts +4 -10
  260. package/dist/pipeline/steps/visibility-gates.d.ts +0 -27
  261. package/dist/pipeline/steps/visibility-gates.d.ts.map +0 -1
  262. package/dist/pipeline/steps/visibility-gates.js +0 -139
  263. package/dist/pipeline/steps/visibility-gates.js.map +0 -1
  264. package/src/authorization/__tests__/fixtures/fga/cross-org-share-creation.fga.yaml +0 -98
  265. package/src/authorization/__tests__/fixtures/fga/public-visibility-setter.fga.yaml +0 -52
  266. package/src/pipeline/steps/__tests__/visibility-gates.test.ts +0 -187
  267. package/src/pipeline/steps/visibility-gates.ts +0 -174
@@ -0,0 +1,703 @@
1
+ /**
2
+ * Pins the reference rule (pipeline/steps/references.ts): the table is
3
+ * exactly the set of kinds the contract lets a spec reference (walked from
4
+ * the schemas of every kind whose chain runs the rule, plus the agent_call
5
+ * task config, through the `reference_kind` field option); each clause on
6
+ * each kind — no org refused, a same-organization target must exist, the
7
+ * floor applies to the kinds the run reads as the person and to no other,
8
+ * a cross-organization target is admitted only at platform visibility with
9
+ * ONE sentence whether it is missing or merely not shared; the MCP-server
10
+ * copy that predates the rule is byte-identical and its siblings take its
11
+ * shape; the walk reads a reference's kind from its field, not from the
12
+ * message; the step over a real store loads each referenced kind once; and
13
+ * the escalation door asks the floor alone — a dependency that has left is
14
+ * not its question — and only when the level is being raised.
15
+ */
16
+ import { create } from "@bufbuild/protobuf";
17
+ import type { DescField, DescMessage } from "@bufbuild/protobuf";
18
+ import { getOption, hasOption } from "@bufbuild/protobuf";
19
+ import { Code, ConnectError } from "@connectrpc/connect";
20
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
21
+
22
+ import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
23
+ import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
24
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
25
+ import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
26
+ import { AgentShareSchema } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
27
+ import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
28
+ import { ExecutionContextSchema } from "@stigmer/protos/ai/stigmer/agentic/executioncontext/v1/api_pb";
29
+ import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
30
+ import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
31
+ import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
32
+ import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
33
+ import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
34
+ import { AgentCallTaskConfigSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/agent_call_pb";
35
+ import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
36
+ import { WorkflowInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowinstance/v1/api_pb";
37
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
38
+ import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
39
+ import { reference_kind } from "@stigmer/protos/ai/stigmer/commons/apiresource/field_options_pb";
40
+ import { UpdateVisibilityInputSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
41
+
42
+ import type { Store } from "../../../store/interface.js";
43
+ import { SqliteStore } from "../../../store/sqlite/store.js";
44
+ import { tempStore } from "../../../store/sqlite/__tests__/support.js";
45
+ import { RequestContext } from "../../request-context.js";
46
+ import { testCallerIdentity } from "../../__tests__/support.js";
47
+ import {
48
+ REFERENCE_TARGET_KINDS,
49
+ belowFloorMessage,
50
+ checkReference,
51
+ checkReferences,
52
+ collectSpecReferences,
53
+ loadReferenceTargets,
54
+ missingReferencesMessage,
55
+ newGuardReferenceFloorOnEscalationStep,
56
+ newValidateReferencesStep,
57
+ noOrgReferenceMessage,
58
+ notAvailableReferenceMessage,
59
+ referenceTargetKind,
60
+ } from "../references.js";
61
+ import type {
62
+ ReferenceParent,
63
+ ReferenceTargets,
64
+ SpecReference,
65
+ } from "../references.js";
66
+
67
+ const V = ApiResourceVisibility;
68
+ const K = ApiResourceKind;
69
+
70
+ /** The error a step throws, awaited whether the step is sync or async. */
71
+ async function failureOf(run: () => void | Promise<void>): Promise<unknown> {
72
+ try {
73
+ await run();
74
+ return undefined;
75
+ } catch (error) {
76
+ return error;
77
+ }
78
+ }
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // The table against the contract.
82
+ // ---------------------------------------------------------------------------
83
+
84
+ /** The schemas of every kind whose create or update chain runs the rule, plus the Struct-borne task config. */
85
+ const SCHEMAS_UNDER_THE_RULE: ReadonlyArray<DescMessage> = [
86
+ AgentSchema,
87
+ McpServerSchema,
88
+ WorkflowSchema,
89
+ AgentInstanceSchema,
90
+ WorkflowInstanceSchema,
91
+ EnvironmentSchema,
92
+ ScheduleSchema,
93
+ AgentChannelSchema,
94
+ AgentShareSchema,
95
+ SessionSchema,
96
+ AgentExecutionSchema,
97
+ WorkflowExecutionSchema,
98
+ ExecutionContextSchema,
99
+ AgentCallTaskConfigSchema,
100
+ ];
101
+
102
+ /** Every `reference_kind` declared on a field reachable from `schema`, recursively. */
103
+ function declaredReferenceKinds(
104
+ schema: DescMessage,
105
+ seen = new Set<string>(),
106
+ out = new Set<ApiResourceKind>(),
107
+ ): Set<ApiResourceKind> {
108
+ if (seen.has(schema.typeName)) {
109
+ return out;
110
+ }
111
+ seen.add(schema.typeName);
112
+ for (const field of schema.fields) {
113
+ if (hasOption(field, reference_kind)) {
114
+ out.add(getOption(field, reference_kind));
115
+ }
116
+ const nested = messageOf(field);
117
+ if (nested !== undefined) {
118
+ declaredReferenceKinds(nested, seen, out);
119
+ }
120
+ }
121
+ return out;
122
+ }
123
+
124
+ function messageOf(field: DescField): DescMessage | undefined {
125
+ switch (field.fieldKind) {
126
+ case "message":
127
+ return field.message;
128
+ case "list":
129
+ return field.listKind === "message" ? field.message : undefined;
130
+ case "map":
131
+ return field.mapKind === "message" ? field.message : undefined;
132
+ case "scalar":
133
+ case "enum":
134
+ return undefined;
135
+ default: {
136
+ const exhaustive: never = field;
137
+ throw new Error(`unknown field kind: ${JSON.stringify(exhaustive)}`);
138
+ }
139
+ }
140
+ }
141
+
142
+ describe("REFERENCE_TARGET_KINDS against the contract", () => {
143
+ it("is exactly the set of reference_kind values a spec under the rule can carry", () => {
144
+ const declared = new Set<ApiResourceKind>();
145
+ for (const schema of SCHEMAS_UNDER_THE_RULE) {
146
+ for (const kind of declaredReferenceKinds(schema)) {
147
+ declared.add(kind);
148
+ }
149
+ }
150
+ expect([...declared].sort()).toEqual(
151
+ REFERENCE_TARGET_KINDS.map((entry) => entry.kind).sort(),
152
+ );
153
+ });
154
+
155
+ it("names the kinds the run reads as the person, and no other", () => {
156
+ expect(
157
+ REFERENCE_TARGET_KINDS.filter((e) => e.readByRun).map((e) => e.kind),
158
+ ).toEqual([K.skill, K.mcp_server, K.agent]);
159
+ expect(
160
+ REFERENCE_TARGET_KINDS.filter((e) => !e.readByRun).map((e) => e.kind),
161
+ ).toEqual([K.environment, K.channel_app, K.oauth_app]);
162
+ });
163
+ });
164
+
165
+ // ---------------------------------------------------------------------------
166
+ // The clauses, over a fixture of targets.
167
+ // ---------------------------------------------------------------------------
168
+
169
+ function targetsOf(
170
+ rows: ReadonlyArray<{
171
+ kind: ApiResourceKind;
172
+ org: string;
173
+ slug: string;
174
+ visibility: ApiResourceVisibility;
175
+ }>,
176
+ ): ReferenceTargets {
177
+ return {
178
+ visibilityOf(ref) {
179
+ return rows.find(
180
+ (row) =>
181
+ row.kind === ref.kind && row.org === ref.org && row.slug === ref.slug,
182
+ )?.visibility;
183
+ },
184
+ };
185
+ }
186
+
187
+ const ACME_ORG: ReferenceParent = { org: "acme", visibility: V.visibility_org };
188
+ const ACME_PRIVATE: ReferenceParent = {
189
+ org: "acme",
190
+ visibility: V.visibility_private,
191
+ };
192
+
193
+ const ref = (
194
+ kind: ApiResourceKind,
195
+ org: string,
196
+ slug: string,
197
+ ): SpecReference => ({ kind, org, slug });
198
+
199
+ describe("checkReference", () => {
200
+ const targets = targetsOf([
201
+ {
202
+ kind: K.skill,
203
+ org: "acme",
204
+ slug: "org-skill",
205
+ visibility: V.visibility_org,
206
+ },
207
+ {
208
+ kind: K.skill,
209
+ org: "acme",
210
+ slug: "my-skill",
211
+ visibility: V.visibility_private,
212
+ },
213
+ {
214
+ kind: K.mcp_server,
215
+ org: "acme",
216
+ slug: "github",
217
+ visibility: V.visibility_org,
218
+ },
219
+ {
220
+ kind: K.environment,
221
+ org: "acme",
222
+ slug: "personal",
223
+ visibility: V.visibility_private,
224
+ },
225
+ {
226
+ kind: K.skill,
227
+ org: "globex",
228
+ slug: "shared",
229
+ visibility: V.visibility_platform,
230
+ },
231
+ {
232
+ kind: K.skill,
233
+ org: "globex",
234
+ slug: "internal",
235
+ visibility: V.visibility_org,
236
+ },
237
+ {
238
+ kind: K.environment,
239
+ org: "globex",
240
+ slug: "theirs",
241
+ visibility: V.visibility_org,
242
+ },
243
+ ]);
244
+
245
+ it("(i) a reference with no organization is refused before anything is looked up", () => {
246
+ expect(
247
+ checkReference(targets, ACME_ORG, ref(K.skill, "", "org-skill")),
248
+ ).toEqual({ kind: "no-org" });
249
+ });
250
+
251
+ it("(ii) a same-organization target must exist, for every kind", () => {
252
+ expect(
253
+ checkReference(targets, ACME_ORG, ref(K.skill, "acme", "org-skill")),
254
+ ).toEqual({ kind: "ok" });
255
+ for (const entry of REFERENCE_TARGET_KINDS) {
256
+ expect(
257
+ checkReference(targets, ACME_ORG, ref(entry.kind, "acme", "ghost")),
258
+ entry.label,
259
+ ).toEqual({ kind: "missing" });
260
+ }
261
+ });
262
+
263
+ it("(ii) the floor: a run-read dependency may not be less visible than the resource", () => {
264
+ expect(
265
+ checkReference(targets, ACME_ORG, ref(K.skill, "acme", "my-skill")),
266
+ ).toEqual({
267
+ kind: "below-floor",
268
+ targetVisibility: V.visibility_private,
269
+ });
270
+ // A private resource sits at the floor's bottom: anything it references clears it.
271
+ expect(
272
+ checkReference(targets, ACME_PRIVATE, ref(K.skill, "acme", "my-skill")),
273
+ ).toEqual({ kind: "ok" });
274
+ // An unset level reads as private.
275
+ expect(
276
+ checkReference(
277
+ targets,
278
+ { org: "acme", visibility: V.api_resource_visibility_unspecified },
279
+ ref(K.skill, "acme", "my-skill"),
280
+ ),
281
+ ).toEqual({ kind: "ok" });
282
+ // A platform-visible resource needs platform-visible dependencies.
283
+ expect(
284
+ checkReference(
285
+ targets,
286
+ { org: "acme", visibility: V.visibility_platform },
287
+ ref(K.mcp_server, "acme", "github"),
288
+ ),
289
+ ).toEqual({ kind: "below-floor", targetVisibility: V.visibility_org });
290
+ });
291
+
292
+ it("(ii) the floor does not apply to what the server resolves on the run's behalf", () => {
293
+ // An org-visible instance over a private personal environment: the
294
+ // ordinary shape of a personal instance, admitted.
295
+ expect(
296
+ checkReference(targets, ACME_ORG, ref(K.environment, "acme", "personal")),
297
+ ).toEqual({ kind: "ok" });
298
+ });
299
+
300
+ it("(iii) another organization's target is admitted only at platform visibility, with one answer for missing and for not shared", () => {
301
+ expect(
302
+ checkReference(targets, ACME_ORG, ref(K.skill, "globex", "shared")),
303
+ ).toEqual({ kind: "ok" });
304
+ const notShared = checkReference(
305
+ targets,
306
+ ACME_ORG,
307
+ ref(K.skill, "globex", "internal"),
308
+ );
309
+ const missing = checkReference(
310
+ targets,
311
+ ACME_ORG,
312
+ ref(K.skill, "globex", "nothing"),
313
+ );
314
+ expect(notShared).toEqual({ kind: "not-available" });
315
+ expect(missing).toEqual(notShared);
316
+ // A kind that is never platform-visible: every cross-organization reference to it.
317
+ expect(
318
+ checkReference(targets, ACME_ORG, ref(K.environment, "globex", "theirs")),
319
+ ).toEqual({ kind: "not-available" });
320
+ });
321
+
322
+ it("(iii) a platform-visible cross-organization target is not held to the floor — the level is the sharing organization's to set", () => {
323
+ expect(
324
+ checkReference(
325
+ targets,
326
+ { org: "acme", visibility: V.visibility_platform },
327
+ ref(K.skill, "globex", "shared"),
328
+ ),
329
+ ).toEqual({ kind: "ok" });
330
+ });
331
+ });
332
+
333
+ // ---------------------------------------------------------------------------
334
+ // The copy.
335
+ // ---------------------------------------------------------------------------
336
+
337
+ describe("the refusal copy", () => {
338
+ const mcp = referenceTargetKind(K.mcp_server)!;
339
+ const skill = referenceTargetKind(K.skill)!;
340
+ const oauth = referenceTargetKind(K.oauth_app)!;
341
+
342
+ it("the MCP-server sentence is byte-identical to the contract that predates the rule", () => {
343
+ expect(
344
+ missingReferencesMessage(mcp, [{ slug: "ghost", org: "acme" }]),
345
+ ).toBe(
346
+ "referenced MCP server(s) not found: 'ghost' (org: acme). " +
347
+ "Verify the slug and org are correct. " +
348
+ "Use 'stigmer get mcp-servers' to list available MCP servers.",
349
+ );
350
+ });
351
+
352
+ it("its siblings take the same shape, and a kind with no CLI verb ends without the hint", () => {
353
+ expect(
354
+ missingReferencesMessage(skill, [
355
+ { slug: "a", org: "acme" },
356
+ { slug: "b", org: "acme" },
357
+ ]),
358
+ ).toBe(
359
+ "referenced skill(s) not found: 'a' (org: acme), 'b' (org: acme). " +
360
+ "Verify the slug and org are correct. " +
361
+ "Use 'stigmer list skills' to list available skills.",
362
+ );
363
+ expect(
364
+ missingReferencesMessage(oauth, [{ slug: "vendor", org: "acme" }]),
365
+ ).toBe(
366
+ "referenced OAuth app(s) not found: 'vendor' (org: acme). " +
367
+ "Verify the slug and org are correct.",
368
+ );
369
+ });
370
+
371
+ it("the floor names the dependency and both levels; the cross-organization sentence names neither existence nor level", () => {
372
+ expect(
373
+ belowFloorMessage(
374
+ skill,
375
+ ref(K.skill, "acme", "my-skill"),
376
+ V.visibility_private,
377
+ V.visibility_org,
378
+ ),
379
+ ).toBe(
380
+ "referenced skill 'acme/my-skill' is visibility_private while this resource is visibility_org; " +
381
+ "a resource may not be more visible than the skills it runs with. " +
382
+ "Widen the referenced resource's visibility or narrow this one.",
383
+ );
384
+ expect(
385
+ notAvailableReferenceMessage(skill, ref(K.skill, "globex", "x")),
386
+ ).toBe(
387
+ "referenced skill 'globex/x' is not available to this organization; " +
388
+ "another organization's resource can be referenced only when that organization shares it at platform visibility.",
389
+ );
390
+ expect(noOrgReferenceMessage(skill, "x")).toBe(
391
+ "referenced skill(s) 'x' names no organization; a reference is 'org/slug', or 'slug' for a resource of this organization.",
392
+ );
393
+ });
394
+ });
395
+
396
+ // ---------------------------------------------------------------------------
397
+ // The walk and the step, over a real store.
398
+ // ---------------------------------------------------------------------------
399
+
400
+ describe("the step over a store", () => {
401
+ let store: SqliteStore;
402
+ let cleanup: () => Promise<void>;
403
+
404
+ beforeEach(() => {
405
+ const temp = tempStore();
406
+ store = temp.store;
407
+ cleanup = temp.cleanup;
408
+ });
409
+
410
+ afterEach(async () => {
411
+ await cleanup();
412
+ });
413
+
414
+ async function seedSkill(
415
+ id: string,
416
+ org: string,
417
+ slug: string,
418
+ visibility: ApiResourceVisibility,
419
+ ): Promise<void> {
420
+ await store.saveResource(
421
+ K.skill,
422
+ id,
423
+ SkillSchema,
424
+ create(SkillSchema, {
425
+ apiVersion: "agentic.stigmer.ai/v1",
426
+ kind: "Skill",
427
+ metadata: { id, name: slug, slug, org, visibility },
428
+ }),
429
+ );
430
+ }
431
+
432
+ function agentWith(
433
+ visibility: ApiResourceVisibility,
434
+ skillRefs: ReadonlyArray<{ org: string; slug: string }>,
435
+ ) {
436
+ return create(AgentSchema, {
437
+ apiVersion: "agentic.stigmer.ai/v1",
438
+ kind: "Agent",
439
+ metadata: { name: "Helper", org: "acme", visibility },
440
+ spec: {
441
+ instructions: "help the user with their tasks",
442
+ skillRefs: skillRefs.map((r) => ({ ...r })),
443
+ },
444
+ });
445
+ }
446
+
447
+ it("the walk reads a reference's kind from its field's declaration, not from the message", () => {
448
+ const agent = create(AgentSchema, {
449
+ apiVersion: "agentic.stigmer.ai/v1",
450
+ kind: "Agent",
451
+ metadata: { name: "Helper", org: "acme" },
452
+ spec: {
453
+ instructions: "help",
454
+ // The message says mcp_server; the field is skill_refs.
455
+ skillRefs: [{ kind: K.mcp_server, org: "acme", slug: "s" }],
456
+ mcpServerUsages: [{ mcpServerRef: { org: "acme", slug: "m" } }],
457
+ },
458
+ });
459
+ // Field declaration order: mcp_server_usages precedes skill_refs.
460
+ expect(collectSpecReferences(AgentSchema, agent)).toEqual([
461
+ { kind: K.mcp_server, org: "acme", slug: "m" },
462
+ { kind: K.skill, org: "acme", slug: "s" },
463
+ ]);
464
+ });
465
+
466
+ it("loads each referenced kind once and admits an existing same-organization target", async () => {
467
+ await seedSkill("skl_1", "acme", "one", V.visibility_org);
468
+ await seedSkill("skl_2", "acme", "two", V.visibility_org);
469
+ let scans = 0;
470
+ const counting: Store = new Proxy(store, {
471
+ get(target, property, receiver) {
472
+ if (property === "listResources") {
473
+ scans += 1;
474
+ }
475
+ return Reflect.get(target, property, receiver) as unknown;
476
+ },
477
+ });
478
+ const refusal = await checkReferences(counting, ACME_ORG, [
479
+ ref(K.skill, "acme", "one"),
480
+ ref(K.skill, "acme", "two"),
481
+ ]);
482
+ expect(refusal).toBeUndefined();
483
+ expect(scans).toBe(1);
484
+ });
485
+
486
+ it("the step refuses a missing target with the contract's code and copy, and passes when the row exists", async () => {
487
+ const missing = new RequestContext(
488
+ AgentSchema,
489
+ agentWith(V.visibility_org, [{ org: "acme", slug: "ghost" }]),
490
+ testCallerIdentity(),
491
+ K.agent,
492
+ );
493
+ const error = await failureOf(() =>
494
+ newValidateReferencesStep<typeof AgentSchema>(store).execute(missing),
495
+ );
496
+ expect(error).toBeInstanceOf(ConnectError);
497
+ expect((error as ConnectError).code).toBe(Code.FailedPrecondition);
498
+ expect((error as ConnectError).rawMessage).toBe(
499
+ "referenced skill(s) not found: 'ghost' (org: acme). " +
500
+ "Verify the slug and org are correct. " +
501
+ "Use 'stigmer list skills' to list available skills.",
502
+ );
503
+
504
+ await seedSkill("skl_1", "acme", "ghost", V.visibility_org);
505
+ await expect(
506
+ newValidateReferencesStep<typeof AgentSchema>(store).execute(missing),
507
+ ).resolves.toBeUndefined();
508
+ });
509
+
510
+ it("the step refuses the floor at the spec door and a cross-organization reference to a row it will not describe", async () => {
511
+ await seedSkill("skl_1", "acme", "mine", V.visibility_private);
512
+ await seedSkill("skl_2", "globex", "theirs", V.visibility_org);
513
+ const ctx = new RequestContext(
514
+ AgentSchema,
515
+ agentWith(V.visibility_org, [
516
+ { org: "acme", slug: "mine" },
517
+ { org: "globex", slug: "theirs" },
518
+ ]),
519
+ testCallerIdentity(),
520
+ K.agent,
521
+ );
522
+ const error = await failureOf(() =>
523
+ newValidateReferencesStep<typeof AgentSchema>(store).execute(ctx),
524
+ );
525
+ expect((error as ConnectError).code).toBe(Code.FailedPrecondition);
526
+ expect((error as ConnectError).rawMessage).toBe(
527
+ belowFloorMessage(
528
+ referenceTargetKind(K.skill)!,
529
+ ref(K.skill, "acme", "mine"),
530
+ V.visibility_private,
531
+ V.visibility_org,
532
+ ) +
533
+ " " +
534
+ notAvailableReferenceMessage(
535
+ referenceTargetKind(K.skill)!,
536
+ ref(K.skill, "globex", "theirs"),
537
+ ),
538
+ );
539
+ });
540
+
541
+ it("a reference left without an organization is malformed input", async () => {
542
+ const ctx = new RequestContext(
543
+ AgentSchema,
544
+ agentWith(V.visibility_org, [{ org: "", slug: "x" }]),
545
+ testCallerIdentity(),
546
+ K.agent,
547
+ );
548
+ const error = await failureOf(() =>
549
+ newValidateReferencesStep<typeof AgentSchema>(store).execute(ctx),
550
+ );
551
+ expect((error as ConnectError).code).toBe(Code.InvalidArgument);
552
+ expect((error as ConnectError).rawMessage).toBe(
553
+ noOrgReferenceMessage(referenceTargetKind(K.skill)!, "x"),
554
+ );
555
+ });
556
+
557
+ it("the loaded targets are indexed by (org, slug): the same slug in another organization is another row", async () => {
558
+ await seedSkill("skl_a", "acme", "shared-name", V.visibility_org);
559
+ await seedSkill("skl_g", "globex", "shared-name", V.visibility_platform);
560
+ const targets = await loadReferenceTargets(store, [
561
+ ref(K.skill, "acme", "shared-name"),
562
+ ]);
563
+ expect(targets.visibilityOf(ref(K.skill, "acme", "shared-name"))).toBe(
564
+ V.visibility_org,
565
+ );
566
+ expect(targets.visibilityOf(ref(K.skill, "globex", "shared-name"))).toBe(
567
+ V.visibility_platform,
568
+ );
569
+ expect(targets.visibilityOf(ref(K.skill, "acme", "none"))).toBeUndefined();
570
+ });
571
+ });
572
+
573
+ describe("GuardReferenceFloorOnEscalation", () => {
574
+ let store: SqliteStore;
575
+ let cleanup: () => Promise<void>;
576
+ const TARGET = "loadedAgent";
577
+
578
+ beforeEach(() => {
579
+ const temp = tempStore();
580
+ store = temp.store;
581
+ cleanup = temp.cleanup;
582
+ });
583
+
584
+ afterEach(async () => {
585
+ await cleanup();
586
+ });
587
+
588
+ async function seedSkill(
589
+ id: string,
590
+ slug: string,
591
+ visibility: ApiResourceVisibility,
592
+ ): Promise<void> {
593
+ await store.saveResource(
594
+ K.skill,
595
+ id,
596
+ SkillSchema,
597
+ create(SkillSchema, {
598
+ apiVersion: "agentic.stigmer.ai/v1",
599
+ kind: "Skill",
600
+ metadata: { id, name: slug, slug, org: "acme", visibility },
601
+ }),
602
+ );
603
+ }
604
+
605
+ function storedAgent(
606
+ visibility: ApiResourceVisibility,
607
+ skillSlugs: ReadonlyArray<string>,
608
+ ) {
609
+ return create(AgentSchema, {
610
+ apiVersion: "agentic.stigmer.ai/v1",
611
+ kind: "Agent",
612
+ metadata: { id: "agt_1", name: "Helper", org: "acme", visibility },
613
+ spec: {
614
+ instructions: "help",
615
+ skillRefs: skillSlugs.map((slug) => ({ org: "acme", slug })),
616
+ },
617
+ });
618
+ }
619
+
620
+ async function escalate(
621
+ stored: ReturnType<typeof storedAgent>,
622
+ requested: ApiResourceVisibility,
623
+ ): Promise<unknown> {
624
+ const ctx = new RequestContext(
625
+ UpdateVisibilityInputSchema,
626
+ create(UpdateVisibilityInputSchema, {
627
+ resourceId: "agt_1",
628
+ visibility: requested,
629
+ }),
630
+ testCallerIdentity(),
631
+ K.agent,
632
+ );
633
+ ctx.set(TARGET, stored);
634
+ return failureOf(() =>
635
+ newGuardReferenceFloorOnEscalationStep(store, TARGET, [
636
+ (row) => collectSpecReferences(AgentSchema, row),
637
+ ]).execute(ctx),
638
+ );
639
+ }
640
+
641
+ it("refuses raising a level above a run-read dependency, naming it", async () => {
642
+ await seedSkill("skl_1", "mine", V.visibility_private);
643
+ const error = await escalate(
644
+ storedAgent(V.visibility_private, ["mine"]),
645
+ V.visibility_org,
646
+ );
647
+ expect((error as ConnectError).code).toBe(Code.FailedPrecondition);
648
+ expect((error as ConnectError).rawMessage).toBe(
649
+ belowFloorMessage(
650
+ referenceTargetKind(K.skill)!,
651
+ ref(K.skill, "acme", "mine"),
652
+ V.visibility_private,
653
+ V.visibility_org,
654
+ ),
655
+ );
656
+ });
657
+
658
+ it("passes when every dependency clears the requested level, and is silent on a lowering or a same-level write", async () => {
659
+ await seedSkill("skl_1", "shared", V.visibility_org);
660
+ expect(
661
+ await escalate(
662
+ storedAgent(V.visibility_private, ["shared"]),
663
+ V.visibility_org,
664
+ ),
665
+ ).toBeUndefined();
666
+ // Lowering never asks; the dependency below the OLD level is not the door's question.
667
+ await seedSkill("skl_2", "mine", V.visibility_private);
668
+ expect(
669
+ await escalate(
670
+ storedAgent(V.visibility_org, ["mine"]),
671
+ V.visibility_private,
672
+ ),
673
+ ).toBeUndefined();
674
+ expect(
675
+ await escalate(storedAgent(V.visibility_org, ["mine"]), V.visibility_org),
676
+ ).toBeUndefined();
677
+ });
678
+
679
+ it("asks the floor alone: a dependency that has left since the write does not block an escalation", async () => {
680
+ expect(
681
+ await escalate(
682
+ storedAgent(V.visibility_private, ["gone"]),
683
+ V.visibility_org,
684
+ ),
685
+ ).toBeUndefined();
686
+ });
687
+
688
+ it("is a wiring fault without a loaded target", async () => {
689
+ const ctx = new RequestContext(
690
+ UpdateVisibilityInputSchema,
691
+ create(UpdateVisibilityInputSchema, {
692
+ resourceId: "agt_1",
693
+ visibility: V.visibility_org,
694
+ }),
695
+ testCallerIdentity(),
696
+ K.agent,
697
+ );
698
+ const error = await failureOf(() =>
699
+ newGuardReferenceFloorOnEscalationStep(store, TARGET, []).execute(ctx),
700
+ );
701
+ expect((error as ConnectError).code).toBe(Code.Internal);
702
+ });
703
+ });