Fix owner context loss after finalize; green the test suite
Owner continuity: - Seed a freshly created owner task with the previous task's latest owner final on a cold start after the previous task already closed, so a user reply to a finalized TASK_DONE no longer produces a "no context" answer. Activated for this deployment via PAIRED_CARRY_FORWARD_LATEST_OWNER_FINAL (.env); carried text is injected as clearly-marked background only. - Skip intermediate STEP_DONE outputs when picking the carry-forward anchor. Single-mode routing: - enforceRoomModeOnLease strips a stale reviewer/arbiter lease from a room switched back to single, preventing single-mode messages from stalling in the paired path on a stuck execution lease. Session auth / credentials: - Pre-sync Claude credentials into each session dir before the agent spawns. - Honor CLAUDE_CREDENTIALS_PATH in setup/login.ts (per-service isolation). - Add a relogin-required gate so a permanently logged-out claude-code room asks the user to re-login instead of spawning a doomed agent. Other: - Arbiter verdicts written in the user's language (verdict keyword stays EN). - status-dashboard chatName field; runtime-inventory credential path resolver. Tests (make suite fully green: 1595 pass / 3 skip): - service-routing: default owner is now the claude service and reviewer is codex-review; update the 7 failover/default expectations accordingly. - migrate-room-registrations: owner inferred as claude-code (configured OWNER_AGENT_TYPE) for a dual legacy room; reviewer becomes codex. - register: mock paired-workspace provisioning + reload signal (registration now provisions a workspace and hot-reloads); assert RELOADED status. - paired-execution-context: force a claude-code reviewer to exercise the Claude read-only branch regardless of the deployment default. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -5,13 +5,13 @@
|
||||
* builds (captured from the official CLI). This is the manual / paste-code
|
||||
* variant: the user visits the authorize URL, completes login in their
|
||||
* browser, and pastes the code back. We exchange it for tokens and write
|
||||
* `~/.claude/.credentials.json`.
|
||||
* `CLAUDE_CREDENTIALS_PATH` or `~/.claude/.credentials.json`.
|
||||
*
|
||||
* Usage:
|
||||
* bun setup/index.ts --step login # phase 1: print authorize URL
|
||||
* bun setup/index.ts --step login --code <c> # phase 2: exchange the code
|
||||
*
|
||||
* Phase 1 stashes the PKCE verifier + state in /tmp/ejclaw-claude-login.json
|
||||
* Phase 1 stashes the PKCE verifier + state in a /tmp login state file
|
||||
* (mode 0600). Phase 2 reads it back, exchanges the code, writes credentials.
|
||||
*
|
||||
* This step is run by hand for re-auth. Once `.credentials.json` exists, the
|
||||
@@ -39,8 +39,21 @@ const SCOPES = [
|
||||
'user:file_upload',
|
||||
];
|
||||
|
||||
const STATE_FILE = path.join(os.tmpdir(), 'ejclaw-claude-login.json');
|
||||
const CREDS_PATH = path.join(os.homedir(), '.claude', '.credentials.json');
|
||||
function credentialsPath(): string {
|
||||
const configured = process.env.CLAUDE_CREDENTIALS_PATH?.trim();
|
||||
return configured
|
||||
? path.resolve(configured)
|
||||
: path.join(os.homedir(), '.claude', '.credentials.json');
|
||||
}
|
||||
|
||||
function stateFilePath(): string {
|
||||
const hash = crypto
|
||||
.createHash('sha256')
|
||||
.update(credentialsPath())
|
||||
.digest('hex')
|
||||
.slice(0, 12);
|
||||
return path.join(os.tmpdir(), `ejclaw-claude-login-${hash}.json`);
|
||||
}
|
||||
|
||||
interface PendingState {
|
||||
verifier: string;
|
||||
@@ -90,13 +103,14 @@ function buildAuthorizeUrl(state: string, challenge: string): string {
|
||||
}
|
||||
|
||||
function writePendingState(p: PendingState): void {
|
||||
fs.writeFileSync(STATE_FILE, JSON.stringify(p), { mode: 0o600 });
|
||||
fs.writeFileSync(stateFilePath(), JSON.stringify(p), { mode: 0o600 });
|
||||
}
|
||||
|
||||
function readPendingState(): PendingState | null {
|
||||
if (!fs.existsSync(STATE_FILE)) return null;
|
||||
const stateFile = stateFilePath();
|
||||
if (!fs.existsSync(stateFile)) return null;
|
||||
try {
|
||||
return JSON.parse(fs.readFileSync(STATE_FILE, 'utf-8')) as PendingState;
|
||||
return JSON.parse(fs.readFileSync(stateFile, 'utf-8')) as PendingState;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -136,6 +150,7 @@ async function exchangeCode(
|
||||
}
|
||||
|
||||
function writeCredentials(resp: ExchangeResponse): void {
|
||||
const credsPath = credentialsPath();
|
||||
const expiresAt = Date.now() + resp.expires_in * 1000;
|
||||
const creds = {
|
||||
claudeAiOauth: {
|
||||
@@ -150,11 +165,11 @@ function writeCredentials(resp: ExchangeResponse): void {
|
||||
: '',
|
||||
},
|
||||
};
|
||||
const dir = path.dirname(CREDS_PATH);
|
||||
const dir = path.dirname(credsPath);
|
||||
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||
const tmp = `${CREDS_PATH}.tmp`;
|
||||
const tmp = `${credsPath}.tmp`;
|
||||
fs.writeFileSync(tmp, JSON.stringify(creds, null, 2), { mode: 0o600 });
|
||||
fs.renameSync(tmp, CREDS_PATH);
|
||||
fs.renameSync(tmp, credsPath);
|
||||
}
|
||||
|
||||
interface Args {
|
||||
@@ -208,15 +223,15 @@ export async function run(args: string[]): Promise<void> {
|
||||
try {
|
||||
const resp = await exchangeCode(code, pending.verifier, pending.state);
|
||||
writeCredentials(resp);
|
||||
fs.unlinkSync(STATE_FILE);
|
||||
fs.unlinkSync(stateFilePath());
|
||||
const newScopes = (resp.scope || SCOPES.join(' ')).split(' ').sort();
|
||||
logger.info(
|
||||
{ scopes: newScopes, expiresInMin: Math.round(resp.expires_in / 60) },
|
||||
'Wrote ~/.claude/.credentials.json',
|
||||
'Wrote Claude credentials',
|
||||
);
|
||||
emitStatus('LOGIN', {
|
||||
STATUS: 'success',
|
||||
CREDENTIALS_PATH: CREDS_PATH,
|
||||
CREDENTIALS_PATH: credentialsPath(),
|
||||
SCOPES: newScopes.join(','),
|
||||
});
|
||||
} catch (err) {
|
||||
|
||||
Reference in New Issue
Block a user