mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
Type-only change (brands are zero-cost casts; no runtime/wire impact). Closes the two gaps in the "brand ids that cross package boundaries" policy and fixes the dependency direction so a capability package never pulls in an unrelated one. - Extract the `Branded<B>` primitive into a new standalone type-only package `@deepseek-ai/dsh-brand` (packages/util/brand) with no harness-package deps. dsh-llm keeps its owned CallId but imports Branded from dsh-brand; dsh-session, dsh-agent, and dsh-bash all import Branded from there. dsh-bash depends on dsh-brand ALONE — never on dsh-llm or dsh-session (the architectural fix: a generic execution backend must not couple to the LLM or session vocabulary). - Mint BashTaskId + OwnerToken in dsh-bash and thread them through BashTask.id, the get/ownerOf/list/readOutput/kill seam, the bash-local generation site, and the dsh-tool-bash validate/access surface. OwnerToken is a DISTINCT brand from SessionId so the seam stays decoupled; dsh-tool-bash is the single boundary that casts SessionId -> OwnerToken. - Brand at the SOURCE, not via mid-pipeline casts: agent-loop's Config types agents[].id as AgentId and resumeSessionId as SessionId, so the brand enters at the config boundary and the inner create()/resume casts disappear (only the genuinely-new per-run session-id string is cast). - Stop brand erosion: propagate CallId/SessionId/AgentId to the registry/store Map keys and public params/exports (SessionStore, AgentRegistry + factory options, the ACP session-id surface + ToolPresenter CallId map, the persistence coordinator, invariants pendingCalls, the pi-ai tool-call maps). - Docs: document BashTaskId/OwnerToken in bash.md (type-equiv re-pasted), point the Branded type-equiv at dsh-brand, fix stale param types in the session/ agent/bash READMEs, regenerate the cordis catalog + module graph. Implements docs/rfc/proposed/architecture/2026-06-20-branded-ids.md
67 lines
3.4 KiB
TypeScript
67 lines
3.4 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { mkdtemp, rm } from 'node:fs/promises'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { PROTOCOL_VERSION } from '@agentclientprotocol/sdk'
|
|
import { AgentId } from '@deepseek-ai/dsh-agent'
|
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
import { makeBridgeHarness, textResponse, type BridgeHarness } from './harness.ts'
|
|
|
|
describe('acp bridge — demux & config edges', () => {
|
|
let storageDir: string
|
|
let harness: BridgeHarness | undefined
|
|
|
|
beforeEach(async () => { storageDir = await mkdtemp(join(tmpdir(), 'acp-edge-')) })
|
|
afterEach(async () => {
|
|
if (harness) await harness.dispose()
|
|
harness = undefined
|
|
await rm(storageDir, { recursive: true, force: true })
|
|
})
|
|
|
|
it('ignores events from an agent the bridge does not own (strict id demux)', async () => {
|
|
// A second agent created directly on the registry (NOT via the bridge) runs
|
|
// a turn. Its session/event + agent/status must NOT produce ACP updates and
|
|
// must not settle anything — the bridge demuxes strictly by its own id.
|
|
harness = await makeBridgeHarness({ storageDir, script: [textResponse('foreign')] })
|
|
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
|
await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
|
const before = harness.updates.length
|
|
|
|
const { agent: foreign } = harness.ctx.agents.create({ agentId: AgentId('foreign'), sessionId: SessionId('foreign-session'), agentOptions: { model: 'mock' } })
|
|
foreign.send([{ type: 'text', text: 'hi' }])
|
|
await foreign.whenIdle()
|
|
await new Promise(r => setTimeout(r, 10))
|
|
|
|
// No update was emitted for the foreign agent's stream.
|
|
expect(harness.updates.length).toBe(before)
|
|
})
|
|
|
|
it('survives a session/update that the client rejects (best-effort notify)', async () => {
|
|
harness = await makeBridgeHarness({ storageDir, script: [textResponse('ok')] })
|
|
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
|
const { sessionId } = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [] })
|
|
// Make the client reject every update — the bridge's notify() must swallow
|
|
// the rejection and the prompt must still settle normally.
|
|
harness.onSessionUpdateError = () => { throw new Error('client update rejected') }
|
|
const res = await harness.client.prompt({ sessionId, prompt: [{ type: 'text', text: 'go' }] })
|
|
expect(res.stopReason).toBe('end_turn')
|
|
})
|
|
|
|
it('accepts session/new with additionalDirectories empty', async () => {
|
|
// Exercises the defined-but-empty additionalDirectories branch (length 0 → allowed).
|
|
harness = await makeBridgeHarness({ storageDir })
|
|
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
|
const a = await harness.client.newSession({ cwd: process.cwd(), mcpServers: [], additionalDirectories: [] })
|
|
expect(a.sessionId).toBeTruthy()
|
|
})
|
|
|
|
it('rejects non-empty mcpServers until MCP wiring is implemented', async () => {
|
|
harness = await makeBridgeHarness({ storageDir })
|
|
await harness.client.initialize({ protocolVersion: PROTOCOL_VERSION, clientCapabilities: {} })
|
|
await expect(harness.client.newSession({
|
|
cwd: process.cwd(),
|
|
mcpServers: [{ name: 'fs', command: 'npx', args: ['server'], env: [] }],
|
|
})).rejects.toThrow(/mcpServers/)
|
|
})
|
|
})
|