@stigmer/server 3.25.0 → 3.27.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 (254) hide show
  1. package/dist/authorization/evaluator.d.ts +4 -1
  2. package/dist/authorization/evaluator.d.ts.map +1 -1
  3. package/dist/authorization/evaluator.js +8 -0
  4. package/dist/authorization/evaluator.js.map +1 -1
  5. package/dist/authorization/model/agent.d.ts.map +1 -1
  6. package/dist/authorization/model/agent.js +4 -2
  7. package/dist/authorization/model/agent.js.map +1 -1
  8. package/dist/authorization/model/agent_channel.d.ts.map +1 -1
  9. package/dist/authorization/model/agent_channel.js +5 -4
  10. package/dist/authorization/model/agent_channel.js.map +1 -1
  11. package/dist/authorization/model/agent_instance.d.ts.map +1 -1
  12. package/dist/authorization/model/agent_instance.js +3 -2
  13. package/dist/authorization/model/agent_instance.js.map +1 -1
  14. package/dist/authorization/model/mcp_server.d.ts.map +1 -1
  15. package/dist/authorization/model/mcp_server.js +3 -2
  16. package/dist/authorization/model/mcp_server.js.map +1 -1
  17. package/dist/authorization/model/organization.d.ts.map +1 -1
  18. package/dist/authorization/model/organization.js +3 -0
  19. package/dist/authorization/model/organization.js.map +1 -1
  20. package/dist/authorization/model/plugin.d.ts.map +1 -1
  21. package/dist/authorization/model/plugin.js +4 -2
  22. package/dist/authorization/model/plugin.js.map +1 -1
  23. package/dist/authorization/model/rewrite.d.ts +25 -7
  24. package/dist/authorization/model/rewrite.d.ts.map +1 -1
  25. package/dist/authorization/model/rewrite.js +40 -1
  26. package/dist/authorization/model/rewrite.js.map +1 -1
  27. package/dist/authorization/model/schedule.d.ts.map +1 -1
  28. package/dist/authorization/model/schedule.js +8 -3
  29. package/dist/authorization/model/schedule.js.map +1 -1
  30. package/dist/authorization/model/skill.d.ts.map +1 -1
  31. package/dist/authorization/model/skill.js +3 -2
  32. package/dist/authorization/model/skill.js.map +1 -1
  33. package/dist/authorization/model/workflow.d.ts.map +1 -1
  34. package/dist/authorization/model/workflow.js +3 -2
  35. package/dist/authorization/model/workflow.js.map +1 -1
  36. package/dist/authorization/model/workflow_execution.d.ts.map +1 -1
  37. package/dist/authorization/model/workflow_execution.js +4 -3
  38. package/dist/authorization/model/workflow_execution.js.map +1 -1
  39. package/dist/authorization/model/workflow_instance.d.ts.map +1 -1
  40. package/dist/authorization/model/workflow_instance.js +5 -1
  41. package/dist/authorization/model/workflow_instance.js.map +1 -1
  42. package/dist/boot/compose.d.ts.map +1 -1
  43. package/dist/boot/compose.js +13 -8
  44. package/dist/boot/compose.js.map +1 -1
  45. package/dist/boot/list-indexes.d.ts +3 -0
  46. package/dist/boot/list-indexes.d.ts.map +1 -0
  47. package/dist/boot/list-indexes.js +24 -0
  48. package/dist/boot/list-indexes.js.map +1 -0
  49. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  50. package/dist/domain/agentexecution/controller.js +6 -8
  51. package/dist/domain/agentexecution/controller.js.map +1 -1
  52. package/dist/domain/agentexecution/list-index.d.ts +2 -0
  53. package/dist/domain/agentexecution/list-index.d.ts.map +1 -0
  54. package/dist/domain/agentexecution/list-index.js +23 -0
  55. package/dist/domain/agentexecution/list-index.js.map +1 -0
  56. package/dist/domain/agentexecution/steps.d.ts +17 -33
  57. package/dist/domain/agentexecution/steps.d.ts.map +1 -1
  58. package/dist/domain/agentexecution/steps.js +94 -95
  59. package/dist/domain/agentexecution/steps.js.map +1 -1
  60. package/dist/domain/agentexecution/usage.d.ts.map +1 -1
  61. package/dist/domain/agentexecution/usage.js +37 -14
  62. package/dist/domain/agentexecution/usage.js.map +1 -1
  63. package/dist/domain/artifact/controller.d.ts.map +1 -1
  64. package/dist/domain/artifact/controller.js +27 -15
  65. package/dist/domain/artifact/controller.js.map +1 -1
  66. package/dist/domain/artifact/list-index.d.ts +2 -0
  67. package/dist/domain/artifact/list-index.d.ts.map +1 -0
  68. package/dist/domain/artifact/list-index.js +21 -0
  69. package/dist/domain/artifact/list-index.js.map +1 -0
  70. package/dist/domain/iampolicy/access-lists.d.ts +5 -3
  71. package/dist/domain/iampolicy/access-lists.d.ts.map +1 -1
  72. package/dist/domain/iampolicy/access-lists.js +72 -19
  73. package/dist/domain/iampolicy/access-lists.js.map +1 -1
  74. package/dist/domain/iampolicy/constants.d.ts +32 -6
  75. package/dist/domain/iampolicy/constants.d.ts.map +1 -1
  76. package/dist/domain/iampolicy/constants.js +41 -6
  77. package/dist/domain/iampolicy/constants.js.map +1 -1
  78. package/dist/domain/iampolicy/controller.d.ts +6 -0
  79. package/dist/domain/iampolicy/controller.d.ts.map +1 -1
  80. package/dist/domain/iampolicy/controller.js +16 -7
  81. package/dist/domain/iampolicy/controller.js.map +1 -1
  82. package/dist/domain/iampolicy/resource-store.d.ts.map +1 -1
  83. package/dist/domain/iampolicy/resource-store.js +2 -6
  84. package/dist/domain/iampolicy/resource-store.js.map +1 -1
  85. package/dist/domain/iampolicy/steps.d.ts.map +1 -1
  86. package/dist/domain/iampolicy/steps.js +49 -7
  87. package/dist/domain/iampolicy/steps.js.map +1 -1
  88. package/dist/domain/session/list-index.d.ts +4 -0
  89. package/dist/domain/session/list-index.d.ts.map +1 -0
  90. package/dist/domain/session/list-index.js +26 -0
  91. package/dist/domain/session/list-index.js.map +1 -0
  92. package/dist/domain/session/steps.d.ts +7 -8
  93. package/dist/domain/session/steps.d.ts.map +1 -1
  94. package/dist/domain/session/steps.js +76 -78
  95. package/dist/domain/session/steps.js.map +1 -1
  96. package/dist/domain/workflow/registry/data/task-kind-registry.json +1 -0
  97. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  98. package/dist/domain/workflowexecution/controller.js +53 -55
  99. package/dist/domain/workflowexecution/controller.js.map +1 -1
  100. package/dist/domain/workflowexecution/execution-filter.d.ts +4 -0
  101. package/dist/domain/workflowexecution/execution-filter.d.ts.map +1 -1
  102. package/dist/domain/workflowexecution/execution-filter.js +11 -5
  103. package/dist/domain/workflowexecution/execution-filter.js.map +1 -1
  104. package/dist/domain/workflowexecution/get-execution-summary.d.ts.map +1 -1
  105. package/dist/domain/workflowexecution/get-execution-summary.js +23 -17
  106. package/dist/domain/workflowexecution/get-execution-summary.js.map +1 -1
  107. package/dist/domain/workflowexecution/list-index.d.ts +2 -0
  108. package/dist/domain/workflowexecution/list-index.d.ts.map +1 -0
  109. package/dist/domain/workflowexecution/list-index.js +21 -0
  110. package/dist/domain/workflowexecution/list-index.js.map +1 -0
  111. package/dist/domain/workflowexecution/list-pending-approvals.d.ts.map +1 -1
  112. package/dist/domain/workflowexecution/list-pending-approvals.js +69 -32
  113. package/dist/domain/workflowexecution/list-pending-approvals.js.map +1 -1
  114. package/dist/domain/workflowexecution/queries.d.ts +30 -4
  115. package/dist/domain/workflowexecution/queries.d.ts.map +1 -1
  116. package/dist/domain/workflowexecution/queries.js +76 -22
  117. package/dist/domain/workflowexecution/queries.js.map +1 -1
  118. package/dist/extensions/drivers.d.ts +12 -0
  119. package/dist/extensions/drivers.d.ts.map +1 -1
  120. package/dist/extensions/gate-slots.d.ts +10 -1
  121. package/dist/extensions/gate-slots.d.ts.map +1 -1
  122. package/dist/extensions/gate-slots.js +1 -0
  123. package/dist/extensions/gate-slots.js.map +1 -1
  124. package/dist/extensions/list-read-scope.d.ts +9 -12
  125. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  126. package/dist/extensions/list-read-scope.js.map +1 -1
  127. package/dist/extensions/principal-display.d.ts +34 -0
  128. package/dist/extensions/principal-display.d.ts.map +1 -0
  129. package/dist/extensions/principal-display.js +2 -0
  130. package/dist/extensions/principal-display.js.map +1 -0
  131. package/dist/extensions/registry.d.ts +6 -0
  132. package/dist/extensions/registry.d.ts.map +1 -1
  133. package/dist/extensions/registry.js +10 -0
  134. package/dist/extensions/registry.js.map +1 -1
  135. package/dist/index.d.ts +4 -2
  136. package/dist/index.d.ts.map +1 -1
  137. package/dist/index.js +6 -1
  138. package/dist/index.js.map +1 -1
  139. package/dist/pipeline/apiresource-meta.d.ts +10 -0
  140. package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
  141. package/dist/pipeline/apiresource-meta.js +16 -0
  142. package/dist/pipeline/apiresource-meta.js.map +1 -1
  143. package/dist/pipeline/steps/list-page.d.ts +63 -0
  144. package/dist/pipeline/steps/list-page.d.ts.map +1 -0
  145. package/dist/pipeline/steps/list-page.js +187 -0
  146. package/dist/pipeline/steps/list-page.js.map +1 -0
  147. package/dist/query/activity/handler.d.ts +6 -7
  148. package/dist/query/activity/handler.d.ts.map +1 -1
  149. package/dist/query/activity/handler.js +42 -59
  150. package/dist/query/activity/handler.js.map +1 -1
  151. package/dist/store/interface.d.ts +29 -0
  152. package/dist/store/interface.d.ts.map +1 -1
  153. package/dist/store/interface.js.map +1 -1
  154. package/dist/store/list-index.d.ts +123 -0
  155. package/dist/store/list-index.d.ts.map +1 -0
  156. package/dist/store/list-index.js +277 -0
  157. package/dist/store/list-index.js.map +1 -0
  158. package/dist/store/logger.d.ts +3 -0
  159. package/dist/store/logger.d.ts.map +1 -1
  160. package/dist/store/logger.js +5 -1
  161. package/dist/store/logger.js.map +1 -1
  162. package/dist/store/postgres/migrations.d.ts +10 -6
  163. package/dist/store/postgres/migrations.d.ts.map +1 -1
  164. package/dist/store/postgres/migrations.js +64 -6
  165. package/dist/store/postgres/migrations.js.map +1 -1
  166. package/dist/store/postgres/store.d.ts +26 -5
  167. package/dist/store/postgres/store.d.ts.map +1 -1
  168. package/dist/store/postgres/store.js +377 -22
  169. package/dist/store/postgres/store.js.map +1 -1
  170. package/dist/store/sqlite/migrations.d.ts +11 -5
  171. package/dist/store/sqlite/migrations.d.ts.map +1 -1
  172. package/dist/store/sqlite/migrations.js +50 -4
  173. package/dist/store/sqlite/migrations.js.map +1 -1
  174. package/dist/store/sqlite/store.d.ts +18 -3
  175. package/dist/store/sqlite/store.d.ts.map +1 -1
  176. package/dist/store/sqlite/store.js +321 -15
  177. package/dist/store/sqlite/store.js.map +1 -1
  178. package/package.json +6 -6
  179. package/src/authorization/__tests__/drivers.ts +6 -1
  180. package/src/authorization/__tests__/evaluator.test.ts +127 -2
  181. package/src/authorization/evaluator.ts +12 -1
  182. package/src/authorization/model/__tests__/registry.test.ts +64 -2
  183. package/src/authorization/model/agent.ts +4 -1
  184. package/src/authorization/model/agent_channel.ts +11 -3
  185. package/src/authorization/model/agent_instance.ts +3 -1
  186. package/src/authorization/model/mcp_server.ts +3 -1
  187. package/src/authorization/model/organization.ts +3 -0
  188. package/src/authorization/model/plugin.ts +4 -1
  189. package/src/authorization/model/rewrite.ts +65 -8
  190. package/src/authorization/model/schedule.ts +11 -2
  191. package/src/authorization/model/skill.ts +3 -1
  192. package/src/authorization/model/workflow.ts +3 -1
  193. package/src/authorization/model/workflow_execution.ts +4 -2
  194. package/src/authorization/model/workflow_instance.ts +5 -0
  195. package/src/boot/__tests__/list-indexes.test.ts +65 -0
  196. package/src/boot/compose.ts +25 -13
  197. package/src/boot/list-indexes.ts +25 -0
  198. package/src/domain/agentexecution/__tests__/agentexecution.test.ts +85 -3
  199. package/src/domain/agentexecution/controller.ts +8 -10
  200. package/src/domain/agentexecution/list-index.ts +24 -0
  201. package/src/domain/agentexecution/steps.ts +118 -115
  202. package/src/domain/agentexecution/usage.ts +38 -17
  203. package/src/domain/artifact/controller.ts +28 -18
  204. package/src/domain/artifact/list-index.ts +22 -0
  205. package/src/domain/iampolicy/__tests__/access-lists.test.ts +120 -6
  206. package/src/domain/iampolicy/__tests__/constants.test.ts +26 -4
  207. package/src/domain/iampolicy/__tests__/controller.test.ts +55 -1
  208. package/src/domain/iampolicy/__tests__/grant-path.test.ts +3 -3
  209. package/src/domain/iampolicy/__tests__/steps.test.ts +123 -1
  210. package/src/domain/iampolicy/access-lists.ts +98 -23
  211. package/src/domain/iampolicy/constants.ts +50 -6
  212. package/src/domain/iampolicy/controller.ts +30 -7
  213. package/src/domain/iampolicy/resource-store.ts +3 -6
  214. package/src/domain/iampolicy/steps.ts +89 -7
  215. package/src/domain/session/list-index.ts +28 -0
  216. package/src/domain/session/steps.ts +108 -123
  217. package/src/domain/workflow/registry/data/task-kind-registry.json +1 -0
  218. package/src/domain/workflowexecution/__tests__/list-read-scope-summaries.test.ts +155 -99
  219. package/src/domain/workflowexecution/__tests__/workflowexecution.test.ts +193 -31
  220. package/src/domain/workflowexecution/controller.ts +54 -82
  221. package/src/domain/workflowexecution/execution-filter.ts +20 -7
  222. package/src/domain/workflowexecution/get-execution-summary.ts +31 -24
  223. package/src/domain/workflowexecution/list-index.ts +22 -0
  224. package/src/domain/workflowexecution/list-pending-approvals.ts +82 -25
  225. package/src/domain/workflowexecution/queries.ts +134 -23
  226. package/src/extensions/__tests__/iam-policy-points.test.ts +30 -0
  227. package/src/extensions/__tests__/registry.test.ts +3 -0
  228. package/src/extensions/drivers.ts +12 -0
  229. package/src/extensions/gate-slots.ts +10 -0
  230. package/src/extensions/list-read-scope.ts +9 -12
  231. package/src/extensions/principal-display.ts +37 -0
  232. package/src/extensions/registry.ts +19 -0
  233. package/src/index.ts +19 -1
  234. package/src/pipeline/apiresource-meta.ts +21 -0
  235. package/src/pipeline/steps/__tests__/list-page.test.ts +186 -0
  236. package/src/pipeline/steps/list-page.ts +284 -0
  237. package/src/query/activity/handler.ts +57 -78
  238. package/src/store/README.md +51 -32
  239. package/src/store/__tests__/list-index.measure.test.ts +293 -0
  240. package/src/store/__tests__/list-index.test.ts +328 -0
  241. package/src/store/__tests__/store-contract.ts +346 -3
  242. package/src/store/interface.ts +39 -0
  243. package/src/store/list-index.ts +465 -0
  244. package/src/store/logger.ts +8 -1
  245. package/src/store/postgres/__tests__/list-index-repair.test.ts +133 -0
  246. package/src/store/postgres/__tests__/migrations.test.ts +27 -19
  247. package/src/store/postgres/__tests__/store-contract.test.ts +50 -3
  248. package/src/store/postgres/migrations.ts +69 -7
  249. package/src/store/postgres/store.ts +485 -29
  250. package/src/store/sqlite/__tests__/migrations.test.ts +51 -7
  251. package/src/store/sqlite/__tests__/store-contract.test.ts +70 -16
  252. package/src/store/sqlite/__tests__/support.ts +12 -3
  253. package/src/store/sqlite/migrations.ts +57 -6
  254. package/src/store/sqlite/store.ts +462 -21
