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 .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,12 @@ jobs:

- name: Setup Bun
uses: oven-sh/setup-bun@v2
with:
# The pin (packageManager: bun@1.3.6) lives in the workspace, not at the
# repo root, so without this setup-bun installs the latest release.
# Bun 1.4.2 hung in `bun ci` for the whole 20-minute budget on every
# suite (#517, #518) before a single test ran.
bun-version-file: trios/agent-server/package.json

- name: Install dependencies
run: bun ci
Expand Down
4 changes: 3 additions & 1 deletion trios/agent-server/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ bower_components
build/Release

# Dependency directories
node_modules/
# No trailing slash: a symlinked node_modules is a file, and `node_modules/`
# let one pointing at a local Mac path be committed and hang CI's install.
node_modules
jspm_packages/

# Snowpack dependency directory (https://snowpack.dev/)
Expand Down
1 change: 0 additions & 1 deletion trios/agent-server/apps/server/node_modules

This file was deleted.

19 changes: 13 additions & 6 deletions trios/agent-server/apps/server/tests/__helpers__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,29 @@ export async function killProcessOnPort(port: number): Promise<void> {
try {
console.log(`Finding process on port ${port}...`)

const pids = execSync(`lsof -ti :${port}`, {
// LISTEN only, and never this process. A bare `lsof -i :port` also lists
// every client still connected to the port - including this test process,
// whose CDP socket to the previous file's browser outlives it - and the
// SIGTERM that followed ended the whole server-tools run (exit 143).
const pids = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'ignore'],
}).trim()
})
.split('\n')
.map((pid) => pid.trim())
.filter((pid) => pid !== '' && pid !== String(process.pid))
.join(' ')

