@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.
- package/dist/domain/agentexecution/constants.d.ts +16 -0
- package/dist/domain/agentexecution/constants.d.ts.map +1 -1
- package/dist/domain/agentexecution/constants.js +22 -0
- package/dist/domain/agentexecution/constants.js.map +1 -1
- package/dist/domain/agentexecution/controller.d.ts.map +1 -1
- package/dist/domain/agentexecution/controller.js +10 -3
- package/dist/domain/agentexecution/controller.js.map +1 -1
- package/dist/domain/agentexecution/run-target.d.ts +26 -0
- package/dist/domain/agentexecution/run-target.d.ts.map +1 -0
- package/dist/domain/agentexecution/run-target.js +31 -0
- package/dist/domain/agentexecution/run-target.js.map +1 -0
- package/dist/domain/session/constants.d.ts +15 -0
- package/dist/domain/session/constants.d.ts.map +1 -0
- package/dist/domain/session/constants.js +17 -0
- package/dist/domain/session/constants.js.map +1 -0
- package/dist/domain/session/controller.d.ts.map +1 -1
- package/dist/domain/session/controller.js +10 -0
- package/dist/domain/session/controller.js.map +1 -1
- package/dist/domain/session/run-target.d.ts +15 -0
- package/dist/domain/session/run-target.d.ts.map +1 -0
- package/dist/domain/session/run-target.js +14 -0
- package/dist/domain/session/run-target.js.map +1 -0
- package/dist/domain/workflowexecution/constants.d.ts +9 -0
- package/dist/domain/workflowexecution/constants.d.ts.map +1 -1
- package/dist/domain/workflowexecution/constants.js +13 -0
- package/dist/domain/workflowexecution/constants.js.map +1 -1
- package/dist/domain/workflowexecution/controller.d.ts.map +1 -1
- package/dist/domain/workflowexecution/controller.js +8 -0
- package/dist/domain/workflowexecution/controller.js.map +1 -1
- package/dist/domain/workflowexecution/run-target.d.ts +18 -0
- package/dist/domain/workflowexecution/run-target.d.ts.map +1 -0
- package/dist/domain/workflowexecution/run-target.js +23 -0
- package/dist/domain/workflowexecution/run-target.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/pipeline/steps/authorize-run-target.d.ts +105 -0
- package/dist/pipeline/steps/authorize-run-target.d.ts.map +1 -0
- package/dist/pipeline/steps/authorize-run-target.js +61 -0
- package/dist/pipeline/steps/authorize-run-target.js.map +1 -0
- package/dist/query/search/criteria.d.ts +10 -3
- package/dist/query/search/criteria.d.ts.map +1 -1
- package/dist/query/search/criteria.js +11 -4
- package/dist/query/search/criteria.js.map +1 -1
- package/package.json +4 -4
- package/src/domain/agentexecution/__tests__/run-gate.test.ts +245 -0
- package/src/domain/agentexecution/__tests__/run-target.test.ts +117 -0
- package/src/domain/agentexecution/constants.ts +25 -0
- package/src/domain/agentexecution/controller.ts +10 -3
- package/src/domain/agentexecution/run-target.ts +64 -0
- package/src/domain/session/__tests__/run-gate.test.ts +219 -0
- package/src/domain/session/__tests__/run-target.test.ts +43 -0
- package/src/domain/session/constants.ts +17 -0
- package/src/domain/session/controller.ts +10 -0
- package/src/domain/session/run-target.ts +30 -0
- package/src/domain/workflowexecution/__tests__/run-gate.test.ts +200 -0
- package/src/domain/workflowexecution/__tests__/run-target.test.ts +80 -0
- package/src/domain/workflowexecution/constants.ts +17 -0
- package/src/domain/workflowexecution/controller.ts +10 -0
- package/src/domain/workflowexecution/run-target.ts +47 -0
- package/src/extensions/__tests__/extension-composition.test.ts +49 -0
- package/src/index.ts +7 -0
- package/src/pipeline/__tests__/support.ts +34 -0
- package/src/pipeline/steps/__tests__/authorize-run-target.test.ts +261 -0
- package/src/pipeline/steps/authorize-run-target.ts +148 -0
- package/src/query/search/__tests__/criteria.test.ts +18 -1
- 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
|
|
282
|
-
* gate
|
|
283
|
-
*
|
|
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
|
+
}
|