@bitkyc08/opencodex 2.11.1 → 2.12.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.
Files changed (175) hide show
  1. package/README.md +1 -1
  2. package/bin/ocx.mjs +4 -0
  3. package/gui/dist/assets/index-BNVYzdn0.css +1 -0
  4. package/gui/dist/assets/index-Cw1_mxO8.js +70 -0
  5. package/gui/dist/index.html +2 -2
  6. package/package.json +1 -1
  7. package/src/adapters/command-code.ts +94 -5
  8. package/src/adapters/cursor/live-transport.ts +14 -1
  9. package/src/adapters/google-tool-schema.ts +80 -15
  10. package/src/adapters/google.ts +24 -7
  11. package/src/adapters/openai-chat.ts +233 -305
  12. package/src/adapters/openai-responses.ts +62 -0
  13. package/src/adapters/tool-catalog-nudge.ts +2 -11
  14. package/src/claude/outbound.ts +42 -14
  15. package/src/cli/claude.ts +61 -9
  16. package/src/cli/codex-shim-autorestore.ts +2 -0
  17. package/src/cli/codex-shim-readiness.ts +8 -1
  18. package/src/cli/config-command.ts +4 -1
  19. package/src/cli/doctor.ts +10 -0
  20. package/src/cli/help.ts +15 -0
  21. package/src/cli/index.ts +27 -11
  22. package/src/cli/lab.ts +367 -0
  23. package/src/cli/provider.ts +9 -1
  24. package/src/clients/config-export.ts +100 -3
  25. package/src/codex/app-server-processes.ts +11 -8
  26. package/src/codex/auth-api.ts +92 -28
  27. package/src/codex/auth-collision.ts +5 -3
  28. package/src/codex/catalog/metadata.ts +7 -3
  29. package/src/codex/catalog/provider-fetch.ts +191 -47
  30. package/src/codex/catalog.ts +2 -2
  31. package/src/codex/history-job.ts +83 -18
  32. package/src/codex/history-migration-guardian.ts +9 -16
  33. package/src/codex/history-provider.ts +178 -1
  34. package/src/codex/history-transition.ts +5 -5
  35. package/src/codex/history-worker.ts +22 -2
  36. package/src/codex/native-profile-store.ts +1 -1
  37. package/src/codex/plan.ts +15 -0
  38. package/src/codex/quota.ts +6 -6
  39. package/src/codex/routing.ts +3 -3
  40. package/src/codex/subagent-model-fallback.ts +246 -22
  41. package/src/codex/user-identity.ts +63 -12
  42. package/src/config.ts +265 -32
  43. package/src/images/loop.ts +3 -1
  44. package/src/images/plan.ts +23 -13
  45. package/src/integrations/omp-yaml-source.ts +225 -0
  46. package/src/integrations/ownership.ts +5 -5
  47. package/src/integrations/state.ts +45 -6
  48. package/src/integrations/writer.ts +38 -3
  49. package/src/lab/artifacts/sanitize.ts +586 -0
  50. package/src/lab/artifacts/secure-fs.ts +475 -0
  51. package/src/lab/artifacts/store.ts +310 -0
  52. package/src/lab/conformance/assertion.ts +325 -0
  53. package/src/lab/conformance/digest.ts +22 -0
  54. package/src/lab/conformance/executor.ts +741 -0
  55. package/src/lab/conformance/fixture-provider.ts +27 -0
  56. package/src/lab/conformance/fixtures/live-v1-cases.json +175 -0
  57. package/src/lab/conformance/fixtures/protocol-v1-cases.json +461 -0
  58. package/src/lab/conformance/harness-budget.ts +47 -0
  59. package/src/lab/conformance/index.ts +5 -0
  60. package/src/lab/conformance/jcs.ts +24 -0
  61. package/src/lab/conformance/json-pointer.ts +39 -0
  62. package/src/lab/conformance/manifest.ts +180 -0
  63. package/src/lab/conformance/mcp-stub.ts +179 -0
  64. package/src/lab/conformance/negative-controls.ts +160 -0
  65. package/src/lab/conformance/observation.ts +355 -0
  66. package/src/lab/conformance/runner.ts +57 -0
  67. package/src/lab/conformance/sse-normalize.ts +59 -0
  68. package/src/lab/conformance/suite-manifest.ts +78 -0
  69. package/src/lab/conformance/types.ts +191 -0
  70. package/src/lab/constants.ts +126 -0
  71. package/src/lab/digest.ts +64 -0
  72. package/src/lab/events/errors.ts +9 -0
  73. package/src/lab/events/limits.ts +117 -0
  74. package/src/lab/events/types.ts +228 -0
  75. package/src/lab/events/validate.ts +780 -0
  76. package/src/lab/index.ts +36 -0
  77. package/src/lab/ledger/artifact-refs.ts +127 -0
  78. package/src/lab/ledger/invalidation.ts +136 -0
  79. package/src/lab/ledger/purge.ts +241 -0
  80. package/src/lab/ledger/store.ts +263 -0
  81. package/src/lab/live/credential-lease.ts +53 -0
  82. package/src/lab/live/destination.ts +155 -0
  83. package/src/lab/live/executor.ts +311 -0
  84. package/src/lab/live/inert-tools.ts +56 -0
  85. package/src/lab/live/manifest.ts +85 -0
  86. package/src/lab/live/mcp-loopback.ts +57 -0
  87. package/src/lab/live/runner.ts +19 -0
  88. package/src/lab/live/sandbox.ts +61 -0
  89. package/src/lab/live/suite-manifest.ts +41 -0
  90. package/src/lab/live/transport.ts +116 -0
  91. package/src/lab/live/types.ts +197 -0
  92. package/src/lab/observe/from-conformance.ts +344 -0
  93. package/src/lab/observe/from-live.ts +103 -0
  94. package/src/lab/paths.ts +65 -0
  95. package/src/lab/projection/rebuild.ts +477 -0
  96. package/src/lab/projection/schema.ts +122 -0
  97. package/src/lab/projection/verdicts.ts +438 -0
  98. package/src/lab/projection/verification.ts +332 -0
  99. package/src/lab/query/catalog.ts +72 -0
  100. package/src/lab/query/connection.ts +107 -0
  101. package/src/lab/query/constants.ts +4 -0
  102. package/src/lab/query/cursor.ts +132 -0
  103. package/src/lab/query/dto-map.ts +277 -0
  104. package/src/lab/query/errors.ts +22 -0
  105. package/src/lab/query/index.ts +34 -0
  106. package/src/lab/query/queries.ts +444 -0
  107. package/src/lab/query/types.ts +266 -0
  108. package/src/lab/subject/behavior-fingerprint.ts +77 -0
  109. package/src/lab/subject/installation-salt.ts +81 -0
  110. package/src/lab/subject/route-subject.ts +73 -0
  111. package/src/lib/bounded-body.ts +104 -0
  112. package/src/lib/lab-live-execution-authority.ts +13 -0
  113. package/src/lib/lab-live-host.ts +30 -0
  114. package/src/lib/lab-live-pinned-sender.ts +45 -0
  115. package/src/lib/pinned-http.ts +36 -7
  116. package/src/oauth/index.ts +20 -1
  117. package/src/oauth/local-token-detect.ts +11 -2
  118. package/src/oauth/login-cli.ts +38 -4
  119. package/src/providers/alibaba-region-migration.ts +1 -1
  120. package/src/providers/codex-capacity.ts +7 -4
  121. package/src/providers/command-code-efforts.ts +5 -2
  122. package/src/providers/derive.ts +105 -0
  123. package/src/providers/label.ts +1 -1
  124. package/src/providers/openai-tiers.ts +59 -3
  125. package/src/providers/opencode-zen-rate-limit.ts +102 -0
  126. package/src/providers/quota.ts +67 -21
  127. package/src/providers/registry.ts +118 -5
  128. package/src/router.ts +15 -1
  129. package/src/routing/analytics.ts +8 -7
  130. package/src/routing/evaluator.ts +51 -5
  131. package/src/routing/profile.ts +10 -1
  132. package/src/routing/trace.ts +42 -6
  133. package/src/server/auth-cors.ts +15 -0
  134. package/src/server/background-lifecycle.ts +169 -0
  135. package/src/server/chat-completions.ts +18 -3
  136. package/src/server/claude-messages.ts +1 -0
  137. package/src/server/images.ts +78 -11
  138. package/src/server/index.ts +72 -18
  139. package/src/server/lifecycle.ts +6 -0
  140. package/src/server/live.ts +5 -1
  141. package/src/server/management/agent-settings-routes.ts +41 -24
  142. package/src/server/management/body.ts +6 -0
  143. package/src/server/management/config-routes.ts +51 -8
  144. package/src/server/management/lab-routes.ts +388 -0
  145. package/src/server/management/logs-usage-routes.ts +19 -1
  146. package/src/server/management/model-rows.ts +25 -11
  147. package/src/server/management/native-integration-routes.ts +3 -3
  148. package/src/server/management/provider-routes.ts +5 -0
  149. package/src/server/management/shared.ts +8 -1
  150. package/src/server/management/usage-summary-cache.ts +2 -0
  151. package/src/server/management/vision-sidecar-options.ts +125 -0
  152. package/src/server/management-api.ts +2 -0
  153. package/src/server/request-decompress.ts +106 -5
  154. package/src/server/responses/collaboration.ts +4 -1
  155. package/src/server/responses/core.ts +117 -31
  156. package/src/server/responses-json-events.ts +50 -12
  157. package/src/server/search.ts +23 -7
  158. package/src/server/ws-bridge.ts +3 -1
  159. package/src/service.ts +40 -20
  160. package/src/types.ts +74 -0
  161. package/src/update/job.ts +10 -2
  162. package/src/usage/cost.ts +0 -0
  163. package/src/usage/expected-prices.ts +9 -0
  164. package/src/usage/log.ts +9 -9
  165. package/src/usage/user-cost-overlay-reconciler.ts +313 -0
  166. package/src/usage/user-cost-overlays.ts +314 -0
  167. package/src/vision/describe.ts +2 -2
  168. package/src/vision/eligibility.ts +229 -0
  169. package/src/vision/index.ts +20 -2
  170. package/src/web-search/executor.ts +2 -2
  171. package/src/web-search/index.ts +4 -2
  172. package/src/web-search/loop.ts +3 -1
  173. package/src/web-search/parse.ts +72 -3
  174. package/gui/dist/assets/index-BA1dgo4q.css +0 -1
  175. package/gui/dist/assets/index-DDZpgzKk.js +0 -70
