@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,366 @@
|
|
|
1
|
+
import type { RemoteControlIdentityKeyPair } from "./crypto";
|
|
2
|
+
import {
|
|
3
|
+
RemoteControlClientHandshake,
|
|
4
|
+
acceptRemoteControlClientHello,
|
|
5
|
+
} from "./crypto";
|
|
6
|
+
import type { RemoteWorkspaceExecutor } from "./workspace-executor";
|
|
7
|
+
import { REMOTE_CONTROL_MAX_SESSIONS_PER_DEVICE } from "./protocol";
|
|
8
|
+
import {
|
|
9
|
+
EncryptedRemoteWorkspaceExecutorEndpoint,
|
|
10
|
+
EncryptedRemoteWorkspaceTransport,
|
|
11
|
+
} from "./workspace-rpc";
|
|
12
|
+
import {
|
|
13
|
+
REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
|
|
14
|
+
parseRemoteWorkspaceAgentMessage,
|
|
15
|
+
parseRemoteWorkspaceHubMessage,
|
|
16
|
+
serializeRemoteWorkspaceAgentMessage,
|
|
17
|
+
serializeRemoteWorkspaceHubMessage,
|
|
18
|
+
type RemoteWorkspaceAgentProfile,
|
|
19
|
+
} from "./workspace-agent-protocol";
|
|
20
|
+
import {
|
|
21
|
+
parseRemoteWorkspaceCapabilities,
|
|
22
|
+
type RemoteWorkspaceCapability,
|
|
23
|
+
} from "./workspace-tools";
|
|
24
|
+
import { truncateRemoteWorkspaceUtf8 } from "./workspace-utf8";
|
|
25
|
+
|
|
26
|
+
const SESSION_OPEN_TIMEOUT_MS = 10_000;
|
|
27
|
+
|
|
28
|
+
export interface RemoteWorkspaceControlSocket {
|
|
29
|
+
send(value: string): void | Promise<void>;
|
|
30
|
+
close(code: number, reason: string): void;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
interface PendingHubSession {
|
|
34
|
+
handshake: RemoteControlClientHandshake;
|
|
35
|
+
resolve(transport: EncryptedRemoteWorkspaceTransport): void;
|
|
36
|
+
reject(error: Error): void;
|
|
37
|
+
timer: ReturnType<typeof setTimeout>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function safeReason(value: string): string {
|
|
41
|
+
const cleaned = value.replace(/[\x00-\x1f\x7f]/g, " ").trim();
|
|
42
|
+
const selected = cleaned || "remote workspace session closed";
|
|
43
|
+
return truncateRemoteWorkspaceUtf8(selected, 120);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Hub-side representation of one authenticated, online OCX-only executor. */
|
|
47
|
+
export class RemoteWorkspaceHubAgentConnection {
|
|
48
|
+
private readonly pending = new Map<string, PendingHubSession>();
|
|
49
|
+
private readonly active = new Map<string, EncryptedRemoteWorkspaceTransport>();
|
|
50
|
+
private readonly cancelledSessionIds = new Set<string>();
|
|
51
|
+
private closed = false;
|
|
52
|
+
private presenceAccepted = false;
|
|
53
|
+
private presencePending = false;
|
|
54
|
+
private currentCapabilities: RemoteWorkspaceCapability[];
|
|
55
|
+
|
|
56
|
+
constructor(private readonly options: {
|
|
57
|
+
deviceId: string;
|
|
58
|
+
devicePublicKey: string;
|
|
59
|
+
hubIdentity: RemoteControlIdentityKeyPair;
|
|
60
|
+
socket: RemoteWorkspaceControlSocket;
|
|
61
|
+
capabilities?: readonly RemoteWorkspaceCapability[];
|
|
62
|
+
onCapabilities?: (capabilities: readonly RemoteWorkspaceCapability[]) => void;
|
|
63
|
+
sessionOpenTimeoutMs?: number;
|
|
64
|
+
}) {
|
|
65
|
+
this.currentCapabilities = parseRemoteWorkspaceCapabilities(options.capabilities);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
isOnline(): boolean {
|
|
69
|
+
return !this.closed && this.presenceAccepted;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
capabilities(): RemoteWorkspaceCapability[] {
|
|
73
|
+
return [...this.currentCapabilities];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async openSession(options: {
|
|
77
|
+
sessionId: string;
|
|
78
|
+
rootId: string;
|
|
79
|
+
profile: RemoteWorkspaceAgentProfile;
|
|
80
|
+
capabilities: readonly RemoteWorkspaceCapability[];
|
|
81
|
+
}): Promise<EncryptedRemoteWorkspaceTransport> {
|
|
82
|
+
if (!this.isOnline()) throw new Error("remote workspace executor is offline");
|
|
83
|
+
if (this.pending.has(options.sessionId) || this.active.has(options.sessionId)) {
|
|
84
|
+
throw new Error("remote workspace session already exists");
|
|
85
|
+
}
|
|
86
|
+
if (this.pending.size + this.active.size >= REMOTE_CONTROL_MAX_SESSIONS_PER_DEVICE) {
|
|
87
|
+
throw new Error("remote workspace executor session limit reached");
|
|
88
|
+
}
|
|
89
|
+
if (!Array.isArray(options.capabilities)) throw new Error("remote workspace session requires explicit capabilities");
|
|
90
|
+
const requestedCapabilities = parseRemoteWorkspaceCapabilities(options.capabilities);
|
|
91
|
+
if (requestedCapabilities.some(capability => !this.currentCapabilities.includes(capability))) {
|
|
92
|
+
throw new Error("remote workspace session requests an unavailable capability");
|
|
93
|
+
}
|
|
94
|
+
const handshake = RemoteControlClientHandshake.create({
|
|
95
|
+
sessionId: options.sessionId,
|
|
96
|
+
deviceId: this.options.deviceId,
|
|
97
|
+
commandProfile: options.profile,
|
|
98
|
+
capabilities: requestedCapabilities,
|
|
99
|
+
accountPrivateKey: this.options.hubIdentity.privateKey,
|
|
100
|
+
});
|
|
101
|
+
const timeoutMs = this.options.sessionOpenTimeoutMs ?? SESSION_OPEN_TIMEOUT_MS;
|
|
102
|
+
const opened = new Promise<EncryptedRemoteWorkspaceTransport>((resolve, reject) => {
|
|
103
|
+
const timer = setTimeout(() => {
|
|
104
|
+
this.pending.delete(options.sessionId);
|
|
105
|
+
this.rememberCancelledSession(options.sessionId);
|
|
106
|
+
reject(new Error("remote workspace session handshake timed out"));
|
|
107
|
+
}, timeoutMs);
|
|
108
|
+
this.pending.set(options.sessionId, { handshake, resolve, reject, timer });
|
|
109
|
+
});
|
|
110
|
+
try {
|
|
111
|
+
await this.options.socket.send(serializeRemoteWorkspaceHubMessage({
|
|
112
|
+
version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
|
|
113
|
+
type: "session_open",
|
|
114
|
+
rootId: options.rootId,
|
|
115
|
+
clientHello: handshake.hello,
|
|
116
|
+
}));
|
|
117
|
+
} catch (error) {
|
|
118
|
+
const pending = this.pending.get(options.sessionId);
|
|
119
|
+
if (pending) {
|
|
120
|
+
clearTimeout(pending.timer);
|
|
121
|
+
this.pending.delete(options.sessionId);
|
|
122
|
+
pending.reject(error instanceof Error ? error : new Error("remote workspace session send failed"));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return await opened;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
receive(raw: string | Uint8Array): void {
|
|
129
|
+
if (this.closed) throw new Error("remote workspace executor is offline");
|
|
130
|
+
const message = parseRemoteWorkspaceAgentMessage(raw);
|
|
131
|
+
if (message.type === "presence") {
|
|
132
|
+
if (this.presenceAccepted || this.presencePending) {
|
|
133
|
+
throw new Error("remote workspace executor sent duplicate presence");
|
|
134
|
+
}
|
|
135
|
+
const approved = parseRemoteWorkspaceCapabilities(this.options.capabilities);
|
|
136
|
+
const capabilities = parseRemoteWorkspaceCapabilities(message.capabilities.filter(capability => approved.includes(capability)));
|
|
137
|
+
this.presencePending = true;
|
|
138
|
+
const accept = () => {
|
|
139
|
+
if (this.closed) return;
|
|
140
|
+
this.options.onCapabilities?.(capabilities);
|
|
141
|
+
this.currentCapabilities = capabilities;
|
|
142
|
+
this.presenceAccepted = true;
|
|
143
|
+
this.presencePending = false;
|
|
144
|
+
};
|
|
145
|
+
let sent: void | Promise<void>;
|
|
146
|
+
try {
|
|
147
|
+
sent = this.options.socket.send(serializeRemoteWorkspaceHubMessage({
|
|
148
|
+
version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
|
|
149
|
+
type: "presence_ack",
|
|
150
|
+
capabilities,
|
|
151
|
+
}));
|
|
152
|
+
} catch (error) {
|
|
153
|
+
this.presencePending = false;
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
if (sent && typeof sent.then === "function") {
|
|
157
|
+
void sent.then(accept).catch(() => this.close("remote workspace presence acknowledgement failed"));
|
|
158
|
+
} else {
|
|
159
|
+
accept();
|
|
160
|
+
}
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
if (!this.presenceAccepted) {
|
|
164
|
+
throw new Error("remote workspace executor presence is required before session traffic");
|
|
165
|
+
}
|
|
166
|
+
if (message.type === "heartbeat") return;
|
|
167
|
+
if (message.type === "session_accept") {
|
|
168
|
+
const pending = this.pending.get(message.sessionId);
|
|
169
|
+
if (!pending) {
|
|
170
|
+
if (!this.cancelledSessionIds.delete(message.sessionId)) {
|
|
171
|
+
throw new Error("remote workspace accepted an unknown session");
|
|
172
|
+
}
|
|
173
|
+
void Promise.resolve(this.options.socket.send(serializeRemoteWorkspaceHubMessage({
|
|
174
|
+
version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
|
|
175
|
+
type: "session_close",
|
|
176
|
+
sessionId: message.sessionId,
|
|
177
|
+
reason: "remote workspace session was already cancelled",
|
|
178
|
+
}))).catch(() => this.close("remote workspace cancelled-session cleanup failed"));
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
const cipher = pending.handshake.complete(message.hostHello, this.options.devicePublicKey);
|
|
182
|
+
const transport = new EncryptedRemoteWorkspaceTransport({
|
|
183
|
+
executorDeviceId: this.options.deviceId,
|
|
184
|
+
cipher,
|
|
185
|
+
sendCiphertext: value => this.options.socket.send(serializeRemoteWorkspaceHubMessage({
|
|
186
|
+
version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
|
|
187
|
+
type: "ciphertext",
|
|
188
|
+
sessionId: message.sessionId,
|
|
189
|
+
payload: value,
|
|
190
|
+
})),
|
|
191
|
+
});
|
|
192
|
+
clearTimeout(pending.timer);
|
|
193
|
+
this.pending.delete(message.sessionId);
|
|
194
|
+
this.active.set(message.sessionId, transport);
|
|
195
|
+
pending.resolve(transport);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
if (message.type === "session_reject") {
|
|
199
|
+
const pending = this.pending.get(message.sessionId);
|
|
200
|
+
if (!pending) return;
|
|
201
|
+
clearTimeout(pending.timer);
|
|
202
|
+
this.pending.delete(message.sessionId);
|
|
203
|
+
pending.reject(new Error(safeReason(message.reason)));
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
const transport = this.active.get(message.sessionId);
|
|
207
|
+
if (!transport) throw new Error("remote workspace ciphertext targeted an unknown session");
|
|
208
|
+
transport.receiveCiphertext(message.payload);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async closeSession(sessionId: string, reason = "remote workspace session closed"): Promise<void> {
|
|
212
|
+
const pending = this.pending.get(sessionId);
|
|
213
|
+
if (pending) {
|
|
214
|
+
clearTimeout(pending.timer);
|
|
215
|
+
this.pending.delete(sessionId);
|
|
216
|
+
this.rememberCancelledSession(sessionId);
|
|
217
|
+
pending.reject(new Error(safeReason(reason)));
|
|
218
|
+
}
|
|
219
|
+
const transport = this.active.get(sessionId);
|
|
220
|
+
if (transport) {
|
|
221
|
+
this.active.delete(sessionId);
|
|
222
|
+
transport.close(safeReason(reason));
|
|
223
|
+
}
|
|
224
|
+
if (this.closed) return;
|
|
225
|
+
await this.options.socket.send(serializeRemoteWorkspaceHubMessage({
|
|
226
|
+
version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
|
|
227
|
+
type: "session_close",
|
|
228
|
+
sessionId,
|
|
229
|
+
reason: safeReason(reason),
|
|
230
|
+
}));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
close(reason = "remote workspace executor disconnected"): void {
|
|
234
|
+
if (this.closed) return;
|
|
235
|
+
this.closed = true;
|
|
236
|
+
for (const pending of this.pending.values()) {
|
|
237
|
+
clearTimeout(pending.timer);
|
|
238
|
+
pending.reject(new Error(safeReason(reason)));
|
|
239
|
+
}
|
|
240
|
+
this.pending.clear();
|
|
241
|
+
for (const transport of this.active.values()) transport.close(safeReason(reason));
|
|
242
|
+
this.active.clear();
|
|
243
|
+
this.cancelledSessionIds.clear();
|
|
244
|
+
try { this.options.socket.close(1008, safeReason(reason)); } catch { /* socket is already gone */ }
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private rememberCancelledSession(sessionId: string): void {
|
|
248
|
+
this.cancelledSessionIds.add(sessionId);
|
|
249
|
+
while (this.cancelledSessionIds.size > 16) {
|
|
250
|
+
const oldest = this.cancelledSessionIds.values().next();
|
|
251
|
+
if (oldest.done) break;
|
|
252
|
+
this.cancelledSessionIds.delete(oldest.value);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/** Executor-side connection. It owns no Codex, Claude Code, Pi, provider key, or model session. */
|
|
258
|
+
export class RemoteWorkspaceExecutorAgentConnection {
|
|
259
|
+
private readonly sessions = new Map<string, EncryptedRemoteWorkspaceExecutorEndpoint>();
|
|
260
|
+
private closed = false;
|
|
261
|
+
|
|
262
|
+
constructor(private readonly options: {
|
|
263
|
+
deviceId: string;
|
|
264
|
+
deviceIdentity: RemoteControlIdentityKeyPair;
|
|
265
|
+
hubPublicKey: string;
|
|
266
|
+
executor: RemoteWorkspaceExecutor;
|
|
267
|
+
capabilities?: readonly RemoteWorkspaceCapability[];
|
|
268
|
+
onPresenceAccepted?: () => void;
|
|
269
|
+
socket: RemoteWorkspaceControlSocket;
|
|
270
|
+
}) {
|
|
271
|
+
this.currentCapabilities = parseRemoteWorkspaceCapabilities(options.capabilities);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private currentCapabilities: RemoteWorkspaceCapability[];
|
|
275
|
+
|
|
276
|
+
async receive(raw: string | Uint8Array): Promise<void> {
|
|
277
|
+
if (this.closed) throw new Error("remote workspace agent connection is closed");
|
|
278
|
+
const message = parseRemoteWorkspaceHubMessage(raw);
|
|
279
|
+
if (message.type === "presence_ack") {
|
|
280
|
+
if (message.capabilities.some(capability => !this.currentCapabilities.includes(capability))) {
|
|
281
|
+
throw new Error("remote workspace Hub acknowledged different executor capabilities");
|
|
282
|
+
}
|
|
283
|
+
this.currentCapabilities = [...message.capabilities];
|
|
284
|
+
this.options.onPresenceAccepted?.();
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
if (message.type === "session_open") {
|
|
288
|
+
let endpoint: EncryptedRemoteWorkspaceExecutorEndpoint | null = null;
|
|
289
|
+
try {
|
|
290
|
+
if (this.sessions.has(message.clientHello.sessionId)) {
|
|
291
|
+
throw new Error("remote workspace executor session already exists");
|
|
292
|
+
}
|
|
293
|
+
if (this.sessions.size >= REMOTE_CONTROL_MAX_SESSIONS_PER_DEVICE) {
|
|
294
|
+
throw new Error("remote workspace executor session limit reached");
|
|
295
|
+
}
|
|
296
|
+
if (message.clientHello.deviceId !== this.options.deviceId) {
|
|
297
|
+
throw new Error("remote workspace session targeted another executor");
|
|
298
|
+
}
|
|
299
|
+
if (!this.options.executor.hasApprovedRoot(message.rootId)) {
|
|
300
|
+
throw new Error("remote workspace root is not approved");
|
|
301
|
+
}
|
|
302
|
+
const accepted = acceptRemoteControlClientHello(message.clientHello, {
|
|
303
|
+
expectedSessionId: message.clientHello.sessionId,
|
|
304
|
+
expectedDeviceId: this.options.deviceId,
|
|
305
|
+
accountPublicKey: this.options.hubPublicKey,
|
|
306
|
+
devicePrivateKey: this.options.deviceIdentity.privateKey,
|
|
307
|
+
allowedCapabilities: this.currentCapabilities,
|
|
308
|
+
});
|
|
309
|
+
endpoint = new EncryptedRemoteWorkspaceExecutorEndpoint({
|
|
310
|
+
executorDeviceId: this.options.deviceId,
|
|
311
|
+
sessionId: message.clientHello.sessionId,
|
|
312
|
+
rootId: message.rootId,
|
|
313
|
+
capabilities: parseRemoteWorkspaceCapabilities(accepted.hello.capabilities),
|
|
314
|
+
cipher: accepted.cipher,
|
|
315
|
+
executor: this.options.executor,
|
|
316
|
+
sendCiphertext: value => this.options.socket.send(serializeRemoteWorkspaceAgentMessage({
|
|
317
|
+
version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
|
|
318
|
+
type: "ciphertext",
|
|
319
|
+
sessionId: message.clientHello.sessionId,
|
|
320
|
+
payload: value,
|
|
321
|
+
})),
|
|
322
|
+
});
|
|
323
|
+
this.sessions.set(message.clientHello.sessionId, endpoint);
|
|
324
|
+
await this.options.socket.send(serializeRemoteWorkspaceAgentMessage({
|
|
325
|
+
version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
|
|
326
|
+
type: "session_accept",
|
|
327
|
+
sessionId: message.clientHello.sessionId,
|
|
328
|
+
hostHello: accepted.hello,
|
|
329
|
+
}));
|
|
330
|
+
} catch (error) {
|
|
331
|
+
if (endpoint) {
|
|
332
|
+
this.sessions.delete(message.clientHello.sessionId);
|
|
333
|
+
endpoint.close();
|
|
334
|
+
}
|
|
335
|
+
await this.options.socket.send(serializeRemoteWorkspaceAgentMessage({
|
|
336
|
+
version: REMOTE_WORKSPACE_AGENT_PROTOCOL_VERSION,
|
|
337
|
+
type: "session_reject",
|
|
338
|
+
sessionId: message.clientHello.sessionId,
|
|
339
|
+
reason: safeReason(error instanceof Error ? error.message : "remote workspace session refused"),
|
|
340
|
+
}));
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
if (message.type === "session_close") {
|
|
345
|
+
this.sessions.get(message.sessionId)?.close();
|
|
346
|
+
this.sessions.delete(message.sessionId);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
const endpoint = this.sessions.get(message.sessionId);
|
|
350
|
+
if (!endpoint) throw new Error("remote workspace ciphertext targeted an unknown executor session");
|
|
351
|
+
// Decryption and counter validation happen synchronously before this returns. The execution
|
|
352
|
+
// promise is intentionally detached so an unencrypted session_close control frame can abort a
|
|
353
|
+
// long-running command instead of waiting behind that command on the socket's ordered queue.
|
|
354
|
+
void endpoint.receiveCiphertext(message.payload).catch(() => {
|
|
355
|
+
this.close();
|
|
356
|
+
this.options.socket.close(1008, "remote workspace protocol error");
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
close(): void {
|
|
361
|
+
if (this.closed) return;
|
|
362
|
+
this.closed = true;
|
|
363
|
+
for (const endpoint of this.sessions.values()) endpoint.close();
|
|
364
|
+
this.sessions.clear();
|
|
365
|
+
}
|
|
366
|
+
}
|
|
@@ -0,0 +1,243 @@
|
|
|
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 { remoteWorkspaceDeveloperInstructions } from "./workspace-tools";
|
|
6
|
+
import { findExecutableOnPath } from "./workspace-executable";
|
|
7
|
+
import {
|
|
8
|
+
remoteWorkspaceProcessInvocation,
|
|
9
|
+
removeRemoteWorkspaceIsolation,
|
|
10
|
+
runRemoteWorkspaceCleanupSteps,
|
|
11
|
+
stopRemoteWorkspaceProcess,
|
|
12
|
+
} from "./workspace-process";
|
|
13
|
+
import { startRemoteWorkspaceToolBridge } from "./workspace-tool-bridge";
|
|
14
|
+
import type {
|
|
15
|
+
RemoteWorkspaceRuntimeFactory,
|
|
16
|
+
RemoteWorkspaceRuntimeHandle,
|
|
17
|
+
} from "./workspace-sessions";
|
|
18
|
+
|
|
19
|
+
const MAX_OUTPUT_LINE_BYTES = 2 * 1024 * 1024;
|
|
20
|
+
const MAX_STDERR_BYTES = 64 * 1024;
|
|
21
|
+
|
|
22
|
+
function safeError(value: unknown, fallback: string): string {
|
|
23
|
+
return (value instanceof Error ? value.message : typeof value === "string" ? value : fallback)
|
|
24
|
+
.replace(/[^\x20-\x7e\n\t]/g, " ")
|
|
25
|
+
.slice(0, 4_096);
|
|
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 assistantText(value: unknown): string | null {
|
|
35
|
+
const message = record(value);
|
|
36
|
+
if (!message || !Array.isArray(message.content)) return null;
|
|
37
|
+
const text = message.content.flatMap(raw => {
|
|
38
|
+
const part = record(raw);
|
|
39
|
+
return part?.type === "text" && typeof part.text === "string" ? [part.text] : [];
|
|
40
|
+
}).join("");
|
|
41
|
+
return text || null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function drain(stream: ReadableStream<Uint8Array>): Promise<string> {
|
|
45
|
+
const reader = stream.getReader();
|
|
46
|
+
const chunks: Uint8Array[] = [];
|
|
47
|
+
let retained = 0;
|
|
48
|
+
try {
|
|
49
|
+
while (true) {
|
|
50
|
+
const next = await reader.read();
|
|
51
|
+
if (next.done) break;
|
|
52
|
+
if (retained >= MAX_STDERR_BYTES) continue;
|
|
53
|
+
const chunk = next.value.subarray(0, MAX_STDERR_BYTES - retained);
|
|
54
|
+
chunks.push(chunk);
|
|
55
|
+
retained += chunk.byteLength;
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
}
|
|
60
|
+
const merged = new Uint8Array(retained);
|
|
61
|
+
let offset = 0;
|
|
62
|
+
for (const chunk of chunks) { merged.set(chunk, offset); offset += chunk.byteLength; }
|
|
63
|
+
return new TextDecoder().decode(merged);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface ClaudeRemoteWorkspaceRuntimeOptions {
|
|
67
|
+
command?: readonly string[];
|
|
68
|
+
env?: Record<string, string | undefined>;
|
|
69
|
+
version?: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class ClaudeRemoteWorkspaceRuntimeFactory implements RemoteWorkspaceRuntimeFactory {
|
|
73
|
+
readonly profile = "claude" as const;
|
|
74
|
+
|
|
75
|
+
constructor(private readonly options: ClaudeRemoteWorkspaceRuntimeOptions = {}) {}
|
|
76
|
+
|
|
77
|
+
async available(): Promise<{ available: boolean; version?: string; reason?: string }> {
|
|
78
|
+
const command = this.options.command && this.options.command.length > 0
|
|
79
|
+
? this.options.command[0]
|
|
80
|
+
: findExecutableOnPath("claude");
|
|
81
|
+
return command
|
|
82
|
+
? { available: true, ...(this.options.version ? { version: this.options.version } : {}) }
|
|
83
|
+
: { available: false, reason: "Claude Code is not installed on this Hub." };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async start(options: Parameters<RemoteWorkspaceRuntimeFactory["start"]>[0]): Promise<RemoteWorkspaceRuntimeHandle> {
|
|
87
|
+
const configuredCommand = this.options.command && this.options.command.length > 0
|
|
88
|
+
? [...this.options.command]
|
|
89
|
+
: null;
|
|
90
|
+
const executable = configuredCommand?.[0] ?? findExecutableOnPath("claude");
|
|
91
|
+
if (!executable) throw new Error("Claude Code is not installed on this Hub");
|
|
92
|
+
const commandPrefix = configuredCommand ?? [executable];
|
|
93
|
+
const isolation = mkdtempSync(join(tmpdir(), "ocx-remote-claude-"));
|
|
94
|
+
try {
|
|
95
|
+
chmodSync(isolation, 0o700);
|
|
96
|
+
} catch (error) {
|
|
97
|
+
removeRemoteWorkspaceIsolation(isolation);
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
const threadId = options.resumeThreadId ?? randomUUID();
|
|
101
|
+
const bridge = (() => {
|
|
102
|
+
try {
|
|
103
|
+
return startRemoteWorkspaceToolBridge({
|
|
104
|
+
coordinator: options.coordinator,
|
|
105
|
+
threadId,
|
|
106
|
+
tools: options.tools,
|
|
107
|
+
onTool: tool => options.emit("tool", `Running ${tool} on ${options.deviceName}/${options.rootLabel}`),
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
removeRemoteWorkspaceIsolation(isolation);
|
|
111
|
+
throw error;
|
|
112
|
+
}
|
|
113
|
+
})();
|
|
114
|
+
const mcpPath = join(isolation, "mcp.json");
|
|
115
|
+
try {
|
|
116
|
+
writeFileSync(mcpPath, `${JSON.stringify({
|
|
117
|
+
mcpServers: {
|
|
118
|
+
ocx_remote_workspace: {
|
|
119
|
+
type: "http",
|
|
120
|
+
url: `${bridge.url}/mcp`,
|
|
121
|
+
headers: { Authorization: `Bearer ${bridge.token}` },
|
|
122
|
+
},
|
|
123
|
+
},
|
|
124
|
+
})}\n`, { mode: 0o600 });
|
|
125
|
+
} catch (error) {
|
|
126
|
+
await bridge.stop();
|
|
127
|
+
removeRemoteWorkspaceIsolation(isolation);
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
let firstTurn = options.resumeThreadId === undefined;
|
|
131
|
+
let active: Bun.Subprocess<"pipe", "pipe", "pipe"> | null = null;
|
|
132
|
+
let stopped = false;
|
|
133
|
+
let stopOperation: Promise<void> | null = null;
|
|
134
|
+
|
|
135
|
+
const runPrompt = async (text: string): Promise<void> => {
|
|
136
|
+
if (stopped) throw new Error("Claude Remote Workspace session is stopped");
|
|
137
|
+
if (active) throw new Error("Claude Remote Workspace turn is already active");
|
|
138
|
+
const args = [
|
|
139
|
+
...commandPrefix,
|
|
140
|
+
"-p",
|
|
141
|
+
"--input-format", "text",
|
|
142
|
+
"--output-format", "stream-json",
|
|
143
|
+
"--verbose",
|
|
144
|
+
"--strict-mcp-config",
|
|
145
|
+
"--mcp-config", mcpPath,
|
|
146
|
+
"--setting-sources", "",
|
|
147
|
+
"--tools", "",
|
|
148
|
+
"--allowedTools", "mcp__ocx_remote_workspace__*",
|
|
149
|
+
"--permission-mode", "dontAsk",
|
|
150
|
+
"--disable-slash-commands",
|
|
151
|
+
"--no-chrome",
|
|
152
|
+
"--system-prompt", remoteWorkspaceDeveloperInstructions(options.deviceName, options.tools),
|
|
153
|
+
firstTurn ? "--session-id" : "--resume",
|
|
154
|
+
threadId,
|
|
155
|
+
];
|
|
156
|
+
const childEnv = { ...process.env, ...this.options.env };
|
|
157
|
+
const invocation = remoteWorkspaceProcessInvocation(args, { env: childEnv });
|
|
158
|
+
const child = Bun.spawn([invocation.file, ...invocation.args], {
|
|
159
|
+
cwd: isolation,
|
|
160
|
+
env: childEnv,
|
|
161
|
+
stdin: "pipe",
|
|
162
|
+
stdout: "pipe",
|
|
163
|
+
stderr: "pipe",
|
|
164
|
+
...invocation.options,
|
|
165
|
+
});
|
|
166
|
+
active = child;
|
|
167
|
+
try {
|
|
168
|
+
child.stdin.write(text);
|
|
169
|
+
child.stdin.end();
|
|
170
|
+
} catch (error) {
|
|
171
|
+
await stopRemoteWorkspaceProcess(child);
|
|
172
|
+
if (active === child) active = null;
|
|
173
|
+
throw error;
|
|
174
|
+
}
|
|
175
|
+
const stderrPromise = drain(child.stderr);
|
|
176
|
+
const reader = child.stdout.getReader();
|
|
177
|
+
const decoder = new TextDecoder("utf-8", { fatal: true });
|
|
178
|
+
let buffer = "";
|
|
179
|
+
let emittedAssistant = false;
|
|
180
|
+
let resultError: string | null = null;
|
|
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_OUTPUT_LINE_BYTES && !buffer.includes("\n")) {
|
|
187
|
+
throw new Error("Claude Code 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_OUTPUT_LINE_BYTES) throw new Error("Claude Code output line is too large");
|
|
194
|
+
if (line) {
|
|
195
|
+
const event = record(JSON.parse(line));
|
|
196
|
+
if (event?.type === "assistant") {
|
|
197
|
+
const answer = assistantText(event.message);
|
|
198
|
+
if (answer) { options.emit("assistant", answer); emittedAssistant = true; }
|
|
199
|
+
}
|
|
200
|
+
if (event?.type === "result") {
|
|
201
|
+
if (event.is_error === true) resultError = safeError(event.result, "Claude Code turn failed");
|
|
202
|
+
else if (!emittedAssistant && typeof event.result === "string" && event.result) {
|
|
203
|
+
options.emit("assistant", event.result);
|
|
204
|
+
emittedAssistant = true;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
newline = buffer.indexOf("\n");
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const exitCode = await child.exited;
|
|
212
|
+
const stderr = await stderrPromise;
|
|
213
|
+
if (resultError) throw new Error(resultError);
|
|
214
|
+
if (exitCode !== 0) throw new Error(safeError(stderr, `Claude Code exited with code ${exitCode}`));
|
|
215
|
+
firstTurn = false;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
await stopRemoteWorkspaceProcess(child);
|
|
218
|
+
await stderrPromise.catch(() => "");
|
|
219
|
+
throw error;
|
|
220
|
+
} finally {
|
|
221
|
+
reader.releaseLock();
|
|
222
|
+
if (active === child) active = null;
|
|
223
|
+
}
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
threadId,
|
|
228
|
+
canResume: () => !firstTurn,
|
|
229
|
+
prompt: runPrompt,
|
|
230
|
+
stop(): Promise<void> {
|
|
231
|
+
if (stopOperation) return stopOperation;
|
|
232
|
+
stopped = true;
|
|
233
|
+
const child = active;
|
|
234
|
+
stopOperation = runRemoteWorkspaceCleanupSteps([
|
|
235
|
+
async () => { if (child) await stopRemoteWorkspaceProcess(child); },
|
|
236
|
+
() => bridge.stop(),
|
|
237
|
+
() => removeRemoteWorkspaceIsolation(isolation),
|
|
238
|
+
]);
|
|
239
|
+
return stopOperation;
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
}
|