@bitkyc08/opencodex 2.52.0-preview.20260912 → 2.53.0-preview.20260913
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/gui/dist/assets/index-BBOZWGB6.css +1 -0
- package/gui/dist/assets/index-D7ynYo2K.js +128 -0
- package/gui/dist/index.html +2 -2
- package/native/remote-workspace-helper/Cargo.lock +130 -0
- package/native/remote-workspace-helper/Cargo.toml +24 -0
- package/native/remote-workspace-helper/src/main.rs +49 -0
- package/native/remote-workspace-helper/src/protocol.rs +246 -0
- package/native/remote-workspace-helper/src/sandbox/macos.rs +19 -0
- package/native/remote-workspace-helper/src/sandbox/mod.rs +77 -0
- package/native/remote-workspace-helper/src/sandbox/windows.rs +15 -0
- package/package.json +6 -1
- package/src/adapters/anthropic-image-normalize.ts +30 -2
- package/src/adapters/anthropic.ts +1 -1
- package/src/adapters/base.ts +8 -2
- package/src/adapters/cursor/cursor-errors.ts +12 -0
- package/src/adapters/cursor/thread-continuity.ts +93 -0
- package/src/adapters/cursor.ts +104 -73
- package/src/adapters/devin/cloud-direct/chat.ts +312 -23
- package/src/adapters/devin/cloud-direct/metadata.ts +31 -2
- package/src/adapters/devin/live-models.ts +70 -3
- package/src/adapters/devin.ts +281 -21
- package/src/adapters/google-wire-compiler.ts +14 -6
- package/src/adapters/google.ts +22 -8
- package/src/adapters/kiro/adapter.ts +316 -0
- package/src/adapters/kiro/conversation.ts +136 -0
- package/src/adapters/kiro/payload.ts +432 -0
- package/src/adapters/kiro/reasoning.ts +56 -0
- package/src/adapters/kiro/stream.ts +1153 -0
- package/src/adapters/kiro/usage.ts +223 -0
- package/src/adapters/kiro/wire.ts +76 -0
- package/src/adapters/kiro.ts +8 -2319
- package/src/adapters/mimo-free.ts +1 -1
- package/src/adapters/openai-chat-images.ts +101 -0
- package/src/adapters/openai-chat.ts +201 -181
- package/src/adapters/openai-responses.ts +92 -224
- package/src/adapters/registry.ts +0 -7
- package/src/adapters/run-turn-queue.ts +13 -6
- package/src/bridge.ts +14 -15
- package/src/chat/inbound.ts +29 -4
- package/src/chat/outbound.ts +145 -107
- package/src/claude/desktop-profile.ts +4 -6
- package/src/cli/account-api.ts +14 -0
- package/src/cli/account-extended.ts +1 -1
- package/src/cli/account-history.ts +60 -0
- package/src/cli/account-main.ts +80 -0
- package/src/cli/account.ts +11 -3
- package/src/cli/capabilities.ts +113 -0
- package/src/cli/catalog.ts +109 -0
- package/src/cli/dispatch.ts +9 -0
- package/src/cli/help.ts +2 -0
- package/src/cli/index.ts +2 -2
- package/src/cli/observe.ts +28 -1
- package/src/cli/opencode.ts +42 -8
- package/src/cli/provider-runtime.ts +11 -1
- package/src/cli/provider.ts +22 -2
- package/src/cli/registry.ts +21 -0
- package/src/cli/remote-workspace.ts +154 -0
- package/src/cli/status.ts +39 -7
- package/src/cli/usage-report.ts +14 -2
- package/src/client/hub-client.ts +34 -0
- package/src/client/hub-state.ts +9 -1
- package/src/codex/account-store.ts +78 -0
- package/src/codex/auth-api.ts +81 -54
- package/src/codex/auth-context.ts +45 -16
- package/src/codex/catalog/effort.ts +1 -1
- package/src/codex/catalog/metadata.ts +3 -6
- package/src/codex/catalog/native-models.ts +4 -4
- package/src/codex/catalog/parsing.ts +2 -20
- package/src/codex/catalog/provider-fetch.ts +10 -1
- package/src/codex/catalog/remote.ts +233 -0
- package/src/codex/catalog/sync.ts +403 -35
- package/src/codex/convergence.ts +1 -1
- package/src/codex/history-manifest.ts +36 -0
- package/src/codex/history-provider.ts +32 -5
- package/src/codex/inject.ts +9 -0
- package/src/codex/main-account.ts +113 -0
- package/src/codex/main-device-reauth-api.ts +89 -0
- package/src/codex/main-device-reauth.ts +217 -0
- package/src/codex/native-residue.ts +9 -2
- package/src/codex/quota-auto-refresh.ts +3 -2
- package/src/codex/quota-capacity.ts +98 -0
- package/src/codex/quota-history.ts +160 -0
- package/src/codex/quota-types.ts +8 -0
- package/src/codex/quota.ts +118 -91
- package/src/codex/refresh.ts +2 -1
- package/src/codex/routing.ts +90 -17
- package/src/codex/sync.ts +33 -4
- package/src/combos/request.ts +19 -1
- package/src/config/multi-agent-surface.ts +61 -0
- package/src/config/provider-validation.ts +176 -0
- package/src/config.ts +213 -11
- package/src/generated/compatibility-version.json +436 -168
- package/src/images/loop.ts +119 -36
- package/src/lib/admission.ts +12 -6
- package/src/lib/redact.ts +7 -0
- package/src/lib/translator-budget.ts +4 -3
- package/src/lib/windows-atomic-replace.ts +1 -0
- package/src/lib/windows-elevation.ts +1 -1
- package/src/oauth/chatgpt-device.ts +62 -5
- package/src/oauth/devin/cli-import.ts +130 -0
- package/src/oauth/devin.ts +63 -8
- package/src/oauth/index.ts +29 -14
- package/src/oauth/kiro.ts +18 -6
- package/src/oauth/login-cli.ts +9 -1
- package/src/oauth/meta-muse-device.ts +464 -0
- package/src/oauth/meta-muse.ts +123 -32
- package/src/oauth/pool-kernel.ts +9 -0
- package/src/oauth/pool-settings-capability.ts +2 -2
- package/src/oauth/store.ts +57 -0
- package/src/oauth/types.ts +31 -0
- package/src/providers/derive.ts +13 -3
- package/src/providers/devin-cli-authmode-migration.ts +57 -35
- package/src/providers/devin-provider-merge-migration.ts +240 -0
- package/src/providers/muse-key-quota.ts +117 -0
- package/src/providers/muse-subscription-usage.ts +14 -2
- package/src/providers/openai-sidecar.ts +25 -3
- package/src/providers/opencode-zen-rate-limit.ts +58 -0
- package/src/providers/provider-id-rewrite.ts +20 -5
- package/src/providers/quota-types.ts +12 -0
- package/src/providers/quota.ts +143 -102
- package/src/providers/reasoning-metadata.ts +543 -0
- package/src/providers/registry.ts +80 -49
- package/src/reasoning-effort.ts +26 -2
- package/src/remote/hub-usage.ts +32 -0
- package/src/remote-control/index.ts +192 -41
- package/src/remote-control/workspace-activation.ts +9 -0
- package/src/remote-control/workspace-agent-connection.ts +366 -0
- package/src/remote-control/workspace-claude-runtime.ts +243 -0
- package/src/remote-control/workspace-codex-runtime.ts +531 -0
- package/src/remote-control/workspace-codex-sandbox.ts +115 -0
- package/src/remote-control/workspace-command-runner.ts +748 -0
- package/src/remote-control/workspace-coordinator.ts +231 -0
- package/src/remote-control/workspace-device.ts +585 -0
- package/src/remote-control/workspace-executable.ts +43 -0
- package/src/remote-control/workspace-executor.ts +397 -0
- package/src/remote-control/workspace-hub.ts +519 -0
- package/src/remote-control/workspace-pi-runtime.ts +382 -0
- package/src/remote-control/workspace-process.ts +129 -0
- package/src/remote-control/workspace-rpc.ts +304 -0
- package/src/remote-control/workspace-runtime.ts +60 -0
- package/src/remote-control/workspace-secret-store.ts +39 -0
- package/src/remote-control/workspace-sessions.ts +799 -0
- package/src/remote-control/workspace-tool-bridge.ts +192 -0
- package/src/responses/code-mode-helper-compat.ts +22 -3
- package/src/responses/hosted-tool-policy.ts +0 -1
- package/src/responses/muse-tool-name-alias.ts +379 -0
- package/src/responses/plaintext-v2-agent-messages.ts +902 -0
- package/src/router.ts +7 -0
- package/src/routing/compatibility/behavior.ts +0 -1
- package/src/server/audio-client.ts +64 -0
- package/src/server/audio-dictation.ts +91 -0
- package/src/server/audio-live.ts +185 -0
- package/src/server/audio-transcriptions.ts +183 -0
- package/src/server/audio-upstream.ts +153 -0
- package/src/server/auth-cors.ts +61 -2
- package/src/server/chat-completions.ts +1 -1
- package/src/server/chat-native-sse.ts +92 -48
- package/src/server/chat-native.ts +37 -15
- package/src/server/hub-usage.ts +57 -0
- package/src/server/images.ts +4 -0
- package/src/server/index.ts +722 -57
- package/src/server/lifecycle.ts +5 -6
- package/src/server/live-call-bindings.ts +60 -0
- package/src/server/live.ts +12 -1
- package/src/server/management/agent-settings-routes.ts +25 -4
- package/src/server/management/api-access.ts +37 -0
- package/src/server/management/api-key-usage.ts +7 -2
- package/src/server/management/config-routes.ts +1 -18
- package/src/server/management/context.ts +15 -0
- package/src/server/management/logs-usage-routes.ts +2 -0
- package/src/server/management/oauth-account-routes.ts +39 -12
- package/src/server/management/provider-routes.ts +125 -2
- package/src/server/management/remote-workspace-routes.ts +140 -0
- package/src/server/management/route-registry.ts +15 -0
- package/src/server/management/usage-aggregate-cache.ts +14 -15
- package/src/server/management/usage-summary-cache.ts +2 -0
- package/src/server/management-api.ts +23 -0
- package/src/server/ports.ts +17 -0
- package/src/server/relay-eager.ts +4 -1
- package/src/server/relay.ts +70 -10
- package/src/server/request-decompress.ts +6 -3
- package/src/server/responses/agent-task-recovery.ts +25 -32
- package/src/server/responses/codex-auth-error.ts +11 -0
- package/src/server/responses/codex-ws-exchange.ts +52 -3
- package/src/server/responses/codex-ws-wire.ts +55 -0
- package/src/server/responses/compact.ts +9 -1
- package/src/server/responses/core.ts +337 -73
- package/src/server/responses/encrypted-payload.ts +45 -2
- package/src/server/responses/ws-upstream.ts +4 -1
- package/src/server/responses-self-named-namespace-scrub.ts +1 -3
- package/src/server/responses-undeclared-tool-guard.ts +1 -1
- package/src/server/search.ts +3 -0
- package/src/server/sse-payload-rewrite.ts +136 -51
- package/src/server/ws-bridge.ts +35 -1
- package/src/service/cli.ts +372 -0
- package/src/service/diagnostics.ts +340 -0
- package/src/service/guards.ts +303 -0
- package/src/service/health.ts +222 -0
- package/src/service/launchd.ts +853 -0
- package/src/service/orchestration.ts +617 -0
- package/src/service/repair.ts +334 -0
- package/src/service/state.ts +363 -0
- package/src/service/systemd.ts +229 -0
- package/src/service/windows-ops.ts +690 -0
- package/src/service/windows-scheduler.ts +769 -0
- package/src/service/windows-taskxml.ts +613 -0
- package/src/service.ts +22 -5550
- package/src/storage/cleanup/db.ts +258 -0
- package/src/storage/cleanup/execute.ts +358 -0
- package/src/storage/cleanup/paths.ts +189 -0
- package/src/storage/cleanup/pending.ts +140 -0
- package/src/storage/cleanup/preview.ts +292 -0
- package/src/storage/cleanup/reconcile.ts +347 -0
- package/src/storage/cleanup/restore.ts +932 -0
- package/src/storage/cleanup/satellite.ts +474 -0
- package/src/storage/cleanup/staging.ts +129 -0
- package/src/storage/cleanup/types.ts +98 -0
- package/src/storage/cleanup.ts +49 -3127
- package/src/types/accounts.ts +2 -0
- package/src/types/config.ts +13 -12
- package/src/types/provider.ts +37 -0
- package/src/types/request.ts +2 -0
- package/src/types/tools.ts +17 -5
- package/src/types.ts +1 -0
- package/src/usage/expected-prices.ts +127 -0
- package/src/usage/log.ts +58 -1
- package/src/vision/eligibility.ts +13 -2
- package/src/web-search/loop.ts +56 -3
- package/gui/dist/assets/index-D_t6sCWs.js +0 -115
- package/gui/dist/assets/index-EdoPnm9_.css +0 -1
- package/src/adapters/devin-cli/acp.ts +0 -204
- package/src/adapters/devin-cli/adapter.ts +0 -345
- package/src/adapters/devin-cli/binary.ts +0 -69
- package/src/adapters/devin-cli/models.ts +0 -57
- package/src/oauth/devin-cli.ts +0 -149
- package/src/server/responses-reasoning-summary-rewrite.ts +0 -178
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
import { chmodSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { findExecutableOnPath } from "./workspace-executable";
|
|
6
|
+
import {
|
|
7
|
+
remoteWorkspaceProcessInvocation,
|
|
8
|
+
removeRemoteWorkspaceIsolation,
|
|
9
|
+
runRemoteWorkspaceCleanupSteps,
|
|
10
|
+
stopRemoteWorkspaceProcess,
|
|
11
|
+
waitForRemoteWorkspaceProcessExit,
|
|
12
|
+
} from "./workspace-process";
|
|
13
|
+
import { startRemoteWorkspaceToolBridge } from "./workspace-tool-bridge";
|
|
14
|
+
import { REMOTE_WORKSPACE_DYNAMIC_TOOLS } from "./workspace-tools";
|
|
15
|
+
import type {
|
|
16
|
+
RemoteWorkspaceRuntimeFactory,
|
|
17
|
+
RemoteWorkspaceRuntimeHandle,
|
|
18
|
+
} from "./workspace-sessions";
|
|
19
|
+
|
|
20
|
+
const MAX_JSON_LINE_BYTES = 2 * 1024 * 1024;
|
|
21
|
+
|
|
22
|
+
interface PendingResponse {
|
|
23
|
+
resolve(value: Record<string, unknown>): void;
|
|
24
|
+
reject(error: Error): void;
|
|
25
|
+
timer: ReturnType<typeof setTimeout>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function record(value: unknown): Record<string, unknown> | null {
|
|
29
|
+
return value && typeof value === "object" && !Array.isArray(value)
|
|
30
|
+
? value as Record<string, unknown>
|
|
31
|
+
: null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function safeError(value: unknown, fallback: string): string {
|
|
35
|
+
return (value instanceof Error ? value.message : typeof value === "string" ? value : fallback)
|
|
36
|
+
.replace(/[^\x20-\x7e\n\t]/g, " ")
|
|
37
|
+
.slice(0, 4_096);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function messageText(value: unknown): string | null {
|
|
41
|
+
const message = record(value);
|
|
42
|
+
if (!message || message.role !== "assistant" || !Array.isArray(message.content)) return null;
|
|
43
|
+
const text = message.content.flatMap(raw => {
|
|
44
|
+
const part = record(raw);
|
|
45
|
+
return part?.type === "text" && typeof part.text === "string" ? [part.text] : [];
|
|
46
|
+
}).join("");
|
|
47
|
+
return text || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function remotePiInstructions(deviceName: string, tools: readonly string[]): string {
|
|
51
|
+
const name = deviceName.replace(/[\x00-\x1f\x7f]/g, " ").slice(0, 120) || "remote executor";
|
|
52
|
+
const remoteTools = tools.map(tool => `remote_${tool}`).join(", ");
|
|
53
|
+
return [
|
|
54
|
+
`You operate only on the OpenCodex remote executor named ${JSON.stringify(name)}.`,
|
|
55
|
+
`Use only these tools for filesystem and command work: ${remoteTools}.`,
|
|
56
|
+
"The Hub working directory is an empty isolation boundary, not the user's project.",
|
|
57
|
+
"If a remote tool fails or the executor is offline, stop and report it. Never substitute local operations.",
|
|
58
|
+
].join(" ");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function extensionSource(tools: readonly string[]): string {
|
|
62
|
+
const allowed = new Set(tools);
|
|
63
|
+
const definitions = REMOTE_WORKSPACE_DYNAMIC_TOOLS[0].tools.filter(tool => allowed.has(tool.name)).map(tool => ({
|
|
64
|
+
remoteName: `remote_${tool.name}`,
|
|
65
|
+
tool: tool.name,
|
|
66
|
+
description: tool.description,
|
|
67
|
+
parameters: tool.inputSchema,
|
|
68
|
+
}));
|
|
69
|
+
return `const definitions = ${JSON.stringify(definitions)};
|
|
70
|
+
const endpoint = process.env.OCX_REMOTE_WORKSPACE_BRIDGE_URL;
|
|
71
|
+
const token = process.env.OCX_REMOTE_WORKSPACE_BRIDGE_TOKEN;
|
|
72
|
+
|
|
73
|
+
export default function registerRemoteWorkspace(pi) {
|
|
74
|
+
if (!endpoint || !token) throw new Error("Remote Workspace bridge is unavailable");
|
|
75
|
+
for (const definition of definitions) {
|
|
76
|
+
pi.registerTool({
|
|
77
|
+
name: definition.remoteName,
|
|
78
|
+
label: definition.remoteName,
|
|
79
|
+
description: definition.description,
|
|
80
|
+
parameters: definition.parameters,
|
|
81
|
+
async execute(_toolCallId, parameters, signal) {
|
|
82
|
+
const response = await fetch(endpoint + "/invoke", {
|
|
83
|
+
method: "POST",
|
|
84
|
+
headers: { "content-type": "application/json", authorization: "Bearer " + token },
|
|
85
|
+
body: JSON.stringify({ tool: definition.tool, arguments: parameters }),
|
|
86
|
+
signal,
|
|
87
|
+
});
|
|
88
|
+
const result = await response.json();
|
|
89
|
+
if (!response.ok || !result || result.success !== true) {
|
|
90
|
+
throw new Error(result && typeof result.text === "string" ? result.text : "Remote Workspace tool failed");
|
|
91
|
+
}
|
|
92
|
+
return { content: [{ type: "text", text: result.text }], details: { remote: true } };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
`;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
class PiRpcProcess {
|
|
101
|
+
private readonly pending = new Map<string, PendingResponse>();
|
|
102
|
+
private nextId = 0;
|
|
103
|
+
private closed = false;
|
|
104
|
+
private activeSettle: { resolve(): void; reject(error: Error): void } | null = null;
|
|
105
|
+
|
|
106
|
+
onEvent: ((event: Record<string, unknown>) => void) | null = null;
|
|
107
|
+
|
|
108
|
+
constructor(private readonly child: Bun.Subprocess<"pipe", "pipe", "pipe">) {
|
|
109
|
+
void this.read();
|
|
110
|
+
void this.drainStderr();
|
|
111
|
+
void child.exited.then(code => this.fail(new Error(`Pi RPC exited with code ${code}`)));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async command(type: string, fields: Record<string, unknown> = {}, timeoutMs = 15_000): Promise<Record<string, unknown>> {
|
|
115
|
+
if (this.closed) throw new Error("Pi RPC is closed");
|
|
116
|
+
const id = `ocx-${++this.nextId}`;
|
|
117
|
+
const result = new Promise<Record<string, unknown>>((resolve, reject) => {
|
|
118
|
+
const timer = setTimeout(() => {
|
|
119
|
+
this.pending.delete(id);
|
|
120
|
+
reject(new Error(`Pi RPC ${type} timed out`));
|
|
121
|
+
}, timeoutMs);
|
|
122
|
+
this.pending.set(id, { resolve, reject, timer });
|
|
123
|
+
});
|
|
124
|
+
try {
|
|
125
|
+
this.send({ id, type, ...fields });
|
|
126
|
+
} catch (error) {
|
|
127
|
+
const pending = this.pending.get(id);
|
|
128
|
+
if (pending) {
|
|
129
|
+
clearTimeout(pending.timer);
|
|
130
|
+
this.pending.delete(id);
|
|
131
|
+
pending.reject(error instanceof Error ? error : new Error("Pi RPC write failed"));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return result;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async prompt(message: string): Promise<void> {
|
|
138
|
+
if (this.activeSettle) throw new Error("Pi Remote Workspace turn is already active");
|
|
139
|
+
const settled = new Promise<void>((resolve, reject) => { this.activeSettle = { resolve, reject }; });
|
|
140
|
+
try {
|
|
141
|
+
const accepted = await this.command("prompt", { message });
|
|
142
|
+
if (accepted.success !== true) throw new Error(safeError(accepted.error, "Pi rejected the prompt"));
|
|
143
|
+
await settled;
|
|
144
|
+
} catch (error) {
|
|
145
|
+
this.activeSettle = null;
|
|
146
|
+
throw error;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async abort(): Promise<void> {
|
|
151
|
+
if (!this.activeSettle) return;
|
|
152
|
+
await this.command("abort", {}, 3_000).catch(() => {});
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async close(): Promise<void> {
|
|
156
|
+
try {
|
|
157
|
+
if (!this.closed) {
|
|
158
|
+
try { this.child.stdin.end(); } catch { /* already closed */ }
|
|
159
|
+
}
|
|
160
|
+
const graceful = await waitForRemoteWorkspaceProcessExit(this.child, 1_500);
|
|
161
|
+
if (!graceful) {
|
|
162
|
+
await stopRemoteWorkspaceProcess(this.child);
|
|
163
|
+
}
|
|
164
|
+
} finally {
|
|
165
|
+
// Active and pending RPC waiters cannot survive a failed process teardown.
|
|
166
|
+
this.fail(new Error("Pi Remote Workspace session closed"));
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private send(value: Record<string, unknown>): void {
|
|
171
|
+
const line = `${JSON.stringify(value)}\n`;
|
|
172
|
+
if (Buffer.byteLength(line, "utf8") > MAX_JSON_LINE_BYTES) throw new Error("Pi RPC message is too large");
|
|
173
|
+
this.child.stdin.write(line);
|
|
174
|
+
this.child.stdin.flush();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
private async read(): Promise<void> {
|
|
178
|
+
const reader = this.child.stdout.getReader();
|
|
179
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
180
|
+
let buffer = "";
|
|
181
|
+
try {
|
|
182
|
+
while (true) {
|
|
183
|
+
const next = await reader.read();
|
|
184
|
+
if (next.done) break;
|
|
185
|
+
buffer += decoder.decode(next.value, { stream: true });
|
|
186
|
+
if (Buffer.byteLength(buffer, "utf8") > MAX_JSON_LINE_BYTES && !buffer.includes("\n")) {
|
|
187
|
+
throw new Error("Pi RPC output line is too large");
|
|
188
|
+
}
|
|
189
|
+
let newline = buffer.indexOf("\n");
|
|
190
|
+
while (newline >= 0) {
|
|
191
|
+
const line = buffer.slice(0, newline).replace(/\r$/, "");
|
|
192
|
+
buffer = buffer.slice(newline + 1);
|
|
193
|
+
if (Buffer.byteLength(line, "utf8") > MAX_JSON_LINE_BYTES) throw new Error("Pi RPC output line is too large");
|
|
194
|
+
if (line) {
|
|
195
|
+
const event = record(JSON.parse(line));
|
|
196
|
+
if (!event) throw new Error("invalid Pi RPC event");
|
|
197
|
+
this.receive(event);
|
|
198
|
+
}
|
|
199
|
+
newline = buffer.indexOf("\n");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (error) {
|
|
203
|
+
void stopRemoteWorkspaceProcess(this.child).catch(() => {});
|
|
204
|
+
this.fail(new Error(safeError(error, "Pi RPC output failed")));
|
|
205
|
+
} finally {
|
|
206
|
+
reader.releaseLock();
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
private async drainStderr(): Promise<void> {
|
|
211
|
+
const reader = this.child.stderr.getReader();
|
|
212
|
+
try { while (!(await reader.read()).done) { /* drain without retaining secrets */ } }
|
|
213
|
+
catch { /* stdout/exit code owns the failure */ }
|
|
214
|
+
finally { reader.releaseLock(); }
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private receive(event: Record<string, unknown>): void {
|
|
218
|
+
if (event.type === "response" && typeof event.id === "string") {
|
|
219
|
+
const pending = this.pending.get(event.id);
|
|
220
|
+
if (!pending) return;
|
|
221
|
+
clearTimeout(pending.timer);
|
|
222
|
+
this.pending.delete(event.id);
|
|
223
|
+
pending.resolve(event);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (event.type === "agent_settled") {
|
|
227
|
+
const active = this.activeSettle;
|
|
228
|
+
this.activeSettle = null;
|
|
229
|
+
active?.resolve();
|
|
230
|
+
}
|
|
231
|
+
if (event.type === "extension_error") {
|
|
232
|
+
const active = this.activeSettle;
|
|
233
|
+
this.activeSettle = null;
|
|
234
|
+
active?.reject(new Error(safeError(event.error, "Pi Remote Workspace extension failed")));
|
|
235
|
+
}
|
|
236
|
+
this.onEvent?.(event);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private fail(error: Error): void {
|
|
240
|
+
if (this.closed) return;
|
|
241
|
+
this.closed = true;
|
|
242
|
+
for (const pending of this.pending.values()) {
|
|
243
|
+
clearTimeout(pending.timer);
|
|
244
|
+
pending.reject(error);
|
|
245
|
+
}
|
|
246
|
+
this.pending.clear();
|
|
247
|
+
const active = this.activeSettle;
|
|
248
|
+
this.activeSettle = null;
|
|
249
|
+
active?.reject(error);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface PiRemoteWorkspaceRuntimeOptions {
|
|
254
|
+
command?: readonly string[];
|
|
255
|
+
env?: Record<string, string | undefined>;
|
|
256
|
+
version?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export class PiRemoteWorkspaceRuntimeFactory implements RemoteWorkspaceRuntimeFactory {
|
|
260
|
+
readonly profile = "pi" as const;
|
|
261
|
+
|
|
262
|
+
constructor(private readonly options: PiRemoteWorkspaceRuntimeOptions = {}) {}
|
|
263
|
+
|
|
264
|
+
async available(): Promise<{ available: boolean; version?: string; reason?: string }> {
|
|
265
|
+
const command = this.options.command && this.options.command.length > 0
|
|
266
|
+
? this.options.command[0]
|
|
267
|
+
: findExecutableOnPath("pi");
|
|
268
|
+
return command
|
|
269
|
+
? { available: true, ...(this.options.version ? { version: this.options.version } : {}) }
|
|
270
|
+
: { available: false, reason: "Pi is not installed on this Hub." };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async start(options: Parameters<RemoteWorkspaceRuntimeFactory["start"]>[0]): Promise<RemoteWorkspaceRuntimeHandle> {
|
|
274
|
+
const configuredCommand = this.options.command && this.options.command.length > 0
|
|
275
|
+
? [...this.options.command]
|
|
276
|
+
: null;
|
|
277
|
+
const executable = configuredCommand?.[0] ?? findExecutableOnPath("pi");
|
|
278
|
+
if (!executable) throw new Error("Pi is not installed on this Hub");
|
|
279
|
+
const commandPrefix = configuredCommand ?? [executable];
|
|
280
|
+
const isolation = mkdtempSync(join(tmpdir(), "ocx-remote-pi-"));
|
|
281
|
+
try {
|
|
282
|
+
chmodSync(isolation, 0o700);
|
|
283
|
+
} catch (error) {
|
|
284
|
+
removeRemoteWorkspaceIsolation(isolation);
|
|
285
|
+
throw error;
|
|
286
|
+
}
|
|
287
|
+
const extensionPath = join(isolation, "remote-workspace-extension.js");
|
|
288
|
+
try {
|
|
289
|
+
writeFileSync(extensionPath, extensionSource(options.tools), { mode: 0o600 });
|
|
290
|
+
} catch (error) {
|
|
291
|
+
removeRemoteWorkspaceIsolation(isolation);
|
|
292
|
+
throw error;
|
|
293
|
+
}
|
|
294
|
+
const threadId = options.resumeThreadId ?? randomUUID();
|
|
295
|
+
const bridge = (() => {
|
|
296
|
+
try {
|
|
297
|
+
return startRemoteWorkspaceToolBridge({
|
|
298
|
+
coordinator: options.coordinator,
|
|
299
|
+
threadId,
|
|
300
|
+
tools: options.tools,
|
|
301
|
+
onTool: tool => options.emit("tool", `Running ${tool} on ${options.deviceName}/${options.rootLabel}`),
|
|
302
|
+
});
|
|
303
|
+
} catch (error) {
|
|
304
|
+
removeRemoteWorkspaceIsolation(isolation);
|
|
305
|
+
throw error;
|
|
306
|
+
}
|
|
307
|
+
})();
|
|
308
|
+
const childEnv = {
|
|
309
|
+
...process.env,
|
|
310
|
+
...this.options.env,
|
|
311
|
+
OCX_REMOTE_WORKSPACE_BRIDGE_URL: bridge.url,
|
|
312
|
+
OCX_REMOTE_WORKSPACE_BRIDGE_TOKEN: bridge.token,
|
|
313
|
+
};
|
|
314
|
+
const invocation = remoteWorkspaceProcessInvocation([
|
|
315
|
+
...commandPrefix,
|
|
316
|
+
"--mode", "rpc",
|
|
317
|
+
"--session-id", threadId,
|
|
318
|
+
"--name", `OCX Remote: ${options.deviceName}`,
|
|
319
|
+
"--no-builtin-tools",
|
|
320
|
+
"--no-extensions",
|
|
321
|
+
"--no-skills",
|
|
322
|
+
"--no-prompt-templates",
|
|
323
|
+
"--no-themes",
|
|
324
|
+
"--no-context-files",
|
|
325
|
+
"--no-approve",
|
|
326
|
+
"--extension", extensionPath,
|
|
327
|
+
"--tools", options.tools.map(tool => `remote_${tool}`).join(","),
|
|
328
|
+
"--system-prompt", remotePiInstructions(options.deviceName, options.tools),
|
|
329
|
+
], { env: childEnv });
|
|
330
|
+
let child: Bun.Subprocess<"pipe", "pipe", "pipe">;
|
|
331
|
+
try {
|
|
332
|
+
child = Bun.spawn([invocation.file, ...invocation.args], {
|
|
333
|
+
cwd: isolation,
|
|
334
|
+
env: childEnv,
|
|
335
|
+
stdin: "pipe",
|
|
336
|
+
stdout: "pipe",
|
|
337
|
+
stderr: "pipe",
|
|
338
|
+
...invocation.options,
|
|
339
|
+
});
|
|
340
|
+
} catch (error) {
|
|
341
|
+
await bridge.stop();
|
|
342
|
+
removeRemoteWorkspaceIsolation(isolation);
|
|
343
|
+
throw error;
|
|
344
|
+
}
|
|
345
|
+
const rpc = new PiRpcProcess(child);
|
|
346
|
+
rpc.onEvent = event => {
|
|
347
|
+
if (event.type === "message_end") {
|
|
348
|
+
const text = messageText(event.message);
|
|
349
|
+
if (text) options.emit("assistant", text);
|
|
350
|
+
}
|
|
351
|
+
if (event.type === "tool_execution_start" && typeof event.toolName === "string") {
|
|
352
|
+
options.emit("tool", `Pi requested ${event.toolName}`);
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
try {
|
|
356
|
+
const state = await rpc.command("get_state");
|
|
357
|
+
if (state.success !== true) throw new Error(safeError(state.error, "Pi RPC failed to initialize"));
|
|
358
|
+
} catch (error) {
|
|
359
|
+
await rpc.close().catch(() => {});
|
|
360
|
+
await bridge.stop();
|
|
361
|
+
removeRemoteWorkspaceIsolation(isolation);
|
|
362
|
+
throw error;
|
|
363
|
+
}
|
|
364
|
+
let stopped = false;
|
|
365
|
+
let stopOperation: Promise<void> | null = null;
|
|
366
|
+
return {
|
|
367
|
+
threadId,
|
|
368
|
+
prompt: text => rpc.prompt(text),
|
|
369
|
+
stop(): Promise<void> {
|
|
370
|
+
if (stopOperation) return stopOperation;
|
|
371
|
+
stopped = true;
|
|
372
|
+
stopOperation = runRemoteWorkspaceCleanupSteps([
|
|
373
|
+
() => rpc.abort(),
|
|
374
|
+
() => rpc.close(),
|
|
375
|
+
() => bridge.stop(),
|
|
376
|
+
() => removeRemoteWorkspaceIsolation(isolation),
|
|
377
|
+
]);
|
|
378
|
+
return stopOperation;
|
|
379
|
+
},
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
}
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { execFileSync } from "node:child_process";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
import { commandInvocation, type SpawnInvocation } from "../lib/win-exec";
|
|
4
|
+
import { resolveTrustedWindowsTaskkillExe } from "../lib/windows-elevation";
|
|
5
|
+
|
|
6
|
+
export interface RemoteWorkspaceProcessInvocationOptions {
|
|
7
|
+
platform?: NodeJS.Platform;
|
|
8
|
+
env?: Record<string, string | undefined>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Preserve argv boundaries on Unix and route Windows npm `.cmd`/`.bat` shims through the
|
|
13
|
+
* repository's audited ComSpec escaping. `shell: true` is deliberately never used.
|
|
14
|
+
*/
|
|
15
|
+
export function remoteWorkspaceProcessInvocation(
|
|
16
|
+
command: readonly string[],
|
|
17
|
+
options: RemoteWorkspaceProcessInvocationOptions = {},
|
|
18
|
+
): SpawnInvocation {
|
|
19
|
+
if (command.length < 1 || !command[0]) throw new Error("remote workspace process command is empty");
|
|
20
|
+
return commandInvocation(
|
|
21
|
+
command[0],
|
|
22
|
+
command.slice(1),
|
|
23
|
+
options.platform ?? process.platform,
|
|
24
|
+
{ env: options.env ?? process.env },
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface RemoteWorkspaceOwnedProcess {
|
|
29
|
+
pid: number;
|
|
30
|
+
exitCode: number | null;
|
|
31
|
+
exited: Promise<number>;
|
|
32
|
+
kill(signal?: number | NodeJS.Signals): void;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface StopRemoteWorkspaceProcessOptions {
|
|
36
|
+
platform?: NodeJS.Platform;
|
|
37
|
+
taskkillPath?: string;
|
|
38
|
+
execFile?: (file: string, args: readonly string[]) => void;
|
|
39
|
+
waitMs?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export async function waitForRemoteWorkspaceProcessExit(
|
|
43
|
+
child: RemoteWorkspaceOwnedProcess,
|
|
44
|
+
waitMs: number,
|
|
45
|
+
): Promise<boolean> {
|
|
46
|
+
if (!Number.isSafeInteger(waitMs) || waitMs < 1) throw new Error("invalid remote workspace process wait");
|
|
47
|
+
let timer: ReturnType<typeof setTimeout> | null = null;
|
|
48
|
+
try {
|
|
49
|
+
return await Promise.race([
|
|
50
|
+
child.exited.then(() => true, () => true),
|
|
51
|
+
new Promise<boolean>(resolve => { timer = setTimeout(() => resolve(false), waitMs); }),
|
|
52
|
+
]);
|
|
53
|
+
} finally {
|
|
54
|
+
if (timer) clearTimeout(timer);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Run every owned-resource cleanup step and report the first failure only after all were attempted. */
|
|
59
|
+
export async function runRemoteWorkspaceCleanupSteps(
|
|
60
|
+
steps: readonly (() => void | Promise<void>)[],
|
|
61
|
+
): Promise<void> {
|
|
62
|
+
let failed = false;
|
|
63
|
+
let firstFailure: unknown;
|
|
64
|
+
for (const step of steps) {
|
|
65
|
+
try {
|
|
66
|
+
await step();
|
|
67
|
+
} catch (error) {
|
|
68
|
+
if (!failed) firstFailure = error;
|
|
69
|
+
failed = true;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
if (failed) {
|
|
73
|
+
throw firstFailure instanceof Error
|
|
74
|
+
? firstFailure
|
|
75
|
+
: new Error("remote workspace cleanup failed");
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Stop only the process OCX spawned; Windows must include its `.cmd` descendant tree. */
|
|
80
|
+
export async function stopRemoteWorkspaceProcess(
|
|
81
|
+
child: RemoteWorkspaceOwnedProcess,
|
|
82
|
+
options: StopRemoteWorkspaceProcessOptions = {},
|
|
83
|
+
): Promise<void> {
|
|
84
|
+
if (child.exitCode !== null) return;
|
|
85
|
+
const platform = options.platform ?? process.platform;
|
|
86
|
+
if (platform === "win32") {
|
|
87
|
+
const exec = options.execFile ?? ((file: string, args: readonly string[]) => {
|
|
88
|
+
execFileSync(file, [...args], { stdio: "ignore", timeout: 5_000, windowsHide: true });
|
|
89
|
+
});
|
|
90
|
+
try {
|
|
91
|
+
exec(options.taskkillPath ?? resolveTrustedWindowsTaskkillExe(), ["/PID", String(child.pid), "/T", "/F"]);
|
|
92
|
+
} catch {
|
|
93
|
+
try { child.kill(); } catch { /* child already exited */ }
|
|
94
|
+
}
|
|
95
|
+
if (!await waitForRemoteWorkspaceProcessExit(child, options.waitMs ?? 1_500)) {
|
|
96
|
+
throw new Error("remote workspace Windows process tree did not exit");
|
|
97
|
+
}
|
|
98
|
+
} else {
|
|
99
|
+
try { child.kill("SIGTERM"); } catch { /* child already exited */ }
|
|
100
|
+
const exited = await waitForRemoteWorkspaceProcessExit(child, options.waitMs ?? 1_500);
|
|
101
|
+
if (!exited) {
|
|
102
|
+
try { child.kill("SIGKILL"); } catch { /* child already exited */ }
|
|
103
|
+
if (!await waitForRemoteWorkspaceProcessExit(child, options.waitMs ?? 1_500)) {
|
|
104
|
+
throw new Error("remote workspace process did not exit after SIGKILL");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Windows AV/indexers can retain just-exited CLI files briefly; use Node's bounded retry. */
|
|
111
|
+
export function removeRemoteWorkspaceIsolation(path: string): void {
|
|
112
|
+
rmSync(path, { recursive: true, force: true, maxRetries: 3, retryDelay: 25 });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* [Decision Log]
|
|
117
|
+
* - 목적과 의도: Make Hub-owned Codex, Claude Code, and Pi processes start and stop identically
|
|
118
|
+
* across Linux, macOS, and Windows without leaving npm-shim descendants behind.
|
|
119
|
+
* - 기존 구현 및 제약 조건: Unix can spawn executable scripts directly. Windows npm exposes
|
|
120
|
+
* `.cmd` files that Bun cannot safely launch shell-less, and killing cmd.exe alone can orphan Node.
|
|
121
|
+
* - 검토한 주요 대안: `shell: true`, three runtime-specific wrappers, direct `.cmd` spawn, or the
|
|
122
|
+
* repository's existing escaped ComSpec invocation plus trusted System32 taskkill.
|
|
123
|
+
* - 선택한 방식: Share one launcher and one owned-process stop helper across all three runtimes.
|
|
124
|
+
* - 다른 대안 대신 이 방식을 선택한 이유: It preserves exact argv boundaries, avoids a PATH-
|
|
125
|
+
* resolved shell/taskkill hijack, and matches already-tested OpenCodex Windows behavior.
|
|
126
|
+
* - 장점, 단점 및 영향: Windows npm installs work and stop cleanly. Windows stop is necessarily
|
|
127
|
+
* forceful because its normal process kill is already forceful; Unix gets a graceful SIGTERM
|
|
128
|
+
* window and then a bounded SIGKILL fallback so an ignoring child cannot outlive the session.
|
|
129
|
+
*/
|