@stigmer/server 3.14.0 → 3.15.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 (178) hide show
  1. package/dist/boot/compose.d.ts +8 -1
  2. package/dist/boot/compose.d.ts.map +1 -1
  3. package/dist/boot/compose.js +98 -12
  4. package/dist/boot/compose.js.map +1 -1
  5. package/dist/domain/agentexecution/constants.d.ts +16 -0
  6. package/dist/domain/agentexecution/constants.d.ts.map +1 -1
  7. package/dist/domain/agentexecution/constants.js +22 -0
  8. package/dist/domain/agentexecution/constants.js.map +1 -1
  9. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  10. package/dist/domain/agentexecution/controller.js +10 -3
  11. package/dist/domain/agentexecution/controller.js.map +1 -1
  12. package/dist/domain/agentexecution/run-target.d.ts +26 -0
  13. package/dist/domain/agentexecution/run-target.d.ts.map +1 -0
  14. package/dist/domain/agentexecution/run-target.js +31 -0
  15. package/dist/domain/agentexecution/run-target.js.map +1 -0
  16. package/dist/domain/apikey/verifier.d.ts +8 -1
  17. package/dist/domain/apikey/verifier.d.ts.map +1 -1
  18. package/dist/domain/apikey/verifier.js +14 -2
  19. package/dist/domain/apikey/verifier.js.map +1 -1
  20. package/dist/domain/identityaccount/constants.d.ts +66 -0
  21. package/dist/domain/identityaccount/constants.d.ts.map +1 -0
  22. package/dist/domain/identityaccount/constants.js +153 -0
  23. package/dist/domain/identityaccount/constants.js.map +1 -0
  24. package/dist/domain/identityaccount/controller.d.ts +34 -0
  25. package/dist/domain/identityaccount/controller.d.ts.map +1 -0
  26. package/dist/domain/identityaccount/controller.js +342 -0
  27. package/dist/domain/identityaccount/controller.js.map +1 -0
  28. package/dist/domain/identityaccount/operator.d.ts +14 -0
  29. package/dist/domain/identityaccount/operator.d.ts.map +1 -0
  30. package/dist/domain/identityaccount/operator.js +67 -0
  31. package/dist/domain/identityaccount/operator.js.map +1 -0
  32. package/dist/domain/identityaccount/provisioning.d.ts +52 -0
  33. package/dist/domain/identityaccount/provisioning.d.ts.map +1 -0
  34. package/dist/domain/identityaccount/provisioning.js +114 -0
  35. package/dist/domain/identityaccount/provisioning.js.map +1 -0
  36. package/dist/domain/identityaccount/resolve.d.ts +36 -0
  37. package/dist/domain/identityaccount/resolve.d.ts.map +1 -0
  38. package/dist/domain/identityaccount/resolve.js +12 -0
  39. package/dist/domain/identityaccount/resolve.js.map +1 -0
  40. package/dist/domain/identityaccount/resource-store.d.ts +4 -0
  41. package/dist/domain/identityaccount/resource-store.d.ts.map +1 -0
  42. package/dist/domain/identityaccount/resource-store.js +131 -0
  43. package/dist/domain/identityaccount/resource-store.js.map +1 -0
  44. package/dist/domain/identityaccount/steps.d.ts +79 -0
  45. package/dist/domain/identityaccount/steps.d.ts.map +1 -0
  46. package/dist/domain/identityaccount/steps.js +291 -0
  47. package/dist/domain/identityaccount/steps.js.map +1 -0
  48. package/dist/domain/identityaccount/store-contract.d.ts +22 -0
  49. package/dist/domain/identityaccount/store-contract.d.ts.map +1 -0
  50. package/dist/domain/identityaccount/store-contract.js +307 -0
  51. package/dist/domain/identityaccount/store-contract.js.map +1 -0
  52. package/dist/domain/identityaccount/store.d.ts +66 -0
  53. package/dist/domain/identityaccount/store.d.ts.map +1 -0
  54. package/dist/domain/identityaccount/store.js +13 -0
  55. package/dist/domain/identityaccount/store.js.map +1 -0
  56. package/dist/domain/session/constants.d.ts +15 -0
  57. package/dist/domain/session/constants.d.ts.map +1 -0
  58. package/dist/domain/session/constants.js +17 -0
  59. package/dist/domain/session/constants.js.map +1 -0
  60. package/dist/domain/session/controller.d.ts.map +1 -1
  61. package/dist/domain/session/controller.js +10 -0
  62. package/dist/domain/session/controller.js.map +1 -1
  63. package/dist/domain/session/run-target.d.ts +15 -0
  64. package/dist/domain/session/run-target.d.ts.map +1 -0
  65. package/dist/domain/session/run-target.js +14 -0
  66. package/dist/domain/session/run-target.js.map +1 -0
  67. package/dist/domain/workflowexecution/constants.d.ts +9 -0
  68. package/dist/domain/workflowexecution/constants.d.ts.map +1 -1
  69. package/dist/domain/workflowexecution/constants.js +13 -0
  70. package/dist/domain/workflowexecution/constants.js.map +1 -1
  71. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  72. package/dist/domain/workflowexecution/controller.js +8 -0
  73. package/dist/domain/workflowexecution/controller.js.map +1 -1
  74. package/dist/domain/workflowexecution/run-target.d.ts +18 -0
  75. package/dist/domain/workflowexecution/run-target.d.ts.map +1 -0
  76. package/dist/domain/workflowexecution/run-target.js +23 -0
  77. package/dist/domain/workflowexecution/run-target.js.map +1 -0
  78. package/dist/extensions/drivers.d.ts +26 -0
  79. package/dist/extensions/drivers.d.ts.map +1 -1
  80. package/dist/extensions/gate-slots.d.ts +16 -1
  81. package/dist/extensions/gate-slots.d.ts.map +1 -1
  82. package/dist/extensions/gate-slots.js +1 -0
  83. package/dist/extensions/gate-slots.js.map +1 -1
  84. package/dist/extensions/identity-federation.d.ts +36 -0
  85. package/dist/extensions/identity-federation.d.ts.map +1 -0
  86. package/dist/extensions/identity-federation.js +2 -0
  87. package/dist/extensions/identity-federation.js.map +1 -0
  88. package/dist/extensions/registry.d.ts +18 -1
  89. package/dist/extensions/registry.d.ts.map +1 -1
  90. package/dist/extensions/registry.js +20 -0
  91. package/dist/extensions/registry.js.map +1 -1
  92. package/dist/identity/oidc-discovery.d.ts +36 -0
  93. package/dist/identity/oidc-discovery.d.ts.map +1 -0
  94. package/dist/identity/oidc-discovery.js +60 -0
  95. package/dist/identity/oidc-discovery.js.map +1 -0
  96. package/dist/identity/oidc-userinfo.d.ts +30 -0
  97. package/dist/identity/oidc-userinfo.d.ts.map +1 -0
  98. package/dist/identity/oidc-userinfo.js +28 -0
  99. package/dist/identity/oidc-userinfo.js.map +1 -0
  100. package/dist/identity/oidc-verifier.d.ts +6 -0
  101. package/dist/identity/oidc-verifier.d.ts.map +1 -1
  102. package/dist/identity/oidc-verifier.js +35 -34
  103. package/dist/identity/oidc-verifier.js.map +1 -1
  104. package/dist/index.d.ts +8 -0
  105. package/dist/index.d.ts.map +1 -1
  106. package/dist/index.js +10 -0
  107. package/dist/index.js.map +1 -1
  108. package/dist/pipeline/interceptors/auth.d.ts +10 -0
  109. package/dist/pipeline/interceptors/auth.d.ts.map +1 -1
  110. package/dist/pipeline/interceptors/auth.js +9 -1
  111. package/dist/pipeline/interceptors/auth.js.map +1 -1
  112. package/dist/pipeline/steps/authorize-run-target.d.ts +105 -0
  113. package/dist/pipeline/steps/authorize-run-target.d.ts.map +1 -0
  114. package/dist/pipeline/steps/authorize-run-target.js +61 -0
  115. package/dist/pipeline/steps/authorize-run-target.js.map +1 -0
  116. package/dist/query/search/criteria.d.ts +10 -3
  117. package/dist/query/search/criteria.d.ts.map +1 -1
  118. package/dist/query/search/criteria.js +11 -4
  119. package/dist/query/search/criteria.js.map +1 -1
  120. package/package.json +4 -4
  121. package/src/boot/__tests__/auth-enabled-composition.test.ts +226 -2
  122. package/src/boot/compose.ts +118 -13
  123. package/src/domain/agentexecution/__tests__/run-gate.test.ts +245 -0
  124. package/src/domain/agentexecution/__tests__/run-target.test.ts +117 -0
  125. package/src/domain/agentexecution/constants.ts +25 -0
  126. package/src/domain/agentexecution/controller.ts +10 -3
  127. package/src/domain/agentexecution/run-target.ts +64 -0
  128. package/src/domain/apikey/__tests__/verifier.test.ts +146 -22
  129. package/src/domain/apikey/verifier.ts +24 -2
  130. package/src/domain/identityaccount/__tests__/constants.test.ts +155 -0
  131. package/src/domain/identityaccount/__tests__/identityaccount.test.ts +484 -0
  132. package/src/domain/identityaccount/__tests__/operator.test.ts +180 -0
  133. package/src/domain/identityaccount/__tests__/provisioning.test.ts +281 -0
  134. package/src/domain/identityaccount/__tests__/resolve.test.ts +114 -0
  135. package/src/domain/identityaccount/__tests__/resource-store.test.ts +216 -0
  136. package/src/domain/identityaccount/__tests__/support.ts +58 -0
  137. package/src/domain/identityaccount/constants.ts +178 -0
  138. package/src/domain/identityaccount/controller.ts +667 -0
  139. package/src/domain/identityaccount/operator.ts +94 -0
  140. package/src/domain/identityaccount/provisioning.ts +182 -0
  141. package/src/domain/identityaccount/resolve.ts +52 -0
  142. package/src/domain/identityaccount/resource-store.ts +157 -0
  143. package/src/domain/identityaccount/steps.ts +377 -0
  144. package/src/domain/identityaccount/store-contract.ts +471 -0
  145. package/src/domain/identityaccount/store.ts +70 -0
  146. package/src/domain/session/__tests__/run-gate.test.ts +219 -0
  147. package/src/domain/session/__tests__/run-target.test.ts +43 -0
  148. package/src/domain/session/constants.ts +17 -0
  149. package/src/domain/session/controller.ts +10 -0
  150. package/src/domain/session/run-target.ts +30 -0
  151. package/src/domain/workflowexecution/__tests__/run-gate.test.ts +200 -0
  152. package/src/domain/workflowexecution/__tests__/run-target.test.ts +80 -0
  153. package/src/domain/workflowexecution/constants.ts +17 -0
  154. package/src/domain/workflowexecution/controller.ts +10 -0
  155. package/src/domain/workflowexecution/run-target.ts +47 -0
  156. package/src/extensions/__tests__/extension-composition.test.ts +49 -0
  157. package/src/extensions/__tests__/identity-account-composed.test.ts +803 -0
  158. package/src/extensions/__tests__/identity-account-points.test.ts +129 -0
  159. package/src/extensions/__tests__/registry.test.ts +4 -0
  160. package/src/extensions/__tests__/tier-truthfulness.test.ts +299 -0
  161. package/src/extensions/drivers.ts +26 -0
  162. package/src/extensions/gate-slots.ts +16 -0
  163. package/src/extensions/identity-federation.ts +58 -0
  164. package/src/extensions/registry.ts +44 -1
  165. package/src/identity/__tests__/oidc-discovery.test.ts +122 -0
  166. package/src/identity/__tests__/oidc-userinfo.test.ts +103 -0
  167. package/src/identity/__tests__/oidc-verifier.test.ts +173 -3
  168. package/src/identity/oidc-discovery.ts +84 -0
  169. package/src/identity/oidc-userinfo.ts +64 -0
  170. package/src/identity/oidc-verifier.ts +42 -45
  171. package/src/index.ts +26 -0
  172. package/src/pipeline/__tests__/support.ts +34 -0
  173. package/src/pipeline/interceptors/auth.ts +13 -1
  174. package/src/pipeline/steps/__tests__/authorize-annotation-completeness.test.ts +184 -0
  175. package/src/pipeline/steps/__tests__/authorize-run-target.test.ts +261 -0
  176. package/src/pipeline/steps/authorize-run-target.ts +148 -0
  177. package/src/query/search/__tests__/criteria.test.ts +18 -1
  178. package/src/query/search/criteria.ts +11 -4
