@stigmer/server 3.14.0 → 3.14.1

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 (68) hide show
  1. package/dist/domain/agentexecution/constants.d.ts +16 -0
  2. package/dist/domain/agentexecution/constants.d.ts.map +1 -1
  3. package/dist/domain/agentexecution/constants.js +22 -0
  4. package/dist/domain/agentexecution/constants.js.map +1 -1
  5. package/dist/domain/agentexecution/controller.d.ts.map +1 -1
  6. package/dist/domain/agentexecution/controller.js +10 -3
  7. package/dist/domain/agentexecution/controller.js.map +1 -1
  8. package/dist/domain/agentexecution/run-target.d.ts +26 -0
  9. package/dist/domain/agentexecution/run-target.d.ts.map +1 -0
  10. package/dist/domain/agentexecution/run-target.js +31 -0
  11. package/dist/domain/agentexecution/run-target.js.map +1 -0
  12. package/dist/domain/session/constants.d.ts +15 -0
  13. package/dist/domain/session/constants.d.ts.map +1 -0
  14. package/dist/domain/session/constants.js +17 -0
  15. package/dist/domain/session/constants.js.map +1 -0
  16. package/dist/domain/session/controller.d.ts.map +1 -1
  17. package/dist/domain/session/controller.js +10 -0
  18. package/dist/domain/session/controller.js.map +1 -1
  19. package/dist/domain/session/run-target.d.ts +15 -0
  20. package/dist/domain/session/run-target.d.ts.map +1 -0
  21. package/dist/domain/session/run-target.js +14 -0
  22. package/dist/domain/session/run-target.js.map +1 -0
  23. package/dist/domain/workflowexecution/constants.d.ts +9 -0
  24. package/dist/domain/workflowexecution/constants.d.ts.map +1 -1
  25. package/dist/domain/workflowexecution/constants.js +13 -0
  26. package/dist/domain/workflowexecution/constants.js.map +1 -1
  27. package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
  28. package/dist/domain/workflowexecution/controller.js +8 -0
  29. package/dist/domain/workflowexecution/controller.js.map +1 -1
  30. package/dist/domain/workflowexecution/run-target.d.ts +18 -0
  31. package/dist/domain/workflowexecution/run-target.d.ts.map +1 -0
  32. package/dist/domain/workflowexecution/run-target.js +23 -0
  33. package/dist/domain/workflowexecution/run-target.js.map +1 -0
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +7 -0
  37. package/dist/index.js.map +1 -1
  38. package/dist/pipeline/steps/authorize-run-target.d.ts +105 -0
  39. package/dist/pipeline/steps/authorize-run-target.d.ts.map +1 -0
  40. package/dist/pipeline/steps/authorize-run-target.js +61 -0
  41. package/dist/pipeline/steps/authorize-run-target.js.map +1 -0
  42. package/dist/query/search/criteria.d.ts +10 -3
  43. package/dist/query/search/criteria.d.ts.map +1 -1
  44. package/dist/query/search/criteria.js +11 -4
  45. package/dist/query/search/criteria.js.map +1 -1
  46. package/package.json +4 -4
  47. package/src/domain/agentexecution/__tests__/run-gate.test.ts +245 -0
  48. package/src/domain/agentexecution/__tests__/run-target.test.ts +117 -0
  49. package/src/domain/agentexecution/constants.ts +25 -0
  50. package/src/domain/agentexecution/controller.ts +10 -3
  51. package/src/domain/agentexecution/run-target.ts +64 -0
  52. package/src/domain/session/__tests__/run-gate.test.ts +219 -0
  53. package/src/domain/session/__tests__/run-target.test.ts +43 -0
  54. package/src/domain/session/constants.ts +17 -0
  55. package/src/domain/session/controller.ts +10 -0
  56. package/src/domain/session/run-target.ts +30 -0
  57. package/src/domain/workflowexecution/__tests__/run-gate.test.ts +200 -0
  58. package/src/domain/workflowexecution/__tests__/run-target.test.ts +80 -0
  59. package/src/domain/workflowexecution/constants.ts +17 -0
  60. package/src/domain/workflowexecution/controller.ts +10 -0
  61. package/src/domain/workflowexecution/run-target.ts +47 -0
  62. package/src/extensions/__tests__/extension-composition.test.ts +49 -0
  63. package/src/index.ts +7 -0
  64. package/src/pipeline/__tests__/support.ts +34 -0
  65. package/src/pipeline/steps/__tests__/authorize-run-target.test.ts +261 -0
  66. package/src/pipeline/steps/authorize-run-target.ts +148 -0
  67. package/src/query/search/__tests__/criteria.test.ts +18 -1
  68. package/src/query/search/criteria.ts +11 -4
