@stigmer/server 3.18.0-dev.20260918103812 → 3.18.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/boot/compose.d.ts +12 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +4 -2
- package/dist/boot/compose.js.map +1 -1
- package/dist/domain/agentexecution/lifecycle.js +1 -1
- package/dist/domain/agentexecution/lifecycle.js.map +1 -1
- package/dist/domain/iampolicy/constants.d.ts +3 -3
- package/dist/domain/iampolicy/constants.js +3 -3
- package/dist/domain/oauthapp/refresolution/refresolution.js +1 -1
- package/dist/domain/oauthapp/refresolution.js +1 -1
- package/dist/domain/platform/controller.d.ts +8 -1
- package/dist/domain/platform/controller.d.ts.map +1 -1
- package/dist/domain/platform/controller.js +2 -3
- package/dist/domain/platform/controller.js.map +1 -1
- package/dist/domain/plugin/constants.d.ts +7 -1
- package/dist/domain/plugin/constants.d.ts.map +1 -1
- package/dist/domain/plugin/constants.js +7 -1
- package/dist/domain/plugin/constants.js.map +1 -1
- package/dist/domain/plugin/materialize/plan.d.ts.map +1 -1
- package/dist/domain/plugin/materialize/plan.js +16 -1
- package/dist/domain/plugin/materialize/plan.js.map +1 -1
- package/dist/domain/plugin/members.d.ts +46 -0
- package/dist/domain/plugin/members.d.ts.map +1 -1
- package/dist/domain/plugin/members.js +31 -8
- package/dist/domain/plugin/members.js.map +1 -1
- package/dist/domain/plugin/push.d.ts +5 -3
- package/dist/domain/plugin/push.d.ts.map +1 -1
- package/dist/domain/plugin/push.js +58 -14
- package/dist/domain/plugin/push.js.map +1 -1
- package/dist/domain/workflowexecution/lifecycle.js +1 -1
- package/dist/domain/workflowexecution/lifecycle.js.map +1 -1
- package/dist/pipeline/apiresource-labels.d.ts +20 -1
- package/dist/pipeline/apiresource-labels.d.ts.map +1 -1
- package/dist/pipeline/apiresource-labels.js +17 -0
- package/dist/pipeline/apiresource-labels.js.map +1 -1
- package/dist/sandbox/kubernetes.d.ts.map +1 -1
- package/dist/sandbox/kubernetes.js +1 -8
- package/dist/sandbox/kubernetes.js.map +1 -1
- package/dist/sandbox/provisioner.d.ts +5 -20
- package/dist/sandbox/provisioner.d.ts.map +1 -1
- package/dist/sandbox/provisioner.js.map +1 -1
- package/dist/sandbox/steps.d.ts +8 -22
- package/dist/sandbox/steps.d.ts.map +1 -1
- package/dist/sandbox/steps.js +12 -17
- package/dist/sandbox/steps.js.map +1 -1
- package/dist/store/interface.d.ts +6 -2
- package/dist/store/interface.d.ts.map +1 -1
- package/dist/store/interface.js.map +1 -1
- package/package.json +5 -5
- package/src/boot/compose.ts +16 -2
- package/src/domain/agentexecution/lifecycle.ts +1 -1
- package/src/domain/iampolicy/constants.ts +3 -3
- package/src/domain/oauthapp/__tests__/oauthapp.test.ts +2 -2
- package/src/domain/oauthapp/__tests__/refresolution.test.ts +2 -2
- package/src/domain/oauthapp/refresolution/refresolution.ts +1 -1
- package/src/domain/oauthapp/refresolution.ts +1 -1
- package/src/domain/platform/__tests__/platform.test.ts +47 -0
- package/src/domain/platform/controller.ts +10 -4
- package/src/domain/plugin/__tests__/materialize.test.ts +42 -47
- package/src/domain/plugin/__tests__/members.test.ts +149 -0
- package/src/domain/plugin/__tests__/plugin.test.ts +219 -8
- package/src/domain/plugin/constants.ts +7 -1
- package/src/domain/plugin/materialize/plan.ts +20 -1
- package/src/domain/plugin/members.ts +76 -7
- package/src/domain/plugin/push.ts +80 -20
- package/src/domain/workflowexecution/lifecycle.ts +1 -1
- package/src/pipeline/apiresource-labels.ts +26 -1
- package/src/sandbox/__tests__/docker.integration.test.ts +0 -2
- package/src/sandbox/__tests__/kubernetes.test.ts +2 -10
- package/src/sandbox/__tests__/local-process.integration.test.ts +0 -3
- package/src/sandbox/__tests__/steps.test.ts +19 -72
- package/src/sandbox/kubernetes.ts +1 -8
- package/src/sandbox/provisioner.ts +5 -20
- package/src/sandbox/steps.ts +12 -29
- package/src/store/interface.ts +6 -2
|
@@ -15,6 +15,9 @@
|
|
|
15
15
|
* execution id;
|
|
16
16
|
* - getRunnerBootstrapConfig echoes the configured Temporal coordinates
|
|
17
17
|
* with the token fields empty (minting a proxy token is cloud-only);
|
|
18
|
+
* - getServerInfo reports the build stamp by default and the release a
|
|
19
|
+
* library consumer states through ComposeOptions.version otherwise
|
|
20
|
+
* (stigmer#1168: unbundled, the stamp is always "dev");
|
|
18
21
|
* - getLicenseStatus answers `absent` with no claims and no key when no
|
|
19
22
|
* provider is composed, and the composed provider's report verbatim
|
|
20
23
|
* when one is — both stamped with the ONE instant the controller
|
|
@@ -229,6 +232,48 @@ describe("platform domain (composed server)", () => {
|
|
|
229
232
|
});
|
|
230
233
|
});
|
|
231
234
|
|
|
235
|
+
describe("platform domain (composed with a stated version)", () => {
|
|
236
|
+
let server: ComposedServer;
|
|
237
|
+
let client: PlatformClient;
|
|
238
|
+
let dir: string;
|
|
239
|
+
|
|
240
|
+
beforeAll(async () => {
|
|
241
|
+
dir = mkdtempSync(path.join(tmpdir(), "platform-domain-version-test-"));
|
|
242
|
+
vi.stubEnv("STIGMER_ENCRYPTION_KEY", ENC_KEY.toString("base64"));
|
|
243
|
+
server = await composeServer({
|
|
244
|
+
config: loadConfig({
|
|
245
|
+
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
246
|
+
DB_PATH: path.join(dir, "stigmer.db"),
|
|
247
|
+
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
248
|
+
}),
|
|
249
|
+
logger: silentLogger,
|
|
250
|
+
version: "3.99.0",
|
|
251
|
+
portOverride: 0,
|
|
252
|
+
host: "127.0.0.1",
|
|
253
|
+
});
|
|
254
|
+
const port = await server.start();
|
|
255
|
+
client = createClient(
|
|
256
|
+
PlatformQueryController,
|
|
257
|
+
createGrpcTransport({ baseUrl: `http://127.0.0.1:${port}` }),
|
|
258
|
+
);
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
afterAll(async () => {
|
|
262
|
+
await server.shutdown();
|
|
263
|
+
rmSync(dir, { recursive: true, force: true });
|
|
264
|
+
vi.unstubAllEnvs();
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("getServerInfo reports the composition's stated release, not the build stamp", async () => {
|
|
268
|
+
// A library consumer (the cloud composition) runs no bundle, so the
|
|
269
|
+
// define is never stamped for it; ComposeOptions.version is how it
|
|
270
|
+
// states the release it installed. Edition is untouched by it.
|
|
271
|
+
const info = await client.getServerInfo({});
|
|
272
|
+
expect(info.version).toBe("3.99.0");
|
|
273
|
+
expect(info.edition).toBe(ServerEdition.oss);
|
|
274
|
+
});
|
|
275
|
+
});
|
|
276
|
+
|
|
232
277
|
describe("platform domain (keyless runner-token service)", () => {
|
|
233
278
|
it("a keyless service answers not-minted — fail-soft, never an error", async () => {
|
|
234
279
|
const transport = createRouterTransport((router) => {
|
|
@@ -239,6 +284,7 @@ describe("platform domain (keyless runner-token service)", () => {
|
|
|
239
284
|
RunnerAuthService.create(undefined),
|
|
240
285
|
),
|
|
241
286
|
edition: ServerEdition.oss,
|
|
287
|
+
version: "dev",
|
|
242
288
|
licenseStatus: ABSENT_LICENSE_STATUS,
|
|
243
289
|
now: () => new Date(),
|
|
244
290
|
logger: silentLogger,
|
|
@@ -273,6 +319,7 @@ describe("platform domain (license status)", () => {
|
|
|
273
319
|
RunnerAuthService.create(undefined),
|
|
274
320
|
),
|
|
275
321
|
edition: ServerEdition.enterprise,
|
|
322
|
+
version: "dev",
|
|
276
323
|
licenseStatus: provider,
|
|
277
324
|
now: () => instant,
|
|
278
325
|
logger: silentLogger,
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* getRunnerBootstrapConfig and getLicenseStatus shapes; getRunnerScopedToken
|
|
8
8
|
* is deliberately excluded there — its arms are exercised mid-execution)
|
|
9
9
|
* and __tests__/platform.test.ts (the fail-soft mint matrix, the license
|
|
10
|
-
* status arms, the #15 composed-server pattern).
|
|
10
|
+
* status arms, the stated-version arm, the #15 composed-server pattern).
|
|
11
11
|
*
|
|
12
12
|
* getLicenseStatus is the one arm every edition answers from a driver
|
|
13
13
|
* point: a server's license is a fact about the server, like its edition,
|
|
@@ -75,7 +75,6 @@ import type {
|
|
|
75
75
|
RunnerScopedTokenRequest,
|
|
76
76
|
} from "../../runnerauth/runner-credential-provider.js";
|
|
77
77
|
import { TOKEN_TYPE_EXECUTION_SCOPED } from "../../runnerauth/runnerauth.js";
|
|
78
|
-
import { SERVER_VERSION } from "./version.js";
|
|
79
78
|
|
|
80
79
|
export interface PlatformControllerDeps {
|
|
81
80
|
/**
|
|
@@ -100,6 +99,13 @@ export interface PlatformControllerDeps {
|
|
|
100
99
|
* cloud composition answers `cloud` without forking this controller.
|
|
101
100
|
*/
|
|
102
101
|
readonly edition: ServerEdition;
|
|
102
|
+
/**
|
|
103
|
+
* The release reported beside the edition. The composition root resolves
|
|
104
|
+
* it (ComposeOptions.version, else SERVER_VERSION), so this controller
|
|
105
|
+
* never reads the build stamp itself: a library consumer with no bundle
|
|
106
|
+
* states its installed release and answers exactly as a bundle would.
|
|
107
|
+
*/
|
|
108
|
+
readonly version: string;
|
|
103
109
|
/**
|
|
104
110
|
* What license this server holds (extensions/license-status.ts). The
|
|
105
111
|
* composition root installs the built-in `absent` provider when no unit
|
|
@@ -130,11 +136,11 @@ export function registerPlatformServices(
|
|
|
130
136
|
});
|
|
131
137
|
}
|
|
132
138
|
|
|
133
|
-
/** Go GetServerInfo: the server edition and
|
|
139
|
+
/** Go GetServerInfo: the server edition and release version. */
|
|
134
140
|
function getServerInfo(deps: PlatformControllerDeps): GetServerInfoOutput {
|
|
135
141
|
return create(GetServerInfoOutputSchema, {
|
|
136
142
|
edition: deps.edition,
|
|
137
|
-
version:
|
|
143
|
+
version: deps.version,
|
|
138
144
|
});
|
|
139
145
|
}
|
|
140
146
|
|
|
@@ -8,8 +8,10 @@
|
|
|
8
8
|
* SKILL.md with the plugin's labels on the request, a version the tag
|
|
9
9
|
* pattern rejects becoming a warning, an overlay that redefines the
|
|
10
10
|
* transport refused, the composed agent declaring its tools' variables
|
|
11
|
-
* (OAuth targets excluded, an authored agent left as written), and
|
|
12
|
-
*
|
|
11
|
+
* (OAuth targets excluded, an authored agent left as written), and each
|
|
12
|
+
* planned member's system flag read from its overlay and never from a
|
|
13
|
+
* skill. The member rules themselves (the slug decision, convergence, the
|
|
14
|
+
* dropped set) are members.test.ts's.
|
|
13
15
|
*/
|
|
14
16
|
import { describe, expect, it } from "vitest";
|
|
15
17
|
|
|
@@ -37,6 +39,7 @@ import { openArchive } from "../../../archive/open.js";
|
|
|
37
39
|
import {
|
|
38
40
|
PLUGIN_LABEL,
|
|
39
41
|
PLUGIN_VERSION_LABEL,
|
|
42
|
+
SYSTEM_LABEL,
|
|
40
43
|
} from "../../../pipeline/apiresource-labels.js";
|
|
41
44
|
import {
|
|
42
45
|
DEFAULT_AGENT_INSTRUCTIONS_VERSION,
|
|
@@ -45,8 +48,6 @@ import {
|
|
|
45
48
|
import type { PluginIdentity } from "../materialize/identity.js";
|
|
46
49
|
import { McpServerOverlayError } from "../materialize/mcp-servers.js";
|
|
47
50
|
import { planMaterialization } from "../materialize/plan.js";
|
|
48
|
-
import { droppedMembers, membersConverge } from "../members.js";
|
|
49
|
-
import type { Member } from "../members.js";
|
|
50
51
|
|
|
51
52
|
const IDENTITY: PluginIdentity = {
|
|
52
53
|
org: "acme",
|
|
@@ -231,7 +232,10 @@ describe("planMaterialization", () => {
|
|
|
231
232
|
|
|
232
233
|
it("declares the servers' variables on the composed agent so a session asks for them", () => {
|
|
233
234
|
expect(plan.agent!.resource.spec?.env).toEqual({
|
|
234
|
-
GITHUB_TOKEN: expect.objectContaining({
|
|
235
|
+
GITHUB_TOKEN: expect.objectContaining({
|
|
236
|
+
isSecret: true,
|
|
237
|
+
optional: false,
|
|
238
|
+
}),
|
|
235
239
|
});
|
|
236
240
|
});
|
|
237
241
|
|
|
@@ -259,7 +263,9 @@ describe("planMaterialization", () => {
|
|
|
259
263
|
);
|
|
260
264
|
// The server still declares it (the connect flow reads it there); the
|
|
261
265
|
// agent does not, because the value comes from a managed environment.
|
|
262
|
-
expect(withOAuth.mcpServers[0]!.resource.spec?.env).toHaveProperty(
|
|
266
|
+
expect(withOAuth.mcpServers[0]!.resource.spec?.env).toHaveProperty(
|
|
267
|
+
"GITHUB_TOKEN",
|
|
268
|
+
);
|
|
263
269
|
expect(withOAuth.agent!.resource.spec?.env).toEqual({});
|
|
264
270
|
});
|
|
265
271
|
|
|
@@ -313,50 +319,39 @@ describe("planMaterialization", () => {
|
|
|
313
319
|
});
|
|
314
320
|
});
|
|
315
321
|
|
|
316
|
-
describe("members:
|
|
317
|
-
const
|
|
318
|
-
const stored: Member[] = [
|
|
319
|
-
{
|
|
320
|
-
kind: ApiResourceKind.skill,
|
|
321
|
-
id: "skl_1",
|
|
322
|
-
slug: "thermo-review",
|
|
323
|
-
name: "thermo-review",
|
|
324
|
-
version: digest,
|
|
325
|
-
},
|
|
326
|
-
{
|
|
327
|
-
kind: ApiResourceKind.agent,
|
|
328
|
-
id: "agt_1",
|
|
329
|
-
slug: "thermos",
|
|
330
|
-
name: "thermos",
|
|
331
|
-
version: digest,
|
|
332
|
-
},
|
|
333
|
-
];
|
|
334
|
-
const planned = [
|
|
335
|
-
{
|
|
336
|
-
kind: ApiResourceKind.skill,
|
|
337
|
-
slug: "thermo-review",
|
|
338
|
-
name: "thermo-review",
|
|
339
|
-
},
|
|
340
|
-
{ kind: ApiResourceKind.agent, slug: "thermos", name: "thermos" },
|
|
341
|
-
];
|
|
322
|
+
describe("planned members: the system flag", () => {
|
|
323
|
+
const pkg = read(thermos);
|
|
342
324
|
|
|
343
|
-
it("
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
325
|
+
it("reads system content from the overlay's labels, never from a skill's request", () => {
|
|
326
|
+
const authored = create(AgentSchema, {
|
|
327
|
+
metadata: { name: "thermos", labels: { [SYSTEM_LABEL]: "true" } },
|
|
328
|
+
spec: create(AgentSpecSchema, { instructions: "You read thermostats." }),
|
|
329
|
+
});
|
|
330
|
+
const plan = planMaterialization(
|
|
331
|
+
pkg,
|
|
332
|
+
inMemoryPluginFiles(thermos),
|
|
333
|
+
{
|
|
334
|
+
agent: { path: "ai.stigmer/agent.yaml", resource: authored },
|
|
335
|
+
workflows: [],
|
|
336
|
+
mcpServers: [],
|
|
337
|
+
},
|
|
338
|
+
IDENTITY,
|
|
339
|
+
"",
|
|
340
|
+
);
|
|
341
|
+
const byKind = new Map(plan.members.map((m) => [m.kind, m.system]));
|
|
342
|
+
expect(byKind.get(ApiResourceKind.agent)).toBe(true);
|
|
343
|
+
expect(byKind.get(ApiResourceKind.skill)).toBe(false);
|
|
344
|
+
expect(byKind.get(ApiResourceKind.mcp_server)).toBe(false);
|
|
354
345
|
});
|
|
355
346
|
|
|
356
|
-
it("
|
|
357
|
-
|
|
358
|
-
|
|
347
|
+
it("plans no system content when no overlay declares it", () => {
|
|
348
|
+
const plan = planMaterialization(
|
|
349
|
+
pkg,
|
|
350
|
+
inMemoryPluginFiles(thermos),
|
|
351
|
+
{ workflows: [], mcpServers: [] },
|
|
352
|
+
IDENTITY,
|
|
353
|
+
"",
|
|
359
354
|
);
|
|
360
|
-
expect(
|
|
355
|
+
expect(plan.members.every((m) => m.system === false)).toBe(true);
|
|
361
356
|
});
|
|
362
357
|
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pins the member rules on plain values, no store: the slug decision
|
|
3
|
+
* (`judgeSlug`) in each of its five arms — free, ours, adopt when system
|
|
4
|
+
* content meets system content, refused for an unmanaged row whenever
|
|
5
|
+
* either side lacks the label, refused for another plugin's member
|
|
6
|
+
* whatever labels either side carries — and the convergence and drop rules
|
|
7
|
+
* over stored and planned members. The same decision through a composed
|
|
8
|
+
* server, with a real authorizer, is plugin.test.ts's.
|
|
9
|
+
*/
|
|
10
|
+
import { describe, expect, it } from "vitest";
|
|
11
|
+
|
|
12
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
13
|
+
|
|
14
|
+
import { droppedMembers, judgeSlug, membersConverge } from "../members.js";
|
|
15
|
+
import type { Member, PlannedMember, SlugHolder } from "../members.js";
|
|
16
|
+
|
|
17
|
+
const DIGEST = "d".repeat(64);
|
|
18
|
+
const OURS = "plg_ours";
|
|
19
|
+
const OTHER = "plg_other";
|
|
20
|
+
|
|
21
|
+
function stored(overrides: Partial<Member> = {}): Member {
|
|
22
|
+
return {
|
|
23
|
+
kind: ApiResourceKind.agent,
|
|
24
|
+
id: "agt_1",
|
|
25
|
+
slug: "assistant",
|
|
26
|
+
name: "assistant",
|
|
27
|
+
version: DIGEST,
|
|
28
|
+
system: false,
|
|
29
|
+
...overrides,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function planned(overrides: Partial<PlannedMember> = {}): PlannedMember {
|
|
34
|
+
return {
|
|
35
|
+
kind: ApiResourceKind.agent,
|
|
36
|
+
slug: "assistant",
|
|
37
|
+
name: "assistant",
|
|
38
|
+
system: false,
|
|
39
|
+
...overrides,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function heldBy(
|
|
44
|
+
byPlugin: string | undefined,
|
|
45
|
+
holder: Member = stored(),
|
|
46
|
+
): SlugHolder {
|
|
47
|
+
return { held: true, byPlugin, holder };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe("judgeSlug", () => {
|
|
51
|
+
it("is free when nothing holds the slug", () => {
|
|
52
|
+
expect(judgeSlug({ held: false }, planned(), OURS)).toEqual({
|
|
53
|
+
kind: "free",
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it("is ours when this plugin already holds it, whatever the labels", () => {
|
|
58
|
+
const holder = stored({ system: true });
|
|
59
|
+
expect(judgeSlug(heldBy(OURS, holder), planned(), OURS)).toEqual({
|
|
60
|
+
kind: "ours",
|
|
61
|
+
holder,
|
|
62
|
+
});
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("adopts an unmanaged row only when both sides are system content", () => {
|
|
66
|
+
const holder = stored({ system: true });
|
|
67
|
+
expect(
|
|
68
|
+
judgeSlug(heldBy(undefined, holder), planned({ system: true }), OURS),
|
|
69
|
+
).toEqual({ kind: "adopt", holder });
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("refuses an unmanaged row when either side lacks the system label", () => {
|
|
73
|
+
const userRow = stored({ system: false });
|
|
74
|
+
expect(
|
|
75
|
+
judgeSlug(heldBy(undefined, userRow), planned({ system: true }), OURS),
|
|
76
|
+
).toEqual({ kind: "held-unmanaged", holder: userRow });
|
|
77
|
+
|
|
78
|
+
const systemRow = stored({ system: true });
|
|
79
|
+
expect(
|
|
80
|
+
judgeSlug(heldBy(undefined, systemRow), planned({ system: false }), OURS),
|
|
81
|
+
).toEqual({ kind: "held-unmanaged", holder: systemRow });
|
|
82
|
+
|
|
83
|
+
expect(
|
|
84
|
+
judgeSlug(heldBy(undefined, userRow), planned({ system: false }), OURS),
|
|
85
|
+
).toEqual({ kind: "held-unmanaged", holder: userRow });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("never transfers another plugin's member, even system content to system content", () => {
|
|
89
|
+
const holder = stored({ system: true });
|
|
90
|
+
expect(
|
|
91
|
+
judgeSlug(heldBy(OTHER, holder), planned({ system: true }), OURS),
|
|
92
|
+
).toEqual({ kind: "held-by-plugin", holder, pluginId: OTHER });
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("members: convergence and drops", () => {
|
|
97
|
+
const stored: Member[] = [
|
|
98
|
+
{
|
|
99
|
+
kind: ApiResourceKind.skill,
|
|
100
|
+
id: "skl_1",
|
|
101
|
+
slug: "thermo-review",
|
|
102
|
+
name: "thermo-review",
|
|
103
|
+
version: DIGEST,
|
|
104
|
+
system: false,
|
|
105
|
+
},
|
|
106
|
+
{
|
|
107
|
+
kind: ApiResourceKind.agent,
|
|
108
|
+
id: "agt_1",
|
|
109
|
+
slug: "thermos",
|
|
110
|
+
name: "thermos",
|
|
111
|
+
version: DIGEST,
|
|
112
|
+
system: false,
|
|
113
|
+
},
|
|
114
|
+
];
|
|
115
|
+
const planned: PlannedMember[] = [
|
|
116
|
+
{
|
|
117
|
+
kind: ApiResourceKind.skill,
|
|
118
|
+
slug: "thermo-review",
|
|
119
|
+
name: "thermo-review",
|
|
120
|
+
system: false,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
kind: ApiResourceKind.agent,
|
|
124
|
+
slug: "thermos",
|
|
125
|
+
name: "thermos",
|
|
126
|
+
system: false,
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
it("converges only when every planned member is present and stamped with the digest", () => {
|
|
131
|
+
expect(membersConverge(stored, planned, DIGEST)).toBe(true);
|
|
132
|
+
expect(membersConverge(stored, planned, "e".repeat(64))).toBe(false);
|
|
133
|
+
expect(membersConverge(stored.slice(1), planned, DIGEST)).toBe(false);
|
|
134
|
+
expect(
|
|
135
|
+
membersConverge(
|
|
136
|
+
[...stored, { ...stored[0]!, slug: "extra", id: "skl_2" }],
|
|
137
|
+
planned,
|
|
138
|
+
DIGEST,
|
|
139
|
+
),
|
|
140
|
+
).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("drops the members the plan no longer names", () => {
|
|
144
|
+
expect(droppedMembers(stored, planned.slice(1)).map((m) => m.slug)).toEqual(
|
|
145
|
+
["thermo-review"],
|
|
146
|
+
);
|
|
147
|
+
expect(droppedMembers(stored, planned)).toEqual([]);
|
|
148
|
+
});
|
|
149
|
+
});
|
|
@@ -9,10 +9,14 @@
|
|
|
9
9
|
* delete of a member is refused naming the plugin while the plugin's own
|
|
10
10
|
* upgrade drops what the archive dropped; uninstall removes every member
|
|
11
11
|
* and is refused while a user's own agent still references one; a slug an
|
|
12
|
-
* unmanaged resource holds refuses the install
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
12
|
+
* unmanaged resource holds refuses the install, and a user's row is never
|
|
13
|
+
* adopted whichever side declares system content; a bad overlay refuses
|
|
14
|
+
* before any write. A second composition, under an authorizer that decides
|
|
15
|
+
* `can_write_reserved_labels` per test, pins the reserved-label refusal the
|
|
16
|
+
* open-source posture allows by design, and the one adoption the platform
|
|
17
|
+
* makes: an operator's plugin taking over the system-content row it
|
|
18
|
+
* replaces in place, the row's id and default instance kept, a member's
|
|
19
|
+
* identical push refused before the slug is judged.
|
|
16
20
|
*/
|
|
17
21
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
18
22
|
import net from "node:net";
|
|
@@ -38,6 +42,7 @@ import {
|
|
|
38
42
|
AgentSpecSchema,
|
|
39
43
|
McpServerUsageSchema,
|
|
40
44
|
} from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
45
|
+
import { AgentInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/query_pb";
|
|
41
46
|
import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
|
|
42
47
|
import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
|
|
43
48
|
import { PluginCommandController } from "@stigmer/protos/ai/stigmer/agentic/plugin/v1/command_pb";
|
|
@@ -59,6 +64,7 @@ import { createLogger } from "../../../boot/logger.js";
|
|
|
59
64
|
import {
|
|
60
65
|
PLUGIN_LABEL,
|
|
61
66
|
PLUGIN_VERSION_LABEL,
|
|
67
|
+
SYSTEM_LABEL,
|
|
62
68
|
} from "../../../pipeline/apiresource-labels.js";
|
|
63
69
|
|
|
64
70
|
const silentLogger = createLogger({
|
|
@@ -507,6 +513,66 @@ describe("Plugin members are the plugin's to redefine", () => {
|
|
|
507
513
|
);
|
|
508
514
|
});
|
|
509
515
|
|
|
516
|
+
it("never adopts a user's agent: a same-slug row without the system label is refused even when the plugin declares system content, and a system row is refused by a plugin that does not", async () => {
|
|
517
|
+
// The open-source authorizer grants reserved-label writes to everyone,
|
|
518
|
+
// so this is the posture where the label alone would be a weak guard;
|
|
519
|
+
// the rule needs BOTH sides to carry it.
|
|
520
|
+
const name = uniqueName("users-own");
|
|
521
|
+
await agents.create(
|
|
522
|
+
createMessage(AgentSchema, {
|
|
523
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
524
|
+
kind: "Agent",
|
|
525
|
+
metadata: createMessage(ApiResourceMetadataSchema, { org: ORG, name }),
|
|
526
|
+
spec: createMessage(AgentSpecSchema, {
|
|
527
|
+
instructions: "Mine, written by hand, under my own name.",
|
|
528
|
+
}),
|
|
529
|
+
}),
|
|
530
|
+
);
|
|
531
|
+
await expectCode(
|
|
532
|
+
plugins.push({ org: ORG, artifact: archiveOf(systemOverlay(name)) }),
|
|
533
|
+
Code.AlreadyExists,
|
|
534
|
+
`'${name}' exists in org '${ORG}' and is not managed by a plugin`,
|
|
535
|
+
);
|
|
536
|
+
|
|
537
|
+
const seeded = uniqueName("seeded-row");
|
|
538
|
+
await agents.create(
|
|
539
|
+
createMessage(AgentSchema, {
|
|
540
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
541
|
+
kind: "Agent",
|
|
542
|
+
metadata: createMessage(ApiResourceMetadataSchema, {
|
|
543
|
+
org: ORG,
|
|
544
|
+
name: seeded,
|
|
545
|
+
labels: { [SYSTEM_LABEL]: "true" },
|
|
546
|
+
}),
|
|
547
|
+
spec: createMessage(AgentSpecSchema, {
|
|
548
|
+
instructions: "System content a plain plugin may not take over.",
|
|
549
|
+
}),
|
|
550
|
+
}),
|
|
551
|
+
);
|
|
552
|
+
await expectCode(
|
|
553
|
+
plugins.push({
|
|
554
|
+
org: ORG,
|
|
555
|
+
artifact: archiveOf(
|
|
556
|
+
withFile(
|
|
557
|
+
thermosLike(seeded),
|
|
558
|
+
"ai.stigmer/agent.yaml",
|
|
559
|
+
[
|
|
560
|
+
"apiVersion: agentic.stigmer.ai/v1",
|
|
561
|
+
"kind: Agent",
|
|
562
|
+
"metadata:",
|
|
563
|
+
` name: ${seeded}`,
|
|
564
|
+
"spec:",
|
|
565
|
+
" instructions: A plain overlay that declares no system content.",
|
|
566
|
+
"",
|
|
567
|
+
].join("\n"),
|
|
568
|
+
),
|
|
569
|
+
),
|
|
570
|
+
}),
|
|
571
|
+
Code.AlreadyExists,
|
|
572
|
+
`'${seeded}' exists in org '${ORG}' and is not managed by a plugin`,
|
|
573
|
+
);
|
|
574
|
+
});
|
|
575
|
+
|
|
510
576
|
it("refuses to install a slug another plugin holds, naming that plugin", async () => {
|
|
511
577
|
const first = uniqueName("holder");
|
|
512
578
|
const second = uniqueName("claimant");
|
|
@@ -705,7 +771,14 @@ describe("Plugin push under an authorizer that enforces reserved labels", () =>
|
|
|
705
771
|
let enforcing: ComposedServer;
|
|
706
772
|
let enforcingPlugins: Client<typeof PluginCommandController>;
|
|
707
773
|
let enforcingQuery: Client<typeof PluginQueryController>;
|
|
774
|
+
let enforcingAgents: Client<typeof AgentCommandController>;
|
|
775
|
+
let enforcingAgentQuery: Client<typeof AgentQueryController>;
|
|
776
|
+
let enforcingInstanceQuery: Client<typeof AgentInstanceQueryController>;
|
|
708
777
|
const observed: AuthzCheck[] = [];
|
|
778
|
+
// Who the caller is to this authorizer: a member (reserved labels denied,
|
|
779
|
+
// the hosted default) or the platform operator (granted). The tests flip
|
|
780
|
+
// it, so one composition shows both sides of every reserved-label rule.
|
|
781
|
+
let reservedLabels: "deny" | "allow" = "deny";
|
|
709
782
|
|
|
710
783
|
beforeAll(async () => {
|
|
711
784
|
enforcingDir = mkdtempSync(path.join(tmpdir(), "plugin-domain-enforcing-"));
|
|
@@ -714,10 +787,11 @@ describe("Plugin push under an authorizer that enforces reserved labels", () =>
|
|
|
714
787
|
authorize(_caller, check) {
|
|
715
788
|
if (check.permission === IamPermission.can_write_reserved_labels) {
|
|
716
789
|
observed.push(check);
|
|
717
|
-
return Promise.resolve(
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
790
|
+
return Promise.resolve(
|
|
791
|
+
reservedLabels === "allow"
|
|
792
|
+
? { kind: "allow" }
|
|
793
|
+
: { kind: "deny", reason: "reserved labels are the platform's" },
|
|
794
|
+
);
|
|
721
795
|
}
|
|
722
796
|
return Promise.resolve({ kind: "allow" });
|
|
723
797
|
},
|
|
@@ -741,6 +815,12 @@ describe("Plugin push under an authorizer that enforces reserved labels", () =>
|
|
|
741
815
|
});
|
|
742
816
|
enforcingPlugins = createClient(PluginCommandController, transport);
|
|
743
817
|
enforcingQuery = createClient(PluginQueryController, transport);
|
|
818
|
+
enforcingAgents = createClient(AgentCommandController, transport);
|
|
819
|
+
enforcingAgentQuery = createClient(AgentQueryController, transport);
|
|
820
|
+
enforcingInstanceQuery = createClient(
|
|
821
|
+
AgentInstanceQueryController,
|
|
822
|
+
transport,
|
|
823
|
+
);
|
|
744
824
|
});
|
|
745
825
|
|
|
746
826
|
afterAll(async () => {
|
|
@@ -799,4 +879,135 @@ describe("Plugin push under an authorizer that enforces reserved labels", () =>
|
|
|
799
879
|
});
|
|
800
880
|
expect(members).toHaveLength(3);
|
|
801
881
|
});
|
|
882
|
+
|
|
883
|
+
it("adopts a system-content row the operator's plugin replaces: same id, same default instance, the plugin's definition, one warning", async () => {
|
|
884
|
+
reservedLabels = "allow";
|
|
885
|
+
const name = uniqueName("plg-adopt");
|
|
886
|
+
// The row the platform seeded before plugins existed: system content,
|
|
887
|
+
// no plugin label, a default instance of its own.
|
|
888
|
+
const seeded = await enforcingAgents.create(
|
|
889
|
+
createMessage(AgentSchema, {
|
|
890
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
891
|
+
kind: "Agent",
|
|
892
|
+
metadata: createMessage(ApiResourceMetadataSchema, {
|
|
893
|
+
org: ORG,
|
|
894
|
+
name,
|
|
895
|
+
labels: { [SYSTEM_LABEL]: "true" },
|
|
896
|
+
}),
|
|
897
|
+
spec: createMessage(AgentSpecSchema, {
|
|
898
|
+
instructions: "The instructions the seedpack wrote, long ago.",
|
|
899
|
+
}),
|
|
900
|
+
}),
|
|
901
|
+
);
|
|
902
|
+
const seededId = seeded.metadata!.id;
|
|
903
|
+
const seededInstanceId = seeded.status?.defaultInstanceId ?? "";
|
|
904
|
+
expect(seededInstanceId).not.toBe("");
|
|
905
|
+
|
|
906
|
+
const installed = await enforcingPlugins.push({
|
|
907
|
+
org: ORG,
|
|
908
|
+
artifact: archiveOf(systemOverlay(name)),
|
|
909
|
+
});
|
|
910
|
+
expect(installed.status?.state).toBe(PluginState.READY);
|
|
911
|
+
const adopted = installed.status!.warnings.filter(
|
|
912
|
+
(warning) => warning.kind === "member-adopted",
|
|
913
|
+
);
|
|
914
|
+
expect(adopted).toHaveLength(1);
|
|
915
|
+
expect(adopted[0]!.path).toBe("ai.stigmer/agent.yaml");
|
|
916
|
+
expect(adopted[0]!.message).toContain(`agent '${name}' (${seededId})`);
|
|
917
|
+
expect(adopted[0]!.message).toContain("conversations continue");
|
|
918
|
+
|
|
919
|
+
const after = await enforcingAgentQuery.get({ value: seededId });
|
|
920
|
+
expect(after.metadata?.labels[PLUGIN_LABEL]).toBe(installed.metadata!.id);
|
|
921
|
+
expect(after.metadata?.labels[PLUGIN_VERSION_LABEL]).toBe(
|
|
922
|
+
installed.status!.digest,
|
|
923
|
+
);
|
|
924
|
+
expect(after.metadata?.labels[SYSTEM_LABEL]).toBe("true");
|
|
925
|
+
expect(after.spec?.instructions).toBe(
|
|
926
|
+
"You are the platform's own agent, re-homed under a plugin.",
|
|
927
|
+
);
|
|
928
|
+
expect(after.status?.defaultInstanceId).toBe(seededInstanceId);
|
|
929
|
+
const instance = await enforcingInstanceQuery.get({
|
|
930
|
+
value: seededInstanceId,
|
|
931
|
+
});
|
|
932
|
+
expect(instance.spec?.agentId).toBe(seededId);
|
|
933
|
+
|
|
934
|
+
const { members } = await enforcingQuery.listMembers({
|
|
935
|
+
value: installed.metadata!.id,
|
|
936
|
+
});
|
|
937
|
+
expect(
|
|
938
|
+
members.find((member) => member.kind === ApiResourceKind.agent)?.id,
|
|
939
|
+
).toBe(seededId);
|
|
940
|
+
|
|
941
|
+
// The second push of the same archive converges without a write.
|
|
942
|
+
const again = await enforcingPlugins.push({
|
|
943
|
+
org: ORG,
|
|
944
|
+
artifact: archiveOf(systemOverlay(name)),
|
|
945
|
+
});
|
|
946
|
+
expect(again.status?.digest).toBe(installed.status?.digest);
|
|
947
|
+
expect(
|
|
948
|
+
(await enforcingAgentQuery.get({ value: seededId })).status
|
|
949
|
+
?.defaultInstanceId,
|
|
950
|
+
).toBe(seededInstanceId);
|
|
951
|
+
reservedLabels = "deny";
|
|
952
|
+
});
|
|
953
|
+
|
|
954
|
+
it("refuses the same archive from a member before the slug is judged, and leaves the seeded row untouched", async () => {
|
|
955
|
+
reservedLabels = "allow";
|
|
956
|
+
const name = uniqueName("plg-member");
|
|
957
|
+
const seeded = await enforcingAgents.create(
|
|
958
|
+
createMessage(AgentSchema, {
|
|
959
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
960
|
+
kind: "Agent",
|
|
961
|
+
metadata: createMessage(ApiResourceMetadataSchema, {
|
|
962
|
+
org: ORG,
|
|
963
|
+
name,
|
|
964
|
+
labels: { [SYSTEM_LABEL]: "true" },
|
|
965
|
+
}),
|
|
966
|
+
spec: createMessage(AgentSpecSchema, {
|
|
967
|
+
instructions: "The instructions the seedpack wrote, long ago.",
|
|
968
|
+
}),
|
|
969
|
+
}),
|
|
970
|
+
);
|
|
971
|
+
reservedLabels = "deny";
|
|
972
|
+
const error = await expectCode(
|
|
973
|
+
enforcingPlugins.push({
|
|
974
|
+
org: ORG,
|
|
975
|
+
artifact: archiveOf(systemOverlay(name)),
|
|
976
|
+
}),
|
|
977
|
+
Code.InvalidArgument,
|
|
978
|
+
);
|
|
979
|
+
expect(error.rawMessage).toContain(
|
|
980
|
+
`cannot be set by a plugin (ai.stigmer/agent.yaml: ${SYSTEM_LABEL})`,
|
|
981
|
+
);
|
|
982
|
+
const untouched = await enforcingAgentQuery.get({
|
|
983
|
+
value: seeded.metadata!.id,
|
|
984
|
+
});
|
|
985
|
+
expect(untouched.metadata?.labels[PLUGIN_LABEL]).toBeUndefined();
|
|
986
|
+
expect(untouched.spec?.instructions).toBe(
|
|
987
|
+
"The instructions the seedpack wrote, long ago.",
|
|
988
|
+
);
|
|
989
|
+
});
|
|
802
990
|
});
|
|
991
|
+
|
|
992
|
+
/**
|
|
993
|
+
* A plugin whose agent overlay declares system content under the given
|
|
994
|
+
* name: the shape of the official `assistant` plugin, whose agent replaces
|
|
995
|
+
* the row the retired seedpack wrote under the same slug.
|
|
996
|
+
*/
|
|
997
|
+
function systemOverlay(name: string): PluginFixture {
|
|
998
|
+
return withFile(
|
|
999
|
+
thermosLike(name),
|
|
1000
|
+
"ai.stigmer/agent.yaml",
|
|
1001
|
+
[
|
|
1002
|
+
"apiVersion: agentic.stigmer.ai/v1",
|
|
1003
|
+
"kind: Agent",
|
|
1004
|
+
"metadata:",
|
|
1005
|
+
` name: ${name}`,
|
|
1006
|
+
" labels:",
|
|
1007
|
+
` ${SYSTEM_LABEL}: "true"`,
|
|
1008
|
+
"spec:",
|
|
1009
|
+
" instructions: You are the platform's own agent, re-homed under a plugin.",
|
|
1010
|
+
"",
|
|
1011
|
+
].join("\n"),
|
|
1012
|
+
);
|
|
1013
|
+
}
|