@stigmer/server 3.21.0 → 3.22.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 (185) hide show
  1. package/dist/authorization/evaluator.d.ts +7 -1
  2. package/dist/authorization/evaluator.d.ts.map +1 -1
  3. package/dist/authorization/evaluator.js.map +1 -1
  4. package/dist/authorization/list-read-scope.d.ts.map +1 -1
  5. package/dist/authorization/list-read-scope.js +19 -11
  6. package/dist/authorization/list-read-scope.js.map +1 -1
  7. package/dist/authorization/model/organization.d.ts.map +1 -1
  8. package/dist/authorization/model/organization.js +2 -0
  9. package/dist/authorization/model/organization.js.map +1 -1
  10. package/dist/domain/agent/controller.d.ts.map +1 -1
  11. package/dist/domain/agent/controller.js +2 -0
  12. package/dist/domain/agent/controller.js.map +1 -1
  13. package/dist/domain/agentchannel/controller.d.ts.map +1 -1
  14. package/dist/domain/agentchannel/controller.js +6 -1
  15. package/dist/domain/agentchannel/controller.js.map +1 -1
  16. package/dist/domain/agentchannel/steps.d.ts +19 -0
  17. package/dist/domain/agentchannel/steps.d.ts.map +1 -1
  18. package/dist/domain/agentchannel/steps.js +32 -0
  19. package/dist/domain/agentchannel/steps.js.map +1 -1
  20. package/dist/domain/agentinstance/controller.d.ts +5 -5
  21. package/dist/domain/agentinstance/controller.d.ts.map +1 -1
  22. package/dist/domain/agentinstance/controller.js +6 -1
  23. package/dist/domain/agentinstance/controller.js.map +1 -1
  24. package/dist/domain/agentinstance/steps.d.ts +43 -8
  25. package/dist/domain/agentinstance/steps.d.ts.map +1 -1
  26. package/dist/domain/agentinstance/steps.js +71 -8
  27. package/dist/domain/agentinstance/steps.js.map +1 -1
  28. package/dist/domain/agentshare/controller.d.ts.map +1 -1
  29. package/dist/domain/agentshare/controller.js +7 -1
  30. package/dist/domain/agentshare/controller.js.map +1 -1
  31. package/dist/domain/agentshare/steps.d.ts +58 -0
  32. package/dist/domain/agentshare/steps.d.ts.map +1 -1
  33. package/dist/domain/agentshare/steps.js +109 -4
  34. package/dist/domain/agentshare/steps.js.map +1 -1
  35. package/dist/domain/artifact/controller.js +21 -25
  36. package/dist/domain/artifact/controller.js.map +1 -1
  37. package/dist/domain/channelapp/controller.d.ts.map +1 -1
  38. package/dist/domain/channelapp/controller.js +2 -0
  39. package/dist/domain/channelapp/controller.js.map +1 -1
  40. package/dist/domain/environment/controller.d.ts.map +1 -1
  41. package/dist/domain/environment/controller.js +2 -0
  42. package/dist/domain/environment/controller.js.map +1 -1
  43. package/dist/domain/executioncontext/controller.d.ts.map +1 -1
  44. package/dist/domain/executioncontext/controller.js +2 -0
  45. package/dist/domain/executioncontext/controller.js.map +1 -1
  46. package/dist/domain/executioncontext/steps.d.ts.map +1 -1
  47. package/dist/domain/executioncontext/steps.js +2 -1
  48. package/dist/domain/executioncontext/steps.js.map +1 -1
  49. package/dist/domain/mcpserver/connect.d.ts.map +1 -1
  50. package/dist/domain/mcpserver/connect.js +8 -7
  51. package/dist/domain/mcpserver/connect.js.map +1 -1
  52. package/dist/domain/mcpserver/controller.d.ts.map +1 -1
  53. package/dist/domain/mcpserver/controller.js +2 -0
  54. package/dist/domain/mcpserver/controller.js.map +1 -1
  55. package/dist/domain/mcpserver/start-connect.d.ts.map +1 -1
  56. package/dist/domain/mcpserver/start-connect.js +8 -7
  57. package/dist/domain/mcpserver/start-connect.js.map +1 -1
  58. package/dist/domain/memory/controller.d.ts.map +1 -1
  59. package/dist/domain/memory/controller.js +6 -1
  60. package/dist/domain/memory/controller.js.map +1 -1
  61. package/dist/domain/memory/steps.d.ts +23 -0
  62. package/dist/domain/memory/steps.d.ts.map +1 -1
  63. package/dist/domain/memory/steps.js +35 -0
  64. package/dist/domain/memory/steps.js.map +1 -1
  65. package/dist/domain/oauthapp/controller.d.ts.map +1 -1
  66. package/dist/domain/oauthapp/controller.js +2 -0
  67. package/dist/domain/oauthapp/controller.js.map +1 -1
  68. package/dist/domain/plugin/controller.d.ts.map +1 -1
  69. package/dist/domain/plugin/controller.js +5 -24
  70. package/dist/domain/plugin/controller.js.map +1 -1
  71. package/dist/domain/schedule/controller.d.ts.map +1 -1
  72. package/dist/domain/schedule/controller.js +7 -1
  73. package/dist/domain/schedule/controller.js.map +1 -1
  74. package/dist/domain/schedule/steps.d.ts +19 -0
  75. package/dist/domain/schedule/steps.d.ts.map +1 -1
  76. package/dist/domain/schedule/steps.js +32 -0
  77. package/dist/domain/schedule/steps.js.map +1 -1
  78. package/dist/domain/session/steps.d.ts +8 -9
  79. package/dist/domain/session/steps.d.ts.map +1 -1
  80. package/dist/domain/session/steps.js +17 -21
  81. package/dist/domain/session/steps.js.map +1 -1
  82. package/dist/domain/skill/controller.d.ts.map +1 -1
  83. package/dist/domain/skill/controller.js +8 -17
  84. package/dist/domain/skill/controller.js.map +1 -1
  85. package/dist/domain/workflow/controller.d.ts.map +1 -1
  86. package/dist/domain/workflow/controller.js +30 -107
  87. package/dist/domain/workflow/controller.js.map +1 -1
  88. package/dist/domain/workflow/version-resolution.d.ts +5 -0
  89. package/dist/domain/workflow/version-resolution.d.ts.map +1 -0
  90. package/dist/domain/workflow/version-resolution.js +113 -0
  91. package/dist/domain/workflow/version-resolution.js.map +1 -0
  92. package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
  93. package/dist/domain/workflowinstance/controller.js +6 -1
  94. package/dist/domain/workflowinstance/controller.js.map +1 -1
  95. package/dist/domain/workflowinstance/steps.d.ts +20 -0
  96. package/dist/domain/workflowinstance/steps.d.ts.map +1 -1
  97. package/dist/domain/workflowinstance/steps.js +39 -0
  98. package/dist/domain/workflowinstance/steps.js.map +1 -1
  99. package/dist/extensions/identity.d.ts +19 -0
  100. package/dist/extensions/identity.d.ts.map +1 -1
  101. package/dist/extensions/identity.js +22 -3
  102. package/dist/extensions/identity.js.map +1 -1
  103. package/dist/extensions/list-read-scope.d.ts +41 -7
  104. package/dist/extensions/list-read-scope.d.ts.map +1 -1
  105. package/dist/extensions/list-read-scope.js +13 -2
  106. package/dist/extensions/list-read-scope.js.map +1 -1
  107. package/dist/pipeline/steps/authorize-resolved-target.d.ts +88 -0
  108. package/dist/pipeline/steps/authorize-resolved-target.d.ts.map +1 -0
  109. package/dist/pipeline/steps/authorize-resolved-target.js +36 -0
  110. package/dist/pipeline/steps/authorize-resolved-target.js.map +1 -0
  111. package/dist/pipeline/steps/authorize.d.ts +27 -0
  112. package/dist/pipeline/steps/authorize.d.ts.map +1 -1
  113. package/dist/pipeline/steps/authorize.js +49 -10
  114. package/dist/pipeline/steps/authorize.js.map +1 -1
  115. package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
  116. package/dist/pipeline/steps/guard-memory-capture.js +2 -1
  117. package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
  118. package/dist/pipeline/steps/guard-reserved-labels.d.ts.map +1 -1
  119. package/dist/pipeline/steps/guard-reserved-labels.js +6 -7
  120. package/dist/pipeline/steps/guard-reserved-labels.js.map +1 -1
  121. package/dist/pipeline/steps/helpers.d.ts +9 -5
  122. package/dist/pipeline/steps/helpers.d.ts.map +1 -1
  123. package/dist/pipeline/steps/helpers.js +11 -15
  124. package/dist/pipeline/steps/helpers.js.map +1 -1
  125. package/dist/pipeline/steps/version-history.d.ts +11 -0
  126. package/dist/pipeline/steps/version-history.d.ts.map +1 -1
  127. package/dist/pipeline/steps/version-history.js +26 -0
  128. package/dist/pipeline/steps/version-history.js.map +1 -1
  129. package/package.json +6 -6
  130. package/src/authorization/__tests__/list-read-scope.test.ts +23 -12
  131. package/src/authorization/__tests__/skip-lane-inventory.test.ts +160 -0
  132. package/src/authorization/__tests__/wire-permissions.test.ts +5 -5
  133. package/src/authorization/evaluator.ts +7 -0
  134. package/src/authorization/list-read-scope.ts +22 -11
  135. package/src/authorization/model/__tests__/registry.test.ts +2 -0
  136. package/src/authorization/model/organization.ts +2 -0
  137. package/src/domain/agent/controller.ts +10 -0
  138. package/src/domain/agentchannel/__tests__/create-authorization.test.ts +58 -0
  139. package/src/domain/agentchannel/controller.ts +19 -0
  140. package/src/domain/agentchannel/steps.ts +40 -0
  141. package/src/domain/agentinstance/__tests__/create-authorization.composed.test.ts +243 -0
  142. package/src/domain/agentinstance/__tests__/create-authorization.test.ts +144 -0
  143. package/src/domain/agentinstance/controller.ts +24 -5
  144. package/src/domain/agentinstance/steps.ts +82 -8
  145. package/src/domain/agentshare/__tests__/create-authorization.test.ts +86 -0
  146. package/src/domain/agentshare/__tests__/member-audience.test.ts +113 -0
  147. package/src/domain/agentshare/controller.ts +21 -0
  148. package/src/domain/agentshare/steps.ts +181 -15
  149. package/src/domain/artifact/controller.ts +18 -22
  150. package/src/domain/channelapp/controller.ts +10 -0
  151. package/src/domain/environment/controller.ts +10 -0
  152. package/src/domain/executioncontext/__tests__/executioncontext.test.ts +1 -0
  153. package/src/domain/executioncontext/controller.ts +10 -0
  154. package/src/domain/executioncontext/steps.ts +2 -1
  155. package/src/domain/mcpserver/connect.ts +11 -10
  156. package/src/domain/mcpserver/controller.ts +13 -1
  157. package/src/domain/mcpserver/start-connect.ts +17 -11
  158. package/src/domain/memory/__tests__/create-authorization.test.ts +59 -0
  159. package/src/domain/memory/controller.ts +11 -0
  160. package/src/domain/memory/steps.ts +41 -0
  161. package/src/domain/oauthapp/controller.ts +10 -0
  162. package/src/domain/plugin/controller.ts +22 -36
  163. package/src/domain/schedule/__tests__/create-authorization.test.ts +58 -0
  164. package/src/domain/schedule/controller.ts +20 -0
  165. package/src/domain/schedule/steps.ts +45 -2
  166. package/src/domain/session/steps.ts +24 -32
  167. package/src/domain/skill/controller.ts +24 -23
  168. package/src/domain/workflow/controller.ts +58 -145
  169. package/src/domain/workflow/version-resolution.ts +166 -0
  170. package/src/domain/workflowinstance/__tests__/create-authorization.test.ts +109 -0
  171. package/src/domain/workflowinstance/controller.ts +19 -0
  172. package/src/domain/workflowinstance/steps.ts +49 -1
  173. package/src/extensions/__tests__/composed-support.ts +38 -4
  174. package/src/extensions/__tests__/identity.test.ts +44 -1
  175. package/src/extensions/__tests__/list-read-scope-composed.test.ts +87 -1
  176. package/src/extensions/__tests__/list-read-scope.test.ts +63 -0
  177. package/src/extensions/identity.ts +23 -5
  178. package/src/extensions/list-read-scope.ts +47 -7
  179. package/src/pipeline/steps/__tests__/authorize-resolved-target.test.ts +285 -0
  180. package/src/pipeline/steps/authorize-resolved-target.ts +129 -0
  181. package/src/pipeline/steps/authorize.ts +70 -13
  182. package/src/pipeline/steps/guard-memory-capture.ts +2 -1
  183. package/src/pipeline/steps/guard-reserved-labels.ts +6 -9
  184. package/src/pipeline/steps/helpers.ts +11 -15
  185. package/src/pipeline/steps/version-history.ts +34 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@stigmer/server",
