@controlflow-ai/daemon 0.1.2 → 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.
Files changed (61) hide show
  1. package/README.md +54 -6
  2. package/package.json +3 -1
  3. package/src/agent-avatar.ts +30 -0
  4. package/src/agent-key.ts +28 -0
  5. package/src/agent-permissions.ts +359 -0
  6. package/src/agent-runtime.ts +795 -28
  7. package/src/agent-workspace.ts +183 -0
  8. package/src/app.ts +1970 -79
  9. package/src/args.ts +54 -7
  10. package/src/cli.ts +873 -14
  11. package/src/client.ts +472 -10
  12. package/src/coco.ts +9 -40
  13. package/src/codex.ts +33 -5
  14. package/src/config.ts +28 -4
  15. package/src/console.ts +230 -20
  16. package/src/daemon-client.ts +116 -3
  17. package/src/daemon.ts +936 -98
  18. package/src/db.ts +3128 -122
  19. package/src/delivery-ws.ts +269 -0
  20. package/src/format.ts +4 -1
  21. package/src/lark/cli.ts +3 -3
  22. package/src/lark/event-router.ts +60 -4
  23. package/src/lark/inbound-events.ts +156 -3
  24. package/src/lark/server-integration.ts +659 -111
  25. package/src/lark/ws-daemon.ts +136 -10
  26. package/src/local-api.ts +545 -15
  27. package/src/local-auth.ts +33 -1
  28. package/src/message-attachments.ts +71 -0
  29. package/src/messaging-cli.ts +741 -0
  30. package/src/messaging-status.ts +669 -0
  31. package/src/migrations/024_agents_model.ts +10 -0
  32. package/src/migrations/025_room_archive.ts +44 -0
  33. package/src/migrations/026_project_archive.ts +44 -0
  34. package/src/migrations/027_agent_permission_profiles.ts +16 -0
  35. package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
  36. package/src/migrations/029_held_message_drafts.ts +32 -0
  37. package/src/migrations/030_agent_room_read_state.ts +25 -0
  38. package/src/migrations/031_room_tasks.ts +29 -0
  39. package/src/migrations/032_room_reminders.ts +29 -0
  40. package/src/migrations/033_room_saved_messages.ts +25 -0
  41. package/src/migrations/034_agent_activity_events.ts +27 -0
  42. package/src/migrations/035_agent_avatars.ts +17 -0
  43. package/src/migrations/036_project_agent_defaults.ts +21 -0
  44. package/src/migrations/037_message_attachments.ts +36 -0
  45. package/src/migrations/038_agent_activity_room_scope.ts +64 -0
  46. package/src/migrations/039_message_attachments_path.ts +34 -0
  47. package/src/migrations/040_message_attachments_file_schema.ts +80 -0
  48. package/src/migrations/041_room_system_events.ts +30 -0
  49. package/src/migrations/042_message_attachment_file_kind.ts +52 -0
  50. package/src/migrations/043_room_mode_skill_registry.ts +92 -0
  51. package/src/migrations/044_workflow_runtime.ts +69 -0
  52. package/src/migrations/045_skill_repository_ownership.ts +64 -0
  53. package/src/migrations.ts +69 -1
  54. package/src/neeko.ts +40 -4
  55. package/src/runtime-env.ts +179 -0
  56. package/src/runtime-registry.ts +83 -13
  57. package/src/server.ts +244 -4
  58. package/src/token-file.ts +13 -6
  59. package/src/types.ts +362 -0
  60. package/src/workflow-runtime.ts +275 -0
  61. package/src/web.ts +0 -904
