Skip to content
Merged
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
15 changes: 14 additions & 1 deletion electron/db/migrations.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type Database from 'better-sqlite3'
import { SCHEMA_V1, SCHEMA_V2, SCHEMA_V3, SCHEMA_V4, SCHEMA_V5, SCHEMA_V6, SCHEMA_V7, SCHEMA_V8, SCHEMA_V9, SCHEMA_V10, SCHEMA_V11, SCHEMA_V12, SCHEMA_V13, SCHEMA_V14, SCHEMA_V15, SCHEMA_V16, SCHEMA_V17, SCHEMA_V18, SCHEMA_V19, SCHEMA_V20, SCHEMA_V21, SCHEMA_V22, SCHEMA_V23, SCHEMA_V24, SCHEMA_V25, SCHEMA_V26, SCHEMA_V27, SCHEMA_V28, SCHEMA_V29, SCHEMA_V30, SCHEMA_V31, SCHEMA_V32, SCHEMA_V33, SCHEMA_V34, SCHEMA_V35, SCHEMA_V36, SCHEMA_V37, SCHEMA_V38, SCHEMA_V39, SCHEMA_V40, SCHEMA_V41, SCHEMA_V42, SCHEMA_V43, SCHEMA_V44, SCHEMA_V45, SCHEMA_V46, SCHEMA_V47, SCHEMA_V48, SCHEMA_V49, SCHEMA_V50, SCHEMA_V51, SCHEMA_V52, SCHEMA_V53, SCHEMA_V54, SCHEMA_V55, SCHEMA_V56, SCHEMA_V57 } from './schema'
import { SCHEMA_V1, SCHEMA_V2, SCHEMA_V3, SCHEMA_V4, SCHEMA_V5, SCHEMA_V6, SCHEMA_V7, SCHEMA_V8, SCHEMA_V9, SCHEMA_V10, SCHEMA_V11, SCHEMA_V12, SCHEMA_V13, SCHEMA_V14, SCHEMA_V15, SCHEMA_V16, SCHEMA_V17, SCHEMA_V18, SCHEMA_V19, SCHEMA_V20, SCHEMA_V21, SCHEMA_V22, SCHEMA_V23, SCHEMA_V24, SCHEMA_V25, SCHEMA_V26, SCHEMA_V27, SCHEMA_V28, SCHEMA_V29, SCHEMA_V30, SCHEMA_V31, SCHEMA_V32, SCHEMA_V33, SCHEMA_V34, SCHEMA_V35, SCHEMA_V36, SCHEMA_V37, SCHEMA_V38, SCHEMA_V39, SCHEMA_V40, SCHEMA_V41, SCHEMA_V42, SCHEMA_V43, SCHEMA_V44, SCHEMA_V45, SCHEMA_V46, SCHEMA_V47, SCHEMA_V48, SCHEMA_V49, SCHEMA_V50, SCHEMA_V51, SCHEMA_V52, SCHEMA_V53, SCHEMA_V54, SCHEMA_V55, SCHEMA_V56, SCHEMA_V57, SCHEMA_RECEIPTS_V61 } from './schema'
import { CLAUDE_MODEL_IDS } from '../../packages/shared/src/constants'

export function runMigrations(db: Database.Database) {
Expand Down Expand Up @@ -728,6 +728,19 @@ export function runMigrations(db: Database.Database) {
})()
}

if (currentVersion < 61) {
db.transaction(() => {
const stmts = SCHEMA_RECEIPTS_V61.split(';').map((s) => s.trim()).filter(Boolean)
for (const stmt of stmts) {
try { db.exec(stmt) } catch (err) {
const msg = (err instanceof Error ? err.message : String(err)).toLowerCase()
if (!msg.includes('already exists')) throw err
}
}
db.prepare('INSERT INTO _migrations (version) VALUES (?)').run(61)
})()
}

// Ensure Solana agent exists (idempotent — handles existing DBs before it was seeded)
try {
const hasSolanaAgent = db.prepare("SELECT id FROM agents WHERE id = 'solana-agent'").get()
Expand Down
21 changes: 21 additions & 0 deletions electron/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1559,3 +1559,24 @@ CREATE INDEX IF NOT EXISTS idx_garrison_commission_payouts_referrer
export const SCHEMA_V57 = `
ALTER TABLE autopilot_mandates ADD COLUMN bought_raw_tokens TEXT NOT NULL DEFAULT '0';
`

// Migration 61: local ledger for attested execution receipts. Every emitted receipt records
// only its content hash (sha256 of the canonical receipt JSON) plus the on-chain
// signature and cluster — never file contents, keys, prompts, or PII. Off by
// default; a row exists only after the operator opts in and an emission lands.
export const SCHEMA_RECEIPTS_V61 = `
CREATE TABLE IF NOT EXISTS receipts (
id TEXT PRIMARY KEY,
content_hash TEXT NOT NULL,
source TEXT NOT NULL,
action_type TEXT NOT NULL,
cluster TEXT NOT NULL,
policy_verdict TEXT NOT NULL,
execution_signature TEXT,
anchor_kind TEXT NOT NULL,
anchor_signature TEXT,
created_at INTEGER DEFAULT (CAST(unixepoch('now') * 1000 AS INTEGER))
);
CREATE INDEX IF NOT EXISTS idx_receipts_created ON receipts(created_at DESC);
CREATE INDEX IF NOT EXISTS idx_receipts_hash ON receipts(content_hash);
`
25 changes: 25 additions & 0 deletions electron/ipc/receipts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ipcMain } from 'electron'
import { ipcHandler } from '../services/IpcHandlerFactory'
import * as ReceiptService from '../services/receipts/ReceiptService'
import type { ReceiptsSettings } from '../services/receipts/ReceiptService'

