@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.
- package/README.md +54 -6
- 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 +936 -98
- 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
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 43;
|
|
4
|
+
export const name = 'room_mode_skill_registry';
|
|
5
|
+
|
|
6
|
+
function hasColumn(db: Database, table: string, column: string): boolean {
|
|
7
|
+
const rows = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
8
|
+
return rows.some((row) => row.name === column);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function up(db: Database): void {
|
|
12
|
+
if (!hasColumn(db, 'chats', 'mode')) {
|
|
13
|
+
db.exec(`ALTER TABLE chats ADD COLUMN mode TEXT NOT NULL DEFAULT 'standard' CHECK (mode IN ('standard', 'idea_development'))`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
db.exec(`
|
|
17
|
+
CREATE TABLE IF NOT EXISTS skill_definitions (
|
|
18
|
+
key TEXT PRIMARY KEY,
|
|
19
|
+
name TEXT NOT NULL,
|
|
20
|
+
description TEXT NOT NULL,
|
|
21
|
+
instruction_content TEXT NOT NULL,
|
|
22
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
|
|
23
|
+
version TEXT NOT NULL DEFAULT '1',
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
CREATE TABLE IF NOT EXISTS skill_bindings (
|
|
29
|
+
id TEXT PRIMARY KEY,
|
|
30
|
+
scope TEXT NOT NULL CHECK (scope IN ('project', 'room', 'agent')),
|
|
31
|
+
scope_id TEXT NOT NULL,
|
|
32
|
+
skill_key TEXT NOT NULL REFERENCES skill_definitions(key) ON DELETE CASCADE,
|
|
33
|
+
enabled INTEGER NOT NULL DEFAULT 1 CHECK (enabled IN (0, 1)),
|
|
34
|
+
priority INTEGER NOT NULL DEFAULT 100,
|
|
35
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
36
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37
|
+
UNIQUE(scope, scope_id, skill_key)
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
CREATE INDEX IF NOT EXISTS idx_skill_bindings_lookup
|
|
41
|
+
ON skill_bindings(scope, scope_id, enabled, priority, skill_key);
|
|
42
|
+
|
|
43
|
+
INSERT INTO skill_definitions (
|
|
44
|
+
key, name, description, instruction_content, status, version, created_at, updated_at
|
|
45
|
+
)
|
|
46
|
+
VALUES (
|
|
47
|
+
'idea_development',
|
|
48
|
+
'Idea Development',
|
|
49
|
+
'Develop early ideas through conversation and project .pal documents before implementation handoff.',
|
|
50
|
+
'When this skill is enabled, treat the room as an Idea Development room. Do not rush vague ideas into implementation tasks. Help the user develop intent through conversation, clarify maturity and technical participation level, and maintain relevant .pal documents for user review. Keep implementation handoff separate until the idea is clear enough. Preserve ordinary rooms as ordinary rooms; this workflow applies only where the skill is enabled.',
|
|
51
|
+
'active',
|
|
52
|
+
'1',
|
|
53
|
+
datetime('now'),
|
|
54
|
+
datetime('now')
|
|
55
|
+
)
|
|
56
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
57
|
+
name = excluded.name,
|
|
58
|
+
description = excluded.description,
|
|
59
|
+
instruction_content = excluded.instruction_content,
|
|
60
|
+
status = excluded.status,
|
|
61
|
+
version = excluded.version,
|
|
62
|
+
updated_at = datetime('now');
|
|
63
|
+
|
|
64
|
+
DROP VIEW IF EXISTS chat_stats;
|
|
65
|
+
CREATE VIEW chat_stats AS
|
|
66
|
+
SELECT
|
|
67
|
+
c.id,
|
|
68
|
+
c.name,
|
|
69
|
+
c.display_name,
|
|
70
|
+
c.kind,
|
|
71
|
+
c.server_id,
|
|
72
|
+
c.provider,
|
|
73
|
+
c.dm_type,
|
|
74
|
+
c.capabilities_json,
|
|
75
|
+
c.audit_visibility,
|
|
76
|
+
c.status,
|
|
77
|
+
c.mode,
|
|
78
|
+
c.project_id,
|
|
79
|
+
p.name AS project_name,
|
|
80
|
+
p.root_path AS project_root_path,
|
|
81
|
+
p.computer_id AS project_computer_id,
|
|
82
|
+
pc.name AS project_computer_name,
|
|
83
|
+
c.created_at,
|
|
84
|
+
COUNT(m.id) AS message_count,
|
|
85
|
+
MAX(m.created_at) AS last_message_at
|
|
86
|
+
FROM chats c
|
|
87
|
+
LEFT JOIN projects p ON p.id = c.project_id
|
|
88
|
+
LEFT JOIN computers pc ON pc.id = p.computer_id
|
|
89
|
+
LEFT JOIN messages m ON m.chat_id = c.id
|
|
90
|
+
GROUP BY c.id;
|
|
91
|
+
`);
|
|
92
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 44;
|
|
4
|
+
export const name = 'workflow_runtime';
|
|
5
|
+
|
|
6
|
+
export function up(db: Database): void {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS workflow_runs (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
file_path TEXT NOT NULL,
|
|
11
|
+
goal TEXT,
|
|
12
|
+
status TEXT NOT NULL DEFAULT 'running' CHECK (status IN ('running', 'completed', 'failed')),
|
|
13
|
+
created_by TEXT,
|
|
14
|
+
final_output_json TEXT,
|
|
15
|
+
error TEXT,
|
|
16
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
17
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
18
|
+
completed_at TEXT
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE TABLE IF NOT EXISTS workflow_nodes (
|
|
22
|
+
id TEXT PRIMARY KEY,
|
|
23
|
+
run_id TEXT NOT NULL REFERENCES workflow_runs(id) ON DELETE CASCADE,
|
|
24
|
+
parent_id TEXT REFERENCES workflow_nodes(id) ON DELETE SET NULL,
|
|
25
|
+
kind TEXT NOT NULL CHECK (kind IN ('phase', 'agent', 'task', 'message', 'final')),
|
|
26
|
+
title TEXT NOT NULL,
|
|
27
|
+
role TEXT,
|
|
28
|
+
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'running', 'done', 'failed')),
|
|
29
|
+
context_json TEXT,
|
|
30
|
+
instruction TEXT,
|
|
31
|
+
capabilities_json TEXT NOT NULL DEFAULT '[]',
|
|
32
|
+
output_contract_json TEXT,
|
|
33
|
+
output_json TEXT,
|
|
34
|
+
evidence_json TEXT,
|
|
35
|
+
task_id TEXT REFERENCES room_tasks(id) ON DELETE SET NULL,
|
|
36
|
+
message_id INTEGER REFERENCES messages(id) ON DELETE SET NULL,
|
|
37
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
38
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
39
|
+
completed_at TEXT
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_runs_status
|
|
43
|
+
ON workflow_runs(status, updated_at);
|
|
44
|
+
|
|
45
|
+
CREATE INDEX IF NOT EXISTS idx_workflow_nodes_run_parent
|
|
46
|
+
ON workflow_nodes(run_id, parent_id, created_at);
|
|
47
|
+
|
|
48
|
+
INSERT INTO skill_definitions (
|
|
49
|
+
key, name, description, instruction_content, status, version, created_at, updated_at
|
|
50
|
+
)
|
|
51
|
+
VALUES (
|
|
52
|
+
'workflow_authoring',
|
|
53
|
+
'Workflow Authoring',
|
|
54
|
+
'Author PAL Dynamic Orchestration workflow scripts using controlled runtime primitives.',
|
|
55
|
+
'Use this skill when a complex PAL task should be decomposed into an observable workflow. Write TypeScript or JavaScript workflow files that call PAL workflow primitives for phases, agent nodes, room messages, tasks, result collection, and finalization. Model agent nodes with role, context, instruction, capabilities, output contract, status, and evidence. Keep project filesystem and shell actions inside agent work rather than direct workflow script operations.',
|
|
56
|
+
'active',
|
|
57
|
+
'1',
|
|
58
|
+
datetime('now'),
|
|
59
|
+
datetime('now')
|
|
60
|
+
)
|
|
61
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
62
|
+
name = excluded.name,
|
|
63
|
+
description = excluded.description,
|
|
64
|
+
instruction_content = excluded.instruction_content,
|
|
65
|
+
status = excluded.status,
|
|
66
|
+
version = excluded.version,
|
|
67
|
+
updated_at = datetime('now');
|
|
68
|
+
`);
|
|
69
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 45;
|
|
4
|
+
export const name = 'skill_repository_ownership';
|
|
5
|
+
|
|
6
|
+
function hasColumn(db: Database, table: string, column: string): boolean {
|
|
7
|
+
const rows = db.query(`PRAGMA table_info(${table})`).all() as Array<{ name: string }>;
|
|
8
|
+
return rows.some((row) => row.name === column);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function up(db: Database): void {
|
|
12
|
+
if (!hasColumn(db, 'skill_definitions', 'source')) {
|
|
13
|
+
db.exec(`ALTER TABLE skill_definitions ADD COLUMN source TEXT NOT NULL DEFAULT 'system'`);
|
|
14
|
+
}
|
|
15
|
+
if (!hasColumn(db, 'skill_definitions', 'owner_user_id')) {
|
|
16
|
+
db.exec(`ALTER TABLE skill_definitions ADD COLUMN owner_user_id TEXT`);
|
|
17
|
+
}
|
|
18
|
+
if (!hasColumn(db, 'skill_definitions', 'project_id')) {
|
|
19
|
+
db.exec(`ALTER TABLE skill_definitions ADD COLUMN project_id TEXT REFERENCES projects(id) ON DELETE SET NULL`);
|
|
20
|
+
}
|
|
21
|
+
if (!hasColumn(db, 'skill_definitions', 'repository_path')) {
|
|
22
|
+
db.exec(`ALTER TABLE skill_definitions ADD COLUMN repository_path TEXT`);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
db.exec(`
|
|
26
|
+
UPDATE skill_definitions
|
|
27
|
+
SET source = 'system',
|
|
28
|
+
owner_user_id = NULL,
|
|
29
|
+
project_id = NULL
|
|
30
|
+
WHERE key IN ('idea_development', 'workflow_authoring');
|
|
31
|
+
|
|
32
|
+
CREATE INDEX IF NOT EXISTS idx_skill_definitions_repository
|
|
33
|
+
ON skill_definitions(source, owner_user_id, project_id, status, key);
|
|
34
|
+
|
|
35
|
+
INSERT INTO skill_definitions (
|
|
36
|
+
key, name, description, instruction_content, status, version, source, owner_user_id, project_id, repository_path, created_at, updated_at
|
|
37
|
+
)
|
|
38
|
+
VALUES (
|
|
39
|
+
'handoff_delivery',
|
|
40
|
+
'Handoff Delivery',
|
|
41
|
+
'Implement handoff-room requests with real code changes, verification, review loops, and evidence-based reporting.',
|
|
42
|
+
'Use this skill in PAL handoff implementation rooms. Convert documented intent into scoped implementation work, preserve unrelated worktree changes, avoid mock or superficial completion, run focused verification, use explicit planning/implementation/testing/review/synthesis loops for complex tasks, and report changed files, commands, evidence, risks, and limitations.',
|
|
43
|
+
'active',
|
|
44
|
+
'1',
|
|
45
|
+
'system',
|
|
46
|
+
NULL,
|
|
47
|
+
NULL,
|
|
48
|
+
NULL,
|
|
49
|
+
datetime('now'),
|
|
50
|
+
datetime('now')
|
|
51
|
+
)
|
|
52
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
53
|
+
name = excluded.name,
|
|
54
|
+
description = excluded.description,
|
|
55
|
+
instruction_content = excluded.instruction_content,
|
|
56
|
+
status = excluded.status,
|
|
57
|
+
version = excluded.version,
|
|
58
|
+
source = excluded.source,
|
|
59
|
+
owner_user_id = excluded.owner_user_id,
|
|
60
|
+
project_id = excluded.project_id,
|
|
61
|
+
repository_path = excluded.repository_path,
|
|
62
|
+
updated_at = datetime('now');
|
|
63
|
+
`);
|
|
64
|
+
}
|
package/src/migrations.ts
CHANGED
|
@@ -22,6 +22,28 @@ import * as computerAgentAssignments from './migrations/020_computer_agent_assig
|
|
|
22
22
|
import * as providerIdentityBindings from './migrations/021_provider_identity_bindings.js';
|
|
23
23
|
import * as larkAuthorizedUsers from './migrations/022_lark_authorized_users.js';
|
|
24
24
|
import * as projects from './migrations/023_projects.js';
|
|
25
|
+
import * as agentsModel from './migrations/024_agents_model.js';
|
|
26
|
+
import * as roomArchive from './migrations/025_room_archive.js';
|
|
27
|
+
import * as projectArchive from './migrations/026_project_archive.js';
|
|
28
|
+
import * as agentPermissionProfiles from './migrations/027_agent_permission_profiles.js';
|
|
29
|
+
import * as larkWebsocketRestartState from './migrations/028_lark_websocket_restart_state.js';
|
|
30
|
+
import * as heldMessageDrafts from './migrations/029_held_message_drafts.js';
|
|
31
|
+
import * as agentRoomReadState from './migrations/030_agent_room_read_state.js';
|
|
32
|
+
import * as roomTasks from './migrations/031_room_tasks.js';
|
|
33
|
+
import * as roomReminders from './migrations/032_room_reminders.js';
|
|
34
|
+
import * as roomSavedMessages from './migrations/033_room_saved_messages.js';
|
|
35
|
+
import * as agentActivityEvents from './migrations/034_agent_activity_events.js';
|
|
36
|
+
import * as agentAvatars from './migrations/035_agent_avatars.js';
|
|
37
|
+
import * as projectAgentDefaults from './migrations/036_project_agent_defaults.js';
|
|
38
|
+
import * as messageAttachments from './migrations/037_message_attachments.js';
|
|
39
|
+
import * as agentActivityRoomScope from './migrations/038_agent_activity_room_scope.js';
|
|
40
|
+
import * as messageAttachmentsPath from './migrations/039_message_attachments_path.js';
|
|
41
|
+
import * as messageAttachmentsFileSchema from './migrations/040_message_attachments_file_schema.js';
|
|
42
|
+
import * as roomSystemEvents from './migrations/041_room_system_events.js';
|
|
43
|
+
import * as messageAttachmentFileKind from './migrations/042_message_attachment_file_kind.js';
|
|
44
|
+
import * as roomModeSkillRegistry from './migrations/043_room_mode_skill_registry.js';
|
|
45
|
+
import * as workflowRuntime from './migrations/044_workflow_runtime.js';
|
|
46
|
+
import * as skillRepositoryOwnership from './migrations/045_skill_repository_ownership.js';
|
|
25
47
|
|
|
26
48
|
interface Migration {
|
|
27
49
|
version: number;
|
|
@@ -29,7 +51,53 @@ interface Migration {
|
|
|
29
51
|
up(db: Database): void;
|
|
30
52
|
}
|
|
31
53
|
|
|
32
|
-
const migrations: Migration[] = [
|
|
54
|
+
const migrations: Migration[] = [
|
|
55
|
+
initial,
|
|
56
|
+
daemonDeliveries,
|
|
57
|
+
sessionsRuns,
|
|
58
|
+
messageIdempotency,
|
|
59
|
+
artifacts,
|
|
60
|
+
larkChannelFoundation,
|
|
61
|
+
agentsA0,
|
|
62
|
+
b0ChatHistory,
|
|
63
|
+
b0TranscriptIngestSeq,
|
|
64
|
+
b0TranscriptShadowExternalIds,
|
|
65
|
+
b0ChannelConversationAuditOnly,
|
|
66
|
+
b0CrossConversationInvariant,
|
|
67
|
+
b10EngInboundRawEvents,
|
|
68
|
+
agentsRuntime,
|
|
69
|
+
agentRuntimeSessions,
|
|
70
|
+
roomParticipants,
|
|
71
|
+
unifiedRoomDelivery,
|
|
72
|
+
roomDisplayNames,
|
|
73
|
+
computerConnections,
|
|
74
|
+
computerAgentAssignments,
|
|
75
|
+
providerIdentityBindings,
|
|
76
|
+
larkAuthorizedUsers,
|
|
77
|
+
projects,
|
|
78
|
+
agentsModel,
|
|
79
|
+
roomArchive,
|
|
80
|
+
projectArchive,
|
|
81
|
+
agentPermissionProfiles,
|
|
82
|
+
larkWebsocketRestartState,
|
|
83
|
+
heldMessageDrafts,
|
|
84
|
+
agentRoomReadState,
|
|
85
|
+
roomTasks,
|
|
86
|
+
roomReminders,
|
|
87
|
+
roomSavedMessages,
|
|
88
|
+
agentActivityEvents,
|
|
89
|
+
agentAvatars,
|
|
90
|
+
projectAgentDefaults,
|
|
91
|
+
messageAttachments,
|
|
92
|
+
agentActivityRoomScope,
|
|
93
|
+
messageAttachmentsPath,
|
|
94
|
+
messageAttachmentsFileSchema,
|
|
95
|
+
roomSystemEvents,
|
|
96
|
+
messageAttachmentFileKind,
|
|
97
|
+
roomModeSkillRegistry,
|
|
98
|
+
workflowRuntime,
|
|
99
|
+
skillRepositoryOwnership,
|
|
100
|
+
].sort((a, b) => a.version - b.version);
|
|
33
101
|
|
|
34
102
|
function assertContiguousMigrations(): void {
|
|
35
103
|
for (let index = 0; index < migrations.length; index += 1) {
|
package/src/neeko.ts
CHANGED
|
@@ -1,17 +1,53 @@
|
|
|
1
|
-
import { buildPalPrompt, runtimeCwd, type AgentRuntime, type AgentRuntimeRunInput } from './agent-runtime.js';
|
|
1
|
+
import { buildPalPrompt, runtimeLaunchRoot, runtimeCwd, type AgentRuntime, type AgentRuntimeRunInput } from './agent-runtime.js';
|
|
2
|
+
import { writeGeneratedOpenCodeConfig } from './agent-permissions.js';
|
|
2
3
|
export type { AgentRuntimeRunInput, AgentRuntimeRunResult } from './agent-runtime.js';
|
|
3
4
|
export { runAgentRuntime } from './agent-runtime.js';
|
|
4
5
|
|
|
5
|
-
export function makeNeekoRuntime(agentUuid: string): AgentRuntime {
|
|
6
|
+
export function makeNeekoRuntime(agentUuid: string, model?: string | null): AgentRuntime {
|
|
6
7
|
void agentUuid;
|
|
8
|
+
const selectedModel = model?.trim() || 'opencode/nemotron-3-super-free';
|
|
7
9
|
return {
|
|
8
10
|
name: 'neeko',
|
|
9
|
-
capabilities: { protocol: '
|
|
11
|
+
capabilities: { protocol: 'json-stream', resume: 'runtime-session-id', busyDeliveryMode: 'queue', supportsMcp: false, supportsSteer: false },
|
|
10
12
|
command: 'neeko',
|
|
11
13
|
buildPrompt: buildPalPrompt,
|
|
12
14
|
buildCwd: runtimeCwd,
|
|
15
|
+
buildEnv(input: AgentRuntimeRunInput): Record<string, string> {
|
|
16
|
+
return { OPENCODE_CONFIG: writeGeneratedOpenCodeConfig(input) };
|
|
17
|
+
},
|
|
13
18
|
buildArgs(input: AgentRuntimeRunInput): string[] {
|
|
14
|
-
|
|
19
|
+
const prompt = buildPalPrompt(input);
|
|
20
|
+
const resumeArgs = input.runtimeSessionId ? ['--session', input.runtimeSessionId] : [];
|
|
21
|
+
return [
|
|
22
|
+
'run',
|
|
23
|
+
'--format', 'json',
|
|
24
|
+
'--model', selectedModel,
|
|
25
|
+
'--dir', runtimeLaunchRoot(input),
|
|
26
|
+
...resumeArgs,
|
|
27
|
+
...input.extraArgs,
|
|
28
|
+
prompt,
|
|
29
|
+
];
|
|
30
|
+
},
|
|
31
|
+
parseOutput({ stdout, stderr, input }) {
|
|
32
|
+
let runtimeSessionId = input.runtimeSessionId ?? null;
|
|
33
|
+
const output: string[] = [];
|
|
34
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
35
|
+
const trimmed = line.trim();
|
|
36
|
+
if (!trimmed.startsWith('{')) continue;
|
|
37
|
+
try {
|
|
38
|
+
const event = JSON.parse(trimmed) as { sessionID?: unknown; type?: unknown; part?: { type?: unknown; text?: unknown } };
|
|
39
|
+
if (typeof event.sessionID === 'string' && event.sessionID.trim()) runtimeSessionId = event.sessionID;
|
|
40
|
+
if (event.type === 'text' && event.part?.type === 'text' && typeof event.part.text === 'string') {
|
|
41
|
+
output.push(event.part.text);
|
|
42
|
+
}
|
|
43
|
+
} catch {
|
|
44
|
+
output.push(trimmed);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
output: [output.join(''), stderr.trim()].filter(Boolean).join('\n'),
|
|
49
|
+
runtimeSessionId,
|
|
50
|
+
};
|
|
15
51
|
},
|
|
16
52
|
};
|
|
17
53
|
}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { delimiter, join } from 'node:path';
|
|
3
|
+
import { homedir, userInfo } from 'node:os';
|
|
4
|
+
import { spawnSync } from 'node:child_process';
|
|
5
|
+
|
|
6
|
+
export interface RuntimeEnvReport {
|
|
7
|
+
effectiveEnv: NodeJS.ProcessEnv;
|
|
8
|
+
hydrated: string[];
|
|
9
|
+
missing: string[];
|
|
10
|
+
warnings: string[];
|
|
11
|
+
krb5: RuntimeKrb5Report;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface RuntimeKrb5Report {
|
|
15
|
+
ccname?: string;
|
|
16
|
+
valid: boolean;
|
|
17
|
+
expiresAt?: Date;
|
|
18
|
+
principal?: string;
|
|
19
|
+
error?: string;
|
|
20
|
+
skipped?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface ProbeKrb5Options {
|
|
24
|
+
runCommand?: (command: string, args: string[], env: NodeJS.ProcessEnv) => { status: number | null; stdout: string; stderr: string; error?: Error };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface HydrateRuntimeEnvOptions extends ProbeKrb5Options {
|
|
28
|
+
userInfo?: () => { username: string; homedir: string };
|
|
29
|
+
getuid?: () => number | undefined;
|
|
30
|
+
exists?: (path: string) => boolean;
|
|
31
|
+
findCommandDir?: (command: string, env: NodeJS.ProcessEnv) => string | undefined;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function defaultRunCommand(command: string, args: string[], env: NodeJS.ProcessEnv): { status: number | null; stdout: string; stderr: string; error?: Error } {
|
|
35
|
+
const result = spawnSync(command, args, { env, encoding: 'utf8' });
|
|
36
|
+
return {
|
|
37
|
+
status: result.status,
|
|
38
|
+
stdout: result.stdout ?? '',
|
|
39
|
+
stderr: result.stderr ?? '',
|
|
40
|
+
error: result.error,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function krb5FilePath(ccname: string): string {
|
|
45
|
+
return ccname.replace(/^FILE:/, '');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function ensurePathContains(pathValue: string, entries: Array<string | undefined>): string {
|
|
49
|
+
const seen = new Set<string>();
|
|
50
|
+
const parts: string[] = [];
|
|
51
|
+
for (const part of [...entries, ...pathValue.split(delimiter)]) {
|
|
52
|
+
if (!part || seen.has(part)) continue;
|
|
53
|
+
seen.add(part);
|
|
54
|
+
parts.push(part);
|
|
55
|
+
}
|
|
56
|
+
return parts.join(delimiter);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function findCommandDir(command: string, env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
60
|
+
for (const dir of (env.PATH ?? '').split(delimiter)) {
|
|
61
|
+
if (!dir) continue;
|
|
62
|
+
if (existsSync(join(dir, command))) return dir;
|
|
63
|
+
}
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function parseKlist(output: string): Pick<RuntimeKrb5Report, 'principal' | 'expiresAt' | 'valid'> {
|
|
68
|
+
const principal = output.match(/^Default principal:\s*(.+)$/m)?.[1]?.trim();
|
|
69
|
+
let expiresAt: Date | undefined;
|
|
70
|
+
for (const line of output.split('\n')) {
|
|
71
|
+
const match = line.match(/^\s*\d{2}\/\d{2}\/\d{4}\s+\d{2}:\d{2}:\d{2}\s+(\d{2}\/\d{2}\/\d{4}\s+\d{2}:\d{2}:\d{2})\s+/);
|
|
72
|
+
if (!match) continue;
|
|
73
|
+
const parsed = new Date(match[1]!);
|
|
74
|
+
if (!Number.isNaN(parsed.getTime()) && (!expiresAt || parsed > expiresAt)) expiresAt = parsed;
|
|
75
|
+
}
|
|
76
|
+
return { principal, expiresAt, valid: Boolean(expiresAt && expiresAt.getTime() > Date.now()) };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function krb5CheckDisabled(): boolean {
|
|
80
|
+
return /^(1|true|yes|on)$/i.test(process.env.PAL_DAEMON_DISABLE_KRB5_CHECK ?? '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function probeKrb5(ccname: string | undefined, options: ProbeKrb5Options = {}): RuntimeKrb5Report {
|
|
84
|
+
if (!ccname) return { valid: false };
|
|
85
|
+
if (krb5CheckDisabled()) {
|
|
86
|
+
return { ccname, valid: false, skipped: true, error: 'disabled' };
|
|
87
|
+
}
|
|
88
|
+
const runCommand = options.runCommand ?? defaultRunCommand;
|
|
89
|
+
const result = runCommand('klist', ['-c', ccname], process.env);
|
|
90
|
+
if (result.error) return { ccname, valid: false, error: result.error.message };
|
|
91
|
+
if (result.status !== 0) return { ccname, valid: false, error: (result.stderr || result.stdout).trim() || `klist exited ${result.status}` };
|
|
92
|
+
return { ccname, ...parseKlist(result.stdout) };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function hydrateRuntimeEnv(base: NodeJS.ProcessEnv = process.env, options: HydrateRuntimeEnvOptions = {}): RuntimeEnvReport {
|
|
96
|
+
const env: NodeJS.ProcessEnv = { ...base };
|
|
97
|
+
const hydrated: string[] = [];
|
|
98
|
+
const missing: string[] = [];
|
|
99
|
+
const warnings: string[] = [];
|
|
100
|
+
const currentUser = options.userInfo?.() ?? userInfo();
|
|
101
|
+
const exists = options.exists ?? existsSync;
|
|
102
|
+
|
|
103
|
+
if (!env.HOME) {
|
|
104
|
+
env.HOME = currentUser.homedir || homedir();
|
|
105
|
+
hydrated.push('HOME');
|
|
106
|
+
}
|
|
107
|
+
if (!env.USER) {
|
|
108
|
+
env.USER = currentUser.username;
|
|
109
|
+
hydrated.push('USER');
|
|
110
|
+
}
|
|
111
|
+
if (!env.KRB5CCNAME) {
|
|
112
|
+
const uid = options.getuid?.() ?? process.getuid?.();
|
|
113
|
+
const candidates = [
|
|
114
|
+
env.HOME ? `FILE:${env.HOME}/.krb5cc` : undefined,
|
|
115
|
+
uid === undefined ? undefined : `FILE:/tmp/krb5cc_${uid}`,
|
|
116
|
+
].filter((candidate): candidate is string => Boolean(candidate));
|
|
117
|
+
const found = candidates.find((candidate) => exists(krb5FilePath(candidate)));
|
|
118
|
+
if (found) {
|
|
119
|
+
env.KRB5CCNAME = found;
|
|
120
|
+
hydrated.push('KRB5CCNAME');
|
|
121
|
+
} else {
|
|
122
|
+
missing.push('KRB5CCNAME');
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (!env.LANG && !env.LC_ALL) {
|
|
126
|
+
env.LANG = 'C.UTF-8';
|
|
127
|
+
hydrated.push('LANG');
|
|
128
|
+
}
|
|
129
|
+
if (!env.TERM) {
|
|
130
|
+
env.TERM = 'dumb';
|
|
131
|
+
hydrated.push('TERM');
|
|
132
|
+
}
|
|
133
|
+
if (!env.HTTPS_PROXY && !env.https_proxy) warnings.push('HTTPS_PROXY is not set; internal runtime auth may require a proxy');
|
|
134
|
+
if (!env.HTTP_PROXY && !env.http_proxy) warnings.push('HTTP_PROXY is not set; internal runtime auth may require a proxy');
|
|
135
|
+
if (!env.NO_PROXY && !env.no_proxy) warnings.push('NO_PROXY is not set; proxy bypasses may be missing');
|
|
136
|
+
|
|
137
|
+
const passthrough = (env.PAL_RUNTIME_ENV_PASSTHROUGH ?? '').split(',').map((value) => value.trim()).filter(Boolean);
|
|
138
|
+
for (const name of passthrough) {
|
|
139
|
+
if (!env[name] && base[name]) env[name] = base[name];
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const cocoDir = (options.findCommandDir ?? findCommandDir)('coco', env);
|
|
143
|
+
env.PATH = ensurePathContains(env.PATH ?? '', [
|
|
144
|
+
env.HOME ? `${env.HOME}/.local/bin` : undefined,
|
|
145
|
+
env.HOME ? `${env.HOME}/.bun/bin` : undefined,
|
|
146
|
+
cocoDir,
|
|
147
|
+
]);
|
|
148
|
+
|
|
149
|
+
const krb5 = probeKrb5(env.KRB5CCNAME, options);
|
|
150
|
+
if (env.KRB5CCNAME && !krb5.valid && !krb5.skipped) warnings.push(`KRB5CCNAME is set but klist did not find a valid ticket${krb5.error ? `: ${krb5.error}` : ''}`);
|
|
151
|
+
return { effectiveEnv: env, hydrated, missing, warnings, krb5 };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function formatDuration(ms: number): string {
|
|
155
|
+
const minutes = Math.max(0, Math.round(ms / 60000));
|
|
156
|
+
const hours = Math.floor(minutes / 60);
|
|
157
|
+
const remainingMinutes = minutes % 60;
|
|
158
|
+
return hours > 0 ? `${hours}h${remainingMinutes}m` : `${remainingMinutes}m`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function logEnvReport(report: RuntimeEnvReport): void {
|
|
162
|
+
const env = report.effectiveEnv;
|
|
163
|
+
console.log('[daemon] runtime env preflight');
|
|
164
|
+
console.log(` HOME=${env.HOME ?? '<missing>'}${report.hydrated.includes('HOME') ? ' (hydrated)' : ''}`);
|
|
165
|
+
console.log(` USER=${env.USER ?? '<missing>'}${report.hydrated.includes('USER') ? ' (hydrated)' : ''}`);
|
|
166
|
+
console.log(` KRB5CCNAME=${env.KRB5CCNAME ?? '<missing>'}${report.hydrated.includes('KRB5CCNAME') ? ' (hydrated)' : ''}`);
|
|
167
|
+
if (report.krb5.valid && report.krb5.expiresAt) {
|
|
168
|
+
console.log(` klist principal=${report.krb5.principal ?? '-'} expires=${report.krb5.expiresAt.toISOString()} (in ${formatDuration(report.krb5.expiresAt.getTime() - Date.now())})`);
|
|
169
|
+
} else if (report.krb5.skipped) {
|
|
170
|
+
console.log(` klist skipped: ${report.krb5.error ?? 'disabled'}`);
|
|
171
|
+
} else if (report.krb5.ccname) {
|
|
172
|
+
console.log(` klist invalid${report.krb5.error ? ` error=${report.krb5.error}` : ''}`);
|
|
173
|
+
} else {
|
|
174
|
+
console.log(' klist skipped: KRB5CCNAME missing');
|
|
175
|
+
}
|
|
176
|
+
const cocoDir = findCommandDir('coco', env);
|
|
177
|
+
console.log(` PATH includes coco=${cocoDir ? `${join(cocoDir, 'coco')} ok` : 'not found'}`);
|
|
178
|
+
console.log(` hydrated=[${report.hydrated.join(', ')}] missing=[${report.missing.join(', ')}] warnings=[${report.warnings.join('; ')}]`);
|
|
179
|
+
}
|
package/src/runtime-registry.ts
CHANGED
|
@@ -3,23 +3,19 @@ import type { AgentRuntime, AgentRuntimeProtocol } from './agent-runtime.js';
|
|
|
3
3
|
const supportedProtocols = new Set<AgentRuntimeProtocol>(['json-stream', 'acp']);
|
|
4
4
|
|
|
5
5
|
const runtimeFactories = {
|
|
6
|
-
codex: async (agentUuid: string): Promise<AgentRuntime> => {
|
|
6
|
+
codex: async (agentUuid: string, model: string | null): Promise<AgentRuntime> => {
|
|
7
7
|
const { makeCodexRuntime } = await import('./codex.js');
|
|
8
|
-
return makeCodexRuntime(agentUuid);
|
|
8
|
+
return makeCodexRuntime(agentUuid, model);
|
|
9
9
|
},
|
|
10
|
-
coco: async (agentUuid: string): Promise<AgentRuntime> => {
|
|
10
|
+
coco: async (agentUuid: string, model: string | null): Promise<AgentRuntime> => {
|
|
11
11
|
const { makeCocoRuntime } = await import('./coco.js');
|
|
12
|
-
return makeCocoRuntime(agentUuid);
|
|
12
|
+
return makeCocoRuntime(agentUuid, model);
|
|
13
13
|
},
|
|
14
|
-
|
|
15
|
-
const { makeCocoStreamJsonRuntime } = await import('./coco.js');
|
|
16
|
-
return makeCocoStreamJsonRuntime(agentUuid);
|
|
17
|
-
},
|
|
18
|
-
neeko: async (agentUuid: string): Promise<AgentRuntime> => {
|
|
14
|
+
neeko: async (agentUuid: string, model: string | null): Promise<AgentRuntime> => {
|
|
19
15
|
const { makeNeekoRuntime } = await import('./neeko.js');
|
|
20
|
-
return makeNeekoRuntime(agentUuid);
|
|
16
|
+
return makeNeekoRuntime(agentUuid, model);
|
|
21
17
|
},
|
|
22
|
-
} satisfies Record<string, (agentUuid: string) => Promise<AgentRuntime>>;
|
|
18
|
+
} satisfies Record<string, (agentUuid: string, model: string | null) => Promise<AgentRuntime>>;
|
|
23
19
|
|
|
24
20
|
export type RuntimeName = keyof typeof runtimeFactories;
|
|
25
21
|
|
|
@@ -27,13 +23,87 @@ export function knownRuntimeNames(): RuntimeName[] {
|
|
|
27
23
|
return Object.keys(runtimeFactories) as RuntimeName[];
|
|
28
24
|
}
|
|
29
25
|
|
|
30
|
-
export
|
|
26
|
+
export interface RuntimeModelLookup {
|
|
27
|
+
models: string[];
|
|
28
|
+
error: string | null;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function modelCommand(runtimeName: string): { command: string; args: string[] } {
|
|
32
|
+
if (runtimeName === 'codex') return { command: 'codex', args: ['debug', 'models'] };
|
|
33
|
+
if (runtimeName === 'coco') return { command: 'coco', args: ['models'] };
|
|
34
|
+
if (runtimeName === 'neeko') return { command: 'neeko', args: ['models'] };
|
|
35
|
+
throw new Error(`Unsupported runtime '${runtimeName}'. Supported runtimes: ${knownRuntimeNames().join(', ')}.`);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function stripAnsi(value: string): string {
|
|
39
|
+
return value.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function uniqueNonEmpty(values: string[]): string[] {
|
|
43
|
+
return Array.from(new Set(values.map((value) => value.trim()).filter(Boolean)));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function parseModelOutput(runtimeName: string, stdout: string): string[] {
|
|
47
|
+
const clean = stripAnsi(stdout).trim();
|
|
48
|
+
if (!clean) return [];
|
|
49
|
+
if (runtimeName === 'codex') {
|
|
50
|
+
const parsed = JSON.parse(clean) as { models?: Array<{ slug?: unknown; id?: unknown; name?: unknown }> };
|
|
51
|
+
return uniqueNonEmpty((parsed.models ?? []).map((model) => String(model.slug ?? model.id ?? model.name ?? '')));
|
|
52
|
+
}
|
|
53
|
+
return uniqueNonEmpty(clean.split(/\r?\n/).filter((line) => !/^\s*(available\s+)?models\s*:?\s*$/i.test(line)));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function runtimeModelOptions(runtimeName: string): Promise<string[]> {
|
|
57
|
+
const runtime = runtimeName.trim();
|
|
58
|
+
modelCommand(runtime);
|
|
59
|
+
const { command, args } = modelCommand(runtime);
|
|
60
|
+
const proc = Bun.spawn([command, ...args], {
|
|
61
|
+
stdout: 'pipe',
|
|
62
|
+
stderr: 'pipe',
|
|
63
|
+
env: process.env,
|
|
64
|
+
});
|
|
65
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
66
|
+
new Response(proc.stdout).text(),
|
|
67
|
+
new Response(proc.stderr).text(),
|
|
68
|
+
proc.exited,
|
|
69
|
+
]);
|
|
70
|
+
if (exitCode !== 0) {
|
|
71
|
+
const detail = stderr.trim() || stdout.trim() || `exit ${exitCode}`;
|
|
72
|
+
throw new Error(`Could not load models for runtime '${runtime}': ${detail}`);
|
|
73
|
+
}
|
|
74
|
+
return parseModelOutput(runtime, stdout);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export async function lookupRuntimeModels(runtimeName: string): Promise<RuntimeModelLookup> {
|
|
78
|
+
try {
|
|
79
|
+
return { models: await runtimeModelOptions(runtimeName), error: null };
|
|
80
|
+
} catch (error) {
|
|
81
|
+
return { models: [], error: error instanceof Error ? error.message : String(error) };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export async function allRuntimeModelOptions(): Promise<Record<RuntimeName, RuntimeModelLookup>> {
|
|
86
|
+
const entries = await Promise.all(knownRuntimeNames().map(async (name) => [name, await lookupRuntimeModels(name)] as const));
|
|
87
|
+
return Object.fromEntries(entries) as Record<RuntimeName, RuntimeModelLookup>;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export async function validateRuntimeModel(runtimeName: string, model: string | null | undefined): Promise<string | null> {
|
|
91
|
+
const trimmed = model?.trim() || null;
|
|
92
|
+
if (!trimmed) return null;
|
|
93
|
+
const options = await runtimeModelOptions(runtimeName);
|
|
94
|
+
if (!options.includes(trimmed)) {
|
|
95
|
+
throw new Error(`Unsupported model '${trimmed}' for runtime '${runtimeName}'. Supported models: ${options.join(', ')}.`);
|
|
96
|
+
}
|
|
97
|
+
return trimmed;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export async function resolveRuntimeDriver(runtimeName: string, agentUuid: string, model?: string | null): Promise<AgentRuntime> {
|
|
31
101
|
const factory = runtimeFactories[runtimeName as RuntimeName];
|
|
32
102
|
if (!factory) {
|
|
33
103
|
throw new Error(`Unsupported runtime '${runtimeName}'. Supported runtimes: ${knownRuntimeNames().join(', ')}.`);
|
|
34
104
|
}
|
|
35
105
|
|
|
36
|
-
const driver = await factory(agentUuid);
|
|
106
|
+
const driver = await factory(agentUuid, model?.trim() || null);
|
|
37
107
|
if (!supportedProtocols.has(driver.capabilities.protocol)) {
|
|
38
108
|
throw new Error(`Runtime '${runtimeName}' uses unsupported protocol '${driver.capabilities.protocol}'. Supported protocols: ${Array.from(supportedProtocols).join(', ')}.`);
|
|
39
109
|
}
|