@stigmer/server 3.15.0 → 3.15.2-dev.20260913223433

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 (69) hide show
  1. package/dist/boot/compose.d.ts.map +1 -1
  2. package/dist/boot/compose.js +23 -2
  3. package/dist/boot/compose.js.map +1 -1
  4. package/dist/boot/config.d.ts +14 -0
  5. package/dist/boot/config.d.ts.map +1 -1
  6. package/dist/boot/config.js +1 -0
  7. package/dist/boot/config.js.map +1 -1
  8. package/dist/domain/agentchannel/controller.js +15 -26
  9. package/dist/domain/agentchannel/controller.js.map +1 -1
  10. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  11. package/dist/domain/agentexecution/controller.js +3 -2
  12. package/dist/domain/agentexecution/controller.js.map +1 -1
  13. package/dist/domain/agentexecution/steps.d.ts +20 -8
  14. package/dist/domain/agentexecution/steps.d.ts.map +1 -1
  15. package/dist/domain/agentexecution/steps.js +32 -12
  16. package/dist/domain/agentexecution/steps.js.map +1 -1
  17. package/dist/domain/agentinstance/steps.d.ts.map +1 -1
  18. package/dist/domain/agentinstance/steps.js +9 -22
  19. package/dist/domain/agentinstance/steps.js.map +1 -1
  20. package/dist/domain/agentshare/controller.js +15 -26
  21. package/dist/domain/agentshare/controller.js.map +1 -1
  22. package/dist/domain/environment/controller.js +8 -10
  23. package/dist/domain/environment/controller.js.map +1 -1
  24. package/dist/domain/memory/steps.d.ts.map +1 -1
  25. package/dist/domain/memory/steps.js +5 -10
  26. package/dist/domain/memory/steps.js.map +1 -1
  27. package/dist/domain/schedule/controller.js +15 -24
  28. package/dist/domain/schedule/controller.js.map +1 -1
  29. package/dist/domain/session/steps.d.ts.map +1 -1
  30. package/dist/domain/session/steps.js +8 -4
  31. package/dist/domain/session/steps.js.map +1 -1
  32. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  33. package/dist/domain/workflowexecution/controller.js +12 -6
  34. package/dist/domain/workflowexecution/controller.js.map +1 -1
  35. package/dist/domain/workflowexecution/list-pending-approvals.d.ts.map +1 -1
  36. package/dist/domain/workflowexecution/list-pending-approvals.js +19 -8
  37. package/dist/domain/workflowexecution/list-pending-approvals.js.map +1 -1
  38. package/dist/domain/workflowinstance/controller.js +6 -13
  39. package/dist/domain/workflowinstance/controller.js.map +1 -1
  40. package/dist/extensions/list-read-scope.d.ts +31 -4
  41. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  42. package/dist/extensions/list-read-scope.js +12 -10
  43. package/dist/extensions/list-read-scope.js.map +1 -1
  44. package/dist/transport/console/handler.d.ts +22 -5
  45. package/dist/transport/console/handler.d.ts.map +1 -1
  46. package/dist/transport/console/handler.js +53 -24
  47. package/dist/transport/console/handler.js.map +1 -1
  48. package/package.json +4 -4
  49. package/src/boot/__tests__/auth-enabled-composition.test.ts +49 -3
  50. package/src/boot/__tests__/config.test.ts +41 -3
  51. package/src/boot/compose.ts +30 -2
  52. package/src/boot/config.ts +15 -0
  53. package/src/domain/agentchannel/controller.ts +23 -31
  54. package/src/domain/agentexecution/controller.ts +11 -5
  55. package/src/domain/agentexecution/steps.ts +47 -22
  56. package/src/domain/agentinstance/steps.ts +17 -26
  57. package/src/domain/agentshare/controller.ts +23 -31
  58. package/src/domain/environment/controller.ts +12 -12
  59. package/src/domain/memory/steps.ts +6 -12
  60. package/src/domain/schedule/controller.ts +25 -33
  61. package/src/domain/session/steps.ts +24 -11
  62. package/src/domain/workflowexecution/controller.ts +32 -26
  63. package/src/domain/workflowexecution/list-pending-approvals.ts +37 -16
  64. package/src/domain/workflowinstance/controller.ts +11 -18
  65. package/src/extensions/__tests__/list-read-scope.test.ts +29 -7
  66. package/src/extensions/list-read-scope.ts +37 -12
  67. package/src/transport/console/__tests__/handler.test.ts +93 -23
  68. package/src/transport/console/__tests__/resolver.test.ts +6 -0
  69. package/src/transport/console/handler.ts +84 -30
