@bitkyc08/opencodex 2.84.0 → 2.86.0
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/README.md +0 -4
- package/bin/ocx.mjs +61 -3
- package/gui/dist/assets/App-BjlttVLy.js +58 -0
- package/gui/dist/assets/{Tray-C9TGtwad.js → Tray-DyR9yBee.js} +1 -1
- package/gui/dist/assets/index-BMssP4k3.js +94 -0
- package/gui/dist/assets/quota-summary-D8M1bc_R.js +1 -0
- package/gui/dist/index.html +1 -1
- package/package.json +1 -2
- package/src/adapters/anthropic/beta-allowlist.ts +87 -8
- package/src/adapters/anthropic/client-identity.ts +1 -1
- package/src/adapters/anthropic/passthrough.ts +33 -4
- package/src/adapters/azure.ts +19 -1
- package/src/adapters/client-fingerprint.ts +24 -0
- package/src/adapters/coding-agent/turn.ts +22 -9
- package/src/adapters/cursor/arg-codec.ts +17 -5
- package/src/adapters/cursor/catalog.ts +8 -0
- package/src/adapters/cursor/effort-map.ts +3 -0
- package/src/adapters/cursor/freeform-args.ts +75 -0
- package/src/adapters/cursor/protobuf-events.ts +36 -23
- package/src/adapters/google-antigravity-wire.ts +22 -1
- package/src/adapters/google.ts +2 -2
- package/src/adapters/openai-chat/response-events.ts +59 -1
- package/src/adapters/openai-chat.ts +8 -5
- package/src/adapters/qoder/adapter.ts +40 -1
- package/src/advisor/consult.ts +220 -0
- package/src/advisor/context.ts +226 -0
- package/src/advisor/disclosure.ts +20 -0
- package/src/advisor/runtime.ts +305 -0
- package/src/advisor/settings.ts +135 -0
- package/src/advisor/state.ts +369 -0
- package/src/advisor/synthetic-tool.ts +33 -0
- package/src/bridge/errors.ts +5 -0
- package/src/chat/inbound.ts +30 -6
- package/src/chat/outbound.ts +5 -0
- package/src/claude/desktop-3p-library.ts +43 -3
- package/src/claude/desktop-3p.ts +22 -8
- package/src/claude/desktop-picker-profile.ts +11 -4
- package/src/claude/desktop-profile.ts +10 -1
- package/src/claude/desktop-remote-store-io.ts +5 -3
- package/src/claude/inbound.ts +121 -50
- package/src/claude/intercept/local-ca.ts +8 -1
- package/src/claude/model-default-settings.ts +154 -0
- package/src/claude/model-info.ts +7 -7
- package/src/claude/outbound.ts +13 -6
- package/src/cli/advisor.ts +151 -0
- package/src/cli/auxiliary-listener-preflight.ts +37 -0
- package/src/cli/capabilities-agents-routing.ts +18 -0
- package/src/cli/capabilities-base.ts +1 -1
- package/src/cli/chatgpt-command.ts +35 -14
- package/src/cli/claude.ts +46 -6
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +9 -0
- package/src/cli/ensure-desktop-cold-start.ts +329 -0
- package/src/cli/export-command.ts +1 -1
- package/src/cli/help.ts +2 -1
- package/src/cli/index.ts +13 -4
- package/src/cli/integration-plan-dto.ts +2 -0
- package/src/cli/registry.ts +15 -3
- package/src/cli/restart-scope.ts +3 -1
- package/src/clients/config-export/contracts.ts +1 -0
- package/src/clients/config-export/model-metadata.ts +3 -2
- package/src/clients/config-export/qoder.ts +93 -0
- package/src/clients/config-export.ts +17 -4
- package/src/codex/auth-api/pool-mode-gate.ts +5 -2
- package/src/codex/auth-context.ts +79 -5
- package/src/codex/autostart-health.ts +4 -0
- package/src/codex/catalog/build-entries.ts +11 -2
- package/src/codex/catalog/configured-tiers.ts +50 -0
- package/src/codex/catalog/effort.ts +4 -1
- package/src/codex/catalog/model-hints.ts +21 -3
- package/src/codex/catalog/native-ultrafast.ts +106 -0
- package/src/codex/catalog/parsing.ts +5 -0
- package/src/codex/catalog/provider-models.ts +4 -0
- package/src/codex/catalog/retained-sync.ts +2 -0
- package/src/codex/catalog/routed-gather.ts +22 -4
- package/src/codex/catalog/vendor-context.ts +82 -0
- package/src/codex/config-write-lock.ts +40 -2
- package/src/codex/convergence.ts +2 -0
- package/src/codex/desktop-app/types.ts +2 -0
- package/src/codex/desktop-app/windows.ts +29 -16
- package/src/codex/desktop-app-restart.ts +4 -2
- package/src/codex/desktop-compatibility/certificate-service.ts +99 -0
- package/src/codex/desktop-compatibility/certificate-store.ts +193 -0
- package/src/codex/desktop-compatibility/connection-store.ts +87 -0
- package/src/codex/desktop-compatibility/installed-build.ts +55 -0
- package/src/codex/desktop-compatibility/json-body.ts +29 -0
- package/src/codex/desktop-compatibility/native-identity.ts +62 -0
- package/src/codex/desktop-compatibility/relay-listener.ts +93 -0
- package/src/codex/desktop-compatibility/routing-binding.ts +10 -0
- package/src/codex/desktop-compatibility/routing-preflight.ts +69 -0
- package/src/codex/desktop-compatibility/runtime-ownership.ts +28 -0
- package/src/codex/desktop-compatibility/runtime.ts +173 -0
- package/src/codex/desktop-compatibility/service.ts +30 -0
- package/src/codex/desktop-compatibility/startup-settings.ts +50 -0
- package/src/codex/desktop-compatibility/usage-activation.ts +86 -0
- package/src/codex/desktop-compatibility/usage-controlled-fetch.ts +59 -0
- package/src/codex/desktop-compatibility/usage-controller.ts +107 -0
- package/src/codex/desktop-compatibility/usage-policy.ts +59 -0
- package/src/codex/desktop-compatibility/usage-refresh.ts +33 -0
- package/src/codex/desktop-compatibility/usage-sse-controller.ts +86 -0
- package/src/codex/desktop-compatibility/windows-activation-source.ts +44 -0
- package/src/codex/desktop-compatibility/windows-certificate-trust.ts +81 -0
- package/src/codex/desktop-compatibility/windows-key-protection.ts +59 -0
- package/src/codex/desktop-compatibility/windows-package-command.ts +86 -0
- package/src/codex/desktop-compatibility/windows-package-launch.ts +28 -0
- package/src/codex/history-provider.ts +1 -1
- package/src/codex/inject/config-write-section.ts +4 -1
- package/src/codex/inject/remove.ts +18 -6
- package/src/codex/journal.ts +16 -1
- package/src/codex/main-account.ts +99 -15
- package/src/codex/native-main-refresh-mode.ts +217 -0
- package/src/codex/routing/health-store.ts +1 -1
- package/src/codex/routing/pool-recovery.ts +79 -0
- package/src/codex/shim-diagnostics.ts +6 -0
- package/src/codex/shim-overlay.ts +7 -2
- package/src/config/diagnostics.ts +2 -1
- package/src/config/live-reconcile.ts +13 -0
- package/src/config/load-degrade.ts +6 -1
- package/src/config/paths.ts +10 -3
- package/src/config/schema/config-schema.ts +17 -0
- package/src/config/schema/desktop-compatibility.ts +9 -0
- package/src/config/schema/leaf-validators.ts +1 -0
- package/src/generated/compatibility-version.json +451 -163
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/catalog-refresh.ts +1 -1
- package/src/integrations/config-io.ts +20 -2
- package/src/integrations/journal.ts +15 -1
- package/src/integrations/mutation-plan.ts +1 -0
- package/src/integrations/registry.ts +7 -0
- package/src/integrations/store.ts +3 -0
- package/src/integrations/writer.ts +10 -1
- package/src/lib/advisor-activation.ts +16 -0
- package/src/lib/background-work.ts +65 -0
- package/src/lib/bounded-file-read.ts +58 -0
- package/src/lib/bun-path-runtime.d.mts +10 -1
- package/src/lib/bun-path-runtime.mjs +62 -3
- package/src/lib/desktop-proxy-route.ts +30 -0
- package/src/lib/desktop-upstream-tunnel.ts +107 -0
- package/src/lib/errors.ts +7 -0
- package/src/lib/local-internal-call-capability.ts +55 -0
- package/src/lib/package-tree-integrity.ts +2 -2
- package/src/lib/socks5-fetch.ts +48 -71
- package/src/lib/socks5-handshake.ts +112 -0
- package/src/lib/translator-budget.ts +7 -1
- package/src/oauth/generic-account-failover.ts +29 -5
- package/src/oauth/index.ts +4 -2
- package/src/oauth/kiro-device-login.ts +2 -1
- package/src/oauth/kiro.ts +7 -1
- package/src/protocols/codecs/chat-ir.ts +467 -0
- package/src/protocols/codecs/chat.ts +10 -1
- package/src/protocols/codecs/messages-ir.ts +500 -0
- package/src/protocols/features.ts +10 -2
- package/src/protocols/path.ts +11 -1
- package/src/protocols/plan-snapshot.ts +39 -16
- package/src/protocols/plan.ts +9 -1
- package/src/protocols/settings.ts +8 -0
- package/src/providers/antigravity-account-roster.ts +102 -0
- package/src/providers/antigravity-models.ts +85 -7
- package/src/providers/model-rename-fields.ts +2 -0
- package/src/providers/model-rename-migration.ts +5 -3
- package/src/providers/registry/entries-core.ts +7 -6
- package/src/providers/registry/entries-extended.ts +19 -0
- package/src/responses/parser.ts +30 -19
- package/src/responses/state.ts +32 -29
- package/src/routing/history/build-file.ts +92 -0
- package/src/routing/history/indexer.ts +248 -216
- package/src/routing/history/ingest.ts +238 -0
- package/src/routing/history/rebuild-job.ts +111 -0
- package/src/routing/history/rebuild-protocol.ts +15 -0
- package/src/routing/history/rebuild-worker.ts +34 -0
- package/src/server/admission-model-scope.ts +4 -1
- package/src/server/antigravity-direct-eligibility.ts +208 -0
- package/src/server/auth-cors.ts +4 -0
- package/src/server/chat-completions.ts +75 -9
- package/src/server/claude-messages.ts +110 -20
- package/src/server/index/desktop-compatibility-startup.ts +56 -0
- package/src/server/index/serve-options.ts +2 -2
- package/src/server/index/startup-warnings.ts +9 -0
- package/src/server/index/websocket-handler.ts +9 -0
- package/src/server/index.ts +13 -10
- package/src/server/inference/client-encoder-delivery.ts +6 -5
- package/src/server/login-shell-path.ts +146 -0
- package/src/server/management/advisor-routes.ts +191 -0
- package/src/server/management/agent-settings-routes.ts +22 -4
- package/src/server/management/companion-routes.ts +4 -0
- package/src/server/management/config-routes.ts +2 -2
- package/src/server/management/context.ts +4 -0
- package/src/server/management/desktop-compatibility-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-runtime-routes.ts +46 -0
- package/src/server/management/desktop-compatibility-settings-routes.ts +27 -0
- package/src/server/management/model-routes.ts +2 -2
- package/src/server/management/protocol-settings-patch.ts +1 -0
- package/src/server/management/route-registry.ts +11 -0
- package/src/server/management/sibling-guard.ts +1 -0
- package/src/server/management-api.ts +6 -0
- package/src/server/messages-antigravity-direct.ts +195 -0
- package/src/server/messages-native.ts +5 -1
- package/src/server/request-log.ts +7 -0
- package/src/server/responses/adapter-continuation.ts +35 -18
- package/src/server/responses/adapter-delivery.ts +24 -2
- package/src/server/responses/adapter-dispatch.ts +29 -2
- package/src/server/responses/advisor-plan-slot.ts +27 -0
- package/src/server/responses/advisor-slot.ts +336 -0
- package/src/server/responses/code-mode-collab-surface.ts +116 -8
- package/src/server/responses/core-combo-failure.ts +10 -1
- package/src/server/responses/core-combo.ts +21 -0
- package/src/server/responses/core-lifetime.ts +2 -0
- package/src/server/responses/core-normalize.ts +11 -0
- package/src/server/responses/core-options.ts +46 -0
- package/src/server/responses/core-replay.ts +7 -4
- package/src/server/responses/core.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +16 -10
- package/src/server/responses/passthrough-dispatch.ts +14 -3
- package/src/server/responses/request-prepare.ts +68 -5
- package/src/server/responses/request-transport.ts +16 -0
- package/src/server/responses/run-turn-execution.ts +70 -8
- package/src/server/responses/sidecar-execution.ts +78 -3
- package/src/server/responses/terminal-guard.ts +2 -1
- package/src/server/responses-undeclared-tool-guard.ts +30 -9
- package/src/server/startup-action-control.ts +16 -2
- package/src/server/startup-health-cache.ts +1 -0
- package/src/service/desktop-startup.ts +14 -0
- package/src/service/health-sentinel-main.ts +23 -4
- package/src/service/health-sentinel-probe.ts +24 -7
- package/src/service/logging.ts +72 -0
- package/src/tray/windows-tray.ps1 +43 -24
- package/src/types/config.ts +63 -6
- package/src/types/provider.ts +7 -0
- package/src/types/request.ts +15 -0
- package/src/types/tools.ts +2 -0
- package/src/update/async-check.ts +1 -1
- package/src/update/badge.ts +2 -2
- package/src/update/index.ts +88 -15
- package/src/update/job-supervised-restart.ts +107 -0
- package/src/update/job.ts +32 -32
- package/src/update/notify.ts +1 -1
- package/src/update/refresh-scheduler.ts +2 -2
- package/src/update/supervised-recovery.d.mts +27 -0
- package/src/update/supervised-recovery.mjs +157 -0
- package/src/usage/cache-diagnostic.ts +2 -0
- package/src/usage/expected-prices.ts +12 -0
- package/src/web-search/gemini-executor.ts +2 -2
- package/assets/sponsors/packycode.png +0 -0
- package/gui/dist/assets/App-CZtqiGcw.js +0 -56
- package/gui/dist/assets/index-CTm4l-HQ.js +0 -94
- package/gui/dist/assets/quota-summary-BbEeRfPY.js +0 -1
|
@@ -111,17 +111,18 @@ export interface ClientEncodedDelivery {
|
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
/**
|
|
114
|
-
* Record the attempt's observed path
|
|
115
|
-
*
|
|
116
|
-
*
|
|
114
|
+
* Record the attempt's observed path. The response reaches the client from the IR without the
|
|
115
|
+
* internal Responses hop. The request side is the bridge's unless the ingress also decoded the
|
|
116
|
+
* conversation straight into the IR (the Antigravity direct lane, `requestLane: "direct"`).
|
|
117
117
|
*/
|
|
118
118
|
function markDirectEncoderPath(logCtx: RequestLogContext, encoder: ClientEncoderOption, adapter: string): void {
|
|
119
119
|
const attempt = logCtx.activeAttempt;
|
|
120
120
|
if (!attempt) return;
|
|
121
121
|
const upstream = upstreamWireForAdapter(attempt.adapter || adapter);
|
|
122
|
+
const lane = encoder.requestLane ?? "bridge";
|
|
122
123
|
markAttemptProtocolPath(attempt, {
|
|
123
|
-
mode: deliveryModeForLane(encoder.protocol,
|
|
124
|
-
requestPath: requestPathForLane(encoder.protocol,
|
|
124
|
+
mode: deliveryModeForLane(encoder.protocol, lane, upstream),
|
|
125
|
+
requestPath: requestPathForLane(encoder.protocol, lane, upstream),
|
|
125
126
|
responsePath: [upstream, "ir", encoder.protocol],
|
|
126
127
|
});
|
|
127
128
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The user's login-shell PATH, for installers the dashboard runs under OpenCodex Desktop (#6971).
|
|
3
|
+
*
|
|
4
|
+
* A Finder or login launch gives the Desktop app, and so its proxy sidecar, only the system PATH
|
|
5
|
+
* (`/usr/bin:/bin:/usr/sbin:/sbin`). `ocx codex-shim install` scans PATH for the native Codex
|
|
6
|
+
* launcher, so from the dashboard it reported "Native Codex launcher not found" for Homebrew, npm,
|
|
7
|
+
* bun, nvm, fnm, volta and mise installs, and an npm launcher had no `node` for its probe.
|
|
8
|
+
*
|
|
9
|
+
* This asks the user's shell once, as an interactive login shell would start, and keeps only PATH:
|
|
10
|
+
* the value is framed by markers so startup-file output cannot leak into it. The read has a
|
|
11
|
+
* deadline, runs in its own process group so a hung startup file is killed with all of its
|
|
12
|
+
* children, and its result, including failure, is cached so repeated dashboard actions do not
|
|
13
|
+
* spawn shells. It is scoped to the shim installer's child environment; the service definition's
|
|
14
|
+
* PATH has its own policy.
|
|
15
|
+
*/
|
|
16
|
+
import { spawn as nodeSpawn, type ChildProcess } from "node:child_process";
|
|
17
|
+
import { userInfo } from "node:os";
|
|
18
|
+
import { basename, isAbsolute } from "node:path";
|
|
19
|
+
|
|
20
|
+
const BEGIN = "__OCX_LOGIN_PATH_BEGIN__";
|
|
21
|
+
const END = "__OCX_LOGIN_PATH_END__";
|
|
22
|
+
export const LOGIN_SHELL_PATH_TIMEOUT_MS = 5_000;
|
|
23
|
+
export const LOGIN_SHELL_PATH_CACHE_MS = 5 * 60_000;
|
|
24
|
+
const MAX_OUTPUT_BYTES = 64 * 1024;
|
|
25
|
+
/** After the shell exits, how long its stdout may stay open before a background job is assumed to hold it. */
|
|
26
|
+
const STDOUT_DRAIN_MS = 500;
|
|
27
|
+
|
|
28
|
+
export function loginShellPathArgs(shell: string): string[] {
|
|
29
|
+
const print = basename(shell) === "fish"
|
|
30
|
+
? `printf '%s%s%s\\n' ${BEGIN} (string join : $PATH) ${END}`
|
|
31
|
+
: `printf '%s%s%s\\n' ${BEGIN} "$PATH" ${END}`;
|
|
32
|
+
return ["-l", "-i", "-c", print];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** The framed PATH, only when every entry is absolute. */
|
|
36
|
+
export function parseLoginShellPath(output: string): string | null {
|
|
37
|
+
const start = output.lastIndexOf(BEGIN);
|
|
38
|
+
if (start < 0) return null;
|
|
39
|
+
const end = output.indexOf(END, start + BEGIN.length);
|
|
40
|
+
if (end < 0) return null;
|
|
41
|
+
const value = output.slice(start + BEGIN.length, end);
|
|
42
|
+
if (!value || /[\r\n\0]/.test(value)) return null;
|
|
43
|
+
const entries = value.split(":").filter(Boolean);
|
|
44
|
+
return entries.length > 0 && entries.every(entry => isAbsolute(entry)) ? entries.join(":") : null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Ordered union: the shell's entries first, then any the process had that the shell did not. */
|
|
48
|
+
export function mergePathLists(preferred: string, fallback: string | undefined): string {
|
|
49
|
+
// POSIX only: the login shell runs on macOS and Linux, where PATH is colon-separated.
|
|
50
|
+
return [...new Set([...preferred.split(":"), ...(fallback ?? "").split(":")].filter(Boolean))].join(":");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface LoginShellPathDeps {
|
|
54
|
+
platform?: NodeJS.Platform;
|
|
55
|
+
shell?: string | null;
|
|
56
|
+
env?: NodeJS.ProcessEnv;
|
|
57
|
+
spawn?: (command: string, args: string[], options: Parameters<typeof nodeSpawn>[2]) => ChildProcess;
|
|
58
|
+
killGroup?: (pid: number) => void;
|
|
59
|
+
timeoutMs?: number;
|
|
60
|
+
now?: () => number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let cached: { at: number; value: string | null } | null = null;
|
|
64
|
+
|
|
65
|
+
export function resetLoginShellPathCacheForTests(): void {
|
|
66
|
+
cached = null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function defaultShell(env: NodeJS.ProcessEnv): string | null {
|
|
70
|
+
const fromEnv = env.SHELL?.trim();
|
|
71
|
+
if (fromEnv && isAbsolute(fromEnv)) return fromEnv;
|
|
72
|
+
try {
|
|
73
|
+
const shell = userInfo().shell;
|
|
74
|
+
return shell && isAbsolute(shell) ? shell : null;
|
|
75
|
+
} catch { return null; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function killProcessGroup(pid: number): void {
|
|
79
|
+
try { process.kill(-pid, "SIGKILL"); } catch { /* already gone */ }
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export async function readLoginShellPath(deps: LoginShellPathDeps = {}): Promise<string | null> {
|
|
83
|
+
const platform = deps.platform ?? process.platform;
|
|
84
|
+
if (platform === "win32") return null;
|
|
85
|
+
const now = deps.now ?? Date.now;
|
|
86
|
+
if (cached && now() - cached.at < LOGIN_SHELL_PATH_CACHE_MS) return cached.value;
|
|
87
|
+
const env = deps.env ?? process.env;
|
|
88
|
+
const shell = deps.shell === undefined ? defaultShell(env) : deps.shell;
|
|
89
|
+
const value = shell ? await probe(shell, env, deps) : null;
|
|
90
|
+
cached = { at: now(), value };
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function probe(shell: string, env: NodeJS.ProcessEnv, deps: LoginShellPathDeps): Promise<string | null> {
|
|
95
|
+
const spawn = deps.spawn ?? nodeSpawn;
|
|
96
|
+
const killGroup = deps.killGroup ?? killProcessGroup;
|
|
97
|
+
return new Promise(resolve => {
|
|
98
|
+
let child: ChildProcess;
|
|
99
|
+
try {
|
|
100
|
+
child = spawn(shell, loginShellPathArgs(shell), {
|
|
101
|
+
detached: true,
|
|
102
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
103
|
+
// Only what a login shell needs to find its startup files; no proxy or credential state.
|
|
104
|
+
env: Object.fromEntries(Object.entries({
|
|
105
|
+
HOME: env.HOME, USER: env.USER, LOGNAME: env.LOGNAME, SHELL: shell, LANG: env.LANG,
|
|
106
|
+
PATH: env.PATH, TERM: "dumb",
|
|
107
|
+
}).filter(([, value]) => value !== undefined)) as NodeJS.ProcessEnv,
|
|
108
|
+
});
|
|
109
|
+
} catch { resolve(null); return; }
|
|
110
|
+
let output = "";
|
|
111
|
+
let settled = false;
|
|
112
|
+
let exitCode: number | null = null;
|
|
113
|
+
let drain: ReturnType<typeof setTimeout> | undefined;
|
|
114
|
+
const finish = (value: string | null, killGroupMembers: boolean) => {
|
|
115
|
+
if (settled) return;
|
|
116
|
+
settled = true;
|
|
117
|
+
clearTimeout(timer);
|
|
118
|
+
clearTimeout(drain);
|
|
119
|
+
// A hung startup file, or a background job still holding the pipe, is killed with its group.
|
|
120
|
+
// A shell that exited and closed its output is left alone: its group id may already be free.
|
|
121
|
+
if (killGroupMembers && child.pid) killGroup(child.pid);
|
|
122
|
+
resolve(value);
|
|
123
|
+
};
|
|
124
|
+
const timer = setTimeout(() => finish(null, true), deps.timeoutMs ?? LOGIN_SHELL_PATH_TIMEOUT_MS);
|
|
125
|
+
child.stdout?.setEncoding("utf8");
|
|
126
|
+
child.stdout?.on("data", (chunk: string) => {
|
|
127
|
+
output += chunk;
|
|
128
|
+
if (output.length > MAX_OUTPUT_BYTES) finish(null, true);
|
|
129
|
+
});
|
|
130
|
+
const parsed = () => exitCode === 0 ? parseLoginShellPath(output) : null;
|
|
131
|
+
child.once("error", () => finish(null, true));
|
|
132
|
+
// Parse only after stdout closes: `exit` can arrive before the last chunk.
|
|
133
|
+
child.once("close", code => { exitCode ??= code; finish(parsed(), false); });
|
|
134
|
+
child.once("exit", code => {
|
|
135
|
+
exitCode = code;
|
|
136
|
+
drain = setTimeout(() => finish(parsed(), true), STDOUT_DRAIN_MS);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** `env` with the login-shell PATH in front, or unchanged when it cannot be read. */
|
|
142
|
+
export async function withLoginShellPath(env: NodeJS.ProcessEnv, deps: LoginShellPathDeps = {}): Promise<NodeJS.ProcessEnv> {
|
|
143
|
+
const loginPath = await readLoginShellPath({ ...deps, env });
|
|
144
|
+
return loginPath ? { ...env, PATH: mergePathLists(loginPath, env.PATH) } : env;
|
|
145
|
+
}
|
|
146
|
+
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* GET / PUT /api/advisor/settings — the advisor sidecar's management surface.
|
|
3
|
+
*
|
|
4
|
+
* GET answers the RESOLVED settings (defaults applied, sources marked) plus availability, so the
|
|
5
|
+
* GUI/CLI show real runtime state rather than stored fields. PUT is a strict partial patch:
|
|
6
|
+
* unknown keys and wrong types are refused, values are validated against the same resolver the
|
|
7
|
+
* runtime reads, and the patch persists through the locked config writer with the snapshot-
|
|
8
|
+
* restore discipline used by PATCH /api/protocols/settings — a refused or failed write never
|
|
9
|
+
* leaves the live config serving a state the file does not hold.
|
|
10
|
+
*/
|
|
11
|
+
import { activateAdvisor } from "../../lib/advisor-activation";
|
|
12
|
+
import { jsonResponse } from "../auth-cors";
|
|
13
|
+
import { readManagementJsonBodyOr } from "./body";
|
|
14
|
+
import type { ManagementContext } from "./context";
|
|
15
|
+
import {
|
|
16
|
+
ADVISOR_CONTEXT_SHARING_CONSENT_VERSION,
|
|
17
|
+
ADVISOR_EFFORTS,
|
|
18
|
+
advisorContextSharingBlocked,
|
|
19
|
+
advisorRunnable,
|
|
20
|
+
isCurrentAdvisorContextSharingConsent,
|
|
21
|
+
isValidAdvisorEffort,
|
|
22
|
+
isValidAdvisorPolicy,
|
|
23
|
+
resolveAdvisorSettings,
|
|
24
|
+
type AdvisorEffort,
|
|
25
|
+
type AdvisorPolicy,
|
|
26
|
+
} from "../../advisor/settings";
|
|
27
|
+
|
|
28
|
+
const INVALID_BODY = Symbol("invalid-body");
|
|
29
|
+
|
|
30
|
+
interface AdvisorPatch {
|
|
31
|
+
enabled?: boolean;
|
|
32
|
+
model?: string;
|
|
33
|
+
effort?: AdvisorEffort;
|
|
34
|
+
policy?: AdvisorPolicy;
|
|
35
|
+
timeoutMs?: number;
|
|
36
|
+
/** "v1" records current consent. null removes it. Enabling does not imply this field. */
|
|
37
|
+
contextSharingConsent?: typeof ADVISOR_CONTEXT_SHARING_CONSENT_VERSION | null;
|
|
38
|
+
reset?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type ParsedPatch = { ok: true; patch: AdvisorPatch } | { ok: false; code: string; message: string };
|
|
42
|
+
|
|
43
|
+
const PATCH_KEYS = new Set(["enabled", "model", "effort", "policy", "timeoutMs", "contextSharingConsent", "reset"]);
|
|
44
|
+
|
|
45
|
+
type Rec = Record<string, unknown>;
|
|
46
|
+
function isRec(value: unknown): value is Rec {
|
|
47
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Strict: unknown keys and wrong types are refused; messages name the field, never the value. */
|
|
51
|
+
export function parseAdvisorSettingsPatch(body: unknown): ParsedPatch {
|
|
52
|
+
if (!isRec(body)) return { ok: false, code: "invalid_body", message: "body must be a JSON object" };
|
|
53
|
+
if (Object.keys(body).length === 0) return { ok: false, code: "empty_body", message: "body must set at least one of enabled, model, effort, policy, timeoutMs, contextSharingConsent or reset" };
|
|
54
|
+
for (const key of Object.keys(body)) {
|
|
55
|
+
if (!PATCH_KEYS.has(key)) return { ok: false, code: "unknown_field", message: "body accepts only enabled, model, effort, policy, timeoutMs, contextSharingConsent and reset" };
|
|
56
|
+
}
|
|
57
|
+
const patch: AdvisorPatch = {};
|
|
58
|
+
if (body.reset !== undefined) {
|
|
59
|
+
if (body.reset !== true) return { ok: false, code: "invalid_reset", message: "reset must be true when present" };
|
|
60
|
+
if (Object.keys(body).length > 1) return { ok: false, code: "reset_with_fields", message: "reset must be the only field when present" };
|
|
61
|
+
patch.reset = true;
|
|
62
|
+
return { ok: true, patch };
|
|
63
|
+
}
|
|
64
|
+
if (body.enabled !== undefined) {
|
|
65
|
+
if (typeof body.enabled !== "boolean") return { ok: false, code: "invalid_enabled", message: "enabled must be a boolean" };
|
|
66
|
+
patch.enabled = body.enabled;
|
|
67
|
+
}
|
|
68
|
+
if (body.model !== undefined) {
|
|
69
|
+
// An empty or whitespace-only value is the supported "clear the model" state, not an error:
|
|
70
|
+
// the resolver defaults to an empty model and `advisorRunnable` requires a non-blank one. The
|
|
71
|
+
// only structural rules are the string type and the trimmed length bound.
|
|
72
|
+
if (typeof body.model !== "string" || body.model.trim().length > 200) {
|
|
73
|
+
return {
|
|
74
|
+
ok: false,
|
|
75
|
+
code: "invalid_model",
|
|
76
|
+
message: "model must be a string whose trimmed value is at most 200 characters; an empty value clears the advisor model",
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
patch.model = body.model.trim();
|
|
80
|
+
}
|
|
81
|
+
if (body.effort !== undefined) {
|
|
82
|
+
if (!isValidAdvisorEffort(body.effort)) {
|
|
83
|
+
return { ok: false, code: "invalid_effort", message: `effort must be one of ${ADVISOR_EFFORTS.join(", ")}` };
|
|
84
|
+
}
|
|
85
|
+
patch.effort = body.effort;
|
|
86
|
+
}
|
|
87
|
+
if (body.policy !== undefined) {
|
|
88
|
+
if (!isValidAdvisorPolicy(body.policy)) {
|
|
89
|
+
return { ok: false, code: "invalid_policy", message: 'policy must be "manual" or "preflight"' };
|
|
90
|
+
}
|
|
91
|
+
patch.policy = body.policy;
|
|
92
|
+
}
|
|
93
|
+
if (body.timeoutMs !== undefined) {
|
|
94
|
+
if (typeof body.timeoutMs !== "number" || !Number.isFinite(body.timeoutMs) || body.timeoutMs < 1_000 || body.timeoutMs > 600_000) {
|
|
95
|
+
return { ok: false, code: "invalid_timeout", message: "timeoutMs must be a number between 1000 and 600000" };
|
|
96
|
+
}
|
|
97
|
+
patch.timeoutMs = Math.floor(body.timeoutMs);
|
|
98
|
+
}
|
|
99
|
+
if (body.contextSharingConsent !== undefined) {
|
|
100
|
+
if (body.contextSharingConsent === null) {
|
|
101
|
+
patch.contextSharingConsent = null;
|
|
102
|
+
} else if (isCurrentAdvisorContextSharingConsent(body.contextSharingConsent)) {
|
|
103
|
+
patch.contextSharingConsent = body.contextSharingConsent;
|
|
104
|
+
} else {
|
|
105
|
+
return {
|
|
106
|
+
ok: false,
|
|
107
|
+
code: "invalid_context_sharing_consent",
|
|
108
|
+
message: `contextSharingConsent must be "${ADVISOR_CONTEXT_SHARING_CONSENT_VERSION}" or null`,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return { ok: true, patch };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function advisorInfo(config: ManagementContext["config"]): Record<string, unknown> {
|
|
116
|
+
const settings = resolveAdvisorSettings(config);
|
|
117
|
+
const warning = settings.enabled && settings.model.trim() === ""
|
|
118
|
+
? "advisor_enabled_without_model"
|
|
119
|
+
: advisorContextSharingBlocked(settings)
|
|
120
|
+
? "advisor_context_sharing_consent_required"
|
|
121
|
+
: undefined;
|
|
122
|
+
return {
|
|
123
|
+
settings,
|
|
124
|
+
runnable: advisorRunnable(settings),
|
|
125
|
+
// Enabled without a model cannot consult. Enabled with a model but without current
|
|
126
|
+
// consent must not send task context; the warning names that block.
|
|
127
|
+
...(warning ? { warning } : {}),
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function applyPatchInMemory(config: ManagementContext["config"], patch: AdvisorPatch): void {
|
|
132
|
+
if (patch.reset) {
|
|
133
|
+
delete config.advisor;
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
const current: Rec = isRec(config.advisor) ? { ...config.advisor } : {};
|
|
137
|
+
if (patch.enabled !== undefined) current.enabled = patch.enabled;
|
|
138
|
+
if (patch.model !== undefined) current.model = patch.model;
|
|
139
|
+
if (patch.effort !== undefined) current.effort = patch.effort;
|
|
140
|
+
if (patch.policy !== undefined) current.policy = patch.policy;
|
|
141
|
+
if (patch.timeoutMs !== undefined) current.timeoutMs = patch.timeoutMs;
|
|
142
|
+
if (patch.contextSharingConsent === null) delete current.contextSharingConsent;
|
|
143
|
+
else if (patch.contextSharingConsent !== undefined) current.contextSharingConsent = patch.contextSharingConsent;
|
|
144
|
+
config.advisor = current as ManagementContext["config"]["advisor"];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isConfigLockContention(error: unknown): boolean {
|
|
148
|
+
if (!error || typeof error !== "object") return false;
|
|
149
|
+
if ((error as { code?: unknown }).code !== "CONFIG_MUTATION_LOCK_UNAVAILABLE") return false;
|
|
150
|
+
return (error as { cause?: { code?: unknown } }).cause?.code === "SQLITE_BUSY";
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
export async function handleAdvisorRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
154
|
+
const { url, req, config } = ctx;
|
|
155
|
+
if (url.pathname !== "/api/advisor/settings") return null;
|
|
156
|
+
|
|
157
|
+
if (req.method === "GET") return jsonResponse(advisorInfo(config), 200, req, config);
|
|
158
|
+
if (req.method === "PUT") return putAdvisorSettings(ctx);
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async function putAdvisorSettings(ctx: ManagementContext): Promise<Response> {
|
|
163
|
+
const { req, config } = ctx;
|
|
164
|
+
|
|
165
|
+
const body = await readManagementJsonBodyOr(req, INVALID_BODY);
|
|
166
|
+
const parsed = body === INVALID_BODY
|
|
167
|
+
? { ok: false as const, code: "invalid_json", message: "body must be valid JSON" }
|
|
168
|
+
: parseAdvisorSettingsPatch(body);
|
|
169
|
+
if (!parsed.ok) return jsonResponse({ error: { code: parsed.code, message: parsed.message } }, 400, req, config);
|
|
170
|
+
|
|
171
|
+
// Resolve the writer BEFORE touching the live config: a resolution failure must leave the
|
|
172
|
+
// in-memory config exactly as it was, not rely on the snapshot restore below.
|
|
173
|
+
// `deps.` first: route tests with an in-memory fixture must never write the real config.
|
|
174
|
+
const persist = ctx.deps.saveConfigPreservingClaudeCode
|
|
175
|
+
?? (await import("../../config")).saveConfigPreservingClaudeCode;
|
|
176
|
+
const snapshot = isRec(config.advisor) ? { ...config.advisor } : undefined;
|
|
177
|
+
applyPatchInMemory(config, parsed.patch);
|
|
178
|
+
try {
|
|
179
|
+
persist(config);
|
|
180
|
+
} catch (error) {
|
|
181
|
+
// Undo in memory too: a live config that serves a state the file does not hold would
|
|
182
|
+
// mislead every GET until the next restart.
|
|
183
|
+
if (snapshot === undefined) delete config.advisor;
|
|
184
|
+
else config.advisor = snapshot as ManagementContext["config"]["advisor"];
|
|
185
|
+
return isConfigLockContention(error)
|
|
186
|
+
? jsonResponse({ error: { code: "config_busy", message: "Another process is saving the configuration. Try again in a moment." } }, 409, req, config)
|
|
187
|
+
: jsonResponse({ error: { code: "write_failed", message: "The configuration could not be saved." } }, 500, req, config);
|
|
188
|
+
}
|
|
189
|
+
activateAdvisor(config);
|
|
190
|
+
return jsonResponse(advisorInfo(config), 200, req, config);
|
|
191
|
+
}
|
|
@@ -1397,6 +1397,24 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1397
1397
|
}
|
|
1398
1398
|
|
|
1399
1399
|
// Claude Code inbound settings (GUI "Claude ON" toggle + Claude page).
|
|
1400
|
+
if (url.pathname === "/api/claude-code/model-defaults" && (req.method === "GET" || req.method === "POST")) {
|
|
1401
|
+
const { previewClaudeModelDefaults, applyClaudeModelDefaults, ModelDefaultsRecoveryError } = await import("../../claude/model-default-settings");
|
|
1402
|
+
try {
|
|
1403
|
+
if (req.method === "GET") return jsonResponse(previewClaudeModelDefaults(config.claudeCode?.tierModels));
|
|
1404
|
+
let body: unknown;
|
|
1405
|
+
try { body = await readManagementJsonBody(req); } catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid JSON body" }, 400); }
|
|
1406
|
+
if (!body || typeof body !== "object" || Array.isArray(body) || Object.keys(body).length !== 1 ||
|
|
1407
|
+
!("fingerprint" in body) || typeof body.fingerprint !== "string" || !/^[a-f0-9]{64}$/.test(body.fingerprint)) {
|
|
1408
|
+
return jsonResponse({ error: "fingerprint is required" }, 400);
|
|
1409
|
+
}
|
|
1410
|
+
return jsonResponse(applyClaudeModelDefaults(config.claudeCode?.tierModels, body.fingerprint));
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
rethrowManagementBodyTooLarge(error);
|
|
1413
|
+
if (error instanceof ModelDefaultsRecoveryError) return jsonResponse({ error: error.message, code: error.message, backupPath: error.backupPath }, 500);
|
|
1414
|
+
const code = error instanceof Error && error.message === "settings_changed" ? "settings_changed" : "settings_unavailable";
|
|
1415
|
+
return jsonResponse({ error: code, code }, code === "settings_changed" ? 409 : 500);
|
|
1416
|
+
}
|
|
1417
|
+
}
|
|
1400
1418
|
if (url.pathname === "/api/claude-code" && req.method === "GET") {
|
|
1401
1419
|
const models = await fetchAllModels(config);
|
|
1402
1420
|
const { listCatalogNativeSlugs } = await import("../../codex/catalog");
|
|
@@ -1513,10 +1531,10 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1513
1531
|
});
|
|
1514
1532
|
}
|
|
1515
1533
|
if (url.pathname === "/api/claude-code" && req.method === "PUT") {
|
|
1516
|
-
// NOTE: model /
|
|
1534
|
+
// NOTE: model / maxContextTokens / alwaysEnableEffort are
|
|
1517
1535
|
// CONFIG-ONLY back-compat fields — the GUI no longer offers controls for them
|
|
1518
|
-
// (default model is owned by Claude Code's /model picker;
|
|
1519
|
-
//
|
|
1536
|
+
// (default model is owned by Claude Code's /model picker;
|
|
1537
|
+
// auto-context supersedes the max-context pair; effort rides
|
|
1520
1538
|
// regardless on 2.1.207). PUT keeps validating them so hand-written configs
|
|
1521
1539
|
// and older GUIs stay safe; GUI saves omit them and the spread preserves them.
|
|
1522
1540
|
let parsedBody: unknown;
|
|
@@ -1835,7 +1853,7 @@ export async function handleAgentSettingsRoutes(ctx: ManagementContext): Promise
|
|
|
1835
1853
|
}
|
|
1836
1854
|
}
|
|
1837
1855
|
if (body.tierModels !== undefined) {
|
|
1838
|
-
//
|
|
1856
|
+
// Family defaults also serve third-party callers that request native tier aliases.
|
|
1839
1857
|
if (body.tierModels === null) {
|
|
1840
1858
|
delete next.tierModels;
|
|
1841
1859
|
} else if (!isPlainObject(body.tierModels)) {
|
|
@@ -27,6 +27,10 @@ function response(): Response {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
export async function handleCompanionRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
30
|
+
if (ctx.url.pathname === "/api/advisor/settings") {
|
|
31
|
+
const { handleAdvisorRoutes } = await import("./advisor-routes");
|
|
32
|
+
return handleAdvisorRoutes(ctx);
|
|
33
|
+
}
|
|
30
34
|
if (ctx.url.pathname === "/api/companion/open-in-browser" && ctx.req.method === "POST") {
|
|
31
35
|
let body: unknown;
|
|
32
36
|
try {
|
|
@@ -329,8 +329,8 @@ export async function syncEnabledClientIntegrations(
|
|
|
329
329
|
config,
|
|
330
330
|
port,
|
|
331
331
|
}, unattended
|
|
332
|
-
? ["mcode", "pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo"]
|
|
333
|
-
: ["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo"],
|
|
332
|
+
? ["mcode", "pi", "aside", "raycast", "omo", "commandcode", "droid", "opencode", "kilo", "qoder"]
|
|
333
|
+
: ["mcode", "pi", "aside", "raycast", "omo", "cline", "commandcode", "droid", "opencode", "kilo", "qoder"],
|
|
334
334
|
...unattended ? [{ refreshOnly: true, admit }] as const : []));
|
|
335
335
|
|
|
336
336
|
return out;
|
|
@@ -45,6 +45,10 @@ export interface ManagementRequestIngress {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export interface ManagementApiDeps {
|
|
48
|
+
desktopStartupSettings?: import("../../codex/desktop-compatibility/startup-settings").DesktopStartupSettingsService;
|
|
49
|
+
desktopCertificateService?: import("../../codex/desktop-compatibility/certificate-service").DesktopCertificateService;
|
|
50
|
+
desktopCompatibilityRuntime?: import("../../codex/desktop-compatibility/runtime").DesktopCompatibilityRuntime;
|
|
51
|
+
onDesktopCompatibilityShutdown?: (shutdown: () => Promise<void>) => void;
|
|
48
52
|
/** Bound to this server's lifecycle owner; absent in direct route tests. */
|
|
49
53
|
listLowQuotaEvents?: (limit?: number) => LowQuotaEvent[];
|
|
50
54
|
/** Bound Claude intercept state, injectable for isolated management-route tests. */
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsonResponse } from "../auth-cors";
|
|
2
|
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
3
|
+
import type { ManagementContext } from "./context";
|
|
4
|
+
import type { DesktopCertificateService } from "../../codex/desktop-compatibility/certificate-service";
|
|
5
|
+
|
|
6
|
+
const PATH = "/api/codex/desktop-compatibility/certificate";
|
|
7
|
+
let service: DesktopCertificateService | undefined;
|
|
8
|
+
|
|
9
|
+
export async function handleDesktopCompatibilityRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
10
|
+
if (ctx.url.pathname !== PATH) return null;
|
|
11
|
+
if (ctx.req.method !== "GET" && ctx.req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405);
|
|
12
|
+
// A local dashboard confirmation precedes any persisted key or CurrentUser Root mutation.
|
|
13
|
+
// This is caller provenance, not protection against an arbitrary same-user process.
|
|
14
|
+
if (ctx.req.method === "POST" && (ctx.principal !== "gui-session" || !ctx.trustedLoopbackIngress)) {
|
|
15
|
+
return jsonResponse({ error: "local_dashboard_confirmation_required" }, 403);
|
|
16
|
+
}
|
|
17
|
+
let action: "prepare" | "trust" | "remove-trust" | "renew" | undefined, fingerprint: string | undefined;
|
|
18
|
+
if (ctx.req.method === "POST") {
|
|
19
|
+
let body: unknown;
|
|
20
|
+
try { body = await readManagementJsonBody(ctx.req); }
|
|
21
|
+
catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
|
|
22
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return jsonResponse({ error: "invalid_request" }, 400);
|
|
23
|
+
const value = body as Record<string, unknown>;
|
|
24
|
+
if (Object.keys(value).some(key => !["action", "fingerprint", "confirmed"].includes(key)) || value.confirmed !== true
|
|
25
|
+
|| typeof value.action !== "string" || !["prepare", "trust", "remove-trust", "renew"].includes(value.action)) return jsonResponse({ error: "invalid_request" }, 400);
|
|
26
|
+
action = value.action as typeof action;
|
|
27
|
+
if (action !== "prepare" && (typeof value.fingerprint !== "string" || !/^[A-F0-9]{64}$/.test(value.fingerprint))) {
|
|
28
|
+
return jsonResponse({ error: "certificate_fingerprint_required" }, 400);
|
|
29
|
+
}
|
|
30
|
+
if (action === "prepare" && value.fingerprint !== undefined) return jsonResponse({ error: "unexpected_fingerprint" }, 400);
|
|
31
|
+
fingerprint = value.fingerprint as string | undefined;
|
|
32
|
+
}
|
|
33
|
+
const controller = ctx.deps.desktopCertificateService ?? (service ??= (await import("../../codex/desktop-compatibility/certificate-service")).createDesktopCertificateService());
|
|
34
|
+
try {
|
|
35
|
+
const status = action === "prepare" ? await controller.prepare()
|
|
36
|
+
: action === "trust" ? await controller.trust(fingerprint!)
|
|
37
|
+
: action === "renew" ? await controller.renew(fingerprint!)
|
|
38
|
+
: action === "remove-trust" ? await controller.removeTrust(fingerprint!) : await controller.status();
|
|
39
|
+
return jsonResponse({ ok: true, certificate: status });
|
|
40
|
+
} catch (error) {
|
|
41
|
+
const code = error && typeof error === "object" && "code" in error ? String(error.code) : "operation_failed";
|
|
42
|
+
const allowed = new Set(["unsupported", "busy", "not_prepared", "fingerprint_changed", "app_running", "runtime_running", "app_state_unknown", "trust_unknown", "trust_not_applied",
|
|
43
|
+
"unsafe_path", "unreadable", "expired", "renewal_required", "protection_failed"]);
|
|
44
|
+
return jsonResponse({ ok: false, error: allowed.has(code) ? code : "operation_failed" }, 409);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsonResponse } from "../auth-cors";
|
|
2
|
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
3
|
+
import type { ManagementContext } from "./context";
|
|
4
|
+
import type { DesktopCompatibilityRuntime } from "../../codex/desktop-compatibility/runtime";
|
|
5
|
+
|
|
6
|
+
const PATH = "/api/codex/desktop-compatibility/runtime";
|
|
7
|
+
const ERROR_CODES = new Set(["busy", "unsupported", "test_environment", "stopping", "build_unverified", "egress_proxy_invalid",
|
|
8
|
+
"trust_required", "certificate_expiring", "certificate_not_prepared", "certificate_invalid", "certificate_expired",
|
|
9
|
+
"native_identity_unverified", "native_routing_unverified", "cleanup_incomplete", "not_running", "connection_invalid", "connection_changed", "connection_cleanup_required", "connection_unavailable"]);
|
|
10
|
+
|
|
11
|
+
export async function handleDesktopCompatibilityRuntimeRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
12
|
+
if (ctx.url.pathname !== PATH) return null;
|
|
13
|
+
if (ctx.req.method !== "GET" && ctx.req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405);
|
|
14
|
+
if (ctx.req.method === "POST" && (ctx.principal !== "gui-session" || !ctx.trustedLoopbackIngress)) return jsonResponse({ error: "local_dashboard_confirmation_required" }, 403);
|
|
15
|
+
let action: "start" | "stop" | "observe" | "apply" | "launch" | undefined;
|
|
16
|
+
if (ctx.req.method === "POST") {
|
|
17
|
+
let body: unknown;
|
|
18
|
+
try { body = await readManagementJsonBody(ctx.req); }
|
|
19
|
+
catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
|
|
20
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return jsonResponse({ error: "invalid_request" }, 400);
|
|
21
|
+
const value = body as Record<string, unknown>;
|
|
22
|
+
if (Object.keys(value).some(key => !["action", "confirmed", "accountWideConsent"].includes(key)) || value.confirmed !== true
|
|
23
|
+
|| typeof value.action !== "string" || !["start", "stop", "observe", "apply", "launch"].includes(value.action)) return jsonResponse({ error: "invalid_request" }, 400);
|
|
24
|
+
if (value.action === "apply" ? value.accountWideConsent !== true : value.accountWideConsent !== undefined) return jsonResponse({ error: "invalid_consent_scope" }, 400);
|
|
25
|
+
action = value.action as typeof action;
|
|
26
|
+
}
|
|
27
|
+
const module = ctx.deps.desktopCompatibilityRuntime ? null : await import("../../codex/desktop-compatibility/service");
|
|
28
|
+
const service: DesktopCompatibilityRuntime = ctx.deps.desktopCompatibilityRuntime ?? module!.getDesktopCompatibilityRuntime();
|
|
29
|
+
if (module) ctx.deps.onDesktopCompatibilityShutdown?.(module.shutdownDesktopCompatibility);
|
|
30
|
+
try {
|
|
31
|
+
if (action === "apply") {
|
|
32
|
+
const result = await service.apply(true);
|
|
33
|
+
return jsonResponse({ ok: result.accepted, activation: result, runtime: service.status() }, result.accepted ? 202 : 409);
|
|
34
|
+
}
|
|
35
|
+
if (action === "launch") {
|
|
36
|
+
const result = await service.launch();
|
|
37
|
+
return jsonResponse({ ok: result.status === "started", launch: result, runtime: service.status() }, result.status === "started" ? 200 : 409);
|
|
38
|
+
}
|
|
39
|
+
const result = action === "start" ? await service.start() : action === "stop" ? await service.stop()
|
|
40
|
+
: action === "observe" ? await service.observe() : service.status();
|
|
41
|
+
return jsonResponse({ ok: true, runtime: result });
|
|
42
|
+
} catch (error) {
|
|
43
|
+
const code = error instanceof Error ? error.message.replace(/^desktop_compatibility_/, "") : "operation_failed";
|
|
44
|
+
return jsonResponse({ ok: false, error: ERROR_CODES.has(code) ? code : "operation_failed", runtime: service.status() }, 409);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { jsonResponse } from "../auth-cors";
|
|
2
|
+
import { readManagementJsonBody, rethrowManagementBodyTooLarge } from "./body";
|
|
3
|
+
import type { ManagementContext } from "./context";
|
|
4
|
+
|
|
5
|
+
const PATH = "/api/codex/desktop-compatibility/settings";
|
|
6
|
+
export async function handleDesktopCompatibilitySettingsRoutes(ctx: ManagementContext): Promise<Response | null> {
|
|
7
|
+
if (ctx.url.pathname !== PATH) return null;
|
|
8
|
+
if (ctx.req.method !== "GET" && ctx.req.method !== "POST") return jsonResponse({ error: "method_not_allowed" }, 405);
|
|
9
|
+
if (ctx.req.method === "POST" && (ctx.principal !== "gui-session" || !ctx.trustedLoopbackIngress)) return jsonResponse({ error: "local_dashboard_confirmation_required" }, 403);
|
|
10
|
+
let update: { startOnProxyStart: boolean; revision: string } | undefined;
|
|
11
|
+
if (ctx.req.method === "POST") {
|
|
12
|
+
let body: unknown;
|
|
13
|
+
try { body = await readManagementJsonBody(ctx.req); }
|
|
14
|
+
catch (error) { rethrowManagementBodyTooLarge(error); return jsonResponse({ error: "invalid_json" }, 400); }
|
|
15
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return jsonResponse({ error: "invalid_request" }, 400);
|
|
16
|
+
const value = body as Record<string, unknown>;
|
|
17
|
+
if (Object.keys(value).some(key => !["startOnProxyStart", "revision", "confirmed"].includes(key)) || value.confirmed !== true
|
|
18
|
+
|| typeof value.startOnProxyStart !== "boolean" || typeof value.revision !== "string" || !/^[a-f0-9]{64}$/.test(value.revision)) return jsonResponse({ error: "invalid_request" }, 400);
|
|
19
|
+
update = { startOnProxyStart: value.startOnProxyStart, revision: value.revision };
|
|
20
|
+
}
|
|
21
|
+
const service = ctx.deps.desktopStartupSettings ?? (await import("../../codex/desktop-compatibility/startup-settings")).createDesktopStartupSettings({ liveConfig: ctx.config });
|
|
22
|
+
try { return jsonResponse({ ok: true, settings: update ? service.set(update.startOnProxyStart, update.revision) : service.status() }); }
|
|
23
|
+
catch (error) {
|
|
24
|
+
const code = error instanceof Error ? error.message.replace(/^desktop_compatibility_settings_/, "") : "operation_unconfirmed";
|
|
25
|
+
return jsonResponse({ ok: false, error: ["unavailable", "invalid", "changed", "invalid_request"].includes(code) ? `settings_${code}` : "settings_operation_unconfirmed" }, 409);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -556,9 +556,9 @@ export async function handleModelRoutes(ctx: ManagementContext): Promise<Respons
|
|
|
556
556
|
if (!(error instanceof ClientPathError)) throw error;
|
|
557
557
|
return jsonResponse({ error: error.message }, 400, req, config);
|
|
558
558
|
}
|
|
559
|
-
if ((requested === "raycast" || requested === "droid") && shouldInjectApiAuthHeader(config)) {
|
|
559
|
+
if ((requested === "raycast" || requested === "droid" || requested === "qoder") && shouldInjectApiAuthHeader(config)) {
|
|
560
560
|
return jsonResponse({
|
|
561
|
-
error: `${requested === "droid" ? "Factory Droid" : "Raycast"} export requires an unauthenticated loopback destination; this listener requires an admission header the client cannot supply.`,
|
|
561
|
+
error: `${requested === "droid" ? "Factory Droid" : requested === "qoder" ? "Qoder" : "Raycast"} export requires an unauthenticated loopback destination; this listener requires an admission header the client cannot supply.`,
|
|
562
562
|
reason: "non_loopback",
|
|
563
563
|
}, 400, req, config);
|
|
564
564
|
}
|
|
@@ -31,6 +31,7 @@ export const PROTOCOL_ROLLOUT_KEYS = [
|
|
|
31
31
|
"managedMessagesNative",
|
|
32
32
|
"managedMessagesNativeOAuth",
|
|
33
33
|
"directEncoders",
|
|
34
|
+
"antigravityDirect",
|
|
34
35
|
"shadowPlan",
|
|
35
36
|
] as const satisfies readonly (keyof ProtocolRolloutSettings)[];
|
|
36
37
|
const ROLLOUT_KEYS = new Set<string>(PROTOCOL_ROLLOUT_KEYS);
|
|
@@ -139,6 +139,8 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
139
139
|
{ method: "POST", path: "/api/native-main-profiles/switch", module: "codex/native-profile-api", mutates: true },
|
|
140
140
|
// server/management/agent-settings-routes
|
|
141
141
|
{ method: "GET", path: "/api/claude-code", module: "server/management/agent-settings-routes", mutates: false },
|
|
142
|
+
{ method: "GET", path: "/api/claude-code/model-defaults", module: "server/management/agent-settings-routes", mutates: false, exempt: { reason: "interactive-preview", why: "Preview half of the GUI-only Claude Code tier-default apply; the fingerprint it returns is the only input POST accepts, and no terminal confirmation workflow exists yet." } },
|
|
143
|
+
{ method: "POST", path: "/api/claude-code/model-defaults", module: "server/management/agent-settings-routes", mutates: true, exempt: { reason: "interactive-preview", why: "Apply half of the GUI-only Claude Code tier-default preview; it writes the shared Claude settings only for a fingerprint the dashboard preview produced." } },
|
|
142
144
|
{ method: "GET", path: "/api/claude-desktop", module: "server/management/agent-settings-routes", mutates: false },
|
|
143
145
|
{ method: "GET", path: "/api/claude-desktop/status", module: "server/management/agent-settings-routes", mutates: false },
|
|
144
146
|
{ method: "GET", path: "/api/codex-auth/features/default-mode-request-user-input", module: "server/management/agent-settings-routes", mutates: false },
|
|
@@ -163,6 +165,12 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
163
165
|
// server/management/claude-desktop-picker-routes
|
|
164
166
|
{ method: "GET", path: "/api/claude-desktop/picker", module: "server/management/claude-desktop-picker-routes", mutates: false },
|
|
165
167
|
{ method: "PUT", path: "/api/claude-desktop/picker", module: "server/management/claude-desktop-picker-routes", mutates: true },
|
|
168
|
+
{ method: "GET", path: "/api/codex/desktop-compatibility/certificate", module: "server/management/desktop-compatibility-routes", mutates: false, mechanism: "path-constant", exempt: { reason: "deferred-verb", owner: "codex-desktop-compatibility", ownerDoc: "structure/clients/codex-desktop.md", why: "Certificate status is available over authenticated HTTP while the desktop compatibility runtime/CLI status contract is integrated." } },
|
|
169
|
+
{ method: "POST", path: "/api/codex/desktop-compatibility/certificate", module: "server/management/desktop-compatibility-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Certificate setup requires a confirmed local dashboard session; raw admin tokens cannot enroll or remove OS trust." } },
|
|
170
|
+
{ method: "GET", path: "/api/codex/desktop-compatibility/runtime", module: "server/management/desktop-compatibility-runtime-routes", mutates: false, mechanism: "path-constant", exempt: { reason: "deferred-verb", owner: "codex-desktop-compatibility", ownerDoc: "structure/clients/codex-desktop.md", why: "Experimental runtime status is authenticated HTTP while the native compatibility UI and CLI contract are integrated." } },
|
|
171
|
+
{ method: "POST", path: "/api/codex/desktop-compatibility/runtime", module: "server/management/desktop-compatibility-runtime-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Native app launch and account-wide UI trial require explicit local dashboard confirmation." } },
|
|
172
|
+
{ method: "GET", path: "/api/codex/desktop-compatibility/settings", module: "server/management/desktop-compatibility-settings-routes", mutates: false, mechanism: "path-constant", exempt: { reason: "deferred-verb", owner: "codex-desktop-compatibility", ownerDoc: "structure/clients/codex-desktop.md", why: "The persisted startup preference is currently owned by the local desktop compatibility panel." } },
|
|
173
|
+
{ method: "POST", path: "/api/codex/desktop-compatibility/settings", module: "server/management/desktop-compatibility-settings-routes", mutates: true, mechanism: "path-constant", exempt: { reason: "session-only", why: "Saved native observation intent requires an explicit local GUI action and fresh field revision." } },
|
|
166
174
|
{ method: "PUT", path: "/api/codex-auth/features/default-mode-request-user-input", module: "server/management/agent-settings-routes", mutates: true },
|
|
167
175
|
{ method: "PUT", path: "/api/effort-caps", module: "server/management/agent-settings-routes", mutates: true },
|
|
168
176
|
{ method: "PUT", path: "/api/grok/selection", module: "server/management/agent-settings-routes", mutates: true },
|
|
@@ -267,6 +275,9 @@ export const MANAGEMENT_ROUTES: readonly ManagementRoute[] = [
|
|
|
267
275
|
{ method: "GET", path: "/api/usage", module: "server/management/logs-usage-routes", mutates: false },
|
|
268
276
|
// server/management/usage-timeline-routes
|
|
269
277
|
{ method: "GET", path: "/api/usage/timeline", module: "server/management/usage-timeline-routes", mutates: false },
|
|
278
|
+
// server/management/advisor-routes
|
|
279
|
+
{ method: "GET", path: "/api/advisor/settings", module: "server/management/advisor-routes", mutates: false },
|
|
280
|
+
{ method: "PUT", path: "/api/advisor/settings", module: "server/management/advisor-routes", mutates: true },
|
|
270
281
|
// server/management/companion-routes
|
|
271
282
|
{ method: "POST", path: "/api/companion/open-in-browser", module: "server/management/companion-routes", mutates: true, exempt: { reason: "browser-navigation", why: "Opens the companion's current view in the system browser under normal management admission; the CLI reads the underlying settings and usage directly." } },
|
|
272
283
|
{ method: "GET", path: "/api/companion/settings", module: "server/management/companion-routes", mutates: false },
|