@bitkyc08/opencodex 2.42.0 → 2.43.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/AGENTS_INSTALL.md +2 -2
- package/README.md +31 -0
- package/bin/ocx.mjs +10 -1
- package/gui/dist/assets/index-DS1NE4Jn.css +1 -0
- package/gui/dist/assets/index-Djowl68T.js +112 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/anthropic-image-codec.ts +304 -0
- package/src/adapters/anthropic-image-normalize.ts +8 -298
- package/src/adapters/anthropic.ts +30 -7
- package/src/adapters/command-code.ts +7 -5
- package/src/adapters/cursor/desktop-executor-contract.ts +15 -0
- package/src/adapters/cursor/images.ts +36 -6
- package/src/adapters/cursor/live-transport.ts +7 -2
- package/src/adapters/cursor/native-exec-desktop.ts +2 -15
- package/src/adapters/cursor/protobuf-request.ts +13 -3
- package/src/adapters/cursor/tool-definitions.ts +5 -670
- package/src/adapters/cursor/tool-guidance.ts +236 -0
- package/src/adapters/cursor/tool-naming.ts +252 -0
- package/src/adapters/cursor/tool-schemas.ts +195 -0
- package/src/adapters/cursor/types.ts +6 -3
- package/src/adapters/exec-tool-result-normalize.ts +1 -1
- package/src/adapters/google-errors.ts +9 -1
- package/src/adapters/google.ts +1 -0
- package/src/adapters/kiro-calibration.ts +181 -0
- package/src/adapters/kiro.ts +135 -3
- package/src/adapters/openai-responses.ts +216 -26
- package/src/adapters/responses-code-mode.ts +59 -0
- package/src/adapters/tool-catalog-nudge.ts +1 -1
- package/src/adapters/xai-schema-analysis.ts +86 -0
- package/src/adapters/xai-tool-schema.ts +2 -87
- package/src/adapters/xai-web-search.ts +1 -1
- package/src/bridge.ts +22 -10
- package/src/chat/inbound.ts +11 -3
- package/src/claude/inbound-content-options.ts +60 -0
- package/src/claude/inbound-model-options.ts +142 -0
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +10 -202
- package/src/claude/model-info.ts +45 -0
- package/src/cli/account-auth.ts +21 -6
- package/src/cli/capabilities.ts +13 -2
- package/src/cli/claude.ts +232 -39
- package/src/cli/config-command.ts +9 -1
- package/src/cli/dispatch.ts +5 -1
- package/src/cli/doctor.ts +10 -0
- package/src/cli/effort.ts +372 -0
- package/src/cli/export-command.ts +3 -9
- package/src/cli/help.ts +1 -0
- package/src/cli/index.ts +13 -0
- package/src/cli/init.ts +4 -0
- package/src/cli/model-selection-guidance.ts +30 -0
- package/src/cli/models-runtime.ts +3 -2
- package/src/cli/models.ts +8 -3
- package/src/cli/opencode.ts +4 -1
- package/src/cli/provider-runtime.ts +65 -0
- package/src/cli/provider.ts +8 -0
- package/src/cli/registry.ts +16 -2
- package/src/cli/runtime-api.ts +3 -1
- package/src/cli/star-prompt.ts +22 -6
- package/src/cli/status-probes.ts +168 -0
- package/src/cli/status.ts +5 -168
- package/src/clients/config-export/constants.ts +69 -0
- package/src/clients/config-export/contracts.ts +154 -0
- package/src/clients/config-export/dsh.ts +132 -0
- package/src/clients/config-export/fast-models.ts +29 -0
- package/src/clients/config-export/mcode.ts +83 -0
- package/src/clients/config-export/model-metadata.ts +108 -0
- package/src/clients/config-export/omp.ts +104 -0
- package/src/clients/config-export/zcode.ts +92 -0
- package/src/clients/config-export.ts +18 -710
- package/src/codex/account-lifecycle.ts +20 -3
- package/src/codex/account-usability.ts +2 -0
- package/src/codex/auth-api.ts +170 -23
- package/src/codex/auth-context.ts +328 -24
- package/src/codex/catalog/effort.ts +30 -4
- package/src/codex/catalog/metadata.ts +39 -9
- package/src/codex/catalog/native-models.ts +53 -12
- package/src/codex/catalog/parsing.ts +119 -5
- package/src/codex/catalog/provider-fetch.ts +150 -20
- package/src/codex/catalog/reserve.ts +52 -0
- package/src/codex/catalog/sync.ts +89 -16
- package/src/codex/convergence-types.ts +1 -0
- package/src/codex/convergence.ts +2 -0
- package/src/codex/data/upstream-models.json +1 -1
- package/src/codex/forward-transport-headers.ts +25 -0
- package/src/codex/inject.ts +3 -28
- package/src/codex/legacy-config-keys.ts +68 -0
- package/src/codex/log-guard/inspect-schema.ts +137 -0
- package/src/codex/log-guard/inspect.ts +2 -134
- package/src/codex/loopback-target.ts +54 -0
- package/src/codex/main-account-cache.ts +63 -1
- package/src/codex/main-account-hard-lock.ts +52 -0
- package/src/codex/main-account.ts +3 -1
- package/src/codex/management-convergence.ts +3 -0
- package/src/codex/model-entitlements.ts +54 -4
- package/src/codex/project-config-warnings.ts +92 -2
- package/src/codex/prompt-layers/encoding.ts +80 -0
- package/src/codex/prompt-layers/paths.ts +54 -0
- package/src/codex/prompt-layers/revision.ts +55 -0
- package/src/codex/prompt-layers/toml-edit.ts +163 -0
- package/src/codex/prompt-layers/toml-read.ts +181 -0
- package/src/codex/prompt-layers.ts +14 -520
- package/src/codex/quota-auto-refresh-state.ts +16 -0
- package/src/codex/quota-auto-refresh.ts +219 -0
- package/src/codex/quota-types.ts +51 -0
- package/src/codex/quota.ts +252 -93
- package/src/codex/reserve-availability.ts +177 -0
- package/src/codex/routing.ts +28 -9
- package/src/codex/shim.ts +53 -11
- package/src/codex/subagent-model-fallback.ts +23 -3
- package/src/combos/failover.ts +125 -7
- package/src/combos/identifiers.ts +89 -0
- package/src/combos/index.ts +4 -0
- package/src/combos/resolve.ts +80 -9
- package/src/combos/types.ts +20 -93
- package/src/config/subagent-models.ts +24 -0
- package/src/config.ts +156 -13
- package/src/generated/compatibility-version.json +454 -166
- package/src/generated/model-metadata.ts +1 -1
- package/src/integrations/journal.ts +65 -4
- package/src/integrations/store.ts +5 -0
- package/src/lab/events/limits.ts +4 -0
- package/src/lib/destination-policy.ts +31 -2
- package/src/lib/errors.ts +39 -0
- package/src/lib/provider-outbound.ts +69 -3
- package/src/lib/proxy-env.ts +22 -0
- package/src/lib/redact-folding.ts +176 -0
- package/src/lib/redact.ts +2 -175
- package/src/lib/state-store-sweeper.ts +20 -6
- package/src/lib/token-estimate.ts +94 -27
- package/src/lib/windows-user-principal.ts +53 -5
- package/src/oauth/anthropic-routing.ts +99 -3
- package/src/oauth/generic-account-failover.ts +36 -13
- package/src/oauth/index.ts +127 -26
- package/src/oauth/login-cli.ts +5 -0
- package/src/oauth/meta-muse.ts +117 -15
- package/src/oauth/pool-settings-capability.ts +15 -4
- package/src/providers/api-keys.ts +8 -10
- package/src/providers/default-aliases.ts +39 -0
- package/src/providers/derive.ts +10 -2
- package/src/providers/fastwire.ts +36 -7
- package/src/providers/initial-model-selection-runtime.ts +90 -0
- package/src/providers/initial-model-selection.ts +120 -0
- package/src/providers/key-failover.ts +134 -54
- package/src/providers/key-store.ts +11 -1
- package/src/providers/label.ts +1 -1
- package/src/providers/model-discovery.ts +76 -0
- package/src/providers/model-rename-startup.ts +72 -8
- package/src/providers/openai-sidecar.ts +17 -5
- package/src/providers/openai-tiers-destination.ts +102 -0
- package/src/providers/openai-tiers.ts +2 -99
- package/src/providers/opencode-go-transport.ts +41 -0
- package/src/providers/quota-key-accounts.ts +141 -0
- package/src/providers/quota-types.ts +9 -0
- package/src/providers/quota.ts +535 -104
- package/src/providers/registry.ts +60 -17
- package/src/providers/xai-responses-opt-in.ts +31 -5
- package/src/quota/reset-activation.ts +81 -0
- package/src/quota/reset-detector.ts +305 -0
- package/src/quota/reset-notify-config.ts +162 -0
- package/src/quota/reset-observer.ts +125 -0
- package/src/quota/reset-poller.ts +160 -0
- package/src/quota/reset-seen-store.ts +385 -0
- package/src/quota/reset-sinks.ts +199 -0
- package/src/quota/window-mapping.ts +106 -0
- package/src/responses/apply-patch-envelope.ts +46 -0
- package/src/responses/code-mode-helper-compat.ts +39 -1
- package/src/responses/custom-tool-compat.ts +10 -4
- package/src/responses/hosted-tool-policy.ts +12 -4
- package/src/responses/parser-content.ts +133 -0
- package/src/responses/parser-text-format.ts +24 -0
- package/src/responses/parser-tools.ts +188 -0
- package/src/responses/parser.ts +3 -326
- package/src/responses/state.ts +124 -28
- package/src/router.ts +48 -13
- package/src/routing/analytics.ts +1 -0
- package/src/routing/capability.ts +17 -4
- package/src/server/auth-cors.ts +7 -1
- package/src/server/background-lifecycle.ts +23 -1
- package/src/server/chat-completions.ts +25 -3
- package/src/server/claude-messages.ts +62 -5
- package/src/server/effort-row.ts +1 -1
- package/src/server/fast-row.ts +295 -0
- package/src/server/gui-static.ts +30 -4
- package/src/server/index.ts +107 -21
- package/src/server/management/agent-settings-routes.ts +2 -2
- package/src/server/management/combo-routes.ts +37 -9
- package/src/server/management/config-routes.ts +93 -2
- package/src/server/management/integration-routes.ts +108 -0
- package/src/server/management/model-routes.ts +13 -3
- package/src/server/management/model-rows.ts +20 -1
- package/src/server/management/native-integration-routes.ts +4 -1
- package/src/server/management/oauth-account-routes.ts +38 -10
- package/src/server/management/provider-routes.ts +34 -3
- package/src/server/management/quota-reset-routes.ts +57 -0
- package/src/server/management/route-registry.ts +7 -4
- package/src/server/management/shared.ts +19 -5
- package/src/server/management/system-routes.ts +3 -2
- package/src/server/management-api.ts +14 -2
- package/src/server/ports.ts +12 -2
- package/src/server/relay-eager.ts +38 -23
- package/src/server/relay.ts +4 -0
- package/src/server/request-log.ts +6 -0
- package/src/server/responses/codex-ws-correlation.ts +65 -0
- package/src/server/responses/codex-ws-exchange.ts +261 -0
- package/src/server/responses/codex-ws-metadata.ts +134 -0
- package/src/server/responses/codex-ws-pool.ts +162 -0
- package/src/server/responses/codex-ws-request.ts +87 -0
- package/src/server/responses/codex-ws-session.ts +93 -0
- package/src/server/responses/codex-ws-wire.ts +144 -0
- package/src/server/responses/collaboration.ts +41 -1
- package/src/server/responses/compact.ts +105 -12
- package/src/server/responses/core.ts +476 -57
- package/src/server/responses/fetch-helpers.ts +10 -3
- package/src/server/responses/input-admission.ts +16 -9
- package/src/server/responses/responses-field-backfill.ts +1 -1
- package/src/server/responses/ws-upstream.ts +34 -318
- package/src/server/responses-custom-tool-repair.ts +20 -4
- package/src/server/responses-undeclared-tool-guard.ts +100 -8
- package/src/server/safe-response-headers.ts +23 -0
- package/src/server/search.ts +9 -0
- package/src/server/subagent-models-startup.ts +27 -0
- package/src/server/system-env-shell.ts +238 -0
- package/src/server/system-env.ts +7 -234
- package/src/server/ws-bridge.ts +3 -25
- package/src/server/xai-responses-startup.ts +21 -0
- package/src/service-manager-probe.ts +1 -1
- package/src/service.ts +55 -16
- package/src/types/config.ts +108 -12
- package/src/types/provider.ts +36 -7
- package/src/types/tools.ts +26 -1
- package/src/types.ts +2 -0
- package/src/update/notify.ts +8 -2
- package/src/usage/cost.ts +38 -28
- package/src/usage/expected-prices.ts +34 -15
- package/src/usage/log.ts +2 -0
- package/src/usage/model-identity.ts +26 -0
- package/src/usage/summary.ts +15 -1
- package/src/vision/describe.ts +6 -0
- package/src/vision/image-rewrite.ts +108 -0
- package/src/vision/index.ts +19 -306
- package/src/vision/plan.ts +205 -0
- package/src/web-search/executor.ts +6 -0
- package/src/web-search/index.ts +8 -1
- package/gui/dist/assets/index-BU1tE0sr.js +0 -112
- package/gui/dist/assets/index-DL9-iS6J.css +0 -1
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
resolveCodexSqliteHome,
|
|
9
9
|
type CodexSqliteHomeDeps,
|
|
10
10
|
} from "../paths";
|
|
11
|
+
export { hasCurrentLogsSchema } from "./inspect-schema";
|
|
12
|
+
import { hasCurrentLogsTable, type ColumnRow } from "./inspect-schema";
|
|
11
13
|
|
|
12
14
|
const IMMUTABLE_READONLY_FLAGS = constants.SQLITE_OPEN_READONLY | constants.SQLITE_OPEN_URI;
|
|
13
15
|
const KNOWN_LOG_LEVELS = new Set(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]);
|
|
@@ -15,56 +17,6 @@ const KNOWN_LOG_LEVELS = new Set(["TRACE", "DEBUG", "INFO", "WARN", "ERROR"]);
|
|
|
15
17
|
// alone; skipping all row aggregates above 64 MiB reduced /api/storage to 628ms.
|
|
16
18
|
const MAX_SYNCHRONOUS_METRICS_DATABASE_BYTES = 64 * 1024 * 1024;
|
|
17
19
|
|
|
18
|
-
interface CurrentLogColumn {
|
|
19
|
-
name: string;
|
|
20
|
-
type: string;
|
|
21
|
-
notnull: number;
|
|
22
|
-
defaultValue: string | null;
|
|
23
|
-
pk: number;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
// Pinned to Codex logs migration 0002. Keep this schema private: inspection reports
|
|
27
|
-
// compatibility, not column names, so sensitive payload-bearing fields never leak through
|
|
28
|
-
// the management API. Any additive/rebuilt future schema is monitor-only until reviewed.
|
|
29
|
-
const CURRENT_LOG_SCHEMA: readonly CurrentLogColumn[] = [
|
|
30
|
-
{ name: "id", type: "INTEGER", notnull: 0, defaultValue: null, pk: 1 },
|
|
31
|
-
{ name: "ts", type: "INTEGER", notnull: 1, defaultValue: null, pk: 0 },
|
|
32
|
-
{ name: "ts_nanos", type: "INTEGER", notnull: 1, defaultValue: null, pk: 0 },
|
|
33
|
-
{ name: "level", type: "TEXT", notnull: 1, defaultValue: null, pk: 0 },
|
|
34
|
-
{ name: "target", type: "TEXT", notnull: 1, defaultValue: null, pk: 0 },
|
|
35
|
-
{ name: "feedback_log_body", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 },
|
|
36
|
-
{ name: "module_path", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 },
|
|
37
|
-
{ name: "file", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 },
|
|
38
|
-
{ name: "line", type: "INTEGER", notnull: 0, defaultValue: null, pk: 0 },
|
|
39
|
-
{ name: "thread_id", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 },
|
|
40
|
-
{ name: "process_uuid", type: "TEXT", notnull: 0, defaultValue: null, pk: 0 },
|
|
41
|
-
{ name: "estimated_bytes", type: "INTEGER", notnull: 1, defaultValue: "0", pk: 0 },
|
|
42
|
-
] as const;
|
|
43
|
-
|
|
44
|
-
const CURRENT_LOG_TABLE_SQL = `CREATE TABLE logs (
|
|
45
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
46
|
-
ts INTEGER NOT NULL,
|
|
47
|
-
ts_nanos INTEGER NOT NULL,
|
|
48
|
-
level TEXT NOT NULL,
|
|
49
|
-
target TEXT NOT NULL,
|
|
50
|
-
feedback_log_body TEXT,
|
|
51
|
-
module_path TEXT,
|
|
52
|
-
file TEXT,
|
|
53
|
-
line INTEGER,
|
|
54
|
-
thread_id TEXT,
|
|
55
|
-
process_uuid TEXT,
|
|
56
|
-
estimated_bytes INTEGER NOT NULL DEFAULT 0
|
|
57
|
-
)`;
|
|
58
|
-
|
|
59
|
-
const CURRENT_LOG_INDEX_SQL = {
|
|
60
|
-
idx_logs_ts: "CREATE INDEX idx_logs_ts ON logs(ts DESC, ts_nanos DESC, id DESC)",
|
|
61
|
-
idx_logs_thread_id: "CREATE INDEX idx_logs_thread_id ON logs(thread_id)",
|
|
62
|
-
idx_logs_thread_id_ts: "CREATE INDEX idx_logs_thread_id_ts ON logs(thread_id, ts DESC, ts_nanos DESC, id DESC)",
|
|
63
|
-
idx_logs_process_uuid_threadless_ts: `CREATE INDEX idx_logs_process_uuid_threadless_ts
|
|
64
|
-
ON logs(process_uuid, ts DESC, ts_nanos DESC, id DESC)
|
|
65
|
-
WHERE thread_id IS NULL`,
|
|
66
|
-
} as const;
|
|
67
|
-
|
|
68
20
|
export type CodexLogGuardCapabilityReason =
|
|
69
21
|
| "database_missing"
|
|
70
22
|
| "database_unreadable"
|
|
@@ -124,15 +76,6 @@ export interface CodexLogGuardInspection {
|
|
|
124
76
|
};
|
|
125
77
|
}
|
|
126
78
|
|
|
127
|
-
interface ColumnRow {
|
|
128
|
-
cid: number;
|
|
129
|
-
name: string;
|
|
130
|
-
type: string;
|
|
131
|
-
notnull: number;
|
|
132
|
-
dflt_value: string | null;
|
|
133
|
-
pk: number;
|
|
134
|
-
}
|
|
135
|
-
interface SchemaObjectRow { name: string; type: string; sql: string | null }
|
|
136
79
|
interface CountRow { n: number }
|
|
137
80
|
interface LevelRow { level: string; rows: number }
|
|
138
81
|
interface TargetCountRow { rows: number }
|
|
@@ -243,81 +186,6 @@ function unavailableInspection(): CodexLogGuardInspection {
|
|
|
243
186
|
};
|
|
244
187
|
}
|
|
245
188
|
|
|
246
|
-
function normalizeDeclaredType(type: string): string {
|
|
247
|
-
return String(type ?? "").trim().toUpperCase();
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function normalizeDefault(value: string | null): string | null {
|
|
251
|
-
return value === null ? null : String(value).trim();
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function normalizeSchemaSql(sql: string | null | undefined): string {
|
|
255
|
-
return (sql ?? "").trim().replace(/;\s*$/, "").replace(/\s+/g, " ");
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
function sameColumns(columns: ColumnRow[]): boolean {
|
|
259
|
-
if (columns.length !== CURRENT_LOG_SCHEMA.length) return false;
|
|
260
|
-
return columns.every((column, index) => {
|
|
261
|
-
const expected = CURRENT_LOG_SCHEMA[index];
|
|
262
|
-
return column.cid === index
|
|
263
|
-
&& column.name === expected.name
|
|
264
|
-
&& normalizeDeclaredType(column.type) === expected.type
|
|
265
|
-
&& Number(column.notnull) === expected.notnull
|
|
266
|
-
&& normalizeDefault(column.dflt_value) === expected.defaultValue
|
|
267
|
-
&& Number(column.pk) === expected.pk;
|
|
268
|
-
});
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
/**
|
|
272
|
-
* The authoritative compatibility predicate: exact table SQL, exact column
|
|
273
|
-
* metadata, and every canonical index.
|
|
274
|
-
*
|
|
275
|
-
* Exported because the mutation paths must apply the SAME test inside their
|
|
276
|
-
* write transaction. They used to check column NAMES only, which is strictly
|
|
277
|
-
* weaker than what the inspector reports, so a schema change landing between
|
|
278
|
-
* the outer inspection and the locked write let Protect install a row-dropping
|
|
279
|
-
* trigger and let Reclaim vacuum pages on a database the inspector classifies
|
|
280
|
-
* as monitor-only. The lock serializes OpenCodex against itself; it does not
|
|
281
|
-
* stop Codex or another SQLite writer, so that TOCTOU window is real.
|
|
282
|
-
*/
|
|
283
|
-
export function hasCurrentLogsSchema(db: Database): boolean {
|
|
284
|
-
const columns = db.query<ColumnRow, []>("PRAGMA table_info(logs)").all();
|
|
285
|
-
return hasCurrentLogsTable(db, columns);
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function hasCurrentLogsTable(db: Database, columns: ColumnRow[]): boolean {
|
|
289
|
-
const table = db.query<SchemaObjectRow, []>(
|
|
290
|
-
"SELECT name, type, sql FROM sqlite_schema WHERE name = 'logs' LIMIT 1",
|
|
291
|
-
).get();
|
|
292
|
-
if (table?.type !== "table"
|
|
293
|
-
|| !sameColumns(columns)
|
|
294
|
-
|| normalizeSchemaSql(table.sql) !== normalizeSchemaSql(CURRENT_LOG_TABLE_SQL)) {
|
|
295
|
-
return false;
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
const indexes = db.query<SchemaObjectRow, []>(`
|
|
299
|
-
SELECT name, type, sql FROM sqlite_schema
|
|
300
|
-
WHERE name IN (
|
|
301
|
-
'idx_logs_ts',
|
|
302
|
-
'idx_logs_thread_id',
|
|
303
|
-
'idx_logs_thread_id_ts',
|
|
304
|
-
'idx_logs_process_uuid_threadless_ts'
|
|
305
|
-
)
|
|
306
|
-
`).all();
|
|
307
|
-
const byName = new Map(indexes.map(row => [row.name, row]));
|
|
308
|
-
for (const [name, expectedSql] of Object.entries(CURRENT_LOG_INDEX_SQL)) {
|
|
309
|
-
const row = byName.get(name);
|
|
310
|
-
if (row?.type !== "index" || normalizeSchemaSql(row.sql) !== normalizeSchemaSql(expectedSql)) {
|
|
311
|
-
return false;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
// Extra indexes and triggers do not redefine the table contract. In particular,
|
|
316
|
-
// Protect intentionally installs OpenCodex-owned triggers and unrelated user triggers
|
|
317
|
-
// are supported, so compatibility is based on the canonical table plus required indexes.
|
|
318
|
-
return true;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
189
|
function pragmaNumber(db: Database, pragma: "page_size" | "page_count" | "freelist_count"): number {
|
|
322
190
|
const row = db.query<Record<string, number>, []>(`PRAGMA ${pragma}`).get();
|
|
323
191
|
return Number(row?.[pragma] ?? 0);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import type { DataPlaneAdmission } from "../server/auth-cors";
|
|
3
|
+
import { NATIVE_RESERVE_MODEL } from "./catalog/native-models";
|
|
4
|
+
|
|
5
|
+
export const CODEX_RESERVE_HELPER_UNSUPPORTED_MESSAGE =
|
|
6
|
+
"Luna Reserve compatibility is only available as a conversation model, not a vision helper. Choose another vision model.";
|
|
7
|
+
|
|
8
|
+
/** Callers classify the concrete destination as canonical forward before using this predicate. */
|
|
9
|
+
export function isCodexReserveHelperUnsupported(
|
|
10
|
+
config: Pick<OcxConfig, "codexDesktopAuthless" | "runtimeRole">,
|
|
11
|
+
modelId: string,
|
|
12
|
+
admission: Pick<DataPlaneAdmission, "source"> | undefined,
|
|
13
|
+
terminalHelper: boolean,
|
|
14
|
+
): boolean {
|
|
15
|
+
return terminalHelper && modelId === NATIVE_RESERVE_MODEL && isCodexReserveRequestEligible(config, admission);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** Runtime authority comes from the receiving listener, not the catalog's injection target. */
|
|
19
|
+
export function isCodexReserveRequestEligible(
|
|
20
|
+
config: Pick<OcxConfig, "codexDesktopAuthless" | "runtimeRole">,
|
|
21
|
+
admission: Pick<DataPlaneAdmission, "source"> | undefined,
|
|
22
|
+
): boolean {
|
|
23
|
+
return config.codexDesktopAuthless === true && config.runtimeRole !== "client"
|
|
24
|
+
&& admission?.source === "loopback";
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Bind scope, not the dial address: wildcard listeners are never loopback-only. */
|
|
28
|
+
export function isLoopbackHostname(hostname: string | undefined): boolean {
|
|
29
|
+
const normalized = (hostname ?? "127.0.0.1").trim().toLowerCase().replace(/\.$/, "");
|
|
30
|
+
return (
|
|
31
|
+
normalized === "" ||
|
|
32
|
+
normalized === "localhost" ||
|
|
33
|
+
normalized === "127.0.0.1" ||
|
|
34
|
+
normalized === "::1" ||
|
|
35
|
+
normalized === "[::1]"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function shouldInjectApiAuthHeader(
|
|
40
|
+
config: Pick<OcxConfig, "hostname" | "unauthenticatedLoopbackListener"> | undefined,
|
|
41
|
+
): boolean {
|
|
42
|
+
// The dedicated listener binds loopback and does not require an admission credential.
|
|
43
|
+
if (config?.unauthenticatedLoopbackListener?.enabled) return false;
|
|
44
|
+
return !isLoopbackHostname(config?.hostname);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Match standalone injection, never a remote client's independently supplied routing target. */
|
|
48
|
+
export function isEffectiveCodexDesktopAuthless(
|
|
49
|
+
config: Pick<OcxConfig, "runtimeRole" | "hostname" | "unauthenticatedLoopbackListener" | "codexDesktopAuthless"> | undefined,
|
|
50
|
+
): boolean {
|
|
51
|
+
return config?.codexDesktopAuthless === true
|
|
52
|
+
&& config.runtimeRole !== "client"
|
|
53
|
+
&& !shouldInjectApiAuthHeader(config);
|
|
54
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { createHash, createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
+
import type { StoredAccountQuota } from "./quota-types";
|
|
2
3
|
import { truncateRetainedUtf8 } from "../lib/admission";
|
|
3
4
|
|
|
4
5
|
const MAX_DIAGNOSTIC_VALUE_BYTES = 8 * 1024;
|
|
@@ -16,6 +17,65 @@ export interface CachedMainAccountInfo extends MainAccountInfo {
|
|
|
16
17
|
let cachedMainAccountInfo: CachedMainAccountInfo | null = null;
|
|
17
18
|
let cachedMainCredentialPresence: boolean | null = null;
|
|
18
19
|
let mainAccountIdentityGeneration = 0;
|
|
20
|
+
let observedMainQuotaIdentityKey: string | undefined;
|
|
21
|
+
const mainQuotaCredentialKey = randomBytes(32);
|
|
22
|
+
let mainQuotaCredential: { bearerHmac: Buffer; writer: MainQuotaWriter } | undefined;
|
|
23
|
+
let mainQuotaCredentialGeneration = 0;
|
|
24
|
+
|
|
25
|
+
/** Process-local transition fence; no credential material or persisted identity. */
|
|
26
|
+
export function getMainQuotaCredentialGeneration(): number { return mainQuotaCredentialGeneration; }
|
|
27
|
+
|
|
28
|
+
export type MainQuotaWriter = Readonly<{ identityKey: string; identityGeneration: number }>;
|
|
29
|
+
|
|
30
|
+
function mainQuotaIdentityKey(accountId: string): string {
|
|
31
|
+
return createHash("sha256").update("opencodex-main-quota-v1\0").update(accountId).digest("hex");
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Only an existing owned physical-identity read may publish this observation. */
|
|
35
|
+
export function observeMainQuotaIdentity(accountId: string): void {
|
|
36
|
+
if (!accountId) return;
|
|
37
|
+
const identityKey = mainQuotaIdentityKey(accountId);
|
|
38
|
+
if (identityKey === observedMainQuotaIdentityKey) return;
|
|
39
|
+
observedMainQuotaIdentityKey = identityKey;
|
|
40
|
+
mainAccountIdentityGeneration += 1;
|
|
41
|
+
mainQuotaCredential = undefined;
|
|
42
|
+
mainQuotaCredentialGeneration += 1;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function captureMainQuotaWriter(accountId: string): MainQuotaWriter | undefined {
|
|
46
|
+
if (!accountId) return undefined;
|
|
47
|
+
const identityKey = mainQuotaIdentityKey(accountId);
|
|
48
|
+
if (identityKey !== observedMainQuotaIdentityKey) return undefined;
|
|
49
|
+
return { identityKey, identityGeneration: mainAccountIdentityGeneration };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Credential material must come from an already-owned read, never an incoming request. */
|
|
53
|
+
export function observeMainQuotaCredential(accessToken: string, accountId: string): MainQuotaWriter | undefined {
|
|
54
|
+
const writer = captureMainQuotaWriter(accountId);
|
|
55
|
+
if (!accessToken || !writer) return undefined;
|
|
56
|
+
const bearerHmac = createHmac("sha256", mainQuotaCredentialKey).update(accessToken).digest();
|
|
57
|
+
if (!mainQuotaCredential || !isMainQuotaWriterLive(mainQuotaCredential.writer)
|
|
58
|
+
|| !timingSafeEqual(bearerHmac, mainQuotaCredential.bearerHmac)) mainQuotaCredentialGeneration += 1;
|
|
59
|
+
mainQuotaCredential = { bearerHmac, writer };
|
|
60
|
+
return { ...writer };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function matchesMainQuotaCredential(accessToken: string, effectiveAccountId: string | undefined): boolean {
|
|
64
|
+
const observed = mainQuotaCredential;
|
|
65
|
+
if (!accessToken || !effectiveAccountId || !observed || !isMainQuotaWriterLive(observed.writer)) return false;
|
|
66
|
+
if (mainQuotaIdentityKey(effectiveAccountId) !== observed.writer.identityKey) return false;
|
|
67
|
+
const candidate = createHmac("sha256", mainQuotaCredentialKey).update(accessToken).digest();
|
|
68
|
+
return timingSafeEqual(candidate, observed.bearerHmac);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function isMainQuotaWriterLive(writer: MainQuotaWriter): boolean {
|
|
72
|
+
return writer.identityKey === observedMainQuotaIdentityKey
|
|
73
|
+
&& writer.identityGeneration === mainAccountIdentityGeneration;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function getObservedMainQuotaIdentityKey(): string | undefined {
|
|
77
|
+
return observedMainQuotaIdentityKey;
|
|
78
|
+
}
|
|
19
79
|
|
|
20
80
|
export function captureMainAccountIdentityGeneration(): number {
|
|
21
81
|
return mainAccountIdentityGeneration;
|
|
@@ -40,6 +100,8 @@ export function setMainAccountInfoCache(value: CachedMainAccountInfo): void {
|
|
|
40
100
|
export function clearMainAccountInfoCache(): void {
|
|
41
101
|
cachedMainAccountInfo = null;
|
|
42
102
|
mainAccountIdentityGeneration += 1;
|
|
103
|
+
mainQuotaCredential = undefined;
|
|
104
|
+
mainQuotaCredentialGeneration += 1;
|
|
43
105
|
}
|
|
44
106
|
|
|
45
107
|
/** Last physical credential presence observed while native-main ownership was held. */
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { OcxConfig } from "../types";
|
|
2
|
+
import { getMainPolicyQuota } from "./quota";
|
|
3
|
+
|
|
4
|
+
export const MAIN_ACCOUNT_HARD_LOCK_PERCENT = 99;
|
|
5
|
+
|
|
6
|
+
export interface MainAccountHardLockStatus {
|
|
7
|
+
enabled: boolean;
|
|
8
|
+
state: "off" | "unknown" | "ready" | "blocked";
|
|
9
|
+
/** Unix milliseconds; absent when a blocking observation has no future reset. */
|
|
10
|
+
resetAt?: number;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
type PolicyConfig = Pick<OcxConfig, "codexMainAccountHardLock">;
|
|
14
|
+
|
|
15
|
+
function resetTimestamp(value: number | undefined): number | undefined {
|
|
16
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined;
|
|
17
|
+
return value < 10_000_000_000 ? value * 1000 : value;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Observed admission policy, not a reservation of the account's remaining quota. */
|
|
21
|
+
export function getMainAccountHardLockStatus(
|
|
22
|
+
config: PolicyConfig,
|
|
23
|
+
now = Date.now(),
|
|
24
|
+
): MainAccountHardLockStatus {
|
|
25
|
+
if (config.codexMainAccountHardLock !== true) return { enabled: false, state: "off" };
|
|
26
|
+
const quota = getMainPolicyQuota();
|
|
27
|
+
if (!quota) return { enabled: true, state: "unknown" };
|
|
28
|
+
// Account window priority is deliberate: a 5h account uses that window, even if
|
|
29
|
+
// its weekly bar is higher. An unknown/expired selected window does not change the choice.
|
|
30
|
+
const hasShort = quota.shortPercent !== undefined || quota.shortResetAt !== undefined
|
|
31
|
+
|| quota.shortWindowSeconds !== undefined;
|
|
32
|
+
const hasWeekly = quota.weeklyPercent !== undefined || quota.weeklyResetAt !== undefined;
|
|
33
|
+
const [percent, rawReset] = hasShort
|
|
34
|
+
? [quota.shortPercent, quota.shortResetAt]
|
|
35
|
+
: hasWeekly ? [quota.weeklyPercent, quota.weeklyResetAt] : [quota.monthlyPercent, quota.monthlyResetAt];
|
|
36
|
+
const resetAt = resetTimestamp(rawReset);
|
|
37
|
+
// The routing score's unknown sentinel is 101. It is never a raw quota observation.
|
|
38
|
+
if (typeof percent !== "number" || !Number.isFinite(percent) || percent < 0 || percent > 100) {
|
|
39
|
+
return { enabled: true, state: "unknown" };
|
|
40
|
+
}
|
|
41
|
+
if (percent < MAIN_ACCOUNT_HARD_LOCK_PERCENT) return { enabled: true, state: "ready" };
|
|
42
|
+
return {
|
|
43
|
+
enabled: true,
|
|
44
|
+
state: "blocked",
|
|
45
|
+
// A predicted reset is not evidence of recovery. Only a fresh lower reading releases.
|
|
46
|
+
...(resetAt !== undefined && resetAt > now ? { resetAt } : {}),
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function isMainAccountHardLocked(config: PolicyConfig, now = Date.now()): boolean {
|
|
51
|
+
return getMainAccountHardLockStatus(config, now).state === "blocked";
|
|
52
|
+
}
|
|
@@ -57,6 +57,8 @@ type MainAuthJsonCredential = {
|
|
|
57
57
|
export interface NativeMainRefreshDependencies {
|
|
58
58
|
refreshToken?: (refreshToken: string, options: { signal: AbortSignal }) => Promise<OAuthCredentials>;
|
|
59
59
|
signal?: AbortSignal;
|
|
60
|
+
/** Metadata-only refresh must not retract a concurrent traffic reauth quarantine. */
|
|
61
|
+
preserveReauth?: boolean;
|
|
60
62
|
}
|
|
61
63
|
|
|
62
64
|
export class MainAuthJsonChangedDuringRefreshError extends Error {
|
|
@@ -283,7 +285,7 @@ async function resolveMainAccountToken(
|
|
|
283
285
|
throw new MainAccountTokenRefreshError(reason, { cause });
|
|
284
286
|
}
|
|
285
287
|
const result = persistRefreshedMainAuthJson(locked, refreshed);
|
|
286
|
-
clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
288
|
+
if (dependencies.preserveReauth !== true) clearAccountNeedsReauth(MAIN_CODEX_ACCOUNT_ID);
|
|
287
289
|
return result;
|
|
288
290
|
}),
|
|
289
291
|
{ waitMs: 30_000, signal },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { OcxConfig } from "../types";
|
|
2
|
+
import { resolvePendingInitialModelSelection } from "../providers/initial-model-selection-runtime";
|
|
2
3
|
import { captureCatalogAdmissionSnapshot } from "./catalog-admission";
|
|
3
4
|
import { convergeCodexCatalog } from "./convergence";
|
|
4
5
|
import type {
|
|
@@ -152,6 +153,8 @@ export function createManagementConvergeCodex(
|
|
|
152
153
|
catalogRefresh: unexpectedCatalogFailure(false),
|
|
153
154
|
});
|
|
154
155
|
}
|
|
156
|
+
// Registration choices are committed independently, before sealing catalog authority.
|
|
157
|
+
await resolvePendingInitialModelSelection(retainedConfig as OcxConfig);
|
|
155
158
|
const snapshot = captureCatalogAdmissionSnapshot(retainedConfig);
|
|
156
159
|
const result = await convergeCodexCatalog(snapshot, request, {
|
|
157
160
|
onCommitBegin: () => { commitBegan = true; },
|
|
@@ -149,6 +149,13 @@ export function compareClientVersionsForTests(left: string, right: string): numb
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
/** Numeric-segment comparison. Only used to pick the highest floor in a known-good set. */
|
|
152
|
+
// Prerelease and build suffixes are deliberately NOT ordered. Splitting on [.+-] turns the
|
|
153
|
+
// suffix into a non-finite segment that reads as 0, so `0.144.0-rc.1` sorts at or above
|
|
154
|
+
// `0.144.0` rather than below it, the inverse of semver. That is tolerable because every
|
|
155
|
+
// version this ranks against is a release version: the gated floor, the measured minimum, and
|
|
156
|
+
// the snapshot's `minimal_client_version` rows. A prerelease runtime is therefore passed
|
|
157
|
+
// through exactly as it was before the floor bound tier 2, so this is not a new hazard. Order
|
|
158
|
+
// the suffix properly before reusing this anywhere a prerelease has to sort below its release.
|
|
152
159
|
function compareClientVersions(left: string, right: string): number {
|
|
153
160
|
const l = left.split(/[.+-]/).map(Number);
|
|
154
161
|
const r = right.split(/[.+-]/).map(Number);
|
|
@@ -165,9 +172,10 @@ function compareClientVersions(left: string, right: string): number {
|
|
|
165
172
|
*
|
|
166
173
|
* 1. the inbound request's own `client_version` — the only value certainly describing the
|
|
167
174
|
* client being answered;
|
|
168
|
-
|
|
169
|
-
* Retained sync refreshes runtime evidence before
|
|
170
|
-
* usable here; the persisted file itself carries no
|
|
175
|
+
* 2. the selected Codex runtime version, for callers with no request of their own — but never
|
|
176
|
+
* below `GATED_MODEL_CLIENT_VERSION_FLOOR`. Retained sync refreshes runtime evidence before
|
|
177
|
+
* discovery, which is what makes this usable here; the persisted file itself carries no
|
|
178
|
+
* freshness guarantee.
|
|
171
179
|
* 3. the floor this build's own bundled roster records for the models being gated
|
|
172
180
|
* (`GATED_MODEL_CLIENT_VERSION_FLOOR`).
|
|
173
181
|
*
|
|
@@ -178,6 +186,25 @@ function compareClientVersions(left: string, right: string): number {
|
|
|
178
186
|
* snapshot states the gated models require, so asking under it is the narrowest question
|
|
179
187
|
* that can still return them.
|
|
180
188
|
*
|
|
189
|
+
* The floor binds tier 2 as well, and that is the whole of #3436. #3022 gave tier 3 the
|
|
190
|
+
* measured minimum but left tier 2 to speak for itself, so a host whose persisted runtime was
|
|
191
|
+
* REAL but OLD — 0.141.0 against a measured 0.144.0 — asked upstream under a version upstream
|
|
192
|
+
* filters on, got an honest roster with no gpt-5.6, and lost sol/terra/luna everywhere. That
|
|
193
|
+
* made an outdated CLI strictly worse than no CLI at all, since the runtime-less host already
|
|
194
|
+
* asked at the floor and kept its models. The clamp only ever raises: a runtime at or above
|
|
195
|
+
* the floor is preserved exactly, because a newer client can drive models the floor cannot
|
|
196
|
+
* name.
|
|
197
|
+
*
|
|
198
|
+
* Which tier answers is a question about WHICH QUESTION IS BEING ASKED, not about background
|
|
199
|
+
* versus request path — `isDirectCallerEntitledToCodexModel` and both authorization paths in
|
|
200
|
+
* `auth-context.ts` are inbound requests that reach tier 2 because they carry no version:
|
|
201
|
+
*
|
|
202
|
+
* - No inbound version: the caller is asking whether the ACCOUNT owns the model. Upstream
|
|
203
|
+
* only incidentally filters that answer by version, so ask at no less than the floor.
|
|
204
|
+
* - An inbound version: the caller is asking what THAT CLIENT may use. Answer verbatim, even
|
|
205
|
+
* when it is older than the floor. Clamping there would advertise rows the client told us
|
|
206
|
+
* it cannot drive (#2548) and would turn an honest `unknown` into a cached `denied`.
|
|
207
|
+
*
|
|
181
208
|
* There is deliberately no `0.0.0`-style fallback. A placeholder describes a client that
|
|
182
209
|
* predates every gated model, which is what made upstream answer with an empty roster and
|
|
183
210
|
* turned absent evidence into a manufactured confirmed negative (#2886).
|
|
@@ -195,7 +222,23 @@ export function resolveCodexEntitlementClientVersion(
|
|
|
195
222
|
const selected = bypass
|
|
196
223
|
? readRuntimeVersion(loadRuntime)
|
|
197
224
|
: memoizedPersistedRuntimeVersion(loadRuntime, options.now ?? Date.now());
|
|
198
|
-
return selected
|
|
225
|
+
return raisedToGatedFloor(selected);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* The gated floor as a lower bound rather than a fallback.
|
|
230
|
+
*
|
|
231
|
+
* Applied only on the way out of the resolver. `readRuntimeVersion` and
|
|
232
|
+
* `memoizedPersistedRuntimeVersion` keep reporting what is actually on disk, because
|
|
233
|
+
* `selectedVersion` is probe evidence that runtime identity, catalog cache keys,
|
|
234
|
+
* `X-Codex-Version` and install provenance all read for their own reasons. Clamping the
|
|
235
|
+
* persisted value itself would corrupt every one of them to fix one question.
|
|
236
|
+
*/
|
|
237
|
+
function raisedToGatedFloor(selected: string | null): string {
|
|
238
|
+
if (selected === null) return GATED_MODEL_CLIENT_VERSION_FLOOR;
|
|
239
|
+
return compareClientVersions(selected, GATED_MODEL_CLIENT_VERSION_FLOOR) >= 0
|
|
240
|
+
? selected
|
|
241
|
+
: GATED_MODEL_CLIENT_VERSION_FLOOR;
|
|
199
242
|
}
|
|
200
243
|
const MODEL_ROSTER_TTL_MS = 5 * 60_000;
|
|
201
244
|
|
|
@@ -575,6 +618,13 @@ async function fetchAccountModels(
|
|
|
575
618
|
return unconfirmedAccountModels(credential, clientVersion, now, { kind: "parsed-empty" });
|
|
576
619
|
}
|
|
577
620
|
const hasUnknownGatedAbsence = [...ACCOUNT_GATED_NATIVE_MODEL_MINIMUM_CLIENT_VERSIONS]
|
|
621
|
+
// Reachable only through tier 1, an inbound client_version below the floor. Every other
|
|
622
|
+
// resolution is now structurally >= every recorded minimum, because the floor is the max
|
|
623
|
+
// of the derived value and the same measured constant these minimums hold. So this is the
|
|
624
|
+
// escape hatch for a self-declared old client, not live protection on the background path:
|
|
625
|
+
// there, an absence really was asked for at an adequate version and is a denial. If
|
|
626
|
+
// upstream ever raises its true requirement above the measured constant, that constant is
|
|
627
|
+
// the only thing standing between an entitled account and a five-minute cached denial.
|
|
578
628
|
.some(([modelId, minimum]) => (
|
|
579
629
|
!models.has(modelId) && compareClientVersions(clientVersion, minimum) < 0
|
|
580
630
|
));
|
|
@@ -32,6 +32,8 @@ interface TomlDocument {
|
|
|
32
32
|
sections: Map<string, Record<string, string>>;
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
type TomlMultilineDelimiter = '"""' | "'''";
|
|
36
|
+
|
|
35
37
|
let diagnosticsCache: { at: number; warnings: ProjectCodexConfigWarning[] } | null = null;
|
|
36
38
|
|
|
37
39
|
function hasInjectedOpenaiBaseUrl(content: string): boolean {
|
|
@@ -55,14 +57,102 @@ function parseTomlString(raw: string): string {
|
|
|
55
57
|
return raw.slice(1, -1);
|
|
56
58
|
}
|
|
57
59
|
|
|
60
|
+
function multilineCloseIndex(
|
|
61
|
+
line: string,
|
|
62
|
+
delimiter: TomlMultilineDelimiter,
|
|
63
|
+
from: number,
|
|
64
|
+
): number {
|
|
65
|
+
let index = line.indexOf(delimiter, from);
|
|
66
|
+
while (index >= 0 && delimiter === '"""') {
|
|
67
|
+
let backslashes = 0;
|
|
68
|
+
for (let cursor = index - 1; cursor >= 0 && line[cursor] === "\\"; cursor -= 1) {
|
|
69
|
+
backslashes += 1;
|
|
70
|
+
}
|
|
71
|
+
if (backslashes % 2 === 0) break;
|
|
72
|
+
index = line.indexOf(delimiter, index + delimiter.length);
|
|
73
|
+
}
|
|
74
|
+
return index;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Find a multiline TOML string that starts outside a comment or single-line string.
|
|
79
|
+
*
|
|
80
|
+
* This parser intentionally understands only the root/table subset needed by Codex
|
|
81
|
+
* diagnostics. It still has to skip multiline string bodies lexically: prose in
|
|
82
|
+
* `developer_instructions` can contain key-shaped examples or `[table]` snippets, and
|
|
83
|
+
* treating those examples as configuration changes the diagnostic's meaning.
|
|
84
|
+
*/
|
|
85
|
+
function multilineStateAfterLine(
|
|
86
|
+
line: string,
|
|
87
|
+
active: TomlMultilineDelimiter | null,
|
|
88
|
+
): { active: TomlMultilineDelimiter | null; consumed: boolean } {
|
|
89
|
+
if (active) {
|
|
90
|
+
const close = multilineCloseIndex(line, active, 0);
|
|
91
|
+
if (close < 0) return { active, consumed: true };
|
|
92
|
+
const tail = multilineStateAfterLine(line.slice(close + active.length), null);
|
|
93
|
+
return {
|
|
94
|
+
active: tail.active,
|
|
95
|
+
consumed: true,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let quote: '"' | "'" | null = null;
|
|
100
|
+
let escaped = false;
|
|
101
|
+
let consumed = false;
|
|
102
|
+
for (let index = 0; index < line.length; index += 1) {
|
|
103
|
+
const character = line[index]!;
|
|
104
|
+
if (quote === '"') {
|
|
105
|
+
if (escaped) escaped = false;
|
|
106
|
+
else if (character === "\\") escaped = true;
|
|
107
|
+
else if (character === quote) quote = null;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (quote === "'") {
|
|
111
|
+
if (character === quote) quote = null;
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
if (character === "#") return { active: null, consumed };
|
|
115
|
+
|
|
116
|
+
const delimiter: TomlMultilineDelimiter | null = line.startsWith('"""', index)
|
|
117
|
+
? '"""'
|
|
118
|
+
: line.startsWith("'''", index)
|
|
119
|
+
? "'''"
|
|
120
|
+
: null;
|
|
121
|
+
if (delimiter) {
|
|
122
|
+
consumed = true;
|
|
123
|
+
const close = multilineCloseIndex(line, delimiter, index + delimiter.length);
|
|
124
|
+
if (close < 0) return { active: delimiter, consumed: true };
|
|
125
|
+
index = close + delimiter.length - 1;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (character === '"' || character === "'") quote = character;
|
|
129
|
+
}
|
|
130
|
+
return { active: null, consumed };
|
|
131
|
+
}
|
|
132
|
+
|
|
58
133
|
/** Lightweight TOML parse for root keys and [section] tables (Codex config shape). */
|
|
59
134
|
export function parseTomlDocument(content: string): TomlDocument {
|
|
60
135
|
const root: Record<string, string> = {};
|
|
61
136
|
const sections = new Map<string, Record<string, string>>();
|
|
62
137
|
let current = root;
|
|
138
|
+
let multiline: TomlMultilineDelimiter | null = null;
|
|
63
139
|
|
|
64
140
|
for (const line of content.split("\n")) {
|
|
65
|
-
const
|
|
141
|
+
const wasMultiline = multiline !== null;
|
|
142
|
+
const multilineState = multilineStateAfterLine(line, multiline);
|
|
143
|
+
multiline = multilineState.active;
|
|
144
|
+
if (multilineState.consumed) {
|
|
145
|
+
// The declaration itself is still configuration even though its multiline VALUE must
|
|
146
|
+
// not be scanned as keys/tables. A legacy root key using a multiline value therefore
|
|
147
|
+
// remains visible to strict-config diagnostics; only the body is opaque.
|
|
148
|
+
if (!wasMultiline) {
|
|
149
|
+
const declaration = line.match(/^\s*([A-Za-z0-9_.-]+)\s*=\s*(?:"""|''')/);
|
|
150
|
+
if (declaration) current[declaration[1]!] = "";
|
|
151
|
+
}
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const table = line.match(/^\s*\[([^\]]+)\]\s*(?:#.*)?$/);
|
|
66
156
|
if (table) {
|
|
67
157
|
const name = table[1]!.trim();
|
|
68
158
|
const section = sections.get(name) ?? {};
|
|
@@ -422,4 +512,4 @@ export function printProjectCodexConfigWarnings(
|
|
|
422
512
|
}
|
|
423
513
|
}
|
|
424
514
|
return warnings;
|
|
425
|
-
}
|
|
515
|
+
}
|