@stigmer/server 3.15.1 → 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 (51) hide show
  1. package/dist/domain/agentchannel/controller.js +15 -26
  2. package/dist/domain/agentchannel/controller.js.map +1 -1
  3. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  4. package/dist/domain/agentexecution/controller.js +3 -2
  5. package/dist/domain/agentexecution/controller.js.map +1 -1
  6. package/dist/domain/agentexecution/steps.d.ts +20 -8
  7. package/dist/domain/agentexecution/steps.d.ts.map +1 -1
  8. package/dist/domain/agentexecution/steps.js +32 -12
  9. package/dist/domain/agentexecution/steps.js.map +1 -1
  10. package/dist/domain/agentinstance/steps.d.ts.map +1 -1
  11. package/dist/domain/agentinstance/steps.js +9 -22
  12. package/dist/domain/agentinstance/steps.js.map +1 -1
  13. package/dist/domain/agentshare/controller.js +15 -26
  14. package/dist/domain/agentshare/controller.js.map +1 -1
  15. package/dist/domain/environment/controller.js +8 -10
  16. package/dist/domain/environment/controller.js.map +1 -1
  17. package/dist/domain/memory/steps.d.ts.map +1 -1
  18. package/dist/domain/memory/steps.js +5 -10
  19. package/dist/domain/memory/steps.js.map +1 -1
  20. package/dist/domain/schedule/controller.js +15 -24
  21. package/dist/domain/schedule/controller.js.map +1 -1
  22. package/dist/domain/session/steps.d.ts.map +1 -1
  23. package/dist/domain/session/steps.js +8 -4
  24. package/dist/domain/session/steps.js.map +1 -1
  25. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  26. package/dist/domain/workflowexecution/controller.js +12 -6
  27. package/dist/domain/workflowexecution/controller.js.map +1 -1
  28. package/dist/domain/workflowexecution/list-pending-approvals.d.ts.map +1 -1
  29. package/dist/domain/workflowexecution/list-pending-approvals.js +19 -8
  30. package/dist/domain/workflowexecution/list-pending-approvals.js.map +1 -1
  31. package/dist/domain/workflowinstance/controller.js +6 -13
  32. package/dist/domain/workflowinstance/controller.js.map +1 -1
  33. package/dist/extensions/list-read-scope.d.ts +31 -4
  34. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  35. package/dist/extensions/list-read-scope.js +12 -10
  36. package/dist/extensions/list-read-scope.js.map +1 -1
  37. package/package.json +4 -4
  38. package/src/domain/agentchannel/controller.ts +23 -31
  39. package/src/domain/agentexecution/controller.ts +11 -5
  40. package/src/domain/agentexecution/steps.ts +47 -22
  41. package/src/domain/agentinstance/steps.ts +17 -26
  42. package/src/domain/agentshare/controller.ts +23 -31
  43. package/src/domain/environment/controller.ts +12 -12
  44. package/src/domain/memory/steps.ts +6 -12
  45. package/src/domain/schedule/controller.ts +25 -33
  46. package/src/domain/session/steps.ts +24 -11
  47. package/src/domain/workflowexecution/controller.ts +32 -26
  48. package/src/domain/workflowexecution/list-pending-approvals.ts +37 -16
  49. package/src/domain/workflowinstance/controller.ts +11 -18
  50. package/src/extensions/__tests__/list-read-scope.test.ts +29 -7
  51. package/src/extensions/list-read-scope.ts +37 -12
@@ -748,9 +748,7 @@ async function getByWorkflow(
748
748
  ),
749
749
  )
750
750
  .addStep(newValidateProtoStep())
751
- .addStep(
752
- newLoadByWorkflowStep(deps.store, deps.logger, deps.listReadScope),
753
- )
751
+ .addStep(newLoadByWorkflowStep(deps.store, deps.logger, deps.listReadScope))
754
752
  .build()
755
753
  .execute(reqCtx);
756
754
 
@@ -794,27 +792,22 @@ function newLoadByWorkflowStep(
794
792
  continue;
795
793
  }
796
794
  }
