pilotswarm-sdk 0.5.38 → 0.5.39

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.
Files changed (37) hide show
  1. package/api/src/http-api-transport.js +26 -7
  2. package/api/src/protocol.js +16 -7
  3. package/dist/agent-manager-tools.d.ts.map +1 -1
  4. package/dist/agent-manager-tools.js +12 -2
  5. package/dist/agent-manager-tools.js.map +1 -1
  6. package/dist/agent-package-installer.d.ts +2 -0
  7. package/dist/agent-package-installer.d.ts.map +1 -1
  8. package/dist/agent-package-installer.js +1 -0
  9. package/dist/agent-package-installer.js.map +1 -1
  10. package/dist/agent-package-service.d.ts +1 -1
  11. package/dist/agent-package-service.d.ts.map +1 -1
  12. package/dist/agent-package-service.js +79 -11
  13. package/dist/agent-package-service.js.map +1 -1
  14. package/dist/cms-migrations.d.ts.map +1 -1
  15. package/dist/cms-migrations.js +59 -0
  16. package/dist/cms-migrations.js.map +1 -1
  17. package/dist/cms.d.ts +9 -0
  18. package/dist/cms.d.ts.map +1 -1
  19. package/dist/cms.js +10 -0
  20. package/dist/cms.js.map +1 -1
  21. package/dist/session-manager.d.ts +84 -7
  22. package/dist/session-manager.d.ts.map +1 -1
  23. package/dist/session-manager.js +137 -16
  24. package/dist/session-manager.js.map +1 -1
  25. package/dist/session-proxy.d.ts +41 -0
  26. package/dist/session-proxy.d.ts.map +1 -1
  27. package/dist/session-proxy.js +188 -159
  28. package/dist/session-proxy.js.map +1 -1
  29. package/dist/web/generated-op-methods.d.ts +34 -5
  30. package/dist/web/generated-op-methods.d.ts.map +1 -1
  31. package/dist/web/generated-op-methods.js +2 -0
  32. package/dist/web/generated-op-methods.js.map +1 -1
  33. package/dist/worker.d.ts +23 -0
  34. package/dist/worker.d.ts.map +1 -1
  35. package/dist/worker.js +189 -17
  36. package/dist/worker.js.map +1 -1
  37. package/package.json +3 -3
@@ -28,6 +28,140 @@ import { createHash, randomUUID } from "node:crypto";
28
28
  import { attemptStoreRecovery, runTurnCommit, runTurnPreamble } from "./session-lifecycle.js";
29
29
  import { supportsVersionedSnapshots, writeTurnSentinel } from "./snapshot-protocol.js";
30
30
  const SYSTEM_AGENT_IDS = new Set(["pilotswarm", "sweeper", "resourcemgr", "facts-manager"]);
