@bitkyc08/opencodex 2.52.0 → 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-CWXut3rG.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,304 @@
|
|
|
1
|
+
import type { RemoteControlCipher } from "./crypto";
|
|
2
|
+
import type {
|
|
3
|
+
RemoteWorkspaceExecutionRequest,
|
|
4
|
+
RemoteWorkspaceExecutor,
|
|
5
|
+
} from "./workspace-executor";
|
|
6
|
+
import {
|
|
7
|
+
isRemoteWorkspaceToolName,
|
|
8
|
+
remoteWorkspaceCapabilityForTool,
|
|
9
|
+
type RemoteWorkspaceCapability,
|
|
10
|
+
type RemoteWorkspaceToolResult,
|
|
11
|
+
} from "./workspace-tools";
|
|
12
|
+
import type { RemoteWorkspaceTransport } from "./workspace-coordinator";
|
|
13
|
+
import {
|
|
14
|
+
REMOTE_WORKSPACE_RPC_MAX_MESSAGE_BYTES,
|
|
15
|
+
RemoteWorkspaceRpcReassembler,
|
|
16
|
+
frameRemoteWorkspaceRpcMessage,
|
|
17
|
+
} from "./workspace-rpc-framing";
|
|
18
|
+
|
|
19
|
+
const REMOTE_WORKSPACE_RPC_VERSION = 1 as const;
|
|
20
|
+
const REMOTE_WORKSPACE_RPC_DEFAULT_TIMEOUT_MS = 30_000;
|
|
21
|
+
const REMOTE_WORKSPACE_RPC_MAX_ACTIVE_REQUESTS = 8;
|
|
22
|
+
interface RemoteWorkspaceRpcRequest {
|
|
23
|
+
version: typeof REMOTE_WORKSPACE_RPC_VERSION;
|
|
24
|
+
kind: "request";
|
|
25
|
+
request: RemoteWorkspaceExecutionRequest;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface RemoteWorkspaceRpcResponse {
|
|
29
|
+
version: typeof REMOTE_WORKSPACE_RPC_VERSION;
|
|
30
|
+
kind: "response";
|
|
31
|
+
requestId: string;
|
|
32
|
+
result: RemoteWorkspaceToolResult;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type RemoteWorkspaceRpcMessage = RemoteWorkspaceRpcRequest | RemoteWorkspaceRpcResponse;
|
|
36
|
+
|
|
37
|
+
interface PendingRequest {
|
|
38
|
+
resolve(value: RemoteWorkspaceToolResult): void;
|
|
39
|
+
reject(error: Error): void;
|
|
40
|
+
timer: ReturnType<typeof setTimeout>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function boundedIdentifier(value: unknown): value is string {
|
|
44
|
+
return typeof value === "string" && value.length >= 1 && value.length <= 256 && !/[\x00-\x1f\x7f]/.test(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function encodeMessage(value: RemoteWorkspaceRpcMessage): Uint8Array {
|
|
48
|
+
const encoded = new TextEncoder().encode(JSON.stringify(value));
|
|
49
|
+
if (encoded.byteLength > REMOTE_WORKSPACE_RPC_MAX_MESSAGE_BYTES) {
|
|
50
|
+
throw new Error("remote workspace RPC message exceeds the bounded message limit");
|
|
51
|
+
}
|
|
52
|
+
return encoded;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function parseResult(value: unknown): RemoteWorkspaceToolResult {
|
|
56
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid remote workspace RPC result");
|
|
57
|
+
const raw = value as Record<string, unknown>;
|
|
58
|
+
if (Object.keys(raw).some(key => key !== "ok" && key !== "value" && key !== "error")) {
|
|
59
|
+
throw new Error("invalid remote workspace RPC result fields");
|
|
60
|
+
}
|
|
61
|
+
if (raw.ok === true && raw.error === undefined) {
|
|
62
|
+
return raw.value === undefined ? { ok: true } : { ok: true, value: raw.value };
|
|
63
|
+
}
|
|
64
|
+
if (raw.ok === false && raw.value === undefined
|
|
65
|
+
&& typeof raw.error === "string" && raw.error.length >= 1 && raw.error.length <= 4096) {
|
|
66
|
+
return { ok: false, error: raw.error };
|
|
67
|
+
}
|
|
68
|
+
throw new Error("invalid remote workspace RPC result status");
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function parseRequest(value: unknown): RemoteWorkspaceExecutionRequest {
|
|
72
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error("invalid remote workspace RPC request");
|
|
73
|
+
const raw = value as Record<string, unknown>;
|
|
74
|
+
if (
|
|
75
|
+
!boundedIdentifier(raw.requestId)
|
|
76
|
+
|| !boundedIdentifier(raw.sessionId)
|
|
77
|
+
|| !boundedIdentifier(raw.executorDeviceId)
|
|
78
|
+
|| !boundedIdentifier(raw.rootId)
|
|
79
|
+
|| !isRemoteWorkspaceToolName(raw.tool)
|
|
80
|
+
) throw new Error("invalid remote workspace RPC request identity");
|
|
81
|
+
return {
|
|
82
|
+
requestId: raw.requestId,
|
|
83
|
+
sessionId: raw.sessionId,
|
|
84
|
+
executorDeviceId: raw.executorDeviceId,
|
|
85
|
+
rootId: raw.rootId,
|
|
86
|
+
tool: raw.tool,
|
|
87
|
+
arguments: raw.arguments,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function parseMessage(value: Uint8Array): RemoteWorkspaceRpcMessage {
|
|
92
|
+
if (!(value instanceof Uint8Array) || value.byteLength < 1 || value.byteLength > REMOTE_WORKSPACE_RPC_MAX_MESSAGE_BYTES) {
|
|
93
|
+
throw new Error("invalid remote workspace RPC message length");
|
|
94
|
+
}
|
|
95
|
+
let parsed: unknown;
|
|
96
|
+
try {
|
|
97
|
+
parsed = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(value));
|
|
98
|
+
} catch {
|
|
99
|
+
throw new Error("invalid remote workspace RPC JSON");
|
|
100
|
+
}
|
|
101
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid remote workspace RPC message");
|
|
102
|
+
const raw = parsed as Record<string, unknown>;
|
|
103
|
+
if (raw.version !== REMOTE_WORKSPACE_RPC_VERSION) throw new Error("unsupported remote workspace RPC version");
|
|
104
|
+
if (raw.kind === "request") {
|
|
105
|
+
return { version: REMOTE_WORKSPACE_RPC_VERSION, kind: "request", request: parseRequest(raw.request) };
|
|
106
|
+
}
|
|
107
|
+
if (raw.kind === "response" && boundedIdentifier(raw.requestId)) {
|
|
108
|
+
return {
|
|
109
|
+
version: REMOTE_WORKSPACE_RPC_VERSION,
|
|
110
|
+
kind: "response",
|
|
111
|
+
requestId: raw.requestId,
|
|
112
|
+
result: parseResult(raw.result),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
throw new Error("invalid remote workspace RPC message kind");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface EncryptedRemoteWorkspaceTransportOptions {
|
|
119
|
+
executorDeviceId: string;
|
|
120
|
+
cipher: RemoteControlCipher;
|
|
121
|
+
sendCiphertext(value: Uint8Array): void | Promise<void>;
|
|
122
|
+
timeoutMs?: number;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Coordinator-side transport. The WebSocket/relay adapter only has to carry ciphertext. */
|
|
126
|
+
export class EncryptedRemoteWorkspaceTransport implements RemoteWorkspaceTransport {
|
|
127
|
+
private readonly pending = new Map<string, PendingRequest>();
|
|
128
|
+
private readonly reassembler = new RemoteWorkspaceRpcReassembler();
|
|
129
|
+
private readonly timeoutMs: number;
|
|
130
|
+
private sendTail: Promise<void> = Promise.resolve();
|
|
131
|
+
private online = true;
|
|
132
|
+
|
|
133
|
+
constructor(private readonly options: EncryptedRemoteWorkspaceTransportOptions) {
|
|
134
|
+
this.timeoutMs = options.timeoutMs ?? REMOTE_WORKSPACE_RPC_DEFAULT_TIMEOUT_MS;
|
|
135
|
+
if (!boundedIdentifier(options.executorDeviceId) || !Number.isSafeInteger(this.timeoutMs) || this.timeoutMs < 1) {
|
|
136
|
+
throw new Error("invalid encrypted remote workspace transport options");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
isOnline(deviceId: string): boolean {
|
|
141
|
+
return this.online && deviceId === this.options.executorDeviceId;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
async invoke(request: RemoteWorkspaceExecutionRequest): Promise<RemoteWorkspaceToolResult> {
|
|
145
|
+
if (!this.isOnline(request.executorDeviceId)) throw new Error("remote workspace executor is offline");
|
|
146
|
+
if (this.pending.has(request.requestId)) throw new Error("duplicate remote workspace request ID");
|
|
147
|
+
if (this.pending.size >= REMOTE_WORKSPACE_RPC_MAX_ACTIVE_REQUESTS) {
|
|
148
|
+
throw new Error("remote workspace request limit reached");
|
|
149
|
+
}
|
|
150
|
+
const response = new Promise<RemoteWorkspaceToolResult>((resolve, reject) => {
|
|
151
|
+
const timer = setTimeout(() => {
|
|
152
|
+
this.pending.delete(request.requestId);
|
|
153
|
+
reject(new Error("remote workspace request timed out"));
|
|
154
|
+
}, this.timeoutMs);
|
|
155
|
+
this.pending.set(request.requestId, { resolve, reject, timer });
|
|
156
|
+
});
|
|
157
|
+
try {
|
|
158
|
+
await this.sendMessage(encodeMessage({
|
|
159
|
+
version: REMOTE_WORKSPACE_RPC_VERSION,
|
|
160
|
+
kind: "request",
|
|
161
|
+
request,
|
|
162
|
+
}));
|
|
163
|
+
} catch {
|
|
164
|
+
// A failed encrypted write consumes a directional counter. Continuing would make every
|
|
165
|
+
// later frame undecryptable, so fail every pending operation instead of waiting for timeout.
|
|
166
|
+
this.close("remote workspace send failed");
|
|
167
|
+
}
|
|
168
|
+
return await response;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
receiveCiphertext(value: Uint8Array): void {
|
|
172
|
+
if (!this.online) throw new Error("remote workspace transport is closed");
|
|
173
|
+
const responsePlaintext = this.reassembler.accept(this.options.cipher.decrypt(value));
|
|
174
|
+
if (!responsePlaintext) return;
|
|
175
|
+
const message = parseMessage(responsePlaintext);
|
|
176
|
+
if (message.kind !== "response") throw new Error("coordinator received a remote workspace request");
|
|
177
|
+
const pending = this.pending.get(message.requestId);
|
|
178
|
+
if (!pending) return;
|
|
179
|
+
clearTimeout(pending.timer);
|
|
180
|
+
this.pending.delete(message.requestId);
|
|
181
|
+
pending.resolve(message.result);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
close(reason = "remote workspace transport closed"): void {
|
|
185
|
+
if (!this.online) return;
|
|
186
|
+
this.online = false;
|
|
187
|
+
this.reassembler.clear();
|
|
188
|
+
this.options.cipher.destroy();
|
|
189
|
+
for (const pending of this.pending.values()) {
|
|
190
|
+
clearTimeout(pending.timer);
|
|
191
|
+
pending.reject(new Error(reason));
|
|
192
|
+
}
|
|
193
|
+
this.pending.clear();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private sendMessage(message: Uint8Array): Promise<void> {
|
|
197
|
+
const operation = this.sendTail.then(async () => {
|
|
198
|
+
if (!this.online) throw new Error("remote workspace transport is closed");
|
|
199
|
+
for (const frame of frameRemoteWorkspaceRpcMessage(message)) {
|
|
200
|
+
await this.options.sendCiphertext(this.options.cipher.encrypt(frame));
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
this.sendTail = operation.catch(() => {});
|
|
204
|
+
return operation;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export interface EncryptedRemoteWorkspaceExecutorEndpointOptions {
|
|
209
|
+
executorDeviceId: string;
|
|
210
|
+
sessionId: string;
|
|
211
|
+
rootId: string;
|
|
212
|
+
capabilities: readonly RemoteWorkspaceCapability[];
|
|
213
|
+
cipher: RemoteControlCipher;
|
|
214
|
+
executor: Pick<RemoteWorkspaceExecutor, "invoke">;
|
|
215
|
+
sendCiphertext(value: Uint8Array): void | Promise<void>;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/** Executor-side endpoint. It accepts only authenticated, ordered E2EE session frames. */
|
|
219
|
+
export class EncryptedRemoteWorkspaceExecutorEndpoint {
|
|
220
|
+
private closed = false;
|
|
221
|
+
private readonly active = new Map<string, AbortController>();
|
|
222
|
+
private readonly reassembler = new RemoteWorkspaceRpcReassembler();
|
|
223
|
+
private sendTail: Promise<void> = Promise.resolve();
|
|
224
|
+
|
|
225
|
+
private readonly grantedCapabilities: ReadonlySet<RemoteWorkspaceCapability>;
|
|
226
|
+
private readonly sessionId: string;
|
|
227
|
+
private readonly rootId: string;
|
|
228
|
+
|
|
229
|
+
constructor(private readonly options: EncryptedRemoteWorkspaceExecutorEndpointOptions) {
|
|
230
|
+
if (!boundedIdentifier(options.executorDeviceId) || !boundedIdentifier(options.sessionId)
|
|
231
|
+
|| !boundedIdentifier(options.rootId)) throw new Error("invalid remote workspace executor endpoint");
|
|
232
|
+
this.options = { ...options };
|
|
233
|
+
this.sessionId = options.sessionId;
|
|
234
|
+
this.rootId = options.rootId;
|
|
235
|
+
this.grantedCapabilities = new Set(options.capabilities);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
async receiveCiphertext(value: Uint8Array): Promise<void> {
|
|
239
|
+
if (this.closed) throw new Error("remote workspace executor endpoint is closed");
|
|
240
|
+
const requestPlaintext = this.reassembler.accept(this.options.cipher.decrypt(value));
|
|
241
|
+
if (!requestPlaintext) return;
|
|
242
|
+
const message = parseMessage(requestPlaintext);
|
|
243
|
+
if (message.kind !== "request") throw new Error("executor received a remote workspace response");
|
|
244
|
+
if (message.request.executorDeviceId !== this.options.executorDeviceId) {
|
|
245
|
+
throw new Error("remote workspace encrypted request targeted another executor");
|
|
246
|
+
}
|
|
247
|
+
if (message.request.sessionId !== this.sessionId || message.request.rootId !== this.rootId) {
|
|
248
|
+
throw new Error("remote workspace encrypted request does not match its session binding");
|
|
249
|
+
}
|
|
250
|
+
if (!this.grantedCapabilities.has(remoteWorkspaceCapabilityForTool(message.request.tool))) {
|
|
251
|
+
throw new Error("remote workspace tool capability was not granted to this session");
|
|
252
|
+
}
|
|
253
|
+
if (this.active.has(message.request.requestId)) throw new Error("duplicate remote workspace executor request ID");
|
|
254
|
+
if (this.active.size >= REMOTE_WORKSPACE_RPC_MAX_ACTIVE_REQUESTS) {
|
|
255
|
+
throw new Error("remote workspace executor request limit reached");
|
|
256
|
+
}
|
|
257
|
+
const controller = new AbortController();
|
|
258
|
+
this.active.set(message.request.requestId, controller);
|
|
259
|
+
let result: RemoteWorkspaceToolResult;
|
|
260
|
+
try {
|
|
261
|
+
result = await this.options.executor.invoke(message.request, controller.signal);
|
|
262
|
+
} finally {
|
|
263
|
+
this.active.delete(message.request.requestId);
|
|
264
|
+
}
|
|
265
|
+
if (this.closed) return;
|
|
266
|
+
let responsePlaintext: Uint8Array;
|
|
267
|
+
try {
|
|
268
|
+
responsePlaintext = encodeMessage({
|
|
269
|
+
version: REMOTE_WORKSPACE_RPC_VERSION,
|
|
270
|
+
kind: "response",
|
|
271
|
+
requestId: message.request.requestId,
|
|
272
|
+
result,
|
|
273
|
+
});
|
|
274
|
+
} catch {
|
|
275
|
+
responsePlaintext = encodeMessage({
|
|
276
|
+
version: REMOTE_WORKSPACE_RPC_VERSION,
|
|
277
|
+
kind: "response",
|
|
278
|
+
requestId: message.request.requestId,
|
|
279
|
+
result: { ok: false, error: "remote workspace result exceeded the encrypted frame limit" },
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
await this.sendMessage(responsePlaintext);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
close(): void {
|
|
286
|
+
if (this.closed) return;
|
|
287
|
+
this.closed = true;
|
|
288
|
+
this.reassembler.clear();
|
|
289
|
+
for (const controller of this.active.values()) controller.abort();
|
|
290
|
+
this.active.clear();
|
|
291
|
+
this.options.cipher.destroy();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
private sendMessage(message: Uint8Array): Promise<void> {
|
|
295
|
+
const operation = this.sendTail.then(async () => {
|
|
296
|
+
if (this.closed) throw new Error("remote workspace executor endpoint is closed");
|
|
297
|
+
for (const frame of frameRemoteWorkspaceRpcMessage(message)) {
|
|
298
|
+
await this.options.sendCiphertext(this.options.cipher.encrypt(frame));
|
|
299
|
+
}
|
|
300
|
+
});
|
|
301
|
+
this.sendTail = operation.catch(() => {});
|
|
302
|
+
return operation;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import {
|
|
3
|
+
RemoteWorkspaceHub,
|
|
4
|
+
RemoteWorkspaceHubFileStore,
|
|
5
|
+
type RemoteWorkspaceHubStateStore,
|
|
6
|
+
} from "./workspace-hub";
|
|
7
|
+
import { CodexRemoteWorkspaceRuntimeFactory } from "./workspace-codex-runtime";
|
|
8
|
+
import { ClaudeRemoteWorkspaceRuntimeFactory } from "./workspace-claude-runtime";
|
|
9
|
+
import { PiRemoteWorkspaceRuntimeFactory } from "./workspace-pi-runtime";
|
|
10
|
+
import {
|
|
11
|
+
RemoteWorkspaceSessionFileStore,
|
|
12
|
+
RemoteWorkspaceSessionService,
|
|
13
|
+
} from "./workspace-sessions";
|
|
14
|
+
|
|
15
|
+
const hubs = new WeakMap<object, RemoteWorkspaceHub>();
|
|
16
|
+
const sessionServices = new WeakMap<object, RemoteWorkspaceSessionService>();
|
|
17
|
+
|
|
18
|
+
export function remoteWorkspaceHubForConfig(
|
|
19
|
+
config: Readonly<OcxConfig>,
|
|
20
|
+
store?: RemoteWorkspaceHubStateStore,
|
|
21
|
+
): RemoteWorkspaceHub {
|
|
22
|
+
if (config.runtimeRole !== "hub") throw new Error("remote workspace requires runtimeRole=hub");
|
|
23
|
+
const existing = hubs.get(config);
|
|
24
|
+
if (existing) return existing;
|
|
25
|
+
const hub = new RemoteWorkspaceHub(store ?? new RemoteWorkspaceHubFileStore());
|
|
26
|
+
hubs.set(config, hub);
|
|
27
|
+
return hub;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function remoteWorkspaceSessionsForConfig(
|
|
31
|
+
config: Readonly<OcxConfig>,
|
|
32
|
+
): RemoteWorkspaceSessionService {
|
|
33
|
+
if (config.runtimeRole !== "hub") throw new Error("remote workspace requires runtimeRole=hub");
|
|
34
|
+
const existing = sessionServices.get(config);
|
|
35
|
+
if (existing) return existing;
|
|
36
|
+
const service = new RemoteWorkspaceSessionService(
|
|
37
|
+
remoteWorkspaceHubForConfig(config),
|
|
38
|
+
[
|
|
39
|
+
new CodexRemoteWorkspaceRuntimeFactory(),
|
|
40
|
+
new ClaudeRemoteWorkspaceRuntimeFactory(),
|
|
41
|
+
new PiRemoteWorkspaceRuntimeFactory(),
|
|
42
|
+
],
|
|
43
|
+
Date.now,
|
|
44
|
+
new RemoteWorkspaceSessionFileStore(),
|
|
45
|
+
);
|
|
46
|
+
sessionServices.set(config, service);
|
|
47
|
+
return service;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function initializedRemoteWorkspaceHubForConfig(
|
|
51
|
+
config: Readonly<OcxConfig>,
|
|
52
|
+
): RemoteWorkspaceHub | null {
|
|
53
|
+
return hubs.get(config) ?? null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function initializedRemoteWorkspaceSessionsForConfig(
|
|
57
|
+
config: Readonly<OcxConfig>,
|
|
58
|
+
): RemoteWorkspaceSessionService | null {
|
|
59
|
+
return sessionServices.get(config) ?? null;
|
|
60
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { chmodSync, lstatSync, mkdirSync } from "node:fs";
|
|
2
|
+
import { assertNotRealHomeUnderTest } from "../lib/test-home-guard";
|
|
3
|
+
import { hardenSecretDir, hardenSecretPath } from "../lib/windows-secret-acl";
|
|
4
|
+
|
|
5
|
+
export interface WorkspaceSecretPermissions {
|
|
6
|
+
prepareDirectory(path: string): void;
|
|
7
|
+
hardenFile(path: string): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Only ENOENT means first-run absence; permission failures must not reset identity. */
|
|
11
|
+
export function workspaceSecretFileExists(path: string): boolean {
|
|
12
|
+
try { lstatSync(path); return true; }
|
|
13
|
+
catch (error) {
|
|
14
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const workspaceSecretPermissions: WorkspaceSecretPermissions = {
|
|
20
|
+
prepareDirectory(path) {
|
|
21
|
+
assertNotRealHomeUnderTest(path);
|
|
22
|
+
mkdirSync(path, { recursive: true, mode: 0o700 });
|
|
23
|
+
const metadata = lstatSync(path);
|
|
24
|
+
if (!metadata.isDirectory() || metadata.isSymbolicLink()) {
|
|
25
|
+
throw new Error("remote workspace secret directory must be a real directory");
|
|
26
|
+
}
|
|
27
|
+
if (process.platform === "win32") hardenSecretDir(path, { required: true });
|
|
28
|
+
else chmodSync(path, 0o700);
|
|
29
|
+
},
|
|
30
|
+
hardenFile(path) {
|
|
31
|
+
assertNotRealHomeUnderTest(path);
|
|
32
|
+
const metadata = lstatSync(path);
|
|
33
|
+
if (!metadata.isFile() || metadata.isSymbolicLink() || metadata.nlink !== 1) {
|
|
34
|
+
throw new Error("remote workspace secret must be a private regular file");
|
|
35
|
+
}
|
|
36
|
+
if (process.platform === "win32") hardenSecretPath(path, { required: true });
|
|
37
|
+
else chmodSync(path, 0o600);
|
|
38
|
+
},
|
|
39
|
+
};
|