@indigoai-us/hq-cli 5.345.110 → 5.345.111
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/CHANGELOG.md +26 -0
- package/dist/command-catalog.generated.d.ts +17 -5
- package/dist/command-catalog.generated.js +20 -5
- package/dist/commands/capacity.d.ts +26 -0
- package/dist/commands/capacity.js +64 -2
- package/dist/commands/db-provision.js +31 -10
- package/dist/commands/db-remote-deps.js +12 -0
- package/dist/commands/db-sql.js +42 -9
- package/dist/commands/db-status.d.ts +2 -1
- package/dist/commands/db-status.js +33 -7
- package/dist/lib/bot/brief-job.d.ts +22 -0
- package/dist/lib/bot/brief-job.js +38 -0
- package/dist/lib/bot/run.d.ts +10 -0
- package/dist/lib/bot/run.js +54 -9
- package/dist/lib/capacity/collectors/claude-oauth.d.ts +225 -2
- package/dist/lib/capacity/collectors/claude-oauth.js +1053 -43
- package/dist/lib/capacity/ledger.d.ts +10 -1
- package/dist/lib/capacity/ledger.js +8 -1
- package/dist/lib/capacity/project.js +3 -0
- package/dist/lib/db/control-plane.d.ts +22 -0
- package/dist/lib/db/control-plane.js +50 -0
- package/dist/lib/db/remote-engine.d.ts +9 -1
- package/dist/lib/db/remote-engine.js +32 -4
- package/package.json +1 -1
package/dist/lib/bot/run.d.ts
CHANGED
|
@@ -53,6 +53,7 @@ import { containsNoReplySentinel, type ProgressPosterOptions } from "./progress.
|
|
|
53
53
|
import { type BotLogger } from "./log.js";
|
|
54
54
|
import { type CodingToolActivity, type DetectCodingToolActivityInput } from "./coding-tool-activity.js";
|
|
55
55
|
import { type BotRuntime, type RuntimeTurnInput, type RuntimeTurnResult } from "./runtime/index.js";
|
|
56
|
+
import { type BotSessionScope } from "./paths.js";
|
|
56
57
|
import type { VaultObjectDownloader } from "../agent-kit/attachment-download.js";
|
|
57
58
|
import { type HeartbeatAgentUidSource, type HeartbeatCredsSource, type HeartbeatIdentityMismatchReporter } from "../heartbeat-identity.js";
|
|
58
59
|
import { type BotState, type BotStatusFile } from "./status.js";
|
|
@@ -79,6 +80,8 @@ export declare function apiPollDelayMs(opts: {
|
|
|
79
80
|
}): number;
|
|
80
81
|
export declare const FAILURE_WINDOW_MS: number;
|
|
81
82
|
export declare const FAILURE_LIMIT = 5;
|
|
83
|
+
/** Resumed-session failures with the model CLI's unknown error, in a row, before the session is cleared. */
|
|
84
|
+
export declare const RESUMED_UNKNOWN_ERROR_LIMIT = 2;
|
|
82
85
|
export declare const MAX_ATTEMPTS_PER_MESSAGE = 3;
|
|
83
86
|
export declare const THINKING_EMOJI = "\uD83D\uDC40";
|
|
84
87
|
export declare const ROOM_THINKING_STATUS = "is thinking\u2026";
|
|
@@ -239,6 +242,13 @@ export interface BotHandle {
|
|
|
239
242
|
* An item that never reaches a model turn gets a lane of its own.
|
|
240
243
|
*/
|
|
241
244
|
export declare function inboxLaneKey(item: InboxItem, config: Pick<BotConfig, "ownerUid" | "agentUid" | "name">): string;
|
|
245
|
+
/**
|
|
246
|
+
* The session an owner DM turn runs in. A Create brief job the desktop app
|
|
247
|
+
* sent as a DM gets a fresh session of its own (one per run), so it neither
|
|
248
|
+
* replays the owner's conversation nor leaves its sweep in it; every other DM
|
|
249
|
+
* resumes the sender's session.
|
|
250
|
+
*/
|
|
251
|
+
export declare function dmTurnSessionScope(item: Pick<InboxItem, "fromPersonUid" | "text">, ownerUid: string): BotSessionScope;
|
|
242
252
|
export declare function preflightCreds(dir: string, config: BotConfig): string | null;
|
|
243
253
|
export declare function runBot(deps: BotRunDeps): Promise<BotHandle>;
|
|
244
254
|
/** True for a setup note the desktop app sent, not words the person typed. */
|
package/dist/lib/bot/run.js
CHANGED
|
@@ -72,6 +72,7 @@ import { setupKeptHereNote, setupOfferAnsweredInWordsNote, setupToolOfferAnswer,
|
|
|
72
72
|
import { FALLBACK_CLAUDE_PERMISSION_MODE, isClaudeBypassDisabled, onPath } from "./runtime/claude.js";
|
|
73
73
|
import { runRuntimeTurn, RuntimeError } from "./runtime/index.js";
|
|
74
74
|
import { botAttachmentsDir, dmSessionScope, isDmSessionScope } from "./paths.js";
|
|
75
|
+
import { briefJobRunId, briefJobSessionScope, isBriefJobSessionScope } from "./brief-job.js";
|
|
75
76
|
import { appendDmHistory, dmHistoryHasModelThread, markDmHistoryModelTurn, readDmHistory, withRecentDmHistory } from "./dm-history.js";
|
|
76
77
|
import { attachmentTurnBlock, downloadTurnAttachments } from "./attachments.js";
|
|
77
78
|
import { inboxEntryAttachments } from "../agent-kit/inbox-state.js";
|
|
@@ -122,6 +123,10 @@ export function apiPollDelayMs(opts) {
|
|
|
122
123
|
}
|
|
123
124
|
export const FAILURE_WINDOW_MS = 10 * 60 * 1000;
|
|
124
125
|
export const FAILURE_LIMIT = 5;
|
|
126
|
+
/** Resumed-session failures with the model CLI's unknown error, in a row, before the session is cleared. */
|
|
127
|
+
export const RESUMED_UNKNOWN_ERROR_LIMIT = 2;
|
|
128
|
+
/** The model CLI's own message, exactly; "Unexpected token" in a stack trace is not it. */
|
|
129
|
+
const RESUMED_UNKNOWN_ERROR = /an unknown error occurred \(unexpected\)/i;
|
|
125
130
|
export const MAX_ATTEMPTS_PER_MESSAGE = 3;
|
|
126
131
|
export const THINKING_EMOJI = "👀";
|
|
127
132
|
export const ROOM_THINKING_STATUS = "is thinking…";
|
|
@@ -238,12 +243,23 @@ export const RESTARTED_REASON = "I was restarted while I was working on it";
|
|
|
238
243
|
*/
|
|
239
244
|
export function inboxLaneKey(item, config) {
|
|
240
245
|
if (isDmItem(item))
|
|
241
|
-
return
|
|
246
|
+
return dmTurnSessionScope(item, config.ownerUid);
|
|
242
247
|
const target = resolveReplyTarget(item, config);
|
|
243
248
|
if (target?.kind === "room" || target?.kind === "job")
|
|
244
249
|
return target.sessionScope;
|
|
245
250
|
return `item:${item.messageId}`;
|
|
246
251
|
}
|
|
252
|
+
/**
|
|
253
|
+
* The session an owner DM turn runs in. A Create brief job the desktop app
|
|
254
|
+
* sent as a DM gets a fresh session of its own (one per run), so it neither
|
|
255
|
+
* replays the owner's conversation nor leaves its sweep in it; every other DM
|
|
256
|
+
* resumes the sender's session.
|
|
257
|
+
*/
|
|
258
|
+
export function dmTurnSessionScope(item, ownerUid) {
|
|
259
|
+
const sender = item.fromPersonUid ?? "";
|
|
260
|
+
const runId = sender === ownerUid ? briefJobRunId(item.text) : null;
|
|
261
|
+
return runId ? briefJobSessionScope(runId) : dmSessionScope(sender, ownerUid);
|
|
262
|
+
}
|
|
247
263
|
class FailureWindow {
|
|
248
264
|
windowMs;
|
|
249
265
|
limit;
|
|
@@ -942,6 +958,7 @@ export async function runBot(deps) {
|
|
|
942
958
|
markSignInWorks();
|
|
943
959
|
// A good turn: the last recorded failure is no longer the bot's state.
|
|
944
960
|
setStatus({ recentFailures: failures.count(now().getTime()), lastError: undefined });
|
|
961
|
+
resumedUnknownErrors.delete(sessionScope);
|
|
945
962
|
if (turn.modelFallback) {
|
|
946
963
|
const { from, to } = turn.modelFallback;
|
|
947
964
|
log("warn", `${runtime.id} on this computer is too old for model ${from || "(its configured default)"}; this turn ran on ${to} instead. Update ${runtime.id} to use ${from || "that model"} again`);
|
|
@@ -951,7 +968,8 @@ export async function runBot(deps) {
|
|
|
951
968
|
setStatus({ runtimeModelFallback: null });
|
|
952
969
|
}
|
|
953
970
|
log("info", `model turn done in ${Math.round((now().getTime() - tTurnStart) / 1000)}s (${Math.round((tTurnStart - tReceived) / 1000)}s before it)`);
|
|
954
|
-
|
|
971
|
+
// A brief job's session is one run long; nothing resumes it.
|
|
972
|
+
if (turn.sessionId && !isBriefJobSessionScope(sessionScope)) {
|
|
955
973
|
writeBotSession(dir, {
|
|
956
974
|
v: 1,
|
|
957
975
|
runtime: config.runtime,
|
|
@@ -983,16 +1001,37 @@ export async function runBot(deps) {
|
|
|
983
1001
|
}
|
|
984
1002
|
};
|
|
985
1003
|
/** Failure bookkeeping shared by every model turn: window, health, status, log, stale session. */
|
|
1004
|
+
/** Consecutive resumed-session failures with the model CLI's unknown error, per scope. */
|
|
1005
|
+
const resumedUnknownErrors = new Map();
|
|
986
1006
|
const recordTurnFailure = (err, sessionScope) => {
|
|
987
1007
|
const excerpt = err instanceof RuntimeError ? err.stderrExcerpt : err instanceof Error ? err.message : String(err);
|
|
988
1008
|
const count = failures.record(now().getTime());
|
|
989
1009
|
modelHealth = count >= FAILURE_LIMIT ? "failed" : "degraded";
|
|
990
1010
|
setStatus({ restarts: (readStatus()?.restarts ?? 0) + 1, recentFailures: count, lastError: excerpt.slice(0, 400) });
|
|
991
1011
|
log("error", `model turn failed (${count}/${FAILURE_LIMIT} in window, ${classifyRuntimeError(err)}): ${excerpt}`);
|
|
992
|
-
// A resumed session that no longer exists must not poison every turn
|
|
1012
|
+
// A resumed session that no longer exists must not poison every turn: the
|
|
1013
|
+
// next turn starts fresh with the recent DM history prepended. The model
|
|
1014
|
+
// CLI's "An unknown error occurred (Unexpected)" (every turn of a
|
|
1015
|
+
// long-lived process, 2026-10-12) is not a session error on its face, so a
|
|
1016
|
+
// single one never clears anything; the session is cleared only once that
|
|
1017
|
+
// error repeats on the same resumed session (RESUMED_UNKNOWN_ERROR_LIMIT
|
|
1018
|
+
// in a row, the count reset by any good turn in that scope).
|
|
993
1019
|
const resumed = Boolean(err?.resumedSession);
|
|
994
|
-
if (resumed && /session|resume|not found/i.test(excerpt))
|
|
1020
|
+
if (resumed && /session|resume|not found/i.test(excerpt)) {
|
|
995
1021
|
clearBotSession(dir, sessionScope);
|
|
1022
|
+
resumedUnknownErrors.delete(sessionScope);
|
|
1023
|
+
}
|
|
1024
|
+
else if (resumed && RESUMED_UNKNOWN_ERROR.test(excerpt)) {
|
|
1025
|
+
const streak = (resumedUnknownErrors.get(sessionScope) ?? 0) + 1;
|
|
1026
|
+
if (streak >= RESUMED_UNKNOWN_ERROR_LIMIT) {
|
|
1027
|
+
log("warn", `resumed session in ${sessionScope} failed ${streak} times in a row with the model CLI's unknown error; starting fresh next turn`);
|
|
1028
|
+
clearBotSession(dir, sessionScope);
|
|
1029
|
+
resumedUnknownErrors.delete(sessionScope);
|
|
1030
|
+
}
|
|
1031
|
+
else {
|
|
1032
|
+
resumedUnknownErrors.set(sessionScope, streak);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
996
1035
|
return { excerpt, count };
|
|
997
1036
|
};
|
|
998
1037
|
/**
|
|
@@ -1953,9 +1992,10 @@ export async function runBot(deps) {
|
|
|
1953
1992
|
const block = attachmentTurnBlock(files);
|
|
1954
1993
|
text = text ? `${text}\n\n${block}` : block;
|
|
1955
1994
|
}
|
|
1956
|
-
const sessionScope = target.kind === "room" ? target.sessionScope :
|
|
1995
|
+
const sessionScope = target.kind === "room" ? target.sessionScope : dmTurnSessionScope(item, config.ownerUid);
|
|
1957
1996
|
if (target.kind === "dm") {
|
|
1958
|
-
|
|
1997
|
+
// The owner's history still records that the job was sent.
|
|
1998
|
+
rememberDm(dmSessionScope(target.peerUid, config.ownerUid), {
|
|
1959
1999
|
role: "user",
|
|
1960
2000
|
from: (item.fromDisplayName ?? "").trim() || item.fromPersonUid || "someone",
|
|
1961
2001
|
...(item.fromPersonUid ? { fromPersonUid: item.fromPersonUid } : {}),
|
|
@@ -2077,7 +2117,9 @@ export async function runBot(deps) {
|
|
|
2077
2117
|
clearInflight(dir, id);
|
|
2078
2118
|
attempts.delete(id);
|
|
2079
2119
|
markSignInBroken(outcome.excerpt);
|
|
2080
|
-
|
|
2120
|
+
// One notice per DM conversation: a brief job's scope is per run, so
|
|
2121
|
+
// the notice keys on the peer's DM scope, not the turn's scope.
|
|
2122
|
+
const conversation = markerTarget.kind === "dm" ? dmSessionScope(markerTarget.peerUid, config.ownerUid) : `room:${markerTarget.channelId}:${item.rootEventId ?? ""}`;
|
|
2081
2123
|
await tellSignInNeeded(conversation, (notice) => postRepair(markerTarget, notice, item.rootEventId), false, item.eventId ?? id);
|
|
2082
2124
|
log("warn", `kept ${id} until ${runtime.id} can sign in again`);
|
|
2083
2125
|
return; // un-acked: answered once the sign-in works
|
|
@@ -2191,8 +2233,11 @@ export async function runBot(deps) {
|
|
|
2191
2233
|
const pending = new Set();
|
|
2192
2234
|
const running = new Set();
|
|
2193
2235
|
const canStart = () => !stopping && state === "running" && !hasPromotionHold(dir);
|
|
2194
|
-
/**
|
|
2195
|
-
|
|
2236
|
+
/**
|
|
2237
|
+
* Job turns yield to people: one at a time, and never the last free slot.
|
|
2238
|
+
* A brief job is a person's click, so it is scheduled like the owner's DM.
|
|
2239
|
+
*/
|
|
2240
|
+
const isJobLane = (lane) => lane.startsWith("job:") && !isBriefJobSessionScope(lane);
|
|
2196
2241
|
const jobLaneBudget = Math.max(1, maxParallelTurns - 1);
|
|
2197
2242
|
const canStartJobLane = () => [...busyLanes].filter(isJobLane).length < MAX_PARALLEL_JOB_TURNS && busyLanes.size < jobLaneBudget;
|
|
2198
2243
|
const pump = () => {
|
|
@@ -5,10 +5,26 @@
|
|
|
5
5
|
* `Claude Code-credentials`, plus `Claude Code-credentials-<hash>` seats,
|
|
6
6
|
* or the credentials JSON under ~/.claude/ on Linux). GETs
|
|
7
7
|
* https://api.anthropic.com/api/oauth/usage with the oauth-2025-04-20 beta
|
|
8
|
-
* header. This module does not
|
|
9
|
-
* ~/.claude, call an OAuth token endpoint, or invoke the claude CLI except
|
|
8
|
+
* header. This module does not invoke the claude CLI except
|
|
10
9
|
* `claude --version` when the caller does not pass cliVersion.
|
|
11
10
|
*
|
|
11
|
+
* US-014 wires in a token-refresh gate. The gate uses two seams,
|
|
12
|
+
* `defaultListLiveLanes` and `defaultListProcesses`, to decide whether a
|
|
13
|
+
* refresh is safe (no running Claude Code holds the home). Both seams
|
|
14
|
+
* FAIL CLOSED: an error, a timeout, or an unexpected JSON shape returns an
|
|
15
|
+
* error-signalling throw and the caller records `skipped-live` /
|
|
16
|
+
* `skipped-process` and does NOT rotate the refresh token.
|
|
17
|
+
*
|
|
18
|
+
* Residual blind spot on darwin: `ps -axo pid=,command=` cannot read a
|
|
19
|
+
* child process's environment, so on darwin a running `claude` process is
|
|
20
|
+
* conservatively treated as holding the DEFAULT home AND blocking any
|
|
21
|
+
* non-default home refresh. A sibling that sets `CLAUDE_CONFIG_DIR` to a
|
|
22
|
+
* different home still blocks the gate; this is the safe side of the
|
|
23
|
+
* precision/safety tradeoff. On linux `/proc/<pid>/environ` lets the gate
|
|
24
|
+
* read the per-process `CLAUDE_CONFIG_DIR`, so it only blocks the matching
|
|
25
|
+
* home. Windows and other platforms get no process gate; the lane gate is
|
|
26
|
+
* authoritative there.
|
|
27
|
+
*
|
|
12
28
|
* The legacy shape (seven_day_opus populated, no limits[]) is drift.
|
|
13
29
|
* observedAt is the response time. One collect appends every account's
|
|
14
30
|
* readings and failures in one rebuild.
|
|
@@ -30,6 +46,17 @@ export declare const CLAUDE_OAUTH_FETCH_TIMEOUT_MS = 10000;
|
|
|
30
46
|
export declare const CLAUDE_OAUTH_SECURITY_TIMEOUT_MS = 5000;
|
|
31
47
|
export declare const CLAUDE_OAUTH_POLL_FILE = "claude-oauth-last-call.json";
|
|
32
48
|
export declare const CLAUDE_KEYCHAIN_SERVICE = "Claude Code-credentials";
|
|
49
|
+
/** Claude Code's refresh endpoint. The URL and the client_id are the ones Claude Code itself posts. */
|
|
50
|
+
export declare const CLAUDE_OAUTH_REFRESH_URL = "https://console.anthropic.com/v1/oauth/token";
|
|
51
|
+
export declare const CLAUDE_OAUTH_REFRESH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
|
|
52
|
+
/** Minimum gap between refresh attempts for one home when the previous attempt did not revoke. */
|
|
53
|
+
export declare const CLAUDE_OAUTH_REFRESH_BACKOFF_MS: number;
|
|
54
|
+
/** Minimum gap between refresh attempts for one home after an invalid_grant. */
|
|
55
|
+
export declare const CLAUDE_OAUTH_REFRESH_REVOKED_BACKOFF_MS: number;
|
|
56
|
+
/** Try a refresh when the token is this close to expiry, so one tick does not land on a stale token. */
|
|
57
|
+
export declare const CLAUDE_OAUTH_REFRESH_PREFLIGHT_MS: number;
|
|
58
|
+
export declare const CLAUDE_OAUTH_REFRESH_TIMEOUT_MS = 10000;
|
|
59
|
+
export declare const CLAUDE_OAUTH_REFRESH_LAST_FILE = "claude-oauth-refresh-last.json";
|
|
33
60
|
export interface ClaudeOAuthFetchInit {
|
|
34
61
|
method: "GET";
|
|
35
62
|
headers: Record<string, string>;
|
|
@@ -43,6 +70,21 @@ export type ClaudeOAuthFetch = (url: string, init: ClaudeOAuthFetchInit) => Prom
|
|
|
43
70
|
get(name: string): string | null;
|
|
44
71
|
};
|
|
45
72
|
}>;
|
|
73
|
+
export interface ClaudeOAuthRefreshFetchInit {
|
|
74
|
+
method: "POST";
|
|
75
|
+
headers: Record<string, string>;
|
|
76
|
+
body: string;
|
|
77
|
+
redirect: "manual";
|
|
78
|
+
signal?: AbortSignal;
|
|
79
|
+
}
|
|
80
|
+
/** Separate seam from the usage fetch: POST + body, same response shape. */
|
|
81
|
+
export type ClaudeOAuthRefreshFetch = (url: string, init: ClaudeOAuthRefreshFetchInit) => Promise<{
|
|
82
|
+
status: number;
|
|
83
|
+
text: () => Promise<string>;
|
|
84
|
+
headers?: {
|
|
85
|
+
get(name: string): string | null;
|
|
86
|
+
};
|
|
87
|
+
}>;
|
|
46
88
|
export interface SecurityExecResult {
|
|
47
89
|
status: number | null;
|
|
48
90
|
stdout: string;
|
|
@@ -58,12 +100,77 @@ export interface ClaudeCredentialAccount {
|
|
|
58
100
|
expiresAt?: number | null;
|
|
59
101
|
scopes?: readonly string[] | null;
|
|
60
102
|
planType?: string | null;
|
|
103
|
+
/** Opaque refresh token. Never logged, never surfaced. Passed to writeCredentials verbatim. */
|
|
104
|
+
refreshToken?: string | null;
|
|
105
|
+
/** Full parsed credential JSON. Preserved so writeCredentials can round-trip mcpOAuth and siblings. */
|
|
106
|
+
credentialDocument?: Record<string, unknown> | null;
|
|
107
|
+
/** The config dir this credential belongs to; needed to resolve the Keychain service or file path. */
|
|
108
|
+
home?: string | null;
|
|
61
109
|
}
|
|
62
110
|
export interface ReadClaudeCredentialsResult {
|
|
63
111
|
keychain: ClaudeKeychainStatus;
|
|
64
112
|
accounts: ClaudeCredentialAccount[];
|
|
65
113
|
}
|
|
66
114
|
export type ReadClaudeCredentials = () => ReadClaudeCredentialsResult;
|
|
115
|
+
export interface WriteClaudeCredentialsRequest {
|
|
116
|
+
home: string;
|
|
117
|
+
/** macOS Keychain service name; empty on non-darwin. */
|
|
118
|
+
service: string;
|
|
119
|
+
platform: NodeJS.Platform;
|
|
120
|
+
/** The full new credential JSON object. The caller preserved sibling keys. */
|
|
121
|
+
document: Record<string, unknown>;
|
|
122
|
+
/** The refreshToken we read before POSTing to the token endpoint. Used to detect rotation races. */
|
|
123
|
+
previousRefreshToken: string | null;
|
|
124
|
+
}
|
|
125
|
+
export interface WriteClaudeCredentialsResult {
|
|
126
|
+
ok: boolean;
|
|
127
|
+
/** True when the on-disk refresh token no longer matches `previousRefreshToken`. */
|
|
128
|
+
raceDetected?: boolean;
|
|
129
|
+
invalidGrant?: boolean;
|
|
130
|
+
/** Human-readable, already scrubbed of tokens. */
|
|
131
|
+
error?: string;
|
|
132
|
+
}
|
|
133
|
+
export type WriteClaudeCredentials = (request: WriteClaudeCredentialsRequest) => Promise<WriteClaudeCredentialsResult>;
|
|
134
|
+
/**
|
|
135
|
+
* One row per lane the gate must consider.
|
|
136
|
+
* - `isLive` is true for state ∈ {queued, running, awaiting_input}: a running
|
|
137
|
+
* Claude Code may hold the home; a refresh would race and get invalid_grant.
|
|
138
|
+
* - `isAuthFailed` is true when the lane terminated with reason
|
|
139
|
+
* `provider_auth_failed` (src/lib/lanes/watcher.ts classifies 401/403 that
|
|
140
|
+
* way). The gate treats this as proof the account is already signed out and
|
|
141
|
+
* records revoked without hitting the token endpoint.
|
|
142
|
+
* - `home` is the resolved per-host provider home for the account; the gate
|
|
143
|
+
* compares it to the home being refreshed with `path.resolve` so symlinks
|
|
144
|
+
* do not slip through. `null` is accepted only when the row is not live.
|
|
145
|
+
*/
|
|
146
|
+
export interface LiveLane {
|
|
147
|
+
home: string | null;
|
|
148
|
+
account: string | null;
|
|
149
|
+
provider: string;
|
|
150
|
+
state: string;
|
|
151
|
+
reason: string | null;
|
|
152
|
+
isLive: boolean;
|
|
153
|
+
isAuthFailed: boolean;
|
|
154
|
+
}
|
|
155
|
+
/** Fail-closed: throw on error/timeout/unexpected shape; the caller skips refresh. */
|
|
156
|
+
export type ListLiveLanes = () => Promise<readonly LiveLane[]>;
|
|
157
|
+
/**
|
|
158
|
+
* One row per Claude Code process the gate must consider.
|
|
159
|
+
* - `probe === "env"` means the probe read `/proc/<pid>/environ` and the
|
|
160
|
+
* `env.CLAUDE_CONFIG_DIR` field is authoritative (undefined means default).
|
|
161
|
+
* - `probe === "command-only"` means we only saw the command line (darwin
|
|
162
|
+
* `ps -axo pid=,command=`), with no visibility into the process env. The
|
|
163
|
+
* gate treats such a row as a conservative blocker for every home.
|
|
164
|
+
*/
|
|
165
|
+
export interface ClaudeProcess {
|
|
166
|
+
pid: number;
|
|
167
|
+
env: {
|
|
168
|
+
CLAUDE_CONFIG_DIR?: string;
|
|
169
|
+
};
|
|
170
|
+
probe: "env" | "command-only";
|
|
171
|
+
}
|
|
172
|
+
/** Fail-closed: throw on error/timeout; the caller skips refresh. */
|
|
173
|
+
export type ListProcesses = () => Promise<readonly ClaudeProcess[]>;
|
|
67
174
|
export interface ClaudeOAuthCollectResult {
|
|
68
175
|
readings: ReadingInput[];
|
|
69
176
|
failures: ParserFailureEvent[];
|
|
@@ -99,6 +206,20 @@ export interface CollectClaudeOAuthOptions {
|
|
|
99
206
|
}[];
|
|
100
207
|
/** One-shot collect reads while the resident collector stays disabled. */
|
|
101
208
|
allowDisabled?: boolean;
|
|
209
|
+
/**
|
|
210
|
+
* Writes a refreshed credential back to disk / Keychain. Default implementation uses
|
|
211
|
+
* `security add-generic-password -U` on darwin and tmp+rename at mode 0600 on linux,
|
|
212
|
+
* preserving all sibling fields (including `mcpOAuth`).
|
|
213
|
+
*/
|
|
214
|
+
writeCredentials?: WriteClaudeCredentials;
|
|
215
|
+
/** Returns homes that live lanes are using. Default implementation reads `hq lanes list --json`. */
|
|
216
|
+
listLiveLanes?: ListLiveLanes;
|
|
217
|
+
/** Returns processes whose `CLAUDE_CONFIG_DIR` identifies a home. Default reads `ps -ewwo pid,command`. */
|
|
218
|
+
listProcesses?: ListProcesses;
|
|
219
|
+
/** Overrides the Claude Code refresh endpoint. Tests set this to a 127.0.0.1 stub. */
|
|
220
|
+
tokenEndpoint?: string;
|
|
221
|
+
/** Overrides the POST transport for the refresh. Default wraps `fetch` with a 10s abort. */
|
|
222
|
+
refreshFetch?: ClaudeOAuthRefreshFetch;
|
|
102
223
|
}
|
|
103
224
|
interface MapOptions {
|
|
104
225
|
observedAt: string;
|
|
@@ -141,5 +262,107 @@ export declare function claudeSeatAccountId(seat: string, orgId: string | null):
|
|
|
141
262
|
export declare function readInstalledClaudeVersion(readVersion?: () => string): string;
|
|
142
263
|
/** Async `claude --version` for the daemon. The sync reader stays for one-shot callers. */
|
|
143
264
|
export declare function readInstalledClaudeVersionAsync(): Promise<string>;
|
|
265
|
+
/** Per-home record of the last refresh attempt. */
|
|
266
|
+
export interface ClaudeRefreshLastEntry {
|
|
267
|
+
attemptedAt: string;
|
|
268
|
+
outcome: "refreshed" | "revoked" | "error" | "skipped-race" | "skipped-live" | "skipped-live-check-failed" | "skipped-process" | "skipped-process-check-failed" | "skipped-backoff" | "skipped-revoked";
|
|
269
|
+
revokedRefreshTokenFingerprint?: string;
|
|
270
|
+
/** Short, token-free diagnostic (never a response body). */
|
|
271
|
+
errorMessage?: string;
|
|
272
|
+
/** Fixed classification string for refresh errors; no response body is stored anywhere. */
|
|
273
|
+
errorClass?: "invalid_grant" | "revoked" | "server_error" | "timeout" | "network" | "shape";
|
|
274
|
+
httpStatus?: number | null;
|
|
275
|
+
backoffUntil?: string;
|
|
276
|
+
/** ISO expiry of the token the entry describes. Set on a successful refresh. */
|
|
277
|
+
tokenExpiresAt?: string;
|
|
278
|
+
}
|
|
279
|
+
export interface RevokedAccountEntry {
|
|
280
|
+
at: string;
|
|
281
|
+
fingerprint?: string;
|
|
282
|
+
}
|
|
283
|
+
export declare function claudeOAuthRefreshLastPath(root: string): string;
|
|
284
|
+
/** Not the token: a 16 char sha256 prefix. */
|
|
285
|
+
export declare function tokenFingerprint(token: string): string;
|
|
286
|
+
/** Reads the last refresh outcome for a home. Exposed for the doctor command. */
|
|
287
|
+
export declare function readClaudeRefreshLast(root: string, home: string): ClaudeRefreshLastEntry | null;
|
|
288
|
+
/** @deprecated prefer {@link revokedMarkerForCurrentCredential}; this is the pre-fingerprint shim. */
|
|
289
|
+
export declare function isAccountRevokedPersisted(root: string, accountId: string): boolean;
|
|
290
|
+
/**
|
|
291
|
+
* Compare the persisted revoked marker (if any) against the refresh token the
|
|
292
|
+
* caller is about to use.
|
|
293
|
+
*
|
|
294
|
+
* - `"match"`: the marker was written for this exact refresh token. The caller
|
|
295
|
+
* must still block (nothing has changed since the account was marked signed
|
|
296
|
+
* out).
|
|
297
|
+
* - `"stale"`: a marker exists but the stored fingerprint does not match the
|
|
298
|
+
* current refresh token, meaning the human signed in again and the token
|
|
299
|
+
* rotated. The caller should clear the marker and proceed; a fresh 401 will
|
|
300
|
+
* re-mark it.
|
|
301
|
+
* - `"none"`: no marker is persisted.
|
|
302
|
+
*
|
|
303
|
+
* A marker written by an older version carries no fingerprint. We treat that
|
|
304
|
+
* as `"stale"` so a current sign-in can recover; if the token really is still
|
|
305
|
+
* revoked the usage fetch will set a fingerprinted marker on the next 401.
|
|
306
|
+
*/
|
|
307
|
+
export declare function revokedMarkerForCurrentCredential(root: string, accountId: string, refreshToken: string | null | undefined): "match" | "stale" | "none";
|
|
308
|
+
/**
|
|
309
|
+
* Shape of a single lane row emitted by `hq lanes list --json`.
|
|
310
|
+
* See src/commands/lanes.ts (withoutProviderHome strips `provider_home_at_spawn`
|
|
311
|
+
* from CLI output; we must consult the accounts registry to map
|
|
312
|
+
* account id -> effective_home).
|
|
313
|
+
*
|
|
314
|
+
* Note: the lane watcher writes a provider-auth-failed signal to the
|
|
315
|
+
* `terminal_reason` field (watcher.ts:~729). The `terminal_details` record
|
|
316
|
+
* carries the structured `account` and `provider` of the rejection
|
|
317
|
+
* (provider-rejection.ts:providerRejectionDetails). Lane records never
|
|
318
|
+
* surface a top-level `reason` for the auth-failed state, so this is the
|
|
319
|
+
* only shape we read.
|
|
320
|
+
*/
|
|
321
|
+
interface RawLaneRow {
|
|
322
|
+
state?: unknown;
|
|
323
|
+
account?: unknown;
|
|
324
|
+
provider?: unknown;
|
|
325
|
+
terminal_reason?: unknown;
|
|
326
|
+
terminal_details?: unknown;
|
|
327
|
+
}
|
|
328
|
+
interface RawAccountRow {
|
|
329
|
+
id?: unknown;
|
|
330
|
+
home?: unknown;
|
|
331
|
+
effective_home?: unknown;
|
|
332
|
+
auth_state?: unknown;
|
|
333
|
+
provider?: unknown;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Parse `hq lanes list --json` payload into typed rows.
|
|
337
|
+
* Exported for tests; fail-closed semantics live in defaultListLiveLanes.
|
|
338
|
+
*/
|
|
339
|
+
export declare function parseLanesListPayload(payload: unknown): readonly RawLaneRow[];
|
|
340
|
+
/**
|
|
341
|
+
* Parse `hq lanes accounts list --json` payload into typed rows.
|
|
342
|
+
* Exported for tests; fail-closed semantics live in defaultListLiveLanes.
|
|
343
|
+
*/
|
|
344
|
+
export declare function parseAccountsListPayload(payload: unknown): readonly RawAccountRow[];
|
|
345
|
+
/**
|
|
346
|
+
* Join lanes rows against accounts rows to produce LiveLane records.
|
|
347
|
+
* Exported for tests.
|
|
348
|
+
*/
|
|
349
|
+
export declare function buildLiveLanesFromPayloads(lanes: readonly RawLaneRow[], accounts: readonly RawAccountRow[]): readonly LiveLane[];
|
|
350
|
+
/**
|
|
351
|
+
* Parse `ps -axo pid=,command=` into rows that mention the `claude` executable.
|
|
352
|
+
* Exported for tests. Returned rows carry `probe: "command-only"` so the gate
|
|
353
|
+
* knows it cannot read per-process CLAUDE_CONFIG_DIR on darwin.
|
|
354
|
+
*/
|
|
355
|
+
export declare function parsePsClaudeOutput(stdout: string): readonly ClaudeProcess[];
|
|
356
|
+
export interface ClaudeTokenRefreshResult {
|
|
357
|
+
outcome: "refreshed" | "revoked" | "error" | "skipped-race" | "skipped-live" | "skipped-live-check-failed" | "skipped-process" | "skipped-process-check-failed" | "skipped-backoff" | "skipped-revoked";
|
|
358
|
+
newAccessToken?: string;
|
|
359
|
+
newExpiresAt?: number;
|
|
360
|
+
newRefreshToken?: string;
|
|
361
|
+
newScopes?: readonly string[] | null;
|
|
362
|
+
errorMessage?: string;
|
|
363
|
+
errorClass?: ClaudeRefreshErrorClass;
|
|
364
|
+
httpStatus?: number | null;
|
|
365
|
+
}
|
|
366
|
+
export type ClaudeRefreshErrorClass = "invalid_grant" | "revoked" | "server_error" | "timeout" | "network" | "shape";
|
|
144
367
|
export {};
|
|
145
368
|
//# sourceMappingURL=claude-oauth.d.ts.map
|