@stigmer/cli 3.2.3 → 3.4.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/commands/delete.js +5 -2
- package/commands/delete.js.map +1 -1
- package/commands/mcp-server.d.ts.map +1 -1
- package/commands/mcp-server.js +1 -0
- package/commands/mcp-server.js.map +1 -1
- package/package.json +5 -5
- package/registry/metadata.d.ts.map +1 -1
- package/registry/metadata.js +2 -0
- package/registry/metadata.js.map +1 -1
- package/registry/verb-support.d.ts.map +1 -1
- package/registry/verb-support.js +4 -0
- package/registry/verb-support.js.map +1 -1
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +13 -10
- package/resources/apply/apply.js.map +1 -1
- package/resources/apply/handlers.d.ts.map +1 -1
- package/resources/apply/handlers.js +11 -0
- package/resources/apply/handlers.js.map +1 -1
- package/resources/delete.d.ts +5 -1
- package/resources/delete.d.ts.map +1 -1
- package/resources/delete.js +40 -8
- package/resources/delete.js.map +1 -1
- package/resources/get-bindings.d.ts.map +1 -1
- package/resources/get-bindings.js +8 -0
- package/resources/get-bindings.js.map +1 -1
- package/resources/list.d.ts.map +1 -1
- package/resources/list.js +1 -0
- package/resources/list.js.map +1 -1
- package/resources/validate.d.ts.map +1 -1
- package/resources/validate.js +2 -0
- package/resources/validate.js.map +1 -1
- package/src/commands/delete.ts +5 -2
- package/src/commands/mcp-server.ts +1 -0
- package/src/registry/metadata.ts +2 -0
- package/src/registry/verb-support.ts +4 -0
- package/src/resources/apply/apply.integration.test.ts +69 -0
- package/src/resources/apply/apply.ts +13 -10
- package/src/resources/apply/handlers.ts +11 -0
- package/src/resources/delete.ts +48 -9
- package/src/resources/get-bindings.ts +8 -0
- package/src/resources/list.ts +1 -0
- package/src/resources/mutate-verbs.integration.test.ts +178 -1
- package/src/resources/read-verbs.integration.test.ts +111 -1
- package/src/resources/validate.ts +2 -0
|
@@ -35,6 +35,7 @@ Environment variables:
|
|
|
35
35
|
STIGMER_SERVER_ADDRESS gRPC address (default: CLI config, else localhost:7234)
|
|
36
36
|
STIGMER_API_KEY API key (auto-resolved from CLI config for cloud)
|
|
37
37
|
STIGMER_MCP_TRANSPORT stdio | http | both (default: stdio)
|
|
38
|
+
STIGMER_MCP_ROSTER full | records (default: full; records = the five record tools only)
|
|
38
39
|
STIGMER_MCP_HTTP_PORT HTTP listen port (default: 8080)
|
|
39
40
|
STIGMER_MCP_LOG_FORMAT text | json (default: text)
|
|
40
41
|
STIGMER_MCP_LOG_LEVEL debug | info | warn | error (default: info)
|
package/src/registry/metadata.ts
CHANGED
|
@@ -35,6 +35,7 @@ export const KIND_META: ReadonlyMap<ApiResourceKind, KindMeta> = new Map([
|
|
|
35
35
|
[ApiResourceKind.agent_share, { name: "AgentShare", displayName: "Agent Share", idPrefix: "ash" }],
|
|
36
36
|
[ApiResourceKind.agent_channel, { name: "AgentChannel", displayName: "Agent Channel", idPrefix: "ach" }],
|
|
37
37
|
[ApiResourceKind.channel_app, { name: "ChannelApp", displayName: "Channel App", idPrefix: "chapp" }],
|
|
38
|
+
[ApiResourceKind.datastore, { name: "Datastore", displayName: "Datastore", idPrefix: "dst" }],
|
|
38
39
|
[ApiResourceKind.agent_instance, { name: "AgentInstance", displayName: "Agent Instance", idPrefix: "ain" }],
|
|
39
40
|
[ApiResourceKind.workflow_instance, { name: "WorkflowInstance", displayName: "Workflow Instance", idPrefix: "win" }],
|
|
40
41
|
[ApiResourceKind.session, { name: "Session", displayName: "Session", idPrefix: "ses" }],
|
|
@@ -60,6 +61,7 @@ export const CLI_RELEVANT_KINDS: readonly ApiResourceKind[] = [
|
|
|
60
61
|
ApiResourceKind.agent_share,
|
|
61
62
|
ApiResourceKind.agent_channel,
|
|
62
63
|
ApiResourceKind.channel_app,
|
|
64
|
+
ApiResourceKind.datastore,
|
|
63
65
|
ApiResourceKind.agent_instance,
|
|
64
66
|
ApiResourceKind.workflow_instance,
|
|
65
67
|
ApiResourceKind.session,
|
|
@@ -48,6 +48,10 @@ export const VERB_SUPPORT: ReadonlyMap<ApiResourceKind, ReadonlySet<Verb>> = new
|
|
|
48
48
|
// Secret fields round-trip as ***REDACTED***; applying a fetched manifest
|
|
49
49
|
// preserves the stored secrets (the OAuthApp marker convention).
|
|
50
50
|
[ApiResourceKind.channel_app, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
|
|
51
|
+
// Record reads/writes are agent tools and record RPCs, not CLI verbs; the
|
|
52
|
+
// generic verbs cover the declarative path (apply a manifest, inspect,
|
|
53
|
+
// tear down — delete is guarded server-side for non-empty datastores).
|
|
54
|
+
[ApiResourceKind.datastore, new Set<Verb>([Verb.Apply, Verb.Validate, Verb.Get, Verb.List, Verb.Delete])],
|
|
51
55
|
[ApiResourceKind.agent_instance, new Set<Verb>([Verb.Apply, Verb.Get, Verb.List, Verb.Delete])],
|
|
52
56
|
// No list: proto exposes getByWorkflow (requires workflow_id), not a generic list.
|
|
53
57
|
[ApiResourceKind.workflow_instance, new Set<Verb>([Verb.Apply, Verb.Get, Verb.Delete])],
|
|
@@ -17,6 +17,8 @@ import { type ConnectRouter, createClient } from "@connectrpc/connect";
|
|
|
17
17
|
import { connectNodeAdapter } from "@connectrpc/connect-node";
|
|
18
18
|
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
19
19
|
import { AgentCommandController } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/command_pb";
|
|
20
|
+
import type { Datastore } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
|
|
21
|
+
import { DatastoreCommandController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/command_pb";
|
|
20
22
|
import type { McpServer } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
21
23
|
import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
|
|
22
24
|
import { createNodeTransport, normalizeEndpoint } from "@stigmer/sdk/node";
|
|
@@ -30,10 +32,12 @@ const openSessions = new Set<ServerHttp2Session>();
|
|
|
30
32
|
|
|
31
33
|
let appliedAgents: Agent[] = [];
|
|
32
34
|
let appliedMcps: McpServer[] = [];
|
|
35
|
+
let appliedDatastores: Datastore[] = [];
|
|
33
36
|
|
|
34
37
|
beforeEach(() => {
|
|
35
38
|
appliedAgents = [];
|
|
36
39
|
appliedMcps = [];
|
|
40
|
+
appliedDatastores = [];
|
|
37
41
|
});
|
|
38
42
|
|
|
39
43
|
function writeYaml(dir: string, name: string, body: string): string {
|
|
@@ -56,6 +60,12 @@ beforeAll(async () => {
|
|
|
56
60
|
return req;
|
|
57
61
|
},
|
|
58
62
|
});
|
|
63
|
+
router.service(DatastoreCommandController, {
|
|
64
|
+
apply: (req) => {
|
|
65
|
+
appliedDatastores.push(req);
|
|
66
|
+
return req;
|
|
67
|
+
},
|
|
68
|
+
});
|
|
59
69
|
};
|
|
60
70
|
|
|
61
71
|
backend = createHttp2Server(connectNodeAdapter({ routes }));
|
|
@@ -146,6 +156,65 @@ describe("file-mode apply — fidelity (raw controller preserves full proto)", (
|
|
|
146
156
|
});
|
|
147
157
|
});
|
|
148
158
|
|
|
159
|
+
const DATASTORE_YAML = [
|
|
160
|
+
"apiVersion: agentic.stigmer.ai/v1",
|
|
161
|
+
"kind: Datastore",
|
|
162
|
+
"metadata:",
|
|
163
|
+
" name: clinic-records",
|
|
164
|
+
" slug: clinic-records",
|
|
165
|
+
"spec:",
|
|
166
|
+
" timezone: Asia/Kolkata",
|
|
167
|
+
" authorization:",
|
|
168
|
+
" roles:",
|
|
169
|
+
" - name: patient",
|
|
170
|
+
" default_role: patient",
|
|
171
|
+
" collections:",
|
|
172
|
+
" - name: bookings",
|
|
173
|
+
" fields:",
|
|
174
|
+
" - name: slot_start",
|
|
175
|
+
" type: timestamp",
|
|
176
|
+
" required: true",
|
|
177
|
+
" uniques:",
|
|
178
|
+
" - name: one_per_slot",
|
|
179
|
+
" fields: [slot_start]",
|
|
180
|
+
' message: "that slot is already booked"',
|
|
181
|
+
"",
|
|
182
|
+
].join("\n");
|
|
183
|
+
|
|
184
|
+
describe("file-mode apply — datastore", () => {
|
|
185
|
+
it("applies a Datastore through the raw controller with the spec intact", async () => {
|
|
186
|
+
const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
|
|
187
|
+
try {
|
|
188
|
+
writeYaml(dir, "datastore.yaml", DATASTORE_YAML);
|
|
189
|
+
const items = resolveApplyItems(dir);
|
|
190
|
+
expect(items).toHaveLength(1);
|
|
191
|
+
|
|
192
|
+
const outcome = await applyItem(controllerFn, items[0], "acme", false);
|
|
193
|
+
expect(outcome.result.message).toBe("Datastore created successfully");
|
|
194
|
+
|
|
195
|
+
expect(appliedDatastores).toHaveLength(1);
|
|
196
|
+
const spec = appliedDatastores[0].spec;
|
|
197
|
+
expect(spec?.timezone).toBe("Asia/Kolkata");
|
|
198
|
+
expect(spec?.authorization?.defaultRole).toBe("patient");
|
|
199
|
+
expect(spec?.collections[0]?.uniques[0]?.message).toBe("that slot is already booked");
|
|
200
|
+
} finally {
|
|
201
|
+
rmSync(dir, { recursive: true, force: true });
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it("sorts a Datastore before the Agent that references it", () => {
|
|
206
|
+
const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
|
|
207
|
+
try {
|
|
208
|
+
writeYaml(dir, "agent.yaml", AGENT_WITH_ID);
|
|
209
|
+
writeYaml(dir, "datastore.yaml", DATASTORE_YAML);
|
|
210
|
+
const items = resolveApplyItems(dir);
|
|
211
|
+
expect(items.map((i) => i.handler.displayName)).toEqual(["Datastore", "Agent"]);
|
|
212
|
+
} finally {
|
|
213
|
+
rmSync(dir, { recursive: true, force: true });
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
|
|
149
218
|
describe("file-mode apply — orchestration", () => {
|
|
150
219
|
it("sorts items into dependency order (mcp_server before agent)", () => {
|
|
151
220
|
const dir = mkdtempSync(join(tmpdir(), "apply-it-"));
|
|
@@ -44,16 +44,19 @@ export interface ApplyOutcome {
|
|
|
44
44
|
const APPLY_ORDER: ReadonlyMap<ApiResourceKind, number> = new Map([
|
|
45
45
|
[ApiResourceKind.organization, 0],
|
|
46
46
|
[ApiResourceKind.mcp_server, 1],
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
[ApiResourceKind.
|
|
50
|
-
[ApiResourceKind.
|
|
51
|
-
[ApiResourceKind.
|
|
52
|
-
[ApiResourceKind.
|
|
53
|
-
[ApiResourceKind.
|
|
54
|
-
[ApiResourceKind.
|
|
55
|
-
|
|
56
|
-
[ApiResourceKind.
|
|
47
|
+
// Before agent: an Agent references a Datastore via datastore_usages
|
|
48
|
+
// (the McpServer-before-Agent pattern; mirrors the SDK manifest registry).
|
|
49
|
+
[ApiResourceKind.datastore, 2],
|
|
50
|
+
[ApiResourceKind.agent, 3],
|
|
51
|
+
[ApiResourceKind.workflow, 4],
|
|
52
|
+
[ApiResourceKind.environment, 5],
|
|
53
|
+
[ApiResourceKind.identity_provider, 6],
|
|
54
|
+
[ApiResourceKind.oauth_app, 7],
|
|
55
|
+
[ApiResourceKind.agent_instance, 8],
|
|
56
|
+
[ApiResourceKind.workflow_instance, 9],
|
|
57
|
+
[ApiResourceKind.session, 10],
|
|
58
|
+
// After agent and environment: a share references both.
|
|
59
|
+
[ApiResourceKind.agent_share, 11],
|
|
57
60
|
]);
|
|
58
61
|
|
|
59
62
|
/** Expand a path into ordered, kind-resolved apply items (strict YAML parse). */
|
|
@@ -23,6 +23,8 @@ import { type AgentInstance, AgentInstanceSchema } from "@stigmer/protos/ai/stig
|
|
|
23
23
|
import { AgentInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/command_pb";
|
|
24
24
|
import { type AgentShare, AgentShareSchema } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
|
|
25
25
|
import { AgentShareCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/command_pb";
|
|
26
|
+
import { type Datastore, DatastoreSchema } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/api_pb";
|
|
27
|
+
import { DatastoreCommandController } from "@stigmer/protos/ai/stigmer/agentic/datastore/v1/command_pb";
|
|
26
28
|
import { type Environment, EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
27
29
|
import { EnvironmentCommandController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/command_pb";
|
|
28
30
|
import { type McpServer, McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
@@ -74,6 +76,15 @@ export const APPLY_HANDLERS: ReadonlyMap<ApiResourceKind, ApplyHandler> = new Ma
|
|
|
74
76
|
apply: (c, m) => c(McpServerCommandController).apply(m as McpServer),
|
|
75
77
|
},
|
|
76
78
|
],
|
|
79
|
+
[
|
|
80
|
+
ApiResourceKind.datastore,
|
|
81
|
+
{
|
|
82
|
+
kind: ApiResourceKind.datastore,
|
|
83
|
+
displayName: "Datastore",
|
|
84
|
+
schema: DatastoreSchema,
|
|
85
|
+
apply: (c, m) => c(DatastoreCommandController).apply(m as Datastore),
|
|
86
|
+
},
|
|
87
|
+
],
|
|
77
88
|
[
|
|
78
89
|
ApiResourceKind.agent,
|
|
79
90
|
{
|
package/src/resources/delete.ts
CHANGED
|
@@ -45,33 +45,60 @@ interface HasMetadata {
|
|
|
45
45
|
readonly metadata?: { readonly id?: string; readonly name?: string; readonly slug?: string; readonly org?: string };
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
// `force` is the RPC-level acknowledgment of destructive side effects (e.g. a
|
|
49
|
+
// datastore's held records) — distinct from, but carried by, the CLI's
|
|
50
|
+
// `-f/--force` flag. Kinds whose delete RPC takes a plain typed ID have no
|
|
51
|
+
// force field and simply ignore the argument.
|
|
52
|
+
type DeleteFn = (client: Stigmer, id: string, force: boolean) => Promise<HasMetadata>;
|
|
49
53
|
|
|
50
54
|
// Kinds the unified delete handles directly, each bound to its SDK delete call.
|
|
51
55
|
// Kinds that carry a Delete verb but no entry here (identity_provider,
|
|
52
|
-
// oauth_app,
|
|
56
|
+
// oauth_app, agent_share, workflow_instance, session) fall through to a
|
|
53
57
|
// "not implemented" usage error, matching Go's default branch.
|
|
54
|
-
//
|
|
58
|
+
// Kinds whose delete takes a DeleteResourceInput (not an ID) thread the force
|
|
59
|
+
// acknowledgment through; today only the datastore server honors it (the
|
|
60
|
+
// non-empty guard), but force is the contract's generic ack carrier.
|
|
55
61
|
const DELETE_HANDLERS: ReadonlyMap<ApiResourceKind, DeleteFn> = new Map<ApiResourceKind, DeleteFn>([
|
|
56
62
|
[ApiResourceKind.agent, (c, id) => c.agent.delete(id)],
|
|
57
63
|
[ApiResourceKind.agent_instance, (c, id) => c.agentInstance.delete(id)],
|
|
58
64
|
[ApiResourceKind.workflow, (c, id) => c.workflow.delete(id)],
|
|
59
|
-
[ApiResourceKind.mcp_server, (c, id) => c.mcpServer.delete({ resourceId: id })],
|
|
65
|
+
[ApiResourceKind.mcp_server, (c, id, force) => c.mcpServer.delete({ resourceId: id, force })],
|
|
60
66
|
[ApiResourceKind.project, (c, id) => c.project.delete(id)],
|
|
67
|
+
[ApiResourceKind.datastore, (c, id, force) => c.datastore.delete({ resourceId: id, force })],
|
|
68
|
+
[ApiResourceKind.environment, (c, id, force) => c.environment.delete({ resourceId: id, force })],
|
|
69
|
+
[ApiResourceKind.agent_channel, (c, id) => c.agentChannel.delete(id)],
|
|
70
|
+
[ApiResourceKind.channel_app, (c, id, force) => c.channelapp.delete({ resourceId: id, force })],
|
|
61
71
|
[ApiResourceKind.skill, (c, id) => c.skill.delete(id)],
|
|
62
72
|
[ApiResourceKind.api_key, (c, id) => c.apiKey.delete(id)],
|
|
63
73
|
]);
|
|
64
74
|
|
|
75
|
+
// The deletable-types list for the unknown-type error, derived from the
|
|
76
|
+
// handler map (plus the two special-cased routes) so it can never drift from
|
|
77
|
+
// what the dispatch below actually supports. Deliberately NOT derived from the
|
|
78
|
+
// registry's verb matrix — that declares Delete for kinds this dispatch does
|
|
79
|
+
// not wire, and listing those would over-promise.
|
|
80
|
+
function availableDeleteTypes(): string {
|
|
81
|
+
const wired = [...DELETE_HANDLERS.keys()]
|
|
82
|
+
.map((kind) => defaultRegistry().getByKind(kind)?.singular)
|
|
83
|
+
.filter((singular): singular is string => singular !== undefined);
|
|
84
|
+
return [...wired, "execution", "organization"].join(", ");
|
|
85
|
+
}
|
|
86
|
+
|
|
65
87
|
/**
|
|
66
88
|
* Build the delete plan for a `<type> <reference>` pair. Routes the three
|
|
67
89
|
* special cases first, then the registry-driven standard path. Throws on
|
|
68
90
|
* unknown/unsupported types before any network call.
|
|
91
|
+
*
|
|
92
|
+
* `force` is the caller's acknowledgment of destructive side effects; it rides
|
|
93
|
+
* the delete RPC for kinds whose contract carries a force field (see
|
|
94
|
+
* DELETE_HANDLERS). The command layer maps `-f/--force` onto it.
|
|
69
95
|
*/
|
|
70
96
|
export async function planDelete(
|
|
71
97
|
client: Stigmer,
|
|
72
98
|
typeArg: string,
|
|
73
99
|
reference: string,
|
|
74
100
|
org: string,
|
|
101
|
+
force = false,
|
|
75
102
|
): Promise<DeletePlan> {
|
|
76
103
|
if (isExecutionAlias(typeArg)) {
|
|
77
104
|
return planExecutionCancel(client, reference);
|
|
@@ -79,9 +106,7 @@ export async function planDelete(
|
|
|
79
106
|
|
|
80
107
|
const info = defaultRegistry().getByAlias(typeArg);
|
|
81
108
|
if (info === undefined) {
|
|
82
|
-
throw new UsageError(
|
|
83
|
-
`unknown resource type: ${typeArg}\n\nAvailable types: agent, agent-instance, workflow, mcpserver, project, skill, execution, organization`,
|
|
84
|
-
);
|
|
109
|
+
throw new UsageError(`unknown resource type: ${typeArg}\n\nAvailable types: ${availableDeleteTypes()}`);
|
|
85
110
|
}
|
|
86
111
|
|
|
87
112
|
if (info.kind === ApiResourceKind.organization) {
|
|
@@ -97,7 +122,7 @@ export async function planDelete(
|
|
|
97
122
|
throw new UsageError(`delete not implemented for ${info.displayName}`);
|
|
98
123
|
}
|
|
99
124
|
|
|
100
|
-
return planStandardDelete(client, info, reference, org, deleteFn);
|
|
125
|
+
return planStandardDelete(client, info, reference, org, deleteFn, force);
|
|
101
126
|
}
|
|
102
127
|
|
|
103
128
|
async function planStandardDelete(
|
|
@@ -106,6 +131,7 @@ async function planStandardDelete(
|
|
|
106
131
|
reference: string,
|
|
107
132
|
org: string,
|
|
108
133
|
deleteFn: DeleteFn,
|
|
134
|
+
force: boolean,
|
|
109
135
|
): Promise<DeletePlan> {
|
|
110
136
|
// Safe by construction: every DELETE_HANDLERS key also has a get binding.
|
|
111
137
|
const getter = getterFor(info.kind);
|
|
@@ -122,7 +148,7 @@ async function planStandardDelete(
|
|
|
122
148
|
return {
|
|
123
149
|
warning: buildDeleteWarning(info, resource),
|
|
124
150
|
confirmPrompt: "Proceed with deletion? [y/N]",
|
|
125
|
-
perform: async () => buildDeleteSuccess(info, await deleteFn(client, id)),
|
|
151
|
+
perform: async () => buildDeleteSuccess(info, await deleteFn(client, id, force)),
|
|
126
152
|
};
|
|
127
153
|
}
|
|
128
154
|
|
|
@@ -200,6 +226,19 @@ function buildDeleteWarning(info: TypeInfo, message: HasMetadata): CommandResult
|
|
|
200
226
|
warning.hint("This will delete the skill and all its versions.");
|
|
201
227
|
}
|
|
202
228
|
|
|
229
|
+
if (info.kind === ApiResourceKind.datastore) {
|
|
230
|
+
// Mirrors the server's non-empty guard (GuardNonEmptyStep): the delete of
|
|
231
|
+
// a datastore holding records is refused unless force acknowledges it.
|
|
232
|
+
warning.hint("All records held by this datastore will be destroyed.");
|
|
233
|
+
warning.hint("A datastore holding records is refused unless --force acknowledges destroying them.");
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (info.kind === ApiResourceKind.agent_channel) {
|
|
237
|
+
// The server's documented distinction: delete is the connection's full
|
|
238
|
+
// teardown; disabling is the config-preserving pause.
|
|
239
|
+
warning.hint("Delete is the connection's full teardown; to pause instead, apply with spec.enabled: false.");
|
|
240
|
+
}
|
|
241
|
+
|
|
203
242
|
warning.hint("This action cannot be undone.");
|
|
204
243
|
return warning;
|
|
205
244
|
}
|
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
|
|
6
6
|
import type { DescMessage, Message } from "@bufbuild/protobuf";
|
|
7
7
|
import { AgentSchema } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
8
|
+
import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
|
|
8
9
|
import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
|
|
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
|
+
import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
9
13
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
10
14
|
import { SkillSchema } from "@stigmer/protos/ai/stigmer/agentic/skill/v1/api_pb";
|
|
11
15
|
import { WorkflowSchema } from "@stigmer/protos/ai/stigmer/agentic/workflow/v1/api_pb";
|
|
@@ -29,6 +33,10 @@ const GET_BINDINGS: ReadonlyMap<ApiResourceKind, Getter> = new Map([
|
|
|
29
33
|
[ApiResourceKind.workflow, refGetter(ApiResourceKind.workflow, WorkflowSchema, (c) => c.workflow)],
|
|
30
34
|
[ApiResourceKind.mcp_server, refGetter(ApiResourceKind.mcp_server, McpServerSchema, (c) => c.mcpServer)],
|
|
31
35
|
[ApiResourceKind.project, refGetter(ApiResourceKind.project, ProjectSchema, (c) => c.project)],
|
|
36
|
+
[ApiResourceKind.datastore, refGetter(ApiResourceKind.datastore, DatastoreSchema, (c) => c.datastore)],
|
|
37
|
+
[ApiResourceKind.environment, refGetter(ApiResourceKind.environment, EnvironmentSchema, (c) => c.environment)],
|
|
38
|
+
[ApiResourceKind.agent_channel, refGetter(ApiResourceKind.agent_channel, AgentChannelSchema, (c) => c.agentChannel)],
|
|
39
|
+
[ApiResourceKind.channel_app, refGetter(ApiResourceKind.channel_app, ChannelAppSchema, (c) => c.channelapp)],
|
|
32
40
|
[ApiResourceKind.skill, refGetter(ApiResourceKind.skill, SkillSchema, (c) => c.skill)],
|
|
33
41
|
[ApiResourceKind.api_key, idOnlyGetter(ApiKeySchema, (c) => c.apiKey, "API keys")],
|
|
34
42
|
]);
|
package/src/resources/list.ts
CHANGED
|
@@ -16,9 +16,21 @@ import { AgentExecutionSchema } from "@stigmer/protos/ai/stigmer/agentic/agentex
|
|
|
16
16
|
import { AgentExecutionCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/command_pb";
|
|
17
17
|
import { ExecutionPhase } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/enum_pb";
|
|
18
18
|
import { AgentExecutionQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentexecution/v1/query_pb";
|
|
19
|
+
import { AgentChannelSchema } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/api_pb";
|
|
20
|
+
import { AgentChannelCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/command_pb";
|
|
21
|
+
import { AgentChannelQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentchannel/v1/query_pb";
|
|
19
22
|
import { AgentInstanceSchema } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/api_pb";
|
|
20
23
|
import { AgentInstanceCommandController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/command_pb";
|
|
21
24
|
import { AgentInstanceQueryController } from "@stigmer/protos/ai/stigmer/agentic/agentinstance/v1/query_pb";
|
|
25
|
+
import { ChannelAppSchema } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/api_pb";
|
|
26
|
+
import { ChannelAppCommandController } from "@stigmer/protos/ai/stigmer/agentic/channelapp/v1/command_pb";
|
|
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
|
+
import { EnvironmentSchema } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/api_pb";
|
|
32
|
+
import { EnvironmentCommandController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/command_pb";
|
|
33
|
+
import { EnvironmentQueryController } from "@stigmer/protos/ai/stigmer/agentic/environment/v1/query_pb";
|
|
22
34
|
import { McpServerSchema } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/api_pb";
|
|
23
35
|
import { McpServerCommandController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/command_pb";
|
|
24
36
|
import { McpServerQueryController } from "@stigmer/protos/ai/stigmer/agentic/mcpserver/v1/query_pb";
|
|
@@ -51,6 +63,28 @@ const knownWorkflow = create(WorkflowSchema, {
|
|
|
51
63
|
metadata: { name: "Deploy", slug: "deploy", org: "acme", id: "wfl_1" },
|
|
52
64
|
});
|
|
53
65
|
|
|
66
|
+
// The mock treats this datastore as NON-EMPTY: delete refuses without the
|
|
67
|
+
// force acknowledgment, mirroring the server's GuardNonEmptyStep contract.
|
|
68
|
+
const knownDatastore = create(DatastoreSchema, {
|
|
69
|
+
metadata: { name: "clinic-records", slug: "clinic-records", org: "acme", id: "dst_1" },
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Byte-shaped like the Go server's GuardNonEmptyStep refusal (delete_guards.go).
|
|
73
|
+
const nonEmptyGuardMessage =
|
|
74
|
+
'datastore "clinic-records" holds 12 records across 3 collections; deleting destroys them — pass force to acknowledge';
|
|
75
|
+
|
|
76
|
+
const knownEnvironment = create(EnvironmentSchema, {
|
|
77
|
+
metadata: { name: "clinic-patient-db", slug: "clinic-patient-db", org: "acme", id: "env_1" },
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const knownChannel = create(AgentChannelSchema, {
|
|
81
|
+
metadata: { name: "clinic-patient-whatsapp", slug: "clinic-patient-whatsapp", org: "acme", id: "ach_1" },
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
const knownChannelApp = create(ChannelAppSchema, {
|
|
85
|
+
metadata: { name: "clinic-meta-app", slug: "clinic-meta-app", org: "acme", id: "chapp_1" },
|
|
86
|
+
});
|
|
87
|
+
|
|
54
88
|
// Pending execution (cancellable) vs. an already-terminal one.
|
|
55
89
|
const pendingExecution = create(AgentExecutionSchema, {
|
|
56
90
|
metadata: { id: "aex_run" },
|
|
@@ -72,10 +106,20 @@ const openSessions = new Set<ServerHttp2Session>();
|
|
|
72
106
|
// Spies for the command-side calls, reset per test.
|
|
73
107
|
let cancelCalls: string[] = [];
|
|
74
108
|
let tagCalls: { workflowId: string; versionHash: string; tag: string }[] = [];
|
|
109
|
+
// Force-carrying deletes record what actually rode the RPC.
|
|
110
|
+
let datastoreDeletes: { resourceId: string; force: boolean }[] = [];
|
|
111
|
+
let environmentDeletes: { resourceId: string; force: boolean }[] = [];
|
|
112
|
+
let channelAppDeletes: { resourceId: string; force: boolean }[] = [];
|
|
113
|
+
// agent_channel's delete is a typed ID — no force field exists on the wire.
|
|
114
|
+
let channelDeleteIds: string[] = [];
|
|
75
115
|
|
|
76
116
|
beforeEach(() => {
|
|
77
117
|
cancelCalls = [];
|
|
78
118
|
tagCalls = [];
|
|
119
|
+
datastoreDeletes = [];
|
|
120
|
+
environmentDeletes = [];
|
|
121
|
+
channelAppDeletes = [];
|
|
122
|
+
channelDeleteIds = [];
|
|
79
123
|
});
|
|
80
124
|
|
|
81
125
|
beforeAll(async () => {
|
|
@@ -118,6 +162,60 @@ beforeAll(async () => {
|
|
|
118
162
|
},
|
|
119
163
|
});
|
|
120
164
|
|
|
165
|
+
router.service(DatastoreQueryController, {
|
|
166
|
+
getByReference: (req) => {
|
|
167
|
+
if (req.slug !== "clinic-records") throw new ConnectError("datastore not found", Code.NotFound);
|
|
168
|
+
return knownDatastore;
|
|
169
|
+
},
|
|
170
|
+
});
|
|
171
|
+
router.service(DatastoreCommandController, {
|
|
172
|
+
delete: (req) => {
|
|
173
|
+
datastoreDeletes.push({ resourceId: req.resourceId, force: req.force });
|
|
174
|
+
// Mirrors GuardNonEmptyStep: a non-empty datastore refuses without force.
|
|
175
|
+
if (!req.force) throw new ConnectError(nonEmptyGuardMessage, Code.FailedPrecondition);
|
|
176
|
+
return knownDatastore;
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
router.service(EnvironmentQueryController, {
|
|
181
|
+
getByReference: (req) => {
|
|
182
|
+
if (req.slug !== "clinic-patient-db") throw new ConnectError("environment not found", Code.NotFound);
|
|
183
|
+
return knownEnvironment;
|
|
184
|
+
},
|
|
185
|
+
});
|
|
186
|
+
router.service(EnvironmentCommandController, {
|
|
187
|
+
delete: (req) => {
|
|
188
|
+
environmentDeletes.push({ resourceId: req.resourceId, force: req.force });
|
|
189
|
+
return knownEnvironment;
|
|
190
|
+
},
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
router.service(AgentChannelQueryController, {
|
|
194
|
+
getByReference: (req) => {
|
|
195
|
+
if (req.slug !== "clinic-patient-whatsapp") throw new ConnectError("agent channel not found", Code.NotFound);
|
|
196
|
+
return knownChannel;
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
router.service(AgentChannelCommandController, {
|
|
200
|
+
delete: (req) => {
|
|
201
|
+
channelDeleteIds.push(req.value);
|
|
202
|
+
return knownChannel;
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
router.service(ChannelAppQueryController, {
|
|
207
|
+
getByReference: (req) => {
|
|
208
|
+
if (req.slug !== "clinic-meta-app") throw new ConnectError("channel app not found", Code.NotFound);
|
|
209
|
+
return knownChannelApp;
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
router.service(ChannelAppCommandController, {
|
|
213
|
+
delete: (req) => {
|
|
214
|
+
channelAppDeletes.push({ resourceId: req.resourceId, force: req.force });
|
|
215
|
+
return knownChannelApp;
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
|
|
121
219
|
router.service(WorkflowQueryController, {
|
|
122
220
|
getByReference: (req) => {
|
|
123
221
|
if (req.slug !== "deploy") throw new ConnectError("workflow not found", Code.NotFound);
|
|
@@ -208,9 +306,17 @@ describe("delete (standard kinds)", () => {
|
|
|
208
306
|
expect(classify(err)?.exitCode).toBe(ExitCode.NotFound);
|
|
209
307
|
});
|
|
210
308
|
|
|
211
|
-
it("rejects an unknown type
|
|
309
|
+
it("rejects an unknown type and lists exactly the wired types", async () => {
|
|
212
310
|
const err = await planDelete(client, "bogus", "x", "acme").catch((e) => e);
|
|
213
311
|
expect(classify(err)?.exitCode).toBe(ExitCode.Usage);
|
|
312
|
+
// The list is derived from DELETE_HANDLERS (+ the two special cases), so
|
|
313
|
+
// it must name the wired kinds and never the declared-but-unwired ones.
|
|
314
|
+
const message = String(err.message);
|
|
315
|
+
for (const wired of ["datastore", "environment", "agentchannel", "channelapp", "execution", "organization"]) {
|
|
316
|
+
expect(message).toContain(wired);
|
|
317
|
+
}
|
|
318
|
+
expect(message).not.toContain("session");
|
|
319
|
+
expect(message).not.toContain("oauthapp");
|
|
214
320
|
});
|
|
215
321
|
|
|
216
322
|
it("rejects a kind that has no delete handler with a usage error", async () => {
|
|
@@ -219,6 +325,77 @@ describe("delete (standard kinds)", () => {
|
|
|
219
325
|
});
|
|
220
326
|
});
|
|
221
327
|
|
|
328
|
+
describe("delete (force acknowledgment)", () => {
|
|
329
|
+
it("threads --force to the datastore delete RPC as the destroy-records ack", async () => {
|
|
330
|
+
const plan = await planDelete(client, "datastore", "clinic-records", "acme", true);
|
|
331
|
+
const result = await plan.perform();
|
|
332
|
+
|
|
333
|
+
expect(result.status).toBe("success");
|
|
334
|
+
expect(result.message).toBe("Datastore deleted successfully");
|
|
335
|
+
expect(datastoreDeletes).toEqual([{ resourceId: "dst_1", force: true }]);
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
it("surfaces the non-empty guard refusal verbatim when force is absent", async () => {
|
|
339
|
+
const plan = await planDelete(client, "datastore", "clinic-records", "acme");
|
|
340
|
+
const err = await plan.perform().catch((e) => e);
|
|
341
|
+
|
|
342
|
+
// A confirmed-but-unforced delete must NOT bypass the server guard: the
|
|
343
|
+
// RPC carries force=false and the refusal reaches the user verbatim.
|
|
344
|
+
expect(datastoreDeletes).toEqual([{ resourceId: "dst_1", force: false }]);
|
|
345
|
+
const classified = classify(err);
|
|
346
|
+
expect(classified?.exitCode).toBe(ExitCode.Usage);
|
|
347
|
+
expect(classified?.message).toBe(`Precondition failed: ${nonEmptyGuardMessage}`);
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it("warns up front that datastore records are destroyed and force-guarded", async () => {
|
|
351
|
+
const plan = await planDelete(client, "datastore", "clinic-records", "acme");
|
|
352
|
+
expect(plan.warning.hints).toContain("All records held by this datastore will be destroyed.");
|
|
353
|
+
expect(plan.warning.hints).toContain(
|
|
354
|
+
"A datastore holding records is refused unless --force acknowledges destroying them.",
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
describe("delete (cutover kinds: environment, agent channel, channel app)", () => {
|
|
360
|
+
it("deletes an environment via the DeleteResourceInput shape", async () => {
|
|
361
|
+
const plan = await planDelete(client, "environment", "clinic-patient-db", "acme");
|
|
362
|
+
const result = await plan.perform();
|
|
363
|
+
|
|
364
|
+
expect(result.status).toBe("success");
|
|
365
|
+
expect(result.message).toBe("Environment deleted successfully");
|
|
366
|
+
expect(environmentDeletes).toEqual([{ resourceId: "env_1", force: false }]);
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
it("threads --force through the environment delete (generic ack carrier)", async () => {
|
|
370
|
+
const plan = await planDelete(client, "environment", "clinic-patient-db", "acme", true);
|
|
371
|
+
await plan.perform();
|
|
372
|
+
expect(environmentDeletes).toEqual([{ resourceId: "env_1", force: true }]);
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it("deletes an agent channel via its typed-ID contract, ignoring force gracefully", async () => {
|
|
376
|
+
const plan = await planDelete(client, "agent-channel", "clinic-patient-whatsapp", "acme", true);
|
|
377
|
+
|
|
378
|
+
// The teardown-vs-pause distinction rides the warning (server doc contract).
|
|
379
|
+
expect(plan.warning.hints).toContain(
|
|
380
|
+
"Delete is the connection's full teardown; to pause instead, apply with spec.enabled: false.",
|
|
381
|
+
);
|
|
382
|
+
|
|
383
|
+
const result = await plan.perform();
|
|
384
|
+
expect(result.status).toBe("success");
|
|
385
|
+
expect(result.message).toBe("Agent Channel deleted successfully");
|
|
386
|
+
expect(channelDeleteIds).toEqual(["ach_1"]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("deletes a channel app via the DeleteResourceInput shape", async () => {
|
|
390
|
+
const plan = await planDelete(client, "channel-app", "clinic-meta-app", "acme");
|
|
391
|
+
const result = await plan.perform();
|
|
392
|
+
|
|
393
|
+
expect(result.status).toBe("success");
|
|
394
|
+
expect(result.message).toBe("Channel App deleted successfully");
|
|
395
|
+
expect(channelAppDeletes).toEqual([{ resourceId: "chapp_1", force: false }]);
|
|
396
|
+
});
|
|
397
|
+
});
|
|
398
|
+
|
|
222
399
|
describe("delete execution (cancel special case)", () => {
|
|
223
400
|
it("cancels a non-terminal execution", async () => {
|
|
224
401
|
const plan = await planDelete(client, "execution", "aex_run", "");
|