Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions PROJECT.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ architecture lives in [docs/architecture.md](docs/architecture.md) and
Current stage and priorities: [ROADMAP.md](ROADMAP.md). How decisions are made:
[docs/governance.md](docs/governance.md). Dated decisions: [decisions/](decisions/README.md).

## SDK authentication — 2026-11-04

#2398 binds readiness and setup to the existing resource profile, preserving its
paths and Task-role context. Known SDK authentication failures reach authRequired
without becoming code verdicts. No OAuth broker, credential copying or live login.

## Role App delivery — 2026-11-04

#2365 opts Project roles into explicit App connections and dedicated short-lived
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,6 @@ HTTP 入口为 `POST /api/bot`、事件为 `GET /api/project-events`,

项目角色可选择以独立 GitHub App 直接提交、开 Draft PR 和 Review;复用现有连接与资源配置。
见 [角色 App 配置、认证与恢复](docs/role-github-apps.md)。默认不启用,不需要全局 gh 登录。

SDK 鉴权失效请使用该 Resource 的配置终端处理;宿主登录不会自动同步到容器。
查看[登录态与资源 profile](docs/backend-operations.md#sdk登录态与资源profile),无需为每个角色/Task重新登录。
3 changes: 3 additions & 0 deletions README.zh.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,3 +95,6 @@ HTTP 入口为 `POST /api/bot`、事件为 `GET /api/project-events`,

项目角色可选择以独立 GitHub App 直接提交、开 Draft PR 和 Review;复用现有连接与资源配置。
见 [角色 App 配置、认证与恢复](docs/role-github-apps.md)。默认不启用,不需要全局 gh 登录。

SDK 鉴权失效请使用该 Resource 的配置终端处理;宿主登录不会自动同步到容器。
查看[登录态与资源 profile](docs/backend-operations.md#sdk登录态与资源profile),无需为每个角色/Task重新登录。
5 changes: 5 additions & 0 deletions ROADMAP.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
# FlyBot — Roadmap

## Resource authentication reliability — 2026-11-04

#2398: existing profile binding, correct SDK auth feedback and same-profile setup;
source/fixture acceptance does not prove a live OAuth cache has been repaired.

## Role App execution — 2026-11-04

#2365 targets delivery with trusted role binding, local/Docker credential helpers,
Expand Down
1 change: 1 addition & 0 deletions changelog.d/2398.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Bind SDK readiness and configuration terminals to the actual persistent resource profile, expose safe authentication failures such as reused refresh tokens, and make the SDK-resolved Codex CLI available for login. Preserve Task-role context and delivery evidence; do not copy credentials or automatically choose a recovery route.
2 changes: 1 addition & 1 deletion core/src/host/project-chief-control.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions core/src/host/project-harness-auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Model login belongs to an explicit resource profile, not a Task-role Session.
import { createHash } from 'node:crypto'
import { isAbsolute, join } from 'node:path'
const ownEntry = (object, key) => Object.hasOwn(object, key) ? object[key] : undefined
const hash = value => createHash('sha256').update(JSON.stringify(value)).digest('hex')
const legacyProfileHash = value => createHash('sha256').update(value).digest('hex')
export function harnessProfile(resource, project, resourceId, harness) {
const principalId = project.ownerUserId
const configured = ownEntry(ownEntry(resource.profiles ?? {}, principalId) ?? {}, harness)
const generated = harness !== 'flybot' && isAbsolute(resource.profileRoot ?? '') ? { home: join(resource.profileRoot, legacyProfileHash(`${project.id}\n${principalId}`).slice(0, 24), harness), configDir: join(resource.profileRoot, legacyProfileHash(`${project.id}\n${principalId}`).slice(0, 24), harness, 'config') } : null
const selected = configured ?? generated
if (!selected || !isAbsolute(selected.home ?? '') || !isAbsolute(selected.configDir ?? '')) throw Object.assign(Error('Resource Harness needs an explicitly authorized principal/profile and model'), { code: 'bot/harness' })
const profile = { id: hash([resourceId, resource.type, resource.container ?? null, resource.user ?? null, resource.runtimeDir ?? null, principalId, harness, selected.home, selected.configDir, selected.baseUrl ?? null, selected.envRefs ?? {}]), source: configured ? 'configured' : 'generated', resourceId, principalId, harness }
return { selected, principalId, profile }
}
const codes = new Set(['refresh_token_reused', 'refresh_token_expired', 'refresh_token_invalidated', 'invalid_refresh_token', 'invalid_grant', 'invalid_api_key', 'authentication_required', 'authentication_error'])
export function sdkAuthenticationFailure(value, harness) {
const error = value?.error && typeof value.error === 'object' ? value.error : value
let code = error?.code
const message = typeof error?.message === 'string' ? error.message.slice(0, 16000).toLowerCase() : ''
if (!codes.has(code)) {
code = [...codes].find(item => message.includes(item))
if (!code && /refresh token.{0,160}(?:already.{0,20}used|reused)/s.test(message)) code = 'refresh_token_reused'
if (!code && /refresh token.{0,80}(?:expired|invalidated|revoked)/s.test(message)) code = 'invalid_refresh_token'
if (!code && /(?:not logged in|please (?:log|sign) in|invalid (?:api key|x-api-key)|incorrect api key)/.test(message)) code = 'authentication_required'
if (!code && (error?.status === 401 || error?.statusCode === 401)) code = 'authentication_required'
}
return codes.has(code) ? { harness, code, reauthenticationRequired: true } : null
}
export function authenticationError(failure) {
return Object.assign(Error(`${failure.harness} authentication requires configuration on the selected resource profile (${failure.code})`), { code: 'bot/harness-auth', authenticationFailure: failure })
}
10 changes: 9 additions & 1 deletion core/src/host/project-harness-turn.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { sdkAuthenticationFailure, authenticationError } from './project-harness-auth.js'
import { fileURLToPath } from 'node:url'
import { validateCodexConfiguration } from './project-codex-config.js'
// One real SDK turn. The host owns Workflow completion and acceptance, never this driver.
export async function runHarnessTurn(request, { emit, permission, signal, validateCodex = validateCodexConfiguration, loadCodex = () => import('@openai/codex-sdk'), loadClaude = () => import('@anthropic-ai/claude-agent-sdk') }) {
async function executeHarnessTurn(request, { emit, permission, signal, validateCodex = validateCodexConfiguration, loadCodex = () => import('@openai/codex-sdk'), loadClaude = () => import('@anthropic-ai/claude-agent-sdk') }) {
const toolsDisabled = request.noTools || request.protocolCorrection === true
let text = '', completed = false, sessionId = request.sessionId ?? null, usage = null
if (request.harness === 'codex') {
Expand Down Expand Up @@ -33,6 +34,7 @@ export async function runHarnessTurn(request, { emit, permission, signal, valida
const { events } = await thread.runStreamed(request.prompt, { signal })
for await (const event of events) {
if (event.type === 'thread.started' && (request.sessionId && event.thread_id !== request.sessionId || request.forbiddenSessionId && event.thread_id === request.forbiddenSessionId)) throw Object.assign(new Error('Codex resumed a different Task-role thread'), { code: 'bot/context-resume' })
if (['turn.failed', 'error'].includes(event.type)) { const auth = sdkAuthenticationFailure(event, request.harness); if (auth) throw authenticationError(auth) }
await emit({ type: 'sdk-event', harness: request.harness, event })
if (event.type === 'thread.started') sessionId = event.thread_id
if (event.type === 'item.completed' && event.item?.type === 'agent_message') text = event.item.text
Expand Down Expand Up @@ -60,6 +62,7 @@ export async function runHarnessTurn(request, { emit, permission, signal, valida
try {
for await (const event of stream) {
if (event.session_id && (request.sessionId && event.session_id !== request.sessionId || request.forbiddenSessionId && event.session_id === request.forbiddenSessionId)) throw Object.assign(new Error('Claude returned an incompatible Task-role Session'), { code: 'bot/context-resume' })
if (event.type === 'result' && event.is_error) { const auth = sdkAuthenticationFailure({ message: event.result, code: event.error }, request.harness); if (auth) throw authenticationError(auth) }
await emit({ type: 'sdk-event', harness: request.harness, event })
if (event.session_id) sessionId = event.session_id
if (event.type === 'result') {
Expand All @@ -72,3 +75,8 @@ export async function runHarnessTurn(request, { emit, permission, signal, valida
if (!completed || !sessionId || signal?.aborted) throw new Error('SDK stream ended without a completed turn and bound Session')
return { text, sessionId, usage }
}

export async function runHarnessTurn(request, ports) {
try { return await executeHarnessTurn(request, ports) }
catch (error) { const auth = sdkAuthenticationFailure({ code: error?.authenticationFailure?.code }, request.harness) ?? sdkAuthenticationFailure(error, request.harness); if (auth) throw authenticationError(auth); throw error }
}
2 changes: 1 addition & 1 deletion core/src/host/project-harness-worker.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ async function turn(message) {
busy = false
write(response)
if (settings.probe) void shutdown()
} catch (error) { write({ type: 'failure', ...(message.turnId ? { turnId: message.turnId } : {}), reason: ['bot/harness-capability', 'bot/context-resume'].includes(error?.code) ? error.message : 'SDK execution failed; inspect the redacted event evidence and resource environment', ...(error?.code === 'bot/context-resume' ? { contextFailure: { code: error.code, kind: 'backend-session-mismatch', contextPreserved: true } } : {}), ...(error?.code === 'bot/harness-capability' ? { capabilityFailure: { code: error.code, kind: error.capability, stage: 'configuration', modelInvoked: false } } : {}) }) }
} catch (error) { write({ type: 'failure', ...(message.turnId ? { turnId: message.turnId } : {}), ...(error?.code === 'bot/harness-auth' ? { authenticationFailure: error.authenticationFailure } : {}), reason: ['bot/harness-capability', 'bot/context-resume', 'bot/harness-auth'].includes(error?.code) ? error.message : 'SDK execution failed; inspect the redacted event evidence and resource environment', ...(error?.code === 'bot/context-resume' ? { contextFailure: { code: error.code, kind: 'backend-session-mismatch', contextPreserved: true } } : {}), ...(error?.code === 'bot/harness-capability' ? { capabilityFailure: { code: error.code, kind: error.capability, stage: 'configuration', modelInvoked: false } } : {}) }) }
finally { busy = false }
}
input.on('line', line => {
Expand Down
Loading
Loading