mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
WIP: Refactor and polyfill updates
- Add crypto polyfill for Expo - Update daemon client and websocket server - Simplify codex agent tests
This commit is contained in:
13
package-lock.json
generated
13
package-lock.json
generated
@@ -12154,6 +12154,18 @@
|
||||
"react-native": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-crypto": {
|
||||
"version": "15.0.8",
|
||||
"resolved": "https://registry.npmjs.org/expo-crypto/-/expo-crypto-15.0.8.tgz",
|
||||
"integrity": "sha512-aF7A914TB66WIlTJvl5J6/itejfY78O7dq3ibvFltL9vnTALJ/7LYHvLT4fwmx9yUNS6ekLBtDGWivFWnj2Fcw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"base64-js": "^1.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"expo": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/expo-dev-client": {
|
||||
"version": "6.0.16",
|
||||
"resolved": "https://registry.npmjs.org/expo-dev-client/-/expo-dev-client-6.0.16.tgz",
|
||||
@@ -21554,6 +21566,7 @@
|
||||
"expo-build-properties": "^1.0.9",
|
||||
"expo-clipboard": "~8.0.7",
|
||||
"expo-constants": "~18.0.9",
|
||||
"expo-crypto": "^15.0.8",
|
||||
"expo-dev-client": "^6.0.15",
|
||||
"expo-file-system": "~19.0.17",
|
||||
"expo-font": "~14.0.9",
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
// Polyfill crypto.randomUUID for React Native before any other imports
|
||||
import { polyfillCrypto } from "./src/polyfills/crypto";
|
||||
polyfillCrypto();
|
||||
|
||||
// Configure Unistyles before Expo Router pulls in any components using StyleSheet.
|
||||
import "./src/styles/unistyles";
|
||||
import "expo-router/entry";
|
||||
|
||||
@@ -41,6 +41,7 @@
|
||||
"expo-build-properties": "^1.0.9",
|
||||
"expo-clipboard": "~8.0.7",
|
||||
"expo-constants": "~18.0.9",
|
||||
"expo-crypto": "^15.0.8",
|
||||
"expo-dev-client": "^6.0.15",
|
||||
"expo-file-system": "~19.0.17",
|
||||
"expo-font": "~14.0.9",
|
||||
|
||||
@@ -103,11 +103,14 @@ const userMessageStylesheet = StyleSheet.create((theme) => ({
|
||||
borderTopRightRadius: theme.borderRadius.sm,
|
||||
paddingHorizontal: theme.spacing[4],
|
||||
paddingVertical: theme.spacing[4],
|
||||
minWidth: 0,
|
||||
flexShrink: 1,
|
||||
},
|
||||
text: {
|
||||
color: theme.colors.foreground,
|
||||
fontSize: theme.fontSize.base,
|
||||
lineHeight: 22,
|
||||
overflowWrap: "anywhere",
|
||||
},
|
||||
bubblePressed: {
|
||||
opacity: 0.85,
|
||||
|
||||
24
packages/app/src/polyfills/crypto.ts
Normal file
24
packages/app/src/polyfills/crypto.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import * as ExpoCrypto from "expo-crypto";
|
||||
|
||||
declare global {
|
||||
interface Crypto {
|
||||
randomUUID(): `${string}-${string}-${string}-${string}-${string}`;
|
||||
}
|
||||
}
|
||||
|
||||
export function polyfillCrypto(): void {
|
||||
if (typeof globalThis.crypto === "undefined") {
|
||||
(globalThis as any).crypto = {};
|
||||
}
|
||||
|
||||
if (typeof globalThis.crypto.randomUUID !== "function") {
|
||||
globalThis.crypto.randomUUID = () =>
|
||||
ExpoCrypto.randomUUID() as `${string}-${string}-${string}-${string}-${string}`;
|
||||
}
|
||||
|
||||
if (typeof globalThis.crypto.getRandomValues !== "function") {
|
||||
globalThis.crypto.getRandomValues = <T extends ArrayBufferView>(array: T): T => {
|
||||
return ExpoCrypto.getRandomValues(array as any) as T;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
import { nanoid } from "nanoid";
|
||||
import type { z } from "zod";
|
||||
import {
|
||||
AgentCreateFailedStatusPayloadSchema,
|
||||
@@ -754,7 +753,7 @@ export class DaemonClientV2 {
|
||||
text: string,
|
||||
options?: SendMessageOptions
|
||||
): Promise<void> {
|
||||
const messageId = options?.messageId ?? nanoid();
|
||||
const messageId = options?.messageId ?? crypto.randomUUID();
|
||||
const message = SessionInboundMessageSchema.parse({
|
||||
type: "send_agent_message",
|
||||
agentId,
|
||||
@@ -1354,7 +1353,7 @@ export class DaemonClientV2 {
|
||||
// ============================================================================
|
||||
|
||||
private createRequestId(requestId?: string): string {
|
||||
return requestId ?? nanoid();
|
||||
return requestId ?? crypto.randomUUID();
|
||||
}
|
||||
|
||||
private cleanupTransport(): void {
|
||||
|
||||
@@ -719,6 +719,7 @@ class ClaudeAgentSession implements AgentSession {
|
||||
async close(): Promise<void> {
|
||||
this.rejectAllPendingPermissions(new Error("Claude session closed"));
|
||||
this.input?.end();
|
||||
await this.query?.interrupt?.();
|
||||
await this.query?.return?.();
|
||||
this.query = null;
|
||||
this.input = null;
|
||||
|
||||
@@ -740,7 +740,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
||||
);
|
||||
|
||||
test(
|
||||
"captures tool call inputs/outputs for commands, file changes, file reads, MCP tools, and web search",
|
||||
"captures tool call inputs/outputs for commands, file changes, file reads, and MCP tools",
|
||||
async () => {
|
||||
const cwd = tmpCwd();
|
||||
const restoreSessionDir = useTempCodexSessionDir();
|
||||
@@ -751,13 +751,9 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
||||
const config = {
|
||||
provider: "codex",
|
||||
cwd,
|
||||
modeId: "auto",
|
||||
approvalPolicy: "on-request",
|
||||
networkAccess: true,
|
||||
modeId: "full-access",
|
||||
extra: {
|
||||
codex: {
|
||||
search: true,
|
||||
features: { web_search_request: true },
|
||||
mcp_servers: {
|
||||
test: {
|
||||
command: process.execPath,
|
||||
@@ -770,38 +766,22 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
||||
} satisfies AgentSessionConfig;
|
||||
|
||||
let session: AgentSession | null = null;
|
||||
let permissionRequest: AgentPermissionRequest | null = null;
|
||||
let permissionResolved = false;
|
||||
const toolCalls: ToolCallItem[] = [];
|
||||
|
||||
try {
|
||||
session = await client.createSession(config);
|
||||
|
||||
const prompt = [
|
||||
"1. Run the command `bash -lc \"printf 'stdout-marker'\"` using your shell tool.",
|
||||
"2. Run the command `bash -lc \"printf 'stderr-marker' 1>&2\"` using your shell tool.",
|
||||
"3. Use apply_patch (not the shell) to create a new file named tool-create.txt containing only the line 'alpha'.",
|
||||
"4. Use apply_patch (not the shell) to edit tool-create.txt, replacing 'alpha' with 'beta'.",
|
||||
"5. Read the file tool-create.txt and report its contents (you can use cat or any file reading method).",
|
||||
"1. Run the command `printf 'stdout-marker'` using your shell tool.",
|
||||
"2. Run the command `printf 'stderr-marker' 1>&2` using your shell tool.",
|
||||
"3. Use apply_patch to create a new file named tool-create.txt containing only the line 'alpha'.",
|
||||
"4. Use apply_patch to edit tool-create.txt, replacing 'alpha' with 'beta'.",
|
||||
"5. Read the file tool-create.txt using read_file tool.",
|
||||
"6. Call the MCP tool test.echo with input {\"text\":\"mcp-ok\"}.",
|
||||
"7. Use the web_search tool to search for \"OpenAI Codex MCP\".",
|
||||
"8. Request approval to run the command `printf \"permit\" > tool-permission.txt`, then run it.",
|
||||
"9. After all tools finish, reply DONE and stop.",
|
||||
"7. Reply DONE and stop.",
|
||||
].join("\n");
|
||||
|
||||
for await (const event of session.stream(prompt)) {
|
||||
if (event.type === "permission_requested" && !permissionRequest) {
|
||||
permissionRequest = event.request;
|
||||
await session.respondToPermission(permissionRequest.id, { behavior: "allow" });
|
||||
}
|
||||
if (
|
||||
event.type === "permission_resolved" &&
|
||||
permissionRequest &&
|
||||
event.requestId === permissionRequest.id &&
|
||||
event.resolution.behavior === "allow"
|
||||
) {
|
||||
permissionResolved = true;
|
||||
}
|
||||
if (event.type === "timeline" && providerFromEvent(event) === "codex") {
|
||||
if (event.item.type === "tool_call" && event.item.name !== "permission") {
|
||||
toolCalls.push(event.item);
|
||||
@@ -812,9 +792,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
||||
}
|
||||
}
|
||||
|
||||
expect.soft(permissionRequest).not.toBeNull();
|
||||
expect.soft(permissionResolved).toBe(true);
|
||||
|
||||
const commandCalls = toolCalls.filter(
|
||||
(item) => item.name === "shell" && item.status === "completed"
|
||||
);
|
||||
@@ -868,15 +845,6 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
||||
expect.soft(stringifyUnknown(mcpCall?.input)).toContain("mcp-ok");
|
||||
expect.soft(stringifyUnknown(mcpCall?.output)).toContain("mcp-ok");
|
||||
|
||||
const webSearchCall = toolCalls.find(
|
||||
(item) => item.name === "web_search"
|
||||
);
|
||||
expect.soft(webSearchCall).toBeTruthy();
|
||||
expect.soft(stringifyUnknown(webSearchCall?.input)).toContain("OpenAI Codex MCP");
|
||||
// NOTE: Codex MCP web_search does not return search results in the event.
|
||||
// The search happens internally but results are not exposed via MCP events.
|
||||
// Only verify that the search was performed (input contains query).
|
||||
|
||||
const callIdStatuses = new Map<string, Set<string>>();
|
||||
for (const toolCall of toolCalls) {
|
||||
if (!toolCall.callId) {
|
||||
@@ -914,7 +882,7 @@ describe("CodexMcpAgentClient (MCP integration)", () => {
|
||||
restoreSessionDir();
|
||||
}
|
||||
},
|
||||
240_000
|
||||
120_000
|
||||
);
|
||||
|
||||
test(
|
||||
|
||||
@@ -3136,41 +3136,38 @@ class CodexMcpAgentSession implements AgentSession {
|
||||
? (rawMetadata as Record<string, unknown>).codex_elicitation
|
||||
: undefined;
|
||||
|
||||
let action: ElicitResult["action"];
|
||||
let content: Record<string, unknown> | undefined;
|
||||
let decision: CodexExecApprovalDecision | undefined;
|
||||
let reason: string | undefined;
|
||||
|
||||
if (codexElicitation === "exec-approval") {
|
||||
decision =
|
||||
response.behavior === "allow"
|
||||
? "approved"
|
||||
: response.interrupt
|
||||
? "abort"
|
||||
: "denied";
|
||||
reason = response.behavior === "deny" ? response.message : undefined;
|
||||
const responsePayload: CodexExecApprovalResponse = {
|
||||
decision,
|
||||
...(reason ? { reason } : {}),
|
||||
};
|
||||
pending.resolve(responsePayload);
|
||||
return;
|
||||
} else {
|
||||
action =
|
||||
// Use MCP ElicitResult format only if explicitly tagged as non-exec-approval
|
||||
// Default to Codex exec-approval format (what Codex SDK actually sends)
|
||||
if (codexElicitation && codexElicitation !== "exec-approval") {
|
||||
const action: ElicitResult["action"] =
|
||||
response.behavior === "allow"
|
||||
? "accept"
|
||||
: response.interrupt
|
||||
? "cancel"
|
||||
: "decline";
|
||||
content =
|
||||
const content =
|
||||
response.behavior === "allow" && response.updatedInput
|
||||
? response.updatedInput
|
||||
: undefined;
|
||||
const responsePayload: ElicitResult = {
|
||||
action,
|
||||
...(content ? { content } : {}),
|
||||
};
|
||||
pending.resolve(responsePayload);
|
||||
return;
|
||||
}
|
||||
|
||||
const responsePayload: ElicitResult = {
|
||||
action,
|
||||
...(content ? { content } : {}),
|
||||
// Default: Codex exec-approval format
|
||||
const decision: CodexExecApprovalDecision =
|
||||
response.behavior === "allow"
|
||||
? "approved"
|
||||
: response.interrupt
|
||||
? "abort"
|
||||
: "denied";
|
||||
const reason = response.behavior === "deny" ? response.message : undefined;
|
||||
const responsePayload: CodexExecApprovalResponse = {
|
||||
decision,
|
||||
...(reason ? { reason } : {}),
|
||||
};
|
||||
pending.resolve(responsePayload);
|
||||
}
|
||||
|
||||
@@ -294,7 +294,17 @@ export class VoiceAssistantWebSocketServer {
|
||||
const cleanupPromises: Promise<void>[] = [];
|
||||
this.sessions.forEach((session, ws) => {
|
||||
cleanupPromises.push(session.cleanup());
|
||||
ws.close();
|
||||
// Wait for WebSocket to actually close before resolving
|
||||
cleanupPromises.push(
|
||||
new Promise<void>((resolve) => {
|
||||
if (ws.readyState === WebSocket.CLOSED) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
ws.once("close", () => resolve());
|
||||
ws.close();
|
||||
})
|
||||
);
|
||||
});
|
||||
await Promise.all(cleanupPromises);
|
||||
this.wss.close();
|
||||
|
||||
@@ -13,10 +13,11 @@ export default defineConfig({
|
||||
globals: true,
|
||||
environment: "node",
|
||||
setupFiles: [path.resolve(__dirname, "./src/test-utils/vitest-setup.ts")],
|
||||
pool: "threads",
|
||||
pool: "forks",
|
||||
poolOptions: {
|
||||
threads: {
|
||||
singleThread: true,
|
||||
forks: {
|
||||
singleFork: false,
|
||||
maxForks: 4,
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user