if (pids) {
const pidList = pids.replace(/\n/g, ', ')
console.log(`Terminating process(es) ${pidList} on port ${port}...`)
console.log(`Terminating process(es) ${pids} on port ${port}...`)

try {
execSync(`kill -15 ${pids.replace(/\n/g, ' ')}`, {
execSync(`kill -15 ${pids}`, {
stdio: 'ignore',
})
await new Promise((resolve) => setTimeout(resolve, 500))
} catch {
execSync(`kill -9 ${pids.replace(/\n/g, ' ')}`, {
execSync(`kill -9 ${pids}`, {
stdio: 'ignore',
})
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -541,7 +541,9 @@ describe('the salvage commit', () => {
expect(head).toContain('trios/docs/a/new.md')
expect(head).not.toContain('trios/docs/z/old.md')
rmSync(f.scratch, { recursive: true, force: true })
})
// Real git over 205 files: ~0.2 s here, but once over bun's 5 s default on
// a loaded CI runner (2026-10-01) while passing on the runs either side.
}, 30_000)
})

// ---------------------------------------------------------------------------
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import {
const source = readServerSource()
const report = auditServer(source, DEFAULT_ALLOWLIST)

// Regression pin for the --no-allowlist run: exactly these seven mounts carry
// Regression pin for the --no-allowlist run: exactly these nine mounts carry
// no guard today, each for a reason the comments beside the mount give.
// RE-MEASURED 2026-09-13: /api/inngest joined. It is not a shell - it is the
// Queen's scheduler endpoint - and it is unguarded on purpose: Inngest signs
Expand All @@ -39,6 +39,7 @@ const report = auditServer(source, DEFAULT_ALLOWLIST)
const EXPECTED_UNGUARDED_WITHOUT_ALLOWLIST = [
'/api/inngest',
'/health',
'/queen/contributor-keys',
'/queen/dashboard',
'/queen/feed',
'/queen/hq',
Expand Down Expand Up @@ -84,7 +85,10 @@ describe('route-guard audit over src/api/server.ts', () => {
// where a route named `public` belongs. Every other number here is
// unchanged, which is the part worth stating: no guarded route quietly lost
// its guard to make room for it.
expect(report.totalMounts).toBe(45)
// RE-MEASURED 2026-10-02: 45 became 46 with /queen/contributor-keys
// (#522), a server-to-server route behind its own capability token. It is
// allowlisted with that reason; no other number moved.
expect(report.totalMounts).toBe(46)
expect(report.prefixGuardCount).toBe(18)
expect(report.guardedSubAppCount).toBe(15)
expect(report.publicReadCount).toBe(8)
Expand All @@ -97,15 +101,15 @@ describe('route-guard audit over src/api/server.ts', () => {
expect(report.entriesMissingReason).toEqual([])
})

it('reports exactly the seven reasoned exceptions when the allowlist is dropped', () => {
it('reports exactly the nine reasoned exceptions when the allowlist is dropped', () => {
// The classifier reports mounts in file order; the assertion is on the
// exact set, so both sides are sorted before comparing.
expect([...unguardedMounts(source, [])].sort()).toEqual(
[...EXPECTED_UNGUARDED_WITHOUT_ALLOWLIST].sort(),
)
})

it('splits the twenty-two /queen mounts into 8 public-read, 8 wrapper-guarded and 6 allowlisted shells', () => {
it('splits the twenty-three /queen mounts into 8 public-read, 8 wrapper-guarded and 7 allowlisted', () => {
const queenMounts = classifyMounts(source).filter(
(mount) => mount.path === '/queen' || mount.path.startsWith('/queen/'),
)
Expand All @@ -127,7 +131,11 @@ describe('route-guard audit over src/api/server.ts', () => {
// issue title, no worker text and no credential: only a key's INDEX ever
// reaches the database, so there is nothing here a stranger could read that
// the board does not already show.
expect(queenMounts.length).toBe(22)
// RE-MEASURED 2026-10-02: twenty-two became twenty-three. The seventh
// allowlisted mount is /queen/contributor-keys (#522), which serves data
// only to a caller holding QUEEN_CONTRIBUTOR_PROXY_TOKEN - its own guard,
// not the trusted-origin one, because its caller is a server.
expect(queenMounts.length).toBe(23)

const counts: Record<string, number> = {
'public-read': 0,
Expand All @@ -144,7 +152,7 @@ describe('route-guard audit over src/api/server.ts', () => {
'public-read': 8,
'prefix-guard': 0,
wrapper: 8,
unguarded: 6,
unguarded: 7,
})

// Every unguarded /queen mount must be one of the allowlisted shells.
Expand Down
7 changes: 3 additions & 4 deletions trios/agent-server/apps/server/tests/tools/dom.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -400,10 +400,9 @@ describe('search_dom', () => {
const newResult = await execute(new_page, { url: RICH_PAGE })
const pageId = pageIdOf(newResult)

const result = await execute(search_dom, {
page: pageId,
query: '.nav-link',
})
// Same load race searchUntil exists for: CI found fewer than 3 once
// (2026-10-01) on a run where the identical query passed before.
const result = await searchUntil(execute, pageId, '.nav-link', 'Found 3')
assert.ok(!result.isError, textOf(result))
const text = textOf(result)
assert.ok(text.includes('Found 3'), 'Should find exactly 3 nav links')
Expand Down
10 changes: 9 additions & 1 deletion trios/agent-server/apps/server/tests/tools/navigation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,15 @@ describe('navigation tools', () => {

it('wait_for finds text on page', async () => {
await withBrowser(async ({ execute }) => {
const newResult = await execute(new_page, { url: 'https://example.com' })
// The text arrives half a second after load, from the page itself, so
// this still exercises the waiting - without depending on the live
// https://example.com, which CI's browser never showed it on.
const page = `<script>setTimeout(function () {
document.body.innerHTML = '<h1>Example Domain</h1>'
}, 500)</script>`
const newResult = await execute(new_page, {
url: `data:text/html,${encodeURIComponent(page)}`,
})
const pageId = structuredOf<{ pageId: number }>(newResult).pageId

const waitResult = await execute(wait_for, {
Expand Down
11 changes: 10 additions & 1 deletion trios/agent-server/apps/server/tests/tools/observation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,8 +157,17 @@ describe('observation tools', () => {

it('get_page_content returns markdown text', async () => {
await withBrowser(async ({ execute }) => {
const newResult = await execute(new_page, { url: 'https://example.com' })
// Constructed in place rather than fetched: reading https://example.com
// raced its own load (CI read it 57 ms after opening, before any text
// arrived), and the test is about extracting text, not about the network.
const newResult = await execute(new_page, { url: 'about:blank' })
const pageId = pageIdOf(newResult)
await execute(evaluate_script, {
page: pageId,
expression: `document.body.innerHTML = ${JSON.stringify(
'<h1>Example Domain</h1><p>This domain is for use in documentation examples.</p>',
)}`,
})

const contentResult = await execute(get_page_content, { page: pageId })
assert.ok(!contentResult.isError, textOf(contentResult))
Expand Down
5 changes: 5 additions & 0 deletions trios/tools/route-guard-audit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,11 @@ export const DEFAULT_ALLOWLIST = [
reason:
'shell only — the operator page holds no state and no token; its numbers come from /queen/lease and its one action POSTs there with a bearer the reader supplies, so both stay guarded (comment at the mount)',
},
{
path: '/queen/contributor-keys',
reason:
'own capability - a server-to-server route for the app render proxy, with no browser Origin; every request is refused unless its bearer equals QUEEN_CONTRIBUTOR_PROXY_TOKEN (at least 32 bytes, compared with timingSafeEqual) and it carries a verified x-queen-contributor-id, and the route is off while that token is unset (src/api/routes/queen-contributor-keys.ts, tests/api/queen-contributor-keys.test.ts)',
},
{
path: '/api/inngest',
reason:
Expand Down
Loading