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
3 changes: 2 additions & 1 deletion assistant/src/daemon/host-cu-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,7 @@ export class HostCuProxy extends HostProxyBase<
stepNumber: number,
reasoning?: string,
signal?: AbortSignal,
targetClientId?: string,
): Promise<ToolExecutionResult> {
if (signal?.aborted) {
return Promise.resolve({
Expand All @@ -139,7 +140,7 @@ export class HostCuProxy extends HostProxyBase<
return this.dispatchRequest(toolName, input, conversationId, signal, {
stepNumber,
reasoning,
})
}, targetClientId)
.then((observation) => {
// Capture pre-update state so formatObservation sees the correct
// previous AX tree.
Expand Down
65 changes: 50 additions & 15 deletions assistant/src/daemon/host-file-proxy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
operation: HostFileInput["operation"];
path: string;
conversationId: string;
targetClientId?: string;
/** Detach the abort listener from the caller's signal. No-op when no signal was passed. */
detachAbort: () => void;
}
Expand Down Expand Up @@ -85,6 +86,21 @@
return Promise.resolve({ content: "Aborted", isError: true });
}

const capableClients = assistantEventHub.listClientsByCapability("host_file");

Check failure on line 89 in assistant/src/daemon/host-file-proxy.ts

View workflow job for this annotation

GitHub Actions / Test

TypeError: assistantEventHub.listClientsByCapability is not a function. (In 'assistantEventHub.listClientsByCapability("host_file")'

at request (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/daemon/host-file-proxy.ts:89:46) at <anonymous> (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/__tests__/host-file-proxy.test.ts:335:24)

Check failure on line 89 in assistant/src/daemon/host-file-proxy.ts

View workflow job for this annotation

GitHub Actions / Test

TypeError: assistantEventHub.listClientsByCapability is not a function. (In 'assistantEventHub.listClientsByCapability("host_file")'

at request (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/daemon/host-file-proxy.ts:89:46) at <anonymous> (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/__tests__/host-file-proxy.test.ts:314:35)

Check failure on line 89 in assistant/src/daemon/host-file-proxy.ts

View workflow job for this annotation

GitHub Actions / Test

TypeError: assistantEventHub.listClientsByCapability is not a function. (In 'assistantEventHub.listClientsByCapability("host_file")'

at request (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/daemon/host-file-proxy.ts:89:46) at <anonymous> (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/__tests__/host-file-proxy.test.ts:253:35)

Check failure on line 89 in assistant/src/daemon/host-file-proxy.ts

View workflow job for this annotation

GitHub Actions / Test

TypeError: assistantEventHub.listClientsByCapability is not a function. (In 'assistantEventHub.listClientsByCapability("host_file")'

at request (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/daemon/host-file-proxy.ts:89:46) at <anonymous> (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/__tests__/host-file-proxy.test.ts:228:35)

Check failure on line 89 in assistant/src/daemon/host-file-proxy.ts

View workflow job for this annotation

GitHub Actions / Test

TypeError: assistantEventHub.listClientsByCapability is not a function. (In 'assistantEventHub.listClientsByCapability("host_file")'

at request (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/daemon/host-file-proxy.ts:89:46) at <anonymous> (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/__tests__/host-file-proxy.test.ts:201:35)

Check failure on line 89 in assistant/src/daemon/host-file-proxy.ts

View workflow job for this annotation

GitHub Actions / Test

TypeError: assistantEventHub.listClientsByCapability is not a function. (In 'assistantEventHub.listClientsByCapability("host_file")'

at request (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/daemon/host-file-proxy.ts:89:46) at <anonymous> (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/__tests__/host-file-proxy.test.ts:171:35)

Check failure on line 89 in assistant/src/daemon/host-file-proxy.ts

View workflow job for this annotation

GitHub Actions / Test

TypeError: assistantEventHub.listClientsByCapability is not a function. (In 'assistantEventHub.listClientsByCapability("host_file")'

at request (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/daemon/host-file-proxy.ts:89:46) at <anonymous> (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/__tests__/host-file-proxy.test.ts:145:35)

Check failure on line 89 in assistant/src/daemon/host-file-proxy.ts

View workflow job for this annotation

GitHub Actions / Test

TypeError: assistantEventHub.listClientsByCapability is not a function. (In 'assistantEventHub.listClientsByCapability("host_file")'

at request (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/daemon/host-file-proxy.ts:89:46) at <anonymous> (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/__tests__/host-file-proxy.test.ts:112:35)

Check failure on line 89 in assistant/src/daemon/host-file-proxy.ts

View workflow job for this annotation

GitHub Actions / Test

TypeError: assistantEventHub.listClientsByCapability is not a function. (In 'assistantEventHub.listClientsByCapability("host_file")'

at request (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/daemon/host-file-proxy.ts:89:46) at <anonymous> (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/__tests__/host-file-proxy.test.ts:88:35)

Check failure on line 89 in assistant/src/daemon/host-file-proxy.ts

View workflow job for this annotation

GitHub Actions / Test

TypeError: assistantEventHub.listClientsByCapability is not a function. (In 'assistantEventHub.listClientsByCapability("host_file")'

at request (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/daemon/host-file-proxy.ts:89:46) at <anonymous> (/home/runner/work/vellum-assistant/vellum-assistant/assistant/src/__tests__/host-file-proxy.test.ts:53:35)
let resolvedTargetClientId: string | undefined;
if (input.targetClientId) {
const target = assistantEventHub.getClientById(input.targetClientId);
if (!target || !target.capabilities.includes("host_file")) {
return Promise.resolve({
content: `Error: client "${input.targetClientId}" is not connected or does not support host_file. Run \`assistant clients list --capability host_file\` to see available clients.`,
isError: true,
});
}
resolvedTargetClientId = input.targetClientId;
} else if (capableClients.length === 1) {
resolvedTargetClientId = capableClients[0].clientId;
}

const requestId = uuid();

return new Promise<ToolExecutionResult>((resolve, reject) => {
Expand All @@ -101,8 +117,11 @@
{ requestId, operation: input.operation },
"Host file proxy request timed out",
);
const timeoutMessage = resolvedTargetClientId
? `Host file proxy timed out waiting for response from client ${resolvedTargetClientId}`
: "Host file proxy timed out waiting for client response";
resolve({
content: "Host file proxy timed out waiting for client response",
content: timeoutMessage,
isError: true,
});
}, timeoutSec * 1000);
Expand All @@ -115,11 +134,16 @@
detachAbort();
pendingInteractions.resolve(requestId);
try {
broadcastMessage({
type: "host_file_cancel",
requestId,
broadcastMessage(
{
type: "host_file_cancel",
requestId,
conversationId,
targetClientId: resolvedTargetClientId,
},
conversationId,
});
{ targetClientId: resolvedTargetClientId },
);
} catch {
// Best-effort cancel notification — connection may already be closed.
}
Expand All @@ -137,16 +161,22 @@
operation: input.operation,
path: input.path,
conversationId,
targetClientId: resolvedTargetClientId,
detachAbort,
});

try {
broadcastMessage({
...input,
type: "host_file_request",
requestId,
broadcastMessage(
{
...input,
type: "host_file_request",
requestId,
conversationId,
targetClientId: resolvedTargetClientId,
},
conversationId,
});
{ targetClientId: resolvedTargetClientId },
);
} catch (err) {
clearTimeout(timer);
this.pending.delete(requestId);
Expand Down Expand Up @@ -195,11 +225,16 @@
entry.detachAbort();
pendingInteractions.resolve(requestId);
try {
broadcastMessage({
type: "host_file_cancel",
requestId,
conversationId: entry.conversationId,
});
broadcastMessage(
{
type: "host_file_cancel",
requestId,
conversationId: entry.conversationId,
targetClientId: entry.targetClientId,
},
entry.conversationId,
{ targetClientId: entry.targetClientId },
);
} catch {
// Best-effort cancel notification — connection may already be closed.
}
Expand Down
20 changes: 15 additions & 5 deletions assistant/src/daemon/host-proxy-base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ const log = getLogger("host-proxy-base");
* narrowing is impossible — subclasses are responsible for passing event
* names that match a real `ServerMessage` variant.
*/
function broadcastDynamic(envelope: Record<string, unknown>): void {
broadcastMessage(envelope as unknown as ServerMessage);
function broadcastDynamic(envelope: Record<string, unknown>, targetClientId?: string): void {
broadcastMessage(
envelope as unknown as ServerMessage,
undefined,
targetClientId ? { targetClientId } : undefined,
);
}

const DEFAULT_TIMEOUT_MS = 60_000;
Expand Down Expand Up @@ -63,6 +67,7 @@ interface PendingEntry<TResultPayload> {
reject: (err: Error) => void;
timer: ReturnType<typeof setTimeout>;
conversationId: string;
targetClientId?: string;
/** Detach the abort listener from the caller's signal. No-op when no signal was passed. */
detachAbort: () => void;
}
Expand Down Expand Up @@ -131,6 +136,7 @@ export abstract class HostProxyBase<TRequest, TResultPayload> {
conversationId: string,
signal?: AbortSignal,
extraFields?: Record<string, unknown>,
targetClientId?: string,
): Promise<TResultPayload> {
const requestId = uuid();

Expand Down Expand Up @@ -162,7 +168,8 @@ export abstract class HostProxyBase<TRequest, TResultPayload> {
type: this.cancelEventName,
requestId,
conversationId,
});
targetClientId,
}, targetClientId);
} catch {
// Best-effort cancel notification — connection may already be closed.
}
Expand All @@ -178,6 +185,7 @@ export abstract class HostProxyBase<TRequest, TResultPayload> {
reject,
timer,
conversationId,
targetClientId,
detachAbort,
});

Expand All @@ -188,8 +196,9 @@ export abstract class HostProxyBase<TRequest, TResultPayload> {
conversationId,
toolName,
input,
targetClientId,
...(extraFields ?? {}),
});
}, targetClientId);
} catch (err) {
clearTimeout(timer);
this.pending.delete(requestId);
Expand Down Expand Up @@ -246,7 +255,8 @@ export abstract class HostProxyBase<TRequest, TResultPayload> {
type: this.cancelEventName,
requestId,
conversationId: entry.conversationId,
});
targetClientId: entry.targetClientId,
}, entry.targetClientId);
} catch {
// Best-effort cancel notification — connection may already be closed.
}
Expand Down
2 changes: 2 additions & 0 deletions assistant/src/daemon/message-types/host-cu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface HostCuRequest {
type: "host_cu_request";
requestId: string;
conversationId: string;
targetClientId?: string;
toolName: string; // "computer_use_click", "computer_use_type_text", etc.
input: Record<string, unknown>;
stepNumber: number;
Expand All @@ -18,6 +19,7 @@ export interface HostCuCancelRequest {
type: "host_cu_cancel";
requestId: string;
conversationId: string;
targetClientId?: string;
}

// --- Domain-level union aliases (consumed by the barrel file) ---
Expand Down
4 changes: 4 additions & 0 deletions assistant/src/daemon/message-types/host-file.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export interface HostFileReadRequest {
type: "host_file_request";
requestId: string;
conversationId: string;
targetClientId?: string;
operation: "read";
path: string;
offset?: number;
Expand All @@ -18,6 +19,7 @@ export interface HostFileWriteRequest {
type: "host_file_request";
requestId: string;
conversationId: string;
targetClientId?: string;
operation: "write";
path: string;
content: string;
Expand All @@ -27,6 +29,7 @@ export interface HostFileEditRequest {
type: "host_file_request";
requestId: string;
conversationId: string;
targetClientId?: string;
operation: "edit";
path: string;
old_string: string;
Expand All @@ -43,6 +46,7 @@ export interface HostFileCancelRequest {
type: "host_file_cancel";
requestId: string;
conversationId: string;
targetClientId?: string;
}

// --- Domain-level union aliases (consumed by the barrel file) ---
Expand Down
2 changes: 2 additions & 0 deletions assistant/src/runtime/assistant-event-hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -627,11 +627,13 @@ function registerPendingInteraction(
pendingInteractions.register(msg.requestId, {
conversationId,
kind: "host_file",
targetClientId,
Comment on lines 627 to +630

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce target client on host_file result submissions

Now that host_file_request stores targetClientId in pending interactions, /v1/host-file-result should verify the submitting client matches that target (as host-bash-result already does). handleHostFileResult currently accepts any authenticated submitter with a requestId, so a different connected client can resolve a targeted file request and inject/override the result if it learns the ID.

Useful? React with 👍 / 👎.

});
} else if (msg.type === "host_cu_request") {
pendingInteractions.register(msg.requestId, {
conversationId,
kind: "host_cu",
targetClientId,
Comment on lines 632 to +636

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Enforce target client on host_cu result submissions

The same targeting gap exists for CU: this change records targetClientId for host_cu_request, but /v1/host-cu-result does not check x-vellum-client-id against that stored target before resolving. That means any authenticated client that can post a known requestId can complete another client’s targeted computer-use action, defeating the new per-client routing guarantee.

Useful? React with 👍 / 👎.

});
} else if (msg.type === "host_app_control_request") {
pendingInteractions.register(msg.requestId, {
Expand Down
Loading