@@ -0,0 +1,275 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { resolve } from 'node:path';
3
+ import { Script, createContext } from 'node:vm';
4
+ import type { MessageStore } from './db.js';
5
+ import type { Message, RoomTask, WorkflowNode, WorkflowRunRecord } from './types.js';
6
+
7
+ export interface WorkflowRunOptions {
8
+ filePath: string;
9
+ goal?: string | null;
10
+ createdBy?: string | null;
11
+ executor?: WorkflowAgentExecutor;
12
+ }
13
+
14
+ export interface WorkflowAgentNodeInput {
15
+ title: string;
16
+ role: string;
17
+ context?: Record<string, unknown> | null;
18
+ instruction: string;
19
+ capabilities?: string[];
20
+ outputContract?: Record<string, unknown> | null;
21
+ }
22
+
23
+ export interface WorkflowAgentExecutionInput extends WorkflowAgentNodeInput {
24
+ runId: string;
25
+ nodeId: string;
26
+ }
27
+
28
+ export interface WorkflowAgentExecutionResult {
29
+ output: Record<string, unknown>;
30
+ evidence?: Record<string, unknown> | null;
31
+ }
32
+
33
+ export type WorkflowAgentExecutor = (input: WorkflowAgentExecutionInput) => Promise<WorkflowAgentExecutionResult> | WorkflowAgentExecutionResult;
34
+
35
+ export interface WorkflowContext {
36
+ readonly runId: string;
37
+ readonly goal: string | null;
38
+ phase<T>(title: string, fn: () => Promise<T> | T): Promise<T>;
39
+ agentNode(input: WorkflowAgentNodeInput): Promise<WorkflowNode>;
40
+ collectResults(nodes: WorkflowNode[]): Record<string, unknown>[];
41
+ createTask(input: { roomId: string; title: string; createdBy?: string | null; sourceMessageId?: number | null }): RoomTask;
42
+ sendRoomMessage(input: { roomId?: string; room?: string; sender: string; content: string; mentions?: string[] }): Message;
43
+ waitForNodeResult(node: WorkflowNode): Record<string, unknown>;
44
+ finalize(output: Record<string, unknown>, evidence?: Record<string, unknown> | null): WorkflowNode;
45
+ }
46
+
47
+ type WorkflowModule = {
48
+ default?: unknown;
49
+ workflow?: unknown;
50
+ };
51
+
52
+ const forbiddenWorkflowPatterns: Array<{ pattern: RegExp; label: string }> = [
53
+ { pattern: /^\s*import\s+(?!type\b)/mu, label: 'runtime imports are not allowed in workflow files' },
54
+ { pattern: /^\s*export\s+[^;\n]*\s+from\s+['"]/mu, label: 're-exports are not allowed in workflow files' },
55
+ { pattern: /\bimport\s*\(/u, label: 'dynamic import is not allowed in workflow files' },
56
+ { pattern: /\brequire\s*\(/u, label: 'require is not allowed in workflow files' },
57
+ { pattern: /\b(?:Bun|process|Deno)\b/u, label: 'host runtime globals are not available to workflow files' },
58
+ { pattern: /\b(?:eval|Function)\s*\(/u, label: 'dynamic code evaluation is not allowed in workflow files' },
59
+ ];
60
+
61
+ function assertWorkflowSourceAllowed(filePath: string): void {
62
+ const source = readFileSync(filePath, 'utf8');
63
+ for (const rule of forbiddenWorkflowPatterns) {
64
+ if (rule.pattern.test(source)) {
65
+ throw new Error(`WORKFLOW_SCRIPT_REJECTED: ${rule.label}`);
66
+ }
67
+ }
68
+ }
69
+
70
+ function transformWorkflowSource(source: string): string {
71
+ const transpiler = new Bun.Transpiler({ loader: 'ts' });
72
+ const js = transpiler.transformSync(source);
73
+ return js
74
+ .replace(/\bexport\s+default\s+async\s+function\s+([A-Za-z_$][\w$]*)?/u, (_match, name: string | undefined) => `module.default = async function ${name ?? ''}`)
75
+ .replace(/\bexport\s+default\s+function\s+([A-Za-z_$][\w$]*)?/u, (_match, name: string | undefined) => `module.default = function ${name ?? ''}`)
76
+ .replace(/\bexport\s+default\s+/u, 'module.default = ')
77
+ .replace(/\bexport\s+async\s+function\s+workflow\s*\(/u, 'module.workflow = async function workflow(')
78
+ .replace(/\bexport\s+function\s+workflow\s*\(/u, 'module.workflow = function workflow(');
79
+ }
80
+
81
+ function asRecord(value: unknown): Record<string, unknown> {
82
+ if (value && typeof value === 'object' && !Array.isArray(value)) return value as Record<string, unknown>;
83
+ return { value };
84
+ }
85
+
86
+ function normalizeAgentExecutionResult(value: unknown): WorkflowAgentExecutionResult {
87
+ if (value && typeof value === 'object' && !Array.isArray(value) && 'output' in value) {
88
+ const candidate = value as { output?: unknown; evidence?: unknown };
89
+ return {
90
+ output: asRecord(candidate.output),
91
+ evidence: candidate.evidence === undefined || candidate.evidence === null ? null : asRecord(candidate.evidence),
92
+ };
93
+ }
94
+ return { output: asRecord(value), evidence: null };
95
+ }
96
+
97
+ function errorMessage(error: unknown): string {
98
+ return error instanceof Error ? error.message : String(error);
99
+ }
100
+
101
+ async function loadWorkflowFunction(filePath: string): Promise<(workflow: WorkflowContext) => Promise<unknown> | unknown> {
102
+ assertWorkflowSourceAllowed(filePath);
103
+ const source = readFileSync(filePath, 'utf8');
104
+ const module: WorkflowModule = {};
105
+ const context = createContext({
106
+ module,
107
+ exports: module,
108
+ console: Object.freeze({
109
+ log: console.log.bind(console),
110
+ warn: console.warn.bind(console),
111
+ error: console.error.bind(console),
112
+ }),
113
+ });
114
+ const script = new Script(`'use strict';\n${transformWorkflowSource(source)}`, {
115
+ filename: filePath,
116
+ });
117
+ script.runInContext(context, { timeout: 1000 });
118
+ const workflow = module.default ?? module.workflow;
119
+ if (typeof workflow !== 'function') {
120
+ throw new Error('workflow file must export a default function or named workflow function');
121
+ }
122
+ return workflow as (workflow: WorkflowContext) => Promise<unknown> | unknown;
123
+ }
124
+
125
+ export async function runWorkflowFile(store: MessageStore, options: WorkflowRunOptions): Promise<WorkflowRunRecord> {
126
+ const filePath = resolve(options.filePath);
127
+ if (!existsSync(filePath)) throw new Error(`workflow file was not found: ${filePath}`);
128
+
129
+ const run = store.createWorkflowRun({
130
+ filePath,
131
+ goal: options.goal ?? null,
132
+ createdBy: options.createdBy ?? null,
133
+ });
134
+ const parentStack: string[] = [];
135
+ let finalized = false;
136
+
137
+ const currentParentId = () => parentStack.at(-1) ?? null;
138
+
139
+ const context: WorkflowContext = {
140
+ runId: run.id,
141
+ goal: options.goal ?? null,
142
+
143
+ async phase<T>(title: string, fn: () => Promise<T> | T): Promise<T> {
144
+ const phase = store.createWorkflowNode({
145
+ runId: run.id,
146
+ parentId: currentParentId(),
147
+ kind: 'phase',
148
+ title,
149
+ status: 'running',
150
+ });
151
+ parentStack.push(phase.id);
152
+ try {
153
+ const result = await fn();
154
+ store.updateWorkflowNode(phase.id, { status: 'done', output: asRecord(result ?? { ok: true }) });
155
+ return result;
156
+ } catch (error) {
157
+ store.updateWorkflowNode(phase.id, { status: 'failed', evidence: { error: errorMessage(error) } });
158
+ throw error;
159
+ } finally {
160
+ parentStack.pop();
161
+ }
162
+ },
163
+
164
+ async agentNode(input: WorkflowAgentNodeInput): Promise<WorkflowNode> {
165
+ if (!input.role.trim()) throw new Error('agent node role is required');
166
+ if (!input.instruction.trim()) throw new Error('agent node instruction is required');
167
+ const node = store.createWorkflowNode({
168
+ runId: run.id,
169
+ parentId: currentParentId(),
170
+ kind: 'agent',
171
+ title: input.title,
172
+ role: input.role,
173
+ status: 'running',
174
+ context: input.context ?? null,
175
+ instruction: input.instruction,
176
+ capabilities: input.capabilities ?? [],
177
+ outputContract: input.outputContract ?? null,
178
+ });
179
+ try {
180
+ if (!options.executor) {
181
+ throw new Error('NO_WORKFLOW_AGENT_EXECUTOR: provide a deterministic or real agent executor');
182
+ }
183
+ const execution = normalizeAgentExecutionResult(await options.executor({ ...input, runId: run.id, nodeId: node.id }));
184
+ return store.updateWorkflowNode(node.id, {
185
+ status: 'done',
186
+ output: execution.output,
187
+ evidence: execution.evidence ?? { executor: 'workflow' },
188
+ });
189
+ } catch (error) {
190
+ store.updateWorkflowNode(node.id, { status: 'failed', evidence: { error: errorMessage(error) } });
191
+ throw error;
192
+ }
193
+ },
194
+
195
+ collectResults(nodes: WorkflowNode[]): Record<string, unknown>[] {
196
+ return nodes.map((node) => context.waitForNodeResult(node));
197
+ },
198
+
199
+ createTask(input: { roomId: string; title: string; createdBy?: string | null; sourceMessageId?: number | null }): RoomTask {
200
+ const task = store.createRoomTasks({
201
+ roomId: input.roomId,
202
+ titles: [input.title],
203
+ createdBy: input.createdBy ?? options.createdBy ?? null,
204
+ sourceMessageId: input.sourceMessageId ?? null,
205
+ })[0]!;
206
+ store.createWorkflowNode({
207
+ runId: run.id,
208
+ parentId: currentParentId(),
209
+ kind: 'task',
210
+ title: input.title,
211
+ status: 'done',
212
+ taskId: task.id,
213
+ output: { task_number: task.task_number, status: task.status, room_id: task.room_id },
214
+ evidence: { task_id: task.id },
215
+ });
216
+ return task;
217
+ },
218
+
219
+ sendRoomMessage(input: { roomId?: string; room?: string; sender: string; content: string; mentions?: string[] }): Message {
220
+ const message = store.createMessage({
221
+ chatId: input.roomId,
222
+ chatName: input.room,
223
+ sender: input.sender,
224
+ content: input.content,
225
+ mentions: input.mentions,
226
+ });
227
+ store.createWorkflowNode({
228
+ runId: run.id,
229
+ parentId: currentParentId(),
230
+ kind: 'message',
231
+ title: `Send room message #${message.id}`,
232
+ status: 'done',
233
+ messageId: message.id,
234
+ output: { message_id: message.id, room_id: message.chat_id },
235
+ evidence: { sender: message.sender },
236
+ });
237
+ return message;
238
+ },
239
+
240
+ waitForNodeResult(node: WorkflowNode): Record<string, unknown> {
241
+ const latest = store.getWorkflowNode(node.id);
242
+ if (!latest) throw new Error(`workflow node ${node.id} was not found`);
243
+ if (latest.status !== 'done') throw new Error(`workflow node ${node.id} is not done`);
244
+ return latest.output ?? {};
245
+ },
246
+
247
+ finalize(output: Record<string, unknown>, evidence?: Record<string, unknown> | null): WorkflowNode {
248
+ finalized = true;
249
+ const node = store.createWorkflowNode({
250
+ runId: run.id,
251
+ parentId: currentParentId(),
252
+ kind: 'final',
253
+ title: 'Finalize workflow output',
254
+ status: 'done',
255
+ output,
256
+ evidence: evidence ?? null,
257
+ });
258
+ store.finishWorkflowRun(run.id, { status: 'completed', finalOutput: output });
259
+ return node;
260
+ },
261
+ };
262
+
263
+ try {
264
+ const workflow = await loadWorkflowFunction(filePath);
265
+ const result = await workflow(context);
266
+ if (!finalized) {
267
+ store.finishWorkflowRun(run.id, { status: 'completed', finalOutput: asRecord(result ?? { ok: true }) });
268
+ }
269
+ } catch (error) {
270
+ store.finishWorkflowRun(run.id, { status: 'failed', error: errorMessage(error) });
271
+ throw error;
272
+ }
273
+
274
+ return store.getWorkflowRunRecord(run.id)!;
275
+ }