pilotswarm-sdk 0.5.37 → 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.
- package/api/src/http-api-transport.js +26 -7
- package/api/src/protocol.js +16 -7
- package/dist/agent-manager-tools.d.ts.map +1 -1
- package/dist/agent-manager-tools.js +12 -2
- package/dist/agent-manager-tools.js.map +1 -1
- package/dist/agent-package-installer.d.ts +2 -0
- package/dist/agent-package-installer.d.ts.map +1 -1
- package/dist/agent-package-installer.js +1 -0
- package/dist/agent-package-installer.js.map +1 -1
- package/dist/agent-package-service.d.ts +1 -1
- package/dist/agent-package-service.d.ts.map +1 -1
- package/dist/agent-package-service.js +79 -11
- package/dist/agent-package-service.js.map +1 -1
- package/dist/cms-migrations.d.ts.map +1 -1
- package/dist/cms-migrations.js +117 -0
- package/dist/cms-migrations.js.map +1 -1
- package/dist/cms.d.ts +50 -0
- package/dist/cms.d.ts.map +1 -1
- package/dist/cms.js +65 -0
- 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 +4 -0
- package/dist/index.js.map +1 -1
- package/dist/managed-session.d.ts +4 -7
- package/dist/managed-session.d.ts.map +1 -1
- package/dist/managed-session.js +99 -18
- package/dist/managed-session.js.map +1 -1
- package/dist/management-client.d.ts.map +1 -1
- package/dist/management-client.js +8 -1
- package/dist/management-client.js.map +1 -1
- package/dist/model-providers.d.ts +142 -6
- package/dist/model-providers.d.ts.map +1 -1
- package/dist/model-providers.js +117 -1
- package/dist/model-providers.js.map +1 -1
- package/dist/session-manager.d.ts +97 -7
- package/dist/session-manager.d.ts.map +1 -1
- package/dist/session-manager.js +292 -31
- package/dist/session-manager.js.map +1 -1
- package/dist/session-proxy.d.ts +43 -0
- package/dist/session-proxy.d.ts.map +1 -1
- package/dist/session-proxy.js +296 -184
- package/dist/session-proxy.js.map +1 -1
- package/dist/web/generated-op-methods.d.ts +34 -5
- package/dist/web/generated-op-methods.d.ts.map +1 -1
- package/dist/web/generated-op-methods.js +2 -0
- package/dist/web/generated-op-methods.js.map +1 -1
- package/dist/worker.d.ts +23 -0
- package/dist/worker.d.ts.map +1 -1
- package/dist/worker.js +189 -17
- package/dist/worker.js.map +1 -1
- package/package.json +3 -3
package/dist/session-proxy.js
CHANGED
|
@@ -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
|
|
@@ -35,6 +169,24 @@ const SYSTEM_AGENT_IDS = new Set(["pilotswarm", "sweeper", "resourcemgr", "facts
|
|
|
35
169
|
* column — see docs/proposals/session-canvas.md.
|
|
36
170
|
*/
|
|
37
171
|
export const CANVAS_ARTIFACT_FILENAME = "canvas.html";
|
|
172
|
+
/** Slot 1 keeps the historical name; 2-5 are canvas2.html .. canvas5.html. */
|
|
173
|
+
export function canvasArtifactFilename(slot) {
|
|
174
|
+
return slot <= 1 ? CANVAS_ARTIFACT_FILENAME : `canvas${slot}.html`;
|
|
175
|
+
}
|
|
176
|
+
/** 1-5, defaulting absent to 1. Returns null for anything else. */
|
|
177
|
+
function normalizeCanvasSlot(value) {
|
|
178
|
+
if (value === undefined || value === null || value === "")
|
|
179
|
+
return 1;
|
|
180
|
+
const n = Number(value);
|
|
181
|
+
if (!Number.isInteger(n) || n < 1 || n > 5)
|
|
182
|
+
return null;
|
|
183
|
+
return n;
|
|
184
|
+
}
|
|
185
|
+
/** Event rows predate slots; a missing slot means slot 1. */
|
|
186
|
+
function eventSlot(row) {
|
|
187
|
+
const n = Number(row?.data?.slot);
|
|
188
|
+
return Number.isInteger(n) && n >= 1 && n <= 5 ? n : 1;
|
|
189
|
+
}
|
|
38
190
|
/**
|
|
39
191
|
* The current canvas revision, from the durable event log — the single
|
|
40
192
|
* authority. Reads a small window rather than exactly one row and takes the
|
|
@@ -45,14 +197,20 @@ export const CANVAS_ARTIFACT_FILENAME = "canvas.html";
|
|
|
45
197
|
// Latest canvas draw's full data (rev + armed contract) — same bounded event
|
|
46
198
|
// query as latestCanvasRev; used by read_canvas(manifestOnly) so an inheriting
|
|
47
199
|
// agent learns what the browser is currently enforcing.
|
|
48
|
-
async function latestCanvasEventData(catalog, sessionId) {
|
|
200
|
+
async function latestCanvasEventData(catalog, sessionId, slot = 1) {
|
|
49
201
|
// No empty-coerce on failure: a transient read error that reads as "no
|
|
50
202
|
// canvas" would mint rev 1 over a live rev-12 canvas (clients' monotonic
|
|
51
203
|
// guards then discard the draw while the tool reports success). Callers
|
|
52
204
|
// route the throw to a structured { error }.
|
|
53
|
-
|
|
205
|
+
//
|
|
206
|
+
// Window 30, not 5: five interleaved slots can push one slot's latest
|
|
207
|
+
// draw well past a five-event window. The session_canvases table is the
|
|
208
|
+
// fast path; this scan is the durable fallback.
|
|
209
|
+
const rows = await catalog.getSessionEventsBefore(sessionId, Number.MAX_SAFE_INTEGER, 30, ["session.canvas_updated"]);
|
|
54
210
|
let latest = { rev: 0 };
|
|
55
211
|
for (const row of rows || []) {
|
|
212
|
+
if (eventSlot(row) !== slot)
|
|
213
|
+
continue;
|
|
56
214
|
const rev = Number(row?.data?.rev);
|
|
57
215
|
if (Number.isFinite(rev) && Number.isInteger(rev) && rev > latest.rev) {
|
|
58
216
|
latest = { rev, responseContract: row?.data?.responseContract };
|
|
@@ -60,10 +218,21 @@ async function latestCanvasEventData(catalog, sessionId) {
|
|
|
60
218
|
}
|
|
61
219
|
return latest;
|
|
62
220
|
}
|
|
63
|
-
async function latestCanvasRev(catalog, sessionId) {
|
|
64
|
-
|
|
221
|
+
async function latestCanvasRev(catalog, sessionId, slot = 1) {
|
|
222
|
+
// Table first (migration 0045); event scan as the durable fallback for
|
|
223
|
+
// rows the table has not seen (legacy sessions, a missed upsert).
|
|
224
|
+
try {
|
|
225
|
+
const rows = await catalog.getSessionCanvases?.(sessionId);
|
|
226
|
+
const hit = (rows || []).find((r) => Number(r.slot) === slot);
|
|
227
|
+
if (hit && Number(hit.latestRev) > 0)
|
|
228
|
+
return Number(hit.latestRev);
|
|
229
|
+
}
|
|
230
|
+
catch { /* fall through to the event scan */ }
|
|
231
|
+
const rows = await catalog.getSessionEventsBefore(sessionId, Number.MAX_SAFE_INTEGER, 30, ["session.canvas_updated"]);
|
|
65
232
|
let latest = 0;
|
|
66
233
|
for (const row of rows || []) {
|
|
234
|
+
if (eventSlot(row) !== slot)
|
|
235
|
+
continue;
|
|
67
236
|
const rev = Number(row?.data?.rev);
|
|
68
237
|
if (Number.isFinite(rev) && rev > latest && Number.isInteger(rev))
|
|
69
238
|
latest = rev;
|
|
@@ -1021,28 +1190,24 @@ artifactStore) {
|
|
|
1021
1190
|
return sanitized;
|
|
1022
1191
|
};
|
|
1023
1192
|
const isReadOnlyTuner = () => runConfig.agentIdentity === "agent-tuner";
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
const candidates = [a.name, a.id, a.title].map(normalizeAgentLookup).filter(Boolean);
|
|
1043
|
-
return candidates.includes(lookup) || (lookupBase && candidates.includes(lookupBase));
|
|
1044
|
-
}) ?? null;
|
|
1045
|
-
};
|
|
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
|
+
});
|
|
1046
1211
|
const loadDirectChildSessions = async () => {
|
|
1047
1212
|
const sdkClient = await getInlineClient();
|
|
1048
1213
|
const sessions = await sdkClient.listSessions();
|
|
@@ -1120,10 +1285,12 @@ artifactStore) {
|
|
|
1120
1285
|
// Same-turn draw serialization: parallel draw_canvas calls in one
|
|
1121
1286
|
// assistant message would otherwise race the derive-write-record
|
|
1122
1287
|
// section and mint duplicate revisions.
|
|
1123
|
-
async function latestCanvasDataRev(catalog, sessionId) {
|
|
1124
|
-
const rows = await catalog.getSessionEventsBefore(sessionId, Number.MAX_SAFE_INTEGER,
|
|
1288
|
+
async function latestCanvasDataRev(catalog, sessionId, slot = 1) {
|
|
1289
|
+
const rows = await catalog.getSessionEventsBefore(sessionId, Number.MAX_SAFE_INTEGER, 30, ["session.canvas_data"]);
|
|
1125
1290
|
let latest = 0;
|
|
1126
1291
|
for (const row of rows || []) {
|
|
1292
|
+
if (eventSlot(row) !== slot)
|
|
1293
|
+
continue;
|
|
1127
1294
|
const rev = Number(row?.data?.dataRev);
|
|
1128
1295
|
if (Number.isFinite(rev) && rev > latest && Number.isInteger(rev))
|
|
1129
1296
|
latest = rev;
|
|
@@ -1248,7 +1415,7 @@ artifactStore) {
|
|
|
1248
1415
|
const agentName = String(args.agent_name || "").trim();
|
|
1249
1416
|
if (!agentName)
|
|
1250
1417
|
return "Error: agent_name is required.";
|
|
1251
|
-
const agentDef = resolveAgentConfigInline(agentName);
|
|
1418
|
+
const agentDef = await resolveAgentConfigInline(agentName);
|
|
1252
1419
|
if (!agentDef) {
|
|
1253
1420
|
return `[SYSTEM: create_agent_session failed — agent "${agentName}" not found. Use list_agent_packages / ps_list_agents to see what is installed.]`;
|
|
1254
1421
|
}
|
|
@@ -1373,7 +1540,7 @@ artifactStore) {
|
|
|
1373
1540
|
: "app-agent");
|
|
1374
1541
|
};
|
|
1375
1542
|
if (resolvedAgentName) {
|
|
1376
|
-
const agentDef = resolveAgentConfigInline(resolvedAgentName);
|
|
1543
|
+
const agentDef = await resolveAgentConfigInline(resolvedAgentName);
|
|
1377
1544
|
if (!agentDef) {
|
|
1378
1545
|
return `[SYSTEM: spawn_agent failed — agent "${resolvedAgentName}" not found. Use ps_list_agents to see available agents.]`;
|
|
1379
1546
|
}
|
|
@@ -1894,18 +2061,25 @@ artifactStore) {
|
|
|
1894
2061
|
// session-proxy's generic onEvent persister treats
|
|
1895
2062
|
// canvas_updated as already-persisted (see EPHEMERAL_TYPES); the
|
|
1896
2063
|
// handler's opts.onEvent emit is the live-push half only.
|
|
1897
|
-
|
|
2064
|
+
// Every session gets the canvas bridge — root, sub-agent, and
|
|
2065
|
+
// sub-sub-agent alike. Each draws its OWN canvases; there is no
|
|
2066
|
+
// cross-session canvas access here, so a child cannot touch its
|
|
2067
|
+
// parent's surface any more than a stranger's.
|
|
2068
|
+
...({
|
|
1898
2069
|
drawCanvas: async (args) => {
|
|
1899
2070
|
if (!artifactStore)
|
|
1900
2071
|
return { error: "this worker has no artifact store" };
|
|
1901
2072
|
if (!catalog)
|
|
1902
2073
|
return { error: "canvas requires the CMS catalog" };
|
|
2074
|
+
const slot = normalizeCanvasSlot(args.slot);
|
|
2075
|
+
if (slot === null)
|
|
2076
|
+
return { error: "slot must be an integer 1-5" };
|
|
2077
|
+
const rawName = args.name === undefined ? undefined : String(args.name).trim();
|
|
2078
|
+
if (rawName !== undefined && rawName.length > 60) {
|
|
2079
|
+
return { error: "name must be 60 characters or fewer" };
|
|
2080
|
+
}
|
|
1903
2081
|
const run = canvasDrawChain.then(async () => {
|
|
1904
|
-
const
|
|
1905
|
-
if (row?.parentSessionId) {
|
|
1906
|
-
return { error: "the canvas is only available on root sessions" };
|
|
1907
|
-
}
|
|
1908
|
-
const rev = (await latestCanvasRev(catalog, input.sessionId)) + 1;
|
|
2082
|
+
const rev = (await latestCanvasRev(catalog, input.sessionId, slot)) + 1;
|
|
1909
2083
|
// Source resolution. fromArtifact pulls bytes store-side —
|
|
1910
2084
|
// the same trust stance as read_artifact/write_artifact's
|
|
1911
2085
|
// cross-session paths (the artifact layer is worker-trusted);
|
|
@@ -1969,7 +2143,7 @@ artifactStore) {
|
|
|
1969
2143
|
}
|
|
1970
2144
|
const app = canvasAppCard(extraction.manifest);
|
|
1971
2145
|
const note = args.note ? String(args.note) : undefined;
|
|
1972
|
-
await artifactStore.uploadArtifact(input.sessionId,
|
|
2146
|
+
await artifactStore.uploadArtifact(input.sessionId, canvasArtifactFilename(slot), html, "text/html",
|
|
1973
2147
|
// Pinned on EVERY draw, not just the first: uploads
|
|
1974
2148
|
// replace artifact metadata wholesale, so a pin set
|
|
1975
2149
|
// once at rev 1 was silently erased by rev 2.
|
|
@@ -1984,14 +2158,27 @@ artifactStore) {
|
|
|
1984
2158
|
eventType: "session.canvas_updated",
|
|
1985
2159
|
data: {
|
|
1986
2160
|
rev,
|
|
2161
|
+
slot,
|
|
2162
|
+
...(rawName !== undefined ? { name: rawName } : {}),
|
|
1987
2163
|
sizeBytes,
|
|
1988
2164
|
...(note ? { note } : {}),
|
|
1989
2165
|
...(effectiveContract ? { responseContract: effectiveContract } : {}),
|
|
1990
2166
|
...(source ? { source } : {}),
|
|
1991
2167
|
},
|
|
1992
2168
|
}], workerNodeId);
|
|
2169
|
+
// The per-slot cache (migration 0045). Non-fatal on
|
|
2170
|
+
// purpose: the event above is durable, and the next
|
|
2171
|
+
// draw falls back to the event scan if this row is
|
|
2172
|
+
// missing — failing the draw over a cache write would
|
|
2173
|
+
// invert the dependency.
|
|
2174
|
+
try {
|
|
2175
|
+
await catalog.upsertSessionCanvas?.(input.sessionId, slot, rawName ?? null, rev, sizeBytes);
|
|
2176
|
+
}
|
|
2177
|
+
catch { /* self-heals on the next draw */ }
|
|
1993
2178
|
return {
|
|
1994
2179
|
rev,
|
|
2180
|
+
slot,
|
|
2181
|
+
...(rawName !== undefined ? { name: rawName } : {}),
|
|
1995
2182
|
sizeBytes,
|
|
1996
2183
|
...(source ? { source } : {}),
|
|
1997
2184
|
...(app ? { app } : {}),
|
|
@@ -2005,15 +2192,14 @@ artifactStore) {
|
|
|
2005
2192
|
updateCanvas: async (args) => {
|
|
2006
2193
|
if (!catalog)
|
|
2007
2194
|
return { error: "canvas requires the CMS catalog" };
|
|
2195
|
+
const slot = normalizeCanvasSlot(args.slot);
|
|
2196
|
+
if (slot === null)
|
|
2197
|
+
return { error: "slot must be an integer 1-5" };
|
|
2008
2198
|
const run = canvasDrawChain.then(async () => {
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
return { error: "the canvas is only available on root sessions" };
|
|
2012
|
-
}
|
|
2013
|
-
if ((await latestCanvasRev(catalog, input.sessionId)) === 0) {
|
|
2014
|
-
return { error: "no canvas has been drawn on this session — draw_canvas first; ticks patch an existing page" };
|
|
2199
|
+
if ((await latestCanvasRev(catalog, input.sessionId, slot)) === 0) {
|
|
2200
|
+
return { error: `no canvas has been drawn in slot ${slot} — draw_canvas first; ticks patch an existing page` };
|
|
2015
2201
|
}
|
|
2016
|
-
const dataRev = (await latestCanvasDataRev(catalog, input.sessionId)) + 1;
|
|
2202
|
+
const dataRev = (await latestCanvasDataRev(catalog, input.sessionId, slot)) + 1;
|
|
2017
2203
|
const sizeBytes = Buffer.byteLength(JSON.stringify(args.data), "utf8");
|
|
2018
2204
|
// The payload rides the durable event — it IS the
|
|
2019
2205
|
// replay source for cold loads; no artifact write.
|
|
@@ -2021,21 +2207,45 @@ artifactStore) {
|
|
|
2021
2207
|
eventType: "session.canvas_data",
|
|
2022
2208
|
data: {
|
|
2023
2209
|
dataRev,
|
|
2210
|
+
slot,
|
|
2024
2211
|
sizeBytes,
|
|
2025
2212
|
payload: args.data,
|
|
2026
2213
|
...(args.note ? { note: String(args.note) } : {}),
|
|
2027
2214
|
},
|
|
2028
2215
|
}], workerNodeId);
|
|
2029
|
-
return { dataRev, sizeBytes };
|
|
2216
|
+
return { dataRev, slot, sizeBytes };
|
|
2030
2217
|
}).catch((err) => ({ error: err?.message || String(err) }));
|
|
2031
2218
|
canvasDrawChain = run.then(() => undefined, () => undefined);
|
|
2032
2219
|
return run;
|
|
2033
2220
|
},
|
|
2221
|
+
// Bring an ALREADY-DRAWN canvas to the user's screen without
|
|
2222
|
+
// redrawing it: no bytes, no new rev, nothing marked unseen.
|
|
2223
|
+
// The portal routes the event through the same flip guards a
|
|
2224
|
+
// draw uses (active session, freshness, per-slot opt-out).
|
|
2225
|
+
showCanvas: async (args) => {
|
|
2226
|
+
if (!catalog)
|
|
2227
|
+
return { error: "canvas requires the CMS catalog" };
|
|
2228
|
+
const slot = normalizeCanvasSlot(args.slot);
|
|
2229
|
+
if (slot === null)
|
|
2230
|
+
return { error: "slot must be an integer 1-5" };
|
|
2231
|
+
const rev = await latestCanvasRev(catalog, input.sessionId, slot);
|
|
2232
|
+
if (rev === 0) {
|
|
2233
|
+
return { error: `nothing has been drawn in slot ${slot} — draw_canvas first; show_canvas only presents an existing canvas` };
|
|
2234
|
+
}
|
|
2235
|
+
await catalog.recordEvents(input.sessionId, [{
|
|
2236
|
+
eventType: "session.canvas_presented",
|
|
2237
|
+
data: { slot, rev },
|
|
2238
|
+
}], workerNodeId);
|
|
2239
|
+
return { presented: true, slot, rev };
|
|
2240
|
+
},
|
|
2034
2241
|
readCanvas: async (args) => {
|
|
2035
2242
|
if (!artifactStore)
|
|
2036
2243
|
return { error: "this worker has no artifact store" };
|
|
2037
2244
|
if (!catalog)
|
|
2038
2245
|
return { error: "canvas requires the CMS catalog" };
|
|
2246
|
+
const slot = normalizeCanvasSlot(args.slot);
|
|
2247
|
+
if (slot === null)
|
|
2248
|
+
return { error: "slot must be an integer 1-5" };
|
|
2039
2249
|
try {
|
|
2040
2250
|
// Log FIRST: the event log is the single authority on
|
|
2041
2251
|
// whether a canvas exists. Orphan bytes from a crashed
|
|
@@ -2051,10 +2261,10 @@ artifactStore) {
|
|
|
2051
2261
|
// ARMED contract from the latest draw event — the
|
|
2052
2262
|
// pair an inheriting agent needs to interpret
|
|
2053
2263
|
// canvas-action messages and author ticks.
|
|
2054
|
-
const latest = await latestCanvasEventData(catalog, input.sessionId);
|
|
2264
|
+
const latest = await latestCanvasEventData(catalog, input.sessionId, slot);
|
|
2055
2265
|
if (latest.rev === 0)
|
|
2056
2266
|
return { exists: false };
|
|
2057
|
-
const docText = await artifactStore.downloadArtifactText(input.sessionId,
|
|
2267
|
+
const docText = await artifactStore.downloadArtifactText(input.sessionId, canvasArtifactFilename(slot));
|
|
2058
2268
|
const extraction = extractCanvasAppManifest(docText);
|
|
2059
2269
|
const card = canvasAppCard(extraction.manifest);
|
|
2060
2270
|
// Events are writable via send_session_event with no
|
|
@@ -2071,10 +2281,10 @@ artifactStore) {
|
|
|
2071
2281
|
...(extraction.error ? { manifestError: extraction.error } : {}),
|
|
2072
2282
|
};
|
|
2073
2283
|
}
|
|
2074
|
-
const rev = await latestCanvasRev(catalog, input.sessionId);
|
|
2284
|
+
const rev = await latestCanvasRev(catalog, input.sessionId, slot);
|
|
2075
2285
|
if (rev === 0)
|
|
2076
2286
|
return { exists: false };
|
|
2077
|
-
const text = await artifactStore.downloadArtifactText(input.sessionId,
|
|
2287
|
+
const text = await artifactStore.downloadArtifactText(input.sessionId, canvasArtifactFilename(slot));
|
|
2078
2288
|
const offset = Math.max(0, Math.floor(Number(args.offset) || 0));
|
|
2079
2289
|
const maxChars = Math.min(262_144, Math.max(1, Math.floor(Number(args.maxBytes) || 65_536)));
|
|
2080
2290
|
const content = text.slice(offset, offset + maxChars);
|
|
@@ -2134,6 +2344,9 @@ artifactStore) {
|
|
|
2134
2344
|
// handler's emit is the live-push half only; recording it here
|
|
2135
2345
|
// too would double-insert every revision.
|
|
2136
2346
|
"session.canvas_updated",
|
|
2347
|
+
// Same contract as canvas_updated: the showCanvas bridge
|
|
2348
|
+
// records it durably itself; the emit is live-push only.
|
|
2349
|
+
"session.canvas_presented",
|
|
2137
2350
|
// Same contract for data ticks: the updateCanvas bridge method
|
|
2138
2351
|
// records the event (payload inline) inside the serialized
|
|
2139
2352
|
// section — the generic path must not double-insert it.
|
|
@@ -3066,143 +3279,42 @@ artifactStore) {
|
|
|
3066
3279
|
// Resolves a loaded agent definition by name. User-creatable agents return
|
|
3067
3280
|
// creatable=true. Worker-managed system agents return creatable=false so
|
|
3068
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.
|
|
3069
3287
|
runtime.registerActivity("resolveAgentConfig", async (_activityCtx, input) => {
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
|
|
3082
|
-
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
rawName = input.agentName;
|
|
3100
|
-
}
|
|
3101
|
-
const lookup = normalize(rawName);
|
|
3102
|
-
// Also try without trailing "agent" suffix for fuzzy matching
|
|
3103
|
-
// (LLM often says "Sweeper agent" which normalizes to "sweeperagent", but id is "sweeper")
|
|
3104
|
-
const lookupBase = lookup.replace(/agent$/, "");
|
|
3105
|
-
/**
|
|
3106
|
-
* PACKAGE PRIVACY, enforced at RESOLUTION rather than at creation.
|
|
3107
|
-
*
|
|
3108
|
-
* Workers install every enabled package, including other users'
|
|
3109
|
-
* user-scope ones, so `userAgents` is a fleet-wide list with no
|
|
3110
|
-
* tenancy in it. The Web API filters package agents when a session is
|
|
3111
|
-
* created (`_authorizePackageAgentCreate`), but `spawn_agent` reaches
|
|
3112
|
-
* this activity directly and never passes through that filter — so a
|
|
3113
|
-
* name match alone would hand Alice's private agent, prompt and all,
|
|
3114
|
-
* to Bob's session.
|
|
3115
|
-
*
|
|
3116
|
-
* It has to be enforced HERE and not at spawn time: the return value
|
|
3117
|
-
* carries the agent's full prompt, and merely returning it writes that
|
|
3118
|
-
* prompt into the caller's orchestration history.
|
|
3119
|
-
*
|
|
3120
|
-
* Deployment-owned plugin agents carry no package owner and stay
|
|
3121
|
-
* public, exactly as before.
|
|
3122
|
-
*/
|
|
3123
|
-
let callerOwnerKey = null;
|
|
3124
|
-
let callerResolved = false;
|
|
3125
|
-
const ownerKeyOf = (owner) => owner?.provider && owner?.subject ? `${owner.provider}\u0001${owner.subject}` : null;
|
|
3126
|
-
/** Resolve the caller's owner once per invocation, memoized and fail-closed. */
|
|
3127
|
-
const callerOwnerKeyOnce = async () => {
|
|
3128
|
-
if (!callerResolved) {
|
|
3129
|
-
callerResolved = true;
|
|
3130
|
-
try {
|
|
3131
|
-
const row = input.callerSessionId
|
|
3132
|
-
? await catalog?.getSession(input.callerSessionId)
|
|
3133
|
-
: null;
|
|
3134
|
-
callerOwnerKey = ownerKeyOf(row?.owner);
|
|
3135
|
-
}
|
|
3136
|
-
catch {
|
|
3137
|
-
// Fail closed: an unresolvable caller gets no private agents.
|
|
3138
|
-
callerOwnerKey = null;
|
|
3139
|
-
}
|
|
3140
|
-
}
|
|
3141
|
-
return callerOwnerKey;
|
|
3142
|
-
};
|
|
3143
|
-
const visibleToCaller = async (agent) => {
|
|
3144
|
-
const agentOwnerKey = ownerKeyOf(agent?.packageOwner);
|
|
3145
|
-
if (agent?.packageScope !== "user" || !agentOwnerKey)
|
|
3146
|
-
return true;
|
|
3147
|
-
const key = await callerOwnerKeyOnce();
|
|
3148
|
-
return key !== null && key === agentOwnerKey;
|
|
3149
|
-
};
|
|
3150
|
-
const candidatesFor = (a) => [a.name, a.id, a.title].map(normalize).filter(Boolean);
|
|
3151
|
-
const nameMatches = (a) => {
|
|
3152
|
-
const candidates = candidatesFor(a);
|
|
3153
|
-
return candidates.includes(lookup) || (lookupBase && candidates.includes(lookupBase));
|
|
3154
|
-
};
|
|
3155
|
-
let matches = agents.filter(a => {
|
|
3156
|
-
if (lookupNamespace && normalize(a.namespace) !== normalize(lookupNamespace))
|
|
3157
|
-
return false;
|
|
3158
|
-
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
|
+
},
|
|
3159
3317
|
});
|
|
3160
|
-
// An ambiguous `a:b` that matched nothing as `namespace:agent` gets its
|
|
3161
|
-
// second reading — `owner:package` — rather than just failing. Order,
|
|
3162
|
-
// not guesswork: the old meaning always wins when it resolves.
|
|
3163
|
-
if (matches.length === 0 && fqn.kind === "ambiguous") {
|
|
3164
|
-
matches = agents.filter(nameMatches);
|
|
3165
|
-
}
|
|
3166
|
-
if (sharedOnly) {
|
|
3167
|
-
// `__shared:x` means the deployment's copy, explicitly. This is the
|
|
3168
|
-
// one thing bare-name precedence otherwise takes away: without it,
|
|
3169
|
-
// a user with their own copy could never reach the shared one.
|
|
3170
|
-
matches = matches.filter(a => a.packageScope !== "user");
|
|
3171
|
-
}
|
|
3172
|
-
else {
|
|
3173
|
-
// SHADOWING, at the agent level: the caller's own copy comes first,
|
|
3174
|
-
// then everything public. Same rule the registry resolver applies,
|
|
3175
|
-
// so "run X" and "show me X" cannot disagree about which X.
|
|
3176
|
-
const callerKey = await callerOwnerKeyOnce();
|
|
3177
|
-
matches = [
|
|
3178
|
-
...matches.filter(a => a.packageScope === "user" && ownerKeyOf(a.packageOwner) === callerKey && callerKey !== null),
|
|
3179
|
-
...matches.filter(a => !(a.packageScope === "user" && ownerKeyOf(a.packageOwner) === callerKey && callerKey !== null)),
|
|
3180
|
-
];
|
|
3181
|
-
}
|
|
3182
|
-
let agent = undefined;
|
|
3183
|
-
for (const candidate of matches) {
|
|
3184
|
-
if (await visibleToCaller(candidate)) {
|
|
3185
|
-
agent = candidate;
|
|
3186
|
-
break;
|
|
3187
|
-
}
|
|
3188
|
-
}
|
|
3189
|
-
if (!agent)
|
|
3190
|
-
return null;
|
|
3191
|
-
return {
|
|
3192
|
-
name: agent.name,
|
|
3193
|
-
prompt: agent.prompt,
|
|
3194
|
-
tools: agent.tools ?? undefined,
|
|
3195
|
-
initialPrompt: agent.initialPrompt ?? undefined,
|
|
3196
|
-
title: agent.title ?? undefined,
|
|
3197
|
-
system: agent.system ?? undefined,
|
|
3198
|
-
id: agent.id ?? undefined,
|
|
3199
|
-
parent: agent.parent ?? undefined,
|
|
3200
|
-
splash: agent.splash ?? undefined,
|
|
3201
|
-
splashMobile: agent.splashMobile ?? undefined,
|
|
3202
|
-
namespace: agent.namespace ?? undefined,
|
|
3203
|
-
promptLayerKind: agent.promptLayerKind ?? undefined,
|
|
3204
|
-
creatable: agent.creatable ?? !agent.system,
|
|
3205
|
-
};
|
|
3206
3318
|
});
|
|
3207
3319
|
// ── spawnChildSession ─────────────────────────────────────
|
|
3208
3320
|
// Creates a child session via the PilotSwarmClient SDK.
|