@stigmer/server 3.23.1-dev.20260923075205 → 3.25.0

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 (226) hide show
  1. package/dist/authorization/authorizer.d.ts +10 -4
  2. package/dist/authorization/authorizer.d.ts.map +1 -1
  3. package/dist/authorization/authorizer.js +11 -4
  4. package/dist/authorization/authorizer.js.map +1 -1
  5. package/dist/authorization/model/index.d.ts +4 -4
  6. package/dist/authorization/model/index.d.ts.map +1 -1
  7. package/dist/authorization/model/index.js +2 -0
  8. package/dist/authorization/model/index.js.map +1 -1
  9. package/dist/authorization/model/oauth_app.d.ts.map +1 -1
  10. package/dist/authorization/model/oauth_app.js +3 -2
  11. package/dist/authorization/model/oauth_app.js.map +1 -1
  12. package/dist/authorization/model/platform_client.d.ts +2 -0
  13. package/dist/authorization/model/platform_client.d.ts.map +1 -0
  14. package/dist/authorization/model/platform_client.js +31 -0
  15. package/dist/authorization/model/platform_client.js.map +1 -0
  16. package/dist/boot/compose.d.ts.map +1 -1
  17. package/dist/boot/compose.js +88 -5
  18. package/dist/boot/compose.js.map +1 -1
  19. package/dist/boot/route-shadowing.d.ts +25 -0
  20. package/dist/boot/route-shadowing.d.ts.map +1 -0
  21. package/dist/boot/route-shadowing.js +17 -0
  22. package/dist/boot/route-shadowing.js.map +1 -0
  23. package/dist/domain/iampolicy/membership.d.ts +6 -1
  24. package/dist/domain/iampolicy/membership.d.ts.map +1 -1
  25. package/dist/domain/iampolicy/membership.js +8 -2
  26. package/dist/domain/iampolicy/membership.js.map +1 -1
  27. package/dist/domain/identityaccount/constants.d.ts +13 -0
  28. package/dist/domain/identityaccount/constants.d.ts.map +1 -1
  29. package/dist/domain/identityaccount/constants.js +17 -0
  30. package/dist/domain/identityaccount/constants.js.map +1 -1
  31. package/dist/domain/identityaccount/controller.d.ts +3 -1
  32. package/dist/domain/identityaccount/controller.d.ts.map +1 -1
  33. package/dist/domain/identityaccount/controller.js +15 -6
  34. package/dist/domain/identityaccount/controller.js.map +1 -1
  35. package/dist/domain/identityaccount/operator.d.ts.map +1 -1
  36. package/dist/domain/identityaccount/operator.js +1 -0
  37. package/dist/domain/identityaccount/operator.js.map +1 -1
  38. package/dist/domain/identityaccount/provisioning.d.ts +18 -0
  39. package/dist/domain/identityaccount/provisioning.d.ts.map +1 -1
  40. package/dist/domain/identityaccount/provisioning.js +1 -0
  41. package/dist/domain/identityaccount/provisioning.js.map +1 -1
  42. package/dist/domain/identityaccount/resource-store.js +9 -5
  43. package/dist/domain/identityaccount/resource-store.js.map +1 -1
  44. package/dist/domain/identityaccount/steps.d.ts +11 -2
  45. package/dist/domain/identityaccount/steps.d.ts.map +1 -1
  46. package/dist/domain/identityaccount/steps.js +42 -4
  47. package/dist/domain/identityaccount/steps.js.map +1 -1
  48. package/dist/domain/identityaccount/store-contract.d.ts.map +1 -1
  49. package/dist/domain/identityaccount/store-contract.js +33 -0
  50. package/dist/domain/identityaccount/store-contract.js.map +1 -1
  51. package/dist/domain/identityaccount/store.d.ts +7 -5
  52. package/dist/domain/identityaccount/store.d.ts.map +1 -1
  53. package/dist/domain/identityaccount/store.js.map +1 -1
  54. package/dist/domain/platform/controller.d.ts +10 -0
  55. package/dist/domain/platform/controller.d.ts.map +1 -1
  56. package/dist/domain/platform/controller.js +2 -1
  57. package/dist/domain/platform/controller.js.map +1 -1
  58. package/dist/domain/platformclient/constants.d.ts +60 -0
  59. package/dist/domain/platformclient/constants.d.ts.map +1 -0
  60. package/dist/domain/platformclient/constants.js +78 -0
  61. package/dist/domain/platformclient/constants.js.map +1 -0
  62. package/dist/domain/platformclient/controller.d.ts +59 -0
  63. package/dist/domain/platformclient/controller.d.ts.map +1 -0
  64. package/dist/domain/platformclient/controller.js +191 -0
  65. package/dist/domain/platformclient/controller.js.map +1 -0
  66. package/dist/domain/platformclient/credentials.d.ts +13 -0
  67. package/dist/domain/platformclient/credentials.d.ts.map +1 -0
  68. package/dist/domain/platformclient/credentials.js +56 -0
  69. package/dist/domain/platformclient/credentials.js.map +1 -0
  70. package/dist/domain/platformclient/mint.d.ts +23 -0
  71. package/dist/domain/platformclient/mint.d.ts.map +1 -0
  72. package/dist/domain/platformclient/mint.js +254 -0
  73. package/dist/domain/platformclient/mint.js.map +1 -0
  74. package/dist/domain/platformclient/origin-guard.d.ts +9 -0
  75. package/dist/domain/platformclient/origin-guard.d.ts.map +1 -0
  76. package/dist/domain/platformclient/origin-guard.js +81 -0
  77. package/dist/domain/platformclient/origin-guard.js.map +1 -0
  78. package/dist/domain/platformclient/resource-store.d.ts +4 -0
  79. package/dist/domain/platformclient/resource-store.d.ts.map +1 -0
  80. package/dist/domain/platformclient/resource-store.js +117 -0
  81. package/dist/domain/platformclient/resource-store.js.map +1 -0
  82. package/dist/domain/platformclient/steps.d.ts +51 -0
  83. package/dist/domain/platformclient/steps.d.ts.map +1 -0
  84. package/dist/domain/platformclient/steps.js +339 -0
  85. package/dist/domain/platformclient/steps.js.map +1 -0
  86. package/dist/domain/platformclient/store-contract.d.ts +8 -0
  87. package/dist/domain/platformclient/store-contract.d.ts.map +1 -0
  88. package/dist/domain/platformclient/store-contract.js +208 -0
  89. package/dist/domain/platformclient/store-contract.js.map +1 -0
  90. package/dist/domain/platformclient/store.d.ts +58 -0
  91. package/dist/domain/platformclient/store.d.ts.map +1 -0
  92. package/dist/domain/platformclient/store.js +13 -0
  93. package/dist/domain/platformclient/store.js.map +1 -0
  94. package/dist/domain/platformclient/system-managed.d.ts +18 -0
  95. package/dist/domain/platformclient/system-managed.d.ts.map +1 -0
  96. package/dist/domain/platformclient/system-managed.js +80 -0
  97. package/dist/domain/platformclient/system-managed.js.map +1 -0
  98. package/dist/domain/platformclient/token-controller.d.ts +11 -0
  99. package/dist/domain/platformclient/token-controller.d.ts.map +1 -0
  100. package/dist/domain/platformclient/token-controller.js +32 -0
  101. package/dist/domain/platformclient/token-controller.js.map +1 -0
  102. package/dist/domain/platformclient/verifier.d.ts +10 -0
  103. package/dist/domain/platformclient/verifier.d.ts.map +1 -0
  104. package/dist/domain/platformclient/verifier.js +76 -0
  105. package/dist/domain/platformclient/verifier.js.map +1 -0
  106. package/dist/domain/workflow/agent-call-references.d.ts +60 -33
  107. package/dist/domain/workflow/agent-call-references.d.ts.map +1 -1
  108. package/dist/domain/workflow/agent-call-references.js +43 -23
  109. package/dist/domain/workflow/agent-call-references.js.map +1 -1
  110. package/dist/domain/workflow/registry/data/task-kind-registry.json +2 -2
  111. package/dist/encryption/key-manager.d.ts +20 -3
  112. package/dist/encryption/key-manager.d.ts.map +1 -1
  113. package/dist/encryption/key-manager.js +64 -42
  114. package/dist/encryption/key-manager.js.map +1 -1
  115. package/dist/extensions/caller-guards.d.ts +8 -3
  116. package/dist/extensions/caller-guards.d.ts.map +1 -1
  117. package/dist/extensions/drivers.d.ts +37 -0
  118. package/dist/extensions/drivers.d.ts.map +1 -1
  119. package/dist/extensions/guest-token-minting.d.ts +26 -0
  120. package/dist/extensions/guest-token-minting.d.ts.map +1 -0
  121. package/dist/extensions/guest-token-minting.js +2 -0
  122. package/dist/extensions/guest-token-minting.js.map +1 -0
  123. package/dist/extensions/identity.d.ts +6 -4
  124. package/dist/extensions/identity.d.ts.map +1 -1
  125. package/dist/extensions/identity.js.map +1 -1
  126. package/dist/extensions/registry.d.ts +30 -1
  127. package/dist/extensions/registry.d.ts.map +1 -1
  128. package/dist/extensions/registry.js +34 -1
  129. package/dist/extensions/registry.js.map +1 -1
  130. package/dist/index.d.ts +13 -1
  131. package/dist/index.d.ts.map +1 -1
  132. package/dist/index.js +6 -0
  133. package/dist/index.js.map +1 -1
  134. package/dist/pipeline/apiresource-labels.d.ts +10 -9
  135. package/dist/pipeline/apiresource-labels.d.ts.map +1 -1
  136. package/dist/pipeline/apiresource-labels.js +10 -9
  137. package/dist/pipeline/apiresource-labels.js.map +1 -1
  138. package/dist/pipeline/interceptors/auth.d.ts +14 -0
  139. package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
  140. package/dist/pipeline/interceptors/auth.js +24 -0
  141. package/dist/pipeline/interceptors/auth.js.map +1 -1
  142. package/dist/pipeline/steps/build-update-state.d.ts +11 -1
  143. package/dist/pipeline/steps/build-update-state.d.ts.map +1 -1
  144. package/dist/pipeline/steps/build-update-state.js +5 -2
  145. package/dist/pipeline/steps/build-update-state.js.map +1 -1
  146. package/dist/pipeline/steps/references.d.ts +30 -4
  147. package/dist/pipeline/steps/references.d.ts.map +1 -1
  148. package/dist/pipeline/steps/references.js +13 -1
  149. package/dist/pipeline/steps/references.js.map +1 -1
  150. package/dist/platformtoken/envelope.d.ts +100 -0
  151. package/dist/platformtoken/envelope.d.ts.map +1 -0
  152. package/dist/platformtoken/envelope.js +184 -0
  153. package/dist/platformtoken/envelope.js.map +1 -0
  154. package/dist/platformtoken/key-ring.d.ts +85 -0
  155. package/dist/platformtoken/key-ring.d.ts.map +1 -0
  156. package/dist/platformtoken/key-ring.js +164 -0
  157. package/dist/platformtoken/key-ring.js.map +1 -0
  158. package/package.json +6 -6
  159. package/src/authorization/README.md +1 -1
  160. package/src/authorization/__tests__/authorizer.test.ts +6 -2
  161. package/src/authorization/__tests__/wire-permissions.test.ts +1 -0
  162. package/src/authorization/authorizer.ts +11 -4
  163. package/src/authorization/model/__tests__/registry.test.ts +8 -1
  164. package/src/authorization/model/index.ts +6 -4
  165. package/src/authorization/model/oauth_app.ts +3 -2
  166. package/src/authorization/model/platform_client.ts +43 -0
  167. package/src/boot/__tests__/auth-enabled-composition.test.ts +2 -1
  168. package/src/boot/__tests__/route-shadowing.test.ts +67 -0
  169. package/src/boot/compose.ts +90 -5
  170. package/src/boot/route-shadowing.ts +47 -0
  171. package/src/domain/iampolicy/__tests__/membership.test.ts +7 -0
  172. package/src/domain/iampolicy/membership.ts +8 -2
  173. package/src/domain/identityaccount/__tests__/platform-client-accounts.test.ts +124 -0
  174. package/src/domain/identityaccount/__tests__/resource-store.test.ts +1 -0
  175. package/src/domain/identityaccount/constants.ts +20 -0
  176. package/src/domain/identityaccount/controller.ts +16 -4
  177. package/src/domain/identityaccount/operator.ts +1 -0
  178. package/src/domain/identityaccount/provisioning.ts +17 -0
  179. package/src/domain/identityaccount/resource-store.ts +9 -5
  180. package/src/domain/identityaccount/steps.ts +58 -3
  181. package/src/domain/identityaccount/store-contract.ts +51 -0
  182. package/src/domain/identityaccount/store.ts +7 -5
  183. package/src/domain/platform/__tests__/platform.test.ts +7 -1
  184. package/src/domain/platform/controller.ts +12 -1
  185. package/src/domain/platformclient/__tests__/controller.test.ts +397 -0
  186. package/src/domain/platformclient/__tests__/credentials.test.ts +46 -0
  187. package/src/domain/platformclient/__tests__/mint.test.ts +415 -0
  188. package/src/domain/platformclient/__tests__/resource-store.test.ts +140 -0
  189. package/src/domain/platformclient/__tests__/system-managed.test.ts +101 -0
  190. package/src/domain/platformclient/__tests__/verifier-and-guard.test.ts +207 -0
  191. package/src/domain/platformclient/constants.ts +120 -0
  192. package/src/domain/platformclient/controller.ts +449 -0
  193. package/src/domain/platformclient/credentials.ts +68 -0
  194. package/src/domain/platformclient/mint.ts +412 -0
  195. package/src/domain/platformclient/origin-guard.ts +117 -0
  196. package/src/domain/platformclient/resource-store.ts +150 -0
  197. package/src/domain/platformclient/steps.ts +472 -0
  198. package/src/domain/platformclient/store-contract.ts +318 -0
  199. package/src/domain/platformclient/store.ts +63 -0
  200. package/src/domain/platformclient/system-managed.ts +115 -0
  201. package/src/domain/platformclient/token-controller.ts +49 -0
  202. package/src/domain/platformclient/verifier.ts +106 -0
  203. package/src/domain/workflow/__tests__/agent-call-references.test.ts +178 -27
  204. package/src/domain/workflow/agent-call-references.ts +95 -48
  205. package/src/domain/workflow/registry/data/task-kind-registry.json +2 -2
  206. package/src/encryption/key-manager.ts +87 -44
  207. package/src/extensions/__tests__/extension-composition.test.ts +3 -2
  208. package/src/extensions/__tests__/platform-client-composed.test.ts +288 -0
  209. package/src/extensions/__tests__/registry.test.ts +4 -1
  210. package/src/extensions/__tests__/tier-truthfulness.test.ts +8 -0
  211. package/src/extensions/caller-guards.ts +8 -3
  212. package/src/extensions/drivers.ts +37 -0
  213. package/src/extensions/guest-token-minting.ts +32 -0
  214. package/src/extensions/identity.ts +6 -4
  215. package/src/extensions/registry.ts +77 -3
  216. package/src/index.ts +55 -0
  217. package/src/pipeline/apiresource-labels.ts +10 -9
  218. package/src/pipeline/interceptors/auth.ts +27 -0
  219. package/src/pipeline/steps/__tests__/references.test.ts +37 -1
  220. package/src/pipeline/steps/build-update-state.ts +5 -2
  221. package/src/pipeline/steps/references.ts +49 -5
  222. package/src/platformtoken/__tests__/envelope.test.ts +277 -0
  223. package/src/platformtoken/__tests__/key-ring.test.ts +205 -0
  224. package/src/platformtoken/envelope.ts +276 -0
  225. package/src/platformtoken/key-ring.ts +266 -0
  226. package/src/runnerauth/__tests__/runner-subject-composed.test.ts +9 -3