@@ -36,7 +36,9 @@
36
36
  * console and SDK show these sentences verbatim, so they are contract. The
37
37
  * new sentences are the two edition refusals (the federation shape), the
38
38
  * unknown permission, the unknown principal kind (the resource sentence's
39
- * shape) and the malformed triple.
39
+ * shape), the malformed triple, and the four principal refusals a team
40
+ * grantee brought (a person's qualifier, a team's qualifier, a kind no
41
+ * team may be granted, a role a team may not hold).
40
42
  */
41
43
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
42
44
  import type {
@@ -157,8 +159,8 @@ export const ROLES_RECONCILED_KEY = "membership_rules_reconciled";
157
159
  /**
158
160
  * The principal kinds a PERSON may grant a role to — the user `create`
159
161
  * lane's grantee vocabulary (2026-09-14, session 9 ruling Q-S9-2; the
160
- * security read's finding 41). A role names a person, so the one grantee
161
- * is the identity account. A row whose principal is a RESOURCE
162
+ * security read's finding 41): a person (the identity account) and a team
163
+ * of people. A row whose principal is a RESOURCE
162
164
  * (`organization:A#organization@platform_client:X`, `#managed_org`) is a
163
165
  * structural link, and structural links are `bootstrapPolicy`'s — the
164
166
  * platform's own lane, which skips role validation by design.
@@ -169,14 +171,27 @@ export const ROLES_RECONCILED_KEY = "membership_rules_reconciled";
169
171
  * two cannot drift), and the hierarchy walk follows it. Without the arm, a
170
172
  * person with `can_grant_access` on organization B could write
171
173
  * `organization:A#member@organization:B` and B's Members page, asked for
172
- * inherited access, would list A's people. The cloud's `create` had the
173
- * same gap; the store's exclusion also keeps the cloud's Java-era `team`,
174
- * which is not an ApiResourceKind and so no wire spec can name.
174
+ * inherited access, would list A's people.
175
+ *
176
+ * Each kind grants through its own list: a person the kind's
177
+ * `grantable_roles`, a team its `team_grantable_roles`, always as the
178
+ * team's members (`TEAM_MEMBERS_RELATION`). A kind that lists no team role
179
+ * — `organization` among them, so open source's organization-only scope —
180
+ * grants a team nothing.
175
181
  */
176
182
  export const USER_GRANT_PRINCIPAL_KINDS: ReadonlyArray<ApiResourceKind> = [
177
183
  ApiResourceKind.identity_account,
184
+ ApiResourceKind.team,
178
185
  ];
179
186
 
187
+ /**
188
+ * The one relation a team principal names: the team's members. A grant to
189
+ * `team:<id>#member` is a grant to every member, and membership is bounded
190
+ * by organization membership in the model, so leaving the organization
191
+ * ends the grant with no cleanup.
192
+ */
193
+ export const TEAM_MEMBERS_RELATION = "member";
194
+
180
195
  // ---------------------------------------------------------------------------
181
196
  // Byte-pinned copy (the cloud handlers' sentences, moved as-is).
182
197
  // ---------------------------------------------------------------------------
@@ -249,6 +264,35 @@ export function principalNotGrantableMessage(kind: string): string {
249
264
  return `Principal kind '${kind}' cannot be granted a role. Grantable principal kinds: [${grantable.join(", ")}]`;
250
265
  }
251
266
 
267
+ /**
268
+ * A person principal carrying a relation qualifier (INVALID_ARGUMENT). A
269
+ * person is granted a role directly; `identity_account:<id>#<relation>`
270
+ * names no one, and OpenFGA would refuse the tuple after the row was
271
+ * written.
272
+ */
273
+ export function personQualifierMessage(relation: string): string {
274
+ return `Principal kind 'identity_account' takes no relation; got '${relation}'`;
275
+ }
276
+
277
+ /** A team principal naming a relation other than its members (INVALID_ARGUMENT). */
278
+ export function teamQualifierMessage(relation: string): string {
279
+ return `A team principal must name the relation '${TEAM_MEMBERS_RELATION}' (the team's members); got '${relation}'`;
280
+ }
281
+
282
+ /** A team grant on a kind that lists no team role (INVALID_ARGUMENT). */
283
+ export function teamNotGrantableMessage(kind: string): string {
284
+ return `A team cannot be granted access to resource kind '${kind}'.`;
285
+ }
286
+
287
+ /** A team grant of a role the kind does not let a team hold (INVALID_ARGUMENT); the list in kind_meta order. */
288
+ export function teamRoleNotGrantableMessage(
289
+ role: string,
290
+ kind: string,
291
+ grantable: ReadonlyArray<string>,
292
+ ): string {
293
+ return `Role '${role}' cannot be granted to a team on resource kind '${kind}'. Roles a team can hold: [${grantable.join(", ")}]`;
294
+ }
295
+
252
296
  /** A triple field holding a canonical-text delimiter (INVALID_ARGUMENT; Q-S2-1). */
253
297
  export function malformedTripleMessage(field: string): string {
254
298
  return `policy ${field} must not contain ':', '#' or '@'`;
@@ -31,9 +31,11 @@
31
31
  * triple — strictly safer than the cloud's silent default instance.
32
32
  * - `create` validates the role BEFORE it writes (the cloud's order): a
33
33
  * held row under a role the kind does not grant is refused, never
34
- * answered as a duplicate. It also grants to PEOPLE only (Q-S9-2,
35
- * 2026-09-14): a principal that is not an identity account is
36
- * INVALID_ARGUMENT — a row naming a resource as its principal is the
34
+ * answered as a duplicate. It also grants to PEOPLE and TEAMS only
35
+ * (Q-S9-2, 2026-09-14): any other principal is INVALID_ARGUMENT, and a
36
+ * team only on the roles its kind lets a team hold (steps.ts) and,
37
+ * where an edition serves teams, only a team the create gate slot
38
+ * admits — a row naming a resource as its principal is the
37
39
  * structural link the hierarchy walk follows, `bootstrapPolicy`'s to
38
40
  * write, and through this lane would have let a right on one
39
41
  * organization list another's members. The cloud's create had the
@@ -136,9 +138,12 @@ import type { ServerEdition } from "@stigmer/protos/ai/stigmer/platform/v1/serve
136
138
  import type { Logger } from "../../boot/logger.js";
137
139
  import type { AuthorizationQueryEngine } from "../../extensions/authorization-queries.js";
138
140
  import type { Authorizer } from "../../extensions/authorizer.js";
141
+ import { stepsForSlot } from "../../extensions/gate-slots.js";
142
+ import type { ResolvedGateSteps } from "../../extensions/gate-slots.js";
139
143
  import { isPlatformPipelineCaller } from "../../extensions/identity.js";
140
144
  import type { CallerIdentity } from "../../extensions/identity.js";
141
145
  import type { PolicyGrantScope } from "../../extensions/policy-grant-scope.js";
146
+ import type { PrincipalDisplay } from "../../extensions/principal-display.js";
142
147
  import {
143
148
  kindByEnumName,
144
149
  kindEnumName,
@@ -209,6 +214,10 @@ export interface IamPolicyControllerDeps {
209
214
  readonly grantScope: PolicyGrantScope;
210
215
  /** The composed query engine — undefined = the tuple-half RPCs refuse UNIMPLEMENTED. */
211
216
  readonly queries: AuthorizationQueryEngine | undefined;
217
+ /** The composed principal display — undefined = a non-person grantee renders in the id fallback shape. */
218
+ readonly principalDisplay: PrincipalDisplay | undefined;
219
+ /** The merged gate steps; `create` splices `iam-policy-create:pre-side-effect-gate`. */
220
+ readonly gateSteps: ResolvedGateSteps;
212
221
  /** The served edition — `checkMyPermission`'s first arm reads a kind's tier against it. */
213
222
  readonly edition: ServerEdition;
214
223
  readonly logger: Logger;
@@ -285,7 +294,10 @@ function guardSystemRpc(method: DescMethod, caller: CallerIdentity): void {
285
294
  // The command chains
286
295
  // ---------------------------------------------------------------------------
287
296
 
288
- /** `create`: the wire refusal, then Authorize → ValidateProto → ValidateGrantableRole → Grant. */
297
+ /**
298
+ * `create`: the wire refusal, then Authorize → ValidateProto →
299
+ * ValidateGrantableRole → [iam-policy-create:pre-side-effect-gate] → Grant.
300
+ */
289
301
  function createPolicy(
290
302
  deps: IamPolicyControllerDeps,
291
303
  spec: IamPolicySpec,
@@ -303,8 +315,8 @@ function createPolicy(
303
315
 
304
316
  /**
305
317
  * The grant chain both `create` and `bootstrapPolicy` run; only the user
306
- * lane validates the role — bootstrap writes structural relations no
307
- * scope would admit.
318
+ * lane validates the role and runs the create gate slot — bootstrap writes
319
+ * structural relations no scope would admit and no edition gates.
308
320
  */
309
321
  async function grantThroughChain(
310
322
  deps: IamPolicyControllerDeps,
@@ -327,6 +339,12 @@ async function grantThroughChain(
327
339
  .addStep(newValidateProtoStep());
328
340
  if (validateRole) {
329
341
  pipeline.addStep(newValidateGrantableRoleStep(deps.grantScope));
342
+ for (const step of stepsForSlot<typeof IamPolicySpecSchema>(
343
+ deps.gateSteps,
344
+ "iam-policy-create:pre-side-effect-gate",
345
+ )) {
346
+ pipeline.addStep(step);
347
+ }
330
348
  }
331
349
  await pipeline.addStep(newGrantStep(deps.grantPath)).build().execute(reqCtx);
332
350
  return policyResultOf(reqCtx, method);
@@ -600,7 +618,12 @@ async function listResourceAccessByPrincipal(
600
618
  resource.id,
601
619
  input.includeInherited,
602
620
  );
603
- return buildPrincipalAccessList(deps.policies, displayResolver, hierarchy);
621
+ return buildPrincipalAccessList(
622
+ deps.policies,
623
+ displayResolver,
624
+ deps.principalDisplay,
625
+ hierarchy,
626
+ );
604
627
  }, "failed to list resource access");
605
628
  return create(ResourceAccessByPrincipalListSchema, {
606
629
  entries: [...entries],
@@ -54,13 +54,10 @@ const KIND = ApiResourceKind.iam_policy;
54
54
  * principal half is derived from the user lane's grantee vocabulary
55
55
  * (constants.ts USER_GRANT_PRINCIPAL_KINDS; Q-S9-2) so the writer's "who a
56
56
  * person may grant to" and the reader's "what is a person, not a parent"
57
- * are one definition; `team` stays for the cloud's Java-era rows (not an
58
- * ApiResourceKind, so no wire spec names it).
57
+ * are one definition: the identity account and the team.
59
58
  */
60
- const NON_STRUCTURAL_PRINCIPAL_KINDS: ReadonlyArray<string> = [
61
- ...USER_GRANT_PRINCIPAL_KINDS.map((kind) => kindEnumName(kind)),
62
- "team",
63
- ];
59
+ const NON_STRUCTURAL_PRINCIPAL_KINDS: ReadonlyArray<string> =
60
+ USER_GRANT_PRINCIPAL_KINDS.map((kind) => kindEnumName(kind));
64
61
  const NON_STRUCTURAL_RELATIONS: ReadonlyArray<string> = ["owner", "creator"];
65
62
 
66
63
  export function newResourceIamPolicyStore(store: Store): IamPolicyStore {
@@ -30,17 +30,32 @@
30
30
  * intersection — the cloud's second copy listing it. Intersecting is
31
31
  * what keeps a scope from WIDENING the contract by accident;
32
32
  * 5. the PRINCIPAL (Q-S9-2, 2026-09-14; constants.ts
33
- * USER_GRANT_PRINCIPAL_KINDS): a person grants roles to people — the
34
- * identity account — and to nothing else. A row whose principal is a
33
+ * USER_GRANT_PRINCIPAL_KINDS): a person grants roles to people and to
34
+ * teams of people, and to nothing else. A row whose principal is a
35
35
  * resource is a structural link (the hierarchy walk's parent edge),
36
36
  * which is `bootstrapPolicy`'s to write; through this lane it would
37
37
  * let a right on one organization read another's members. Last, so
38
38
  * every answer the cloud's create gives today is unchanged and only
39
- * the row it should never have written is refused.
39
+ * the row it should never have written is refused. A person names no
40
+ * relation qualifier: `identity_account:<id>#<anything>` names
41
+ * nobody, and OpenFGA would refuse the tuple after the row was
42
+ * written, a grant on paper that is denied in practice.
43
+ *
44
+ * A TEAM principal takes its own list in place of arm 4: the kind's
45
+ * `team_grantable_roles` ∩ scope, never the person list, because a team
46
+ * is granted only what its members could each be granted and never
47
+ * ownership. It must name the team's members (`TEAM_MEMBERS_RELATION`).
48
+ * A kind that lists no team role refuses a team whatever the role, so
49
+ * open source (organization-only scope, and `organization` lists none)
50
+ * grants a team nothing without a branch of its own. Whether the team
51
+ * exists and belongs to the resource's organization needs a read, which
52
+ * this synchronous step does not make: that is the
53
+ * `iam-policy-create:pre-side-effect-gate` slot's, spliced after it.
40
54
  */
41
55
  import { create } from "@bufbuild/protobuf";
42
56
  import { Code, ConnectError } from "@connectrpc/connect";
43
57
 
58
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
44
59
  import { IamPolicySchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/api_pb";
45
60
  import type { RevokeOrgAccessInputSchema } from "@stigmer/protos/ai/stigmer/iam/iampolicy/v1/io_pb";
46
61
  import type {
@@ -51,15 +66,23 @@ import type {
51
66
  import { IamRole } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
52
67
 
53
68
  import type { PolicyGrantScope } from "../../extensions/policy-grant-scope.js";
54
- import { grantableRolesFor } from "../../pipeline/apiresource-meta.js";
69
+ import {
70
+ grantableRolesFor,
71
+ kindEnumName,
72
+ teamGrantableRolesFor,
73
+ } from "../../pipeline/apiresource-meta.js";
55
74
  import type { PipelineStep } from "../../pipeline/pipeline.js";
56
75
  import type { RequestContext } from "../../pipeline/request-context.js";
57
76
  import {
58
77
  PER_RESOURCE_GRANTS_UNIMPLEMENTED_MESSAGE,
59
- USER_GRANT_PRINCIPAL_KINDS,
78
+ TEAM_MEMBERS_RELATION,
60
79
  noGrantableRolesMessage,
80
+ personQualifierMessage,
61
81
  principalNotGrantableMessage,
62
82
  roleNotGrantableMessage,
83
+ teamNotGrantableMessage,
84
+ teamQualifierMessage,
85
+ teamRoleNotGrantableMessage,
63
86
  } from "./constants.js";
64
87
  import type { IamPolicyGrantPath } from "./grant-path.js";
65
88
  import {
@@ -94,6 +117,17 @@ export function newValidateGrantableRoleStep(
94
117
  Code.Unimplemented,
95
118
  );
96
119
  }
120
+ const principal = spec.principal;
121
+ if (principal?.kind === TEAM_KIND_NAME) {
122
+ requireTeamGrant(
123
+ spec.relation,
124
+ principal.relation,
125
+ kind,
126
+ kindName,
127
+ scoped,
128
+ );
129
+ return;
130
+ }
97
131
  const grantable = contract.filter((role) => scoped.has(role));
98
132
  if (!grantable.some((role) => IamRole[role] === spec.relation)) {
99
133
  throw new ConnectError(
@@ -105,18 +139,66 @@ export function newValidateGrantableRoleStep(
105
139
  Code.InvalidArgument,
106
140
  );
107
141
  }
108
- const principalKindName = spec.principal?.kind ?? "";
142
+ const principalKindName = principal?.kind ?? "";
109
143
  const principalKind = requireKnownPrincipalKind(principalKindName);
110
- if (!USER_GRANT_PRINCIPAL_KINDS.includes(principalKind)) {
144
+ if (principalKind !== ApiResourceKind.identity_account) {
111
145
  throw new ConnectError(
112
146
  principalNotGrantableMessage(principalKindName),
113
147
  Code.InvalidArgument,
114
148
  );
115
149
  }
150
+ const qualifier = principal?.relation ?? "";
151
+ if (qualifier !== "") {
152
+ throw new ConnectError(
153
+ personQualifierMessage(qualifier),
154
+ Code.InvalidArgument,
155
+ );
156
+ }
116
157
  },
117
158
  };
118
159
  }
119
160
 
161
+ /** The wire name of the team kind, as an `ApiResourceRef.kind` carries it. */
162
+ const TEAM_KIND_NAME = kindEnumName(ApiResourceKind.team);
163
+
164
+ /**
165
+ * The team arm of ValidateGrantableRole (the module header): the role
166
+ * against the kind's team list narrowed by the scope, then the qualifier.
167
+ */
168
+ function requireTeamGrant(
169
+ role: string,
170
+ qualifier: string,
171
+ kind: ApiResourceKind,
172
+ kindName: string,
173
+ scoped: ReadonlySet<IamRole>,
174
+ ): void {
175
+ const teamGrantable = teamGrantableRolesFor(kind).filter((r) =>
176
+ scoped.has(r),
177
+ );
178
+ if (teamGrantable.length === 0) {
179
+ throw new ConnectError(
180
+ teamNotGrantableMessage(kindName),
181
+ Code.InvalidArgument,
182
+ );
183
+ }
184
+ if (!teamGrantable.some((r) => IamRole[r] === role)) {
185
+ throw new ConnectError(
186
+ teamRoleNotGrantableMessage(
187
+ role,
188
+ kindName,
189
+ teamGrantable.map((r) => IamRole[r]),
190
+ ),
191
+ Code.InvalidArgument,
192
+ );
193
+ }
194
+ if (qualifier !== TEAM_MEMBERS_RELATION) {
195
+ throw new ConnectError(
196
+ teamQualifierMessage(qualifier),
197
+ Code.InvalidArgument,
198
+ );
199
+ }
200
+ }
201
+
120
202
  /** Grant the spec as the chain's caller; the row (fresh or held) is the result. */
121
203
  export function newGrantStep(
122
204
  grantPath: IamPolicyGrantPath,
@@ -0,0 +1,28 @@
1
+ /**
2
+ * The session list index (store/list-index.ts): `agent_instance` is the
3
+ * key `listByAgentInstance` reads, `channel` the one `listByChannel` reads
4
+ * (the channel's id is stamped as a label by the channel lanes, not held
5
+ * in the spec). A session belongs to one organization, which is every
6
+ * declaration's first fact, so `session.list` narrows by it when the
7
+ * request names one.
8
+ *
9
+ * A change to `keys` bumps `revision` (boot/__tests__/list-indexes.test.ts
10
+ * pins the pair).
11
+ */
12
+ import { SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
13
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
14
+
15
+ import { declareListIndex, field, label } from "../../store/list-index.js";
16
+
17
+ /** The label the channel lanes stamp on every session they open. */
18
+ export const SESSION_CHANNEL_ID_LABEL = "stigmer.ai/channel-id";
19
+
20
+ export const sessionListIndex = declareListIndex({
21
+ kind: ApiResourceKind.session,
22
+ schema: SessionSchema,
23
+ revision: 1,
24
+ keys: {
25
+ agent_instance: field("spec.agent_instance_id"),
26
+ channel: label(SESSION_CHANNEL_ID_LABEL),
27
+ },
28
+ });