read the probe off stdout by position, and renumber comms
host's finding on 288679af, and the directory restructure the owner asked for.
THE MARKERS WERE SUBSTRINGS OF THE PATHS THEY TESTED. `bin` is inside
…/.local/bin/claude and `cred` is inside …/.claude/.credentials.json, and the
match ran against a string that merged stdout and stderr — so anything writing
either path to stderr set the flag. Verified on the live server against an
account with neither file: one `set -x` made the trace of the test command
itself report installed and signed in. Not live, and it fails in the unsafe
direction, on the endpoint whose whole job is explaining a broken agent.
No marker spelling fixes it, because a trace echoes the literal along with the
path. The channel was the bug. Two characters on stdout read by position, with
parsing extracted as parseLoginProbe so it cannot see stderr at all, and stderr
kept separately because a failure has to stay diagnosable. Seven tests including
the exact trace host captured — true/true before, false/false now.
COMMS is renumbered: ten dated files in a day, each restating the others'
status, replaced by one file holding only what nobody has resolved. Odd numbers
mine, even numbers host's, alternation encoding push-then-wait, numbers ending
when the feature does. The reasoning that produced the deleted files is in the
commit history, which is where it belongs.
Carried forward and unowned: deprovisionOsAccount, the terminal replay bug, the
two docker handbacks, and the two verify items neither of us can execute.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, test } from 'bun:test';
|
||||
import { parseLoginProbe } from './os-user-claude';
|
||||
|
||||
// Pins the shape of the answer `/agent-status` gives a member about their own agent.
|
||||
//
|
||||
// The version before this one decided with `out.includes('bin')` / `out.includes('cred')` against a string
|
||||
// that merged stdout and stderr — and both markers are substrings of the paths the probe tests. A shell trace
|
||||
// echoing the command was enough to report a member as installed and signed in when neither was true,
|
||||
// verified on the live server against an account that had never logged in. It failed in the unsafe direction:
|
||||
// the endpoint exists to explain a broken agent, and that is the mode where it explains the wrong thing.
|
||||
//
|
||||
// So the assertions below are mostly about what CANNOT move the answer.
|
||||
|
||||
describe('parseLoginProbe', () => {
|
||||
test('reads both flags by position', () => {
|
||||
expect(parseLoginProbe('11')).toEqual({ installed: true, loggedIn: true });
|
||||
expect(parseLoginProbe('10')).toEqual({ installed: true, loggedIn: false });
|
||||
expect(parseLoginProbe('01')).toEqual({ installed: false, loggedIn: true });
|
||||
expect(parseLoginProbe('00')).toEqual({ installed: false, loggedIn: false });
|
||||
});
|
||||
|
||||
test('the freshly provisioned case: installed, not signed in', () => {
|
||||
// What a member sees before they run `claude` once themselves — the case the UI renders instructions for.
|
||||
expect(parseLoginProbe('10')).toEqual({ installed: true, loggedIn: false });
|
||||
});
|
||||
|
||||
test('a trace of the probe cannot set either flag', () => {
|
||||
// Exactly the stderr the live server captured under `sh -x`, now on the channel the answer is read from.
|
||||
// Under the old substring match this returned true/true.
|
||||
const trace =
|
||||
'+ test -x /data/jg@pertento.ai/home/.local/bin/claude\n' +
|
||||
'+ test -s /data/jg@pertento.ai/home/.claude/.credentials.json\n';
|
||||
expect(parseLoginProbe(trace)).toEqual({ installed: false, loggedIn: false });
|
||||
});
|
||||
|
||||
test('the paths themselves cannot set either flag', () => {
|
||||
expect(parseLoginProbe('/home/x/.local/bin/claude')).toEqual({ installed: false, loggedIn: false });
|
||||
expect(parseLoginProbe('/home/x/.claude/.credentials.json')).toEqual({ installed: false, loggedIn: false });
|
||||
});
|
||||
|
||||
test('no output refuses rather than assuming', () => {
|
||||
// A failed spawn must not read as a working agent.
|
||||
expect(parseLoginProbe('')).toEqual({ installed: false, loggedIn: false });
|
||||
});
|
||||
|
||||
test('a sudo banner or any other prefix cannot shift the positions into truth', () => {
|
||||
expect(parseLoginProbe('sudo: a password is required\n')).toEqual({ installed: false, loggedIn: false });
|
||||
});
|
||||
});
|
||||
@@ -55,11 +55,35 @@ export const claudeBinPath = (email: string): string => claudeBinIn(osUserHome(e
|
||||
*/
|
||||
const credentialsPath = (email: string): string => join(osUserHome(email), '.claude', '.credentials.json');
|
||||
|
||||
/** Run a command as the member and report only whether it succeeded, with its output for the log. */
|
||||
async function asMember(osUser: string, command: string[]): Promise<{ ok: boolean; out: string }> {
|
||||
/**
|
||||
* Run a command as the member.
|
||||
*
|
||||
* `out` merges stdout and stderr and exists for logging — a failure is diagnosable only if both are in it.
|
||||
* `stdout` is kept separate for anything that *decides* on output, because merging the two channels means the
|
||||
* decision can be moved by anything that writes to stderr: a shell trace, a sudo banner, a wrapper echoing
|
||||
* argv. Never match on `out`.
|
||||
*/
|
||||
async function asMember(osUser: string, command: string[]): Promise<{ ok: boolean; out: string; stdout: string }> {
|
||||
const proc = runAs(osUser, command);
|
||||
const [out, err] = await Promise.all([new Response(proc.stdout).text(), new Response(proc.stderr).text()]);
|
||||
return { ok: (await proc.exited) === 0, out: `${out}${err}`.trim() };
|
||||
return { ok: (await proc.exited) === 0, out: `${out}${err}`.trim(), stdout: out };
|
||||
}
|
||||
|
||||
/**
|
||||
* Read the two-character probe below: position 0 is the binary, position 1 is the credential.
|
||||
*
|
||||
* Split out as a pure function so the parsing is testable without a subprocess, and — more to the point —
|
||||
* so it can only ever see stdout. The previous version decided with `out.includes('bin')` and
|
||||
* `out.includes('cred')` against the merged channel, and both markers are substrings of the paths being
|
||||
* tested: `bin` ⊂ `…/.local/bin/claude`, `cred` ⊂ `…/.claude/.credentials.json`. One `set -x` and the trace
|
||||
* of the test command itself set both flags true with neither file present — verified on the live server
|
||||
* against an account that had never logged in.
|
||||
*
|
||||
* No marker spelling fixes that, because a trace echoes the literal along with the path. The channel was the
|
||||
* bug, so the fix is the channel plus reading by position rather than by substring.
|
||||
*/
|
||||
export function parseLoginProbe(stdout: string): ClaudeLoginState {
|
||||
return { installed: stdout[0] === '1', loggedIn: stdout[1] === '1' };
|
||||
}
|
||||
|
||||
export type ClaudeProvisionResult =
|
||||
@@ -117,15 +141,15 @@ export async function claudeLoginState(params: { email: string; osUser: string }
|
||||
// member. Individually cheap, unbounded in aggregate, and the auth log is where a real sudo event has to
|
||||
// stay visible.
|
||||
//
|
||||
// Markers rather than an exit code because one call now answers two questions. `-x` follows symlinks, which
|
||||
// is what the installer produces: a link into a versioned directory, not a file.
|
||||
// Two characters on stdout, read by position — see `parseLoginProbe` for why not markers. `-x` follows
|
||||
// symlinks, which is what the installer produces: a link into a versioned directory, not a file.
|
||||
const probe = await asMember(params.osUser, [
|
||||
'sh',
|
||||
'-c',
|
||||
'test -x "$1" && printf bin; test -s "$2" && printf cred',
|
||||
'if test -x "$1"; then printf 1; else printf 0; fi; if test -s "$2"; then printf 1; else printf 0; fi',
|
||||
'_',
|
||||
claudeBinPath(params.email),
|
||||
credentialsPath(params.email),
|
||||
]);
|
||||
return { installed: probe.out.includes('bin'), loggedIn: probe.out.includes('cred') };
|
||||
return parseLoginProbe(probe.stdout);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user