// Offline upgrade contract: real bus verbs, pinned proxy, no real credentials. import fs from 'node:fs'; import path from 'node:path'; import net from 'node:net'; import assert from 'node:assert/strict'; import { spawn } from 'node:child_process'; import { createHash } from 'node:crypto'; import { getPriority } from 'node:os'; import { initialConfig } from '../../../../packages/model-proxy/src/settings.mjs'; import { PIN } from '../../../../packages/model-proxy/src/common.mjs'; import { ManagementClient } from '../../../../packages/model-proxy/src/client.mjs'; import { ProxyPage } from '../../../../packages/bus/src/proxy-page.mjs'; import { Broker } from '../../../../packages/bus/src/broker.mjs'; import { Store } from '../../../../packages/bus/src/store.mjs'; const [root, binary] = process.argv.slice(2); if (process.env.P4_UPGRADE_ISOLATED !== 'yes' || !root?.startsWith('/mnt/storage/scratch/tmp/')) throw Error('wrapper required'); if (createHash('sha256').update(fs.readFileSync(binary)).digest('hex') !== PIN.binary.sha256) throw Error('binary does not match PIN'); const reserve = net.createServer(); await new Promise(r => reserve.listen(0, '127.0.0.1', r)); const port = reserve.address().port; await new Promise(r => reserve.close(r)); if (port === 8317) throw Error('forbidden port'); const management = 'p4-management', canary = 'p4-upstream'; const config = initialConfig({ dataRoot: root, port, key: management, sentinel: 'msk-fixture-sentinel-x' }); config.management['disable-control-panel'] = true; config.observability.logs['logging-to-file'] = false; fs.mkdirSync(path.join(root, 'proxy/auth'), { recursive: true, mode: 0o700 }); fs.mkdirSync(path.join(root, 'home')); fs.writeFileSync(path.join(root, 'config.yaml'), JSON.stringify(config), { mode: 0o600 }); const child = spawn(binary, ['-config', path.join(root, 'config.yaml'), '-local-model'], { cwd: root, env: { HOME: path.join(root, 'home'), PATH: '/usr/bin:/bin' }, stdio: 'ignore', }); // Diagnostic-only (sage-4432): observe the same run conditions, never change // priority or print exception values, fixture credentials or upstream content. let stage = 'binary-start', lastClientErrorCode = null; const clientErrorCodes = ['proxy-client-down', 'proxy-client-response', 'proxy-client-refused', 'proxy-client-redirect', 'proxy-client-invalid']; const errorCodes = [...clientErrorCodes, 'ERR_ASSERTION', 'ERR_SQLITE_ERROR', 'SQLITE_ERROR', 'proxy-down', 'not-configured', 'human-required', 'invalid-request', 'invalid-business', 'priority-invalid', 'priority-unconfirmed', 'priority-write-refused', 'login-state-unknown']; function schedulingPolicy(pid) { try { // Only our Node process or the still-live child we spawned. Never print // raw /proc content; retain just Linux's numeric scheduling-policy field. const policy = fs.readFileSync(`/proc/${pid}/sched`, 'utf8').match(/^policy\s*:\s*(\d+)\s*$/m)?.[1]; return policy === undefined ? null : Number(policy); } catch { return null; } } function observe() { let nodePriority = null, childPriority = null, childSchedulingPolicy = null; try { nodePriority = getPriority(); } catch {} if (Number.isInteger(child.pid) && child.exitCode === null && child.signalCode === null) { try { childPriority = getPriority(child.pid); } catch {} childSchedulingPolicy = schedulingPolicy(child.pid); } console.log('P4 diagnostic ' + JSON.stringify({ stage, nodePriority, childPriority, lastClientErrorCode, nodeSchedulingPolicy: schedulingPolicy(process.pid), childSchedulingPolicy, childExitCode: Number.isInteger(child.exitCode) ? child.exitCode : null, childSignal: /^SIG[A-Z0-9]{1,16}$/.test(child.signalCode ?? '') ? child.signalCode : null })); } child.once('exit', observe); let store; const pause = ms => new Promise(r => setTimeout(r, ms)); const receipts = [], answers = []; const client = new ManagementClient({ baseUrl: `http://127.0.0.1:${port}`, key: management, receipt: r => receipts.push(r) }); try { console.log('P4 offline upgrade version=' + PIN.version + ' nice=' + getPriority()); observe(); let ready = false; for (let n = 0; n < 100; n++) { try { await client.getCredentials(); ready = true; break; } catch (error) { lastClientErrorCode = clientErrorCodes.includes(error.code) ? error.code : 'unclassified'; await pause(50); } } assert.equal(ready, true, 'fixture listener starts'); stage = 'management-ready'; observe(); stage = 'store-open'; store = new Store(root); observe(); const page = new ProxyPage({ client, instanceTag: 'fixture' }); const broker = new Broker({ store, proxyPage: (...args) => page.request(...args), businesses: { demo: { id: 'demo', human: 'owner', roles: { coder: { authority: { withinRole: [], crossRole: [] } } }, arbiters: { technical: 'coder', delivery: 'coder' } }, } }); const cap = broker.bindHuman({ business: 'demo', human: 'owner', via: 'cli', outsideAgent: true }); stage = 'broker-bound'; observe(); const call = async (verb, args = {}) => { const answer = await broker.request(cap, { verb, args }); answers.push(answer); return answer; }; stage = 'initial-accounts'; observe(); // Diagnostic-only (sage-4448/4452, mos-1683): one direct empty-provider // read, types/counts only. Never print field names, values or error text. const receiptCount = receipts.length; try { const groups = await client.getUpstreamGroups(); const isArray = Array.isArray(groups); console.log('P4 empty-provider diagnostic ' + JSON.stringify({ httpStatus: receipts.length === receiptCount + 1 ? receipts.at(-1).status : null, type: groups === null ? 'null' : typeof groups, isArray, count: isArray ? groups.length : groups !== null && typeof groups === 'object' ? Object.keys(groups).length : 0, })); } catch (error) { console.log('P4 empty-provider diagnostic ' + JSON.stringify({ httpStatus: receipts.length === receiptCount + 1 ? receipts.at(-1).status : null, code: clientErrorCodes.includes(error.code) ? error.code : 'proxy-client-error', })); } assert.deepEqual((await call('proxy.accounts')).accounts, []); console.log('proxy.accounts initial empty; GET /credentials + GET /config/api-keys/claude'); for (const provider of ['claude', 'codex']) { const login = await call('proxy.login.start', { provider }); assert.equal(typeof login.url, 'string'); assert.equal(typeof login.state, 'string'); console.log('proxy.login.start ' + provider + ' GET /oauth/auth-url; fields=url,state only'); const redirect = provider === 'claude' ? 'http://localhost:54545/callback' : 'http://localhost:1455/auth/callback'; const submitted = await call('proxy.login.submit', { redirect_url: redirect + '?state=' + encodeURIComponent(login.state) + '&code=p4-fake-code' }); assert.notEqual(submitted.status, 'ok'); const status = await call('proxy.login.status', { state: login.state }); assert.notEqual(status.status, 'ok'); console.log('proxy.login.submit POST /oauth/callback; fake code never ok; GET /oauth/status=' + status.status); if (status.status === 'wait') { assert.equal((await call('proxy.login.cancel', { state: login.state })).status, 'cancelled'); console.log('proxy.login.cancel DELETE /oauth/session confirmed'); } } await call('proxy.apikey.add', { provider: 'z.ai', key: canary }); await pause(200); const accounts = (await call('proxy.accounts')).accounts; assert.equal(accounts.length, 1); assert.equal(accounts[0].provider, 'z.ai'); assert.deepEqual(Object.keys(accounts[0]).sort(), ['accountId', 'displayName', 'priority', 'provider', 'status']); console.log('proxy.apikey.add PUT /config/api-keys/claude; account fields=accountId,provider,displayName,status,priority; id from auth_index'); await call('proxy.priority.set', { accountId: accounts[0].accountId, priority: 7 }); assert.equal((await call('proxy.accounts')).accounts[0].priority, 7); console.log('proxy.priority.set API-key PUT /config/api-keys/claude; priority read-back=7'); const keys = await call('proxy.keys'); assert.equal(keys.keys.length, 1); assert.equal(keys.keys[0].management, 'sentinel'); assert.match(keys.keys[0].keyId, /^[a-f0-9]{12}$/); console.log('proxy.keys GET /config/access/api-keys; fields=keyId,management,agent,issuedAt,state; sentinel labelled; values absent'); // Fixture-only upload exercises the OAuth priority API relied on by the page. const uploaded = await fetch(`http://127.0.0.1:${port}/v8/management/credentials?name=p4-fixture.json`, { method: 'POST', headers: { Authorization: `Bearer ${management}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'claude', access_token: 'p4-access', refresh_token: 'p4-refresh', expired: '2099-01-01T00:00:00Z' }), signal: AbortSignal.timeout(5000), redirect: 'error', }); assert.equal(uploaded.status, 200); await uploaded.body?.cancel(); await pause(200); const oauth = (await call('proxy.accounts')).accounts.find(a => a.provider === 'claude'); assert.ok(oauth); await call('proxy.priority.set', { accountId: oauth.accountId, priority: 8 }); assert.equal((await call('proxy.accounts')).accounts.find(a => a.accountId === oauth.accountId).priority, 8); console.log('proxy.priority.set OAuth PATCH /credentials/fields body={name,priority}; priority read-back=8'); for (const value of [canary, management, 'p4-access', 'p4-refresh', 'p4-fake-code']) { assert.equal(JSON.stringify(receipts).includes(value), false, 'secret absent from receipts'); assert.equal(JSON.stringify(store.all('SELECT body FROM events')).includes(value), false, 'secret absent from bus events'); assert.equal(JSON.stringify(answers).includes(value), false, 'credential/code absent from bus replies'); } console.log('PASS offline native-page bus contract and secret-free receipts'); } catch (error) { // Never print assertion actual/expected values or a raw upstream exception. observe(); const errorClass = ['Error', 'AssertionError', 'ProxyError', 'BusError', 'TypeError', 'RangeError'].includes(error.name) ? error.name : 'Error'; console.error('P4 upgrade FAILED ' + JSON.stringify({ stage, errorClass, code: errorCodes.includes(error.code) ? error.code : 'acceptance-failed' })); process.exitCode = 1; } finally { store?.close(); child.kill('SIGTERM'); await Promise.race([new Promise(r => child.once('exit', r)), pause(3000).then(() => child.kill('SIGKILL'))]); } // One end-to-end acceptance phase, emitted only after assertions and cleanup. console.log(`# tests 1\n# pass ${process.exitCode ? 0 : 1}\n# fail ${process.exitCode ? 1 : 0}\n# skipped 0\n# cancelled 0`);