@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
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The relation-rewrite vocabulary the model declarations are written in —
3
- * the four forms an OpenFGA `define` line takes in the cloud's 27 `.fga`
4
- * files, and nothing more:
3
+ * the forms an OpenFGA `define` line takes in the cloud's `.fga` files,
4
+ * and nothing more:
5
5
  *
6
6
  * define organization: [organization] → direct(objectOf("organization"))
7
7
  * define owner: [identity_account] or admin from organization
@@ -10,8 +10,20 @@
10
10
  * or owner or platform_viewer
11
11
  * → union(direct(objectOf(...), usersetOf(...), usersetOf(...)),
12
12
  * computed("owner"), computed("platform_viewer"))
13
+ * define member: [identity_account] and viewer from organization
14
+ * → intersection(direct(objectOf("identity_account")), from("viewer", "organization"))
13
15
  * define can_view: viewer → computed("viewer")
14
16
  *
17
+ * `and` exists for one shape: a relation that is true only while another
18
+ * holds too — a team's member is a person granted the role AND still one
19
+ * of the organization's viewers, so leaving the organization ends every
20
+ * team-derived grant with no cleanup. A line is either a union or an
21
+ * intersection of leaves, never both: the DSL would need parentheses to
22
+ * mix them, the cloud's reader refuses parentheses, and `declareKind`
23
+ * refuses the nesting that would need them. `but not` has no node; the day
24
+ * a line needs it, the transcript cannot be written and the gap is a
25
+ * design act.
26
+ *
15
27
  * A subject type is an object type or a userset; the model admits no
16
28
  * wildcard (`type:*`) and declares no condition, so neither has a form
17
29
  * here. The day a line needs one, the transcript cannot be written and
@@ -21,10 +33,7 @@
21
33
  * file: the same relations, in the file's order, each line rewritten in
22
34
  * these builders so a reader with the model open sees the same text, and
23
35
  * so a structural compare against the file (the cloud's drift test at
24
- * the re-pin) needs no translation table. Intersection and exclusion
25
- * (`and`, `but not`) do not appear in the model and have no node here;
26
- * the day they do, the transcript cannot be written and the gap is a
27
- * design act, not a silent approximation.
36
+ * the re-pin) needs no translation table.
28
37
  *
29
38
  * The direct form carries its type restrictions (`[identity_account,
30
39
  * organization#viewer]`) because the evaluator ENFORCES them: a tuple
@@ -72,7 +81,12 @@ export type Rewrite =
72
81
  readonly tupleset: string;
73
82
  }
74
83
  /** `a or b or c` — the first true member wins, in the line's order. */
75
- | { readonly node: "union"; readonly members: ReadonlyArray<Rewrite> };
84
+ | { readonly node: "union"; readonly members: ReadonlyArray<Rewrite> }
85
+ /** `a and b` — true only when every member is, in the line's order; the first false one ends it. */
86
+ | {
87
+ readonly node: "intersection";
88
+ readonly members: ReadonlyArray<Rewrite>;
89
+ };
76
90
 
77
91
  // ---------------------------------------------------------------------------
78
92
  // Builders — named so a transcript reads like its `.fga` line.
@@ -102,6 +116,10 @@ export function union(...members: ReadonlyArray<Rewrite>): Rewrite {
102
116
  return { node: "union", members };
103
117
  }
104
118
 
119
+ export function intersection(...members: ReadonlyArray<Rewrite>): Rewrite {
120
+ return { node: "intersection", members };
121
+ }
122
+
105
123
  // ---------------------------------------------------------------------------
106
124
  // A kind's declaration.
107
125
  // ---------------------------------------------------------------------------
