@controlflow-ai/daemon 0.1.1 → 0.1.3
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 +66 -24
- package/package.json +16 -3
- package/src/agent-avatar.ts +30 -0
- package/src/agent-key.ts +28 -0
- package/src/agent-permissions.ts +359 -0
- package/src/agent-runtime.ts +810 -28
- package/src/agent-workspace.ts +183 -0
- package/src/app.ts +2183 -79
- package/src/args.ts +54 -7
- package/src/cli.ts +873 -14
- package/src/client.ts +482 -12
- package/src/coco.ts +9 -40
- package/src/codex.ts +33 -5
- package/src/config.ts +28 -4
- package/src/console.ts +460 -26
- package/src/daemon-client.ts +116 -3
- package/src/daemon.ts +958 -101
- package/src/db.ts +3216 -113
- package/src/delivery-ws.ts +269 -0
- package/src/format.ts +4 -1
- package/src/lark/app-registration.ts +141 -0
- package/src/lark/cli.ts +7 -137
- package/src/lark/credentials.ts +36 -3
- package/src/lark/event-router.ts +61 -5
- package/src/lark/inbound-events.ts +156 -3
- package/src/lark/server-integration.ts +659 -111
- package/src/lark/setup.ts +74 -5
- package/src/lark/ws-daemon.ts +136 -10
- package/src/local-api.ts +611 -14
- package/src/local-auth.ts +36 -3
- package/src/message-attachments.ts +71 -0
- package/src/messaging-cli.ts +741 -0
- package/src/messaging-status.ts +669 -0
- package/src/migrations/023_projects.ts +65 -0
- package/src/migrations/024_agents_model.ts +10 -0
- package/src/migrations/025_room_archive.ts +44 -0
- package/src/migrations/026_project_archive.ts +44 -0
- package/src/migrations/027_agent_permission_profiles.ts +16 -0
- package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
- package/src/migrations/029_held_message_drafts.ts +32 -0
- package/src/migrations/030_agent_room_read_state.ts +25 -0
- package/src/migrations/031_room_tasks.ts +29 -0
- package/src/migrations/032_room_reminders.ts +29 -0
- package/src/migrations/033_room_saved_messages.ts +25 -0
- package/src/migrations/034_agent_activity_events.ts +27 -0
- package/src/migrations/035_agent_avatars.ts +17 -0
- package/src/migrations/036_project_agent_defaults.ts +21 -0
- package/src/migrations/037_message_attachments.ts +36 -0
- package/src/migrations/038_agent_activity_room_scope.ts +64 -0
- package/src/migrations/039_message_attachments_path.ts +34 -0
- package/src/migrations/040_message_attachments_file_schema.ts +80 -0
- package/src/migrations/041_room_system_events.ts +30 -0
- package/src/migrations/042_message_attachment_file_kind.ts +52 -0
- package/src/migrations/043_room_mode_skill_registry.ts +92 -0
- package/src/migrations/044_workflow_runtime.ts +69 -0
- package/src/migrations/045_skill_repository_ownership.ts +64 -0
- package/src/migrations.ts +70 -1
- package/src/neeko.ts +40 -4
- package/src/runtime-env.ts +179 -0
- package/src/runtime-registry.ts +83 -13
- package/src/server.ts +244 -4
- package/src/token-file.ts +13 -6
- package/src/types.ts +394 -0
- package/src/workflow-runtime.ts +275 -0
- package/src/web.ts +0 -904
package/src/local-api.ts
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { LockClient } from './client.js';
|
|
2
|
-
import {
|
|
1
|
+
import { LockClient, type CreateHandoffRoomRequest } from './client.js';
|
|
2
|
+
import { authenticateLocalRequest, assertLoopbackBindHost, type LocalApiPrincipal, type RuntimeTokenLookup } from './local-auth.js';
|
|
3
3
|
import { failure, HttpError, json, numberParam, readJson, stringParam } from './http.js';
|
|
4
|
-
import
|
|
4
|
+
import { ALL_AGENTS_MENTION } from './db.js';
|
|
5
|
+
import { readdir, stat } from 'node:fs/promises';
|
|
6
|
+
import { homedir } from 'node:os';
|
|
7
|
+
import { dirname, isAbsolute, join, normalize } from 'node:path';
|
|
8
|
+
import type { HeldDraftStatus, MessageFreshnessInput, RemoteFileEntry, RemoteFileList, RoomParticipant, RoomReminderStatus, RoomTaskStatus, RunAction } from './types.js';
|
|
5
9
|
|
|
6
10
|
interface LocalApiOptions {
|
|
7
11
|
serverUrl: string;
|
|
8
12
|
token?: string;
|
|
9
|
-
|
|
13
|
+
controlToken?: string | (() => string | undefined);
|
|
14
|
+
daemonAuth?: ConstructorParameters<typeof LockClient>[1] | (() => ConstructorParameters<typeof LockClient>[1]);
|
|
15
|
+
runtimeTokenLookup?: RuntimeTokenLookup;
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
interface ArtifactBody {
|
|
@@ -32,16 +38,385 @@ interface SendBody {
|
|
|
32
38
|
type?: 'message' | 'system';
|
|
33
39
|
idempotency_key?: string | null;
|
|
34
40
|
mentions?: string[];
|
|
41
|
+
mention_lint?: 'strict' | 'literal' | 'infer';
|
|
42
|
+
attachments?: Array<{
|
|
43
|
+
kind?: 'image' | 'file';
|
|
44
|
+
mime_type?: string;
|
|
45
|
+
filename?: string | null;
|
|
46
|
+
content_base64?: string;
|
|
47
|
+
}>;
|
|
48
|
+
messages?: Array<{
|
|
49
|
+
parent_id?: number;
|
|
50
|
+
channel_id?: string | null;
|
|
51
|
+
recipient?: string | null;
|
|
52
|
+
content?: string;
|
|
53
|
+
type?: 'message' | 'system';
|
|
54
|
+
idempotency_key?: string | null;
|
|
55
|
+
mentions?: string[];
|
|
56
|
+
attachments?: Array<{
|
|
57
|
+
kind?: 'image' | 'file';
|
|
58
|
+
mime_type?: string;
|
|
59
|
+
filename?: string | null;
|
|
60
|
+
content_base64?: string;
|
|
61
|
+
}>;
|
|
62
|
+
}>;
|
|
63
|
+
freshness?: MessageFreshnessInput;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
interface MentionLintResult {
|
|
67
|
+
missing_mentions: string[];
|
|
68
|
+
available_mentions: string[];
|
|
69
|
+
message: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function normalizeSendAttachments(attachments: SendBody['attachments']) {
|
|
73
|
+
return attachments?.map((attachment) => ({
|
|
74
|
+
kind: attachment.kind === 'image' ? 'image' as const : 'file' as const,
|
|
75
|
+
mime_type: attachment.mime_type ?? '',
|
|
76
|
+
filename: attachment.filename,
|
|
77
|
+
content_base64: attachment.content_base64 ?? '',
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function normalizeMentionValue(value: unknown): string {
|
|
82
|
+
if (typeof value !== 'string') return '';
|
|
83
|
+
const mention = value.trim().replace(/^@/, '');
|
|
84
|
+
const lower = mention.toLowerCase();
|
|
85
|
+
if (lower === 'all' || lower === '_all' || mention === '所有人' || mention === ALL_AGENTS_MENTION) return ALL_AGENTS_MENTION;
|
|
86
|
+
return mention;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function mentionBoundaryChar(value: string | undefined): boolean {
|
|
90
|
+
if (!value) return true;
|
|
91
|
+
return !/[\p{Letter}\p{Number}._-]/u.test(value);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function textMentionsKey(content: string, key: string): boolean {
|
|
95
|
+
if (!key) return false;
|
|
96
|
+
let start = 0;
|
|
97
|
+
const needle = `@${key}`;
|
|
98
|
+
while (start < content.length) {
|
|
99
|
+
const index = content.indexOf(needle, start);
|
|
100
|
+
if (index < 0) return false;
|
|
101
|
+
const previous = index > 0 ? content[index - 1] : undefined;
|
|
102
|
+
const next = content[index + needle.length];
|
|
103
|
+
if (mentionBoundaryChar(previous) && mentionBoundaryChar(next)) return true;
|
|
104
|
+
start = index + 1;
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function textMentionsAllAgents(content: string): boolean {
|
|
110
|
+
return /(^|[^\p{Letter}\p{Number}._-])@(?:all|_all|所有人)(?=$|[^\p{Letter}\p{Number}._-])/iu.test(content);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function lintAgentTextMentions(input: {
|
|
114
|
+
sender: string;
|
|
115
|
+
items: Array<{ content?: string; mentions?: string[] }>;
|
|
116
|
+
participants: RoomParticipant[];
|
|
117
|
+
}): MentionLintResult | null {
|
|
118
|
+
const agentKeys = input.participants
|
|
119
|
+
.filter((participant) => participant.kind === 'agent' && participant.status === 'active')
|
|
120
|
+
.map((participant) => participant.participant_id)
|
|
121
|
+
.filter(Boolean);
|
|
122
|
+
if (!agentKeys.includes(input.sender)) return null;
|
|
123
|
+
const available = agentKeys;
|
|
124
|
+
const missing = new Set<string>();
|
|
125
|
+
|
|
126
|
+
for (const item of input.items) {
|
|
127
|
+
const content = item.content ?? '';
|
|
128
|
+
if (!content) continue;
|
|
129
|
+
const explicitMentions = new Set((item.mentions ?? []).map(normalizeMentionValue).filter(Boolean));
|
|
130
|
+
if (textMentionsAllAgents(content) && !explicitMentions.has(ALL_AGENTS_MENTION)) {
|
|
131
|
+
missing.add(ALL_AGENTS_MENTION);
|
|
132
|
+
}
|
|
133
|
+
for (const key of agentKeys) {
|
|
134
|
+
if (key === input.sender) continue;
|
|
135
|
+
if (explicitMentions.has(key)) continue;
|
|
136
|
+
if (textMentionsKey(content, key)) missing.add(key);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (missing.size === 0) return null;
|
|
141
|
+
const display = [...missing].map((mention) => mention === ALL_AGENTS_MENTION ? 'all' : mention);
|
|
142
|
+
return {
|
|
143
|
+
missing_mentions: [...missing],
|
|
144
|
+
available_mentions: available,
|
|
145
|
+
message: `Message text contains ${display.map((mention) => `@${mention}`).join(', ')} but no matching --mention flag was provided.`,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function lintLocalSendMentions(client: LockClient, principal: LocalApiPrincipal, body: SendBody): Promise<MentionLintResult | null> {
|
|
150
|
+
if (body.mention_lint === 'literal') return null;
|
|
151
|
+
const sender = body.sender?.trim() ?? '';
|
|
152
|
+
if (!sender) return null;
|
|
153
|
+
const roomRef = principal.kind === 'agent'
|
|
154
|
+
? principal.chatId
|
|
155
|
+
: body.room_id ?? body.chat_id ?? body.room ?? body.chat;
|
|
156
|
+
if (!roomRef) return null;
|
|
157
|
+
let members: Awaited<ReturnType<LockClient['listRoomMembers']>>;
|
|
158
|
+
try {
|
|
159
|
+
members = await client.listRoomMembers(roomRef);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof HttpError && error.code === 'ROOM_NOT_FOUND') return null;
|
|
162
|
+
throw error;
|
|
163
|
+
}
|
|
164
|
+
const items = body.messages?.length
|
|
165
|
+
? body.messages.map((message) => ({
|
|
166
|
+
content: message.content ?? '',
|
|
167
|
+
mentions: [...(body.mentions ?? []), ...(message.mentions ?? [])],
|
|
168
|
+
}))
|
|
169
|
+
: [{ content: body.content ?? '', mentions: body.mentions ?? [] }];
|
|
170
|
+
return lintAgentTextMentions({ sender, items, participants: members.participants });
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function appendInferredMentions(base: string[] | undefined, missing: string[]): string[] {
|
|
174
|
+
const out: string[] = [];
|
|
175
|
+
const seen = new Set<string>();
|
|
176
|
+
for (const value of [...(base ?? []), ...missing]) {
|
|
177
|
+
const normalized = normalizeMentionValue(value);
|
|
178
|
+
if (!normalized || seen.has(normalized)) continue;
|
|
179
|
+
seen.add(normalized);
|
|
180
|
+
out.push(normalized);
|
|
181
|
+
}
|
|
182
|
+
return out;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
interface CreateRoomTasksBody {
|
|
186
|
+
title?: string;
|
|
187
|
+
tasks?: Array<{ title?: string }>;
|
|
188
|
+
created_by?: string | null;
|
|
189
|
+
source_message_id?: number | null;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
interface ClaimRoomTaskBody {
|
|
193
|
+
assignee?: string;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
interface UpdateRoomTaskBody {
|
|
197
|
+
status?: RoomTaskStatus;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
interface CreateRoomReminderBody {
|
|
201
|
+
msg_id?: number;
|
|
202
|
+
title?: string;
|
|
203
|
+
created_by?: string | null;
|
|
204
|
+
fire_at?: string | null;
|
|
205
|
+
delay_seconds?: number | null;
|
|
206
|
+
repeat?: string | null;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
interface CreateRoomSavedMessageBody {
|
|
210
|
+
msg_id?: number;
|
|
211
|
+
source_message_id?: number;
|
|
212
|
+
saved_by?: string | null;
|
|
213
|
+
note?: string | null;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
interface LocalHandoffRoomBody extends Partial<CreateHandoffRoomRequest> {}
|
|
217
|
+
|
|
218
|
+
interface ReviseHeldDraftBody {
|
|
219
|
+
content?: string;
|
|
35
220
|
}
|
|
36
221
|
|
|
37
222
|
function routeNotFound(): Response {
|
|
38
223
|
return Response.json({ ok: false, code: 'NOT_FOUND', message: 'not found' }, { status: 404 });
|
|
39
224
|
}
|
|
40
225
|
|
|
226
|
+
function requireDaemonPrincipal(principal: LocalApiPrincipal): void {
|
|
227
|
+
if (principal.kind !== 'daemon') {
|
|
228
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token is not allowed for this local API route');
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function requireMessageWritePrincipal(principal: LocalApiPrincipal, body: SendBody): void {
|
|
233
|
+
if (principal.kind === 'daemon') return;
|
|
234
|
+
const sender = body.sender?.trim();
|
|
235
|
+
const chatId = body.room_id ?? body.chat_id;
|
|
236
|
+
if (sender !== principal.agent) {
|
|
237
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only send as its own agent');
|
|
238
|
+
}
|
|
239
|
+
if (!chatId || chatId !== principal.chatId) {
|
|
240
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only send to its active room by chat_id');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function requireAgentRoomScope(principal: LocalApiPrincipal, roomRef: string): void {
|
|
245
|
+
if (principal.kind === 'daemon') return;
|
|
246
|
+
if (roomRef !== principal.chatId) {
|
|
247
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only manage tasks in its active room by chat_id');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function requireAgentHandoffCreate(principal: LocalApiPrincipal, body: LocalHandoffRoomBody): void {
|
|
252
|
+
if (principal.kind === 'daemon') return;
|
|
253
|
+
if ((body.actor?.trim() || '') !== principal.agent) {
|
|
254
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only create handoff rooms as its own agent');
|
|
255
|
+
}
|
|
256
|
+
if ((body.source_room_id?.trim() || '') !== principal.chatId) {
|
|
257
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only create handoff rooms from its active room');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function requireAgentTaskCreator(principal: LocalApiPrincipal, roomRef: string, body: CreateRoomTasksBody): void {
|
|
262
|
+
requireAgentRoomScope(principal, roomRef);
|
|
263
|
+
if (principal.kind === 'daemon') return;
|
|
264
|
+
if ((body.created_by?.trim() || '') !== principal.agent) {
|
|
265
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only create tasks as its own agent');
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
function requireAgentTaskAssignee(principal: LocalApiPrincipal, roomRef: string, body: ClaimRoomTaskBody): void {
|
|
270
|
+
requireAgentRoomScope(principal, roomRef);
|
|
271
|
+
if (principal.kind === 'daemon') return;
|
|
272
|
+
if ((body.assignee?.trim() || '') !== principal.agent) {
|
|
273
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only claim tasks as its own agent');
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function requireAgentReminderCreator(principal: LocalApiPrincipal, body: CreateRoomReminderBody): void {
|
|
278
|
+
if (principal.kind === 'daemon') return;
|
|
279
|
+
if ((body.created_by?.trim() || '') !== principal.agent) {
|
|
280
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only schedule reminders as its own agent');
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
async function requireAgentReminderSource(client: LockClient, principal: LocalApiPrincipal, sourceMessageId: number): Promise<void> {
|
|
285
|
+
if (principal.kind === 'daemon') return;
|
|
286
|
+
const message = await client.getMessage(sourceMessageId, principal.agent);
|
|
287
|
+
if (message.chat_id !== principal.chatId) {
|
|
288
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only schedule reminders from its active room');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function requireAgentReminderAccess(client: LockClient, principal: LocalApiPrincipal, reminderId: string): Promise<void> {
|
|
293
|
+
if (principal.kind === 'daemon') return;
|
|
294
|
+
const reminders = await client.listReminders({
|
|
295
|
+
status: 'all',
|
|
296
|
+
created_by: principal.agent,
|
|
297
|
+
room_id: principal.chatId,
|
|
298
|
+
limit: 500,
|
|
299
|
+
});
|
|
300
|
+
if (!reminders.some((reminder) => reminder.id === reminderId)) {
|
|
301
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only cancel reminders it scheduled in its active room');
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function requireAgentSavedMessageCreator(principal: LocalApiPrincipal, body: CreateRoomSavedMessageBody): void {
|
|
306
|
+
if (principal.kind === 'daemon') return;
|
|
307
|
+
if ((body.saved_by?.trim() || '') !== principal.agent) {
|
|
308
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only save messages as its own agent');
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
async function requireAgentSavedMessageSource(client: LockClient, principal: LocalApiPrincipal, sourceMessageId: number): Promise<void> {
|
|
313
|
+
if (principal.kind === 'daemon') return;
|
|
314
|
+
const message = await client.getMessage(sourceMessageId, principal.agent);
|
|
315
|
+
if (message.chat_id !== principal.chatId) {
|
|
316
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only save messages from its active room');
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
async function requireAgentSavedMessageAccess(client: LockClient, principal: LocalApiPrincipal, savedMessageId: string): Promise<void> {
|
|
321
|
+
if (principal.kind === 'daemon') return;
|
|
322
|
+
const savedMessages = await client.listSavedMessages({
|
|
323
|
+
saved_by: principal.agent,
|
|
324
|
+
room_id: principal.chatId,
|
|
325
|
+
limit: 500,
|
|
326
|
+
});
|
|
327
|
+
if (!savedMessages.some((savedMessage) => savedMessage.id === savedMessageId)) {
|
|
328
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only remove messages it saved in its active room');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function requireAgentHeldDraftAccess(client: LockClient, principal: LocalApiPrincipal, draftId: string): Promise<void> {
|
|
333
|
+
if (principal.kind === 'daemon') return;
|
|
334
|
+
const drafts = await client.listHeldDrafts({
|
|
335
|
+
agent: principal.agent,
|
|
336
|
+
room_id: principal.chatId,
|
|
337
|
+
status: 'held',
|
|
338
|
+
limit: 500,
|
|
339
|
+
});
|
|
340
|
+
if (!drafts.some((draft) => draft.id === draftId)) {
|
|
341
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only manage held drafts it owns in its active room');
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function listRoomsForPrincipal(client: LockClient, principal: LocalApiPrincipal) {
|
|
346
|
+
return principal.kind === 'agent' ? client.listRooms(principal.agent) : client.listRooms();
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
async function resolveRoomForPrincipal(client: LockClient, principal: LocalApiPrincipal, roomRef: string): Promise<string> {
|
|
350
|
+
if (principal.kind === 'daemon') return roomRef;
|
|
351
|
+
const rooms = await listRoomsForPrincipal(client, principal);
|
|
352
|
+
const room = rooms.find((candidate) => candidate.id === roomRef || candidate.name === roomRef);
|
|
353
|
+
if (!room) {
|
|
354
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only read rooms it can participate in');
|
|
355
|
+
}
|
|
356
|
+
return room.id;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
async function canReadDirectory(path: string): Promise<boolean> {
|
|
360
|
+
try {
|
|
361
|
+
await readdir(path);
|
|
362
|
+
return true;
|
|
363
|
+
} catch {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
async function listLocalFiles(input: { path?: string; showHidden?: boolean }): Promise<RemoteFileList> {
|
|
369
|
+
const home = homedir();
|
|
370
|
+
const rawPath = input.path?.trim() || home;
|
|
371
|
+
if (!isAbsolute(rawPath)) throw new HttpError(400, 'PATH_NOT_ABSOLUTE', 'path must be absolute');
|
|
372
|
+
const targetPath = normalize(rawPath);
|
|
373
|
+
const targetStat = await stat(targetPath).catch(() => null);
|
|
374
|
+
if (!targetStat) throw new HttpError(404, 'PATH_NOT_FOUND', 'path was not found');
|
|
375
|
+
if (!targetStat.isDirectory()) throw new HttpError(400, 'PATH_NOT_DIRECTORY', 'path is not a directory');
|
|
376
|
+
|
|
377
|
+
let dirents: Array<{ name: string; isDirectory(): boolean; isFile(): boolean }>;
|
|
378
|
+
try {
|
|
379
|
+
dirents = await readdir(targetPath, { withFileTypes: true }) as Array<{ name: string; isDirectory(): boolean; isFile(): boolean }>;
|
|
380
|
+
} catch {
|
|
381
|
+
throw new HttpError(403, 'PATH_NOT_READABLE', 'directory is not readable');
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const entries = await Promise.all(dirents
|
|
385
|
+
.filter((entry) => input.showHidden || !entry.name.startsWith('.'))
|
|
386
|
+
.map(async (entry): Promise<RemoteFileEntry> => {
|
|
387
|
+
const entryPath = join(targetPath, entry.name);
|
|
388
|
+
const type = entry.isDirectory() ? 'directory' : entry.isFile() ? 'file' : 'other';
|
|
389
|
+
const readable = type === 'directory' ? await canReadDirectory(entryPath) : false;
|
|
390
|
+
return {
|
|
391
|
+
name: entry.name,
|
|
392
|
+
path: entryPath,
|
|
393
|
+
type,
|
|
394
|
+
hidden: entry.name.startsWith('.'),
|
|
395
|
+
readable,
|
|
396
|
+
selectable: type === 'directory' && readable,
|
|
397
|
+
};
|
|
398
|
+
}));
|
|
399
|
+
|
|
400
|
+
entries.sort((left, right) => {
|
|
401
|
+
if (left.type === 'directory' && right.type !== 'directory') return -1;
|
|
402
|
+
if (left.type !== 'directory' && right.type === 'directory') return 1;
|
|
403
|
+
return left.name.localeCompare(right.name);
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
return {
|
|
407
|
+
path: targetPath,
|
|
408
|
+
parent: targetPath === dirname(targetPath) ? null : dirname(targetPath),
|
|
409
|
+
home,
|
|
410
|
+
entries,
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
41
414
|
export async function localRoute(request: Request, options: LocalApiOptions): Promise<Response> {
|
|
42
415
|
try {
|
|
43
|
-
|
|
44
|
-
const
|
|
416
|
+
const controlToken = typeof options.controlToken === 'function' ? options.controlToken() : options.controlToken;
|
|
417
|
+
const principal = authenticateLocalRequest(request, [options.token, controlToken].filter((item): item is string => Boolean(item)), options.runtimeTokenLookup);
|
|
418
|
+
const daemonAuth = typeof options.daemonAuth === 'function' ? options.daemonAuth() : options.daemonAuth;
|
|
419
|
+
const client = new LockClient(options.serverUrl, daemonAuth ?? null);
|
|
45
420
|
const url = new URL(request.url);
|
|
46
421
|
const { pathname } = url;
|
|
47
422
|
|
|
@@ -49,53 +424,264 @@ export async function localRoute(request: Request, options: LocalApiOptions): Pr
|
|
|
49
424
|
return json({ status: 'ok', mode: 'daemon' });
|
|
50
425
|
}
|
|
51
426
|
|
|
427
|
+
if (request.method === 'GET' && pathname === '/local/files') {
|
|
428
|
+
requireDaemonPrincipal(principal);
|
|
429
|
+
const files = await listLocalFiles({
|
|
430
|
+
path: stringParam(url, 'path'),
|
|
431
|
+
showHidden: url.searchParams.get('show_hidden') === 'true',
|
|
432
|
+
});
|
|
433
|
+
return json(files);
|
|
434
|
+
}
|
|
435
|
+
|
|
52
436
|
if (request.method === 'GET' && pathname === '/local/chats') {
|
|
437
|
+
requireDaemonPrincipal(principal);
|
|
53
438
|
return json({ chats: await client.listChats() });
|
|
54
439
|
}
|
|
55
440
|
|
|
56
441
|
if (request.method === 'GET' && pathname === '/local/rooms') {
|
|
57
|
-
return json({ rooms: await client
|
|
442
|
+
return json({ rooms: await listRoomsForPrincipal(client, principal) });
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
if (request.method === 'POST' && pathname === '/local/rooms/handoff') {
|
|
446
|
+
const body = await readJson<LocalHandoffRoomBody>(request);
|
|
447
|
+
requireAgentHandoffCreate(principal, body);
|
|
448
|
+
return json(await client.createHandoffRoom({
|
|
449
|
+
source_room_id: body.source_room_id ?? '',
|
|
450
|
+
source_message_id: body.source_message_id,
|
|
451
|
+
actor: body.actor ?? '',
|
|
452
|
+
purpose: body.purpose ?? '',
|
|
453
|
+
name: body.name ?? '',
|
|
454
|
+
team: body.team,
|
|
455
|
+
handoff_content: body.handoff_content,
|
|
456
|
+
}), 201);
|
|
58
457
|
}
|
|
59
458
|
|
|
60
459
|
const roomMembersMatch = pathname.match(/^\/local\/rooms\/([^/]+)\/members$/);
|
|
61
460
|
if (request.method === 'GET' && roomMembersMatch) {
|
|
62
|
-
|
|
461
|
+
const roomRef = await resolveRoomForPrincipal(client, principal, decodeURIComponent(roomMembersMatch[1]!));
|
|
462
|
+
return json(await client.listRoomMembers(roomRef));
|
|
63
463
|
}
|
|
64
464
|
|
|
65
465
|
|
|
66
466
|
const roomInviteMatch = pathname.match(/^\/local\/rooms\/([^/]+)\/agents$/);
|
|
67
467
|
if (request.method === 'POST' && roomInviteMatch) {
|
|
468
|
+
requireDaemonPrincipal(principal);
|
|
68
469
|
const body = await readJson<{ agent?: string; mode?: string }>(request);
|
|
69
470
|
return json(await client.inviteAgentToRoom(decodeURIComponent(roomInviteMatch[1]!), { agent: body.agent ?? '', mode: body.mode as never }), 201);
|
|
70
471
|
}
|
|
71
472
|
|
|
72
473
|
const roomTopicMatch = pathname.match(/^\/local\/rooms\/([^/]+)\/topics$/);
|
|
73
474
|
if (request.method === 'POST' && roomTopicMatch) {
|
|
475
|
+
requireDaemonPrincipal(principal);
|
|
74
476
|
const body = await readJson<{ name?: string; created_by?: string | null }>(request);
|
|
75
477
|
if (!body.name?.trim()) throw new HttpError(400, 'MISSING_TOPIC_NAME', 'name is required');
|
|
76
478
|
const channel = await client.createTopic(decodeURIComponent(roomTopicMatch[1]!), { name: body.name, created_by: body.created_by });
|
|
77
479
|
return json({ channel }, 201);
|
|
78
480
|
}
|
|
79
481
|
|
|
482
|
+
const roomTasksMatch = pathname.match(/^\/local\/rooms\/([^/]+)\/tasks$/);
|
|
483
|
+
if (request.method === 'GET' && roomTasksMatch) {
|
|
484
|
+
const roomRef = decodeURIComponent(roomTasksMatch[1]!);
|
|
485
|
+
requireAgentRoomScope(principal, roomRef);
|
|
486
|
+
return json({
|
|
487
|
+
tasks: await client.listRoomTasks(
|
|
488
|
+
roomRef,
|
|
489
|
+
(stringParam(url, 'status') ?? 'all') as RoomTaskStatus | 'all',
|
|
490
|
+
numberParam(url, 'limit', 50),
|
|
491
|
+
),
|
|
492
|
+
});
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (request.method === 'POST' && roomTasksMatch) {
|
|
496
|
+
const roomRef = decodeURIComponent(roomTasksMatch[1]!);
|
|
497
|
+
const body = await readJson<CreateRoomTasksBody>(request);
|
|
498
|
+
requireAgentTaskCreator(principal, roomRef, body);
|
|
499
|
+
return json({
|
|
500
|
+
tasks: await client.createRoomTasks(roomRef, {
|
|
501
|
+
title: body.title,
|
|
502
|
+
tasks: body.tasks?.map((task) => ({ title: task.title ?? '' })),
|
|
503
|
+
created_by: body.created_by,
|
|
504
|
+
source_message_id: body.source_message_id,
|
|
505
|
+
}),
|
|
506
|
+
}, 201);
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const roomTaskClaimMatch = pathname.match(/^\/local\/rooms\/([^/]+)\/tasks\/([^/]+)\/claim$/);
|
|
510
|
+
if (request.method === 'POST' && roomTaskClaimMatch) {
|
|
511
|
+
const roomRef = decodeURIComponent(roomTaskClaimMatch[1]!);
|
|
512
|
+
const body = await readJson<ClaimRoomTaskBody>(request);
|
|
513
|
+
requireAgentTaskAssignee(principal, roomRef, body);
|
|
514
|
+
return json({ task: await client.claimRoomTask(roomRef, Number(roomTaskClaimMatch[2]), { assignee: body.assignee ?? '' }) });
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
const roomTaskUnclaimMatch = pathname.match(/^\/local\/rooms\/([^/]+)\/tasks\/([^/]+)\/unclaim$/);
|
|
518
|
+
if (request.method === 'POST' && roomTaskUnclaimMatch) {
|
|
519
|
+
const roomRef = decodeURIComponent(roomTaskUnclaimMatch[1]!);
|
|
520
|
+
requireAgentRoomScope(principal, roomRef);
|
|
521
|
+
return json({ task: await client.unclaimRoomTask(roomRef, Number(roomTaskUnclaimMatch[2])) });
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
const roomTaskStatusMatch = pathname.match(/^\/local\/rooms\/([^/]+)\/tasks\/([^/]+)$/);
|
|
525
|
+
if (request.method === 'PATCH' && roomTaskStatusMatch) {
|
|
526
|
+
const roomRef = decodeURIComponent(roomTaskStatusMatch[1]!);
|
|
527
|
+
requireAgentRoomScope(principal, roomRef);
|
|
528
|
+
const body = await readJson<UpdateRoomTaskBody>(request);
|
|
529
|
+
return json({ task: await client.updateRoomTaskStatus(roomRef, Number(roomTaskStatusMatch[2]), body.status as RoomTaskStatus) });
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
const roomAgentRestartMatch = pathname.match(/^\/local\/rooms\/([^/]+)\/agents\/([^/]+)\/restart$/);
|
|
533
|
+
if (request.method === 'POST' && roomAgentRestartMatch) {
|
|
534
|
+
const roomRef = decodeURIComponent(roomAgentRestartMatch[1]!);
|
|
535
|
+
requireAgentRoomScope(principal, roomRef);
|
|
536
|
+
const restartAgent = decodeURIComponent(roomAgentRestartMatch[2]!);
|
|
537
|
+
if (principal.kind === 'agent' && restartAgent !== principal.agent) {
|
|
538
|
+
throw new HttpError(403, 'LOCAL_AGENT_FORBIDDEN', 'agent-scoped token can only restart its own runtime');
|
|
539
|
+
}
|
|
540
|
+
return json({ run: await client.restartRoomAgent(roomRef, restartAgent) });
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (request.method === 'GET' && pathname === '/local/reminders') {
|
|
544
|
+
return json({
|
|
545
|
+
reminders: await client.listReminders({
|
|
546
|
+
status: (stringParam(url, 'status') ?? 'scheduled') as RoomReminderStatus | 'all',
|
|
547
|
+
created_by: principal.kind === 'agent' ? principal.agent : stringParam(url, 'created_by'),
|
|
548
|
+
room_id: principal.kind === 'agent'
|
|
549
|
+
? principal.chatId
|
|
550
|
+
: stringParam(url, 'room_id') ?? stringParam(url, 'chat_id'),
|
|
551
|
+
limit: numberParam(url, 'limit', 50),
|
|
552
|
+
}),
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
if (request.method === 'POST' && pathname === '/local/reminders') {
|
|
557
|
+
const body = await readJson<CreateRoomReminderBody>(request);
|
|
558
|
+
requireAgentReminderCreator(principal, body);
|
|
559
|
+
const sourceMessageId = Number(body.msg_id);
|
|
560
|
+
if (!Number.isInteger(sourceMessageId) || sourceMessageId < 1) {
|
|
561
|
+
throw new HttpError(400, 'INVALID_SOURCE_MESSAGE_ID', 'msg_id must be a positive integer');
|
|
562
|
+
}
|
|
563
|
+
await requireAgentReminderSource(client, principal, sourceMessageId);
|
|
564
|
+
return json({
|
|
565
|
+
reminder: await client.scheduleReminder({
|
|
566
|
+
msg_id: sourceMessageId,
|
|
567
|
+
title: body.title ?? '',
|
|
568
|
+
created_by: body.created_by,
|
|
569
|
+
fire_at: body.fire_at,
|
|
570
|
+
delay_seconds: body.delay_seconds,
|
|
571
|
+
repeat: body.repeat,
|
|
572
|
+
}),
|
|
573
|
+
}, 201);
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
const reminderCancelMatch = pathname.match(/^\/local\/reminders\/([^/]+)\/cancel$/);
|
|
577
|
+
if (request.method === 'POST' && reminderCancelMatch) {
|
|
578
|
+
const reminderId = decodeURIComponent(reminderCancelMatch[1]!);
|
|
579
|
+
await requireAgentReminderAccess(client, principal, reminderId);
|
|
580
|
+
return json({ reminder: await client.cancelReminder(reminderId) });
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
if (request.method === 'GET' && pathname === '/local/saved-messages') {
|
|
584
|
+
return json({
|
|
585
|
+
saved_messages: await client.listSavedMessages({
|
|
586
|
+
saved_by: principal.kind === 'agent' ? principal.agent : stringParam(url, 'saved_by'),
|
|
587
|
+
room_id: principal.kind === 'agent'
|
|
588
|
+
? principal.chatId
|
|
589
|
+
: stringParam(url, 'room_id') ?? stringParam(url, 'chat_id'),
|
|
590
|
+
limit: numberParam(url, 'limit', 50),
|
|
591
|
+
}),
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (request.method === 'POST' && pathname === '/local/saved-messages') {
|
|
596
|
+
const body = await readJson<CreateRoomSavedMessageBody>(request);
|
|
597
|
+
requireAgentSavedMessageCreator(principal, body);
|
|
598
|
+
const sourceMessageId = Number(body.msg_id ?? body.source_message_id);
|
|
599
|
+
if (!Number.isInteger(sourceMessageId) || sourceMessageId < 1) {
|
|
600
|
+
throw new HttpError(400, 'INVALID_SOURCE_MESSAGE_ID', 'msg_id must be a positive integer');
|
|
601
|
+
}
|
|
602
|
+
await requireAgentSavedMessageSource(client, principal, sourceMessageId);
|
|
603
|
+
return json({
|
|
604
|
+
saved_message: await client.saveMessage({
|
|
605
|
+
msg_id: sourceMessageId,
|
|
606
|
+
saved_by: body.saved_by ?? '',
|
|
607
|
+
note: body.note,
|
|
608
|
+
}),
|
|
609
|
+
}, 201);
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
const savedMessageDeleteMatch = pathname.match(/^\/local\/saved-messages\/([^/]+)\/delete$/);
|
|
613
|
+
if (request.method === 'POST' && savedMessageDeleteMatch) {
|
|
614
|
+
const savedMessageId = decodeURIComponent(savedMessageDeleteMatch[1]!);
|
|
615
|
+
await requireAgentSavedMessageAccess(client, principal, savedMessageId);
|
|
616
|
+
return json({ saved_message: await client.removeSavedMessage(savedMessageId) });
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
if (request.method === 'GET' && pathname === '/local/held-drafts') {
|
|
620
|
+
const agent = principal.kind === 'agent'
|
|
621
|
+
? principal.agent
|
|
622
|
+
: stringParam(url, 'agent') ?? stringParam(url, 'from') ?? stringParam(url, 'sender');
|
|
623
|
+
if (!agent) throw new HttpError(400, 'MISSING_AGENT', 'held draft list requires agent');
|
|
624
|
+
return json({
|
|
625
|
+
drafts: await client.listHeldDrafts({
|
|
626
|
+
agent,
|
|
627
|
+
room_id: principal.kind === 'agent'
|
|
628
|
+
? principal.chatId
|
|
629
|
+
: stringParam(url, 'room_id') ?? stringParam(url, 'chat_id'),
|
|
630
|
+
status: (stringParam(url, 'status') ?? 'held') as HeldDraftStatus | 'all',
|
|
631
|
+
limit: numberParam(url, 'limit', 50),
|
|
632
|
+
}),
|
|
633
|
+
});
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
const heldDraftActionMatch = pathname.match(/^\/local\/held-drafts\/([^/]+)\/(abandon|stay-silent|send-anyway|revise)$/);
|
|
637
|
+
if (request.method === 'POST' && heldDraftActionMatch) {
|
|
638
|
+
const draftId = decodeURIComponent(heldDraftActionMatch[1]!);
|
|
639
|
+
const action = heldDraftActionMatch[2]!;
|
|
640
|
+
await requireAgentHeldDraftAccess(client, principal, draftId);
|
|
641
|
+
if (action === 'revise') {
|
|
642
|
+
const body = await readJson<ReviseHeldDraftBody>(request);
|
|
643
|
+
return json(await client.reviseHeldDraft(draftId, body.content ?? ''));
|
|
644
|
+
}
|
|
645
|
+
if (action === 'send-anyway') {
|
|
646
|
+
return json(await client.sendHeldDraftAnyway(draftId));
|
|
647
|
+
}
|
|
648
|
+
return json(await client.abandonHeldDraft(draftId));
|
|
649
|
+
}
|
|
650
|
+
|
|
80
651
|
if (request.method === 'GET' && pathname === '/local/messages') {
|
|
81
652
|
const params = new URLSearchParams();
|
|
82
653
|
for (const key of ['chat', 'room', 'chat_id', 'room_id', 'parent_id', 'channel_id', 'after', 'limit', 'q']) {
|
|
83
654
|
const value = url.searchParams.get(key);
|
|
84
655
|
if (value !== null) params.set(key, value);
|
|
85
656
|
}
|
|
657
|
+
if (principal.kind === 'agent') {
|
|
658
|
+
const chatId = params.get('chat_id') ?? params.get('room_id');
|
|
659
|
+
const chat = params.get('chat') ?? params.get('room');
|
|
660
|
+
if (!chatId && !chat) throw new HttpError(400, 'MISSING_ROOM_SCOPE', 'agent-scoped message reads require chat_id or chat');
|
|
661
|
+
params.set('agent_scope', principal.agent);
|
|
662
|
+
}
|
|
86
663
|
return json({ messages: await client.getMessages(params) });
|
|
87
664
|
}
|
|
88
665
|
|
|
89
666
|
const messageMatch = pathname.match(/^\/local\/messages\/(\d+)$/);
|
|
90
667
|
if (request.method === 'GET' && messageMatch) {
|
|
91
|
-
return json({ message: await client.getMessage(Number(messageMatch[1])) });
|
|
668
|
+
return json({ message: await client.getMessage(Number(messageMatch[1]), principal.kind === 'agent' ? principal.agent : undefined) });
|
|
92
669
|
}
|
|
93
670
|
|
|
94
671
|
if (request.method === 'POST' && pathname === '/local/messages') {
|
|
95
672
|
const body = await readJson<SendBody>(request);
|
|
673
|
+
requireMessageWritePrincipal(principal, body);
|
|
96
674
|
if (!body.sender?.trim()) throw new HttpError(400, 'MISSING_SENDER', 'sender is required');
|
|
97
|
-
|
|
98
|
-
|
|
675
|
+
const hasContent = body.content?.trim()
|
|
676
|
+
|| body.attachments?.length
|
|
677
|
+
|| body.messages?.some((message) => message.content?.trim() || message.attachments?.length);
|
|
678
|
+
if (!hasContent) throw new HttpError(400, 'MISSING_CONTENT', 'content or attachments are required');
|
|
679
|
+
const mentionLint = await lintLocalSendMentions(client, principal, body);
|
|
680
|
+
if (mentionLint && body.mention_lint !== 'infer') {
|
|
681
|
+
return json({ mention_lint: mentionLint });
|
|
682
|
+
}
|
|
683
|
+
const inferredMentions = mentionLint?.missing_mentions ?? [];
|
|
684
|
+
const result = await client.sendMessageResult({
|
|
99
685
|
chat: body.room ?? body.chat,
|
|
100
686
|
room: body.room,
|
|
101
687
|
chat_id: body.room_id ?? body.chat_id,
|
|
@@ -104,19 +690,29 @@ export async function localRoute(request: Request, options: LocalApiOptions): Pr
|
|
|
104
690
|
channel_id: body.channel_id,
|
|
105
691
|
sender: body.sender,
|
|
106
692
|
recipient: body.recipient,
|
|
107
|
-
content: body.content,
|
|
693
|
+
content: body.content ?? '',
|
|
108
694
|
type: body.type,
|
|
109
695
|
idempotency_key: body.idempotency_key,
|
|
110
|
-
mentions: body.mentions,
|
|
696
|
+
mentions: appendInferredMentions(body.mentions, inferredMentions),
|
|
697
|
+
attachments: normalizeSendAttachments(body.attachments),
|
|
698
|
+
messages: body.messages?.map((message) => ({
|
|
699
|
+
...message,
|
|
700
|
+
content: message.content ?? '',
|
|
701
|
+
mentions: appendInferredMentions(message.mentions, inferredMentions),
|
|
702
|
+
attachments: normalizeSendAttachments(message.attachments),
|
|
703
|
+
})),
|
|
704
|
+
freshness: body.freshness,
|
|
111
705
|
});
|
|
112
|
-
return json(
|
|
706
|
+
return json(result, result.message ? 201 : 202);
|
|
113
707
|
}
|
|
114
708
|
|
|
115
709
|
if (request.method === 'GET' && pathname === '/local/runs') {
|
|
710
|
+
requireDaemonPrincipal(principal);
|
|
116
711
|
return json({ runs: await client.listRuns() });
|
|
117
712
|
}
|
|
118
713
|
|
|
119
714
|
if (request.method === 'POST' && pathname === '/local/artifacts') {
|
|
715
|
+
requireDaemonPrincipal(principal);
|
|
120
716
|
const body = await readJson<ArtifactBody>(request);
|
|
121
717
|
if (body.source_path || body.path) throw new HttpError(400, 'PATH_NOT_ALLOWED', 'artifact upload must include content, not a path');
|
|
122
718
|
if (!body.content_base64) throw new HttpError(400, 'MISSING_CONTENT', 'content_base64 is required');
|
|
@@ -133,6 +729,7 @@ export async function localRoute(request: Request, options: LocalApiOptions): Pr
|
|
|
133
729
|
|
|
134
730
|
const actionMatch = pathname.match(/^\/local\/runs\/([^/]+)\/(kill|restart)$/);
|
|
135
731
|
if (request.method === 'POST' && actionMatch) {
|
|
732
|
+
requireDaemonPrincipal(principal);
|
|
136
733
|
const run = await client.requestRunAction(actionMatch[1]!, actionMatch[2] as RunAction);
|
|
137
734
|
return json({ run });
|
|
138
735
|
}
|