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
11 changes: 5 additions & 6 deletions docs/development/managed-runtime-rollout.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,12 +118,11 @@ session, or sole remaining session as ownership or identity proof.
1. Run the dry run and resolve all blockers.
2. Run the repository's managed provider acceptance tests for the intended
provider/mode.
The P2-G04 isolated browser test harness currently requires
`FRESHELL_MANAGED_OPENCODE_AUTH_FILE` to point to its existing OpenCode
`auth.json`; its temporary server home cannot discover the host credential.
This is a test-harness-only legacy input, not the managed provider contract.
Production launches use `FRESHELL_MANAGED_<PROVIDER>_ONECLI_ENV_FILE` or
`FRESHELL_MANAGED_<PROVIDER>_ONECLI_AUTH_FILE` as typed OneCLI references.
The P2-G04 isolated browser test harness requires
`FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE` to point to the private
existing OpenCode `auth.json`; its temporary server home cannot discover
the host credential. This uses the same typed OneCLI reference as managed
provider launches.
3. Apply `managed-opt-in` with the current control epoch and a unique request ID.
4. Verify the returned inventory revision and rollout mode.
5. Open one managed agent for every provider currently marked
Expand Down
5 changes: 4 additions & 1 deletion packages/freshell-mcp-runtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,8 @@
"@modelcontextprotocol/sdk": "1.30.0",
"zod": "4.3.6"
},
"files": ["generated"]
"files": ["generated"],
"devDependencies": {
"esbuild": "0.28.2"
}
}
4 changes: 4 additions & 0 deletions pnpm-lock.yaml

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