@@ -0,0 +1,245 @@
1
+ /**
2
+ * Pins the run gate's SPLICE in agent-execution-create over the real router
3
+ * (P1 sp.run-gate): AuthorizeRunTarget sits after EnsureSessionOrAgentResolved
4
+ * and BEFORE EnsureEngineAvailable, the pre-side-effect slot and
5
+ * CreateDefaultInstanceIfNeeded. Pinned here: (1) each request shape is
6
+ * checked against its own target with its own byte-pinned copy; (2) a
7
+ * denial leaves no execution row AND creates no default instance — the
8
+ * "no side effect precedes the check" property; (3) the gate precedes the
9
+ * engine gate: an ALLOWED create on this engineless server answers the
10
+ * engine's UNAVAILABLE, a DENIED one PERMISSION_DENIED. The authorizer
11
+ * under test answers only run-gate checks (runGateOnlyAuthorizer), so every
12
+ * refusal here is the run gate's and nobody else's.
13
+ */
14
+ import { mkdtempSync, rmSync } from "node:fs";
15
+ import { tmpdir } from "node:os";
16
+ import path from "node:path";
17
+
18
+ import { afterAll, beforeAll, describe, expect, it } from "vitest";
19
+ import type { Client, Transport } from "@connectrpc/connect";
20
+ import { Code, ConnectError, createClient } from "@connectrpc/connect";
21
+ import { createGrpcTransport } from "@connectrpc/connect-node";
22
+
23
+ import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
24
+ import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
25
+ import { AgentExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/command_pb";
26
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
27
+ import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
28
+
29
+ import { loadConfig } from "../../../boot/config.js";
30
+ import { composeServer } from "../../../boot/compose.js";
31
+ import type { ComposedServer } from "../../../boot/compose.js";
32
+ import { createLogger } from "../../../boot/logger.js";
33
+ import type { AuthzDecision } from "../../../extensions/authorizer.js";
34
+ import { runGateOnlyAuthorizer } from "../../../pipeline/__tests__/support.js";
35
+ import {
36
+ ENGINE_UNAVAILABLE_MESSAGE,
37
+ addExecutionToSessionDeniedMessage,
38
+ runAgentDeniedMessage,
39
+ runAgentInstanceDeniedMessage,
40
+ } from "../constants.js";
41
+
42
+ const silentLogger = createLogger({
43
+ level: "error",
44
+ pretty: false,
45
+ write: () => {},
46
+ });
47
+
48
+ const API_VERSION = "agentic.stigmer.ai/v1";
49
+ const ORG = "acme";
50
+
51
+ let dir: string;
52
+ let server: ComposedServer;
53
+ let decision: AuthzDecision = { kind: "allow" };
54
+ const gate = runGateOnlyAuthorizer(() => decision);
55
+ let agentCommand: Client<typeof AgentCommandController>;
56
+ let command: Client<typeof AgentExecutionCommandController>;
57
+
58
+ beforeAll(async () => {
59
+ dir = mkdtempSync(path.join(tmpdir(), "agentexecution-run-gate-test-"));
60
+ server = await composeServer({
61
+ config: loadConfig({
62
+ STIGMER_MODEL_REGISTRY_REFRESH: "off",
63
+ TEMPORAL_HOST_PORT: "127.0.0.1:1",
64
+ DB_PATH: path.join(dir, "stigmer.db"),
65
+ STORAGE_PATH: path.join(dir, "storage"),
66
+ ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
67
+ }),
68
+ logger: silentLogger,
69
+ portOverride: 0,
70
+ host: "127.0.0.1",
71
+ extensions: [{ name: "run-gate-test", authorizer: gate.authorizer }],
72
+ });
73
+ const port = await server.start();
74
+ const transport: Transport = createGrpcTransport({
75
+ baseUrl: `http://127.0.0.1:${port}`,
76
+ });
77
+ agentCommand = createClient(AgentCommandController, transport);
78
+ command = createClient(AgentExecutionCommandController, transport);
79
+ });
80
+
81
+ afterAll(async () => {
82
+ await server.shutdown();
83
+ rmSync(dir, { recursive: true, force: true });
84
+ });
85
+
86
+ async function count(kind: ApiResourceKind): Promise<number> {
87
+ return (await server.store.listResources(kind)).length;
88
+ }
89
+
90
+ /**
91
+ * An agent whose stored status names NO default instance, so a create by
92
+ * agent_id would have CreateDefaultInstanceIfNeeded mint one — the side
93
+ * effect the gate must precede.
94
+ */
95
+ async function createAgentWithoutDefaultInstance(
96
+ name: string,
97
+ ): Promise<string> {
98
+ const agent = await agentCommand.create({
99
+ apiVersion: API_VERSION,
100
+ kind: "Agent",
101
+ metadata: { name, org: ORG },
102
+ spec: { instructions: "You are the agent the run-gate tests target." },
103
+ });
104
+ const id = agent.metadata!.id;
105
+ const stored = await server.store.getResource(
106
+ ApiResourceKind.agent,
107
+ id,
108
+ AgentSchema,
109
+ );
110
+ if (stored.status !== undefined) {
111
+ stored.status.defaultInstanceId = "";
112
+ }
113
+ await server.store.saveResource(
114
+ ApiResourceKind.agent,
115
+ id,
116
+ AgentSchema,
117
+ stored,
118
+ );
119
+ return id;
120
+ }
121
+
122
+ async function captureError(
123
+ run: () => Promise<unknown>,
124
+ ): Promise<ConnectError> {
125
+ try {
126
+ await run();
127
+ } catch (error) {
128
+ return ConnectError.from(error);
129
+ }
130
+ throw new Error("expected the create to reject");
131
+ }
132
+
133
+ function createInput(spec: {
134
+ agentId?: string;
135
+ sessionId?: string;
136
+ sessionSpec?: { agentInstanceId: string };
137
+ }) {
138
+ return {
139
+ apiVersion: API_VERSION,
140
+ kind: "AgentExecution",
141
+ metadata: { name: "run-gate-exec", org: ORG },
142
+ spec: { message: "hello", ...spec },
143
+ };
144
+ }
145
+
146
+ describe("agent-execution-create run gate (composed server)", () => {
147
+ it("agent_id: denies with the agent copy, persists nothing, mints no default instance", async () => {
148
+ decision = { kind: "deny", reason: "" };
149
+ gate.runGateChecks.length = 0;
150
+ const agentId = await createAgentWithoutDefaultInstance("Private agent");
151
+ const executionsBefore = await count(ApiResourceKind.agent_execution);
152
+ const instancesBefore = await count(ApiResourceKind.agent_instance);
153
+
154
+ const err = await captureError(() =>
155
+ command.create(createInput({ agentId })),
156
+ );
157
+
158
+ expect(err.code).toBe(Code.PermissionDenied);
159
+ expect(err.rawMessage).toBe(runAgentDeniedMessage(agentId));
160
+ expect(gate.runGateChecks).toEqual([
161
+ {
162
+ permission: IamPermission.can_execute,
163
+ resourceKind: ApiResourceKind.agent,
164
+ resourceId: agentId,
165
+ },
166
+ ]);
167
+ expect(
168
+ await count(ApiResourceKind.agent_execution),
169
+ "no execution row",
170
+ ).toBe(executionsBefore);
171
+ expect(
172
+ await count(ApiResourceKind.agent_instance),
173
+ "no default instance minted",
174
+ ).toBe(instancesBefore);
175
+ const stored = await server.store.getResource(
176
+ ApiResourceKind.agent,
177
+ agentId,
178
+ AgentSchema,
179
+ );
180
+ expect(stored.status?.defaultInstanceId ?? "").toBe("");
181
+ });
182
+
183
+ it("session_id: denies with the session copy on session#can_create_execution_in", async () => {
184
+ decision = { kind: "deny", reason: "" };
185
+ gate.runGateChecks.length = 0;
186
+
187
+ const err = await captureError(() =>
188
+ command.create(createInput({ sessionId: "ses_01someoneelses" })),
189
+ );
190
+
191
+ expect(err.code).toBe(Code.PermissionDenied);
192
+ expect(err.rawMessage).toBe(
193
+ addExecutionToSessionDeniedMessage("ses_01someoneelses"),
194
+ );
195
+ expect(gate.runGateChecks).toEqual([
196
+ {
197
+ permission: IamPermission.can_create_execution_in,
198
+ resourceKind: ApiResourceKind.session,
199
+ resourceId: "ses_01someoneelses",
200
+ },
201
+ ]);
202
+ });
203
+
204
+ it("session_spec.agent_instance_id: denies with the instance copy", async () => {
205
+ decision = { kind: "deny", reason: "" };
206
+ gate.runGateChecks.length = 0;
207
+
208
+ const err = await captureError(() =>
209
+ command.create(
210
+ createInput({ sessionSpec: { agentInstanceId: "agi_01private" } }),
211
+ ),
212
+ );
213
+
214
+ expect(err.code).toBe(Code.PermissionDenied);
215
+ expect(err.rawMessage).toBe(runAgentInstanceDeniedMessage("agi_01private"));
216
+ expect(gate.runGateChecks[0]?.resourceKind).toBe(
217
+ ApiResourceKind.agent_instance,
218
+ );
219
+ });
220
+
221
+ it("not-found answers NOT_FOUND naming the target (the stigmer#224 order)", async () => {
222
+ decision = { kind: "not-found" };
223
+ const err = await captureError(() =>
224
+ command.create(createInput({ agentId: "agt_01missing" })),
225
+ );
226
+ expect(err.code).toBe(Code.NotFound);
227
+ expect(err.rawMessage).toContain("agt_01missing");
228
+ });
229
+
230
+ it("the gate precedes the engine gate: an ALLOWED create reaches EnsureEngineAvailable", async () => {
231
+ decision = { kind: "allow" };
232
+ gate.runGateChecks.length = 0;
233
+ const agentId = await createAgentWithoutDefaultInstance("Allowed agent");
234
+
235
+ const err = await captureError(() =>
236
+ command.create(createInput({ agentId })),
237
+ );
238
+
239
+ // The engineless composed server refuses at EnsureEngineAvailable —
240
+ // AFTER the run gate ran and allowed.
241
+ expect(err.code).toBe(Code.Unavailable);
242
+ expect(err.rawMessage).toBe(ENGINE_UNAVAILABLE_MESSAGE);
243
+ expect(gate.runGateChecks.map((c) => c.resourceId)).toEqual([agentId]);
244
+ });
245
+ });
@@ -0,0 +1,117 @@
1
+ /**
2
+ * Pins the agent-execution run-target resolver (P1 sp.run-gate, ruling
3
+ * Q-RG-2): the target is dispatched on the request shape in the CHAIN's
4
+ * own precedence — session_id, then session_spec.agent_instance_id, then
5
+ * agent_id (CreateDefaultInstanceIfNeeded / CreateSessionIfNeeded read
6
+ * them in exactly that order) — so the gate authorizes the target the
7
+ * chain will actually use. Each shape carries its own permission and its
8
+ * own byte-pinned deny copy; a record with none of the three has no
9
+ * target (EnsureSessionOrAgentResolved owns that arm as an invariant).
10
+ */
11
+ import { describe, expect, it } from "vitest";
12
+ import { create } from "@bufbuild/protobuf";
13
+
14
+ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
15
+ import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
16
+ import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
17
+
18
+ import {
19
+ addExecutionToSessionDeniedMessage,
20
+ runAgentDeniedMessage,
21
+ runAgentInstanceDeniedMessage,
22
+ } from "../constants.js";
23
+ import { agentExecutionRunTarget } from "../run-target.js";
24
+
25
+ describe("agentExecutionRunTarget", () => {
26
+ it("session_id → session#can_create_execution_in", () => {
27
+ expect(
28
+ agentExecutionRunTarget(
29
+ create(AgentExecutionSchema, { spec: { sessionId: "ses_01abc" } }),
30
+ ),
31
+ ).toEqual({
32
+ permission: IamPermission.can_create_execution_in,
33
+ resourceKind: ApiResourceKind.session,
34
+ resourceId: "ses_01abc",
35
+ deniedMessage: addExecutionToSessionDeniedMessage("ses_01abc"),
36
+ });
37
+ expect(addExecutionToSessionDeniedMessage("ses_01abc")).toBe(
38
+ "unauthorized to add an execution to session 'ses_01abc'",
39
+ );
40
+ });
41
+
42
+ it("session_spec.agent_instance_id → agent_instance#can_execute", () => {
43
+ expect(
44
+ agentExecutionRunTarget(
45
+ create(AgentExecutionSchema, {
46
+ spec: { sessionSpec: { agentInstanceId: "agi_01abc" } },
47
+ }),
48
+ ),
49
+ ).toEqual({
50
+ permission: IamPermission.can_execute,
51
+ resourceKind: ApiResourceKind.agent_instance,
52
+ resourceId: "agi_01abc",
53
+ deniedMessage: runAgentInstanceDeniedMessage("agi_01abc"),
54
+ });
55
+ expect(runAgentInstanceDeniedMessage("agi_01abc")).toBe(
56
+ "unauthorized to run agent instance 'agi_01abc'",
57
+ );
58
+ });
59
+
60
+ it("agent_id → agent#can_execute", () => {
61
+ expect(
62
+ agentExecutionRunTarget(
63
+ create(AgentExecutionSchema, { spec: { agentId: "agt_01abc" } }),
64
+ ),
65
+ ).toEqual({
66
+ permission: IamPermission.can_execute,
67
+ resourceKind: ApiResourceKind.agent,
68
+ resourceId: "agt_01abc",
69
+ deniedMessage: runAgentDeniedMessage("agt_01abc"),
70
+ });
71
+ expect(runAgentDeniedMessage("agt_01abc")).toBe(
72
+ "unauthorized to run agent 'agt_01abc'",
73
+ );
74
+ });
75
+
76
+ it("precedence is the chain's: session_id wins over both, the instance over agent_id", () => {
77
+ const all = agentExecutionRunTarget(
78
+ create(AgentExecutionSchema, {
79
+ spec: {
80
+ sessionId: "ses_01abc",
81
+ agentId: "agt_01abc",
82
+ sessionSpec: { agentInstanceId: "agi_01abc" },
83
+ },
84
+ }),
85
+ );
86
+ expect(all?.resourceKind).toBe(ApiResourceKind.session);
87
+ expect(all?.resourceId).toBe("ses_01abc");
88
+
89
+ const instanceAndAgent = agentExecutionRunTarget(
90
+ create(AgentExecutionSchema, {
91
+ spec: {
92
+ agentId: "agt_01abc",
93
+ sessionSpec: { agentInstanceId: "agi_01abc" },
94
+ },
95
+ }),
96
+ );
97
+ expect(instanceAndAgent?.resourceKind).toBe(ApiResourceKind.agent_instance);
98
+ expect(instanceAndAgent?.resourceId).toBe("agi_01abc");
99
+ });
100
+
101
+ it("answers no target when none of the three references is set", () => {
102
+ expect(
103
+ agentExecutionRunTarget(create(AgentExecutionSchema, {})),
104
+ ).toBeUndefined();
105
+ expect(
106
+ agentExecutionRunTarget(
107
+ create(AgentExecutionSchema, {
108
+ spec: {
109
+ sessionId: "",
110
+ agentId: "",
111
+ sessionSpec: { agentInstanceId: "" },
112
+ },
113
+ }),
114
+ ),
115
+ ).toBeUndefined();
116
+ });
117
+ });
@@ -28,3 +28,28 @@ export function executionUsageReportNotFoundMessage(
28
28
  ): string {
29
29
  return `agent execution '%s' not found not found: ${executionId}`;
30
30
  }
31
+
32
+ /**
33
+ * The run gate's deny copy per request shape (P1 sp.run-gate, ruling
34
+ * Q-RG-4; wire once merged, asserted by the conformance run-gate suite).
35
+ * NEW copy quotes the handle single-quoted (the ratified 2026-08-26 rule).
36
+ * An AgentExecution is a RUN in the ubiquitous language, hence "run";
37
+ * a turn added to an existing conversation is "an execution in a session",
38
+ * the session's own permission.
39
+ */
40
+ export function runAgentDeniedMessage(agentId: string): string {
41
+ return `unauthorized to run agent '${agentId}'`;
42
+ }
43
+
44
+ /**
45
+ * Identical to the session domain's instance copy on purpose — one fact,
46
+ * one sentence (the ENGINE_UNAVAILABLE_MESSAGE precedent for cross-domain
47
+ * twins: each domain owns its constant, the twin is named here).
48
+ */
49
+ export function runAgentInstanceDeniedMessage(agentInstanceId: string): string {
50
+ return `unauthorized to run agent instance '${agentInstanceId}'`;
51
+ }
52
+
53
+ export function addExecutionToSessionDeniedMessage(sessionId: string): string {
54
+ return `unauthorized to add an execution to session '${sessionId}'`;
55
+ }
@@ -44,6 +44,7 @@ import { newPipeline } from "../../pipeline/pipeline.js";
44
44
  import { callerIdentityOf } from "../../pipeline/interceptors/auth.js";
45
45
  import { RequestContext } from "../../pipeline/request-context.js";
46
46
  import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
47
+ import { newAuthorizeRunTargetStep } from "../../pipeline/steps/authorize-run-target.js";
47
48
  import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
48
49
  import type { RunnerCredentialProvider } from "../../runnerauth/runner-credential-provider.js";
49
50
  import { newRecordRunnerLineageLabelsStep } from "./record-runner-lineage-labels.js";
@@ -113,6 +114,7 @@ import {
113
114
  resumeExecution,
114
115
  terminateExecution,
115
116
  } from "./lifecycle.js";
117
+ import { agentExecutionRunTarget } from "./run-target.js";
116
118
  import { agentExecutionSearchExtractor } from "./search-extractor.js";
117
119
  import type { StreamBroker } from "./stream-broker.js";
118
120
  import { submitApproval } from "./submit-approval.js";
@@ -278,9 +280,13 @@ export function registerAgentExecutionServices(
278
280
  /**
279
281
  * Create — create.go buildCreatePipeline, step-for-step: validation
280
282
  * (proto → visibility → tier #357 → thinking #772) → target resolution
281
- * (default agent → invariant guard) → the standard build → the engine
282
- * gate (fail fast BEFORE the first side effect, so a down engine orphans
283
- * nothing) → the pre-side-effect gate slot (O4; empty in OSS) → the
283
+ * (default agent → invariant guard) → the run gate (AuthorizeRunTarget,
284
+ * P1 sp.run-gate: the first step after the target is guaranteed, asking
285
+ * the target's own permission by request shape — session, instance or
286
+ * blueprint — before the engine gate so a denied caller learns nothing
287
+ * about engine state, and before every side effect) → the standard build
288
+ * → the engine gate (fail fast BEFORE the first side effect, so a down
289
+ * engine orphans nothing) → the pre-side-effect gate slot (O4; empty in OSS) → the
284
290
  * side-effecting steps (default instance, session bootstrap,
285
291
  * preference/memory snapshots, initial phase, the ExecutionContext with
286
292
  * merged env, attachment validation) → Persist → IndexSearch →
@@ -314,6 +320,7 @@ async function createExecution(
314
320
  .addStep(newValidateThinkingModeStep(deps.modelRegistry))
315
321
  .addStep(newResolveDefaultAgentStep(deps.store, deps.logger))
316
322
  .addStep(newEnsureSessionOrAgentResolvedStep(deps.logger))
323
+ .addStep(newAuthorizeRunTargetStep(deps.authorizer, agentExecutionRunTarget))
317
324
  .addStep(newResolveSlugStep())
318
325
  .addStep(newBuildNewStateStep())
319
326
  // Vouches the runner-stamped workflow lineage labels (or refuses a
@@ -0,0 +1,64 @@
1
+ /**
2
+ * The agent-execution run-target resolver (P1 sp.run-gate, ruling Q-RG-2):
3
+ * an execution names its run target in one of three shapes, and the gate
4
+ * dispatches on them in the CHAIN's own precedence so it authorizes the
5
+ * target the chain will actually use (CreateDefaultInstanceIfNeeded and
6
+ * CreateSessionIfNeeded read them in exactly this order):
7
+ *
8
+ * 1. spec.session_id — a turn added to an existing conversation: the
9
+ * session's own permission, session#can_create_execution_in;
10
+ * 2. spec.session_spec.agent_instance_id — a new conversation on an
11
+ * explicit instance: agent_instance#can_execute;
12
+ * 3. spec.agent_id — a new conversation on the blueprint's default
13
+ * instance, which the chain resolves (or mints) only AFTER the gate:
14
+ * agent#can_execute, the blueprint's permission (the model derives
15
+ * the default instance's viewers from the blueprint's, so the two
16
+ * questions have one answer; asking the blueprint's is what lets the
17
+ * gate precede the side effect).
18
+ *
19
+ * None of the three set is EnsureSessionOrAgentResolved's invariant arm
20
+ * (ResolveDefaultAgent guarantees one), never a check. Pure over the record
21
+ * being built, where ResolveDefaultAgent writes the resolved agent_id.
22
+ */
23
+ import type { AgentExecution } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/api_pb";
24
+
25
+ import {
26
+ RUN_GATE_CHECKS,
27
+ type RunTarget,
28
+ } from "../../pipeline/steps/authorize-run-target.js";
29
+ import {
30
+ addExecutionToSessionDeniedMessage,
31
+ runAgentDeniedMessage,
32
+ runAgentInstanceDeniedMessage,
33
+ } from "./constants.js";
34
+
35
+ export function agentExecutionRunTarget(
36
+ execution: AgentExecution,
37
+ ): RunTarget | undefined {
38
+ const spec = execution.spec;
39
+ const sessionId = spec?.sessionId ?? "";
40
+ if (sessionId !== "") {
41
+ return {
42
+ ...RUN_GATE_CHECKS.session,
43
+ resourceId: sessionId,
44
+ deniedMessage: addExecutionToSessionDeniedMessage(sessionId),
45
+ };
46
+ }
47
+ const agentInstanceId = spec?.sessionSpec?.agentInstanceId ?? "";
48
+ if (agentInstanceId !== "") {
49
+ return {
50
+ ...RUN_GATE_CHECKS.agentInstance,
51
+ resourceId: agentInstanceId,
52
+ deniedMessage: runAgentInstanceDeniedMessage(agentInstanceId),
53
+ };
54
+ }
55
+ const agentId = spec?.agentId ?? "";
56
+ if (agentId !== "") {
57
+ return {
58
+ ...RUN_GATE_CHECKS.agent,
59
+ resourceId: agentId,
60
+ deniedMessage: runAgentDeniedMessage(agentId),
61
+ };
62
+ }
63
+ return undefined;
64
+ }