package/src/cli/lab.ts ADDED
@@ -0,0 +1,367 @@
1
+ /**
2
+ * `ocx lab` — read-only Compatibility Lab inspection (CL-04).
3
+ *
4
+ * Local SQLite projection reads; no daemon, network, probes, or rebuilds.
5
+ */
6
+ import { getConfigDir } from "../config";
7
+ import {
8
+ ARTIFACT_CLASSES,
9
+ EVIDENCE_LAYERS,
10
+ EVENT_KINDS,
11
+ EXECUTION_MODES,
12
+ OUTCOMES,
13
+ VERDICTS,
14
+ type ArtifactClass,
15
+ type CompatibilityVerdict,
16
+ type EvidenceLayer,
17
+ type ExecutionMode,
18
+ type LabEventKind,
19
+ type ObservationOutcome,
20
+ } from "../lab/constants";
21
+ import {
22
+ InvalidCursorError,
23
+ LabProjectionIncompatibleError,
24
+ LabProjectionUnavailableError,
25
+ queryLabArtifactByDigest,
26
+ queryLabArtifacts,
27
+ queryLabCatalogEntries,
28
+ queryLabEventById,
29
+ queryLabEvents,
30
+ queryLabObservations,
31
+ queryLabStatus,
32
+ queryLabSubjectById,
33
+ queryLabSubjects,
34
+ queryLabVerdicts,
35
+ } from "../lab/query";
36
+ import {
37
+ CliUsageError,
38
+ RuntimeApiError,
39
+ printData,
40
+ rejectArgs,
41
+ runCliAction,
42
+ takeFlag,
43
+ takeIntegerOption,
44
+ takeOption,
45
+ } from "./runtime-api";
46
+
47
+ const USAGE = `Usage:
48
+ ocx lab status [--json]
49
+ ocx lab verdicts [--subject <id>] [--layer <layer>] [--suite <id>] [--verdict <v>] [--from <ms>] [--to <ms>] [--limit <n>] [--cursor <c>] [--json]
50
+ ocx lab subjects [--kind <kind>] [--limit <n>] [--cursor <c>] [--json]
51
+ ocx lab subject <subjectId> [--json]
52
+ ocx lab observations [--subject <id>] [--layer <layer>] [--suite <id>] [--scenario <id>] [--outcome <o>] [--execution-mode <m>] [--from <ms>] [--to <ms>] [--limit <n>] [--cursor <c>] [--json]
53
+ ocx lab events [--event-kind <k>] [--subject <id>] [--from <ms>] [--to <ms>] [--excluded <true|false>] [--limit <n>] [--cursor <c>] [--json]
54
+ ocx lab event <eventId> [--json]
55
+ ocx lab artifacts [--status <s>] [--artifact-class <c>] [--limit <n>] [--cursor <c>] [--json]
56
+ ocx lab artifact <digest> [--json]
57
+ ocx lab catalog [--layer <layer>] [--suite <id>] [--json]`;
58
+
59
+ const ARTIFACT_STATUSES = ["present", "corrupt", "purged_unavailable"] as const;
60
+ type ArtifactStatus = (typeof ARTIFACT_STATUSES)[number];
61
+
62
+ export interface LabCliDeps {
63
+ configDir?: string;
64
+ }
65
+
66
+ class LabStateError extends RuntimeApiError {
67
+ constructor(message: string) {
68
+ super(message, 503, null);
69
+ this.name = "LabStateError";
70
+ }
71
+ }
72
+
73
+ function labErrorMessage(err: unknown): string {
74
+ if (err instanceof LabProjectionUnavailableError) return "lab projection is not available";
75
+ if (err instanceof LabProjectionIncompatibleError) return "lab projection schema or spec version is incompatible";
76
+ if (err instanceof InvalidCursorError) return "invalid cursor";
77
+ return "lab read failed";
78
+ }
79
+
80
+ function takeEnumOption<T extends string>(
81
+ args: string[],
82
+ flag: string,
83
+ values: readonly T[],
84
+ message: string,
85
+ ): T | undefined {
86
+ const raw = takeOption(args, flag);
87
+ if (raw === undefined) return undefined;
88
+ if (!(values as readonly string[]).includes(raw)) {
89
+ throw new CliUsageError(message, USAGE);
90
+ }
91
+ return raw as T;
92
+ }
93
+
94
+ function assertRange(from: number | undefined, to: number | undefined): void {
95
+ if (from !== undefined && to !== undefined && from > to) {
96
+ throw new CliUsageError("--from must not be greater than --to", USAGE);
97
+ }
98
+ }
99
+
100
+ function appendPaginationHint(
101
+ lines: string[],
102
+ page: { hasMore: boolean; nextCursor?: string | null },
103
+ ): void {
104
+ if (page.hasMore) lines.push(`(more available; pass --cursor ${page.nextCursor ?? ""})`);
105
+ }
106
+
107
+ function statusSummary(status: ReturnType<typeof queryLabStatus>): string[] {
108
+ if (!status.projectionAvailable) {
109
+ if (status.projectionIncompatible) return ["Lab projection: incompatible"];
110
+ return ["Lab projection: unavailable"];
111
+ }
112
+ return [
113
+ "Lab projection: available",
114
+ `SQLite schema: ${status.sqliteSchemaVersion}`,
115
+ `Projection spec: ${status.projectionSpecVersion}`,
116
+ `Built at: ${status.builtAtMs}`,
117
+ `Events: ${status.eventCount} | Subjects: ${status.subjectCount} | Observations: ${status.observationCount}`,
118
+ `Claims: ${status.claimCount} | Verdicts: ${status.verdictCount} | Artifacts: ${status.artifactCount}`,
119
+ `Corruption rows: ${status.corruptionCount}`,
120
+ ];
121
+ }
122
+
123
+ function verdictLines(page: ReturnType<typeof queryLabVerdicts>): string[] {
124
+ const lines = page.items.map((v) =>
125
+ `${v.verdict} ${v.evidenceLayer} ${v.suiteId} subject=${v.subjectId} asOf=${v.asOf}`,
126
+ );
127
+ appendPaginationHint(lines, page);
128
+ return lines.length > 0 ? lines : ["No verdicts"];
129
+ }
130
+
131
+ function subjectListLines(page: ReturnType<typeof queryLabSubjects>): string[] {
132
+ const lines = page.items.map((s) => `${s.subjectId} (${s.subjectKind})`);
133
+ appendPaginationHint(lines, page);
134
+ return lines.length > 0 ? lines : ["No subjects"];
135
+ }
136
+
137
+ function observationLines(page: ReturnType<typeof queryLabObservations>): string[] {
138
+ const lines = page.items.map((o) =>
139
+ `${o.outcome} ${o.evidenceLayer} ${o.scenarioId} event=${o.eventId} completed=${o.completedAt}`,
140
+ );
141
+ appendPaginationHint(lines, page);
142
+ return lines.length > 0 ? lines : ["No observations"];
143
+ }
144
+
145
+ function eventListLines(page: ReturnType<typeof queryLabEvents>): string[] {
146
+ const lines = page.items.map((e) =>
147
+ `${e.eventKind} ${e.eventId} recorded=${e.recordedAt}${e.excluded ? " excluded" : ""}`,
148
+ );
149
+ appendPaginationHint(lines, page);
150
+ return lines.length > 0 ? lines : ["No events"];
151
+ }
152
+
153
+ function artifactLines(page: ReturnType<typeof queryLabArtifacts>): string[] {
154
+ const lines = page.items.map((a) => `${a.status} ${a.digest} class=${a.artifactClass ?? "unknown"}`);
155
+ appendPaginationHint(lines, page);
156
+ return lines.length > 0 ? lines : ["No artifacts"];
157
+ }
158
+
159
+ function catalogLines(scenarios: ReturnType<typeof queryLabCatalogEntries>): string[] {
160
+ const lines = scenarios.map((s) =>
161
+ `${s.evidenceLayer} ${s.suiteId} ${s.scenarioId} digest=${s.scenarioManifestDigest.slice(0, 12)}…`,
162
+ );
163
+ return lines.length > 0 ? lines : ["No catalog scenarios"];
164
+ }
165
+
166
+ export async function handleLabCommand(argv: string[], deps: LabCliDeps = {}): Promise<number> {
167
+ return runCliAction(async () => {
168
+ const configDir = deps.configDir ?? getConfigDir();
169
+ const argvCopy = [...argv];
170
+ const wantsJson = takeFlag(argvCopy, "--json");
171
+ const [sub = "status", ...rest] = argvCopy;
172
+
173
+ try {
174
+ switch (sub) {
175
+ case "status": {
176
+ rejectArgs(rest, USAGE);
177
+ const status = queryLabStatus(configDir);
178
+ printData(status, wantsJson, statusSummary(status));
179
+ return;
180
+ }
181
+ case "verdicts": {
182
+ const subjectId = takeOption(rest, "--subject");
183
+ const layer = takeEnumOption<EvidenceLayer>(
184
+ rest,
185
+ "--layer",
186
+ EVIDENCE_LAYERS,
187
+ "--layer must be a supported evidence layer",
188
+ );
189
+ const suiteId = takeOption(rest, "--suite");
190
+ const verdict = takeEnumOption<CompatibilityVerdict>(
191
+ rest,
192
+ "--verdict",
193
+ VERDICTS,
194
+ "--verdict must be a supported compatibility verdict",
195
+ );
196
+ const from = takeIntegerOption(rest, "--from", { min: 0 });
197
+ const to = takeIntegerOption(rest, "--to", { min: 0 });
198
+ assertRange(from, to);
199
+ const limit = takeIntegerOption(rest, "--limit", { min: 1 });
200
+ const cursor = takeOption(rest, "--cursor");
201
+ rejectArgs(rest, USAGE);
202
+ const page = queryLabVerdicts({
203
+ subjectId,
204
+ layer,
205
+ suiteId,
206
+ verdict,
207
+ from,
208
+ to,
209
+ }, cursor, limit, configDir);
210
+ printData(page, wantsJson, verdictLines(page));
211
+ return;
212
+ }
213
+ case "subjects": {
214
+ const kind = takeOption(rest, "--kind");
215
+ const limit = takeIntegerOption(rest, "--limit", { min: 1 });
216
+ const cursor = takeOption(rest, "--cursor");
217
+ rejectArgs(rest, USAGE);
218
+ const page = queryLabSubjects(kind, cursor, limit, configDir);
219
+ printData(page, wantsJson, subjectListLines(page));
220
+ return;
221
+ }
222
+ case "subject": {
223
+ const subjectId = rest[0];
224
+ if (!subjectId) throw new CliUsageError("subject id required", USAGE);
225
+ rest.splice(0, 1);
226
+ rejectArgs(rest, USAGE);
227
+ const subject = queryLabSubjectById(subjectId, configDir);
228
+ if (!subject) throw new CliUsageError("unknown subject", USAGE);
229
+ printData({ subject }, wantsJson, [`Subject ${subjectId}`]);
230
+ return;
231
+ }
232
+ case "observations": {
233
+ const subjectId = takeOption(rest, "--subject");
234
+ const layer = takeEnumOption<EvidenceLayer>(
235
+ rest,
236
+ "--layer",
237
+ EVIDENCE_LAYERS,
238
+ "--layer must be a supported evidence layer",
239
+ );
240
+ const suiteId = takeOption(rest, "--suite");
241
+ const scenarioId = takeOption(rest, "--scenario");
242
+ const outcome = takeEnumOption<ObservationOutcome>(
243
+ rest,
244
+ "--outcome",
245
+ OUTCOMES,
246
+ "--outcome must be a supported observation outcome",
247
+ );
248
+ const executionMode = takeEnumOption<ExecutionMode>(
249
+ rest,
250
+ "--execution-mode",
251
+ EXECUTION_MODES,
252
+ "--execution-mode must be a supported execution mode",
253
+ );
254
+ const from = takeIntegerOption(rest, "--from", { min: 0 });
255
+ const to = takeIntegerOption(rest, "--to", { min: 0 });
256
+ assertRange(from, to);
257
+ const limit = takeIntegerOption(rest, "--limit", { min: 1 });
258
+ const cursor = takeOption(rest, "--cursor");
259
+ rejectArgs(rest, USAGE);
260
+ const page = queryLabObservations({
261
+ subjectId,
262
+ layer,
263
+ suiteId,
264
+ scenarioId,
265
+ outcome,
266
+ executionMode,
267
+ from,
268
+ to,
269
+ }, cursor, limit, configDir);
270
+ printData(page, wantsJson, observationLines(page));
271
+ return;
272
+ }
273
+ case "events": {
274
+ const eventKind = takeEnumOption<LabEventKind>(
275
+ rest,
276
+ "--event-kind",
277
+ EVENT_KINDS,
278
+ "--event-kind must be a supported lab event kind",
279
+ );
280
+ const subjectId = takeOption(rest, "--subject");
281
+ const from = takeIntegerOption(rest, "--from", { min: 0 });
282
+ const to = takeIntegerOption(rest, "--to", { min: 0 });
283
+ assertRange(from, to);
284
+ const excludedRaw = takeOption(rest, "--excluded");
285
+ if (excludedRaw !== undefined && excludedRaw !== "true" && excludedRaw !== "false") {
286
+ throw new CliUsageError("--excluded must be true or false", USAGE);
287
+ }
288
+ const limit = takeIntegerOption(rest, "--limit", { min: 1 });
289
+ const cursor = takeOption(rest, "--cursor");
290
+ rejectArgs(rest, USAGE);
291
+ const page = queryLabEvents({
292
+ eventKind,
293
+ subjectId,
294
+ from,
295
+ to,
296
+ excluded: excludedRaw === "true" ? true : excludedRaw === "false" ? false : undefined,
297
+ }, cursor, limit, configDir);
298
+ printData(page, wantsJson, eventListLines(page));
299
+ return;
300
+ }
301
+ case "event": {
302
+ const eventId = rest[0];
303
+ if (!eventId) throw new CliUsageError("event id required", USAGE);
304
+ rest.splice(0, 1);
305
+ rejectArgs(rest, USAGE);
306
+ const event = queryLabEventById(eventId, configDir);
307
+ if (!event) throw new CliUsageError("unknown event", USAGE);
308
+ printData({ event }, wantsJson, [`Event ${eventId}`]);
309
+ return;
310
+ }
311
+ case "artifacts": {
312
+ const status = takeEnumOption<ArtifactStatus>(
313
+ rest,
314
+ "--status",
315
+ ARTIFACT_STATUSES,
316
+ "--status must be present, corrupt, or purged_unavailable",
317
+ );
318
+ const artifactClass = takeEnumOption<ArtifactClass>(
319
+ rest,
320
+ "--artifact-class",
321
+ ARTIFACT_CLASSES,
322
+ "--artifact-class must be a supported artifact class",
323
+ );
324
+ const limit = takeIntegerOption(rest, "--limit", { min: 1 });
325
+ const cursor = takeOption(rest, "--cursor");
326
+ rejectArgs(rest, USAGE);
327
+ const page = queryLabArtifacts({ status, artifactClass }, cursor, limit, configDir);
328
+ printData(page, wantsJson, artifactLines(page));
329
+ return;
330
+ }
331
+ case "artifact": {
332
+ const digest = rest[0];
333
+ if (!digest) throw new CliUsageError("artifact digest required", USAGE);
334
+ rest.splice(0, 1);
335
+ rejectArgs(rest, USAGE);
336
+ const artifact = queryLabArtifactByDigest(digest, configDir);
337
+ if (!artifact) throw new CliUsageError("unknown artifact", USAGE);
338
+ printData({ artifact }, wantsJson, [`Artifact ${digest}`]);
339
+ return;
340
+ }
341
+ case "catalog": {
342
+ const layer = takeEnumOption<EvidenceLayer>(
343
+ rest,
344
+ "--layer",
345
+ EVIDENCE_LAYERS,
346
+ "--layer must be a supported evidence layer",
347
+ );
348
+ const suiteId = takeOption(rest, "--suite");
349
+ rejectArgs(rest, USAGE);
350
+ const scenarios = queryLabCatalogEntries({ layer, suiteId });
351
+ printData({ scenarios }, wantsJson, catalogLines(scenarios));
352
+ return;
353
+ }
354
+ default:
355
+ throw new CliUsageError(`unknown lab subcommand: ${sub}`, USAGE);
356
+ }
357
+ } catch (err) {
358
+ if (err instanceof CliUsageError) throw err;
359
+ if (err instanceof LabProjectionUnavailableError || err instanceof LabProjectionIncompatibleError) {
360
+ throw new LabStateError(labErrorMessage(err));
361
+ }
362
+ throw new CliUsageError(labErrorMessage(err), USAGE);
363
+ }
364
+ });
365
+ }
366
+
367
+ export const LAB_USAGE = USAGE;
@@ -8,7 +8,7 @@
8
8
  * show <name> Show provider config details (secrets masked)