797
- // 20260830.01 census lane 20: the scope narrows the decoded scan;
798
- // the org/workflow filters below are contract parity in both editions.
799
- const visible = await restrictListByReadScope(
795
+ // 20260830.01 census lane 20: the org/workflow filters are contract
796
+ // parity in both editions and run FIRST; the scope narrows the
797
+ // workflow's instances last (the scope is the last per-row
798
+ // predicate, stigmer-cloud 20260913.04 T02).
799
+ const filtered = await restrictListByReadScope(
800
800
  listReadScope,
801
801
  ctx.callerIdentity,
802
802
  ApiResourceKind.workflow_instance,
803
- decoded,
803
+ decoded.filter(
804
+ (instance) =>
805
+ instance.spec?.workflowId === workflowId &&
806
+ (request.org === "" || instance.metadata?.org === request.org),
807
+ ),
804
808
  "",
805
809
  );
806
810
 
807
- const filtered: WorkflowInstance[] = [];
808
- for (const instance of visible) {
809
- if (instance.spec?.workflowId !== workflowId) {
810
- continue;
811
- }
812
- if (request.org !== "" && instance.metadata?.org !== request.org) {
813
- continue;
814
- }
815
- filtered.push(instance);
816
- }
817
-
818
811
  logger.info("found workflow instances for workflow", {
819
812
  workflowId,
820
813
  count: filtered.length,
@@ -7,8 +7,11 @@
7
7
  * byte-identity arm the four local conformance rosters also pin);
8
8
  * - a composed scope narrows to the kept ids — and can only narrow:
9
9
  * ids the scope answers that were never offered add nothing;
10
- * - the org argument narrows AFTER the scope, only when non-blank
11
- * (the Java repos' uniform blank-org posture);
10
+ * - the org argument narrows BEFORE the scope, only when non-blank
11
+ * (the Java repos' uniform blank-org posture; since stigmer-cloud
12
+ * 20260913.04 T02 the scope is the last per-row predicate, so it is
13
+ * offered the org's rows and never the kind's — the same result set,
14
+ * a fraction of the candidates);
12
15
  * - the candidates carry {id, org, labels} — the driver's guest
13
16
  * cookie rule keys on labels;
14
17
  * - a scope failure PROPAGATES — never an empty result (the outage
@@ -76,24 +79,39 @@ describe("restrictListByReadScope", () => {
76
79
  expect(kept.map((r) => r.metadata.id)).toEqual(["ses_b"]);
77
80
  });
78
81
 
79
- it("a non-blank org narrows AFTER the scope; blank org does not", async () => {
80
- const scoped = scopeKeeping(["ses_a", "ses_c"]);
82
+ it("a non-blank org narrows BEFORE the scope: the scope is offered the org's rows only; blank org offers them all", async () => {
83
+ const offered: ReadonlyArray<ListEntryMeta>[] = [];
84
+ const recording: ListReadScope = {
85
+ authorizedResourceIds: () => Promise.resolve(new Set<string>()),
86
+ restrictListEntries: (_caller, _kind, entries) => {
87
+ offered.push(entries);
88
+ return Promise.resolve(new Set(["ses_a", "ses_c"]));
89
+ },
90
+ };
81
91
  const withOrg = await restrictListByReadScope(
82
- scoped,
92
+ recording,
83
93
  caller,
84
94
  ApiResourceKind.session,
85
95
  rows,
86
96
  "acme",
87
97
  );
88
98
  expect(withOrg.map((r) => r.metadata.id)).toEqual(["ses_a"]);
99
+ // The rival's row was never a candidate: the engine is not asked
100
+ // about rows the request's own predicate excludes.
101
+ expect(offered[0]!.map((entry) => entry.id)).toEqual(["ses_a", "ses_b"]);
89
102
  const blankOrg = await restrictListByReadScope(
90
- scoped,
103
+ recording,
91
104
  caller,
92
105
  ApiResourceKind.session,
93
106
  rows,
94
107
  "",
95
108
  );
96
109
  expect(blankOrg.map((r) => r.metadata.id)).toEqual(["ses_a", "ses_c"]);
110
+ expect(offered[1]!.map((entry) => entry.id)).toEqual([
111
+ "ses_a",
112
+ "ses_b",
113
+ "ses_c",
114
+ ]);
97
115
  });
98
116
 
99
117
  it("an empty kept set is a real answer — the empty list", async () => {
@@ -127,7 +145,11 @@ describe("restrictListByReadScope", () => {
127
145
  );
128
146
  expect(seenKind).toBe(ApiResourceKind.session);
129
147
  expect(seen).toEqual([
130
- { id: "ses_a", org: "acme", labels: { "stigmer.ai/guest-cookie-id": "ck_1" } },
148
+ {
149
+ id: "ses_a",
150
+ org: "acme",
151
+ labels: { "stigmer.ai/guest-cookie-id": "ck_1" },
152
+ },
131
153
  { id: "ses_b", org: "acme", labels: {} },
132
154
  { id: "ses_c", org: "rival", labels: {} },
133
155
  ]);
@@ -47,6 +47,30 @@
47
47
  * semantics (the Java baseline propagates the original caller
48
48
  * through in-process calls, so a composed driver normally never
49
49
  * sees `internal` on these lanes).
50
+ * - THE SCOPE IS THE LAST PER-ROW PREDICATE IN A LANE (stigmer-cloud
51
+ * 20260913.04 T02, Q-LB-11). Every request predicate a lane applies
52
+ * — the request's org, a phase, labels, a parent id, filter criteria
53
+ * — is a pure function of one row and the request; the scope is a
54
+ * pure function of one row and the caller; an intersection of
55
+ * per-row predicates commutes, so the order is free and the cheap
56
+ * ones go first: a lane offers the scope ONLY the rows its own
57
+ * predicates keep. Sort, page and aggregate are not per-row and stay
58
+ * after the scope, where they were. Why it matters: a composed
59
+ * driver's cost is proportional to the candidates offered (the cloud
60
+ * checks each one against its authorization engine), and the whole
61
+ * kind across every tenant is what rolled back on 2026-09-14; the
62
+ * request's org is tens of rows. Two idioms carry the rule and both
63
+ * keep their reason: a lane whose Go/Java contract org-narrows in
64
+ * BOTH editions (environment, memory, schedule, agent-instance,
65
+ * agent-share and agent-channel lists; the getBy* parent filters)
66
+ * applies its own filter ABOVE the helper call and passes `""`; a lane
67
+ * whose contract is cross-org on OSS (`agentExecution.list`,
68
+ * `workflowExecution.list`, `listPendingApprovals`) hands the request
69
+ * org to the helper, which applies it before the scope ONLY when a
70
+ * scope is composed — a scope-less server still returns the input
71
+ * untouched with the org unconsulted (the first line above). Two lanes
72
+ * have no org on the wire (`session.list`, `apiKey.findAll`) and offer
73
+ * the kind until P1 entry 12 pages them.
50
74
  */
51
75
  import type { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
52
76
 
@@ -112,10 +136,13 @@ export interface ScopedListResource {
112
136
  * `requestOrg` deliberately not consulted — the OSS single-tenant
113
137
  * posture treats the org field as a no-op, and new filtering on a
114
138
  * scope-less server would be a silent wire change);
115
- * - scope composed → entries narrowed to the kept ids, then to
116
- * `requestOrg` when non-blank (the Java repos' uniform posture:
117
- * blank org = permission-bounded across orgs, verified per lane in
118
- * the entry's census — lanes Java does not org-narrow pass "").
139
+ * - scope composed → entries narrowed to `requestOrg` when non-blank
140
+ * FIRST (the Java repos' uniform posture: blank org =
141
+ * permission-bounded across orgs, verified per lane in the entry's
142
+ * census — lanes Java does not org-narrow pass ""), then to the kept
143
+ * ids. The same set either way (both are per-row predicates); the
144
+ * order is the header's last contract line: the scope sees the org's
145
+ * rows, not the kind's.
119
146
  */
120
147
  export async function restrictListByReadScope<T extends ScopedListResource>(
121
148
  scope: ListReadScope | undefined,
@@ -127,17 +154,15 @@ export async function restrictListByReadScope<T extends ScopedListResource>(
127
154
  if (scope === undefined) {
128
155
  return [...resources];
129
156
  }
130
- const entries: ListEntryMeta[] = resources.map((resource) => ({
157
+ const offered =
158
+ requestOrg === ""
159
+ ? resources
160
+ : resources.filter((resource) => resource.metadata?.org === requestOrg);
161
+ const entries: ListEntryMeta[] = offered.map((resource) => ({
131
162
  id: resource.metadata?.id ?? "",
132
163
  org: resource.metadata?.org ?? "",
133
164
  labels: resource.metadata?.labels ?? {},
134
165
  }));
135
166
  const keptIds = await scope.restrictListEntries(caller, kind, entries);
136
- let kept = resources.filter((resource) =>
137
- keptIds.has(resource.metadata?.id ?? ""),
138
- );
139
- if (requestOrg !== "") {
140
- kept = kept.filter((resource) => resource.metadata?.org === requestOrg);
141
- }
142
- return kept;
167
+ return offered.filter((resource) => keptIds.has(resource.metadata?.id ?? ""));
143
168
  }