export function registerReceiptHandlers() {
ipcMain.handle('receipts:get-settings', ipcHandler(async () => {
return ReceiptService.getReceiptsSettings()
}))

ipcMain.handle('receipts:set-settings', ipcHandler(async (_event, next: Partial<ReceiptsSettings>) => {
if (!next || typeof next !== 'object') throw new Error('Invalid receipts settings')
return ReceiptService.setReceiptsSettings(next)
}))

// Aggregates only — raw receipt rows summarized to a count + latest timestamp.
ipcMain.handle('receipts:summary', ipcHandler(async () => {
return ReceiptService.summarizeReceipts()
}))

// Hash + signature ledger rows for the local "receipts emitted" view.
ipcMain.handle('receipts:list', ipcHandler(async (_event, limit?: number) => {
return ReceiptService.listReceipts(Number(limit) || 50)
}))
}
2 changes: 2 additions & 0 deletions electron/main/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ import { registerAllowanceHandlers } from '../ipc/allowances'
import { registerSignalhouseHandlers } from '../ipc/signalhouse'
import { registerFlywheelHandlers } from '../ipc/flywheel'
import { registerFeeHandlers } from '../ipc/fees'
import { registerReceiptHandlers } from '../ipc/receipts'
import { registerColosseumHandlers } from '../ipc/colosseum'
import { registerIdleHandlers } from '../ipc/idle'
import { registerMeterflowHandlers } from '../ipc/meterflow'
Expand Down Expand Up @@ -387,6 +388,7 @@ function registerAllIpc() {
registerSynapseHandlers()
registerAllowanceHandlers()
registerFeeHandlers()
registerReceiptHandlers()
registerValidatorHandlers()
registerSeekerHandlers()
registerFeedbackHandlers()
Expand Down
7 changes: 7 additions & 0 deletions electron/preload/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,13 @@ contextBridge.exposeInMainWorld('daemon', {
summary: (sinceMs: number) => ipcRenderer.invoke('fees:summary', sinceMs),
},

receipts: {
getSettings: () => ipcRenderer.invoke('receipts:get-settings'),
setSettings: (next: unknown) => ipcRenderer.invoke('receipts:set-settings', next),
summary: () => ipcRenderer.invoke('receipts:summary'),
list: (limit?: number) => ipcRenderer.invoke('receipts:list', limit),
},

forensics: {
scan: (input: object) => ipcRenderer.invoke('forensics:scan', input),
expand: (input: object) => ipcRenderer.invoke('forensics:expand', input),
Expand Down
14 changes: 14 additions & 0 deletions electron/services/AriaAgentService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { assembleSystemPrompt } from './aria/contextAssembler'
import { clusterMark } from './aria/tools/shared'
import { toAnthropicTools, type AriaTool, type AriaContextSnapshot, type AriaUiEffect } from './aria/AriaTool'
import { laneToClaudeModel, buildPlanSteps, buildPatchProposal } from './aria/patchUtils'
import { emitReceiptSafe } from './receipts/ReceiptService'
import type { AgentMessage, AgentToolUse } from './providers/agentTurn'
import type { ProviderId } from './providers/ProviderInterface'
import type {
Expand Down Expand Up @@ -630,6 +631,19 @@ async function executeTool(
kind: 'tool-call', callId: use.id, name: tool.name, label: tool.name, toolKind: tool.kind, risk: tool.risk,
status: result.ok ? 'done' : 'error', meta: result.summary,
})
// Attested receipt: after a successful write/sensitive tool, emit the content
// hash on-chain (behind the toggle + devnet guard). Fire-and-forget — a
// receipt failure can never touch the tool result we just returned.
if (result.ok && tool.risk !== 'read') {
emitReceiptSafe({
source: 'aria',
agentId: tool.name,
actionType: `tool.${tool.risk}`,
summary: tool.name,
verdict: needsApproval ? 'approved' : 'auto',
riskTier: tool.risk,
})
}
return { ...base, status: result.ok ? 'done' : 'error', summary: result.summary, result: result.data ?? result.summary }
} catch (err) {
const message = (err as Error).message
Expand Down
14 changes: 14 additions & 0 deletions electron/services/AutopilotService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { getDb } from '../db/db'
import { getConnectionStrict } from './SolanaService'
import { getSwapQuote, executeSwap, getMintDecimals, getServerSwapImpactPct } from './WalletService'
import { getWalletInfrastructureSettings } from './SettingsService'
import { emitReceiptSafe } from './receipts/ReceiptService'
import type {
Mandate,
MandateAction,
Expand Down Expand Up @@ -668,6 +669,19 @@ export async function tickMandate(mandateId: string): Promise<MandateAction | nu
// fee_lamports stays null: swaps sign Jupiter's prebuilt tx, which the fee meter can't append
// a transfer leg to, so no fee is actually charged. Never fabricate a charge in the ledger.
finalizeAction(actionId, { status: 'executed', signature: result.signature, error: null })
// Attested receipt for the landed action (behind the toggle + devnet guard).
// Uses the mandate's own cluster so a mandate can never mislabel its receipt;
// fire-and-forget so a receipt failure never disturbs the mandate ledger.
emitReceiptSafe({
source: 'autopilot',
agentId: mandateId,
actionType: 'swap.execute',
summary: 'autopilot buy',
cluster: mandate.cluster,
verdict: 'auto',
riskTier: 'sensitive',
executionTxSignature: result.signature,
})
const spent = mandate.spentLamports + clipLamports
const exhausted = spent >= mandate.maxExposureLamports
// Track the tokens this buy accumulated (quoted human out * 10^decimals) so exit sells
Expand Down
Loading
Loading