@bridge4dev/runner 0.22.1 → 0.26.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/auth-relay.d.ts +33 -3
- package/dist/auth-relay.js +199 -16
- package/dist/environment.d.ts +171 -0
- package/dist/environment.js +409 -0
- package/dist/git.d.ts +10 -0
- package/dist/git.js +94 -5
- package/dist/index.js +328 -19
- package/dist/paths.d.ts +9 -0
- package/dist/paths.js +11 -0
- package/dist/protocol.d.ts +2 -2
- package/dist/self-update.d.ts +14 -0
- package/dist/self-update.js +45 -0
- 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
package/dist/auth-relay.d.ts
CHANGED
|
@@ -39,11 +39,41 @@ export declare class AuthRelay {
|
|
|
39
39
|
cancel(): void;
|
|
40
40
|
}
|
|
41
41
|
/**
|
|
42
|
-
* Claude: the subscription
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
* Claude: the subscription login is recorded in the CLI's own credentials file.
|
|
43
|
+
* The RUNNER reads it (its own host user's file) — the agent itself is still
|
|
44
|
+
* denied this path by layer-1 policy.
|
|
45
|
+
*
|
|
46
|
+
* `expiresAt` is NOT the login. It is the expiry of a short-lived access token
|
|
47
|
+
* (~8 hours on a live file), and next to it sits `refreshToken` with
|
|
48
|
+
* `refreshTokenExpiresAt` ~26 days out, which the CLI spends silently on its
|
|
49
|
+
* next run. Judging the login by `expiresAt` alone is why every server nobody
|
|
50
|
+
* had touched since the morning reported "login expired — re-login needed"
|
|
51
|
+
* over a login that was good for another three weeks (#121). Codex has carried
|
|
52
|
+
* exactly this guard since day one (`readCodexCredential`); Claude did not.
|
|
53
|
+
*
|
|
54
|
+
* Deliberately NOT asking the CLI. `claude auth status --json` looks like an
|
|
55
|
+
* arbiter and is not one: measured live (SDK binary 2.1.218), it answers
|
|
56
|
+
* `loggedIn: true` for a credential whose access token has expired AND which
|
|
57
|
+
* carries no refresh token at all — i.e. for a genuinely dead login. It never
|
|
58
|
+
* leaves the machine, so it cannot see a server-side revocation either. It
|
|
59
|
+
* would have cost a ~800 ms / ~300 MB subprocess per poll under `MemoryMax=2G`
|
|
60
|
+
* (gotcha #100) and echoed the account's e-mail and org name to every member of
|
|
61
|
+
* the organization, in exchange for no truth at all. A revoked login is caught
|
|
62
|
+
* instead by `noteAgentAuthFailure` below — from a real refusal, not a guess.
|
|
45
63
|
*/
|
|
46
64
|
export declare function claudeAuthStatus(homedir?: string): Promise<AgentAuthStatus>;
|
|
65
|
+
/** A session just failed to authenticate as this agent. */
|
|
66
|
+
export declare function noteAgentAuthFailure(agent: RelayAgent): void;
|
|
67
|
+
/** The agent just worked — whatever was wrong with the sign-in is not. */
|
|
68
|
+
export declare function clearAgentAuthFailure(agent: RelayAgent): void;
|
|
69
|
+
/**
|
|
70
|
+
* Is a refusal still being held against this agent?
|
|
71
|
+
*
|
|
72
|
+
* Exported so the wiring in `supervisor.ts` can be pinned by a test without
|
|
73
|
+
* shelling out to the agent CLIs: this predicate IS the mechanism the panel's
|
|
74
|
+
* demotion reads, and three unguarded lines were carrying it (QA-117 M6).
|
|
75
|
+
*/
|
|
76
|
+
export declare function agentAuthFailureActive(agent: RelayAgent): boolean;
|
|
47
77
|
/**
|
|
48
78
|
* Codex reports its own login state via an exit code (0 signed in / 1 not).
|
|
49
79
|
* Probed against the RUNNER's home: the host user can be signed in while our
|
package/dist/auth-relay.js
CHANGED
|
@@ -92,6 +92,12 @@ export class AuthRelay {
|
|
|
92
92
|
try {
|
|
93
93
|
if (code === 0 && adoptLoginResult(stagingCodexHomePath())) {
|
|
94
94
|
log.info('codex: device login completed — credential adopted');
|
|
95
|
+
// The ONLY place a Codex sign-in can be reported as finished. Its
|
|
96
|
+
// flow never sends `login_code` (device auth needs no paste-back),
|
|
97
|
+
// so without this line a refusal we recorded earlier would keep the
|
|
98
|
+
// panel demanding a re-login the user has just done — #121's own
|
|
99
|
+
// complaint, reproduced on the other half of the panel (QA-117 H1).
|
|
100
|
+
clearAgentAuthFailure('codex');
|
|
95
101
|
}
|
|
96
102
|
else {
|
|
97
103
|
discardStagingHome();
|
|
@@ -174,35 +180,192 @@ export class AuthRelay {
|
|
|
174
180
|
function sleep(ms) {
|
|
175
181
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
176
182
|
}
|
|
177
|
-
|
|
183
|
+
function isNonEmptyString(value) {
|
|
184
|
+
return typeof value === 'string' && value.length > 0;
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* A millisecond timestamp we are willing to hand to `new Date(...)`.
|
|
188
|
+
*
|
|
189
|
+
* The bound is not decoration: `new Date(1e21).toISOString()` throws
|
|
190
|
+
* `RangeError`, and thrown out of here it takes BOTH agents' verdicts down
|
|
191
|
+
* with it (they share one `Promise.all`) on every poll, forever, because only
|
|
192
|
+
* successes are cached (QA-117 M1).
|
|
193
|
+
*/
|
|
194
|
+
const MAX_TIMESTAMP_MS = 8.64e15;
|
|
195
|
+
function asTimestamp(value) {
|
|
196
|
+
if (typeof value !== 'number' || !Number.isFinite(value))
|
|
197
|
+
return undefined;
|
|
198
|
+
return Math.abs(value) <= MAX_TIMESTAMP_MS ? value : undefined;
|
|
199
|
+
}
|
|
178
200
|
/**
|
|
179
|
-
* Claude: the subscription
|
|
180
|
-
*
|
|
181
|
-
*
|
|
201
|
+
* Claude: the subscription login is recorded in the CLI's own credentials file.
|
|
202
|
+
* The RUNNER reads it (its own host user's file) — the agent itself is still
|
|
203
|
+
* denied this path by layer-1 policy.
|
|
204
|
+
*
|
|
205
|
+
* `expiresAt` is NOT the login. It is the expiry of a short-lived access token
|
|
206
|
+
* (~8 hours on a live file), and next to it sits `refreshToken` with
|
|
207
|
+
* `refreshTokenExpiresAt` ~26 days out, which the CLI spends silently on its
|
|
208
|
+
* next run. Judging the login by `expiresAt` alone is why every server nobody
|
|
209
|
+
* had touched since the morning reported "login expired — re-login needed"
|
|
210
|
+
* over a login that was good for another three weeks (#121). Codex has carried
|
|
211
|
+
* exactly this guard since day one (`readCodexCredential`); Claude did not.
|
|
212
|
+
*
|
|
213
|
+
* Deliberately NOT asking the CLI. `claude auth status --json` looks like an
|
|
214
|
+
* arbiter and is not one: measured live (SDK binary 2.1.218), it answers
|
|
215
|
+
* `loggedIn: true` for a credential whose access token has expired AND which
|
|
216
|
+
* carries no refresh token at all — i.e. for a genuinely dead login. It never
|
|
217
|
+
* leaves the machine, so it cannot see a server-side revocation either. It
|
|
218
|
+
* would have cost a ~800 ms / ~300 MB subprocess per poll under `MemoryMax=2G`
|
|
219
|
+
* (gotcha #100) and echoed the account's e-mail and org name to every member of
|
|
220
|
+
* the organization, in exchange for no truth at all. A revoked login is caught
|
|
221
|
+
* instead by `noteAgentAuthFailure` below — from a real refusal, not a guess.
|
|
182
222
|
*/
|
|
183
223
|
export async function claudeAuthStatus(homedir = os.homedir()) {
|
|
184
224
|
if (process.env['CLAUDE_CODE_OAUTH_TOKEN']) {
|
|
185
225
|
return { status: 'ok', detail: 'CLAUDE_CODE_OAUTH_TOKEN is configured' };
|
|
186
226
|
}
|
|
187
227
|
const file = path.join(homedir, '.claude', '.credentials.json');
|
|
228
|
+
let raw;
|
|
188
229
|
try {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
230
|
+
raw = fs.readFileSync(file, 'utf8');
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
const code = error.code;
|
|
234
|
+
// Never signed in here is a different answer from "we could not look".
|
|
235
|
+
// EACCES on somebody else's HOME used to read as "not signed in", which
|
|
236
|
+
// sends the user re-authenticating a credential that is sitting right
|
|
237
|
+
// there (the same mistake #121 is about, one layer down).
|
|
238
|
+
if (code === 'ENOENT' || code === 'ENOTDIR') {
|
|
239
|
+
return { status: 'missing', detail: 'No Claude login on this server' };
|
|
196
240
|
}
|
|
241
|
+
// No `log.warn` here: this probe is on a 60-second timer since #121, and a
|
|
242
|
+
// machine with EACCES on that file would write the same line forever.
|
|
243
|
+
// `logVerdictChange` already reports the `unknown`, once, when it starts
|
|
244
|
+
// (QA-117 L5).
|
|
197
245
|
return {
|
|
198
|
-
status: '
|
|
199
|
-
|
|
200
|
-
...(oauth.subscriptionType ? { detail: `subscription ${oauth.subscriptionType}` } : {}),
|
|
246
|
+
status: 'unknown',
|
|
247
|
+
detail: `could not read the login on this server (${code ?? 'unknown error'})`,
|
|
201
248
|
};
|
|
202
249
|
}
|
|
250
|
+
let oauth;
|
|
251
|
+
try {
|
|
252
|
+
oauth = JSON.parse(raw).claudeAiOauth;
|
|
253
|
+
}
|
|
203
254
|
catch {
|
|
204
|
-
|
|
255
|
+
// Truncated or hand-edited file: the CLI cannot use it either, and signing
|
|
256
|
+
// in again is the fix — so say `missing` (which offers that button) rather
|
|
257
|
+
// than `unknown` (which offers nothing).
|
|
258
|
+
return { status: 'missing', detail: 'the stored login could not be read — sign in again' };
|
|
259
|
+
}
|
|
260
|
+
// A parseable file is not a credential. `{"claudeAiOauth":{}}` is what a
|
|
261
|
+
// partial write and a hand-edit both leave behind, and reading it as a
|
|
262
|
+
// healthy login puts a green dot and NO way out on the panel — «I cannot
|
|
263
|
+
// tell» turned into «all good», which is the rule this ticket exists to
|
|
264
|
+
// uphold, upside down (QA-117 M2).
|
|
265
|
+
if (!oauth || typeof oauth !== 'object' || Array.isArray(oauth)) {
|
|
266
|
+
return { status: 'missing', detail: 'the stored login could not be read — sign in again' };
|
|
267
|
+
}
|
|
268
|
+
const hasAccess = isNonEmptyString(oauth.accessToken);
|
|
269
|
+
const hasRefresh = isNonEmptyString(oauth.refreshToken);
|
|
270
|
+
const now = Date.now();
|
|
271
|
+
const accessExpiry = asTimestamp(oauth.expiresAt);
|
|
272
|
+
const refreshExpiry = asTimestamp(oauth.refreshTokenExpiresAt);
|
|
273
|
+
// Nothing recognisable in the blob at all — no token, not even a date. That
|
|
274
|
+
// is «never signed in here», and it must offer the button that fixes it.
|
|
275
|
+
if (!hasAccess && !hasRefresh && accessExpiry === undefined) {
|
|
276
|
+
return { status: 'missing', detail: 'No subscription login found' };
|
|
277
|
+
}
|
|
278
|
+
// A date we can read outranks the token beside it (the pre-#121 contract, and
|
|
279
|
+
// the reason a dated-but-token-less fixture still reads as expired); with no
|
|
280
|
+
// readable date, the presence of the token is all we have.
|
|
281
|
+
const accessLive = accessExpiry === undefined ? hasAccess : accessExpiry > now;
|
|
282
|
+
// No `refreshTokenExpiresAt` next to a refresh token means the CLI did not
|
|
283
|
+
// record one — that is not evidence of death, so we do not read it as death.
|
|
284
|
+
const refreshLive = hasRefresh && (refreshExpiry === undefined || refreshExpiry > now);
|
|
285
|
+
// Report the date this login actually dies on, not the one that moves every
|
|
286
|
+
// eight hours: a panel reading "token until <today>" is alarming and wrong.
|
|
287
|
+
// Only while the refresh token is the operative one, though — a live access
|
|
288
|
+
// token beside a dead refresh token dies on its OWN date. And when the CLI
|
|
289
|
+
// recorded no date for a live refresh token we say NOTHING: printing the
|
|
290
|
+
// access token's lapsed date beside the word «signed in» is the very screen
|
|
291
|
+
// this function's docblock promises not to draw (QA-117 M3).
|
|
292
|
+
const effectiveExpiry = refreshLive ? refreshExpiry : accessExpiry;
|
|
293
|
+
const expiresAt = effectiveExpiry === undefined ? undefined : new Date(effectiveExpiry).toISOString();
|
|
294
|
+
if (!accessLive && !refreshLive) {
|
|
295
|
+
return {
|
|
296
|
+
status: 'expired',
|
|
297
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
298
|
+
detail: 'the stored login has expired',
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
status: 'ok',
|
|
303
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
304
|
+
// Only a plain string, and only a short one: this value is read off disk
|
|
305
|
+
// and printed in every member's panel.
|
|
306
|
+
...(isNonEmptyString(oauth.subscriptionType)
|
|
307
|
+
? { detail: `subscription ${oauth.subscriptionType.slice(0, 40)}` }
|
|
308
|
+
: {}),
|
|
309
|
+
};
|
|
310
|
+
}
|
|
311
|
+
// ─── What the agent actually experienced ─────────────────────────────
|
|
312
|
+
//
|
|
313
|
+
// The credentials file cannot tell us the provider revoked this login: the
|
|
314
|
+
// refresh token still sits there, dated. The one authority on that is a real
|
|
315
|
+
// refusal from a real run — so a session that failed to authenticate demotes
|
|
316
|
+
// the file's verdict for a while, and any successful turn clears it again.
|
|
317
|
+
//
|
|
318
|
+
// Process memory on purpose: no protocol, no disk. The consequence is worth
|
|
319
|
+
// naming — restarting the daemon forgets the refusal and the panel goes back
|
|
320
|
+
// to trusting the file until the next session tries.
|
|
321
|
+
const AUTH_FAILURE_TTL_MS = 15 * 60_000;
|
|
322
|
+
const authFailures = new Map();
|
|
323
|
+
/** A session just failed to authenticate as this agent. */
|
|
324
|
+
export function noteAgentAuthFailure(agent) {
|
|
325
|
+
authFailures.set(agent, Date.now());
|
|
326
|
+
log.warn('auth-relay: agent sign-in refused during a session', { agent });
|
|
327
|
+
}
|
|
328
|
+
/** The agent just worked — whatever was wrong with the sign-in is not. */
|
|
329
|
+
export function clearAgentAuthFailure(agent) {
|
|
330
|
+
if (authFailures.delete(agent)) {
|
|
331
|
+
log.info('auth-relay: agent sign-in is working again', { agent });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
/**
|
|
335
|
+
* Is a refusal still being held against this agent?
|
|
336
|
+
*
|
|
337
|
+
* Exported so the wiring in `supervisor.ts` can be pinned by a test without
|
|
338
|
+
* shelling out to the agent CLIs: this predicate IS the mechanism the panel's
|
|
339
|
+
* demotion reads, and three unguarded lines were carrying it (QA-117 M6).
|
|
340
|
+
*/
|
|
341
|
+
export function agentAuthFailureActive(agent) {
|
|
342
|
+
return hasRecentAuthFailure(agent);
|
|
343
|
+
}
|
|
344
|
+
function hasRecentAuthFailure(agent) {
|
|
345
|
+
const at = authFailures.get(agent);
|
|
346
|
+
if (at === undefined)
|
|
347
|
+
return false;
|
|
348
|
+
if (Date.now() - at > AUTH_FAILURE_TTL_MS) {
|
|
349
|
+
authFailures.delete(agent);
|
|
350
|
+
return false;
|
|
205
351
|
}
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
/** Last verdict we published per agent — logged only when it changes. */
|
|
355
|
+
const lastVerdict = new Map();
|
|
356
|
+
function logVerdictChange(agent, status) {
|
|
357
|
+
const previous = lastVerdict.get(agent);
|
|
358
|
+
if (previous === status.status)
|
|
359
|
+
return;
|
|
360
|
+
lastVerdict.set(agent, status.status);
|
|
361
|
+
// Without this line there is no way to confirm on a live server that the
|
|
362
|
+
// verdict changed — and no way to see it flapping (lesson from #103).
|
|
363
|
+
log.info('auth-relay: login verdict', {
|
|
364
|
+
agent,
|
|
365
|
+
from: previous ?? 'none',
|
|
366
|
+
to: status.status,
|
|
367
|
+
...(status.expiresAt ? { expiresAt: status.expiresAt } : {}),
|
|
368
|
+
});
|
|
206
369
|
}
|
|
207
370
|
/**
|
|
208
371
|
* Codex reports its own login state via an exit code (0 signed in / 1 not).
|
|
@@ -282,8 +445,28 @@ function readCodexCredential(homePath) {
|
|
|
282
445
|
return null;
|
|
283
446
|
}
|
|
284
447
|
}
|
|
448
|
+
/**
|
|
449
|
+
* A verdict read off disk, overruled by what a real session experienced.
|
|
450
|
+
*
|
|
451
|
+
* Only ever downgrades: a refusal we witnessed outranks a file that looks fine,
|
|
452
|
+
* and never the other way round — a file saying `expired` is not made `ok` by
|
|
453
|
+
* the absence of failures.
|
|
454
|
+
*/
|
|
455
|
+
function withObservedFailures(agent, status) {
|
|
456
|
+
if (status.status !== 'ok' || !hasRecentAuthFailure(agent))
|
|
457
|
+
return status;
|
|
458
|
+
return {
|
|
459
|
+
status: 'expired',
|
|
460
|
+
...(status.expiresAt ? { expiresAt: status.expiresAt } : {}),
|
|
461
|
+
detail: 'the agent was refused with this sign-in — re-login needed',
|
|
462
|
+
};
|
|
463
|
+
}
|
|
285
464
|
export async function agentAuthStatuses() {
|
|
286
|
-
const [
|
|
465
|
+
const [claudeRaw, codexRaw] = await Promise.all([claudeAuthStatus(), codexAuthStatus()]);
|
|
466
|
+
const claude = withObservedFailures('claude', claudeRaw);
|
|
467
|
+
const codex = withObservedFailures('codex', codexRaw);
|
|
468
|
+
logVerdictChange('claude', claude);
|
|
469
|
+
logVerdictChange('codex', codex);
|
|
287
470
|
return { claude, codex };
|
|
288
471
|
}
|
|
289
472
|
//# sourceMappingURL=auth-relay.js.map
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything that depends on WHICH USER the runner runs as.
|
|
3
|
+
*
|
|
4
|
+
* The install instructions offer a choice — root or a dedicated user — and both
|
|
5
|
+
* are legitimate. What is not legitimate is the way the second choice used to
|
|
6
|
+
* fail: silently, later, and in the agent's voice. A dedicated user typically
|
|
7
|
+
* owns none of the project directories, has an empty `$HOME` where the agent
|
|
8
|
+
* CLIs keep their login and settings, and reaches systemd only with
|
|
9
|
+
* `XDG_RUNTIME_DIR` set. Each of those produces a symptom that reads like a
|
|
10
|
+
* broken runner ("agent can't do anything", "not signed in", "service is fine"
|
|
11
|
+
* with exit code 0) while the real cause is a permission nobody was told about.
|
|
12
|
+
*
|
|
13
|
+
* This module turns each of them into a fact with a command next to it.
|
|
14
|
+
*/
|
|
15
|
+
export interface RunnerIdentity {
|
|
16
|
+
user: string;
|
|
17
|
+
uid: number;
|
|
18
|
+
gid: number;
|
|
19
|
+
home: string;
|
|
20
|
+
isRoot: boolean;
|
|
21
|
+
}
|
|
22
|
+
export declare function runnerIdentity(): RunnerIdentity;
|
|
23
|
+
export interface PathAccess {
|
|
24
|
+
path: string;
|
|
25
|
+
exists: boolean;
|
|
26
|
+
isDirectory: boolean;
|
|
27
|
+
/** uid of the owner, or -1 when we could not stat it. */
|
|
28
|
+
ownerUid: number;
|
|
29
|
+
ownedByUs: boolean;
|
|
30
|
+
readable: boolean;
|
|
31
|
+
writable: boolean;
|
|
32
|
+
/**
|
|
33
|
+
* True when we could not even look — a directory ABOVE this one denies us
|
|
34
|
+
* traversal. Distinct from `!exists` on purpose: those are opposite answers
|
|
35
|
+
* to the person reading them («create it» vs «grant access»), and `statSync`
|
|
36
|
+
* reports both by throwing.
|
|
37
|
+
*/
|
|
38
|
+
unreachable: boolean;
|
|
39
|
+
}
|
|
40
|
+
export declare function inspectPath(target: string): PathAccess;
|
|
41
|
+
/**
|
|
42
|
+
* The first directory on this path the runner cannot enter.
|
|
43
|
+
*
|
|
44
|
+
* «Permission denied» on `/srv/apps/shop` is usually not about `shop` at all —
|
|
45
|
+
* it is about `/srv/apps`, and naming the wrong one sends the person to chmod
|
|
46
|
+
* a directory that was never the problem.
|
|
47
|
+
*/
|
|
48
|
+
export declare function firstUnreachableAncestor(target: string): string | null;
|
|
49
|
+
/**
|
|
50
|
+
* Git refuses to work in a repository owned by somebody else — since 2022, and
|
|
51
|
+
* with no exception for root. So the "obvious" fix for a dedicated user (chown
|
|
52
|
+
* the project to it) breaks git for the person who was committing there before,
|
|
53
|
+
* and the exception has to be added on BOTH sides.
|
|
54
|
+
*/
|
|
55
|
+
export declare function safeDirectoryCommand(repoPath: string): string;
|
|
56
|
+
export declare function looksLikeDubiousOwnership(message: string): boolean;
|
|
57
|
+
/** Is this path already excused in the current user's git config? */
|
|
58
|
+
export declare function hasSafeDirectory(repoPath: string): Promise<boolean>;
|
|
59
|
+
export declare function addSafeDirectory(repoPath: string): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* What the agent will find in this user's home besides a login.
|
|
62
|
+
*
|
|
63
|
+
* Copying only `.credentials.json` to a fresh user is the usual half-measure:
|
|
64
|
+
* the agent starts, and then behaves like a stranger — no permission allowlist,
|
|
65
|
+
* no slash commands, no plugins, default model. From the outside that reads as
|
|
66
|
+
* «the runner can't do anything», which is why this is worth reporting BEFORE
|
|
67
|
+
* the first session rather than diagnosing after it.
|
|
68
|
+
*/
|
|
69
|
+
export interface AgentConfigContour {
|
|
70
|
+
home: string;
|
|
71
|
+
claudeDir: boolean;
|
|
72
|
+
settings: boolean;
|
|
73
|
+
localSettings: boolean;
|
|
74
|
+
/** Number of entries in `permissions.allow`, or null when unreadable. */
|
|
75
|
+
allowRules: number | null;
|
|
76
|
+
commands: number;
|
|
77
|
+
plugins: boolean;
|
|
78
|
+
codexDir: boolean;
|
|
79
|
+
codexConfig: boolean;
|
|
80
|
+
}
|
|
81
|
+
export declare function agentConfigContour(home?: string): AgentConfigContour;
|
|
82
|
+
/**
|
|
83
|
+
* Another user's home that already has an agent set up.
|
|
84
|
+
*
|
|
85
|
+
* Only reported, never copied: a copied OAuth credential means two accounts
|
|
86
|
+
* share one refresh token, and a rotation in either one silently invalidates
|
|
87
|
+
* the other. Signing in as the runner's own user is the clean answer; the copy
|
|
88
|
+
* is the fast one, and the person choosing between them deserves to be told
|
|
89
|
+
* which is which.
|
|
90
|
+
*/
|
|
91
|
+
export declare function otherHomeWithAgents(me?: RunnerIdentity): string | null;
|
|
92
|
+
/**
|
|
93
|
+
* `systemctl --user` talks over a per-user D-Bus socket, and finds it through
|
|
94
|
+
* `XDG_RUNTIME_DIR`. Under `sudo -iu <user>` that variable is not set, and the
|
|
95
|
+
* failure is worse than an error: it prints «Failed to connect to bus» and
|
|
96
|
+
* exits **0**, so a health check reads it as success.
|
|
97
|
+
*/
|
|
98
|
+
export declare function systemdUserEnv(): NodeJS.ProcessEnv;
|
|
99
|
+
/** True when the user bus is actually reachable — `systemctl --user` lies with exit 0. */
|
|
100
|
+
export declare function systemdUserBusReachable(): Promise<boolean>;
|
|
101
|
+
/** The command form that works under `sudo -iu <user>` — printed in hints. */
|
|
102
|
+
export declare function systemctlHint(args: string): string;
|
|
103
|
+
/**
|
|
104
|
+
* Remembered from binding and from session starts, so `doctor` can check the
|
|
105
|
+
* permissions of real projects instead of asking the person to name them.
|
|
106
|
+
* Best-effort on purpose: a runner that cannot write its own state directory
|
|
107
|
+
* has bigger problems than a diagnostic list, and none of them should turn a
|
|
108
|
+
* session start into an error.
|
|
109
|
+
*/
|
|
110
|
+
export declare function rememberWorkspacePath(workspacePath: string): void;
|
|
111
|
+
export declare function knownWorkspacePaths(): string[];
|
|
112
|
+
export interface ToolCheck {
|
|
113
|
+
/** `null` when the tool is not installed at all. */
|
|
114
|
+
path: string | null;
|
|
115
|
+
version?: string;
|
|
116
|
+
/** Set when the tool is there but this user cannot use it. */
|
|
117
|
+
problem?: string;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Node, as THIS user sees it.
|
|
121
|
+
*
|
|
122
|
+
* A dedicated user does not automatically inherit a Node installed for
|
|
123
|
+
* somebody else — fnm, nvm and a root-only prefix are all per-user by design.
|
|
124
|
+
* The runner itself is running, so node clearly exists somewhere; the question
|
|
125
|
+
* this answers is whether it is on the daemon user's own PATH, because that is
|
|
126
|
+
* what agent tooling and `npm install -g` will look at.
|
|
127
|
+
*/
|
|
128
|
+
export declare function nodeCheck(): Promise<ToolCheck>;
|
|
129
|
+
/**
|
|
130
|
+
* Docker, as THIS user sees it.
|
|
131
|
+
*
|
|
132
|
+
* Reported rather than judged: plenty of projects never touch it. But when the
|
|
133
|
+
* project's own workflow is `docker compose`, a dedicated user without access
|
|
134
|
+
* to the socket produces a session that fails on its first command, and the
|
|
135
|
+
* error will be about a socket rather than about a group nobody was added to.
|
|
136
|
+
*
|
|
137
|
+
* Worth stating where it is stated: being in the `docker` group is equivalent
|
|
138
|
+
* to root on this machine. That is a fact for the owner to accept knowingly.
|
|
139
|
+
*/
|
|
140
|
+
export declare function dockerCheck(): Promise<ToolCheck>;
|
|
141
|
+
/**
|
|
142
|
+
* Does the service survive a logout?
|
|
143
|
+
*
|
|
144
|
+
* `loginctl enable-linger` is what keeps a user's systemd services running with
|
|
145
|
+
* nobody logged in. `install-service` turns it on, but a unit installed by hand
|
|
146
|
+
* — or a user created afterwards — can miss it, and the failure looks like
|
|
147
|
+
* «the server goes offline whenever I close the terminal».
|
|
148
|
+
*/
|
|
149
|
+
export declare function lingerEnabled(): Promise<boolean | null>;
|
|
150
|
+
/**
|
|
151
|
+
* Make the agent CLIs findable, without taking anything away.
|
|
152
|
+
*
|
|
153
|
+
* A systemd user service starts with the manager's PATH, which is the
|
|
154
|
+
* distribution default — `/usr/bin` and friends. Both agent CLIs are commonly
|
|
155
|
+
* installed somewhere else: `~/.local/bin` for a per-user install, and a
|
|
156
|
+
* node managed by fnm/nvm/volta lives under its own version directory. When
|
|
157
|
+
* `codex` sits there, the runner scans PATH, does not find it, and reports to
|
|
158
|
+
* the dashboard that this machine has no Codex at all — the agent the person
|
|
159
|
+
* installed simply never appears.
|
|
160
|
+
*
|
|
161
|
+
* Deliberately APPEND-ONLY, and deliberately in the process rather than in the
|
|
162
|
+
* unit file. Writing `Environment=PATH=…` into the unit would REPLACE whatever
|
|
163
|
+
* systemd gives the service today (`/snap/bin`, anything set through
|
|
164
|
+
* `environment.d`), trading something that works for something that is
|
|
165
|
+
* missing. Here nothing can be lost: entries are only added when they are
|
|
166
|
+
* absent and the directory actually exists.
|
|
167
|
+
*
|
|
168
|
+
* Returns what it added, so the caller can say so once at startup.
|
|
169
|
+
*/
|
|
170
|
+
export declare function ensureAgentPath(): string[];
|
|
171
|
+
//# sourceMappingURL=environment.d.ts.map
|