@@ -148,7 +166,10 @@ export interface KindDeclarationInput {
148
166
  * module load: a relation declared twice, a `computed` naming a relation
149
167
  * the kind does not define, a `from` whose tupleset the kind does not
150
168
  * define (its `relation` is the PARENT's and is resolved at evaluation),
151
- * a derived rule for a relation the transcript does not declare.
169
+ * a derived rule for a relation the transcript does not declare, and a
170
+ * line no `.fga` file could hold without parentheses (a union inside an
171
+ * intersection or the reverse, an operator nested in itself, an operator
172
+ * over fewer than two members).
152
173
  */
153
174
  export function declareKind(input: KindDeclarationInput): KindDeclaration {
154
175
  const type = kindEnumName(input.kind);
@@ -157,6 +178,10 @@ export function declareKind(input: KindDeclarationInput): KindDeclaration {
157
178
  if (relations.has(name)) {
158
179
  throw new Error(`${input.source}: relation '${name}' is declared twice`);
159
180
  }
181
+ const shape = unwritableShape(rewrite);
182
+ if (shape !== undefined) {
183
+ throw new Error(`${input.source}: '${name}' ${shape}`);
184
+ }
160
185
  relations.set(name, rewrite);
161
186
  }
162
187
  for (const [name, rewrite] of relations) {
@@ -199,6 +224,7 @@ function localReferences(
199
224
  case "from":
200
225
  return [{ role: "tupleset", relation: rewrite.tupleset }];
201
226
  case "union":
227
+ case "intersection":
202
228
  return rewrite.members.flatMap(localReferences);
203
229
  default: {
204
230
  const exhaustive: never = rewrite;
@@ -206,3 +232,34 @@ function localReferences(
206
232
  }
207
233
  }
208
234
  }
235
+
236
+ /**
237
+ * Why a line could not be written in a `.fga` file without parentheses,
238
+ * or undefined when it can: an operator's members are leaves (a direct
239
+ * list, a sibling relation, a tuple-to-userset), and there are at least
240
+ * two of them.
241
+ */
242
+ function unwritableShape(rewrite: Rewrite): string | undefined {
243
+ switch (rewrite.node) {
244
+ case "this":
245
+ case "computed":
246
+ case "from":
247
+ return undefined;
248
+ case "union":
249
+ case "intersection": {
250
+ if (rewrite.members.length < 2) {
251
+ return `is ${rewrite.node === "union" ? "an or" : "an and"} over fewer than two members`;
252
+ }
253
+ const nested = rewrite.members.find(
254
+ (member) => member.node === "union" || member.node === "intersection",
255
+ );
256
+ return nested === undefined
257
+ ? undefined
258
+ : `nests ${nested.node === "union" ? "or" : "and"} inside ${rewrite.node === "union" ? "or" : "and"}, which a .fga line cannot say without parentheses`;
259
+ }
260
+ default: {
261
+ const exhaustive: never = rewrite;
262
+ throw new Error(`unknown rewrite node: ${JSON.stringify(exhaustive)}`);
263
+ }
264
+ }
265
+ }
@@ -3,7 +3,9 @@
3
3
  * definition, admin-owned like a blueprint and viewable by its owners and
4
4
  * explicit grantees. The sessions its fires create name it back through
5
5
  * `session#schedule`, a link the cloud writes and this edition does not
6
- * (session.ts); the schedule's own relations are unaffected.
6
+ * (session.ts); the schedule's own relations are unaffected. An explicit
7
+ * grant may name an Enterprise team (`team#member`), which no open-source
8
+ * tuple ever does.
7
9
  */
8
10
  import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
9
11
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
@@ -15,6 +17,7 @@ import {
15
17
  from,
16
18
  objectOf,
17
19
  union,
20
+ usersetOf,
18
21
  } from "./rewrite.js";
19
22
 
20
23
  export const scheduleDeclaration = declareKind({
@@ -30,7 +33,13 @@ export const scheduleDeclaration = declareKind({
30
33
  from("admin", "organization"),
31
34
  ),
32
35
  ],
33
- ["viewer", union(direct(objectOf("identity_account")), computed("owner"))],
36
+ [
37
+ "viewer",
38
+ union(
39
+ direct(objectOf("identity_account"), usersetOf("team", "member")),
40
+ computed("owner"),
41
+ ),
42
+ ],
34
43
  ["can_view", computed("viewer")],
35
44
  ["can_edit", computed("owner")],
36
45
  ["can_delete", computed("owner")],
@@ -3,7 +3,8 @@
3
3
  * `can_use` and `can_clone` rather than `can_execute`. Only `can_view`,
4
4
  * `can_edit`, `can_delete`, `can_grant_access` and `can_view_access` are
5
5
  * in the wire's IamPermission vocabulary; the others are the model's and
6
- * are carried so the transcript is the file.
6
+ * are carried so the transcript is the file. `team#member` on `viewer` is
7
+ * the Enterprise team grant, which no open-source tuple ever names.
7
8
  */
8
9
  import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
9
10
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
@@ -42,6 +43,7 @@ export const skillDeclaration = declareKind({
42
43
  objectOf("identity_account"),
43
44
  usersetOf("organization", "member"),
44
45
  usersetOf("organization", "viewer"),
46
+ usersetOf("team", "member"),
45
47
  ),
46
48
  computed("owner"),
47
49
  computed("platform_viewer"),
@@ -3,7 +3,8 @@
3
3
  * without `can_create_instance` (a workflow instance is created through
4
4
  * the workflow-instance lane, gated by the organization). Kept as its own
5
5
  * transcript, not a shared "blueprint" helper, so the file-by-file drift
6
- * compare sees exactly this file's lines.
6
+ * compare sees exactly this file's lines. `team#member` on `viewer` is the
7
+ * Enterprise team grant, which no open-source tuple ever names.
7
8
  */
8
9
  import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
9
10
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
@@ -42,6 +43,7 @@ export const workflowDeclaration = declareKind({
42
43
  objectOf("identity_account"),
43
44
  usersetOf("organization", "member"),
44
45
  usersetOf("organization", "viewer"),
46
+ usersetOf("team", "member"),
45
47
  ),
46
48
  computed("owner"),
47
49
  computed("platform_viewer"),
@@ -6,7 +6,8 @@
6
6
  * audience through `execution_viewer from workflow_instance`, standing on
7
7
  * the `workflow_instance` link the derivation writes from
8
8
  * `spec.workflow_instance_id`. No `can_delete`: run history outlives its
9
- * instance by design.
9
+ * instance by design. The per-run grant may name an Enterprise team
10
+ * (`team#member`), which no open-source tuple ever does.
10
11
  */
11
12
  import { WorkflowExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
12
13
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
@@ -18,6 +19,7 @@ import {
18
19
  from,
19
20
  objectOf,
20
21
  union,
22
+ usersetOf,
21
23
  } from "./rewrite.js";
22
24
 
23
25
  export const workflowExecutionDeclaration = declareKind({
@@ -31,7 +33,7 @@ export const workflowExecutionDeclaration = declareKind({
31
33
  [
32
34
  "viewer",
33
35
  union(
34
- direct(objectOf("identity_account")),
36
+ direct(objectOf("identity_account"), usersetOf("team", "member")),
35
37
  computed("owner"),
36
38
  from("execution_viewer", "workflow_instance"),
37
39
  ),
@@ -14,6 +14,10 @@
14
14
  * admits — and `private` or unset derives nothing, so each run stays its
15
15
  * triggerer's. The cloud has no writer for this tuple today; this
16
16
  * edition enforces the field as the contract states it.
17
+ *
18
+ * `team#member` on `viewer` is the Enterprise team grant, which no
19
+ * open-source tuple ever names; `execution_viewer` is not a grantable role
20
+ * and admits no team.
17
21
  */
18
22
  import { isMessage } from "@bufbuild/protobuf";
19
23
 
@@ -85,6 +89,7 @@ export const workflowInstanceDeclaration = declareKind({
85
89
  objectOf("identity_account"),
86
90
  usersetOf("organization", "member"),
87
91
  usersetOf("organization", "viewer"),
92
+ usersetOf("team", "member"),
88
93
  ),
89
94
  computed("owner"),
90
95
  from("viewer", "default_of"),
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Pins the list-indexed surface (list-indexes.ts). The table below is
3
+ * every declaration's keys against its revision: a store trusts a row's
4
+ * facts only under the revision they were derived with, so a change to a
5
+ * declaration's keys without a new revision would read rows written under
6
+ * the old keys as if they carried the new ones. Changing a declaration
7
+ * therefore fails here until its revision is bumped and this table is
8
+ * updated in the same change.
9
+ */
10
+ import { describe, expect, it } from "vitest";
11
+
12
+ import {
13
+ ListIndexRegistry,
14
+ listIndexFingerprint,
15
+ } from "../../store/list-index.js";
16
+ import { LIST_INDEXES } from "../list-indexes.js";
17
+
18
+ const PINNED: Readonly<
19
+ Record<string, { revision: number; fingerprint: string }>
20
+ > = {
21
+ agent_execution: {
22
+ revision: 1,
23
+ fingerprint: "agent_execution{session=field:spec.session_id}",
24
+ },
25
+ artifact: {
26
+ revision: 1,
27
+ fingerprint:
28
+ "artifact{agent_execution=field:spec.source.agent_execution_id,workflow_execution=field:spec.source.workflow_execution_id}",
29
+ },
30
+ session: {
31
+ revision: 1,
32
+ fingerprint:
33
+ "session{agent_instance=field:spec.agent_instance_id,channel=label:stigmer.ai/channel-id}",
34
+ },
35
+ workflow_execution: {
36
+ revision: 1,
37
+ fingerprint:
38
+ "workflow_execution{workflow=field:spec.workflow_id,workflow_instance=field:spec.workflow_instance_id}",
39
+ },
40
+ };
41
+
42
+ describe("the list-indexed surface", () => {
43
+ it("opens as one registry: no kind declared twice", () => {
44
+ expect(() => new ListIndexRegistry(LIST_INDEXES)).not.toThrow();
45
+ });
46
+
47
+ it("declares exactly the pinned kinds", () => {
48
+ const kinds = LIST_INDEXES.map(
49
+ (d) => listIndexFingerprint(d).split("{")[0],
50
+ );
51
+ expect(kinds.sort()).toEqual(Object.keys(PINNED).sort());
52
+ });
53
+
54
+ it.each(LIST_INDEXES.map((d) => [listIndexFingerprint(d), d] as const))(
55
+ "%s carries the revision its keys are pinned to",
56
+ (fingerprint, declaration) => {
57
+ const kind = fingerprint.split("{")[0] ?? "";
58
+ const pinned = PINNED[kind];
59
+ expect(
60
+ { revision: declaration.revision, fingerprint },
61
+ "a declaration's keys changed: bump its revision and update this table together",
62
+ ).toEqual(pinned);
63
+ },
64
+ );
65
+ });
@@ -213,6 +213,7 @@ import { registerExtensionServicesUnshadowed } from "./route-shadowing.js";
213
213
  import type { ServerConfig } from "./config.js";
214
214
  import { createInProcessClients } from "./inprocess.js";
215
215
  import type { InProcessClients } from "./inprocess.js";
216
+ import { LIST_INDEXES } from "./list-indexes.js";
216
217
  import type { Logger } from "./logger.js";
217
218
 
218
219
  export interface ComposedServer {
@@ -336,20 +337,22 @@ export async function composeServer(
336
337
  }
337
338
  // Stage: storage — the driver selection seam (DD-010): DATABASE_URL
338
339
  // present → Postgres (async connect + advisory-locked migrations), else
339
- // sqlite on DB_PATH (migrations v1–v7, incl. adopting a Go-created
340
+ // sqlite on DB_PATH (its whole chain, incl. adopting a Go-created
340
341
  // database — D2 §3 schema continuity). Postgres wins when both are set:
341
342
  // DB_PATH always has a default value, so no other precedence could ever
342
- // select Postgres (config.ts documents the contract). A failure here is
343
- // a loud boot throw, never a degraded server. The operator identity
344
- // (#400) is installed by main.ts — once per PROCESS, before any writer
345
- // exists — not here: composeServer is re-entrant for tests, the
346
- // identity seam deliberately is not.
343
+ // select Postgres (config.ts documents the contract). Either driver
344
+ // opens with the list indexes (list-indexes.ts) and reconciles them
345
+ // before it returns. A failure here is a loud boot throw, never a
346
+ // degraded server. The operator identity (#400) is installed by main.ts
347
+ // — once per PROCESS, before any writer exists — not here: composeServer
348
+ // is re-entrant for tests, the identity seam deliberately is not.
347
349
  let store: Store;
350
+ const storeOptions = { listIndexes: LIST_INDEXES };
348
351
  if (config.databaseUrl !== "") {
349
- store = await PostgresStore.open(config.databaseUrl, logger);
352
+ store = await PostgresStore.open(config.databaseUrl, logger, storeOptions);
350
353
  logger.info("storage driver selected", { driver: "postgres" });
351
354
  } else {
352
- store = SqliteStore.open(config.dbPath, logger);
355
+ store = SqliteStore.open(config.dbPath, logger, storeOptions);
353
356
  logger.info("storage driver selected", {
354
357
  driver: "sqlite",
355
358
  dbPath: config.dbPath,
@@ -1020,7 +1023,10 @@ export async function composeServer(
1020
1023
  reserve: (key, declaredSizeBytes) => {
1021
1024
  const { ttlMs } = skillUploadSlots.reserve(key, declaredSizeBytes);
1022
1025
  return {
1023
- url: skillUploadUrl(config.skillTransferBaseUrl, uploadRefOf(key)),
1026
+ url: skillUploadUrl(
1027
+ config.skillTransferBaseUrl,
1028
+ uploadRefOf(key),
1029
+ ),
1024
1030
  ttlMs,
1025
1031
  };
1026
1032
  },
@@ -1196,9 +1202,12 @@ export async function composeServer(
1196
1202
  case "configured":
1197
1203
  break;
1198
1204
  case "derived":
1199
- logger.info("STIGMER_OAUTH_REDIRECT_URI is not set — deriving the served console's callback for MCP server OAuth Connect", {
1200
- redirectUri: oauthRedirect.uri,
1201
- });
1205
+ logger.info(
1206
+ "STIGMER_OAUTH_REDIRECT_URI is not set — deriving the served console's callback for MCP server OAuth Connect",
1207
+ {
1208
+ redirectUri: oauthRedirect.uri,
1209
+ },
1210
+ );
1202
1211
  break;
1203
1212
  case "absent":
1204
1213
  logger.warn(
@@ -1210,7 +1219,8 @@ export async function composeServer(
1210
1219
  throw new Error(String(exhaustive));
1211
1220
  }
1212
1221
  }
1213
- const oauthRedirectUri = oauthRedirect.kind === "absent" ? "" : oauthRedirect.uri;
1222
+ const oauthRedirectUri =
1223
+ oauthRedirect.kind === "absent" ? "" : oauthRedirect.uri;
1214
1224
  // The ONE fetch the McpServer domain dials user-supplied URLs with: the
1215
1225
  // endpoint it probes at save time and the login server it reaches on
1216
1226
  // Sign in. Every hop is judged under the edition's egress policy
@@ -1299,6 +1309,8 @@ export async function composeServer(
1299
1309
  grantScope:
1300
1310
  extensions.drivers.policyGrantScope ?? newOrganizationOnlyGrantScope(),
1301
1311
  queries: extensions.drivers.authorizationQueries,
1312
+ principalDisplay: extensions.drivers.principalDisplay,
1313
+ gateSteps: extensions.gateSteps,
1302
1314
  edition: extensions.edition,
1303
1315
  logger,
1304
1316
  });
@@ -0,0 +1,25 @@
1
+ /**
2
+ * The whole list-indexed surface, one explicit list (the search
3
+ * registry's composition-root idiom, query/search/registry.ts; house rule:
4
+ * no import side effects). The store is opened with it
5
+ * (`StoreOpenOptions.listIndexes`), keeps a declared kind's facts on every
6
+ * write, and refuses a read through a declaration it was not opened with,
7
+ * so a lane that reads an index and this list cannot disagree silently.
8
+ *
9
+ * A kind joins when a lane needs to read one organization's or one
10
+ * parent's rows without decoding the whole kind; the kinds here were
11
+ * chosen on the hosted edition's row counts and sizes (2026-09-23: every
12
+ * other org-scoped kind held under a hundred rows and a hundred kilobytes).
13
+ */
14
+ import { agentExecutionListIndex } from "../domain/agentexecution/list-index.js";
15
+ import { artifactListIndex } from "../domain/artifact/list-index.js";
16
+ import { sessionListIndex } from "../domain/session/list-index.js";
17
+ import { workflowExecutionListIndex } from "../domain/workflowexecution/list-index.js";
18
+ import type { ListIndexDeclaration } from "../store/list-index.js";
19
+
20
+ export const LIST_INDEXES: ReadonlyArray<ListIndexDeclaration> = [
21
+ agentExecutionListIndex,
22
+ artifactListIndex,
23
+ sessionListIndex,
24
+ workflowExecutionListIndex,
25
+ ];
@@ -12,8 +12,9 @@
12
12
  * byte-for-byte (sub-project DD-001: faithful-port + OSS issue);
13
13
  * - getAgentUsageReport org scoping (oss#389) incl. the no-name-oracle
14
14
  * rule (Go get_agent_usage_report_test.go case-for-case);
15
- * - list/listBySession filter semantics over seeded rows and the
16
- * total_pages placeholder;
15
+ * - list/listBySession filter semantics over seeded rows; list's cursor
16
+ * pages (newest created first, one organization, a token refused on
17
+ * another request) and listBySession returning its parent whole;
17
18
  * - update's status-clearing standard build and delete's audit-trail
18
19
  * return, over the wire;
19
20
  * - the populated getExecutionSummary arm (phase counts, active count,
@@ -28,6 +29,7 @@ import path from "node:path";
28
29
 
29
30
  import { create, fromBinary } from "@bufbuild/protobuf";
30
31
  import type { MessageInitShape } from "@bufbuild/protobuf";
32
+ import { timestampFromMs } from "@bufbuild/protobuf/wkt";
31
33
  import { Code, ConnectError, createClient } from "@connectrpc/connect";
32
34
  import type { Client, Transport } from "@connectrpc/connect";
33
35
  import { createGrpcTransport } from "@connectrpc/connect-node";
@@ -146,6 +148,7 @@ function seedInput(overrides?: {
146
148
  phase?: ExecutionPhase;
147
149
  startedAt?: string;
148
150
  completedAt?: string;
151
+ createdAtMs?: number;
149
152
  }): MessageInitShape<typeof AgentExecutionSchema> & {
150
153
  metadata: { id: string };
151
154
  } {
@@ -170,6 +173,12 @@ function seedInput(overrides?: {
170
173
  phase: overrides?.phase ?? ExecutionPhase.EXECUTION_COMPLETED,
171
174
  startedAt: overrides?.startedAt ?? "",
172
175
  completedAt: overrides?.completedAt ?? "",
176
+ audit:
177
+ overrides?.createdAtMs === undefined
178
+ ? undefined
179
+ : {
180
+ specAudit: { createdAt: timestampFromMs(overrides.createdAtMs) },
181
+ },
173
182
  },
174
183
  };
175
184
  }
@@ -322,7 +331,7 @@ describe("get / list / listBySession over the wire", () => {
322
331
  expect(got.spec?.message).toBe("Say hello.");
323
332
  });
324
333
 
325
- it("list returns every row with the total_pages placeholder, filtered by phase on request", async () => {
334
+ it("list with no page_size returns every row whole (total_pages 1), filtered by phase on request", async () => {
326
335
  const completed = await seed(
327
336
  seedInput({ phase: ExecutionPhase.EXECUTION_COMPLETED }),
328
337
  );
@@ -359,6 +368,79 @@ describe("get / list / listBySession over the wire", () => {
359
368
  expect(ids).toContain(inSession);
360
369
  expect(ids).not.toContain(outOfSession);
361
370
  });
371
+
372
+ it("list pages one organization newest created first, with a token until the last page", async () => {
373
+ const org = "paging-org";
374
+ const base = Date.parse("2026-09-01T00:00:00Z");
375
+ const newestFirst: string[] = [];
376
+ for (let i = 0; i < 5; i++) {
377
+ newestFirst.unshift(
378
+ await seed(seedInput({ org, createdAtMs: base + i * 1000 })),
379
+ );
380
+ }
381
+ await seed(seedInput({ org: "other-org", createdAtMs: base + 10_000 }));
382
+
383
+ const walked: string[] = [];
384
+ let pageToken = "";
385
+ do {
386
+ const page = await query.list({ org, pageSize: 2, pageToken });
387
+ expect(page.entries.length).toBeLessThanOrEqual(2);
388
+ expect(page.totalPages).toBe(page.nextPageToken === "" ? 1 : 0);
389
+ walked.push(...page.entries.map((e) => e.metadata!.id));
390
+ pageToken = page.nextPageToken;
391
+ } while (pageToken !== "");
392
+
393
+ expect(walked).toEqual(newestFirst);
394
+ });
395
+
396
+ it("list refuses a page_token issued for another request, and a negative page_size", async () => {
397
+ const org = "token-org";
398
+ await seed(
399
+ seedInput({ org, createdAtMs: Date.parse("2026-09-02T00:00:00Z") }),
400
+ );
401
+ await seed(
402
+ seedInput({ org, createdAtMs: Date.parse("2026-09-02T00:00:01Z") }),
403
+ );
404
+ const first = await query.list({ org, pageSize: 1 });
405
+ expect(first.nextPageToken).not.toBe("");
406
+
407
+ const replayed = await expectCode(
408
+ () =>
409
+ query.list({
410
+ org,
411
+ phase: ExecutionPhase.EXECUTION_FAILED,
412
+ pageSize: 1,
413
+ pageToken: first.nextPageToken,
414
+ }),
415
+ Code.InvalidArgument,
416
+ );
417
+ expect(replayed.rawMessage).toBe(
418
+ "page_token was issued for a different request",
419
+ );
420
+ await expectCode(
421
+ () => query.list({ org, pageToken: "not-a-token" }),
422
+ Code.InvalidArgument,
423
+ );
424
+ await expectCode(
425
+ () => query.list({ org, pageSize: -1 }),
426
+ Code.InvalidArgument,
427
+ );
428
+ });
429
+
430
+ it("listBySession returns the session's executions whole, whatever page_size says", async () => {
431
+ const ids = new Set<string>();
432
+ for (let i = 0; i < 3; i++) {
433
+ ids.add(await seed(seedInput({ sessionId: "ses_whole" })));
434
+ }
435
+
436
+ const result = await query.listBySession({
437
+ sessionId: "ses_whole",
438
+ pageSize: 1,
439
+ });
440
+ expect(new Set(result.entries.map((e) => e.metadata!.id))).toEqual(ids);
441
+ expect(result.nextPageToken).toBe("");
442
+ expect(result.totalPages).toBe(1);
443
+ });
362
444
  });
363
445
 
364
446
  describe("update / delete over the wire", () => {
@@ -128,10 +128,8 @@ import { newValidateVisibilityStep } from "../../pipeline/steps/validate-visibil
128
128
  import { newBuildNewStateStep } from "../../pipeline/steps/defaults.js";
129
129
  import {
130
130
  EXECUTION_LIST_KEY,
131
- newApplyPhaseFilterStep,
132
131
  newBuildExecutionListResponseStep,
133
- newQueryAllExecutionsStep,
134
- newRestrictByReadScopeStep,
132
+ newQueryExecutionPageStep,
135
133
  newQueryExecutionsBySessionStep,
136
134
  newValidateListBySessionRequestStep,
137
135
  newValidateListRequestStep,
@@ -527,9 +525,9 @@ async function get(
527
525
  }
528
526
 
529
527
  /**
530
- * List — list.go: full scan, optional phase filter, no sorting, no
531
- * pagination (total_pages placeholder 1); the request org field is a
532
- * deliberate no-op on this single-tenant edition.
528
+ * List — list.go: one page of the request's org (or every org), newest
529
+ * first, the optional phase filter and the read scope per batch
530
+ * (steps.ts, QueryExecutionPage).
533
531
  */
534
532
  async function list(
535
533
  deps: AgentExecutionControllerDeps,
@@ -553,16 +551,16 @@ async function list(
553
551
  ),
554
552
  )
555
553
  .addStep(newValidateListRequestStep())
556
- .addStep(newQueryAllExecutionsStep(deps.store, deps.logger))
557
- .addStep(newApplyPhaseFilterStep(deps.logger))
558
- .addStep(newRestrictByReadScopeStep(deps.listReadScope))
554
+ .addStep(
555
+ newQueryExecutionPageStep(deps.store, deps.logger, deps.listReadScope),
556
+ )
559
557
  .addStep(newBuildExecutionListResponseStep())
560
558
  .build()
561
559
  .execute(reqCtx);
562
560
  return requireListResult(reqCtx.get(EXECUTION_LIST_KEY));
563
561
  }
564
562
 
565
- /** ListBySession — list_by_session.go: spec.session_id equality filter. */
563
+ /** ListBySession — list_by_session.go: the session's executions, whole, newest first. */
566
564
  async function listBySession(
567
565
  deps: AgentExecutionControllerDeps,
568
566
  req: ListAgentExecutionsBySessionRequest,
@@ -0,0 +1,24 @@
1
+ /**
2
+ * The agent-execution list index (store/list-index.ts): the kind whose
3
+ * rows are the fattest in the store (every message and tool call of a run
4
+ * rides the row), so a lane that decodes the whole kind to keep one
5
+ * session's runs pays for every run on the platform. `session` is the key
6
+ * `listBySession` reads; the organization and the creation order are
7
+ * every declaration's.
8
+ *
9
+ * A change to `keys` bumps `revision` (boot/__tests__/list-indexes.test.ts
10
+ * pins the pair).
11
+ */
12
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
13
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
14
+
15
+ import { declareListIndex, field } from "../../store/list-index.js";
16
+
17
+ export const agentExecutionListIndex = declareListIndex({
18
+ kind: ApiResourceKind.agent_execution,
19
+ schema: AgentExecutionSchema,
20
+ revision: 1,
21
+ keys: {
22
+ session: field("spec.session_id"),
23
+ },
24
+ });