@stigmer/cli 3.1.8 → 3.1.9
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/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 +3 -0
- package/registry/verb-support.js.map +1 -1
- package/resources/apply/apply.d.ts.map +1 -1
- package/resources/apply/apply.js +2 -0
- 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/share.d.ts +5 -4
- package/resources/share.d.ts.map +1 -1
- package/resources/share.js +97 -53
- package/resources/share.js.map +1 -1
- package/src/registry/metadata.ts +2 -0
- package/src/registry/verb-support.ts +3 -0
- package/src/resources/apply/apply.ts +2 -0
- package/src/resources/apply/handlers.ts +11 -0
- package/src/resources/share.test.ts +208 -99
- package/src/resources/share.ts +124 -64
package/src/resources/share.ts
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
|
-
// `share agent` dispatch: enable or disable
|
|
2
|
-
//
|
|
1
|
+
// `share agent` dispatch: enable or disable sharing for an agent and report
|
|
2
|
+
// the hosted chat link + embed snippet.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
4
|
+
// Sharing lives in its own AgentShare resource (decision 011) — the agent is
|
|
5
|
+
// never modified. This module resolves the agent, reads its canonical share,
|
|
6
|
+
// and commits changes via `agentShare.apply`, an idempotent upsert keyed on
|
|
7
|
+
// the share's (org, slug) identity: the first enable creates the share, later
|
|
8
|
+
// toggles update it, one code path.
|
|
9
|
+
//
|
|
10
|
+
// The one correctness invariant here: apply replaces the share's spec
|
|
11
|
+
// WHOLESALE. A naive "set enabled" would silently wipe any allowed_origins,
|
|
12
|
+
// visitor messages, audience, or credential bindings the owner configured in
|
|
13
|
+
// the console. So this module always reads the current share first and sends
|
|
14
|
+
// the complete spec back with only the requested fields flipped — the same
|
|
15
|
+
// merge-preserve discipline as the web dialog. (The rotatable link token is
|
|
16
|
+
// exempt: it is server-owned status, which survives every apply verbatim.)
|
|
10
17
|
//
|
|
11
18
|
// URL and snippet shapes come from @stigmer/sdk's sharing helpers — the single
|
|
12
19
|
// source of truth shared with the web console — so every surface emits
|
|
@@ -15,17 +22,18 @@
|
|
|
15
22
|
|
|
16
23
|
import { create } from "@bufbuild/protobuf";
|
|
17
24
|
import type { Agent } from "@stigmer/protos/ai/stigmer/agentic/agent/v1/api_pb";
|
|
25
|
+
import type { AgentShare } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/api_pb";
|
|
18
26
|
import {
|
|
27
|
+
GetAgentSharesByAgentRequestSchema,
|
|
19
28
|
RotateShareLinkInputSchema,
|
|
20
|
-
|
|
21
|
-
} from "@stigmer/protos/ai/stigmer/agentic/
|
|
29
|
+
} from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/io_pb";
|
|
30
|
+
import { AgentShareAudience } from "@stigmer/protos/ai/stigmer/agentic/agentshare/v1/spec_pb";
|
|
22
31
|
import {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
type
|
|
27
|
-
} from "@stigmer/
|
|
28
|
-
import { buildChatUrl, buildEmbedSnippet, type Stigmer } from "@stigmer/sdk";
|
|
32
|
+
buildChatUrl,
|
|
33
|
+
buildEmbedSnippet,
|
|
34
|
+
type AgentShareInput,
|
|
35
|
+
type Stigmer,
|
|
36
|
+
} from "@stigmer/sdk";
|
|
29
37
|
import { UsageError } from "../errors/index.js";
|
|
30
38
|
import { CommandResult } from "../output/index.js";
|
|
31
39
|
import { isAgentId } from "./reference.js";
|
|
@@ -39,7 +47,7 @@ export interface ShareAgentOptions {
|
|
|
39
47
|
/** Desired sharing state: `true` to enable, `false` to disable. */
|
|
40
48
|
readonly enabled: boolean;
|
|
41
49
|
/**
|
|
42
|
-
* Desired audience. Omitted means "keep the
|
|
50
|
+
* Desired audience. Omitted means "keep the share's current audience" —
|
|
43
51
|
* a plain toggle must never flip an org-members-only share back to
|
|
44
52
|
* public (or vice versa).
|
|
45
53
|
*/
|
|
@@ -48,7 +56,7 @@ export interface ShareAgentOptions {
|
|
|
48
56
|
* Rotate the share-link token: the server generates a fresh `?k=`
|
|
49
57
|
* secret and the current link (tokened or plain) stops working
|
|
50
58
|
* immediately. The token is server-owned status — never part of the
|
|
51
|
-
*
|
|
59
|
+
* spec this module merges.
|
|
52
60
|
*/
|
|
53
61
|
readonly resetLink?: boolean;
|
|
54
62
|
/** The app origin serving the hosted chat page and `embed.js`. */
|
|
@@ -63,8 +71,9 @@ export interface ShareAgentOptions {
|
|
|
63
71
|
|
|
64
72
|
/**
|
|
65
73
|
* Enable or disable sharing for the referenced agent and describe the
|
|
66
|
-
* outcome. Idempotent: when the
|
|
67
|
-
* write is issued (the link is still reported when sharing is
|
|
74
|
+
* outcome. Idempotent: when the canonical share is already in the desired
|
|
75
|
+
* state, no write is issued (the link is still reported when sharing is
|
|
76
|
+
* on). When the agent has never been shared, enabling creates the share.
|
|
68
77
|
*/
|
|
69
78
|
export async function shareAgent(
|
|
70
79
|
client: Stigmer,
|
|
@@ -84,7 +93,8 @@ export async function shareAgent(
|
|
|
84
93
|
}
|
|
85
94
|
|
|
86
95
|
const agent = await resolveAgentRef(client, ref, org);
|
|
87
|
-
|
|
96
|
+
let share = await resolveCanonicalShare(client, agent);
|
|
97
|
+
const current = share?.spec;
|
|
88
98
|
|
|
89
99
|
const currentAudience = audienceFromProto(current?.audience);
|
|
90
100
|
const targetAudience = options.audience ?? currentAudience;
|
|
@@ -103,83 +113,133 @@ export async function shareAgent(
|
|
|
103
113
|
);
|
|
104
114
|
}
|
|
105
115
|
|
|
116
|
+
// Never-shared + disable is a no-op, not a write: creating a share row
|
|
117
|
+
// just to mark it disabled would materialize a resource the owner never
|
|
118
|
+
// asked for. The one exception is an explicit --audience org, which is
|
|
119
|
+
// real configuration worth persisting as a paused share (mirroring the
|
|
120
|
+
// pre-promotion behavior of storing audience on a disabled block).
|
|
106
121
|
const alreadyInState =
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
share !== null
|
|
123
|
+
? (current?.enabled ?? false) === options.enabled &&
|
|
124
|
+
currentAudience === targetAudience
|
|
125
|
+
: !options.enabled && targetAudience !== "org";
|
|
109
126
|
|
|
110
127
|
if (!alreadyInState) {
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// messages, or an org-members-only audience.
|
|
114
|
-
await client.
|
|
115
|
-
|
|
116
|
-
resourceId: agent.metadata?.id ?? "",
|
|
117
|
-
sharing: preservingSharing(current, options.enabled, targetAudience),
|
|
118
|
-
}),
|
|
128
|
+
// Apply the COMPLETE spec with only the requested fields changed, so a
|
|
129
|
+
// CLI toggle can never wipe console-configured origins, visitor
|
|
130
|
+
// messages, credential bindings, or an org-members-only audience.
|
|
131
|
+
share = await client.agentShare.apply(
|
|
132
|
+
preservingShareInput(agent, share, options.enabled, targetAudience),
|
|
119
133
|
);
|
|
120
134
|
}
|
|
121
135
|
|
|
122
136
|
// The rotation is a separate targeted RPC (the token is server-owned
|
|
123
|
-
// status, not part of the
|
|
124
|
-
// fresh token, so the link printed below is the new one.
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
const rotated = await client.agent.rotateShareLink(
|
|
137
|
+
// status, not part of the spec apply merges). The returned share carries
|
|
138
|
+
// the fresh token, so the link printed below is the new one.
|
|
139
|
+
if (options.resetLink === true && share !== null) {
|
|
140
|
+
share = await client.agentShare.rotateShareLink(
|
|
128
141
|
create(RotateShareLinkInputSchema, {
|
|
129
|
-
resourceId:
|
|
142
|
+
resourceId: share.metadata?.id ?? "",
|
|
130
143
|
}),
|
|
131
144
|
);
|
|
132
|
-
linkToken = rotated.status?.shareLinkToken ?? "";
|
|
133
145
|
}
|
|
134
146
|
|
|
135
|
-
return describeOutcome(
|
|
147
|
+
return describeOutcome(
|
|
148
|
+
agent,
|
|
149
|
+
share,
|
|
150
|
+
options,
|
|
151
|
+
targetAudience,
|
|
152
|
+
alreadyInState && options.resetLink !== true,
|
|
153
|
+
);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* The agent's canonical share: the one whose slug equals the agent's (the
|
|
158
|
+
* server's default on create), else the first entry, else null when the
|
|
159
|
+
* agent has never been shared. Mirrors the web dialog's selection so both
|
|
160
|
+
* surfaces manage the same row (decision 011 D3: one canonical share in
|
|
161
|
+
* Phase A).
|
|
162
|
+
*/
|
|
163
|
+
async function resolveCanonicalShare(
|
|
164
|
+
client: Stigmer,
|
|
165
|
+
agent: Agent,
|
|
166
|
+
): Promise<AgentShare | null> {
|
|
167
|
+
const result = await client.agentShare.getByAgent(
|
|
168
|
+
create(GetAgentSharesByAgentRequestSchema, {
|
|
169
|
+
agentId: agent.metadata?.id ?? "",
|
|
170
|
+
}),
|
|
171
|
+
);
|
|
172
|
+
const agentSlug = agent.metadata?.slug ?? "";
|
|
173
|
+
return (
|
|
174
|
+
result.items.find((share) => share.metadata?.slug === agentSlug) ??
|
|
175
|
+
result.items[0] ??
|
|
176
|
+
null
|
|
177
|
+
);
|
|
136
178
|
}
|
|
137
179
|
|
|
138
|
-
// Unspecified means public by contract (
|
|
139
|
-
// anyone-with-link
|
|
140
|
-
function audienceFromProto(audience:
|
|
141
|
-
return audience ===
|
|
180
|
+
// Unspecified means public by contract (a share created without an explicit
|
|
181
|
+
// audience is an anyone-with-link share).
|
|
182
|
+
function audienceFromProto(audience: AgentShareAudience | undefined): ShareAudience {
|
|
183
|
+
return audience === AgentShareAudience.org ? "org" : "public";
|
|
142
184
|
}
|
|
143
185
|
|
|
144
|
-
// The full
|
|
145
|
-
//
|
|
146
|
-
// shared before).
|
|
147
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
186
|
+
// The full share input with the desired `enabled` and audience, preserving
|
|
187
|
+
// origins, messages, and credential bindings (empty defaults when the agent
|
|
188
|
+
// was never shared before). Identity comes from the existing share when one
|
|
189
|
+
// exists — a manifest-created share may carry a non-default slug, and
|
|
190
|
+
// applying with the agent's slug would create a SECOND share — and from the
|
|
191
|
+
// agent otherwise (the server's own D2 default, made explicit). The audience
|
|
192
|
+
// is written explicitly — never left unspecified — so a console-managed org
|
|
193
|
+
// share can't drift back to public.
|
|
194
|
+
function preservingShareInput(
|
|
195
|
+
agent: Agent,
|
|
196
|
+
share: AgentShare | null,
|
|
150
197
|
enabled: boolean,
|
|
151
198
|
audience: ShareAudience,
|
|
152
|
-
):
|
|
153
|
-
|
|
199
|
+
): AgentShareInput {
|
|
200
|
+
const agentOrg = agent.metadata?.org ?? "";
|
|
201
|
+
const agentSlug = agent.metadata?.slug ?? "";
|
|
202
|
+
const current = share?.spec;
|
|
203
|
+
return {
|
|
204
|
+
org: share?.metadata?.org || agentOrg,
|
|
205
|
+
slug: share?.metadata?.slug || agentSlug,
|
|
206
|
+
name: share?.metadata?.name || agent.metadata?.name || agentSlug,
|
|
207
|
+
agentRef: { org: agentOrg, slug: agentSlug },
|
|
154
208
|
enabled,
|
|
155
209
|
audience:
|
|
156
210
|
audience === "org"
|
|
157
|
-
?
|
|
158
|
-
:
|
|
211
|
+
? AgentShareAudience.org
|
|
212
|
+
: AgentShareAudience.public,
|
|
159
213
|
allowedOrigins: [...(current?.allowedOrigins ?? [])],
|
|
160
|
-
messages:
|
|
214
|
+
messages: {
|
|
161
215
|
rateLimited: current?.messages?.rateLimited ?? "",
|
|
162
216
|
unavailable: current?.messages?.unavailable ?? "",
|
|
163
217
|
conversationEnded: current?.messages?.conversationEnded ?? "",
|
|
164
|
-
}
|
|
165
|
-
|
|
218
|
+
},
|
|
219
|
+
environmentRefs: (current?.environmentRefs ?? []).map((envRef) => ({
|
|
220
|
+
org: envRef.org,
|
|
221
|
+
slug: envRef.slug,
|
|
222
|
+
})),
|
|
223
|
+
};
|
|
166
224
|
}
|
|
167
225
|
|
|
168
|
-
// Build the user-facing result. Org/slug come from the
|
|
169
|
-
//
|
|
170
|
-
//
|
|
171
|
-
//
|
|
172
|
-
//
|
|
226
|
+
// Build the user-facing result. Org/slug come from the SHARE's metadata —
|
|
227
|
+
// the identity in the hosted chat URL (a share may carry a non-default
|
|
228
|
+
// slug) — falling back to the resolved agent's on disable-when-never-shared.
|
|
229
|
+
// The link token comes from the share's status (post-rotation when
|
|
230
|
+
// --reset-link ran) and rides the printed URL and snippet — public audience
|
|
231
|
+
// only (org access is gated by membership).
|
|
173
232
|
function describeOutcome(
|
|
174
233
|
agent: Agent,
|
|
234
|
+
share: AgentShare | null,
|
|
175
235
|
options: ShareAgentOptions,
|
|
176
236
|
audience: ShareAudience,
|
|
177
|
-
linkToken: string,
|
|
178
237
|
alreadyInState: boolean,
|
|
179
238
|
): CommandResult {
|
|
180
|
-
const org = agent.metadata?.org ?? "";
|
|
181
|
-
const slug = agent.metadata?.slug ?? "";
|
|
239
|
+
const org = share?.metadata?.org || (agent.metadata?.org ?? "");
|
|
240
|
+
const slug = share?.metadata?.slug || (agent.metadata?.slug ?? "");
|
|
182
241
|
const name = agent.metadata?.name || slug;
|
|
242
|
+
const linkToken = share?.status?.shareLinkToken ?? "";
|
|
183
243
|
|
|
184
244
|
if (!options.enabled) {
|
|
185
245
|
const result = CommandResult.success(
|