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 +24 -10
- package/dist/application/top-level-agent-manager.d.ts +7 -0
- package/dist/application/top-level-agent-manager.js +21 -3
- package/dist/cli/controls.js +4 -0
- package/dist/cli/protocol.d.ts +5 -0
- package/dist/cli/protocol.js +45 -0
- package/dist/cli/provider-auth-command.d.ts +32 -0
- package/dist/cli/provider-auth-command.js +249 -0
- package/dist/cli/workspace-trust-prompt.js +2 -2
- package/dist/cli-runtime.d.ts +34 -1
- package/dist/cli-runtime.js +587 -184
- package/dist/maintenance/doctor.js +100 -22
- package/dist/persistence/provider-credential-vault.d.ts +72 -0
- package/dist/persistence/provider-credential-vault.js +498 -0
- package/dist/providers/codex-oauth.d.ts +104 -0
- package/dist/providers/codex-oauth.js +770 -0
- package/dist/providers/codex-subscription.d.ts +33 -0
- package/dist/providers/codex-subscription.js +849 -0
- package/dist/providers/provider-auth.d.ts +71 -0
- package/dist/providers/provider-auth.js +149 -0
- package/dist/providers/provider-registry.d.ts +52 -0
- package/dist/providers/provider-registry.js +187 -0
- package/dist/providers/provider-settings.d.ts +38 -0
- package/dist/providers/provider-settings.js +334 -0
- package/dist/security/workspace-trust.d.ts +4 -3
- package/dist/security/workspace-trust.js +35 -1
- package/package.json +6 -3
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
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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
|
|
191
|
-
|
|
192
|
-
|
|
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,
|
|
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
|
|
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
|
});
|
package/dist/cli/controls.js
CHANGED
|
@@ -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
|
package/dist/cli/protocol.d.ts
CHANGED
|
@@ -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: {
|
package/dist/cli/protocol.js
CHANGED
|
@@ -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
|
|
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
|
|
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');
|
package/dist/cli-runtime.d.ts
CHANGED
|
@@ -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
|