@@ -78,28 +78,21 @@ export function newValidateListRequestStep(): PipelineStep<ListDesc> {
78
78
  }
79
79
 
80
80
  /**
81
- * QueryAllExecutions — list.go: full scan into the context. With a
82
- * composed ListReadScope (20260830.01, census lane 4) the scan narrows
83
- * to the caller's authorized executions ∩ the request's org (the Java
84
- * AgentExecutionListHandler baseline: org set = one-org view, blank =
85
- * permission-bounded across orgs; the guest cookie rule rides the
86
- * driver); no scope = the full scan, org a no-op — byte-identical.
81
+ * QueryAllExecutions — list.go: full scan into the context. The read
82
+ * scope no longer runs here: since stigmer-cloud 20260913.04 T02 the scope
83
+ * is the LAST per-row predicate of the chain (RestrictByReadScope below,
84
+ * after ApplyPhaseFilter), so a composed driver is offered the request's
85
+ * org and phase, not the kind (extensions/list-read-scope.ts, the
86
+ * header's last contract line).
87
87
  */
88
88
  export function newQueryAllExecutionsStep(
89
89
  store: Store,
90
90
  logger: Logger,
91
- listReadScope: ListReadScope | undefined,
92
91
  ): PipelineStep<ListDesc> {
93
92
  return {
94
93
  name: "QueryAllExecutions",
95
94
  async execute(ctx) {
96
- const executions = await restrictListByReadScope(
97
- listReadScope,
98
- ctx.callerIdentity,
99
- ApiResourceKind.agent_execution,
100
- await loadAllAgentExecutions(store, logger),
101
- ctx.input.org,
102
- );
95
+ const executions = await loadAllAgentExecutions(store, logger);
103
96
  logger.debug("Successfully queried executions", {
104
97
  count: executions.length,
105
98
  });
@@ -108,6 +101,37 @@ export function newQueryAllExecutionsStep(
108
101
  };
109
102
  }
110
103
 
104
+ /**
105
+ * RestrictByReadScope — the list chain's last per-row predicate (census
106
+ * lane 4). With a composed ListReadScope (20260830.01) the working list
107
+ * narrows to the caller's authorized executions ∩ the request's org (the
108
+ * Java AgentExecutionListHandler baseline: org set = one-org view, blank
109
+ * = permission-bounded across orgs; the guest cookie rule rides the
110
+ * driver); no scope = the working list unchanged, org a no-op —
111
+ * byte-identical. Placed after ApplyPhaseFilter so the driver is asked
112
+ * about the rows the request keeps, never about the whole kind.
113
+ */
114
+ export function newRestrictByReadScopeStep(
115
+ listReadScope: ListReadScope | undefined,
116
+ ): PipelineStep<ListDesc> {
117
+ return {
118
+ name: "RestrictByReadScope",
119
+ async execute(ctx) {
120
+ const executions = requireExecutions(ctx.get(EXECUTION_LIST_KEY));
121
+ ctx.set(
122
+ EXECUTION_LIST_KEY,
123
+ await restrictListByReadScope(
124
+ listReadScope,
125
+ ctx.callerIdentity,
126
+ ApiResourceKind.agent_execution,
127
+ executions,
128
+ ctx.input.org,
129
+ ),
130
+ );
131
+ },
132
+ };
133
+ }
134
+
111
135
  /**
112
136
  * ApplyPhaseFilter — list.go: status.phase equality filter, skipped when
113
137
  * the request carries no phase.
@@ -153,7 +177,8 @@ export function newValidateListBySessionRequestStep(): PipelineStep<ListBySessio
153
177
 
154
178
  /**
155
179
  * QueryExecutionsBySession — list_by_session.go: full scan +
156
- * spec.session_id equality filter.
180
+ * spec.session_id equality filter, then the read scope over the session's
181
+ * executions only (the scope is the last per-row predicate).
157
182
  */
158
183
  export function newQueryExecutionsBySessionStep(
159
184
  store: Store,
@@ -163,19 +188,19 @@ export function newQueryExecutionsBySessionStep(
163
188
  return {
164
189
  name: "QueryExecutionsBySession",
165
190
  async execute(ctx) {
166
- // Census lane 5: authorized ids ∩ session filter; the Java handler
191
+ // Census lane 5: session filter ∩ authorized ids; the Java handler
167
192
  // never consults org on this lane (bounded by the session id).
168
- const all = await restrictListByReadScope(
193
+ const ofSession = (await loadAllAgentExecutions(store, logger)).filter(
194
+ (execution) =>
195
+ (execution.spec?.sessionId ?? "") === ctx.input.sessionId,
196
+ );
197
+ const executions = await restrictListByReadScope(
169
198
  listReadScope,
170
199
  ctx.callerIdentity,
171
200
  ApiResourceKind.agent_execution,
172
- await loadAllAgentExecutions(store, logger),
201
+ ofSession,
173
202
  "",
174
203
  );
175
- const executions = all.filter(
176
- (execution) =>
177
- (execution.spec?.sessionId ?? "") === ctx.input.sessionId,
178
- );
179
204
  logger.debug("Successfully queried executions by session", {
180
205
  sessionId: ctx.input.sessionId,
181
206
  count: executions.length,
@@ -264,25 +264,21 @@ export function newListByOrgAndLabelsStep(
264
264
  continue;
265
265
  }
266
266
  }
267
- const visible = await restrictListByReadScope(
267
+ // Census lane 14: the org and label filters (contract in both
268
+ // editions) first, the scope last over the org's rows (the scope is
269
+ // the last per-row predicate, stigmer-cloud 20260913.04 T02).
270
+ const instances = await restrictListByReadScope(
268
271
  listReadScope,
269
272
  ctx.callerIdentity,
270
273
  ApiResourceKind.agent_instance,
271
- decoded,
274
+ decoded.filter(
275
+ (instance) =>
276
+ (instance.metadata?.org ?? "") === org &&
277
+ matchesAllLabels(instance.metadata?.labels ?? {}, filterLabels),
278
+ ),
272
279
  "",
273
280
  );
274
281
 
275
- const instances: AgentInstance[] = [];
276
- for (const instance of visible) {
277
- if ((instance.metadata?.org ?? "") !== org) {
278
- continue;
279
- }
280
- if (!matchesAllLabels(instance.metadata?.labels ?? {}, filterLabels)) {
281
- continue;
282
- }
283
- instances.push(instance);
284
- }
285
-
286
282
  instances.sort((a, b) =>
287
283
  compareCreatedAtDesc(
288
284
  a.status?.audit?.specAudit?.createdAt,
@@ -346,25 +342,20 @@ export function newLoadByAgentStep(
346
342
  continue;
347
343
  }
348
344
  }
349
- const visible = await restrictListByReadScope(
345
+ // Census lane 15: the agent's instances (and the optional org) first,
346
+ // the scope last.
347
+ const instances = await restrictListByReadScope(
350
348
  listReadScope,
351
349
  ctx.callerIdentity,
352
350
  ApiResourceKind.agent_instance,
353
- decoded,
351
+ decoded.filter(
352
+ (instance) =>
353
+ (instance.spec?.agentId ?? "") === agentId &&
354
+ (req.org === "" || (instance.metadata?.org ?? "") === req.org),
355
+ ),
354
356
  "",
355
357
  );
356
358
 
357
- const instances: AgentInstance[] = [];
358
- for (const instance of visible) {
359
- if ((instance.spec?.agentId ?? "") !== agentId) {
360
- continue;
361
- }
362
- if (req.org !== "" && (instance.metadata?.org ?? "") !== req.org) {
363
- continue;
364
- }
365
- instances.push(instance);
366
- }
367
-
368
359
  ctx.set(
369
360
  INSTANCE_LIST_KEY,
370
361
  create(AgentInstanceListSchema, {
@@ -596,28 +596,25 @@ function newLoadSharesByAgentStep(
596
596
  continue;
597
597
  }
598
598
  }
599
- // 20260830.01 census lane 17: the scope narrows the decoded scan;
600
- // the org/agent filters below are contract parity in both editions.
601
- const visible = await restrictListByReadScope(
599
+ // 20260830.01 census lane 17: the org/agent filters are contract
600
+ // parity in both editions and run FIRST; the scope narrows the
601
+ // agent's shares last (the scope is the last per-row predicate,
602
+ // stigmer-cloud 20260913.04 T02).
603
+ const shares = await restrictListByReadScope(
602
604
  listReadScope,
603
605
  ctx.callerIdentity,
604
606
  ApiResourceKind.agent_share,
605
- decoded,
607
+ decoded.filter((share) => {
608
+ const ref = share.spec?.agentRef;
609
+ return (
610
+ (ref?.org ?? "") === agentOrg &&
611
+ (ref?.slug ?? "") === agentSlug &&
612
+ (req.org === "" || (share.metadata?.org ?? "") === req.org)
613
+ );
614
+ }),
606
615
  "",
607
616
  );
608
617
 
609
- const shares: AgentShare[] = [];
610
- for (const share of visible) {
611
- const ref = share.spec?.agentRef;
612
- if ((ref?.org ?? "") !== agentOrg || (ref?.slug ?? "") !== agentSlug) {
613
- continue;
614
- }
615
- if (req.org !== "" && (share.metadata?.org ?? "") !== req.org) {
616
- continue;
617
- }
618
- shares.push(share);
619
- }
620
-
621
618
  ctx.set(
622
619
  SHARE_LIST_KEY,
623
620
  create(AgentShareListSchema, {
@@ -700,27 +697,22 @@ function newListByOrgAndLabelsStep(
700
697
  continue;
701
698
  }
702
699
  }
703
- // 20260830.01 census lane 16: the scope narrows the decoded scan;
704
- // the org equality below already serves the Java handler's org arm.
705
- const visible = await restrictListByReadScope(
700
+ // 20260830.01 census lane 16: the org and label filters serve the
701
+ // Java handler's arms in both editions and run FIRST; the scope
702
+ // narrows the org's rows last (the scope is the last per-row
703
+ // predicate, stigmer-cloud 20260913.04 T02).
704
+ const shares = await restrictListByReadScope(
706
705
  listReadScope,
707
706
  ctx.callerIdentity,
708
707
  ApiResourceKind.agent_share,
709
- decoded,
708
+ decoded.filter(
709
+ (share) =>
710
+ (share.metadata?.org ?? "") === org &&
711
+ matchesAllLabels(share.metadata?.labels ?? {}, filterLabels),
712
+ ),
710
713
  "",
711
714
  );
712
715
 
713
- const shares: AgentShare[] = [];
714
- for (const share of visible) {
715
- if ((share.metadata?.org ?? "") !== org) {
716
- continue;
717
- }
718
- if (!matchesAllLabels(share.metadata?.labels ?? {}, filterLabels)) {
719
- continue;
720
- }
721
- shares.push(share);
722
- }
723
-
724
716
  shares.sort((a, b) =>
725
717
  compareCreatedAtDesc(
726
718
  a.status?.audit?.specAudit?.createdAt,
@@ -797,24 +797,24 @@ function newListByOrgAndLabelsStep(
797
797
  continue; // skip malformed rows, as Go does
798
798
  }
799
799
  }
800
- const visible = await restrictListByReadScope(
800
+ // The request's org and labels first (this lane's contract in both
801
+ // editions), the read scope last (census lane 10; the scope is the
802
+ // last per-row predicate): a composed driver is asked about the
803
+ // org's environments, never every tenant's.
804
+ const requested = decoded.filter(
805
+ (env) =>
806
+ (env.metadata?.org ?? "") === org &&
807
+ matchesAllLabels(env.metadata?.labels ?? {}, filterLabels),
808
+ );
809
+ const environments = await restrictListByReadScope(
801
810
  listReadScope,
802
811
  ctx.callerIdentity,
803
812
  ApiResourceKind.environment,
804
- decoded,
813
+ requested,
805
814
  "",
806
815
  );
807
-
808
- const environments: Environment[] = [];
809
- for (const env of visible) {
810
- if ((env.metadata?.org ?? "") !== org) {
811
- continue;
812
- }
813
- if (!matchesAllLabels(env.metadata?.labels ?? {}, filterLabels)) {
814
- continue;
815
- }
816
+ for (const env of environments) {
816
817
  redactEnvironmentSecrets(env);
817
- environments.push(env);
818
818
  }
819
819
 
820
820
  environments.sort((a, b) =>
@@ -539,24 +539,18 @@ export function newListMemoriesByOrgStep(
539
539
  decoded.push(memory);
540
540
  }
541
541
  }
542
- // 20260830.01 census lane 11: the scope narrows the decoded scan;
543
- // the org equality below already serves the Java handler's org arm.
544
- const visible = await restrictListByReadScope(
542
+ // 20260830.01 census lane 11: the org equality serves the Java
543
+ // handler's org arm in both editions and runs FIRST; the scope
544
+ // narrows the org's rows last (the scope is the last per-row
545
+ // predicate, stigmer-cloud 20260913.04 T02).
546
+ const memories = await restrictListByReadScope(
545
547
  listReadScope,
546
548
  ctx.callerIdentity,
547
549
  ApiResourceKind.memory,
548
- decoded,
550
+ decoded.filter((memory) => (memory.metadata?.org ?? "") === org),
549
551
  "",
550
552
  );
551
553
 
552
- const memories: Memory[] = [];
553
- for (const memory of visible) {
554
- if ((memory.metadata?.org ?? "") !== org) {
555
- continue;
556
- }
557
- memories.push(memory);
558
- }
559
-
560
554
  memories.sort((a, b) =>
561
555
  compareCreatedAtDesc(
562
556
  a.status?.audit?.specAudit?.createdAt,
@@ -575,35 +575,32 @@ function newLoadSchedulesByAgentStep(
575
575
  continue;
576
576
  }
577
577
  }
578
- // 20260830.01 census lane 13: the scope narrows the decoded scan;
579
- // the org/agent filters below are contract parity in both editions.
580
- const visible = await restrictListByReadScope(
581
- listReadScope,
582
- ctx.callerIdentity,
583
- ApiResourceKind.schedule,
584
- decoded,
585
- "",
586
- );
587
-
588
- const schedules: Schedule[] = [];
589
- for (const schedule of visible) {
578
+ // 20260830.01 census lane 13: the org/agent filters are contract
579
+ // parity in both editions and run FIRST; the scope narrows the
580
+ // agent's schedules last (the scope is the last per-row predicate,
581
+ // stigmer-cloud 20260913.04 T02).
582
+ const ofAgent = decoded.filter((schedule) => {
590
583
  const ref =
591
584
  schedule.spec?.target.case === "agent"
592
585
  ? schedule.spec.target.value.agentRef
593
586
  : undefined;
594
587
  if ((ref?.org ?? "") !== agentOrg || (ref?.slug ?? "") !== agentSlug) {
595
- continue;
588
+ return false;
596
589
  }
597
590
  // Org scope: a multi-org caller asking for one org's schedules
598
591
  // must not see another org's schedules of the same agent.
599
592
  // (Schedules are same-org by invariant, so today this only
600
593
  // excludes rows when the requested org differs from the agent's —
601
594
  // kept anyway for contract parity with the sibling RPCs.)
602
- if (req.org !== "" && (schedule.metadata?.org ?? "") !== req.org) {
603
- continue;
604
- }
605
- schedules.push(schedule);
606
- }
595
+ return req.org === "" || (schedule.metadata?.org ?? "") === req.org;
596
+ });
597
+ const schedules = await restrictListByReadScope(
598
+ listReadScope,
599
+ ctx.callerIdentity,
600
+ ApiResourceKind.schedule,
601
+ ofAgent,
602
+ "",
603
+ );
607
604
 
608
605
  ctx.set(
609
606
  SCHEDULE_LIST_KEY,
@@ -677,27 +674,22 @@ function newListByOrgAndLabelsStep(
677
674
  continue;
678
675
  }
679
676
  }
680
- // 20260830.01 census lane 12: the scope narrows the decoded scan;
681
- // the org equality below already serves the Java handler's org arm.
682
- const visible = await restrictListByReadScope(
677
+ // 20260830.01 census lane 12: the org and label filters serve the
678
+ // Java handler's arms in both editions and run FIRST; the scope
679
+ // narrows the org's rows last (the scope is the last per-row
680
+ // predicate, stigmer-cloud 20260913.04 T02).
681
+ const schedules = await restrictListByReadScope(
683
682
  listReadScope,
684
683
  ctx.callerIdentity,
685
684
  ApiResourceKind.schedule,
686
- decoded,
685
+ decoded.filter(
686
+ (schedule) =>
687
+ (schedule.metadata?.org ?? "") === req.org &&
688
+ matchesAllLabels(schedule.metadata?.labels ?? {}, req.labels),
689
+ ),
687
690
  "",
688
691
  );
689
692
 
690
- const schedules: Schedule[] = [];
691
- for (const schedule of visible) {
692
- if ((schedule.metadata?.org ?? "") !== req.org) {
693
- continue;
694
- }
695
- if (!matchesAllLabels(schedule.metadata?.labels ?? {}, req.labels)) {
696
- continue;
697
- }
698
- schedules.push(schedule);
699
- }
700
-
701
693
  schedules.sort((a, b) =>
702
694
  compareCreatedAtDesc(
703
695
  a.status?.audit?.specAudit?.createdAt,
@@ -725,16 +725,23 @@ export function newFilterByAgentInstanceStep(
725
725
  throw invalidArgumentError("agent_instance_id is required");
726
726
  }
727
727
 
728
- const sessions = await restrictListByReadScope(
728
+ // The instance's sessions first, the scope last (census lane 2): a
729
+ // composed driver is asked about one instance's rows, not the kind.
730
+ const sessions = await loadAllSessions(
731
+ store,
732
+ logger,
733
+ ctx.apiResourceKind,
734
+ );
735
+ const filtered = await restrictListByReadScope(
729
736
  listReadScope,
730
737
  ctx.callerIdentity,
731
738
  ApiResourceKind.session,
732
- await loadAllSessions(store, logger, ctx.apiResourceKind),
739
+ sessions.filter(
740
+ (session) =>
741
+ (session.spec?.agentInstanceId ?? "") === agentInstanceId,
742
+ ),
733
743
  "",
734
744
  );
735
- const filtered = sessions.filter(
736
- (session) => (session.spec?.agentInstanceId ?? "") === agentInstanceId,
737
- );
738
745
 
739
746
  logger.info("Filtered sessions by agent instance", {
740
747
  agentInstanceId,
@@ -816,17 +823,23 @@ export function newFilterByChannelStep(
816
823
  throw invalidArgumentError("channel_id is required");
817
824
  }
818
825
 
819
- const sessions = await restrictListByReadScope(
826
+ // The channel's sessions first, the scope last (census lane 3).
827
+ const sessions = await loadAllSessions(
828
+ store,
829
+ logger,
830
+ ctx.apiResourceKind,
831
+ );
832
+ const filtered = await restrictListByReadScope(
820
833
  listReadScope,
821
834
  ctx.callerIdentity,
822
835
  ApiResourceKind.session,
823
- await loadAllSessions(store, logger, ctx.apiResourceKind),
836
+ sessions.filter(
837
+ (session) =>
838
+ (session.metadata?.labels ?? {})[CHANNEL_ID_LABEL_KEY] ===
839
+ channelId,
840
+ ),
824
841
  "",
825
842
  );
826
- const filtered = sessions.filter(
827
- (session) =>
828
- (session.metadata?.labels ?? {})[CHANNEL_ID_LABEL_KEY] === channelId,
829
- );
830
843
 
831
844
  logger.info("Filtered sessions by channel", {
832
845
  channelId,
@@ -230,7 +230,8 @@ export function registerWorkflowExecutionServices(
230
230
  router.service(WorkflowExecutionQueryController, {
231
231
  get: (id, ctx) => get(deps, id, ctx),
232
232
  list: (req, ctx) => list(deps, req, callerIdentityOf(ctx)),
233
- listByWorkflow: (req, ctx) => listByWorkflow(deps, req, callerIdentityOf(ctx)),
233
+ listByWorkflow: (req, ctx) =>
234
+ listByWorkflow(deps, req, callerIdentityOf(ctx)),
234
235
  subscribe: (req, ctx) => subscribeExecution(deps, req, ctx),
235
236
  getEventLog: (req, ctx) => getEventLog(deps, req, callerIdentityOf(ctx)),
236
237
  subscribeEvents: (req, ctx) => subscribeEvents(deps, req, ctx),
@@ -482,33 +483,37 @@ async function get(
482
483
  * filter.phases is absent), structured filter criteria (T13), and the
483
484
  * sort default started_at descending. No pagination (total_pages
484
485
  * placeholder 1). With a composed ListReadScope (20260830.01, census
485
- * lane 6) the scan narrows to the caller's authorized executions and the
486
+ * lane 6) the list narrows to the caller's authorized executions and the
486
487
  * request `org` narrows when non-blank (the Java
487
488
  * WorkflowExecutionListHandler baseline: blank = permission-bounded
488
489
  * across orgs); no scope = the full scan with `org` a deliberate no-op
489
- * on this single-tenant edition — byte-identical.
490
+ * on this single-tenant edition — byte-identical. The scope runs AFTER
491
+ * the phase and criteria filters (stigmer-cloud 20260913.04 T02: the
492
+ * scope is the last per-row predicate), so a composed driver is asked
493
+ * about the rows the request keeps, not the kind.
490
494
  */
491
495
  async function list(
492
496
  deps: WorkflowExecutionControllerDeps,
493
497
  req: ListWorkflowExecutionsRequest,
494
498
  identity: CallerIdentity,
495
499
  ): Promise<WorkflowExecutionList> {
496
- let executions = await restrictListByReadScope(
497
- deps.listReadScope,
498
- identity,
499
- ApiResourceKind.workflow_execution,
500
- await loadAllWorkflowExecutions(
501
- deps.store,
502
- deps.logger,
503
- "failed to list workflow executions",
504
- ),
505
- req.org,
500
+ let executions = await loadAllWorkflowExecutions(
501
+ deps.store,
502
+ deps.logger,
503
+ "failed to list workflow executions",
506
504
  );
507
505
 
508
506
  if (req.filter === undefined || req.filter.phases.length === 0) {
509
507
  executions = applyLegacyPhaseFilter(executions, req.phase);
510
508
  }
511
509
  executions = applyFilterCriteria(executions, req.filter);
510
+ executions = await restrictListByReadScope(
511
+ deps.listReadScope,
512
+ identity,
513
+ ApiResourceKind.workflow_execution,
514
+ executions,
515
+ req.org,
516
+ );
512
517
 
513
518
  let sortField = req.sortField;
514
519
  let ascending = req.sortAscending;
@@ -538,26 +543,27 @@ async function listByWorkflow(
538
543
  throw invalidArgumentError("workflow_id is required");
539
544
  }
540
545
 
541
- // Census lane 7: authorized ids ∩ workflow filter; the Java handler
542
- // never consults org on this lane (bounded by the workflow id).
543
- const all = await restrictListByReadScope(
544
- deps.listReadScope,
545
- identity,
546
- ApiResourceKind.workflow_execution,
547
- await loadAllWorkflowExecutions(
548
- deps.store,
549
- deps.logger,
550
- "failed to list workflow executions",
551
- ),
552
- "",
546
+ // Census lane 7: workflow filter ∩ criteria ∩ authorized ids, the scope
547
+ // last (bounded by the workflow id; the Java handler never consults org
548
+ // on this lane).
549
+ const all = await loadAllWorkflowExecutions(
550
+ deps.store,
551
+ deps.logger,
552
+ "failed to list workflow executions",
553
553
  );
554
554
  let executions = all.filter(
555
555
  (execution) =>
556
556
  execution.spec?.workflowInstanceId === req.workflowId ||
557
557
  execution.spec?.workflowId === req.workflowId,
558
558
  );
559
-
560
559
  executions = applyFilterCriteria(executions, req.filter);
560
+ executions = await restrictListByReadScope(
561
+ deps.listReadScope,
562
+ identity,
563
+ ApiResourceKind.workflow_execution,
564
+ executions,
565
+ "",
566
+ );
561
567
 
562
568
  let sortField = req.sortField;
563
569
  let ascending = req.sortAscending;
@@ -8,7 +8,11 @@ import { create } from "@bufbuild/protobuf";
8
8
  import type { Timestamp } from "@bufbuild/protobuf/wkt";
9
9
  import { timestampFromMs } from "@bufbuild/protobuf/wkt";
10
10
 
11
- import { ExecutionPhase, WorkflowTaskStatus } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
11
+ import type { WorkflowExecution } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/api_pb";
12
+ import {
13
+ ExecutionPhase,
14
+ WorkflowTaskStatus,
15
+ } from "@stigmer/protos/ai/stigmer/agentic/workflowexecution/v1/enum_pb";
12
16
  import {
13
17
  PendingApprovalSchema,
14
18
  PendingApprovalsListSchema,
@@ -46,19 +50,25 @@ export async function listPendingApprovals(
46
50
  req: ListPendingApprovalsRequest,
47
51
  identity: CallerIdentity,
48
52
  ): Promise<PendingApprovalsList> {
49
- // Census lane 8 (20260830.01): the scope narrows the EXECUTION scan —
50
- // before the approvals projection and its pagination, the Java
53
+ // Census lane 8 (20260830.01): the scope narrows the EXECUTIONS — before
54
+ // the approvals projection and its pagination, the Java
51
55
  // WorkflowExecutionListPendingApprovalsHandler order — and the request
52
56
  // org narrows when non-blank (blank = permission-bounded across orgs).
57
+ // It runs over the executions that CAN carry a pending approval (in
58
+ // progress, a task waiting) rather than the org's whole history: the
59
+ // predicate is per-row, so the scope is last (stigmer-cloud 20260913.04
60
+ // T02) and a composed driver is asked about a handful of rows.
53
61
  const executions = await restrictListByReadScope(
54
62
  deps.listReadScope,
55
63
  identity,
56
64
  ApiResourceKind.workflow_execution,
57
- await loadAllWorkflowExecutions(
58
- deps.store,
59
- deps.logger,
60
- "failed to list workflow executions for pending approvals",
61
- ),
65
+ (
66
+ await loadAllWorkflowExecutions(
67
+ deps.store,
68
+ deps.logger,
69
+ "failed to list workflow executions for pending approvals",
70
+ )
71
+ ).filter(mayHavePendingApproval),
62
72
  req.org,
63
73
  );
64
74
 
@@ -72,12 +82,6 @@ export async function listPendingApprovals(
72
82
 
73
83
  let approvals: PendingApproval[] = [];
74
84
  for (const execution of executions) {
75
- const phase =
76
- execution.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
77
- if (phase !== ExecutionPhase.EXECUTION_IN_PROGRESS) {
78
- continue;
79
- }
80
-
81
85
  for (const task of execution.status?.tasks ?? []) {
82
86
  if (task.status !== WorkflowTaskStatus.WORKFLOW_TASK_WAITING_APPROVAL) {
83
87
  continue;
@@ -90,8 +94,7 @@ export async function listPendingApprovals(
90
94
  // would break submitWorkflowTaskApproval, whose runner-side
91
95
  // signal is keyed by the plain task name.
92
96
  taskName: task.taskName,
93
- requester:
94
- execution.status?.audit?.specAudit?.createdBy?.id ?? "",
97
+ requester: execution.status?.audit?.specAudit?.createdBy?.id ?? "",
95
98
  requestedAt: parseTimestampString(task.startedAt),
96
99
  uiHint: task.uiHint,
97
100
  }),
@@ -118,3 +121,21 @@ function parseTimestampString(value: string): Timestamp | undefined {
118
121
  }
119
122
  return timestampFromMs(ms);
120
123
  }
124
+
125
+ /**
126
+ * The per-row predicate of the projection above: only an execution in
127
+ * progress with a task waiting for approval contributes an entry, so only
128
+ * those are worth a scope check. Same rows kept as the loop's own `continue`
129
+ * arms, decided once before the scope.
130
+ */
131
+ function mayHavePendingApproval(execution: WorkflowExecution): boolean {
132
+ const phase =
133
+ execution.status?.phase ?? ExecutionPhase.EXECUTION_PHASE_UNSPECIFIED;
134
+ return (
135
+ phase === ExecutionPhase.EXECUTION_IN_PROGRESS &&
136
+ (execution.status?.tasks ?? []).some(
137
+ (task) =>
138
+ task.status === WorkflowTaskStatus.WORKFLOW_TASK_WAITING_APPROVAL,
139
+ )
140
+ );
141
+ }