@@ -0,0 +1,261 @@
1
+ /**
2
+ * Pins the AuthorizeRunTarget step's contract (P1 sp.run-gate, 2026-09-11):
3
+ * the step is a thin shell around authorizeResolvedResource — a resolver
4
+ * that answers no target makes NO check (the chain's own invariant guards
5
+ * own the shape-less arm); a resolved target reaches the Authorizer
6
+ * verbatim; deny carries the RESOLVER's copy; not-found answers the
7
+ * load-first NOT_FOUND; unavailable is INTERNAL; the internal caller class
8
+ * skips. Also pins isRunGateCheck: exactly the five (kind, permission)
9
+ * pairs the resolvers can produce, and nothing adjacent — the cloud's
10
+ * lane-admission decorator keys on this predicate, so a drift here would
11
+ * either deny a schedule fire or admit a lane to a check it never owned.
12
+ */
13
+ import { describe, expect, it } from "vitest";
14
+ import { create } from "@bufbuild/protobuf";
15
+ import { Code, ConnectError } from "@connectrpc/connect";
16
+
17
+ import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
18
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
19
+ import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
20
+
21
+ import type {
22
+ Authorizer,
23
+ AuthzCheck,
24
+ AuthzDecision,
25
+ } from "../../../extensions/authorizer.js";
26
+ import { testCallerIdentity } from "../../__tests__/support.js";
27
+ import { RequestContext } from "../../request-context.js";
28
+ import { AUTHORIZATION_UNAVAILABLE_MESSAGE } from "../authorize.js";
29
+ import {
30
+ RUN_GATE_CHECKS,
31
+ isRunGateCheck,
32
+ newAuthorizeRunTargetStep,
33
+ } from "../authorize-run-target.js";
34
+ import type { RunTarget } from "../authorize-run-target.js";
35
+
36
+ function fakeAuthorizer(decision: AuthzDecision): {
37
+ authorizer: Authorizer;
38
+ checks: AuthzCheck[];
39
+ } {
40
+ const checks: AuthzCheck[] = [];
41
+ return {
42
+ checks,
43
+ authorizer: {
44
+ authorize(_caller, check) {
45
+ checks.push(check);
46
+ return Promise.resolve(decision);
47
+ },
48
+ },
49
+ };
50
+ }
51
+
52
+ async function captureError(
53
+ run: () => void | Promise<void>,
54
+ ): Promise<ConnectError> {
55
+ try {
56
+ await run();
57
+ } catch (error) {
58
+ return ConnectError.from(error);
59
+ }
60
+ throw new Error("expected the step to reject");
61
+ }
62
+
63
+ const TARGET: RunTarget = {
64
+ ...RUN_GATE_CHECKS.agentInstance,
65
+ resourceId: "agi_01target",
66
+ deniedMessage: "unauthorized to run agent instance 'agi_01target'",
67
+ };
68
+
69
+ function sessionCtx(callerClass?: string) {
70
+ return new RequestContext(
71
+ SessionSchema,
72
+ create(SessionSchema, { metadata: { name: "s", org: "acme" } }),
73
+ callerClass === undefined
74
+ ? testCallerIdentity()
75
+ : testCallerIdentity({ callerClass }),
76
+ ApiResourceKind.session,
77
+ );
78
+ }
79
+
80
+ describe("AuthorizeRunTarget — the step shell", () => {
81
+ it("is named AuthorizeRunTarget in every chain (shared vocabulary)", () => {
82
+ const { authorizer } = fakeAuthorizer({ kind: "allow" });
83
+ const step = newAuthorizeRunTargetStep<typeof SessionSchema>(
84
+ authorizer,
85
+ () => TARGET,
86
+ );
87
+ expect(step.name).toBe("AuthorizeRunTarget");
88
+ });
89
+
90
+ it("makes NO check when the resolver answers no target", async () => {
91
+ const { authorizer, checks } = fakeAuthorizer({ kind: "deny", reason: "" });
92
+ const step = newAuthorizeRunTargetStep<typeof SessionSchema>(
93
+ authorizer,
94
+ () => undefined,
95
+ );
96
+ await step.execute(sessionCtx());
97
+ expect(checks).toEqual([]);
98
+ });
99
+
100
+ it("hands the resolved target to the Authorizer verbatim and proceeds on allow", async () => {
101
+ const { authorizer, checks } = fakeAuthorizer({ kind: "allow" });
102
+ const seen: unknown[] = [];
103
+ const step = newAuthorizeRunTargetStep<typeof SessionSchema>(
104
+ authorizer,
105
+ (record) => {
106
+ seen.push(record);
107
+ return TARGET;
108
+ },
109
+ );
110
+ const ctx = sessionCtx();
111
+ await step.execute(ctx);
112
+ expect(checks).toEqual([
113
+ {
114
+ permission: IamPermission.can_execute,
115
+ resourceKind: ApiResourceKind.agent_instance,
116
+ resourceId: "agi_01target",
117
+ },
118
+ ]);
119
+ // The resolver reads the record being BUILT (newState), where the
120
+ // chain's own resolution steps write, never the immutable input.
121
+ expect(seen).toEqual([ctx.newState]);
122
+ });
123
+
124
+ it("deny → PERMISSION_DENIED carrying the resolver's copy", async () => {
125
+ const { authorizer } = fakeAuthorizer({
126
+ kind: "deny",
127
+ reason: "fga said no",
128
+ });
129
+ const step = newAuthorizeRunTargetStep<typeof SessionSchema>(
130
+ authorizer,
131
+ () => TARGET,
132
+ );
133
+ const err = await captureError(() => step.execute(sessionCtx()));
134
+ expect(err.code).toBe(Code.PermissionDenied);
135
+ expect(err.rawMessage).toBe(TARGET.deniedMessage);
136
+ });
137
+
138
+ it("not-found → the load-first chain's NOT_FOUND naming the target", async () => {
139
+ const { authorizer } = fakeAuthorizer({ kind: "not-found" });
140
+ const step = newAuthorizeRunTargetStep<typeof SessionSchema>(
141
+ authorizer,
142
+ () => TARGET,
143
+ );
144
+ const err = await captureError(() => step.execute(sessionCtx()));
145
+ expect(err.code).toBe(Code.NotFound);
146
+ expect(err.rawMessage).toContain("agi_01target");
147
+ });
148
+
149
+ it("unavailable → INTERNAL, never a softened denial", async () => {
150
+ const { authorizer } = fakeAuthorizer({
151
+ kind: "unavailable",
152
+ cause: new Error("fga down"),
153
+ });
154
+ const step = newAuthorizeRunTargetStep<typeof SessionSchema>(
155
+ authorizer,
156
+ () => TARGET,
157
+ );
158
+ const err = await captureError(() => step.execute(sessionCtx()));
159
+ expect(err.code).toBe(Code.Internal);
160
+ expect(err.rawMessage).toBe(AUTHORIZATION_UNAVAILABLE_MESSAGE);
161
+ });
162
+
163
+ it("the internal caller class skips (the server acting as itself)", async () => {
164
+ const { authorizer, checks } = fakeAuthorizer({ kind: "deny", reason: "" });
165
+ const step = newAuthorizeRunTargetStep<typeof SessionSchema>(
166
+ authorizer,
167
+ () => TARGET,
168
+ );
169
+ await step.execute(sessionCtx("internal"));
170
+ expect(checks).toEqual([]);
171
+ });
172
+
173
+ it("every other caller class is checked — a lane is admitted by its Authorizer, never by this step", async () => {
174
+ for (const callerClass of [
175
+ "user",
176
+ "machine",
177
+ "runner",
178
+ "guest",
179
+ "schedule",
180
+ "workflow_sandbox",
181
+ ]) {
182
+ const { authorizer, checks } = fakeAuthorizer({ kind: "allow" });
183
+ const step = newAuthorizeRunTargetStep<typeof SessionSchema>(
184
+ authorizer,
185
+ () => TARGET,
186
+ );
187
+ await step.execute(sessionCtx(callerClass));
188
+ expect(checks, callerClass).toHaveLength(1);
189
+ }
190
+ });
191
+ });
192
+
193
+ describe("isRunGateCheck — the one definition of the run-gate check set", () => {
194
+ const pairs: ReadonlyArray<[string, ApiResourceKind, IamPermission]> = [
195
+ ["agent", ApiResourceKind.agent, IamPermission.can_execute],
196
+ [
197
+ "agent_instance",
198
+ ApiResourceKind.agent_instance,
199
+ IamPermission.can_execute,
200
+ ],
201
+ ["session", ApiResourceKind.session, IamPermission.can_create_execution_in],
202
+ ["workflow", ApiResourceKind.workflow, IamPermission.can_execute],
203
+ [
204
+ "workflow_instance",
205
+ ApiResourceKind.workflow_instance,
206
+ IamPermission.can_execute,
207
+ ],
208
+ ];
209
+
210
+ it.each(pairs)(
211
+ "%s is a run-gate check",
212
+ (_name, resourceKind, permission) => {
213
+ expect(
214
+ isRunGateCheck({ permission, resourceKind, resourceId: "x" }),
215
+ ).toBe(true);
216
+ },
217
+ );
218
+
219
+ it("names exactly five checks, one per entry the resolvers draw from", () => {
220
+ expect(Object.keys(RUN_GATE_CHECKS).sort()).toEqual(
221
+ [
222
+ "agent",
223
+ "agentInstance",
224
+ "session",
225
+ "workflow",
226
+ "workflowInstance",
227
+ ].sort(),
228
+ );
229
+ for (const [, resourceKind, permission] of pairs) {
230
+ expect(
231
+ Object.values(RUN_GATE_CHECKS).filter(
232
+ (c) => c.resourceKind === resourceKind && c.permission === permission,
233
+ ),
234
+ ).toHaveLength(1);
235
+ }
236
+ });
237
+
238
+ it.each([
239
+ ["session#can_view", ApiResourceKind.session, IamPermission.can_view],
240
+ ["agent#can_edit", ApiResourceKind.agent, IamPermission.can_edit],
241
+ ["agent#can_view", ApiResourceKind.agent, IamPermission.can_view],
242
+ [
243
+ "agent_execution#can_execute",
244
+ ApiResourceKind.agent_execution,
245
+ IamPermission.can_execute,
246
+ ],
247
+ [
248
+ "organization#can_create_session",
249
+ ApiResourceKind.organization,
250
+ IamPermission.can_create_session,
251
+ ],
252
+ ["session#can_execute", ApiResourceKind.session, IamPermission.can_execute],
253
+ ] as const)(
254
+ "%s is NOT a run-gate check",
255
+ (_name, resourceKind, permission) => {
256
+ expect(
257
+ isRunGateCheck({ permission, resourceKind, resourceId: "x" }),
258
+ ).toBe(false);
259
+ },
260
+ );
261
+ });
@@ -0,0 +1,148 @@
1
+ /**
2
+ * AuthorizeRunTarget — the run gate (P1 sp.run-gate, 2026-09-11;
3
+ * stigmer-cloud#709): may this caller RUN what this record targets?
4
+ *
5
+ * Every create that starts or continues a run names a target — a blueprint
6
+ * (`agent`, `workflow`), an instance (`agent_instance`, `workflow_instance`)
7
+ * or a conversation (`session`) — and until this step nothing asked whether
8
+ * the caller may use it: session-create authorized only the organization's
9
+ * `can_create_session`, agent- and workflow-execution create are
10
+ * `is_skip_authorization`, and `can_execute` was defined on every blueprint
11
+ * and instance type of the FGA model and checked nowhere. The invariant the
12
+ * model states ("you can run what you can read") is enforced here.
13
+ *
14
+ * One shared step, one domain resolver: the shape of IndexSearch (a shared
15
+ * step over a domain extractor). The resolver is a PURE function of the
16
+ * record being built (`ctx.newState`, where the chain's own resolution
17
+ * steps write) that names the target and the domain's byte-pinned deny
18
+ * copy; this step hands it to authorizeResolvedResource — the ratified
19
+ * mid-chain resolved-id form (DD-007; the ListVersions and listByChannel
20
+ * precedent) for lanes the position-1 annotation cannot express. A
21
+ * resolver that answers no target makes NO check: the chain's own
22
+ * invariant guards (EnsureSessionOrAgentResolved,
23
+ * ValidateWorkflowOrInstance) own the shape-less arm, and this step never
24
+ * second-guesses them.
25
+ *
26
+ * Position, in every chain: immediately after the step that guarantees the
27
+ * target reference exists — before EnsureEngineAvailable (a denied caller
28
+ * learns nothing about engine state), before every gate slot, before every
29
+ * side effect the chain owns. The chains' own convention, Authorize before
30
+ * Validate, is what places it there.
31
+ *
32
+ * Admission has one owner per lane. This step asks for EVERY caller but the
33
+ * `internal` class (authorizeResolvedResource's contract): a direct
34
+ * principal is admitted by the Authorizer; a runtime lane (a guest share, a
35
+ * channel, a schedule fire, a workflow sandbox's child run) is admitted by
36
+ * the gate step that vouches for it, and the edition's Authorizer says so
37
+ * for the lanes it mints — the OSS chain stays policy-free. RUN_GATE_CHECKS
38
+ * is the one definition of "the run-gate checks" that such an Authorizer
39
+ * keys on (isRunGateCheck, exported on the barrel); the resolvers draw their
40
+ * (kind, permission) pairs from the same table, so the predicate cannot
41
+ * drift from what the step actually asks.
42
+ */
43
+ import type { DescMessage, MessageShape } from "@bufbuild/protobuf";
44
+
45
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
46
+ import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
47
+
48
+ import type { Authorizer, AuthzCheck } from "../../extensions/authorizer.js";
49
+ import type { PipelineStep } from "../pipeline.js";
50
+ import type { RequestContext } from "../request-context.js";
51
+ import { authorizeResolvedResource } from "./authorize.js";
52
+
53
+ /** One (kind, permission) pair the run gate asks about. */
54
+ export interface RunGateCheck {
55
+ readonly permission: IamPermission;
56
+ readonly resourceKind: ApiResourceKind;
57
+ }
58
+
59
+ /**
60
+ * The run-gate check set — exactly the pairs a resolver can produce. Each
61
+ * is the FGA model's own relation for "may run this": `can_execute` on the
62
+ * blueprints and instances (`agent.fga` `can_execute: viewer`;
63
+ * `agent_instance.fga` `can_execute: can_view`; the workflow twins), and
64
+ * `can_create_execution_in` on a session (`session.fga`: adding a turn to a
65
+ * conversation is the session's own permission, not the agent's).
66
+ */
67
+ export const RUN_GATE_CHECKS = {
68
+ agent: {
69
+ permission: IamPermission.can_execute,
70
+ resourceKind: ApiResourceKind.agent,
71
+ },
72
+ agentInstance: {
73
+ permission: IamPermission.can_execute,
74
+ resourceKind: ApiResourceKind.agent_instance,
75
+ },
76
+ session: {
77
+ permission: IamPermission.can_create_execution_in,
78
+ resourceKind: ApiResourceKind.session,
79
+ },
80
+ workflow: {
81
+ permission: IamPermission.can_execute,
82
+ resourceKind: ApiResourceKind.workflow,
83
+ },
84
+ workflowInstance: {
85
+ permission: IamPermission.can_execute,
86
+ resourceKind: ApiResourceKind.workflow_instance,
87
+ },
88
+ } as const satisfies Record<string, RunGateCheck>;
89
+
90
+ const RUN_GATE_CHECK_LIST: ReadonlyArray<RunGateCheck> =
91
+ Object.values(RUN_GATE_CHECKS);
92
+
93
+ /**
94
+ * Whether `check` is one of the run gate's — the predicate an edition's
95
+ * Authorizer keys its lane admission on. Reads only the pair (an AuthzCheck
96
+ * is one); the resource id is the target's identity, not part of the
97
+ * question.
98
+ */
99
+ export function isRunGateCheck(check: RunGateCheck | AuthzCheck): boolean {
100
+ return RUN_GATE_CHECK_LIST.some(
101
+ (candidate) =>
102
+ candidate.permission === check.permission &&
103
+ candidate.resourceKind === check.resourceKind,
104
+ );
105
+ }
106
+
107
+ /**
108
+ * A resolved run target: the check plus the domain's byte-pinned deny copy
109
+ * for it (the lane's error_msg, in the position-1 annotation's terms).
110
+ */
111
+ export interface RunTarget extends RunGateCheck {
112
+ readonly resourceId: string;
113
+ readonly deniedMessage: string;
114
+ }
115
+
116
+ /**
117
+ * Names a record's run target, or `undefined` when the record names none
118
+ * (a state the chain's own guards refuse; never this step's arm). Pure over
119
+ * the record being built — no store, no clock, no caller.
120
+ */
121
+ export type RunTargetResolver<Desc extends DescMessage> = (
122
+ record: MessageShape<Desc>,
123
+ ) => RunTarget | undefined;
124
+
125
+ export function newAuthorizeRunTargetStep<Desc extends DescMessage>(
126
+ authorizer: Authorizer,
127
+ resolveRunTarget: RunTargetResolver<Desc>,
128
+ ): PipelineStep<Desc> {
129
+ return {
130
+ name: "AuthorizeRunTarget",
131
+ async execute(ctx: RequestContext<Desc>): Promise<void> {
132
+ const target = resolveRunTarget(ctx.newState);
133
+ if (target === undefined) {
134
+ return;
135
+ }
136
+ await authorizeResolvedResource(
137
+ authorizer,
138
+ ctx.callerIdentity,
139
+ {
140
+ permission: target.permission,
141
+ resourceKind: target.resourceKind,
142
+ resourceId: target.resourceId,
143
+ },
144
+ target.deniedMessage,
145
+ );
146
+ },
147
+ };
148
+ }
@@ -7,8 +7,12 @@
7
7
  */