3
- "version": "3.21.0",
3
+ "version": "3.22.0",
4
4
  "description": "The Stigmer OSS control plane as a composable library: composeServer, the extension registry and the driver seams a composition builds on. The self-contained deployable of the same server is @stigmer/server-slim.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -62,11 +62,11 @@
62
62
  "@connectrpc/connect-node": "^2.0.0",
63
63
  "@kubernetes/client-node": "2.0.0",
64
64
  "@opentelemetry/api": "^1.9.0",
65
- "@stigmer/outbound": "3.21.0",
66
- "@stigmer/plugin-package": "3.21.0",
67
- "@stigmer/protos": "3.21.0",
68
- "@stigmer/temporal-codecs": "3.21.0",
69
- "@stigmer/zip-structure": "3.21.0",
65
+ "@stigmer/outbound": "3.22.0",
66
+ "@stigmer/plugin-package": "3.22.0",
67
+ "@stigmer/protos": "3.22.0",
68
+ "@stigmer/temporal-codecs": "3.22.0",
69
+ "@stigmer/zip-structure": "3.22.0",
70
70
  "@temporalio/activity": "1.16.2",
71
71
  "@temporalio/client": "1.16.2",
72
72
  "@temporalio/common": "1.16.2",
@@ -17,7 +17,9 @@
17
17
  * org-observable reaches the organization's viewers; a memory with no
