@tokenfactory/acc-runner 0.40.10 → 0.40.12
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/engines/account-identity.d.ts +54 -0
- package/dist/engines/account-identity.d.ts.map +1 -0
- package/dist/engines/account-identity.js +93 -0
- package/dist/engines/account-identity.js.map +1 -0
- package/dist/engines/account-probe.d.ts +124 -0
- package/dist/engines/account-probe.d.ts.map +1 -0
- package/dist/engines/account-probe.js +134 -0
- package/dist/engines/account-probe.js.map +1 -0
- package/dist/engines/claude-code-chat.d.ts +15 -1
- package/dist/engines/claude-code-chat.d.ts.map +1 -1
- package/dist/engines/claude-code-chat.js +118 -1
- package/dist/engines/claude-code-chat.js.map +1 -1
- package/dist/engines/codex-chat.d.ts.map +1 -1
- package/dist/engines/codex-chat.js +23 -0
- package/dist/engines/codex-chat.js.map +1 -1
- package/dist/engines/gemini.d.ts.map +1 -1
- package/dist/engines/gemini.js +20 -0
- package/dist/engines/gemini.js.map +1 -1
- package/dist/engines/types.d.ts +43 -0
- package/dist/engines/types.d.ts.map +1 -1
- package/dist/locks/index.d.ts +36 -0
- package/dist/locks/index.d.ts.map +1 -0
- package/dist/locks/index.js +36 -0
- package/dist/locks/index.js.map +1 -0
- package/dist/task-runner.d.ts.map +1 -1
- package/dist/task-runner.js +17 -0
- package/dist/task-runner.js.map +1 -1
- package/dist/watch.d.ts +11 -1
- package/dist/watch.d.ts.map +1 -1
- package/dist/watch.js +25 -3
- package/dist/watch.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { EngineAccountIdentity, EngineAuthMode, EnginePlanHint } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Derive the stable synthetic account id.
|
|
4
|
+
* - With an email: `acct:<lowercased-trimmed-email>` — the SAME login on two
|
|
5
|
+
* hosts/channels collapses to one id, which is exactly the convergence the
|
|
6
|
+
* broadcaster wants.
|
|
7
|
+
* - Without an email: `host:<hostname>/<engineId>/<channel>` — stable for a
|
|
8
|
+
* given runner install + channel, distinct across hosts, and never null.
|
|
9
|
+
*/
|
|
10
|
+
export declare function syntheticAccountId(args: {
|
|
11
|
+
engineId: string;
|
|
12
|
+
channel: EngineAuthMode;
|
|
13
|
+
email?: string | null;
|
|
14
|
+
hostname?: string;
|
|
15
|
+
}): string;
|
|
16
|
+
/**
|
|
17
|
+
* Build the HONEST "unknown-account" identity an adapter reports when it cannot
|
|
18
|
+
* introspect a real account (codex/gemini today, or claude-code in api-key mode
|
|
19
|
+
* where a bare key carries no email). Never fabricates an email or a plan.
|
|
20
|
+
*/
|
|
21
|
+
export declare function unknownAccountIdentity(args: {
|
|
22
|
+
engineId: string;
|
|
23
|
+
channel: EngineAuthMode;
|
|
24
|
+
planHint?: EnginePlanHint;
|
|
25
|
+
detail: string;
|
|
26
|
+
hostname?: string;
|
|
27
|
+
}): EngineAccountIdentity;
|
|
28
|
+
/**
|
|
29
|
+
* Build a RESOLVED account identity from an introspected email (+ optional
|
|
30
|
+
* plan). The synthetic id is email-derived so it converges across hosts.
|
|
31
|
+
*/
|
|
32
|
+
export declare function resolvedAccountIdentity(args: {
|
|
33
|
+
engineId: string;
|
|
34
|
+
channel: EngineAuthMode;
|
|
35
|
+
email: string;
|
|
36
|
+
planHint?: EnginePlanHint;
|
|
37
|
+
detail: string;
|
|
38
|
+
}): EngineAccountIdentity;
|
|
39
|
+
/**
|
|
40
|
+
* The identity used for change detection / broadcast keying. Two probes name the
|
|
41
|
+
* same account iff their `synthetic_account` matches (email-derived when known,
|
|
42
|
+
* host-derived otherwise) — so a login swap on one host, an email that appears
|
|
43
|
+
* where there was none, or a channel flip are all detected, while a plain
|
|
44
|
+
* cadence refresh that resolves the same account is a no-op.
|
|
45
|
+
*/
|
|
46
|
+
export declare function accountKey(identity: EngineAccountIdentity): string;
|
|
47
|
+
/**
|
|
48
|
+
* True when `next` names a different account than `prev` (or there was no prior
|
|
49
|
+
* account). The broadcaster audits on `true` and last-writer-wins-skips on
|
|
50
|
+
* `false`. A plan_hint-only change (same account, upgraded plan) also counts as
|
|
51
|
+
* a change worth re-broadcasting so downstream cap logic sees the new plan.
|
|
52
|
+
*/
|
|
53
|
+
export declare function accountChanged(prev: EngineAccountIdentity | null, next: EngineAccountIdentity): boolean;
|
|
54
|
+
//# sourceMappingURL=account-identity.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-identity.d.ts","sourceRoot":"","sources":["../../src/engines/account-identity.ts"],"names":[],"mappings":"AAoBA,OAAO,KAAK,EACV,qBAAqB,EACrB,cAAc,EACd,cAAc,EACf,MAAM,YAAY,CAAC;AAEpB;;;;;;;GAOG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE;IACvC,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,MAAM,CAKT;AAED;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,IAAI,EAAE;IAC3C,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,GAAG,qBAAqB,CAYxB;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE;IAC5C,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,cAAc,CAAC;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,cAAc,CAAC;IAC1B,MAAM,EAAE,MAAM,CAAC;CAChB,GAAG,qBAAqB,CAaxB;AAED;;;;;;GAMG;AACH,wBAAgB,UAAU,CAAC,QAAQ,EAAE,qBAAqB,GAAG,MAAM,CAElE;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,IAAI,EAAE,qBAAqB,GAAG,IAAI,EAClC,IAAI,EAAE,qBAAqB,GAC1B,OAAO,CAGT"}
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AS-1 (ACCOUNT-PROBE): shared, side-effect-free helpers for the engine
|
|
3
|
+
* `accountIdentity()` surface (types.ts). Two concerns live here so every
|
|
4
|
+
* adapter (claude-code / codex / gemini) spells them identically:
|
|
5
|
+
*
|
|
6
|
+
* 1. Synthetic-account derivation — a STABLE id an engine reports so the
|
|
7
|
+
* identity broadcaster always has a concrete participant to converge on,
|
|
8
|
+
* even when the real account email is unintrospectable. Email-derived when
|
|
9
|
+
* an email resolves (same login → same id across hosts); host+engine+channel
|
|
10
|
+
* derived otherwise (an unknown-account runner is still stable + distinct
|
|
11
|
+
* rather than a flapping null).
|
|
12
|
+
* 2. Change detection / audit — `accountChanged()` compares the last-broadcast
|
|
13
|
+
* identity against a freshly-probed one so the last-writer-wins broadcaster
|
|
14
|
+
* can decide whether an emit is a no-op refresh or a real login swap worth
|
|
15
|
+
* auditing (the ajay→gaurav evidence that motivated the task).
|
|
16
|
+
*
|
|
17
|
+
* PURE by construction (the one impurity — hostname — is passed in), so the
|
|
18
|
+
* cadence + parsing logic is unit-testable without touching `os` or a clock.
|
|
19
|
+
*/
|
|
20
|
+
import os from "node:os";
|
|
21
|
+
/**
|
|
22
|
+
* Derive the stable synthetic account id.
|
|
23
|
+
* - With an email: `acct:<lowercased-trimmed-email>` — the SAME login on two
|
|
24
|
+
* hosts/channels collapses to one id, which is exactly the convergence the
|
|
25
|
+
* broadcaster wants.
|
|
26
|
+
* - Without an email: `host:<hostname>/<engineId>/<channel>` — stable for a
|
|
27
|
+
* given runner install + channel, distinct across hosts, and never null.
|
|
28
|
+
*/
|
|
29
|
+
export function syntheticAccountId(args) {
|
|
30
|
+
const email = (args.email ?? "").trim().toLowerCase();
|
|
31
|
+
if (email)
|
|
32
|
+
return `acct:${email}`;
|
|
33
|
+
const host = (args.hostname ?? os.hostname() ?? "").trim() || "unknown-host";
|
|
34
|
+
return `host:${host}/${args.engineId}/${args.channel}`;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build the HONEST "unknown-account" identity an adapter reports when it cannot
|
|
38
|
+
* introspect a real account (codex/gemini today, or claude-code in api-key mode
|
|
39
|
+
* where a bare key carries no email). Never fabricates an email or a plan.
|
|
40
|
+
*/
|
|
41
|
+
export function unknownAccountIdentity(args) {
|
|
42
|
+
return {
|
|
43
|
+
account_email: null,
|
|
44
|
+
plan_hint: args.planHint ?? "unknown",
|
|
45
|
+
channel: args.channel,
|
|
46
|
+
synthetic_account: syntheticAccountId({
|
|
47
|
+
engineId: args.engineId,
|
|
48
|
+
channel: args.channel,
|
|
49
|
+
hostname: args.hostname,
|
|
50
|
+
}),
|
|
51
|
+
detail: args.detail,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build a RESOLVED account identity from an introspected email (+ optional
|
|
56
|
+
* plan). The synthetic id is email-derived so it converges across hosts.
|
|
57
|
+
*/
|
|
58
|
+
export function resolvedAccountIdentity(args) {
|
|
59
|
+
const email = args.email.trim();
|
|
60
|
+
return {
|
|
61
|
+
account_email: email,
|
|
62
|
+
plan_hint: args.planHint ?? "unknown",
|
|
63
|
+
channel: args.channel,
|
|
64
|
+
synthetic_account: syntheticAccountId({
|
|
65
|
+
engineId: args.engineId,
|
|
66
|
+
channel: args.channel,
|
|
67
|
+
email,
|
|
68
|
+
}),
|
|
69
|
+
detail: args.detail,
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* The identity used for change detection / broadcast keying. Two probes name the
|
|
74
|
+
* same account iff their `synthetic_account` matches (email-derived when known,
|
|
75
|
+
* host-derived otherwise) — so a login swap on one host, an email that appears
|
|
76
|
+
* where there was none, or a channel flip are all detected, while a plain
|
|
77
|
+
* cadence refresh that resolves the same account is a no-op.
|
|
78
|
+
*/
|
|
79
|
+
export function accountKey(identity) {
|
|
80
|
+
return identity.synthetic_account;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* True when `next` names a different account than `prev` (or there was no prior
|
|
84
|
+
* account). The broadcaster audits on `true` and last-writer-wins-skips on
|
|
85
|
+
* `false`. A plan_hint-only change (same account, upgraded plan) also counts as
|
|
86
|
+
* a change worth re-broadcasting so downstream cap logic sees the new plan.
|
|
87
|
+
*/
|
|
88
|
+
export function accountChanged(prev, next) {
|
|
89
|
+
if (!prev)
|
|
90
|
+
return true;
|
|
91
|
+
return accountKey(prev) !== accountKey(next) || prev.plan_hint !== next.plan_hint;
|
|
92
|
+
}
|
|
93
|
+
//# sourceMappingURL=account-identity.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-identity.js","sourceRoot":"","sources":["../../src/engines/account-identity.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,OAAO,EAAE,MAAM,SAAS,CAAC;AAOzB;;;;;;;GAOG;AACH,MAAM,UAAU,kBAAkB,CAAC,IAKlC;IACC,MAAM,KAAK,GAAG,CAAC,IAAI,CAAC,KAAK,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACtD,IAAI,KAAK;QAAE,OAAO,QAAQ,KAAK,EAAE,CAAC;IAClC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,EAAE,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,IAAI,cAAc,CAAC;IAC7E,OAAO,QAAQ,IAAI,IAAI,IAAI,CAAC,QAAQ,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;AACzD,CAAC;AAED;;;;GAIG;AACH,MAAM,UAAU,sBAAsB,CAAC,IAMtC;IACC,OAAO;QACL,aAAa,EAAE,IAAI;QACnB,SAAS,EAAE,IAAI,CAAC,QAAQ,IAAI,SAAS;QACrC,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,iBAAiB,EAAE,kBAAkB,CAAC;YACpC,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ;SACxB,CAAC;QACF,MAAM,EAAE,IAAI,CAAC,MAAM;KACpB,CAAC;AACJ,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,uBAAuB,CAAC,IAMvC;IACC,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;IAChC,OAAO;QACL,aAAa,EAAE,KAAK;QACpB,SAAS,EAAE,IAAI,CAAC,QAAQ,IAAI,SAAS;QACrC,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,iBAAiB,EAAE,kBAAkB,CAAC;YACpC,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,KAAK;SACN,CAAC;QACF,MAAM,EAAE,IAAI,CAAC,MAAM;KACpB,CAAC;AACJ,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,UAAU,CAAC,QAA+B;IACxD,OAAO,QAAQ,CAAC,iBAAiB,CAAC;AACpC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,cAAc,CAC5B,IAAkC,EAClC,IAA2B;IAE3B,IAAI,CAAC,IAAI;QAAE,OAAO,IAAI,CAAC;IACvB,OAAO,UAAU,CAAC,IAAI,CAAC,KAAK,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,SAAS,KAAK,IAAI,CAAC,SAAS,CAAC;AACpF,CAAC"}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AS-1 (ACCOUNT-PROBE): the account-probe CADENCE scheduler.
|
|
3
|
+
*
|
|
4
|
+
* Extends the engine credential probe with a WHO-am-I probe fired on four
|
|
5
|
+
* triggers so a mid-flight login swap can never go undetected (operator
|
|
6
|
+
* evidence: swapping the keychain login ajay→gaurav went undetected while the
|
|
7
|
+
* runner sat paused on the old account's reset timer):
|
|
8
|
+
*
|
|
9
|
+
* 1. startup — one probe the moment the scheduler starts.
|
|
10
|
+
* 2. pause-tick — every ~60s WHILE the runner is capacity-paused (the exact
|
|
11
|
+
* window the ajay→gaurav swap slipped through: a paused
|
|
12
|
+
* runner otherwise only re-probed on the 5-min heartbeat).
|
|
13
|
+
* 3. heartbeat — every ~5min while healthy (unpaused).
|
|
14
|
+
* 4. auth-failure — immediately after any 401/403-class engine failure
|
|
15
|
+
* (debounced so a burst of 401s is a single probe).
|
|
16
|
+
*
|
|
17
|
+
* Each probe result is handed to `emit` with a MONOTONIC sequence + wall-clock
|
|
18
|
+
* stamp so the identity broadcaster resolves concurrent writers last-writer-wins,
|
|
19
|
+
* and a `changed` flag so a real login swap is audited while a plain cadence
|
|
20
|
+
* refresh of the same account is a cheap no-op.
|
|
21
|
+
*
|
|
22
|
+
* FULLY injectable (probe / emit / clock / timer) so the four-trigger cadence is
|
|
23
|
+
* exercised with a fake clock — no real binary, no wall-clock sleep, no network.
|
|
24
|
+
* Wiring this scheduler onto the companion's identity broadcaster is a follow-up
|
|
25
|
+
* (companion.ts is outside this track's file scope); this module is the engine
|
|
26
|
+
* primitive that follow-up drives.
|
|
27
|
+
*/
|
|
28
|
+
import type { EngineAccountIdentity } from "./types.js";
|
|
29
|
+
/** Which cadence trigger fired a given probe. */
|
|
30
|
+
export type AccountProbeTrigger = "startup" | "pause-tick" | "heartbeat" | "auth-failure";
|
|
31
|
+
/** One account-probe broadcast handed to the identity broadcaster. */
|
|
32
|
+
export interface AccountProbeBroadcast {
|
|
33
|
+
/** The probed account identity. */
|
|
34
|
+
identity: EngineAccountIdentity;
|
|
35
|
+
/** Which cadence trigger produced this probe. */
|
|
36
|
+
trigger: AccountProbeTrigger;
|
|
37
|
+
/**
|
|
38
|
+
* True when this probe named a DIFFERENT account (or plan) than the last
|
|
39
|
+
* broadcast — the audit signal a login swap fires on. False on a plain
|
|
40
|
+
* same-account cadence refresh.
|
|
41
|
+
*/
|
|
42
|
+
changed: boolean;
|
|
43
|
+
/** Monotonic 1-based sequence for last-writer-wins resolution on the topic. */
|
|
44
|
+
seq: number;
|
|
45
|
+
/** Wall-clock ms of the probe (from the injected clock). */
|
|
46
|
+
at: number;
|
|
47
|
+
}
|
|
48
|
+
/** Timer seam so the cadence is drivable by a fake clock in tests. */
|
|
49
|
+
export interface ProbeTimer {
|
|
50
|
+
/** Schedule `fn` after `ms`; returns an opaque handle. */
|
|
51
|
+
setTimer(fn: () => void, ms: number): unknown;
|
|
52
|
+
/** Cancel a scheduled handle. */
|
|
53
|
+
clearTimer(handle: unknown): void;
|
|
54
|
+
}
|
|
55
|
+
export interface AccountProbeSchedulerDeps {
|
|
56
|
+
/** Probe the engine account. Should itself degrade to an honest `"unknown"` identity rather than throw. */
|
|
57
|
+
probe: () => Promise<EngineAccountIdentity>;
|
|
58
|
+
/** Emit a probed identity on the identity broadcaster (last-writer-wins keyed on `seq`). */
|
|
59
|
+
emit: (broadcast: AccountProbeBroadcast) => void | Promise<void>;
|
|
60
|
+
/** Healthy heartbeat cadence (ms). Default 5min. */
|
|
61
|
+
heartbeatMs?: number;
|
|
62
|
+
/** While-paused probe cadence (ms). Default 60s. */
|
|
63
|
+
pauseTickMs?: number;
|
|
64
|
+
/** Min gap between auth-failure probes (ms) so a 401 burst is one probe. Default 1s. */
|
|
65
|
+
authFailureDebounceMs?: number;
|
|
66
|
+
/** Clock (test seam). Default `Date.now`. */
|
|
67
|
+
now?: () => number;
|
|
68
|
+
/** Timer seam (test seam). Default `setTimeout`/`clearTimeout`. */
|
|
69
|
+
timer?: ProbeTimer;
|
|
70
|
+
/** Best-effort audit/log sink for detected swaps + probe errors. */
|
|
71
|
+
onLog?: (line: string) => void;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Drives the four-trigger account-probe cadence. Probes are SERIALIZED onto one
|
|
75
|
+
* chain so sequence numbers are strictly monotonic and two triggers never race
|
|
76
|
+
* to emit an out-of-order identity. Call `start()` to fire the startup probe and
|
|
77
|
+
* arm the recurring timer, `setPaused()` on capacity transitions, and
|
|
78
|
+
* `onAuthFailure()` from the engine failure path.
|
|
79
|
+
*/
|
|
80
|
+
export declare class AccountProbeScheduler {
|
|
81
|
+
private readonly deps;
|
|
82
|
+
private readonly heartbeatMs;
|
|
83
|
+
private readonly pauseTickMs;
|
|
84
|
+
private readonly authFailureDebounceMs;
|
|
85
|
+
private readonly now;
|
|
86
|
+
private readonly timer;
|
|
87
|
+
private started;
|
|
88
|
+
private stopped;
|
|
89
|
+
private paused;
|
|
90
|
+
private recurringHandle;
|
|
91
|
+
private seq;
|
|
92
|
+
private last;
|
|
93
|
+
private lastAuthFailureAt;
|
|
94
|
+
/** Serial probe chain: every probe awaits the prior one so `seq` stays ordered. */
|
|
95
|
+
private tail;
|
|
96
|
+
constructor(deps: AccountProbeSchedulerDeps);
|
|
97
|
+
/** The last identity broadcast (null before the first probe). */
|
|
98
|
+
lastIdentity(): EngineAccountIdentity | null;
|
|
99
|
+
/** Fire the startup probe and arm the recurring cadence timer. Idempotent. */
|
|
100
|
+
start(): void;
|
|
101
|
+
/**
|
|
102
|
+
* Note a capacity pause/resume transition. Re-arms the recurring timer to the
|
|
103
|
+
* matching cadence (~60s paused, ~5min healthy). A no-op when the state is
|
|
104
|
+
* unchanged, so repeated pause ticks don't reset the timer out from under
|
|
105
|
+
* themselves.
|
|
106
|
+
*/
|
|
107
|
+
setPaused(paused: boolean): void;
|
|
108
|
+
/**
|
|
109
|
+
* Probe immediately after a 401/403-class engine failure — the single most
|
|
110
|
+
* likely moment the underlying login just changed. Debounced so a burst of
|
|
111
|
+
* auth failures collapses to one probe.
|
|
112
|
+
*/
|
|
113
|
+
onAuthFailure(): void;
|
|
114
|
+
/** Cancel the recurring timer; in-flight probes still settle. */
|
|
115
|
+
stop(): void;
|
|
116
|
+
/** Resolve once every queued probe has settled (test seam). */
|
|
117
|
+
settle(): Promise<void>;
|
|
118
|
+
private armRecurring;
|
|
119
|
+
private onRecurringFire;
|
|
120
|
+
/** Chain a probe onto the serial tail so sequence order is guaranteed. */
|
|
121
|
+
private enqueue;
|
|
122
|
+
private runProbe;
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=account-probe.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-probe.d.ts","sourceRoot":"","sources":["../../src/engines/account-probe.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAGxD,iDAAiD;AACjD,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,cAAc,CAAC;AAE1F,sEAAsE;AACtE,MAAM,WAAW,qBAAqB;IACpC,mCAAmC;IACnC,QAAQ,EAAE,qBAAqB,CAAC;IAChC,iDAAiD;IACjD,OAAO,EAAE,mBAAmB,CAAC;IAC7B;;;;OAIG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB,+EAA+E;IAC/E,GAAG,EAAE,MAAM,CAAC;IACZ,4DAA4D;IAC5D,EAAE,EAAE,MAAM,CAAC;CACZ;AAED,sEAAsE;AACtE,MAAM,WAAW,UAAU;IACzB,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9C,iCAAiC;IACjC,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI,CAAC;CACnC;AAOD,MAAM,WAAW,yBAAyB;IACxC,2GAA2G;IAC3G,KAAK,EAAE,MAAM,OAAO,CAAC,qBAAqB,CAAC,CAAC;IAC5C,4FAA4F;IAC5F,IAAI,EAAE,CAAC,SAAS,EAAE,qBAAqB,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,oDAAoD;IACpD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,wFAAwF;IACxF,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,6CAA6C;IAC7C,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;IACnB,mEAAmE;IACnE,KAAK,CAAC,EAAE,UAAU,CAAC;IACnB,oEAAoE;IACpE,KAAK,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CAChC;AAED;;;;;;GAMG;AACH,qBAAa,qBAAqB;IAiBpB,OAAO,CAAC,QAAQ,CAAC,IAAI;IAhBjC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,qBAAqB,CAAS;IAC/C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAa;IAEnC,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,eAAe,CAAiB;IACxC,OAAO,CAAC,GAAG,CAAK;IAChB,OAAO,CAAC,IAAI,CAAsC;IAClD,OAAO,CAAC,iBAAiB,CAA4B;IACrD,mFAAmF;IACnF,OAAO,CAAC,IAAI,CAAoC;gBAEnB,IAAI,EAAE,yBAAyB;IAQ5D,iEAAiE;IACjE,YAAY,IAAI,qBAAqB,GAAG,IAAI;IAI5C,8EAA8E;IAC9E,KAAK,IAAI,IAAI;IAOb;;;;;OAKG;IACH,SAAS,CAAC,MAAM,EAAE,OAAO,GAAG,IAAI;IAMhC;;;;OAIG;IACH,aAAa,IAAI,IAAI;IAQrB,iEAAiE;IACjE,IAAI,IAAI,IAAI;IAQZ,+DAA+D;IACzD,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAI7B,OAAO,CAAC,YAAY;IAUpB,OAAO,CAAC,eAAe;IAQvB,0EAA0E;IAC1E,OAAO,CAAC,OAAO;YAID,QAAQ;CA2BvB"}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { accountChanged } from "./account-identity.js";
|
|
2
|
+
const DEFAULT_TIMER = {
|
|
3
|
+
setTimer: (fn, ms) => setTimeout(fn, ms),
|
|
4
|
+
clearTimer: (h) => clearTimeout(h),
|
|
5
|
+
};
|
|
6
|
+
/**
|
|
7
|
+
* Drives the four-trigger account-probe cadence. Probes are SERIALIZED onto one
|
|
8
|
+
* chain so sequence numbers are strictly monotonic and two triggers never race
|
|
9
|
+
* to emit an out-of-order identity. Call `start()` to fire the startup probe and
|
|
10
|
+
* arm the recurring timer, `setPaused()` on capacity transitions, and
|
|
11
|
+
* `onAuthFailure()` from the engine failure path.
|
|
12
|
+
*/
|
|
13
|
+
export class AccountProbeScheduler {
|
|
14
|
+
deps;
|
|
15
|
+
heartbeatMs;
|
|
16
|
+
pauseTickMs;
|
|
17
|
+
authFailureDebounceMs;
|
|
18
|
+
now;
|
|
19
|
+
timer;
|
|
20
|
+
started = false;
|
|
21
|
+
stopped = false;
|
|
22
|
+
paused = false;
|
|
23
|
+
recurringHandle = null;
|
|
24
|
+
seq = 0;
|
|
25
|
+
last = null;
|
|
26
|
+
lastAuthFailureAt = Number.NEGATIVE_INFINITY;
|
|
27
|
+
/** Serial probe chain: every probe awaits the prior one so `seq` stays ordered. */
|
|
28
|
+
tail = Promise.resolve();
|
|
29
|
+
constructor(deps) {
|
|
30
|
+
this.deps = deps;
|
|
31
|
+
this.heartbeatMs = Math.max(1, deps.heartbeatMs ?? 5 * 60_000);
|
|
32
|
+
this.pauseTickMs = Math.max(1, deps.pauseTickMs ?? 60_000);
|
|
33
|
+
this.authFailureDebounceMs = Math.max(0, deps.authFailureDebounceMs ?? 1_000);
|
|
34
|
+
this.now = deps.now ?? Date.now;
|
|
35
|
+
this.timer = deps.timer ?? DEFAULT_TIMER;
|
|
36
|
+
}
|
|
37
|
+
/** The last identity broadcast (null before the first probe). */
|
|
38
|
+
lastIdentity() {
|
|
39
|
+
return this.last;
|
|
40
|
+
}
|
|
41
|
+
/** Fire the startup probe and arm the recurring cadence timer. Idempotent. */
|
|
42
|
+
start() {
|
|
43
|
+
if (this.started || this.stopped)
|
|
44
|
+
return;
|
|
45
|
+
this.started = true;
|
|
46
|
+
this.enqueue("startup");
|
|
47
|
+
this.armRecurring();
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Note a capacity pause/resume transition. Re-arms the recurring timer to the
|
|
51
|
+
* matching cadence (~60s paused, ~5min healthy). A no-op when the state is
|
|
52
|
+
* unchanged, so repeated pause ticks don't reset the timer out from under
|
|
53
|
+
* themselves.
|
|
54
|
+
*/
|
|
55
|
+
setPaused(paused) {
|
|
56
|
+
if (this.stopped || paused === this.paused)
|
|
57
|
+
return;
|
|
58
|
+
this.paused = paused;
|
|
59
|
+
if (this.started)
|
|
60
|
+
this.armRecurring();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Probe immediately after a 401/403-class engine failure — the single most
|
|
64
|
+
* likely moment the underlying login just changed. Debounced so a burst of
|
|
65
|
+
* auth failures collapses to one probe.
|
|
66
|
+
*/
|
|
67
|
+
onAuthFailure() {
|
|
68
|
+
if (this.stopped || !this.started)
|
|
69
|
+
return;
|
|
70
|
+
const t = this.now();
|
|
71
|
+
if (t - this.lastAuthFailureAt < this.authFailureDebounceMs)
|
|
72
|
+
return;
|
|
73
|
+
this.lastAuthFailureAt = t;
|
|
74
|
+
this.enqueue("auth-failure");
|
|
75
|
+
}
|
|
76
|
+
/** Cancel the recurring timer; in-flight probes still settle. */
|
|
77
|
+
stop() {
|
|
78
|
+
this.stopped = true;
|
|
79
|
+
if (this.recurringHandle !== null) {
|
|
80
|
+
this.timer.clearTimer(this.recurringHandle);
|
|
81
|
+
this.recurringHandle = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/** Resolve once every queued probe has settled (test seam). */
|
|
85
|
+
async settle() {
|
|
86
|
+
await this.tail;
|
|
87
|
+
}
|
|
88
|
+
armRecurring() {
|
|
89
|
+
if (this.recurringHandle !== null) {
|
|
90
|
+
this.timer.clearTimer(this.recurringHandle);
|
|
91
|
+
this.recurringHandle = null;
|
|
92
|
+
}
|
|
93
|
+
if (this.stopped)
|
|
94
|
+
return;
|
|
95
|
+
const ms = this.paused ? this.pauseTickMs : this.heartbeatMs;
|
|
96
|
+
this.recurringHandle = this.timer.setTimer(() => this.onRecurringFire(), ms);
|
|
97
|
+
}
|
|
98
|
+
onRecurringFire() {
|
|
99
|
+
if (this.stopped)
|
|
100
|
+
return;
|
|
101
|
+
// Re-arm BEFORE the async probe so the cadence is decoupled from probe
|
|
102
|
+
// latency and stays deterministic under a fake clock.
|
|
103
|
+
this.armRecurring();
|
|
104
|
+
this.enqueue(this.paused ? "pause-tick" : "heartbeat");
|
|
105
|
+
}
|
|
106
|
+
/** Chain a probe onto the serial tail so sequence order is guaranteed. */
|
|
107
|
+
enqueue(trigger) {
|
|
108
|
+
this.tail = this.tail.then(() => this.runProbe(trigger));
|
|
109
|
+
}
|
|
110
|
+
async runProbe(trigger) {
|
|
111
|
+
let identity;
|
|
112
|
+
try {
|
|
113
|
+
identity = await this.deps.probe();
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
this.deps.onLog?.(`[acc-runner] account probe (${trigger}) failed: ${err?.message ?? String(err)}`);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const changed = accountChanged(this.last, identity);
|
|
120
|
+
const seq = ++this.seq;
|
|
121
|
+
const at = this.now();
|
|
122
|
+
if (changed && this.last) {
|
|
123
|
+
this.deps.onLog?.(`[acc-runner] account swap detected (${trigger}): ${this.last.synthetic_account} → ${identity.synthetic_account}`);
|
|
124
|
+
}
|
|
125
|
+
try {
|
|
126
|
+
await this.deps.emit({ identity, trigger, changed, seq, at });
|
|
127
|
+
}
|
|
128
|
+
catch (err) {
|
|
129
|
+
this.deps.onLog?.(`[acc-runner] account identity emit failed (${trigger}): ${err?.message ?? String(err)}`);
|
|
130
|
+
}
|
|
131
|
+
this.last = identity;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
//# sourceMappingURL=account-probe.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"account-probe.js","sourceRoot":"","sources":["../../src/engines/account-probe.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type ExecaChildProcess } from "execa";
|
|
2
2
|
import { type AnthropicAuthMode } from "../provider-auth.js";
|
|
3
3
|
import { type ParsedChatStreamEvent } from "./chat-stream.js";
|
|
4
|
-
import type { AccMcpToolBusFence, ConformantAgentEngine, EngineCapabilityMatrix, EngineStreamTurnOptions, RunnerCapacityClass } from "./types.js";
|
|
4
|
+
import type { AccMcpToolBusFence, ConformantAgentEngine, EngineCapabilityMatrix, EnginePlanHint, EngineStreamTurnOptions, RunnerCapacityClass } from "./types.js";
|
|
5
5
|
/**
|
|
6
6
|
* Native (built-in) claude tools disabled for a chat-lane turn. The tool bus is
|
|
7
7
|
* fenced to acc-mcp only, so none of claude's file/shell/web/task tools may run
|
|
@@ -67,6 +67,18 @@ export declare function parseChatStreamLine(line: string): ParsedChatStreamEvent
|
|
|
67
67
|
* otherwise overloaded (scheduler stops assigning turns).
|
|
68
68
|
*/
|
|
69
69
|
export declare function classifyCapacityFromLoad(load1: number, cores: number): RunnerCapacityClass;
|
|
70
|
+
/**
|
|
71
|
+
* TOLERANT parse of the claude config JSON into `{ email, plan }`. Reads
|
|
72
|
+
* `oauthAccount.emailAddress` for the email and, for the plan, the first of
|
|
73
|
+
* `oauthAccount.subscriptionType`, top-level `subscriptionType`, or
|
|
74
|
+
* `claudeAiOauth.subscriptionType` that resolves — matching the fields observed
|
|
75
|
+
* across CLI builds. Returns `{ email: null, plan: "unknown" }` for missing
|
|
76
|
+
* file / non-JSON / unexpected shape.
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseClaudeAccountConfig(raw: string | null): {
|
|
79
|
+
email: string | null;
|
|
80
|
+
plan: EnginePlanHint;
|
|
81
|
+
};
|
|
70
82
|
/** Static capability declaration for the claude-code chat reference build. */
|
|
71
83
|
export declare const CLAUDE_CODE_CHAT_CAPABILITIES: EngineCapabilityMatrix;
|
|
72
84
|
export interface ClaudeCodeChatDeps {
|
|
@@ -83,6 +95,8 @@ export interface ClaudeCodeChatDeps {
|
|
|
83
95
|
load1: number;
|
|
84
96
|
cores: number;
|
|
85
97
|
};
|
|
98
|
+
/** Read the claude config JSON for account introspection (default: reads `~/.claude.json`). */
|
|
99
|
+
readClaudeConfig?: (env?: NodeJS.ProcessEnv) => string | null;
|
|
86
100
|
}
|
|
87
101
|
/**
|
|
88
102
|
* Build a chat-conformant claude-code engine. Composes the FROZEN batch
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"claude-code-chat.d.ts","sourceRoot":"","sources":["../../src/engines/claude-code-chat.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"claude-code-chat.d.ts","sourceRoot":"","sources":["../../src/engines/claude-code-chat.ts"],"names":[],"mappings":"AAmCA,OAAO,EAAS,KAAK,iBAAiB,EAAE,MAAM,OAAO,CAAC;AACtD,OAAO,EAML,KAAK,iBAAiB,EACvB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAkB,KAAK,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAK9E,OAAO,KAAK,EACV,kBAAkB,EAClB,qBAAqB,EAErB,sBAAsB,EAGtB,cAAc,EAEd,uBAAuB,EACvB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAIpB;;;;;;;;GAQG;AACH,eAAO,MAAM,qBAAqB,EAAE,SAAS,MAAM,EAgBlD,CAAC;AAEF,mGAAmG;AACnG,eAAO,MAAM,4BAA4B,6BAA6B,CAAC;AAEvE;;;;;GAKG;AACH,wBAAgB,iBAAiB,IAAI,kBAAkB,CAEtD;AAED;;;;;;;;;;;;GAYG;AACH,wBAAgB,aAAa,CAAC,IAAI,GAAE;IAAE,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAA;CAAO,GAAG,MAAM,EAAE,CAmBtG;AAQD;;;;;;GAMG;AACH,wBAAgB,OAAO,CAAC,IAAI,EAAE,uBAAuB,GAAG,MAAM,CAAC,UAAU,CAKxE;AAgBD,YAAY,EAAE,qBAAqB,EAAE,CAAC;AAkBtC;;;;;;;;;GASG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,qBAAqB,GAAG,IAAI,CAoB9E;AASD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,mBAAmB,CAO1F;AAuDD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG;IAC5D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,cAAc,CAAC;CACtB,CAuBA;AAmED,8EAA8E;AAC9E,eAAO,MAAM,6BAA6B,EAAE,sBAO3C,CAAC;AAEF,MAAM,WAAW,kBAAkB;IACjC,uEAAuE;IACvE,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,uBAAuB,KAAK,iBAAiB,CAAC;IACjE,oEAAoE;IACpE,WAAW,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,CAAC,UAAU,KAAK,iBAAiB,CAAC;IAC5D,2IAA2I;IAC3I,qBAAqB,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IAC/C,uGAAuG;IACvG,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACxC,sFAAsF;IACtF,WAAW,CAAC,EAAE,MAAM;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC;IACrD,+FAA+F;IAC/F,gBAAgB,CAAC,EAAE,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,KAAK,MAAM,GAAG,IAAI,CAAC;CAC/D;AAaD;;;;;;GAMG;AACH,wBAAgB,0BAA0B,CAAC,IAAI,GAAE,kBAAuB,GAAG,qBAAqB,CAW/F;AAED,6DAA6D;AAC7D,eAAO,MAAM,oBAAoB,EAAE,qBAAoD,CAAC"}
|
|
@@ -31,11 +31,14 @@
|
|
|
31
31
|
* `claudeCodeChatEngine` is the wired default built from the production deps.
|
|
32
32
|
*/
|
|
33
33
|
import os from "node:os";
|
|
34
|
+
import fs from "node:fs";
|
|
35
|
+
import path from "node:path";
|
|
34
36
|
import { execa } from "execa";
|
|
35
|
-
import { resolveAuthMode, describeAuthMode, withAnthropicAuth, } from "../provider-auth.js";
|
|
37
|
+
import { resolveAuthMode, describeAuthMode, withAnthropicAuth, isolatedClaudeConfigDir, claudeConfigIsolationEnabled, } from "../provider-auth.js";
|
|
36
38
|
import { ACC_MCP_KEY } from "../mcp-spawn.js";
|
|
37
39
|
import { claudeCodeEngine } from "./claude-code.js";
|
|
38
40
|
import { streamChatTurn } from "./chat-stream.js";
|
|
41
|
+
import { resolvedAccountIdentity, unknownAccountIdentity, } from "./account-identity.js";
|
|
39
42
|
const BINARY = "claude";
|
|
40
43
|
/**
|
|
41
44
|
* Native (built-in) claude tools disabled for a chat-lane turn. The tool bus is
|
|
@@ -238,6 +241,118 @@ async function credentialProbeImpl(deps) {
|
|
|
238
241
|
detail: `${describeAuthMode(mode)}; repo-write ${repoWrite ? "granted (full tier)" : "absent (draft-only tier)"}`,
|
|
239
242
|
};
|
|
240
243
|
}
|
|
244
|
+
/* ------------------------------------------------------------------ *
|
|
245
|
+
* AS-1 (ACCOUNT-PROBE): claude-code account introspection.
|
|
246
|
+
*
|
|
247
|
+
* The claude CLI persists its logged-in account into the config JSON it
|
|
248
|
+
* maintains (`~/.claude.json`, or `<CLAUDE_CONFIG_DIR>/.claude.json` when the
|
|
249
|
+
* operator/isolation pins a config dir): an `oauthAccount.emailAddress` plus, on
|
|
250
|
+
* builds that store it, a subscription-type field. Reading that artifact IS the
|
|
251
|
+
* CLI's auth introspection — there is no stable `claude whoami` to spawn, and a
|
|
252
|
+
* file read is cheaper + can't be rate-limited. In api-key mode a bare key
|
|
253
|
+
* carries no email, so we honestly report `account_email: null` + `plan_hint:
|
|
254
|
+
* "api"`. The parse is TOLERANT (the codex tolerant-parser lesson): any shape
|
|
255
|
+
* drift degrades to `unknown`, never a throw.
|
|
256
|
+
* ------------------------------------------------------------------ */
|
|
257
|
+
/** Map a raw claude subscription-type string to the coarse plan hint. */
|
|
258
|
+
function planHintFromSubscription(raw) {
|
|
259
|
+
if (typeof raw !== "string")
|
|
260
|
+
return "unknown";
|
|
261
|
+
const v = raw.trim().toLowerCase();
|
|
262
|
+
if (v === "max" || v.startsWith("max"))
|
|
263
|
+
return "max";
|
|
264
|
+
if (v === "pro" || v.startsWith("pro"))
|
|
265
|
+
return "pro";
|
|
266
|
+
if (v === "free")
|
|
267
|
+
return "free";
|
|
268
|
+
if (v === "team")
|
|
269
|
+
return "team";
|
|
270
|
+
if (v === "enterprise" || v === "enterprise_plus")
|
|
271
|
+
return "enterprise";
|
|
272
|
+
return "unknown";
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* TOLERANT parse of the claude config JSON into `{ email, plan }`. Reads
|
|
276
|
+
* `oauthAccount.emailAddress` for the email and, for the plan, the first of
|
|
277
|
+
* `oauthAccount.subscriptionType`, top-level `subscriptionType`, or
|
|
278
|
+
* `claudeAiOauth.subscriptionType` that resolves — matching the fields observed
|
|
279
|
+
* across CLI builds. Returns `{ email: null, plan: "unknown" }` for missing
|
|
280
|
+
* file / non-JSON / unexpected shape.
|
|
281
|
+
*/
|
|
282
|
+
export function parseClaudeAccountConfig(raw) {
|
|
283
|
+
const s = (raw ?? "").trim();
|
|
284
|
+
if (!s.startsWith("{"))
|
|
285
|
+
return { email: null, plan: "unknown" };
|
|
286
|
+
let obj;
|
|
287
|
+
try {
|
|
288
|
+
obj = JSON.parse(s);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return { email: null, plan: "unknown" };
|
|
292
|
+
}
|
|
293
|
+
if (typeof obj !== "object" || obj === null)
|
|
294
|
+
return { email: null, plan: "unknown" };
|
|
295
|
+
const rec = obj;
|
|
296
|
+
const oauth = (typeof rec.oauthAccount === "object" && rec.oauthAccount !== null
|
|
297
|
+
? rec.oauthAccount
|
|
298
|
+
: {});
|
|
299
|
+
const claudeAi = (typeof rec.claudeAiOauth === "object" && rec.claudeAiOauth !== null
|
|
300
|
+
? rec.claudeAiOauth
|
|
301
|
+
: {});
|
|
302
|
+
const rawEmail = oauth.emailAddress ?? oauth.email ?? claudeAi.emailAddress;
|
|
303
|
+
const email = typeof rawEmail === "string" && rawEmail.trim() ? rawEmail.trim() : null;
|
|
304
|
+
const plan = planHintFromSubscription(oauth.subscriptionType ?? rec.subscriptionType ?? claudeAi.subscriptionType);
|
|
305
|
+
return { email, plan };
|
|
306
|
+
}
|
|
307
|
+
/** Resolve the config-JSON path the claude CLI is expected to maintain. */
|
|
308
|
+
function claudeConfigPath(env) {
|
|
309
|
+
const explicit = (env.CLAUDE_CONFIG_DIR ?? "").trim();
|
|
310
|
+
if (explicit)
|
|
311
|
+
return path.join(explicit, ".claude.json");
|
|
312
|
+
// Mirror provider-auth's isolation: when opted in, the CLI writes under the
|
|
313
|
+
// runner-owned config dir rather than $HOME.
|
|
314
|
+
if (claudeConfigIsolationEnabled(env)) {
|
|
315
|
+
return path.join(isolatedClaudeConfigDir(env), ".claude.json");
|
|
316
|
+
}
|
|
317
|
+
return path.join(os.homedir(), ".claude.json");
|
|
318
|
+
}
|
|
319
|
+
/** Default introspection: read the claude config JSON, or null when absent/unreadable. */
|
|
320
|
+
function defaultReadClaudeConfig(env = process.env) {
|
|
321
|
+
try {
|
|
322
|
+
return fs.readFileSync(claudeConfigPath(env), "utf8");
|
|
323
|
+
}
|
|
324
|
+
catch {
|
|
325
|
+
return null;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
async function accountIdentityImpl(deps) {
|
|
329
|
+
const channel = claudeCodeEngine.authMode; // "api-key-or-oauth"
|
|
330
|
+
const mode = deps.resolveAuth(process.env);
|
|
331
|
+
if (mode === "api-key") {
|
|
332
|
+
// A bare API key carries no email; report the billing channel honestly.
|
|
333
|
+
return unknownAccountIdentity({
|
|
334
|
+
engineId: claudeCodeEngine.id,
|
|
335
|
+
channel,
|
|
336
|
+
planHint: "api",
|
|
337
|
+
detail: "api-key channel (ANTHROPIC_API_KEY set); no consumer account email to introspect",
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
const { email, plan } = parseClaudeAccountConfig(deps.readClaudeConfig(process.env));
|
|
341
|
+
if (!email) {
|
|
342
|
+
return unknownAccountIdentity({
|
|
343
|
+
engineId: claudeCodeEngine.id,
|
|
344
|
+
channel,
|
|
345
|
+
detail: "interactive-session channel; no logged-in account found in the claude config",
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
return resolvedAccountIdentity({
|
|
349
|
+
engineId: claudeCodeEngine.id,
|
|
350
|
+
channel,
|
|
351
|
+
email,
|
|
352
|
+
planHint: plan,
|
|
353
|
+
detail: `interactive-session login ${email}${plan === "unknown" ? "" : ` (${plan})`}`,
|
|
354
|
+
});
|
|
355
|
+
}
|
|
241
356
|
/**
|
|
242
357
|
* Default repo-write probe: `gh auth status` exit 0. A companion runner that can
|
|
243
358
|
* push / open a PR authorises the `full` tier; one that only holds a model
|
|
@@ -268,6 +383,7 @@ const DEFAULT_DEPS = {
|
|
|
268
383
|
hasInteractiveSession: async () => true,
|
|
269
384
|
repoWriteProbe: defaultRepoWriteProbe,
|
|
270
385
|
loadSampler: () => ({ load1: os.loadavg()[0] ?? 0, cores: os.cpus().length }),
|
|
386
|
+
readClaudeConfig: defaultReadClaudeConfig,
|
|
271
387
|
};
|
|
272
388
|
/**
|
|
273
389
|
* Build a chat-conformant claude-code engine. Composes the FROZEN batch
|
|
@@ -285,6 +401,7 @@ export function createClaudeCodeChatEngine(deps = {}) {
|
|
|
285
401
|
accMcpToolBusOnly,
|
|
286
402
|
credentialProbe: () => credentialProbeImpl(d),
|
|
287
403
|
classifyCapacity: () => classifyCapacityImpl(d),
|
|
404
|
+
accountIdentity: () => accountIdentityImpl(d),
|
|
288
405
|
};
|
|
289
406
|
}
|
|
290
407
|
/** The wired default chat-conformant claude-code adapter. */
|