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.
Files changed (40) hide show
  1. package/.runtime-dist/scripts/bundle-host-package.js +46 -0
  2. package/.runtime-dist/scripts/demo-multi-agent.js +130 -0
  3. package/.runtime-dist/scripts/home-agent-host.js +1403 -0
  4. package/.runtime-dist/scripts/host-doctor.js +28 -0
  5. package/.runtime-dist/scripts/host-login.js +32 -0
  6. package/.runtime-dist/scripts/host-menu.js +227 -0
  7. package/.runtime-dist/scripts/host-open.js +20 -0
  8. package/.runtime-dist/scripts/install-host.js +108 -0
  9. package/.runtime-dist/scripts/lib/host-config.js +171 -0
  10. package/.runtime-dist/scripts/lib/local-runner.js +287 -0
  11. package/.runtime-dist/scripts/office-cli.js +698 -0
  12. package/.runtime-dist/scripts/run-local-project.js +277 -0
  13. package/.runtime-dist/src/auth/session-token.js +62 -0
  14. package/.runtime-dist/src/discord/outbox-ledger.js +56 -0
  15. package/.runtime-dist/src/do/AgentDO.js +205 -0
  16. package/.runtime-dist/src/do/GatewayShardDO.js +9 -0
  17. package/.runtime-dist/src/do/ProjectDO.js +829 -0
  18. package/.runtime-dist/src/do/TaskDO.js +356 -0
  19. package/.runtime-dist/src/index.js +123 -0
  20. package/.runtime-dist/src/project/office-view.js +405 -0
  21. package/.runtime-dist/src/project/read-model.js +79 -0
  22. package/.runtime-dist/src/routes/agents-bootstrap.js +9 -0
  23. package/.runtime-dist/src/routes/agents-descriptor.js +12 -0
  24. package/.runtime-dist/src/routes/agents-events.js +17 -0
  25. package/.runtime-dist/src/routes/agents-heartbeat.js +21 -0
  26. package/.runtime-dist/src/routes/agents-task-context.js +17 -0
  27. package/.runtime-dist/src/routes/bundles.js +198 -0
  28. package/.runtime-dist/src/routes/local-host.js +49 -0
  29. package/.runtime-dist/src/routes/projects.js +119 -0
  30. package/.runtime-dist/src/routes/tasks.js +67 -0
  31. package/.runtime-dist/src/task/reducer.js +464 -0
  32. package/.runtime-dist/src/types/project.js +1 -0
  33. package/.runtime-dist/src/types/protocol.js +3 -0
  34. package/.runtime-dist/src/types/runtime.js +1 -0
  35. package/README.md +148 -0
  36. package/bin/double-penetration-host.mjs +83 -0
  37. package/package.json +48 -0
  38. package/public/index.html +1581 -0
  39. package/public/install-host.ps1 +64 -0
  40. package/scripts/run-runtime-script.mjs +43 -0