31
+ /**
32
+ * THE agent-name resolver: FQN parsing, fuzzy matching, package privacy, and
33
+ * owner shadowing in one place.
34
+ *
35
+ * This used to exist twice — the resolveAgentConfig activity had the full
36
+ * rule set while the control bridge's inline copy had none of it, so
37
+ * `spawn_agent`/`create_agent_session` could bind another user's private
38
+ * agent, could not address `__shared:<name>`, and ignored the caller's own
39
+ * shadowing copy. A security rule with two implementations has one that is
40
+ * wrong; this is now the only one.
41
+ *
42
+ * `getCallerOwnerKey` is awaited lazily — only when a user-scope package
43
+ * agent is actually in play — and must resolve to the caller's owner key
44
+ * (`provider\u0001subject`) or null. Fail closed: null means no private
45
+ * agents match.
46
+ */
47
+ export async function resolveAgentDefinitionForCaller(opts) {
48
+ const agents = [
49
+ ...(opts.userAgents ?? []).map(a => ({ ...a, system: false, creatable: true })),
50
+ ...(opts.systemAgents ?? []).map(a => ({ ...a, creatable: false })),
51
+ ];
52
+ const normalize = (value) => (value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
53
+ // Parse the reference. `a:b` has meant `namespace:agent` since long
54
+ // before FQNs existed, so the parser reports that shape as AMBIGUOUS
55
+ // and we keep the namespace reading first — an existing binding must
56
+ // not change meaning because per-user namespaces shipped.
57
+ const fqn = parseAgentFqn(opts.agentName);
58
+ let lookupNamespace;
59
+ let rawName = opts.agentName;
60
+ /** `__shared:` explicitly asks past the caller's own copy. */
61
+ let sharedOnly = false;
62
+ if (fqn.kind === "shared") {
63
+ rawName = fqn.name;
64
+ sharedOnly = true;
65
+ }
66
+ else if (fqn.kind === "bare") {
67
+ rawName = fqn.name;
68
+ }
69
+ else if (fqn.kind === "ambiguous" || fqn.kind === "owner") {
70
+ rawName = fqn.name;
71
+ lookupNamespace = fqn.namespaceRef ?? fqn.ownerRef;
72
+ }
73
+ else {
74
+ // Invalid (reserved prefix, bad semver, malformed). Fall back to
75
+ // the raw string so a legitimately odd agent name still resolves
76
+ // the way it always did; a reserved name simply will not match.
77
+ rawName = opts.agentName;
78
+ }
79
+ const lookup = normalize(rawName);
80
+ // Also try without trailing "agent" suffix for fuzzy matching
81
+ // (LLM often says "Sweeper agent" which normalizes to "sweeperagent", but id is "sweeper")
82
+ const lookupBase = lookup.replace(/agent$/, "");
83
+ let callerKeyResolved = false;
84
+ let callerKey = null;
85
+ const callerOwnerKeyOnce = async () => {
86
+ if (!callerKeyResolved) {
87
+ callerKeyResolved = true;
88
+ try {
89
+ callerKey = await opts.getCallerOwnerKey();
90
+ }
91
+ catch {
92
+ // Fail closed: an unresolvable caller gets no private agents.
93
+ callerKey = null;
94
+ }
95
+ }
96
+ return callerKey;
97
+ };
98
+ const ownerKeyOf = (owner) => owner?.provider && owner?.subject ? `${owner.provider}\u0001${owner.subject}` : null;
99
+ const visibleToCaller = async (agent) => {
100
+ const agentOwnerKey = ownerKeyOf(agent?.packageOwner);
101
+ if (agent?.packageScope !== "user" || !agentOwnerKey)
102
+ return true;
103
+ const key = await callerOwnerKeyOnce();
104
+ return key !== null && key === agentOwnerKey;
105
+ };
106
+ const candidatesFor = (a) => [a.name, a.id, a.title].map(normalize).filter(Boolean);
107
+ const nameMatches = (a) => {
108
+ const candidates = candidatesFor(a);
109
+ return candidates.includes(lookup) || (lookupBase && candidates.includes(lookupBase));
110
+ };
111
+ let matches = agents.filter(a => {
112
+ if (lookupNamespace && normalize(a.namespace) !== normalize(lookupNamespace))
113
+ return false;
114
+ return nameMatches(a);
115
+ });
116
+ // An ambiguous `a:b` that matched nothing as `namespace:agent` gets its
117
+ // second reading — `owner:package` — rather than just failing. Order,
118
+ // not guesswork: the old meaning always wins when it resolves.
119
+ if (matches.length === 0 && fqn.kind === "ambiguous") {
120
+ matches = agents.filter(nameMatches);
121
+ }
122
+ if (sharedOnly) {
123
+ // `__shared:x` means the deployment's copy, explicitly. This is the
124
+ // one thing bare-name precedence otherwise takes away: without it,
125
+ // a user with their own copy could never reach the shared one.
126
+ matches = matches.filter(a => a.packageScope !== "user");
127
+ }
128
+ else {
129
+ // SHADOWING, at the agent level: the caller's own copy comes first,
130
+ // then everything public. Same rule the registry resolver applies,
131
+ // so "run X" and "show me X" cannot disagree about which X.
132
+ const key = matches.some(a => a.packageScope === "user") ? await callerOwnerKeyOnce() : null;
133
+ matches = [
134
+ ...matches.filter(a => a.packageScope === "user" && ownerKeyOf(a.packageOwner) === key && key !== null),
135
+ ...matches.filter(a => !(a.packageScope === "user" && ownerKeyOf(a.packageOwner) === key && key !== null)),
136
+ ];
137
+ }
138
+ let agent = undefined;
139
+ for (const candidate of matches) {
140
+ if (await visibleToCaller(candidate)) {
141
+ agent = candidate;
142
+ break;
143
+ }
144
+ }
145
+ if (!agent)
146
+ return null;
147
+ return {
148
+ name: agent.name,
149
+ prompt: agent.prompt,
150
+ tools: agent.tools ?? undefined,
151
+ initialPrompt: agent.initialPrompt ?? undefined,
152
+ title: agent.title ?? undefined,
153
+ system: agent.system ?? undefined,
154
+ id: agent.id ?? undefined,
155
+ parent: agent.parent ?? undefined,
156
+ splash: agent.splash ?? undefined,
157
+ splashMobile: agent.splashMobile ?? undefined,
158
+ namespace: agent.namespace ?? undefined,
159
+ promptLayerKind: agent.promptLayerKind ?? undefined,
160
+ creatable: agent.creatable ?? !agent.system,
161
+ packageId: agent.packageId ?? undefined,
162
+ packageScope: agent.packageScope ?? undefined,
163
+ };
164
+ }
31
165
  /**
32
166
  * The session canvas: one reserved artifact per ROOT session, drawn by the
33
167
  * agent with draw_canvas and rendered live in the portal. The revision is
@@ -1056,28 +1190,24 @@ artifactStore) {
1056
1190
  return sanitized;
1057
1191
  };
1058
1192
  const isReadOnlyTuner = () => runConfig.agentIdentity === "agent-tuner";
1059
- const normalizeAgentLookup = (value) => (value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
1060
- const resolveAgentConfigInline = (agentName) => {
1061
- const agents = [
1062
- ...(userAgents ?? []).map(a => ({ ...a, system: false, creatable: true })),
1063
- ...(systemAgents ?? []).map(a => ({ ...a, creatable: false })),
1064
- ];
1065
- let lookupNamespace;
1066
- let rawName = agentName;
1067
- if (agentName.includes(":")) {
1068
- const parts = agentName.split(":");
1069
- lookupNamespace = parts[0];
1070
- rawName = parts.slice(1).join(":");
1071
- }
1072
- const lookup = normalizeAgentLookup(rawName);
1073
- const lookupBase = lookup.replace(/agent$/, "");
1074
- return agents.find(a => {
1075
- if (lookupNamespace && normalizeAgentLookup(a.namespace) !== normalizeAgentLookup(lookupNamespace))
1076
- return false;
1077
- const candidates = [a.name, a.id, a.title].map(normalizeAgentLookup).filter(Boolean);
1078
- return candidates.includes(lookup) || (lookupBase && candidates.includes(lookupBase));
1079
- }) ?? null;
1080
- };
1193
+ // The one shared resolver — FQN parsing (`__shared:` works here too),
1194
+ // package privacy, and the caller's own-copy shadowing included. The
1195
+ // previous inline copy had none of that: it could hand another user's
1196
+ // private agent to this session and could not address the shared copy
1197
+ // of a shadowed name.
1198
+ const resolveAgentConfigInline = (agentName) => resolveAgentDefinitionForCaller({
1199
+ agentName,
1200
+ userAgents,
1201
+ systemAgents,
1202
+ getCallerOwnerKey: async () => {
1203
+ const owner = catalog
1204
+ ? await resolveEffectiveSpawnOwner((id) => catalog.getSession(id), input.sessionId).catch(() => null)
1205
+ : null;
1206
+ return owner?.provider && owner?.subject
1207
+ ? `${owner.provider}\u0001${owner.subject}`
1208
+ : null;
1209
+ },
1210
+ });
1081
1211
  const loadDirectChildSessions = async () => {
1082
1212
  const sdkClient = await getInlineClient();
1083
1213
  const sessions = await sdkClient.listSessions();
@@ -1285,7 +1415,7 @@ artifactStore) {
1285
1415
  const agentName = String(args.agent_name || "").trim();
1286
1416
  if (!agentName)
1287
1417
  return "Error: agent_name is required.";
1288
- const agentDef = resolveAgentConfigInline(agentName);
1418
+ const agentDef = await resolveAgentConfigInline(agentName);
1289
1419
  if (!agentDef) {
1290
1420
  return `[SYSTEM: create_agent_session failed — agent "${agentName}" not found. Use list_agent_packages / ps_list_agents to see what is installed.]`;
1291
1421
  }
@@ -1410,7 +1540,7 @@ artifactStore) {
1410
1540
  : "app-agent");
1411
1541
  };
1412
1542
  if (resolvedAgentName) {
1413
- const agentDef = resolveAgentConfigInline(resolvedAgentName);
1543
+ const agentDef = await resolveAgentConfigInline(resolvedAgentName);
1414
1544
  if (!agentDef) {
1415
1545
  return `[SYSTEM: spawn_agent failed — agent "${resolvedAgentName}" not found. Use ps_list_agents to see available agents.]`;
1416
1546
  }
@@ -3149,143 +3279,42 @@ artifactStore) {
3149
3279
  // Resolves a loaded agent definition by name. User-creatable agents return
3150
3280
  // creatable=true. Worker-managed system agents return creatable=false so
3151
3281
  // callers can surface a clear error instead of spawning them.
3282
+ //
3283
+ // Matching, package privacy, and owner shadowing all live in
3284
+ // resolveAgentDefinitionForCaller — shared with the control bridge so
3285
+ // spawn_agent/create_agent_session and this activity can never disagree
3286
+ // about which agent a name means.
3152
3287
  runtime.registerActivity("resolveAgentConfig", async (_activityCtx, input) => {
3153
- const agents = [
3154
- ...(userAgents ?? []).map(a => ({ ...a, system: false, creatable: true })),
3155
- ...(systemAgents ?? []).map(a => ({ ...a, creatable: false })),
3156
- ];
3157
- const normalize = (value) => (value || "").toLowerCase().replace(/[^a-z0-9]+/g, "");
3158
- // Parse the reference. `a:b` has meant `namespace:agent` since long
3159
- // before FQNs existed, so the parser reports that shape as AMBIGUOUS
3160
- // and we keep the namespace reading first — an existing binding must
3161
- // not change meaning because per-user namespaces shipped.
3162
- const fqn = parseAgentFqn(input.agentName);
3163
- let lookupNamespace;
3164
- let rawName = input.agentName;
3165
- /** `__shared:` explicitly asks past the caller's own copy. */
3166
- let sharedOnly = false;
3167
- if (fqn.kind === "shared") {
3168
- rawName = fqn.name;
3169
- sharedOnly = true;
3170
- }
3171
- else if (fqn.kind === "bare") {
3172
- rawName = fqn.name;
3173
- }
3174
- else if (fqn.kind === "ambiguous" || fqn.kind === "owner") {
3175
- rawName = fqn.name;
3176
- lookupNamespace = fqn.namespaceRef ?? fqn.ownerRef;
3177
- }
3178
- else {
3179
- // Invalid (reserved prefix, bad semver, malformed). Fall back to
3180
- // the raw string so a legitimately odd agent name still resolves
3181
- // the way it always did; a reserved name simply will not match.
3182
- rawName = input.agentName;
3183
- }
3184
- const lookup = normalize(rawName);
3185
- // Also try without trailing "agent" suffix for fuzzy matching
3186
- // (LLM often says "Sweeper agent" which normalizes to "sweeperagent", but id is "sweeper")
3187
- const lookupBase = lookup.replace(/agent$/, "");
3188
- /**
3189
- * PACKAGE PRIVACY, enforced at RESOLUTION rather than at creation.
3190
- *
3191
- * Workers install every enabled package, including other users'
3192
- * user-scope ones, so `userAgents` is a fleet-wide list with no
3193
- * tenancy in it. The Web API filters package agents when a session is
3194
- * created (`_authorizePackageAgentCreate`), but `spawn_agent` reaches
3195
- * this activity directly and never passes through that filter — so a
3196
- * name match alone would hand Alice's private agent, prompt and all,
3197
- * to Bob's session.
3198
- *
3199
- * It has to be enforced HERE and not at spawn time: the return value
3200
- * carries the agent's full prompt, and merely returning it writes that
3201
- * prompt into the caller's orchestration history.
3202
- *
3203
- * Deployment-owned plugin agents carry no package owner and stay
3204
- * public, exactly as before.
3205
- */
3206
- let callerOwnerKey = null;
3207
- let callerResolved = false;
3208
- const ownerKeyOf = (owner) => owner?.provider && owner?.subject ? `${owner.provider}\u0001${owner.subject}` : null;
3209
- /** Resolve the caller's owner once per invocation, memoized and fail-closed. */
3210
- const callerOwnerKeyOnce = async () => {
3211
- if (!callerResolved) {
3212
- callerResolved = true;
3213
- try {
3214
- const row = input.callerSessionId
3215
- ? await catalog?.getSession(input.callerSessionId)
3216
- : null;
3217
- callerOwnerKey = ownerKeyOf(row?.owner);
3218
- }
3219
- catch {
3220
- // Fail closed: an unresolvable caller gets no private agents.
3221
- callerOwnerKey = null;
3222
- }
3223
- }
3224
- return callerOwnerKey;
3225
- };
3226
- const visibleToCaller = async (agent) => {
3227
- const agentOwnerKey = ownerKeyOf(agent?.packageOwner);
3228
- if (agent?.packageScope !== "user" || !agentOwnerKey)
3229
- return true;
3230
- const key = await callerOwnerKeyOnce();
3231
- return key !== null && key === agentOwnerKey;
3232
- };
3233
- const candidatesFor = (a) => [a.name, a.id, a.title].map(normalize).filter(Boolean);
3234
- const nameMatches = (a) => {
3235
- const candidates = candidatesFor(a);
3236
- return candidates.includes(lookup) || (lookupBase && candidates.includes(lookupBase));
3237
- };
3238
- let matches = agents.filter(a => {
3239
- if (lookupNamespace && normalize(a.namespace) !== normalize(lookupNamespace))
3240
- return false;
3241
- return nameMatches(a);
3288
+ return resolveAgentDefinitionForCaller({
3289
+ agentName: input.agentName,
3290
+ userAgents,
3291
+ systemAgents,
3292
+ // PACKAGE PRIVACY, enforced at RESOLUTION rather than at creation.
3293
+ //
3294
+ // Workers install every enabled package, including other users'
3295
+ // user-scope ones, so `userAgents` is a fleet-wide list with no
3296
+ // tenancy in it. The Web API filters package agents when a session
3297
+ // is created (`_authorizePackageAgentCreate`), but `spawn_agent`
3298
+ // reaches this activity directly and never passes through that
3299
+ // filter — so a name match alone would hand Alice's private agent,
3300
+ // prompt and all, to Bob's session.
3301
+ //
3302
+ // It has to be enforced HERE and not at spawn time: the return
3303
+ // value carries the agent's full prompt, and merely returning it
3304
+ // writes that prompt into the caller's orchestration history.
3305
+ //
3306
+ // Deployment-owned plugin agents carry no package owner and stay
3307
+ // public, exactly as before.
3308
+ getCallerOwnerKey: async () => {
3309
+ const row = input.callerSessionId
3310
+ ? await catalog?.getSession(input.callerSessionId)
3311
+ : null;
3312
+ const owner = row?.owner;
3313
+ return owner?.provider && owner?.subject
3314
+ ? `${owner.provider}\u0001${owner.subject}`
3315
+ : null;
3316
+ },
3242
3317
  });
3243
- // An ambiguous `a:b` that matched nothing as `namespace:agent` gets its
3244
- // second reading — `owner:package` — rather than just failing. Order,
3245
- // not guesswork: the old meaning always wins when it resolves.
3246
- if (matches.length === 0 && fqn.kind === "ambiguous") {
3247
- matches = agents.filter(nameMatches);
3248
- }
3249
- if (sharedOnly) {
3250
- // `__shared:x` means the deployment's copy, explicitly. This is the
3251
- // one thing bare-name precedence otherwise takes away: without it,
3252
- // a user with their own copy could never reach the shared one.
3253
- matches = matches.filter(a => a.packageScope !== "user");
3254
- }
3255
- else {
3256
- // SHADOWING, at the agent level: the caller's own copy comes first,
3257
- // then everything public. Same rule the registry resolver applies,
3258
- // so "run X" and "show me X" cannot disagree about which X.
3259
- const callerKey = await callerOwnerKeyOnce();
3260
- matches = [
3261
- ...matches.filter(a => a.packageScope === "user" && ownerKeyOf(a.packageOwner) === callerKey && callerKey !== null),
3262
- ...matches.filter(a => !(a.packageScope === "user" && ownerKeyOf(a.packageOwner) === callerKey && callerKey !== null)),
3263
- ];
3264
- }
3265
- let agent = undefined;
3266
- for (const candidate of matches) {
3267
- if (await visibleToCaller(candidate)) {
3268
- agent = candidate;
3269
- break;
3270
- }
3271
- }
3272
- if (!agent)
3273
- return null;
3274
- return {
3275
- name: agent.name,
3276
- prompt: agent.prompt,
3277
- tools: agent.tools ?? undefined,
3278
- initialPrompt: agent.initialPrompt ?? undefined,
3279
- title: agent.title ?? undefined,
3280
- system: agent.system ?? undefined,
3281
- id: agent.id ?? undefined,
3282
- parent: agent.parent ?? undefined,
3283
- splash: agent.splash ?? undefined,
3284
- splashMobile: agent.splashMobile ?? undefined,
3285
- namespace: agent.namespace ?? undefined,
3286
- promptLayerKind: agent.promptLayerKind ?? undefined,
3287
- creatable: agent.creatable ?? !agent.system,
3288
- };
3289
3318
  });
3290
3319
  // ── spawnChildSession ─────────────────────────────────────
3291
3320
  // Creates a child session via the PilotSwarmClient SDK.