@kafca/agentdock 0.1.13 → 0.1.15

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 (27) hide show
  1. package/dist/renderer/assets/{index-BM0Jl5gB.js → index-BSFT2vom.js} +136 -121
  2. package/dist/renderer/assets/index-tUukeThX.css +11 -0
  3. package/dist/renderer/index.html +2 -2
  4. package/dist-electron/electron/agent-runtime-detector.test.js +121 -0
  5. package/dist-electron/electron/lac-cli.test.js +63 -0
  6. package/dist-electron/electron/local-core-refactor.test.js +54 -0
  7. package/dist-electron/electron/plugin-kernel.test.js +6 -6
  8. package/dist-electron/electron/runtime-detection-service.test.js +116 -0
  9. package/dist-electron/electron/security-approval-store.test.js +89 -0
  10. package/dist-electron/electron/workspace-task-store.test.js +66 -0
  11. package/dist-electron/packages/core-sdk/src/index.js +124 -0
  12. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-backend.js +68 -0
  13. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-store.js +617 -0
  14. package/dist-electron/services/local-ai-core/src/acp/local-core-acp-turn-coordinator.js +11 -0
  15. package/dist-electron/services/local-ai-core/src/cli/lac.js +19 -12
  16. package/dist-electron/services/local-ai-core/src/kernel/bootstrap.js +5 -0
  17. package/dist-electron/services/local-ai-core/src/plugins/builtin/scheduler-local-plugin.js +35 -0
  18. package/dist-electron/services/local-ai-core/src/router/workspace-router.js +197 -0
  19. package/dist-electron/services/local-ai-core/src/runtime/agent-runtime-detector.js +237 -0
  20. package/dist-electron/services/local-ai-core/src/runtime/local-core-controller.js +72 -0
  21. package/dist-electron/services/local-ai-core/src/runtime/runtime-detection-service.js +138 -0
  22. package/dist-electron/services/local-ai-core/src/runtime/runtime-detection-store.js +46 -0
  23. package/dist-electron/services/local-ai-core/src/runtime/server.js +151 -0
  24. package/dist-electron/services/local-ai-core/src/scheduler/local-schedule-adapter.js +61 -0
  25. package/dist-electron/services/local-ai-core/src/security/command-risk.js +57 -0
  26. package/package.json +1 -1
  27. package/dist/renderer/assets/index-DrLbJPYN.css +0 -11
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.LocalCoreAcpStore = void 0;
4
+ exports.redactSecrets = redactSecrets;
4
5
  const node_crypto_1 = require("node:crypto");
5
6
  const node_fs_1 = require("node:fs");
6
7
  const node_sqlite_1 = require("node:sqlite");
@@ -123,6 +124,98 @@ class LocalCoreAcpStore {
123
124
  FOREIGN KEY (job_id) REFERENCES scheduled_jobs(id) ON DELETE CASCADE
124
125
  );
125
126
  CREATE INDEX IF NOT EXISTS idx_scheduled_job_runs_job_triggered ON scheduled_job_runs (job_id, triggered_at DESC);
