diff --git a/bin/contributor-relay.sh b/bin/contributor-relay.sh index dc0405ca0..64a02094c 100755 --- a/bin/contributor-relay.sh +++ b/bin/contributor-relay.sh @@ -2301,7 +2301,15 @@ function handleMessage(data, hub) { injectGhToken(msg.github_token); tokenExpiresAt = msg.token_expires_at ? new Date(msg.token_expires_at).getTime() : null; } - fs.writeFileSync(TASK_FILE, JSON.stringify(msg, null, 2)); + // TASK_FILE is observability/debug state with no reader that needs the + // credential; the live token's one legitimate on-disk home is the 0600 + // GH_TOKEN_CACHE written by injectGhToken above. Strip it and keep the + // file owner-only (chmod covers overwriting a pre-existing 0644 file) + // so a task-scoped GitHub token never sits world-readable under /tmp + // (kubestellar/hive#5065). + const { github_token: _omittedToken, ...taskFileRecord } = msg; + fs.writeFileSync(TASK_FILE, JSON.stringify(taskFileRecord, null, 2), { mode: 0o600 }); + try { fs.chmodSync(TASK_FILE, 0o600); } catch (_) { /* content is already token-free */ } send({ type: 'task_accepted', seq: nextSeq(), task_id: msg.task_id, task_gen: msg.task_gen }); if (CONTRIBUTOR_MODE === MODE_HEADLESS) { // Non-interactive path (kubestellar/hive#2538): drive a one-shot CLI diff --git a/bin/contributor-relay.test.js b/bin/contributor-relay.test.js index 209e7c51b..63147bbb5 100644 --- a/bin/contributor-relay.test.js +++ b/bin/contributor-relay.test.js @@ -823,6 +823,35 @@ test('task_assign queues rather than typing when the CLI is not ready', () => { } finally { teardown(relay); } }); +test('task_assign never persists github_token to the task file (kubestellar/hive#5065)', () => { + const relay = loadRelay({ backend: 'copilot' }); + try { + relay.setCliReady(false); + relay.setPendingTask(null); + relay.handleMessage(JSON.stringify({ + type: 'task_assign', + task_id: 'ct-token-1', + kind: 'issue', + repo: 'foo/bar', + number: 422, + title: 'token hygiene', + prompt: 'do a thing', + github_token: `ghs_${'a'.repeat(36)}`, + token_expires_at: '2099-01-01T00:00:00Z', + })); + + const taskFile = path.join(relay.__tmpDir, 'contributor-task.json'); + const raw = fs.readFileSync(taskFile, 'utf8'); + assert.ok(!raw.includes('ghs_'), 'task file must not contain the credential value'); + const persisted = JSON.parse(raw); + assert.ok(!('github_token' in persisted), 'github_token key must be stripped from the task file'); + assert.strictEqual(persisted.token_expires_at, '2099-01-01T00:00:00Z', + 'non-secret task fields must survive the strip'); + const mode = fs.statSync(taskFile).mode & 0o777; + assert.strictEqual(mode, 0o600, `task file must be owner-only, got 0o${mode.toString(8)}`); + } finally { teardown(relay); } +}); + test('auth_response includes optional HIVE_AGENT_ROLE', () => { const relay = loadRelay({ env: { HIVE_AGENT_ROLE: 'scanner' } }); try {