• feat(self-modification): add dynamic Cordis plugin runtime and UI

This commit is contained in:
imccyu
2026-08-12 23:51:31 +08:00
parent 0367506471
commit 4064198560
147 changed files with 20904 additions and 2412 deletions

View File

@@ -1,7 +1,14 @@
// Web e2e scenario for the opt-in Cordis tools. Record mode drives a real
// model through inspect, mount, and unmount; replay pins the same shipped Web
// composition, durable calls, generic rows, highlighted Plugin source, and
// conversation accessibility tree.
// model through inspect, define, run, and stop; replay pins the same shipped Web
// composition, durable calls, generic rows, the define card's own source view,
// and conversation accessibility tree.
//
// The approval is never in the fixture. The fixture pins what the MODEL said;
// tools execute for real, so `cordis_run` genuinely blocks on a person and this
// test is that person — which is what lets the run/approve boundary be asserted
// instead of assumed. The package therefore carries a browser half whose only
// job is to be visible (`[data-snapshot-probe]`): its absence before the answer
// and presence after it is the v3 user gate, proven rather than described.
import { readFile } from 'node:fs/promises'
import { fileURLToPath } from 'node:url'
import type { Browser, Page } from 'playwright'
@@ -17,12 +24,21 @@ import { connectFreshWorkspace, newEnglishPage, saveFailureShot } from './suppor
const FIXTURE = fileURLToPath(new URL('./snapshots/cordis-tool-round/session.jsonl', import.meta.url))
const UI_EXPECTED = fileURLToPath(new URL('./snapshots/cordis-tool-round/ui.expected.md', import.meta.url))
const MODE = webSnapshotMode()
const CORDIS_TOOLS = ['cordis_inspect', 'cordis_mount', 'cordis_unmount'] as const
const MOUNT_CODE = 'return { name: "snapshot-noop", apply(ctx) {} }'
const PROMPT = 'Use only Cordis tools. First call cordis_inspect with what "temporary". '
+ `Then call cordis_mount with this exact code: ${JSON.stringify(MOUNT_CODE)}. `
+ 'Read its returned id and call cordis_unmount with that exact id. '
+ 'After all three calls succeed, reply exactly CORDIS_UI_DONE and stop.'
const CORDIS_TOOLS = ['cordis_runtime_inspect', 'cordis_package_inspect', 'cordis_define', 'cordis_run', 'cordis_stop'] as const
const PACKAGE_CODE = 'return { name: "snapshot-noop", apply(ctx) {} }'
// The browser half is the PROBE this scenario turns on: it renders a marker into
// the frame-wide overlay, so "did the plugin actually run in this page" becomes a
// DOM fact. A host-only package would sidestep the approval round trip entirely
// (the host runs those immediately), which would drop the v3 user gate out of
// coverage — the one thing this scenario exists to prove.
const CLIENT_CODE = 'return { inject: ["slots"], apply(ctx) { ctx.slots.register('
+ '{ name: "shell.overlay", id: "snapshot-probe" }, '
+ '() => React.createElement("div", { "data-snapshot-probe": "loaded" })) } }'
const PROMPT = 'Use only Cordis tools. First call cordis_runtime_inspect with what "temporary". '
+ 'Then call cordis_define with name "snapshot noop", purpose "does nothing, for the snapshot", '
+ `code exactly ${JSON.stringify(PACKAGE_CODE)} and client exactly ${JSON.stringify(CLIENT_CODE)}. `
+ 'Read its returned id and call cordis_run with that exact id, then cordis_stop with the same id. '
+ 'After all four calls succeed, reply exactly CORDIS_UI_DONE and stop.'
function assertCompleteCordisLifecycle(events: readonly SessionEvent[]): void {
const turnEnd = events.findLast(
@@ -82,6 +98,22 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
const settled = scaffold.whenTurnSettled()
await input.fill(PROMPT)
await input.press('Enter')
// `cordis_run` blocks host-side on a person's answer — no timer, no default.
// The approval is the TEST's action in every mode: the fixture pins what the
// model said, and the gate is a real round trip through the real panel.
const badge = page.locator('[data-cordis-badge]')
await expect.poll(() => badge.getAttribute('data-cordis-awaiting'), { timeout: 90_000 }).toBe('true')
await badge.click()
const approve = page.locator('[data-cordis-approve]').first()
await approve.waitFor({ timeout: 10_000 })
// The one assertion this scenario cannot give up: the model asking to run is
// NOT the plugin running. Until a person answers, the browser half has not
// been fetched, evaluated, or mounted anywhere on this page.
expect(await page.locator('[data-snapshot-probe]').count()).toBe(0)
await approve.click()
await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 30_000 }).toBe(1)
const sessionId = await settled
if (MODE === 'record') {
assertCompleteCordisLifecycle(sessionEvents)
@@ -100,20 +132,30 @@ describe('web e2e: Cordis tools use the generic row variants', () => {
await expect.poll(() => page.getByText('CORDIS_UI_DONE', { exact: true }).count(), { timeout: 15_000 })
.toBeGreaterThanOrEqual(1)
const inspectRow = page.locator('[data-tool="cordis_inspect"]').filter({ hasText: 'Inspect' }).first()
const inspectRow = page.locator('[data-tool="cordis_runtime_inspect"]').filter({ hasText: 'Inspect' }).first()
await inspectRow.waitFor({ timeout: 10_000 })
const mountRow = page.locator('[data-tool="cordis_mount"]').filter({ hasText: 'Mount temporary Plugin' }).first()
await mountRow.waitFor({ timeout: 10_000 })
// cordis_define does NOT go through the generic row: ui-cordis registers a
// keyed toolview for it, and a keyed hit replaces the generic card. So the
// title here is the CARD's ("Cordis Plugin"), and the expanded body is the
// card's own two code sections rather than a generic args dump.
const defineRow = page.locator('[data-tool="cordis_define"]').filter({ hasText: 'Cordis Plugin' }).first()
await defineRow.waitFor({ timeout: 10_000 })
// The whole summary row is the expand toggle (unified tool-row interaction).
await mountRow.locator('[aria-expanded]').first().click()
await expect.poll(() => mountRow.locator('pre.shiki').textContent(), { timeout: 10_000 })
.toContain(MOUNT_CODE)
await defineRow.locator('[aria-expanded]').first().click()
await expect.poll(() => defineRow.textContent(), { timeout: 10_000 }).toContain(PACKAGE_CODE)
await expect.poll(() => defineRow.textContent()).toContain('data-snapshot-probe')
const unmountRow = page.locator('[data-tool="cordis_unmount"]').filter({ hasText: 'Unmount temporary Plugin' }).first()
await unmountRow.waitFor({ timeout: 10_000 })
await expect.poll(() => unmountRow.textContent()).toContain('dyn-')
await expect(unmountRow.getAttribute('data-state')).resolves.toBe('ok')
const runRow = page.locator('[data-tool="cordis_run"]').filter({ hasText: 'Run dynamic package' }).first()
await runRow.waitFor({ timeout: 10_000 })
await expect.poll(() => runRow.textContent()).toContain('dyn-')
const stopRow = page.locator('[data-tool="cordis_stop"]').filter({ hasText: 'Stop dynamic package' }).first()
await stopRow.waitFor({ timeout: 10_000 })
await expect.poll(() => stopRow.textContent()).toContain('dyn-')
await expect(stopRow.getAttribute('data-state')).resolves.toBe('ok')
// Stopping withdraws the browser half from every page, probe included.
await expect.poll(() => page.locator('[data-snapshot-probe]').count(), { timeout: 15_000 }).toBe(0)
})
it.skipIf(MODE === 'record')('matches the conversation aria golden', async () => {

View File

@@ -70,6 +70,7 @@ import SessionStore, {
type SessionHeader,
} from '@deepseek-ai/dsh-session'
import JsonlSessionPersistence from '@deepseek-ai/dsh-session-persistence-jsonl'
import * as CordisHostRunner from '@deepseek-ai/dsh-cordis-host-runner'
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
// Empty type imports carry the webServer/agents/sessionPersistence Context merges.
import type {} from '@deepseek-ai/dsh-host-webserver'
@@ -460,8 +461,19 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
// be able to change a golden, whatever roots a scenario asks for.
: [{ id: 'agent-presets', config: { ...options.agentPresets, includeUserRoot: false } }],
...options.toolsMode === undefined ? [] : [{ id: 'tools', config: { mode: options.toolsMode } }],
// The host halves ride Loader builtins (below) so the shipped CLI keeps no
// dependency on this opt-in package, but the two browser rows must carry
// their real package names: the modules node half reads `dshClient` from a
// row's resolved package root, and a `cordis:` builtin has none — it is
// permanently not a client row, so a builtin here would silently drop the
// browser half from the roster.
...options.cordisTools === true
? [{ insert: [{ id: 'tool-cordis', name: 'cordis:tool-cordis' }] }]
? [{ insert: [
{ id: 'cordis-host-runner', name: 'cordis:cordis-host-runner' },
{ id: 'tool-cordis', name: 'cordis:tool-cordis' },
{ id: 'cordis-client-runner', name: '@deepseek-ai/dsh-cordis-client-runner' },
{ id: 'ui-cordis', name: '@deepseek-ai/dsh-client-ui-cordis' },
] }]
: [],
...options.deepSeekSearch === undefined
? []
@@ -515,7 +527,10 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
ctx.loader.builtins.group = Group
// The shipped CLI deliberately has no dependency on this opt-in package.
// Keep the Loader row real without broadening the product installation.
if (options.cordisTools === true) ctx.loader.builtins['tool-cordis'] = ToolCordis
if (options.cordisTools === true) {
ctx.loader.builtins['cordis-host-runner'] = CordisHostRunner
ctx.loader.builtins['tool-cordis'] = ToolCordis
}
await ctx.loader.create({
name: 'cordis:include',
config: { path: pathToFileURL(rootConfig).href, patches },