8
8
  import { describe, expect, it } from "vitest";
9
9
 
10
- import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
10
+ import {
11
+ ApiResourceKind,
12
+ ResourceTier,
13
+ } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
11
14
 
15
+ import { getKindMeta } from "../../../pipeline/apiresource-meta.js";
12
16
  import {
13
17
  DEFAULT_PAGE_SIZE,
14
18
  MAX_PAGE_SIZE,
@@ -68,6 +72,19 @@ describe("searchIndexedKinds derivation (kind_meta)", () => {
68
72
  expect(kinds).not.toContain(ApiResourceKind.channel_app);
69
73
  expect(kinds).not.toContain(ApiResourceKind.memory);
70
74
  });
75
+
76
+ it("contains no kind above the open_source tier — the index is the core's (editions program)", () => {
77
+ // Read from the descriptors, not a hand list: a kind re-tiered to
78
+ // enterprise or cloud_only must drop out of the derivation whatever
79
+ // its not_search_indexed flag says. Today every such kind is also
80
+ // not_search_indexed, so this pins a guard, not a behaviour change.
81
+ for (const kind of searchIndexedKinds()) {
82
+ expect(
83
+ getKindMeta(kind).tier,
84
+ `${ApiResourceKind[kind]} is searchable but not open_source-tier`,
85
+ ).toBe(ResourceTier.open_source);
86
+ }
87
+ });
71
88
  });