127
+ CREATE TABLE IF NOT EXISTS workspace_registry (
128
+ id TEXT PRIMARY KEY,
129
+ display_name TEXT NOT NULL,
130
+ path TEXT NOT NULL,
131
+ device_id TEXT NOT NULL,
132
+ default_runtime_id TEXT,
133
+ git_json TEXT NOT NULL DEFAULT '{}',
134
+ health_json TEXT NOT NULL DEFAULT '{}',
135
+ metadata_json TEXT NOT NULL DEFAULT '{}',
136
+ created_at TEXT NOT NULL,
137
+ updated_at TEXT NOT NULL,
138
+ last_opened_at TEXT
139
+ );
140
+ CREATE INDEX IF NOT EXISTS idx_workspace_registry_updated ON workspace_registry (updated_at DESC);
141
+ CREATE TABLE IF NOT EXISTS agent_tasks (
142
+ id TEXT PRIMARY KEY,
143
+ workspace_id TEXT NOT NULL,
144
+ device_id TEXT NOT NULL,
145
+ runtime_id TEXT NOT NULL,
146
+ thread_id TEXT,
147
+ run_id TEXT,
148
+ title TEXT NOT NULL,
149
+ prompt TEXT,
150
+ status TEXT NOT NULL,
151
+ created_at TEXT NOT NULL,
152
+ updated_at TEXT NOT NULL,
153
+ queued_at TEXT,
154
+ started_at TEXT,
155
+ completed_at TEXT,
156
+ summary TEXT,
157
+ error TEXT,
158
+ timeline_json TEXT NOT NULL DEFAULT '[]',
159
+ logs_json TEXT NOT NULL DEFAULT '[]',
160
+ artifacts_json TEXT NOT NULL DEFAULT '[]',
161
+ approval_ids_json TEXT NOT NULL DEFAULT '[]',
162
+ metadata_json TEXT NOT NULL DEFAULT '{}'
163
+ );
164
+ CREATE INDEX IF NOT EXISTS idx_agent_tasks_workspace_updated ON agent_tasks (workspace_id, updated_at DESC);
165
+ CREATE INDEX IF NOT EXISTS idx_agent_tasks_status_updated ON agent_tasks (status, updated_at DESC);
166
+ CREATE INDEX IF NOT EXISTS idx_agent_tasks_run ON agent_tasks (run_id);
167
+ CREATE TABLE IF NOT EXISTS workspace_security_settings (
168
+ workspace_id TEXT PRIMARY KEY,
169
+ permissions_json TEXT NOT NULL,
170
+ allow_paths_json TEXT NOT NULL DEFAULT '[]',
171
+ deny_paths_json TEXT NOT NULL DEFAULT '[]',
172
+ updated_at TEXT NOT NULL,
173
+ updated_by TEXT
174
+ );
175
+ CREATE TABLE IF NOT EXISTS approval_requests (
176
+ id TEXT PRIMARY KEY,
177
+ workspace_id TEXT NOT NULL,
178
+ task_id TEXT,
179
+ thread_id TEXT,
180
+ run_id TEXT,
181
+ device_id TEXT NOT NULL,
182
+ kind TEXT NOT NULL,
183
+ status TEXT NOT NULL,
184
+ risk_level TEXT NOT NULL,
185
+ title TEXT NOT NULL,
186
+ description TEXT NOT NULL,
187
+ requested_action TEXT NOT NULL,
188
+ command TEXT,
189
+ scopes_json TEXT NOT NULL DEFAULT '[]',
190
+ options_json TEXT NOT NULL DEFAULT '[]',
191
+ requested_by TEXT,
192
+ resolved_by TEXT,
193
+ resolution TEXT,
194
+ created_at TEXT NOT NULL,
195
+ updated_at TEXT NOT NULL,
196
+ resolved_at TEXT,
197
+ expires_at TEXT,
198
+ metadata_json TEXT NOT NULL DEFAULT '{}'
199
+ );
200
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_workspace_updated ON approval_requests (workspace_id, updated_at DESC);
201
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_status_updated ON approval_requests (status, updated_at DESC);
202
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_task ON approval_requests (task_id);
203
+ CREATE INDEX IF NOT EXISTS idx_approval_requests_run ON approval_requests (run_id);
204
+ CREATE TABLE IF NOT EXISTS audit_events (
205
+ id TEXT PRIMARY KEY,
206
+ type TEXT NOT NULL,
207
+ workspace_id TEXT,
208
+ task_id TEXT,
209
+ approval_id TEXT,
210
+ actor TEXT,
211
+ summary TEXT NOT NULL,
212
+ risk_level TEXT,
213
+ created_at TEXT NOT NULL,
214
+ metadata_json TEXT NOT NULL DEFAULT '{}'
215
+ );
216
+ CREATE INDEX IF NOT EXISTS idx_audit_events_workspace_created ON audit_events (workspace_id, created_at DESC);
217
+ CREATE INDEX IF NOT EXISTS idx_audit_events_task_created ON audit_events (task_id, created_at DESC);
218
+ CREATE INDEX IF NOT EXISTS idx_audit_events_type_created ON audit_events (type, created_at DESC);
126
219
  `);
127
220
  this.ensureColumn('scheduled_jobs', 'execution_mode', "TEXT NOT NULL DEFAULT 'same-thread'");
128
221
  }
@@ -311,6 +404,394 @@ class LocalCoreAcpStore {
311
404
  WHERE id = ?
312
405
  `).get(runId);
313
406
  }