@@ -1,40 +1,50 @@
1
1
  /**
2
2
  * Pins the workflow domain's collector for the reference rule
3
- * (../agent-call-references.ts): the strict `slug` / `org/slug` parser and
4
- * its refusal copy, the walk over top-level, nested and compensate tasks
5
- * in declaration order with the bare form resolved to the workflow's
6
- * organization, a config that does not decode skipped rather than
7
- * double-reported, and the step over a real store answering the rule's
8
- * verdicts — a missing agent with the rule's copy, another organization's
9
- * org-visible agent with the one cross-organization sentence, a
10
- * platform-visible one admitted.
3
+ * (../agent-call-references.ts): the classifier's four forms (a literal
4
+ * `org/slug`, a relative bare slug, a value holding `${` that is fixed
5
+ * only at run, anything else malformed) and the refusal copy; the walk
6
+ * over top-level, nested and compensate tasks in declaration order, the
7
+ * relative form filled with the workflow's organization and marked, a
8
+ * run-time value collected as nothing; a config that does not decode
9
+ * skipped rather than double-reported; the step over a real store
10
+ * answering the rule's verdicts — a missing agent with the rule's copy,
11
+ * another organization's org-visible agent with the one
12
+ * cross-organization sentence, a platform-visible one admitted, the floor
13
+ * for a literal, the floor capped at org for a bare slug, a run-time value
14
+ * saved with no agent behind it; and the workflow's escalation door
15
+ * applying the same cap when a workflow calling a bare slug is raised to
16
+ * platform.
11
17
  */
