@stigmer/server 3.15.3-dev.20260916211208 → 3.16.1-dev.20260917224426
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/lane-admission.d.ts +50 -0
- package/dist/authorization/lane-admission.d.ts.map +1 -0
- package/dist/authorization/lane-admission.js +13 -0
- package/dist/authorization/lane-admission.js.map +1 -0
- package/dist/authorization/schedule-fire-caller.d.ts +2 -2
- package/dist/authorization/schedule-fire-caller.d.ts.map +1 -1
- package/dist/authorization/schedule-fire-caller.js +8 -8
- package/dist/authorization/schedule-fire-caller.js.map +1 -1
- package/dist/boot/compose.d.ts +12 -0
- package/dist/boot/compose.d.ts.map +1 -1
- package/dist/boot/compose.js +68 -14
- package/dist/boot/compose.js.map +1 -1
- package/dist/boot/inprocess.d.ts.map +1 -1
- package/dist/boot/inprocess.js +6 -1
- package/dist/boot/inprocess.js.map +1 -1
- package/dist/domain/executioncontext/resolve-values-for-caller.d.ts +33 -14
- package/dist/domain/executioncontext/resolve-values-for-caller.d.ts.map +1 -1
- package/dist/domain/executioncontext/resolve-values-for-caller.js +32 -8
- package/dist/domain/executioncontext/resolve-values-for-caller.js.map +1 -1
- package/dist/domain/identityaccount/actor.d.ts +33 -18
- package/dist/domain/identityaccount/actor.d.ts.map +1 -1
- package/dist/domain/identityaccount/actor.js +20 -3
- package/dist/domain/identityaccount/actor.js.map +1 -1
- package/dist/domain/identityaccount/resolve.d.ts +19 -0
- package/dist/domain/identityaccount/resolve.d.ts.map +1 -1
- package/dist/domain/identityaccount/resolve.js +12 -0
- package/dist/domain/identityaccount/resolve.js.map +1 -1
- package/dist/domain/mcpserver/connect-execution-id.d.ts +5 -0
- package/dist/domain/mcpserver/connect-execution-id.d.ts.map +1 -0
- package/dist/domain/mcpserver/connect-execution-id.js +32 -0
- package/dist/domain/mcpserver/connect-execution-id.js.map +1 -0
- package/dist/domain/mcpserver/connect.d.ts +28 -2
- package/dist/domain/mcpserver/connect.d.ts.map +1 -1
- package/dist/domain/mcpserver/connect.js +27 -7
- package/dist/domain/mcpserver/connect.js.map +1 -1
- package/dist/domain/mcpserver/engine.d.ts +9 -3
- package/dist/domain/mcpserver/engine.d.ts.map +1 -1
- package/dist/domain/mcpserver/engine.js.map +1 -1
- package/dist/domain/mcpserver/start-connect.js +1 -1
- package/dist/domain/mcpserver/start-connect.js.map +1 -1
- package/dist/domain/platform/controller.d.ts +31 -11
- 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/materialize/agent.d.ts +10 -1
- package/dist/domain/plugin/materialize/agent.d.ts.map +1 -1
- package/dist/domain/plugin/materialize/agent.js +35 -1
- package/dist/domain/plugin/materialize/agent.js.map +1 -1
- package/dist/domain/plugin/materialize/plan.d.ts.map +1 -1
- package/dist/domain/plugin/materialize/plan.js +1 -1
- package/dist/domain/plugin/materialize/plan.js.map +1 -1
- package/dist/domain/plugin/overlay/sanitize.d.ts.map +1 -1
- package/dist/domain/plugin/overlay/sanitize.js +4 -2
- package/dist/domain/plugin/overlay/sanitize.js.map +1 -1
- package/dist/domain/project/reconcile.d.ts.map +1 -1
- package/dist/domain/project/reconcile.js +14 -10
- package/dist/domain/project/reconcile.js.map +1 -1
- package/dist/domain/workflowexecution/phases.d.ts +21 -0
- package/dist/domain/workflowexecution/phases.d.ts.map +1 -0
- package/dist/domain/workflowexecution/phases.js +31 -0
- package/dist/domain/workflowexecution/phases.js.map +1 -0
- package/dist/identity/oidc-verifier.js +8 -1
- package/dist/identity/oidc-verifier.js.map +1 -1
- package/dist/pipeline/apiresource-meta.d.ts +15 -0
- package/dist/pipeline/apiresource-meta.d.ts.map +1 -1
- package/dist/pipeline/apiresource-meta.js +34 -0
- package/dist/pipeline/apiresource-meta.js.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.d.ts.map +1 -1
- package/dist/pipeline/steps/guard-memory-capture.js +5 -3
- package/dist/pipeline/steps/guard-memory-capture.js.map +1 -1
- package/dist/runnerauth/bound-execution.d.ts +27 -0
- package/dist/runnerauth/bound-execution.d.ts.map +1 -0
- package/dist/runnerauth/bound-execution.js +137 -0
- package/dist/runnerauth/bound-execution.js.map +1 -0
- package/dist/runnerauth/built-in-runner-credential-provider.d.ts +28 -0
- package/dist/runnerauth/built-in-runner-credential-provider.d.ts.map +1 -0
- package/dist/runnerauth/built-in-runner-credential-provider.js +217 -0
- package/dist/runnerauth/built-in-runner-credential-provider.js.map +1 -0
- package/dist/runnerauth/constants.d.ts +63 -0
- package/dist/runnerauth/constants.d.ts.map +1 -0
- package/dist/runnerauth/constants.js +63 -0
- package/dist/runnerauth/constants.js.map +1 -0
- package/dist/runnerauth/dispatch-credential.d.ts +38 -0
- package/dist/runnerauth/dispatch-credential.d.ts.map +1 -0
- package/dist/runnerauth/dispatch-credential.js +17 -0
- package/dist/runnerauth/dispatch-credential.js.map +1 -0
- package/dist/runnerauth/runner-credential-provider.d.ts +49 -11
- package/dist/runnerauth/runner-credential-provider.d.ts.map +1 -1
- package/dist/runnerauth/runner-credential-provider.js +16 -4
- package/dist/runnerauth/runner-credential-provider.js.map +1 -1
- package/dist/runnerauth/runner-subject-verifier.d.ts +14 -0
- package/dist/runnerauth/runner-subject-verifier.d.ts.map +1 -0
- package/dist/runnerauth/runner-subject-verifier.js +115 -0
- package/dist/runnerauth/runner-subject-verifier.js.map +1 -0
- package/dist/runnerauth/runnerauth.d.ts +33 -4
- package/dist/runnerauth/runnerauth.d.ts.map +1 -1
- package/dist/runnerauth/runnerauth.js +131 -35
- package/dist/runnerauth/runnerauth.js.map +1 -1
- package/dist/temporal/agentexecution/engine-client.d.ts +13 -0
- package/dist/temporal/agentexecution/engine-client.d.ts.map +1 -1
- package/dist/temporal/agentexecution/engine-client.js +10 -2
- package/dist/temporal/agentexecution/engine-client.js.map +1 -1
- package/dist/temporal/agentexecution/workflow-input.d.ts +13 -0
- package/dist/temporal/agentexecution/workflow-input.d.ts.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.d.ts.map +1 -1
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js +35 -31
- package/dist/temporal/agentexecution/workflows/invoke-agent-execution.js.map +1 -1
- package/dist/temporal/workflowexecution/engine-client.d.ts +24 -0
- package/dist/temporal/workflowexecution/engine-client.d.ts.map +1 -1
- package/dist/temporal/workflowexecution/engine-client.js +26 -19
- package/dist/temporal/workflowexecution/engine-client.js.map +1 -1
- package/dist/temporal/workflowexecution/workflow-input.d.ts +14 -0
- package/dist/temporal/workflowexecution/workflow-input.d.ts.map +1 -1
- package/package.json +5 -5
- package/src/authorization/__tests__/lane-admission.test.ts +115 -0
- package/src/authorization/lane-admission.ts +73 -0
- package/src/authorization/schedule-fire-caller.ts +10 -14
- package/src/boot/compose.ts +93 -22
- package/src/boot/inprocess.ts +7 -1
- package/src/domain/executioncontext/__tests__/executioncontext.test.ts +104 -0
- package/src/domain/executioncontext/__tests__/resolve-values-capability.test.ts +33 -4
- package/src/domain/executioncontext/resolve-values-for-caller.ts +79 -22
- package/src/domain/identityaccount/__tests__/account-for-stamp.test.ts +111 -0
- package/src/domain/identityaccount/actor.ts +61 -21
- package/src/domain/identityaccount/resolve.ts +31 -0
- package/src/domain/mcpserver/__tests__/connect.test.ts +17 -2
- package/src/domain/mcpserver/connect-execution-id.ts +34 -0
- package/src/domain/mcpserver/connect.ts +38 -3
- package/src/domain/mcpserver/engine.ts +9 -3
- package/src/domain/mcpserver/start-connect.ts +1 -1
- package/src/domain/platform/__tests__/platform.test.ts +47 -1
- package/src/domain/platform/controller.ts +33 -14
- package/src/domain/plugin/__tests__/materialize.test.ts +60 -1
- package/src/domain/plugin/__tests__/plugin.test.ts +122 -1
- package/src/domain/plugin/__tests__/sanitize.test.ts +2 -2
- package/src/domain/plugin/materialize/agent.ts +38 -0
- package/src/domain/plugin/materialize/plan.ts +7 -1
- package/src/domain/plugin/overlay/sanitize.ts +4 -2
- package/src/domain/project/__tests__/reconcile.test.ts +51 -5
- package/src/domain/project/reconcile.ts +20 -15
- package/src/domain/workflowexecution/__tests__/phases.test.ts +58 -0
- package/src/domain/workflowexecution/phases.ts +33 -0
- package/src/extensions/__tests__/extension-composition.test.ts +7 -1
- package/src/identity/__tests__/oidc-verifier.test.ts +22 -0
- package/src/identity/oidc-verifier.ts +8 -1
- package/src/pipeline/__tests__/kind-by-id-prefix.test.ts +75 -0
- package/src/pipeline/apiresource-meta.ts +41 -0
- package/src/pipeline/steps/__tests__/guard-memory-capture.test.ts +55 -35
- package/src/pipeline/steps/guard-memory-capture.ts +5 -3
- package/src/runnerauth/__tests__/bound-execution.test.ts +334 -0
- package/src/runnerauth/__tests__/built-in-runner-credential-provider.test.ts +516 -0
- package/src/runnerauth/__tests__/dispatch-credential.test.ts +94 -0
- package/src/runnerauth/__tests__/runner-credential-provider.test.ts +27 -0
- package/src/runnerauth/__tests__/runner-subject-composed.test.ts +497 -0
- package/src/runnerauth/__tests__/runner-subject-verifier.test.ts +541 -0
- package/src/runnerauth/bound-execution.ts +270 -0
- package/src/runnerauth/built-in-runner-credential-provider.ts +300 -0
- package/src/runnerauth/constants.ts +74 -0
- package/src/runnerauth/dispatch-credential.ts +63 -0
- package/src/runnerauth/runner-credential-provider.ts +60 -11
- package/src/runnerauth/runner-subject-verifier.ts +152 -0
- package/src/runnerauth/runnerauth.ts +148 -38
- package/src/temporal/agentexecution/__tests__/engine-client.test.ts +107 -38
- package/src/temporal/agentexecution/__tests__/invoke-workflow.test.ts +105 -2
- package/src/temporal/agentexecution/engine-client.ts +27 -2
- package/src/temporal/agentexecution/workflow-input.ts +13 -0
- package/src/temporal/agentexecution/workflows/invoke-agent-execution.ts +90 -44
- package/src/temporal/workflowexecution/__tests__/engine-client.test.ts +139 -33
- package/src/temporal/workflowexecution/engine-client.ts +48 -21
- package/src/temporal/workflowexecution/workflow-input.ts +14 -0
|
@@ -118,7 +118,7 @@ export async function startConnect(
|
|
|
118
118
|
}
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
const prepared = await prepareConnect(deps, mcpServer, input);
|
|
121
|
+
const prepared = await prepareConnect(deps, mcpServer, input, identity);
|
|
122
122
|
|
|
123
123
|
// Taken before the start so the advisory describes the queue the run is
|
|
124
124
|
// about to join. Warn-only by design: a worker may be booting (the
|
|
@@ -14,7 +14,10 @@
|
|
|
14
14
|
* executioncontext decrypt lane uses, bound to exactly the named
|
|
15
15
|
* execution id;
|
|
16
16
|
* - getRunnerBootstrapConfig echoes the configured Temporal coordinates
|
|
17
|
-
* with the token fields empty (minting a proxy token is cloud-only)
|
|
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
|
*/
|
|
19
22
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
20
23
|
import { tmpdir } from "node:os";
|
|
@@ -178,6 +181,48 @@ describe("platform domain (composed server)", () => {
|
|
|
178
181
|
});
|
|
179
182
|
});
|
|
180
183
|
|
|
184
|
+
describe("platform domain (composed with a stated version)", () => {
|
|
185
|
+
let server: ComposedServer;
|
|
186
|
+
let client: PlatformClient;
|
|
187
|
+
let dir: string;
|
|
188
|
+
|
|
189
|
+
beforeAll(async () => {
|
|
190
|
+
dir = mkdtempSync(path.join(tmpdir(), "platform-domain-version-test-"));
|
|
191
|
+
vi.stubEnv("STIGMER_ENCRYPTION_KEY", ENC_KEY.toString("base64"));
|
|
192
|
+
server = await composeServer({
|
|
193
|
+
config: loadConfig({
|
|
194
|
+
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
195
|
+
DB_PATH: path.join(dir, "stigmer.db"),
|
|
196
|
+
ARTIFACT_LOCAL_BASE_PATH: path.join(dir, "artifacts"),
|
|
197
|
+
}),
|
|
198
|
+
logger: silentLogger,
|
|
199
|
+
version: "3.99.0",
|
|
200
|
+
portOverride: 0,
|
|
201
|
+
host: "127.0.0.1",
|
|
202
|
+
});
|
|
203
|
+
const port = await server.start();
|
|
204
|
+
client = createClient(
|
|
205
|
+
PlatformQueryController,
|
|
206
|
+
createGrpcTransport({ baseUrl: `http://127.0.0.1:${port}` }),
|
|
207
|
+
);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
afterAll(async () => {
|
|
211
|
+
await server.shutdown();
|
|
212
|
+
rmSync(dir, { recursive: true, force: true });
|
|
213
|
+
vi.unstubAllEnvs();
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it("getServerInfo reports the composition's stated release, not the build stamp", async () => {
|
|
217
|
+
// A library consumer (the cloud composition) runs no bundle, so the
|
|
218
|
+
// define is never stamped for it; ComposeOptions.version is how it
|
|
219
|
+
// states the release it installed. Edition is untouched by it.
|
|
220
|
+
const info = await client.getServerInfo({});
|
|
221
|
+
expect(info.version).toBe("3.99.0");
|
|
222
|
+
expect(info.edition).toBe(ServerEdition.oss);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
|
|
181
226
|
describe("platform domain (keyless runner-token service)", () => {
|
|
182
227
|
it("a keyless service answers not-minted — fail-soft, never an error", async () => {
|
|
183
228
|
const transport = createRouterTransport((router) => {
|
|
@@ -188,6 +233,7 @@ describe("platform domain (keyless runner-token service)", () => {
|
|
|
188
233
|
RunnerAuthService.create(undefined),
|
|
189
234
|
),
|
|
190
235
|
edition: ServerEdition.oss,
|
|
236
|
+
version: "dev",
|
|
191
237
|
logger: silentLogger,
|
|
192
238
|
});
|
|
193
239
|
});
|
|
@@ -10,21 +10,34 @@
|
|
|
10
10
|
* composed-server pattern).
|
|
11
11
|
*
|
|
12
12
|
* getRunnerScopedToken is the mint side of the runner-token lane
|
|
13
|
-
* (oss#535) — the seam src/runnerauth
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
13
|
+
* (oss#535) — the seam src/runnerauth/. What the token IS depends on the
|
|
14
|
+
* server's posture, and so does who may obtain one:
|
|
15
|
+
*
|
|
16
|
+
* - Under trusted-local (the laptop: one caller, nothing to separate)
|
|
17
|
+
* the arms below mint a clocked token for any caller naming an
|
|
18
|
+
* execution. The token is the LANE DISCRIMINATOR that lets the
|
|
19
|
+
* ExecutionContext read RPCs redact by default without breaking the
|
|
20
|
+
* runner, not a trust boundary — there is no identity for it to
|
|
21
|
+
* change.
|
|
22
|
+
* - Under the built-in authorization posture the same token is an
|
|
23
|
+
* IDENTITY: the runner-subject verifier admits its bearer as the
|
|
24
|
+
* human whose run it is (runnerauth/runner-subject-verifier.ts). A
|
|
25
|
+
* mint for anyone would be impersonation, so the built-in provider
|
|
26
|
+
* defines exchangeScopedToken and this controller delegates to it:
|
|
27
|
+
* the run credential is minted for the run's own person and nobody
|
|
28
|
+
* else (runnerauth/built-in-runner-credential-provider.ts). Runs get
|
|
29
|
+
* their credential from the dispatch itself (the engine clients put
|
|
30
|
+
* it on the workflow input); this exchange is the runner's fallback.
|
|
19
31
|
*
|
|
20
32
|
* The C4 capability delegation (20260827.09, gate ruling Q1): when the
|
|
21
33
|
* composed provider defines exchangeScopedToken / bootstrapCredentials,
|
|
22
34
|
* this controller delegates the exchange arms and the bootstrap
|
|
23
|
-
* credential fields to it wholesale —
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
* suites pin that
|
|
35
|
+
* credential fields to it wholesale — per-arm caller gating, row
|
|
36
|
+
* authorization, and multi-lane minting are POLICY, not contract, and
|
|
37
|
+
* live behind the seam (the cloud's four-arm exchange; open source's
|
|
38
|
+
* built-in gate). With neither defined (the trusted-local default), every
|
|
39
|
+
* arm below behaves exactly as before; the conformance suites pin that
|
|
40
|
+
* byte-identity.
|
|
28
41
|
*/
|
|
29
42
|
import type { ConnectRouter, HandlerContext } from "@connectrpc/connect";
|
|
30
43
|
import { create } from "@bufbuild/protobuf";
|
|
@@ -50,7 +63,6 @@ import type {
|
|
|
50
63
|
RunnerScopedTokenRequest,
|
|
51
64
|
} from "../../runnerauth/runner-credential-provider.js";
|
|
52
65
|
import { TOKEN_TYPE_EXECUTION_SCOPED } from "../../runnerauth/runnerauth.js";
|
|
53
|
-
import { SERVER_VERSION } from "./version.js";
|
|
54
66
|
|
|
55
67
|
export interface PlatformControllerDeps {
|
|
56
68
|
/**
|
|
@@ -75,6 +87,13 @@ export interface PlatformControllerDeps {
|
|
|
75
87
|
* cloud composition answers `cloud` without forking this controller.
|
|
76
88
|
*/
|
|
77
89
|
readonly edition: ServerEdition;
|
|
90
|
+
/**
|
|
91
|
+
* The release reported beside the edition. The composition root resolves
|
|
92
|
+
* it (ComposeOptions.version, else SERVER_VERSION), so this controller
|
|
93
|
+
* never reads the build stamp itself: a library consumer with no bundle
|
|
94
|
+
* states its installed release and answers exactly as a bundle would.
|
|
95
|
+
*/
|
|
96
|
+
readonly version: string;
|
|
78
97
|
readonly logger: Logger;
|
|
79
98
|
}
|
|
80
99
|
|
|
@@ -92,11 +111,11 @@ export function registerPlatformServices(
|
|
|
92
111
|
});
|
|
93
112
|
}
|
|
94
113
|
|
|
95
|
-
/** Go GetServerInfo: the server edition and
|
|
114
|
+
/** Go GetServerInfo: the server edition and release version. */
|
|
96
115
|
function getServerInfo(deps: PlatformControllerDeps): GetServerInfoOutput {
|
|
97
116
|
return create(GetServerInfoOutputSchema, {
|
|
98
117
|
edition: deps.edition,
|
|
99
|
-
version:
|
|
118
|
+
version: deps.version,
|
|
100
119
|
});
|
|
101
120
|
}
|
|
102
121
|
|
|
@@ -7,7 +7,9 @@
|
|
|
7
7
|
* agent, the `mcpServers` key-to-slug rule, a skill archive rooted at its
|
|
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
|
-
* transport refused,
|
|
10
|
+
* transport refused, the composed agent declaring its tools' variables
|
|
11
|
+
* (OAuth targets excluded, an authored agent left as written), and the
|
|
12
|
+
* convergence and drop rules over members.
|
|
11
13
|
*/
|
|
12
14
|
import { describe, expect, it } from "vitest";
|
|
13
15
|
|
|
@@ -19,8 +21,11 @@ import type { PluginPackage } from "@stigmer/plugin-package";
|
|
|
19
21
|
import { cursorPlugin, openPlugin } from "@stigmer/plugin-package/testing";
|
|
20
22
|
import type { PluginFixture } from "@stigmer/plugin-package/testing";
|
|
21
23
|
import { create } from "@bufbuild/protobuf";
|
|
24
|
+
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
25
|
+
import { AgentSpecSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/spec_pb";
|
|
22
26
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
23
27
|
import {
|
|
28
|
+
McpServerAuthSchema,
|
|
24
29
|
McpServerSpecSchema,
|
|
25
30
|
StdioServerConfigSchema,
|
|
26
31
|
} from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/spec_pb";
|
|
@@ -224,6 +229,60 @@ describe("planMaterialization", () => {
|
|
|
224
229
|
expect(builtPlan.skills[0]!.request.tag).toBe("");
|
|
225
230
|
});
|
|
226
231
|
|
|
232
|
+
it("declares the servers' variables on the composed agent so a session asks for them", () => {
|
|
233
|
+
expect(plan.agent!.resource.spec?.env).toEqual({
|
|
234
|
+
GITHUB_TOKEN: expect.objectContaining({ isSecret: true, optional: false }),
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("leaves an OAuth-managed target out of the agent's declarations", () => {
|
|
239
|
+
const overlay = create(McpServerSchema, {
|
|
240
|
+
spec: create(McpServerSpecSchema, {
|
|
241
|
+
auth: create(McpServerAuthSchema, { targetEnvVar: "GITHUB_TOKEN" }),
|
|
242
|
+
}),
|
|
243
|
+
});
|
|
244
|
+
const withOAuth = planMaterialization(
|
|
245
|
+
pkg,
|
|
246
|
+
inMemoryPluginFiles(thermos),
|
|
247
|
+
{
|
|
248
|
+
workflows: [],
|
|
249
|
+
mcpServers: [
|
|
250
|
+
{
|
|
251
|
+
path: "ai.stigmer/mcp-servers/my_github.yaml",
|
|
252
|
+
server: "my_github",
|
|
253
|
+
resource: overlay,
|
|
254
|
+
},
|
|
255
|
+
],
|
|
256
|
+
},
|
|
257
|
+
IDENTITY,
|
|
258
|
+
"",
|
|
259
|
+
);
|
|
260
|
+
// The server still declares it (the connect flow reads it there); the
|
|
261
|
+
// agent does not, because the value comes from a managed environment.
|
|
262
|
+
expect(withOAuth.mcpServers[0]!.resource.spec?.env).toHaveProperty("GITHUB_TOKEN");
|
|
263
|
+
expect(withOAuth.agent!.resource.spec?.env).toEqual({});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it("takes an authored agent as written, declarations included", () => {
|
|
267
|
+
const authored = create(AgentSchema, {
|
|
268
|
+
spec: create(AgentSpecSchema, {
|
|
269
|
+
instructions: "The author's own instructions for this agent.",
|
|
270
|
+
}),
|
|
271
|
+
});
|
|
272
|
+
const withOverlay = planMaterialization(
|
|
273
|
+
pkg,
|
|
274
|
+
inMemoryPluginFiles(thermos),
|
|
275
|
+
{
|
|
276
|
+
agent: { path: "ai.stigmer/agent.yaml", resource: authored },
|
|
277
|
+
workflows: [],
|
|
278
|
+
mcpServers: [],
|
|
279
|
+
},
|
|
280
|
+
IDENTITY,
|
|
281
|
+
"",
|
|
282
|
+
);
|
|
283
|
+
expect(withOverlay.agent!.resource.spec?.env).toEqual({});
|
|
284
|
+
});
|
|
285
|
+
|
|
227
286
|
it("refuses a server overlay that redefines the transport", () => {
|
|
228
287
|
const overlay = create(McpServerSchema, {
|
|
229
288
|
spec: create(McpServerSpecSchema, {
|
|
@@ -10,7 +10,9 @@
|
|
|
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
12
|
* unmanaged resource holds refuses the install; a bad overlay refuses
|
|
13
|
-
* before any write.
|
|
13
|
+
* before any write. A second composition, under an authorizer that denies
|
|
14
|
+
* `can_write_reserved_labels`, pins the reserved-label refusal the
|
|
15
|
+
* open-source posture allows by design.
|
|
14
16
|
*/
|
|
15
17
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
16
18
|
import net from "node:net";
|
|
@@ -46,11 +48,13 @@ import { SkillQueryController } from "@stigmer/protos/ai/stigmer/agentic/skill/v
|
|
|
46
48
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
47
49
|
import { ApiResourceMetadataSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/metadata_pb";
|
|
48
50
|
import { ApiResourceReferenceSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
51
|
+
import { IamPermission } from "@stigmer/protos/ai/stigmer/iam/v1/enum_pb";
|
|
49
52
|
|
|
50
53
|
import { writeArchive } from "../../../archive/write.js";
|
|
51
54
|
import { loadConfig } from "../../../boot/config.js";
|
|
52
55
|
import { composeServer } from "../../../boot/compose.js";
|
|
53
56
|
import type { ComposedServer } from "../../../boot/compose.js";
|
|
57
|
+
import type { Authorizer, AuthzCheck } from "../../../extensions/authorizer.js";
|
|
54
58
|
import { createLogger } from "../../../boot/logger.js";
|
|
55
59
|
import {
|
|
56
60
|
PLUGIN_LABEL,
|
|
@@ -292,6 +296,12 @@ describe("Plugin push — materialisation", () => {
|
|
|
292
296
|
isSecret: true,
|
|
293
297
|
optional: false,
|
|
294
298
|
});
|
|
299
|
+
// The agent declares the same variable, which is what makes a session
|
|
300
|
+
// ask for it and the execution's least-privilege filter pass it.
|
|
301
|
+
expect(agent.spec?.env["GITHUB_TOKEN"]).toMatchObject({
|
|
302
|
+
isSecret: true,
|
|
303
|
+
optional: false,
|
|
304
|
+
});
|
|
295
305
|
});
|
|
296
306
|
|
|
297
307
|
it("installs an MCP-only plugin as its servers and no agent", async () => {
|
|
@@ -679,3 +689,114 @@ describe("Plugin push — refusals before any write", () => {
|
|
|
679
689
|
expect(agent.metadata?.labels[PLUGIN_LABEL]).toBe(installed.metadata?.id);
|
|
680
690
|
});
|
|
681
691
|
});
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* The reserved-label refusal where an authorizer enforces. The open-source
|
|
695
|
+
* built-in authorizer allows `can_write_reserved_labels` by design, so the
|
|
696
|
+
* default composition above cannot show the refusal; this composition
|
|
697
|
+
* registers an authorizer that denies exactly that permission and allows
|
|
698
|
+
* everything else, the posture a hosted edition takes. An archive whose
|
|
699
|
+
* overlay carries a `stigmer.ai/*` label is refused before a byte is
|
|
700
|
+
* written, and a plain overlay under the same authorizer still installs —
|
|
701
|
+
* so the sanitiser asks the platform capability and nothing broader.
|
|
702
|
+
*/
|
|
703
|
+
describe("Plugin push under an authorizer that enforces reserved labels", () => {
|
|
704
|
+
let enforcingDir: string;
|
|
705
|
+
let enforcing: ComposedServer;
|
|
706
|
+
let enforcingPlugins: Client<typeof PluginCommandController>;
|
|
707
|
+
let enforcingQuery: Client<typeof PluginQueryController>;
|
|
708
|
+
const observed: AuthzCheck[] = [];
|
|
709
|
+
|
|
710
|
+
beforeAll(async () => {
|
|
711
|
+
enforcingDir = mkdtempSync(path.join(tmpdir(), "plugin-domain-enforcing-"));
|
|
712
|
+
const grpcPort = await reserveFreePort();
|
|
713
|
+
const authorizer: Authorizer = {
|
|
714
|
+
authorize(_caller, check) {
|
|
715
|
+
if (check.permission === IamPermission.can_write_reserved_labels) {
|
|
716
|
+
observed.push(check);
|
|
717
|
+
return Promise.resolve({
|
|
718
|
+
kind: "deny",
|
|
719
|
+
reason: "reserved labels are the platform's",
|
|
720
|
+
});
|
|
721
|
+
}
|
|
722
|
+
return Promise.resolve({ kind: "allow" });
|
|
723
|
+
},
|
|
724
|
+
};
|
|
725
|
+
enforcing = await composeServer({
|
|
726
|
+
config: loadConfig({
|
|
727
|
+
STIGMER_MODEL_REGISTRY_REFRESH: "off",
|
|
728
|
+
TEMPORAL_HOST_PORT: "127.0.0.1:1",
|
|
729
|
+
GRPC_PORT: String(grpcPort),
|
|
730
|
+
DB_PATH: path.join(enforcingDir, "stigmer.db"),
|
|
731
|
+
ARTIFACT_LOCAL_BASE_PATH: path.join(enforcingDir, "artifacts"),
|
|
732
|
+
STORAGE_PATH: path.join(enforcingDir, "storage"),
|
|
733
|
+
}),
|
|
734
|
+
logger: silentLogger,
|
|
735
|
+
host: "127.0.0.1",
|
|
736
|
+
extensions: [{ name: "reserved-labels-enforced", authorizer }],
|
|
737
|
+
});
|
|
738
|
+
const port = await enforcing.start();
|
|
739
|
+
const transport: Transport = createGrpcTransport({
|
|
740
|
+
baseUrl: `http://127.0.0.1:${port}`,
|
|
741
|
+
});
|
|
742
|
+
enforcingPlugins = createClient(PluginCommandController, transport);
|
|
743
|
+
enforcingQuery = createClient(PluginQueryController, transport);
|
|
744
|
+
});
|
|
745
|
+
|
|
746
|
+
afterAll(async () => {
|
|
747
|
+
await enforcing.shutdown();
|
|
748
|
+
rmSync(enforcingDir, { recursive: true, force: true });
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
it("refuses an archive whose overlay carries a reserved label, naming the document and the key, and writes nothing", async () => {
|
|
752
|
+
const name = uniqueName("plg-forged");
|
|
753
|
+
const forged = withFile(
|
|
754
|
+
thermosLike(name),
|
|
755
|
+
"ai.stigmer/agent.yaml",
|
|
756
|
+
[
|
|
757
|
+
"apiVersion: agentic.stigmer.ai/v1",
|
|
758
|
+
"kind: Agent",
|
|
759
|
+
"metadata:",
|
|
760
|
+
` name: ${name}`,
|
|
761
|
+
" labels:",
|
|
762
|
+
" stigmer.ai/plugin: plg_somebody_else",
|
|
763
|
+
"spec:",
|
|
764
|
+
" instructions: Long enough instructions for the forged agent.",
|
|
765
|
+
"",
|
|
766
|
+
].join("\n"),
|
|
767
|
+
);
|
|
768
|
+
const error = await expectCode(
|
|
769
|
+
enforcingPlugins.push({ org: ORG, artifact: archiveOf(forged) }),
|
|
770
|
+
Code.InvalidArgument,
|
|
771
|
+
);
|
|
772
|
+
expect(error.rawMessage).toContain(
|
|
773
|
+
"cannot be set by a plugin (ai.stigmer/agent.yaml: stigmer.ai/plugin)",
|
|
774
|
+
);
|
|
775
|
+
expect(observed.map((c) => c.permission)).toEqual([
|
|
776
|
+
IamPermission.can_write_reserved_labels,
|
|
777
|
+
]);
|
|
778
|
+
await expectCode(
|
|
779
|
+
enforcingQuery.getByReference(
|
|
780
|
+
createMessage(ApiResourceReferenceSchema, {
|
|
781
|
+
org: ORG,
|
|
782
|
+
kind: ApiResourceKind.plugin,
|
|
783
|
+
slug: name,
|
|
784
|
+
}),
|
|
785
|
+
),
|
|
786
|
+
Code.NotFound,
|
|
787
|
+
);
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
it("installs a plain overlay under the same authorizer: the platform stamps its own labels through the in-process origin", async () => {
|
|
791
|
+
const name = uniqueName("plg-honest");
|
|
792
|
+
const installed = await enforcingPlugins.push({
|
|
793
|
+
org: ORG,
|
|
794
|
+
artifact: archiveOf(thermosLike(name)),
|
|
795
|
+
});
|
|
796
|
+
expect(installed.status?.state).toBe(PluginState.READY);
|
|
797
|
+
const { members } = await enforcingQuery.listMembers({
|
|
798
|
+
value: installed.metadata!.id,
|
|
799
|
+
});
|
|
800
|
+
expect(members).toHaveLength(3);
|
|
801
|
+
});
|
|
802
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Pins the sanitiser, the one place plugin-provided metadata is judged
|
|
3
3
|
* before it rides the in-process lane: a `stigmer.ai/*` label in an
|
|
4
|
-
* overlay is refused
|
|
4
|
+
* overlay is refused INVALID_ARGUMENT naming the document and the keys
|
|
5
5
|
* under a denying authorizer and passes under an allowing one (the
|
|
6
6
|
* open-source posture), the operator check is consulted lazily and only
|
|
7
7
|
* for the platform capability, a preset id and a name that describes
|
|
@@ -86,7 +86,7 @@ describe("sanitizeOverlays", () => {
|
|
|
86
86
|
USER,
|
|
87
87
|
),
|
|
88
88
|
);
|
|
89
|
-
expect(error.code).toBe(Code.
|
|
89
|
+
expect(error.code).toBe(Code.InvalidArgument);
|
|
90
90
|
expect(error.rawMessage).toContain(
|
|
91
91
|
"ai.stigmer/agent.yaml: stigmer.ai/default-agent",
|
|
92
92
|
);
|
|
@@ -14,6 +14,18 @@
|
|
|
14
14
|
* `mcp_server_usage`, every `agents/*.md` as a `sub_agent` with access to
|
|
15
15
|
* all the plugin's servers and the skills it asked for.
|
|
16
16
|
*
|
|
17
|
+
* The composed agent declares in `env` the union of its servers' variables,
|
|
18
|
+
* OAuth-managed target variables excluded. An execution filters its
|
|
19
|
+
* environment to the AGENT's declared keys, and every client's session
|
|
20
|
+
* start asks the user for the AGENT's declared keys, so an agent declaring
|
|
21
|
+
* nothing over a server declaring `${API_TOKEN}` is never asked for the
|
|
22
|
+
* token and fails at its first tool call. The declaration on the agent is
|
|
23
|
+
* what routes the user's values to the execution; the declaration on the
|
|
24
|
+
* server stays too (the connect flow and the runner's per-server filter
|
|
25
|
+
* read it there). An OAuth target is excluded because its value is injected
|
|
26
|
+
* from a managed environment, never asked of the user. An author's
|
|
27
|
+
* `agent.yaml` is taken as written: they chose what to declare.
|
|
28
|
+
*
|
|
17
29
|
* Model hints are recorded, never applied: a dialect's `model` becomes a
|
|
18
30
|
* status warning and the sub-agent runs on the session's model. The
|
|
19
31
|
* catalog seam exposes neither family nor cost tier, the runner drops a
|
|
@@ -25,6 +37,7 @@
|
|
|
25
37
|
import { create } from "@bufbuild/protobuf";
|
|
26
38
|
|
|
27
39
|
import type { PluginPackage, PluginSubAgent } from "@stigmer/plugin-package";
|
|
40
|
+
import type { EnvVarDeclaration } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/spec_pb";
|
|
28
41
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
29
42
|
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
30
43
|
import {
|
|
@@ -47,6 +60,7 @@ import { renderDefaultAgentInstructions } from "./default-agent-instructions.js"
|
|
|
47
60
|
import { memberMetadata } from "./identity.js";
|
|
48
61
|
import type { PluginIdentity } from "./identity.js";
|
|
49
62
|
import { mcpServerSlugOf } from "./mcp-servers.js";
|
|
63
|
+
import type { PlannedMcpServer } from "./mcp-servers.js";
|
|
50
64
|
import { skillSlugOf } from "./skills.js";
|
|
51
65
|
|
|
52
66
|
export interface PlannedAgent {
|
|
@@ -67,6 +81,7 @@ export function carriesAgent(plugin: PluginPackage): boolean {
|
|
|
67
81
|
export function planAgent(
|
|
68
82
|
plugin: PluginPackage,
|
|
69
83
|
overlayAgent: Agent | undefined,
|
|
84
|
+
mcpServers: readonly PlannedMcpServer[],
|
|
70
85
|
identity: PluginIdentity,
|
|
71
86
|
warnings: PluginWarning[],
|
|
72
87
|
): PlannedAgent | undefined {
|
|
@@ -95,6 +110,7 @@ export function planAgent(
|
|
|
95
110
|
const spec = create(AgentSpecSchema, {
|
|
96
111
|
description: plugin.description ?? "",
|
|
97
112
|
instructions: renderDefaultAgentInstructions(plugin),
|
|
113
|
+
env: toolVariables(mcpServers),
|
|
98
114
|
skillRefs: [...skillSlugByName.values()].map((slug) =>
|
|
99
115
|
create(ApiResourceReferenceSchema, {
|
|
100
116
|
org: identity.org,
|
|
@@ -127,6 +143,28 @@ export function planAgent(
|
|
|
127
143
|
};
|
|
128
144
|
}
|
|
129
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The variables the agent's tools need from the user: every server's
|
|
148
|
+
* declarations, minus each server's OAuth target. Two servers declaring
|
|
149
|
+
* one name share one declaration; the first server's wins, and the
|
|
150
|
+
* library already refuses a plugin whose variables disagree about a name.
|
|
151
|
+
*/
|
|
152
|
+
export function toolVariables(
|
|
153
|
+
mcpServers: readonly PlannedMcpServer[],
|
|
154
|
+
): Record<string, EnvVarDeclaration> {
|
|
155
|
+
const declarations: Record<string, EnvVarDeclaration> = {};
|
|
156
|
+
for (const server of mcpServers) {
|
|
157
|
+
const spec = server.resource.spec;
|
|
158
|
+
if (spec === undefined) continue;
|
|
159
|
+
const oauthTarget = spec.auth?.targetEnvVar ?? "";
|
|
160
|
+
for (const [name, declaration] of Object.entries(spec.env)) {
|
|
161
|
+
if (name === oauthTarget || name in declarations) continue;
|
|
162
|
+
declarations[name] = declaration;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return declarations;
|
|
166
|
+
}
|
|
167
|
+
|
|
130
168
|
function planSubAgent(
|
|
131
169
|
subAgent: PluginSubAgent,
|
|
132
170
|
identity: PluginIdentity,
|
|
@@ -96,7 +96,13 @@ export function planMaterialization(
|
|
|
96
96
|
),
|
|
97
97
|
identity,
|
|
98
98
|
);
|
|
99
|
-
const agent = planAgent(
|
|
99
|
+
const agent = planAgent(
|
|
100
|
+
plugin,
|
|
101
|
+
overlays.agent?.resource,
|
|
102
|
+
mcpServers,
|
|
103
|
+
identity,
|
|
104
|
+
warnings,
|
|
105
|
+
);
|
|
100
106
|
const workflows = planWorkflows(overlays.workflows, identity);
|
|
101
107
|
|
|
102
108
|
const members: PlannedMember[] = [
|
|
@@ -13,7 +13,9 @@
|
|
|
13
13
|
* `platform:stigmer`, the same check GuardReservedLabels runs and the
|
|
14
14
|
* same lazy consultation of the one composed Authorizer (the
|
|
15
15
|
* open-source permissive default allows; an enforcing authorizer
|
|
16
|
-
* denies with
|
|
16
|
+
* denies with INVALID_ARGUMENT naming the keys, the code the
|
|
17
|
+
* reserved-label guard answers a client mutation with, so one refusal
|
|
18
|
+
* class carries one code wherever it is met);
|
|
17
19
|
* - `metadata.id` is the server's to mint, never an author's;
|
|
18
20
|
* - the agent overlay's name is the plugin's, a server overlay's name is
|
|
19
21
|
* its `mcpServers` key, a workflow overlay's name is its file stem: an
|
|
@@ -130,7 +132,7 @@ export async function sanitizeOverlays(
|
|
|
130
132
|
throw new ConnectError(
|
|
131
133
|
`Labels in the reserved '${RESERVED_LABEL_PREFIX}' namespace are platform-managed and ` +
|
|
132
134
|
`cannot be set by a plugin (${listed}). Remove them from the plugin's ai.stigmer/ documents.`,
|
|
133
|
-
Code.
|
|
135
|
+
Code.InvalidArgument,
|
|
134
136
|
);
|
|
135
137
|
}
|
|
136
138
|
case "unavailable":
|
|
@@ -237,10 +237,12 @@ describe("orphan deletion (Go service_test.go)", () => {
|
|
|
237
237
|
]);
|
|
238
238
|
});
|
|
239
239
|
|
|
240
|
-
it("orphan lookup is
|
|
241
|
-
// Seeded under org "local"; the orphan
|
|
242
|
-
//
|
|
243
|
-
//
|
|
240
|
+
it("orphan lookup is scoped to the reference's org: another org's same-slug resource is never touched", async () => {
|
|
241
|
+
// Slugs are org-scoped identifiers. Seeded under org "local"; the orphan
|
|
242
|
+
// reference names a DIFFERENT org, so it must not resolve to this row.
|
|
243
|
+
// (The Go port matched by slug alone; on a backend with a system org
|
|
244
|
+
// beside the user's, a prune could then delete the wrong org's
|
|
245
|
+
// resource — stigmer/stigmer#1151.)
|
|
244
246
|
await seedAgent("agent-id-x", "cross-org-agent");
|
|
245
247
|
const deleter = new RecordingDeleter();
|
|
246
248
|
const previous = [
|
|
@@ -251,9 +253,53 @@ describe("orphan deletion (Go service_test.go)", () => {
|
|
|
251
253
|
}),
|
|
252
254
|
];
|
|
253
255
|
const result = await service(deleter).reconcile(previous, [], DEFAULT_RECONCILIATION_OPTIONS);
|
|
256
|
+
expect(result.removed).toHaveLength(0);
|
|
257
|
+
expect(result.errors).toHaveLength(1);
|
|
258
|
+
expect(result.errors[0]?.resourceKey).toBe("agent:cross-org-agent");
|
|
259
|
+
expect(result.errors[0]?.message).toBe("failed to resolve resource for deletion");
|
|
260
|
+
expect(deleter.calls).toHaveLength(0);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("orphan lookup picks the reference's org when two orgs hold the slug", async () => {
|
|
264
|
+
await seedAgent("agent-id-local", "shared-slug");
|
|
265
|
+
await store.saveResource(
|
|
266
|
+
ApiResourceKind.agent,
|
|
267
|
+
"agent-id-other",
|
|
268
|
+
AgentSchema,
|
|
269
|
+
create(AgentSchema, {
|
|
270
|
+
apiVersion: "agentic.stigmer.ai/v1",
|
|
271
|
+
kind: "Agent",
|
|
272
|
+
metadata: { id: "agent-id-other", name: "shared-slug", slug: "shared-slug", org: "another-org" },
|
|
273
|
+
}),
|
|
274
|
+
);
|
|
275
|
+
const deleter = new RecordingDeleter();
|
|
276
|
+
const previous = [
|
|
277
|
+
create(ApiResourceReferenceSchema, {
|
|
278
|
+
org: "another-org",
|
|
279
|
+
kind: ApiResourceKind.agent,
|
|
280
|
+
slug: "shared-slug",
|
|
281
|
+
}),
|
|
282
|
+
];
|
|
283
|
+
const result = await service(deleter).reconcile(previous, [], DEFAULT_RECONCILIATION_OPTIONS);
|
|
284
|
+
expect(result.removed).toHaveLength(1);
|
|
285
|
+
expect(deleter.calls).toEqual([
|
|
286
|
+
{ kind: ApiResourceKind.agent, resourceId: "agent-id-other" },
|
|
287
|
+
]);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
it("a reference without an org keeps the unscoped lookup (a pre-scoping client's project)", async () => {
|
|
291
|
+
await seedAgent("agent-id-y", "legacy-agent");
|
|
292
|
+
const deleter = new RecordingDeleter();
|
|
293
|
+
const previous = [
|
|
294
|
+
create(ApiResourceReferenceSchema, {
|
|
295
|
+
kind: ApiResourceKind.agent,
|
|
296
|
+
slug: "legacy-agent",
|
|
297
|
+
}),
|
|
298
|
+
];
|
|
299
|
+
const result = await service(deleter).reconcile(previous, [], DEFAULT_RECONCILIATION_OPTIONS);
|
|
254
300
|
expect(result.removed).toHaveLength(1);
|
|
255
301
|
expect(deleter.calls).toEqual([
|
|
256
|
-
{ kind: ApiResourceKind.agent, resourceId: "agent-id-
|
|
302
|
+
{ kind: ApiResourceKind.agent, resourceId: "agent-id-y" },
|
|
257
303
|
]);
|
|
258
304
|
});
|
|
259
305
|
|
|
@@ -37,6 +37,7 @@ import { ReconciliationSummarySchema } from "@stigmer/protos/ai/stigmer/tenancy/
|
|
|
37
37
|
import type { ReconciliationSummary } from "@stigmer/protos/ai/stigmer/tenancy/project/v1/status_pb";
|
|
38
38
|
|
|
39
39
|
import type { Logger } from "../../boot/logger.js";
|
|
40
|
+
import { findResourceBySlug } from "../../pipeline/steps/helpers.js";
|
|
40
41
|
import { metadataOf } from "../../pipeline/steps/shapes.js";
|
|
41
42
|
import type { Store } from "../../store/interface.js";
|
|
42
43
|
|
|
@@ -205,28 +206,32 @@ async function deleteOrphans(
|
|
|
205
206
|
}
|
|
206
207
|
|
|
207
208
|
/**
|
|
208
|
-
* Resolves an orphan reference to its resource id
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
209
|
+
* Resolves an orphan reference to its resource id, within the org the
|
|
210
|
+
* reference names. Slugs are org-scoped identifiers: a local backend holds
|
|
211
|
+
* the `stigmer` system org beside the user's, a self-host holds many, and
|
|
212
|
+
* the same slug may exist in several. The Go port matched by slug alone and
|
|
213
|
+
* could prune another org's resource (stigmer/stigmer#1151); the CLI has
|
|
214
|
+
* always stamped `org` on every member reference, so scoping is a change
|
|
215
|
+
* of lookup, not of contract. A reference without an org (a project written
|
|
216
|
+
* before members carried one) keeps the unscoped match, the helper's
|
|
217
|
+
* documented empty-org arm.
|
|
212
218
|
*/
|
|
213
219
|
async function resolveResourceId(
|
|
214
220
|
store: Store,
|
|
215
221
|
ref: ApiResourceReference,
|
|
216
222
|
): Promise<string> {
|
|
217
223
|
const schema = schemaForKind(ref.kind);
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
224
|
+
const resource = await findResourceBySlug(
|
|
225
|
+
store,
|
|
226
|
+
ref.kind,
|
|
227
|
+
schema,
|
|
228
|
+
ref.slug,
|
|
229
|
+
ref.org,
|
|
230
|
+
);
|
|
231
|
+
if (resource === undefined) {
|
|
232
|
+
const where = ref.org === "" ? "" : ` in org '${ref.org}'`;
|
|
227
233
|
throw new Error(
|
|
228
|
-
`resource ${kindName(ref.kind)}/${ref.slug} not found
|
|
229
|
-
{ cause },
|
|
234
|
+
`resource ${kindName(ref.kind)}/${ref.slug} not found${where}`,
|
|
230
235
|
);
|
|
231
236
|
}
|
|
232
237
|
|