@integrity-labs/agt-cli 0.28.594 → 0.28.596
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/bin/agt.js +7 -5
- package/dist/bin/agt.js.map +1 -1
- package/dist/{chunk-74UJCPIS.js → chunk-3PNRUCHD.js} +10295 -14614
- package/dist/chunk-3PNRUCHD.js.map +1 -0
- package/dist/chunk-6NVRWZ5W.js +4352 -0
- package/dist/chunk-6NVRWZ5W.js.map +1 -0
- package/dist/{chunk-U7DHWGAZ.js → chunk-EAZQXZ4Q.js} +44 -18
- package/dist/chunk-EAZQXZ4Q.js.map +1 -0
- package/dist/{claude-pair-runtime-CGVDKWFC.js → claude-pair-runtime-5WB2FMIX.js} +2 -2
- package/dist/lib/manager-worker.js +73 -58
- package/dist/lib/manager-worker.js.map +1 -1
- package/dist/{persistent-session-YRCF7AAC.js → persistent-session-BGMQQYW2.js} +3 -2
- package/dist/{responsiveness-probe-MFWXIRTJ.js → responsiveness-probe-VAGPO5FZ.js} +3 -2
- package/dist/{responsiveness-probe-MFWXIRTJ.js.map → responsiveness-probe-VAGPO5FZ.js.map} +1 -1
- package/dist/session-auth-dead-CWFGB472.js +206 -0
- package/dist/session-auth-dead-CWFGB472.js.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-74UJCPIS.js.map +0 -1
- package/dist/chunk-U7DHWGAZ.js.map +0 -1
- /package/dist/{claude-pair-runtime-CGVDKWFC.js.map → claude-pair-runtime-5WB2FMIX.js.map} +0 -0
- /package/dist/{persistent-session-YRCF7AAC.js.map → persistent-session-BGMQQYW2.js.map} +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import {
|
|
2
|
+
sessionTranscriptDir
|
|
3
|
+
} from "./chunk-3PNRUCHD.js";
|
|
4
|
+
|
|
5
|
+
// src/lib/session-auth-dead.ts
|
|
6
|
+
import { closeSync, openSync, readSync, readdirSync, statSync } from "fs";
|
|
7
|
+
import { join } from "path";
|
|
8
|
+
var SIGNATURES = [
|
|
9
|
+
{
|
|
10
|
+
// packages/mcp/src/index.ts:279-283, verbatim.
|
|
11
|
+
re: /agent-session rejected \(expired or revoked\)/i,
|
|
12
|
+
kind: "session-token-expired",
|
|
13
|
+
selfRecovering: false,
|
|
14
|
+
remedy: "impersonation session token is dead and does not refresh \u2014 needs a fresh `agt impersonate connect`; a respawn will re-read the same token from .mcp.json"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
// packages/mcp/src/index.ts:216-219, thrown when the exchange fails and no
|
|
18
|
+
// cached token exists. The tier is down; the credential was never tested.
|
|
19
|
+
// The connection-refused variants are scoped to the exchange on purpose:
|
|
20
|
+
// `ECONNREFUSED` on its own could be any tool's HTTP client, and grading
|
|
21
|
+
// that as a platform-auth state would be a claim about the wrong thing.
|
|
22
|
+
re: /Cannot authenticate: token exchange failed and no cached token available|Token exchange failed: 5\d\d|token exchange failed:[\s\S]{0,200}?(?:ECONNREFUSED|econnrefused|errno 111|connection refused|delayed connect error|upstream connect error|disconnect\/reset before headers|no healthy upstream|fetch failed|socket hang up)/i,
|
|
23
|
+
kind: "exchange-unavailable",
|
|
24
|
+
selfRecovering: true,
|
|
25
|
+
remedy: "our identity tier is unreachable, not the credential \u2014 the child re-exchanges on the next call once it recovers; do not restart and do not tell the customer to reconnect"
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
// The host-JWT path after `:268-272` already forced one re-exchange. A 401
|
|
29
|
+
// that survives that is a verdict on the key, not an expiry.
|
|
30
|
+
re: /returned 401[\s\S]{0,200}?Invalid or expired token/i,
|
|
31
|
+
kind: "host-key-rejected",
|
|
32
|
+
selfRecovering: false,
|
|
33
|
+
remedy: "the host key was rejected after an automatic re-exchange \u2014 the agent is unbound or its key is revoked; needs a human to re-bind, not a restart"
|
|
34
|
+
}
|
|
35
|
+
];
|
|
36
|
+
function classifyPlatformAuthFailure(text) {
|
|
37
|
+
if (!text) return null;
|
|
38
|
+
for (const { re, kind, selfRecovering, remedy } of SIGNATURES) {
|
|
39
|
+
if (re.test(text)) return { kind, selfRecovering, remedy };
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
function isCollateralOf(o, outageInWindow) {
|
|
44
|
+
return outageInWindow && o.kind === "host-key-rejected";
|
|
45
|
+
}
|
|
46
|
+
function decideSessionAuthState(observations) {
|
|
47
|
+
if (observations.length === 0) {
|
|
48
|
+
return { state: "ok", worst: null, count: 0, detail: "auth=ok" };
|
|
49
|
+
}
|
|
50
|
+
const outageInWindow = observations.some((o) => o.kind === "exchange-unavailable");
|
|
51
|
+
const dead = observations.find((o) => !o.selfRecovering && !isCollateralOf(o, outageInWindow)) ?? null;
|
|
52
|
+
if (dead) {
|
|
53
|
+
return {
|
|
54
|
+
state: "dead",
|
|
55
|
+
worst: dead,
|
|
56
|
+
count: observations.length,
|
|
57
|
+
detail: `auth=dead kind=${dead.kind} n=${observations.length}`
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const demoted = observations.some((o) => !o.selfRecovering);
|
|
61
|
+
if (demoted) {
|
|
62
|
+
const outage = observations.find((o) => o.kind === "exchange-unavailable");
|
|
63
|
+
return {
|
|
64
|
+
state: "degraded",
|
|
65
|
+
worst: outage,
|
|
66
|
+
count: observations.length,
|
|
67
|
+
detail: `auth=degraded kind=exchange-unavailable n=${observations.length} 401s-demoted`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const first = observations[0];
|
|
71
|
+
return {
|
|
72
|
+
state: "degraded",
|
|
73
|
+
worst: first,
|
|
74
|
+
count: observations.length,
|
|
75
|
+
detail: `auth=degraded kind=${first.kind} n=${observations.length}`
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function collectAuthFailuresFromTranscript(content, sinceMs) {
|
|
79
|
+
const out = [];
|
|
80
|
+
if (!content) return out;
|
|
81
|
+
for (const line of content.split("\n")) {
|
|
82
|
+
if (!line) continue;
|
|
83
|
+
if (!line.includes("tool_result")) continue;
|
|
84
|
+
let entry;
|
|
85
|
+
try {
|
|
86
|
+
entry = JSON.parse(line);
|
|
87
|
+
} catch {
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
if (typeof entry !== "object" || entry === null) continue;
|
|
91
|
+
const rec = entry;
|
|
92
|
+
const ts = typeof rec["timestamp"] === "string" ? Date.parse(rec["timestamp"]) : NaN;
|
|
93
|
+
if (!Number.isFinite(ts) || ts < sinceMs) continue;
|
|
94
|
+
const message = rec["message"];
|
|
95
|
+
if (typeof message !== "object" || message === null) continue;
|
|
96
|
+
const blocks = message["content"];
|
|
97
|
+
if (!Array.isArray(blocks)) continue;
|
|
98
|
+
for (const block of blocks) {
|
|
99
|
+
if (typeof block !== "object" || block === null) continue;
|
|
100
|
+
const b = block;
|
|
101
|
+
if (b["type"] !== "tool_result") continue;
|
|
102
|
+
if (b["is_error"] !== true) continue;
|
|
103
|
+
const failure = classifyPlatformAuthFailure(flattenResultText(b["content"]));
|
|
104
|
+
if (!failure) continue;
|
|
105
|
+
out.push({ ...failure, toolName: readToolName(b) });
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return out;
|
|
109
|
+
}
|
|
110
|
+
function flattenResultText(content) {
|
|
111
|
+
if (typeof content === "string") return content;
|
|
112
|
+
if (!Array.isArray(content)) return null;
|
|
113
|
+
const parts = [];
|
|
114
|
+
for (const item of content) {
|
|
115
|
+
if (typeof item === "string") parts.push(item);
|
|
116
|
+
else if (typeof item === "object" && item !== null) {
|
|
117
|
+
const t = item["text"];
|
|
118
|
+
if (typeof t === "string") parts.push(t);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return parts.length > 0 ? parts.join("\n") : null;
|
|
122
|
+
}
|
|
123
|
+
function readToolName(block) {
|
|
124
|
+
const n = block["name"] ?? block["tool_name"];
|
|
125
|
+
return typeof n === "string" && n.length > 0 ? n : null;
|
|
126
|
+
}
|
|
127
|
+
var AUTH_PROBE_WINDOW_MS = 5 * 6e4;
|
|
128
|
+
var MAX_TRANSCRIPT_TAIL_BYTES = 512 * 1024;
|
|
129
|
+
function readTail(path, maxBytes) {
|
|
130
|
+
const size = statSync(path).size;
|
|
131
|
+
const start = size > maxBytes ? size - maxBytes : 0;
|
|
132
|
+
const length = size - start;
|
|
133
|
+
if (length <= 0) return "";
|
|
134
|
+
const buf = Buffer.allocUnsafe(length);
|
|
135
|
+
const fd = openSync(path, "r");
|
|
136
|
+
try {
|
|
137
|
+
const read = readSync(fd, buf, 0, length, start);
|
|
138
|
+
return buf.subarray(0, read).toString("utf-8");
|
|
139
|
+
} finally {
|
|
140
|
+
closeSync(fd);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
function probeSessionAuth(args) {
|
|
144
|
+
const now = args.now ?? /* @__PURE__ */ new Date();
|
|
145
|
+
const sinceMs = now.getTime() - (args.windowMs ?? AUTH_PROBE_WINDOW_MS);
|
|
146
|
+
const dir = args.transcriptDir ?? sessionTranscriptDir(args.projectDir);
|
|
147
|
+
let entries;
|
|
148
|
+
try {
|
|
149
|
+
entries = readdirSync(dir);
|
|
150
|
+
} catch {
|
|
151
|
+
return decideSessionAuthState([]);
|
|
152
|
+
}
|
|
153
|
+
const maxBytes = args.maxBytesPerFile ?? MAX_TRANSCRIPT_TAIL_BYTES;
|
|
154
|
+
const observations = [];
|
|
155
|
+
for (const path of transcriptFilesIn(dir)) {
|
|
156
|
+
try {
|
|
157
|
+
const st = statSync(path);
|
|
158
|
+
if (!st.isFile() || st.mtimeMs < sinceMs) continue;
|
|
159
|
+
} catch {
|
|
160
|
+
continue;
|
|
161
|
+
}
|
|
162
|
+
let content;
|
|
163
|
+
try {
|
|
164
|
+
content = readTail(path, maxBytes);
|
|
165
|
+
} catch {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
observations.push(...collectAuthFailuresFromTranscript(content, sinceMs));
|
|
169
|
+
}
|
|
170
|
+
return decideSessionAuthState(observations);
|
|
171
|
+
}
|
|
172
|
+
function transcriptFilesIn(dir) {
|
|
173
|
+
const out = [];
|
|
174
|
+
let entries;
|
|
175
|
+
try {
|
|
176
|
+
entries = readdirSync(dir);
|
|
177
|
+
} catch {
|
|
178
|
+
return out;
|
|
179
|
+
}
|
|
180
|
+
for (const name of entries) {
|
|
181
|
+
if (name.endsWith(".jsonl")) {
|
|
182
|
+
out.push(join(dir, name));
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
const subagentDir = join(dir, name, "subagents");
|
|
186
|
+
let subs;
|
|
187
|
+
try {
|
|
188
|
+
subs = readdirSync(subagentDir);
|
|
189
|
+
} catch {
|
|
190
|
+
continue;
|
|
191
|
+
}
|
|
192
|
+
for (const sub of subs) {
|
|
193
|
+
if (sub.endsWith(".jsonl")) out.push(join(subagentDir, sub));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
return out;
|
|
197
|
+
}
|
|
198
|
+
export {
|
|
199
|
+
AUTH_PROBE_WINDOW_MS,
|
|
200
|
+
MAX_TRANSCRIPT_TAIL_BYTES,
|
|
201
|
+
classifyPlatformAuthFailure,
|
|
202
|
+
collectAuthFailuresFromTranscript,
|
|
203
|
+
decideSessionAuthState,
|
|
204
|
+
probeSessionAuth
|
|
205
|
+
};
|
|
206
|
+
//# sourceMappingURL=session-auth-dead-CWFGB472.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/lib/session-auth-dead.ts"],"sourcesContent":["/**\n * ENG-8877 — tell apart the three ways an agent's platform auth can fail.\n *\n * WHY THIS EXISTS, AND WHY IT IS NOT THE FIX I FIRST PROPOSED\n * ----------------------------------------------------------\n * I filed ENG-8877 claiming that a session preserved through the 2026-08-14\n * outage (ENG-8869) was permanently stranded: healthy to the manager, 401 to\n * every MCP call, unable to refresh itself because `token_refresh` is an MCP\n * tool. That was wrong, and the fix I proposed — respawn the session to\n * re-mint — was wrong twice over. Recording it here because the wrong model is\n * the thing that will be re-derived by the next person reading a 401 in an\n * incident channel:\n *\n * * Refresh is NOT a tool call. It happens inside the MCP child, with no\n * agent involvement. `packages/mcp/src/index.ts:207` re-exchanges when the\n * cached JWT is within 5 minutes of expiry, and `:268-272` zeroes the cache\n * and retries once on a 401. A normal agent therefore self-heals on the\n * first call after the tier recovers, and a respawn would be pure churn —\n * the ENG-8344 cost (gordon: 23 respawns in 17 hours) that ENG-8877's own\n * description argued against while proposing it.\n *\n * * The one shape that IS stranded is not fixed by a respawn either. An\n * `AGT_AGENT_SESSION_TOKEN` (impersonation) session never exchanges at all\n * — `getToken()` returns it verbatim (`:198-205`) and the 401 retry is\n * explicitly gated `&& !AGT_AGENT_SESSION_TOKEN` (`:268`). The value is\n * baked into `.mcp.json` by `impersonate-mcp-rewrite.ts:203`, so a respawn\n * re-reads the same dead token.\n *\n * So the defect worth fixing is not the recovery — it is that the host cannot\n * SEE any of this. `401 Invalid or expired token` appears nowhere under\n * `apps/cli`. `isSessionHealthy` (`persistent-session.ts:3014`) is\n * `tmux has-session` plus a pgrep-style zombie probe; it cannot observe auth.\n * A session whose every platform call 401s reports `decision=healthy` forever\n * (`manager-worker.ts` decision tail).\n *\n * That is ENG-8872's rule — never collapse three states into two — arriving in\n * the session layer. These are the three, and they have three different\n * remedies:\n *\n * exchange-unavailable our identity tier is down; the credential is fine.\n * Wait. This is what the whole fleet looked like on\n * 2026-08-14 and it is NOT a credential problem.\n * host-key-rejected the exchange itself succeeded or was retried and the\n * API still says no. The host key is revoked or the\n * agent is unbound. A human has to re-bind it.\n * session-token-expired an impersonation session's fixed token is dead. No\n * refresh path exists by design. Needs a fresh\n * `agt impersonate connect` — a respawn will not do it.\n *\n * DIRECTION OF THE DEFAULT. Unrecognised text is NOT an auth failure. Same\n * posture as the CLI connectivity probe (ENG-5463): the loud-and-wrong error\n * here is calling a healthy session auth-dead, because the remedies are a\n * restart or a page to a human. A missed signature leaves today's behaviour,\n * which is silence. So a signature has to opt IN.\n *\n * WHERE THE SIGNAL COMES FROM. The transcript, not the pane. Measured on this\n * host: a tool error's text reaches the JSONL `tool_result` block in full\n * (`{\"is_error\":true,...,\"Slack error: invalid_arguments\"}`), and does NOT\n * reach `pane.log` — the TUI collapses it to `Error: API Error`. A pane\n * scraper for this, which is where I started, would never have fired.\n */\n\nimport { closeSync, openSync, readSync, readdirSync, statSync } from 'node:fs';\nimport { join } from 'node:path';\n\nimport { sessionTranscriptDir } from './daily-session.js';\n\n/** Which of the three failures a tool_result describes. */\nexport type PlatformAuthFailureKind =\n /** `/host/exchange` itself is unreachable or erroring. Our tier, not the credential. */\n | 'exchange-unavailable'\n /** A 401 that survived the automatic re-exchange. The host key is rejected. */\n | 'host-key-rejected'\n /** An impersonation session token expired. There is no refresh path by design. */\n | 'session-token-expired';\n\nexport interface PlatformAuthFailure {\n kind: PlatformAuthFailureKind;\n /**\n * True when the MCP child's own retry path will recover this without anyone\n * doing anything, once the underlying condition clears.\n *\n * This is the field that would have stopped ENG-8877 being filed wrong:\n * `exchange-unavailable` is self-recovering and looks identical, in a chat\n * message, to the two that are not.\n */\n selfRecovering: boolean;\n /** What a human or the manager should do. One line, operator-facing. */\n remedy: string;\n}\n\n/**\n * Signatures, each anchored on text a platform component actually emits.\n *\n * Order matters: the agent-session shape is checked first because its message\n * also contains `returned 401`, and it is the only one where a respawn is\n * actively useless. Getting that precedence backwards would report the one\n * genuinely stranded case as the one that heals itself.\n */\nconst SIGNATURES: ReadonlyArray<{\n re: RegExp;\n kind: PlatformAuthFailureKind;\n selfRecovering: boolean;\n remedy: string;\n}> = [\n {\n // packages/mcp/src/index.ts:279-283, verbatim.\n re: /agent-session rejected \\(expired or revoked\\)/i,\n kind: 'session-token-expired',\n selfRecovering: false,\n remedy:\n 'impersonation session token is dead and does not refresh — needs a fresh `agt impersonate connect`; a respawn will re-read the same token from .mcp.json',\n },\n {\n // packages/mcp/src/index.ts:216-219, thrown when the exchange fails and no\n // cached token exists. The tier is down; the credential was never tested.\n // The connection-refused variants are scoped to the exchange on purpose:\n // `ECONNREFUSED` on its own could be any tool's HTTP client, and grading\n // that as a platform-auth state would be a claim about the wrong thing.\n re: /Cannot authenticate: token exchange failed and no cached token available|Token exchange failed: 5\\d\\d|token exchange failed:[\\s\\S]{0,200}?(?:ECONNREFUSED|econnrefused|errno 111|connection refused|delayed connect error|upstream connect error|disconnect\\/reset before headers|no healthy upstream|fetch failed|socket hang up)/i,\n kind: 'exchange-unavailable',\n selfRecovering: true,\n remedy:\n 'our identity tier is unreachable, not the credential — the child re-exchanges on the next call once it recovers; do not restart and do not tell the customer to reconnect',\n },\n {\n // The host-JWT path after `:268-272` already forced one re-exchange. A 401\n // that survives that is a verdict on the key, not an expiry.\n re: /returned 401[\\s\\S]{0,200}?Invalid or expired token/i,\n kind: 'host-key-rejected',\n selfRecovering: false,\n remedy:\n 'the host key was rejected after an automatic re-exchange — the agent is unbound or its key is revoked; needs a human to re-bind, not a restart',\n },\n];\n\n/**\n * Classify one tool_result's error text.\n *\n * @returns the failure, or `null` when nothing recognises the text — which is\n * the overwhelmingly common case and must never be read as \"fine\".\n * It means \"not an auth failure we can name\", nothing more.\n */\nexport function classifyPlatformAuthFailure(text: string | null | undefined): PlatformAuthFailure | null {\n if (!text) return null;\n for (const { re, kind, selfRecovering, remedy } of SIGNATURES) {\n if (re.test(text)) return { kind, selfRecovering, remedy };\n }\n return null;\n}\n\n/** One classified observation, with enough context to report it. */\nexport interface AuthFailureObservation {\n kind: PlatformAuthFailureKind;\n selfRecovering: boolean;\n remedy: string;\n /** Tool the failing call belonged to, when the transcript names it. */\n toolName: string | null;\n}\n\n/**\n * What the manager should say about this session's platform auth this tick.\n *\n * ok nothing recognised. NOT a claim that auth is verified — see the\n * note on `classifyPlatformAuthFailure`.\n * degraded only self-recovering failures seen. Worth reporting, worth NOT\n * acting on. This is the state the fleet was in on 2026-08-14, and\n * the state that must never be escalated to a customer.\n * dead at least one failure that will not clear on its own.\n */\nexport type SessionAuthState = 'ok' | 'degraded' | 'dead';\n\nexport interface SessionAuthVerdict {\n state: SessionAuthState;\n /** The worst failure seen, or null when `state === 'ok'`. */\n worst: AuthFailureObservation | null;\n /** How many recognised auth failures were in the window. */\n count: number;\n /**\n * Short non-secret tag for the `[persistent-session-decision]` log line, so\n * a healthy-but-auth-dead session stops being indistinguishable from a\n * healthy one in the only place an operator looks.\n */\n detail: string;\n}\n\n/**\n * Reduce a window of observations to one verdict.\n *\n * `dead` beats `degraded` regardless of counts. A single non-recovering\n * failure is a fact about the credential; a hundred exchange timeouts are a\n * fact about our tier. Counting them against each other would let a noisy\n * outage bury the one observation that needs a human.\n */\n/**\n * ENG-8877, after review — is this non-recovering observation more likely to be\n * collateral from a token-exchange outage than an independent verdict?\n *\n * THE HOLE THIS CLOSES, WHICH WAS MINE. When `/host/exchange` is down but the\n * MCP child still holds a stale cached JWT, `getToken()` logs the exchange\n * failure and returns the stale token anyway (packages/mcp/src/index.ts:224-232).\n * The API then 401s it, the forced re-exchange fails too, and the agent gets a\n * plain `returned 401 … Invalid or expired token` — textually IDENTICAL to a\n * genuinely revoked host key.\n *\n * My first cut graded that `host-key-rejected`, i.e. \"a human must re-bind\n * this agent\". During a tier outage that is the wrong tier for every affected\n * agent at once — which is ENG-8878's defect, reproduced inside ENG-8877's fix,\n * by me, on the same day I wrote the ticket complaining about it.\n *\n * The transcript cannot separate the two texts, but it does not have to: if\n * the SAME window also contains direct evidence that the exchange is failing,\n * the parsimonious reading of a 401 is collateral, not a second independent\n * fault. A revoked key still reports `dead` the moment the outage evidence\n * ages out of the window, which is minutes — a delay, against telling the\n * whole fleet to re-bind during an outage.\n *\n * `session-token-expired` is never demoted: it carries its own distinct\n * message, it is unambiguous, and it does not self-heal at any point.\n */\nfunction isCollateralOf(o: AuthFailureObservation, outageInWindow: boolean): boolean {\n return outageInWindow && o.kind === 'host-key-rejected';\n}\n\nexport function decideSessionAuthState(observations: readonly AuthFailureObservation[]): SessionAuthVerdict {\n if (observations.length === 0) {\n return { state: 'ok', worst: null, count: 0, detail: 'auth=ok' };\n }\n const outageInWindow = observations.some((o) => o.kind === 'exchange-unavailable');\n const dead =\n observations.find((o) => !o.selfRecovering && !isCollateralOf(o, outageInWindow)) ?? null;\n if (dead) {\n return {\n state: 'dead',\n worst: dead,\n count: observations.length,\n detail: `auth=dead kind=${dead.kind} n=${observations.length}`,\n };\n }\n // Only self-recovering evidence survived. If a host-key 401 was demoted to\n // get here, say so in the tag rather than reporting a plain outage — the\n // operator should know a 401 was seen and why it is not being called a\n // verdict.\n const demoted = observations.some((o) => !o.selfRecovering);\n if (demoted) {\n const outage = observations.find((o) => o.kind === 'exchange-unavailable')!;\n return {\n state: 'degraded',\n worst: outage,\n count: observations.length,\n detail: `auth=degraded kind=exchange-unavailable n=${observations.length} 401s-demoted`,\n };\n }\n const first = observations[0]!;\n return {\n state: 'degraded',\n worst: first,\n count: observations.length,\n detail: `auth=degraded kind=${first.kind} n=${observations.length}`,\n };\n}\n\n/**\n * Pull recognised auth failures out of one transcript's JSONL content.\n *\n * Pure so the parsing is testable without a filesystem — same split as\n * `classifyTranscriptRateLimit` in `agent-serving-probe.ts`, which this\n * deliberately mirrors rather than inventing a second transcript-walking\n * shape.\n *\n * THE TRANSCRIPT, NOT THE PANE. Measured on a live host before writing this:\n * a failing tool's text reaches the JSONL `tool_result` block in full, and\n * does NOT reach `pane.log` — the TUI collapses it to `Error: API Error`. The\n * first version of this was a pane scraper and it would never have matched.\n *\n * @param sinceMs Only entries at or after this epoch ms count. An entry with\n * an unparseable or missing timestamp is SKIPPED, not counted:\n * an undateable 401 could be from six weeks ago, and reporting\n * a stale credential failure as current is the same class of\n * wrong this whole ticket is about.\n */\nexport function collectAuthFailuresFromTranscript(\n content: string,\n sinceMs: number,\n): AuthFailureObservation[] {\n const out: AuthFailureObservation[] = [];\n if (!content) return out;\n\n for (const line of content.split('\\n')) {\n if (!line) continue;\n // Cheap pre-filter. Parsing every line of a multi-megabyte transcript to\n // find the handful that are errors is the difference between a scan that\n // fits in a 30s poll and one that does not.\n if (!line.includes('tool_result')) continue;\n\n let entry: unknown;\n try {\n entry = JSON.parse(line);\n } catch {\n continue;\n }\n if (typeof entry !== 'object' || entry === null) continue;\n const rec = entry as Record<string, unknown>;\n\n const ts = typeof rec['timestamp'] === 'string' ? Date.parse(rec['timestamp']) : NaN;\n if (!Number.isFinite(ts) || ts < sinceMs) continue;\n\n const message = rec['message'];\n if (typeof message !== 'object' || message === null) continue;\n const blocks = (message as Record<string, unknown>)['content'];\n if (!Array.isArray(blocks)) continue;\n\n for (const block of blocks) {\n if (typeof block !== 'object' || block === null) continue;\n const b = block as Record<string, unknown>;\n if (b['type'] !== 'tool_result') continue;\n // Only errored results carry a failure to classify. An `is_error` that\n // is absent means success (the API's default); an explicit null means\n // \"not determined\" — neither is evidence of an auth failure, and\n // treating the null as one would invent a verdict the producer refused\n // to give. Same reading of the field as tool-call-extractor.ts:405-417.\n if (b['is_error'] !== true) continue;\n\n const failure = classifyPlatformAuthFailure(flattenResultText(b['content']));\n if (!failure) continue;\n out.push({ ...failure, toolName: readToolName(b) });\n }\n }\n return out;\n}\n\n/**\n * A `tool_result`'s content is either a string or the block-array form\n * (`[{type:'text',text:'…'}]`). Both shapes appear in real transcripts, so\n * handle both rather than matching only the one that happened to be in the\n * first file I opened.\n */\nfunction flattenResultText(content: unknown): string | null {\n if (typeof content === 'string') return content;\n if (!Array.isArray(content)) return null;\n const parts: string[] = [];\n for (const item of content) {\n if (typeof item === 'string') parts.push(item);\n else if (typeof item === 'object' && item !== null) {\n const t = (item as Record<string, unknown>)['text'];\n if (typeof t === 'string') parts.push(t);\n }\n }\n return parts.length > 0 ? parts.join('\\n') : null;\n}\n\n/** Best-effort tool name; the block does not always carry one. */\nfunction readToolName(block: Record<string, unknown>): string | null {\n const n = block['name'] ?? block['tool_name'];\n return typeof n === 'string' && n.length > 0 ? n : null;\n}\n\n/**\n * How far back a poll looks for auth failures.\n *\n * Two poll intervals (~30s each) plus slack. Long enough that a failure cannot\n * fall between ticks, short enough that a credential problem fixed ten minutes\n * ago stops being reported as current. The window is the whole reason this\n * reports a STATE rather than a count: \"this agent has ever seen a 401\" is\n * true of almost every agent and means nothing.\n */\nexport const AUTH_PROBE_WINDOW_MS = 5 * 60_000;\n\n/**\n * How much of each transcript to read.\n *\n * This runs for every agent on every ~30s poll, and a long-lived transcript is\n * tens of megabytes. Reading them whole would make a health probe the most\n * expensive thing in the tick — the kind of cost that gets a useful signal\n * switched off six months later by someone chasing manager latency.\n *\n * Only the last few minutes matter (see AUTH_PROBE_WINDOW_MS), and the tail is\n * where they are. A tail read can slice mid-line; the parser drops\n * unparseable lines already, so the worst case is losing the one straddling\n * entry, which the next poll's window still covers.\n */\nexport const MAX_TRANSCRIPT_TAIL_BYTES = 512 * 1024;\n\n/** Read at most `maxBytes` from the end of a file, as UTF-8. */\nfunction readTail(path: string, maxBytes: number): string {\n const size = statSync(path).size;\n const start = size > maxBytes ? size - maxBytes : 0;\n const length = size - start;\n if (length <= 0) return '';\n const buf = Buffer.allocUnsafe(length);\n const fd = openSync(path, 'r');\n try {\n const read = readSync(fd, buf, 0, length, start);\n return buf.subarray(0, read).toString('utf-8');\n } finally {\n closeSync(fd);\n }\n}\n\n/**\n * Observe one agent's platform-auth state from its own transcripts.\n *\n * Mirrors `probeRateLimit` (agent-serving-probe.ts) deliberately: same\n * directory resolution, same mtime pre-filter, same injectable `transcriptDir`\n * seam for tests. Returns `ok` on any filesystem trouble — an unreadable\n * transcript is an absence of evidence, and this must never manufacture a\n * credential verdict out of a missing file.\n */\nexport function probeSessionAuth(args: {\n projectDir: string;\n now?: Date;\n windowMs?: number;\n /** Test seam: override the transcript directory. */\n transcriptDir?: string;\n /** Test seam: shrink the tail so the truncation behaviour is drivable. */\n maxBytesPerFile?: number;\n}): SessionAuthVerdict {\n const now = args.now ?? new Date();\n const sinceMs = now.getTime() - (args.windowMs ?? AUTH_PROBE_WINDOW_MS);\n const dir = args.transcriptDir ?? sessionTranscriptDir(args.projectDir);\n\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return decideSessionAuthState([]);\n }\n\n const maxBytes = args.maxBytesPerFile ?? MAX_TRANSCRIPT_TAIL_BYTES;\n const observations: AuthFailureObservation[] = [];\n for (const path of transcriptFilesIn(dir)) {\n try {\n const st = statSync(path);\n // A file untouched since before the window opened cannot hold an\n // in-window failure. Cheap upper bound, same trick as probeRateLimit.\n if (!st.isFile() || st.mtimeMs < sinceMs) continue;\n } catch {\n continue;\n }\n let content: string;\n try {\n content = readTail(path, maxBytes);\n } catch {\n continue;\n }\n observations.push(...collectAuthFailuresFromTranscript(content, sinceMs));\n }\n\n return decideSessionAuthState(observations);\n}\n\n/**\n * Every transcript that belongs to this project: the per-session JSONL files,\n * plus each session's sub-agent sidecars.\n *\n * Claude Code writes a delegated worker's transcript to\n * `<transcriptDir>/<sessionId>/subagents/agent-<id>.jsonl` (ENG-6294 relies on\n * the same layout). The first version of this walk read only the top level,\n * which would have missed auth failures in exactly the place a lot of tool\n * calls happen — this session dispatches most of its heavy work to sub-agents,\n * and their calls carry the same platform token. CodeRabbit caught it.\n *\n * Directory shapes we do not recognise are skipped rather than guessed at.\n */\nfunction transcriptFilesIn(dir: string): string[] {\n const out: string[] = [];\n let entries: string[];\n try {\n entries = readdirSync(dir);\n } catch {\n return out;\n }\n for (const name of entries) {\n if (name.endsWith('.jsonl')) {\n out.push(join(dir, name));\n continue;\n }\n // A session directory. Its sub-agent sidecars live one level down.\n const subagentDir = join(dir, name, 'subagents');\n let subs: string[];\n try {\n subs = readdirSync(subagentDir);\n } catch {\n continue;\n }\n for (const sub of subs) {\n if (sub.endsWith('.jsonl')) out.push(join(subagentDir, sub));\n }\n }\n return out;\n}\n"],"mappings":"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","names":[]}
|