@ours.network/fleet 1.2.0-nightly.20 → 1.2.0-nightly.22
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/dist/build-info.json +4 -4
- package/dist/init-wizard.js +1 -0
- package/dist/isolation/policy.js +2 -0
- package/dist/isolation/types.d.ts +5 -0
- package/dist/runner.js +8 -1
- package/dist/session/acp.js +7 -0
- package/dist/subscriptions/cli.d.ts +78 -0
- package/dist/subscriptions/cli.js +186 -0
- package/dist/subscriptions/launch.d.ts +37 -0
- package/dist/subscriptions/launch.js +93 -0
- package/dist/subscriptions/login.d.ts +75 -0
- package/dist/subscriptions/login.js +182 -0
- package/dist/subscriptions/observed.d.ts +7 -0
- package/dist/subscriptions/observed.js +44 -0
- package/dist/subscriptions/probe.d.ts +10 -0
- package/dist/subscriptions/probe.js +73 -0
- package/dist/subscriptions/service.d.ts +102 -0
- package/dist/subscriptions/service.js +364 -0
- package/dist/subscriptions/store.d.ts +71 -0
- package/dist/subscriptions/store.js +140 -0
- package/dist/web/runtime.js +7 -0
- package/dist/web/server.d.ts +2 -0
- package/dist/web/server.js +81 -0
- package/package.json +1 -1
- package/presets/README.md +12 -0
- package/presets/brain-catalog.json +2 -1
- package/presets/fleet/brains/claude-fable-5-1-high.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-1-low.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-1-max.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-1-medium.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-1-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-fable-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-haiku-4-5-20251001-default.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-opus-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-high.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-low.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-max.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-medium.yaml +1 -1
- package/presets/fleet/brains/claude-sonnet-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-3-codex-spark-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-mini-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-4-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-5-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-luna-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-sol-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-5-6-terra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-1-sol-high.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-low.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-max.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-medium.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-ultra.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-1-sol-xhigh.yaml +5 -0
- package/presets/fleet/brains/codex-gpt-6-astra-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-astra-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-luna-xhigh.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-sol-high.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-sol-low.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-sol-max.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-sol-medium.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-sol-ultra.yaml +1 -1
- package/presets/fleet/brains/codex-gpt-6-sol-xhigh.yaml +1 -1
- package/presets/manifest.json +2 -2
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.22",
|
|
3
|
+
"buildId": "f5776b7613c5",
|
|
4
|
+
"commit": "769fba35bc2918e792c353d2234d0fe5e2a48f57",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-30T07:56:23.700Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"cowork.http-management-v1",
|
|
9
9
|
"monitor.interrupt.after_tool"
|
package/dist/init-wizard.js
CHANGED
package/dist/isolation/policy.js
CHANGED
|
@@ -168,6 +168,8 @@ export function resolveIsolation(cfg, ctx) {
|
|
|
168
168
|
}
|
|
169
169
|
for (const dir of ctx.additionalWriteDirs ?? [])
|
|
170
170
|
addRw(dir);
|
|
171
|
+
if (ctx.subscriptionHome)
|
|
172
|
+
addRw(ctx.subscriptionHome);
|
|
171
173
|
// The selected harness/session command may live outside the system allowlist
|
|
172
174
|
// (for example Node and a bundled ACP adapter under ~/.local). The runner
|
|
173
175
|
// resolves its exact executable/package closure; keep every such bind RO.
|
|
@@ -63,6 +63,11 @@ export interface WrapContext {
|
|
|
63
63
|
* them for itself or for its peers.
|
|
64
64
|
*/
|
|
65
65
|
harnessSharedPaths?: string[];
|
|
66
|
+
/**
|
|
67
|
+
* The pinned subscription profile home (CLAUDE_CONFIG_DIR / CODEX_HOME), mounted
|
|
68
|
+
* read-write so the CLI can refresh that profile's login. Absent for the default.
|
|
69
|
+
*/
|
|
70
|
+
subscriptionHome?: string;
|
|
66
71
|
/** Exact launcher/interpreter/module closure required by the selected command. */
|
|
67
72
|
runtimeReadPaths?: string[];
|
|
68
73
|
brokerEndpoint?: string;
|
package/dist/runner.js
CHANGED
|
@@ -14,6 +14,7 @@ import { createMonitor, probeIdentityPresence, } from './monitor.js';
|
|
|
14
14
|
import { DaemonGenerationObserver, RoleRecoveryController, probeDaemonGeneration, } from './daemon-recovery.js';
|
|
15
15
|
import { realExec } from './exec.js';
|
|
16
16
|
import { resolveIsolation } from './isolation/policy.js';
|
|
17
|
+
import { pinSubscriptionForLaunch } from './subscriptions/launch.js';
|
|
17
18
|
import { selectIsolationBackend } from './isolation/registry.js';
|
|
18
19
|
import { resourceArgs, cpuControllerDelegated } from './isolation/resources.js';
|
|
19
20
|
import { resolveLaunchRuntime } from './isolation/runtime.js';
|
|
@@ -525,6 +526,11 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
525
526
|
const sessionBackend = role.session ?? 'acp';
|
|
526
527
|
const sessionLabel = sessionBackend === 'acp' ? 'ACP' : 'Codex app-server';
|
|
527
528
|
let launch = adapter.agentSession.prepareLaunch(role, prep);
|
|
529
|
+
// Resolve the fleet-wide active subscription profile once, now: a later switch
|
|
530
|
+
// never changes this session (the operator restarts agents to move them).
|
|
531
|
+
const subscription = pinSubscriptionForLaunch(role, dir);
|
|
532
|
+
if (subscription?.pin.home)
|
|
533
|
+
deps.log(`[${name}] subscription: ${subscription.pin.provider} profile ${subscription.pin.profileId}`);
|
|
528
534
|
// Preserve the role's existing isolation policy.
|
|
529
535
|
let wrappedArgv = launch.argv;
|
|
530
536
|
if (role.isolation) {
|
|
@@ -535,6 +541,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
535
541
|
launch = { ...launch, argv: runtime.argv };
|
|
536
542
|
const ctx = {
|
|
537
543
|
...isolationContextFor(role), stateDir: dir, runCwd,
|
|
544
|
+
subscriptionHome: subscription?.writablePath,
|
|
538
545
|
runtimeReadPaths: [...runtime.readPaths, ...resolveLaunchRuntime([process.execPath, fileURLToPath(new URL('./agent-ours/bridge.js', import.meta.url))]).readPaths],
|
|
539
546
|
};
|
|
540
547
|
const policy = resolveIsolation(role.isolation, ctx);
|
|
@@ -560,7 +567,7 @@ export async function runOnce(name, opts = {}, partialDeps = {}) {
|
|
|
560
567
|
const managedService = await deps.prepareAgentOurs(role, dir, opts.identityLifetime ? opts.identityLifetime === 'temporary' : temp);
|
|
561
568
|
let layoutRetiring = false;
|
|
562
569
|
try {
|
|
563
|
-
const managedHarness = prepareManagedHarness(role, dir, runCwd, managedService.descriptor, harnessChildEnv(role, launch.env, dir));
|
|
570
|
+
const managedHarness = prepareManagedHarness(role, dir, runCwd, managedService.descriptor, { ...harnessChildEnv(role, launch.env, dir), ...subscription?.env });
|
|
564
571
|
// Start-stagger: space this launch at least start_stagger_ms after the previous
|
|
565
572
|
// agent launch across the whole host, so a burst of boots (systemd starts every
|
|
566
573
|
// user unit concurrently on boot; `ours-fleet up`/restart-all bulk-start) does not
|
package/dist/session/acp.js
CHANGED
|
@@ -8,6 +8,7 @@ import * as acp from '@agentclientprotocol/sdk';
|
|
|
8
8
|
import { normalizeSessionUpdate } from './conversation-normalizer.js';
|
|
9
9
|
import { ConversationEventStore } from './conversation-store.js';
|
|
10
10
|
import { SessionEvents } from './events.js';
|
|
11
|
+
import { recordClaudeRateLimit } from '../subscriptions/observed.js';
|
|
11
12
|
import { DEFAULT_STALL_TIMEOUT_MS, STALL_RECOVERY_PROMPT, StallWatchdog, StallToolHistory, hasStallRecoveryClaim } from './stall-watchdog.js';
|
|
12
13
|
import { ACP_CANCEL_DEADLINE_EXCEEDED, SessionControlError, classifyChildExit, sessionBackendCapabilities, turnResult, } from './types.js';
|
|
13
14
|
const CANCEL_SETTLE_GRACE_MS = 15_000;
|
|
@@ -1713,6 +1714,12 @@ export class AcpSession {
|
|
|
1713
1714
|
this.recordConversationUpdate(update, scheduled, messagePhase === 'commentary');
|
|
1714
1715
|
if (update.sessionUpdate === 'config_option_update')
|
|
1715
1716
|
this.captureRuntimeMetadata(update.configOptions);
|
|
1717
|
+
if (update.sessionUpdate === 'usage_update') {
|
|
1718
|
+
// Claude subscription rate-limit snapshot (claude-agent-acp forwards the SDK rate_limit_event).
|
|
1719
|
+
const rateLimit = update._meta?.['_claude/rateLimit'];
|
|
1720
|
+
if (rateLimit)
|
|
1721
|
+
recordClaudeRateLimit(this.options.stateDir, rateLimit);
|
|
1722
|
+
}
|
|
1716
1723
|
switch (update.sessionUpdate) {
|
|
1717
1724
|
case 'agent_message_chunk': {
|
|
1718
1725
|
const phase = messagePhase;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin adapters over the official CLIs. Nothing here reads or returns a token
|
|
3
|
+
* value: credentials stay in files the CLIs own, and secrets typed by the user
|
|
4
|
+
* (a Claude paste code) travel only over a child's stdin, never argv or logs.
|
|
5
|
+
*/
|
|
6
|
+
export interface SubscriptionBinaries {
|
|
7
|
+
claude: string;
|
|
8
|
+
codex: string;
|
|
9
|
+
/** `script(1)`, used to give `claude auth login` the terminal it expects. */
|
|
10
|
+
script: string;
|
|
11
|
+
}
|
|
12
|
+
export declare const defaultBinaries: () => SubscriptionBinaries;
|
|
13
|
+
/**
|
|
14
|
+
* Child env for a profile helper: explicit home for added profiles, the CLI
|
|
15
|
+
* default otherwise. Every credential selector either CLI honours is removed,
|
|
16
|
+
* so a helper can only ever see the profile's own login.
|
|
17
|
+
*/
|
|
18
|
+
export declare function profileEnv(homeVar: string, profileId: string, home: string, base?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
|
|
19
|
+
export interface ClaudeStatus {
|
|
20
|
+
loggedIn: boolean;
|
|
21
|
+
email?: string;
|
|
22
|
+
plan?: string;
|
|
23
|
+
org?: string;
|
|
24
|
+
authMethod?: string;
|
|
25
|
+
}
|
|
26
|
+
/** `claude auth status --json` (documented: exit 0 when logged in, 1 otherwise). */
|
|
27
|
+
export declare function claudeAuthStatus(bin: string, env: NodeJS.ProcessEnv, timeoutMs?: number): Promise<ClaudeStatus>;
|
|
28
|
+
/**
|
|
29
|
+
* Login expiry timestamps from the Claude credentials file. Field names are
|
|
30
|
+
* observed (Claude Code 2.1.x), not a documented contract, so absence is normal.
|
|
31
|
+
* Only the two numbers are kept; the parsed object is discarded immediately.
|
|
32
|
+
*/
|
|
33
|
+
export declare function claudeLoginExpiry(home: string): {
|
|
34
|
+
accessExpiresAt?: number;
|
|
35
|
+
refreshExpiresAt?: number;
|
|
36
|
+
};
|
|
37
|
+
/** Minimal stdio JSON-RPC client for `codex app-server` (the supported transport). */
|
|
38
|
+
export declare class CodexAppServer {
|
|
39
|
+
private readonly child;
|
|
40
|
+
private id;
|
|
41
|
+
private buffer;
|
|
42
|
+
private readonly pending;
|
|
43
|
+
private readonly listeners;
|
|
44
|
+
private closed;
|
|
45
|
+
private constructor();
|
|
46
|
+
static start(bin: string, env: NodeJS.ProcessEnv): Promise<CodexAppServer>;
|
|
47
|
+
call<T = unknown>(method: string, params: unknown, timeoutMs?: number): Promise<T>;
|
|
48
|
+
onNotification(fn: (method: string, params: unknown) => void): () => void;
|
|
49
|
+
close(): void;
|
|
50
|
+
private onData;
|
|
51
|
+
}
|
|
52
|
+
/** One-shot use of an app-server for a profile. */
|
|
53
|
+
export declare function withCodex<T>(bin: string, env: NodeJS.ProcessEnv, fn: (s: CodexAppServer) => Promise<T>): Promise<T>;
|
|
54
|
+
export interface UsageWindow {
|
|
55
|
+
id: string;
|
|
56
|
+
label: string;
|
|
57
|
+
usedPercent: number;
|
|
58
|
+
windowMins?: number;
|
|
59
|
+
resetsAt?: string;
|
|
60
|
+
}
|
|
61
|
+
interface CodexWindow {
|
|
62
|
+
usedPercent?: number;
|
|
63
|
+
windowDurationMins?: number | null;
|
|
64
|
+
resetsAt?: number | null;
|
|
65
|
+
}
|
|
66
|
+
interface CodexSnapshot {
|
|
67
|
+
limitId?: string | null;
|
|
68
|
+
limitName?: string | null;
|
|
69
|
+
primary?: CodexWindow | null;
|
|
70
|
+
secondary?: CodexWindow | null;
|
|
71
|
+
}
|
|
72
|
+
/** Flatten `account/rateLimits/read` into display windows. */
|
|
73
|
+
export declare function codexUsageWindows(result: {
|
|
74
|
+
rateLimits?: CodexSnapshot;
|
|
75
|
+
rateLimitsByLimitId?: Record<string, CodexSnapshot | undefined> | null;
|
|
76
|
+
}): UsageWindow[];
|
|
77
|
+
export declare function windowName(mins?: number): string;
|
|
78
|
+
export {};
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
import { CONFLICTING_AUTH_ENV } from './launch.js';
|
|
5
|
+
import { DEFAULT_PROFILE_ID } from './store.js';
|
|
6
|
+
export const defaultBinaries = () => ({
|
|
7
|
+
claude: process.env.OURS_FLEET_CLAUDE_BIN ?? 'claude',
|
|
8
|
+
codex: process.env.OURS_FLEET_CODEX_BIN ?? 'codex',
|
|
9
|
+
script: 'script',
|
|
10
|
+
});
|
|
11
|
+
/**
|
|
12
|
+
* Child env for a profile helper: explicit home for added profiles, the CLI
|
|
13
|
+
* default otherwise. Every credential selector either CLI honours is removed,
|
|
14
|
+
* so a helper can only ever see the profile's own login.
|
|
15
|
+
*/
|
|
16
|
+
export function profileEnv(homeVar, profileId, home, base = process.env) {
|
|
17
|
+
const env = { ...base, BROWSER: 'true', NO_COLOR: '1' };
|
|
18
|
+
for (const key of [...CONFLICTING_AUTH_ENV.claude, ...CONFLICTING_AUTH_ENV.codex, 'ANTHROPIC_BASE_URL', 'OPENAI_BASE_URL'])
|
|
19
|
+
delete env[key];
|
|
20
|
+
if (profileId !== DEFAULT_PROFILE_ID)
|
|
21
|
+
env[homeVar] = home;
|
|
22
|
+
return env;
|
|
23
|
+
}
|
|
24
|
+
/** `claude auth status --json` (documented: exit 0 when logged in, 1 otherwise). */
|
|
25
|
+
export function claudeAuthStatus(bin, env, timeoutMs = 20_000) {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
const child = spawn(bin, ['auth', 'status', '--json'], { env, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
28
|
+
let out = '';
|
|
29
|
+
const timer = setTimeout(() => { child.kill('SIGKILL'); reject(new Error('claude auth status timed out')); }, timeoutMs);
|
|
30
|
+
child.stdout.on('data', d => { out += d; if (out.length > 64 * 1024)
|
|
31
|
+
child.kill('SIGKILL'); });
|
|
32
|
+
child.once('error', error => { clearTimeout(timer); reject(error); });
|
|
33
|
+
child.once('close', code => {
|
|
34
|
+
clearTimeout(timer);
|
|
35
|
+
let parsed = {};
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(out);
|
|
38
|
+
}
|
|
39
|
+
catch { /* non-JSON: rely on exit code */ }
|
|
40
|
+
const str = (v) => typeof v === 'string' && v ? v.slice(0, 120) : undefined;
|
|
41
|
+
resolve({
|
|
42
|
+
loggedIn: code === 0 && parsed.loggedIn !== false,
|
|
43
|
+
email: str(parsed.email), plan: str(parsed.subscriptionType), org: str(parsed.orgName),
|
|
44
|
+
authMethod: str(parsed.authMethod),
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Login expiry timestamps from the Claude credentials file. Field names are
|
|
51
|
+
* observed (Claude Code 2.1.x), not a documented contract, so absence is normal.
|
|
52
|
+
* Only the two numbers are kept; the parsed object is discarded immediately.
|
|
53
|
+
*/
|
|
54
|
+
export function claudeLoginExpiry(home) {
|
|
55
|
+
try {
|
|
56
|
+
const oauth = JSON.parse(readFileSync(join(home, '.credentials.json'), 'utf8')).claudeAiOauth;
|
|
57
|
+
const num = (v) => typeof v === 'number' && Number.isFinite(v) ? v : undefined;
|
|
58
|
+
return { accessExpiresAt: num(oauth?.expiresAt), refreshExpiresAt: num(oauth?.refreshTokenExpiresAt) };
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return {};
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
/** Minimal stdio JSON-RPC client for `codex app-server` (the supported transport). */
|
|
65
|
+
export class CodexAppServer {
|
|
66
|
+
child;
|
|
67
|
+
id = 0;
|
|
68
|
+
buffer = '';
|
|
69
|
+
pending = new Map();
|
|
70
|
+
listeners = new Set();
|
|
71
|
+
closed = false;
|
|
72
|
+
constructor(child) {
|
|
73
|
+
this.child = child;
|
|
74
|
+
child.stdout.on('data', chunk => this.onData(String(chunk)));
|
|
75
|
+
child.stdin.on('error', () => { });
|
|
76
|
+
child.stderr.resume();
|
|
77
|
+
child.once('close', () => {
|
|
78
|
+
this.closed = true;
|
|
79
|
+
for (const p of this.pending.values())
|
|
80
|
+
p.reject(new Error('codex app-server exited'));
|
|
81
|
+
this.pending.clear();
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
static async start(bin, env) {
|
|
85
|
+
const child = spawn(bin, ['app-server'], { env, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
86
|
+
await new Promise((resolve, reject) => { child.once('spawn', resolve); child.once('error', reject); });
|
|
87
|
+
const server = new CodexAppServer(child);
|
|
88
|
+
await server.call('initialize', { clientInfo: { name: 'ours-fleet', title: 'Ours Fleet', version: '1' } });
|
|
89
|
+
child.stdin.write(`${JSON.stringify({ method: 'initialized' })}\n`);
|
|
90
|
+
return server;
|
|
91
|
+
}
|
|
92
|
+
call(method, params, timeoutMs = 20_000) {
|
|
93
|
+
if (this.closed)
|
|
94
|
+
return Promise.reject(new Error('codex app-server exited'));
|
|
95
|
+
const id = ++this.id;
|
|
96
|
+
return new Promise((resolve, reject) => {
|
|
97
|
+
const timer = setTimeout(() => { this.pending.delete(id); reject(new Error(`${method} timed out`)); }, timeoutMs);
|
|
98
|
+
this.pending.set(id, {
|
|
99
|
+
resolve: v => { clearTimeout(timer); resolve(v); },
|
|
100
|
+
reject: e => { clearTimeout(timer); reject(e); },
|
|
101
|
+
});
|
|
102
|
+
this.child.stdin.write(`${JSON.stringify({ id, method, params })}\n`);
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
onNotification(fn) {
|
|
106
|
+
this.listeners.add(fn);
|
|
107
|
+
return () => this.listeners.delete(fn);
|
|
108
|
+
}
|
|
109
|
+
close() {
|
|
110
|
+
if (!this.closed)
|
|
111
|
+
this.child.kill();
|
|
112
|
+
}
|
|
113
|
+
onData(chunk) {
|
|
114
|
+
this.buffer += chunk;
|
|
115
|
+
for (let i = this.buffer.indexOf('\n'); i >= 0; i = this.buffer.indexOf('\n')) {
|
|
116
|
+
const line = this.buffer.slice(0, i).trim();
|
|
117
|
+
this.buffer = this.buffer.slice(i + 1);
|
|
118
|
+
if (!line)
|
|
119
|
+
continue;
|
|
120
|
+
let message;
|
|
121
|
+
try {
|
|
122
|
+
message = JSON.parse(line);
|
|
123
|
+
}
|
|
124
|
+
catch {
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (typeof message.id === 'number' && this.pending.has(message.id) && !message.method) {
|
|
128
|
+
const p = this.pending.get(message.id);
|
|
129
|
+
this.pending.delete(message.id);
|
|
130
|
+
if (message.error)
|
|
131
|
+
p.reject(Object.assign(new Error(String(message.error.message ?? 'codex error').slice(0, 300)), { rpcCode: message.error.code }));
|
|
132
|
+
else
|
|
133
|
+
p.resolve(message.result);
|
|
134
|
+
}
|
|
135
|
+
else if (message.method) {
|
|
136
|
+
for (const fn of this.listeners)
|
|
137
|
+
fn(message.method, message.params);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** One-shot use of an app-server for a profile. */
|
|
143
|
+
export async function withCodex(bin, env, fn) {
|
|
144
|
+
const server = await CodexAppServer.start(bin, env);
|
|
145
|
+
try {
|
|
146
|
+
return await fn(server);
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
server.close();
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
/** Flatten `account/rateLimits/read` into display windows. */
|
|
153
|
+
export function codexUsageWindows(result) {
|
|
154
|
+
const snapshots = result.rateLimitsByLimitId && Object.keys(result.rateLimitsByLimitId).length
|
|
155
|
+
? Object.entries(result.rateLimitsByLimitId) : [['codex', result.rateLimits]];
|
|
156
|
+
const windows = [];
|
|
157
|
+
for (const [limitId, snap] of snapshots) {
|
|
158
|
+
if (!snap)
|
|
159
|
+
continue;
|
|
160
|
+
for (const [slot, w] of [['primary', snap.primary], ['secondary', snap.secondary]]) {
|
|
161
|
+
if (!w || typeof w.usedPercent !== 'number')
|
|
162
|
+
continue;
|
|
163
|
+
windows.push({
|
|
164
|
+
id: `${limitId}:${slot}`,
|
|
165
|
+
label: `${snap.limitName ?? limitId} · ${windowName(w.windowDurationMins ?? undefined)}`,
|
|
166
|
+
usedPercent: w.usedPercent,
|
|
167
|
+
windowMins: w.windowDurationMins ?? undefined,
|
|
168
|
+
resetsAt: typeof w.resetsAt === 'number' ? new Date(w.resetsAt * 1000).toISOString() : undefined,
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return windows;
|
|
173
|
+
}
|
|
174
|
+
export function windowName(mins) {
|
|
175
|
+
if (!mins)
|
|
176
|
+
return 'window';
|
|
177
|
+
if (mins === 300)
|
|
178
|
+
return '5h';
|
|
179
|
+
if (mins === 10_080)
|
|
180
|
+
return 'weekly';
|
|
181
|
+
if (mins % 1440 === 0)
|
|
182
|
+
return `${mins / 1440}d`;
|
|
183
|
+
if (mins % 60 === 0)
|
|
184
|
+
return `${mins / 60}h`;
|
|
185
|
+
return `${mins}m`;
|
|
186
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { type SubscriptionPin, type SubscriptionProvider } from './store.js';
|
|
2
|
+
/** The subscription provider a harness authenticates with, if Fleet manages it. */
|
|
3
|
+
export declare function providerForHarness(harness: string): SubscriptionProvider | undefined;
|
|
4
|
+
/** Env var that selects the CLI home for each provider. */
|
|
5
|
+
export declare const HOME_ENV: Record<SubscriptionProvider, string>;
|
|
6
|
+
/**
|
|
7
|
+
* Credential sources that would silently outrank the fleet-wide active profile
|
|
8
|
+
* (see Claude Code's documented authentication precedence and Codex's
|
|
9
|
+
* API-key login). While a non-default profile is active they are refused.
|
|
10
|
+
*/
|
|
11
|
+
export declare const CONFLICTING_AUTH_ENV: Record<SubscriptionProvider, string[]>;
|
|
12
|
+
/** Claude managed-settings file (Linux), which outranks user settings. */
|
|
13
|
+
export declare const CLAUDE_MANAGED_SETTINGS = "/etc/claude-code/managed-settings.json";
|
|
14
|
+
/**
|
|
15
|
+
* Competing Claude credential sources declared in settings files: an `env`
|
|
16
|
+
* block entry from CONFLICTING_AUTH_ENV, or an `apiKeyHelper`. Settings are
|
|
17
|
+
* shared into profiles, so they can silently outrank a profile's login.
|
|
18
|
+
*/
|
|
19
|
+
export declare function claudeSettingsAuthSources(home: string, managed?: string): string[];
|
|
20
|
+
export interface LaunchPin {
|
|
21
|
+
pin: SubscriptionPin;
|
|
22
|
+
/** Added to the harness child env last. Empty for the adopted default. */
|
|
23
|
+
env: Record<string, string>;
|
|
24
|
+
/** Host path the sandbox must mount read-write, if any. */
|
|
25
|
+
writablePath?: string;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve the provider's active profile for a launch and record it in the
|
|
29
|
+
* agent's state dir. Resolution happens once, here: a later switch never
|
|
30
|
+
* changes what this session uses.
|
|
31
|
+
*/
|
|
32
|
+
export declare function pinSubscriptionForLaunch(role: {
|
|
33
|
+
name: string;
|
|
34
|
+
harness: string;
|
|
35
|
+
env?: Record<string, string>;
|
|
36
|
+
auth_proxy?: unknown;
|
|
37
|
+
}, stateDir: string, inherited?: NodeJS.ProcessEnv, now?: () => Date): LaunchPin | undefined;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { FleetError } from '../application/errors.js';
|
|
4
|
+
import { DEFAULT_PROFILE_ID, profileHome, readSubscriptionState, writePin, } from './store.js';
|
|
5
|
+
/** The subscription provider a harness authenticates with, if Fleet manages it. */
|
|
6
|
+
export function providerForHarness(harness) {
|
|
7
|
+
if (harness === 'claude-code')
|
|
8
|
+
return 'claude';
|
|
9
|
+
if (harness === 'codex')
|
|
10
|
+
return 'codex';
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
/** Env var that selects the CLI home for each provider. */
|
|
14
|
+
export const HOME_ENV = {
|
|
15
|
+
claude: 'CLAUDE_CONFIG_DIR',
|
|
16
|
+
codex: 'CODEX_HOME',
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Credential sources that would silently outrank the fleet-wide active profile
|
|
20
|
+
* (see Claude Code's documented authentication precedence and Codex's
|
|
21
|
+
* API-key login). While a non-default profile is active they are refused.
|
|
22
|
+
*/
|
|
23
|
+
export const CONFLICTING_AUTH_ENV = {
|
|
24
|
+
claude: [
|
|
25
|
+
'CLAUDE_CONFIG_DIR', 'ANTHROPIC_API_KEY', 'ANTHROPIC_AUTH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN',
|
|
26
|
+
'CLAUDE_CODE_OAUTH_REFRESH_TOKEN', 'CLAUDE_CODE_OAUTH_TOKEN_FILE_DESCRIPTOR',
|
|
27
|
+
'CLAUDE_CODE_USE_BEDROCK', 'CLAUDE_CODE_USE_VERTEX', 'CLAUDE_CODE_USE_FOUNDRY', 'ANTHROPIC_PROFILE',
|
|
28
|
+
'ANTHROPIC_FEDERATION_RULE_ID', 'ANTHROPIC_ORGANIZATION_ID', 'ANTHROPIC_IDENTITY_TOKEN_FILE',
|
|
29
|
+
],
|
|
30
|
+
codex: ['CODEX_HOME', 'OPENAI_API_KEY', 'CODEX_API_KEY'],
|
|
31
|
+
};
|
|
32
|
+
/** Claude managed-settings file (Linux), which outranks user settings. */
|
|
33
|
+
export const CLAUDE_MANAGED_SETTINGS = '/etc/claude-code/managed-settings.json';
|
|
34
|
+
/**
|
|
35
|
+
* Competing Claude credential sources declared in settings files: an `env`
|
|
36
|
+
* block entry from CONFLICTING_AUTH_ENV, or an `apiKeyHelper`. Settings are
|
|
37
|
+
* shared into profiles, so they can silently outrank a profile's login.
|
|
38
|
+
*/
|
|
39
|
+
export function claudeSettingsAuthSources(home, managed = CLAUDE_MANAGED_SETTINGS) {
|
|
40
|
+
const found = [];
|
|
41
|
+
for (const file of [join(home, 'settings.json'), managed]) {
|
|
42
|
+
let settings;
|
|
43
|
+
try {
|
|
44
|
+
settings = JSON.parse(readFileSync(file, 'utf8'));
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
for (const key of CONFLICTING_AUTH_ENV.claude)
|
|
50
|
+
if (settings?.env && typeof settings.env === 'object' && key in settings.env)
|
|
51
|
+
found.push(`${key} (${file})`);
|
|
52
|
+
if (settings?.apiKeyHelper)
|
|
53
|
+
found.push(`apiKeyHelper (${file})`);
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the provider's active profile for a launch and record it in the
|
|
59
|
+
* agent's state dir. Resolution happens once, here: a later switch never
|
|
60
|
+
* changes what this session uses.
|
|
61
|
+
*/
|
|
62
|
+
export function pinSubscriptionForLaunch(role, stateDir, inherited = process.env, now = () => new Date()) {
|
|
63
|
+
const provider = providerForHarness(role.harness);
|
|
64
|
+
// A loopback auth proxy supplies credentials itself; it is not managed here.
|
|
65
|
+
if (!provider || role.auth_proxy)
|
|
66
|
+
return undefined;
|
|
67
|
+
const profileId = readSubscriptionState().providers[provider].activeProfileId;
|
|
68
|
+
const pin = { provider, profileId, pinnedAt: now().toISOString() };
|
|
69
|
+
if (profileId === DEFAULT_PROFILE_ID) {
|
|
70
|
+
// The adopted default launches exactly as before; credential env that points
|
|
71
|
+
// elsewhere is recorded so the panel can say this session is not managed.
|
|
72
|
+
const unmanaged = CONFLICTING_AUTH_ENV[provider].filter(key => role.env?.[key] !== undefined || inherited[key]);
|
|
73
|
+
if (unmanaged.length)
|
|
74
|
+
pin.unmanaged = unmanaged;
|
|
75
|
+
writePin(stateDir, pin);
|
|
76
|
+
return { pin, env: {} };
|
|
77
|
+
}
|
|
78
|
+
const conflicts = CONFLICTING_AUTH_ENV[provider].filter(key => role.env?.[key] !== undefined || (key !== HOME_ENV[provider] && inherited[key]));
|
|
79
|
+
if (conflicts.length) {
|
|
80
|
+
throw new FleetError('invalid_request', `role '${role.name}': ${conflicts.join(', ')} would bypass the active ${provider} subscription profile; ` +
|
|
81
|
+
'remove it from the role env / supervisor environment or switch back to the default profile');
|
|
82
|
+
}
|
|
83
|
+
const home = profileHome(provider, profileId);
|
|
84
|
+
if (provider === 'claude') {
|
|
85
|
+
const settings = claudeSettingsAuthSources(home);
|
|
86
|
+
if (settings.length) {
|
|
87
|
+
throw new FleetError('invalid_request', `role '${role.name}': Claude settings declare ${settings.join(', ')}, which would bypass the active subscription profile`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
pin.home = home;
|
|
91
|
+
writePin(stateDir, pin);
|
|
92
|
+
return { pin, env: { [HOME_ENV[provider]]: home }, writablePath: home };
|
|
93
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { type SubscriptionBinaries } from './cli.js';
|
|
2
|
+
import type { SubscriptionProvider } from './store.js';
|
|
3
|
+
/**
|
|
4
|
+
* A short-lived, browser-owned login. The URL comes from the official CLI and
|
|
5
|
+
* is shown only if its origin is on the provider's allowlist; the user's code
|
|
6
|
+
* goes to the CLI's stdin exactly once and is never stored.
|
|
7
|
+
*/
|
|
8
|
+
export type LoginState = 'starting' | 'awaiting_user' | 'verifying' | 'succeeded' | 'failed' | 'cancelled' | 'expired';
|
|
9
|
+
export interface LoginView {
|
|
10
|
+
loginId: string;
|
|
11
|
+
provider: SubscriptionProvider;
|
|
12
|
+
kind: 'paste_code' | 'device_code';
|
|
13
|
+
state: LoginState;
|
|
14
|
+
url?: string;
|
|
15
|
+
/** Device code to type at `url`; only exposed while the user still needs it. */
|
|
16
|
+
userCode?: string;
|
|
17
|
+
expiresAt: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare const LOGIN_TTL_MS: number;
|
|
21
|
+
export declare const LOGIN_URL_ALLOWLIST: Record<SubscriptionProvider, string[]>;
|
|
22
|
+
export declare function allowedLoginUrl(provider: SubscriptionProvider, raw: string): string | undefined;
|
|
23
|
+
export interface LoginHandle {
|
|
24
|
+
view(): LoginView;
|
|
25
|
+
/** Browser session that started the login; only it may act on it. */
|
|
26
|
+
readonly owner: string;
|
|
27
|
+
submitCode(code: string): void;
|
|
28
|
+
cancel(): void;
|
|
29
|
+
/** Resolves when the CLI has finished (success or not). */
|
|
30
|
+
readonly done: Promise<boolean>;
|
|
31
|
+
}
|
|
32
|
+
declare abstract class BaseLogin implements LoginHandle {
|
|
33
|
+
readonly provider: SubscriptionProvider;
|
|
34
|
+
readonly owner: string;
|
|
35
|
+
readonly kind: LoginView['kind'];
|
|
36
|
+
readonly loginId: string;
|
|
37
|
+
readonly expiresAt: number;
|
|
38
|
+
state: LoginState;
|
|
39
|
+
url?: string;
|
|
40
|
+
userCode?: string;
|
|
41
|
+
error?: string;
|
|
42
|
+
done: Promise<boolean>;
|
|
43
|
+
protected finish: (ok: boolean) => void;
|
|
44
|
+
private readonly timer;
|
|
45
|
+
constructor(provider: SubscriptionProvider, owner: string, kind: LoginView['kind'], now?: number);
|
|
46
|
+
view(): LoginView;
|
|
47
|
+
abstract submitCode(code: string): void;
|
|
48
|
+
protected abstract stop(): void;
|
|
49
|
+
cancel(): void;
|
|
50
|
+
protected end(state: LoginState, error?: string): void;
|
|
51
|
+
/** Called by the service after verifying the new home. */
|
|
52
|
+
complete(ok: boolean, error?: string): void;
|
|
53
|
+
}
|
|
54
|
+
/** `claude auth login` inside script(1): prints an authorize URL, then reads a paste code. */
|
|
55
|
+
export declare class ClaudeLogin extends BaseLogin {
|
|
56
|
+
private child?;
|
|
57
|
+
private output;
|
|
58
|
+
private submitted;
|
|
59
|
+
/** Resolves with the CLI exit status once it exits. */
|
|
60
|
+
exited: Promise<number | null>;
|
|
61
|
+
constructor(owner: string, bins: SubscriptionBinaries, home: string, profileId: string);
|
|
62
|
+
submitCode(code: string): void;
|
|
63
|
+
protected stop(): void;
|
|
64
|
+
}
|
|
65
|
+
/** Codex device-code login through the app-server's supported account API. */
|
|
66
|
+
export declare class CodexDeviceLogin extends BaseLogin {
|
|
67
|
+
private server?;
|
|
68
|
+
private codexLoginId?;
|
|
69
|
+
/** Resolves true when the app-server reports a successful login. */
|
|
70
|
+
completed: Promise<boolean>;
|
|
71
|
+
constructor(owner: string, bins: SubscriptionBinaries, home: string, profileId: string);
|
|
72
|
+
submitCode(): void;
|
|
73
|
+
protected stop(): void;
|
|
74
|
+
}
|
|
75
|
+
export {};
|