1 change: 0 additions & 1 deletion scripts/testing/runtime-sandbox.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,6 @@ export function phase2BootstrapFiles(
}
for (const key of [
'FRESHELL_MANAGED_CLAUDE_CREDENTIAL_FILE',
'FRESHELL_MANAGED_OPENCODE_AUTH_FILE',
'FRESHELL_MANAGED_CODEX_AUTH_FILE',
]) {
addRegularFile(env[key]?.trim())
Expand Down
39 changes: 39 additions & 0 deletions test/e2e-browser/helpers/opencode-auth-file.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import fs from 'node:fs'
import path from 'node:path'

/** Require the explicit typed OneCLI auth-file grant used by isolated OpenCode gates. */
export function requireOpenCodeAuthFile(
env: NodeJS.ProcessEnv = process.env,
qualification = 'P2-G04',
): string {
const configured = env.FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE?.trim()
if (!configured) {
throw new Error(
`${qualification} requires FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE to point at a private OpenCode auth.json OneCLI grant`,
)
}

const authFile = path.resolve(configured)
let stat: fs.Stats
try {
stat = fs.lstatSync(authFile)
} catch {
throw new Error(`${qualification} OpenCode OneCLI auth grant is not an existing file: ${authFile}`)
}
if (!stat.isFile()) {
throw new Error(`${qualification} OpenCode OneCLI auth grant must be a regular file: ${authFile}`)
}
if ((stat.mode & 0o077) !== 0) {
throw new Error(`${qualification} OpenCode OneCLI auth grant must be private (mode 0600 or stricter): ${authFile}`)
}
if ((stat.mode & 0o400) === 0) {
throw new Error(`${qualification} OpenCode OneCLI auth grant must be owner-readable: ${authFile}`)
}
try {
const descriptor = fs.openSync(authFile, 'r')
fs.closeSync(descriptor)
return fs.realpathSync(authFile)
} catch {
throw new Error(`${qualification} OpenCode OneCLI auth grant is unreadable: ${authFile}`)
}
}
7 changes: 7 additions & 0 deletions test/e2e-browser/helpers/opencode-native-history.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ export type { NativeAssistantTurn, NativeHistory } from './provider-native-histo
export { nativeTurnProof } from './provider-native-history/proof.js'
import type { NativeAssistantTurn } from './provider-native-history/types.js'

export function openCodeCredentialFailureMessage(output: string): string | null {
if (/\bToken refresh failed:\s*401\b/i.test(stripVTControlCharacters(output))) {
return 'OpenCode credential refresh was rejected with HTTP 401; provide a currently valid OpenAI auth grant'
}
return null
}

/**
* Self-contained query-only form of the current OpenCode native-history reader.
* Runtime chaos tests execute it only through an ownership-checked provider
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,19 @@ import {
P2_OPENCODE_VERSION,
type ManagedRuntimeView,
} from '../helpers/managed-runtime.js'
import { requireOpenCodeAuthFile } from '../helpers/opencode-auth-file.js'
import { openPanePicker } from '../helpers/pane-picker.js'
import { TerminalHelper } from '../helpers/terminal-helpers.js'
import { TestHarness } from '../helpers/test-harness.js'
import { hasOpenCodePromptModelText, nativeTurnProof, openCodeTerminalReady, selectNativeAssistantTurn, type NativeAssistantTurn } from '../helpers/opencode-native-history.js'
import {
hasOpenCodePromptModelText,
nativeTurnProof,
openCodeCredentialFailureMessage,
openCodeTerminalReady,
OPENCODE_NATIVE_HISTORY_SCRIPT,
selectNativeAssistantTurn,
type NativeAssistantTurn,
} from '../helpers/opencode-native-history.js'
import type { ProviderQualificationRow } from '../../../scripts/testing/provider-qualification-receipt.js'

function leavesByMode(node: any, mode: string): any[] {
Expand All @@ -41,10 +50,13 @@ async function waitForValue<T>(
description: string,
probe: () => T | null | undefined | Promise<T | null | undefined>,
timeoutMs: number,
abortProbe?: () => Error | null | undefined | Promise<Error | null | undefined>,
): Promise<T> {
const deadline = Date.now() + timeoutMs
let lastError: unknown
while (Date.now() < deadline) {
const abortError = await abortProbe?.()
if (abortError) throw abortError
try {
const value = await probe()
if (value !== null && value !== undefined) return value
Expand Down Expand Up @@ -275,10 +287,8 @@ async function paneSessionId(

function nativeAssistantTurns(rig: ManagedRuntimeBrowserRig, view: ManagedRuntimeView, sessionId: string): NativeAssistantTurn[] {
if (!view.containerId) throw new Error('native evidence probe has no exact owned container')
const probe = path.join(rig.repoRoot, 'test/e2e-browser/helpers/provider-native-history/probe-cli.ts')
const tsxLoader = path.join(rig.repoRoot, 'node_modules/tsx/dist/loader.mjs')
const raw = rig.ownedProviderExec(view.containerId, [
'node', '--no-warnings', '--import', tsxLoader, probe, 'opencode',
'node', '--no-warnings', '-e', OPENCODE_NATIVE_HISTORY_SCRIPT,
'/home/freshell/provider/.local/share/opencode/opencode.db', sessionId,
])
const evidence = JSON.parse(raw)
Expand All @@ -288,12 +298,20 @@ function nativeAssistantTurns(rig: ManagedRuntimeBrowserRig, view: ManagedRuntim
}

async function nextNativeAssistantTurn(
page: Page,
terminalId: string,
rig: ManagedRuntimeBrowserRig, view: ManagedRuntimeView, sessionId: string,
priorMessageIds: ReadonlySet<string>, expectedText: string,
): Promise<NativeAssistantTurn> {
return waitForValue('the correlated completed native assistant response, not a rendered echo', () => (
selectNativeAssistantTurn(nativeAssistantTurns(rig, view, sessionId), priorMessageIds, expectedText)
), 180_000)
), 180_000, async () => {
const terminalOutput = await page.evaluate((id) => (
window.__FRESHELL_TEST_HARNESS__?.getTerminalBuffer?.(id) ?? ''
), terminalId)
const failure = openCodeCredentialFailureMessage(terminalOutput)
return failure ? new Error(failure) : undefined
})
}

function verifyMemoryAnswer(turn: NativeAssistantTurn, projectName: string): void {
Expand Down Expand Up @@ -345,11 +363,15 @@ test.describe.serial('OpenCode provider qualification', () => {
)
test.setTimeout(1_800_000)

const authFile = requireOpenCodeAuthFile(process.env, 'OpenCode provider qualification')
const blockerEvidence = runBlockerMatrixTests(process.cwd())
const rig = new ManagedRuntimeBrowserRig(
process.cwd(),
5,
{},
{
FRESHELL_BIND_HOST: '0.0.0.0',
FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE: authFile,
},
{ FRESHELL_RUNTIME_OBSERVER_INTERVAL_MS: '750' },
'release',
)
Expand Down Expand Up @@ -391,6 +413,13 @@ test.describe.serial('OpenCode provider qualification', () => {
.toBe(P2_OPENCODE_VERSION)
const processArgs = rig.ownedContainerProcessTable(first.view.containerId)
expect(processArgs).toContain(P2_OPENCODE_MODEL)
const authProbe = rig.ownedProviderExec(first.view.containerId, [
'node', '--no-warnings', '-e',
"const fs=require('node:fs');const auth=JSON.parse(fs.readFileSync('/home/freshell/provider/.local/share/opencode/auth.json','utf8'));const openai=auth.openai;if(!openai||typeof openai.access!=='string'||typeof openai.refresh!=='string')process.exit(2);process.stdout.write('OpenAI credential present')",
])
expect(authProbe.trim()).toBe('OpenAI credential present')
const availableModels = rig.ownedProviderExec(first.view.containerId, ['opencode', 'models', 'openai'])
expect(availableModels).toContain(P2_OPENCODE_MODEL.split('/')[1])
const exactLimits = cgroupLimitEvidence(rig, first.view)
expect(exactLimits.swapMax).toBe('0')

Expand All @@ -406,7 +435,7 @@ test.describe.serial('OpenCode provider qualification', () => {
await paneSessionId(harness, tabId, first.paneId)
), 120_000)
expect(nativeSessionId).toMatch(/^ses_/)
const firstAnswer = await nextNativeAssistantTurn(rig, first.view, nativeSessionId, new Set(), nonce)
const firstAnswer = await nextNativeAssistantTurn(page, first.terminalId, rig, first.view, nativeSessionId, new Set(), nonce)
verifyMemoryAnswer(firstAnswer, nonce)
const nativeTurnProofs = [nativeTurnProof('initial', nativeSessionId, firstAnswer, nonce)]

Expand Down Expand Up @@ -435,7 +464,7 @@ test.describe.serial('OpenCode provider qualification', () => {
await waitForReplacementPrompt(page, harness, rig, tabId, first.paneId, afterHostCrash)
const beforeRecall = new Set(nativeAssistantTurns(rig, afterHostCrash, nativeSessionId).map((turn) => turn.messageId))
await executeInPane(page, first.paneId, 'What is the name of the project we chose earlier?')
const recalledAnswer = await nextNativeAssistantTurn(rig, afterHostCrash, nativeSessionId, beforeRecall, nonce)
const recalledAnswer = await nextNativeAssistantTurn(page, first.terminalId, rig, afterHostCrash, nativeSessionId, beforeRecall, nonce)
verifyMemoryAnswer(recalledAnswer, nonce)
expect(recalledAnswer.messageId).not.toBe(firstAnswer.messageId)
nativeTurnProofs.push(nativeTurnProof('after_session_host_crash', nativeSessionId, recalledAnswer, nonce))
Expand Down Expand Up @@ -469,7 +498,7 @@ test.describe.serial('OpenCode provider qualification', () => {
await waitForReplacementPrompt(page, harness, rig, tabId, first.paneId, afterProviderCrash)
const beforeProviderFollowup = new Set(nativeAssistantTurns(rig, afterProviderCrash, nativeSessionId).map((turn) => turn.messageId))
await executeInPane(page, first.paneId, 'Please remind me of the project name we selected.')
const providerAnswer = await nextNativeAssistantTurn(rig, afterProviderCrash, nativeSessionId, beforeProviderFollowup, nonce)
const providerAnswer = await nextNativeAssistantTurn(page, first.terminalId, rig, afterProviderCrash, nativeSessionId, beforeProviderFollowup, nonce)
verifyMemoryAnswer(providerAnswer, nonce)
expect(providerAnswer.messageId).not.toBe(recalledAnswer.messageId)
nativeTurnProofs.push(nativeTurnProof('after_provider_process_crash', nativeSessionId, providerAnswer, nonce))
Expand All @@ -486,7 +515,7 @@ test.describe.serial('OpenCode provider qualification', () => {
const secondSessionId = await waitForValue('second exact OpenCode session id', async () => (
await paneSessionId(harness, tabId, second.paneId)
), 120_000)
const secondAnswer = await nextNativeAssistantTurn(rig, second.view, secondSessionId, new Set(), secondNonce)
const secondAnswer = await nextNativeAssistantTurn(page, second.terminalId, rig, second.view, secondSessionId, new Set(), secondNonce)
verifyMemoryAnswer(secondAnswer, secondNonce)
expect(secondAnswer.text).not.toContain(nonce)
expect(second.view.soulId).not.toBe(first.view.soulId)
Expand Down
22 changes: 6 additions & 16 deletions test/e2e-browser/specs/runtime-terminal-continuity-rust.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,23 +13,12 @@ import fs from 'node:fs'
import path from 'node:path'

import { ManagedRuntimeBrowserRig, P2_OPENCODE_MODEL, P2_OPENCODE_VERSION, type ManagedRuntimeView } from '../helpers/managed-runtime.js'
import { requireOpenCodeAuthFile } from '../helpers/opencode-auth-file.js'
import { OPENCODE_NATIVE_HISTORY_SCRIPT, type NativeHistory } from '../helpers/opencode-native-history.js'
import { TestHarness } from '../helpers/test-harness.js'
import { TerminalHelper } from '../helpers/terminal-helpers.js'
import { openPanePicker } from '../helpers/pane-picker.js'

function requireOpenCodeAuthFile(): string {
const configured = process.env.FRESHELL_MANAGED_OPENCODE_AUTH_FILE
if (!configured) {
throw new Error('P2-G04 requires FRESHELL_MANAGED_OPENCODE_AUTH_FILE to point at the existing OpenCode auth.json')
}
const authFile = path.resolve(configured)
if (!fs.statSync(authFile).isFile()) {
throw new Error(`P2-G04 OpenCode auth reference is not a file: ${authFile}`)
}
return authFile
}

function findTerminalLeaves(node: any, out: any[] = []): any[] {
if (!node) return out
if (node.type === 'leaf' && node.content?.kind === 'terminal') out.push(node)
Expand Down Expand Up @@ -334,12 +323,13 @@ test.describe.serial('Phase 2 managed runtime continuity', () => {
test('P2-G04: OpenAI-authenticated OpenCode tool turn survives web replacement in one native session', async ({ page }) => {
test.setTimeout(900_000)

// The rig runs the web server with an isolated HOME. Pass the host auth
// file explicitly; otherwise the managed runtime starts without the
// configured provider credential and OpenCode silently uses its default.
// The rig runs the web server with an isolated HOME. Pass the existing
// private auth file as a typed OneCLI grant; otherwise OpenCode silently
// uses its default model in the managed provider volume.
const authFile = requireOpenCodeAuthFile()
const rig = new ManagedRuntimeBrowserRig(process.cwd(), 2, {
FRESHELL_MANAGED_OPENCODE_AUTH_FILE: authFile,
FRESHELL_BIND_HOST: '0.0.0.0',
FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE: authFile,
})
try {
const info = await rig.start()
Expand Down
2 changes: 1 addition & 1 deletion test/runtime/gate-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@
{
"id": "P2-G04",
"required": true,
"procedure": "Start OpenCode 1.18.21 with openai/gpt-5.6-luna using the explicit FRESHELL_MANAGED_OPENCODE_AUTH_FILE reference to the configured OpenAI OAuth credential; verify the managed provider volume contains the OpenAI credential and the requested model is available; complete a first Bash-tool turn, begin a controlled long Bash tool, SIGKILL/restart web while the tool is running, then run a provider-side tool in a follow-up.",
"procedure": "Start OpenCode 1.18.21 with openai/gpt-5.6-luna using the explicit FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE reference to the configured private OpenAI OAuth credential; verify the managed provider volume contains the OpenAI credential and the requested model is available; complete a first Bash-tool turn, begin a controlled long Bash tool, SIGKILL/restart web while the tool is running, then run a provider-side tool in a follow-up.",
"pass_assertion": "Same native ses_* session, soul, OS incarnation, container and host boot; tool effects occur exactly once; provider launch count stays one; no paid-model fallback or CLI auto-update.",
"status": "NOT_RUN"
},
Expand Down
63 changes: 63 additions & 0 deletions test/unit/tooling/testing/opencode-auth-file.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// @vitest-environment node
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'

import { requireOpenCodeAuthFile } from '../../../e2e-browser/helpers/opencode-auth-file.js'

let root: string

beforeEach(() => {
root = fs.mkdtempSync(path.join(os.tmpdir(), 'opencode-auth-reference-'))
})

afterEach(() => {
fs.rmSync(root, { recursive: true, force: true })
})

describe('OpenCode qualification auth reference', () => {
it('fails fast when no explicit OneCLI auth grant is configured', () => {
expect(() => requireOpenCodeAuthFile({})).toThrow(/FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE/)
})

it('returns the resolved path of a private regular OneCLI auth grant', () => {
const authFile = path.join(root, 'auth.json')
fs.writeFileSync(authFile, '{}')
fs.chmodSync(authFile, 0o600)

expect(requireOpenCodeAuthFile({ FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE: authFile }))
.toBe(path.resolve(authFile))
})

it('rejects missing paths, directories, linked grants, and unusable file permissions', () => {
expect(() => requireOpenCodeAuthFile({
FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE: path.join(root, 'missing.json'),
})).toThrow(/existing file/)
expect(() => requireOpenCodeAuthFile({
FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE: root,
})).toThrow(/regular file/)

const privateFile = path.join(root, 'private.json')
fs.writeFileSync(privateFile, '{}', { mode: 0o600 })
const linkedFile = path.join(root, 'linked.json')
fs.symlinkSync(privateFile, linkedFile)
expect(() => requireOpenCodeAuthFile({
FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE: linkedFile,
})).toThrow(/regular file/)

const publicFile = path.join(root, 'public.json')
fs.writeFileSync(publicFile, '{}', { mode: 0o644 })
fs.chmodSync(publicFile, 0o644)
expect(() => requireOpenCodeAuthFile({
FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE: publicFile,
})).toThrow(/private/)

const unreadableFile = path.join(root, 'unreadable.json')
fs.writeFileSync(unreadableFile, '{}', { mode: 0o000 })
fs.chmodSync(unreadableFile, 0o000)
expect(() => requireOpenCodeAuthFile({
FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE: unreadableFile,
})).toThrow(/owner-readable/)
})
})
9 changes: 8 additions & 1 deletion test/unit/tooling/testing/opencode-native-history.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import os from 'node:os'
import path from 'node:path'
import { DatabaseSync } from 'node:sqlite'
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { hasOpenCodePromptModelText, nativeTurnProof, openCodeTerminalReady, selectNativeAssistantTurn } from '../../../e2e-browser/helpers/opencode-native-history.js'
import { hasOpenCodePromptModelText, nativeTurnProof, openCodeCredentialFailureMessage, openCodeTerminalReady, selectNativeAssistantTurn } from '../../../e2e-browser/helpers/opencode-native-history.js'
import { readOpenCodeNativeHistory } from '../../../e2e-browser/helpers/provider-native-history/opencode.js'

let root: string
Expand Down Expand Up @@ -36,6 +36,13 @@ function read(session = 'ses_owned') {
}

describe('live recovery proves new native assistant responses, never TUI echo or replay', () => {
it('surfaces an OpenAI token refresh rejection instead of waiting for the native response timeout', () => {
expect(openCodeCredentialFailureMessage('\u001b[31mToken refresh failed: 401\u001b[0m'))
.toMatch(/credential refresh was rejected.*401/i)
expect(openCodeCredentialFailureMessage('Token refresh failed: 403')).toBeNull()
expect(openCodeCredentialFailureMessage('GPT-5.6 Luna')).toBeNull()
})

it('reads only completed assistant messages for the exact native session', () => {
message('echo', 'ses_owned', 'user', 'nonce-in-echo')
message('unfinished', 'ses_owned', 'assistant', 'nonce-unfinished', null)
Expand Down
10 changes: 10 additions & 0 deletions test/unit/tooling/testing/runtime-amplifier-onecli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ describe('live Amplifier OneCLI qualification grants', () => {
expect(phase2BootstrapFiles(env)).toEqual([environmentGrant, authGrant])
})

it('admits only the typed OpenCode OneCLI auth grant, not the legacy raw auth path', () => {
const onecliGrant = privateGrant('onecli-auth.json')
const legacyAuth = privateGrant('legacy-auth.json')

expect(phase2BootstrapFiles({
FRESHELL_MANAGED_OPENCODE_ONECLI_AUTH_FILE: onecliGrant,
FRESHELL_MANAGED_OPENCODE_AUTH_FILE: legacyAuth,
})).toEqual([onecliGrant])
})

it('rejects a missing grant even when the old keys-file input is set', () => {
const legacyKeys = privateGrant('keys.env')
const env = { FRESHELL_MANAGED_AMPLIFIER_ONECLI_KEYS_FILE: legacyKeys }
Expand Down
Loading