pilotswarm-sdk 0.5.30 → 0.5.32
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/api/index.d.ts +40 -0
- package/api/index.js +8 -0
- package/api/src/session-authz.d.ts +76 -0
- package/api/src/session-authz.js +240 -0
- package/dist/agent-fqn.d.ts +72 -0
- package/dist/agent-fqn.d.ts.map +1 -0
- package/dist/agent-fqn.js +137 -0
- package/dist/agent-fqn.js.map +1 -0
- package/dist/agent-loader.d.ts.map +1 -1
- package/dist/agent-loader.js +22 -6
- package/dist/agent-loader.js.map +1 -1
- package/dist/agent-manager-tools.d.ts +135 -0
- package/dist/agent-manager-tools.d.ts.map +1 -0
- package/dist/agent-manager-tools.js +813 -0
- package/dist/agent-manager-tools.js.map +1 -0
- package/dist/agent-package-diff.d.ts +97 -0
- package/dist/agent-package-diff.d.ts.map +1 -0
- package/dist/agent-package-diff.js +203 -0
- package/dist/agent-package-diff.js.map +1 -0
- package/dist/agent-package-format.d.ts +2 -0
- package/dist/agent-package-format.d.ts.map +1 -1
- package/dist/agent-package-format.js +15 -0
- package/dist/agent-package-format.js.map +1 -1
- package/dist/agent-package-import-fetch.d.ts +68 -0
- package/dist/agent-package-import-fetch.d.ts.map +1 -0
- package/dist/agent-package-import-fetch.js +156 -0
- package/dist/agent-package-import-fetch.js.map +1 -0
- package/dist/agent-package-import-policy.d.ts +133 -0
- package/dist/agent-package-import-policy.d.ts.map +1 -0
- package/dist/agent-package-import-policy.js +347 -0
- package/dist/agent-package-import-policy.js.map +1 -0
- package/dist/agent-package-installer.d.ts.map +1 -1
- package/dist/agent-package-installer.js +30 -4
- package/dist/agent-package-installer.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +653 -1
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +103 -10
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +85 -10
- package/dist/cms.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/inspect-tools.d.ts +68 -8
- package/dist/inspect-tools.d.ts.map +1 -1
- package/dist/inspect-tools.js +282 -21
- package/dist/inspect-tools.js.map +1 -1
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +87 -0
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts +20 -1
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +25 -0
- package/dist/management-client.js.map +1 -1
- package/dist/session-manager.d.ts +61 -0
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +143 -1
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +9 -1
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +345 -12
- package/dist/session-proxy.js.map +1 -1
- package/dist/types.d.ts +31 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/web/web-management-client.d.ts +2 -0
- package/dist/web/web-management-client.d.ts.map +1 -1
- package/dist/web/web-management-client.js +6 -0
- package/dist/web/web-management-client.js.map +1 -1
- package/dist/worker.d.ts +5 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +28 -0
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
- package/plugins/mgmt/agents/pilotswarm.agent.md +2 -2
- package/plugins/mgmt/skills/cost-latency-analysis/SKILL.md +1 -1
- package/plugins/mgmt/skills/graph-debug/SKILL.md +4 -4
- package/plugins/mgmt/skills/orchestration-session-lifecycle/SKILL.md +1 -1
- package/plugins/mgmt/agents/agent-tuner.agent.md +0 -316
|
@@ -0,0 +1,813 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Agent Manager write bundle — package mutation, diffing and import.
|
|
3
|
+
*
|
|
4
|
+
* Kept out of `inspect-tools.ts` deliberately: that file is the READ surface
|
|
5
|
+
* and its grep-guard reasons about session-scoped reads. These tools are
|
|
6
|
+
* package-scoped and mutating, with a different authorization story
|
|
7
|
+
* (creator-or-admin on the package, not visibility on a session), and mixing
|
|
8
|
+
* the two would blur which rule applies where.
|
|
9
|
+
*
|
|
10
|
+
* **Authorization is not implemented here.** Every mutation calls the same
|
|
11
|
+
* `agent-package-service` / catalog functions the Web API calls, which enforce
|
|
12
|
+
* creator-or-admin inside the database with a row lock. That is the whole
|
|
13
|
+
* point of §4: one authz implementation serving portal, CLI, MCP and the
|
|
14
|
+
* manager, so a tool cannot become a side door.
|
|
15
|
+
*
|
|
16
|
+
* @module
|
|
17
|
+
*/
|
|
18
|
+
import { defineTool } from "@github/copilot-sdk";
|
|
19
|
+
import * as fs from "node:fs";
|
|
20
|
+
import * as os from "node:os";
|
|
21
|
+
import * as path from "node:path";
|
|
22
|
+
import { readAgentPackageTarGz } from "./agent-package-format.js";
|
|
23
|
+
import { fetchAgentPackageTarGz, publishAgentPackageDir } from "./agent-package-service.js";
|
|
24
|
+
import { diffPackageTrees, patchArtifacts, entriesToMap } from "./agent-package-diff.js";
|
|
25
|
+
import { guardedImportFetch } from "./agent-package-import-fetch.js";
|
|
26
|
+
import { loadImportPolicy } from "./agent-package-import-policy.js";
|
|
27
|
+
import { parseAgentFqn } from "./agent-fqn.js";
|
|
28
|
+
/**
|
|
29
|
+
* Staging lives in ONE session artifact rather than N.
|
|
30
|
+
*
|
|
31
|
+
* Durable (so it survives the session moving workers), atomic (a half-written
|
|
32
|
+
* edit is not a thing), and visible to a human in the portal's artifact list
|
|
33
|
+
* while they are deciding whether to approve it.
|
|
34
|
+
*/
|
|
35
|
+
export const STAGING_ARTIFACT = "agent-package-staging.json";
|
|
36
|
+
/**
|
|
37
|
+
* Agent ids that hold the manager bundle.
|
|
38
|
+
*
|
|
39
|
+
* Lives here rather than in inspect-tools so BOTH halves of a manager tool can
|
|
40
|
+
* gate on one list: the declaration (this bundle) and the per-turn handler
|
|
41
|
+
* (managed-session). Declaring a tool the model cannot see but whose handler is
|
|
42
|
+
* still registered is a latent capability, so the two must agree.
|
|
43
|
+
*
|
|
44
|
+
* Per §15 A10 a name must never GRANT: this selects which agents BEHAVE like
|
|
45
|
+
* managers, not what a manager may do. Every one of these tools re-checks
|
|
46
|
+
* authority against the live catalog (owner-or-admin, system sessions
|
|
47
|
+
* refused), so a package declaring this id gains nothing its owner did not
|
|
48
|
+
* already have.
|
|
49
|
+
*/
|
|
50
|
+
export const MANAGER_AGENT_IDS = new Set(["agent-tuner", "agent-manager"]);
|
|
51
|
+
/**
|
|
52
|
+
* May the calling principal act on `target`?
|
|
53
|
+
*
|
|
54
|
+
* The DECISION only — the caller does the IO and hands in what it read. Kept
|
|
55
|
+
* pure and exported so the rule is directly testable: it is the gate on
|
|
56
|
+
* messaging a session as its user and on completing/cancelling/deleting one,
|
|
57
|
+
* and a gate nobody can unit-test is a gate nobody can trust.
|
|
58
|
+
*
|
|
59
|
+
* Owner-or-admin, deliberately narrower than "can read". Visibility includes
|
|
60
|
+
* sessions shared WITH you, and being allowed to watch a run is not being
|
|
61
|
+
* allowed to type into it or end it.
|
|
62
|
+
*/
|
|
63
|
+
export function decideSessionControl(args) {
|
|
64
|
+
const { target, targetIdLabel, caller, callerIsAdmin, refuseSystem } = args;
|
|
65
|
+
if (!target)
|
|
66
|
+
return { ok: false, reason: `session ${targetIdLabel} not found` };
|
|
67
|
+
// Checked BEFORE ownership and before admin, so being an admin is not a
|
|
68
|
+
// way to reach one. System sessions hold the deployment's background
|
|
69
|
+
// machinery and the fleet recreates them anyway.
|
|
70
|
+
if (refuseSystem && target.isSystem) {
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
reason: `session ${targetIdLabel} is a system session — its lifecycle belongs to the deployment, not to any user or admin`,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
// Fail closed: an unidentifiable caller drives nothing.
|
|
77
|
+
if (!caller?.provider || !caller?.subject) {
|
|
78
|
+
return { ok: false, reason: "this session's owner could not be identified" };
|
|
79
|
+
}
|
|
80
|
+
if (target.owner?.provider === caller.provider && target.owner?.subject === caller.subject) {
|
|
81
|
+
return { ok: true };
|
|
82
|
+
}
|
|
83
|
+
if (callerIsAdmin)
|
|
84
|
+
return { ok: true };
|
|
85
|
+
return { ok: false, reason: `you neither own session ${targetIdLabel} nor hold the admin role` };
|
|
86
|
+
}
|
|
87
|
+
/** Does this agent identity hold the manager bundle? */
|
|
88
|
+
export function holdsManagerBundle(agentIdentity) {
|
|
89
|
+
return MANAGER_AGENT_IDS.has(String(agentIdentity ?? ""));
|
|
90
|
+
}
|
|
91
|
+
/** The changelog every package carries. Read before editing, appended on publish. */
|
|
92
|
+
export const CHANGELOG_PATH = "CHANGELOG.md";
|
|
93
|
+
/** Turn an FQN into a registry selector, or explain why it cannot be one. */
|
|
94
|
+
export function selectorFromReference(reference, viewer) {
|
|
95
|
+
const fqn = parseAgentFqn(reference);
|
|
96
|
+
switch (fqn.kind) {
|
|
97
|
+
case "bare":
|
|
98
|
+
// No selector = resolve own-then-shared, the same rule agent
|
|
99
|
+
// binding uses. "Edit X" therefore means the same X that "run X"
|
|
100
|
+
// would have used.
|
|
101
|
+
return { name: fqn.name, selector: null };
|
|
102
|
+
case "shared":
|
|
103
|
+
return { name: fqn.name, selector: { scope: "shared" }, semver: fqn.semver };
|
|
104
|
+
case "owner":
|
|
105
|
+
case "ambiguous":
|
|
106
|
+
// An owner FQN names a PERSON, and people are resolved to
|
|
107
|
+
// (provider, subject) once — never re-resolved later (§15 A8).
|
|
108
|
+
// Only the viewer's own reference is resolvable without a
|
|
109
|
+
// directory lookup, so that is all this accepts for now.
|
|
110
|
+
if (fqn.ownerRef && (fqn.ownerRef === viewer.subject || fqn.ownerRef === "me")) {
|
|
111
|
+
return {
|
|
112
|
+
name: fqn.name,
|
|
113
|
+
selector: { scope: "user", owner: { provider: viewer.provider, subject: viewer.subject } },
|
|
114
|
+
semver: fqn.semver,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
name: fqn.name,
|
|
119
|
+
selector: null,
|
|
120
|
+
error: `"${reference}" names another owner. Package operations act on your own copy or the shared one; `
|
|
121
|
+
+ `use __shared:${fqn.name} for the deployment copy.`,
|
|
122
|
+
};
|
|
123
|
+
default:
|
|
124
|
+
return { name: "", selector: null, error: fqn.reason ?? "unusable package reference" };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Resolve a reference, including one naming ANOTHER owner.
|
|
129
|
+
*
|
|
130
|
+
* `selectorFromReference` stays sync and directory-free, so it can only ever
|
|
131
|
+
* resolve the viewer's own copy. That is the right answer for a normal user —
|
|
132
|
+
* their manager reaches their own packages and the shared ones, nothing else.
|
|
133
|
+
*
|
|
134
|
+
* An administrator is a different case: their manager is meant to reach the
|
|
135
|
+
* whole fleet, and refusing `<owner>:<package>` left admins able to LIST
|
|
136
|
+
* another user's package (`cms_list_agent_packages` returns everything when
|
|
137
|
+
* `p_is_admin`) while being unable to open it. The database already accepts an
|
|
138
|
+
* owner selector and already gates on `p_is_admin`; only this layer said no.
|
|
139
|
+
*
|
|
140
|
+
* The owner is resolved through the users table — by subject, email, or
|
|
141
|
+
* display name — so an admin can name a person the way a person is named.
|
|
142
|
+
* Resolution is by lookup, never by trusting the string: a non-admin keeps the
|
|
143
|
+
* original refusal, and an unmatched name is an error rather than a fallback
|
|
144
|
+
* to the viewer's own copy.
|
|
145
|
+
*/
|
|
146
|
+
export async function resolveReference(reference, viewer, catalog) {
|
|
147
|
+
const base = selectorFromReference(reference, viewer);
|
|
148
|
+
if (!base.error)
|
|
149
|
+
return base;
|
|
150
|
+
const fqn = parseAgentFqn(reference);
|
|
151
|
+
const ownerRef = fqn.ownerRef?.trim();
|
|
152
|
+
// Only a cross-owner reference is rescuable, and only for the fleet-wide
|
|
153
|
+
// principals. Everything else keeps the message it already had.
|
|
154
|
+
if (!ownerRef || !(viewer.isAdmin || viewer.isSystemPrincipal))
|
|
155
|
+
return base;
|
|
156
|
+
let users;
|
|
157
|
+
try {
|
|
158
|
+
users = await catalog.listKnownUsers({ limit: 1000 });
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
return { name: fqn.name, selector: null, error: `could not resolve owner "${ownerRef}": ${err?.message || String(err)}` };
|
|
162
|
+
}
|
|
163
|
+
const needle = ownerRef.toLowerCase();
|
|
164
|
+
const match = users.find((u) => u.subject?.toLowerCase() === needle
|
|
165
|
+
|| u.email?.toLowerCase() === needle
|
|
166
|
+
|| u.displayName?.toLowerCase() === needle);
|
|
167
|
+
if (!match) {
|
|
168
|
+
return {
|
|
169
|
+
name: fqn.name,
|
|
170
|
+
selector: null,
|
|
171
|
+
error: `no user matches "${ownerRef}". Name the owner by subject, email, or display name.`,
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
name: fqn.name,
|
|
176
|
+
selector: { scope: "user", owner: { provider: match.provider, subject: match.subject } },
|
|
177
|
+
semver: fqn.semver,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
const principalOf = (viewer) => viewer.provider && viewer.subject ? { provider: viewer.provider, subject: viewer.subject } : null;
|
|
181
|
+
export function createAgentManagerTools(opts) {
|
|
182
|
+
const { catalog, artifactStore, resolveViewer } = opts;
|
|
183
|
+
let cachedPolicy = opts.importPolicy ?? null;
|
|
184
|
+
const policy = () => {
|
|
185
|
+
// Loaded lazily and cached: a malformed config throws, and throwing at
|
|
186
|
+
// worker construction would take the whole worker down for a feature
|
|
187
|
+
// nobody may be using.
|
|
188
|
+
if (!cachedPolicy)
|
|
189
|
+
cachedPolicy = loadImportPolicy({ configDir: process.cwd() });
|
|
190
|
+
return cachedPolicy;
|
|
191
|
+
};
|
|
192
|
+
/** Read a package's active (or named) version as tar entries. */
|
|
193
|
+
const entriesForVersion = async (name, selector, semver, viewer) => {
|
|
194
|
+
const detail = await catalog.getAgentPackage(name, principalOf(viewer), viewer.isAdmin, selector);
|
|
195
|
+
if (!detail)
|
|
196
|
+
return { error: `package "${name}" not found, or not visible to you.` };
|
|
197
|
+
const version = semver
|
|
198
|
+
? detail.versions.find((v) => v.semver === semver)
|
|
199
|
+
: detail.versions.find((v) => v.versionId === detail.activeVersionId) ?? detail.versions[0];
|
|
200
|
+
if (!version)
|
|
201
|
+
return { error: semver ? `${name}@${semver} is not a published version.` : `package "${name}" has no versions.` };
|
|
202
|
+
if (!artifactStore)
|
|
203
|
+
return { error: "no artifact store is configured on this worker." };
|
|
204
|
+
const targz = await fetchAgentPackageTarGz(artifactStore, version.artifactFilename, version.sha256);
|
|
205
|
+
return { entries: readAgentPackageTarGz(targz), version, detail };
|
|
206
|
+
};
|
|
207
|
+
const listAgentPackagesTool = defineTool("list_agent_packages", {
|
|
208
|
+
description: "List agent packages you can see: the shared ones plus your own. "
|
|
209
|
+
+ "`shadowed` marks a shared package your own copy is currently overriding.",
|
|
210
|
+
parameters: { type: "object", properties: {}, required: [] },
|
|
211
|
+
handler: async () => {
|
|
212
|
+
const viewer = await resolveViewer();
|
|
213
|
+
try {
|
|
214
|
+
const rows = await catalog.listAgentPackages(principalOf(viewer), viewer.isAdmin);
|
|
215
|
+
return {
|
|
216
|
+
packages: rows.map((p) => ({
|
|
217
|
+
name: p.name, scope: p.scope, enabled: p.enabled, shadowed: p.shadowed,
|
|
218
|
+
owner: p.owner ? `${p.owner.provider}:${p.owner.subject}` : null,
|
|
219
|
+
activeSemver: p.active?.semver ?? null,
|
|
220
|
+
sha256: p.active?.sha256 ?? null,
|
|
221
|
+
})),
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
catch (err) {
|
|
225
|
+
return { error: `list_agent_packages: ${err?.message || String(err)}` };
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
});
|
|
229
|
+
const readAgentPackageTool = defineTool("read_agent_package", {
|
|
230
|
+
description: "One package with its full version history. Accepts a bare name (your copy, else shared), "
|
|
231
|
+
+ "`__shared:<name>` to reach the deployment copy past your own, or `<owner>:<name>` "
|
|
232
|
+
+ "naming a person by subject, email, or display name. "
|
|
233
|
+
+ "An administrator reaches every user's packages — a bare name resolving someone else's "
|
|
234
|
+
+ "package is the intended fleet-wide reach, not a leak.",
|
|
235
|
+
parameters: {
|
|
236
|
+
type: "object",
|
|
237
|
+
properties: { package: { type: "string", description: "Package name or __shared:<name>." } },
|
|
238
|
+
required: ["package"],
|
|
239
|
+
},
|
|
240
|
+
handler: async (args) => {
|
|
241
|
+
const viewer = await resolveViewer();
|
|
242
|
+
const ref = await resolveReference(args.package, viewer, catalog);
|
|
243
|
+
if (ref.error)
|
|
244
|
+
return { error: `read_agent_package: ${ref.error}` };
|
|
245
|
+
try {
|
|
246
|
+
const detail = await catalog.getAgentPackage(ref.name, principalOf(viewer), viewer.isAdmin, ref.selector);
|
|
247
|
+
if (!detail)
|
|
248
|
+
return { error: `read_agent_package: "${ref.name}" not found, or not visible to you.` };
|
|
249
|
+
return {
|
|
250
|
+
name: detail.name, scope: detail.scope, enabled: detail.enabled,
|
|
251
|
+
owner: detail.owner ? `${detail.owner.provider}:${detail.owner.subject}` : null,
|
|
252
|
+
activeVersionId: detail.activeVersionId,
|
|
253
|
+
versions: detail.versions.map((v) => ({
|
|
254
|
+
semver: v.semver, sha256: v.sha256, sizeBytes: v.sizeBytes,
|
|
255
|
+
createdAt: v.createdAt?.toISOString?.() ?? null, createdBy: v.createdBy,
|
|
256
|
+
})),
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
catch (err) {
|
|
260
|
+
return { error: `read_agent_package: ${err?.message || String(err)}` };
|
|
261
|
+
}
|
|
262
|
+
},
|
|
263
|
+
});
|
|
264
|
+
const diffAgentVersionsTool = defineTool("diff_agent_versions", {
|
|
265
|
+
description: "Compare two package versions and return one unified diff per changed file. "
|
|
266
|
+
+ "Comparison happens AFTER canonical packing, so the packer's own file reshuffling never appears — "
|
|
267
|
+
+ "an empty diff therefore means genuinely nothing changed. "
|
|
268
|
+
+ "Use it to see what changed between versions, or how your copy differs from the shared one.",
|
|
269
|
+
parameters: {
|
|
270
|
+
type: "object",
|
|
271
|
+
properties: {
|
|
272
|
+
left: { type: "string", description: "Package reference, e.g. `my-agent` or `__shared:my-agent`." },
|
|
273
|
+
left_semver: { type: "string", description: "Version on the left. Defaults to active." },
|
|
274
|
+
right: { type: "string", description: "Package reference for the right side. Defaults to `left`." },
|
|
275
|
+
right_semver: { type: "string", description: "Version on the right. Defaults to active." },
|
|
276
|
+
},
|
|
277
|
+
required: ["left"],
|
|
278
|
+
},
|
|
279
|
+
handler: async (args) => {
|
|
280
|
+
const viewer = await resolveViewer();
|
|
281
|
+
const leftRef = await resolveReference(args.left, viewer, catalog);
|
|
282
|
+
if (leftRef.error)
|
|
283
|
+
return { error: `diff_agent_versions: ${leftRef.error}` };
|
|
284
|
+
const rightRef = await resolveReference(args.right ?? args.left, viewer, catalog);
|
|
285
|
+
if (rightRef.error)
|
|
286
|
+
return { error: `diff_agent_versions: ${rightRef.error}` };
|
|
287
|
+
try {
|
|
288
|
+
const left = await entriesForVersion(leftRef.name, leftRef.selector, args.left_semver ?? leftRef.semver, viewer);
|
|
289
|
+
if ("error" in left)
|
|
290
|
+
return { error: `diff_agent_versions (left): ${left.error}` };
|
|
291
|
+
const right = await entriesForVersion(rightRef.name, rightRef.selector, args.right_semver ?? rightRef.semver, viewer);
|
|
292
|
+
if ("error" in right)
|
|
293
|
+
return { error: `diff_agent_versions (right): ${right.error}` };
|
|
294
|
+
// Identity is a hash compare, not a diff — packages are
|
|
295
|
+
// content-addressed, so this is free.
|
|
296
|
+
if (left.version.sha256 === right.version.sha256) {
|
|
297
|
+
return { identical: true, note: "both sides are the same content (identical sha256).", patches: [] };
|
|
298
|
+
}
|
|
299
|
+
const diff = diffPackageTrees(left.entries, right.entries);
|
|
300
|
+
return {
|
|
301
|
+
identical: diff.identical,
|
|
302
|
+
left: `${leftRef.name}@${left.version.semver}`,
|
|
303
|
+
right: `${rightRef.name}@${right.version.semver}`,
|
|
304
|
+
truncated: diff.truncated ?? undefined,
|
|
305
|
+
patches: diff.patches.map((p) => ({ path: p.path, status: p.status, note: p.note, diff: p.diff })),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
return { error: `diff_agent_versions: ${err?.message || String(err)}` };
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
const proposeAgentPatchTool = defineTool("propose_agent_patch", {
|
|
314
|
+
description: "Render the change as ordered `.patch` artifacts on this session so a human can review the real diff "
|
|
315
|
+
+ "in the portal before anything is published. "
|
|
316
|
+
+ "With no `to_semver`, diffs your STAGED edit against the live version — this is the normal path: "
|
|
317
|
+
+ "stage, propose, show the human, wait for approval, then publish.",
|
|
318
|
+
parameters: {
|
|
319
|
+
type: "object",
|
|
320
|
+
properties: {
|
|
321
|
+
package: { type: "string" },
|
|
322
|
+
from_semver: { type: "string", description: "Base version. Defaults to active." },
|
|
323
|
+
to_semver: { type: "string", description: "Compare against this published version instead of your staged edit." },
|
|
324
|
+
},
|
|
325
|
+
required: ["package"],
|
|
326
|
+
},
|
|
327
|
+
handler: async (args) => {
|
|
328
|
+
const viewer = await resolveViewer();
|
|
329
|
+
if (!opts.sessionId)
|
|
330
|
+
return { error: "propose_agent_patch: no session to attach artifacts to." };
|
|
331
|
+
if (!artifactStore)
|
|
332
|
+
return { error: "propose_agent_patch: no artifact store is configured." };
|
|
333
|
+
const ref = await resolveReference(args.package, viewer, catalog);
|
|
334
|
+
if (ref.error)
|
|
335
|
+
return { error: `propose_agent_patch: ${ref.error}` };
|
|
336
|
+
try {
|
|
337
|
+
const from = await entriesForVersion(ref.name, ref.selector, args.from_semver, viewer);
|
|
338
|
+
const baseEntries = "error" in from ? [] : from.entries;
|
|
339
|
+
const baseSemver = "error" in from ? null : from.version.semver;
|
|
340
|
+
// A brand-new package has no base; that is an all-added diff,
|
|
341
|
+
// not an error.
|
|
342
|
+
if ("error" in from && args.to_semver)
|
|
343
|
+
return { error: `propose_agent_patch (base): ${from.error}` };
|
|
344
|
+
let toEntries;
|
|
345
|
+
let label;
|
|
346
|
+
if (args.to_semver) {
|
|
347
|
+
const to = await entriesForVersion(ref.name, ref.selector, args.to_semver, viewer);
|
|
348
|
+
if ("error" in to)
|
|
349
|
+
return { error: `propose_agent_patch (target): ${to.error}` };
|
|
350
|
+
toEntries = to.entries;
|
|
351
|
+
label = `${ref.name}@${args.to_semver}`;
|
|
352
|
+
}
|
|
353
|
+
else {
|
|
354
|
+
const staged = await loadStaging();
|
|
355
|
+
if (!staged || staged.package !== ref.name) {
|
|
356
|
+
return { error: `propose_agent_patch: nothing staged for "${ref.name}". Use stage_agent_package_edit first.` };
|
|
357
|
+
}
|
|
358
|
+
toEntries = Object.entries(staged.files).map(([p, content]) => ({ path: p, content: Buffer.from(content, "utf8") }));
|
|
359
|
+
label = "staged edit";
|
|
360
|
+
}
|
|
361
|
+
const diff = diffPackageTrees(baseEntries, toEntries);
|
|
362
|
+
if (diff.identical)
|
|
363
|
+
return { written: 0, note: "no differences — nothing to propose." };
|
|
364
|
+
const artifacts = patchArtifacts(diff, { baseSemver: baseSemver ?? "(new package)" });
|
|
365
|
+
for (const artifact of artifacts) {
|
|
366
|
+
await artifactStore.uploadArtifact(opts.sessionId, artifact.filename, Buffer.from(artifact.content, "utf8"), "text/x-diff");
|
|
367
|
+
}
|
|
368
|
+
const changelogTouched = diff.patches.some((p) => p.path === CHANGELOG_PATH);
|
|
369
|
+
return {
|
|
370
|
+
written: artifacts.length,
|
|
371
|
+
base: baseSemver ?? "(new package)",
|
|
372
|
+
proposed: label,
|
|
373
|
+
files: artifacts.map((a) => a.filename),
|
|
374
|
+
changelogUpdated: changelogTouched,
|
|
375
|
+
...(changelogTouched ? {} : { warning: `This change does not touch ${CHANGELOG_PATH}. Add an entry before publishing.` }),
|
|
376
|
+
next: "Show the human what changed and why, point them at these artifacts, and WAIT for an explicit approval before publish_agent_package.",
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
catch (err) {
|
|
380
|
+
return { error: `propose_agent_patch: ${err?.message || String(err)}` };
|
|
381
|
+
}
|
|
382
|
+
},
|
|
383
|
+
});
|
|
384
|
+
// ── Authoring ────────────────────────────────────────────────────────
|
|
385
|
+
/** Load the session's staged edit, or null when nothing is staged. */
|
|
386
|
+
const loadStaging = async () => {
|
|
387
|
+
if (!artifactStore || !opts.sessionId)
|
|
388
|
+
return null;
|
|
389
|
+
try {
|
|
390
|
+
const text = await artifactStore.downloadArtifactText(opts.sessionId, STAGING_ARTIFACT);
|
|
391
|
+
const parsed = JSON.parse(text);
|
|
392
|
+
if (!parsed || typeof parsed !== "object" || typeof parsed.files !== "object")
|
|
393
|
+
return null;
|
|
394
|
+
return parsed;
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
};
|
|
400
|
+
const saveStaging = async (staged) => {
|
|
401
|
+
await artifactStore.uploadArtifact(opts.sessionId, STAGING_ARTIFACT, Buffer.from(JSON.stringify(staged, null, 2), "utf8"), "application/json");
|
|
402
|
+
};
|
|
403
|
+
const readAgentPackageFileTool = defineTool("read_agent_package_file", {
|
|
404
|
+
description: "Read ONE file out of a package version — an agent prompt, a skill, plugin.json, or CHANGELOG.md. "
|
|
405
|
+
+ "Read CHANGELOG.md before editing: it is where previous changes and their reasons are recorded.",
|
|
406
|
+
parameters: {
|
|
407
|
+
type: "object",
|
|
408
|
+
properties: {
|
|
409
|
+
package: { type: "string" },
|
|
410
|
+
path: { type: "string", description: "Path inside the package, e.g. `agents/triager.agent.md`." },
|
|
411
|
+
semver: { type: "string", description: "Defaults to the active version." },
|
|
412
|
+
},
|
|
413
|
+
required: ["package", "path"],
|
|
414
|
+
},
|
|
415
|
+
handler: async (args) => {
|
|
416
|
+
const viewer = await resolveViewer();
|
|
417
|
+
const ref = await resolveReference(args.package, viewer, catalog);
|
|
418
|
+
if (ref.error)
|
|
419
|
+
return { error: `read_agent_package_file: ${ref.error}` };
|
|
420
|
+
try {
|
|
421
|
+
const got = await entriesForVersion(ref.name, ref.selector, args.semver ?? ref.semver, viewer);
|
|
422
|
+
if ("error" in got)
|
|
423
|
+
return { error: `read_agent_package_file: ${got.error}` };
|
|
424
|
+
const map = entriesToMap(got.entries);
|
|
425
|
+
const buf = map.get(args.path.replace(/^\.\//, ""));
|
|
426
|
+
if (!buf) {
|
|
427
|
+
return {
|
|
428
|
+
error: `read_agent_package_file: "${args.path}" is not in ${ref.name}@${got.version.semver}.`,
|
|
429
|
+
available: [...map.keys()].slice(0, 50),
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
if (buf.includes(0))
|
|
433
|
+
return { error: `read_agent_package_file: "${args.path}" is binary.` };
|
|
434
|
+
return { path: args.path, semver: got.version.semver, content: buf.toString("utf8") };
|
|
435
|
+
}
|
|
436
|
+
catch (err) {
|
|
437
|
+
return { error: `read_agent_package_file: ${err?.message || String(err)}` };
|
|
438
|
+
}
|
|
439
|
+
},
|
|
440
|
+
});
|
|
441
|
+
const stageAgentPackageEditTool = defineTool("stage_agent_package_edit", {
|
|
442
|
+
description: "Stage package content for a new version. Call with `from_package` to seed staging from a real "
|
|
443
|
+
+ "published version (the bytes actually running) and edit from there; call without it to author a "
|
|
444
|
+
+ "brand-new package from an empty staging area. Repeat calls accumulate. "
|
|
445
|
+
+ "Staging is written to a session artifact, so a human can see it while deciding. "
|
|
446
|
+
+ "Nothing is published until you call publish_agent_package.",
|
|
447
|
+
parameters: {
|
|
448
|
+
type: "object",
|
|
449
|
+
properties: {
|
|
450
|
+
package: { type: "string", description: "Package name this edit is for." },
|
|
451
|
+
from_package: { type: "string", description: "Seed staging from this package's content. Omit for a brand-new package." },
|
|
452
|
+
from_semver: { type: "string", description: "Version to seed from. Defaults to active." },
|
|
453
|
+
files: {
|
|
454
|
+
type: "object",
|
|
455
|
+
description: "Map of path → full file content, e.g. {\"agents/x.agent.md\": \"---\\nname: x\\n---\\n...\"}. Replaces the whole file.",
|
|
456
|
+
additionalProperties: { type: "string" },
|
|
457
|
+
},
|
|
458
|
+
remove: { type: "array", items: { type: "string" }, description: "Paths to delete from the staged tree." },
|
|
459
|
+
reset: { type: "boolean", description: "Discard existing staging first." },
|
|
460
|
+
},
|
|
461
|
+
required: ["package"],
|
|
462
|
+
},
|
|
463
|
+
handler: async (args) => {
|
|
464
|
+
const viewer = await resolveViewer();
|
|
465
|
+
if (!artifactStore || !opts.sessionId)
|
|
466
|
+
return { error: "stage_agent_package_edit: no artifact store on this session." };
|
|
467
|
+
const ref = await resolveReference(args.package, viewer, catalog);
|
|
468
|
+
if (ref.error)
|
|
469
|
+
return { error: `stage_agent_package_edit: ${ref.error}` };
|
|
470
|
+
try {
|
|
471
|
+
let staged = args.reset ? null : await loadStaging();
|
|
472
|
+
if (!staged || staged.package !== ref.name) {
|
|
473
|
+
staged = { package: ref.name, fromSemver: null, files: {} };
|
|
474
|
+
}
|
|
475
|
+
if (args.from_package) {
|
|
476
|
+
const seedRef = await resolveReference(args.from_package, viewer, catalog);
|
|
477
|
+
if (seedRef.error)
|
|
478
|
+
return { error: `stage_agent_package_edit: ${seedRef.error}` };
|
|
479
|
+
const seed = await entriesForVersion(seedRef.name, seedRef.selector, args.from_semver ?? seedRef.semver, viewer);
|
|
480
|
+
if ("error" in seed)
|
|
481
|
+
return { error: `stage_agent_package_edit (seed): ${seed.error}` };
|
|
482
|
+
// Seed from the REAL bytes. Anything the agent cannot read
|
|
483
|
+
// as text is skipped rather than silently corrupted.
|
|
484
|
+
for (const [p, buf] of entriesToMap(seed.entries)) {
|
|
485
|
+
if (!buf.includes(0))
|
|
486
|
+
staged.files[p] = buf.toString("utf8");
|
|
487
|
+
}
|
|
488
|
+
staged.fromSemver = seed.version.semver;
|
|
489
|
+
// Seeded from someone else's copy (only an admin can reach
|
|
490
|
+
// one) — remember whose, so the publish lands there.
|
|
491
|
+
const seedOwner = seedRef.selector?.owner;
|
|
492
|
+
staged.targetOwner = seedOwner && seedOwner.subject !== viewer.subject
|
|
493
|
+
? { provider: seedOwner.provider, subject: seedOwner.subject }
|
|
494
|
+
: null;
|
|
495
|
+
}
|
|
496
|
+
for (const [p, content] of Object.entries(args.files ?? {})) {
|
|
497
|
+
staged.files[p.replace(/^\.\//, "")] = String(content);
|
|
498
|
+
}
|
|
499
|
+
for (const p of args.remove ?? [])
|
|
500
|
+
delete staged.files[p.replace(/^\.\//, "")];
|
|
501
|
+
await saveStaging(staged);
|
|
502
|
+
return {
|
|
503
|
+
package: staged.package,
|
|
504
|
+
seededFrom: staged.fromSemver,
|
|
505
|
+
files: Object.keys(staged.files).sort(),
|
|
506
|
+
hasChangelog: Boolean(staged.files[CHANGELOG_PATH]),
|
|
507
|
+
note: staged.files[CHANGELOG_PATH]
|
|
508
|
+
? undefined
|
|
509
|
+
: `No ${CHANGELOG_PATH} staged yet — add one describing this change, signed as Agent Manager.`,
|
|
510
|
+
};
|
|
511
|
+
}
|
|
512
|
+
catch (err) {
|
|
513
|
+
return { error: `stage_agent_package_edit: ${err?.message || String(err)}` };
|
|
514
|
+
}
|
|
515
|
+
},
|
|
516
|
+
});
|
|
517
|
+
const publishAgentPackageTool = defineTool("publish_agent_package", {
|
|
518
|
+
description: "Publish the staged edit as a new version. ONLY call this after you have shown the human the diff "
|
|
519
|
+
+ "from propose_agent_patch and they have explicitly approved it. "
|
|
520
|
+
+ "Requires a CHANGELOG.md entry for this version. Published versions are immutable — bump the semver.",
|
|
521
|
+
parameters: {
|
|
522
|
+
type: "object",
|
|
523
|
+
properties: {
|
|
524
|
+
package: { type: "string" },
|
|
525
|
+
semver: { type: "string", description: "New version, e.g. 1.3.0. Must be greater than the active one." },
|
|
526
|
+
scope: { type: "string", enum: ["user", "shared"], description: "Defaults to user (your own copy)." },
|
|
527
|
+
approved_by: { type: "string", description: "Who approved this publish. Recorded in provenance." },
|
|
528
|
+
},
|
|
529
|
+
required: ["package", "semver", "approved_by"],
|
|
530
|
+
},
|
|
531
|
+
handler: async (args) => {
|
|
532
|
+
const viewer = await resolveViewer();
|
|
533
|
+
if (!artifactStore || !opts.sessionId)
|
|
534
|
+
return { error: "publish_agent_package: no artifact store on this session." };
|
|
535
|
+
const staged = await loadStaging();
|
|
536
|
+
if (!staged)
|
|
537
|
+
return { error: "publish_agent_package: nothing staged. Use stage_agent_package_edit first." };
|
|
538
|
+
if (Object.keys(staged.files).length === 0)
|
|
539
|
+
return { error: "publish_agent_package: the staged tree is empty." };
|
|
540
|
+
// The changelog is part of the package, and this version has to be
|
|
541
|
+
// in it. Refusing here is what keeps the history from drifting
|
|
542
|
+
// away from what was actually shipped.
|
|
543
|
+
const changelog = staged.files[CHANGELOG_PATH];
|
|
544
|
+
if (!changelog) {
|
|
545
|
+
return { error: `publish_agent_package: no ${CHANGELOG_PATH} staged. Add one with an entry for ${args.semver}, signed as Agent Manager.` };
|
|
546
|
+
}
|
|
547
|
+
if (!changelog.includes(args.semver)) {
|
|
548
|
+
return { error: `publish_agent_package: ${CHANGELOG_PATH} has no entry for ${args.semver}. Add one before publishing.` };
|
|
549
|
+
}
|
|
550
|
+
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ps-agent-publish-"));
|
|
551
|
+
try {
|
|
552
|
+
for (const [rel, content] of Object.entries(staged.files)) {
|
|
553
|
+
// Containment check: a staged path is model-authored, and
|
|
554
|
+
// `..` in it would write outside the staging root.
|
|
555
|
+
const target = path.resolve(dir, rel);
|
|
556
|
+
if (!target.startsWith(path.resolve(dir) + path.sep)) {
|
|
557
|
+
return { error: `publish_agent_package: staged path "${rel}" escapes the package root.` };
|
|
558
|
+
}
|
|
559
|
+
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
560
|
+
fs.writeFileSync(target, content, "utf8");
|
|
561
|
+
}
|
|
562
|
+
// Publish to the package's real owner when this edit was
|
|
563
|
+
// seeded from someone else's copy. Only the fleet-wide
|
|
564
|
+
// principals can have staged such a seed in the first place,
|
|
565
|
+
// and the database re-checks creator-or-admin under a row
|
|
566
|
+
// lock, so this widens nothing on its own.
|
|
567
|
+
const target = staged.targetOwner;
|
|
568
|
+
const owner = target && (viewer.isAdmin || viewer.isSystemPrincipal)
|
|
569
|
+
? { provider: target.provider, subject: target.subject }
|
|
570
|
+
: principalOf(viewer);
|
|
571
|
+
const outcome = await publishAgentPackageDir({ catalog, artifactStore }, {
|
|
572
|
+
dir,
|
|
573
|
+
scope: args.scope === "shared" ? "shared" : "user",
|
|
574
|
+
owner,
|
|
575
|
+
// Provenance: a reader must be able to tell an
|
|
576
|
+
// agent-authored version from a human `agents push`.
|
|
577
|
+
createdBy: `agent-manager (approved by ${args.approved_by})`,
|
|
578
|
+
isAdmin: viewer.isAdmin,
|
|
579
|
+
name: staged.package,
|
|
580
|
+
semver: args.semver,
|
|
581
|
+
});
|
|
582
|
+
await artifactStore.deleteArtifact(opts.sessionId, STAGING_ARTIFACT).catch(() => { });
|
|
583
|
+
return {
|
|
584
|
+
status: outcome?.status ?? "published",
|
|
585
|
+
package: staged.package,
|
|
586
|
+
semver: args.semver,
|
|
587
|
+
scope: args.scope === "shared" ? "shared" : "user",
|
|
588
|
+
approvedBy: args.approved_by,
|
|
589
|
+
note: "Published. The fleet converges on the next registry poll — verify before regenerating anything onto it.",
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
catch (err) {
|
|
593
|
+
return { error: `publish_agent_package: ${err?.message || String(err)}` };
|
|
594
|
+
}
|
|
595
|
+
finally {
|
|
596
|
+
fs.rmSync(dir, { recursive: true, force: true });
|
|
597
|
+
}
|
|
598
|
+
},
|
|
599
|
+
});
|
|
600
|
+
const setEnabledTool = defineTool("set_agent_package_enabled", {
|
|
601
|
+
description: "Enable or disable a package copy. Disabling YOUR copy of a shared package is the recovery path: "
|
|
602
|
+
+ "resolution falls back to the shared one. Creator-or-admin, enforced server-side.",
|
|
603
|
+
parameters: {
|
|
604
|
+
type: "object",
|
|
605
|
+
properties: { package: { type: "string" }, enabled: { type: "boolean" } },
|
|
606
|
+
required: ["package", "enabled"],
|
|
607
|
+
},
|
|
608
|
+
handler: async (args) => {
|
|
609
|
+
const viewer = await resolveViewer();
|
|
610
|
+
const ref = await resolveReference(args.package, viewer, catalog);
|
|
611
|
+
if (ref.error)
|
|
612
|
+
return { error: `set_agent_package_enabled: ${ref.error}` };
|
|
613
|
+
try {
|
|
614
|
+
await catalog.setAgentPackageEnabled(ref.name, args.enabled, principalOf(viewer), viewer.isAdmin, ref.selector);
|
|
615
|
+
return { ok: true, package: ref.name, enabled: args.enabled };
|
|
616
|
+
}
|
|
617
|
+
catch (err) {
|
|
618
|
+
return { error: `set_agent_package_enabled: ${err?.message || String(err)}` };
|
|
619
|
+
}
|
|
620
|
+
},
|
|
621
|
+
});
|
|
622
|
+
const pinVersionTool = defineTool("pin_agent_package_version", {
|
|
623
|
+
description: "Pin a package's active version — the rollback control. The fleet converges on the next registry poll. "
|
|
624
|
+
+ "Creator-or-admin, enforced server-side.",
|
|
625
|
+
parameters: {
|
|
626
|
+
type: "object",
|
|
627
|
+
properties: { package: { type: "string" }, semver: { type: "string" } },
|
|
628
|
+
required: ["package", "semver"],
|
|
629
|
+
},
|
|
630
|
+
handler: async (args) => {
|
|
631
|
+
const viewer = await resolveViewer();
|
|
632
|
+
const ref = await resolveReference(args.package, viewer, catalog);
|
|
633
|
+
if (ref.error)
|
|
634
|
+
return { error: `pin_agent_package_version: ${ref.error}` };
|
|
635
|
+
try {
|
|
636
|
+
await catalog.pinAgentPackageVersion(ref.name, args.semver, principalOf(viewer), viewer.isAdmin, ref.selector);
|
|
637
|
+
return { ok: true, package: ref.name, pinned: args.semver };
|
|
638
|
+
}
|
|
639
|
+
catch (err) {
|
|
640
|
+
return { error: `pin_agent_package_version: ${err?.message || String(err)}` };
|
|
641
|
+
}
|
|
642
|
+
},
|
|
643
|
+
});
|
|
644
|
+
const importAgentPackageTool = defineTool("import_agent_package", {
|
|
645
|
+
description: "Fetch a package from an ALLOWLISTED origin and report what importing it would change. "
|
|
646
|
+
+ "Only origins the deployment has named can be reached; anything else is refused without a request being made. "
|
|
647
|
+
+ "`dry_run` (the default) publishes nothing — it answers 'is there anything new?'.",
|
|
648
|
+
parameters: {
|
|
649
|
+
type: "object",
|
|
650
|
+
properties: {
|
|
651
|
+
url: { type: "string", description: "https URL of a packed agent package." },
|
|
652
|
+
compare_to: { type: "string", description: "Package name to diff against. Defaults to none." },
|
|
653
|
+
dry_run: { type: "boolean", description: "Default true. When true, nothing is written." },
|
|
654
|
+
},
|
|
655
|
+
required: ["url"],
|
|
656
|
+
},
|
|
657
|
+
handler: async (args) => {
|
|
658
|
+
const viewer = await resolveViewer();
|
|
659
|
+
const dryRun = args.dry_run !== false;
|
|
660
|
+
try {
|
|
661
|
+
const fetched = await guardedImportFetch(args.url, policy());
|
|
662
|
+
const entries = readAgentPackageTarGz(fetched.bytes);
|
|
663
|
+
let comparison = undefined;
|
|
664
|
+
if (args.compare_to) {
|
|
665
|
+
const ref = await resolveReference(args.compare_to, viewer, catalog);
|
|
666
|
+
if (ref.error)
|
|
667
|
+
return { error: `import_agent_package: ${ref.error}` };
|
|
668
|
+
const current = await entriesForVersion(ref.name, ref.selector, undefined, viewer);
|
|
669
|
+
if (!("error" in current)) {
|
|
670
|
+
const diff = diffPackageTrees(current.entries, entries);
|
|
671
|
+
comparison = {
|
|
672
|
+
identical: diff.identical,
|
|
673
|
+
changedFiles: diff.patches.length,
|
|
674
|
+
patches: diff.patches.map((p) => ({ path: p.path, status: p.status, diff: p.diff })),
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
return {
|
|
679
|
+
fetched: { url: fetched.finalUrl, hops: fetched.hops, bytes: fetched.bytes.length, files: entries.length },
|
|
680
|
+
dryRun,
|
|
681
|
+
...(comparison ? { comparison } : {}),
|
|
682
|
+
...(dryRun ? { note: "dry run — nothing was published." } : {}),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
catch (err) {
|
|
686
|
+
// A denied import is a security signal: it is what a prompt
|
|
687
|
+
// injection looks like from the outside. The reason names the
|
|
688
|
+
// URL and how to extend the list, and NEVER carries a body.
|
|
689
|
+
return { error: `import_agent_package: ${err?.message || String(err)}` };
|
|
690
|
+
}
|
|
691
|
+
},
|
|
692
|
+
});
|
|
693
|
+
/**
|
|
694
|
+
* The DECLARATION half of create_agent_session.
|
|
695
|
+
*
|
|
696
|
+
* The real handler needs the inline SDK client, which only exists inside
|
|
697
|
+
* the turn runner, so it is swapped in per turn from the control bridge —
|
|
698
|
+
* the same split `spawn_agent`, `wait` and `ask_user` already use. The
|
|
699
|
+
* declaration lives HERE rather than with the system tools because this
|
|
700
|
+
* bundle is identity-gated to manager agents: putting it in the system set
|
|
701
|
+
* would hand every agent in the deployment the ability to create top-level
|
|
702
|
+
* sessions, which is not a capability the design grants them.
|
|
703
|
+
*
|
|
704
|
+
* If this stub ever runs, the wiring is missing — say so rather than
|
|
705
|
+
* silently reporting success for a session that was never created.
|
|
706
|
+
*/
|
|
707
|
+
const createAgentSessionTool = defineTool("create_agent_session", {
|
|
708
|
+
description: "Create a TOP-LEVEL session running an agent — the way a user actually runs it, "
|
|
709
|
+
+ "with no parent above it. Use this to VERIFY a package you just published: spawn_agent "
|
|
710
|
+
+ "would only make a child of you, which is not the same thing. "
|
|
711
|
+
+ "The session is owned by this session's owner and does NOT report back to you — "
|
|
712
|
+
+ "watch it with read_session_info / read_agent_events on the returned id. "
|
|
713
|
+
+ "Publishes converge on the next registry poll (<30s), so poll read_agent_package until "
|
|
714
|
+
+ "the active version is the one you published BEFORE creating the session, or you will be "
|
|
715
|
+
+ "testing the old definition and the edit will look like it did nothing.",
|
|
716
|
+
parameters: {
|
|
717
|
+
type: "object",
|
|
718
|
+
properties: {
|
|
719
|
+
agent_name: { type: "string", description: "Agent to run, e.g. `triager` or `__shared:triager`." },
|
|
720
|
+
prompt: { type: "string", description: "Opening message. Defaults to the agent's own initialPrompt." },
|
|
721
|
+
title: { type: "string", description: "Optional session title." },
|
|
722
|
+
model: { type: "string", description: "Optional exact provider:model from list_available_models." },
|
|
723
|
+
reasoning_effort: {
|
|
724
|
+
type: "string",
|
|
725
|
+
enum: ["none", "minimal", "low", "medium", "high", "xhigh", "max"],
|
|
726
|
+
description: "Optional reasoning effort for the new session.",
|
|
727
|
+
},
|
|
728
|
+
test_of: { type: "string", description: "Tag the session as a verification run, e.g. `triager@1.2.0`, so the sweeper can reap it." },
|
|
729
|
+
key: { type: "string", description: "Idempotency key. The same agent_name+key reuses the live session instead of creating another; change it to get a fresh one." },
|
|
730
|
+
},
|
|
731
|
+
required: ["agent_name"],
|
|
732
|
+
},
|
|
733
|
+
handler: async () => ({
|
|
734
|
+
error: "create_agent_session is not wired in this session (no control bridge). "
|
|
735
|
+
+ "No session was created.",
|
|
736
|
+
}),
|
|
737
|
+
});
|
|
738
|
+
/**
|
|
739
|
+
* DECLARATION half of message_agent_session (real handler per turn, as
|
|
740
|
+
* above).
|
|
741
|
+
*
|
|
742
|
+
* Drives a session as its USER — the other half of the test loop, since a
|
|
743
|
+
* verification run you cannot talk to only tells you the agent boots.
|
|
744
|
+
* Authorization is owner-or-admin and is enforced in the bridge against
|
|
745
|
+
* the live catalog, never here: this schema is only what the model sees.
|
|
746
|
+
*/
|
|
747
|
+
const messageAgentSessionTool = defineTool("message_agent_session", {
|
|
748
|
+
description: "Send a message to a session AS ITS USER — it lands in that session's chat as a user turn. "
|
|
749
|
+
+ "Use it to drive a session you created with create_agent_session, or any session you own. "
|
|
750
|
+
+ "Allowed only if you OWN the target session or hold the admin role; otherwise it refuses and sends nothing. "
|
|
751
|
+
+ "This is not `send_session_message` (an auditable cross-session request) and not `message_agent` "
|
|
752
|
+
+ "(your own children only). The target runs on its own schedule — poll read_session_info / "
|
|
753
|
+
+ "read_agent_events for what it did rather than assuming a reply.",
|
|
754
|
+
parameters: {
|
|
755
|
+
type: "object",
|
|
756
|
+
properties: {
|
|
757
|
+
session_id: { type: "string", description: "Target session id." },
|
|
758
|
+
message: { type: "string", description: "The message, exactly as a user would type it." },
|
|
759
|
+
},
|
|
760
|
+
required: ["session_id", "message"],
|
|
761
|
+
},
|
|
762
|
+
handler: async () => ({
|
|
763
|
+
error: "message_agent_session is not wired in this session (no control bridge). "
|
|
764
|
+
+ "Nothing was sent.",
|
|
765
|
+
}),
|
|
766
|
+
});
|
|
767
|
+
/**
|
|
768
|
+
* DECLARATION half of manage_agent_session (real handler per turn).
|
|
769
|
+
*
|
|
770
|
+
* The sub-agent lifecycle tools only reach the caller's own children, so
|
|
771
|
+
* a manager could create a top-level test session and then had no way to
|
|
772
|
+
* clean it up. Authorization is enforced in the bridge against the live
|
|
773
|
+
* catalog: owner-or-admin, and system sessions refused for everyone.
|
|
774
|
+
*/
|
|
775
|
+
const manageAgentSessionTool = defineTool("manage_agent_session", {
|
|
776
|
+
description: "Complete, cancel or delete a session — including a top-level one. "
|
|
777
|
+
+ "Use it to clean up test sessions you created with create_agent_session. "
|
|
778
|
+
+ "`complete` marks it finished, `cancel` stops it, `delete` removes it (cancelling descendants first). "
|
|
779
|
+
+ "Allowed only on a session you OWN, or on any session if you hold the admin role; "
|
|
780
|
+
+ "SYSTEM sessions are refused for everyone, admins included. "
|
|
781
|
+
+ "It refuses rather than partially acting, and the target settles on its own schedule — "
|
|
782
|
+
+ "poll read_session_info to confirm rather than assuming.",
|
|
783
|
+
parameters: {
|
|
784
|
+
type: "object",
|
|
785
|
+
properties: {
|
|
786
|
+
session_id: { type: "string", description: "Target session id." },
|
|
787
|
+
action: { type: "string", enum: ["complete", "cancel", "delete"], description: "What to do." },
|
|
788
|
+
reason: { type: "string", description: "Why — recorded on the session." },
|
|
789
|
+
},
|
|
790
|
+
required: ["session_id", "action"],
|
|
791
|
+
},
|
|
792
|
+
handler: async () => ({
|
|
793
|
+
error: "manage_agent_session is not wired in this session (no control bridge). "
|
|
794
|
+
+ "Nothing was changed.",
|
|
795
|
+
}),
|
|
796
|
+
});
|
|
797
|
+
return [
|
|
798
|
+
listAgentPackagesTool,
|
|
799
|
+
readAgentPackageTool,
|
|
800
|
+
readAgentPackageFileTool,
|
|
801
|
+
stageAgentPackageEditTool,
|
|
802
|
+
diffAgentVersionsTool,
|
|
803
|
+
proposeAgentPatchTool,
|
|
804
|
+
publishAgentPackageTool,
|
|
805
|
+
setEnabledTool,
|
|
806
|
+
pinVersionTool,
|
|
807
|
+
importAgentPackageTool,
|
|
808
|
+
createAgentSessionTool,
|
|
809
|
+
messageAgentSessionTool,
|
|
810
|
+
manageAgentSessionTool,
|
|
811
|
+
];
|
|
812
|
+
}
|
|
813
|
+
//# sourceMappingURL=agent-manager-tools.js.map
|