@bitkyc08/opencodex 2.82.0 → 2.84.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/gui/dist/assets/App-CZtqiGcw.js +56 -0
- package/gui/dist/assets/Tray-C9TGtwad.js +1 -0
- package/gui/dist/assets/{index-BAbcj51J.css → index-BB6uK6tU.css} +1 -1
- package/gui/dist/assets/index-CTm4l-HQ.js +94 -0
- package/gui/dist/assets/{quota-summary-DTBC8n7-.js → quota-summary-BbEeRfPY.js} +1 -1
- package/gui/dist/index.html +2 -2
- package/package.json +2 -2
- package/src/adapters/coding-agent/protocol.ts +15 -8
- package/src/adapters/cursor/catalog.ts +2 -2
- package/src/adapters/cursor/effort-map.ts +1 -1
- package/src/adapters/devin/live-models.ts +2 -2
- package/src/adapters/google-truncation.ts +22 -0
- package/src/adapters/google.ts +7 -3
- package/src/adapters/ollama-native.ts +47 -15
- package/src/adapters/openai-chat/messages.ts +8 -4
- package/src/adapters/openai-responses/empty-assistant.ts +32 -0
- package/src/adapters/openai-responses/passthrough.ts +5 -0
- package/src/adapters/openai-responses/plaintext-reasoning-notice.ts +85 -0
- package/src/adapters/openai-responses/reasoning.ts +11 -1
- package/src/adapters/openai-responses/request-strips.ts +10 -2
- package/src/adapters/qoder/adapter.ts +16 -14
- package/src/bridge/errors.ts +26 -1
- package/src/bridge.ts +1 -1
- package/src/claude/context-windows.ts +57 -4
- package/src/claude/desktop-picker.ts +1 -1
- package/src/claude/intercept/connect-proxy.ts +10 -3
- package/src/claude/intercept/picker-ca-persistence.ts +3 -1
- package/src/claude/intercept/picker-listener.ts +49 -1
- package/src/claude/intercept/settings-no-proxy.ts +146 -0
- package/src/claude/intercept/settings.ts +101 -9
- package/src/cli/account-api.ts +65 -0
- package/src/cli/account-auth.ts +44 -12
- package/src/cli/account.ts +41 -5
- package/src/cli/capabilities-access-remote.ts +8 -0
- package/src/cli/capabilities-base.ts +13 -1
- package/src/cli/catalog-command-result.ts +2 -1
- package/src/cli/claude.ts +15 -6
- package/src/cli/config-command.ts +8 -2
- package/src/cli/debug.ts +8 -0
- package/src/cli/dispatch.ts +16 -3
- package/src/cli/export-command.ts +14 -1
- package/src/cli/hub-disable.ts +99 -0
- package/src/cli/hub.ts +4 -2
- package/src/cli/index.ts +13 -4
- package/src/cli/link-child.ts +104 -0
- package/src/cli/link-enrollment.ts +96 -0
- package/src/cli/link.ts +39 -24
- package/src/cli/observe-stream.ts +6 -1
- package/src/cli/observe.ts +10 -2
- package/src/cli/provider.ts +12 -0
- package/src/cli/registry.ts +5 -4
- package/src/cli/resolve.ts +1 -1
- package/src/cli/runtime-api.ts +27 -2
- package/src/cli/status.ts +104 -20
- package/src/client/connect.ts +39 -4
- package/src/client/hub-client.ts +14 -7
- package/src/client/hub-state.ts +25 -14
- package/src/client/link-home.ts +248 -0
- package/src/client/link-management-relay.ts +61 -0
- package/src/client/link-relay.ts +6 -1
- package/src/client/link-status.ts +14 -0
- package/src/client/machine-api.ts +14 -7
- package/src/client/machine-listener.ts +29 -4
- package/src/clients/config-export/model-metadata.ts +16 -0
- package/src/clients/config-export/zcode-store.ts +46 -11
- package/src/clients/config-export/zcode.ts +3 -5
- package/src/clients/config-export.ts +47 -17
- package/src/clients/omo-role-models.ts +6 -3
- package/src/codex/agent-role-models.ts +26 -8
- package/src/codex/auth-api/account-list.ts +17 -2
- package/src/codex/auth-api/account-selection.ts +17 -7
- package/src/codex/auth-api/pool-mode-gate.ts +4 -5
- package/src/codex/auth-api/routes.ts +5 -1
- package/src/codex/auth-context.ts +8 -3
- package/src/codex/autostart-health.ts +3 -1
- package/src/codex/catalog/bundled.ts +5 -1
- package/src/codex/catalog/effort.ts +2 -2
- package/src/codex/catalog/metadata.ts +4 -2
- package/src/codex/catalog/native-models.ts +17 -3
- package/src/codex/catalog/provider-models.ts +25 -0
- package/src/codex/desktop-app/darwin.ts +1 -1
- package/src/codex/desktop-app/handoff.ts +1 -1
- package/src/codex/desktop-app/linux.ts +1 -1
- package/src/codex/desktop-app/lock.ts +1 -1
- package/src/codex/desktop-app/types.ts +1 -1
- package/src/codex/desktop-app/windows.ts +1 -1
- package/src/codex/desktop-app-restart.ts +1 -1
- package/src/codex/main-account.ts +6 -7
- package/src/codex/native-profile-startup.ts +4 -1
- package/src/codex/prompt-journal.ts +20 -2
- package/src/codex/prompt-layers/encoding.ts +49 -0
- package/src/codex/prompt-layers/import-source.ts +30 -0
- package/src/codex/prompt-layers/toml-edit.ts +269 -37
- package/src/codex/prompt-layers/toml-read.ts +52 -4
- package/src/codex/prompt-layers.ts +383 -20
- package/src/codex/prompt-lock-claim.ts +13 -3
- package/src/codex/prompt-lock-evidence.ts +3 -3
- package/src/codex/prompt-lock-owner.ts +14 -6
- package/src/codex/prompt-text-probe.ts +34 -5
- package/src/codex/quota.ts +13 -6
- package/src/codex/role-auto-assign.ts +2 -5
- package/src/codex/routing/active-account.ts +21 -0
- package/src/codex/routing.ts +1 -1
- package/src/codex/runtime.ts +196 -8
- package/src/codex/shim-overlay.ts +8 -4
- package/src/codex/shim.ts +1 -1
- package/src/combos/failover.ts +3 -0
- package/src/config/api-key-reconcile.ts +34 -0
- package/src/config/diagnostics.ts +16 -0
- package/src/config/feature-flags.ts +5 -0
- package/src/config/live-reconcile.ts +43 -29
- package/src/config/live-topology.ts +29 -0
- package/src/config/load-degrade.ts +2 -1
- package/src/config/paths.ts +3 -0
- package/src/config/salvage.ts +3 -1
- package/src/config/schema/config-schema.ts +2 -0
- package/src/generated/compatibility-version.json +373 -217
- package/src/grok/grpc-web.ts +23 -1
- package/src/grok/inject.ts +245 -221
- package/src/grok/reset-coupon-ledger.ts +293 -57
- package/src/grok/reset-coupons.ts +15 -6
- package/src/integrations/cursor-detect.ts +1 -1
- package/src/integrations/mutation-plan.ts +13 -7
- package/src/integrations/registry.ts +45 -11
- package/src/integrations/state.ts +19 -7
- package/src/integrations/target.ts +42 -5
- package/src/integrations/writer.ts +14 -6
- package/src/lib/app-owned-memory-stores.ts +4 -0
- package/src/lib/app-owned-memory.ts +11 -0
- package/src/lib/errors.ts +25 -0
- package/src/lib/json-byte-size.ts +31 -0
- package/src/lib/optional-shutdown-hooks.ts +35 -0
- package/src/lib/outbound-credential-registry.ts +210 -0
- package/src/lib/passive-read.ts +35 -0
- package/src/lib/redact.ts +2 -2
- package/src/lib/shadow-call.ts +44 -5
- package/src/link/codex-settings.ts +155 -0
- package/src/link/management-routes.ts +89 -0
- package/src/link/remote-probe.ts +38 -0
- package/src/link/ssh-argv.ts +26 -5
- package/src/link/ssh-runner.ts +58 -27
- package/src/link/status-projection.ts +22 -4
- package/src/link/supervisor.ts +144 -49
- package/src/link/tunnel-state.ts +11 -4
- package/src/oauth/anthropic-continuity.ts +4 -3
- package/src/oauth/anthropic-identity.ts +13 -2
- package/src/oauth/anthropic-model-quota.ts +13 -1
- package/src/oauth/anthropic-routing.ts +230 -23
- package/src/oauth/anthropic.ts +18 -4
- package/src/oauth/google-antigravity.ts +24 -7
- package/src/oauth/index.ts +25 -16
- package/src/oauth/kiro-account-identity.ts +106 -0
- package/src/oauth/kiro.ts +48 -5
- package/src/oauth/meta-muse-device.ts +1 -1
- package/src/oauth/plan.ts +29 -0
- package/src/oauth/store.ts +40 -3
- package/src/oauth/types.ts +3 -1
- package/src/plugins/loader.ts +11 -0
- package/src/providers/anthropic-billing-ownership.ts +74 -0
- package/src/providers/anthropic-billing-recovery.ts +31 -0
- package/src/providers/anthropic-billing-refusal.ts +55 -0
- package/src/providers/anthropic-fast.ts +1 -1
- package/src/providers/anthropic-reset-admission.ts +62 -0
- package/src/providers/anthropic-reset-grant-ledger.ts +29 -7
- package/src/providers/api-key-resolve.ts +17 -2
- package/src/providers/devin-cli-authmode-migration.ts +1 -1
- package/src/providers/devin-provider-merge-migration.ts +1 -1
- package/src/providers/github-copilot-auto.ts +169 -0
- package/src/providers/key-failover.ts +52 -5
- package/src/providers/model-presets.ts +1 -1
- package/src/providers/model-rename-startup.ts +5 -3
- package/src/providers/muse-subscription-usage.ts +1 -1
- package/src/providers/quota/account-cache.ts +11 -0
- package/src/providers/quota/anthropic-account-quota.ts +4 -2
- package/src/providers/quota/report-cache.ts +14 -0
- package/src/providers/quota/vendor-probes-oauth.ts +3 -3
- package/src/providers/quota.ts +4 -1
- package/src/providers/registry/entries-core.ts +20 -20
- package/src/providers/registry/entries-extended.ts +4 -1
- package/src/providers/registry/model-seeds.ts +20 -3
- package/src/providers/request-pacing.ts +9 -0
- package/src/providers/stale-no-vision-seed-migration.ts +72 -0
- package/src/providers/xai-fast-model.ts +1 -1
- package/src/responses/compaction.ts +14 -0
- package/src/responses/parser.ts +7 -1
- package/src/server/auth-cors.ts +42 -0
- package/src/server/configured-credentials.ts +194 -0
- package/src/server/inbound-body-admission.ts +24 -7
- package/src/server/index/link-listener.ts +30 -17
- package/src/server/index/link-management.ts +58 -0
- package/src/server/index/optional-listeners.ts +5 -1
- package/src/server/index/serve-options.ts +21 -2
- package/src/server/index.ts +1 -1
- package/src/server/lifecycle.ts +33 -5
- package/src/server/management/agent-settings-routes.ts +28 -10
- package/src/server/management/anthropic-reset-grant-routes.ts +17 -1
- package/src/server/management/codex-agent-role-routes.ts +19 -6
- package/src/server/management/codex-prompt-routes.ts +136 -5
- package/src/server/management/codex-role-auto-assign.ts +1 -1
- package/src/server/management/grok-coupon-routes.ts +333 -57
- package/src/server/management/link-enrollment.ts +192 -0
- package/src/server/management/link-route-state.ts +185 -0
- package/src/server/management/link-routes.ts +92 -390
- package/src/server/management/model-rows.ts +3 -1
- package/src/server/management/provider-overwrite-carry.ts +10 -0
- package/src/server/management/provider-routes.ts +1 -0
- package/src/server/management/route-registry.ts +7 -5
- package/src/server/management/shared.ts +3 -0
- package/src/server/management-api.ts +3 -1
- package/src/server/messages-native.ts +68 -42
- package/src/server/relay-eager.ts +4 -0
- package/src/server/relay.ts +18 -7
- package/src/server/request-log-failure-attribution.ts +3 -0
- package/src/server/request-log-filter.ts +6 -1
- package/src/server/request-log-terminal-status.ts +243 -0
- package/src/server/request-log.ts +15 -118
- package/src/server/responses/adapter-continuation.ts +9 -1
- package/src/server/responses/adapter-dispatch.ts +293 -81
- package/src/server/responses/code-mode-collab-surface.ts +205 -0
- package/src/server/responses/codex-ws-exchange.ts +147 -6
- package/src/server/responses/codex-ws-wire.ts +203 -0
- package/src/server/responses/collaboration.ts +36 -7
- package/src/server/responses/compact.ts +129 -50
- package/src/server/responses/context-overflow.ts +13 -1
- package/src/server/responses/core-codex-account.ts +105 -1
- package/src/server/responses/core-combo-failure.ts +14 -0
- package/src/server/responses/core-combo.ts +7 -2
- package/src/server/responses/core-errors.ts +29 -1
- package/src/server/responses/core-opaque-recovery.ts +5 -5
- package/src/server/responses/core-options.ts +2 -0
- package/src/server/responses/core.ts +6 -6
- package/src/server/responses/deferred-collab-surface.ts +158 -0
- package/src/server/responses/deferred-send-reporter.ts +25 -0
- package/src/server/responses/empty-completion-guard.ts +18 -2
- package/src/server/responses/fetch-helpers.ts +12 -1
- package/src/server/responses/jev-model-invoke.ts +1 -1
- package/src/server/responses/passthrough-delivery.ts +4 -1
- package/src/server/responses/passthrough-dispatch.ts +130 -64
- package/src/server/responses/passthrough-execution.ts +3 -3
- package/src/server/responses/request-prepare.ts +6 -4
- package/src/server/responses/request-transport.ts +131 -13
- package/src/server/responses/run-turn-execution.ts +13 -5
- package/src/server/responses/sidecar-execution.ts +16 -2
- package/src/server/responses/spend-storage-error.ts +33 -0
- package/src/server/responses/ws-upstream.ts +9 -1
- package/src/server/startup-action-control.ts +22 -2
- package/src/server/startup-health-cache.ts +4 -0
- package/src/server/system-env-shell.ts +7 -2
- package/src/server/system-env.ts +11 -1
- package/src/service/cli.ts +5 -1
- package/src/service/desktop-startup.ts +147 -34
- package/src/service/desktop-supervision.d.mts +27 -1
- package/src/service/desktop-supervision.mjs +111 -5
- package/src/service/health-sentinel-launch.ts +176 -0
- package/src/service/health-sentinel-main.ts +447 -0
- package/src/service/health-sentinel-policy.ts +241 -0
- package/src/service/health-sentinel-probe.ts +54 -0
- package/src/service/health-sentinel-windows.ts +87 -0
- package/src/service/linux-linger.ts +20 -0
- package/src/types/config.ts +5 -0
- package/src/types/request.ts +5 -0
- package/src/usage/expected-prices.ts +5 -1
- package/src/usage/log.ts +19 -1
- package/gui/dist/assets/App-Ds_qL7o8.js +0 -54
- package/gui/dist/assets/Tray-BqHF5Trd.js +0 -1
- package/gui/dist/assets/index-D3FBCNiP.js +0 -94
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { fetchCopilotAutoModels, copilotPickerModels, copilotRequiresAuto } from "../../providers/github-copilot-auto";
|
|
2
|
+
import { resolveGithubCopilotTransport } from "../../providers/github-copilot-transport";
|
|
3
|
+
import { resolveCopilotApiBaseUrl } from "../../oauth/github-copilot";
|
|
1
4
|
import { isAnthropicOAuthInstance } from "../../providers/anthropic-instance";
|
|
2
5
|
import { effectiveProviderAlias, effectiveProviderAliasDecision } from "../../providers/default-aliases";
|
|
3
6
|
import { initialModelSelectionPending } from "../../providers/initial-model-selection";
|
|
@@ -164,6 +167,7 @@ export function observedModelsAuthResolver(
|
|
|
164
167
|
},
|
|
165
168
|
};
|
|
166
169
|
}
|
|
170
|
+
/** Project captured discovery through config hints and retention; Auto-only Copilot exposes only Auto. */
|
|
167
171
|
export async function fetchProviderModelsWithAuth(
|
|
168
172
|
captured: CapturedProviderGather,
|
|
169
173
|
ttlMs: number,
|
|
@@ -282,6 +286,27 @@ export async function fetchProviderModelsWithAuth(
|
|
|
282
286
|
: { apiKey: await resolveModelsAuthToken(name, prov), observed: false }
|
|
283
287
|
: resolveAuth.resolve(name, prov));
|
|
284
288
|
const apiKey = auth.apiKey;
|
|
289
|
+
if (name === "github-copilot" && apiKey) {
|
|
290
|
+
const transport = resolveGithubCopilotTransport({ ...prov, apiKey }, prov.authMode === "oauth"
|
|
291
|
+
? resolveCopilotApiBaseUrl(auth.oauthApiBaseUrl) : undefined);
|
|
292
|
+
try {
|
|
293
|
+
const models = await fetchCopilotAutoModels(transport, AbortSignal.timeout(8_000), undefined, ttlMs);
|
|
294
|
+
const picker = copilotPickerModels(prov, models).map(/** Apply configured hints to the permission-filtered account catalog without changing upstream metadata. */
|
|
295
|
+
model => applyProviderConfigHints(name, prov, {
|
|
296
|
+
id: model.id, provider: name,
|
|
297
|
+
...catalogHintsFromModelsApiItem(name, model as ProviderModelsApiItem),
|
|
298
|
+
}, contextCap, metadataModelIdCaseFold, captured.effectiveAlias));
|
|
299
|
+
if (!isCurrentCacheGeneration()) return observed(configured, "degraded");
|
|
300
|
+
markProviderDiscoveryOk(name, picker.length);
|
|
301
|
+
// Retain callable manual/combo selectors, but do not advertise named selection
|
|
302
|
+
// for an Auto-only account even when its saved configuration still contains it.
|
|
303
|
+
return observed(copilotRequiresAuto(prov, models)
|
|
304
|
+
? picker : withConfiguredRetention(picker, { warnDrops: true }), "authoritative");
|
|
305
|
+
} catch {
|
|
306
|
+
if (isCurrentCacheGeneration()) markProviderDiscoveryFailed(name, { reason: "provider" });
|
|
307
|
+
return observed(configured, "degraded");
|
|
308
|
+
}
|
|
309
|
+
}
|
|
285
310
|
const maySendAnthropicDiscovery = () => {
|
|
286
311
|
if (!isAnthropicOAuthInstance(name) || prov.authMode !== "oauth") return true;
|
|
287
312
|
if (!mayResolveModelsOAuth(name, prov)) return false;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* macOS adapter for the Codex desktop-app restart.
|
|
3
3
|
*
|
|
4
|
-
* Measured shape (devlog/
|
|
4
|
+
* Measured shape (devlog/_fin/260913_cross_platform_desktop_app_restart/001_platform_topology.md):
|
|
5
5
|
*
|
|
6
6
|
* 15901 1 /Applications/ChatGPT.app/Contents/MacOS/ChatGPT
|
|
7
7
|
* 16733 15901 /Applications/ChatGPT.app/Contents/Resources/codex ... app-server ...
|
|
@@ -23,7 +23,7 @@
|
|
|
23
23
|
* it passes `allowHandoff: false` so it can only ever take the direct path or refuse.
|
|
24
24
|
* Recursion is structurally impossible rather than merely unlikely.
|
|
25
25
|
*
|
|
26
|
-
* Design: devlog/
|
|
26
|
+
* Design: devlog/_fin/260913_cross_platform_desktop_app_restart/020_phase2_detached_self_handoff.md
|
|
27
27
|
*/
|
|
28
28
|
import { spawn } from "node:child_process";
|
|
29
29
|
import { appendFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, unlinkSync, writeSync } from "node:fs";
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Linux adapter for the Codex desktop-app restart.
|
|
3
3
|
*
|
|
4
|
-
* Measured shape (devlog/
|
|
4
|
+
* Measured shape (devlog/_fin/260913_cross_platform_desktop_app_restart/001_platform_topology.md):
|
|
5
5
|
*
|
|
6
6
|
* /usr/bin/chatgpt -> /usr/lib/chatgpt/codex-launcher (2-line sh script)
|
|
7
7
|
* that execs /usr/lib/chatgpt/ChatGPT
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
* itself. An unconditional unlink would let a late release destroy somebody else's
|
|
23
23
|
* live lock, which is exactly the mutual exclusion this file exists to provide.
|
|
24
24
|
*
|
|
25
|
-
* Design: `devlog/
|
|
25
|
+
* Design: `devlog/_fin/260913_cross_platform_desktop_app_restart/020_phase2_detached_self_handoff.md` §4.1.
|
|
26
26
|
*/
|
|
27
27
|
import { mkdirSync, openSync, closeSync, writeSync, readFileSync, unlinkSync, renameSync } from "node:fs";
|
|
28
28
|
import { join, dirname } from "node:path";
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* PID-reuse and fail-closed reasoning from being re-derived three times, once per
|
|
8
8
|
* operating system, with two of the three getting it subtly wrong.
|
|
9
9
|
*
|
|
10
|
-
* Design and audit history: `devlog/
|
|
10
|
+
* Design and audit history: `devlog/_fin/260913_cross_platform_desktop_app_restart/`.
|
|
11
11
|
*/
|
|
12
12
|
import { sep } from "node:path";
|
|
13
13
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* AUMID. The shape is DesktopAppAdapter so the ladder, not this file, owns
|
|
8
8
|
* PID-reuse re-verification and the fail-closed sequencing.
|
|
9
9
|
*
|
|
10
|
-
* Measured (devlog/
|
|
10
|
+
* Measured (devlog/_fin/260913_cross_platform_desktop_app_restart/001_platform_topology.md §3):
|
|
11
11
|
* OpenAI.Codex MSIX, ChatGPT.exe, InstallLocation under WindowsApps.
|
|
12
12
|
*/
|
|
13
13
|
import { execFileSync } from "node:child_process";
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
* never reported as "nothing to do". A stale picker is a much smaller problem than a
|
|
30
30
|
* wrongly killed process.
|
|
31
31
|
*
|
|
32
|
-
* Design and audit history: devlog/
|
|
32
|
+
* Design and audit history: devlog/_fin/260913_cross_platform_desktop_app_restart/.
|
|
33
33
|
*/
|
|
34
34
|
import {
|
|
35
35
|
acquireDesktopRestartLock,
|
|
@@ -510,8 +510,9 @@ async function resolveMainAccountToken(
|
|
|
510
510
|
}
|
|
511
511
|
const refresh = dependencies.refreshToken ?? refreshNativeMainGrant;
|
|
512
512
|
let refreshed: OAuthCredentials;
|
|
513
|
+
if (signal.aborted) throw signal.reason;
|
|
513
514
|
try {
|
|
514
|
-
refreshed = await refresh(locked.refreshToken, { signal });
|
|
515
|
+
refreshed = await refresh(locked.refreshToken, { signal: refreshTimeout });
|
|
515
516
|
} catch (cause) {
|
|
516
517
|
// A refusal already classified at the endpoint keeps that verdict. An injected refresh
|
|
517
518
|
// threw an ordinary Error and has only its message, so prose still decides there --
|
|
@@ -535,16 +536,14 @@ async function resolveMainAccountToken(
|
|
|
535
536
|
}
|
|
536
537
|
throw new MainAccountTokenRefreshError(reason, { cause });
|
|
537
538
|
}
|
|
538
|
-
//
|
|
539
|
-
//
|
|
540
|
-
//
|
|
541
|
-
// of a request that no longer exists -- the same fence the reauth twin applies
|
|
542
|
-
// before its own commit above.
|
|
543
|
-
if (dependencies.signal?.aborted) throw dependencies.signal.reason;
|
|
539
|
+
// Once the exchange starts, only its own timeout may abort it: a successful
|
|
540
|
+
// rotating grant retires the old refresh token. Save the only live grant before
|
|
541
|
+
// honoring caller cancellation, while retaining the external-writer guards.
|
|
544
542
|
const result = persistRefreshedMainAuthJson(locked, refreshed);
|
|
545
543
|
// A permitted in-flight success speaks only for its own profile and grant.
|
|
546
544
|
clearMainRefreshGrantRejection(mainRefreshGrantKey(locked)!);
|
|
547
545
|
if (dependencies.preserveReauth !== true) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
546
|
+
if (dependencies.signal?.aborted) throw dependencies.signal.reason;
|
|
548
547
|
return result;
|
|
549
548
|
}),
|
|
550
549
|
{ waitMs: 30_000, signal },
|
|
@@ -20,6 +20,7 @@ import { withNativeMainExclusiveClaim } from "./native-main-claim";
|
|
|
20
20
|
import { scrubNativeMainAuthTempResidues } from "./native-main-auth-temp";
|
|
21
21
|
import { NATIVE_STAGE_SWEEP_INTERVAL_MS } from "./native-profile-stage-store";
|
|
22
22
|
import type { NativeCodexOwnership } from "../integrations/native/ownership-preflight";
|
|
23
|
+
import { isPassiveRead } from "../lib/passive-read";
|
|
23
24
|
|
|
24
25
|
export type NativeMainStartupGateSnapshot =
|
|
25
26
|
| { status: "ready"; homeId: string | null }
|
|
@@ -701,7 +702,9 @@ export async function releaseNativeMainStartupLifecycle(server: object): Promise
|
|
|
701
702
|
|
|
702
703
|
export function isNativeMainTrafficBlocked(): boolean {
|
|
703
704
|
const reason = activeServiceOwnershipBlockReason();
|
|
704
|
-
|
|
705
|
+
// A passive read (a Remote Link Child's, #6959) reports the gate as it stands: re-inspecting
|
|
706
|
+
// service ownership can activate the native-main lifecycle and its recovery on this machine.
|
|
707
|
+
if (reason !== null && !isPassiveRead() && reprobeServiceOwnership(reason)) {
|
|
705
708
|
// The host became ownable after boot (#2108): the fence lifts here rather than
|
|
706
709
|
// waiting for the restart the reporter had to perform by hand.
|
|
707
710
|
return activeServiceOwnershipBlockReason() !== null || snapshot.status === "blocked";
|
|
@@ -318,7 +318,7 @@ export function recoverIfNeeded(
|
|
|
318
318
|
};
|
|
319
319
|
}
|
|
320
320
|
try {
|
|
321
|
-
publishConfigWrite(expectedTargets.configPath, held, (destination, hooks) => restore(destination, record.preConfigBytes, hooks));
|
|
321
|
+
publishConfigWrite(expectedTargets.configPath, held, (destination, hooks) => restore(destination, record.preConfigBytes, expectingImage(hooks, record.postConfig)));
|
|
322
322
|
} catch (error) {
|
|
323
323
|
return {
|
|
324
324
|
ok: false,
|
|
@@ -336,7 +336,7 @@ export function recoverIfNeeded(
|
|
|
336
336
|
};
|
|
337
337
|
}
|
|
338
338
|
try {
|
|
339
|
-
publishConfigWriteTarget(expectedTargets.configPath, held, expectedTargets.storePath, (destination, hooks) => restore(destination, record.preStoreBytes, hooks));
|
|
339
|
+
publishConfigWriteTarget(expectedTargets.configPath, held, expectedTargets.storePath, (destination, hooks) => restore(destination, record.preStoreBytes, expectingImage(hooks, record.postStore)));
|
|
340
340
|
} catch (error) {
|
|
341
341
|
return {
|
|
342
342
|
ok: false,
|
|
@@ -363,3 +363,21 @@ function restore(path: string, bytes: string | null, hooks: AtomicWriteHooks): v
|
|
|
363
363
|
if (bytes === null) durableDelete(path, hooks);
|
|
364
364
|
else durableWrite(path, bytes, hooks);
|
|
365
365
|
}
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Bind a write to the image the caller classified. The rename retry hooks check
|
|
369
|
+
* path and inode identity only; an in-place edit by another process during a
|
|
370
|
+
* Windows sharing-violation retry keeps the inode, so without this re-read a
|
|
371
|
+
* later attempt would replace newer bytes. Checked before every attempt.
|
|
372
|
+
*/
|
|
373
|
+
export function expectingImage(hooks: AtomicWriteHooks, expectedHash: string): AtomicWriteHooks {
|
|
374
|
+
return {
|
|
375
|
+
...hooks,
|
|
376
|
+
validateBeforeRename: (targetPath: string) => {
|
|
377
|
+
hooks.validateBeforeRename?.(targetPath);
|
|
378
|
+
if (hashBytes(readOrNull(targetPath)) !== expectedHash) {
|
|
379
|
+
throw new Error(targetPath + " changed during the write; refusing to overwrite it. Files and recovery evidence were preserved.");
|
|
380
|
+
}
|
|
381
|
+
},
|
|
382
|
+
};
|
|
383
|
+
}
|
|
@@ -119,3 +119,52 @@ export function decodeTomlBasicString(literal: string): string | null {
|
|
|
119
119
|
}
|
|
120
120
|
return out;
|
|
121
121
|
}
|
|
122
|
+
|
|
123
|
+
/** Shared scalar assignment matching; quoted and escaped keys retain their spelling. */
|
|
124
|
+
export function matchKeyHead(line: string, key: string): { prefix: string; rest: string } | null {
|
|
125
|
+
const m = /^\s*("(?:[^"\\]|\\.)*"|'[^']*'|[A-Za-z0-9_-]+)\s*=\s*/.exec(line);
|
|
126
|
+
if (!m) return null;
|
|
127
|
+
const token = m[1]!;
|
|
128
|
+
const decoded = token.startsWith('"') ? decodeTomlBasicString(token)
|
|
129
|
+
: token.startsWith("'") ? token.slice(1, -1) : token;
|
|
130
|
+
return decoded === key ? { prefix: m[0], rest: line.slice(m[0].length) } : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* For each line, whether it begins outside every string and composite span, or
|
|
135
|
+
* `null` when a span never closes and the scope of later lines is unknown.
|
|
136
|
+
*
|
|
137
|
+
* Line matchers for assignments and table headers must only look at lines that
|
|
138
|
+
* start at top level: `"model_instructions_file" = "x"` written inside a
|
|
139
|
+
* `"""…"""` value or a multi-line array is prose, not syntax. Readers and
|
|
140
|
+
* editors share this so a write and the snapshot read back agree.
|
|
141
|
+
*/
|
|
142
|
+
export function lexicalLineStarts(lines: readonly string[]): boolean[] | null {
|
|
143
|
+
const starts: boolean[] = [];
|
|
144
|
+
let open: string | null = null;
|
|
145
|
+
let depth = 0;
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
starts.push(open === null && depth === 0);
|
|
148
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
149
|
+
const char = line[i]!;
|
|
150
|
+
if (open !== null) {
|
|
151
|
+
if (char === "\\" && (open === '"' || open === '"""')) { i += 1; continue; }
|
|
152
|
+
if (char !== open[0]) continue;
|
|
153
|
+
if (open.length === 1) { open = null; continue; }
|
|
154
|
+
let run = 0;
|
|
155
|
+
while (line[i + run] === char) run += 1;
|
|
156
|
+
// Up to two quotes may sit inside the value just before the closing three.
|
|
157
|
+
if (run >= 3) open = null;
|
|
158
|
+
i += run - 1;
|
|
159
|
+
} else if (char === "#") break;
|
|
160
|
+
else if (char === '"' || char === "'") {
|
|
161
|
+
open = line.startsWith(char.repeat(3), i) ? char.repeat(3) : char;
|
|
162
|
+
i += open.length - 1;
|
|
163
|
+
} else if (char === "[" || char === "{") depth += 1;
|
|
164
|
+
else if (char === "]" || char === "}") { depth -= 1; if (depth < 0) return null; }
|
|
165
|
+
}
|
|
166
|
+
// Single-line strings cannot cross a newline.
|
|
167
|
+
if (open !== null && open.length === 1) return null;
|
|
168
|
+
}
|
|
169
|
+
return open === null && depth === 0 ? starts : null;
|
|
170
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { closeSync, constants, fstatSync, openSync, readSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
// CRLF normalization can halve the byte count. Every body within 64 KiB
|
|
4
|
+
// therefore fits this raw ceiling; title bytes stay outside the body budget.
|
|
5
|
+
const MAX_IMPORT_SOURCE_BYTES = 2 * 64 * 1024;
|
|
6
|
+
|
|
7
|
+
/** The prompt-text-probe descriptor pattern: nonblocking, regular, bounded. */
|
|
8
|
+
export function readBaseImportSource(path: string): string {
|
|
9
|
+
const nonBlocking = (constants as { O_NONBLOCK?: number }).O_NONBLOCK ?? 0;
|
|
10
|
+
const descriptor = openSync(path, constants.O_RDONLY | nonBlocking);
|
|
11
|
+
try {
|
|
12
|
+
if (!fstatSync(descriptor).isFile()) throw new Error("prompt source is not a regular file");
|
|
13
|
+
const parts: Buffer[] = [];
|
|
14
|
+
const view = Buffer.allocUnsafe(8192);
|
|
15
|
+
let total = 0;
|
|
16
|
+
for (;;) {
|
|
17
|
+
const count = readSync(descriptor, view, 0, Math.min(view.length, MAX_IMPORT_SOURCE_BYTES + 1 - total), total);
|
|
18
|
+
if (count === 0) return Buffer.concat(parts).toString("utf8");
|
|
19
|
+
total += count;
|
|
20
|
+
if (total > MAX_IMPORT_SOURCE_BYTES) {
|
|
21
|
+
const error = new Error("prompt source exceeds the import byte ceiling") as NodeJS.ErrnoException;
|
|
22
|
+
error.code = "EFBIG";
|
|
23
|
+
throw error;
|
|
24
|
+
}
|
|
25
|
+
parts.push(Buffer.from(view.subarray(0, count)));
|
|
26
|
+
}
|
|
27
|
+
} finally {
|
|
28
|
+
closeSync(descriptor);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { OCX_SECTION_MARKER } from "../injected-marker";
|
|
2
|
-
import { encodeBasicString } from "./encoding";
|
|
2
|
+
import { encodeBasicString, lexicalLineStarts, matchKeyHead } from "./encoding";
|
|
3
3
|
import { TABLE_HEADER, ANY_DEV_INSTRUCTIONS, DEV_INSTRUCTIONS_KEY } from "./toml-read";
|
|
4
4
|
|
|
5
5
|
/** Line editing, not re-serialization: the user's comments and layout survive. */
|
|
@@ -37,28 +37,156 @@ function joinLines(lines: string[], eol: "\r\n" | "\n"): string {
|
|
|
37
37
|
return eol === "\n" ? text : text.replace(/\n/g, "\r\n");
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function firstTableIndex(lines: string[]): number {
|
|
41
|
-
const idx = lines.findIndex(l => TABLE_HEADER.test(l));
|
|
40
|
+
function firstTableIndex(lines: string[], starts?: readonly boolean[]): number {
|
|
41
|
+
const idx = lines.findIndex((l, i) => (starts === undefined || starts[i] === true) && TABLE_HEADER.test(l));
|
|
42
42
|
return idx === -1 ? lines.length : idx;
|
|
43
43
|
}
|
|
44
44
|
|
|
45
|
-
/**
|
|
46
|
-
|
|
45
|
+
/**
|
|
46
|
+
* Raised when a recognized `key =` line carries a value a line editor cannot
|
|
47
|
+
* replace safely — a multi-line `"""…"""`/`'''…'''` span, an unterminated
|
|
48
|
+
* quote, or trailing junk that is neither value nor comment. Callers refuse
|
|
49
|
+
* BEFORE writing rather than splice over a span they cannot see the end of.
|
|
50
|
+
*/
|
|
51
|
+
export class UnsupportedTomlForm extends Error {
|
|
52
|
+
constructor(readonly line: string) {
|
|
53
|
+
super(`unsupported TOML value form: ${line.trim()}`);
|
|
54
|
+
this.name = "UnsupportedTomlForm";
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Where `<key> =` ends and the value begins. `prefix` keeps indent and key quoting. */
|
|
59
|
+
interface AssignmentHead {
|
|
60
|
+
prefix: string;
|
|
61
|
+
rest: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Lexical line scope for the editors below (see `lexicalLineStarts`). A span
|
|
66
|
+
* that never closes means the scope of every later line is unknown, so the
|
|
67
|
+
* whole edit refuses instead of guessing.
|
|
68
|
+
*/
|
|
69
|
+
function scopedLineStarts(lines: readonly string[]): boolean[] {
|
|
70
|
+
const starts = lexicalLineStarts(lines);
|
|
71
|
+
if (starts === null) throw new UnsupportedTomlForm("configuration contains an unterminated string or composite value");
|
|
72
|
+
return starts;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Split `rest` (everything after `key =`) into the value token and a trailing
|
|
77
|
+
* `#…` comment — adjacent or whitespace-separated, both preserved.
|
|
78
|
+
*
|
|
79
|
+
* The scan is string-aware so a `#` inside a quoted value is never mistaken
|
|
80
|
+
* for a comment opener: `"a#b" # note` parses as value `"a#b"` + comment
|
|
81
|
+
* ` # note`, while `false# note` parses as `false` + `# note`. Anything the
|
|
82
|
+
* token scan cannot classify — a multi-line string opener, an unterminated
|
|
83
|
+
* quote, text after the value that is not a comment — throws
|
|
84
|
+
* UnsupportedTomlForm so the caller refuses rather than guesses a span.
|
|
85
|
+
*/
|
|
86
|
+
function splitValueComment(head: AssignmentHead, line: string): { value: string; comment: string } {
|
|
87
|
+
const { rest } = head;
|
|
88
|
+
if (rest.startsWith('"""') || rest.startsWith("'''")) throw new UnsupportedTomlForm(line);
|
|
89
|
+
let value: string;
|
|
90
|
+
const first = rest[0];
|
|
91
|
+
if (first === '"') {
|
|
92
|
+
// Basic string: an escaped quote does not close, an unterminated one refuses.
|
|
93
|
+
let i = 1;
|
|
94
|
+
while (i < rest.length && rest[i] !== '"') i += rest[i] === "\\" ? 2 : 1;
|
|
95
|
+
if (i >= rest.length) throw new UnsupportedTomlForm(line);
|
|
96
|
+
value = rest.slice(0, i + 1);
|
|
97
|
+
} else if (first === "'") {
|
|
98
|
+
// Literal string: no escapes, the next ' closes it.
|
|
99
|
+
const end = rest.indexOf("'", 1);
|
|
100
|
+
if (end === -1) throw new UnsupportedTomlForm(line);
|
|
101
|
+
value = rest.slice(0, end + 1);
|
|
102
|
+
} else {
|
|
103
|
+
// Composite values may continue onto another line. This scalar editor
|
|
104
|
+
// refuses them instead of replacing only their opener and orphaning the tail.
|
|
105
|
+
if (first === "[" || first === "{") throw new UnsupportedTomlForm(line);
|
|
106
|
+
const m = /^[^\s#]+/.exec(rest);
|
|
107
|
+
if (!m) throw new UnsupportedTomlForm(line);
|
|
108
|
+
value = m[0];
|
|
109
|
+
}
|
|
110
|
+
const tail = rest.slice(value.length);
|
|
111
|
+
if (tail !== "" && !/^\s*(?:#|$)/.test(tail)) throw new UnsupportedTomlForm(line);
|
|
112
|
+
return { value, comment: tail };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The shape of `key`'s root-scope assignment, or "absent". "unsupported" means
|
|
117
|
+
* a `key =` line was found but its value is a form the line editor refuses —
|
|
118
|
+
* multiline strings and the like — so a caller can fail BEFORE touching disk
|
|
119
|
+
* instead of inside the transaction.
|
|
120
|
+
*/
|
|
121
|
+
export function rootKeyValueForm(content: string, key: string): "absent" | "simple" | "unsupported" {
|
|
122
|
+
const { body } = splitBom(content);
|
|
123
|
+
const lines = splitLines(body);
|
|
124
|
+
let starts: boolean[];
|
|
125
|
+
try { starts = scopedLineStarts(lines); } catch { return "unsupported"; }
|
|
126
|
+
const limit = firstTableIndex(lines, starts);
|
|
127
|
+
for (let i = 0; i < limit; i += 1) {
|
|
128
|
+
if (!starts[i]) continue;
|
|
129
|
+
const head = matchKeyHead(lines[i]!, key);
|
|
130
|
+
if (!head) continue;
|
|
131
|
+
try {
|
|
132
|
+
splitValueComment(head, lines[i]!);
|
|
133
|
+
return "simple";
|
|
134
|
+
} catch {
|
|
135
|
+
return "unsupported";
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return "absent";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Replace or remove one matched line. The line's own indentation, key quoting,
|
|
143
|
+
* and trailing comment are preserved; `null` removes the assignment but keeps
|
|
144
|
+
* a comment behind so a `# note` written beside the key survives a reset.
|
|
145
|
+
*/
|
|
146
|
+
function applyLineEdit(lines: string[], i: number, key: string, replacement: string | null): void {
|
|
147
|
+
const head = matchKeyHead(lines[i]!, key)!;
|
|
148
|
+
const { comment } = splitValueComment(head, lines[i]!);
|
|
149
|
+
if (replacement === null) {
|
|
150
|
+
if (comment.trim()) lines[i] = comment.trim();
|
|
151
|
+
else lines.splice(i, 1);
|
|
152
|
+
} else {
|
|
153
|
+
lines[i] = `${head.prefix}${replacement}${comment}`;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type EditMode = "inplace" | "appended" | "noop";
|
|
158
|
+
interface LineEdit { text: string; mode: EditMode }
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Set a root-scope boolean, inserting above the first table when absent.
|
|
162
|
+
*
|
|
163
|
+
* `null` REMOVES the key rather than writing a value, which is what restoring a
|
|
164
|
+
* documented default means: `include_permissions_instructions = true` and an
|
|
165
|
+
* absent key are different facts about the same file, and only the absent one
|
|
166
|
+
* lets a changed upstream default ever take effect again.
|
|
167
|
+
*
|
|
168
|
+
* The value slot is classified, not just skipped over: any value form the line
|
|
169
|
+
* editor cannot see the end of — a multi-line string span, an unterminated
|
|
170
|
+
* quote — throws UnsupportedTomlForm so the write is refused instead of
|
|
171
|
+
* appending a second assignment TOML would reject as a duplicate key. A value
|
|
172
|
+
* that is not a boolean is a bad fact about the same key, so it is REPLACED,
|
|
173
|
+
* and a trailing comment survives a write or a removal whether or not a space
|
|
174
|
+
* precedes the `#`.
|
|
175
|
+
*/
|
|
176
|
+
function setRootBoolLines(content: string, key: string, value: boolean | null): LineEdit {
|
|
47
177
|
const eol = dominantEol(content);
|
|
48
178
|
const { bom, body } = splitBom(content);
|
|
49
179
|
const lines = splitLines(body);
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
const pattern = new RegExp(`^(\\s*${escaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
|
|
180
|
+
const starts = scopedLineStarts(lines);
|
|
181
|
+
const limit = firstTableIndex(lines, starts);
|
|
53
182
|
for (let i = 0; i < limit; i += 1) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return bom + joinLines(lines, eol);
|
|
58
|
-
}
|
|
183
|
+
if (!starts[i] || !matchKeyHead(lines[i]!, key)) continue;
|
|
184
|
+
applyLineEdit(lines, i, key, value === null ? null : String(value));
|
|
185
|
+
return { text: bom + joinLines(lines, eol), mode: "inplace" };
|
|
59
186
|
}
|
|
187
|
+
if (value === null) return { text: bom + joinLines(lines, eol), mode: "noop" };
|
|
60
188
|
lines.splice(limit, 0, `${key} = ${value}`);
|
|
61
|
-
return bom + joinLines(lines, eol);
|
|
189
|
+
return { text: bom + joinLines(lines, eol), mode: "appended" };
|
|
62
190
|
}
|
|
63
191
|
|
|
64
192
|
/**
|
|
@@ -68,51 +196,56 @@ export function setRootBool(content: string, key: string, value: boolean): strin
|
|
|
68
196
|
* rather than an empty string: `model_instructions_file = ""` is a path Codex would try
|
|
69
197
|
* to read, not an absent setting.
|
|
70
198
|
*/
|
|
71
|
-
|
|
199
|
+
function setRootStringLines(content: string, key: string, value: string | null): LineEdit {
|
|
72
200
|
const eol = dominantEol(content);
|
|
73
201
|
const { bom, body } = splitBom(content);
|
|
74
202
|
const lines = splitLines(body);
|
|
75
|
-
const
|
|
76
|
-
const
|
|
77
|
-
const pattern = new RegExp(`^\\s*${escaped}\\s*=\\s*"[^"]*"\\s*(?:#.*)?$`);
|
|
203
|
+
const starts = scopedLineStarts(lines);
|
|
204
|
+
const limit = firstTableIndex(lines, starts);
|
|
78
205
|
for (let i = 0; i < limit; i += 1) {
|
|
79
|
-
if (!
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
return bom + joinLines(lines, eol);
|
|
206
|
+
if (!starts[i] || !matchKeyHead(lines[i]!, key)) continue;
|
|
207
|
+
applyLineEdit(lines, i, key, value === null ? null : encodeBasicString(value));
|
|
208
|
+
return { text: bom + joinLines(lines, eol), mode: "inplace" };
|
|
83
209
|
}
|
|
84
|
-
if (value === null) return bom + joinLines(lines, eol);
|
|
210
|
+
if (value === null) return { text: bom + joinLines(lines, eol), mode: "noop" };
|
|
85
211
|
lines.splice(limit, 0, `${key} = ${encodeBasicString(value)}`);
|
|
86
|
-
return bom + joinLines(lines, eol);
|
|
212
|
+
return { text: bom + joinLines(lines, eol), mode: "appended" };
|
|
87
213
|
}
|
|
88
214
|
|
|
89
|
-
/**
|
|
90
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Set a boolean inside `[table]`, appending the table when absent.
|
|
217
|
+
*
|
|
218
|
+
* `null` removes the key line but leaves the table header: an empty `[skills]`
|
|
219
|
+
* is valid TOML, and deleting the header would also orphan any comments the
|
|
220
|
+
* user wrote inside the table.
|
|
221
|
+
*/
|
|
222
|
+
function setTableBoolLines(content: string, table: string, key: string, value: boolean | null): LineEdit {
|
|
91
223
|
const eol = dominantEol(content);
|
|
92
224
|
const { bom, body } = splitBom(content);
|
|
93
225
|
const lines = splitLines(body);
|
|
226
|
+
const starts = scopedLineStarts(lines);
|
|
94
227
|
const escaped = table.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
95
|
-
const
|
|
228
|
+
const header = new RegExp(`^\\s*\\[${escaped}\\]\\s*(?:#.*)?$`);
|
|
229
|
+
const start = lines.findIndex((l, i) => starts[i] === true && header.test(l));
|
|
96
230
|
if (start === -1) {
|
|
231
|
+
if (value === null) return { text: bom + joinLines(lines, eol), mode: "noop" };
|
|
97
232
|
const tail = lines.length > 0 && lines[lines.length - 1] === "" ? lines.length - 1 : lines.length;
|
|
98
233
|
lines.splice(tail, 0, `[${table}]`, `${key} = ${value}`);
|
|
99
|
-
return bom + joinLines(lines, eol);
|
|
234
|
+
return { text: bom + joinLines(lines, eol), mode: "appended" };
|
|
100
235
|
}
|
|
101
|
-
const keyEscaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
102
|
-
const pattern = new RegExp(`^(\\s*${keyEscaped}\\s*=\\s*)(?:true|false)(\\s*(?:#.*)?)$`);
|
|
103
236
|
let end = start + 1;
|
|
104
|
-
while (end < lines.length && !TABLE_HEADER.test(lines[end]!)) end += 1;
|
|
237
|
+
while (end < lines.length && !(starts[end] && TABLE_HEADER.test(lines[end]!))) end += 1;
|
|
105
238
|
for (let i = start + 1; i < end; i += 1) {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
return bom + joinLines(lines, eol);
|
|
110
|
-
}
|
|
239
|
+
if (!starts[i] || !matchKeyHead(lines[i]!, key)) continue;
|
|
240
|
+
applyLineEdit(lines, i, key, value === null ? null : String(value));
|
|
241
|
+
return { text: bom + joinLines(lines, eol), mode: "inplace" };
|
|
111
242
|
}
|
|
243
|
+
if (value === null) return { text: bom + joinLines(lines, eol), mode: "noop" };
|
|
112
244
|
lines.splice(end, 0, `${key} = ${value}`);
|
|
113
|
-
return bom + joinLines(lines, eol);
|
|
245
|
+
return { text: bom + joinLines(lines, eol), mode: "appended" };
|
|
114
246
|
}
|
|
115
247
|
|
|
248
|
+
|
|
116
249
|
/**
|
|
117
250
|
* Replace, insert, or remove the generated two-line block. Canonical form is
|
|
118
251
|
* marker + assignment at the top of the document; replacement is "find the
|
|
@@ -161,3 +294,102 @@ export function removeUnownedProjection(content: string): string {
|
|
|
161
294
|
}
|
|
162
295
|
return joinLines(lines, eol);
|
|
163
296
|
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Refuse, before any write, an edit that does not do exactly what it was asked.
|
|
300
|
+
*
|
|
301
|
+
* The line editors above address bare table headers and bare or quoted keys. A
|
|
302
|
+
* quoted, spaced, inline or dotted spelling of the same table makes them append a
|
|
303
|
+
* duplicate header (a file Codex cannot load) or, for restore-default, leave the
|
|
304
|
+
* override in place while the caller reports success. Both are caught by parsing
|
|
305
|
+
* the before and after images: the parsed result must equal the parsed input with
|
|
306
|
+
* only the intended path set or removed. When Bun cannot parse the input (its
|
|
307
|
+
* parser has documented gaps a valid Codex config can hit, such as i64 extremes),
|
|
308
|
+
* only an in-place replacement of a key the editor located is trusted; an append
|
|
309
|
+
* or a no-op restore cannot be verified and is refused. A target value with
|
|
310
|
+
* control characters is checked for presence and type only, for the same reason.
|
|
311
|
+
*/
|
|
312
|
+
function checkedEdit(before: string, edit: LineEdit, path: readonly string[], value: string | boolean | null): string {
|
|
313
|
+
const after = edit.text;
|
|
314
|
+
let parsedBefore: Record<string, unknown>;
|
|
315
|
+
try { parsedBefore = Bun.TOML.parse(before.replace(/^\ufeff/, "")) as Record<string, unknown>; } catch {
|
|
316
|
+
if (edit.mode === "inplace") return after;
|
|
317
|
+
throw new UnsupportedTomlForm("config.toml uses syntax this editor cannot verify; edit it by hand");
|
|
318
|
+
}
|
|
319
|
+
let parsedAfter: Record<string, unknown>;
|
|
320
|
+
try { parsedAfter = Bun.TOML.parse(after.replace(/^\ufeff/, "")) as Record<string, unknown>; } catch {
|
|
321
|
+
throw new UnsupportedTomlForm("edit would leave config.toml unparseable");
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
const expected = tomlClone(parsedBefore) as Record<string, unknown>;
|
|
325
|
+
let parent: Record<string, unknown> | null = expected;
|
|
326
|
+
let afterParent: unknown = parsedAfter;
|
|
327
|
+
for (const segment of path.slice(0, -1)) {
|
|
328
|
+
const next: unknown = parent?.[segment];
|
|
329
|
+
afterParent = isPlainTable(afterParent) ? afterParent[segment] : undefined;
|
|
330
|
+
if (next === undefined) {
|
|
331
|
+
if (value === null) { parent = null; break; }
|
|
332
|
+
parent![segment] = {};
|
|
333
|
+
parent = parent![segment] as Record<string, unknown>;
|
|
334
|
+
continue;
|
|
335
|
+
}
|
|
336
|
+
if (!isPlainTable(next)) throw new UnsupportedTomlForm("edit target is not a plain table");
|
|
337
|
+
parent = next;
|
|
338
|
+
}
|
|
339
|
+
const key = path[path.length - 1]!;
|
|
340
|
+
if (parent !== null) {
|
|
341
|
+
if (value === null) delete parent[key];
|
|
342
|
+
else if (typeof value === "string" && /[\u0000-\u001f\u007f]/.test(value)) {
|
|
343
|
+
const landed = isPlainTable(afterParent) ? afterParent[key] : undefined;
|
|
344
|
+
if (typeof landed !== "string") throw new UnsupportedTomlForm("edit did not land on the intended key");
|
|
345
|
+
parent[key] = landed;
|
|
346
|
+
} else parent[key] = value;
|
|
347
|
+
}
|
|
348
|
+
if (!tomlEqual(parsedAfter, expected)) throw new UnsupportedTomlForm("edit did not land on the intended key");
|
|
349
|
+
} catch (error) {
|
|
350
|
+
if (error instanceof UnsupportedTomlForm) throw error;
|
|
351
|
+
throw new UnsupportedTomlForm("edit could not be verified");
|
|
352
|
+
}
|
|
353
|
+
return after;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function isPlainTable(value: unknown): value is Record<string, unknown> {
|
|
357
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
|
|
358
|
+
const proto = Object.getPrototypeOf(value);
|
|
359
|
+
return proto === Object.prototype || proto === null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
/** Arrays and plain tables are copied; TOML date/time values are immutable and shared. */
|
|
363
|
+
function tomlClone(value: unknown): unknown {
|
|
364
|
+
if (Array.isArray(value)) return value.map(tomlClone);
|
|
365
|
+
if (isPlainTable(value)) return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, tomlClone(v)]));
|
|
366
|
+
return value;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function tomlEqual(a: unknown, b: unknown): boolean {
|
|
370
|
+
if (Object.is(a, b)) return true;
|
|
371
|
+
if (Array.isArray(a) || Array.isArray(b)) {
|
|
372
|
+
return Array.isArray(a) && Array.isArray(b) && a.length === b.length && a.every((v, i) => tomlEqual(v, b[i]));
|
|
373
|
+
}
|
|
374
|
+
if (isPlainTable(a) || isPlainTable(b)) {
|
|
375
|
+
if (!isPlainTable(a) || !isPlainTable(b)) return false;
|
|
376
|
+
const ka = Object.keys(a), kb = Object.keys(b);
|
|
377
|
+
return ka.length === kb.length && ka.every(k => Object.hasOwn(b, k) && tomlEqual(a[k], b[k]));
|
|
378
|
+
}
|
|
379
|
+
if (a !== null && b !== null && typeof a === "object" && typeof b === "object") {
|
|
380
|
+
return Object.getPrototypeOf(a) === Object.getPrototypeOf(b) && String(a) === String(b);
|
|
381
|
+
}
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export function setRootBool(content: string, key: string, value: boolean | null): string {
|
|
386
|
+
return checkedEdit(content, setRootBoolLines(content, key, value), [key], value);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
export function setRootString(content: string, key: string, value: string | null): string {
|
|
390
|
+
return checkedEdit(content, setRootStringLines(content, key, value), [key], value);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
export function setTableBool(content: string, table: string, key: string, value: boolean | null): string {
|
|
394
|
+
return checkedEdit(content, setTableBoolLines(content, table, key, value), [...table.split("."), key], value);
|
|
395
|
+
}
|