@netnodeag/kraftwerk 0.76.0 → 0.78.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 (65) hide show
  1. package/README.md +33 -27
  2. package/dist/cli/cloud.d.ts +2 -0
  3. package/dist/cli/cloud.js +262 -0
  4. package/dist/cli/daemon.js +9 -0
  5. package/dist/cli/doctor.js +16 -8
  6. package/dist/cli/init.js +2 -2
  7. package/dist/cli/invite.d.ts +2 -0
  8. package/dist/cli/invite.js +48 -0
  9. package/dist/cli/kraftwerk.js +7 -0
  10. package/dist/client/relay.d.ts +40 -0
  11. package/dist/client/relay.js +35 -0
  12. package/dist/client/routes.d.ts +5 -0
  13. package/dist/client/routes.js +5 -0
  14. package/dist/config.d.ts +15 -28
  15. package/dist/config.js +5 -19
  16. package/dist/core/chat/channel-turns.d.ts +2 -2
  17. package/dist/core/chat/channel-turns.js +2 -2
  18. package/dist/core/chat/sessions.d.ts +20 -3
  19. package/dist/core/chat/sessions.js +22 -7
  20. package/dist/core/chat/types.d.ts +21 -0
  21. package/dist/core/cloud.d.ts +152 -0
  22. package/dist/core/cloud.js +98 -0
  23. package/dist/core/routines.d.ts +22 -2
  24. package/dist/core/routines.js +26 -0
  25. package/dist/server/api/chats.js +45 -8
  26. package/dist/server/api/cloud.d.ts +86 -0
  27. package/dist/server/api/cloud.js +51 -0
  28. package/dist/server/api/devices.d.ts +15 -9
  29. package/dist/server/api/devices.js +3 -2
  30. package/dist/server/api/index.d.ts +108 -11
  31. package/dist/server/api/index.js +2 -0
  32. package/dist/server/api/router.d.ts +3 -2
  33. package/dist/server/api/router.js +12 -0
  34. package/dist/server/api/workspace.d.ts +16 -1
  35. package/dist/server/api/workspace.js +2 -1
  36. package/dist/server/cloud.d.ts +104 -33
  37. package/dist/server/cloud.js +279 -185
  38. package/dist/server/hub-context.d.ts +1 -1
  39. package/dist/server/hub-context.js +3 -3
  40. package/dist/server/members.d.ts +87 -0
  41. package/dist/server/members.js +117 -0
  42. package/dist/server/relay.d.ts +21 -1
  43. package/dist/server/relay.js +74 -3
  44. package/dist/server/server.d.ts +3 -2
  45. package/dist/server/server.js +13 -6
  46. package/dist/server/trust.d.ts +16 -0
  47. package/dist/server/trust.js +12 -1
  48. package/package.json +1 -1
  49. package/schema/kraftwerk.schema.json +4 -4
  50. package/web/dist/assets/{dist-T1sNLC62.js → dist-B-yVL5lk.js} +1 -1
  51. package/web/dist/assets/{dist-DFXmaDc5.js → dist-B9EMB1Ie.js} +1 -1
  52. package/web/dist/assets/{dist-BQpWYkxm.js → dist-BTc878j7.js} +1 -1
  53. package/web/dist/assets/{dist-fgB7OEJy.js → dist-C90kUzps.js} +1 -1
  54. package/web/dist/assets/{dist-l7foWlFx.js → dist-CpLdAqFQ.js} +1 -1
  55. package/web/dist/assets/{dist-flnLXEzU.js → dist-Crnb8VVO.js} +1 -1
  56. package/web/dist/assets/{dist-vuciRCSz.js → dist-D0rjnN8S.js} +1 -1
  57. package/web/dist/assets/{dist-BTVP-6On.js → dist-D4SDXoGw.js} +1 -1
  58. package/web/dist/assets/{dist-BwIca_Uj.js → dist-DHRhdODt.js} +1 -1
  59. package/web/dist/assets/{dist-BYkcY-Vl.js → dist-DQbifCX0.js} +1 -1
  60. package/web/dist/assets/dist-lSJ-O7kT.js +1 -0
  61. package/web/dist/assets/{editor-C6Ok8NPo.js → editor-DHAdW1g_.js} +3 -3
  62. package/web/dist/assets/{index-z4tpCCzk.js → index-D7JVAjll.js} +5 -5
  63. package/web/dist/index.html +1 -1
  64. package/web/dist-remote/sw.js +1 -1
  65. package/web/dist/assets/dist-BURf1l-A.js +0 -1
@@ -18,7 +18,12 @@
18
18
  * runs_on is exactly this machine's identity. No runs_on, no runner — an