12
18
  import { create } from "@bufbuild/protobuf";
13
19
  import { Code, ConnectError } from "@connectrpc/connect";
14
20
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
15
21
 
16
22
  import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
23
+ import type { Workflow } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
17
24
  import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
18
25
  import { WorkflowTaskKind } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/enum_pb";
19
26
  import type { WorkflowSpec } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/spec_pb";
20
27
  import { WorkflowSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/spec_pb";
21
28
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
22
29
  import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
30
+ import { UpdateVisibilityInputSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
23
31
 
24
32
  import { RequestContext } from "../../../pipeline/request-context.js";
25
33
  import { testCallerIdentity } from "../../../pipeline/__tests__/support.js";
26
34
  import {
35
+ belowFloorMessage,
27
36
  missingReferencesMessage,
37
+ newGuardReferenceFloorOnEscalationStep,
28
38
  notAvailableReferenceMessage,
29
39
  referenceTargetKind,
30
40
  } from "../../../pipeline/steps/references.js";
31
41
  import type { SqliteStore } from "../../../store/sqlite/store.js";
32
42
  import { tempStore } from "../../../store/sqlite/__tests__/support.js";
33
43
  import {
44
+ classifyAgentReference,
34
45
  collectAgentCallReferences,
35
46
  malformedAgentReferenceMessage,
36
47
  newValidateAgentCallReferencesStep,
37
- parseAgentReference,
38
48
  } from "../agent-call-references.js";
39
49
 
40
50
  const V = ApiResourceVisibility;
@@ -52,24 +62,48 @@ function specOf(tasks: ReadonlyArray<Record<string, unknown>>): WorkflowSpec {
52
62
  return create(WorkflowSpecSchema, { tasks } as never);
53
63
  }
54
64
 
55
- describe("parseAgentReference", () => {
56
- it("accepts `slug` and `org/slug` and nothing else", () => {
57
- expect(parseAgentReference("reviewer")).toEqual({
58
- org: "",
65
+ /** The three shapes the runner evaluates, whole and embedded, as authors write them. */
66
+ const RUN_TIME_VALUES = [
67
+ "${.env_vars.TEAM_ORG}/assistant",
68
+ "${.secrets.AGENT_REF}",
69
+ '${ "acme/reviewer" }',
70
+ "${ .input.agent }",
71
+ "acme/${ .input.slug }",
72
+ "${ .input.org }/a/b",
73
+ ];
74
+
75
+ describe("classifyAgentReference", () => {
76
+ it("reads `org/slug` as a literal and a bare slug as relative", () => {
77
+ expect(classifyAgentReference("acme/reviewer")).toEqual({
78
+ kind: "literal",
79
+ org: "acme",
59
80
  slug: "reviewer",
60
81
  });
61
- expect(parseAgentReference("acme/reviewer")).toEqual({
62
- org: "acme",
82
+ expect(classifyAgentReference("reviewer")).toEqual({
83
+ kind: "relative",
63
84
  slug: "reviewer",
64
85
  });
86
+ });
87
+
88
+ it("reads any value holding `${` as fixed only at run, whatever its shape", () => {
89
+ for (const value of RUN_TIME_VALUES) {
90
+ expect(classifyAgentReference(value), value).toEqual({
91
+ kind: "run-time",
92
+ });
93
+ }
94
+ });
95
+
96
+ it("reads everything else as malformed", () => {
65
97
  for (const malformed of ["", "/reviewer", "acme/", "a/b/c", "//"]) {
66
- expect(parseAgentReference(malformed), malformed).toBeUndefined();
98
+ expect(classifyAgentReference(malformed), malformed).toEqual({
99
+ kind: "malformed",
100
+ });
67
101
  }
68
102
  });
69
103
  });
70
104
 
71
105
  describe("collectAgentCallReferences", () => {
72
- it("collects top-level, nested and compensate agent_call tasks in declaration order, the bare form resolved to the workflow's organization", () => {
106
+ it("collects top-level, nested and compensate agent_call tasks in declaration order, a bare slug filled with the workflow's organization and marked relative", () => {
73
107
  const spec = specOf([
74
108
  agentCall("first", "reviewer"),
75
109
  {
@@ -96,9 +130,29 @@ describe("collectAgentCallReferences", () => {
96
130
  },
97
131
  ]);
98
132
  expect(collectAgentCallReferences(spec, "acme")).toEqual([
99
- { kind: ApiResourceKind.agent, org: "acme", slug: "reviewer" },
133
+ {
134
+ kind: ApiResourceKind.agent,
135
+ org: "acme",
136
+ slug: "reviewer",
137
+ resolvesIn: "running-organization",
138
+ },
139
+ { kind: ApiResourceKind.agent, org: "globex", slug: "summarizer" },
140
+ {
141
+ kind: ApiResourceKind.agent,
142
+ org: "acme",
143
+ slug: "cleaner",
144
+ resolvesIn: "running-organization",
145
+ },
146
+ ]);
147
+ });
148
+
149
+ it("collects nothing for a value fixed only at run, beside the references it does collect", () => {
150
+ const spec = specOf([
151
+ ...RUN_TIME_VALUES.map((value, i) => agentCall(`dynamic_${i}`, value)),
152
+ agentCall("fixed", "globex/summarizer"),
153
+ ]);
154
+ expect(collectAgentCallReferences(spec, "acme")).toEqual([
100
155
  { kind: ApiResourceKind.agent, org: "globex", slug: "summarizer" },
101
- { kind: ApiResourceKind.agent, org: "acme", slug: "cleaner" },
102
156
  ]);
103
157
  });
104
158
 
@@ -162,16 +216,19 @@ describe("ValidateAgentCallReferences over a store", () => {
162
216
  );
163
217
  }
164
218
 
165
- function workflowCalling(...agents: string[]) {
219
+ function workflowCalling(
220
+ visibility: ApiResourceVisibility,
221
+ ...agents: string[]
222
+ ): Workflow {
166
223
  return create(WorkflowSchema, {
167
224
  apiVersion: "agentic.stigmer.ai/v1",
168
225
  kind: "Workflow",
169
- metadata: { name: "Pipeline", org: "acme", visibility: V.visibility_org },
226
+ metadata: { id: "wfl_1", name: "Pipeline", org: "acme", visibility },
170
227
  spec: specOf(agents.map((agent, i) => agentCall(`call_${i}`, agent))),
171
228
  });
172
229
  }
173
230
 
174
- async function run(workflow: ReturnType<typeof workflowCalling>) {
231
+ async function run(workflow: Workflow): Promise<unknown> {
175
232
  const ctx = new RequestContext(
176
233
  WorkflowSchema,
177
234
  workflow,
@@ -187,21 +244,27 @@ describe("ValidateAgentCallReferences over a store", () => {
187
244
  }
188
245
 
189
246
  it("a missing agent is refused with the rule's copy; an existing one passes", async () => {
190
- const error = await run(workflowCalling("reviewer"));
247
+ const error = await run(workflowCalling(V.visibility_org, "reviewer"));
191
248
  expect(error).toBeInstanceOf(ConnectError);
192
249
  expect((error as ConnectError).code).toBe(Code.FailedPrecondition);
193
250
  expect((error as ConnectError).rawMessage).toBe(
194
251
  missingReferencesMessage(AGENT, [{ slug: "reviewer", org: "acme" }]),
195
252
  );
196
253
  await seedAgent("agt_1", "acme", "reviewer", V.visibility_org);
197
- expect(await run(workflowCalling("reviewer"))).toBeUndefined();
254
+ expect(
255
+ await run(workflowCalling(V.visibility_org, "reviewer")),
256
+ ).toBeUndefined();
198
257
  });
199
258
 
200
259
  it("another organization's agent is admitted only at platform visibility, with the one sentence otherwise", async () => {
201
260
  await seedAgent("agt_shared", "globex", "shared", V.visibility_platform);
202
261
  await seedAgent("agt_internal", "globex", "internal", V.visibility_org);
203
- expect(await run(workflowCalling("globex/shared"))).toBeUndefined();
204
- const error = await run(workflowCalling("globex/internal"));
262
+ expect(
263
+ await run(workflowCalling(V.visibility_org, "globex/shared")),
264
+ ).toBeUndefined();
265
+ const error = await run(
266
+ workflowCalling(V.visibility_org, "globex/internal"),
267
+ );
205
268
  expect((error as ConnectError).rawMessage).toBe(
206
269
  notAvailableReferenceMessage(AGENT, {
207
270
  kind: ApiResourceKind.agent,
@@ -213,10 +276,98 @@ describe("ValidateAgentCallReferences over a store", () => {
213
276
 
214
277
  it("the floor holds for the agent a workflow calls: an org-visible workflow may not call a private agent", async () => {
215
278
  await seedAgent("agt_mine", "acme", "mine", V.visibility_private);
216
- const error = await run(workflowCalling("mine"));
279
+ const error = await run(workflowCalling(V.visibility_org, "mine"));
217
280
  expect((error as ConnectError).code).toBe(Code.FailedPrecondition);
218
281
  expect((error as ConnectError).rawMessage).toContain(
219
282
  "referenced agent 'acme/mine' is visibility_private while this resource is visibility_org",
220
283
  );
221
284
  });
285
+
286
+ it("a platform-visible workflow may call its organization's org-visible agent by bare slug, never by literal, and never a private one", async () => {
287
+ await seedAgent("agt_team", "acme", "team", V.visibility_org);
288
+ await seedAgent("agt_mine", "acme", "mine", V.visibility_private);
289
+ expect(
290
+ await run(workflowCalling(V.visibility_platform, "team")),
291
+ ).toBeUndefined();
292
+ // The literal names a fixed row that every organization's runs read.
293
+ const literal = await run(
294
+ workflowCalling(V.visibility_platform, "acme/team"),
295
+ );
296
+ expect((literal as ConnectError).rawMessage).toContain(
297
+ "referenced agent 'acme/team' is visibility_org while this resource is visibility_platform",
298
+ );
299
+ const below = await run(workflowCalling(V.visibility_platform, "mine"));
300
+ expect((below as ConnectError).rawMessage).toContain(
301
+ "referenced agent 'acme/mine' is visibility_private while this resource is visibility_platform",
302
+ );
303
+ });
304
+
305
+ it("a value fixed only at run saves with no agent behind it, at every level", async () => {
306
+ for (const visibility of [
307
+ V.visibility_private,
308
+ V.visibility_org,
309
+ V.visibility_platform,
310
+ ]) {
311
+ expect(
312
+ await run(workflowCalling(visibility, ...RUN_TIME_VALUES)),
313
+ V[visibility],
314
+ ).toBeUndefined();
315
+ }
316
+ });
317
+
318
+ describe("the workflow's escalation door", () => {
319
+ const TARGET = "loadedWorkflow";
320
+
321
+ async function escalate(
322
+ stored: Workflow,
323
+ requested: ApiResourceVisibility,
324
+ ): Promise<unknown> {
325
+ const ctx = new RequestContext(
326
+ UpdateVisibilityInputSchema,
327
+ create(UpdateVisibilityInputSchema, {
328
+ resourceId: "wfl_1",
329
+ visibility: requested,
330
+ }),
331
+ testCallerIdentity(),
332
+ ApiResourceKind.workflow,
333
+ );
334
+ ctx.set(TARGET, stored);
335
+ try {
336
+ await newGuardReferenceFloorOnEscalationStep(store, TARGET, [
337
+ (row) =>
338
+ collectAgentCallReferences(
339
+ (row as Workflow).spec,
340
+ (row as Workflow).metadata?.org ?? "",
341
+ ),
342
+ ]).execute(ctx);
343
+ return undefined;
344
+ } catch (error) {
345
+ return error;
346
+ }
347
+ }
348
+
349
+ it("raising a workflow that calls a bare slug to platform asks the capped floor", async () => {
350
+ await seedAgent("agt_team", "acme", "team", V.visibility_org);
351
+ await seedAgent("agt_mine", "acme", "mine", V.visibility_private);
352
+ expect(
353
+ await escalate(
354
+ workflowCalling(V.visibility_org, "team"),
355
+ V.visibility_platform,
356
+ ),
357
+ ).toBeUndefined();
358
+ const error = await escalate(
359
+ workflowCalling(V.visibility_private, "mine"),
360
+ V.visibility_platform,
361
+ );
362
+ expect((error as ConnectError).code).toBe(Code.FailedPrecondition);
363
+ expect((error as ConnectError).rawMessage).toBe(
364
+ belowFloorMessage(
365
+ AGENT,
366
+ { kind: ApiResourceKind.agent, org: "acme", slug: "mine" },
367
+ V.visibility_private,
368
+ V.visibility_platform,
369
+ ),
370
+ );
371
+ });
372
+ });
222
373
  });
@@ -1,31 +1,50 @@
1
1
  /**
2
2
  * ValidateAgentCallReferences — the second collector for the reference rule
3
3
  * (pipeline/steps/references.ts): a workflow's `agent_call` tasks name their
4
- * agent as a STRING, `"slug"` or `"org/slug"`, inside a `google.protobuf.Struct`
5
- * config the generic spec walker cannot see, so the rule would never meet
6
- * them. This step parses each task's config through the converter's own
7
- * strict decoder (`unmarshalTaskConfig`, the one reader of a task config),
8
- * walks nested control-flow tasks exactly as the constraint validator does,
9
- * turns every `agent` string into a reference with the workflow as parent,
10
- * and asks the rule's one function — same clauses, same copy. It lives in
11
- * the workflow domain and not inside `InProcessValidator` because the
12
- * validator is pure over the spec by design and a rule that reads the
4
+ * agent as a STRING inside a `google.protobuf.Struct` config the generic
5
+ * spec walker cannot see, so the rule would never meet them. This step
6
+ * parses each task's config through the converter's own strict decoder
7
+ * (`unmarshalTaskConfig`, the one reader of a task config), walks nested
8
+ * control-flow tasks exactly as the constraint validator does, turns every
9
+ * `agent` string the write can judge into a reference with the workflow as
10
+ * parent, and asks the rule's one function — same clauses, same copy. It
11
+ * lives in the workflow domain and not inside `InProcessValidator` because
12
+ * the validator is pure over the spec by design and a rule that reads the
13
13
  * target's row cannot be.
14
14
  *
15
- * The parser here is STRICT: exactly `slug` or `org/slug`, both segments
16
- * non-empty, anything else refused at write naming the task. Two lenient
17
- * readers of the same string exist downstream and disagree with each other
18
- * (the runner's `parseAgentReference` in activities/call-agent.ts splits on
19
- * the first `/` and drops a third segment; the CLI's `parseReference` in
20
- * client-apps/cli/src/resources/reference.ts refuses a leading slash);
21
- * refusing every malformed shape here means neither ever meets one. The
22
- * three cannot share code — the packages share only the protos — so the
23
- * execution conformance suite is where their agreement is proven.
15
+ * The string has three forms (the `agent` field's contract in
16
+ * agent_call.proto), told apart by `classifyAgentReference`:
17
+ *
18
+ * - `org/slug`, a LITERAL: judged now, like any stored reference;
19
+ * - a bare `slug`, RELATIVE: the runner resolves it in the organization
20
+ * the workflow runs in (`__stigmer_org_id`, the execution's), which is
21
+ * not the workflow's when a person runs it from another organization by
22
+ * id. It is judged now in the workflow's own organization with its
23
+ * floor capped at org (references.ts, "RELATIVE");
24
+ * - a value holding `${`, fixed only AT RUN: the runner's jq phase
25
+ * (`${ expr }`, whole or embedded) or its activity phase
26
+ * (`${.env_vars.KEY}`, `${.secrets.KEY}`) produces the reference when
27
+ * the task runs. The write collects nothing for it; the run reads the
28
+ * resolved agent as the person who ran the workflow, as it reads every
29
+ * id-bound binding. The slug grammar (metadata.proto) can never contain
30
+ * `${`, so the test is exact. It deliberately does not recognise only
31
+ * the runner's shapes: that would be a second copy of the runner's
32
+ * grammar in a package that shares only the protos with it, and a
33
+ * mistyped expression already fails at run with the read's not-found.
34
+ *
35
+ * Anything else is refused at write naming the task. The runner's own
36
+ * parser (`parseAgentReference` in runner/src/activities/call-agent.ts) is
37
+ * lenient — it splits on the first `/` and drops a third segment — and the
38
+ * CLI's `parseReference` (client-apps/cli/src/resources/reference.ts)
39
+ * refuses a leading slash; refusing every malformed literal here means
40
+ * neither ever meets one. The three cannot share code; the conformance
41
+ * suites are where their agreement is proven.
24
42
  *
25
43
  * Runs after NormalizeReferences and ValidateReferences on the workflow
26
- * create and update chains, so the org the walk sees is the workflow's own
27
- * for a bare slug. Proven by __tests__/agent-call-references.test.ts and
28
- * the workflow conformance suite's reference arm.
44
+ * create and update chains, and feeds the workflow's escalation door
45
+ * (controller.ts). Proven by __tests__/agent-call-references.test.ts, the
46
+ * workflow conformance suite's agent_call reference arm, and the
47
+ * runner-as-subject execution arm for a reference fixed only at run.
29
48
  */
30
49
  import type { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
31
50
  import type { AgentCallTaskConfig } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/tasks/agent_call_pb";
@@ -49,7 +68,7 @@ import { metadataOf } from "../../pipeline/steps/shapes.js";
49
68
  import { unmarshalTaskConfig } from "./converter/unmarshal.js";
50
69
  import { nestedTasks } from "./validation/task-config-constraints.js";
51
70
 
52
- /** The one sentence for an `agent` string that is neither `slug` nor `org/slug`. */
71
+ /** The one sentence for an `agent` string that is none of the three forms. */
53
72
  export function malformedAgentReferenceMessage(
54
73
  taskName: string,
55
74
  agent: string,
@@ -57,29 +76,38 @@ export function malformedAgentReferenceMessage(
57
76
  return `task '${taskName}': agent '${agent}' is not a reference; write 'slug' for an agent of this organization or 'org/slug' for another organization's.`;
58
77
  }
59
78
 
60
- /**
61
- * `slug` or `org/slug` → the reference, with `org` empty for the bare form
62
- * (the caller fills the parent's). Returns undefined for any other shape.
63
- */
64
- export function parseAgentReference(
65
- agent: string,
66
- ): { readonly org: string; readonly slug: string } | undefined {
79
+ /** What an `agent` string is before the rule reads it (the module header). */
80
+ export type AgentReferenceForm =
81
+ | { readonly kind: "literal"; readonly org: string; readonly slug: string }
82
+ | { readonly kind: "relative"; readonly slug: string }
83
+ | { readonly kind: "run-time" }
84
+ | { readonly kind: "malformed" };
85
+
86
+ /** Opens every runtime expression and placeholder the runner evaluates; never part of a slug. */
87
+ const RUNTIME_EXPRESSION_MARKER = "${";
88
+
89
+ export function classifyAgentReference(agent: string): AgentReferenceForm {
90
+ if (agent.includes(RUNTIME_EXPRESSION_MARKER)) {
91
+ return { kind: "run-time" };
92
+ }
67
93
  const parts = agent.split("/");
68
- if (parts.length === 1) {
69
- return parts[0] === "" ? undefined : { org: "", slug: parts[0]! };
94
+ if (parts.length === 1 && parts[0] !== "") {
95
+ return { kind: "relative", slug: parts[0]! };
70
96
  }
71
97
  if (parts.length === 2 && parts[0] !== "" && parts[1] !== "") {
72
- return { org: parts[0]!, slug: parts[1]! };
98
+ return { kind: "literal", org: parts[0]!, slug: parts[1]! };
73
99
  }
74
- return undefined;
100
+ return { kind: "malformed" };
75
101
  }
76
102
 
77
103
  /**
78
- * Every `agent_call` task's agent as a reference, nested tasks included in
79
- * declaration order, the bare form resolved to `parentOrg`. Throws
80
- * INVALID_ARGUMENT on the first malformed string. A config that does not
81
- * decode is skipped: the spec validation step has already refused the
82
- * workflow for it, and this collector never double-reports.
104
+ * Every `agent_call` task's agent the write can judge, as a reference,
105
+ * nested tasks included in declaration order: a literal as written, a
106
+ * relative slug in `parentOrg` and marked relative, a value fixed only at
107
+ * run not at all. Throws INVALID_ARGUMENT on the first malformed string. A
108
+ * config that does not decode is skipped: the spec validation step has
109
+ * already refused the workflow for it, and this collector never
110
+ * double-reports.
83
111
  */
84
112
  export function collectAgentCallReferences(
85
113
  spec: WorkflowSpec | undefined,
@@ -100,17 +128,36 @@ export function collectAgentCallReferences(
100
128
  if (config !== undefined) {
101
129
  if (task.kind === WorkflowTaskKind.agent_call) {
102
130
  const agent = (config as AgentCallTaskConfig).agent;
103
- const parsed = parseAgentReference(agent);
104
- if (parsed === undefined) {
105
- throw invalidArgumentError(
106
- malformedAgentReferenceMessage(task.name, agent),
107
- );
131
+ const form = classifyAgentReference(agent);
132
+ switch (form.kind) {
133
+ case "literal":
134
+ refs.push({
135
+ kind: ApiResourceKind.agent,
136
+ org: form.org,
137
+ slug: form.slug,
138
+ });
139
+ break;
140
+ case "relative":
141
+ refs.push({
142
+ kind: ApiResourceKind.agent,
143
+ org: parentOrg,
144
+ slug: form.slug,
145
+ resolvesIn: "running-organization",
146
+ });
147
+ break;
148
+ case "run-time":
149
+ break;
150
+ case "malformed":
151
+ throw invalidArgumentError(
152
+ malformedAgentReferenceMessage(task.name, agent),
153
+ );
154
+ default: {
155
+ const exhaustive: never = form;
156
+ throw new Error(
157
+ `unknown agent reference form: ${JSON.stringify(exhaustive)}`,
158
+ );
159
+ }
108
160
  }
109
- refs.push({
110
- kind: ApiResourceKind.agent,
111
- org: parsed.org === "" ? parentOrg : parsed.org,
112
- slug: parsed.slug,
113
- });
114
161
  }
115
162
  for (const nested of nestedTasks(config)) {
116
163
  visit(nested);
@@ -1362,7 +1362,7 @@
1362
1362
  {
1363
1363
  "name": "agent",
1364
1364
  "displayName": "Agent",
1365
- "description": "Agent reference in \"org/slug\" or \"slug\" format. - \"slug\" only: uses the workflow's organization - \"org/slug\": explicit organization reference Examples: \"code-reviewer\", \"stigmer/code-reviewer\", \"acme/data-analyst\" Required field.",
1365
+ "description": "Agent to invoke, as \"slug\", \"org/slug\", or a value holding a runtime expression. - \"slug\": an agent of the organization the workflow runs in; checked when the workflow is saved, against the workflow's own organization - \"org/slug\": that organization's agent; checked when the workflow is saved, and another organization's agent must be shared at platform visibility - a value holding \"${ ... }\", \"${.env_vars.KEY}\" or \"${.secrets.KEY}\": resolved to \"slug\" or \"org/slug\" when the task runs, and read as the person who ran the workflow Examples: \"code-reviewer\", \"acme/data-analyst\", \"${.env_vars.TEAM_ORG}/assistant\" Required field.",
1366
1366
  "type": "string",
1367
1367
  "required": true,
1368
1368
  "groupId": "core",
@@ -1470,7 +1470,7 @@
1470
1470
  "additionalProperties": false,
1471
1471
  "properties": {
1472
1472
  "agent": {
1473
- "description": "Agent reference in \"org/slug\" or \"slug\" format. - \"slug\" only: uses the workflow's organization - \"org/slug\": explicit organization reference Examples: \"code-reviewer\", \"stigmer/code-reviewer\", \"acme/data-analyst\" Required field.",
1473
+ "description": "Agent to invoke, as \"slug\", \"org/slug\", or a value holding a runtime expression. - \"slug\": an agent of the organization the workflow runs in; checked when the workflow is saved, against the workflow's own organization - \"org/slug\": that organization's agent; checked when the workflow is saved, and another organization's agent must be shared at platform visibility - a value holding \"${ ... }\", \"${.env_vars.KEY}\" or \"${.secrets.KEY}\": resolved to \"slug\" or \"org/slug\" when the task runs, and read as the person who ran the workflow Examples: \"code-reviewer\", \"acme/data-analyst\", \"${.env_vars.TEAM_ORG}/assistant\" Required field.",
1474
1474
  "maxLength": 127,
1475
1475
  "minLength": 1,
1476
1476
  "type": "string"