9
9
  * set-default <name> Change the default provider
10
10
  */
11
- import { apiKeyTransportConfigError, hasOwnProvider, isValidProviderName, loadConfig, saveConfig } from "../config";
11
+ import { apiKeyTransportConfigError, hasOwnProvider, isValidProviderName, loadConfig, sanitizeModelCostsForDisplay, saveConfig } from "../config";
12
12
  import { hasHelpFlag } from "./help";
13
13
  import { getProviderRegistryEntry, PROVIDER_REGISTRY } from "../providers/registry";
14
14
  import { providerConfigSeed } from "../providers/derive";
@@ -210,7 +210,14 @@ async function handleAdd(args: string[]): Promise<void> {
210
210
  provConfig.apiKeyTransport = apiKeyTransport;
211
211
  }
212
212
 
213
+ const existingProvider = config.providers[name];
213
214
  config.providers[name] = provConfig;
215
+ // A --force overwrite rotates the key/endpoint but must not drop a
216
+ // user-configured price overlay (same rule as the /api/providers path and
217
+ // the login paths); there is no explicit clear/replace flag yet.
218
+ if (existingProvider?.modelCosts !== undefined && provConfig.modelCosts === undefined) {
219
+ provConfig.modelCosts = existingProvider.modelCosts;
220
+ }
214
221
  if (allowPrivateNetwork) provConfig.allowPrivateNetwork = true;
