feat(settings): add Codex link controls and authenticated phone bridge

This commit is contained in:
yilsem committed 2026-10-09 11:55:16 +03:00
1 parent 1acb5dd90f
commit 55a1beaf9d
22 files changed
+2857 -3

No files matched your search

@@ -0,0 +1,173 @@
import { createHash } from 'node:crypto'
import { EventEmitter } from 'node:events'
import { PassThrough } from 'node:stream'
import { describe, expect, it, vi } from 'vitest'
import { parseControlResponse, validateControlRequest } from '../codex-link-control'
import controlSchema from '../codex-link-control.schema.json'
import { CODEX_LINK_CONTROL_SCHEMA_HASH } from '../codex-link-control.types'
import type { CodexLinkControlRequest } from '../codex-link-control.types'
import { canonicalJson } from '../codex-link-json'
import { createCodexLinkClient } from './codex-link-client'
const request: CodexLinkControlRequest = {
contract_version: 'codex-link/1',
kind: 'settings_get',
request_id: 'req-1',
operation_id: 'op-1'
}
const response = {
...request,
kind: 'settings_result',
settings: {
settings_revision: 1,
enabled: true,
compression_enabled: true,
threshold_tokens: 250000,
source_binding_handle: null,
compact_route_id: null,
legacy_intent_imported: false,
ownership_manifest_revision: 0,
activation_state: 'pending_runtime_acceptance'
},
available_sources: []
}
function harness(reply: string = JSON.stringify(response), env: Record<string, string> = {}) {
let stdin = ''
const child = Object.assign(new EventEmitter(), {
stdout: new PassThrough(),
stderr: new PassThrough(),
stdin: new PassThrough(),
kill: vi.fn()
})
child.stdin.on('data', chunk => {
stdin += chunk.toString()
})
child.stdin.on('finish', () =>
queueMicrotask(() => {
const bytes = Buffer.from(reply)
// UTF-8 chunks may end inside a multi-byte character.
for (const byte of bytes) {
child.stdout.write(Buffer.from([byte]))
}
child.stderr.write('diagnostic that must not become a response')
child.emit('close', 0)
})
)
const spawn = vi.fn(() => child)
const control = createCodexLinkClient({
spawn,
existsSync: () => true,
platform: 'win32',
env: { LOCALAPPDATA: 'C:\\Users\\Example User\\AppData\\Local', SystemRoot: 'C:\\Windows', ...env }
})
return { control, spawn, child, stdin: () => stdin }
}
describe('Codex connection control boundary', () => {
it('pins the generated control-only schema', () => {
expect('sha256:' + createHash('sha256').update(canonicalJson(controlSchema)).digest('hex')).toBe(CODEX_LINK_CONTROL_SCHEMA_HASH)
expect(controlSchema.$defs).not.toHaveProperty('runtime_attach')
expect(controlSchema.$defs).not.toHaveProperty('checkpoint_commit')
})
it('uses only fixed command arguments and sends the exact request on stdin with EOF', async () => {
const h = harness()
expect(await h.control(request)).toEqual(response)
expect(h.stdin()).toBe(JSON.stringify(request) + '\n')
const [file, args, options] = h.spawn.mock.calls[0] as unknown as [string, string[], any]
expect(file).toBe('C:\\Windows\\System32\\cmd.exe')
expect(args).toEqual([
'/d',
'/s',
'/c',
'""C:\\Users\\Example User\\AppData\\Local\\TurkServis\\AITURK\\program\\bin\\aiturk.cmd" codex-link control --stdin"'
])
expect(options).toMatchObject({ shell: false, windowsHide: true, stdio: ['pipe', 'pipe', 'pipe'] })
})
it('rejects runtime and arbitrary path/credential commands before spawning', async () => {
const h = harness()
await expect(h.control({ ...request, kind: 'runtime_attach' })).rejects.toThrow()
await expect(h.control({ ...request, path: 'C:\\private' })).rejects.toThrow()
expect(h.spawn).not.toHaveBeenCalled()
})
it('rejects shell expansion in the entrypoint path', async () => {
const h = harness(undefined, { LOCALAPPDATA: 'C:\\Users\\%PATH%\\Local' })
await expect(h.control(request)).rejects.toThrow('invalid_entrypoint')
expect(h.spawn).not.toHaveBeenCalled()
})
it('binds the response to both request and operation identity', () => {
expect(() =>
parseControlResponse(JSON.stringify({ ...response, operation_id: 'other-chat-op' }), request)
).toThrow()
expect(() => parseControlResponse(JSON.stringify({ ...response, request_id: 'other-request' }), request)).toThrow()
})
it('rejects duplicate keys, floats, trailing JSON and invalid UTF8', () => {
expect(() =>
parseControlResponse(
JSON.stringify(response).replace('"settings_revision":1', '"settings_revision":1,"settings_revision":2'),
request
)
).toThrow('duplicate_json_key')
expect(() => parseControlResponse(JSON.stringify(response).replace('250000', '250000.0'), request)).toThrow(
'floats_or_non_json_type'
)
expect(() => parseControlResponse(JSON.stringify(response) + '{}', request)).toThrow()
expect(() => parseControlResponse(new Uint8Array([0xc3, 0x28]), request)).toThrow()
})
it('rejects oversized requests and unsafe integer settings', () => {
expect(() =>
validateControlRequest({
...request,
kind: 'settings_update',
expected_settings_revision: 1,
patch: { threshold_tokens: Number.MAX_SAFE_INTEGER + 1 }
})
).toThrow()
expect(() => validateControlRequest({ ...request, operation_id: 'x'.repeat(70000) })).toThrow()
})
it('does not retry a write after a broken reply', async () => {
const h = harness('broken')
await expect(h.control({ ...request, kind: 'catalog_sync' })).rejects.toThrow('outcome_unknown')
expect(h.spawn).toHaveBeenCalledTimes(1)
})
it('preserves a structured CLI rejection returned with exit code one', async () => {
const rejection = { ...request, kind: 'control_error', settings_revision: 2, error: { code: 'stale_settings_cas', message: 'Ayarlar değişti.', recovery_action: 'retry_control_read' } }
const h = harness()
h.child.stdin.removeAllListeners('finish')
h.child.stdin.on('finish', () => queueMicrotask(() => {
h.child.stdout.write(JSON.stringify(rejection))
h.child.emit('close', 1)
}))
expect(await h.control(request)).toEqual(rejection)
expect(h.spawn).toHaveBeenCalledTimes(1)
})
it('preserves exact Unicode when response bytes arrive separately', async () => {
const h = harness(
JSON.stringify({
...response,
available_sources: [{ source_binding_handle: 'source-a', label: 'Türkçe é/é 🧪', authorized: true }]
})
)
const result = await h.control(request)
expect(result.kind === 'settings_result' && result.available_sources[0].label).toBe('Türkçe é/é 🧪')
})
it('bounds stdout and terminates only the short proxy', async () => {
const h = harness('x'.repeat(1024 * 1024 + 1))
// Avoid a million small chunks: replace the synthetic finish listener.
h.child.stdin.removeAllListeners('finish')
h.child.stdin.on('finish', () => queueMicrotask(() => h.child.stdout.emit('data', Buffer.alloc(1024 * 1024 + 1))))
await expect(h.control(request)).rejects.toThrow('outcome_unknown')
expect(h.child.kill).toHaveBeenCalledTimes(1)
})
})
+132
View File
@@ -0,0 +1,132 @@
import { CODEX_LINK_RESPONSE_BYTES, parseControlResponse, validateControlRequest } from '../codex-link-control'
import type { CodexLinkControlResponse } from '../codex-link-control.types'
import { type AiturkCliRunnerDeps, resolveAiturkCliPath } from './aiturk-login'
type Dependencies = AiturkCliRunnerDeps
// Only the fixed entrypoint and fixed arguments go through cmd.exe. All values travel on stdin.
// eslint-disable-next-line no-control-regex -- Reject control bytes in executable paths before cmd parsing.
const UNSAFE_PATH = /["%^!\u0000-\u001f]/
const ABSOLUTE_PATH = /^[A-Za-z]:\\/
export function createCodexLinkClient(deps: Dependencies) {
const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms))
const clearTimer = deps.clearTimer ?? (timer => clearTimeout(timer as ReturnType<typeof setTimeout>))
return async function control(raw: unknown): Promise<CodexLinkControlResponse> {
validateControlRequest(raw)
const cli = resolveAiturkCliPath(deps)
if (!cli) {
throw new Error('codex-link:cli_unavailable')
}
const systemRoot = String(deps.env.SystemRoot || deps.env.SYSTEMROOT || 'C:\\Windows').replace(/[\\/]+$/, '')
if (UNSAFE_PATH.test(cli) || UNSAFE_PATH.test(systemRoot) || !ABSOLUTE_PATH.test(systemRoot)) {
throw new Error('codex-link:invalid_entrypoint')
}
return new Promise((resolve, reject) => {
let settled = false
let timer: unknown
let bytes = 0
const chunks: Buffer[] = []
let child: any
const finish = (error?: Error, response?: CodexLinkControlResponse) => {
if (settled) {
return
}
settled = true
if (timer !== undefined) {
clearTimer(timer)
}
error ? reject(error) : resolve(response!)
}
try {
child = deps.spawn(
`${systemRoot}\\System32\\cmd.exe`,
['/d', '/s', '/c', `""${cli}" codex-link control --stdin"`],
{
shell: false,
windowsHide: true,
windowsVerbatimArguments: true,
stdio: ['pipe', 'pipe', 'pipe']
}
)
} catch {
finish(new Error('codex-link:cli_unavailable'))
return
}
timer = setTimer(() => {
// The durable manager may have accepted a write. Never repeat it under a new ID.
finish(new Error('codex-link:outcome_unknown'))
try {
child.kill()
} catch {
/* proxy already exited; manager is independent */
}
}, deps.timeoutMs ?? 30_000)
child.stdout.on('data', (chunk: Buffer) => {
if (settled) {
return
}
bytes += chunk.length
if (bytes > CODEX_LINK_RESPONSE_BYTES) {
finish(new Error('codex-link:outcome_unknown'))
try {
child.kill()
} catch {
/* no process tree termination */
}
} else {
chunks.push(Buffer.from(chunk))
}
})
// Drain concurrently, with no history, paths, credentials or uncontrolled diagnostics logged.
child.stderr.on('data', () => {})
child.stdin.on('error', () => {})
child.once('error', () => finish(new Error('codex-link:outcome_unknown')))
child.once('close', (code: number | null) => {
if (settled) {
return
}
if (code !== 0 && code !== 1) {
finish(new Error('codex-link:outcome_unknown'))
return
}
try {
const response = parseControlResponse(Buffer.concat(chunks), raw)
if (code === 1 && response.kind !== 'control_error') {
finish(new Error('codex-link:outcome_unknown'))
} else {
finish(undefined, response)
}
} catch {
finish(new Error('codex-link:outcome_unknown'))
}
})
try {
child.stdin.end(JSON.stringify(raw) + '\n')
} catch {
finish(new Error('codex-link:outcome_unknown'))
}
})
}
}
+12
View File
@@ -36,6 +36,7 @@ import { WebSocket as RelayWebSocket } from 'ws'
import { classifyActiveRuntime, needsPackagedRuntimeUpgrade } from './active-runtime-state'
import { createAiturkCliRunner, createAiturkLogin } from './aiturk-login'
import { createCodexLinkClient } from './codex-link-client'
import { createMobileBackendResolver } from './mobile-backend'
import {
createMobileLink,
@@ -17266,6 +17267,16 @@ const aiturkLogin = createAiturkLogin({
now: () => Date.now()
})
const codexLinkControl = createCodexLinkClient({ spawn, existsSync: fs.existsSync, env: process.env })
ipcMain.handle('aiturk:codex-link:control', async (event, request) => {
const window = BrowserWindow.fromWebContents(event.sender)
if (!window || (window !== mainWindow && !instanceWindows.has(window)) ||
event.senderFrame !== event.sender.mainFrame) {
throw new Error('codex-link:unavailable')
}
return codexLinkControl(request)
})
// Only the primary window's top-level frame may drive a sign-in.
function assertAiturkLoginSender(event) {
const contents = mainWindow && !mainWindow.isDestroyed() ? mainWindow.webContents : null
@@ -17863,6 +17874,7 @@ function getMobileLink(): MobileLink {
const directory = path.join(app.getPath('userData'), 'aiturk-mobile', 'mobile')
mobileLink = createMobileLink({
codexLinkControl,
// Same vault file as before (mobile-secrets.bin, safeStorage); older fields are kept.
vault: new MobileVault(directory, safeStorage),
store: createMobileLinkFileStore(directory, path.join(directory, 'lan-link.json'), fs.promises),
@@ -271,6 +271,63 @@ async function startProxy(over: Record<string, unknown> = {}) {
return { proxy, port, identity, clock, logs, saved, ended, pair, ca: identity.cert }
}
describe('LAN proxy: narrow Codex control', () => {
const route = '/api/aiturk/codex-link/control'
const body = { contract_version: 'codex-link/1', kind: 'settings_get', request_id: 'phone-read-1', operation_id: 'phone-op-1' }
it('requires pairing before invoking local control and never starts a backend', async () => {
const calls: unknown[] = []
const t = await startProxy({ codexLinkControl: async request => { calls.push(request); return { ...request, kind: 'control_error', settings_revision: 0, error: { code: 'pending', message: 'Not ready', recovery_action: 'none' } } } })
const before = resolveCalls
try {
expect((await call(t.port, 'POST', route, { ca: t.ca, body })).status).toBe(401)
expect(calls).toHaveLength(0)
const phone = await t.pair()
const result = await call(t.port, 'POST', route, { ca: t.ca, token: phone.token, body })
expect(result.status).toBe(200)
expect(calls).toEqual([body])
expect(resolveCalls).toBe(before)
expect(JSON.stringify(calls)).not.toContain(phone.token)
} finally { await t.proxy.stop() }
})
it('rejects runtime commands, extra paths, duplicate keys and large bodies', async () => {
let calls = 0
const t = await startProxy({ codexLinkControl: async () => { calls++; throw new Error('must not execute') } })
try {
const phone = await t.pair()
for (const rawBody of [
JSON.stringify({ ...body, kind: 'runtime_attach' }),
JSON.stringify({ ...body, path: 'C:\\private' }),
JSON.stringify(body).replace('"kind":"settings_get"', '"kind":"settings_get","kind":"status_get"'),
'x'.repeat(65537)
]) {
expect((await call(t.port, 'POST', route, { ca: t.ca, token: phone.token, rawBody })).status).toBe(400)
}
expect(calls).toBe(0)
} finally { await t.proxy.stop() }
})
it('returns a fixed unknown-outcome code without leaking CLI diagnostics', async () => {
const t = await startProxy({ codexLinkControl: async () => { throw new Error('private-key-and-path') } })
try {
const phone = await t.pair()
const result = await call(t.port, 'POST', route, { ca: t.ca, token: phone.token, body })
expect(result.status).toBe(503)
expect(result.json).toEqual({ error: 'codex_link_outcome_unknown' })
expect(result.text).not.toContain('private-key')
} finally { await t.proxy.stop() }
})
it('rejects revoked phones and unsupported methods', async () => {
let calls = 0
const t = await startProxy({ codexLinkControl: async () => { calls++; return {} } })
try {
const phone = await t.pair()
expect((await call(t.port, 'GET', route, { ca: t.ca, token: phone.token })).status).toBe(405)
await t.proxy.revoke(phone.deviceId)
expect((await call(t.port, 'POST', route, { ca: t.ca, token: phone.token, body })).status).toBe(401)
expect(calls).toBe(0)
} finally { await t.proxy.stop() }
})
})
describe('LAN proxy: pairing (hello / claim)', () => {
it('hello is unauthenticated and reports whether pairing is open', async () => {
const t = await startProxy()
+35
View File
@@ -42,6 +42,9 @@ import http from 'node:http'
import https from 'node:https'
import net from 'node:net'
import type { Duplex } from 'node:stream'
import { CODEX_LINK_REQUEST_BYTES, validateControlRequest } from '../codex-link-control'
import { parseStrictJson } from '../codex-link-json'
import type { CodexLinkControlRequest, CodexLinkControlResponse } from '../codex-link-control.types'
import { loadOrCreateLanIdentity, type MobileLanIdentity, type MobileLanVault } from './mobile-lan-cert'
import {
@@ -706,6 +709,7 @@ export interface MobileDeviceView {
export type PairingEndReason = 'claimed' | 'locked' | 'expired' | 'cancelled'
export interface LanProxyOptions {
codexLinkControl?: (request: CodexLinkControlRequest) => Promise<CodexLinkControlResponse>
tls: { key: string; cert: string }
/** Must resolve the LOCAL backend or throw (remote mode, backend down). */
resolveBackend: (profile: string) => Promise<MobileBackend>
@@ -1108,6 +1112,35 @@ export function createLanProxy(options: LanProxyOptions) {
return reply(req, res, path, 403, { error: 'forbidden' })
}
if (path === '/api/aiturk/codex-link/control') {
if (req.method !== 'POST') return reply(req, res, path, 405, { error: 'method_not_allowed' })
if (!options.codexLinkControl) return reply(req, res, path, 503, { error: 'codex_link_unavailable' })
if (!/^application\/json(?:;|$)/i.test(String(req.headers['content-type'] ?? ''))) {
return reply(req, res, path, 400, { error: 'invalid_request' })
}
let request: CodexLinkControlRequest
try {
if (Number(req.headers['content-length'] ?? 0) > CODEX_LINK_REQUEST_BYTES) throw new Error('large_body')
let size = 0
const chunks: Buffer[] = []
for await (const chunk of req) {
size += chunk.length
if (size > CODEX_LINK_REQUEST_BYTES) throw new Error('large_body')
chunks.push(Buffer.from(chunk))
}
const parsed = parseStrictJson(Buffer.concat(chunks), CODEX_LINK_REQUEST_BYTES)
validateControlRequest(parsed)
request = parsed
} catch { return reply(req, res, path, 400, { error: 'invalid_request' }) }
// Pairing can be revoked while a slow body is being read.
if (!devices.has(device.tokenHash)) return reply(req, res, path, 401, { error: 'unauthorized' })
try {
const result = await options.codexLinkControl(request)
if (!devices.has(device.tokenHash)) return reply(req, res, path, 401, { error: 'unauthorized' })
return reply(req, res, path, 200, result)
} catch { return reply(req, res, path, 503, { error: 'codex_link_outcome_unknown' }) }
}
const backend = await localBackend(url)
if (res.destroyed) return
@@ -1510,6 +1543,7 @@ export type MobileLinkEvent =
{ type: 'state'; state: MobileLinkState } | { type: 'paired'; name: string; state: MobileLinkState }
export interface MobileLinkDeps {
codexLinkControl?: (request: CodexLinkControlRequest) => Promise<CodexLinkControlResponse>
vault: MobileLanVault
store: MobileLinkStore
resolveBackend: (profile: string) => Promise<MobileBackend>
@@ -1820,6 +1854,7 @@ export function createMobileLink(deps: MobileLinkDeps) {
}
proxy = createLanProxy({
codexLinkControl: deps.codexLinkControl,
tls: identity,
resolveBackend: async profile => {
if (deps.isRemoteMode()) {
+1
View File
@@ -13,6 +13,7 @@ const hudNativeDrag = hudWindowing?.nativeDrag === true
const launchFlags = ipcRenderer.sendSync('hermes:launch-flags')
contextBridge.exposeInMainWorld('hermesDesktop', {
codexLink: { control: request => ipcRenderer.invoke('aiturk:codex-link:control', request) },
setUiLanguage: (locale: string) => ipcRenderer.send('hermes:ui-language', locale),
glassSupported: translucencySupport?.glass === true,
translucencySupported: translucencySupport?.translucency === true,