407
+ listWorkspaceRegistry() {
408
+ const rows = this.db.prepare(`
409
+ SELECT id, display_name, path, device_id, default_runtime_id, git_json, health_json, metadata_json, created_at, updated_at, last_opened_at
410
+ FROM workspace_registry
411
+ ORDER BY display_name ASC
412
+ `).all();
413
+ return rows.map((row) => this.toWorkspaceRegistryEntry(row));
414
+ }
415
+ getWorkspaceRegistryEntry(workspaceId) {
416
+ const row = this.db.prepare(`
417
+ SELECT id, display_name, path, device_id, default_runtime_id, git_json, health_json, metadata_json, created_at, updated_at, last_opened_at
418
+ FROM workspace_registry
419
+ WHERE id = ?
420
+ `).get(workspaceId);
421
+ return row ? this.toWorkspaceRegistryEntry(row) : undefined;
422
+ }
423
+ upsertWorkspaceRegistryEntry(input) {
424
+ const id = input.workspaceId || input.displayName;
425
+ const now = new Date().toISOString();
426
+ const existing = this.getWorkspaceRegistryEntry(id);
427
+ const health = input.health || existing?.health || {
428
+ status: 'unknown',
429
+ summary: 'Workspace health has not been checked.',
430
+ issues: [],
431
+ };
432
+ this.db.prepare(`
433
+ INSERT INTO workspace_registry (
434
+ id, display_name, path, device_id, default_runtime_id, git_json, health_json, metadata_json, created_at, updated_at, last_opened_at
435
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
436
+ ON CONFLICT(id) DO UPDATE SET
437
+ display_name = excluded.display_name,
438
+ path = excluded.path,
439
+ device_id = excluded.device_id,
440
+ default_runtime_id = excluded.default_runtime_id,
441
+ git_json = excluded.git_json,
442
+ health_json = excluded.health_json,
443
+ metadata_json = excluded.metadata_json,
444
+ updated_at = excluded.updated_at
445
+ `).run(id, input.displayName, input.path, input.deviceId, input.defaultRuntimeId || null, JSON.stringify(input.git || existing?.git || {}), JSON.stringify(health), JSON.stringify(input.metadata || existing?.metadata || {}), existing?.createdAt || now, now, existing?.lastOpenedAt || null);
446
+ return this.getWorkspaceRegistryEntry(id);
447
+ }
448
+ updateWorkspaceRegistryEntry(workspaceId, input) {
449
+ const existing = this.getWorkspaceRegistryEntry(workspaceId);
450
+ if (!existing) {
451
+ throw new Error(`Workspace not found: ${workspaceId}`);
452
+ }
453
+ return this.upsertWorkspaceRegistryEntry({
454
+ workspaceId,
455
+ displayName: input.displayName || existing.displayName,
456
+ path: input.path || existing.path,
457
+ deviceId: existing.deviceId,
458
+ defaultRuntimeId: input.defaultRuntimeId === null ? undefined : input.defaultRuntimeId || existing.defaultRuntimeId,
459
+ git: existing.git,
460
+ health: existing.health,
461
+ metadata: input.metadata || existing.metadata,
462
+ });
463
+ }
464
+ deleteWorkspaceRegistryEntry(workspaceId) {
465
+ this.db.prepare('DELETE FROM workspace_registry WHERE id = ?').run(workspaceId);
466
+ return { deleted: true };
467
+ }
468
+ touchWorkspaceRegistryEntry(workspaceId) {
469
+ this.db.prepare('UPDATE workspace_registry SET last_opened_at = ?, updated_at = ? WHERE id = ?')
470
+ .run(new Date().toISOString(), new Date().toISOString(), workspaceId);
471
+ }
472
+ getWorkspaceSecuritySettings(workspaceId) {
473
+ const row = this.db.prepare(`
474
+ SELECT workspace_id, permissions_json, allow_paths_json, deny_paths_json, updated_at, updated_by
475
+ FROM workspace_security_settings
476
+ WHERE workspace_id = ?
477
+ `).get(workspaceId);
478
+ if (row) {
479
+ return this.toWorkspaceSecuritySettings(row);
480
+ }
481
+ const now = new Date().toISOString();
482
+ return {
483
+ workspaceId,
484
+ permissions: defaultPermissions(),
485
+ allowPaths: [],
486
+ denyPaths: [],
487
+ updatedAt: now,
488
+ };
489
+ }
490
+ updateWorkspaceSecuritySettings(workspaceId, input) {
491
+ const existing = this.getWorkspaceSecuritySettings(workspaceId);
492
+ const now = new Date().toISOString();
493
+ const next = {
494
+ workspaceId,
495
+ permissions: {
496
+ ...existing.permissions,
497
+ ...(input.permissions || {}),
498
+ },
499
+ allowPaths: Array.isArray(input.allowPaths) ? input.allowPaths : existing.allowPaths,
500
+ denyPaths: Array.isArray(input.denyPaths) ? input.denyPaths : existing.denyPaths,
501
+ updatedAt: now,
502
+ updatedBy: input.updatedBy || existing.updatedBy,
503
+ };
504
+ this.db.prepare(`
505
+ INSERT INTO workspace_security_settings (workspace_id, permissions_json, allow_paths_json, deny_paths_json, updated_at, updated_by)
506
+ VALUES (?, ?, ?, ?, ?, ?)
507
+ ON CONFLICT(workspace_id) DO UPDATE SET
508
+ permissions_json = excluded.permissions_json,
509
+ allow_paths_json = excluded.allow_paths_json,
510
+ deny_paths_json = excluded.deny_paths_json,
511
+ updated_at = excluded.updated_at,
512
+ updated_by = excluded.updated_by
513
+ `).run(workspaceId, JSON.stringify(next.permissions), JSON.stringify(next.allowPaths), JSON.stringify(next.denyPaths), now, next.updatedBy || null);
514
+ this.createAuditEvent({
515
+ type: 'permission.changed',
516
+ workspaceId,
517
+ actor: next.updatedBy || 'local',
518
+ summary: 'Workspace security settings changed.',
519
+ metadata: {
520
+ permissions: next.permissions,
521
+ allowPaths: next.allowPaths,
522
+ denyPaths: next.denyPaths,
523
+ },
524
+ });
525
+ return this.getWorkspaceSecuritySettings(workspaceId);
526
+ }
527
+ createApprovalRequest(input) {
528
+ const id = `approval:${(0, node_crypto_1.randomUUID)()}`;
529
+ const now = new Date().toISOString();
530
+ this.db.prepare(`
531
+ INSERT INTO approval_requests (
532
+ id, workspace_id, task_id, thread_id, run_id, device_id, kind, status, risk_level, title, description,
533
+ requested_action, command, scopes_json, options_json, requested_by, created_at, updated_at, expires_at, metadata_json
534
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
535
+ `).run(id, input.workspaceId, input.taskId || null, input.threadId || null, input.runId || null, input.deviceId || 'local', input.kind, input.riskLevel, input.title, redactSecrets(input.description), redactSecrets(input.requestedAction), input.command ? redactSecrets(input.command) : null, JSON.stringify(input.scopes || []), JSON.stringify(input.options || []), input.requestedBy || null, now, now, input.expiresAt || null, JSON.stringify(input.metadata || {}));
536
+ const approval = this.getApprovalRequest(id);
537
+ this.createAuditEvent({
538
+ type: 'approval.requested',
539
+ workspaceId: approval.workspaceId,
540
+ taskId: approval.taskId,
541
+ approvalId: approval.approvalId,
542
+ actor: approval.requestedBy || 'agent',
543
+ summary: approval.title,
544
+ riskLevel: approval.riskLevel,
545
+ metadata: {
546
+ kind: approval.kind,
547
+ requestedAction: approval.requestedAction,
548
+ scopes: approval.scopes,
549
+ },
550
+ });
551
+ if (approval.taskId) {
552
+ this.updateAgentTask(approval.taskId, {
553
+ approvalId: approval.approvalId,
554
+ timelineItem: {
555
+ type: 'approval_requested',
556
+ title: approval.title,
557
+ description: approval.description,
558
+ metadata: { approvalId: approval.approvalId, riskLevel: approval.riskLevel },
559
+ },
560
+ });
561
+ }
562
+ return approval;
563
+ }
564
+ listApprovalRequests(query = {}) {
565
+ const predicates = [];
566
+ const params = [];
567
+ if (query.workspaceId) {
568
+ predicates.push('workspace_id = ?');
569
+ params.push(query.workspaceId);
570
+ }
571
+ if (query.taskId) {
572
+ predicates.push('task_id = ?');
573
+ params.push(query.taskId);
574
+ }
575
+ if (query.status) {
576
+ const statuses = Array.isArray(query.status) ? query.status : [query.status];
577
+ predicates.push(`status IN (${statuses.map(() => '?').join(', ')})`);
578
+ params.push(...statuses);
579
+ }
580
+ const limit = Math.max(1, Math.min(Number(query.limit || 50), 100));
581
+ const where = predicates.length ? `WHERE ${predicates.join(' AND ')}` : '';
582
+ const rows = this.db.prepare(`
583
+ SELECT *
584
+ FROM approval_requests
585
+ ${where}
586
+ ORDER BY updated_at DESC
587
+ LIMIT ?
588
+ `).all(...params, limit);
589
+ return { approvals: rows.map((row) => this.toApprovalRequest(row)) };
590
+ }
591
+ getApprovalRequest(approvalId) {
592
+ const row = this.db.prepare('SELECT * FROM approval_requests WHERE id = ?').get(approvalId);
593
+ return row ? this.toApprovalRequest(row) : undefined;
594
+ }
595
+ resolveApprovalRequest(approvalId, input) {
596
+ const existing = this.getApprovalRequest(approvalId);
597
+ if (!existing) {
598
+ throw new Error(`Approval not found: ${approvalId}`);
599
+ }
600
+ const now = new Date().toISOString();
601
+ this.db.prepare(`
602
+ UPDATE approval_requests
603
+ SET status = ?, resolved_by = ?, resolution = ?, resolved_at = ?, updated_at = ?
604
+ WHERE id = ?
605
+ `).run(input.status, input.resolvedBy || existing.resolvedBy || 'local', input.resolution || existing.resolution || null, now, now, approvalId);
606
+ const approval = this.getApprovalRequest(approvalId);
607
+ this.createAuditEvent({
608
+ type: input.status === 'rejected' ? 'approval.rejected' : 'approval.resolved',
609
+ workspaceId: approval.workspaceId,
610
+ taskId: approval.taskId,
611
+ approvalId: approval.approvalId,
612
+ actor: approval.resolvedBy || 'local',
613
+ summary: `Approval ${input.status}: ${approval.title}`,
614
+ riskLevel: approval.riskLevel,
615
+ metadata: { resolution: approval.resolution },
616
+ });
617
+ if (approval.taskId) {
618
+ this.updateAgentTask(approval.taskId, {
619
+ timelineItem: {
620
+ type: 'approval_resolved',
621
+ title: `Approval ${input.status}`,
622
+ description: approval.resolution || approval.title,
623
+ metadata: { approvalId: approval.approvalId, status: input.status },
624
+ },
625
+ });
626
+ }
627
+ return approval;
628
+ }
629
+ createAuditEvent(input) {
630
+ const id = `audit:${(0, node_crypto_1.randomUUID)()}`;
631
+ const now = new Date().toISOString();
632
+ this.db.prepare(`
633
+ INSERT INTO audit_events (id, type, workspace_id, task_id, approval_id, actor, summary, risk_level, created_at, metadata_json)
634
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
635
+ `).run(id, input.type, input.workspaceId || null, input.taskId || null, input.approvalId || null, input.actor || null, redactSecrets(input.summary), input.riskLevel || null, now, JSON.stringify(input.metadata || {}));
636
+ return this.toAuditEvent(this.db.prepare('SELECT * FROM audit_events WHERE id = ?').get(id));
637
+ }
638
+ listAuditEvents(query = {}) {
639
+ const predicates = [];
640
+ const params = [];
641
+ if (query.workspaceId) {
642
+ predicates.push('workspace_id = ?');
643
+ params.push(query.workspaceId);
644
+ }
645
+ if (query.taskId) {
646
+ predicates.push('task_id = ?');
647
+ params.push(query.taskId);
648
+ }
649
+ if (query.approvalId) {
650
+ predicates.push('approval_id = ?');
651
+ params.push(query.approvalId);
652
+ }
653
+ if (query.type) {
654
+ const types = Array.isArray(query.type) ? query.type : [query.type];
655
+ predicates.push(`type IN (${types.map(() => '?').join(', ')})`);
656
+ params.push(...types);
657
+ }
658
+ const limit = Math.max(1, Math.min(Number(query.limit || 50), 100));
659
+ const where = predicates.length ? `WHERE ${predicates.join(' AND ')}` : '';
660
+ const rows = this.db.prepare(`
661
+ SELECT *
662
+ FROM audit_events
663
+ ${where}
664
+ ORDER BY created_at DESC
665
+ LIMIT ?
666
+ `).all(...params, limit);
667
+ return { events: rows.map((row) => this.toAuditEvent(row)) };
668
+ }
669
+ createAgentTask(input) {
670
+ const id = `task:${(0, node_crypto_1.randomUUID)()}`;
671
+ const now = new Date().toISOString();
672
+ const status = input.status || 'created';
673
+ const timeline = [{
674
+ id: `timeline:${(0, node_crypto_1.randomUUID)()}`,
675
+ type: 'status_change',
676
+ title: `Task ${status}`,
677
+ status,
678
+ timestamp: now,
679
+ }];
680
+ this.db.prepare(`
681
+ INSERT INTO agent_tasks (
682
+ id, workspace_id, device_id, runtime_id, thread_id, run_id, title, prompt, status, created_at, updated_at, queued_at,
683
+ started_at, completed_at, summary, error, timeline_json, logs_json, artifacts_json, approval_ids_json, metadata_json
684
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NULL, NULL, NULL, ?, '[]', '[]', '[]', ?)
685
+ `).run(id, input.workspaceId, input.deviceId, input.runtimeId, input.threadId || null, input.runId || null, input.title, input.prompt ? redactSecrets(input.prompt) : null, status, now, now, status === 'queued' ? now : null, status === 'running' ? now : null, JSON.stringify(timeline), JSON.stringify(input.metadata || {}));
686
+ const task = this.getAgentTask(id);
687
+ this.createAuditEvent({
688
+ type: 'task.created',
689
+ workspaceId: task.workspaceId,
690
+ taskId: task.taskId,
691
+ actor: 'local',
692
+ summary: `Task created: ${task.title}`,
693
+ metadata: { runtimeId: task.runtimeId, threadId: task.threadId, runId: task.runId },
694
+ });
695
+ return task;
696
+ }
697
+ listAgentTasks(query = {}) {
698
+ const predicates = [];
699
+ const params = [];
700
+ if (query.workspaceId) {
701
+ predicates.push('workspace_id = ?');
702
+ params.push(query.workspaceId);
703
+ }
704
+ if (query.runtimeId) {
705
+ predicates.push('runtime_id = ?');
706
+ params.push(query.runtimeId);
707
+ }
708
+ if (query.status) {
709
+ const statuses = Array.isArray(query.status) ? query.status : [query.status];
710
+ predicates.push(`status IN (${statuses.map(() => '?').join(', ')})`);
711
+ params.push(...statuses);
712
+ }
713
+ const limit = Math.max(1, Math.min(Number(query.limit || 50), 100));
714
+ const where = predicates.length ? `WHERE ${predicates.join(' AND ')}` : '';
715
+ const rows = this.db.prepare(`
716
+ SELECT *
717
+ FROM agent_tasks
718
+ ${where}
719
+ ORDER BY updated_at DESC
720
+ LIMIT ?
721
+ `).all(...params, limit);
722
+ return { tasks: rows.map((row) => this.toAgentTask(row)) };
723
+ }
724
+ getAgentTask(taskId) {
725
+ const row = this.db.prepare('SELECT * FROM agent_tasks WHERE id = ?').get(taskId);
726
+ return row ? this.toAgentTask(row) : undefined;
727
+ }
728
+ getAgentTaskByRunId(runId) {
729
+ const row = this.db.prepare('SELECT * FROM agent_tasks WHERE run_id = ? ORDER BY updated_at DESC LIMIT 1').get(runId);
730
+ return row ? this.toAgentTask(row) : undefined;
731
+ }
732
+ updateAgentTask(taskId, input) {
733
+ const existing = this.getAgentTask(taskId);
734
+ if (!existing) {
735
+ throw new Error(`Task not found: ${taskId}`);
736
+ }
737
+ const now = new Date().toISOString();
738
+ const nextStatus = input.status || existing.status;
739
+ const timeline = [...existing.timeline];
740
+ const logs = [...existing.logs];
741
+ const artifacts = [...existing.artifacts];
742
+ const approvalIds = [...existing.approvalIds];
743
+ if (input.status && input.status !== existing.status) {
744
+ timeline.push({
745
+ id: `timeline:${(0, node_crypto_1.randomUUID)()}`,
746
+ type: 'status_change',
747
+ title: `Task ${input.status}`,
748
+ status: input.status,
749
+ timestamp: now,
750
+ });
751
+ }
752
+ if (input.timelineItem) {
753
+ timeline.push({
754
+ id: input.timelineItem.id || `timeline:${(0, node_crypto_1.randomUUID)()}`,
755
+ timestamp: input.timelineItem.timestamp || now,
756
+ ...input.timelineItem,
757
+ });
758
+ }
759
+ if (input.log) {
760
+ logs.push({
761
+ id: input.log.id || `log:${(0, node_crypto_1.randomUUID)()}`,
762
+ timestamp: input.log.timestamp || now,
763
+ ...input.log,
764
+ message: redactSecrets(input.log.message),
765
+ });
766
+ }
767
+ if (input.artifact) {
768
+ artifacts.push({
769
+ id: input.artifact.id || `artifact:${(0, node_crypto_1.randomUUID)()}`,
770
+ ...input.artifact,
771
+ });
772
+ }
773
+ if (input.approvalId && !approvalIds.includes(input.approvalId)) {
774
+ approvalIds.push(input.approvalId);
775
+ }
776
+ this.db.prepare(`
777
+ UPDATE agent_tasks
778
+ SET status = ?, thread_id = ?, run_id = ?, title = ?, updated_at = ?, queued_at = ?, started_at = ?, completed_at = ?,
779
+ summary = ?, error = ?, timeline_json = ?, logs_json = ?, artifacts_json = ?, approval_ids_json = ?, metadata_json = ?
780
+ WHERE id = ?
781
+ `).run(nextStatus, input.threadId || existing.threadId || null, input.runId || existing.runId || null, input.title || existing.title, now, existing.queuedAt || (nextStatus === 'queued' ? now : null), existing.startedAt || (nextStatus === 'running' ? now : null), existing.completedAt || (['completed', 'failed', 'cancelled'].includes(nextStatus) ? now : null), input.summary ?? existing.summary ?? null, input.error === null ? null : input.error ?? existing.error ?? null, JSON.stringify(timeline), JSON.stringify(logs), JSON.stringify(artifacts), JSON.stringify(approvalIds), JSON.stringify(input.metadata || existing.metadata || {}), taskId);
782
+ const task = this.getAgentTask(taskId);
783
+ if (input.status && input.status !== existing.status) {
784
+ this.createAuditEvent({
785
+ type: 'task.updated',
786
+ workspaceId: task.workspaceId,
787
+ taskId: task.taskId,
788
+ actor: 'local',
789
+ summary: `Task status changed to ${input.status}.`,
790
+ metadata: { previousStatus: existing.status, status: input.status, runId: task.runId },
791
+ });
792
+ }
793
+ return task;
794
+ }
314
795
  listScheduledJobs(workspaceId) {
315
796
  const query = workspaceId
316
797
  ? `
@@ -663,6 +1144,118 @@ class LocalCoreAcpStore {
663
1144
  platformMessageId: row.platform_message_id || undefined,
664
1145
  };
665
1146
  }
1147
+ toWorkspaceRegistryEntry(row) {
1148
+ const activeTaskCount = Number(this.db.prepare(`
1149
+ SELECT COUNT(*) AS total
1150
+ FROM agent_tasks
1151
+ WHERE workspace_id = ? AND status IN ('created', 'queued', 'running', 'waiting_for_user')
1152
+ `).get(row.id)?.total || 0);
1153
+ const recentTaskRows = this.db.prepare(`
1154
+ SELECT id
1155
+ FROM agent_tasks
1156
+ WHERE workspace_id = ?
1157
+ ORDER BY updated_at DESC
1158
+ LIMIT 8
1159
+ `).all(row.id);
1160
+ return {
1161
+ workspaceId: row.id,
1162
+ displayName: row.display_name,
1163
+ path: row.path,
1164
+ deviceId: row.device_id,
1165
+ createdAt: row.created_at,
1166
+ updatedAt: row.updated_at,
1167
+ lastOpenedAt: row.last_opened_at || undefined,
1168
+ defaultRuntimeId: row.default_runtime_id || undefined,
1169
+ git: parseJson(row.git_json, { isRepo: false }),
1170
+ health: parseJson(row.health_json, {
1171
+ status: 'unknown',
1172
+ summary: 'Workspace health has not been checked.',
1173
+ issues: [],
1174
+ }),
1175
+ activeTaskCount,
1176
+ recentTaskIds: recentTaskRows.map((item) => item.id),
1177
+ metadata: parseJson(row.metadata_json, {}),
1178
+ };
1179
+ }
1180
+ toAgentTask(row) {
1181
+ return {
1182
+ taskId: row.id,
1183
+ workspaceId: row.workspace_id,
1184
+ deviceId: row.device_id,
1185
+ runtimeId: row.runtime_id,
1186
+ threadId: row.thread_id || undefined,
1187
+ runId: row.run_id || undefined,
1188
+ title: row.title,
1189
+ prompt: row.prompt || undefined,
1190
+ status: row.status,
1191
+ createdAt: row.created_at,
1192
+ updatedAt: row.updated_at,
1193
+ queuedAt: row.queued_at || undefined,
1194
+ startedAt: row.started_at || undefined,
1195
+ completedAt: row.completed_at || undefined,
1196
+ summary: row.summary || undefined,
1197
+ error: row.error || undefined,
1198
+ timeline: parseJson(row.timeline_json, []),
1199
+ logs: parseJson(row.logs_json, []),
1200
+ artifacts: parseJson(row.artifacts_json, []),
1201
+ approvalIds: parseJson(row.approval_ids_json, []),
1202
+ metadata: parseJson(row.metadata_json, {}),
1203
+ };
1204
+ }
1205
+ toWorkspaceSecuritySettings(row) {
1206
+ return {
1207
+ workspaceId: row.workspace_id,
1208
+ permissions: {
1209
+ ...defaultPermissions(),
1210
+ ...parseJson(row.permissions_json, {}),
1211
+ },
1212
+ allowPaths: parseJson(row.allow_paths_json, []),
1213
+ denyPaths: parseJson(row.deny_paths_json, []),
1214
+ updatedAt: row.updated_at,
1215
+ updatedBy: row.updated_by || undefined,
1216
+ };
1217
+ }
1218
+ toApprovalRequest(row) {
1219
+ return {
1220
+ approvalId: row.id,
1221
+ workspaceId: row.workspace_id,
1222
+ taskId: row.task_id || undefined,
1223
+ threadId: row.thread_id || undefined,
1224
+ runId: row.run_id || undefined,
1225
+ deviceId: row.device_id,
1226
+ kind: row.kind,
1227
+ status: row.status,
1228
+ riskLevel: row.risk_level,
1229
+ title: row.title,
1230
+ description: row.description,
1231
+ requestedAction: row.requested_action,
1232
+ command: row.command || undefined,
1233
+ scopes: parseJson(row.scopes_json, []),
1234
+ options: parseJson(row.options_json, []),
1235
+ requestedBy: row.requested_by || undefined,
1236
+ resolvedBy: row.resolved_by || undefined,
1237
+ resolution: row.resolution || undefined,
1238
+ createdAt: row.created_at,
1239
+ updatedAt: row.updated_at,
1240
+ resolvedAt: row.resolved_at || undefined,
1241
+ expiresAt: row.expires_at || undefined,
1242
+ metadata: parseJson(row.metadata_json, {}),
1243
+ };
1244
+ }
1245
+ toAuditEvent(row) {
1246
+ return {
1247
+ auditId: row.id,
1248
+ type: row.type,
1249
+ workspaceId: row.workspace_id || undefined,
1250
+ taskId: row.task_id || undefined,
1251
+ approvalId: row.approval_id || undefined,
1252
+ actor: row.actor || undefined,
1253
+ summary: row.summary,
1254
+ riskLevel: row.risk_level || undefined,
1255
+ createdAt: row.created_at,
1256
+ metadata: parseJson(row.metadata_json, {}),
1257
+ };
1258
+ }
666
1259
  ensureColumn(table, column, definition) {
667
1260
  const rows = this.db.prepare(`PRAGMA table_info(${table})`).all();
668
1261
  if (rows.some((row) => row.name === column)) {
@@ -672,3 +1265,27 @@ class LocalCoreAcpStore {
672
1265
  }
673
1266
  }
674
1267
  exports.LocalCoreAcpStore = LocalCoreAcpStore;
1268
+ function parseJson(value, fallback) {
1269
+ try {
1270
+ return JSON.parse(value);
1271
+ }
1272
+ catch {
1273
+ return fallback;
1274
+ }
1275
+ }
1276
+ function defaultPermissions() {
1277
+ return {
1278
+ 'workspace.read': 'allow',
1279
+ 'workspace.write': 'ask',
1280
+ 'command.execute': 'ask',
1281
+ 'network.access': 'ask',
1282
+ 'secrets.access': 'deny',
1283
+ 'git.modify': 'ask',
1284
+ };
1285
+ }
1286
+ function redactSecrets(value) {
1287
+ return value
1288
+ .replace(/\b(sk-[A-Za-z0-9_-]{12,})\b/g, '[REDACTED_SECRET]')
1289
+ .replace(/\b([A-Za-z0-9_]*TOKEN[A-Za-z0-9_]*|[A-Za-z0-9_]*SECRET[A-Za-z0-9_]*|[A-Za-z0-9_]*KEY[A-Za-z0-9_]*)=([^\s]+)/gi, '$1=[REDACTED_SECRET]')
1290
+ .replace(/\b(Bearer\s+)[A-Za-z0-9._-]+/gi, '$1[REDACTED_SECRET]');
1291
+ }
@@ -103,6 +103,17 @@ class LocalCoreAcpTurnCoordinator {
103
103
  isSchedulerAdd,
104
104
  options,
105
105
  };
106
+ const approvalId = this.options.createApprovalRequest?.({
107
+ threadId: session.threadId,
108
+ runId: currentRunId,
109
+ title: toolTitle ? `Approve ${toolTitle}` : 'Approve agent action',
110
+ description: toolTitle || 'Agent requested permission before continuing.',
111
+ command: toolTitle,
112
+ options,
113
+ });
114
+ if (approvalId) {
115
+ permissionRequest.approvalId = approvalId;
116
+ }
106
117
  session.pendingPermissionByRun.set(currentRunId, permissionRequest);
107
118
  if (session.currentTurn) {
108
119
  session.currentTurn.permission = permissionRequest;