@stigmer/cli 3.11.1-dev.20260812192248 → 3.12.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/commands/delete.js +2 -2
- package/commands/delete.js.map +1 -1
- package/package.json +5 -5
- package/registry/aliases.d.ts +3 -1
- package/registry/aliases.d.ts.map +1 -1
- package/registry/aliases.js +18 -6
- package/registry/aliases.js.map +1 -1
- package/registry/metadata.d.ts.map +1 -1
- package/registry/metadata.js +6 -6
- package/registry/metadata.js.map +1 -1
- package/registry/registry.d.ts.map +1 -1
- package/registry/registry.js +7 -2
- package/registry/registry.js.map +1 -1
- package/registry/verb-support.d.ts.map +1 -1
- package/registry/verb-support.js +0 -4
- package/registry/verb-support.js.map +1 -1
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +49 -2
- package/resources/apply/apply.js.map +1 -1
- package/resources/apply/declarative.d.ts +3 -3
- package/resources/apply/declarative.d.ts.map +1 -1
- package/resources/apply/declarative.js +28 -9
- package/resources/apply/declarative.js.map +1 -1
- package/resources/apply/handlers.d.ts +9 -0
- package/resources/apply/handlers.d.ts.map +1 -1
- package/resources/apply/handlers.js +1 -0
- package/resources/apply/handlers.js.map +1 -1
- package/resources/delete.d.ts.map +1 -1
- package/resources/delete.js +1 -9
- package/resources/delete.js.map +1 -1
- package/resources/get-bindings.d.ts.map +1 -1
- package/resources/get-bindings.js +0 -2
- package/resources/get-bindings.js.map +1 -1
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +0 -1
- package/resources/list.js.map +1 -1
- package/resources/validate.d.ts.map +1 -1
- package/resources/validate.js +0 -2
- package/resources/validate.js.map +1 -1
- package/src/commands/delete.ts +2 -2
- package/src/registry/aliases.test.ts +29 -5
- package/src/registry/aliases.ts +19 -6
- package/src/registry/metadata.ts +6 -6
- package/src/registry/registry.test.ts +48 -0
- package/src/registry/registry.ts +6 -2
- package/src/registry/verb-support.ts +0 -4
- package/src/resources/apply/apply.integration.test.ts +0 -67
- package/src/resources/apply/apply.test.ts +134 -0
- package/src/resources/apply/apply.ts +63 -2
- package/src/resources/apply/declarative.integration.test.ts +79 -1
- package/src/resources/apply/declarative.ts +27 -10
- package/src/resources/apply/handlers.ts +10 -0
- package/src/resources/delete.ts +3 -13
- package/src/resources/get-bindings.ts +0 -2
- package/src/resources/list.ts +0 -1
- package/src/resources/mutate-verbs.integration.test.ts +0 -62
- package/src/resources/validate.ts +0 -2
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// Unit tests for the shared apply core's declared-visibility follow-up
|
|
2
|
+
// (oss#573). Plain updates preserve stored visibility on both editions, so
|
|
3
|
+
// the apply RPC's response carries the STORED level; when a manifest
|
|
4
|
+
// declares a different one, the core must land it through the guarded
|
|
5
|
+
// updateVisibility RPC — or warn when the kind has no such door.
|
|
6
|
+
|
|
7
|
+
import { create, type Message } from "@bufbuild/protobuf";
|
|
8
|
+
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
9
|
+
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
10
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
11
|
+
import type { UpdateVisibilityInput } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { applyMessage } from "./apply.js";
|
|
14
|
+
import type { ApplyHandler, ControllerFn } from "./handlers.js";
|
|
15
|
+
|
|
16
|
+
// The controller accessor is only ever forwarded to handler methods, which
|
|
17
|
+
// these tests stub out — a throwing dummy proves nothing else touches it.
|
|
18
|
+
const controller: ControllerFn = () => {
|
|
19
|
+
throw new Error("unexpected controller access");
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
function agent(visibility: ApiResourceVisibility, id = "agent-1") {
|
|
23
|
+
return create(AgentSchema, {
|
|
24
|
+
metadata: { id, name: "a", org: "acme", visibility },
|
|
25
|
+
spec: { instructions: "i" },
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface HandlerOptions {
|
|
30
|
+
applyReturns: Message;
|
|
31
|
+
updateVisibility?: (input: UpdateVisibilityInput) => Promise<Message>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function handlerWith(opts: HandlerOptions): { handler: ApplyHandler; calls: UpdateVisibilityInput[] } {
|
|
35
|
+
const calls: UpdateVisibilityInput[] = [];
|
|
36
|
+
const handler: ApplyHandler = {
|
|
37
|
+
kind: ApiResourceKind.agent,
|
|
38
|
+
displayName: "Agent",
|
|
39
|
+
schema: AgentSchema,
|
|
40
|
+
applyOrder: 3,
|
|
41
|
+
apply: () => Promise.resolve(opts.applyReturns),
|
|
42
|
+
...(opts.updateVisibility !== undefined && {
|
|
43
|
+
updateVisibility: (_c: ControllerFn, input: UpdateVisibilityInput) => {
|
|
44
|
+
calls.push(input);
|
|
45
|
+
return opts.updateVisibility!(input);
|
|
46
|
+
},
|
|
47
|
+
}),
|
|
48
|
+
};
|
|
49
|
+
return { handler, calls };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe("applyMessage declared-visibility follow-up", () => {
|
|
53
|
+
it("lands a declared level the update preserved away, and reflects it on the outcome", async () => {
|
|
54
|
+
// Server preserved stored org; manifest declares public.
|
|
55
|
+
const { handler, calls } = handlerWith({
|
|
56
|
+
applyReturns: agent(ApiResourceVisibility.visibility_org),
|
|
57
|
+
updateVisibility: () => Promise.resolve(agent(ApiResourceVisibility.visibility_public)),
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const outcome = await applyMessage(controller, handler, agent(ApiResourceVisibility.visibility_public), "acme", false);
|
|
61
|
+
|
|
62
|
+
expect(calls).toHaveLength(1);
|
|
63
|
+
expect(calls[0].resourceId).toBe("agent-1");
|
|
64
|
+
expect(calls[0].visibility).toBe(ApiResourceVisibility.visibility_public);
|
|
65
|
+
expect(outcome.warning).toBeUndefined();
|
|
66
|
+
const appliedMeta = (outcome.applied as { metadata?: { visibility?: ApiResourceVisibility } })?.metadata;
|
|
67
|
+
expect(appliedMeta?.visibility).toBe(ApiResourceVisibility.visibility_public);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it("skips the follow-up when the manifest omits visibility", async () => {
|
|
71
|
+
const { handler, calls } = handlerWith({
|
|
72
|
+
applyReturns: agent(ApiResourceVisibility.visibility_org),
|
|
73
|
+
updateVisibility: () => Promise.reject(new Error("must not be called")),
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
const outcome = await applyMessage(
|
|
77
|
+
controller,
|
|
78
|
+
handler,
|
|
79
|
+
agent(ApiResourceVisibility.api_resource_visibility_unspecified),
|
|
80
|
+
"acme",
|
|
81
|
+
false,
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
expect(calls).toHaveLength(0);
|
|
85
|
+
expect(outcome.warning).toBeUndefined();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("skips the follow-up when the server already matches (idempotent re-apply)", async () => {
|
|
89
|
+
const { handler, calls } = handlerWith({
|
|
90
|
+
applyReturns: agent(ApiResourceVisibility.visibility_public),
|
|
91
|
+
updateVisibility: () => Promise.reject(new Error("must not be called")),
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const outcome = await applyMessage(controller, handler, agent(ApiResourceVisibility.visibility_public), "acme", false);
|
|
95
|
+
|
|
96
|
+
expect(calls).toHaveLength(0);
|
|
97
|
+
expect(outcome.warning).toBeUndefined();
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("warns instead of silently swallowing a diff on kinds without the RPC", async () => {
|
|
101
|
+
const { handler } = handlerWith({
|
|
102
|
+
applyReturns: agent(ApiResourceVisibility.visibility_private),
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const outcome = await applyMessage(controller, handler, agent(ApiResourceVisibility.visibility_public), "acme", false);
|
|
106
|
+
|
|
107
|
+
expect(outcome.warning).toMatch(/visibility cannot be changed declaratively/);
|
|
108
|
+
expect(outcome.warning).toMatch(/stored value is kept/);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it("fails loudly when the guarded door rejects, naming the partial state", async () => {
|
|
112
|
+
// e.g. the default-instance FAILED_PRECONDITION or an unsupported level.
|
|
113
|
+
const { handler } = handlerWith({
|
|
114
|
+
applyReturns: agent(ApiResourceVisibility.visibility_org),
|
|
115
|
+
updateVisibility: () => Promise.reject(new Error("default instances do not have their own visibility")),
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await expect(
|
|
119
|
+
applyMessage(controller, handler, agent(ApiResourceVisibility.visibility_public), "acme", false),
|
|
120
|
+
).rejects.toThrow(/spec applied, but the manifest's visibility change was rejected/);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it("does not follow up in dry-run mode", async () => {
|
|
124
|
+
const { handler, calls } = handlerWith({
|
|
125
|
+
applyReturns: agent(ApiResourceVisibility.visibility_org),
|
|
126
|
+
updateVisibility: () => Promise.reject(new Error("must not be called")),
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const outcome = await applyMessage(controller, handler, agent(ApiResourceVisibility.visibility_public), "acme", true);
|
|
130
|
+
|
|
131
|
+
expect(calls).toHaveLength(0);
|
|
132
|
+
expect(outcome.applied).toBeUndefined();
|
|
133
|
+
});
|
|
134
|
+
});
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
import { create, fromJson, type JsonValue, type Message } from "@bufbuild/protobuf";
|
|
10
10
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
11
11
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
12
|
+
import { ApiResourceVisibility } from "@stigmer/protos/ai/stigmer/commons/apiresource/enum_pb";
|
|
13
|
+
import { UpdateVisibilityInputSchema } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
12
14
|
import {
|
|
13
15
|
type ApiResourceMetadata,
|
|
14
16
|
ApiResourceMetadataSchema,
|
|
@@ -131,14 +133,16 @@ export async function applyMessage(
|
|
|
131
133
|
org: string,
|
|
132
134
|
dryRun: boolean,
|
|
133
135
|
): Promise<ApplyOutcome> {
|
|
134
|
-
const
|
|
136
|
+
const orgWarning = injectOrg(message, org);
|
|
135
137
|
const created = (metaOf(message)?.id ?? "") === "";
|
|
136
138
|
|
|
137
139
|
if (dryRun) {
|
|
138
|
-
return { result: buildDryRunResult(handler, message), warning };
|
|
140
|
+
return { result: buildDryRunResult(handler, message), warning: orgWarning };
|
|
139
141
|
}
|
|
140
142
|
|
|
141
143
|
const applied = await handler.apply(controller, message);
|
|
144
|
+
const visibilityWarning = await applyDeclaredVisibility(controller, handler, message, applied);
|
|
145
|
+
const warning = combineWarnings(orgWarning, visibilityWarning);
|
|
142
146
|
const result = buildApplyResult(handler, applied, created);
|
|
143
147
|
if (handler.kind === ApiResourceKind.mcp_server) {
|
|
144
148
|
return { result, appliedMcpServer: applied as McpServer, warning, applied };
|
|
@@ -146,6 +150,63 @@ export async function applyMessage(
|
|
|
146
150
|
return { result, warning, applied };
|
|
147
151
|
}
|
|
148
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Land a manifest-declared visibility through the guarded door. Plain
|
|
155
|
+
* updates preserve the stored visibility on both editions (oss#573) — the
|
|
156
|
+
* `updateVisibility` RPC is the only mutation path, so when the applied
|
|
157
|
+
* resource comes back with a different level than the manifest declared,
|
|
158
|
+
* follow up with one RPC (the skill-push precedent: no-ops are skipped, an
|
|
159
|
+
* unchanged manifest costs nothing extra). Server-side guard rejections
|
|
160
|
+
* (unsupported level, default-instance) propagate as command failures —
|
|
161
|
+
* the spec update has landed at that point, and the error says so.
|
|
162
|
+
*
|
|
163
|
+
* Returns a warning (instead of following up) for kinds without the RPC:
|
|
164
|
+
* their manifests can only carry a level the create path already rejected,
|
|
165
|
+
* so a diff here means a pre-existing resource and an unsupported ask.
|
|
166
|
+
*/
|
|
167
|
+
async function applyDeclaredVisibility(
|
|
168
|
+
controller: ControllerFn,
|
|
169
|
+
handler: ApplyHandler,
|
|
170
|
+
message: Message,
|
|
171
|
+
applied: Message,
|
|
172
|
+
): Promise<string | undefined> {
|
|
173
|
+
const declared = metaOf(message)?.visibility ?? ApiResourceVisibility.api_resource_visibility_unspecified;
|
|
174
|
+
if (declared === ApiResourceVisibility.api_resource_visibility_unspecified) return undefined;
|
|
175
|
+
|
|
176
|
+
const appliedMeta = metaOf(applied);
|
|
177
|
+
const resourceId = appliedMeta?.id ?? "";
|
|
178
|
+
if (resourceId === "" || appliedMeta?.visibility === declared) return undefined;
|
|
179
|
+
|
|
180
|
+
if (handler.updateVisibility === undefined) {
|
|
181
|
+
return (
|
|
182
|
+
`${handler.displayName} visibility cannot be changed declaratively — ` +
|
|
183
|
+
"the manifest's metadata.visibility was ignored (the stored value is kept)"
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const updated = await handler.updateVisibility(
|
|
189
|
+
controller,
|
|
190
|
+
create(UpdateVisibilityInputSchema, { resourceId, visibility: declared }),
|
|
191
|
+
);
|
|
192
|
+
// Reflect the landed level on the outcome the caller already holds.
|
|
193
|
+
const updatedMeta = metaOf(updated);
|
|
194
|
+
if (appliedMeta !== undefined && updatedMeta !== undefined) {
|
|
195
|
+
appliedMeta.visibility = updatedMeta.visibility;
|
|
196
|
+
}
|
|
197
|
+
return undefined;
|
|
198
|
+
} catch (err) {
|
|
199
|
+
throw new UsageError(
|
|
200
|
+
`${handler.displayName} spec applied, but the manifest's visibility change was rejected: ${(err as Error).message}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
function combineWarnings(...warnings: (string | undefined)[]): string | undefined {
|
|
206
|
+
const present = warnings.filter((w): w is string => w !== undefined);
|
|
207
|
+
return present.length > 0 ? present.join("; ") : undefined;
|
|
208
|
+
}
|
|
209
|
+
|
|
149
210
|
/** Read a resource message's metadata (id/name/slug/org), if present. */
|
|
150
211
|
export function resourceMetadata(message: Message): ApiResourceMetadata | undefined {
|
|
151
212
|
return metaOf(message);
|
|
@@ -16,6 +16,7 @@ import { create } from "@bufbuild/protobuf";
|
|
|
16
16
|
import { type ConnectRouter, createClient } from "@connectrpc/connect";
|
|
17
17
|
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
18
18
|
import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
|
|
19
|
+
import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
|
|
19
20
|
import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
20
21
|
import { SkillCommandController } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/command_pb";
|
|
21
22
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
@@ -34,8 +35,12 @@ const openSessions = new Set<ServerHttp2Session>();
|
|
|
34
35
|
|
|
35
36
|
let appliedProject: Project | undefined;
|
|
36
37
|
|
|
38
|
+
// Ordered log of backend calls, for the discovery-before-dependents contract.
|
|
39
|
+
let events: string[] = [];
|
|
40
|
+
|
|
37
41
|
beforeEach(() => {
|
|
38
42
|
appliedProject = undefined;
|
|
43
|
+
events = [];
|
|
39
44
|
});
|
|
40
45
|
|
|
41
46
|
function makeProject(dir: string): void {
|
|
@@ -53,7 +58,22 @@ function makeProject(dir: string): void {
|
|
|
53
58
|
|
|
54
59
|
beforeAll(async () => {
|
|
55
60
|
const routes = (router: ConnectRouter) => {
|
|
56
|
-
router.service(AgentCommandController, {
|
|
61
|
+
router.service(AgentCommandController, {
|
|
62
|
+
apply: (req) => {
|
|
63
|
+
events.push("agent.apply");
|
|
64
|
+
return req;
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
router.service(McpServerCommandController, {
|
|
68
|
+
apply: (req) => {
|
|
69
|
+
events.push("mcpserver.apply");
|
|
70
|
+
return req;
|
|
71
|
+
},
|
|
72
|
+
connect: () => {
|
|
73
|
+
events.push("mcpserver.connect");
|
|
74
|
+
return {};
|
|
75
|
+
},
|
|
76
|
+
});
|
|
57
77
|
router.service(SkillCommandController, {
|
|
58
78
|
push: (req) =>
|
|
59
79
|
create(SkillSchema, {
|
|
@@ -123,4 +143,62 @@ describe("applyDeclarative", () => {
|
|
|
123
143
|
rmSync(dir, { recursive: true, force: true });
|
|
124
144
|
}
|
|
125
145
|
});
|
|
146
|
+
|
|
147
|
+
it("discovers MCP capabilities before applying dependent resources", async () => {
|
|
148
|
+
// Pins the ordering contract behind apply-time enabled_tools validation
|
|
149
|
+
// (stigmer/stigmer#402): the server rejects agent enabled_tools against
|
|
150
|
+
// STORED capabilities, so discovery must refresh them between the MCP
|
|
151
|
+
// server applies and the applies that depend on them. Regressing to
|
|
152
|
+
// discovery-at-the-end resurrects the stale-capabilities race (a seedpack
|
|
153
|
+
// upgrade adding a tool + enabling it in an agent would fail bootstrap).
|
|
154
|
+
const dir = mkdtempSync(join(tmpdir(), "decl-it-order-"));
|
|
155
|
+
try {
|
|
156
|
+
writeFileSync(
|
|
157
|
+
join(dir, "stigmer.yaml"),
|
|
158
|
+
["kind: Project", "metadata:", " name: Ordered", " slug: ordered", "spec:", " description: d", ""].join("\n"),
|
|
159
|
+
);
|
|
160
|
+
// Named so a naive filename sort would apply the agent first — the
|
|
161
|
+
// reconciler must order by dependency, not by scan order.
|
|
162
|
+
writeFileSync(
|
|
163
|
+
join(dir, "a-agent.yaml"),
|
|
164
|
+
[
|
|
165
|
+
"kind: Agent",
|
|
166
|
+
"metadata:",
|
|
167
|
+
" name: Consumer",
|
|
168
|
+
" slug: consumer",
|
|
169
|
+
"spec:",
|
|
170
|
+
" description: c",
|
|
171
|
+
"",
|
|
172
|
+
].join("\n"),
|
|
173
|
+
);
|
|
174
|
+
writeFileSync(
|
|
175
|
+
join(dir, "z-server.yaml"),
|
|
176
|
+
[
|
|
177
|
+
"kind: McpServer",
|
|
178
|
+
"metadata:",
|
|
179
|
+
" name: Tools",
|
|
180
|
+
" slug: tools",
|
|
181
|
+
"spec:",
|
|
182
|
+
" description: t",
|
|
183
|
+
" stdio:",
|
|
184
|
+
" command: npx",
|
|
185
|
+
"",
|
|
186
|
+
].join("\n"),
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
const detect = detectTrack(dir);
|
|
190
|
+
const result = await applyDeclarative(detect, {
|
|
191
|
+
controller: controllerFn,
|
|
192
|
+
stigmer,
|
|
193
|
+
org: "acme",
|
|
194
|
+
info: () => {},
|
|
195
|
+
warn: () => {},
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
expect(result.status).toBe("success");
|
|
199
|
+
expect(events).toEqual(["mcpserver.apply", "mcpserver.connect", "agent.apply"]);
|
|
200
|
+
} finally {
|
|
201
|
+
rmSync(dir, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
126
204
|
});
|
|
@@ -276,9 +276,9 @@ const DEFAULT_IGNORE_OPTIONS = { respectGitignore: true, extraIgnore: [] as stri
|
|
|
276
276
|
|
|
277
277
|
/**
|
|
278
278
|
* Reconcile a project's membership: push skills first (agents may reference
|
|
279
|
-
* them), apply
|
|
280
|
-
* to the member-eligible ones,
|
|
281
|
-
* project with that exact member set so the server can reconcile by
|
|
279
|
+
* them), apply MCP servers and run capability discovery on them, apply the
|
|
280
|
+
* remaining resources, collect references to the member-eligible ones, then
|
|
281
|
+
* apply the project with that exact member set so the server can reconcile by
|
|
282
282
|
* set-difference (orphan pruning is server-side; see S1 in commands/apply.ts).
|
|
283
283
|
*/
|
|
284
284
|
export async function reconcileProjectMembers(
|
|
@@ -295,13 +295,14 @@ export async function reconcileProjectMembers(
|
|
|
295
295
|
members.push(reference(deps.org, ApiResourceKind.skill, pushed.slug));
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
//
|
|
299
|
-
//
|
|
300
|
-
//
|
|
301
|
-
//
|
|
302
|
-
//
|
|
298
|
+
// Each resource is applied through the shared apply core, collecting member
|
|
299
|
+
// references from the APPLIED result so membership carries the server's
|
|
300
|
+
// authoritative slug. (This is the unified policy: the declarative track
|
|
301
|
+
// used to read the pre-apply YAML slug, the Go synthesis track read the
|
|
302
|
+
// applied result — converging on the applied result is correct for both and
|
|
303
|
+
// matches what the backend actually stored.)
|
|
303
304
|
const appliedMcpServers: McpServer[] = [];
|
|
304
|
-
|
|
305
|
+
const applyOne = async (res: ReconcileResource): Promise<void> => {
|
|
305
306
|
const outcome = await applyMessage(deps.controller, res.handler, res.message, deps.org, false);
|
|
306
307
|
if (outcome.warning !== undefined) deps.warn(outcome.warning);
|
|
307
308
|
if (outcome.appliedMcpServer !== undefined) appliedMcpServers.push(outcome.appliedMcpServer);
|
|
@@ -309,9 +310,25 @@ export async function reconcileProjectMembers(
|
|
|
309
310
|
const slug = memberSlugOf(outcome.applied);
|
|
310
311
|
if (slug) members.push(reference(deps.org, res.handler.kind, slug));
|
|
311
312
|
}
|
|
312
|
-
}
|
|
313
|
+
};
|
|
313
314
|
|
|
315
|
+
// MCP servers first, then capability discovery, then everything else — the
|
|
316
|
+
// dependency principle applied to capabilities, not just existence. The
|
|
317
|
+
// server validates agent enabled_tools against discovered capabilities at
|
|
318
|
+
// apply time (stigmer/stigmer#402), and the backend's own post-apply
|
|
319
|
+
// connect is asynchronous; discovering here means agents in this apply
|
|
320
|
+
// validate against the toolset this apply just shipped, not the previous
|
|
321
|
+
// generation's (which would falsely reject e.g. a seedpack upgrade that
|
|
322
|
+
// adds a tool and enables it in the same pass). Discovery stays
|
|
323
|
+
// best-effort: a failed connect warns, capabilities stay stale, and the
|
|
324
|
+
// dependent apply surfaces an actionable error listing last-known tools.
|
|
325
|
+
for (const res of resources) {
|
|
326
|
+
if (res.handler.kind === ApiResourceKind.mcp_server) await applyOne(res);
|
|
327
|
+
}
|
|
314
328
|
await discoverAppliedMcpServers(deps.stigmer, appliedMcpServers, deps.org, deps.info);
|
|
329
|
+
for (const res of resources) {
|
|
330
|
+
if (res.handler.kind !== ApiResourceKind.mcp_server) await applyOne(res);
|
|
331
|
+
}
|
|
315
332
|
|
|
316
333
|
injectOrg(project, deps.org);
|
|
317
334
|
if (project.spec === undefined) project.spec = create(ProjectSpecSchema, {});
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
|
|
25
25
|
import type { DescMessage, DescService, Message } from "@bufbuild/protobuf";
|
|
26
26
|
import type { Client } from "@connectrpc/connect";
|
|
27
|
+
import type { UpdateVisibilityInput } from "@stigmer/protos/ai/stigmer/commons/apiresource/io_pb";
|
|
27
28
|
import { type Session, SessionSchema } from "@stigmer/protos/ai/stigmer/agentic/session/v1/api_pb";
|
|
28
29
|
import { SessionCommandController } from "@stigmer/protos/ai/stigmer/agentic/session/v1/command_pb";
|
|
29
30
|
import {
|
|
@@ -54,6 +55,14 @@ export interface ApplyHandler {
|
|
|
54
55
|
readonly applyOrder: number;
|
|
55
56
|
/** Drive the controller's `apply` RPC with the full resource message. */
|
|
56
57
|
apply(controller: ControllerFn, message: Message): Promise<Message>;
|
|
58
|
+
/**
|
|
59
|
+
* Drive the controller's `updateVisibility` RPC — the only door for
|
|
60
|
+
* visibility changes (plain updates preserve stored visibility, oss#573).
|
|
61
|
+
* Present exactly when the kind has the RPC; the apply core follows up
|
|
62
|
+
* through it when a manifest-declared level differs from the stored one.
|
|
63
|
+
* Manifest kinds inherit the binding from the SDK registry.
|
|
64
|
+
*/
|
|
65
|
+
updateVisibility?(controller: ControllerFn, input: UpdateVisibilityInput): Promise<Message>;
|
|
57
66
|
}
|
|
58
67
|
|
|
59
68
|
// The SDK registry's applyOrder values end at schedule = 12; the extras slot
|
|
@@ -72,6 +81,7 @@ const CLI_EXTRA_HANDLERS: readonly ApplyHandler[] = [
|
|
|
72
81
|
schema: WorkflowInstanceSchema,
|
|
73
82
|
applyOrder: 13,
|
|
74
83
|
apply: (c, m) => c(WorkflowInstanceCommandController).apply(m as WorkflowInstance),
|
|
84
|
+
updateVisibility: (c, i) => c(WorkflowInstanceCommandController).updateVisibility(i),
|
|
75
85
|
},
|
|
76
86
|
{
|
|
77
87
|
kind: ApiResourceKind.session,
|
package/src/resources/delete.ts
CHANGED
|
@@ -45,9 +45,8 @@ interface HasMetadata {
|
|
|
45
45
|
readonly metadata?: { readonly id?: string; readonly name?: string; readonly slug?: string; readonly org?: string };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
// `force` is the RPC-level acknowledgment of destructive side effects
|
|
49
|
-
//
|
|
50
|
-
// `-f/--force` flag. Kinds whose delete RPC takes a plain typed ID have no
|
|
48
|
+
// `force` is the RPC-level acknowledgment of destructive side effects —
|
|
49
|
+
// distinct from, but carried by, the CLI's `-f/--force` flag. Kinds whose delete RPC takes a plain typed ID have no
|
|
51
50
|
// force field and simply ignore the argument.
|
|
52
51
|
type DeleteFn = (client: Stigmer, id: string, force: boolean) => Promise<HasMetadata>;
|
|
53
52
|
|
|
@@ -56,8 +55,7 @@ type DeleteFn = (client: Stigmer, id: string, force: boolean) => Promise<HasMeta
|
|
|
56
55
|
// documented special case) — the conformance test in registry/registry.test.ts
|
|
57
56
|
// enforces it, so the two cannot drift.
|
|
58
57
|
// Kinds whose delete takes a DeleteResourceInput (not an ID) thread the force
|
|
59
|
-
// acknowledgment through
|
|
60
|
-
// non-empty guard), but force is the contract's generic ack carrier.
|
|
58
|
+
// acknowledgment through — force is the contract's generic ack carrier.
|
|
61
59
|
export const DELETE_HANDLERS: ReadonlyMap<ApiResourceKind, DeleteFn> = new Map<ApiResourceKind, DeleteFn>([
|
|
62
60
|
[ApiResourceKind.agent, (c, id) => c.agent.delete(id)],
|
|
63
61
|
[ApiResourceKind.agent_instance, (c, id) => c.agentInstance.delete(id)],
|
|
@@ -65,7 +63,6 @@ export const DELETE_HANDLERS: ReadonlyMap<ApiResourceKind, DeleteFn> = new Map<A
|
|
|
65
63
|
[ApiResourceKind.workflow_instance, (c, id) => c.workflowInstance.delete(id)],
|
|
66
64
|
[ApiResourceKind.mcp_server, (c, id, force) => c.mcpServer.delete({ resourceId: id, force })],
|
|
67
65
|
[ApiResourceKind.project, (c, id) => c.project.delete(id)],
|
|
68
|
-
[ApiResourceKind.datastore, (c, id, force) => c.datastore.delete({ resourceId: id, force })],
|
|
69
66
|
[ApiResourceKind.environment, (c, id, force) => c.environment.delete({ resourceId: id, force })],
|
|
70
67
|
[ApiResourceKind.agent_channel, (c, id) => c.agentChannel.delete(id)],
|
|
71
68
|
[ApiResourceKind.channel_app, (c, id, force) => c.channelapp.delete({ resourceId: id, force })],
|
|
@@ -230,13 +227,6 @@ function buildDeleteWarning(info: TypeInfo, message: HasMetadata): CommandResult
|
|
|
230
227
|
warning.hint("This will delete the skill and all its versions.");
|
|
231
228
|
}
|
|
232
229
|
|
|
233
|
-
if (info.kind === ApiResourceKind.datastore) {
|
|
234
|
-
// Mirrors the server's non-empty guard (GuardNonEmptyStep): the delete of
|
|
235
|
-
// a datastore holding records is refused unless force acknowledges it.
|
|
236
|
-
warning.hint("All records held by this datastore will be destroyed.");
|
|
237
|
-
warning.hint("A datastore holding records is refused unless --force acknowledges destroying them.");
|
|
238
|
-
}
|
|
239
|
-
|
|
240
230
|
if (info.kind === ApiResourceKind.agent_channel) {
|
|
241
231
|
// The server's documented distinction: delete is the connection's full
|
|
242
232
|
// teardown; disabling is the config-preserving pause.
|
|
@@ -8,7 +8,6 @@ import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb"
|
|
|
8
8
|
import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
|
|
9
9
|
import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
|
|
10
10
|
import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
|
|
11
|
-
import { DatastoreSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
|
|
12
11
|
import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
13
12
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
14
13
|
import { ScheduleSchema } from "@stigmer/protos/ai/stigmer/agentic/schedule/v1/api_pb";
|
|
@@ -39,7 +38,6 @@ export const GET_BINDINGS: ReadonlyMap<ApiResourceKind, Getter> = new Map([
|
|
|
39
38
|
[ApiResourceKind.workflow_instance, refGetter(ApiResourceKind.workflow_instance, WorkflowInstanceSchema, (c) => c.workflowInstance)],
|
|
40
39
|
[ApiResourceKind.mcp_server, refGetter(ApiResourceKind.mcp_server, McpServerSchema, (c) => c.mcpServer)],
|
|
41
40
|
[ApiResourceKind.project, refGetter(ApiResourceKind.project, ProjectSchema, (c) => c.project)],
|
|
42
|
-
[ApiResourceKind.datastore, refGetter(ApiResourceKind.datastore, DatastoreSchema, (c) => c.datastore)],
|
|
43
41
|
[ApiResourceKind.environment, refGetter(ApiResourceKind.environment, EnvironmentSchema, (c) => c.environment)],
|
|
44
42
|
[ApiResourceKind.agent_channel, refGetter(ApiResourceKind.agent_channel, AgentChannelSchema, (c) => c.agentChannel)],
|
|
45
43
|
[ApiResourceKind.channel_app, refGetter(ApiResourceKind.channel_app, ChannelAppSchema, (c) => c.channelapp)],
|
package/src/resources/list.ts
CHANGED
|
@@ -25,9 +25,6 @@ import { AgentInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic
|
|
|
25
25
|
import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
|
|
26
26
|
import { ChannelAppCommandController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/command_pb";
|
|
27
27
|
import { ChannelAppQueryController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/query_pb";
|
|
28
|
-
import { DatastoreSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
|
|
29
|
-
import { DatastoreCommandController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/command_pb";
|
|
30
|
-
import { DatastoreQueryController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/query_pb";
|
|
31
28
|
import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
32
29
|
import { EnvironmentCommandController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/command_pb";
|
|
33
30
|
import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/query_pb";
|
|
@@ -69,16 +66,6 @@ const knownWorkflow = create(WorkflowSchema, {
|
|
|
69
66
|
metadata: { name: "Deploy", slug: "deploy", org: "acme", id: "wfl_1" },
|
|
70
67
|
});
|
|
71
68
|
|
|
72
|
-
// The mock treats this datastore as NON-EMPTY: delete refuses without the
|
|
73
|
-
// force acknowledgment, mirroring the server's GuardNonEmptyStep contract.
|
|
74
|
-
const knownDatastore = create(DatastoreSchema, {
|
|
75
|
-
metadata: { name: "clinic-records", slug: "clinic-records", org: "acme", id: "dst_1" },
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
// Byte-shaped like the Go server's GuardNonEmptyStep refusal (delete_guards.go).
|
|
79
|
-
const nonEmptyGuardMessage =
|
|
80
|
-
'datastore "clinic-records" holds 12 records across 3 collections; deleting destroys them — pass force to acknowledge';
|
|
81
|
-
|
|
82
69
|
const knownEnvironment = create(EnvironmentSchema, {
|
|
83
70
|
metadata: { name: "clinic-patient-db", slug: "clinic-patient-db", org: "acme", id: "env_1" },
|
|
84
71
|
});
|
|
@@ -121,7 +108,6 @@ const openSessions = new Set<ServerHttp2Session>();
|
|
|
121
108
|
let cancelCalls: string[] = [];
|
|
122
109
|
let tagCalls: { workflowId: string; versionHash: string; tag: string }[] = [];
|
|
123
110
|
// Force-carrying deletes record what actually rode the RPC.
|
|
124
|
-
let datastoreDeletes: { resourceId: string; force: boolean }[] = [];
|
|
125
111
|
let environmentDeletes: { resourceId: string; force: boolean }[] = [];
|
|
126
112
|
let channelAppDeletes: { resourceId: string; force: boolean }[] = [];
|
|
127
113
|
// agent_channel, schedule, and workflow_instance delete by typed ID — no
|
|
@@ -133,7 +119,6 @@ let workflowInstanceDeleteIds: string[] = [];
|
|
|
133
119
|
beforeEach(() => {
|
|
134
120
|
cancelCalls = [];
|
|
135
121
|
tagCalls = [];
|
|
136
|
-
datastoreDeletes = [];
|
|
137
122
|
environmentDeletes = [];
|
|
138
123
|
channelAppDeletes = [];
|
|
139
124
|
channelDeleteIds = [];
|
|
@@ -181,21 +166,6 @@ beforeAll(async () => {
|
|
|
181
166
|
},
|
|
182
167
|
});
|
|
183
168
|
|
|
184
|
-
router.service(DatastoreQueryController, {
|
|
185
|
-
getByReference: (req) => {
|
|
186
|
-
if (req.slug !== "clinic-records") throw new ConnectError("datastore not found", Code.NotFound);
|
|
187
|
-
return knownDatastore;
|
|
188
|
-
},
|
|
189
|
-
});
|
|
190
|
-
router.service(DatastoreCommandController, {
|
|
191
|
-
delete: (req) => {
|
|
192
|
-
datastoreDeletes.push({ resourceId: req.resourceId, force: req.force });
|
|
193
|
-
// Mirrors GuardNonEmptyStep: a non-empty datastore refuses without force.
|
|
194
|
-
if (!req.force) throw new ConnectError(nonEmptyGuardMessage, Code.FailedPrecondition);
|
|
195
|
-
return knownDatastore;
|
|
196
|
-
},
|
|
197
|
-
});
|
|
198
|
-
|
|
199
169
|
router.service(EnvironmentQueryController, {
|
|
200
170
|
getByReference: (req) => {
|
|
201
171
|
if (req.slug !== "clinic-patient-db") throw new ConnectError("environment not found", Code.NotFound);
|
|
@@ -374,7 +344,6 @@ describe("delete (standard kinds)", () => {
|
|
|
374
344
|
// it must name the wired kinds and never the narrowed ones (#354).
|
|
375
345
|
const message = String(err.message);
|
|
376
346
|
for (const wired of [
|
|
377
|
-
"datastore",
|
|
378
347
|
"environment",
|
|
379
348
|
"agentchannel",
|
|
380
349
|
"channelapp",
|
|
@@ -399,37 +368,6 @@ describe("delete (standard kinds)", () => {
|
|
|
399
368
|
});
|
|
400
369
|
});
|
|
401
370
|
|
|
402
|
-
describe("delete (force acknowledgment)", () => {
|
|
403
|
-
it("threads --force to the datastore delete RPC as the destroy-records ack", async () => {
|
|
404
|
-
const plan = await planDelete(client, "datastore", "clinic-records", "acme", true);
|
|
405
|
-
const result = await plan.perform();
|
|
406
|
-
|
|
407
|
-
expect(result.status).toBe("success");
|
|
408
|
-
expect(result.message).toBe("Datastore deleted successfully");
|
|
409
|
-
expect(datastoreDeletes).toEqual([{ resourceId: "dst_1", force: true }]);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it("surfaces the non-empty guard refusal verbatim when force is absent", async () => {
|
|
413
|
-
const plan = await planDelete(client, "datastore", "clinic-records", "acme");
|
|
414
|
-
const err = await plan.perform().catch((e) => e);
|
|
415
|
-
|
|
416
|
-
// A confirmed-but-unforced delete must NOT bypass the server guard: the
|
|
417
|
-
// RPC carries force=false and the refusal reaches the user verbatim.
|
|
418
|
-
expect(datastoreDeletes).toEqual([{ resourceId: "dst_1", force: false }]);
|
|
419
|
-
const classified = classify(err);
|
|
420
|
-
expect(classified?.exitCode).toBe(ExitCode.Usage);
|
|
421
|
-
expect(classified?.message).toBe(`Precondition failed: ${nonEmptyGuardMessage}`);
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it("warns up front that datastore records are destroyed and force-guarded", async () => {
|
|
425
|
-
const plan = await planDelete(client, "datastore", "clinic-records", "acme");
|
|
426
|
-
expect(plan.warning.hints).toContain("All records held by this datastore will be destroyed.");
|
|
427
|
-
expect(plan.warning.hints).toContain(
|
|
428
|
-
"A datastore holding records is refused unless --force acknowledges destroying them.",
|
|
429
|
-
);
|
|
430
|
-
});
|
|
431
|
-
});
|
|
432
|
-
|
|
433
371
|
describe("delete (cutover kinds: environment, agent channel, channel app)", () => {
|
|
434
372
|
it("deletes an environment via the DeleteResourceInput shape", async () => {
|
|
435
373
|
const plan = await planDelete(client, "environment", "clinic-patient-db", "acme");
|
|
@@ -7,7 +7,6 @@
|
|
|
7
7
|
|
|
8
8
|
import { type DescMessage, fromJson, type JsonValue } from "@bufbuild/protobuf";
|
|
9
9
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
10
|
-
import { DatastoreSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
|
|
11
10
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
12
11
|
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
13
12
|
import { ApiResourceKind } from "@stigmer/protos/ai/stigmer/commons/apiresource/apiresourcekind/api_resource_kind_pb";
|
|
@@ -22,7 +21,6 @@ export const VALIDATE_SCHEMAS: ReadonlyMap<ApiResourceKind, DescMessage> = new M
|
|
|
22
21
|
[ApiResourceKind.workflow, WorkflowSchema],
|
|
23
22
|
[ApiResourceKind.mcp_server, McpServerSchema],
|
|
24
23
|
[ApiResourceKind.project, ProjectSchema],
|
|
25
|
-
[ApiResourceKind.datastore, DatastoreSchema],
|
|
26
24
|
]);
|
|
27
25
|
|
|
28
26
|
export function schemaForValidate(kind: ApiResourceKind): DescMessage | undefined {
|