@bridge4dev/runner 0.22.1 → 0.27.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-auth.d.ts +29 -0
- package/dist/agent-auth.js +136 -0
- package/dist/auth-relay.d.ts +62 -4
- package/dist/auth-relay.js +423 -25
- package/dist/environment.d.ts +186 -0
- package/dist/environment.js +433 -0
- package/dist/git.d.ts +10 -0
- package/dist/git.js +94 -5
- package/dist/index.js +466 -21
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +11 -0
- package/dist/protocol.d.ts +7 -7
- package/dist/recipe-schema.d.ts +1 -1
- package/dist/self-update.d.ts +55 -0
- package/dist/self-update.js +164 -25
- package/dist/service-unit.d.ts +13 -1
- package/dist/service-unit.js +41 -10
- package/dist/supervisor.js +35 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export declare function agentAuthPath(): string;
|
|
2
|
+
/** The token we hold for Claude, or null once it is too old to trust. */
|
|
3
|
+
export declare function storedClaudeToken(): string | null;
|
|
4
|
+
export declare function storeClaudeToken(token: string): void;
|
|
5
|
+
export declare function clearStoredClaudeToken(): void;
|
|
6
|
+
/**
|
|
7
|
+
* Put a stored token into this process's environment, unless the operator
|
|
8
|
+
* already set one.
|
|
9
|
+
*
|
|
10
|
+
* Their variable wins on purpose: an explicitly exported
|
|
11
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` in the unit or in `environment.d` is a deliberate
|
|
12
|
+
* choice, and silently overriding it with something we captured months ago is
|
|
13
|
+
* the kind of surprise this file exists to prevent.
|
|
14
|
+
*
|
|
15
|
+
* Returns true when it applied one.
|
|
16
|
+
*/
|
|
17
|
+
export declare function applyStoredClaudeToken(): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* The OAuth token `claude setup-token` printed, reassembled out of pty output.
|
|
20
|
+
*
|
|
21
|
+
* Written against how the output actually arrives rather than how it looks in a
|
|
22
|
+
* terminal: the pty wraps at its width, so a ~100-character token is routinely
|
|
23
|
+
* split across lines. Continuation lines are joined only while they consist
|
|
24
|
+
* ENTIRELY of token characters — prose ("Store this token securely") contains
|
|
25
|
+
* spaces or punctuation and stops the join, which is what keeps the reassembly
|
|
26
|
+
* from swallowing the sentence after it.
|
|
27
|
+
*/
|
|
28
|
+
export declare function extractOauthToken(text: string): string | null;
|
|
29
|
+
//# sourceMappingURL=agent-auth.d.ts.map
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { configDir } from './paths.js';
|
|
4
|
+
/**
|
|
5
|
+
* A Claude login this runner owns, for the one CLI that cannot store one.
|
|
6
|
+
*
|
|
7
|
+
* Background, because this file only makes sense with it. The dashboard's
|
|
8
|
+
* sign-in button relays a real OAuth flow to the machine — and until 0.27.0 it
|
|
9
|
+
* relayed `claude setup-token`, which is not a login at all. Measured against
|
|
10
|
+
* the 2.1.220 binary and confirmed by Anthropic's own docs: `setup-token` mints
|
|
11
|
+
* a long-lived INFERENCE-ONLY token, prints it, and «does not save the token
|
|
12
|
+
* anywhere». The runner threw the printed token away, then judged the login by
|
|
13
|
+
* reading `~/.claude/.credentials.json` — a file that command never writes. So
|
|
14
|
+
* the flow completed, the panel re-probed within a second, and answered «No
|
|
15
|
+
* Claude login on this server» underneath a success screen. That is the bug
|
|
16
|
+
* behind every «I signed in and it still says signed out».
|
|
17
|
+
*
|
|
18
|
+
* The main path is now `claude auth login`, which persists properly and needs
|
|
19
|
+
* nothing from this module. This exists for the fallback: a CLI old enough not
|
|
20
|
+
* to have `auth login` still has `setup-token`, and rather than leave that
|
|
21
|
+
* machine with no way in, we capture the token it prints and keep it ourselves.
|
|
22
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` is already on the session env allowlist, so a stored
|
|
23
|
+
* token reaches the agent exactly like an operator-configured one.
|
|
24
|
+
*
|
|
25
|
+
* Stored on the machine, 0600, in the runner's own config directory. DevBridge
|
|
26
|
+
* never receives it: the token travels from the provider into a pty on this
|
|
27
|
+
* host and onto this host's disk.
|
|
28
|
+
*/
|
|
29
|
+
const FILE_MODE = 0o600;
|
|
30
|
+
export function agentAuthPath() {
|
|
31
|
+
return path.join(configDir(), 'agent-auth.json');
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* How long a captured token is believed without further evidence.
|
|
35
|
+
*
|
|
36
|
+
* `claude setup-token` mints a one-year credential and there is no way to ask
|
|
37
|
+
* whether it is still good without spending a request. A stored token that
|
|
38
|
+
* never ages out is a green light nobody can turn off: the panel would keep
|
|
39
|
+
* saying «signed in» about a revoked account for as long as the file exists.
|
|
40
|
+
* Eleven months keeps it inside the token's own life while still expiring.
|
|
41
|
+
*/
|
|
42
|
+
const TOKEN_MAX_AGE_MS = 334 * 24 * 60 * 60 * 1000;
|
|
43
|
+
function read() {
|
|
44
|
+
try {
|
|
45
|
+
const parsed = JSON.parse(fs.readFileSync(agentAuthPath(), 'utf8'));
|
|
46
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
47
|
+
return {};
|
|
48
|
+
return parsed;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The token we hold for Claude, or null once it is too old to trust. */
|
|
55
|
+
export function storedClaudeToken() {
|
|
56
|
+
const file = read();
|
|
57
|
+
const value = file.claudeOauthToken;
|
|
58
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
59
|
+
return null;
|
|
60
|
+
const stamped = typeof file.updatedAt === 'string' ? Date.parse(file.updatedAt) : NaN;
|
|
61
|
+
if (Number.isFinite(stamped) && Date.now() - stamped > TOKEN_MAX_AGE_MS)
|
|
62
|
+
return null;
|
|
63
|
+
return value;
|
|
64
|
+
}
|
|
65
|
+
export function storeClaudeToken(token) {
|
|
66
|
+
const dir = configDir();
|
|
67
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
68
|
+
const file = agentAuthPath();
|
|
69
|
+
// Same write-then-rename as the config: a torn file here reads as «signed
|
|
70
|
+
// out» and sends somebody through the whole login again.
|
|
71
|
+
const tmp = path.join(dir, `.agent-auth.${process.pid}.tmp`);
|
|
72
|
+
fs.writeFileSync(tmp, `${JSON.stringify({ claudeOauthToken: token, updatedAt: new Date().toISOString() }, null, 2)}\n`, { mode: FILE_MODE });
|
|
73
|
+
fs.renameSync(tmp, file);
|
|
74
|
+
fs.chmodSync(file, FILE_MODE);
|
|
75
|
+
}
|
|
76
|
+
export function clearStoredClaudeToken() {
|
|
77
|
+
try {
|
|
78
|
+
fs.rmSync(agentAuthPath(), { force: true });
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
/* nothing to clear */
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Put a stored token into this process's environment, unless the operator
|
|
86
|
+
* already set one.
|
|
87
|
+
*
|
|
88
|
+
* Their variable wins on purpose: an explicitly exported
|
|
89
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` in the unit or in `environment.d` is a deliberate
|
|
90
|
+
* choice, and silently overriding it with something we captured months ago is
|
|
91
|
+
* the kind of surprise this file exists to prevent.
|
|
92
|
+
*
|
|
93
|
+
* Returns true when it applied one.
|
|
94
|
+
*/
|
|
95
|
+
export function applyStoredClaudeToken() {
|
|
96
|
+
if (process.env['CLAUDE_CODE_OAUTH_TOKEN'])
|
|
97
|
+
return false;
|
|
98
|
+
const token = storedClaudeToken();
|
|
99
|
+
if (!token)
|
|
100
|
+
return false;
|
|
101
|
+
process.env['CLAUDE_CODE_OAUTH_TOKEN'] = token;
|
|
102
|
+
return true;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The OAuth token `claude setup-token` printed, reassembled out of pty output.
|
|
106
|
+
*
|
|
107
|
+
* Written against how the output actually arrives rather than how it looks in a
|
|
108
|
+
* terminal: the pty wraps at its width, so a ~100-character token is routinely
|
|
109
|
+
* split across lines. Continuation lines are joined only while they consist
|
|
110
|
+
* ENTIRELY of token characters — prose ("Store this token securely") contains
|
|
111
|
+
* spaces or punctuation and stops the join, which is what keeps the reassembly
|
|
112
|
+
* from swallowing the sentence after it.
|
|
113
|
+
*/
|
|
114
|
+
export function extractOauthToken(text) {
|
|
115
|
+
const lines = text.split('\n').map((line) => line.trim());
|
|
116
|
+
for (let i = 0; i < lines.length; i++) {
|
|
117
|
+
const line = lines[i] ?? '';
|
|
118
|
+
const start = line.match(/sk-ant-oat[0-9]*-[A-Za-z0-9_-]*$/);
|
|
119
|
+
// The token must run to the end of its line; anything after it on the same
|
|
120
|
+
// line means this is prose mentioning a token, not the token itself.
|
|
121
|
+
if (!start)
|
|
122
|
+
continue;
|
|
123
|
+
let token = start[0];
|
|
124
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
125
|
+
const next = lines[j] ?? '';
|
|
126
|
+
if (!/^[A-Za-z0-9_-]+$/.test(next))
|
|
127
|
+
break;
|
|
128
|
+
token += next;
|
|
129
|
+
}
|
|
130
|
+
// Long enough to be real, short enough not to be a runaway join.
|
|
131
|
+
if (token.length >= 40 && token.length <= 400)
|
|
132
|
+
return token;
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
//# sourceMappingURL=agent-auth.js.map
|
package/dist/auth-relay.d.ts
CHANGED
|
@@ -24,26 +24,84 @@ export interface AgentAuthStatus {
|
|
|
24
24
|
expiresAt?: string;
|
|
25
25
|
detail?: string;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Rejoin a secret the pty split across lines, so the masker can see it.
|
|
29
|
+
*
|
|
30
|
+
* `maskString` matches `sk-ant-[A-Za-z0-9_-]{8,}`, which a line break inside
|
|
31
|
+
* the token defeats — and the pty wraps at its width, so a ~100-character
|
|
32
|
+
* token arrives in pieces as a matter of course. Measured: the head gets
|
|
33
|
+
* masked and the tail is printed verbatim into an error detail that travels to
|
|
34
|
+
* the dashboard. Rejoining first costs nothing and closes it.
|
|
35
|
+
*/
|
|
36
|
+
export declare function rejoinWrappedSecrets(text: string): string;
|
|
27
37
|
/** Strip ANSI/OSC control sequences so text matching sees plain output. */
|
|
28
38
|
export declare function stripControl(raw: string): string;
|
|
29
39
|
export declare function extractLoginUrl(agent: RelayAgent, raw: string): string | null;
|
|
30
40
|
export declare function extractDeviceCode(raw: string): string | null;
|
|
41
|
+
export interface AuthRelayDeps {
|
|
42
|
+
/**
|
|
43
|
+
* How we find out whether Claude is signed in, after the CLI says it is.
|
|
44
|
+
* Injected so a test can exercise the contract without an OAuth provider.
|
|
45
|
+
*/
|
|
46
|
+
claudeStatus?: () => Promise<AgentAuthStatus>;
|
|
47
|
+
}
|
|
31
48
|
export declare class AuthRelay {
|
|
32
49
|
private readonly commands;
|
|
50
|
+
private readonly deps;
|
|
33
51
|
private active;
|
|
34
|
-
constructor(commands?: Record<RelayAgent, string
|
|
52
|
+
constructor(commands?: Record<RelayAgent, string>, deps?: AuthRelayDeps);
|
|
35
53
|
/** Start (or restart) a login flow and wait until the sign-in URL appears. */
|
|
36
54
|
start(agent: RelayAgent): Promise<LoginStartResult>;
|
|
55
|
+
private startWith;
|
|
37
56
|
/** Paste the confirmation code back into the waiting CLI (Claude flow). */
|
|
38
57
|
submitCode(agent: RelayAgent, code: string): Promise<LoginCodeResult>;
|
|
58
|
+
/**
|
|
59
|
+
* The CLI exited 0 — but is the machine actually signed in?
|
|
60
|
+
*
|
|
61
|
+
* Asked rather than assumed. Reporting a success the panel then contradicts
|
|
62
|
+
* one second later is worse than reporting a failure: it sends the person
|
|
63
|
+
* looking for a permissions problem that does not exist, which is exactly
|
|
64
|
+
* what happened on axon-prod-01.
|
|
65
|
+
*/
|
|
66
|
+
private confirmSignedIn;
|
|
39
67
|
cancel(): void;
|
|
40
68
|
}
|
|
41
69
|
/**
|
|
42
|
-
* Claude: the subscription
|
|
43
|
-
*
|
|
44
|
-
*
|
|
70
|
+
* Claude: the subscription login is recorded in the CLI's own credentials file.
|
|
71
|
+
* The RUNNER reads it (its own host user's file) — the agent itself is still
|
|
72
|
+
* denied this path by layer-1 policy.
|
|
73
|
+
*
|
|
74
|
+
* `expiresAt` is NOT the login. It is the expiry of a short-lived access token
|
|
75
|
+
* (~8 hours on a live file), and next to it sits `refreshToken` with
|
|
76
|
+
* `refreshTokenExpiresAt` ~26 days out, which the CLI spends silently on its
|
|
77
|
+
* next run. Judging the login by `expiresAt` alone is why every server nobody
|
|
78
|
+
* had touched since the morning reported "login expired — re-login needed"
|
|
79
|
+
* over a login that was good for another three weeks (#121). Codex has carried
|
|
80
|
+
* exactly this guard since day one (`readCodexCredential`); Claude did not.
|
|
81
|
+
*
|
|
82
|
+
* Deliberately NOT asking the CLI. `claude auth status --json` looks like an
|
|
83
|
+
* arbiter and is not one: measured live (SDK binary 2.1.218), it answers
|
|
84
|
+
* `loggedIn: true` for a credential whose access token has expired AND which
|
|
85
|
+
* carries no refresh token at all — i.e. for a genuinely dead login. It never
|
|
86
|
+
* leaves the machine, so it cannot see a server-side revocation either. It
|
|
87
|
+
* would have cost a ~800 ms / ~300 MB subprocess per poll under `MemoryMax=2G`
|
|
88
|
+
* (gotcha #100) and echoed the account's e-mail and org name to every member of
|
|
89
|
+
* the organization, in exchange for no truth at all. A revoked login is caught
|
|
90
|
+
* instead by `noteAgentAuthFailure` below — from a real refusal, not a guess.
|
|
45
91
|
*/
|
|
46
92
|
export declare function claudeAuthStatus(homedir?: string): Promise<AgentAuthStatus>;
|
|
93
|
+
/** A session just failed to authenticate as this agent. */
|
|
94
|
+
export declare function noteAgentAuthFailure(agent: RelayAgent): void;
|
|
95
|
+
/** The agent just worked — whatever was wrong with the sign-in is not. */
|
|
96
|
+
export declare function clearAgentAuthFailure(agent: RelayAgent): void;
|
|
97
|
+
/**
|
|
98
|
+
* Is a refusal still being held against this agent?
|
|
99
|
+
*
|
|
100
|
+
* Exported so the wiring in `supervisor.ts` can be pinned by a test without
|
|
101
|
+
* shelling out to the agent CLIs: this predicate IS the mechanism the panel's
|
|
102
|
+
* demotion reads, and three unguarded lines were carrying it (QA-117 M6).
|
|
103
|
+
*/
|
|
104
|
+
export declare function agentAuthFailureActive(agent: RelayAgent): boolean;
|
|
47
105
|
/**
|
|
48
106
|
* Codex reports its own login state via an exit code (0 signed in / 1 not).
|
|
49
107
|
* Probed against the RUNNER's home: the host user can be signed in while our
|