Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,6 @@
"@vitejs/plugin-react": "^4.7.0",
"@vitest/coverage-v8": "^4.1.1",
"autoprefixer": "^10.4.21",
"better-sqlite3": "^12.8.0",
"drizzle-kit": "^0.31.10",
"electron": "41.0.3",
"eslint": "^8.57.1",
Expand All @@ -86,6 +85,7 @@
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
"better-sqlite3": "^12.8.0",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
Expand Down
6 changes: 3 additions & 3 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions src/electron/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import path from 'node:path';
import started from 'electron-squirrel-startup';

import { createQuitCoordinator } from '@/electron/main/quit-coordinator';
import { AuditDatabase } from '@/electron/main/audit/audit-db';
import { resolveAgentLiteRuntimeRoot } from '@/electron/main/dune-paths';
import { registerMainIpcHandlers } from '@/electron/main/ipc/register-main-ipc-handlers';
import { createTelegramPowerCoordinator } from '@/electron/main/lifecycle/telegram-power-coordinator';
Expand Down Expand Up @@ -69,6 +70,7 @@ void app.whenReady().then(async () => {
const agentLiteHomeDir = process.env.DUNE_AGENTLITE_HOME_DIR;
const agentLiteRuntimeRoot = resolveAgentLiteRuntimeRoot(agentLiteHomeDir);
const userDataDir = app.getPath('userData');
const auditLog = new AuditDatabase(userDataDir);
const stores = {
agents: new JsonFileStorage(userDataDir, 'agents'),
secrets: new EncryptedFileStorage(userDataDir, 'secrets'),
Expand Down Expand Up @@ -114,6 +116,7 @@ void app.whenReady().then(async () => {
runtimeBootstrap = createRuntimeBootstrap({
agentStore: stores.agents,
app,
auditLog,
...(agentLiteHomeDir ? { agentLiteHomeDir } : {}),
onAgentIdle: workflowCoordinator.onAgentIdle,
onItemActivityChanged: (payload) => {
Expand All @@ -134,6 +137,7 @@ void app.whenReady().then(async () => {
workflowCoordinator.stop();
telegramPowerCoordinator.shutdown();
await runtimeBootstrap?.shutdown();
auditLog.close();
};

const applyPersistedNetworkSettings = async () => {
Expand All @@ -155,6 +159,7 @@ void app.whenReady().then(async () => {

registerMainIpcHandlers({
applyPersistedNetworkSettings,
auditLog,
createInitialRuntimeSnapshot,
deleteLocalData: async () => {
await shutdownMainProcess();
Expand Down
78 changes: 74 additions & 4 deletions src/electron/main/agent-actions/handlers/items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ import {
} from './validators';
import { ToolHandlerError, type RegisteredTool } from './types';

/** Returns the actor label for audit events emitted by agent actions. */
function auditActor(agentContext: { agentId?: string; agentName?: string }) {
return agentContext.agentName ?? 'user';
}

/** Lists item tools. */
export const itemTools: RegisteredTool[] = [
{
Expand Down Expand Up @@ -76,7 +81,7 @@ export const itemTools: RegisteredTool[] = [
),
name: 'workflow.items.create',
},
handler: async ({ agentContext, onWorkflowChanged, workflowStore }, args) => {
handler: async ({ agentContext, auditLog, onWorkflowChanged, workflowStore }, args) => {
const snapshot = await readWorkflowSnapshot(workflowStore);
const projectId = resolveProjectId(args.projectId, agentContext.projectId);
const title = requireString(args.title, 'title');
Expand Down Expand Up @@ -124,6 +129,16 @@ export const itemTools: RegisteredTool[] = [

touchProject(snapshot, projectId, now);
await writeWorkflowSnapshot(workflowStore, snapshot, onWorkflowChanged);
auditLog?.record({
actor: auditActor(agentContext),
actorType: 'agent',
eventType: 'item.created',
itemId,
itemTitle: title,
projectId,
summary: `"${title}" created`,
details: { status },
});
return { item: presentItem(snapshot, item), itemId };
},
},
Expand All @@ -142,12 +157,14 @@ export const itemTools: RegisteredTool[] = [
),
name: 'workflow.items.update',
},
handler: async ({ agentContext, getRuntimeController, onWorkflowChanged, workflowStore }, args) => {
handler: async ({ agentContext, auditLog, getRuntimeController, onWorkflowChanged, workflowStore }, args) => {
const snapshot = await readWorkflowSnapshot(workflowStore);
const item = findItem(snapshot, requireString(args.itemId, 'itemId'));
const note = optionalString(args.note);
const title = optionalString(args.title);
const now = Date.now();
const previousTitle = item.title;
const previousBrief = item.brief;

const touchesDetails = args.title !== undefined || args.brief !== undefined;
const touchesAssignment = args.primaryAgentId !== undefined;
Expand Down Expand Up @@ -209,6 +226,36 @@ export const itemTools: RegisteredTool[] = [
touchProject(snapshot, item.projectId, now);

await writeWorkflowSnapshot(workflowStore, snapshot, onWorkflowChanged);
if (touchesDetails) {
auditLog?.record({
actor: auditActor(agentContext),
actorType: 'agent',
eventType: 'item.updated',
itemId: item.id,
itemTitle: item.title,
projectId: item.projectId,
summary: `"${item.title}" updated`,
details: {
briefChanged: previousBrief !== item.brief,
titleChanged: previousTitle !== item.title,
},
});
}

if (touchesAssignment) {
auditLog?.record({
actor: auditActor(agentContext),
actorType: 'agent',
eventType: 'agent.assigned',
itemId: item.id,
itemTitle: item.title,
projectId: item.projectId,
summary: item.primaryAgentId
? `Agent assigned to "${item.title}"`
: `Agent cleared for "${item.title}"`,
details: { primaryAgentId: item.primaryAgentId },
});
}
return { item: presentItem(snapshot, item) };
},
},
Expand All @@ -226,7 +273,7 @@ export const itemTools: RegisteredTool[] = [
),
name: 'workflow.items.move',
},
handler: async ({ agentContext, onWorkflowChanged, workflowStore }, args) => {
handler: async ({ agentContext, auditLog, onWorkflowChanged, workflowStore }, args) => {
const snapshot = await readWorkflowSnapshot(workflowStore);
const item = findItem(snapshot, requireString(args.itemId, 'itemId'));
const note = optionalString(args.note);
Expand Down Expand Up @@ -281,6 +328,19 @@ export const itemTools: RegisteredTool[] = [
touchProject(snapshot, item.projectId, now);

await writeWorkflowSnapshot(workflowStore, snapshot, onWorkflowChanged);
auditLog?.record({
actor: auditActor(agentContext),
actorType: 'agent',
eventType: 'item.moved',
itemId: item.id,
itemTitle: item.title,
projectId: item.projectId,
summary: `"${item.title}" moved from ${previousStatus} to ${status}`,
details: {
from: previousStatus,
to: status,
},
});
return { item: presentItem(snapshot, item) };
},
},
Expand All @@ -296,7 +356,7 @@ export const itemTools: RegisteredTool[] = [
),
name: 'workflow.items.add_feedback',
},
handler: async ({ agentContext, onWorkflowChanged, workflowStore }, args) => {
handler: async ({ agentContext, auditLog, onWorkflowChanged, workflowStore }, args) => {
const snapshot = await readWorkflowSnapshot(workflowStore);
const item = findItem(snapshot, requireString(args.itemId, 'itemId'));
const feedback = requireString(args.feedback, 'feedback');
Expand All @@ -307,6 +367,16 @@ export const itemTools: RegisteredTool[] = [
touchProject(snapshot, item.projectId, now);

await writeWorkflowSnapshot(workflowStore, snapshot, onWorkflowChanged);
auditLog?.record({
actor: auditActor(agentContext),
actorType: 'agent',
eventType: 'feedback.added',
itemId: item.id,
itemTitle: item.title,
projectId: item.projectId,
summary: `Feedback added to "${item.title}"`,
details: { feedback },
});
return { item: presentItem(snapshot, item) };
},
},
Expand Down
17 changes: 16 additions & 1 deletion src/electron/main/agent-actions/handlers/projects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,7 @@ export const projectTools: RegisteredTool[] = [
inputSchema: objectSchema({ projectId: optionalStringSchema }),
name: 'workflow.projects.delete',
},
handler: async ({ agentContext, getRuntimeController, onWorkflowChanged, workflowStore }, args) => {
handler: async ({ agentContext, auditLog, getRuntimeController, onWorkflowChanged, workflowStore }, args) => {
const snapshot = await readWorkflowSnapshot(workflowStore);
const projectId = resolveProjectId(args.projectId, agentContext.projectId);

Expand All @@ -154,6 +154,7 @@ export const projectTools: RegisteredTool[] = [
const runtimeController = getRuntimeController();
const projectAgents = runtimeController.getSnapshot().agents
.filter((agent) => agent.projectId === projectId);
const deletedItems = snapshot.items.filter((item) => item.projectId === projectId);

await Promise.all(projectAgents.map((agent) => runtimeController.deleteAgent(agent.id)));

Expand All @@ -167,6 +168,20 @@ export const projectTools: RegisteredTool[] = [
}

await writeWorkflowSnapshot(workflowStore, snapshot, onWorkflowChanged);

for (const item of deletedItems) {
auditLog?.record({
actor: agentContext.agentName ?? 'user',
actorType: 'agent',
eventType: 'item.deleted',
itemId: item.id,
itemTitle: item.title,
projectId,
summary: `Deleted work item "${item.title}".`,
details: { viaProjectDelete: true },
});
}

return { success: true };
},
},
Expand Down
86 changes: 84 additions & 2 deletions src/electron/main/agent-actions/handlers/tasks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,11 @@ import { objectSchema, optionalStringSchema, stringSchema } from './schemas';
import { assertAgentCanMutateTasks } from './validators';
import { ToolHandlerError, type RegisteredTool } from './types';

/** Returns the actor label for audit events emitted by agent actions. */
function auditActor(agentContext: { agentId?: string; agentName?: string }) {
return agentContext.agentName ?? 'user';
}

/** Lists task tools. */
export const taskTools: RegisteredTool[] = [
{
Expand All @@ -30,7 +35,7 @@ export const taskTools: RegisteredTool[] = [
),
name: 'workflow.tasks.add',
},
handler: async ({ agentContext, onWorkflowChanged, workflowStore }, args) => {
handler: async ({ agentContext, auditLog, onWorkflowChanged, workflowStore }, args) => {
const snapshot = await readWorkflowSnapshot(workflowStore);
const item = findItem(snapshot, requireString(args.itemId, 'itemId'));
const note = optionalString(args.note);
Expand All @@ -56,6 +61,16 @@ export const taskTools: RegisteredTool[] = [
touchProject(snapshot, item.projectId, now);

await writeWorkflowSnapshot(workflowStore, snapshot, onWorkflowChanged);
auditLog?.record({
actor: auditActor(agentContext),
actorType: 'agent',
eventType: 'task.created',
itemId: item.id,
itemTitle: item.title,
projectId: item.projectId,
summary: `Added task "${title}" to "${item.title}".`,
details: { taskId, taskTitle: title },
});
return { taskId };
},
},
Expand All @@ -75,7 +90,7 @@ export const taskTools: RegisteredTool[] = [
),
name: 'workflow.tasks.update',
},
handler: async ({ agentContext, onWorkflowChanged, workflowStore }, args) => {
handler: async ({ agentContext, auditLog, onWorkflowChanged, workflowStore }, args) => {
const snapshot = await readWorkflowSnapshot(workflowStore);
const item = findItem(snapshot, requireString(args.itemId, 'itemId'));
const note = optionalString(args.note);
Expand Down Expand Up @@ -114,7 +129,74 @@ export const taskTools: RegisteredTool[] = [
touchProject(snapshot, item.projectId, task.updatedAt);

await writeWorkflowSnapshot(workflowStore, snapshot, onWorkflowChanged);
auditLog?.record({
actor: auditActor(agentContext),
actorType: 'agent',
eventType: 'task.updated',
itemId: item.id,
itemTitle: item.title,
projectId: item.projectId,
summary: `Updated task "${task.title}" on "${item.title}".`,
details: {
status: task.status,
taskId: task.id,
taskTitle: task.title,
},
});
return { task };
},
},
{
definition: {
description: 'Delete a task from a Dune work item.',
inputSchema: objectSchema(
{
itemId: stringSchema,
note: optionalStringSchema,
taskId: stringSchema,
},
['itemId', 'taskId'],
),
name: 'workflow.tasks.delete',
},
handler: async ({ agentContext, auditLog, onWorkflowChanged, workflowStore }, args) => {
const snapshot = await readWorkflowSnapshot(workflowStore);
const item = findItem(snapshot, requireString(args.itemId, 'itemId'));
const note = optionalString(args.note);
const taskId = requireString(args.taskId, 'taskId');
const task = item.tasks.find((candidate) => candidate.id === taskId) ?? null;

assertAgentCanMutateTasks(agentContext.agentId, item);

if (!task) {
throw new ToolHandlerError('not-found', `Task ${taskId} not found.`);
}

const now = Date.now();

item.tasks = item.tasks.filter((candidate) => candidate.id !== taskId);
item.updatedAt = now;
prependWorkflowEvents(item, [
...(note ? [createWorkflowEvent('note', note, now, agentContext.agentName)] : []),
createWorkflowEvent('task', `Task "${task.title}" was deleted.`, now, agentContext.agentName),
]);
touchProject(snapshot, item.projectId, now);

await writeWorkflowSnapshot(workflowStore, snapshot, onWorkflowChanged);
auditLog?.record({
actor: auditActor(agentContext),
actorType: 'agent',
eventType: 'task.deleted',
itemId: item.id,
itemTitle: item.title,
projectId: item.projectId,
summary: `Deleted task "${task.title}" from "${item.title}".`,
details: {
taskId,
taskTitle: task.title,
},
});
return { taskId };
},
},
];
2 changes: 2 additions & 0 deletions src/electron/main/agent-actions/handlers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import type { AppStorage } from '@/electron/main/storage/app-storage';
import type { DesktopRuntimeController } from '@/electron/main/runtime/desktop-runtime-controller';
import type { AuditDatabase } from '@/electron/main/audit/audit-db';

/** MCP-style tool definition. Used internally by RegisteredTool. */
export interface ToolDefinition {
Expand All @@ -21,6 +22,7 @@ export interface ToolHandlerContext {

/** Tool handler options. */
export interface ToolHandlerOptions {
auditLog?: AuditDatabase;
getRuntimeController: () => DesktopRuntimeController;
onWorkflowChanged: () => void;
workflowStore: AppStorage;
Expand Down
Loading