18
18
  * subject is nobody's; an instance reaches whoever reads its blueprint
19
19
  * only when it is the default. Then the contract: a scope only narrows
20
- * and never reorders; the `internal` class is the in-process skip; an
20
+ * and never reorders; the `internal` class is the in-process skip on the
21
+ * enumeration verb and a loud consumer bug on the restrict verb (the
22
+ * shared helper answers the class before any driver, stigmer#1207); an
21
23
  * unprovisioned subject is their raw subject (the 3.14 verifiers' stamp); a
22
24
  * fault throws and is never an empty answer; an undeclared kind is a
23
25
  * consumer bug, loud; the enumeration verb equals the restrict verb over
@@ -583,17 +585,7 @@ describe.each(driverFixtures(SEEDED_KINDS))(
583
585
  expect(accountReads).toBe(0);
584
586
  });
585
587
 
586
- it("the `internal` class — the server acting as itself — keeps every offered id and enumerates every id of the kind: the in-process authorization skip, with no account even asked for", async () => {
587
- const offered = rowsOf(ApiResourceKind.agent).map((row) =>
588
- entryOf(ApiResourceKind.agent, row),
589
- );
590
- expect([
591
- ...(await scope().restrictListEntries(
592
- INTERNAL,
593
- ApiResourceKind.agent,
594
- offered,
595
- )),
596
- ]).toEqual(offered.map((entry) => entry.id));
588
+ it("the `internal` class on the enumeration verb — the server acting as itself — is every id of the kind, with no account even asked for (the in-process skip; no helper stands before this verb)", async () => {
597
589
  expect(
598
590
  [
599
591
  ...(await scope().authorizedResourceIds(
@@ -605,6 +597,25 @@ describe.each(driverFixtures(SEEDED_KINDS))(
605
597
  expect(accountReads).toBe(0);
606
598
  });
607
599
 
600
+ it("the `internal` class on the restrict verb is a consumer bug, loud: the shared helper answers that class before any driver, so a driver offered it was reached around the idiom", async () => {
601
+ const offered = rowsOf(ApiResourceKind.agent).map((row) =>
602
+ entryOf(ApiResourceKind.agent, row),
603
+ );
604
+ const refusal = await scope()
605
+ .restrictListEntries(INTERNAL, ApiResourceKind.agent, offered)
606
+ .then(
607
+ () => undefined,
608
+ (error: unknown) => error,
609
+ );
610
+ expect(refusal).toBeInstanceOf(AuthorizationEvaluationError);
611
+ expect((refusal as AuthorizationEvaluationError).reason).toBe(
612
+ "internal-caller-offered",
613
+ );
614
+ // Refused before any read: the server was never evaluated as a person.
615
+ expect(accountReads).toBe(0);
616
+ expect(storeReads).toEqual({ rows: 0, scans: 0 });
617
+ });
618
+
608
619
  it("an unprovisioned subject is their raw subject and nothing else: the 3.14-stamped row is theirs, the organization's rows are not", async () => {
609
620
  // `agt_legacy` is stamped with LEGACY's raw subject; a caller
610
621
  // who IS that subject and holds no account reaches it through
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Pins the coverage inventory's own rule for `is_skip_authorization` lanes
3
+ * (docs/authorization-coverage.md, "How to read the tables"): a skip lane
4
+ * reaches no Authorizer in either edition, so what guards it is whatever
5
+ * its Handler cell names — and a cell that names nothing is a lane open to
6
+ * every authenticated caller that nobody has said so about. Two things are
7
+ * held here, so the doc cannot drift from the server it describes:
8
+ *
9
+ * 1. The set of skip-annotated methods the open-source server SERVES
10
+ * (the empty composition's routes replayed into a recorder, a method
11
+ * counted only when its service's implementation map carries it — a
12
+ * partially implemented service lists more on its descriptor than it
13
+ * serves) equals the set of skip rows in the doc, service by method.
14
+ * A new skip lane without a row, or a row for a lane that is no
15
+ * longer a skip, fails here.
16
+ * 2. Every skip row's Handler cell carries one of the three markers the
17
+ * doc defines — `guard: <Name>`, `driver: <Seam>`,
18
+ * `by design: <reason>` — and every `guard:` names a pipeline step or
19
+ * a handler's guard function that exists by that name in `src/`
20
+ * outside the tests, so a renamed or removed guard cannot leave the
21
+ * doc pointing at nothing.
22
+ *
23
+ * The rule lives in the doc; this test only enforces it. The doc is the
24
+ * one inventory (the package guide mandates updating it with every
25
+ * annotation change), and a second list here would be the drift this
26
+ * test exists to prevent.
27
+ */
28
+ import {
29
+ mkdtempSync,
30
+ readFileSync,
31
+ readdirSync,
32
+ rmSync,
33
+ statSync,
34
+ } from "node:fs";
35
+ import { tmpdir } from "node:os";
36
+ import path from "node:path";
37
+ import { fileURLToPath } from "node:url";
38
+
39
+ import { getOption } from "@bufbuild/protobuf";
40
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
41
+
42
+ import { is_skip_authorization } from "@stigmer/protos/ai/stigmer/commons/rpc/method_options_pb";
43
+
44
+ import { loadConfig } from "../../boot/config.js";
45
+ import { composeServer } from "../../boot/compose.js";
46
+ import type { ComposedServer } from "../../boot/compose.js";
47
+ import {
48
+ baseConfig,
49
+ servedMethods,
50
+ silentLogger,
51
+ } from "../../extensions/__tests__/composed-support.js";
52
+
53
+ const HERE = path.dirname(fileURLToPath(import.meta.url));
54
+ const PACKAGE_ROOT = path.resolve(HERE, "../../..");
55
+ const COVERAGE_DOC = path.join(PACKAGE_ROOT, "docs/authorization-coverage.md");
56
+ const SRC = path.join(PACKAGE_ROOT, "src");
57
+
58
+ /** A skip row: `| Service.method | is_skip_authorization | <handler cell> |`. */
59
+ const SKIP_ROW =
60
+ /^\| ([A-Za-z]+)\.([A-Za-z]+) \| is_skip_authorization \| (.*) \|$/;
61
+
62
+ const MARKERS = ["guard:", "driver:", "by design:"] as const;
63
+
64
+ interface SkipRow {
65
+ readonly method: string;
66
+ readonly handler: string;
67
+ }
68
+
69
+ function skipRowsOf(doc: string): SkipRow[] {
70
+ const rows: SkipRow[] = [];
71
+ for (const line of doc.split("\n")) {
72
+ const m = SKIP_ROW.exec(line);
73
+ if (m !== null) {
74
+ rows.push({ method: `${m[1]}.${m[2]}`, handler: m[3] ?? "" });
75
+ }
76
+ }
77
+ return rows;
78
+ }
79
+
80
+ /** Every `guard: <Name>` a handler cell names. */
81
+ function guardsOf(handler: string): string[] {
82
+ return [...handler.matchAll(/guard: ([A-Za-z]+)/g)].map((m) => m[1] ?? "");
83
+ }
84
+
85
+ /** Every non-test `.ts` source under `src/`, read once. */
86
+ function sourceText(): string {
87
+ const parts: string[] = [];
88
+ const walk = (dir: string): void => {
89
+ for (const entry of readdirSync(dir)) {
90
+ const full = path.join(dir, entry);
91
+ if (statSync(full).isDirectory()) {
92
+ if (entry !== "__tests__") {
93
+ walk(full);
94
+ }
95
+ } else if (full.endsWith(".ts") && !full.endsWith(".test.ts")) {
96
+ parts.push(readFileSync(full, "utf8"));
97
+ }
98
+ }
99
+ };
100
+ walk(SRC);
101
+ return parts.join("\n");
102
+ }
103
+
104
+ describe("the skip-lane inventory (docs/authorization-coverage.md) is true to the served server", () => {
105
+ let dir: string;
106
+ let server: ComposedServer;
107
+
108
+ beforeAll(async () => {
109
+ dir = mkdtempSync(path.join(tmpdir(), "skip-lane-inventory-test-"));
110
+ server = await composeServer({
111
+ config: loadConfig(baseConfig(dir)),
112
+ logger: silentLogger,
113
+ portOverride: 0,
114
+ host: "127.0.0.1",
115
+ });
116
+ });
117
+
118
+ afterAll(async () => {
119
+ await server.shutdown();
120
+ rmSync(dir, { recursive: true, force: true });
121
+ });
122
+
123
+ it("the doc's skip rows are exactly the skip-annotated methods the server serves", () => {
124
+ const served = new Set<string>();
125
+ for (const method of servedMethods(server.routes)) {
126
+ if (getOption(method, is_skip_authorization)) {
127
+ const serviceName = method.parent.typeName.split(".").at(-1) ?? "";
128
+ served.add(`${serviceName}.${method.name}`);
129
+ }
130
+ }
131
+ const documented = new Set(
132
+ skipRowsOf(readFileSync(COVERAGE_DOC, "utf8")).map((r) => r.method),
133
+ );
134
+ expect([...served].sort()).toEqual([...documented].sort());
135
+ });
136
+
137
+ it("every skip row names its guard, its driver or says the lane is open by design", () => {
138
+ const bare = skipRowsOf(readFileSync(COVERAGE_DOC, "utf8"))
139
+ .filter((r) => !MARKERS.some((marker) => r.handler.includes(marker)))
140
+ .map((r) => r.method);
141
+ expect(bare).toEqual([]);
142
+ });
143
+
144
+ it("every guard a skip row names is a step or a guard function that exists by that name in src/", () => {
145
+ const source = sourceText();
146
+ const dangling: string[] = [];
147
+ for (const row of skipRowsOf(readFileSync(COVERAGE_DOC, "utf8"))) {
148
+ for (const guard of guardsOf(row.handler)) {
149
+ // A step is registered by its quoted name; a guard function by its
150
+ // declaration.
151
+ const isStep = source.includes(`"${guard}"`);
152
+ const isFunction = new RegExp(`function ${guard}\\(`).test(source);
153
+ if (!isStep && !isFunction) {
154
+ dangling.push(`${row.method} names guard ${guard}`);
155
+ }
156
+ }
157
+ }
158
+ expect(dangling).toEqual([]);
159
+ });
160
+ });
@@ -117,12 +117,11 @@ describe("every static (kind, permission) the served RPCs ask about is a declare
117
117
  expect(undeclared).toEqual([]);
118
118
  });
119
119
 
120
- it("asks about every served kind the model declares except the two no RPC targets statically", () => {
120
+ it("asks about every served kind the model declares except the one no RPC targets statically", () => {
121
121
  // iam_policy's RPCs target the policy's RESOURCE (`resource_kind_path`
122
- // or the load-then-authorize `get`); execution_context's RPCs are
123
- // `is_skip_authorization` lanes (or unannotated) whose handlers gate on
124
- // the runner's execution-scoped token. Every other declared kind is
125
- // asked about by at least one static annotation.
122
+ // or the load-then-authorize `get`). Every other declared kind is asked
123
+ // about by at least one static annotation — execution_context through
124
+ // its `get` and `delete`; its runner-token lane stays a skip.
126
125
  const asked = new Set(wireQuestions().map((q) => kindEnumName(q.kind)));
127
126
  expect([...asked].sort()).toEqual(
128
127
  [
@@ -135,6 +134,7 @@ describe("every static (kind, permission) the served RPCs ask about is a declare
135
134
  "artifact",
136
135
  "channel_app",
137
136
  "environment",
137
+ "execution_context",
138
138
  "identity_account",
139
139
  "mcp_server",
140
140
  "memory",
@@ -67,8 +67,15 @@ import {
67
67
  */
68
68
  export const MAX_RESOLUTION_DEPTH = 25;
69
69
 
70
+ /**
71
+ * The faults an evaluation can raise. Two are consumer bugs by the seam
72
+ * contracts (a kind the model does not declare; the `internal` class
73
+ * offered to the list scope's restrict verb, which the shared helper
74
+ * answers before any driver); the rest are the model's own.
75
+ */
70
76
  export type EvaluationFault =
71
77
  | "undeclared-target-kind"
78
+ | "internal-caller-offered"
72
79
  | "resolution-depth-exceeded"
73
80
  | "resolution-cycle"
74
81
  | "unknown-condition";
@@ -38,17 +38,25 @@
38
38
  * lane can show it either.
39
39
  *
40
40
  * The `internal` class — the server acting as itself over the in-process
41
- * transport — keeps every offered id and enumerates every id of the kind:
42
- * the in-process authorization skip (pipeline/interceptors/auth.ts, ruling
43
- * Q4) applied to a list answer, as the built-in directory applies it to a
44
- * directory answer. No in-process edge reaches a list lane today; the arm
45
- * is pinned so a future one meets the same rule every other lane keeps.
41
+ * transport — is answered by the seam, not here: `restrictListByReadScope`
42
+ * returns the org-narrowed rows for the class before any driver is asked
43
+ * (extensions/list-read-scope.ts, the header's contract line), the
44
+ * Authorize step's trust-domain rule applied to a list answer. So this
45
+ * driver's `restrictListEntries` REFUSES the class (`internal-caller-offered`):
46
+ * being offered it means a caller reached the driver around the one
47
+ * consumption idiom, and evaluating the server as a person would return
48
+ * a quiet short list — the failure shape stigmer#1207 fixed. The
49
+ * enumeration verb has no helper in front of it and keeps its own arm:
50
+ * every id of the kind, as the built-in directory answers the class with
51
+ * every organization. No in-process edge reaches an enumeration lane
52
+ * today; the arm is pinned so a future one meets the rule.
46
53
  *
47
54
  * Faults THROW and are never an empty answer (the seam: an empty set means
48
55
  * "authorized to see nothing"; an outage is the pipeline's sanitized
49
- * INTERNAL). A kind the model does not declare is a consumer bug by the
50
- * seam's contract and faults through the evaluator, loud; the model
51
- * declares exactly the open-source tier, so no served lane can reach it.
56
+ * INTERNAL). A kind the model does not declare, like the internal class
57
+ * above, is a consumer bug by the seam's contract and faults through the
58
+ * evaluator, loud; the model declares exactly the open-source tier, so no
59
+ * served lane can reach it.
52
60
  *
53
61
  * Logging. One debug line per call — kind, offered, kept, elapsed — what
54
62
  * an operator needs to answer "why is Alice's list short" beside the
@@ -188,6 +196,12 @@ export function newBuiltInListReadScope(
188
196
  entries: ReadonlyArray<ListEntryMeta>,
189
197
  ): Promise<ReadonlySet<string>> {
190
198
  requireDeclared(kind);
199
+ if (caller.callerClass === "internal") {
200
+ throw new AuthorizationEvaluationError(
201
+ "internal-caller-offered",
202
+ `the internal caller class was offered to the list scope for kind '${kindEnumName(kind)}' — the shared helper answers that class before any driver; a list lane reached the driver around it`,
203
+ );
204
+ }
191
205
  // One candidate per unique id: a lane offers each row once, and a
192
206
  // duplicate would only spend a walk to learn the same answer.
193
207
  const unique = new Map<string, RowFacts>();
@@ -196,9 +210,6 @@ export function newBuiltInListReadScope(
196
210
  unique.set(entry.id, rowFactsOfEntry(kind, entry));
197
211
  }
198
212
  }
199
- if (caller.callerClass === "internal") {
200
- return new Set(unique.keys());
201
- }
202
213
  return visibleAmong(caller, kind, [...unique.values()]);
203
214
  },
204
215
 
@@ -104,6 +104,7 @@ const WIRE_PERMISSIONS_BY_TYPE: Readonly<
104
104
  "can_create_environment",
105
105
  "can_create_skill",
106
106
  "can_create_plugin",
107
+ "can_create_mcp_server",
107
108
  "can_create_idp",
108
109
  "can_create_identity_account",
109
110
  "can_create_oauth_app",
@@ -111,6 +112,7 @@ const WIRE_PERMISSIONS_BY_TYPE: Readonly<
111
112
  "can_create_channel_app",
112
113
  "can_create_execution_in",
113
114
  "can_create_agent_share",
115
+ "can_create_agent_instance",
114
116
  "can_grant_access",
115
117
  "can_view_access",
116
118
  "can_view_billing",
@@ -39,6 +39,7 @@ export const organizationDeclaration = declareKind({
39
39
  ["can_create_environment", computed("member")],
40
40
  ["can_create_skill", computed("admin")],
41
41
  ["can_create_plugin", computed("admin")],
42
+ ["can_create_mcp_server", computed("admin")],
42
43
  ["can_create_idp", computed("admin")],
43
44
  ["can_create_identity_account", computed("admin")],
44
45
  ["can_create_oauth_app", computed("admin")],
@@ -46,6 +47,7 @@ export const organizationDeclaration = declareKind({
46
47
  ["can_create_channel_app", computed("admin")],
47
48
  ["can_create_execution_in", union(computed("member"), computed("guest"))],
48
49
  ["can_create_agent_share", computed("admin")],
50
+ ["can_create_agent_instance", computed("member")],
49
51
  ["can_grant_access", computed("admin")],
50
52
  ["can_view_access", computed("viewer")],
51
53
  ["can_view_billing", computed("viewer")],
@@ -37,6 +37,10 @@ import type { PipelineStep } from "../../pipeline/pipeline.js";
37
37
  import { callerIdentityOf } from "../../pipeline/interceptors/auth.js";
38
38
  import { RequestContext } from "../../pipeline/request-context.js";
39
39
  import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
40
+ import {
41
+ loadedTargetAsMethod,
42
+ newAuthorizeResolvedTargetStep,
43
+ } from "../../pipeline/steps/authorize-resolved-target.js";
40
44
  import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
41
45
  import {
42
46
  newAuthorizeVisibilityTransitionStep,
@@ -527,6 +531,12 @@ async function getByReference(
527
531
  )
528
532
  .addStep(newValidateProtoStep())
529
533
  .addStep(newLoadByReferenceStep(deps.store, AgentSchema))
534
+ .addStep(
535
+ newAuthorizeResolvedTargetStep(
536
+ deps.authorizer,
537
+ loadedTargetAsMethod(AgentQueryController.method.get),
538
+ ),
539
+ )
530
540
  .build()
531
541
  .execute(reqCtx);
532
542
  return reqCtx.get(TARGET_RESOURCE_KEY) as Agent;
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Pins the create lane's authorization question (steps.ts,
3
+ * resolveChannelCreateTargets): can_edit on the REFERENCED AGENT with the
4
+ * Java handler's copy, and nothing on the organization (the same-org
5
+ * invariant already binds the channel to the agent's); a missing
6
+ * referenced agent throws.
7
+ */
8
+ import { describe, expect, it } from "vitest";
9
+ import { create } from "@bufbuild/protobuf";
10
+
11
+ import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
12
+ import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
13
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
14
+ import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
15
+
16
+ import { testCallerIdentity } from "../../../pipeline/__tests__/support.js";
17
+ import { RequestContext } from "../../../pipeline/request-context.js";
18
+ import {
19
+ CHANNEL_CREATE_DENIED_MESSAGE,
20
+ REFERENCED_AGENT_KEY,
21
+ resolveChannelCreateTargets,
22
+ } from "../steps.js";
23
+
24
+ const agent = create(AgentSchema, {
25
+ metadata: { id: "agt_01target", name: "helper", org: "acme" },
26
+ });
27
+
28
+ function ctxFor(stash = true) {
29
+ const ctx = new RequestContext(
30
+ AgentChannelSchema,
31
+ create(AgentChannelSchema, { metadata: { name: "support", org: "acme" } }),
32
+ testCallerIdentity(),
33
+ ApiResourceKind.agent_channel,
34
+ );
35
+ if (stash) {
36
+ ctx.set(REFERENCED_AGENT_KEY, agent);
37
+ }
38
+ return ctx;
39
+ }
40
+
41
+ describe("resolveChannelCreateTargets", () => {
42
+ it("asks can_edit on the referenced agent and nothing else", () => {
43
+ expect(resolveChannelCreateTargets(ctxFor())).toEqual([
44
+ {
45
+ permission: IamPermission.can_edit,
46
+ resourceKind: ApiResourceKind.agent,
47
+ resourceId: "agt_01target",
48
+ deniedMessage: CHANNEL_CREATE_DENIED_MESSAGE,
49
+ },
50
+ ]);
51
+ });
52
+
53
+ it("a missing referenced agent is a broken chain invariant and throws", () => {
54
+ expect(() => resolveChannelCreateTargets(ctxFor(false))).toThrow(
55
+ "referenced agent not found in context",
56
+ );
57
+ });
58
+ });
@@ -66,6 +66,10 @@ import {
66
66
  authorizeDirect,
67
67
  newAuthorizeStep,
68
68
  } from "../../pipeline/steps/authorize.js";
69
+ import {
70
+ loadedTargetAsMethod,
71
+ newAuthorizeResolvedTargetStep,
72
+ } from "../../pipeline/steps/authorize-resolved-target.js";
69
73
  import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
70
74
  import { newBuildNewStateStep } from "../../pipeline/steps/defaults.js";
71
75
  import { newBuildUpdateStateStep } from "../../pipeline/steps/build-update-state.js";
@@ -110,6 +114,7 @@ import {
110
114
  newResolveChannelDefaultsStep,
111
115
  newTeardownChannelRuntimeStep,
112
116
  newValidateChannelUpdateStep,
117
+ resolveChannelCreateTargets,
113
118
  } from "./steps.js";
114
119
 
115
120
  export interface AgentChannelControllerDeps {
@@ -191,6 +196,14 @@ async function createChannel(
191
196
  deps.channelRuntime,
192
197
  ),
193
198
  )
199
+ // Before the duplicate check and the install state (steps.ts,
200
+ // resolveChannelCreateTargets).
201
+ .addStep(
202
+ newAuthorizeResolvedTargetStep(
203
+ deps.authorizer,
204
+ resolveChannelCreateTargets,
205
+ ),
206
+ )
194
207
  .addStep(newResolveSlugStep())
195
208
  .addStep(newCheckDuplicateStep(deps.store))
196
209
  .addStep(newBuildNewStateStep())
@@ -499,6 +512,12 @@ async function getByReference(
499
512
  )
500
513
  .addStep(newValidateProtoStep())
501
514
  .addStep(newLoadByReferenceStep(deps.store, AgentChannelSchema))
515
+ .addStep(
516
+ newAuthorizeResolvedTargetStep(
517
+ deps.authorizer,
518
+ loadedTargetAsMethod(AgentChannelQueryController.method.get),
519
+ ),
520
+ )
502
521
  .build()
503
522
  .execute(reqCtx);
504
523
  return reqCtx.get(TARGET_RESOURCE_KEY) as AgentChannel;
@@ -21,7 +21,9 @@ import {
21
21
  AgentChannelStatusSchema,
22
22
  } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/status_pb";
23
23
  import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
24
+ import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
24
25
  import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
26
+ import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
25
27
 
26
28
  import {
27
29
  failedPreconditionError,
@@ -31,6 +33,7 @@ import {
31
33
  } from "../../pipeline/errors.js";
32
34
  import type { PipelineStep } from "../../pipeline/pipeline.js";
33
35
  import type { RequestContext } from "../../pipeline/request-context.js";
36
+ import type { AuthorizationTarget } from "../../pipeline/steps/authorize.js";
34
37
  import { findResourceBySlug } from "../../pipeline/steps/helpers.js";
35
38
  import { EXISTING_RESOURCE_KEY } from "../../pipeline/steps/load-existing.js";
36
39
  import type { Store } from "../../store/interface.js";
@@ -161,6 +164,7 @@ export function newResolveChannelDefaultsStep(
161
164
  }
162
165
 
163
166
  agentRef!.org = refOrg;
167
+ ctx.set(REFERENCED_AGENT_KEY, agent);
164
168
 
165
169
  // Write-time constraints only a serving runtime can state (the
166
170
  // pin-REQUIRED rule and its successors — channel-runtime.ts).
@@ -174,6 +178,42 @@ export function newResolveChannelDefaultsStep(
174
178
  };
175
179
  }
176
180
 
181
+ /**
182
+ * Context key for the agent ResolveChannelDefaults resolved from
183
+ * spec.agent_ref, read by the create lane's authorization step.
184
+ */
185
+ export const REFERENCED_AGENT_KEY = "agentChannelReferencedAgent";
186
+
187
+ /** The deny copy of the create bar, the Java AgentChannelCreateHandler's. */
188
+ export const CHANNEL_CREATE_DENIED_MESSAGE =
189
+ "You don't have permission to connect this agent to a channel";
190
+
191
+ /**
192
+ * The create lane's authorization question, for AuthorizeResolvedTarget
193
+ * after ResolveChannelDefaults: can_edit on the REFERENCED AGENT. The RPC
194
+ * is is_skip_authorization because its target is that agent, resolved from
195
+ * a slug, not a request field. Binding an agent to a channel puts it in
196
+ * front of a wider audience, an editor's act; the same-org invariant the
197
+ * resolve step enforces already binds the channel's organization to the
198
+ * agent's, so there is no separate organization bar (command.proto).
199
+ */
200
+ export function resolveChannelCreateTargets(
201
+ ctx: RequestContext<AgentChannelDesc>,
202
+ ): ReadonlyArray<AuthorizationTarget> {
203
+ const agent = ctx.get(REFERENCED_AGENT_KEY) as Agent | undefined;
204
+ if (agent === undefined) {
205
+ throw new Error("referenced agent not found in context");
206
+ }
207
+ return [
208
+ {
209
+ permission: IamPermission.can_edit,
210
+ resourceKind: ApiResourceKind.agent,
211
+ resourceId: agent.metadata?.id ?? "",
212
+ deniedMessage: CHANNEL_CREATE_DENIED_MESSAGE,
213
+ },
214
+ ];
215
+ }
216
+
177
217
  /**
178
218
  * InitInstallState — Go initInstallStateStep: writes
179
219
  * status.install_state = pending_install; the channel exists but serves