215
222
  if (setDefault) config.defaultProvider = name;
216
223
 
@@ -349,6 +356,7 @@ function handleShow(args: string[]): void {
349
356
  const prov = config.providers[name];
350
357
  const display = {
351
358
  ...prov,
359
+ ...(prov.modelCosts !== undefined ? { modelCosts: sanitizeModelCostsForDisplay(prov.modelCosts) } : {}),
352
360
  ...(prov.apiKey ? { apiKey: maskSecret(prov.apiKey) } : {}),
353
361
  ...(prov.apiKeyPool ? { apiKeyPool: prov.apiKeyPool.map(e => ({ ...e, key: maskSecret(e.key) })) } : {}),
354
362
  };
@@ -391,6 +391,9 @@ export interface ExportModel {
391
391
  displayName?: string;
392
392
  contextWindow?: number;
393
393
  inputModalities?: string[];
394
+ /** Optional effort ladder exported only to clients that support it. */
395
+ reasoningEfforts?: string[];
396
+ defaultReasoningEffort?: string;
394
397
  }
395
398
 
396
399
  export interface ExportContext {
@@ -637,6 +640,50 @@ export interface PiGeneratedConfig {
637
640
  providers: Record<string, PiProviderBlock>;
638
641
  }
639
642
 
643
+ /**
644
+ * omp accepts a model-level API override. Keep the provider on Chat
645
+ * Completions so routed providers retain their established wire format, while
646
+ * native OpenAI models can use the lossless Responses surface.
647
+ */
648
+ export interface OmpModelEntry extends PiModelEntry {
649
+ api?: "openai-responses";
650
+ /** omp requires this flag before it honors a thinking block. */
651
+ reasoning?: true;
652
+ thinking?: {
653
+ mode: "effort";
654
+ efforts: string[];
655
+ defaultLevel?: string;
656
+ };
657
+ }
658
+
659
+ export interface OmpProviderBlock {
660
+ baseUrl: string;
661
+ api: typeof PI_API_DIALECT;
662
+ apiKey: string;
663
+ models: OmpModelEntry[];
664
+ }
665
+
666
+ export interface OmpGeneratedConfig {
667
+ providers: Record<string, OmpProviderBlock>;
668
+ }
669
+
670
+ /**
671
+ * omp validates model entries strictly. These are its documented effort
672
+ * values; omit an unknown value rather than invalidating the whole provider.
673
+ */
674
+ const OMP_EFFORT_VOCABULARY = new Set(["minimal", "low", "medium", "high", "xhigh", "max"]);
675
+
676
+ function ompEfforts(model: ExportModel): string[] {
677
+ const efforts: string[] = [];
678
+ for (const effort of model.reasoningEfforts ?? []) {
679
+ const normalized = effort.trim().toLowerCase();
680
+ if (OMP_EFFORT_VOCABULARY.has(normalized) && !efforts.includes(normalized)) {
681
+ efforts.push(normalized);
682
+ }
683
+ }
684
+ return efforts;
685
+ }
686
+
640
687
  /**
641
688
  * Hermes `~/.hermes/config.yaml`. We emit ONLY the provider entry — never
642
689
  * `model.default` — because hijacking the user's main model is not what a
@@ -772,6 +819,51 @@ function buildPiClientConfig(ctx: ExportContext): PiGeneratedConfig {
772
819
  };
773
820
  }
774
821
 
822
+ /**
823
+ * omp's models.yml is Pi-like, but it supports effort metadata and a per-model
824
+ * API dialect. Native OpenAI models use Responses; all routed models inherit
825
+ * the provider's existing Chat Completions dialect.
826
+ */
827
+ function buildOmpClientConfig(ctx: ExportContext): OmpGeneratedConfig {
828
+ const models: OmpModelEntry[] = [];
829
+ for (const model of normalizeExportModels(ctx.models)) {
830
+ const input = inputModalitiesForClient("pi", model.inputModalities);
831
+ if (input === null) continue;
832
+ const entry: OmpModelEntry = {
833
+ id: model.namespaced,
834
+ name: exportModelLabel(model),
835
+ input,
836
+ ...(model.native && model.provider === "openai" ? { api: "openai-responses" } : {}),
837
+ };
838
+ const context = authoritativeContextWindow(model.contextWindow);
839
+ if (context !== undefined) {
840
+ entry.contextWindow = context;
841
+ entry.maxTokens = outputBudgetFor(context);
842
+ }
843
+ const efforts = ompEfforts(model);
844
+ if (efforts.length > 0) {
845
+ const defaultLevel = model.defaultReasoningEffort?.trim().toLowerCase();
846
+ entry.reasoning = true;
847
+ entry.thinking = {
848
+ mode: "effort",
849
+ efforts,
850
+ ...(defaultLevel && efforts.includes(defaultLevel) ? { defaultLevel } : {}),
851
+ };
852
+ }
853
+ models.push(entry);
854
+ }
855
+ return {
856
+ providers: {
857
+ [OPENCODE_PROVIDER_ID]: {
858
+ baseUrl: ctx.baseUrl,
859
+ api: PI_API_DIALECT,
860
+ apiKey: LOOPBACK_API_KEY_PLACEHOLDER,
861
+ models,
862
+ },
863
+ },
864
+ };
865
+ }
866
+
775
867
  /** Extra headers a non-loopback bind needs, or nothing on loopback. */
776
868
  function proxyAdmissionHeaders(config: OcxConfig | undefined, envRef: string): Record<string, string> | undefined {
777
869
  return shouldInjectApiAuthHeader(config) ? { "x-opencodex-api-key": envRef } : undefined;
@@ -899,6 +991,11 @@ function summarizePi(document: unknown): { modelCount: number; modelsWithoutLimi
899
991
  return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
900
992
  }
901
993
 
994
+ function summarizeOmp(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
995
+ const models = (document as OmpGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
996
+ return { modelCount: models.length, modelsWithoutLimits: models.filter(model => model.contextWindow === undefined).length };
997
+ }
998
+
902
999
  function summarizeHermes(document: unknown): { modelCount: number; modelsWithoutLimits: number } {
903
1000
  const models = (document as HermesGeneratedConfig | undefined)?.providers?.[OPENCODE_PROVIDER_ID]?.models ?? [];
904
1001
  // Hermes carries selectors only; it has no per-model limit to be missing.
@@ -938,7 +1035,7 @@ function buildPiContribution(ctx: ExportContext): ManagedContribution {
938
1035
  }
939
1036
 
940
1037
  function buildOmpContribution(ctx: ExportContext): ManagedContribution {
941
- const doc = buildPiClientConfig(ctx);
1038
+ const doc = buildOmpClientConfig(ctx);
942
1039
  return singleFragment("omp", ["providers", OPENCODE_PROVIDER_ID], doc.providers[OPENCODE_PROVIDER_ID]);
943
1040
  }
944
1041
 
@@ -1007,9 +1104,9 @@ export const EXPORT_CLIENTS: Record<ExportClientId, ExportClientSpec> = {
1007
1104
  destination: env => ompModelsConfigPath(env),
1008
1105
  apiKeyEnv: "",
1009
1106
  exportHint: "OMP reads a non-secret placeholder from models.yml; loopback needs no key.",
1010
- build: buildPiClientConfig,
1107
+ build: buildOmpClientConfig,
1011
1108
  format: "yaml",
1012
- summarize: summarizePi,
1109
+ summarize: summarizeOmp,
1013
1110
  buildContribution: buildOmpContribution,
1014
1111
  // OMP supports provider-level headers, but remote credential wiring is
1015
1112
  // intentionally deferred from this initial loopback-only integration.
@@ -10,6 +10,7 @@
10
10
  import { execFileSync } from "node:child_process";
11
11
  import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
12
12
  import { isProcessAlive, waitForExit } from "../lib/process-control";
13
+ import { resolveTrustedWindowsPowerShellExe } from "../lib/windows-elevation";
13
14
  import { readCodexCatalogPath } from "./catalog/parsing";
14
15
 
15
16
  export const STALE_CODEX_APP_SERVER_HINT =
@@ -271,12 +272,12 @@ function listDarwinSnapshots(uid: number | undefined): ProcessSnapshot[] {
271
272
  // Top-level exec failure propagates: callers decide their own safe default
272
273
  // (restart flow → treat as none; staleness check → unknown, never "fresh").
273
274
  const output = uid !== undefined
274
- ? execFileSync("ps", ["-u", String(uid), "-o", "pid=,command="], {
275
+ ? execFileSync("/bin/ps", ["-u", String(uid), "-o", "pid=,command="], {
275
276
  encoding: "utf-8",
276
277
  stdio: ["ignore", "pipe", "ignore"],
277
278
  timeout: 5_000,
278
279
  })
279
- : execFileSync("ps", ["-axo", "pid=,uid=,command="], {
280
+ : execFileSync("/bin/ps", ["-axo", "pid=,uid=,command="], {
280
281
  encoding: "utf-8",
281
282
  stdio: ["ignore", "pipe", "ignore"],
282
283
  timeout: 5_000,
@@ -345,8 +346,10 @@ export function listWindowsSnapshots(): ProcessSnapshot[] {
345
346
  " } catch { \"__OCX_ENUM_INCOMPLETE__\" }",
346
347
  "}",
347
348
  ].join("\n");
348
- // Top-level exec failure propagates (see listDarwinSnapshots note).
349
- const output = execFileSync("powershell.exe", [
349
+ // Top-level exec failure propagates (see listDarwinSnapshots note). The
350
+ // executable resolves from the trusted System32 directory (never PATH), and
351
+ // windowsHide keeps the enumeration console-less on desktop sessions (#1278).
352
+ const output = execFileSync(resolveTrustedWindowsPowerShellExe(), [
350
353
  "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
351
354
  "-Command",
352
355
  psCommand,
@@ -441,7 +444,7 @@ function readLinuxProcStartMs(pid: number): number | null {
441
444
  /** `ps` lstart → epoch ms, or null (macOS). */
442
445
  function readDarwinProcStartMs(pid: number): number | null {
443
446
  try {
444
- const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
447
+ const out = execFileSync("/bin/ps", ["-o", "lstart=", "-p", String(pid)], {
445
448
  encoding: "utf-8",
446
449
  stdio: ["ignore", "pipe", "ignore"],
447
450
  timeout: 4_000,
@@ -457,7 +460,7 @@ function readDarwinProcStartMs(pid: number): number | null {
457
460
  /** Win32_Process.CreationDate → epoch ms, or null (Windows). */
458
461
  function readWindowsProcStartMs(pid: number): number | null {
459
462
  try {
460
- const out = execFileSync("powershell.exe", [
463
+ const out = execFileSync(resolveTrustedWindowsPowerShellExe(), [
461
464
  "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
462
465
  "-Command",
463
466
  `(Get-CimInstance Win32_Process -Filter "ProcessId=${pid}").CreationDate.ToUniversalTime().ToString("o")`,
@@ -490,7 +493,7 @@ export function readProcessStartMsBatch(
490
493
  if (pids.length === 0) return out;
491
494
  if (platform === "darwin") {
492
495
  try {
493
- const stdout = execFileSync("ps", ["-o", "pid=,lstart=", "-p", pids.join(",")], {
496
+ const stdout = execFileSync("/bin/ps", ["-o", "pid=,lstart=", "-p", pids.join(",")], {
494
497
  encoding: "utf-8",
495
498
  stdio: ["ignore", "pipe", "ignore"],
496
499
  timeout: 3_000,
@@ -513,7 +516,7 @@ export function readProcessStartMsBatch(
513
516
  if (platform === "win32") {
514
517
  try {
515
518
  const filter = pids.map(pid => `ProcessId=${pid}`).join(" OR ");
516
- const stdout = execFileSync("powershell.exe", [
519
+ const stdout = execFileSync(resolveTrustedWindowsPowerShellExe(), [
517
520
  "-NoProfile", "-NoLogo", "-NonInteractive", "-WindowStyle", "Hidden",
518
521
  "-Command",
519
522
  `Get-CimInstance Win32_Process -Filter "${filter}" | ForEach-Object { "$($_.ProcessId)\t$($_.CreationDate.ToUniversalTime().ToString("o"))" }`,