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
120 changes: 67 additions & 53 deletions assistant/src/__tests__/__snapshots__/ipc-snapshot.test.ts.snap
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP
// Bun Snapshot v1, https://bun.sh/docs/test/snapshots

exports[`IPC message snapshots ClientMessage types auth serializes to expected JSON 1`] = `
{
"token": "abc123def456",
"type": "auth",
}
`;

exports[`IPC message snapshots ClientMessage types user_message serializes to expected JSON 1`] = `
{
Expand Down Expand Up @@ -375,6 +382,13 @@ exports[`IPC message snapshots ClientMessage types apps_list serializes to expec
}
`;

exports[`IPC message snapshots ClientMessage types home_base_get serializes to expected JSON 1`] = `
{
"ensureLinked": true,
"type": "home_base_get",
}
`;

exports[`IPC message snapshots ClientMessage types shared_apps_list serializes to expected JSON 1`] = `
{
"type": "shared_apps_list",
Expand Down Expand Up @@ -540,6 +554,21 @@ exports[`IPC message snapshots ClientMessage types integration_disconnect serial
}
`;

exports[`IPC message snapshots ServerMessage types auth_result serializes to expected JSON 1`] = `
{
"success": true,
"type": "auth_result",
}
`;

exports[`IPC message snapshots ServerMessage types user_message_echo serializes to expected JSON 1`] = `
{
"sessionId": "sess-001",
"text": "Check the weather for me",
"type": "user_message_echo",
}
`;

exports[`IPC message snapshots ServerMessage types assistant_text_delta serializes to expected JSON 1`] = `
{
"sessionId": "sess-001",
Expand Down Expand Up @@ -1256,6 +1285,43 @@ exports[`IPC message snapshots ServerMessage types apps_list_response serializes
}
`;

exports[`IPC message snapshots ServerMessage types home_base_get_response serializes to expected JSON 1`] = `
{
"homeBase": {
"appId": "home-base-001",
"onboardingTasks": [
"Make it mine",
"Enable voice mode",
"Enable computer control",
"Try ambient mode",
],
"preview": {
"description": "Prebuilt onboarding + starter task canvas",
"icon": "🏠",
"metrics": [
{
"label": "Starter tasks",
"value": "3",
},
{
"label": "Onboarding tasks",
"value": "4",
},
],
"subtitle": "Dashboard",
"title": "Home Base",
},
"source": "prebuilt_seed",
"starterTasks": [
"Change the look and feel",
"Research something for me about X",
"Turn it into a webpage or interactive UI",
],
},
"type": "home_base_get_response",
}
`;

exports[`IPC message snapshots ServerMessage types shared_apps_list_response serializes to expected JSON 1`] = `
{
"apps": [
Expand Down Expand Up @@ -1502,58 +1568,6 @@ exports[`IPC message snapshots ServerMessage types integration_connect_result se
}
`;

exports[`IPC message snapshots ClientMessage types home_base_get serializes to expected JSON 1`] = `
{
"ensureLinked": true,
"type": "home_base_get",
}
`;

exports[`IPC message snapshots ServerMessage types home_base_get_response serializes to expected JSON 1`] = `
{
"homeBase": {
"appId": "home-base-001",
"onboardingTasks": [
"Make it mine",
"Enable voice mode",
"Enable computer control",
"Try ambient mode",
],
"preview": {
"description": "Prebuilt onboarding + starter task canvas",
"icon": "🏠",
"metrics": [
{
"label": "Starter tasks",
"value": "3",
},
{
"label": "Onboarding tasks",
"value": "4",
},
],
"subtitle": "Dashboard",
"title": "Home Base",
},
"source": "prebuilt_seed",
"starterTasks": [
"Change the look and feel",
"Research something for me about X",
"Turn it into a webpage or interactive UI",
],
},
"type": "home_base_get_response",
}
`;

exports[`IPC message snapshots ServerMessage types user_message_echo serializes to expected JSON 1`] = `
{
"sessionId": "sess-001",
"text": "Check the weather for me",
"type": "user_message_echo",
}
`;

exports[`IPC message snapshots ServerMessage types app_files_changed serializes to expected JSON 1`] = `
{
"appId": "app-001",
Expand Down
8 changes: 8 additions & 0 deletions assistant/src/__tests__/ipc-snapshot.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ import type {

type ClientMessageType = ClientMessage['type'];
const clientMessages: Record<ClientMessageType, ClientMessage> = {
auth: {
type: 'auth',
token: 'abc123def456',
},
user_message: {
type: 'user_message',
sessionId: 'sess-001',
Expand Down Expand Up @@ -354,6 +358,10 @@ const clientMessages: Record<ClientMessageType, ClientMessage> = {

type ServerMessageType = ServerMessage['type'];
const serverMessages: Record<ServerMessageType, ServerMessage> = {
auth_result: {
type: 'auth_result',
success: true,
},
user_message_echo: {
type: 'user_message_echo',
text: 'Check the weather for me',
Expand Down
1 change: 1 addition & 0 deletions assistant/src/__tests__/ipc-validate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ describe('IPC Validate', () => {
describe('contract parity', () => {
// Minimal valid payloads for high-risk message types that require extra fields
const HIGH_RISK_FIXTURES: Record<string, Record<string, unknown>> = {
auth: { token: 'abc123' },
user_message: { sessionId: 's1', content: 'hi' },
confirmation_response: { requestId: 'r1', decision: 'allow' },
secret_response: { requestId: 'r1' },
Expand Down
29 changes: 26 additions & 3 deletions assistant/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as net from 'node:net';
import * as readline from 'node:readline';
import { readFileSync, appendFileSync, mkdirSync } from 'node:fs';
import { dirname } from 'node:path';
import { getSocketPath, getHistoryPath } from './util/platform.js';
import { getSocketPath, getHistoryPath, readSessionToken } from './util/platform.js';
import {
serialize,
createMessageParser,
Expand Down Expand Up @@ -656,17 +656,40 @@ export async function startCli(): Promise<void> {
parser = createMessageParser();
const newSocket = net.createConnection(socketPath);
let connected = false;
let authenticated = false;

newSocket.on('connect', () => {
connected = true;
socket = newSocket;
startHeartbeat();
resolve();

// Authenticate with session token before the server will
// accept any other messages.
const token = readSessionToken();
if (!token) {
reject(new Error('Session token not found — is the daemon running?'));
newSocket.destroy();
Comment thread
siddseethepalli marked this conversation as resolved.
return;
}
newSocket.write(serialize({ type: 'auth', token }));
});

newSocket.on('data', (data) => {
const messages = parser.feed(data.toString()) as ServerMessage[];
for (const msg of messages) {
// Wait for auth_result before processing other messages
if (!authenticated) {
if (msg.type === 'auth_result') {
if ((msg as { success: boolean }).success) {
authenticated = true;
startHeartbeat();
resolve();
} else {
reject(new Error((msg as { message?: string }).message ?? 'Authentication failed'));
newSocket.destroy();
}
}
continue;
}
handleMessage(msg);
}
});
Expand Down
7 changes: 6 additions & 1 deletion assistant/src/daemon/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,13 +107,15 @@ export {
// ─── Typed dispatch ──────────────────────────────────────────────────────────

type MessageType = ClientMessage['type'];
// 'auth' is handled at the transport layer (server.ts) and never reaches dispatch.
type DispatchableType = Exclude<MessageType, 'auth'>;
type MessageOfType<T extends MessageType> = Extract<ClientMessage, { type: T }>;
type MessageHandler<T extends MessageType> = (
msg: MessageOfType<T>,
socket: net.Socket,
ctx: HandlerContext,
) => void | Promise<void>;
type DispatchMap = { [T in MessageType]: MessageHandler<T> };
type DispatchMap = { [T in DispatchableType]: MessageHandler<T> };

const handlers: DispatchMap = {
user_message: handleUserMessage,
Expand Down Expand Up @@ -226,6 +228,9 @@ export function handleMessage(
socket: net.Socket,
ctx: HandlerContext,
): void {
// 'auth' is handled at the transport layer and should never reach dispatch.
if (msg.type === 'auth') return;

const handler = handlers[msg.type] as
| ((msg: ClientMessage, socket: net.Socket, ctx: HandlerContext) => void)
| undefined;
Expand Down
4 changes: 4 additions & 0 deletions assistant/src/daemon/ipc-contract-inventory.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"AppOpenRequest",
"AppUpdatePreviewRequest",
"AppsListRequest",
"AuthMessage",
"BundleAppRequest",
"CancelRequest",
"ConfirmationResponse",
Expand Down Expand Up @@ -78,6 +79,7 @@
"AppsListResponse",
"AssistantTextDelta",
"AssistantThinkingDelta",
"AuthResult",
"BundleAppResponse",
"ConfirmationRequest",
"ContextCompacted",
Expand Down Expand Up @@ -157,6 +159,7 @@
"app_open_request",
"app_update_preview",
"apps_list",
"auth",
"bundle_app",
"cancel",
"confirmation_response",
Expand Down Expand Up @@ -230,6 +233,7 @@
"apps_list_response",
"assistant_text_delta",
"assistant_thinking_delta",
"auth_result",
"bundle_app_response",
"confirmation_request",
"context_compacted",
Expand Down
13 changes: 13 additions & 0 deletions assistant/src/daemon/ipc-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,11 @@ export interface SessionSwitchRequest {
sessionId: string;
}

export interface AuthMessage {
type: 'auth';
token: string;
}

export interface PingMessage {
type: 'ping';
}
Expand Down Expand Up @@ -633,6 +638,7 @@ export interface AppFilesChanged {
}

export type ClientMessage =
| AuthMessage
| UserMessage
| ConfirmationResponse
| SecretResponse
Expand Down Expand Up @@ -815,6 +821,12 @@ export interface ErrorMessage {
message: string;
}

export interface AuthResult {
type: 'auth_result';
success: boolean;
message?: string;
}

export interface PongMessage {
type: 'pong';
}
Expand Down Expand Up @@ -1472,6 +1484,7 @@ export interface UiSurfaceUndoResult {
}

export type ServerMessage =
| AuthResult
| UserMessageEcho
| AssistantTextDelta
| AssistantThinkingDelta
Expand Down
7 changes: 7 additions & 0 deletions assistant/src/daemon/ipc-validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,13 @@ export function isClientMessageEnvelope(value: unknown): value is ClientMessage
type PropertyValidator = (obj: Record<string, unknown>) => string | null;

const HIGH_RISK_VALIDATORS: Record<string, PropertyValidator> = {
auth: (obj) => {
if (typeof obj.token !== 'string' || obj.token === '') {
return 'auth requires a non-empty string "token"';
}
return null;
},

user_message: (obj) => {
if (typeof obj.sessionId !== 'string' || obj.sessionId === '') {
return 'user_message requires a non-empty string "sessionId"';
Expand Down
Loading