praxis-agent 0.43.1 → 0.44.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -27,7 +27,9 @@ sessions, configuration, or compatibility directories.
27
27
  - macOS or Linux
28
28
  - Node.js 24 or newer
29
29
  - [`ripgrep`](https://github.com/BurntSushi/ripgrep) (`rg`) for the Grep tool
30
- - an API key and model ID for an Anthropic or OpenAI-compatible provider
30
+ - an API key and model ID for an Anthropic or OpenAI-compatible provider (the
31
+ stable setup), or the explicitly enabled experimental ChatGPT-backed Codex
32
+ subscription integration
31
33
 
32
34
  Praxis does not use Claude subscription authentication. Claude-shaped message,
33
35
  tool, and CLI protocol forms remain supported where they are part of the
@@ -61,6 +63,15 @@ cd /path/to/project
61
63
  praxis
62
64
  ```
63
65
 
66
+ Praxis also has an experimental `openai-codex` provider for ChatGPT-backed
67
+ Codex subscriptions. It is separate from OpenAI API-key access, requires
68
+ `experimental.codexSubscription: true`, and stores OAuth credentials in the
69
+ native Vault. Start with `praxis auth login openai-codex`; see [Getting
70
+ Started](docs/GETTING_STARTED.md) for the browser/device flow and limitations.
71
+ This uses an undocumented third-party subscription/backend contract and may
72
+ change; it is not Claude subscription authentication. Subscription runs retain
73
+ token usage but do not provide API-dollar cost or enforce USD budgets.
74
+
64
75
  For Anthropic Messages:
65
76
 
66
77
  ```sh
@@ -154,11 +165,11 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
154
165
  isolation, explicit ask/deny precedence, sandbox-only auto-allow,
155
166
  write-allowlist/deny-within-allow enforcement, per-command overrides and
156
167
  exclusions, violation reporting, and bare-repository control-file cleanup,
157
- safe-property Skill auto-allow, interactive
158
- workspace-directory add/remove controls, path confinement, credential
159
- redaction, sanitized child processes, and exact-fingerprint workspace trust
160
- that blocks automatically discovered project/local hooks and MCP servers
161
- until the canonical workspace configuration is explicitly accepted.
168
+ safe-property Skill auto-allow, interactive workspace-directory add/remove
169
+ controls, path confinement, credential redaction, sanitized child processes,
170
+ and exact-fingerprint workspace trust that blocks automatically discovered
171
+ project/local provider selection, hooks, and MCP until the canonical
172
+ workspace configuration is accepted.
162
173
  - **Durable local work** — resumable sessions, full-history forks, file
163
174
  checkpoints, tasks, foreground/background subagents, top-level agents, and
164
175
  Claude-compatible main-thread agent definitions with native prompt, model,
@@ -187,9 +198,10 @@ troubleshooting. Run `praxis --help` for the authoritative command surface.
187
198
  plugins, and append-only `praxis.transcript` JSONL sessions under `~/.praxis`,
188
199
  with bounded MCP connection, discovery, and tool operations plus safe
189
200
  disconnect recovery that never replays an already-dispatched call.
190
- - **Provider-neutral models** — native Anthropic Messages and OpenAI-compatible
191
- streaming adapters with explicit capability checks, metering controls, and
192
- a bounded absolute deadline for every provider attempt.
201
+ - **Provider-neutral models** — native Provider Registry/Vault routing, API
202
+ adapters, an experimental Codex OAuth adapter, explicit capability checks,
203
+ per-attempt bounded deadlines, and token-only/no-API-dollar accounting for
204
+ subscription runs.
193
205
  - **Transactional self-update** — `praxis update` verifies the package before
194
206
  installing it, rejects concurrent updates, and can roll back after an
195
207
  interruption or crash.
@@ -216,7 +228,7 @@ not in this entry-point README.
216
228
 
217
229
  Praxis targets one local OS user working across multiple repositories and
218
230
  sessions. It is CLI-only and provider-capability-aware. Organization, tenant,
219
- RBAC, subscription authentication and billing, enterprise gateway,
231
+ RBAC, billing, enterprise gateway,
220
232
  IDE/Desktop/mobile clients, Remote Control, Claude Desktop import, and hosted
221
233
  review-product surfaces are permanent non-goals.
222
234
 
@@ -251,6 +263,8 @@ implemented profile checks, not qualification of the full native package.
251
263
  injected regression protection, plus Quiet Operator input echo `<50 ms` and
252
264
  normal/low-capability full-frame p95 budgets of `<16.7/<33 ms`.
253
265
  `npm run check` also enforces the corresponding source dependency direction.
266
+ `npm run test:coverage` measures all production code under `src/**` with V8 and
267
+ enforces global floors of 79% statements, 70% branches, 85% functions, and 81% lines.
254
268
  `npm run test:core-completion` runs the 56-story #402 audit and reports
255
269
  implemented, qualified, blocked, deferred, and out-of-scope states separately;
256
270
  it never treats missing live prerequisites as a pass.
@@ -35,8 +35,15 @@ export interface TopLevelAgentManagerOptions {
35
35
  cliPath: string;
36
36
  executablePath?: string;
37
37
  environment?: NodeJS.ProcessEnv;
38
+ resolveProviderEnvironment?: (request: {
39
+ cwd: string;
40
+ argv: readonly string[];
41
+ }) => Promise<ProviderEnvironmentOverride>;
38
42
  version: string;
39
43
  }
44
+ export interface ProviderEnvironmentOverride {
45
+ PRAXIS_API_KEY?: string;
46
+ }
40
47
  export declare function topLevelAgentProcessRegistryRoot(configRoot: string, dataPlane?: DataPlane): string;
41
48
  export declare class TopLevelAgentManager {
42
49
  private readonly options;
@@ -24,8 +24,11 @@ const SOCKET_RETRY_INTERVAL_MS = 25;
24
24
  // own provider from the same CLI/environment contract.
25
25
  const WORKER_RUNTIME_ENVIRONMENT = [
26
26
  'PRAXIS_API_KEY',
27
+ 'CLAUDE_CODE_SIMPLE',
27
28
  'PRAXIS_MODEL',
28
29
  'PRAXIS_PROVIDER',
30
+ 'PRAXIS_PROVIDER_PROFILE',
31
+ 'PRAXIS_PROVIDER_DEADLINE_MS',
29
32
  'PRAXIS_BASE_URL',
30
33
  'PRAXIS_MAX_OUTPUT_TOKENS',
31
34
  'PRAXIS_ANTHROPIC_VERSION',
@@ -38,10 +41,11 @@ const WORKER_RUNTIME_ENVIRONMENT = [
38
41
  'PRAXIS_FILES_BASE_URL',
39
42
  'PRAXIS_FILES_BEARER_TOKEN',
40
43
  'PRAXIS_FILES_API_KEY',
44
+ 'PRAXIS_PROVIDER_CREDENTIAL_STORE',
41
45
  'PRAXIS_MCP_OAUTH_STORE',
42
46
  'CLAUDE_CODE_ENABLE_SDK_FILE_CHECKPOINTING',
43
47
  ];
44
- function workerEnvironment(source, configRoot) {
48
+ function workerEnvironment(source, configRoot, resolved) {
45
49
  const environment = sanitizeChildEnvironment({ PATH: source.PATH }, {});
46
50
  for (const name of WORKER_RUNTIME_ENVIRONMENT) {
47
51
  const value = source[name];
@@ -50,6 +54,8 @@ function workerEnvironment(source, configRoot) {
50
54
  }
51
55
  environment.PRAXIS_DATA_PLANE = 'native';
52
56
  environment.PRAXIS_HOME = configRoot;
57
+ if (resolved?.PRAXIS_API_KEY !== undefined)
58
+ environment.PRAXIS_API_KEY = resolved.PRAXIS_API_KEY;
53
59
  return environment;
54
60
  }
55
61
  function socketPath(configRoot, id) {
@@ -285,8 +291,15 @@ export class TopLevelAgentManager {
285
291
  throw new Error('Could not allocate agent ID');
286
292
  const allocatedExecution = execution;
287
293
  let child;
294
+ let resolvedProviderEnvironment;
288
295
  const failLaunch = async (error) => {
289
- const message = redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveEnvironmentValues(this.options.environment ?? process.env));
296
+ const sensitiveValues = [
297
+ ...sensitiveEnvironmentValues(this.options.environment ?? process.env),
298
+ ...(resolvedProviderEnvironment?.PRAXIS_API_KEY === undefined
299
+ ? []
300
+ : [resolvedProviderEnvironment.PRAXIS_API_KEY]),
301
+ ];
302
+ const message = redactSensitiveText(error instanceof Error ? error.message : String(error), sensitiveValues);
290
303
  const secondary = [];
291
304
  if (child?.pid !== undefined) {
292
305
  try {
@@ -321,9 +334,14 @@ export class TopLevelAgentManager {
321
334
  throw new AggregateError([error, ...secondary], 'Could not start background agent');
322
335
  };
323
336
  try {
337
+ resolvedProviderEnvironment =
338
+ await this.options.resolveProviderEnvironment?.({
339
+ cwd,
340
+ argv: options.argv,
341
+ });
324
342
  child = spawn(this.options.executablePath ?? process.execPath, [this.options.cliPath, '__background-worker', identity.id], {
325
343
  cwd,
326
- env: workerEnvironment(this.options.environment ?? process.env, this.options.configRoot),
344
+ env: workerEnvironment(this.options.environment ?? process.env, this.options.configRoot, resolvedProviderEnvironment),
327
345
  detached: true,
328
346
  stdio: 'ignore',
329
347
  });
@@ -206,6 +206,10 @@ export async function resolveCliControls(controls, cwd) {
206
206
  name: controls.name,
207
207
  sessionPersistence: controls.sessionPersistence,
208
208
  ...(controls.model === undefined ? {} : { model: controls.model }),
209
+ ...(controls.provider === undefined ? {} : { provider: controls.provider }),
210
+ ...(controls.providerProfile === undefined
211
+ ? {}
212
+ : { providerProfile: controls.providerProfile }),
209
213
  ...(controls.effort === undefined ? {} : { effort: controls.effort }),
210
214
  ...(controls.thinking === undefined ? {} : { thinking: controls.thinking }),
211
215
  ...(controls.maxThinkingTokens === undefined
@@ -52,6 +52,8 @@ export interface CliControls {
52
52
  rewindFiles?: string;
53
53
  name: string | undefined;
54
54
  sessionPersistence: boolean;
55
+ provider?: string;
56
+ providerProfile?: string;
55
57
  model?: string;
56
58
  effort?: 'low' | 'medium' | 'high' | 'xhigh' | 'max';
57
59
  thinking?: CliThinkingMode;
@@ -114,6 +116,9 @@ export interface CliInvocation extends CliControls {
114
116
  mcpClientSecret: boolean;
115
117
  mcpNoBrowser: boolean;
116
118
  mcpDebug: boolean;
119
+ /** Auth-command-only profile selector; distinct from providerProfile. */
120
+ authProfile?: string;
121
+ authDevice: boolean;
117
122
  }
118
123
  export interface StreamUserMessage {
119
124
  message: {
@@ -387,6 +387,8 @@ export function parseCliInvocation(argv) {
387
387
  let worktreeRequested = false;
388
388
  let tmux;
389
389
  let model;
390
+ let provider;
391
+ let providerProfile;
390
392
  let effort;
391
393
  let thinking;
392
394
  let maxThinkingTokens;
@@ -404,6 +406,8 @@ export function parseCliInvocation(argv) {
404
406
  let mcpClientSecret = false;
405
407
  let mcpNoBrowser = false;
406
408
  let mcpDebug = false;
409
+ let authProfile;
410
+ let authDevice = false;
407
411
  for (let index = 0; index < argv.length; index += 1) {
408
412
  const value = argv[index];
409
413
  if (value === undefined)
@@ -444,6 +448,30 @@ export function parseCliInvocation(argv) {
444
448
  index += selectedModel.consumed;
445
449
  continue;
446
450
  }
451
+ const selectedProvider = optionValue(argv, index, '--provider');
452
+ if (selectedProvider) {
453
+ if (provider !== undefined)
454
+ throw new Error('--provider may only be specified once');
455
+ provider = selectedProvider.value;
456
+ index += selectedProvider.consumed;
457
+ continue;
458
+ }
459
+ const selectedProviderProfile = optionValue(argv, index, '--provider-profile');
460
+ if (selectedProviderProfile) {
461
+ if (providerProfile !== undefined)
462
+ throw new Error('--provider-profile may only be specified once');
463
+ providerProfile = selectedProviderProfile.value;
464
+ index += selectedProviderProfile.consumed;
465
+ continue;
466
+ }
467
+ const selectedAuthProfile = optionValue(argv, index, '--profile');
468
+ if (selectedAuthProfile) {
469
+ if (authProfile !== undefined)
470
+ throw new Error('--profile may only be specified once');
471
+ authProfile = selectedAuthProfile.value;
472
+ index += selectedAuthProfile.consumed;
473
+ continue;
474
+ }
447
475
  const selectedAutocompact = optionValue(argv, index, '--autocompact');
448
476
  if (selectedAutocompact) {
449
477
  if (autocompact !== undefined)
@@ -1123,6 +1151,10 @@ export function parseCliInvocation(argv) {
1123
1151
  mcpNoBrowser = true;
1124
1152
  continue;
1125
1153
  }
1154
+ if (value === '--device') {
1155
+ authDevice = true;
1156
+ continue;
1157
+ }
1126
1158
  if (value === '--json') {
1127
1159
  legacyJson = true;
1128
1160
  continue;
@@ -1174,6 +1206,14 @@ export function parseCliInvocation(argv) {
1174
1206
  if (debug !== undefined && args[0] === 'mcp' && args[1] === 'serve') {
1175
1207
  mcpDebug = true;
1176
1208
  }
1209
+ if (authProfile !== undefined && args[0] !== 'auth')
1210
+ throw new Error('--profile is only valid with auth commands');
1211
+ if (authDevice && !(args[0] === 'auth' && args[1] === 'login'))
1212
+ throw new Error('--device is only valid with auth login');
1213
+ if (mcpNoBrowser &&
1214
+ !((args[0] === 'mcp' && args[1] === 'login') ||
1215
+ (args[0] === 'auth' && args[1] === 'login')))
1216
+ throw new Error('--no-browser is only valid with mcp login or auth login');
1177
1217
  if (background && print) {
1178
1218
  throw new Error("--bg and --print conflict: --print never starts the interactive session that `claude agents` attaches to, so the job would be unattachable. The prompt is the positional — drop --print: `claude --bg '<task>'`.");
1179
1219
  }
@@ -1215,6 +1255,7 @@ export function parseCliInvocation(argv) {
1215
1255
  'plugin',
1216
1256
  'doctor',
1217
1257
  'import',
1258
+ 'auth',
1218
1259
  ].includes(args[0] ?? '');
1219
1260
  if (promptSuggestions &&
1220
1261
  !managementCommand &&
@@ -1385,6 +1426,8 @@ export function parseCliInvocation(argv) {
1385
1426
  ...(worktreeRequested ? { worktreeRequested: true } : {}),
1386
1427
  ...(tmux === undefined ? {} : { tmux }),
1387
1428
  ...(model === undefined ? {} : { model }),
1429
+ ...(provider === undefined ? {} : { provider }),
1430
+ ...(providerProfile === undefined ? {} : { providerProfile }),
1388
1431
  ...(effort === undefined ? {} : { effort }),
1389
1432
  ...(thinking === undefined ? {} : { thinking }),
1390
1433
  ...(maxThinkingTokens === undefined ? {} : { maxThinkingTokens }),
@@ -1402,6 +1445,8 @@ export function parseCliInvocation(argv) {
1402
1445
  mcpClientSecret,
1403
1446
  mcpNoBrowser,
1404
1447
  mcpDebug,
1448
+ ...(authProfile === undefined ? {} : { authProfile }),
1449
+ authDevice,
1405
1450
  };
1406
1451
  }
1407
1452
  function parseUserMessage(value, lineNumber) {
@@ -0,0 +1,32 @@
1
+ import { type ProviderCredentialKey, type ProviderCredentialMetadata, type ProviderCredentialInput, type ProviderCredentialRecord } from '../persistence/provider-credential-vault.js';
2
+ import { deviceLoginWithCodexOAuth, loginWithCodexOAuth } from '../providers/codex-oauth.js';
3
+ export interface ProviderAuthCommandIO {
4
+ stdout(message: string): void;
5
+ stderr?(message: string): void;
6
+ readSecret?(prompt: string, signal?: AbortSignal): Promise<string>;
7
+ }
8
+ export interface ProviderAuthVault {
9
+ read(key: ProviderCredentialKey): Promise<ProviderCredentialRecord | undefined>;
10
+ list(): Promise<ProviderCredentialMetadata[]>;
11
+ modify(key: ProviderCredentialKey, callback: (current: ProviderCredentialRecord | undefined) => ProviderCredentialInput | undefined | Promise<ProviderCredentialInput | undefined>): Promise<ProviderCredentialRecord | undefined>;
12
+ delete(key: ProviderCredentialKey): Promise<void>;
13
+ }
14
+ export interface ProviderAuthCommandOptions {
15
+ io: ProviderAuthCommandIO;
16
+ vault?: ProviderAuthVault;
17
+ configRoot?: string;
18
+ cwd?: string;
19
+ environment?: Readonly<Record<string, string | undefined>>;
20
+ signal?: AbortSignal;
21
+ profile?: string;
22
+ providerProfile?: string;
23
+ noBrowser?: boolean;
24
+ json?: boolean;
25
+ device?: boolean;
26
+ loginWithCodexOAuth?: typeof loginWithCodexOAuth;
27
+ deviceLoginWithCodexOAuth?: typeof deviceLoginWithCodexOAuth;
28
+ assertCodexLoginEnabled?: (profileId: string) => Promise<void>;
29
+ }
30
+ export declare function secretLine(value: string): string;
31
+ export declare function executeProviderAuthCommand(args: readonly string[], options: ProviderAuthCommandOptions): Promise<number>;
32
+ //# sourceMappingURL=provider-auth-command.d.ts.map
@@ -0,0 +1,249 @@
1
+ import { ProviderCredentialVault, } from '../persistence/provider-credential-vault.js';
2
+ import { deviceLoginWithCodexOAuth, loginWithCodexOAuth, } from '../providers/codex-oauth.js';
3
+ import { resolveProviderTarget } from '../providers/provider-settings.js';
4
+ import { resolveDataPlaneRoot } from '../persistence/data-plane.js';
5
+ const IDENTIFIER = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/u;
6
+ const MAX_SECRET_BYTES = 64 * 1024;
7
+ function fail(message) {
8
+ throw new Error(message);
9
+ }
10
+ function identifier(value, label) {
11
+ if (!IDENTIFIER.test(value))
12
+ fail(`${label} must match [A-Za-z0-9][A-Za-z0-9._-]{0,127}`);
13
+ return value;
14
+ }
15
+ function parse(args, options) {
16
+ if (args[0] === 'auth')
17
+ args = args.slice(1);
18
+ const action = args[0];
19
+ if (action !== 'status' &&
20
+ action !== 'set-key' &&
21
+ action !== 'login' &&
22
+ action !== 'logout') {
23
+ fail('auth requires status, set-key, login, or logout');
24
+ }
25
+ let provider;
26
+ let profile;
27
+ let json = options.json ?? false;
28
+ let noBrowser = options.noBrowser ?? false;
29
+ let device = options.device ?? false;
30
+ for (let index = 1; index < args.length; index += 1) {
31
+ const value = args[index];
32
+ if (value === '--json') {
33
+ if (json)
34
+ fail('--json may only be specified once');
35
+ json = true;
36
+ continue;
37
+ }
38
+ if (value === '--device') {
39
+ if (device)
40
+ fail('--device may only be specified once');
41
+ device = true;
42
+ continue;
43
+ }
44
+ if (value === '--no-browser') {
45
+ if (noBrowser)
46
+ fail('--no-browser may only be specified once');
47
+ noBrowser = true;
48
+ continue;
49
+ }
50
+ if (value === '--profile' || value?.startsWith('--profile=')) {
51
+ if (profile !== undefined)
52
+ fail('--profile may only be specified once');
53
+ const selected = value === '--profile' ? args[++index] : value.slice('--profile='.length);
54
+ if (typeof selected !== 'string' || selected.length === 0)
55
+ fail('--profile requires a value');
56
+ profile = identifier(selected, 'profile');
57
+ continue;
58
+ }
59
+ if (value?.startsWith('-'))
60
+ fail(`Unknown option: ${value}`);
61
+ if (typeof value !== 'string')
62
+ fail(`Unexpected operand for auth ${action}`);
63
+ if (provider !== undefined)
64
+ fail(`Unexpected operand for auth ${action}: ${value}`);
65
+ provider = identifier(value, 'provider');
66
+ }
67
+ if (options.profile !== undefined &&
68
+ profile !== undefined &&
69
+ options.profile !== profile)
70
+ fail('auth --profile conflicts with the selected profile');
71
+ if (options.providerProfile !== undefined &&
72
+ profile !== undefined &&
73
+ options.providerProfile !== profile)
74
+ fail('auth --profile conflicts with --provider-profile');
75
+ if (options.profile !== undefined &&
76
+ options.providerProfile !== undefined &&
77
+ options.profile !== options.providerProfile)
78
+ fail('auth --profile conflicts with --provider-profile');
79
+ const selectedProfile = profile ?? options.profile ?? options.providerProfile;
80
+ profile =
81
+ action === 'status' && selectedProfile === undefined
82
+ ? undefined
83
+ : identifier(selectedProfile ?? 'default', 'profile');
84
+ if (action === 'status' && provider === undefined) {
85
+ // status accepts zero or one provider operand.
86
+ }
87
+ else if (action !== 'status' && provider === undefined) {
88
+ fail(`auth ${action} requires a provider`);
89
+ }
90
+ if (action === 'set-key' && provider === 'openai-codex')
91
+ fail('auth set-key does not support openai-codex; use auth login');
92
+ if (action === 'login' && provider !== 'openai-codex')
93
+ fail('auth login only supports openai-codex');
94
+ if (action !== 'login' && (device || noBrowser))
95
+ fail('--device and --no-browser are only valid with auth login');
96
+ return {
97
+ action,
98
+ ...(provider === undefined ? {} : { provider }),
99
+ ...(profile === undefined ? {} : { profile }),
100
+ json,
101
+ device,
102
+ noBrowser,
103
+ };
104
+ }
105
+ function defaultVault(options) {
106
+ const environment = options.environment ?? process.env;
107
+ const vaultOptions = {
108
+ configRoot: options.configRoot ?? resolveDataPlaneRoot({ environment }),
109
+ environment,
110
+ };
111
+ return new ProviderCredentialVault(vaultOptions);
112
+ }
113
+ async function assertCodexLoginEnabled(options, profileId) {
114
+ const environment = options.environment ?? process.env;
115
+ await resolveProviderTarget({
116
+ configRoot: options.configRoot ?? resolveDataPlaneRoot({ environment }),
117
+ cwd: options.cwd ?? process.cwd(),
118
+ environment,
119
+ provider: 'openai-codex',
120
+ profile: profileId,
121
+ model: 'codex-authentication',
122
+ });
123
+ }
124
+ function json(io, value) {
125
+ io.stdout(`${JSON.stringify(value)}\n`);
126
+ }
127
+ function text(io, message) {
128
+ io.stdout(`${message}\n`);
129
+ }
130
+ function metadata(value) {
131
+ return {
132
+ provider: value.key.providerId,
133
+ profile: value.key.profileId,
134
+ type: value.type,
135
+ updatedAt: value.updatedAt,
136
+ ...(value.expiresAt === undefined
137
+ ? {}
138
+ : { expiry: value.expiresAt > Date.now() ? 'valid' : 'expired' }),
139
+ };
140
+ }
141
+ function sortedMetadata(records, provider, profile) {
142
+ return records
143
+ .filter((record) => (provider === undefined || record.key.providerId === provider) &&
144
+ (profile === undefined || record.key.profileId === profile))
145
+ .sort((left, right) => {
146
+ const providerOrder = left.key.providerId.localeCompare(right.key.providerId);
147
+ return (providerOrder || left.key.profileId.localeCompare(right.key.profileId));
148
+ })
149
+ .map(metadata);
150
+ }
151
+ export function secretLine(value) {
152
+ if (Buffer.byteLength(value, 'utf8') > MAX_SECRET_BYTES)
153
+ fail('Credential exceeds the 64 KiB limit');
154
+ const stripped = value.endsWith('\r\n')
155
+ ? value.slice(0, -2)
156
+ : value.endsWith('\r') || value.endsWith('\n')
157
+ ? value.slice(0, -1)
158
+ : value;
159
+ if (/[\r\n]/u.test(stripped) || stripped.trim().length === 0)
160
+ fail('Credential must be exactly one non-blank line');
161
+ return stripped;
162
+ }
163
+ export async function executeProviderAuthCommand(args, options) {
164
+ const parsed = parse(args, options);
165
+ const vault = options.vault ?? defaultVault(options);
166
+ if (parsed.action === 'status') {
167
+ const credentials = sortedMetadata(await vault.list(), parsed.provider, parsed.profile);
168
+ if (parsed.json) {
169
+ json(options.io, { type: 'provider-auth-status', credentials });
170
+ }
171
+ else if (credentials.length === 0) {
172
+ text(options.io, 'No provider credentials configured.');
173
+ }
174
+ else {
175
+ text(options.io, credentials
176
+ .map((credential) => `${credential.provider}/${credential.profile}: ${credential.type}${credential.expiry === undefined ? '' : ` (${credential.expiry})`}`)
177
+ .join('\n'));
178
+ }
179
+ return 0;
180
+ }
181
+ const provider = parsed.provider;
182
+ if (provider === undefined)
183
+ fail(`auth ${parsed.action} requires a provider`);
184
+ const key = {
185
+ providerId: provider,
186
+ profileId: parsed.profile ?? 'default',
187
+ };
188
+ if (parsed.action === 'set-key') {
189
+ if (!options.io.readSecret)
190
+ fail('auth set-key requires an interactive secret reader');
191
+ const secret = secretLine(await options.io.readSecret('API key: ', options.signal));
192
+ await vault.modify(key, () => ({ type: 'api-key', secret }));
193
+ const result = {
194
+ provider: key.providerId,
195
+ profile: key.profileId,
196
+ type: 'api-key',
197
+ };
198
+ if (parsed.json)
199
+ json(options.io, result);
200
+ else
201
+ text(options.io, `Stored api-key credential for ${key.providerId}/${key.profileId}.`);
202
+ return 0;
203
+ }
204
+ if (parsed.action === 'logout') {
205
+ const existing = await vault.read(key);
206
+ if (existing !== undefined)
207
+ await vault.delete(key);
208
+ const result = {
209
+ provider: key.providerId,
210
+ profile: key.profileId,
211
+ deleted: true,
212
+ };
213
+ if (parsed.json)
214
+ json(options.io, result);
215
+ else
216
+ text(options.io, `Logged out ${key.providerId}/${key.profileId}.`);
217
+ return 0;
218
+ }
219
+ await (options.assertCodexLoginEnabled ??
220
+ ((profileId) => assertCodexLoginEnabled(options, profileId)))(key.profileId);
221
+ const loginOptions = {
222
+ profileId: key.profileId,
223
+ vault: createCodexOAuthVault(vault),
224
+ ...(options.signal === undefined ? {} : { signal: options.signal }),
225
+ ...(parsed.noBrowser ? { noBrowser: true } : {}),
226
+ write: (message) => options.io.stderr?.(message),
227
+ };
228
+ const login = parsed.device
229
+ ? (options.deviceLoginWithCodexOAuth ?? deviceLoginWithCodexOAuth)
230
+ : (options.loginWithCodexOAuth ?? loginWithCodexOAuth);
231
+ await login(loginOptions);
232
+ const result = {
233
+ provider: key.providerId,
234
+ profile: key.profileId,
235
+ type: 'oauth',
236
+ };
237
+ if (parsed.json)
238
+ json(options.io, result);
239
+ else
240
+ text(options.io, `Logged in ${key.providerId}/${key.profileId}.`);
241
+ return 0;
242
+ }
243
+ function createCodexOAuthVault(vault) {
244
+ return {
245
+ read: (key) => vault.read(key),
246
+ modify: (key, callback) => vault.modify(key, async (current) => callback(current)),
247
+ };
248
+ }
249
+ //# sourceMappingURL=provider-auth-command.js.map
@@ -23,10 +23,10 @@ export async function promptWorkspaceTrust(inventory, io = {}) {
23
23
  const output = io.output ?? ((text) => process.stderr.write(text));
24
24
  if (io.signal?.aborted)
25
25
  return false;
26
- output(`Workspace executable resources found for ${safeWorkspaceTrustDisplayField(inventory.canonicalPath)}\n`);
26
+ output(`Workspace-controlled resources/configuration found for ${safeWorkspaceTrustDisplayField(inventory.canonicalPath)}\n`);
27
27
  for (const origin of inventory.origins)
28
28
  output(` ${origin.kind} (${origin.scope}) ${safeWorkspaceTrustDisplayField(origin.path)}: ${safeWorkspaceTrustDisplayField(origin.label)}\n`);
29
- output('Trust these workspace executables? [y/N] ');
29
+ output('Trust these workspace-controlled resources/configuration? [y/N] ');
30
30
  const input = io.input ?? process.stdin;
31
31
  const iterator = input[Symbol.asyncIterator]();
32
32
  const abortedResult = Symbol('workspace-trust-prompt-aborted');
@@ -11,7 +11,7 @@ import type { TuiSlashCommand } from './cli/tui/slash-commands.js';
11
11
  import { type TuiHookConfiguration } from './cli/tui/hook-settings.js';
12
12
  import { type PraxisRuntimeSettings } from './cli/tui/runtime-settings.js';
13
13
  import { type ClaudePermissionMode } from './permissions/claude-permission-resolver.js';
14
- import { type WorkspaceTrustInventory } from './security/workspace-trust.js';
14
+ import { type WorkspaceTrustAssessment, type WorkspaceTrustInventory } from './security/workspace-trust.js';
15
15
  import { type ClaudeMcpServerStatus, type ClaudeMcpToolInspection } from './mcp/claude-mcp-tools.js';
16
16
  import { authenticateMcpServer } from './mcp/claude-mcp-oauth.js';
17
17
  import { servePraxisMcpStdio } from './mcp/praxis-mcp-server.js';
@@ -20,6 +20,7 @@ import { type ClaudeInteractiveToolCallbacks } from './tools/claude-interactive-
20
20
  import { type TopLevelAgentSummary } from './application/top-level-agent-manager.js';
21
21
  import { launchTmuxWorktree } from './platform/tmux-worktree.js';
22
22
  import { type CliControls, type CliRuntimeInfo, type CliElicitationRequest, type CliElicitationResult } from './cli/protocol.js';
23
+ import { executeProviderAuthCommand } from './cli/provider-auth-command.js';
23
24
  import { type PluginEvalDependencies } from './plugins/claude-plugin-eval.js';
24
25
  import { type SelfUpdateResult } from './maintenance/self-update.js';
25
26
  export { parseContextEnvironment, parseProviderEnvironment };
@@ -28,6 +29,7 @@ export interface CliIO {
28
29
  stderr(message: string): void;
29
30
  isTTY?: boolean;
30
31
  readStdinLines?: () => AsyncIterable<string | Uint8Array>;
32
+ readSecret?: (prompt: string, signal?: AbortSignal) => Promise<string>;
31
33
  }
32
34
  interface SessionCommands {
33
35
  teamLeadOperations?: TeamLeadOperations;
@@ -143,6 +145,8 @@ export interface CliDependencies extends InteractiveServiceFactory {
143
145
  }): Promise<SessionCommands>;
144
146
  createAutoModeCritic?(options: {
145
147
  model?: string;
148
+ provider?: string;
149
+ providerProfile?: string;
146
150
  dataPlane?: DataPlane;
147
151
  configRoot?: string;
148
152
  statePath?: string;
@@ -170,6 +174,7 @@ export interface CliDependencies extends InteractiveServiceFactory {
170
174
  launchTmux?: typeof launchTmuxWorktree;
171
175
  mcpAuthenticate?: typeof authenticateMcpServer;
172
176
  mcpServe?: typeof servePraxisMcpStdio;
177
+ executeProviderAuthCommand?: typeof executeProviderAuthCommand;
173
178
  selfUpdate?: (options: {
174
179
  operation: 'install' | 'update';
175
180
  target?: string;
@@ -177,6 +182,20 @@ export interface CliDependencies extends InteractiveServiceFactory {
177
182
  signal?: AbortSignal;
178
183
  }) => Promise<SelfUpdateResult>;
179
184
  }
185
+ interface ConsoleSecretInput extends AsyncIterable<string | Uint8Array> {
186
+ readonly isTTY?: boolean;
187
+ readonly isRaw?: boolean;
188
+ isPaused(): boolean;
189
+ setRawMode?(enabled: boolean): unknown;
190
+ pause(): unknown;
191
+ resume(): unknown;
192
+ on(event: 'data', listener: (chunk: Buffer | string) => void): unknown;
193
+ removeListener(event: 'data', listener: (chunk: Buffer | string) => void): unknown;
194
+ }
195
+ interface ConsoleSecretOutput {
196
+ write(message: string): unknown;
197
+ }
198
+ export declare function readConsoleSecret(prompt: string, signal?: AbortSignal, input?: ConsoleSecretInput, output?: ConsoleSecretOutput): Promise<string>;
180
199
  /**
181
200
  * Shared runtime model precedence used by every consumer (provider
182
201
  * construction, status/doctor output, and the interactive display):
@@ -194,9 +213,23 @@ export declare function resolveInteractiveRuntimeSettingsLocation(dataPlane: Dat
194
213
  statePath: string;
195
214
  };
196
215
  export declare function resolveUnknownCostSidecarPath(dataPlane: DataPlane, configRoot: string): string;
216
+ export declare function resolveInteractiveProviderStartup(options: {
217
+ controls: CliControls;
218
+ configRoot: string;
219
+ statePath: string;
220
+ cwd: string;
221
+ environment?: NodeJS.ProcessEnv;
222
+ approveWorkspaceTrust?: (assessment: WorkspaceTrustAssessment) => boolean | Promise<boolean>;
223
+ }): Promise<{
224
+ effectiveModel: string | undefined;
225
+ trustProjectRequestAvailable: boolean;
226
+ }>;
197
227
  export declare function createDefaultDependencies(entrypoint?: string): CliDependencies;
198
228
  export declare function createBackgroundWorkerRuntime(workerSink: RuntimeEventSink, dispatch: {
199
229
  argv: string[];
200
230
  }, createService?: CliDependencies['createService']): Promise<Awaited<ReturnType<CliDependencies['createService']>>>;
231
+ export declare function agentDashboardWorkerArgv(invocation: CliControls & {
232
+ agent: string | undefined;
233
+ }): string[];
201
234
  export declare function run(argv: readonly string[], io?: CliIO, dependencies?: CliDependencies, signal?: AbortSignal): Promise<number>;
202
235
  //# sourceMappingURL=cli-runtime.d.ts.map