office-core 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.runtime-dist/scripts/bundle-host-package.js +46 -0
- package/.runtime-dist/scripts/demo-multi-agent.js +130 -0
- package/.runtime-dist/scripts/home-agent-host.js +1403 -0
- package/.runtime-dist/scripts/host-doctor.js +28 -0
- package/.runtime-dist/scripts/host-login.js +32 -0
- package/.runtime-dist/scripts/host-menu.js +227 -0
- package/.runtime-dist/scripts/host-open.js +20 -0
- package/.runtime-dist/scripts/install-host.js +108 -0
- package/.runtime-dist/scripts/lib/host-config.js +171 -0
- package/.runtime-dist/scripts/lib/local-runner.js +287 -0
- package/.runtime-dist/scripts/office-cli.js +698 -0
- package/.runtime-dist/scripts/run-local-project.js +277 -0
- package/.runtime-dist/src/auth/session-token.js +62 -0
- package/.runtime-dist/src/discord/outbox-ledger.js +56 -0
- package/.runtime-dist/src/do/AgentDO.js +205 -0
- package/.runtime-dist/src/do/GatewayShardDO.js +9 -0
- package/.runtime-dist/src/do/ProjectDO.js +829 -0
- package/.runtime-dist/src/do/TaskDO.js +356 -0
- package/.runtime-dist/src/index.js +123 -0
- package/.runtime-dist/src/project/office-view.js +405 -0
- package/.runtime-dist/src/project/read-model.js +79 -0
- package/.runtime-dist/src/routes/agents-bootstrap.js +9 -0
- package/.runtime-dist/src/routes/agents-descriptor.js +12 -0
- package/.runtime-dist/src/routes/agents-events.js +17 -0
- package/.runtime-dist/src/routes/agents-heartbeat.js +21 -0
- package/.runtime-dist/src/routes/agents-task-context.js +17 -0
- package/.runtime-dist/src/routes/bundles.js +198 -0
- package/.runtime-dist/src/routes/local-host.js +49 -0
- package/.runtime-dist/src/routes/projects.js +119 -0
- package/.runtime-dist/src/routes/tasks.js +67 -0
- package/.runtime-dist/src/task/reducer.js +464 -0
- package/.runtime-dist/src/types/project.js +1 -0
- package/.runtime-dist/src/types/protocol.js +3 -0
- package/.runtime-dist/src/types/runtime.js +1 -0
- package/README.md +148 -0
- package/bin/double-penetration-host.mjs +83 -0
- package/package.json +48 -0
- package/public/index.html +1581 -0
- package/public/install-host.ps1 +64 -0
- package/scripts/run-runtime-script.mjs +43 -0
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
import { mintHostToken, parseHostToken, readBearerToken } from "../auth/session-token.js";
|
|
2
|
+
export class ProjectDO {
|
|
3
|
+
state;
|
|
4
|
+
env;
|
|
5
|
+
projectCache;
|
|
6
|
+
constructor(state, env) {
|
|
7
|
+
this.state = state;
|
|
8
|
+
this.env = env;
|
|
9
|
+
}
|
|
10
|
+
async fetch(request) {
|
|
11
|
+
const url = new URL(request.url);
|
|
12
|
+
if (request.method === "GET" && url.pathname === "/summary") {
|
|
13
|
+
return Response.json((await this.load())?.summary ?? null);
|
|
14
|
+
}
|
|
15
|
+
if (request.method === "GET" && url.pathname === "/state") {
|
|
16
|
+
const project = await this.load();
|
|
17
|
+
return Response.json(project ? withDerivedHostSessions(project) : null);
|
|
18
|
+
}
|
|
19
|
+
if (request.method === "GET" && url.pathname === "/status") {
|
|
20
|
+
const project = await this.load();
|
|
21
|
+
return Response.json(project
|
|
22
|
+
? Object.values(project.status_feed).sort((left, right) => right.updated_at.localeCompare(left.updated_at))
|
|
23
|
+
: []);
|
|
24
|
+
}
|
|
25
|
+
if (request.method === "GET" && url.pathname === "/tasks") {
|
|
26
|
+
const project = await this.load();
|
|
27
|
+
return Response.json(project
|
|
28
|
+
? Object.values(project.active_tasks).sort((left, right) => (right.updated_at ?? "").localeCompare(left.updated_at ?? ""))
|
|
29
|
+
: []);
|
|
30
|
+
}
|
|
31
|
+
if (request.method === "GET" && url.pathname === "/events") {
|
|
32
|
+
const project = await this.load();
|
|
33
|
+
return Response.json(project?.recent_events ?? []);
|
|
34
|
+
}
|
|
35
|
+
if (request.method === "GET" && url.pathname === "/local-hosts") {
|
|
36
|
+
const project = await this.load();
|
|
37
|
+
const decorated = project ? withDerivedHostSessions(project) : null;
|
|
38
|
+
return Response.json(decorated
|
|
39
|
+
? Object.values(decorated.local_hosts)
|
|
40
|
+
.map((host) => ({
|
|
41
|
+
...host,
|
|
42
|
+
commands: (decorated.local_host_commands[host.host_id] ?? []).slice(-6).reverse(),
|
|
43
|
+
}))
|
|
44
|
+
.sort((left, right) => right.last_seen_at.localeCompare(left.last_seen_at))
|
|
45
|
+
: []);
|
|
46
|
+
}
|
|
47
|
+
if (request.method === "GET" && url.pathname === "/room") {
|
|
48
|
+
const project = await this.load();
|
|
49
|
+
return Response.json(project?.room ?? buildDefaultRoom());
|
|
50
|
+
}
|
|
51
|
+
if (request.method === "GET" && url.pathname === "/room/messages") {
|
|
52
|
+
const project = await this.load();
|
|
53
|
+
return Response.json(project?.room.messages ?? []);
|
|
54
|
+
}
|
|
55
|
+
if (request.method === "POST" && url.pathname === "/init") {
|
|
56
|
+
const body = await request.json();
|
|
57
|
+
const project = await this.ensure(body.project_id, body.name ?? body.project_id);
|
|
58
|
+
return Response.json(project.summary);
|
|
59
|
+
}
|
|
60
|
+
if (request.method === "POST" && url.pathname === "/status/upsert") {
|
|
61
|
+
const body = await request.json();
|
|
62
|
+
const projectId = url.searchParams.get("project_id") ?? body.project_id ?? "prj_demo";
|
|
63
|
+
const project = await this.ensure(projectId, projectId);
|
|
64
|
+
const now = new Date().toISOString();
|
|
65
|
+
const next = {
|
|
66
|
+
...project,
|
|
67
|
+
summary: {
|
|
68
|
+
...project.summary,
|
|
69
|
+
project_status_seq: project.summary.project_status_seq + 1,
|
|
70
|
+
},
|
|
71
|
+
status_feed: {
|
|
72
|
+
...project.status_feed,
|
|
73
|
+
[body.agent_id]: {
|
|
74
|
+
...body,
|
|
75
|
+
updated_at: body.updated_at ?? now,
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
await this.save(next);
|
|
80
|
+
return Response.json({ ok: true, project_status_seq: next.summary.project_status_seq });
|
|
81
|
+
}
|
|
82
|
+
if (request.method === "POST" && (url.pathname === "/tasks/add" || url.pathname === "/tasks/upsert")) {
|
|
83
|
+
const body = await request.json();
|
|
84
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
85
|
+
const active_tasks = {
|
|
86
|
+
...project.active_tasks,
|
|
87
|
+
[body.task_id]: {
|
|
88
|
+
task_id: body.task_id,
|
|
89
|
+
title: body.title,
|
|
90
|
+
status: body.status,
|
|
91
|
+
updated_at: body.updated_at ?? new Date().toISOString(),
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
const next = {
|
|
95
|
+
...project,
|
|
96
|
+
summary: {
|
|
97
|
+
...project.summary,
|
|
98
|
+
active_task_count: Object.values(active_tasks).filter((task) => task.status !== "completed").length,
|
|
99
|
+
},
|
|
100
|
+
active_tasks,
|
|
101
|
+
};
|
|
102
|
+
await this.save(next);
|
|
103
|
+
return Response.json({ ok: true });
|
|
104
|
+
}
|
|
105
|
+
if (request.method === "POST" && url.pathname === "/manifest") {
|
|
106
|
+
const body = await request.json();
|
|
107
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
108
|
+
const next = {
|
|
109
|
+
...project,
|
|
110
|
+
summary: {
|
|
111
|
+
...project.summary,
|
|
112
|
+
current_manifest_id: body.manifest_id,
|
|
113
|
+
current_manifest_seq: body.manifest_seq,
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
await this.save(next);
|
|
117
|
+
return Response.json({ ok: true });
|
|
118
|
+
}
|
|
119
|
+
if (request.method === "POST" && url.pathname === "/understanding") {
|
|
120
|
+
const body = await request.json();
|
|
121
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
122
|
+
const next = {
|
|
123
|
+
...project,
|
|
124
|
+
summary: {
|
|
125
|
+
...project.summary,
|
|
126
|
+
accepted_understanding: body.accepted_understanding,
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
await this.save(next);
|
|
130
|
+
return Response.json({ ok: true });
|
|
131
|
+
}
|
|
132
|
+
if (request.method === "POST" && url.pathname === "/events/add") {
|
|
133
|
+
const body = await request.json();
|
|
134
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
135
|
+
const next = {
|
|
136
|
+
...project,
|
|
137
|
+
recent_events: prependEvent(project.recent_events, {
|
|
138
|
+
ts: body.ts,
|
|
139
|
+
kind: body.kind,
|
|
140
|
+
actor: body.actor,
|
|
141
|
+
type: body.type,
|
|
142
|
+
detail: body.detail ?? null,
|
|
143
|
+
payload: body.payload ?? null,
|
|
144
|
+
}),
|
|
145
|
+
};
|
|
146
|
+
await this.save(next);
|
|
147
|
+
return Response.json({ ok: true, event_count: next.recent_events.length });
|
|
148
|
+
}
|
|
149
|
+
if (request.method === "POST" && url.pathname === "/room/settings") {
|
|
150
|
+
const body = await request.json();
|
|
151
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
152
|
+
const next = {
|
|
153
|
+
...project,
|
|
154
|
+
room: {
|
|
155
|
+
...project.room,
|
|
156
|
+
settings: {
|
|
157
|
+
all_agents_listening: body.all_agents_listening ?? project.room.settings.all_agents_listening,
|
|
158
|
+
conductor_name: body.conductor_name?.trim() || project.room.settings.conductor_name,
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
recent_events: prependEvent(project.recent_events, {
|
|
162
|
+
ts: new Date().toISOString(),
|
|
163
|
+
kind: "human",
|
|
164
|
+
actor: body.conductor_name?.trim() || project.room.settings.conductor_name,
|
|
165
|
+
type: "room.settings.updated",
|
|
166
|
+
detail: `all_agents_listening=${String(body.all_agents_listening ?? project.room.settings.all_agents_listening)}`,
|
|
167
|
+
}),
|
|
168
|
+
};
|
|
169
|
+
await this.save(next);
|
|
170
|
+
return Response.json(next.room.settings);
|
|
171
|
+
}
|
|
172
|
+
if (request.method === "POST" && url.pathname === "/room/messages") {
|
|
173
|
+
const body = await request.json();
|
|
174
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
175
|
+
const now = new Date().toISOString();
|
|
176
|
+
const message = {
|
|
177
|
+
message_id: `msg_${crypto.randomUUID()}`,
|
|
178
|
+
seq: project.room.next_seq + 1,
|
|
179
|
+
author_type: body.author_type,
|
|
180
|
+
author_id: body.author_id,
|
|
181
|
+
author_label: body.author_label?.trim() || body.author_id,
|
|
182
|
+
text: String(body.text ?? "").trim(),
|
|
183
|
+
created_at: now,
|
|
184
|
+
task_id: body.task_id ?? null,
|
|
185
|
+
host_id: body.host_id ?? null,
|
|
186
|
+
session_id: body.session_id ?? null,
|
|
187
|
+
reply_to_seq: body.reply_to_seq ?? null,
|
|
188
|
+
target_host_ids: normalizeIdList(body.target_host_ids),
|
|
189
|
+
target_session_ids: normalizeIdList(body.target_session_ids),
|
|
190
|
+
target_agent_ids: normalizeIdList(body.target_agent_ids),
|
|
191
|
+
};
|
|
192
|
+
if (!message.text) {
|
|
193
|
+
return new Response("text required", { status: 400 });
|
|
194
|
+
}
|
|
195
|
+
const next = {
|
|
196
|
+
...project,
|
|
197
|
+
room: {
|
|
198
|
+
...project.room,
|
|
199
|
+
next_seq: message.seq,
|
|
200
|
+
messages: [...project.room.messages, message].slice(-200),
|
|
201
|
+
},
|
|
202
|
+
recent_events: prependEvent(project.recent_events, {
|
|
203
|
+
ts: now,
|
|
204
|
+
kind: body.author_type === "user" ? "human" : body.author_type === "agent" ? "evt" : "sys",
|
|
205
|
+
actor: message.author_label,
|
|
206
|
+
type: body.author_type === "user" ? "room.message" : body.author_type === "agent" ? "room.reply" : "room.system",
|
|
207
|
+
detail: truncate(message.text, 120),
|
|
208
|
+
payload: buildRoomEventPayload(message),
|
|
209
|
+
}),
|
|
210
|
+
};
|
|
211
|
+
await this.save(next);
|
|
212
|
+
return Response.json(message);
|
|
213
|
+
}
|
|
214
|
+
if (request.method === "POST" && url.pathname === "/room/messages/upsert") {
|
|
215
|
+
const body = await request.json();
|
|
216
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
217
|
+
const now = new Date().toISOString();
|
|
218
|
+
const existing = body.message_id
|
|
219
|
+
? project.room.messages.find((entry) => entry.message_id === body.message_id)
|
|
220
|
+
: null;
|
|
221
|
+
const message = existing
|
|
222
|
+
? {
|
|
223
|
+
...existing,
|
|
224
|
+
author_type: body.author_type ?? existing.author_type,
|
|
225
|
+
author_id: body.author_id ?? existing.author_id,
|
|
226
|
+
author_label: body.author_label?.trim() || existing.author_label,
|
|
227
|
+
text: String(body.text ?? existing.text).trim(),
|
|
228
|
+
task_id: body.task_id ?? existing.task_id ?? null,
|
|
229
|
+
host_id: body.host_id ?? existing.host_id ?? null,
|
|
230
|
+
session_id: body.session_id ?? existing.session_id ?? null,
|
|
231
|
+
reply_to_seq: body.reply_to_seq ?? existing.reply_to_seq ?? null,
|
|
232
|
+
target_host_ids: normalizeIdList(body.target_host_ids) ?? existing.target_host_ids ?? null,
|
|
233
|
+
target_session_ids: normalizeIdList(body.target_session_ids) ?? existing.target_session_ids ?? null,
|
|
234
|
+
target_agent_ids: normalizeIdList(body.target_agent_ids) ?? existing.target_agent_ids ?? null,
|
|
235
|
+
}
|
|
236
|
+
: {
|
|
237
|
+
message_id: body.message_id?.trim() || `msg_${crypto.randomUUID()}`,
|
|
238
|
+
seq: project.room.next_seq + 1,
|
|
239
|
+
author_type: body.author_type,
|
|
240
|
+
author_id: body.author_id,
|
|
241
|
+
author_label: body.author_label?.trim() || body.author_id,
|
|
242
|
+
text: String(body.text ?? "").trim(),
|
|
243
|
+
created_at: now,
|
|
244
|
+
task_id: body.task_id ?? null,
|
|
245
|
+
host_id: body.host_id ?? null,
|
|
246
|
+
session_id: body.session_id ?? null,
|
|
247
|
+
reply_to_seq: body.reply_to_seq ?? null,
|
|
248
|
+
target_host_ids: normalizeIdList(body.target_host_ids),
|
|
249
|
+
target_session_ids: normalizeIdList(body.target_session_ids),
|
|
250
|
+
target_agent_ids: normalizeIdList(body.target_agent_ids),
|
|
251
|
+
};
|
|
252
|
+
if (!message.text) {
|
|
253
|
+
return new Response("text required", { status: 400 });
|
|
254
|
+
}
|
|
255
|
+
const nextMessages = existing
|
|
256
|
+
? project.room.messages.map((entry) => (entry.message_id === message.message_id ? message : entry))
|
|
257
|
+
: [...project.room.messages, message].slice(-200);
|
|
258
|
+
const next = {
|
|
259
|
+
...project,
|
|
260
|
+
room: {
|
|
261
|
+
...project.room,
|
|
262
|
+
next_seq: existing ? project.room.next_seq : message.seq,
|
|
263
|
+
messages: nextMessages,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
await this.save(next);
|
|
267
|
+
return Response.json(message);
|
|
268
|
+
}
|
|
269
|
+
if (request.method === "POST" && url.pathname === "/local-host/register") {
|
|
270
|
+
const body = await request.json();
|
|
271
|
+
const enrollSecret = request.headers.get("x-enroll-secret")?.trim() ?? "";
|
|
272
|
+
if (!isValidEnrollSecret(enrollSecret, this.env.LOCAL_HOST_ENROLL_SECRET)) {
|
|
273
|
+
return new Response("Invalid enrollment secret", { status: 401 });
|
|
274
|
+
}
|
|
275
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
276
|
+
const now = new Date().toISOString();
|
|
277
|
+
const requestedHostId = sanitizeId(body.requested_host_id || body.machine_name || body.display_name || `host-${crypto.randomUUID().slice(0, 8)}`);
|
|
278
|
+
const displayName = (body.display_name?.trim() || `${body.machine_name?.trim() || requestedHostId} host`).slice(0, 96);
|
|
279
|
+
const machineName = (body.machine_name?.trim() || requestedHostId).slice(0, 96);
|
|
280
|
+
const hostToken = mintHostToken({
|
|
281
|
+
host_id: requestedHostId,
|
|
282
|
+
project_id: body.project_id,
|
|
283
|
+
issued_at: now,
|
|
284
|
+
});
|
|
285
|
+
const next = {
|
|
286
|
+
...project,
|
|
287
|
+
host_registrations: {
|
|
288
|
+
...project.host_registrations,
|
|
289
|
+
[requestedHostId]: {
|
|
290
|
+
host_id: requestedHostId,
|
|
291
|
+
display_name: displayName,
|
|
292
|
+
machine_name: machineName,
|
|
293
|
+
host_token: hostToken,
|
|
294
|
+
created_at: project.host_registrations[requestedHostId]?.created_at ?? now,
|
|
295
|
+
updated_at: now,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
local_hosts: {
|
|
299
|
+
...project.local_hosts,
|
|
300
|
+
[requestedHostId]: {
|
|
301
|
+
host_id: requestedHostId,
|
|
302
|
+
display_name: displayName,
|
|
303
|
+
hostname: machineName,
|
|
304
|
+
platform: project.local_hosts[requestedHostId]?.platform ?? "unknown",
|
|
305
|
+
status: project.local_hosts[requestedHostId]?.status ?? "offline",
|
|
306
|
+
default_workdir: project.local_hosts[requestedHostId]?.default_workdir ?? "",
|
|
307
|
+
last_seen_at: project.local_hosts[requestedHostId]?.last_seen_at ?? now,
|
|
308
|
+
available_runners: project.local_hosts[requestedHostId]?.available_runners ?? { codex: false, claude: false },
|
|
309
|
+
environment: project.local_hosts[requestedHostId]?.environment,
|
|
310
|
+
sessions: project.local_hosts[requestedHostId]?.sessions ?? [],
|
|
311
|
+
},
|
|
312
|
+
},
|
|
313
|
+
recent_events: prependEvent(project.recent_events, {
|
|
314
|
+
ts: now,
|
|
315
|
+
kind: "sys",
|
|
316
|
+
actor: "host-registry",
|
|
317
|
+
type: "host.registered",
|
|
318
|
+
detail: `${displayName} → ${requestedHostId}`,
|
|
319
|
+
}),
|
|
320
|
+
room: appendRoomSystemMessage(project.room, {
|
|
321
|
+
text: `Host ${displayName} joined the office`,
|
|
322
|
+
host_id: requestedHostId,
|
|
323
|
+
}),
|
|
324
|
+
};
|
|
325
|
+
await this.save(next);
|
|
326
|
+
return Response.json({
|
|
327
|
+
ok: true,
|
|
328
|
+
host_id: requestedHostId,
|
|
329
|
+
host_token: hostToken,
|
|
330
|
+
project_id: body.project_id,
|
|
331
|
+
room_settings: next.room.settings,
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
if (request.method === "POST" && url.pathname === "/local-host/heartbeat") {
|
|
335
|
+
const body = await request.json();
|
|
336
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
337
|
+
const authError = this.authorizeHost(project, request, body.host_id, body.project_id);
|
|
338
|
+
if (authError) {
|
|
339
|
+
return authError;
|
|
340
|
+
}
|
|
341
|
+
const next = {
|
|
342
|
+
...project,
|
|
343
|
+
local_hosts: {
|
|
344
|
+
...project.local_hosts,
|
|
345
|
+
[body.host_id]: {
|
|
346
|
+
...body,
|
|
347
|
+
status: "online",
|
|
348
|
+
last_seen_at: body.last_seen_at ?? new Date().toISOString(),
|
|
349
|
+
environment: body.environment ?? project.local_hosts[body.host_id]?.environment,
|
|
350
|
+
sessions: body.sessions ?? [],
|
|
351
|
+
},
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
await this.save(next);
|
|
355
|
+
return Response.json({ ok: true, room_settings: next.room.settings });
|
|
356
|
+
}
|
|
357
|
+
if (request.method === "POST" && url.pathname === "/local-host/commands/queue") {
|
|
358
|
+
const body = await request.json();
|
|
359
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
360
|
+
const now = new Date().toISOString();
|
|
361
|
+
const hostLabel = project.local_hosts[body.host_id]?.display_name ?? body.host_id;
|
|
362
|
+
const command = {
|
|
363
|
+
command_id: `spawn_${crypto.randomUUID()}`,
|
|
364
|
+
command_type: "spawn",
|
|
365
|
+
runner: body.runner,
|
|
366
|
+
agent_id: body.agent_id,
|
|
367
|
+
mode: body.mode,
|
|
368
|
+
effort: body.effort ?? "high",
|
|
369
|
+
workdir: body.workdir,
|
|
370
|
+
task_id: body.task_id ?? null,
|
|
371
|
+
prompt: body.prompt ?? null,
|
|
372
|
+
status: "queued",
|
|
373
|
+
created_at: now,
|
|
374
|
+
updated_at: now,
|
|
375
|
+
};
|
|
376
|
+
const next = {
|
|
377
|
+
...project,
|
|
378
|
+
local_host_commands: {
|
|
379
|
+
...project.local_host_commands,
|
|
380
|
+
[body.host_id]: [...(project.local_host_commands[body.host_id] ?? []), command].slice(-100),
|
|
381
|
+
},
|
|
382
|
+
room: appendRoomSystemMessage(project.room, {
|
|
383
|
+
text: `Starting ${command.agent_id} on ${hostLabel}`,
|
|
384
|
+
host_id: body.host_id,
|
|
385
|
+
task_id: command.task_id ?? null,
|
|
386
|
+
target_agent_ids: [command.agent_id],
|
|
387
|
+
}),
|
|
388
|
+
recent_events: prependEvent(project.recent_events, {
|
|
389
|
+
ts: now,
|
|
390
|
+
kind: "sys",
|
|
391
|
+
actor: "ui",
|
|
392
|
+
type: "spawn.requested",
|
|
393
|
+
detail: `${command.agent_id} on ${hostLabel}`,
|
|
394
|
+
}),
|
|
395
|
+
};
|
|
396
|
+
await this.save(next);
|
|
397
|
+
return Response.json({ ok: true, command_id: command.command_id });
|
|
398
|
+
}
|
|
399
|
+
if (request.method === "POST" && url.pathname === "/local-host/sessions/remove") {
|
|
400
|
+
const body = await request.json();
|
|
401
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
402
|
+
const now = new Date().toISOString();
|
|
403
|
+
const host = project.local_hosts[body.host_id];
|
|
404
|
+
const session = host?.sessions.find((entry) => entry.session_id === body.session_id) ??
|
|
405
|
+
(() => {
|
|
406
|
+
const command = (project.local_host_commands[body.host_id] ?? []).find((entry) => entry.result?.session_id === body.session_id);
|
|
407
|
+
if (!command) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
return {
|
|
411
|
+
session_id: body.session_id,
|
|
412
|
+
runner: command.runner ?? "codex",
|
|
413
|
+
agent_id: command.agent_id,
|
|
414
|
+
mode: command.mode ?? "attach",
|
|
415
|
+
workdir: command.workdir ?? host?.default_workdir ?? "",
|
|
416
|
+
status: "running",
|
|
417
|
+
launched_at: command.updated_at,
|
|
418
|
+
task_id: command.task_id ?? null,
|
|
419
|
+
};
|
|
420
|
+
})();
|
|
421
|
+
if (!host || !session) {
|
|
422
|
+
return new Response("session not found", { status: 404 });
|
|
423
|
+
}
|
|
424
|
+
const command = {
|
|
425
|
+
command_id: `remove_${crypto.randomUUID()}`,
|
|
426
|
+
command_type: "remove",
|
|
427
|
+
agent_id: session.agent_id,
|
|
428
|
+
session_id: session.session_id,
|
|
429
|
+
task_id: session.task_id ?? null,
|
|
430
|
+
status: "queued",
|
|
431
|
+
created_at: now,
|
|
432
|
+
updated_at: now,
|
|
433
|
+
};
|
|
434
|
+
const next = {
|
|
435
|
+
...project,
|
|
436
|
+
local_host_commands: {
|
|
437
|
+
...project.local_host_commands,
|
|
438
|
+
[body.host_id]: [...(project.local_host_commands[body.host_id] ?? []), command].slice(-100),
|
|
439
|
+
},
|
|
440
|
+
room: appendRoomSystemMessage(project.room, {
|
|
441
|
+
text: `Removing ${session.agent_id} from ${host.display_name}`,
|
|
442
|
+
host_id: body.host_id,
|
|
443
|
+
session_id: body.session_id,
|
|
444
|
+
task_id: session.task_id ?? null,
|
|
445
|
+
target_agent_ids: [session.agent_id],
|
|
446
|
+
}),
|
|
447
|
+
recent_events: prependEvent(project.recent_events, {
|
|
448
|
+
ts: now,
|
|
449
|
+
kind: "sys",
|
|
450
|
+
actor: "ui",
|
|
451
|
+
type: "session.remove.requested",
|
|
452
|
+
detail: `${session.agent_id} on ${host.display_name}`,
|
|
453
|
+
}),
|
|
454
|
+
};
|
|
455
|
+
await this.save(next);
|
|
456
|
+
return Response.json({ ok: true, command_id: command.command_id });
|
|
457
|
+
}
|
|
458
|
+
if (request.method === "POST" && url.pathname === "/local-host/switch") {
|
|
459
|
+
const body = await request.json();
|
|
460
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
461
|
+
const host = project.local_hosts[body.host_id];
|
|
462
|
+
if (!host) {
|
|
463
|
+
return new Response("host not found", { status: 404 });
|
|
464
|
+
}
|
|
465
|
+
const targetProjectId = String(body.target_project_id ?? "").trim();
|
|
466
|
+
if (!targetProjectId) {
|
|
467
|
+
return new Response("target_project_id required", { status: 400 });
|
|
468
|
+
}
|
|
469
|
+
const now = new Date().toISOString();
|
|
470
|
+
const command = {
|
|
471
|
+
command_id: `switch_${crypto.randomUUID()}`,
|
|
472
|
+
command_type: "switch_project",
|
|
473
|
+
agent_id: host.display_name,
|
|
474
|
+
target_project_id: targetProjectId,
|
|
475
|
+
target_project_name: body.target_project_name ? String(body.target_project_name) : targetProjectId,
|
|
476
|
+
status: "queued",
|
|
477
|
+
created_at: now,
|
|
478
|
+
updated_at: now,
|
|
479
|
+
};
|
|
480
|
+
const next = {
|
|
481
|
+
...project,
|
|
482
|
+
local_host_commands: {
|
|
483
|
+
...project.local_host_commands,
|
|
484
|
+
[body.host_id]: [...(project.local_host_commands[body.host_id] ?? []), command].slice(-100),
|
|
485
|
+
},
|
|
486
|
+
recent_events: prependEvent(project.recent_events, {
|
|
487
|
+
ts: now,
|
|
488
|
+
kind: "sys",
|
|
489
|
+
actor: "ui",
|
|
490
|
+
type: "project.switch.requested",
|
|
491
|
+
detail: `${host.display_name} → ${command.target_project_name}`,
|
|
492
|
+
}),
|
|
493
|
+
};
|
|
494
|
+
await this.save(next);
|
|
495
|
+
return Response.json({ ok: true, command_id: command.command_id });
|
|
496
|
+
}
|
|
497
|
+
if (request.method === "POST" && url.pathname === "/local-host/commands/pull") {
|
|
498
|
+
const body = await request.json();
|
|
499
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
500
|
+
const authError = this.authorizeHost(project, request, body.host_id, body.project_id);
|
|
501
|
+
if (authError) {
|
|
502
|
+
return authError;
|
|
503
|
+
}
|
|
504
|
+
const commands = project.local_host_commands[body.host_id] ?? [];
|
|
505
|
+
const queued = commands.filter((command) => command.status === "queued");
|
|
506
|
+
if (queued.length === 0) {
|
|
507
|
+
return Response.json({ commands: [] });
|
|
508
|
+
}
|
|
509
|
+
const now = new Date().toISOString();
|
|
510
|
+
const next = {
|
|
511
|
+
...project,
|
|
512
|
+
local_host_commands: {
|
|
513
|
+
...project.local_host_commands,
|
|
514
|
+
[body.host_id]: commands.map((command) => command.status === "queued"
|
|
515
|
+
? {
|
|
516
|
+
...command,
|
|
517
|
+
status: "dispatched",
|
|
518
|
+
updated_at: now,
|
|
519
|
+
}
|
|
520
|
+
: command),
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
await this.save(next);
|
|
524
|
+
return Response.json({
|
|
525
|
+
commands: queued.map((command) => ({
|
|
526
|
+
...command,
|
|
527
|
+
status: "dispatched",
|
|
528
|
+
updated_at: now,
|
|
529
|
+
})),
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
if (request.method === "POST" && url.pathname === "/local-host/room/pull") {
|
|
533
|
+
const body = await request.json();
|
|
534
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
535
|
+
const authError = this.authorizeHost(project, request, body.host_id, body.project_id);
|
|
536
|
+
if (authError) {
|
|
537
|
+
return authError;
|
|
538
|
+
}
|
|
539
|
+
const cursorSeq = Number(body.cursor_seq ?? 0);
|
|
540
|
+
const messages = project.room.messages.filter((message) => message.seq > cursorSeq && shouldDeliverRoomMessageToHost(message, body.host_id));
|
|
541
|
+
return Response.json({
|
|
542
|
+
settings: project.room.settings,
|
|
543
|
+
messages,
|
|
544
|
+
latest_seq: project.room.next_seq,
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
if (request.method === "POST" && url.pathname === "/local-host/commands/complete") {
|
|
548
|
+
const body = await request.json();
|
|
549
|
+
const project = await this.ensure(body.project_id, body.project_id);
|
|
550
|
+
const authError = this.authorizeHost(project, request, body.host_id, body.project_id);
|
|
551
|
+
if (authError) {
|
|
552
|
+
return authError;
|
|
553
|
+
}
|
|
554
|
+
const commands = project.local_host_commands[body.host_id] ?? [];
|
|
555
|
+
const now = new Date().toISOString();
|
|
556
|
+
const hostLabel = project.local_hosts[body.host_id]?.display_name ?? body.host_id;
|
|
557
|
+
const matchedCommand = commands.find((c) => c.command_id === body.command_id);
|
|
558
|
+
const agentLabel = matchedCommand?.agent_id ?? "Agent";
|
|
559
|
+
const commandType = matchedCommand?.command_type ?? null;
|
|
560
|
+
const targetProjectLabel = matchedCommand?.target_project_name ?? matchedCommand?.target_project_id ?? null;
|
|
561
|
+
const roomText = body.status === "failed"
|
|
562
|
+
? commandType === "remove"
|
|
563
|
+
? `${agentLabel} could not be removed from ${hostLabel}`
|
|
564
|
+
: commandType === "switch_project"
|
|
565
|
+
? `${hostLabel} could not switch rooms`
|
|
566
|
+
: `${agentLabel} could not start on ${hostLabel}`
|
|
567
|
+
: commandType === "remove"
|
|
568
|
+
? `${agentLabel} was removed from ${hostLabel}`
|
|
569
|
+
: commandType === "switch_project"
|
|
570
|
+
? `${hostLabel} moved to ${targetProjectLabel ?? "the new room"}`
|
|
571
|
+
: `${agentLabel} joined ${hostLabel}`;
|
|
572
|
+
const eventType = body.status === "failed"
|
|
573
|
+
? commandType === "remove"
|
|
574
|
+
? "session.remove.failed"
|
|
575
|
+
: commandType === "switch_project"
|
|
576
|
+
? "project.switch.failed"
|
|
577
|
+
: "spawn.failed"
|
|
578
|
+
: commandType === "remove"
|
|
579
|
+
? "session.removed"
|
|
580
|
+
: commandType === "switch_project"
|
|
581
|
+
? "project.switched"
|
|
582
|
+
: "spawn.completed";
|
|
583
|
+
const eventDetail = body.status === "failed"
|
|
584
|
+
? commandType === "remove"
|
|
585
|
+
? `${agentLabel} could not be removed`
|
|
586
|
+
: commandType === "switch_project"
|
|
587
|
+
? `${hostLabel} could not switch`
|
|
588
|
+
: `${agentLabel} could not start`
|
|
589
|
+
: commandType === "remove"
|
|
590
|
+
? `${agentLabel} removed`
|
|
591
|
+
: commandType === "switch_project"
|
|
592
|
+
? `${hostLabel} moved to ${targetProjectLabel ?? "the new room"}`
|
|
593
|
+
: `${agentLabel} is ready`;
|
|
594
|
+
const next = {
|
|
595
|
+
...project,
|
|
596
|
+
local_host_commands: {
|
|
597
|
+
...project.local_host_commands,
|
|
598
|
+
[body.host_id]: commands.map((command) => command.command_id === body.command_id
|
|
599
|
+
? {
|
|
600
|
+
...command,
|
|
601
|
+
status: body.status,
|
|
602
|
+
updated_at: now,
|
|
603
|
+
result: {
|
|
604
|
+
session_id: body.session_id,
|
|
605
|
+
note: body.note ?? null,
|
|
606
|
+
error: body.error ?? null,
|
|
607
|
+
},
|
|
608
|
+
}
|
|
609
|
+
: command),
|
|
610
|
+
},
|
|
611
|
+
room: appendRoomSystemMessage(project.room, {
|
|
612
|
+
text: roomText,
|
|
613
|
+
host_id: body.host_id,
|
|
614
|
+
session_id: body.session_id ?? null,
|
|
615
|
+
task_id: matchedCommand?.task_id ?? null,
|
|
616
|
+
target_agent_ids: commandType === "switch_project" ? null : agentLabel !== "Agent" ? [agentLabel] : null,
|
|
617
|
+
}),
|
|
618
|
+
recent_events: prependEvent(project.recent_events, {
|
|
619
|
+
ts: now,
|
|
620
|
+
kind: body.status === "failed" ? "sys" : "evt",
|
|
621
|
+
actor: hostLabel,
|
|
622
|
+
type: eventType,
|
|
623
|
+
detail: eventDetail,
|
|
624
|
+
}),
|
|
625
|
+
};
|
|
626
|
+
await this.save(next);
|
|
627
|
+
return Response.json({ ok: true });
|
|
628
|
+
}
|
|
629
|
+
return new Response("Not found", { status: 404 });
|
|
630
|
+
}
|
|
631
|
+
authorizeHost(project, request, hostId, projectId) {
|
|
632
|
+
const token = readBearerToken(request);
|
|
633
|
+
if (!token) {
|
|
634
|
+
return new Response("Missing bearer token", { status: 401 });
|
|
635
|
+
}
|
|
636
|
+
const claims = parseHostToken(token);
|
|
637
|
+
if (!claims || claims.host_id !== hostId || claims.project_id !== projectId) {
|
|
638
|
+
return new Response("Unauthorized", { status: 401 });
|
|
639
|
+
}
|
|
640
|
+
const registration = project.host_registrations[hostId];
|
|
641
|
+
if (!registration || registration.host_token !== token) {
|
|
642
|
+
return new Response("Unauthorized", { status: 401 });
|
|
643
|
+
}
|
|
644
|
+
return null;
|
|
645
|
+
}
|
|
646
|
+
async load() {
|
|
647
|
+
if (this.projectCache) {
|
|
648
|
+
return this.projectCache;
|
|
649
|
+
}
|
|
650
|
+
const project = (await this.state.storage.get("project")) ?? null;
|
|
651
|
+
const normalized = project ? normalizeProjectState(project) : null;
|
|
652
|
+
if (normalized) {
|
|
653
|
+
this.projectCache = normalized;
|
|
654
|
+
}
|
|
655
|
+
return normalized;
|
|
656
|
+
}
|
|
657
|
+
async ensure(project_id, name) {
|
|
658
|
+
const existing = await this.load();
|
|
659
|
+
if (existing) {
|
|
660
|
+
return existing;
|
|
661
|
+
}
|
|
662
|
+
const created = {
|
|
663
|
+
summary: {
|
|
664
|
+
project_id,
|
|
665
|
+
name,
|
|
666
|
+
status: "active",
|
|
667
|
+
current_manifest_id: null,
|
|
668
|
+
current_manifest_seq: null,
|
|
669
|
+
accepted_understanding: null,
|
|
670
|
+
active_task_count: 0,
|
|
671
|
+
project_status_seq: 0,
|
|
672
|
+
},
|
|
673
|
+
status_feed: {},
|
|
674
|
+
active_tasks: {},
|
|
675
|
+
recent_events: [],
|
|
676
|
+
local_hosts: {},
|
|
677
|
+
local_host_commands: {},
|
|
678
|
+
host_registrations: {},
|
|
679
|
+
room: buildDefaultRoom(),
|
|
680
|
+
};
|
|
681
|
+
await this.save(created);
|
|
682
|
+
return created;
|
|
683
|
+
}
|
|
684
|
+
async save(project) {
|
|
685
|
+
const normalized = normalizeProjectState(project);
|
|
686
|
+
this.projectCache = normalized;
|
|
687
|
+
await this.state.storage.put("project", normalized);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
function buildDefaultRoom() {
|
|
691
|
+
return {
|
|
692
|
+
settings: {
|
|
693
|
+
all_agents_listening: true,
|
|
694
|
+
conductor_name: "You",
|
|
695
|
+
},
|
|
696
|
+
next_seq: 0,
|
|
697
|
+
messages: [],
|
|
698
|
+
};
|
|
699
|
+
}
|
|
700
|
+
function prependEvent(events, event) {
|
|
701
|
+
return [event, ...events].slice(0, 120);
|
|
702
|
+
}
|
|
703
|
+
function truncate(value, maxLength) {
|
|
704
|
+
if (value.length <= maxLength) {
|
|
705
|
+
return value;
|
|
706
|
+
}
|
|
707
|
+
return `${value.slice(0, maxLength - 1)}…`;
|
|
708
|
+
}
|
|
709
|
+
function normalizeIdList(value) {
|
|
710
|
+
const ids = (value ?? []).map((entry) => String(entry).trim()).filter(Boolean);
|
|
711
|
+
return ids.length > 0 ? ids : null;
|
|
712
|
+
}
|
|
713
|
+
function appendRoomSystemMessage(room, input) {
|
|
714
|
+
const message = {
|
|
715
|
+
message_id: `msg_${crypto.randomUUID()}`,
|
|
716
|
+
seq: room.next_seq + 1,
|
|
717
|
+
author_type: "system",
|
|
718
|
+
author_id: "system",
|
|
719
|
+
author_label: "system",
|
|
720
|
+
text: input.text,
|
|
721
|
+
created_at: new Date().toISOString(),
|
|
722
|
+
task_id: input.task_id ?? null,
|
|
723
|
+
host_id: input.host_id ?? null,
|
|
724
|
+
session_id: input.session_id ?? null,
|
|
725
|
+
reply_to_seq: null,
|
|
726
|
+
target_host_ids: null,
|
|
727
|
+
target_session_ids: null,
|
|
728
|
+
target_agent_ids: normalizeIdList(input.target_agent_ids),
|
|
729
|
+
};
|
|
730
|
+
return {
|
|
731
|
+
...room,
|
|
732
|
+
next_seq: message.seq,
|
|
733
|
+
messages: [...room.messages, message].slice(-200),
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
function findCommandAgentId(commands, commandId) {
|
|
737
|
+
return commands.find((command) => command.command_id === commandId)?.agent_id ?? null;
|
|
738
|
+
}
|
|
739
|
+
function findCommandType(commands, commandId) {
|
|
740
|
+
return commands.find((command) => command.command_id === commandId)?.command_type ?? null;
|
|
741
|
+
}
|
|
742
|
+
function findCommandTaskId(commands, commandId) {
|
|
743
|
+
return commands.find((command) => command.command_id === commandId)?.task_id ?? null;
|
|
744
|
+
}
|
|
745
|
+
function findCommandTargetProject(commands, commandId) {
|
|
746
|
+
const command = commands.find((entry) => entry.command_id === commandId);
|
|
747
|
+
return command?.target_project_name ?? command?.target_project_id ?? null;
|
|
748
|
+
}
|
|
749
|
+
function buildRoomEventPayload(message) {
|
|
750
|
+
const payload = {};
|
|
751
|
+
if (message.reply_to_seq != null) {
|
|
752
|
+
payload.reply_to_seq = message.reply_to_seq;
|
|
753
|
+
}
|
|
754
|
+
if (message.target_host_ids?.length) {
|
|
755
|
+
payload.target_host_ids = message.target_host_ids;
|
|
756
|
+
}
|
|
757
|
+
if (message.target_session_ids?.length) {
|
|
758
|
+
payload.target_session_ids = message.target_session_ids;
|
|
759
|
+
}
|
|
760
|
+
if (message.target_agent_ids?.length) {
|
|
761
|
+
payload.target_agent_ids = message.target_agent_ids;
|
|
762
|
+
}
|
|
763
|
+
return Object.keys(payload).length > 0 ? JSON.stringify(payload) : null;
|
|
764
|
+
}
|
|
765
|
+
function shouldDeliverRoomMessageToHost(message, hostId) {
|
|
766
|
+
if (message.target_host_ids?.length) {
|
|
767
|
+
return message.target_host_ids.includes(hostId);
|
|
768
|
+
}
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
function sanitizeId(value) {
|
|
772
|
+
return String(value)
|
|
773
|
+
.trim()
|
|
774
|
+
.toLowerCase()
|
|
775
|
+
.replace(/[^a-z0-9_-]+/g, "-")
|
|
776
|
+
.replace(/^-+|-+$/g, "")
|
|
777
|
+
.slice(0, 64);
|
|
778
|
+
}
|
|
779
|
+
const DEFAULT_ENROLL_SECRET = "office-host-enroll-secret";
|
|
780
|
+
function isValidEnrollSecret(providedSecret, configuredSecret) {
|
|
781
|
+
if (!providedSecret) {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
const expected = configuredSecret?.trim() || DEFAULT_ENROLL_SECRET;
|
|
785
|
+
return providedSecret === expected;
|
|
786
|
+
}
|
|
787
|
+
function normalizeProjectState(project) {
|
|
788
|
+
return {
|
|
789
|
+
summary: {
|
|
790
|
+
project_id: project.summary?.project_id ?? "prj_demo",
|
|
791
|
+
name: project.summary?.name ?? project.summary?.project_id ?? "prj_demo",
|
|
792
|
+
status: project.summary?.status ?? "active",
|
|
793
|
+
current_manifest_id: project.summary?.current_manifest_id ?? null,
|
|
794
|
+
current_manifest_seq: project.summary?.current_manifest_seq ?? null,
|
|
795
|
+
accepted_understanding: project.summary?.accepted_understanding ?? null,
|
|
796
|
+
active_task_count: project.summary?.active_task_count ?? 0,
|
|
797
|
+
project_status_seq: project.summary?.project_status_seq ?? 0,
|
|
798
|
+
},
|
|
799
|
+
status_feed: project.status_feed ?? {},
|
|
800
|
+
active_tasks: project.active_tasks ?? {},
|
|
801
|
+
recent_events: project.recent_events ?? [],
|
|
802
|
+
local_hosts: project.local_hosts ?? {},
|
|
803
|
+
local_host_commands: project.local_host_commands ?? {},
|
|
804
|
+
host_registrations: project.host_registrations ?? {},
|
|
805
|
+
room: {
|
|
806
|
+
settings: {
|
|
807
|
+
all_agents_listening: project.room?.settings?.all_agents_listening ?? true,
|
|
808
|
+
conductor_name: project.room?.settings?.conductor_name ?? "You",
|
|
809
|
+
},
|
|
810
|
+
next_seq: project.room?.next_seq ?? 0,
|
|
811
|
+
messages: project.room?.messages ?? [],
|
|
812
|
+
},
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
function withDerivedHostSessions(project) {
|
|
816
|
+
const nextLocalHosts = {};
|
|
817
|
+
for (const [hostId, host] of Object.entries(project.local_hosts)) {
|
|
818
|
+
nextLocalHosts[hostId] = {
|
|
819
|
+
...host,
|
|
820
|
+
// Session truth comes from the host heartbeat only. Do not resurrect
|
|
821
|
+
// agents from old command history when revisiting archived rooms.
|
|
822
|
+
sessions: (host.sessions ?? []).filter((session) => session.status === "running"),
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
return {
|
|
826
|
+
...project,
|
|
827
|
+
local_hosts: nextLocalHosts,
|
|
828
|
+
};
|
|
829
|
+
}
|