@controlflow-ai/daemon 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +360 -0
- package/bin/console.js +2 -0
- package/bin/daemon.js +2 -0
- package/bin/pal.js +2 -0
- package/bin/server.js +2 -0
- package/package.json +31 -0
- package/src/agent-runtime.ts +285 -0
- package/src/app.ts +745 -0
- package/src/args.ts +54 -0
- package/src/artifacts.ts +85 -0
- package/src/cli.ts +284 -0
- package/src/client.ts +310 -0
- package/src/coco.ts +52 -0
- package/src/codex.ts +41 -0
- package/src/coding-agent-runtime.ts +20 -0
- package/src/config.ts +106 -0
- package/src/console.ts +349 -0
- package/src/daemon-client.ts +91 -0
- package/src/daemon.ts +580 -0
- package/src/db.ts +2830 -0
- package/src/failure-message.ts +17 -0
- package/src/format.ts +13 -0
- package/src/http.ts +55 -0
- package/src/lark/agent-runtime.ts +142 -0
- package/src/lark/cli.ts +549 -0
- package/src/lark/credentials.ts +105 -0
- package/src/lark/daemon-integration.ts +108 -0
- package/src/lark/dispatcher.ts +374 -0
- package/src/lark/event-router.ts +329 -0
- package/src/lark/inbound-events.ts +131 -0
- package/src/lark/server-integration.ts +445 -0
- package/src/lark/setup.ts +326 -0
- package/src/lark/ws-daemon.ts +224 -0
- package/src/lark-fixture-diagnostics.ts +56 -0
- package/src/lark-fixture.ts +277 -0
- package/src/local-api.ts +155 -0
- package/src/local-auth.ts +45 -0
- package/src/migrations/001_initial.ts +61 -0
- package/src/migrations/002_daemon_deliveries.ts +52 -0
- package/src/migrations/003_sessions_runs.ts +49 -0
- package/src/migrations/004_message_idempotency.ts +21 -0
- package/src/migrations/005_artifacts.ts +24 -0
- package/src/migrations/006_lark_channel_foundation.ts +119 -0
- package/src/migrations/007_agents_a0.ts +17 -0
- package/src/migrations/008_b0_chat_history.ts +31 -0
- package/src/migrations/009_b0_transcript_ingest_seq.ts +35 -0
- package/src/migrations/010_b0_transcript_shadow_external_ids.ts +32 -0
- package/src/migrations/011_b0_channel_conversation_audit_only.ts +27 -0
- package/src/migrations/012_b0_cross_conversation_invariant.ts +45 -0
- package/src/migrations/013_b1_0_eng_inbound_raw_events.ts +56 -0
- package/src/migrations/014_agents_runtime.ts +10 -0
- package/src/migrations/015_agent_runtime_sessions.ts +15 -0
- package/src/migrations/016_room_participants.ts +27 -0
- package/src/migrations/017_unified_room_delivery.ts +203 -0
- package/src/migrations/018_room_display_names.ts +36 -0
- package/src/migrations/019_computer_connections.ts +63 -0
- package/src/migrations/020_computer_agent_assignments.ts +20 -0
- package/src/migrations/021_provider_identity_bindings.ts +32 -0
- package/src/migrations.ts +85 -0
- package/src/neeko.ts +23 -0
- package/src/provider-identity.ts +40 -0
- package/src/runtime-registry.ts +41 -0
- package/src/server-auth.ts +13 -0
- package/src/server.ts +63 -0
- package/src/token-file.ts +57 -0
- package/src/types.ts +408 -0
- package/src/web.ts +565 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 18;
|
|
4
|
+
export const name = 'room_display_names';
|
|
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', 'display_name')) {
|
|
13
|
+
db.exec(`ALTER TABLE chats ADD COLUMN display_name TEXT`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
db.exec(`
|
|
17
|
+
DROP VIEW IF EXISTS chat_stats;
|
|
18
|
+
CREATE VIEW chat_stats AS
|
|
19
|
+
SELECT
|
|
20
|
+
c.id,
|
|
21
|
+
c.name,
|
|
22
|
+
c.display_name,
|
|
23
|
+
c.kind,
|
|
24
|
+
c.server_id,
|
|
25
|
+
c.provider,
|
|
26
|
+
c.dm_type,
|
|
27
|
+
c.capabilities_json,
|
|
28
|
+
c.audit_visibility,
|
|
29
|
+
c.created_at,
|
|
30
|
+
COUNT(m.id) AS message_count,
|
|
31
|
+
MAX(m.created_at) AS last_message_at
|
|
32
|
+
FROM chats c
|
|
33
|
+
LEFT JOIN messages m ON m.chat_id = c.id
|
|
34
|
+
GROUP BY c.id;
|
|
35
|
+
`);
|
|
36
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 19;
|
|
4
|
+
export const name = 'computer_connections';
|
|
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
|
+
function addColumnIfMissing(db: Database, table: string, column: string, ddl: string): void {
|
|
12
|
+
if (!hasColumn(db, table, column)) {
|
|
13
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${ddl}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function up(db: Database): void {
|
|
18
|
+
db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS computers (
|
|
20
|
+
id TEXT PRIMARY KEY,
|
|
21
|
+
name TEXT NOT NULL DEFAULT '',
|
|
22
|
+
credential_hash TEXT NOT NULL,
|
|
23
|
+
status TEXT NOT NULL DEFAULT 'offline' CHECK (status IN ('online', 'offline', 'revoked')),
|
|
24
|
+
active_connection_id TEXT,
|
|
25
|
+
last_seen_at TEXT,
|
|
26
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
27
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
28
|
+
);
|
|
29
|
+
|
|
30
|
+
CREATE TABLE IF NOT EXISTS computer_connections (
|
|
31
|
+
id TEXT PRIMARY KEY,
|
|
32
|
+
computer_id TEXT NOT NULL REFERENCES computers(id) ON DELETE CASCADE,
|
|
33
|
+
token_hash TEXT NOT NULL,
|
|
34
|
+
epoch INTEGER NOT NULL,
|
|
35
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'revoked', 'closed')),
|
|
36
|
+
connected_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
37
|
+
last_heartbeat_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
38
|
+
revoked_at TEXT,
|
|
39
|
+
UNIQUE(computer_id, epoch)
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
CREATE INDEX IF NOT EXISTS idx_computer_connections_computer_status
|
|
43
|
+
ON computer_connections(computer_id, status, epoch);
|
|
44
|
+
`);
|
|
45
|
+
|
|
46
|
+
addColumnIfMissing(db, 'message_deliveries', 'connection_id', 'connection_id TEXT REFERENCES computer_connections(id) ON DELETE SET NULL');
|
|
47
|
+
addColumnIfMissing(db, 'agent_sessions', 'computer_id', 'computer_id TEXT REFERENCES computers(id) ON DELETE SET NULL');
|
|
48
|
+
addColumnIfMissing(db, 'agent_sessions', 'runtime_provider', 'runtime_provider TEXT');
|
|
49
|
+
addColumnIfMissing(db, 'agent_runs', 'computer_id', 'computer_id TEXT REFERENCES computers(id) ON DELETE SET NULL');
|
|
50
|
+
addColumnIfMissing(db, 'agent_runs', 'connection_id', 'connection_id TEXT REFERENCES computer_connections(id) ON DELETE SET NULL');
|
|
51
|
+
addColumnIfMissing(db, 'agent_runs', 'runtime_provider', 'runtime_provider TEXT');
|
|
52
|
+
addColumnIfMissing(db, 'agent_runs', 'runtime_invocation_id', 'runtime_invocation_id TEXT');
|
|
53
|
+
|
|
54
|
+
db.exec(`
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_message_deliveries_connection_id ON message_deliveries(connection_id);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_connection_id ON agent_runs(connection_id);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_agent_runs_computer_room_active
|
|
58
|
+
ON agent_runs(computer_id, agent, status, session_id);
|
|
59
|
+
CREATE UNIQUE INDEX IF NOT EXISTS idx_agent_sessions_computer_context
|
|
60
|
+
ON agent_sessions(chat_id, agent, computer_id, runtime_provider)
|
|
61
|
+
WHERE computer_id IS NOT NULL AND runtime_provider IS NOT NULL;
|
|
62
|
+
`);
|
|
63
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 20;
|
|
4
|
+
export const name = 'computer_agent_assignments';
|
|
5
|
+
|
|
6
|
+
export function up(db: Database): void {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS computer_agent_assignments (
|
|
9
|
+
agent TEXT PRIMARY KEY REFERENCES agents(agent_key) ON DELETE CASCADE,
|
|
10
|
+
computer_id TEXT NOT NULL REFERENCES computers(id) ON DELETE CASCADE,
|
|
11
|
+
cwd TEXT NOT NULL DEFAULT '',
|
|
12
|
+
status TEXT NOT NULL DEFAULT 'active' CHECK (status IN ('active', 'disabled')),
|
|
13
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
14
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE INDEX IF NOT EXISTS idx_computer_agent_assignments_computer
|
|
18
|
+
ON computer_agent_assignments(computer_id, status, agent);
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 21;
|
|
4
|
+
export const name = 'provider_identity_bindings';
|
|
5
|
+
|
|
6
|
+
export function up(db: Database): void {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS pal_identities (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
kind TEXT NOT NULL CHECK (kind IN ('user', 'bot', 'agent', 'room')),
|
|
11
|
+
display_name TEXT,
|
|
12
|
+
stable_handle TEXT NOT NULL UNIQUE,
|
|
13
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
14
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
15
|
+
);
|
|
16
|
+
|
|
17
|
+
CREATE TABLE IF NOT EXISTS provider_identity_bindings (
|
|
18
|
+
id TEXT PRIMARY KEY,
|
|
19
|
+
provider TEXT NOT NULL CHECK (provider IN ('lark', 'wechat')),
|
|
20
|
+
provider_account_id TEXT NOT NULL REFERENCES provider_accounts(id) ON DELETE CASCADE,
|
|
21
|
+
external_type TEXT NOT NULL CHECK (external_type IN ('user', 'bot', 'room')),
|
|
22
|
+
external_id TEXT NOT NULL,
|
|
23
|
+
identity_id TEXT NOT NULL REFERENCES pal_identities(id) ON DELETE CASCADE,
|
|
24
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
25
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
26
|
+
UNIQUE(provider, provider_account_id, external_type, external_id)
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
CREATE INDEX IF NOT EXISTS idx_provider_identity_bindings_identity
|
|
30
|
+
ON provider_identity_bindings(identity_id);
|
|
31
|
+
`);
|
|
32
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
import * as initial from './migrations/001_initial.js';
|
|
3
|
+
import * as daemonDeliveries from './migrations/002_daemon_deliveries.js';
|
|
4
|
+
import * as sessionsRuns from './migrations/003_sessions_runs.js';
|
|
5
|
+
import * as messageIdempotency from './migrations/004_message_idempotency.js';
|
|
6
|
+
import * as artifacts from './migrations/005_artifacts.js';
|
|
7
|
+
import * as larkChannelFoundation from './migrations/006_lark_channel_foundation.js';
|
|
8
|
+
import * as agentsA0 from './migrations/007_agents_a0.js';
|
|
9
|
+
import * as b0ChatHistory from './migrations/008_b0_chat_history.js';
|
|
10
|
+
import * as b0TranscriptIngestSeq from './migrations/009_b0_transcript_ingest_seq.js';
|
|
11
|
+
import * as b0TranscriptShadowExternalIds from './migrations/010_b0_transcript_shadow_external_ids.js';
|
|
12
|
+
import * as b0ChannelConversationAuditOnly from './migrations/011_b0_channel_conversation_audit_only.js';
|
|
13
|
+
import * as b0CrossConversationInvariant from './migrations/012_b0_cross_conversation_invariant.js';
|
|
14
|
+
import * as b10EngInboundRawEvents from './migrations/013_b1_0_eng_inbound_raw_events.js';
|
|
15
|
+
import * as agentsRuntime from './migrations/014_agents_runtime.js';
|
|
16
|
+
import * as agentRuntimeSessions from './migrations/015_agent_runtime_sessions.js';
|
|
17
|
+
import * as roomParticipants from './migrations/016_room_participants.js';
|
|
18
|
+
import * as unifiedRoomDelivery from './migrations/017_unified_room_delivery.js';
|
|
19
|
+
import * as roomDisplayNames from './migrations/018_room_display_names.js';
|
|
20
|
+
import * as computerConnections from './migrations/019_computer_connections.js';
|
|
21
|
+
import * as computerAgentAssignments from './migrations/020_computer_agent_assignments.js';
|
|
22
|
+
import * as providerIdentityBindings from './migrations/021_provider_identity_bindings.js';
|
|
23
|
+
|
|
24
|
+
interface Migration {
|
|
25
|
+
version: number;
|
|
26
|
+
name: string;
|
|
27
|
+
up(db: Database): void;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const migrations: Migration[] = [initial, daemonDeliveries, sessionsRuns, messageIdempotency, artifacts, larkChannelFoundation, agentsA0, b0ChatHistory, b0TranscriptIngestSeq, b0TranscriptShadowExternalIds, b0ChannelConversationAuditOnly, b0CrossConversationInvariant, b10EngInboundRawEvents, agentsRuntime, agentRuntimeSessions, roomParticipants, unifiedRoomDelivery, roomDisplayNames, computerConnections, computerAgentAssignments, providerIdentityBindings].sort((a, b) => a.version - b.version);
|
|
31
|
+
|
|
32
|
+
function assertContiguousMigrations(): void {
|
|
33
|
+
for (let index = 0; index < migrations.length; index += 1) {
|
|
34
|
+
const expected = index + 1;
|
|
35
|
+
const actual = migrations[index]!.version;
|
|
36
|
+
if (actual !== expected) {
|
|
37
|
+
throw new Error(`migration gap: expected version ${expected}, got ${actual}`);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function ensureMigrationsTable(db: Database): void {
|
|
43
|
+
db.exec(`
|
|
44
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
45
|
+
version INTEGER PRIMARY KEY,
|
|
46
|
+
name TEXT NOT NULL,
|
|
47
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
48
|
+
);
|
|
49
|
+
`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function appliedVersions(db: Database): Set<number> {
|
|
53
|
+
const rows = db.query('SELECT version FROM schema_migrations ORDER BY version ASC').all() as Array<{ version: number }>;
|
|
54
|
+
return new Set(rows.map((row) => Number(row.version)));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function assertNoMigrationGaps(applied: Set<number>): void {
|
|
58
|
+
if (applied.size === 0) return;
|
|
59
|
+
const max = Math.max(...applied);
|
|
60
|
+
for (let version = 1; version <= max; version += 1) {
|
|
61
|
+
if (!applied.has(version)) {
|
|
62
|
+
throw new Error(`MIGRATION_GAP: schema_migrations is missing version ${version}`);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function markApplied(db: Database, migration: Migration): void {
|
|
68
|
+
db.query('INSERT INTO schema_migrations (version, name) VALUES (?, ?)').run(migration.version, migration.name);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function runMigrations(db: Database): void {
|
|
72
|
+
assertContiguousMigrations();
|
|
73
|
+
ensureMigrationsTable(db);
|
|
74
|
+
const applied = appliedVersions(db);
|
|
75
|
+
assertNoMigrationGaps(applied);
|
|
76
|
+
|
|
77
|
+
for (const migration of migrations) {
|
|
78
|
+
if (applied.has(migration.version)) continue;
|
|
79
|
+
|
|
80
|
+
db.transaction(() => {
|
|
81
|
+
migration.up(db);
|
|
82
|
+
markApplied(db, migration);
|
|
83
|
+
})();
|
|
84
|
+
}
|
|
85
|
+
}
|
package/src/neeko.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { buildPalPrompt, runtimeCwd, type AgentRuntime, type AgentRuntimeRunInput } from './agent-runtime.js';
|
|
2
|
+
export type { AgentRuntimeRunInput, AgentRuntimeRunResult } from './agent-runtime.js';
|
|
3
|
+
export { runAgentRuntime } from './agent-runtime.js';
|
|
4
|
+
|
|
5
|
+
export function makeNeekoRuntime(agentUuid: string): AgentRuntime {
|
|
6
|
+
void agentUuid;
|
|
7
|
+
return {
|
|
8
|
+
name: 'neeko',
|
|
9
|
+
capabilities: { protocol: 'acp', resume: 'runtime-session-id', busyDeliveryMode: 'queue', supportsMcp: true },
|
|
10
|
+
command: 'neeko',
|
|
11
|
+
buildPrompt: buildPalPrompt,
|
|
12
|
+
buildCwd: runtimeCwd,
|
|
13
|
+
buildArgs(input: AgentRuntimeRunInput): string[] {
|
|
14
|
+
return ['acp', '--cwd', runtimeCwd(input), ...input.extraArgs];
|
|
15
|
+
},
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** @deprecated Use makeNeekoRuntime() + runAgentRuntime() instead. */
|
|
20
|
+
export async function runNeeko(input: AgentRuntimeRunInput): Promise<import('./agent-runtime.js').AgentRuntimeRunResult> {
|
|
21
|
+
const { runAgentRuntime } = await import('./agent-runtime.js');
|
|
22
|
+
return runAgentRuntime(makeNeekoRuntime(crypto.randomUUID()), input);
|
|
23
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
|
|
3
|
+
export type ProviderExternalType = 'user' | 'bot' | 'room';
|
|
4
|
+
export type PalIdentityKind = ProviderExternalType | 'agent';
|
|
5
|
+
|
|
6
|
+
const providerNativeIdPattern = /\b(?:ou|oc|om|omt|cli)_[A-Za-z0-9_-]+\b/g;
|
|
7
|
+
|
|
8
|
+
function hash8(value: string): string {
|
|
9
|
+
return createHash('sha256').update(value).digest('hex').slice(0, 8);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function palIdentityHandle(kind: ProviderExternalType, seed: string): string {
|
|
13
|
+
return `${kind}:pal_${kind}_${hash8(seed)}`;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function providerVirtualHandle(value: string): string {
|
|
17
|
+
if (value.startsWith('ou_')) return palIdentityHandle('user', value);
|
|
18
|
+
if (value.startsWith('oc_')) return palIdentityHandle('room', value);
|
|
19
|
+
if (value.startsWith('cli_')) return `app:pal_app_${hash8(value)}`;
|
|
20
|
+
if (value.startsWith('om_') || value.startsWith('omt_')) return `message:pal_message_${hash8(value)}`;
|
|
21
|
+
return virtualizeProviderId(value);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function virtualizeProviderId(value: string, kind: ProviderExternalType | 'user' = 'user'): string {
|
|
25
|
+
return palIdentityHandle(kind, value);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function sanitizeProviderIds(value: string): string {
|
|
29
|
+
return value.replace(providerNativeIdPattern, (id) => providerVirtualHandle(id));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function sanitizeProviderIdValue(value: string | null | undefined): string {
|
|
33
|
+
if (!value) return '';
|
|
34
|
+
return sanitizeProviderIds(value);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function containsProviderNativeId(value: string): boolean {
|
|
38
|
+
providerNativeIdPattern.lastIndex = 0;
|
|
39
|
+
return providerNativeIdPattern.test(value);
|
|
40
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AgentRuntime, AgentRuntimeProtocol } from './agent-runtime.js';
|
|
2
|
+
|
|
3
|
+
const supportedProtocols = new Set<AgentRuntimeProtocol>(['json-stream', 'acp']);
|
|
4
|
+
|
|
5
|
+
const runtimeFactories = {
|
|
6
|
+
codex: async (agentUuid: string): Promise<AgentRuntime> => {
|
|
7
|
+
const { makeCodexRuntime } = await import('./codex.js');
|
|
8
|
+
return makeCodexRuntime(agentUuid);
|
|
9
|
+
},
|
|
10
|
+
coco: async (agentUuid: string): Promise<AgentRuntime> => {
|
|
11
|
+
const { makeCocoRuntime } = await import('./coco.js');
|
|
12
|
+
return makeCocoRuntime(agentUuid);
|
|
13
|
+
},
|
|
14
|
+
'coco-stream-json': async (agentUuid: string): Promise<AgentRuntime> => {
|
|
15
|
+
const { makeCocoStreamJsonRuntime } = await import('./coco.js');
|
|
16
|
+
return makeCocoStreamJsonRuntime(agentUuid);
|
|
17
|
+
},
|
|
18
|
+
neeko: async (agentUuid: string): Promise<AgentRuntime> => {
|
|
19
|
+
const { makeNeekoRuntime } = await import('./neeko.js');
|
|
20
|
+
return makeNeekoRuntime(agentUuid);
|
|
21
|
+
},
|
|
22
|
+
} satisfies Record<string, (agentUuid: string) => Promise<AgentRuntime>>;
|
|
23
|
+
|
|
24
|
+
export type RuntimeName = keyof typeof runtimeFactories;
|
|
25
|
+
|
|
26
|
+
export function knownRuntimeNames(): RuntimeName[] {
|
|
27
|
+
return Object.keys(runtimeFactories) as RuntimeName[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function resolveRuntimeDriver(runtimeName: string, agentUuid: string): Promise<AgentRuntime> {
|
|
31
|
+
const factory = runtimeFactories[runtimeName as RuntimeName];
|
|
32
|
+
if (!factory) {
|
|
33
|
+
throw new Error(`Unsupported runtime '${runtimeName}'. Supported runtimes: ${knownRuntimeNames().join(', ')}.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const driver = await factory(agentUuid);
|
|
37
|
+
if (!supportedProtocols.has(driver.capabilities.protocol)) {
|
|
38
|
+
throw new Error(`Runtime '${runtimeName}' uses unsupported protocol '${driver.capabilities.protocol}'. Supported protocols: ${Array.from(supportedProtocols).join(', ')}.`);
|
|
39
|
+
}
|
|
40
|
+
return driver;
|
|
41
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { defaultServerToken } from './config.js';
|
|
2
|
+
import { HttpError } from './http.js';
|
|
3
|
+
|
|
4
|
+
export function assertServerAuth(request: Request, token = defaultServerToken()): void {
|
|
5
|
+
if (!token) throw new HttpError(401, 'UNAUTHORIZED', 'server token is required');
|
|
6
|
+
if (request.headers.get('authorization') !== `Bearer ${token}`) {
|
|
7
|
+
throw new HttpError(401, 'UNAUTHORIZED', 'invalid server token');
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function serverAuthHeaders(token = defaultServerToken()): HeadersInit {
|
|
12
|
+
return token ? { authorization: `Bearer ${token}` } : {};
|
|
13
|
+
}
|
package/src/server.ts
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { DEFAULT_HOST, DEFAULT_PORT, defaultDbPath } from './config.js';
|
|
3
|
+
import { MessageStore } from './db.js';
|
|
4
|
+
import { handleRequest } from './app.js';
|
|
5
|
+
import { startLarkOnServer } from './lark/server-integration.js';
|
|
6
|
+
|
|
7
|
+
const dbPath = defaultDbPath();
|
|
8
|
+
const store = new MessageStore(dbPath);
|
|
9
|
+
const port = Number(process.env.PAL_PORT ?? DEFAULT_PORT);
|
|
10
|
+
const host = process.env.PAL_HOST ?? DEFAULT_HOST;
|
|
11
|
+
|
|
12
|
+
const server = Bun.serve({
|
|
13
|
+
hostname: host,
|
|
14
|
+
port,
|
|
15
|
+
async fetch(request) {
|
|
16
|
+
const url = new URL(request.url);
|
|
17
|
+
if (request.method === 'POST' && url.pathname === '/api/lark/reload') {
|
|
18
|
+
return reloadLarkIntegration();
|
|
19
|
+
}
|
|
20
|
+
return handleRequest(store, request);
|
|
21
|
+
},
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
console.log(`pal server listening on http://${server.hostname}:${server.port}`);
|
|
25
|
+
console.log(`database: ${dbPath}`);
|
|
26
|
+
|
|
27
|
+
function startLarkIntegration() {
|
|
28
|
+
return startLarkOnServer({
|
|
29
|
+
store,
|
|
30
|
+
logger: { log: console.log, warn: console.warn, error: console.error },
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Start Lark bot integration on the server
|
|
35
|
+
let larkIntegration = startLarkIntegration();
|
|
36
|
+
|
|
37
|
+
async function reloadLarkIntegration(): Promise<Response> {
|
|
38
|
+
const result = larkIntegration.reload();
|
|
39
|
+
if (result.ok && larkIntegration.handles.length > 0) {
|
|
40
|
+
console.log(`[server] lark integrated: ${larkIntegration.handles.map((h) => h.appId).join(',')}`);
|
|
41
|
+
}
|
|
42
|
+
return new Response(JSON.stringify(result.ok ? { ok: true, data: result } : { ok: false, error: result.error, data: { bots: result.bots } }), {
|
|
43
|
+
status: result.ok ? 200 : 400,
|
|
44
|
+
headers: { 'content-type': 'application/json' },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (larkIntegration.handles.length > 0) {
|
|
48
|
+
console.log(`[server] lark integrated: ${larkIntegration.handles.map((h) => h.appId).join(',')}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
process.on('SIGINT', () => {
|
|
52
|
+
larkIntegration.stop();
|
|
53
|
+
server.stop();
|
|
54
|
+
process.exit(0);
|
|
55
|
+
});
|
|
56
|
+
process.on('SIGTERM', () => {
|
|
57
|
+
larkIntegration.stop();
|
|
58
|
+
server.stop();
|
|
59
|
+
process.exit(0);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Keep the process alive so the Lark WSClient and HTTP server continue running
|
|
63
|
+
const keepAlive = setInterval(() => {}, 60000);
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { chmodSync, constants, existsSync, lstatSync, mkdirSync, openSync, readFileSync, statSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { dirname, join, resolve } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { homeDir } from './config.js';
|
|
5
|
+
|
|
6
|
+
const TOKEN_BYTES = 32;
|
|
7
|
+
|
|
8
|
+
function assertPrivateDir(path: string): void {
|
|
9
|
+
if (!existsSync(path)) {
|
|
10
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const link = lstatSync(path);
|
|
14
|
+
if (link.isSymbolicLink()) throw new Error(`unsafe token directory: ${path} is a symlink`);
|
|
15
|
+
const stat = statSync(path);
|
|
16
|
+
if (!stat.isDirectory()) throw new Error(`unsafe token directory: ${path} is not a directory`);
|
|
17
|
+
if ((stat.mode & 0o777) !== 0o700) {
|
|
18
|
+
chmodSync(path, 0o700);
|
|
19
|
+
const fixed = statSync(path);
|
|
20
|
+
if ((fixed.mode & 0o777) !== 0o700) throw new Error(`unsafe token directory: ${path} permissions must be 0700`);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function assertPrivateTokenFile(path: string): void {
|
|
25
|
+
const link = lstatSync(path);
|
|
26
|
+
if (link.isSymbolicLink()) throw new Error(`unsafe token file: ${path} is a symlink`);
|
|
27
|
+
const stat = statSync(path);
|
|
28
|
+
if (!stat.isFile()) throw new Error(`unsafe token file: ${path} is not a regular file`);
|
|
29
|
+
if ((stat.mode & 0o177) !== 0) throw new Error(`unsafe token file: ${path} permissions must be 0600`);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function defaultTokenPath(): string {
|
|
33
|
+
const lockHome = process.env.LOCK_HOME ? resolve(process.env.LOCK_HOME) : resolve(join(homedir(), '.lock'));
|
|
34
|
+
return resolve(process.env.LOCK_DAEMON_TOKEN_FILE ?? join(lockHome, 'daemon-token'));
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function legacyTokenPath(): string {
|
|
38
|
+
return join(homeDir(), 'daemon-token');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function loadOrCreateDaemonToken(path = defaultTokenPath()): string {
|
|
42
|
+
const resolved = resolve(path);
|
|
43
|
+
assertPrivateDir(dirname(resolved));
|
|
44
|
+
|
|
45
|
+
if (existsSync(resolved)) {
|
|
46
|
+
assertPrivateTokenFile(resolved);
|
|
47
|
+
const token = readFileSync(resolved, 'utf8').trim();
|
|
48
|
+
if (!token) throw new Error(`empty token file: ${resolved}`);
|
|
49
|
+
return token;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const token = crypto.getRandomValues(new Uint8Array(TOKEN_BYTES)).reduce((value, byte) => value + byte.toString(16).padStart(2, '0'), '');
|
|
53
|
+
const fd = openSync(resolved, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY, 0o600);
|
|
54
|
+
writeFileSync(fd, `${token}\n`);
|
|
55
|
+
assertPrivateTokenFile(resolved);
|
|
56
|
+
return token;
|
|
57
|
+
}
|