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
2 changes: 2 additions & 0 deletions assistant/scripts/ipc/check-swift-decoder-drift.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ const SWIFT_OMIT_ALLOWLIST = new Set<string>([
'agent_heartbeat_alert',
// Browser handoff — not yet consumed by the macOS client
'browser_handoff_request',
// Guardian verification — daemon-internal for Telegram channel setup
'guardian_verification_response',
// Work item messages — not yet consumed by the macOS client
'work_item_get_response',
'work_item_run_task_response',
Expand Down
36 changes: 18 additions & 18 deletions assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,15 @@ exports[`IPC message snapshots ClientMessage types telegram_config serializes to
}
`;

exports[`IPC message snapshots ClientMessage types guardian_verification serializes to expected JSON 1`] = `
{
"action": "create_challenge",
"channel": "telegram",
"sessionId": "sess-001",
"type": "guardian_verification",
}
`;

exports[`IPC message snapshots ClientMessage types twitter_auth_start serializes to expected JSON 1`] = `
{
"type": "twitter_auth_start",
Expand Down Expand Up @@ -892,15 +901,6 @@ exports[`IPC message snapshots ClientMessage types tool_names_list serializes to
}
`;

exports[`IPC message snapshots ClientMessage types guardian_verification serializes to expected JSON 1`] = `
{
"action": "create_challenge",
"channel": "telegram",
"sessionId": "sess-001",
"type": "guardian_verification",
}
`;

exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = `
{
"success": true,
Expand Down Expand Up @@ -1920,6 +1920,15 @@ exports[`IPC message snapshots ServerMessage types telegram_config_response seri
}
`;

exports[`IPC message snapshots ServerMessage types guardian_verification_response serializes to expected JSON 1`] = `
{
"instruction": "Send this code to the Telegram bot",
"secret": "verify-secret-123",
"success": true,
"type": "guardian_verification_response",
}
`;

exports[`IPC message snapshots ServerMessage types twitter_auth_result serializes to expected JSON 1`] = `
{
"accountInfo": "@vellum_test",
Expand Down Expand Up @@ -2474,12 +2483,3 @@ exports[`IPC message snapshots ServerMessage types tool_names_list_response seri
"type": "tool_names_list_response",
}
`;

exports[`IPC message snapshots ServerMessage types guardian_verification_response serializes to expected JSON 1`] = `
{
"instruction": "Send this code to the bot",
"secret": "VERIFY-ABC123",
"success": true,
"type": "guardian_verification_response",
}
`;
4 changes: 2 additions & 2 deletions assistant/src/__tests__/channel-approval-routes.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ describe('inbound callback metadata triggers decision handling', () => {
// First, send a normal message to establish the conversation.
const initReq = makeInboundRequest({ content: 'init' });
const initRes = await handleChannelInbound(initReq, noopProcessMessage, 'token', orchestrator);
const initBody = await initRes.json() as { conversationId?: string; eventId?: string; accepted?: boolean };
const _initBody = await initRes.json() as { conversationId?: string; eventId?: string; accepted?: boolean };

// Now we need to find the actual conversationId that was created.
// Check the channel_inbound_events table.
Expand Down Expand Up @@ -722,7 +722,7 @@ describe('terminal state check before markProcessed', () => {
};

// getRun always returns 'running' — the run never completes within the poll
const orchestrator = {
const _orchestrator = {
submitDecision: mock(() => 'applied' as const),
getRun: mock(() => ({ ...mockRun, status: 'running' as const })),
startRun: mock(async () => mockRun),
Expand Down
7 changes: 5 additions & 2 deletions assistant/src/__tests__/channel-approval.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,10 @@ afterAll(() => {
// ---------------------------------------------------------------------------

function ensureConversation(conversationId: string): void {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { getDb } = require('../memory/db.js');
const db = getDb();
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { conversations } = require('../memory/schema.js');
try {
db.insert(conversations).values({
Expand All @@ -63,6 +65,7 @@ function ensureConversation(conversationId: string): void {
}

function resetTables(): void {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { getDb } = require('../memory/db.js');
const db = getDb();
db.run('DELETE FROM message_runs');
Expand Down Expand Up @@ -255,8 +258,8 @@ describe('getPendingConfirmationsByConversation', () => {
ensureConversation('conv-1');

const run1 = createRun('conv-1', 'msg-1');
const run2 = createRun('conv-1', 'msg-2');
const run3 = createRun('conv-1', 'msg-3');
const _run2 = createRun('conv-1', 'msg-2');
const _run3 = createRun('conv-1', 'msg-3');

setRunConfirmation(run1.id, sampleConfirmation);
// run2 stays in 'running' state
Expand Down
24 changes: 12 additions & 12 deletions assistant/src/__tests__/ipc-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -378,6 +378,12 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
type: 'telegram_config',
action: 'get',
},
guardian_verification: {
type: 'guardian_verification',
action: 'create_challenge',
channel: 'telegram',
sessionId: 'sess-001',
},
twitter_auth_start: {
type: 'twitter_auth_start',
},
Expand Down Expand Up @@ -554,12 +560,6 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {
tool_names_list: {
type: 'tool_names_list',
},
guardian_verification: {
type: 'guardian_verification',
action: 'create_challenge',
channel: 'telegram',
sessionId: 'sess-001',
},
};

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -1218,6 +1218,12 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
connected: true,
hasWebhookSecret: true,
},
guardian_verification_response: {
type: 'guardian_verification_response',
success: true,
secret: 'verify-secret-123',
instruction: 'Send this code to the Telegram bot',
},
twitter_auth_result: {
type: 'twitter_auth_result',
success: true,
Expand Down Expand Up @@ -1592,12 +1598,6 @@ const serverMessages: Record<ServerMessageType, ServerMessage> = {
type: 'tool_names_list_response',
names: ['bash', 'file_read', 'file_write'],
},
guardian_verification_response: {
type: 'guardian_verification_response',
success: true,
secret: 'VERIFY-ABC123',
instruction: 'Send this code to the bot',
},
};

// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ function extractLocalFile(buffer: Buffer, offset: number, cdCompressedSize: numb
if (compressionMethod === 0) {
return fileData.toString('utf-8');
} else if (compressionMethod === 8) {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { inflateRawSync } = require('node:zlib') as typeof import('node:zlib');
return inflateRawSync(fileData).toString('utf-8');
} else {
Expand Down
1 change: 1 addition & 0 deletions clients/ios/App/VellumAssistantApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ struct VellumAssistantApp: App {
.environmentObject(appDelegate.clientProvider)
} else {
OnboardingView(isCompleted: $onboardingCompleted, authManager: appDelegate.authManager)
.environmentObject(appDelegate.clientProvider)
}
}
.preferredColorScheme(preferredScheme)
Expand Down
2 changes: 2 additions & 0 deletions clients/ios/Resources/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
<string>Vellum Assistant needs access to your microphone for voice input.</string>
<key>NSSpeechRecognitionUsageDescription</key>
<string>Vellum Assistant needs access to speech recognition to transcribe your voice input.</string>
<key>NSCameraUsageDescription</key>
<string>Vellum Assistant uses the camera to scan QR codes for pairing with your Mac.</string>
<key>NSPhotoLibraryUsageDescription</key>
<string>Select photos to attach to your message</string>
<key>CFBundleURLTypes</key>
Expand Down
89 changes: 70 additions & 19 deletions clients/ios/Views/OnboardingView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -136,11 +136,15 @@ struct ChoosePathStep: View {

struct DaemonSetupStep: View {
var onContinue: (() -> Void)?
@State private var hostname = "localhost"
@State private var hostname = ""
@State private var port = "8765"
@State private var sessionToken = ""
@State private var showingAlert = false
@State private var alertMessage = ""
@State private var showingQRPairing = false
/// Tracks whether a token is configured (via QR or manual entry).
/// Re-checked on appear and after QR sheet dismissal.
@State private var hasConfiguredToken = false

var body: some View {
VStack(spacing: VSpacing.xl) {
Expand All @@ -150,14 +154,45 @@ struct DaemonSetupStep: View {
.font(VFont.title)
.foregroundColor(VColor.textPrimary)

Text("Enter your Mac's address and the session token from the Vellum desktop app.")
Text("Scan the QR code from your Mac, or enter the connection details manually.")
.font(VFont.body)
.foregroundColor(VColor.textSecondary)
.multilineTextAlignment(.center)
.padding(.horizontal, VSpacing.xl)

// QR Scanner button
Button {
showingQRPairing = true
} label: {
HStack(spacing: VSpacing.md) {
Image(systemName: "qrcode.viewfinder")
.font(.system(size: 24))
VStack(alignment: .leading, spacing: VSpacing.xxs) {
Text("Scan QR Code")
.font(VFont.bodyBold)
Text("Open Vellum on your Mac > Settings > Show QR Code")
.font(VFont.caption)
}
}
.frame(maxWidth: .infinity, alignment: .leading)
.padding(VSpacing.lg)
.background(VColor.surface)
.cornerRadius(VRadius.md)
.overlay(
RoundedRectangle(cornerRadius: VRadius.md)
.stroke(VColor.surfaceBorder, lineWidth: 1)
)
}
.foregroundColor(VColor.textPrimary)
.padding(.horizontal, VSpacing.xl)

// Manual entry section
VStack(spacing: VSpacing.lg) {
TextField("Hostname (e.g. localhost)", text: $hostname)
Text("Or enter manually:")
.font(VFont.caption)
.foregroundColor(VColor.textMuted)

TextField("Hostname (e.g. 192.168.1.100)", text: $hostname)
.textFieldStyle(.roundedBorder)
.autocapitalization(.none)
.autocorrectionDisabled()
Expand All @@ -166,15 +201,10 @@ struct DaemonSetupStep: View {
.textFieldStyle(.roundedBorder)
.keyboardType(.numberPad)

SecureField("Session token (from ~/.vellum/session-token)", text: $sessionToken)
SecureField("Session token", text: $sessionToken)
.textFieldStyle(.roundedBorder)
.autocapitalization(.none)
.autocorrectionDisabled()

Text("Find the token at ~/.vellum/session-token on your Mac, or in Mac app Settings.")
.font(VFont.caption)
.foregroundColor(VColor.textMuted)
.multilineTextAlignment(.center)
}
.padding(.horizontal, VSpacing.xl)

Expand All @@ -184,20 +214,20 @@ struct DaemonSetupStep: View {
showingAlert = true
return
}
UserDefaults.standard.set(hostname, forKey: UserDefaultsKeys.daemonHostname)
if !hostname.isEmpty {
UserDefaults.standard.set(hostname, forKey: UserDefaultsKeys.daemonHostname)
}
UserDefaults.standard.set(portInt, forKey: UserDefaultsKeys.daemonPort)
// iOS always uses TLS for TCP connections
UserDefaults.standard.set(true, forKey: UserDefaultsKeys.daemonTLSEnabled)
if sessionToken.isEmpty {
_ = APIKeyManager.shared.deleteAPIKey(provider: "daemon-token")
UserDefaults.standard.removeObject(forKey: UserDefaultsKeys.legacyDaemonToken)
} else {
if !sessionToken.isEmpty {
_ = APIKeyManager.shared.setAPIKey(sessionToken, provider: "daemon-token")
}
onContinue?()
}
.buttonStyle(.borderedProminent)
.disabled(hostname.isEmpty || port.isEmpty || sessionToken.isEmpty)
// Enable if either QR pairing configured a token, or manual fields are filled
.disabled(!hasConfiguredToken && (hostname.isEmpty || port.isEmpty || sessionToken.isEmpty))

Button("Skip for now") {
onContinue?()
Expand All @@ -212,13 +242,34 @@ struct DaemonSetupStep: View {
} message: {
Text(alertMessage)
}
.sheet(isPresented: $showingQRPairing, onDismiss: {
reloadSettings()
}) {
QRPairingSheet()
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
}
.onAppear {
hostname = UserDefaults.standard.string(forKey: UserDefaultsKeys.daemonHostname) ?? "localhost"
let portValue = UserDefaults.standard.integer(forKey: UserDefaultsKeys.daemonPort)
port = portValue > 0 ? String(portValue) : "8765"
sessionToken = APIKeyManager.shared.getAPIKey(provider: "daemon-token") ?? ""
reloadSettings()
}
}

private func reloadSettings() {
let storedHostname = UserDefaults.standard.string(forKey: UserDefaultsKeys.daemonHostname) ?? ""
if !storedHostname.isEmpty && storedHostname != "localhost" {
hostname = storedHostname
}
let portValue = UserDefaults.standard.integer(forKey: UserDefaultsKeys.daemonPort)
port = portValue > 0 ? String(portValue) : "8765"
sessionToken = APIKeyManager.shared.getAPIKey(provider: "daemon-token") ?? ""
// Check if any token is configured (bare key or host-specific)
hasConfiguredToken = !sessionToken.isEmpty || hasHostSpecificToken()
}

private func hasHostSpecificToken() -> Bool {
let h = UserDefaults.standard.string(forKey: UserDefaultsKeys.daemonHostname) ?? ""
let p = UserDefaults.standard.integer(forKey: UserDefaultsKeys.daemonPort)
guard !h.isEmpty, p > 0 else { return false }
return APIKeyManager.shared.getAPIKey(provider: "daemon-token:\(h):\(p)") != nil
}
}

// MARK: - PermissionsStep
Expand Down
Loading
Loading