@@ -0,0 +1,198 @@
1
+ import { parseSessionToken, readBearerToken } from "../auth/session-token.js";
2
+ import { buildOfficeSnapshot } from "../project/office-view.js";
3
+ import { fetchProjectState, fetchProjectStatus, fetchProjectTasks, normalizeBlockerThreshold } from "../project/read-model.js";
4
+ export async function handleManifestRead(request, env, manifestId) {
5
+ const session = await requireAuthorizedSession(request, env);
6
+ if (session.current_manifest_id !== manifestId) {
7
+ return new Response("Manifest not available for this session", { status: 403 });
8
+ }
9
+ const taskSnapshot = session.active_task_id
10
+ ? await fetchTaskSnapshot(env, session.active_task_id)
11
+ : null;
12
+ const projectStub = env.PROJECTS.get(env.PROJECTS.idFromName(session.active_project_id));
13
+ const [statusFeed, projectState, tasks] = await Promise.all([
14
+ fetchProjectStatus(projectStub),
15
+ fetchProjectState(projectStub),
16
+ fetchProjectTasks(env, projectStub),
17
+ ]);
18
+ const office = buildOfficeSnapshot({
19
+ project_id: session.active_project_id,
20
+ project_state: projectState,
21
+ tasks,
22
+ blocker_threshold: normalizeBlockerThreshold(env.BLOCKER_THRESHOLD),
23
+ });
24
+ const roomContext = {
25
+ project_id: session.active_project_id,
26
+ as_of: new Date().toISOString(),
27
+ settings: office.room.settings,
28
+ directives: office.attention.human_directives,
29
+ recent_messages: office.room.recent_messages.slice(-12),
30
+ recent_decisions: office.room.recent_decisions.slice(0, 8),
31
+ recent_events: office.recent_events.slice(0, 8),
32
+ };
33
+ return Response.json({
34
+ manifest_id: manifestId,
35
+ manifest_seq: session.current_manifest_seq,
36
+ project_id: session.active_project_id,
37
+ task_id: session.active_task_id,
38
+ task_version: taskSnapshot?.task_version ?? 1,
39
+ reason_code: "bootstrap",
40
+ reason_note: null,
41
+ blobs: [
42
+ {
43
+ blob_id: `task_summary:${session.active_task_id}`,
44
+ kind: "task_summary",
45
+ scope: "task",
46
+ redaction_level: "team",
47
+ etag: `sha256:${manifestId}:task`,
48
+ fetch_url: session.active_task_id ? `/api/bundles/blobs/task_summary:${encodeURIComponent(session.active_task_id)}` : null,
49
+ size_bytes: taskSnapshot ? JSON.stringify(taskSnapshot).length : 0,
50
+ fresh_until: new Date(Date.now() + 5 * 60 * 1000).toISOString(),
51
+ },
52
+ {
53
+ blob_id: `project_snapshot:${session.active_project_id}`,
54
+ kind: "project_snapshot",
55
+ scope: "project",
56
+ redaction_level: "team",
57
+ etag: `sha256:${manifestId}:project`,
58
+ fetch_url: `/api/bundles/blobs/project_snapshot:${encodeURIComponent(session.active_project_id)}`,
59
+ size_bytes: JSON.stringify(office.current_project).length,
60
+ fresh_until: new Date(Date.now() + 60 * 1000).toISOString(),
61
+ },
62
+ {
63
+ blob_id: `room_context:${session.active_project_id}`,
64
+ kind: "room_context",
65
+ scope: "project",
66
+ redaction_level: "team",
67
+ etag: `sha256:${manifestId}:room`,
68
+ fetch_url: `/api/bundles/blobs/room_context:${encodeURIComponent(session.active_project_id)}`,
69
+ size_bytes: JSON.stringify(roomContext).length,
70
+ fresh_until: new Date(Date.now() + 30 * 1000).toISOString(),
71
+ },
72
+ {
73
+ blob_id: `status_feed:${session.active_project_id}`,
74
+ kind: "status_feed",
75
+ scope: "project",
76
+ redaction_level: "team",
77
+ etag: `sha256:${manifestId}:status`,
78
+ fetch_url: `/api/bundles/blobs/status_feed:${encodeURIComponent(session.active_project_id)}`,
79
+ size_bytes: JSON.stringify(statusFeed).length,
80
+ fresh_until: new Date(Date.now() + 60 * 1000).toISOString(),
81
+ },
82
+ ].filter((blob) => blob.fetch_url !== null),
83
+ });
84
+ }
85
+ export async function handleBlobRead(request, env, blobId) {
86
+ const session = await requireAuthorizedSession(request, env);
87
+ if (blobId.startsWith("task_summary:")) {
88
+ const taskId = blobId.slice("task_summary:".length);
89
+ if (taskId !== session.active_task_id) {
90
+ return new Response("Task blob forbidden", { status: 403 });
91
+ }
92
+ return Response.json(await fetchTaskSnapshot(env, taskId));
93
+ }
94
+ if (blobId.startsWith("project_snapshot:")) {
95
+ const projectId = blobId.slice("project_snapshot:".length);
96
+ if (projectId !== session.active_project_id) {
97
+ return new Response("Project blob forbidden", { status: 403 });
98
+ }
99
+ return Response.json(await buildProjectSnapshotBlob(env, projectId));
100
+ }
101
+ if (blobId.startsWith("room_context:")) {
102
+ const projectId = blobId.slice("room_context:".length);
103
+ if (projectId !== session.active_project_id) {
104
+ return new Response("Project blob forbidden", { status: 403 });
105
+ }
106
+ return Response.json(await buildRoomContextBlob(env, projectId));
107
+ }
108
+ if (blobId.startsWith("status_feed:")) {
109
+ const projectId = blobId.slice("status_feed:".length);
110
+ if (projectId !== session.active_project_id) {
111
+ return new Response("Project blob forbidden", { status: 403 });
112
+ }
113
+ return Response.json({
114
+ project_id: projectId,
115
+ as_of: new Date().toISOString(),
116
+ entries: await fetchProjectStatus(env.PROJECTS.get(env.PROJECTS.idFromName(projectId))),
117
+ });
118
+ }
119
+ return new Response("Unknown blob", { status: 404 });
120
+ }
121
+ async function buildProjectSnapshotBlob(env, projectId) {
122
+ const projectStub = env.PROJECTS.get(env.PROJECTS.idFromName(projectId));
123
+ const [projectState, tasks] = await Promise.all([
124
+ fetchProjectState(projectStub),
125
+ fetchProjectTasks(env, projectStub),
126
+ ]);
127
+ const office = buildOfficeSnapshot({
128
+ project_id: projectId,
129
+ project_state: projectState,
130
+ tasks,
131
+ blocker_threshold: normalizeBlockerThreshold(env.BLOCKER_THRESHOLD),
132
+ });
133
+ return {
134
+ project_id: projectId,
135
+ as_of: new Date().toISOString(),
136
+ current_project: office.current_project,
137
+ task_board: {
138
+ now: office.task_board.now.slice(0, 8),
139
+ review: office.task_board.review.slice(0, 8),
140
+ blocked: office.task_board.blocked.slice(0, 8),
141
+ ready: office.task_board.ready.slice(0, 8),
142
+ done: office.task_board.done.slice(0, 8),
143
+ },
144
+ attention: office.attention,
145
+ artifacts: office.artifacts.slice(0, 10),
146
+ hosts: office.hosts.slice(0, 8),
147
+ };
148
+ }
149
+ async function buildRoomContextBlob(env, projectId) {
150
+ const projectStub = env.PROJECTS.get(env.PROJECTS.idFromName(projectId));
151
+ const [projectState, tasks] = await Promise.all([
152
+ fetchProjectState(projectStub),
153
+ fetchProjectTasks(env, projectStub),
154
+ ]);
155
+ const office = buildOfficeSnapshot({
156
+ project_id: projectId,
157
+ project_state: projectState,
158
+ tasks,
159
+ blocker_threshold: normalizeBlockerThreshold(env.BLOCKER_THRESHOLD),
160
+ });
161
+ return {
162
+ project_id: projectId,
163
+ as_of: new Date().toISOString(),
164
+ settings: office.room.settings,
165
+ human_directives: office.attention.human_directives,
166
+ recent_messages: office.room.recent_messages.slice(-16),
167
+ recent_decisions: office.room.recent_decisions.slice(0, 12),
168
+ recent_events: office.recent_events.slice(0, 12),
169
+ };
170
+ }
171
+ async function requireAuthorizedSession(request, env) {
172
+ const token = readBearerToken(request);
173
+ if (!token) {
174
+ throw new Response("Missing bearer token", { status: 401 });
175
+ }
176
+ const claims = parseSessionToken(token);
177
+ if (!claims) {
178
+ throw new Response("Invalid bearer token", { status: 401 });
179
+ }
180
+ const agentId = env.AGENTS.idFromName(claims.agent_id);
181
+ const sessionResponse = await env.AGENTS.get(agentId).fetch("https://do.internal/session", {
182
+ method: "GET",
183
+ headers: { authorization: `Bearer ${token}` },
184
+ });
185
+ if (!sessionResponse.ok) {
186
+ throw new Response("Unauthorized", { status: 401 });
187
+ }
188
+ return sessionResponse.json();
189
+ }
190
+ async function fetchTaskSnapshot(env, taskId) {
191
+ const task = env.TASKS.get(env.TASKS.idFromName(taskId));
192
+ const response = await task.fetch("https://do.internal/snapshot");
193
+ if (!response.ok) {
194
+ return null;
195
+ }
196
+ const state = await response.json();
197
+ return state?.snapshot ?? state;
198
+ }
@@ -0,0 +1,49 @@
1
+ function forwardHeaders(request) {
2
+ const headers = {
3
+ "content-type": "application/json",
4
+ };
5
+ const authorization = request.headers.get("authorization");
6
+ if (authorization) {
7
+ headers.authorization = authorization;
8
+ }
9
+ const enrollSecret = request.headers.get("x-enroll-secret");
10
+ if (enrollSecret) {
11
+ headers["x-enroll-secret"] = enrollSecret;
12
+ }
13
+ return headers;
14
+ }
15
+ async function forwardToProject(request, env, internalPath) {
16
+ const { projectId, stub } = resolveProjectStub(request, env);
17
+ if (!projectId || !stub) {
18
+ return new Response("projectId required", { status: 400 });
19
+ }
20
+ const body = await request.json();
21
+ return stub.fetch(`https://do.internal${internalPath}`, {
22
+ method: "POST",
23
+ headers: forwardHeaders(request),
24
+ body: JSON.stringify({
25
+ ...body,
26
+ project_id: projectId,
27
+ }),
28
+ });
29
+ }
30
+ export const handleLocalHostRegister = (r, e) => forwardToProject(r, e, "/local-host/register");
31
+ export const handleLocalHostHeartbeat = (r, e) => forwardToProject(r, e, "/local-host/heartbeat");
32
+ export const handleLocalHostCommandQueue = (r, e) => forwardToProject(r, e, "/local-host/commands/queue");
33
+ export const handleLocalHostCommandPull = (r, e) => forwardToProject(r, e, "/local-host/commands/pull");
34
+ export const handleLocalHostRoomPull = (r, e) => forwardToProject(r, e, "/local-host/room/pull");
35
+ export const handleLocalHostCommandComplete = (r, e) => forwardToProject(r, e, "/local-host/commands/complete");
36
+ export const handleLocalHostSessionRemove = (r, e) => forwardToProject(r, e, "/local-host/sessions/remove");
37
+ export const handleLocalHostProjectSwitch = (r, e) => forwardToProject(r, e, "/local-host/switch");
38
+ function resolveProjectStub(request, env) {
39
+ const url = new URL(request.url);
40
+ const parts = url.pathname.split("/").filter(Boolean);
41
+ const projectId = parts[2] ?? null;
42
+ if (!projectId) {
43
+ return { projectId: null, stub: null };
44
+ }
45
+ return {
46
+ projectId,
47
+ stub: env.PROJECTS.get(env.PROJECTS.idFromName(projectId)),
48
+ };
49
+ }
@@ -0,0 +1,119 @@
1
+ import { buildProjectPayload, fetchProjectState, fetchProjectTasks } from "../project/read-model.js";
2
+ import { buildOfficeSnapshot } from "../project/office-view.js";
3
+ import { normalizeBlockerThreshold } from "../project/read-model.js";
4
+ async function respondWithEtag(request, payload) {
5
+ const body = JSON.stringify(payload);
6
+ const digest = await crypto.subtle.digest("SHA-1", new TextEncoder().encode(body));
7
+ const etag = `"${Array.from(new Uint8Array(digest)).map((b) => b.toString(16).padStart(2, "0")).join("")}"`;
8
+ if (request.headers.get("if-none-match") === etag) {
9
+ return new Response(null, { status: 304 });
10
+ }
11
+ return new Response(body, {
12
+ headers: { "content-type": "application/json", etag },
13
+ });
14
+ }
15
+ export async function handleProjectCreate(request, env) {
16
+ const body = await request.json();
17
+ const projectId = String(body.project_id ?? "").trim();
18
+ if (!projectId) {
19
+ return new Response("project_id required", { status: 400 });
20
+ }
21
+ const stub = env.PROJECTS.get(env.PROJECTS.idFromName(projectId));
22
+ return stub.fetch("https://do.internal/init", {
23
+ method: "POST",
24
+ headers: { "content-type": "application/json" },
25
+ body: JSON.stringify({
26
+ project_id: projectId,
27
+ name: String(body.name ?? projectId).trim() || projectId,
28
+ }),
29
+ });
30
+ }
31
+ export async function handleProjectRead(request, env) {
32
+ const url = new URL(request.url);
33
+ const parts = url.pathname.split("/").filter(Boolean);
34
+ const projectId = parts[2];
35
+ if (!projectId) {
36
+ return new Response("projectId required", { status: 400 });
37
+ }
38
+ const stub = env.PROJECTS.get(env.PROJECTS.idFromName(projectId));
39
+ if (parts[3] === "status") {
40
+ return stub.fetch("https://do.internal/status");
41
+ }
42
+ if (parts[3] === "tasks") {
43
+ return Response.json(await fetchProjectTasks(env, stub));
44
+ }
45
+ if (parts[3] === "events") {
46
+ return stub.fetch("https://do.internal/events");
47
+ }
48
+ if (parts[3] === "local-hosts") {
49
+ return stub.fetch("https://do.internal/local-hosts");
50
+ }
51
+ const projectState = await fetchProjectState(stub);
52
+ const tasks = await fetchProjectTasks(env, stub);
53
+ if (parts[3] === "office") {
54
+ const payload = buildOfficeSnapshot({
55
+ project_id: projectId,
56
+ project_state: projectState,
57
+ tasks,
58
+ blocker_threshold: normalizeBlockerThreshold(env.BLOCKER_THRESHOLD),
59
+ });
60
+ return respondWithEtag(request, payload);
61
+ }
62
+ const payload = buildProjectPayload({
63
+ env,
64
+ project_id: projectId,
65
+ project_state: projectState,
66
+ tasks,
67
+ });
68
+ return respondWithEtag(request, payload);
69
+ }
70
+ export async function handleProjectRoomRead(request, env) {
71
+ const url = new URL(request.url);
72
+ const parts = url.pathname.split("/").filter(Boolean);
73
+ const projectId = parts[2];
74
+ if (!projectId) {
75
+ return new Response("projectId required", { status: 400 });
76
+ }
77
+ const stub = env.PROJECTS.get(env.PROJECTS.idFromName(projectId));
78
+ if (parts[4] === "messages") {
79
+ return stub.fetch("https://do.internal/room/messages");
80
+ }
81
+ return stub.fetch("https://do.internal/room");
82
+ }
83
+ export async function handleProjectRoomMessageWrite(request, env) {
84
+ const url = new URL(request.url);
85
+ const parts = url.pathname.split("/").filter(Boolean);
86
+ const projectId = parts[2];
87
+ if (!projectId) {
88
+ return new Response("projectId required", { status: 400 });
89
+ }
90
+ const stub = env.PROJECTS.get(env.PROJECTS.idFromName(projectId));
91
+ const body = await request.json();
92
+ const targetPath = parts[5] === "upsert" ? "https://do.internal/room/messages/upsert" : "https://do.internal/room/messages";
93
+ return stub.fetch(targetPath, {
94
+ method: "POST",
95
+ headers: { "content-type": "application/json" },
96
+ body: JSON.stringify({
97
+ ...body,
98
+ project_id: projectId,
99
+ }),
100
+ });
101
+ }
102
+ export async function handleProjectRoomSettingsWrite(request, env) {
103
+ const url = new URL(request.url);
104
+ const parts = url.pathname.split("/").filter(Boolean);
105
+ const projectId = parts[2];
106
+ if (!projectId) {
107
+ return new Response("projectId required", { status: 400 });
108
+ }
109
+ const stub = env.PROJECTS.get(env.PROJECTS.idFromName(projectId));
110
+ const body = await request.json();
111
+ return stub.fetch("https://do.internal/room/settings", {
112
+ method: "POST",
113
+ headers: { "content-type": "application/json" },
114
+ body: JSON.stringify({
115
+ ...body,
116
+ project_id: projectId,
117
+ }),
118
+ });
119
+ }
@@ -0,0 +1,67 @@
1
+ export async function handleTaskCreate(request, env) {
2
+ const body = await request.json();
3
+ if (!body.project_id || !body.title) {
4
+ return new Response("project_id and title are required", { status: 400 });
5
+ }
6
+ const task_id = body.task_id ?? `tsk_${crypto.randomUUID()}`;
7
+ const taskId = env.TASKS.idFromName(task_id);
8
+ return env.TASKS.get(taskId).fetch("https://do.internal/create-from-discord", {
9
+ method: "POST",
10
+ headers: { "content-type": "application/json" },
11
+ body: JSON.stringify({
12
+ ...body,
13
+ task_id,
14
+ raw_content: body.raw_content ?? body.title,
15
+ requested_by_discord_user_id: body.requested_by_discord_user_id ?? "human_dashboard",
16
+ channel_id: body.channel_id ?? "dashboard-project-channel",
17
+ discord_event_id: body.discord_event_id ?? `dashboard:${crypto.randomUUID()}`,
18
+ thread_id: body.thread_id ?? body.discord_thread_id ?? null,
19
+ collaboration_policy: body.collaboration_policy ?? "multi_agent",
20
+ confirmation_required: body.confirmation_required ?? false,
21
+ }),
22
+ });
23
+ }
24
+ export async function handleTaskHumanConfirm(request, env) {
25
+ const parts = new URL(request.url).pathname.split("/").filter(Boolean);
26
+ const taskId = parts[2];
27
+ if (!taskId) {
28
+ return new Response("taskId required", { status: 400 });
29
+ }
30
+ const body = await request.json();
31
+ const mapped = body.action === "approve"
32
+ ? { ...body, type: "accept", actor_id: body.actor_id ?? "human_dashboard" }
33
+ : body.action === "modify"
34
+ ? {
35
+ ...body,
36
+ type: "correct",
37
+ actor_id: body.actor_id ?? "human_dashboard",
38
+ correction_text: body.correction_text ?? "Human requested modifications before execution.",
39
+ }
40
+ : body.action === "reject"
41
+ ? {
42
+ ...body,
43
+ type: "correct",
44
+ actor_id: body.actor_id ?? "human_dashboard",
45
+ correction_text: body.correction_text ?? "Human rejected the current interpretation. Rework required.",
46
+ }
47
+ : {
48
+ ...body,
49
+ type: body.type ?? "accept",
50
+ actor_id: body.actor_id ?? "human_dashboard",
51
+ };
52
+ const stub = env.TASKS.get(env.TASKS.idFromName(taskId));
53
+ return stub.fetch("https://do.internal/human-confirm", {
54
+ method: "POST",
55
+ headers: { "content-type": "application/json" },
56
+ body: JSON.stringify(mapped),
57
+ });
58
+ }
59
+ export async function handleTaskRead(request, env) {
60
+ const parts = new URL(request.url).pathname.split("/").filter(Boolean);
61
+ const taskId = parts[2];
62
+ if (!taskId) {
63
+ return new Response("taskId required", { status: 400 });
64
+ }
65
+ const stub = env.TASKS.get(env.TASKS.idFromName(taskId));
66
+ return stub.fetch("https://do.internal/snapshot");
67
+ }