@bitkyc08/opencodex 2.66.0-preview.20260925 → 2.67.0-preview.20260926
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/bin/ocx.mjs +24 -11
- package/gui/dist/assets/{App-BVjA0T6O.js → App-B3VRRYSj.js} +15 -15
- package/gui/dist/assets/{App-gkoJIEOV.css → App-BJsT8Icc.css} +1 -1
- package/gui/dist/assets/{Tray-BCFtiWwD.js → Tray-TYAA0EQY.js} +1 -1
- package/gui/dist/assets/index-9Ju7nHsu.js +86 -0
- package/gui/dist/assets/index-DtNmX7hW.css +1 -0
- package/gui/dist/assets/{tray-data-C4FiNaxk.js → tray-data-xKfS2gSG.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/chat/inbound.ts +82 -3
- package/src/chat/outbound.ts +5 -3
- package/src/claude/desktop-first-party.ts +12 -3
- package/src/claude/first-party-settings.ts +97 -0
- package/src/claude/intercept/client-class.ts +18 -0
- package/src/claude/intercept/listener.ts +5 -0
- package/src/claude/intercept/runtime.ts +6 -0
- package/src/cli/capabilities.ts +12 -0
- package/src/cli/claude-desktop.ts +11 -6
- package/src/cli/claude.ts +25 -1
- package/src/cli/ensure-desired-integrations.ts +14 -1
- package/src/cli/integrations.ts +9 -1
- package/src/cli/uninstall-client-state.ts +6 -0
- package/src/cli/uninstall-integrations.ts +115 -0
- package/src/codex/history-job.ts +2 -1
- package/src/codex/main-account-hard-lock.ts +36 -19
- package/src/codex/quota.ts +11 -1
- package/src/combos/resolve.ts +11 -3
- package/src/config/load-degrade.ts +4 -0
- package/src/generated/compatibility-version.json +67 -43
- package/src/generated/worker-bundles.gen.ts +4 -0
- package/src/images/loop.ts +10 -3
- package/src/integrations/aside-profile-context.ts +17 -0
- package/src/lab/fabric/executor.ts +25 -3
- package/src/lab/fabric/producer-isolate.ts +199 -32
- package/src/lab/fabric/scratch.ts +9 -2
- package/src/lib/shadow-call.ts +11 -4
- package/src/lib/worker-embed.ts +33 -0
- package/src/oauth/generic-account-failover.ts +5 -3
- package/src/oauth/index.ts +19 -29
- package/src/oauth/store.ts +106 -34
- package/src/remote-control/workspace-rpc.ts +114 -19
- package/src/router.ts +7 -1
- package/src/server/chat-completions.ts +2 -1
- package/src/server/index/claude-intercept-lifecycle.ts +13 -0
- package/src/server/management/agent-settings-routes.ts +123 -5
- package/src/server/management/context.ts +4 -0
- package/src/server/management/native-integration-routes.ts +19 -8
- package/src/server/responses/core-normalize.ts +13 -1
- package/src/server/responses/request-prepare.ts +5 -3
- package/src/server/responses/sidecar-execution.ts +9 -2
- package/src/server/search.ts +28 -7
- package/src/storage/policy-job.ts +2 -1
- package/src/storage/restore-job.ts +2 -1
- package/src/types/config.ts +11 -5
- package/src/update/transactional-install.mjs +5 -1
- package/src/web-search/alpha-search.ts +71 -0
- package/src/web-search/devin-executor.ts +192 -0
- package/src/web-search/loop.ts +10 -3
- package/gui/dist/assets/index-ComVsdSk.css +0 -1
- package/gui/dist/assets/index-vO4dgfiU.js +0 -86
package/src/oauth/store.ts
CHANGED
|
@@ -765,7 +765,7 @@ function serializeMutation<T>(work: () => Promise<T>, retainedValues: readonly u
|
|
|
765
765
|
drainOAuthMutations();
|
|
766
766
|
return result;
|
|
767
767
|
}
|
|
768
|
-
export function mutateStore<T>(fn:(store:AuthStore)=>T|Promise<T>, retainedValues: readonly unknown[] = [], options?: { waitMs?: number; assertBeforePersist?: () => void; scrubLegacyBackup?: (result: T) => readonly string[] }):Promise<T>{return serializeMutation(async()=>{const guard=await createOAuthFileLock({path:getAuthStoreLockPath(),staleAfterMs:30000}).acquire();try{
|
|
768
|
+
export function mutateStore<T>(fn:(store:AuthStore)=>T|Promise<T>, retainedValues: readonly unknown[] = [], options?: { waitMs?: number; assertBeforePersist?: () => void; scrubLegacyBackup?: (result: T) => readonly string[]; finalizeResult?: (result: T, store: AuthStore) => void }):Promise<T>{return serializeMutation(async()=>{const guard=await createOAuthFileLock({path:getAuthStoreLockPath(),staleAfterMs:30000}).acquire();try{
|
|
769
769
|
const { store, hadLegacy } = loadAuthStoreInternal();
|
|
770
770
|
if (hadLegacy) backupLegacyOnce();
|
|
771
771
|
const selections = new Map(Object.entries(store).map(([provider, set]) => [provider, {
|
|
@@ -798,6 +798,9 @@ export function mutateStore<T>(fn:(store:AuthStore)=>T|Promise<T>, retainedValue
|
|
|
798
798
|
changedProviders.push(provider);
|
|
799
799
|
}
|
|
800
800
|
}
|
|
801
|
+
// Receipt-producing mutations need the revision assigned by the bookkeeping above, not the
|
|
802
|
+
// provisional value visible inside their callback. Finalization cannot await or mutate disk.
|
|
803
|
+
options?.finalizeResult?.(result, store);
|
|
801
804
|
persist(store);
|
|
802
805
|
if (scrubbedProviders.length > 0) scrubLegacyBackup(scrubbedProviders);
|
|
803
806
|
for (const provider of changedProviders) publishAccountSelection(provider, "oauth");
|
|
@@ -819,63 +822,132 @@ export function getCredential(provider: string): OAuthCredentials | null {
|
|
|
819
822
|
* active slot / whole set instead. An explicit add-account login can preserve the legacy slot;
|
|
820
823
|
* an identity-less credential then gets its deterministic refresh-derived account id.
|
|
821
824
|
*/
|
|
822
|
-
export
|
|
825
|
+
export interface OAuthCredentialWriteReceipt {
|
|
826
|
+
provider: string;
|
|
827
|
+
accountId: string;
|
|
828
|
+
credentialGeneration: string;
|
|
829
|
+
selectionRevision: string | undefined;
|
|
830
|
+
previousActiveAccountId: string | undefined;
|
|
831
|
+
previousAccount: ProviderAccount | undefined;
|
|
832
|
+
}
|
|
833
|
+
|
|
834
|
+
export async function saveCredentialWithReceipt(
|
|
823
835
|
provider: string,
|
|
824
836
|
cred: OAuthCredentials,
|
|
825
837
|
opts: { preserveIdentityless?: boolean; assertBeforePersist?: () => void } = {},
|
|
826
|
-
): Promise<
|
|
838
|
+
): Promise<OAuthCredentialWriteReceipt | null> {
|
|
827
839
|
const safe = normalizeCredential(cred);
|
|
828
|
-
if (!safe) return;
|
|
829
|
-
await mutateStore(store => {
|
|
840
|
+
if (!safe) return null;
|
|
841
|
+
return await mutateStore(store => {
|
|
830
842
|
const set = store[provider];
|
|
843
|
+
const previousActiveAccountId = set?.activeAccountId;
|
|
844
|
+
const previousAccounts = new Map(
|
|
845
|
+
set?.accounts.map(account => [account.id, structuredClone(account)]) ?? [],
|
|
846
|
+
);
|
|
831
847
|
// Login explicitly selects an account, including a re-login to the same slot.
|
|
832
848
|
if (set) set.selectionRevision = randomUUID();
|
|
833
849
|
const identity = safe.accountId ?? safe.email;
|
|
850
|
+
let accountId: string;
|
|
834
851
|
if (!set || SINGLE_SLOT_PROVIDERS.has(provider)) {
|
|
835
852
|
const id = newAccountId(safe);
|
|
836
853
|
store[provider] = { activeAccountId: id, accounts: [{ id, credential: safe, addedAt: Date.now() }] };
|
|
837
|
-
|
|
838
|
-
}
|
|
839
|
-
if (identity) {
|
|
854
|
+
accountId = id;
|
|
855
|
+
} else if (identity) {
|
|
840
856
|
const existing = set.accounts.find(a => (a.credential.accountId ?? a.credential.email) === identity);
|
|
841
857
|
if (existing) {
|
|
842
858
|
existing.credential = safe;
|
|
843
859
|
delete existing.needsReauth;
|
|
844
860
|
set.activeAccountId = existing.id;
|
|
845
|
-
|
|
861
|
+
accountId = existing.id;
|
|
862
|
+
} else {
|
|
863
|
+
// Legacy migration: a pre-identity row (no accountId/email) for this provider is the
|
|
864
|
+
// SAME human re-logging in after the identity extraction shipped — upgrading the
|
|
865
|
+
// active identity-less row in place prevents a stale duplicate that stays selectable
|
|
866
|
+
// and would re-refresh into a second row with the same identity.
|
|
867
|
+
const active = set.accounts.find(a => a.id === set.activeAccountId);
|
|
868
|
+
if (!opts.preserveIdentityless && active && active.credential.accountId === undefined && active.credential.email === undefined) {
|
|
869
|
+
active.credential = safe;
|
|
870
|
+
delete active.needsReauth;
|
|
871
|
+
accountId = active.id;
|
|
872
|
+
} else {
|
|
873
|
+
const id = distinctAccountId(safe, set.accounts);
|
|
874
|
+
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
875
|
+
set.activeAccountId = id;
|
|
876
|
+
accountId = id;
|
|
877
|
+
}
|
|
846
878
|
}
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
879
|
+
} else if (opts.preserveIdentityless) {
|
|
880
|
+
const id = distinctAccountId(safe, set.accounts);
|
|
881
|
+
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
882
|
+
set.activeAccountId = id;
|
|
883
|
+
accountId = id;
|
|
884
|
+
} else {
|
|
885
|
+
// No identity during a normal login: replace the active slot in place.
|
|
851
886
|
const active = set.accounts.find(a => a.id === set.activeAccountId);
|
|
852
|
-
if (
|
|
887
|
+
if (active) {
|
|
853
888
|
active.credential = safe;
|
|
854
889
|
delete active.needsReauth;
|
|
855
|
-
|
|
890
|
+
accountId = active.id;
|
|
891
|
+
} else {
|
|
892
|
+
const id = distinctAccountId(safe, set.accounts);
|
|
893
|
+
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
894
|
+
set.activeAccountId = id;
|
|
895
|
+
accountId = id;
|
|
856
896
|
}
|
|
857
|
-
const id = distinctAccountId(safe, set.accounts);
|
|
858
|
-
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
859
|
-
set.activeAccountId = id;
|
|
860
|
-
return;
|
|
861
897
|
}
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
898
|
+
return {
|
|
899
|
+
provider,
|
|
900
|
+
accountId,
|
|
901
|
+
credentialGeneration: credentialGeneration(safe),
|
|
902
|
+
selectionRevision: store[provider]?.selectionRevision,
|
|
903
|
+
previousActiveAccountId,
|
|
904
|
+
previousAccount: previousAccounts.get(accountId),
|
|
905
|
+
};
|
|
906
|
+
}, [provider, safe], {
|
|
907
|
+
assertBeforePersist: opts.assertBeforePersist,
|
|
908
|
+
finalizeResult: (receipt, store) => {
|
|
909
|
+
receipt.selectionRevision = store[provider]?.selectionRevision;
|
|
910
|
+
},
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** Ordinary callers do not acquire rollback authority merely by saving a credential. */
|
|
915
|
+
export async function saveCredential(
|
|
916
|
+
provider: string,
|
|
917
|
+
cred: OAuthCredentials,
|
|
918
|
+
opts: { preserveIdentityless?: boolean; assertBeforePersist?: () => void } = {},
|
|
919
|
+
): Promise<void> {
|
|
920
|
+
await saveCredentialWithReceipt(provider, cred, opts);
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** Compensate one owned login write without deleting or selecting over concurrent work. */
|
|
924
|
+
export async function rollbackCredentialWriteIfMatch(
|
|
925
|
+
receipt: OAuthCredentialWriteReceipt,
|
|
926
|
+
): Promise<"rolled-back" | "stale"> {
|
|
927
|
+
return await mutateStore(store => {
|
|
928
|
+
const set = store[receipt.provider];
|
|
929
|
+
const index = set?.accounts.findIndex(account => account.id === receipt.accountId) ?? -1;
|
|
930
|
+
if (!set || index < 0) return "stale" as const;
|
|
931
|
+
const current = set.accounts[index]!;
|
|
932
|
+
if (credentialGeneration(current.credential) !== receipt.credentialGeneration) return "stale" as const;
|
|
933
|
+
// A later explicit selection of the same account owns that choice. Do not remove or rewrite
|
|
934
|
+
// the selected slot underneath it; the failed login can report failure without erasing newer state.
|
|
935
|
+
if (set.activeAccountId === receipt.accountId
|
|
936
|
+
&& set.selectionRevision !== receipt.selectionRevision) return "stale" as const;
|
|
937
|
+
|
|
938
|
+
if (receipt.previousAccount) set.accounts[index] = structuredClone(receipt.previousAccount);
|
|
939
|
+
else set.accounts.splice(index, 1);
|
|
940
|
+
if (set.accounts.length === 0) {
|
|
941
|
+
delete store[receipt.provider];
|
|
942
|
+
return "rolled-back" as const;
|
|
867
943
|
}
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
delete active.needsReauth;
|
|
873
|
-
} else {
|
|
874
|
-
const id = distinctAccountId(safe, set.accounts);
|
|
875
|
-
set.accounts.push({ id, credential: safe, addedAt: Date.now() });
|
|
876
|
-
set.activeAccountId = id;
|
|
944
|
+
if (set.activeAccountId === receipt.accountId) {
|
|
945
|
+
const previousStillExists = receipt.previousActiveAccountId
|
|
946
|
+
&& set.accounts.some(account => account.id === receipt.previousActiveAccountId);
|
|
947
|
+
set.activeAccountId = previousStillExists ? receipt.previousActiveAccountId! : set.accounts[0]!.id;
|
|
877
948
|
}
|
|
878
|
-
|
|
949
|
+
return "rolled-back" as const;
|
|
950
|
+
}, [receipt]);
|
|
879
951
|
}
|
|
880
952
|
|
|
881
953
|
/**
|
|
@@ -16,15 +16,30 @@ import {
|
|
|
16
16
|
frameRemoteWorkspaceRpcMessage,
|
|
17
17
|
} from "./workspace-rpc-framing";
|
|
18
18
|
|
|
19
|
-
|
|
20
|
-
const
|
|
19
|
+
// Old endpoints execute request frames immediately and cannot safely participate in prepare/grant.
|
|
20
|
+
const REMOTE_WORKSPACE_RPC_VERSION = 2 as const;
|
|
21
|
+
const REMOTE_WORKSPACE_RPC_DEFAULT_TIMEOUT_MS = 65_000;
|
|
22
|
+
const REMOTE_WORKSPACE_RPC_MAX_TIMEOUT_MS = 120_000;
|
|
21
23
|
const REMOTE_WORKSPACE_RPC_MAX_ACTIVE_REQUESTS = 8;
|
|
22
24
|
interface RemoteWorkspaceRpcRequest {
|
|
23
25
|
version: typeof REMOTE_WORKSPACE_RPC_VERSION;
|
|
24
|
-
kind: "
|
|
26
|
+
kind: "prepare";
|
|
27
|
+
timeoutMs: number;
|
|
25
28
|
request: RemoteWorkspaceExecutionRequest;
|
|
26
29
|
}
|
|
27
30
|
|
|
31
|
+
interface RemoteWorkspaceRpcCancel {
|
|
32
|
+
version: typeof REMOTE_WORKSPACE_RPC_VERSION;
|
|
33
|
+
kind: "cancel";
|
|
34
|
+
requestId: string;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
interface RemoteWorkspaceRpcGrant {
|
|
38
|
+
version: typeof REMOTE_WORKSPACE_RPC_VERSION;
|
|
39
|
+
kind: "grant";
|
|
40
|
+
requestId: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
28
43
|
interface RemoteWorkspaceRpcResponse {
|
|
29
44
|
version: typeof REMOTE_WORKSPACE_RPC_VERSION;
|
|
30
45
|
kind: "response";
|
|
@@ -32,7 +47,7 @@ interface RemoteWorkspaceRpcResponse {
|
|
|
32
47
|
result: RemoteWorkspaceToolResult;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
|
-
type RemoteWorkspaceRpcMessage = RemoteWorkspaceRpcRequest | RemoteWorkspaceRpcResponse;
|
|
50
|
+
type RemoteWorkspaceRpcMessage = RemoteWorkspaceRpcRequest | RemoteWorkspaceRpcResponse | RemoteWorkspaceRpcCancel | RemoteWorkspaceRpcGrant;
|
|
36
51
|
|
|
37
52
|
interface PendingRequest {
|
|
38
53
|
resolve(value: RemoteWorkspaceToolResult): void;
|
|
@@ -101,8 +116,14 @@ function parseMessage(value: Uint8Array): RemoteWorkspaceRpcMessage {
|
|
|
101
116
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error("invalid remote workspace RPC message");
|
|
102
117
|
const raw = parsed as Record<string, unknown>;
|
|
103
118
|
if (raw.version !== REMOTE_WORKSPACE_RPC_VERSION) throw new Error("unsupported remote workspace RPC version");
|
|
104
|
-
if (raw.kind === "
|
|
105
|
-
|
|
119
|
+
if (raw.kind === "prepare" && Number.isSafeInteger(raw.timeoutMs)
|
|
120
|
+
&& (raw.timeoutMs as number) >= 1 && (raw.timeoutMs as number) <= REMOTE_WORKSPACE_RPC_MAX_TIMEOUT_MS) {
|
|
121
|
+
return {
|
|
122
|
+
version: REMOTE_WORKSPACE_RPC_VERSION,
|
|
123
|
+
kind: "prepare",
|
|
124
|
+
timeoutMs: raw.timeoutMs as number,
|
|
125
|
+
request: parseRequest(raw.request),
|
|
126
|
+
};
|
|
106
127
|
}
|
|
107
128
|
if (raw.kind === "response" && boundedIdentifier(raw.requestId)) {
|
|
108
129
|
return {
|
|
@@ -112,6 +133,9 @@ function parseMessage(value: Uint8Array): RemoteWorkspaceRpcMessage {
|
|
|
112
133
|
result: parseResult(raw.result),
|
|
113
134
|
};
|
|
114
135
|
}
|
|
136
|
+
if ((raw.kind === "cancel" || raw.kind === "grant") && boundedIdentifier(raw.requestId)) {
|
|
137
|
+
return { version: REMOTE_WORKSPACE_RPC_VERSION, kind: raw.kind, requestId: raw.requestId };
|
|
138
|
+
}
|
|
115
139
|
throw new Error("invalid remote workspace RPC message kind");
|
|
116
140
|
}
|
|
117
141
|
|
|
@@ -132,7 +156,8 @@ export class EncryptedRemoteWorkspaceTransport implements RemoteWorkspaceTranspo
|
|
|
132
156
|
|
|
133
157
|
constructor(private readonly options: EncryptedRemoteWorkspaceTransportOptions) {
|
|
134
158
|
this.timeoutMs = options.timeoutMs ?? REMOTE_WORKSPACE_RPC_DEFAULT_TIMEOUT_MS;
|
|
135
|
-
if (!boundedIdentifier(options.executorDeviceId) || !Number.isSafeInteger(this.timeoutMs)
|
|
159
|
+
if (!boundedIdentifier(options.executorDeviceId) || !Number.isSafeInteger(this.timeoutMs)
|
|
160
|
+
|| this.timeoutMs < 1 || this.timeoutMs > REMOTE_WORKSPACE_RPC_MAX_TIMEOUT_MS) {
|
|
136
161
|
throw new Error("invalid encrypted remote workspace transport options");
|
|
137
162
|
}
|
|
138
163
|
}
|
|
@@ -150,22 +175,38 @@ export class EncryptedRemoteWorkspaceTransport implements RemoteWorkspaceTranspo
|
|
|
150
175
|
const response = new Promise<RemoteWorkspaceToolResult>((resolve, reject) => {
|
|
151
176
|
const timer = setTimeout(() => {
|
|
152
177
|
this.pending.delete(request.requestId);
|
|
153
|
-
|
|
178
|
+
void this.sendCancellation(request.requestId);
|
|
179
|
+
reject(new Error("remote workspace request timed out; executor cancellation was requested"));
|
|
154
180
|
}, this.timeoutMs);
|
|
155
181
|
this.pending.set(request.requestId, { resolve, reject, timer });
|
|
156
182
|
});
|
|
183
|
+
const pending = this.pending.get(request.requestId)!;
|
|
184
|
+
// Observe the response immediately: transport backpressure must not defer timeout delivery
|
|
185
|
+
// or leave a rejected response promise unobserved while a write is still waiting to settle.
|
|
186
|
+
void this.prepareAndGrant(request, pending);
|
|
187
|
+
return await response;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
private async prepareAndGrant(request: RemoteWorkspaceExecutionRequest, pending: PendingRequest): Promise<void> {
|
|
157
191
|
try {
|
|
158
192
|
await this.sendMessage(encodeMessage({
|
|
159
193
|
version: REMOTE_WORKSPACE_RPC_VERSION,
|
|
160
|
-
kind: "
|
|
194
|
+
kind: "prepare",
|
|
195
|
+
timeoutMs: this.timeoutMs,
|
|
161
196
|
request,
|
|
162
197
|
}));
|
|
198
|
+
// Check again inside the serialized send queue: another write can delay this grant after
|
|
199
|
+
// prepare has settled. A request that has already timed out must never receive a grant.
|
|
200
|
+
await this.sendMessage(encodeMessage({
|
|
201
|
+
version: REMOTE_WORKSPACE_RPC_VERSION,
|
|
202
|
+
kind: "grant",
|
|
203
|
+
requestId: request.requestId,
|
|
204
|
+
}), () => this.pending.get(request.requestId) === pending);
|
|
163
205
|
} catch {
|
|
164
206
|
// A failed encrypted write consumes a directional counter. Continuing would make every
|
|
165
207
|
// later frame undecryptable, so fail every pending operation instead of waiting for timeout.
|
|
166
208
|
this.close("remote workspace send failed");
|
|
167
209
|
}
|
|
168
|
-
return await response;
|
|
169
210
|
}
|
|
170
211
|
|
|
171
212
|
receiveCiphertext(value: Uint8Array): void {
|
|
@@ -193,8 +234,9 @@ export class EncryptedRemoteWorkspaceTransport implements RemoteWorkspaceTranspo
|
|
|
193
234
|
this.pending.clear();
|
|
194
235
|
}
|
|
195
236
|
|
|
196
|
-
private sendMessage(message: Uint8Array): Promise<void> {
|
|
237
|
+
private sendMessage(message: Uint8Array, shouldSend: () => boolean = () => true): Promise<void> {
|
|
197
238
|
const operation = this.sendTail.then(async () => {
|
|
239
|
+
if (!shouldSend()) return;
|
|
198
240
|
if (!this.online) throw new Error("remote workspace transport is closed");
|
|
199
241
|
for (const frame of frameRemoteWorkspaceRpcMessage(message)) {
|
|
200
242
|
await this.options.sendCiphertext(this.options.cipher.encrypt(frame));
|
|
@@ -203,6 +245,19 @@ export class EncryptedRemoteWorkspaceTransport implements RemoteWorkspaceTranspo
|
|
|
203
245
|
this.sendTail = operation.catch(() => {});
|
|
204
246
|
return operation;
|
|
205
247
|
}
|
|
248
|
+
|
|
249
|
+
private async sendCancellation(requestId: string): Promise<void> {
|
|
250
|
+
try {
|
|
251
|
+
await this.sendMessage(encodeMessage({
|
|
252
|
+
version: REMOTE_WORKSPACE_RPC_VERSION,
|
|
253
|
+
kind: "cancel",
|
|
254
|
+
requestId,
|
|
255
|
+
}));
|
|
256
|
+
} catch {
|
|
257
|
+
// A failed encrypted write consumes the send counter; the session cannot safely continue.
|
|
258
|
+
this.close("remote workspace cancellation send failed");
|
|
259
|
+
}
|
|
260
|
+
}
|
|
206
261
|
}
|
|
207
262
|
|
|
208
263
|
export interface EncryptedRemoteWorkspaceExecutorEndpointOptions {
|
|
@@ -218,7 +273,12 @@ export interface EncryptedRemoteWorkspaceExecutorEndpointOptions {
|
|
|
218
273
|
/** Executor-side endpoint. It accepts only authenticated, ordered E2EE session frames. */
|
|
219
274
|
export class EncryptedRemoteWorkspaceExecutorEndpoint {
|
|
220
275
|
private closed = false;
|
|
221
|
-
private readonly active = new Map<string,
|
|
276
|
+
private readonly active = new Map<string, {
|
|
277
|
+
controller: AbortController;
|
|
278
|
+
timer: ReturnType<typeof setTimeout>;
|
|
279
|
+
request: RemoteWorkspaceExecutionRequest;
|
|
280
|
+
started: boolean;
|
|
281
|
+
}>();
|
|
222
282
|
private readonly reassembler = new RemoteWorkspaceRpcReassembler();
|
|
223
283
|
private sendTail: Promise<void> = Promise.resolve();
|
|
224
284
|
|
|
@@ -240,7 +300,26 @@ export class EncryptedRemoteWorkspaceExecutorEndpoint {
|
|
|
240
300
|
const requestPlaintext = this.reassembler.accept(this.options.cipher.decrypt(value));
|
|
241
301
|
if (!requestPlaintext) return;
|
|
242
302
|
const message = parseMessage(requestPlaintext);
|
|
243
|
-
if (message.kind
|
|
303
|
+
if (message.kind === "cancel") {
|
|
304
|
+
const active = this.active.get(message.requestId);
|
|
305
|
+
if (active) {
|
|
306
|
+
active.controller.abort();
|
|
307
|
+
if (!active.started) {
|
|
308
|
+
clearTimeout(active.timer);
|
|
309
|
+
this.active.delete(message.requestId);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (message.kind === "grant") {
|
|
315
|
+
const active = this.active.get(message.requestId);
|
|
316
|
+
if (!active || active.controller.signal.aborted) return;
|
|
317
|
+
if (active.started) throw new Error("duplicate remote workspace execution grant");
|
|
318
|
+
active.started = true;
|
|
319
|
+
await this.executeGranted(active.request, active.controller, active.timer);
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
if (message.kind !== "prepare") throw new Error("executor received a remote workspace response");
|
|
244
323
|
if (message.request.executorDeviceId !== this.options.executorDeviceId) {
|
|
245
324
|
throw new Error("remote workspace encrypted request targeted another executor");
|
|
246
325
|
}
|
|
@@ -255,12 +334,25 @@ export class EncryptedRemoteWorkspaceExecutorEndpoint {
|
|
|
255
334
|
throw new Error("remote workspace executor request limit reached");
|
|
256
335
|
}
|
|
257
336
|
const controller = new AbortController();
|
|
258
|
-
|
|
337
|
+
const timer = setTimeout(() => {
|
|
338
|
+
controller.abort();
|
|
339
|
+
const active = this.active.get(message.request.requestId);
|
|
340
|
+
if (active && !active.started) this.active.delete(message.request.requestId);
|
|
341
|
+
}, message.timeoutMs);
|
|
342
|
+
this.active.set(message.request.requestId, { controller, timer, request: message.request, started: false });
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
private async executeGranted(
|
|
346
|
+
request: RemoteWorkspaceExecutionRequest,
|
|
347
|
+
controller: AbortController,
|
|
348
|
+
timer: ReturnType<typeof setTimeout>,
|
|
349
|
+
): Promise<void> {
|
|
259
350
|
let result: RemoteWorkspaceToolResult;
|
|
260
351
|
try {
|
|
261
|
-
result = await this.options.executor.invoke(
|
|
352
|
+
result = await this.options.executor.invoke(request, controller.signal);
|
|
262
353
|
} finally {
|
|
263
|
-
|
|
354
|
+
clearTimeout(timer);
|
|
355
|
+
this.active.delete(request.requestId);
|
|
264
356
|
}
|
|
265
357
|
if (this.closed) return;
|
|
266
358
|
let responsePlaintext: Uint8Array;
|
|
@@ -268,14 +360,14 @@ export class EncryptedRemoteWorkspaceExecutorEndpoint {
|
|
|
268
360
|
responsePlaintext = encodeMessage({
|
|
269
361
|
version: REMOTE_WORKSPACE_RPC_VERSION,
|
|
270
362
|
kind: "response",
|
|
271
|
-
requestId:
|
|
363
|
+
requestId: request.requestId,
|
|
272
364
|
result,
|
|
273
365
|
});
|
|
274
366
|
} catch {
|
|
275
367
|
responsePlaintext = encodeMessage({
|
|
276
368
|
version: REMOTE_WORKSPACE_RPC_VERSION,
|
|
277
369
|
kind: "response",
|
|
278
|
-
requestId:
|
|
370
|
+
requestId: request.requestId,
|
|
279
371
|
result: { ok: false, error: "remote workspace result exceeded the encrypted frame limit" },
|
|
280
372
|
});
|
|
281
373
|
}
|
|
@@ -286,7 +378,10 @@ export class EncryptedRemoteWorkspaceExecutorEndpoint {
|
|
|
286
378
|
if (this.closed) return;
|
|
287
379
|
this.closed = true;
|
|
288
380
|
this.reassembler.clear();
|
|
289
|
-
for (const
|
|
381
|
+
for (const active of this.active.values()) {
|
|
382
|
+
clearTimeout(active.timer);
|
|
383
|
+
active.controller.abort();
|
|
384
|
+
}
|
|
290
385
|
this.active.clear();
|
|
291
386
|
this.options.cipher.destroy();
|
|
292
387
|
}
|
package/src/router.ts
CHANGED
|
@@ -627,6 +627,7 @@ function routeModelInternal(
|
|
|
627
627
|
bypassCombos: boolean,
|
|
628
628
|
policyEvidence?: PolicyRequestEvidence,
|
|
629
629
|
allowCompactionNativeFallback = false,
|
|
630
|
+
preview = false,
|
|
630
631
|
): RouteResult {
|
|
631
632
|
const slash = modelId.indexOf("/");
|
|
632
633
|
// Policy namespace is system-reserved: an explicit `policy/<id>` or a
|
|
@@ -694,7 +695,7 @@ function routeModelInternal(
|
|
|
694
695
|
}
|
|
695
696
|
|
|
696
697
|
if (!bypassCombos && !preservesPhysicalComboProvider(config)) {
|
|
697
|
-
const combo = tryPickComboModel(config, modelId);
|
|
698
|
+
const combo = tryPickComboModel(config, modelId, preview);
|
|
698
699
|
if (combo) {
|
|
699
700
|
const concrete = `${combo.target.provider}/${combo.target.model}`;
|
|
700
701
|
// The selected target is already a concrete provider/model reference. Resolve it without
|
|
@@ -894,6 +895,11 @@ export function routeModel(
|
|
|
894
895
|
return routeWithDecisionTrace(config, modelId, route);
|
|
895
896
|
}
|
|
896
897
|
|
|
898
|
+
/** Resolve a route for capability inspection without creating combo selection state. */
|
|
899
|
+
export function previewRouteModel(config: OcxConfig, modelId: string): RouteResult {
|
|
900
|
+
return routeWithDecisionTrace(config, modelId, routeModelInternal(config, modelId, false, undefined, false, true));
|
|
901
|
+
}
|
|
902
|
+
|
|
897
903
|
/**
|
|
898
904
|
* Route a client-selected compaction model. Codex may send a bare native model
|
|
899
905
|
* even when its ordinary turns are configured for another provider; in that
|
|
@@ -341,7 +341,8 @@ async function handleChatCompletionsWithBudget(
|
|
|
341
341
|
} else if (internalBody.store === undefined) {
|
|
342
342
|
internalBody.store = false;
|
|
343
343
|
}
|
|
344
|
-
if (settledRoute &&
|
|
344
|
+
if (settledRoute && !settledRoute.combo && settledRoute.routeKind !== "policy"
|
|
345
|
+
&& internalBody.reasoning !== undefined) {
|
|
345
346
|
const { stripEmptyLadderEffort, supportedLadderFor } = await import("./effort-policy");
|
|
346
347
|
const ladder = supportedLadderFor({ provider: settledRoute.provider, modelId: settledRoute.modelId });
|
|
347
348
|
const next = stripEmptyLadderEffort(internalBody.reasoning, ladder);
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import type { Server } from "bun";
|
|
2
|
+
import type { OcxConfig } from "../../types";
|
|
2
3
|
import type { PickerRouteInput } from "../../claude/intercept/picker-models";
|
|
4
|
+
import { observeClaudeDesktopMode, type ClaudeDesktopModeObservation } from "../../claude/desktop-first-party";
|
|
5
|
+
import { firstPartyDesired, type ClaudeFirstPartyDesired } from "../../claude/first-party-settings";
|
|
3
6
|
import {
|
|
7
|
+
claudeInterceptEnabled,
|
|
4
8
|
startClaudeIntercept,
|
|
5
9
|
type ClaudeInterceptHandle,
|
|
6
10
|
type StartClaudeInterceptOptions,
|
|
@@ -19,6 +23,12 @@ export interface ClaudeInterceptLifecycle<T> {
|
|
|
19
23
|
stop(): Promise<void>;
|
|
20
24
|
}
|
|
21
25
|
|
|
26
|
+
export function buildInterceptDesiredClients(config: OcxConfig, observed: ClaudeDesktopModeObservation): () => ClaudeFirstPartyDesired {
|
|
27
|
+
return () => claudeInterceptEnabled(config)
|
|
28
|
+
? firstPartyDesired(config, observed)
|
|
29
|
+
: { desktop: false, cli: false };
|
|
30
|
+
}
|
|
31
|
+
|
|
22
32
|
/**
|
|
23
33
|
* Routes for Desktop's Code-tab picker: the same inputs `/api/sync` gives the gateway profile
|
|
24
34
|
* (src/server/management/config-routes.ts), read from the persisted config at call time. Dynamic
|
|
@@ -48,8 +58,11 @@ export function createClaudeInterceptLifecycle<T>(): ClaudeInterceptLifecycle<T>
|
|
|
48
58
|
ownsListener: requestServer => listener !== null && requestServer === listener,
|
|
49
59
|
start(options) {
|
|
50
60
|
const dispatch = options.dispatch;
|
|
61
|
+
const observed = observeClaudeDesktopMode(options.config);
|
|
51
62
|
pending = startClaudeIntercept<T>({
|
|
52
63
|
...options,
|
|
64
|
+
// A disabled Claude surface relays everything while the bound listener lives until restart.
|
|
65
|
+
desiredClients: buildInterceptDesiredClients(options.config, observed),
|
|
53
66
|
loadPickerRoutes: options.loadPickerRoutes ?? loadPickerRoutesFromCatalog,
|
|
54
67
|
dispatch: (req, requestServer) => {
|
|
55
68
|
listener ??= requestServer;
|