@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.
- package/README.md +33 -27
- package/dist/cli/cloud.d.ts +2 -0
- package/dist/cli/cloud.js +262 -0
- package/dist/cli/daemon.js +9 -0
- package/dist/cli/doctor.js +16 -8
- package/dist/cli/init.js +2 -2
- package/dist/cli/invite.d.ts +2 -0
- package/dist/cli/invite.js +48 -0
- package/dist/cli/kraftwerk.js +7 -0
- package/dist/client/relay.d.ts +40 -0
- package/dist/client/relay.js +35 -0
- package/dist/client/routes.d.ts +5 -0
- package/dist/client/routes.js +5 -0
- package/dist/config.d.ts +15 -28
- package/dist/config.js +5 -19
- package/dist/core/chat/channel-turns.d.ts +2 -2
- package/dist/core/chat/channel-turns.js +2 -2
- package/dist/core/chat/sessions.d.ts +20 -3
- package/dist/core/chat/sessions.js +22 -7
- package/dist/core/chat/types.d.ts +21 -0
- package/dist/core/cloud.d.ts +152 -0
- package/dist/core/cloud.js +98 -0
- package/dist/core/routines.d.ts +22 -2
- package/dist/core/routines.js +26 -0
- package/dist/server/api/chats.js +45 -8
- package/dist/server/api/cloud.d.ts +86 -0
- package/dist/server/api/cloud.js +51 -0
- package/dist/server/api/devices.d.ts +15 -9
- package/dist/server/api/devices.js +3 -2
- package/dist/server/api/index.d.ts +108 -11
- package/dist/server/api/index.js +2 -0
- package/dist/server/api/router.d.ts +3 -2
- package/dist/server/api/router.js +12 -0
- package/dist/server/api/workspace.d.ts +16 -1
- package/dist/server/api/workspace.js +2 -1
- package/dist/server/cloud.d.ts +104 -33
- package/dist/server/cloud.js +279 -185
- package/dist/server/hub-context.d.ts +1 -1
- package/dist/server/hub-context.js +3 -3
- package/dist/server/members.d.ts +87 -0
- package/dist/server/members.js +117 -0
- package/dist/server/relay.d.ts +21 -1
- package/dist/server/relay.js +74 -3
- package/dist/server/server.d.ts +3 -2
- package/dist/server/server.js +13 -6
- package/dist/server/trust.d.ts +16 -0
- package/dist/server/trust.js +12 -1
- package/package.json +1 -1
- package/schema/kraftwerk.schema.json +4 -4
- package/web/dist/assets/{dist-T1sNLC62.js → dist-B-yVL5lk.js} +1 -1
- package/web/dist/assets/{dist-DFXmaDc5.js → dist-B9EMB1Ie.js} +1 -1
- package/web/dist/assets/{dist-BQpWYkxm.js → dist-BTc878j7.js} +1 -1
- package/web/dist/assets/{dist-fgB7OEJy.js → dist-C90kUzps.js} +1 -1
- package/web/dist/assets/{dist-l7foWlFx.js → dist-CpLdAqFQ.js} +1 -1
- package/web/dist/assets/{dist-flnLXEzU.js → dist-Crnb8VVO.js} +1 -1
- package/web/dist/assets/{dist-vuciRCSz.js → dist-D0rjnN8S.js} +1 -1
- package/web/dist/assets/{dist-BTVP-6On.js → dist-D4SDXoGw.js} +1 -1
- package/web/dist/assets/{dist-BwIca_Uj.js → dist-DHRhdODt.js} +1 -1
- package/web/dist/assets/{dist-BYkcY-Vl.js → dist-DQbifCX0.js} +1 -1
- package/web/dist/assets/dist-lSJ-O7kT.js +1 -0
- package/web/dist/assets/{editor-C6Ok8NPo.js → editor-DHAdW1g_.js} +3 -3
- package/web/dist/assets/{index-z4tpCCzk.js → index-D7JVAjll.js} +5 -5
- package/web/dist/index.html +1 -1
- package/web/dist-remote/sw.js +1 -1
- package/web/dist/assets/dist-BURf1l-A.js +0 -1
package/dist/core/routines.d.ts
CHANGED
|
@@ -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.
|
|
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
|
-
/**
|
|
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,
|
package/dist/core/routines.js
CHANGED
|
@@ -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 }));
|
package/dist/server/api/chats.js
CHANGED
|
@@ -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 () =>
|
|
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({
|
|
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
|
|
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<"
|
|
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:
|
|
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;
|
package/dist/server/api/index.js
CHANGED
|
@@ -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
|
|
63
|
-
* `
|
|
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:
|
|
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
|
|
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
|
}),
|