72
89
 
73
90
  describe("SearchCriteria normalization (Go NewSearchCriteria)", () => {
@@ -5,7 +5,7 @@
5
5
  * discover).
6
6
  *
7
7
  * The searchable-kind set is DERIVED from the proto kind_meta extension
8
- * (not_search_indexed: false, tier != cloud_only) — Go's
8
+ * (not_search_indexed: false, tier == open_source) — Go's
9
9
  * SearchIndexedKinds() posture since stigmer/stigmer#439, where hand-copied
10
10
  * kind lists drifted from the proto and from each other. Go still carries a
11
11
  * hand map (SearchableKinds) pinned to the derivation by an invariant test;
@@ -13,6 +13,13 @@
13
13
  * registry unit test pins the registered extractors against this same
14
14
  * derivation (the #439 invariant's TS home).
15
15
  *
16
+ * The tier clause admits open_source alone, not "anything but cloud_only":
17
+ * the index is the core's, and a kind tiered enterprise (editions program,
18
+ * DD-001) must not enter it by accident. This derivation is edition-blind
19
+ * by design (the same code runs in every composition); an edition-aware
20
+ * derivation is a design act with a named trigger — the first searchable
21
+ * kind above open_source.
22
+ *
16
23
  * kinds are kept VERBATIM from the request; filtering to the searchable set
17
24
  * happens in effectiveKinds(). Keeping the raw request is what
18
25
  * distinguishes "no kinds requested" (discover mode) from "kinds requested
@@ -45,8 +52,8 @@ let searchableKindSetCache: ReadonlySet<ApiResourceKind> | undefined;
45
52
 
46
53
  /**
47
54
  * Go SearchIndexedKinds(): every kind this edition's search read side
48
- * serves — kind_meta declares it search-indexed and servable outside the
49
- * cloud. Sorted ascending by enum value, exactly Go's sort. Kinds whose
55
+ * serves — kind_meta declares it search-indexed and open_source-tier.
56
+ * Sorted ascending by enum value, exactly Go's sort. Kinds whose
50
57
  * kind_meta cannot be read are skipped (Go's contract; the registry unit
51
58
  * test separately fails on such a defect).
52
59
  */
@@ -64,7 +71,7 @@ export function searchIndexedKinds(): readonly ApiResourceKind[] {
64
71
  } catch {
65
72
  continue;
66
73
  }
67
- if (meta.notSearchIndexed || meta.tier === ResourceTier.cloud_only) {
74
+ if (meta.notSearchIndexed || meta.tier !== ResourceTier.open_source) {
68
75
  continue;
69
76
  }
70
77
  kinds.push(kind);