@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.
- package/dist/authorization/evaluator.d.ts +7 -1
- package/dist/authorization/evaluator.d.ts.map +1 -1
- package/dist/authorization/evaluator.js.map +1 -1
- package/dist/authorization/list-read-scope.d.ts.map +1 -1
- package/dist/authorization/list-read-scope.js +19 -11
- package/dist/authorization/list-read-scope.js.map +1 -1
- package/dist/authorization/model/organization.d.ts.map +1 -1
- package/dist/authorization/model/organization.js +2 -0
- package/dist/authorization/model/organization.js.map +1 -1
- package/dist/domain/agent/controller.d.ts.map +1 -1
- package/dist/domain/agent/controller.js +2 -0
- package/dist/domain/agent/controller.js.map +1 -1
- package/dist/domain/agentchannel/controller.d.ts.map +1 -1
- package/dist/domain/agentchannel/controller.js +6 -1
- package/dist/domain/agentchannel/controller.js.map +1 -1
- package/dist/domain/agentchannel/steps.d.ts +19 -0
- package/dist/domain/agentchannel/steps.d.ts.map +1 -1
- package/dist/domain/agentchannel/steps.js +32 -0
- package/dist/domain/agentchannel/steps.js.map +1 -1
- package/dist/domain/agentinstance/controller.d.ts +5 -5
- package/dist/domain/agentinstance/controller.d.ts.map +1 -1
- package/dist/domain/agentinstance/controller.js +6 -1
- package/dist/domain/agentinstance/controller.js.map +1 -1
- package/dist/domain/agentinstance/steps.d.ts +43 -8
- package/dist/domain/agentinstance/steps.d.ts.map +1 -1
- package/dist/domain/agentinstance/steps.js +71 -8
- package/dist/domain/agentinstance/steps.js.map +1 -1
- package/dist/domain/agentshare/controller.d.ts.map +1 -1
- package/dist/domain/agentshare/controller.js +7 -1
- package/dist/domain/agentshare/controller.js.map +1 -1
- package/dist/domain/agentshare/steps.d.ts +58 -0
- package/dist/domain/agentshare/steps.d.ts.map +1 -1
- package/dist/domain/agentshare/steps.js +109 -4
- package/dist/domain/agentshare/steps.js.map +1 -1
- package/dist/domain/artifact/controller.js +21 -25
- package/dist/domain/artifact/controller.js.map +1 -1
- package/dist/domain/channelapp/controller.d.ts.map +1 -1
- package/dist/domain/channelapp/controller.js +2 -0
- package/dist/domain/channelapp/controller.js.map +1 -1
- package/dist/domain/environment/controller.d.ts.map +1 -1
- package/dist/domain/environment/controller.js +2 -0
- package/dist/domain/environment/controller.js.map +1 -1
- package/dist/domain/executioncontext/controller.d.ts.map +1 -1
- package/dist/domain/executioncontext/controller.js +2 -0
- package/dist/domain/executioncontext/controller.js.map +1 -1
- package/dist/domain/executioncontext/steps.d.ts.map +1 -1
- package/dist/domain/executioncontext/steps.js +2 -1
- package/dist/domain/executioncontext/steps.js.map +1 -1
- package/dist/domain/mcpserver/connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/connect.js +8 -7
- package/dist/domain/mcpserver/connect.js.map +1 -1
- package/dist/domain/mcpserver/controller.d.ts.map +1 -1
- package/dist/domain/mcpserver/controller.js +2 -0
- package/dist/domain/mcpserver/controller.js.map +1 -1
- package/dist/domain/mcpserver/start-connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/start-connect.js +8 -7
- package/dist/domain/mcpserver/start-connect.js.map +1 -1
- package/dist/domain/memory/controller.d.ts.map +1 -1
- package/dist/domain/memory/controller.js +6 -1
- package/dist/domain/memory/controller.js.map +1 -1
- package/dist/domain/memory/steps.d.ts +23 -0
- package/dist/domain/memory/steps.d.ts.map +1 -1
- package/dist/domain/memory/steps.js +35 -0
- package/dist/domain/memory/steps.js.map +1 -1
- package/dist/domain/oauthapp/controller.d.ts.map +1 -1
- package/dist/domain/oauthapp/controller.js +2 -0
- package/dist/domain/oauthapp/controller.js.map +1 -1
- package/dist/domain/plugin/controller.d.ts.map +1 -1
- package/dist/domain/plugin/controller.js +5 -24
- package/dist/domain/plugin/controller.js.map +1 -1
- package/dist/domain/schedule/controller.d.ts.map +1 -1
- package/dist/domain/schedule/controller.js +7 -1
- package/dist/domain/schedule/controller.js.map +1 -1
- package/dist/domain/schedule/steps.d.ts +19 -0
- package/dist/domain/schedule/steps.d.ts.map +1 -1
- package/dist/domain/schedule/steps.js +32 -0
- package/dist/domain/schedule/steps.js.map +1 -1
- package/dist/domain/session/steps.d.ts +8 -9
- package/dist/domain/session/steps.d.ts.map +1 -1
- package/dist/domain/session/steps.js +17 -21
- package/dist/domain/session/steps.js.map +1 -1
- package/dist/domain/skill/controller.d.ts.map +1 -1
- package/dist/domain/skill/controller.js +8 -17
- package/dist/domain/skill/controller.js.map +1 -1
- package/dist/domain/workflow/controller.d.ts.map +1 -1
- package/dist/domain/workflow/controller.js +30 -107
- package/dist/domain/workflow/controller.js.map +1 -1
- package/dist/domain/workflow/version-resolution.d.ts +5 -0
- package/dist/domain/workflow/version-resolution.d.ts.map +1 -0
- package/dist/domain/workflow/version-resolution.js +113 -0
- package/dist/domain/workflow/version-resolution.js.map +1 -0
- package/dist/domain/workflowinstance/controller.d.ts.map +1 -1
- package/dist/domain/workflowinstance/controller.js +6 -1
- package/dist/domain/workflowinstance/controller.js.map +1 -1
- package/dist/domain/workflowinstance/steps.d.ts +20 -0
- package/dist/domain/workflowinstance/steps.d.ts.map +1 -1
- package/dist/domain/workflowinstance/steps.js +39 -0
- package/dist/domain/workflowinstance/steps.js.map +1 -1
- package/dist/extensions/identity.d.ts +19 -0
- package/dist/extensions/identity.d.ts.map +1 -1
- package/dist/extensions/identity.js +22 -3
- package/dist/extensions/identity.js.map +1 -1
- package/dist/extensions/list-read-scope.d.ts +41 -7
- package/dist/extensions/list-read-scope.d.ts.map +1 -1
- package/dist/extensions/list-read-scope.js +13 -2
- package/dist/extensions/list-read-scope.js.map +1 -1
- package/dist/pipeline/steps/authorize-resolved-target.d.ts +88 -0
- package/dist/pipeline/steps/authorize-resolved-target.d.ts.map +1 -0
- package/dist/pipeline/steps/authorize-resolved-target.js +36 -0
- package/dist/pipeline/steps/authorize-resolved-target.js.map +1 -0
- package/dist/pipeline/steps/authorize.d.ts +27 -0
- package/dist/pipeline/steps/authorize.d.ts.map +1 -1
- package/dist/pipeline/steps/authorize.js +49 -10
- package/dist/pipeline/steps/authorize.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +2 -1
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/dist/pipeline/steps/guard-reserved-labels.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-reserved-labels.js +6 -7
- package/dist/pipeline/steps/guard-reserved-labels.js.map +1 -1
- package/dist/pipeline/steps/helpers.d.ts +9 -5
- package/dist/pipeline/steps/helpers.d.ts.map +1 -1
- package/dist/pipeline/steps/helpers.js +11 -15
- package/dist/pipeline/steps/helpers.js.map +1 -1
- package/dist/pipeline/steps/version-history.d.ts +11 -0
- package/dist/pipeline/steps/version-history.d.ts.map +1 -1
- package/dist/pipeline/steps/version-history.js +26 -0
- package/dist/pipeline/steps/version-history.js.map +1 -1
- package/package.json +6 -6
- package/src/authorization/__tests__/list-read-scope.test.ts +23 -12
- package/src/authorization/__tests__/skip-lane-inventory.test.ts +160 -0
- package/src/authorization/__tests__/wire-permissions.test.ts +5 -5
- package/src/authorization/evaluator.ts +7 -0
- package/src/authorization/list-read-scope.ts +22 -11
- package/src/authorization/model/__tests__/registry.test.ts +2 -0
- package/src/authorization/model/organization.ts +2 -0
- package/src/domain/agent/controller.ts +10 -0
- package/src/domain/agentchannel/__tests__/create-authorization.test.ts +58 -0
- package/src/domain/agentchannel/controller.ts +19 -0
- package/src/domain/agentchannel/steps.ts +40 -0
- package/src/domain/agentinstance/__tests__/create-authorization.composed.test.ts +243 -0
- package/src/domain/agentinstance/__tests__/create-authorization.test.ts +144 -0
- package/src/domain/agentinstance/controller.ts +24 -5
- package/src/domain/agentinstance/steps.ts +82 -8
- package/src/domain/agentshare/__tests__/create-authorization.test.ts +86 -0
- package/src/domain/agentshare/__tests__/member-audience.test.ts +113 -0
- package/src/domain/agentshare/controller.ts +21 -0
- package/src/domain/agentshare/steps.ts +181 -15
- package/src/domain/artifact/controller.ts +18 -22
- package/src/domain/channelapp/controller.ts +10 -0
- package/src/domain/environment/controller.ts +10 -0
- package/src/domain/executioncontext/__tests__/executioncontext.test.ts +1 -0
- package/src/domain/executioncontext/controller.ts +10 -0
- package/src/domain/executioncontext/steps.ts +2 -1
- package/src/domain/mcpserver/connect.ts +11 -10
- package/src/domain/mcpserver/controller.ts +13 -1
- package/src/domain/mcpserver/start-connect.ts +17 -11
- package/src/domain/memory/__tests__/create-authorization.test.ts +59 -0
- package/src/domain/memory/controller.ts +11 -0
- package/src/domain/memory/steps.ts +41 -0
- package/src/domain/oauthapp/controller.ts +10 -0
- package/src/domain/plugin/controller.ts +22 -36
- package/src/domain/schedule/__tests__/create-authorization.test.ts +58 -0
- package/src/domain/schedule/controller.ts +20 -0
- package/src/domain/schedule/steps.ts +45 -2
- package/src/domain/session/steps.ts +24 -32
- package/src/domain/skill/controller.ts +24 -23
- package/src/domain/workflow/controller.ts +58 -145
- package/src/domain/workflow/version-resolution.ts +166 -0
- package/src/domain/workflowinstance/__tests__/create-authorization.test.ts +109 -0
- package/src/domain/workflowinstance/controller.ts +19 -0
- package/src/domain/workflowinstance/steps.ts +49 -1
- package/src/extensions/__tests__/composed-support.ts +38 -4
- package/src/extensions/__tests__/identity.test.ts +44 -1
- package/src/extensions/__tests__/list-read-scope-composed.test.ts +87 -1
- package/src/extensions/__tests__/list-read-scope.test.ts +63 -0
- package/src/extensions/identity.ts +23 -5
- package/src/extensions/list-read-scope.ts +47 -7
- package/src/pipeline/steps/__tests__/authorize-resolved-target.test.ts +285 -0
- package/src/pipeline/steps/authorize-resolved-target.ts +129 -0
- package/src/pipeline/steps/authorize.ts +70 -13
- package/src/pipeline/steps/guard-memory-capture.ts +2 -1
- package/src/pipeline/steps/guard-reserved-labels.ts +6 -9
- package/src/pipeline/steps/helpers.ts +11 -15
- package/src/pipeline/steps/version-history.ts +34 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the instance create lane's authorization at the wire, on a real boot
|
|
3
|
+
* under the require-authentication posture with no unit Authorizer (the
|
|
4
|
+
* self-host's shape, the built-in Authorizer composed):
|
|
5
|
+
*
|
|
6
|
+
* - a member creates a personal instance of an org-visible agent in their
|
|
7
|
+
* organization (can_create_agent_instance is member-level; the parent's
|
|
8
|
+
* can_create_instance is its viewer set);
|
|
9
|
+
* - a member is refused an instance of a PRIVATE agent with the parent's
|
|
10
|
+
* copy — the organization's bar passed, the agent's did not;
|
|
11
|
+
* - an outsider naming an organization they hold no row in is refused
|
|
12
|
+
* with the organization's copy, before anything about the agent;
|
|
13
|
+
* - a member's wire request carrying the reserved default-instance label
|
|
14
|
+
* is refused: the label buys nothing from the wire (the reserved-label
|
|
15
|
+
* guard refuses it after the caller passes the personal bar);
|
|
16
|
+
* - the SAME labelled request the server composes in-process for the
|
|
17
|
+
* member (the default-instance self-heal's exact shape: the in-process
|
|
18
|
+
* transport, the propagated caller, the parent's organization) is
|
|
19
|
+
* admitted with no caller check and attributed to the member — the
|
|
20
|
+
* arm that keeps every lane whose run was already admitted (a guest, a
|
|
21
|
+
* channel turn, a schedule fire) working when the agent has no default
|
|
22
|
+
* instance yet.
|
|
23
|
+
*
|
|
24
|
+
* Every negative arm sits beside a positive one so the file cannot pass
|
|
25
|
+
* vacuously. The resolver's own table is create-authorization.test.ts.
|
|
26
|
+
*/
|
|
27
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
28
|
+
import { tmpdir } from "node:os";
|
|
29
|
+
import path from "node:path";
|
|
30
|
+
|
|
31
|
+
import { Code, ConnectError, createClient } from "@connectrpc/connect";
|
|
32
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
33
|
+
|
|
34
|
+
import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
|
|
35
|
+
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
36
|
+
import { AgentInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/command_pb";
|
|
37
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
38
|
+
import { IdentityAccountCommandController } from "@stigmer/protos/ai/stigmer/iam/identityaccount/v1/command_pb";
|
|
39
|
+
import { OrganizationCommandController } from "@stigmer/protos/ai/stigmer/tenancy/organization/v1/command_pb";
|
|
40
|
+
|
|
41
|
+
import { loadConfig } from "../../../boot/config.js";
|
|
42
|
+
import { composeServer } from "../../../boot/compose.js";
|
|
43
|
+
import type { ComposedServer } from "../../../boot/compose.js";
|
|
44
|
+
import type { ServerExtension } from "../../../extensions/registry.js";
|
|
45
|
+
import {
|
|
46
|
+
baseConfig,
|
|
47
|
+
fakeJwt,
|
|
48
|
+
fakeVerifier,
|
|
49
|
+
silentLogger,
|
|
50
|
+
transportFor,
|
|
51
|
+
} from "../../../extensions/__tests__/composed-support.js";
|
|
52
|
+
import {
|
|
53
|
+
DEFAULT_INSTANCE_LABEL,
|
|
54
|
+
RESERVED_LABEL_TRUE,
|
|
55
|
+
} from "../../../pipeline/apiresource-labels.js";
|
|
56
|
+
import {
|
|
57
|
+
IN_PROCESS_CALLER_HEADER,
|
|
58
|
+
encodeInProcessCaller,
|
|
59
|
+
} from "../../../pipeline/interceptors/auth.js";
|
|
60
|
+
import { buildDefaultInstanceRequest } from "../defaultinstance.js";
|
|
61
|
+
import {
|
|
62
|
+
INSTANCE_ORGANIZATION_DENIED_MESSAGE,
|
|
63
|
+
INSTANCE_PARENT_DENIED_MESSAGE,
|
|
64
|
+
} from "../steps.js";
|
|
65
|
+
|
|
66
|
+
const FOUNDER = "fake|founder";
|
|
67
|
+
const MEMBER = "fake|member";
|
|
68
|
+
const STRANGER = "fake|stranger";
|
|
69
|
+
const ORG = "instance-authz-org";
|
|
70
|
+
|
|
71
|
+
function organizationInput(slug: string) {
|
|
72
|
+
return {
|
|
73
|
+
apiVersion: "tenancy.stigmer.ai/v1",
|
|
74
|
+
kind: "Organization",
|
|
75
|
+
metadata: { name: slug, slug, org: "" },
|
|
76
|
+
spec: { description: slug },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function agentInput(name: string, visibility: ApiResourceVisibility) {
|
|
81
|
+
return {
|
|
82
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
83
|
+
kind: "Agent",
|
|
84
|
+
metadata: { name, org: ORG, visibility },
|
|
85
|
+
spec: { instructions: "a conformant instruction body" },
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function instanceInput(
|
|
90
|
+
name: string,
|
|
91
|
+
agentId: string,
|
|
92
|
+
org: string = ORG,
|
|
93
|
+
labels: Record<string, string> = {},
|
|
94
|
+
) {
|
|
95
|
+
return {
|
|
96
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
97
|
+
kind: "AgentInstance",
|
|
98
|
+
metadata: { name, org, labels },
|
|
99
|
+
spec: { agentId, description: name },
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function failureOf(promise: Promise<unknown>): Promise<ConnectError> {
|
|
104
|
+
try {
|
|
105
|
+
await promise;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
return ConnectError.from(error);
|
|
108
|
+
}
|
|
109
|
+
throw new Error("expected the call to fail");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
describe("agent instance create under the built-in authorizer", () => {
|
|
113
|
+
let dir: string;
|
|
114
|
+
let server: ComposedServer;
|
|
115
|
+
let port: number;
|
|
116
|
+
let orgAgent: Agent;
|
|
117
|
+
let privateAgent: Agent;
|
|
118
|
+
|
|
119
|
+
const asFounder = () =>
|
|
120
|
+
transportFor(port, fakeJwt(FOUNDER, "founder@example.com"));
|
|
121
|
+
const asMember = () =>
|
|
122
|
+
transportFor(port, fakeJwt(MEMBER, "member@example.com"));
|
|
123
|
+
const asStranger = () =>
|
|
124
|
+
transportFor(port, fakeJwt(STRANGER, "stranger@example.com"));
|
|
125
|
+
|
|
126
|
+
beforeAll(async () => {
|
|
127
|
+
dir = mkdtempSync(path.join(tmpdir(), "instance-create-authz-"));
|
|
128
|
+
const unit: ServerExtension = {
|
|
129
|
+
name: "fake-oidc-only",
|
|
130
|
+
requireAuthentication: true,
|
|
131
|
+
identityVerifiers: [fakeVerifier],
|
|
132
|
+
};
|
|
133
|
+
server = await composeServer({
|
|
134
|
+
config: loadConfig(baseConfig(dir)),
|
|
135
|
+
logger: silentLogger,
|
|
136
|
+
extensions: [unit],
|
|
137
|
+
portOverride: 0,
|
|
138
|
+
host: "127.0.0.1",
|
|
139
|
+
});
|
|
140
|
+
port = await server.start();
|
|
141
|
+
|
|
142
|
+
// The membership rules of a self-host: a person who provisions while
|
|
143
|
+
// the organization exists joins it as a member; a person provisioned
|
|
144
|
+
// BEFORE it was founded holds no row on it. So the stranger provisions
|
|
145
|
+
// first, the founder founds, and the member provisions after.
|
|
146
|
+
await createClient(
|
|
147
|
+
IdentityAccountCommandController,
|
|
148
|
+
asFounder(),
|
|
149
|
+
).provisionMyAccount({});
|
|
150
|
+
await createClient(
|
|
151
|
+
IdentityAccountCommandController,
|
|
152
|
+
asStranger(),
|
|
153
|
+
).provisionMyAccount({});
|
|
154
|
+
await createClient(OrganizationCommandController, asFounder()).create(
|
|
155
|
+
organizationInput(ORG),
|
|
156
|
+
);
|
|
157
|
+
await createClient(
|
|
158
|
+
IdentityAccountCommandController,
|
|
159
|
+
asMember(),
|
|
160
|
+
).provisionMyAccount({});
|
|
161
|
+
|
|
162
|
+
const founderAgents = createClient(AgentCommandController, asFounder());
|
|
163
|
+
orgAgent = await founderAgents.create(
|
|
164
|
+
agentInput("Org Agent", ApiResourceVisibility.visibility_org),
|
|
165
|
+
);
|
|
166
|
+
privateAgent = await founderAgents.create(
|
|
167
|
+
agentInput("Private Agent", ApiResourceVisibility.visibility_private),
|
|
168
|
+
);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
afterAll(async () => {
|
|
172
|
+
await server.shutdown();
|
|
173
|
+
rmSync(dir, { recursive: true, force: true });
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("a member creates a personal instance of an org-visible agent in their organization", async () => {
|
|
177
|
+
const created = await createClient(
|
|
178
|
+
AgentInstanceCommandController,
|
|
179
|
+
asMember(),
|
|
180
|
+
).create(instanceInput("member-personal", orgAgent.metadata!.id));
|
|
181
|
+
expect(created.metadata?.org).toBe(ORG);
|
|
182
|
+
expect(created.status?.audit?.specAudit?.createdBy?.id).toBe(MEMBER);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it("a member is refused an instance of a PRIVATE agent with the parent's copy", async () => {
|
|
186
|
+
const error = await failureOf(
|
|
187
|
+
createClient(AgentInstanceCommandController, asMember()).create(
|
|
188
|
+
instanceInput("member-on-private", privateAgent.metadata!.id),
|
|
189
|
+
),
|
|
190
|
+
);
|
|
191
|
+
expect(error.code).toBe(Code.PermissionDenied);
|
|
192
|
+
expect(error.rawMessage).toBe(INSTANCE_PARENT_DENIED_MESSAGE);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("an outsider naming the organization is refused with the organization's copy, before the agent is named", async () => {
|
|
196
|
+
const error = await failureOf(
|
|
197
|
+
createClient(AgentInstanceCommandController, asStranger()).create(
|
|
198
|
+
instanceInput("stranger-instance", orgAgent.metadata!.id),
|
|
199
|
+
),
|
|
200
|
+
);
|
|
201
|
+
expect(error.code).toBe(Code.PermissionDenied);
|
|
202
|
+
expect(error.rawMessage).toBe(INSTANCE_ORGANIZATION_DENIED_MESSAGE);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("the reserved default-instance label buys nothing from the wire", async () => {
|
|
206
|
+
const error = await failureOf(
|
|
207
|
+
createClient(AgentInstanceCommandController, asMember()).create(
|
|
208
|
+
instanceInput("wire-default", orgAgent.metadata!.id, ORG, {
|
|
209
|
+
[DEFAULT_INSTANCE_LABEL]: RESERVED_LABEL_TRUE,
|
|
210
|
+
}),
|
|
211
|
+
),
|
|
212
|
+
);
|
|
213
|
+
// The personal bar admits a member on an org-visible agent; the label
|
|
214
|
+
// guard then refuses the reserved label a wire caller may not write.
|
|
215
|
+
expect(error.code).toBe(Code.InvalidArgument);
|
|
216
|
+
expect(error.rawMessage).toContain(DEFAULT_INSTANCE_LABEL);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it("the same labelled request composed in-process for the member is admitted and attributed to them (the self-heal's shape)", async () => {
|
|
220
|
+
const member = await fakeVerifier.verify(
|
|
221
|
+
fakeJwt(MEMBER, "member@example.com"),
|
|
222
|
+
);
|
|
223
|
+
if (member === null) {
|
|
224
|
+
throw new Error("the fake verifier must admit the member");
|
|
225
|
+
}
|
|
226
|
+
// The agent's own default instance already exists (agent create made
|
|
227
|
+
// it); the self-heal's request is the same shape under a slug the store
|
|
228
|
+
// does not hold yet, which is what a legacy agent without one presents.
|
|
229
|
+
const request = buildDefaultInstanceRequest(orgAgent.metadata!);
|
|
230
|
+
request.metadata!.name = "org-agent-default-healed";
|
|
231
|
+
const created = await createClient(
|
|
232
|
+
AgentInstanceCommandController,
|
|
233
|
+
server.inProcessTransport,
|
|
234
|
+
).create(request, {
|
|
235
|
+
headers: { [IN_PROCESS_CALLER_HEADER]: encodeInProcessCaller(member) },
|
|
236
|
+
});
|
|
237
|
+
expect(created.metadata?.labels?.[DEFAULT_INSTANCE_LABEL]).toBe(
|
|
238
|
+
RESERVED_LABEL_TRUE,
|
|
239
|
+
);
|
|
240
|
+
expect(created.metadata?.org).toBe(ORG);
|
|
241
|
+
expect(created.status?.audit?.specAudit?.createdBy?.id).toBe(MEMBER);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the create lane's authorization questions (steps.ts,
|
|
3
|
+
* resolveInstanceCreateTargets): a personal instance asks the
|
|
4
|
+
* organization's bar (can_create_agent_instance on metadata.org) and then
|
|
5
|
+
* the parent's (can_create_instance on the agent), each with its own copy;
|
|
6
|
+
* a default instance the server composed in-process for the parent's
|
|
7
|
+
* organization asks nothing; a labelled request that arrived on the wire,
|
|
8
|
+
* or that names another organization, takes the personal bar in full; a
|
|
9
|
+
* missing parent is a thrown chain invariant, never an open lane.
|
|
10
|
+
*/
|
|
11
|
+
import { describe, expect, it } from "vitest";
|
|
12
|
+
import { create } from "@bufbuild/protobuf";
|
|
13
|
+
|
|
14
|
+
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
15
|
+
import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
|
|
16
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
17
|
+
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
18
|
+
|
|
19
|
+
import type { CallerIdentity } from "../../../extensions/identity.js";
|
|
20
|
+
import {
|
|
21
|
+
DEFAULT_INSTANCE_LABEL,
|
|
22
|
+
RESERVED_LABEL_TRUE,
|
|
23
|
+
} from "../../../pipeline/apiresource-labels.js";
|
|
24
|
+
import { testCallerIdentity } from "../../../pipeline/__tests__/support.js";
|
|
25
|
+
import { RequestContext } from "../../../pipeline/request-context.js";
|
|
26
|
+
import {
|
|
27
|
+
INSTANCE_ORGANIZATION_DENIED_MESSAGE,
|
|
28
|
+
INSTANCE_PARENT_DENIED_MESSAGE,
|
|
29
|
+
PARENT_AGENT_KEY,
|
|
30
|
+
resolveInstanceCreateTargets,
|
|
31
|
+
} from "../steps.js";
|
|
32
|
+
|
|
33
|
+
const parent = create(AgentSchema, {
|
|
34
|
+
metadata: { id: "agt_01parent", name: "helper", org: "acme" },
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function ctxFor(
|
|
38
|
+
org: string,
|
|
39
|
+
caller: CallerIdentity,
|
|
40
|
+
labels: Record<string, string> = {},
|
|
41
|
+
stashParent = true,
|
|
42
|
+
) {
|
|
43
|
+
const ctx = new RequestContext(
|
|
44
|
+
AgentInstanceSchema,
|
|
45
|
+
create(AgentInstanceSchema, {
|
|
46
|
+
metadata: { name: "mine", org, labels },
|
|
47
|
+
spec: { agentId: parent.metadata!.id },
|
|
48
|
+
}),
|
|
49
|
+
caller,
|
|
50
|
+
ApiResourceKind.agent_instance,
|
|
51
|
+
);
|
|
52
|
+
if (stashParent) {
|
|
53
|
+
ctx.set(PARENT_AGENT_KEY, parent);
|
|
54
|
+
}
|
|
55
|
+
return ctx;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DEFAULT_LABELS = { [DEFAULT_INSTANCE_LABEL]: RESERVED_LABEL_TRUE };
|
|
59
|
+
|
|
60
|
+
describe("resolveInstanceCreateTargets", () => {
|
|
61
|
+
it("a personal instance asks the organization's bar first, then the parent's, each with its own copy", () => {
|
|
62
|
+
expect(
|
|
63
|
+
resolveInstanceCreateTargets(ctxFor("acme", testCallerIdentity())),
|
|
64
|
+
).toEqual([
|
|
65
|
+
{
|
|
66
|
+
permission: IamPermission.can_create_agent_instance,
|
|
67
|
+
resourceKind: ApiResourceKind.organization,
|
|
68
|
+
resourceId: "acme",
|
|
69
|
+
deniedMessage: INSTANCE_ORGANIZATION_DENIED_MESSAGE,
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
permission: IamPermission.can_create_instance,
|
|
73
|
+
resourceKind: ApiResourceKind.agent,
|
|
74
|
+
resourceId: "agt_01parent",
|
|
75
|
+
deniedMessage: INSTANCE_PARENT_DENIED_MESSAGE,
|
|
76
|
+
},
|
|
77
|
+
]);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it("a cross-org personal instance asks about ITS organization, not the parent's (the marketplace case)", () => {
|
|
81
|
+
const [orgBar] = resolveInstanceCreateTargets(
|
|
82
|
+
ctxFor("consumer-org", testCallerIdentity()),
|
|
83
|
+
);
|
|
84
|
+
expect(orgBar?.resourceId).toBe("consumer-org");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("a default instance the server composed in-process for the parent's organization asks nothing", () => {
|
|
88
|
+
expect(
|
|
89
|
+
resolveInstanceCreateTargets(
|
|
90
|
+
ctxFor(
|
|
91
|
+
"acme",
|
|
92
|
+
testCallerIdentity({ callerClass: "user", origin: "in-process" }),
|
|
93
|
+
DEFAULT_LABELS,
|
|
94
|
+
),
|
|
95
|
+
),
|
|
96
|
+
).toEqual([]);
|
|
97
|
+
expect(
|
|
98
|
+
resolveInstanceCreateTargets(
|
|
99
|
+
ctxFor(
|
|
100
|
+
"acme",
|
|
101
|
+
testCallerIdentity({ callerClass: "channel", origin: "in-process" }),
|
|
102
|
+
DEFAULT_LABELS,
|
|
103
|
+
),
|
|
104
|
+
),
|
|
105
|
+
).toEqual([]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("the label alone buys nothing: a labelled request from the wire takes the personal bar in full, the machine class included", () => {
|
|
109
|
+
expect(
|
|
110
|
+
resolveInstanceCreateTargets(
|
|
111
|
+
ctxFor("acme", testCallerIdentity(), DEFAULT_LABELS),
|
|
112
|
+
),
|
|
113
|
+
).toHaveLength(2);
|
|
114
|
+
expect(
|
|
115
|
+
resolveInstanceCreateTargets(
|
|
116
|
+
ctxFor(
|
|
117
|
+
"acme",
|
|
118
|
+
testCallerIdentity({ callerClass: "machine" }),
|
|
119
|
+
DEFAULT_LABELS,
|
|
120
|
+
),
|
|
121
|
+
),
|
|
122
|
+
).toHaveLength(2);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("a labelled in-process request for another organization is not a default instance to this step", () => {
|
|
126
|
+
expect(
|
|
127
|
+
resolveInstanceCreateTargets(
|
|
128
|
+
ctxFor(
|
|
129
|
+
"other-org",
|
|
130
|
+
testCallerIdentity({ callerClass: "user", origin: "in-process" }),
|
|
131
|
+
DEFAULT_LABELS,
|
|
132
|
+
),
|
|
133
|
+
),
|
|
134
|
+
).toHaveLength(2);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
it("a missing parent is a broken chain invariant and throws", () => {
|
|
138
|
+
expect(() =>
|
|
139
|
+
resolveInstanceCreateTargets(
|
|
140
|
+
ctxFor("acme", testCallerIdentity(), {}, false),
|
|
141
|
+
),
|
|
142
|
+
).toThrow("parent agent not found in context");
|
|
143
|
+
});
|
|
144
|
+
});
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
* Proven by agentinstance.conformance.test.ts (CONFORMANCE_TARGET=local)
|
|
11
11
|
* and __tests__/agentinstance.test.ts.
|
|
12
12
|
*
|
|
13
|
-
* Versus Stigmer Cloud, OSS excludes the
|
|
14
|
-
* can_create_instance on the parent agent), CreateIamPolicies, and Publish
|
|
13
|
+
* Versus Stigmer Cloud, OSS excludes the CreateIamPolicies and Publish
|
|
15
14
|
* steps. Deliberately NO same-org rule on create, unlike WorkflowInstance:
|
|
16
15
|
* an agent is a shareable blueprint, and one agent legitimately has
|
|
17
|
-
* instances in several orgs (the marketplace case)
|
|
18
|
-
* cross-org
|
|
19
|
-
*
|
|
16
|
+
* instances in several orgs (the marketplace case). What governs a
|
|
17
|
+
* cross-org instance in both editions is the create lane's authorization
|
|
18
|
+
* (steps.ts, resolveInstanceCreateTargets): the caller's standing in the
|
|
19
|
+
* instance's organization and on the parent agent.
|
|
20
20
|
*/
|
|
21
21
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
22
22
|
|
|
@@ -47,6 +47,10 @@ import type { PipelineStep } from "../../pipeline/pipeline.js";
|
|
|
47
47
|
import { callerIdentityOf } from "../../pipeline/interceptors/auth.js";
|
|
48
48
|
import { RequestContext } from "../../pipeline/request-context.js";
|
|
49
49
|
import { newAuthorizeStep } from "../../pipeline/steps/authorize.js";
|
|
50
|
+
import {
|
|
51
|
+
loadedTargetAsMethod,
|
|
52
|
+
newAuthorizeResolvedTargetStep,
|
|
53
|
+
} from "../../pipeline/steps/authorize-resolved-target.js";
|
|
50
54
|
import { newGuardReservedLabelsStep } from "../../pipeline/steps/guard-reserved-labels.js";
|
|
51
55
|
import {
|
|
52
56
|
newAuthorizeVisibilityTransitionStep,
|
|
@@ -106,6 +110,7 @@ import {
|
|
|
106
110
|
newLoadParentAgentStep,
|
|
107
111
|
newRejectDefaultInstanceVisibilityUpdateStep,
|
|
108
112
|
newValidateInstanceUpdateStep,
|
|
113
|
+
resolveInstanceCreateTargets,
|
|
109
114
|
} from "./steps.js";
|
|
110
115
|
import type { ParentAgentLoaderProvider } from "./steps.js";
|
|
111
116
|
|
|
@@ -181,6 +186,14 @@ async function createInstance(
|
|
|
181
186
|
.addStep(newValidateVisibilityStep())
|
|
182
187
|
.addStep(newResolveSlugStep())
|
|
183
188
|
.addStep(newLoadParentAgentStep(deps.parentAgentLoader, deps.logger))
|
|
189
|
+
// Before the duplicate check, so a refused caller learns nothing about
|
|
190
|
+
// which slugs exist (steps.ts, resolveInstanceCreateTargets).
|
|
191
|
+
.addStep(
|
|
192
|
+
newAuthorizeResolvedTargetStep(
|
|
193
|
+
deps.authorizer,
|
|
194
|
+
resolveInstanceCreateTargets,
|
|
195
|
+
),
|
|
196
|
+
)
|
|
184
197
|
.addStep(newCheckDuplicateStep(deps.store))
|
|
185
198
|
.addStep(newBuildNewStateStep())
|
|
186
199
|
.addStep(newGuardPublicVisibilityStep(deps.authorizer))
|
|
@@ -548,6 +561,12 @@ async function getByReference(
|
|
|
548
561
|
)
|
|
549
562
|
.addStep(newValidateProtoStep())
|
|
550
563
|
.addStep(newLoadByReferenceStep(deps.store, AgentInstanceSchema))
|
|
564
|
+
.addStep(
|
|
565
|
+
newAuthorizeResolvedTargetStep(
|
|
566
|
+
deps.authorizer,
|
|
567
|
+
loadedTargetAsMethod(AgentInstanceQueryController.method.get),
|
|
568
|
+
),
|
|
569
|
+
)
|
|
551
570
|
.build()
|
|
552
571
|
.execute(reqCtx);
|
|
553
572
|
return reqCtx.get(TARGET_RESOURCE_KEY) as AgentInstance;
|
|
@@ -16,8 +16,10 @@ import type { AgentInstance } from "@stigmer/protos/ai/stigmer/agentic/agentinst
|
|
|
16
16
|
import { AgentInstanceListSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/io_pb";
|
|
17
17
|
import { AgentInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/query_pb";
|
|
18
18
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
19
|
+
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
19
20
|
|
|
20
21
|
import type { Logger } from "../../boot/logger.js";
|
|
22
|
+
import { isServerComposedRequest } from "../../extensions/identity.js";
|
|
21
23
|
import { isDefaultInstance } from "../../pipeline/apiresource-labels.js";
|
|
22
24
|
import {
|
|
23
25
|
failedPreconditionError,
|
|
@@ -29,6 +31,7 @@ import type { ListReadScope } from "../../extensions/list-read-scope.js";
|
|
|
29
31
|
import { restrictListByReadScope } from "../../extensions/list-read-scope.js";
|
|
30
32
|
import type { PipelineStep } from "../../pipeline/pipeline.js";
|
|
31
33
|
import type { RequestContext } from "../../pipeline/request-context.js";
|
|
34
|
+
import type { AuthorizationTarget } from "../../pipeline/steps/authorize.js";
|
|
32
35
|
import {
|
|
33
36
|
compareCreatedAtDesc,
|
|
34
37
|
matchesAllLabels,
|
|
@@ -54,18 +57,19 @@ export interface ParentAgentLoader {
|
|
|
54
57
|
*/
|
|
55
58
|
export type ParentAgentLoaderProvider = () => ParentAgentLoader;
|
|
56
59
|
|
|
60
|
+
/** Context key for the loaded parent agent, read by the create check below. */
|
|
61
|
+
export const PARENT_AGENT_KEY = "parent_agent";
|
|
62
|
+
|
|
57
63
|
/**
|
|
58
64
|
* LoadParentAgent — create.go: an unknown spec.agent_id is rejected with
|
|
59
65
|
* NotFound instead of persisting a dangling instance (oss#645), converging
|
|
60
66
|
* on cloud's LoadParentAgent step and this server's own WorkflowInstance
|
|
61
|
-
* create pipeline.
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
66
|
-
*
|
|
67
|
-
* have — an agent is a shareable blueprint, and one agent legitimately has
|
|
68
|
-
* instances in several orgs: the marketplace case).
|
|
67
|
+
* create pipeline. The loaded agent is stashed under PARENT_AGENT_KEY for
|
|
68
|
+
* the authorization step that follows; no same-org rule reads it, because
|
|
69
|
+
* an agent is a shareable blueprint and one agent legitimately has
|
|
70
|
+
* instances in several orgs (the marketplace case) — the instance's
|
|
71
|
+
* organization is the caller's, and the check below asks the caller's
|
|
72
|
+
* standing there.
|
|
69
73
|
*/
|
|
70
74
|
export function newLoadParentAgentStep(
|
|
71
75
|
agentLoader: ParentAgentLoaderProvider,
|
|
@@ -95,10 +99,80 @@ export function newLoadParentAgentStep(
|
|
|
95
99
|
agentId,
|
|
96
100
|
org: parentAgent.metadata?.org ?? "",
|
|
97
101
|
});
|
|
102
|
+
ctx.set(PARENT_AGENT_KEY, parentAgent);
|
|
98
103
|
},
|
|
99
104
|
};
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
/** The deny copy of the parent's bar, the Java AgentInstanceCreateHandler's. */
|
|
108
|
+
export const INSTANCE_PARENT_DENIED_MESSAGE =
|
|
109
|
+
"You don't have permission to create instances of this agent";
|
|
110
|
+
|
|
111
|
+
/** The deny copy of the organization's bar. */
|
|
112
|
+
export const INSTANCE_ORGANIZATION_DENIED_MESSAGE =
|
|
113
|
+
"unauthorized to create agent instance in this organization";
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The create lane's authorization questions, for AuthorizeResolvedTarget
|
|
117
|
+
* after LoadParentAgent. The RPC is is_skip_authorization because its
|
|
118
|
+
* target is the PARENT, loaded from spec.agent_id, not a request field.
|
|
119
|
+
*
|
|
120
|
+
* A personal instance asks two questions, the organization's first so an
|
|
121
|
+
* outsider learns nothing about the agent from which copy came back:
|
|
122
|
+
* may the caller create instances in metadata.org (can_create_agent_instance,
|
|
123
|
+
* member-level: the instance is the caller's conversation shell in their
|
|
124
|
+
* own organization, which may differ from the agent's — the marketplace
|
|
125
|
+
* case), and may the caller instantiate this agent (can_create_instance on
|
|
126
|
+
* the parent, which the model resolves to whoever may run it).
|
|
127
|
+
*
|
|
128
|
+
* A DEFAULT instance asks nothing: it is the agent's row, not the caller's.
|
|
129
|
+
* The server composes one for the run's human when the agent has none
|
|
130
|
+
* (attribution stays the person's), and the run was already admitted one
|
|
131
|
+
* step earlier by the lane that vouched for it — a guest, a channel turn,
|
|
132
|
+
* a schedule fire hold no viewer tuple on the agent and would be refused
|
|
133
|
+
* here for a row that is not theirs. The arm is keyed on what only the
|
|
134
|
+
* server can produce: the request entered through the in-process transport
|
|
135
|
+
* (the wire cannot claim that; the propagation header is stripped at the
|
|
136
|
+
* chassis), it carries the reserved default-instance label (which the label
|
|
137
|
+
* guard lets only such a request write), and its organization is the
|
|
138
|
+
* parent's (the invariant buildDefaultInstanceRequest holds). A labelled
|
|
139
|
+
* request that fails any of the three takes the personal bar in full — a
|
|
140
|
+
* mis-orged default instance is judged as the human's own act, never waved
|
|
141
|
+
* through.
|
|
142
|
+
*/
|
|
143
|
+
export function resolveInstanceCreateTargets(
|
|
144
|
+
ctx: RequestContext<typeof AgentInstanceSchema>,
|
|
145
|
+
): ReadonlyArray<AuthorizationTarget> {
|
|
146
|
+
const parent = ctx.get(PARENT_AGENT_KEY) as Agent | undefined;
|
|
147
|
+
if (parent === undefined) {
|
|
148
|
+
throw new Error("parent agent not found in context");
|
|
149
|
+
}
|
|
150
|
+
const metadata = ctx.newState.metadata;
|
|
151
|
+
const org = metadata?.org ?? "";
|
|
152
|
+
const parentId = parent.metadata?.id ?? "";
|
|
153
|
+
if (
|
|
154
|
+
isDefaultInstance(metadata) &&
|
|
155
|
+
isServerComposedRequest(ctx.callerIdentity) &&
|
|
156
|
+
org === (parent.metadata?.org ?? "")
|
|
157
|
+
) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
return [
|
|
161
|
+
{
|
|
162
|
+
permission: IamPermission.can_create_agent_instance,
|
|
163
|
+
resourceKind: ApiResourceKind.organization,
|
|
164
|
+
resourceId: org,
|
|
165
|
+
deniedMessage: INSTANCE_ORGANIZATION_DENIED_MESSAGE,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
permission: IamPermission.can_create_instance,
|
|
169
|
+
resourceKind: ApiResourceKind.agent,
|
|
170
|
+
resourceId: parentId,
|
|
171
|
+
deniedMessage: INSTANCE_PARENT_DENIED_MESSAGE,
|
|
172
|
+
},
|
|
173
|
+
];
|
|
174
|
+
}
|
|
175
|
+
|
|
102
176
|
/**
|
|
103
177
|
* ValidateInstanceUpdate — update.go: enforces the instance's immutable
|
|
104
178
|
* identity on update — spec.agent_id must keep referencing the same agent.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the create lane's two-arm consent bar (steps.ts,
|
|
3
|
+
* resolveShareCreateTargets), the Java handler's: a same-org share asks
|
|
4
|
+
* can_edit on the referenced agent; a cross-org share asks can_execute on
|
|
5
|
+
* the agent and then can_create_agent_share on the SHARING organization,
|
|
6
|
+
* agent first; each question carries its byte-pinned copy; a missing
|
|
7
|
+
* referenced agent throws.
|
|
8
|
+
*/
|
|
9
|
+
import { describe, expect, it } from "vitest";
|
|
10
|
+
import { create } from "@bufbuild/protobuf";
|
|
11
|
+
|
|
12
|
+
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
13
|
+
import { AgentShareSchema } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
|
|
14
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
15
|
+
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
16
|
+
|
|
17
|
+
import { testCallerIdentity } from "../../../pipeline/__tests__/support.js";
|
|
18
|
+
import { RequestContext } from "../../../pipeline/request-context.js";
|
|
19
|
+
import {
|
|
20
|
+
REFERENCED_AGENT_KEY,
|
|
21
|
+
SHARE_AGENT_DENIED_MESSAGE,
|
|
22
|
+
SHARE_ORGANIZATION_DENIED_MESSAGE,
|
|
23
|
+
resolveShareCreateTargets,
|
|
24
|
+
} from "../steps.js";
|
|
25
|
+
|
|
26
|
+
const agent = create(AgentSchema, {
|
|
27
|
+
metadata: { id: "agt_01public", name: "helper", org: "publisher-org" },
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
function ctxFor(shareOrg: string, agentOrg: string, stash = true) {
|
|
31
|
+
const ctx = new RequestContext(
|
|
32
|
+
AgentShareSchema,
|
|
33
|
+
create(AgentShareSchema, {
|
|
34
|
+
metadata: { name: "helper", org: shareOrg },
|
|
35
|
+
spec: { agentRef: { org: agentOrg, slug: "helper" } },
|
|
36
|
+
}),
|
|
37
|
+
testCallerIdentity(),
|
|
38
|
+
ApiResourceKind.agent_share,
|
|
39
|
+
);
|
|
40
|
+
if (stash) {
|
|
41
|
+
ctx.set(REFERENCED_AGENT_KEY, agent);
|
|
42
|
+
}
|
|
43
|
+
return ctx;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("resolveShareCreateTargets", () => {
|
|
47
|
+
it("a same-org share asks can_edit on the referenced agent", () => {
|
|
48
|
+
expect(
|
|
49
|
+
resolveShareCreateTargets(ctxFor("publisher-org", "publisher-org")),
|
|
50
|
+
).toEqual([
|
|
51
|
+
{
|
|
52
|
+
permission: IamPermission.can_edit,
|
|
53
|
+
resourceKind: ApiResourceKind.agent,
|
|
54
|
+
resourceId: "agt_01public",
|
|
55
|
+
deniedMessage: SHARE_AGENT_DENIED_MESSAGE,
|
|
56
|
+
},
|
|
57
|
+
]);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("a cross-org share asks can_execute on the agent, then can_create_agent_share on the sharing organization", () => {
|
|
61
|
+
expect(
|
|
62
|
+
resolveShareCreateTargets(ctxFor("consumer-org", "publisher-org")),
|
|
63
|
+
).toEqual([
|
|
64
|
+
{
|
|
65
|
+
permission: IamPermission.can_execute,
|
|
66
|
+
resourceKind: ApiResourceKind.agent,
|
|
67
|
+
resourceId: "agt_01public",
|
|
68
|
+
deniedMessage: SHARE_AGENT_DENIED_MESSAGE,
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
permission: IamPermission.can_create_agent_share,
|
|
72
|
+
resourceKind: ApiResourceKind.organization,
|
|
73
|
+
resourceId: "consumer-org",
|
|
74
|
+
deniedMessage: SHARE_ORGANIZATION_DENIED_MESSAGE,
|
|
75
|
+
},
|
|
76
|
+
]);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("a missing referenced agent is a broken chain invariant and throws", () => {
|
|
80
|
+
expect(() =>
|
|
81
|
+
resolveShareCreateTargets(
|
|
82
|
+
ctxFor("publisher-org", "publisher-org", false),
|
|
83
|
+
),
|
|
84
|
+
).toThrow("referenced agent not found in context");
|
|
85
|
+
});
|
|
86
|
+
});
|