19
19
  * unassigned routine fires nowhere until someone adopts it ("run these
20
20
  * here"), which writes runs_on and commits routines.yml so the other clones
21
- * learn who runs it. `private:` is stored and round-tripped; nothing reads it yet.
21
+ * learn who runs it.
22
+ *
23
+ * `private: true` keeps a routine's runs to this machine: each run's chat is
24
+ * marked private when it is created (ChatMeta.private), and remote callers —
25
+ * member sessions and paired devices over the relay — never get it listed;
26
+ * the machine's cloud report (routineReport) leaves the routine out too.
22
27
  */
23
28
  export interface Routine {
24
29
  id: string;
@@ -29,7 +34,7 @@ export interface Routine {
29
34
  enabled: boolean;
30
35
  /** The one identity (`handle@machine`) that fires it. Absent: unassigned, fires nowhere. */
31
36
  runsOn?: string;
32
- /** Stored as given; no behaviour yet. */
37
+ /** Its runs' chats stay off remote callers' listings, and out of the cloud's report. */
33
38
  private?: boolean;
34
39
  }
35
40
  /**
@@ -101,6 +106,21 @@ export declare function routineStatuses(slug: string): Promise<RoutineStatus[]>;
101
106
  export declare function runRoutineNow(slug: string, id: string): Promise<{
102
107
  chatId: string;
103
108
  }>;
109
+ /** One routine that ran on this machine, as the machine's cloud heartbeat reports it (server/cloud.ts). */
110
+ export interface RoutineRan {
111
+ /** `<agent>/<routine id>`: the key in routines-state.json. */
112
+ routineId: string;
113
+ name: string;
114
+ /** Null when it never started (only failed to). */
115
+ lastRunAt: string | null;
116
+ /** False when its last attempt could not start (lastError). */
117
+ ok: boolean;
118
+ }
119
+ /**
120
+ * The current workspace's routines that ran (or tried to) on this machine,
121
+ * from routines-state.json and the listings; private routines left out.
122
+ */
123
+ export declare function routineReport(): Promise<RoutineRan[]>;
104
124
  /**
105
125
  * The routines due at `now` that fire on this machine: enabled, runs_on
106
126
  * exactly this identity, cron matching. Everything else — unassigned,
@@ -252,6 +252,7 @@ async function fireRoutine(slug, routine) {
252
252
  agent: def.harness,
253
253
  scope: { kind: "agent", slug: slug, routine: routine.id },
254
254
  title: `⏰ ${routine.name}`,
255
+ ...(routine.private ? { private: true } : {}),
255
256
  });
256
257
  const { error } = await postMessage(meta.id, routine.prompt);
257
258
  if (error)
@@ -288,6 +289,31 @@ export async function runRoutineNow(slug, id) {
288
289
  throw err;
289
290
  }
290
291
  }
292
+ /**
293
+ * The current workspace's routines that ran (or tried to) on this machine,
294
+ * from routines-state.json and the listings; private routines left out.
295
+ */
296
+ export async function routineReport() {
297
+ const state = await readState();
298
+ const out = [];
299
+ const bySlug = new Map();
300
+ for (const [key, s] of Object.entries(state)) {
301
+ if (!s.lastRunAt && !s.lastError)
302
+ continue;
303
+ const cut = key.indexOf("/");
304
+ if (cut <= 0)
305
+ continue;
306
+ const slug = key.slice(0, cut);
307
+ const id = key.slice(cut + 1);
308
+ if (!bySlug.has(slug))
309
+ bySlug.set(slug, await listRoutines(slug).catch(() => []));
310
+ const r = bySlug.get(slug).find((x) => x.id === id);
311
+ if (r?.private)
312
+ continue;
313
+ out.push({ routineId: key, name: r?.name ?? id, lastRunAt: s.lastRunAt ?? null, ok: !s.lastError });
314
+ }
315
+ return out.sort((a, b) => a.routineId.localeCompare(b.routineId));
316
+ }
291
317
  /* ---------- scheduler ---------- */
292
318
  /** The scheduler's timer, one per workspace. */
293
319
  const scheduler = perWorkspace(() => ({ timer: null }));
@@ -2,8 +2,9 @@ import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import * as z from "zod";
4
4
  import { getAgent } from "../../core/agents.js";
5
- import { answerElicitation, cancelChat, createChat, deleteChat, forkChat, getChat, listAgentSessionsFor, listChats, postMessage, renameChat, resetSession, resolvePermission, setChatConfig, setChatProject, setChatVibeable, steerChat, stopChatTask, subscribeChat, } from "../../core/chat/sessions.js";
6
- import { attachmentPath, saveAttachment } from "../../core/chat/store.js";
5
+ import { answerElicitation, cancelChat, createChat, deleteChat, forkChat, getChat, listAgentSessionsFor, listChats, localPerson, postMessage, renameChat, resetSession, resolvePermission, setChatConfig, setChatProject, setChatVibeable, steerChat, stopChatTask, subscribeChat, } from "../../core/chat/sessions.js";
6
+ import { attachmentPath, readMeta, saveAttachment } from "../../core/chat/store.js";
7
+ import { remoteCaller } from "../trust.js";
7
8
  import { getProject } from "../../core/projects.js";
8
9
  import { MIME } from "./mime.js";
9
10
  import { projectErrors } from "./projects.js";
@@ -75,9 +76,39 @@ async function projectSlug(slug) {
75
76
  fail(404, "project not found");
76
77
  return found.slug;
77
78
  }
79
+ /** Who a chat or message is signed with: a member session's member, else this machine's person. */
80
+ const personOf = async (trust) => trust.kind === "member" ? { handle: trust.member.handle, email: trust.member.email } : localPerson();
81
+ /**
82
+ * A private routine's run (meta.private) is not there for a remote caller
83
+ * (trust.ts remoteCaller): a 404, as if it did not exist. This machine and
84
+ * the LAN see it.
85
+ */
86
+ async function hiddenFrom(trust, id) {
87
+ if (!remoteCaller(trust))
88
+ return;
89
+ const meta = await readMeta(id).catch(() => null);
90
+ if (meta?.private)
91
+ fail(404, "not found");
92
+ }
93
+ /** Every route on one chat (`:id`) checks hiddenFrom first. */
94
+ function guarded(r) {
95
+ if (!r.path.includes("/:id"))
96
+ return r;
97
+ const handle = r.handle;
98
+ return {
99
+ ...r,
100
+ handle: async (c) => {
101
+ await hiddenFrom(c.trust, c.params.id);
102
+ return handle(c);
103
+ },
104
+ };
105
+ }
78
106
  /** Chats: ACP sessions with a coding agent, their event stream, and what a person does in them. */
79
107
  export const chatRoutes = [
80
- route({ name: "chats.list", method: "GET", path: "/api/chats", summary: "every chat with its live state" }, async () => ({ chats: await listChats() })),
108
+ route({ name: "chats.list", method: "GET", path: "/api/chats", summary: "every chat with its live state; a remote caller's list leaves private routines' runs out" }, async (c) => {
109
+ const chats = await listChats();
110
+ return { chats: remoteCaller(c.trust) ? chats.filter((x) => !x.private) : chats };
111
+ }),
81
112
  route({ name: "chats.create", method: "POST", path: "/api/chats", summary: "start a chat {agent, scope, resume?, project?}", body: NewChat }, async (c) => {
82
113
  // `resume` continues one of the agent's own sessions (see agents.sessions).
83
114
  const body = await c.body();
@@ -88,7 +119,13 @@ export const chatRoutes = [
88
119
  fail(400, "only an agent's chat takes a project");
89
120
  const project = body.project ? await projectSlug(body.project) : undefined;
90
121
  try {
91
- return await createChat({ agent: agent, scope, ...(project ? { project } : {}), ...(body.resume ? { resume: body.resume } : {}) });
122
+ return await createChat({
123
+ agent: agent,
124
+ scope,
125
+ author: await personOf(c.trust),
126
+ ...(project ? { project } : {}),
127
+ ...(body.resume ? { resume: body.resume } : {}),
128
+ });
92
129
  }
93
130
  catch (err) {
94
131
  throw new ApiError(400, err.message);
@@ -146,7 +183,7 @@ export const chatRoutes = [
146
183
  const { slug } = await c.body();
147
184
  return metaOf(await setChatVibeable(c.params.id, slug || null));
148
185
  }),
149
- route({ name: "chats.fork", method: "POST", path: "/api/chats/:id/fork", summary: "a new chat continuing a copy of this one's session", errors: 400 }, async (c) => metaOf(await forkChat(c.params.id))),
186
+ route({ name: "chats.fork", method: "POST", path: "/api/chats/:id/fork", summary: "a new chat continuing a copy of this one's session", errors: 400 }, async (c) => metaOf(await forkChat(c.params.id, await personOf(c.trust)))),
150
187
  route({ name: "chats.resetSession", method: "POST", path: "/api/chats/:id/reset-session", summary: "forget the agent's session; the next message starts fresh", errors: 400 }, async (c) => metaOf(await resetSession(c.params.id))),
151
188
  // The body is the file itself; x-file-name names it, content-type types it.
152
189
  route({ name: "chats.attach", method: "POST", path: "/api/chats/:id/attachments", summary: "upload an attachment for the next message", errors: 400, upload: true }, async (c) => {
@@ -178,8 +215,8 @@ export const chatRoutes = [
178
215
  res.end(data);
179
216
  }),
180
217
  // Channels: `from` is the poster's display name, which signs the message.
181
- act("chats.message", "/api/chats/:id/message", "send a message {text, attachments?, from?}", Said.extend({ from: z.string().optional() }), (c, body) => postMessage(c.params.id, textOf(body), { from: body.from, attachments: body.attachments })),
182
- act("chats.steer", "/api/chats/:id/steer", "add to the running turn {text, attachments?}", Said, (c, body) => steerChat(c.params.id, textOf(body), body.attachments)),
218
+ act("chats.message", "/api/chats/:id/message", "send a message {text, attachments?, from?}", Said.extend({ from: z.string().optional() }), async (c, body) => postMessage(c.params.id, textOf(body), { from: body.from, attachments: body.attachments, by: await personOf(c.trust) })),
219
+ act("chats.steer", "/api/chats/:id/steer", "add to the running turn {text, attachments?}", Said, async (c, body) => steerChat(c.params.id, textOf(body), body.attachments, await personOf(c.trust))),
183
220
  act("chats.permission", "/api/chats/:id/permission", "answer a permission request {requestId, optionId | null}", z.object({ requestId: z.string().default(""), optionId: z.string().nullable().default(null) }), (c, body) => resolvePermission(c.params.id, body.requestId, body.optionId)),
184
221
  // Channels: `agent` stops one member; without it every agent in the chat.
185
222
  act("chats.cancel", "/api/chats/:id/cancel", "stop the running turn {agent?}", z.object({ agent: z.string().optional() }), (c, body) => cancelChat(c.params.id, body.agent || undefined)),
@@ -190,4 +227,4 @@ export const chatRoutes = [
190
227
  }), (c, { requestId, action, content }) => answerElicitation(c.params.id, requestId, action === "accept" ? { action, content } : { action })),
191
228
  act("chats.stopTask", "/api/chats/:id/task-stop", "stop one background task {taskId}", z.object({ taskId: z.string().default("") }), (c, body) => stopChatTask(c.params.id, body.taskId)),
192
229
  act("chats.config", "/api/chats/:id/config", "set a session option {configId, value}", z.object({ configId: z.string().default(""), value: z.union([z.string(), z.boolean()]).default("") }), (c, body) => setChatConfig(c.params.id, body.configId, body.value)),
193
- ];
230
+ ].map(guarded);
@@ -0,0 +1,86 @@
1
+ import * as z from "zod";
2
+ /**
3
+ * The machine in the kraftwerk cloud (server/cloud.ts): its record, turning
4
+ * it on or off, a round now, releasing the claim. What `kraftwerk cloud`,
5
+ * `login` and `logout` ask the running daemon; a standalone server answers
6
+ * "off" (only the daemon registers). This machine only: the claim code
7
+ * claims the machine. And an invite to one served workspace (`kraftwerk
8
+ * invite`): someone joins it as a remote member, without a clone.
9
+ */
10
+ export declare const cloudRoutes: (import("./router.js").Route<"cloud.invite", import("../../core/cloud.js").InviteResponse & {
11
+ workspace: {
12
+ id: string;
13
+ slug: string;
14
+ name: string;
15
+ };
16
+ }, "/api/cloud/invite", z.ZodObject<{
17
+ slug: z.ZodOptional<z.ZodString>;
18
+ root: z.ZodOptional<z.ZodString>;
19
+ role: z.ZodOptional<z.ZodEnum<{
20
+ member: "member";
21
+ owner: "owner";
22
+ }>>;
23
+ }, z.core.$strip>> | import("./router.js").Route<"cloud.set", {
24
+ daemon: boolean;
25
+ url: string;
26
+ state: import("../cloud.js").CloudState;
27
+ reason?: string;
28
+ source?: import("../../core/cloud.js").CloudUrlSource;
29
+ error?: string;
30
+ id?: string;
31
+ machine?: string;
32
+ account?: string | null;
33
+ interval?: number;
34
+ lastSeen?: string;
35
+ claimCode?: string | null;
36
+ claimUrl?: string | null;
37
+ workspaces?: number;
38
+ }, "/api/cloud", z.ZodObject<{
39
+ enabled: z.ZodBoolean;
40
+ url: z.ZodOptional<z.ZodString>;
41
+ }, z.core.$strip>> | import("./router.js").Route<"cloud.status", {
42
+ daemon: boolean;
43
+ url: string;
44
+ state: import("../cloud.js").CloudState;
45
+ reason?: string;
46
+ source?: import("../../core/cloud.js").CloudUrlSource;
47
+ error?: string;
48
+ id?: string;
49
+ machine?: string;
50
+ account?: string | null;
51
+ interval?: number;
52
+ lastSeen?: string;
53
+ claimCode?: string | null;
54
+ claimUrl?: string | null;
55
+ workspaces?: number;
56
+ }, "/api/cloud", undefined> | import("./router.js").Route<"cloud.sync", {
57
+ daemon: boolean;
58
+ url: string;
59
+ state: import("../cloud.js").CloudState;
60
+ reason?: string;
61
+ source?: import("../../core/cloud.js").CloudUrlSource;
62
+ error?: string;
63
+ id?: string;
64
+ machine?: string;
65
+ account?: string | null;
66
+ interval?: number;
67
+ lastSeen?: string;
68
+ claimCode?: string | null;
69
+ claimUrl?: string | null;
70
+ workspaces?: number;
71
+ }, "/api/cloud/sync", undefined> | import("./router.js").Route<"cloud.unlink", {
72
+ daemon: boolean;
73
+ url: string;
74
+ state: import("../cloud.js").CloudState;
75
+ reason?: string;
76
+ source?: import("../../core/cloud.js").CloudUrlSource;
77
+ error?: string;
78
+ id?: string;
79
+ machine?: string;
80
+ account?: string | null;
81
+ interval?: number;
82
+ lastSeen?: string;
83
+ claimCode?: string | null;
84
+ claimUrl?: string | null;
85
+ workspaces?: number;
86
+ }, "/api/cloud/unlink", undefined>)[];
@@ -0,0 +1,51 @@
1
+ import * as z from "zod";
2
+ import { writeCloudFile } from "../../core/cloud.js";
3
+ import { applyMachineCloud, cloudStatus, createWorkspaceInvite, syncMachineCloud, unlinkMachineCloud } from "../cloud.js";
4
+ import { currentHub } from "../hub-context.js";
5
+ import { route } from "./router.js";
6
+ /**
7
+ * The machine in the kraftwerk cloud (server/cloud.ts): its record, turning
8
+ * it on or off, a round now, releasing the claim. What `kraftwerk cloud`,
9
+ * `login` and `logout` ask the running daemon; a standalone server answers
10
+ * "off" (only the daemon registers). This machine only: the claim code
11
+ * claims the machine. And an invite to one served workspace (`kraftwerk
12
+ * invite`): someone joins it as a remote member, without a clone.
13
+ */
14
+ export const cloudRoutes = [
15
+ route({ name: "cloud.status", method: "GET", path: "/api/cloud", summary: "the machine's cloud record: state, claim code, account, last heartbeat", access: "local" }, async () => ({
16
+ daemon: currentHub().daemon === true,
17
+ ...cloudStatus(),
18
+ })),
19
+ route({
20
+ name: "cloud.set",
21
+ method: "POST",
22
+ path: "/api/cloud",
23
+ summary: "turn the machine's cloud on or off {enabled, url?} (~/.kraftwerk/cloud.json); off says goodbye",
24
+ access: "local",
25
+ body: z.object({ enabled: z.boolean(), url: z.string().url().optional() }),
26
+ }, async (c) => {
27
+ const { enabled, url } = await c.body();
28
+ await writeCloudFile({ enabled, url });
29
+ return { daemon: currentHub().daemon === true, ...(await applyMachineCloud()) };
30
+ }),
31
+ route({ name: "cloud.sync", method: "POST", path: "/api/cloud/sync", summary: "register or heartbeat now; answers the record (a claim made in the cloud shows here)", access: "local" }, async () => ({
32
+ daemon: currentHub().daemon === true,
33
+ ...(await syncMachineCloud()),
34
+ })),
35
+ route({
36
+ name: "cloud.invite",
37
+ method: "POST",
38
+ path: "/api/cloud/invite",
39
+ summary: "an invite to a served workspace {slug | root, role?}: the cloud's join code and link (a claimed machine only)",
40
+ access: "local",
41
+ errors: 409,
42
+ body: z.object({ slug: z.string().optional(), root: z.string().optional(), role: z.enum(["owner", "member"]).optional() }).refine((b) => b.slug || b.root, "name the workspace: slug or root"),
43
+ }, async (c) => {
44
+ const { slug, root, role } = await c.body();
45
+ return createWorkspaceInvite({ slug, root }, role);
46
+ }),
47
+ route({ name: "cloud.unlink", method: "POST", path: "/api/cloud/unlink", summary: "release the machine from its account; the cloud hands out a fresh claim code", access: "local", errors: 400 }, async () => ({
48
+ daemon: currentHub().daemon === true,
49
+ ...(await unlinkMachineCloud()),
50
+ })),
51
+ ];
@@ -16,21 +16,27 @@ export declare const deviceRoutes: (import("./router.js").Route<"devices.code",
16
16
  code: z.ZodString;
17
17
  name: z.ZodDefault<z.ZodString>;
18
18
  }, z.core.$strip>> | import("./router.js").Route<"devices.relay", {
19
+ enabled: boolean;
20
+ url: string | null;
21
+ fingerprint: string | null;
22
+ daemon: boolean;
19
23
  state: import("../relay.js").RelayState;
20
24
  id?: string;
21
25
  key?: string;
22
26
  error?: string;
23
27
  clients: number;
24
28
  since?: string;
25
- enabled: boolean;
26
- url: string | null;
27
- fingerprint: string | null;
28
- daemon: boolean;
29
29
  }, "/api/devices/relay", undefined> | import("./router.js").Route<"devices.revoke", {
30
30
  ok: boolean;
31
31
  }, "/api/devices/:id", undefined> | import("./router.js").Route<"devices.self", {
32
- trust: "device" | "local" | "none";
32
+ trust: "device" | "local" | "member" | "none";
33
33
  device?: import("../../core/devices.js").Device | undefined;
34
+ relay?: boolean | undefined;
35
+ member?: import("../../core/cloud.js").MemberVerdict | undefined;
36
+ workspace?: {
37
+ id: string;
38
+ slug: string;
39
+ } | undefined;
34
40
  }, "/api/devices/self", undefined> | import("./router.js").Route<"devices.setPush", {
35
41
  ok: boolean;
36
42
  }, "/api/devices/push", z.ZodObject<{
@@ -41,16 +47,16 @@ export declare const deviceRoutes: (import("./router.js").Route<"devices.code",
41
47
  }>;
42
48
  key: z.ZodString;
43
49
  }, z.core.$strip>> | import("./router.js").Route<"devices.setRelay", {
50
+ enabled: boolean;
51
+ url: string;
52
+ fingerprint: string;
53
+ daemon: boolean;
44
54
  state: import("../relay.js").RelayState;
45
55
  id?: string;
46
56
  key?: string;
47
57
  error?: string;
48
58
  clients: number;
49
59
  since?: string;
50
- enabled: boolean;
51
- url: string;
52
- fingerprint: string;
53
- daemon: boolean;
54
60
  }, "/api/devices/relay", z.ZodObject<{
55
61
  enabled: z.ZodBoolean;
56
62
  url: z.ZodOptional<z.ZodString>;
@@ -44,9 +44,10 @@ export const deviceRoutes = [
44
44
  throw new ApiError(400, err.message);
45
45
  }
46
46
  }),
47
- route({ name: "devices.self", method: "GET", path: "/api/devices/self", summary: "how this caller is trusted: this machine or a paired device" }, async (c) => ({
47
+ route({ name: "devices.self", method: "GET", path: "/api/devices/self", summary: "how this caller is trusted: this machine, a paired device, or a workspace's member" }, async (c) => ({
48
48
  trust: c.trust.kind,
49
- ...(c.trust.kind === "device" ? { device: c.trust.device } : {}),
49
+ ...(c.trust.kind === "device" ? { device: c.trust.device, ...(c.trust.relay ? { relay: true } : {}) } : {}),
50
+ ...(c.trust.kind === "member" ? { member: c.trust.member, workspace: { id: c.trust.workspaceId, slug: c.trust.slug } } : {}),
50
51
  })),
51
52
  // A paired device's app says where its push notifications go (and the key it seals them with); see server/push.ts.
52
53
  route({
@@ -291,7 +291,83 @@ export declare const routes: (Route<"agents.addJournal", {
291
291
  ok: true;
292
292
  }, "/api/chats/:id/task-stop", z.ZodObject<{
293
293
  taskId: z.ZodDefault<z.ZodString>;
294
- }, z.core.$strip>> | Route<"devices.code", {
294
+ }, z.core.$strip>> | Route<"cloud.invite", import("../../core/cloud.js").InviteResponse & {
295
+ workspace: {
296
+ id: string;
297
+ slug: string;
298
+ name: string;
299
+ };
300
+ }, "/api/cloud/invite", z.ZodObject<{
301
+ slug: z.ZodOptional<z.ZodString>;
302
+ root: z.ZodOptional<z.ZodString>;
303
+ role: z.ZodOptional<z.ZodEnum<{
304
+ member: "member";
305
+ owner: "owner";
306
+ }>>;
307
+ }, z.core.$strip>> | Route<"cloud.set", {
308
+ daemon: boolean;
309
+ url: string;
310
+ state: import("../cloud.js").CloudState;
311
+ reason?: string;
312
+ source?: import("../../core/cloud.js").CloudUrlSource;
313
+ error?: string;
314
+ id?: string;
315
+ machine?: string;
316
+ account?: string | null;
317
+ interval?: number;
318
+ lastSeen?: string;
319
+ claimCode?: string | null;
320
+ claimUrl?: string | null;
321
+ workspaces?: number;
322
+ }, "/api/cloud", z.ZodObject<{
323
+ enabled: z.ZodBoolean;
324
+ url: z.ZodOptional<z.ZodString>;
325
+ }, z.core.$strip>> | Route<"cloud.status", {
326
+ daemon: boolean;
327
+ url: string;
328
+ state: import("../cloud.js").CloudState;
329
+ reason?: string;
330
+ source?: import("../../core/cloud.js").CloudUrlSource;
331
+ error?: string;
332
+ id?: string;
333
+ machine?: string;
334
+ account?: string | null;
335
+ interval?: number;
336
+ lastSeen?: string;
337
+ claimCode?: string | null;
338
+ claimUrl?: string | null;
339
+ workspaces?: number;
340
+ }, "/api/cloud", undefined> | Route<"cloud.sync", {
341
+ daemon: boolean;
342
+ url: string;
343
+ state: import("../cloud.js").CloudState;
344
+ reason?: string;
345
+ source?: import("../../core/cloud.js").CloudUrlSource;
346
+ error?: string;
347
+ id?: string;
348
+ machine?: string;
349
+ account?: string | null;
350
+ interval?: number;
351
+ lastSeen?: string;
352
+ claimCode?: string | null;
353
+ claimUrl?: string | null;
354
+ workspaces?: number;
355
+ }, "/api/cloud/sync", undefined> | Route<"cloud.unlink", {
356
+ daemon: boolean;
357
+ url: string;
358
+ state: import("../cloud.js").CloudState;
359
+ reason?: string;
360
+ source?: import("../../core/cloud.js").CloudUrlSource;
361
+ error?: string;
362
+ id?: string;
363
+ machine?: string;
364
+ account?: string | null;
365
+ interval?: number;
366
+ lastSeen?: string;
367
+ claimCode?: string | null;
368
+ claimUrl?: string | null;
369
+ workspaces?: number;
370
+ }, "/api/cloud/unlink", undefined> | Route<"devices.code", {
295
371
  code: string;
296
372
  expiresAt: string;
297
373
  urls: string[];
@@ -307,21 +383,27 @@ export declare const routes: (Route<"agents.addJournal", {
307
383
  code: z.ZodString;
308
384
  name: z.ZodDefault<z.ZodString>;
309
385
  }, z.core.$strip>> | Route<"devices.relay", {
386
+ enabled: boolean;
387
+ url: string | null;
388
+ fingerprint: string | null;
389
+ daemon: boolean;
310
390
  state: import("../relay.js").RelayState;
311
391
  id?: string;
312
392
  key?: string;
313
393
  error?: string;
314
394
  clients: number;
315
395
  since?: string;
316
- enabled: boolean;
317
- url: string | null;
318
- fingerprint: string | null;
319
- daemon: boolean;
320
396
  }, "/api/devices/relay", undefined> | Route<"devices.revoke", {
321
397
  ok: boolean;
322
398
  }, "/api/devices/:id", undefined> | Route<"devices.self", {
323
- trust: "device" | "local" | "none";
399
+ trust: "device" | "local" | "member" | "none";
324
400
  device?: import("../../core/devices.js").Device | undefined;
401
+ relay?: boolean | undefined;
402
+ member?: import("../../core/cloud.js").MemberVerdict | undefined;
403
+ workspace?: {
404
+ id: string;
405
+ slug: string;
406
+ } | undefined;
325
407
  }, "/api/devices/self", undefined> | Route<"devices.setPush", {
326
408
  ok: boolean;
327
409
  }, "/api/devices/push", z.ZodObject<{
@@ -332,16 +414,16 @@ export declare const routes: (Route<"agents.addJournal", {
332
414
  }>;
333
415
  key: z.ZodString;
334
416
  }, z.core.$strip>> | Route<"devices.setRelay", {
417
+ enabled: boolean;
418
+ url: string;
419
+ fingerprint: string;
420
+ daemon: boolean;
335
421
  state: import("../relay.js").RelayState;
336
422
  id?: string;
337
423
  key?: string;
338
424
  error?: string;
339
425
  clients: number;
340
426
  since?: string;
341
- enabled: boolean;
342
- url: string;
343
- fingerprint: string;
344
- daemon: boolean;
345
427
  }, "/api/devices/relay", z.ZodObject<{
346
428
  enabled: z.ZodBoolean;
347
429
  url: z.ZodOptional<z.ZodString>;
@@ -439,7 +521,22 @@ export declare const routes: (Route<"agents.addJournal", {
439
521
  vibeables: boolean;
440
522
  projects: boolean;
441
523
  publicUrl: string;
442
- cloud: import("../cloud.js").CloudStatus;
524
+ cloud: {
525
+ included: boolean;
526
+ url: string;
527
+ state: import("../cloud.js").CloudState;
528
+ reason?: string;
529
+ source?: import("../../core/cloud.js").CloudUrlSource;
530
+ error?: string;
531
+ id?: string;
532
+ machine?: string;
533
+ account?: string | null;
534
+ interval?: number;
535
+ lastSeen?: string;
536
+ claimCode?: string | null;
537
+ claimUrl?: string | null;
538
+ workspaces?: number;
539
+ };
443
540
  switcher: import("../../config.js").SwitcherEntry[];
444
541
  }, "/api/meta", undefined> | Route<"notifications.clear", {
445
542
  ok: boolean;
@@ -3,6 +3,7 @@ import { agentRoutes } from "./agents.js";
3
3
  import { attentionRoutes } from "./attention.js";
4
4
  import { channelRoutes } from "./channels.js";
5
5
  import { chatRoutes } from "./chats.js";
6
+ import { cloudRoutes } from "./cloud.js";
6
7
  import { deviceRoutes } from "./devices.js";
7
8
  import { hubRoutes } from "./hub.js";
8
9
  import { fileRoutes } from "./files.js";
@@ -35,6 +36,7 @@ const domainRoutes = [
35
36
  ...gitRoutes,
36
37
  ...vibeableRoutes,
37
38
  ...deviceRoutes,
39
+ ...cloudRoutes,
38
40
  ...hubRoutes,
39
41
  ];
40
42
  const protocolRoute = route({ name: "protocol.get", method: "GET", path: "/api/protocol", summary: "the protocol version and every route", access: "public" }, async () => ({
@@ -59,8 +59,9 @@ export interface RouteDef<N extends string, P extends string = string, S extends
59
59
  body?: S;
60
60
  /**
61
61
  * Who may call it. Default: anyone trusted (this machine, a paired
62
- * device). `public`: also unpaired callers (pairing, the protocol).
63
- * `local`: this machine only (managing devices).
62
+ * device, a workspace's member — except the machine-level routes
63
+ * MEMBER_DENIED names). `public`: also unpaired callers (pairing, the
64
+ * protocol). `local`: this machine only (managing devices).
64
65
  */
65
66
  access?: "public" | "local";
66
67
  }
@@ -39,12 +39,24 @@ function validate(route, raw) {
39
39
  }
40
40
  /** Maps an error message to a status by the usual wording: "no <thing>" is 404, "… are off" 409, else 400. */
41
41
  export const statusByMessage = (missing) => (message) => missing.test(message) ? 404 : /are off/.test(message) ? 409 : 400;
42
+ /**
43
+ * What a member session (one workspace, over the relay; see members.ts) may
44
+ * not call: the machine's own business — its workspaces, devices, cloud
45
+ * record, settings, updates, restarts, identity, trash. server.ts already
46
+ * keeps the session inside its workspace; this keeps it out of the machine.
47
+ */
48
+ const MEMBER_DENIED = /^(hub|devices|cloud|settings|server|update|workspaces|identity|trash)\./;
49
+ /** …except asking who it is. */
50
+ const MEMBER_ALLOWED = new Set(["devices.self"]);
42
51
  /** Why a caller may not use a route, or undefined when it may. */
43
52
  function refusal(route, trust) {
44
53
  if (route.access === "public")
45
54
  return undefined;
46
55
  if (trust.kind === "none")
47
56
  return new ApiError(401, "pair this device first", { error: "pair this device first", pair: true });
57
+ if (trust.kind === "member" && (route.access === "local" || (MEMBER_DENIED.test(route.name) && !MEMBER_ALLOWED.has(route.name)))) {
58
+ return new ApiError(403, `${route.name} is not open to a workspace member — only to the machine kraftwerk runs on`);
59
+ }
48
60
  if (route.access === "local" && trust.kind !== "local")
49
61
  return new ApiError(403, "only on the machine kraftwerk runs on");
50
62
  return undefined;
@@ -35,7 +35,22 @@ export declare const workspaceRoutes: (import("./router.js").Route<"identity.get
35
35
  projects: boolean;
36
36
  /** @deprecated Always "": the Cloudflare Tunnel that set it was removed in 0.65. Removed with protocol 2. */
37
37
  publicUrl: string;
38
- cloud: import("../cloud.js").CloudStatus;
38
+ cloud: {
39
+ included: boolean;
40
+ url: string;
41
+ state: import("../cloud.js").CloudState;
42
+ reason?: string;
43
+ source?: import("../../core/cloud.js").CloudUrlSource;
44
+ error?: string;
45
+ id?: string;
46
+ machine?: string;
47
+ account?: string | null;
48
+ interval?: number;
49
+ lastSeen?: string;
50
+ claimCode?: string | null;
51
+ claimUrl?: string | null;
52
+ workspaces?: number;
53
+ };
39
54
  switcher: import("../../config.js").SwitcherEntry[];
40
55
  }, "/api/meta", undefined> | import("./router.js").Route<"search.agents", {
41
56
  workspaces: (import("../../core/search.js").WorkspaceAgents & {
@@ -105,7 +105,8 @@ export const workspaceRoutes = [
105
105
  projects: (ws?.config.projects && ws.config.projects.enabled !== false) === true,
106
106
  /** @deprecated Always "": the Cloudflare Tunnel that set it was removed in 0.65. Removed with protocol 2. */
107
107
  publicUrl: "",
108
- cloud: cloudStatus(),
108
+ // The machine's cloud record (only the daemon registers one); `included`: this workspace rides along (no cloud.enabled: false veto).
109
+ cloud: { ...cloudStatus(), included: ws?.config.cloud?.enabled !== false },
109
110
  switcher,
110
111
  };
111
112
  }),