mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
`ui-question`'s node half called `ctx.tools.register` on the host context. `ScopedLayers.merge()` combines the global layer with the agent's exact-scope layer, and an unscoped registration lands in the global one — so the tool reached every agent no matter which preset composed it. `core-web`, sold as a two-tool benchmark surface, really presented three. Rendering a question is a host UI capability; having the tool is an agent capability, and only a preset decides that. The node half is now empty and the `tool-ask-user` row moved into the preset that wants it. The TUI keeps its own row, having no presets. The composition tests now assert the global tool layer is EMPTY, which is the invariant that would have caught this: any tool outside a preset reaches every agent. The browser lane's composition, seeded-history, and hermetic-skill assertions address their registries through a composed agent for the same reason — those services are per session now, and the host cannot resolve an `isolate` realm by name.
100 lines
4.0 KiB
TypeScript
100 lines
4.0 KiB
TypeScript
// Boots the shipped Web composition over the built dist this lane already uses
|
|
// and asserts what that composition produces: the model-visible tool catalog
|
|
// and the sandbox/approval knobs it ships with. No browser and no model call —
|
|
// these are composition facts, and the browser scenarios in this lane cover the
|
|
// surface itself.
|
|
import { tmpdir } from 'node:os'
|
|
import { afterEach, expect, it } from 'vitest'
|
|
import { canonicalPath, writableRoots } from '@deepseek-ai/dsh-sandbox'
|
|
// Empty type imports carry the tools/sandboxPolicy/approval Context merges.
|
|
import type {} from '@deepseek-ai/dsh-tools'
|
|
import type {} from '@deepseek-ai/dsh-sandbox-policy'
|
|
import type {} from '@deepseek-ai/dsh-user-approval'
|
|
import type {} from '@deepseek-ai/dsh-permission'
|
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
import type {} from '@deepseek-ai/dsh-agent-presets'
|
|
import { launchWebScaffold, type WebScaffold } from './scaffold.ts'
|
|
|
|
/**
|
|
* The catalog the shipped Web composition puts in front of the model, minus the
|
|
* ripgrep-dependent pair below. The absences are deliberate, not incidental
|
|
* gaps: the `cordis_*` toolset executes model-written JavaScript that no
|
|
* sandbox row confines, `web_fetch` chooses its own request target, and
|
|
* `mcp_*` servers spawn outside `ctx.bash`. The composition Agent Note owns the
|
|
* rationale and its sources.
|
|
*/
|
|
const EXPECTED_TOOLS = [
|
|
'ask_user_question',
|
|
'bash',
|
|
'create_goal',
|
|
'edit',
|
|
'exit_plan_mode',
|
|
'get_goal',
|
|
'list_agents',
|
|
'ralph',
|
|
'read',
|
|
'send_message',
|
|
'skill',
|
|
'str_replace_editor',
|
|
'subagent',
|
|
'subagent_fork',
|
|
'task_kill',
|
|
'task_list',
|
|
'task_output',
|
|
'todo_write',
|
|
'update_goal',
|
|
'web_search',
|
|
'workflow',
|
|
'write',
|
|
]
|
|
|
|
/**
|
|
* `glob` and `grep` come from `dsh-tool-fs-search`, which spawns the PACKAGED
|
|
* ripgrep binary (`@vscode/ripgrep`) through the subprocess seam, so the pair
|
|
* is always present on every host — asserted as fixed members, not a host
|
|
* dependency.
|
|
*/
|
|
const RIPGREP_TOOLS = ['glob', 'grep']
|
|
|
|
let scaffold: WebScaffold | undefined
|
|
|
|
afterEach(async () => {
|
|
await scaffold?.close()
|
|
scaffold = undefined
|
|
})
|
|
|
|
it('assembles the shipped Web catalog with the confined access default', async () => {
|
|
scaffold = await launchWebScaffold()
|
|
const ctx = scaffold.ctx
|
|
// The catalog belongs to an AGENT, not to the process: every model-facing row
|
|
// now lives in a preset mounted under one session's scope, so the global
|
|
// layer holds nothing and a caller must name the agent to see anything. This
|
|
// composes from the deployment default — what a session that names no preset
|
|
// gets — which is the shape this test has always been about.
|
|
expect(ctx.tools.schemas().map(schema => schema.name)).toEqual([])
|
|
const handle = await ctx.agents.create({
|
|
sessionId: SessionId('shipped-composition'),
|
|
setup: agentCtx => ctx.agentPresets.mount(agentCtx).then(() => undefined),
|
|
})
|
|
try {
|
|
const names = ctx.tools.schemas(handle.agent).map(schema => schema.name).sort()
|
|
expect(names.filter(name => !RIPGREP_TOOLS.includes(name))).toEqual(EXPECTED_TOOLS)
|
|
// The packaged ripgrep binary ships with the dependency, so the pair is a
|
|
// fixed roster member on every host.
|
|
expect(names.filter(name => RIPGREP_TOOLS.includes(name))).toEqual(RIPGREP_TOOLS)
|
|
} finally {
|
|
await handle.dispose()
|
|
}
|
|
// `workspace-write` is not "the workspace and nothing else": the shared roots
|
|
// helper always admits the temp directories too. Pinning it against an
|
|
// explicit mode keeps the claim independent of this surface's default, and
|
|
// keeps a future boundary test from being run inside /tmp — where an
|
|
// "escape" write succeeds by design and reads as a sandbox failure.
|
|
expect(writableRoots(scaffold.ctx.sandboxPolicy.resolve({ mode: 'workspace-write' }))).toEqual(
|
|
expect.arrayContaining([canonicalPath('/tmp'), canonicalPath(tmpdir())]),
|
|
)
|
|
expect(scaffold.ctx.sandboxPolicy.defaultMode).toBe('workspace-write')
|
|
expect(scaffold.ctx.approval.config.policy).toBe('ask')
|
|
expect(scaffold.ctx.permission.defaultPreset).toBe('workspace-write')
|
|
}, 120_000)
|