@controlflow-ai/daemon 0.1.2 → 0.1.4
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 +54 -6
- package/bin/daemon.js +6 -1
- package/package.json +3 -1
- 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 +795 -28
- package/src/agent-workspace.ts +183 -0
- package/src/app.ts +1970 -79
- package/src/args.ts +54 -7
- package/src/cli.ts +873 -14
- package/src/client.ts +472 -10
- package/src/coco.ts +9 -40
- package/src/codex.ts +33 -5
- package/src/config.ts +28 -4
- package/src/console.ts +230 -20
- package/src/daemon-client.ts +116 -3
- package/src/daemon.ts +937 -99
- package/src/db.ts +3128 -122
- package/src/delivery-ws.ts +269 -0
- package/src/format.ts +4 -1
- package/src/lark/cli.ts +3 -3
- package/src/lark/event-router.ts +60 -4
- package/src/lark/inbound-events.ts +156 -3
- package/src/lark/server-integration.ts +659 -111
- package/src/lark/ws-daemon.ts +136 -10
- package/src/local-api.ts +545 -15
- package/src/local-auth.ts +33 -1
- package/src/message-attachments.ts +71 -0
- package/src/messaging-cli.ts +741 -0
- package/src/messaging-status.ts +669 -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 +69 -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 +362 -0
- package/src/workflow-runtime.ts +275 -0
- package/src/web.ts +0 -904
package/src/local-auth.ts
CHANGED
|
@@ -9,7 +9,21 @@ function hostName(value: string | null): string {
|
|
|
9
9
|
return withoutPort.toLowerCase();
|
|
10
10
|
}
|
|
11
11
|
|
|
12
|
-
export
|
|
12
|
+
export interface LocalApiAgentPrincipal {
|
|
13
|
+
kind: 'agent';
|
|
14
|
+
agent: string;
|
|
15
|
+
runId: string;
|
|
16
|
+
chatId: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface LocalApiDaemonPrincipal {
|
|
20
|
+
kind: 'daemon';
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export type LocalApiPrincipal = LocalApiAgentPrincipal | LocalApiDaemonPrincipal;
|
|
24
|
+
export type RuntimeTokenLookup = (token: string) => LocalApiAgentPrincipal | null | undefined;
|
|
25
|
+
|
|
26
|
+
function assertLocalRequestBasics(request: Request): void {
|
|
13
27
|
const host = hostName(request.headers.get('host'));
|
|
14
28
|
if (!ALLOWED_HOSTS.has(host)) {
|
|
15
29
|
throw new HttpError(403, 'BAD_HOST', 'local daemon host is not allowed');
|
|
@@ -23,7 +37,10 @@ export function assertLocalRequest(request: Request, token: string | string[] |
|
|
|
23
37
|
if (request.method === 'OPTIONS') {
|
|
24
38
|
throw new HttpError(403, 'CORS_DENIED', 'CORS preflight is not allowed');
|
|
25
39
|
}
|
|
40
|
+
}
|
|
26
41
|
|
|
42
|
+
export function assertLocalRequest(request: Request, token: string | string[] | undefined): void {
|
|
43
|
+
assertLocalRequestBasics(request);
|
|
27
44
|
const tokens = Array.isArray(token) ? token.filter(Boolean) : token ? [token] : [];
|
|
28
45
|
if (tokens.length === 0) {
|
|
29
46
|
throw new HttpError(401, 'UNAUTHORIZED', 'local daemon token is required');
|
|
@@ -35,6 +52,21 @@ export function assertLocalRequest(request: Request, token: string | string[] |
|
|
|
35
52
|
}
|
|
36
53
|
}
|
|
37
54
|
|
|
55
|
+
export function authenticateLocalRequest(
|
|
56
|
+
request: Request,
|
|
57
|
+
token: string | string[] | undefined,
|
|
58
|
+
runtimeTokenLookup?: RuntimeTokenLookup,
|
|
59
|
+
): LocalApiPrincipal {
|
|
60
|
+
assertLocalRequestBasics(request);
|
|
61
|
+
const header = request.headers.get('authorization');
|
|
62
|
+
const bearer = header?.startsWith('Bearer ') ? header.slice('Bearer '.length) : '';
|
|
63
|
+
const tokens = Array.isArray(token) ? token.filter(Boolean) : token ? [token] : [];
|
|
64
|
+
if (tokens.some((item) => header === `Bearer ${item}`)) return { kind: 'daemon' };
|
|
65
|
+
const runtimePrincipal = bearer ? runtimeTokenLookup?.(bearer) : null;
|
|
66
|
+
if (runtimePrincipal) return runtimePrincipal;
|
|
67
|
+
throw new HttpError(401, 'UNAUTHORIZED', 'invalid local daemon token');
|
|
68
|
+
}
|
|
69
|
+
|
|
38
70
|
export function assertLoopbackBindHost(host: string): void {
|
|
39
71
|
if (!LOOPBACK_BIND_HOSTS.has(host.toLowerCase())) {
|
|
40
72
|
throw new Error('local daemon API must bind to a loopback host');
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { extname, join } from 'node:path';
|
|
3
|
+
import { HttpError } from './http.js';
|
|
4
|
+
|
|
5
|
+
export const MAX_MESSAGE_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
const ALLOWED_IMAGE_MIME_TYPES = new Set([
|
|
8
|
+
'image/png',
|
|
9
|
+
'image/jpeg',
|
|
10
|
+
'image/webp',
|
|
11
|
+
'image/gif',
|
|
12
|
+
]);
|
|
13
|
+
|
|
14
|
+
const EXTENSION_BY_MIME = new Map([
|
|
15
|
+
['image/png', '.png'],
|
|
16
|
+
['image/jpeg', '.jpg'],
|
|
17
|
+
['image/webp', '.webp'],
|
|
18
|
+
['image/gif', '.gif'],
|
|
19
|
+
['text/plain', '.txt'],
|
|
20
|
+
['text/markdown', '.md'],
|
|
21
|
+
['application/json', '.json'],
|
|
22
|
+
['text/html', '.html'],
|
|
23
|
+
['application/pdf', '.pdf'],
|
|
24
|
+
['application/zip', '.zip'],
|
|
25
|
+
]);
|
|
26
|
+
|
|
27
|
+
export function validateMessageAttachment(input: { kind: string; mimeType: string; content: Uint8Array }): void {
|
|
28
|
+
if (input.kind !== 'image' && input.kind !== 'file') throw new HttpError(400, 'UNSUPPORTED_ATTACHMENT_KIND', 'attachment kind must be image or file');
|
|
29
|
+
if (!input.mimeType.trim()) throw new HttpError(400, 'MISSING_ATTACHMENT_MIME', 'attachment mime_type is required');
|
|
30
|
+
if (input.kind === 'image' && !ALLOWED_IMAGE_MIME_TYPES.has(input.mimeType)) throw new HttpError(400, 'UNSUPPORTED_ATTACHMENT_MIME', 'unsupported image MIME type');
|
|
31
|
+
if (input.content.length === 0) throw new HttpError(400, 'EMPTY_ATTACHMENT', 'attachment content is required');
|
|
32
|
+
if (input.content.length > MAX_MESSAGE_ATTACHMENT_BYTES) throw new HttpError(400, 'ATTACHMENT_TOO_LARGE', 'attachment exceeds size limit');
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function safeAttachmentFilename(input: { id: string; filename?: string | null; mimeType: string }): string {
|
|
36
|
+
const raw = input.filename?.trim() || input.id;
|
|
37
|
+
const sanitized = raw.replace(/[/\\\0]+/g, '-').replace(/[^a-zA-Z0-9._ -]+/g, '-').trim().slice(0, 120) || input.id;
|
|
38
|
+
if (extname(sanitized)) return sanitized;
|
|
39
|
+
return `${sanitized}${EXTENSION_BY_MIME.get(input.mimeType) ?? '.bin'}`;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function safePathSegment(value: string): string {
|
|
43
|
+
return value.replace(/[/\\\0]+/g, '-').replace(/[^a-zA-Z0-9._-]+/g, '-').trim().slice(0, 160) || 'unknown';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function storeMessageAttachmentFile(input: {
|
|
47
|
+
palHome: string;
|
|
48
|
+
roomId: string;
|
|
49
|
+
messageId: number;
|
|
50
|
+
attachmentId: string;
|
|
51
|
+
filename?: string | null;
|
|
52
|
+
mimeType: string;
|
|
53
|
+
content: Uint8Array;
|
|
54
|
+
}): { path: string; filename: string } {
|
|
55
|
+
const filename = safeAttachmentFilename({
|
|
56
|
+
id: input.attachmentId,
|
|
57
|
+
filename: input.filename,
|
|
58
|
+
mimeType: input.mimeType,
|
|
59
|
+
});
|
|
60
|
+
const dir = join(
|
|
61
|
+
input.palHome,
|
|
62
|
+
'rooms',
|
|
63
|
+
safePathSegment(input.roomId),
|
|
64
|
+
'attachments',
|
|
65
|
+
`message-${input.messageId}`,
|
|
66
|
+
);
|
|
67
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
68
|
+
const path = join(dir, `${safePathSegment(input.attachmentId)}-${filename}`);
|
|
69
|
+
writeFileSync(path, input.content, { mode: 0o600 });
|
|
70
|
+
return { path, filename };
|
|
71
|
+
}
|