refactor(cmdline): trim the command-line seams to existing interfaces

The web runtime creates its dev-mode client-hmr row in the root tree
after Loader settlement with plain loader.create, deleting the vendored
Entry.enableRuntime state machine and dsh-cmdline's enableRow export.
Include declares the existing EntryGroup.key tree-carrier marker instead
of the EntryConfigResolver protocol (its own path stays literal; nothing
used a dynamic path). The launcher recognizes no app row: SIGTERM exits
0 on every surface, every boot watches its user patch layers, and the
headless runner exits through ctx.appExit, deleting ctx.headlessIo. Also
restores the vendor README rescope entry to the position the
rescope-vendor exact-edit anchor requires, fixing the master hygiene
regression.
This commit is contained in:
Turtle
2026-08-11 14:17:13 +08:00
parent 2d2b89825e
commit c598989d08
32 changed files with 312 additions and 299 deletions

View File

@@ -8,8 +8,8 @@
# The web-startup plugin injects `cmdlineArgs` and provides `webStartup` as an
# ordinary Cordis service. Rows configured from flags inject that service, so
# Loader resolves their expressions only after it exists. The web runtime then
# provides bind-dependent `webRuntime` values to the trust fence and client
# roster. `dsh --profile web --help` provides neither service, so no server binds.
# provides bind-dependent `webRuntime` values to the trust fence.
# `dsh --profile web --help` provides neither service, so no server binds.
# ── surface-specific values the base deliberately omits ─────────────────────
@@ -119,25 +119,17 @@
surfaceContext: true
trustedHosts: !!js ctx.webStartup.trustedHosts
# The client-plugin reload chain: a dev-only row this bundle ships off,
# which the runtime row turns on before client discovery. It is a row rather
# than a child of web-runtime because its node half is a client-side package,
# which a host-side bundle cannot import.
- id: client-hmr
name: '@deepseek-ai/dsh-client-hmr'
inject: [webStartup]
disabled: true
# ── browser plugin roster (dsh.client rows; node halves are layer-2 hosts) ──
# Dual-face: this waits for the runtime row to decide whether HMR belongs
# in the first graph. The node half then scans this tree, composes
# window.__DSH_BOOT__, and serves /plugins/<id>/client.js; the browser half
# is the module table the shell kernel constructs before cordis exists
# (adopted as a plugin entry by the kernel, never fetched).
# Dual-face: the node half scans this tree, composes window.__DSH_BOOT__,
# and serves /plugins/<id>/client.js; the browser half is the module table
# the shell kernel constructs before cordis exists (adopted as a plugin
# entry by the kernel, never fetched). In development mode the web-runtime
# row creates the client-plugin reload chain (dsh-client-hmr) as a root
# tree row after Loader settlement; the incremental scan adds it to the
# roster before any page loads.
- id: modules
name: '@deepseek-ai/dsh-client-modules'
inject: [webRuntime]
# Owns both ends of the web transport: node half binds the gateway to the
# webserver under /api; browser half is the fetch/SSE client.

View File

@@ -16,7 +16,6 @@ import { fileURLToPath } from 'node:url'
import type { Context } from '@deepseek-ai/cordis'
import z from '@deepseek-ai/schemastery'
import { addHarnessSourceSection } from '@deepseek-ai/dsh-app-boot'
import { enableRow } from '@deepseek-ai/dsh-cmdline'
import * as FrontendStatic from '@deepseek-ai/dsh-frontend-static'
import type {} from '@deepseek-ai/cordis-plugin-loader'
import type {} from '@deepseek-ai/dsh-host-webserver'
@@ -28,7 +27,7 @@ export const name = 'web-app'
/** This dsh installation's root, from either this package's source or built entry. */
const SOURCE_ROOT = fileURLToPath(new URL('../../../..', import.meta.url))
const HMR_ROW_ID = 'client-hmr'
const HMR_ROW_NAME = '@deepseek-ai/dsh-client-hmr'
/** Runtime service that releases Web rows after bind-dependent values resolve. */
const WEB_RUNTIME_SERVICE = 'webRuntime'
@@ -141,19 +140,36 @@ export const internals: { resolveDistIndex: () => string } = { resolveDistIndex
/**
* Mount the Web runtime: dist serving, surface prompt, bash runtime
* variables, and the URL line.
* variables, the development-mode client-hmr row, and the URL line.
* @param ctx - plugin context carrying the httpServer service.
* @param config - validated {@link Config}.
* @returns nothing once the invocation's client roster and runtime contributions are registered.
*/
export async function apply(ctx: Context, config: Config): Promise<void> {
// Client discovery must start after the optional HMR row has a pending
// fiber. Otherwise its first browser graph omits the reload receiver, which
// cannot use that receiver to discover itself later.
if (config.mode === 'development') await enableRow(ctx, HMR_ROW_ID)
export function apply(ctx: Context, config: Config): void {
if (config.mode === 'development') {
// The dev reload chain is mounted as a real tree row so the browser
// roster scan includes its client half; it is a row rather than a child
// of this plugin because its node half is a client-side package, which a
// host-side bundle cannot import. Created in the root tree after Loader
// settlement: row creation must stay out of the mounting transaction,
// and a root-tree row survives user-patch reapplication of the include.
// The incremental roster scan picks it up before any page load — a
// browser arrives only after a human reads the URL line.
const loader = ctx.get('loader')
if (loader === undefined) {
ctx.logger.warn('web-app: development mode without a Loader tree mounts no client-hmr row')
} else {
void loader.await().then(async () => {
// The tree can be disposed while settlement was in flight (early
// SIGTERM); re-check before mutating it. A reload of this fiber must
// not duplicate the row a previous generation created.
if (ctx.get('loader') === undefined) return
const mounted = [...ctx.loader.entries()].some(entry => entry.options.name === HMR_ROW_NAME)
if (!mounted) await ctx.loader.create({ name: HMR_ROW_NAME })
}).catch((error: unknown) => { ctx.logger.error(error) })
}
}
const runtime = resolveLanTrust(ctx.httpServer.host, config.trustedHosts)
// Release dependent rows only after the optional row has a pending fiber and
// bind-dependent trust has been sampled once.
// Release dependent rows only after bind-dependent trust has been sampled once.
ctx.provide(WEB_RUNTIME_SERVICE, runtime)
ctx.plugin(FrontendStatic, { distIndex: internals.resolveDistIndex() })
if (config.surfaceContext) {

View File

@@ -58,17 +58,20 @@ function fakeHttpServer(host: '127.0.0.1' | '0.0.0.0' = '127.0.0.1'): { server:
return { server, seat: () => fallback }
}
/** Install the optional HMR row the runtime sequences before client discovery. */
/** A fake Loader capturing the dev-mode row creation the runtime performs after settlement. */
function provideHmrRow(ctx: Context, settle: () => Promise<void> = async () => {}): string[] {
const updates: string[] = []
const created: string[] = []
const entries: { options: { name: string } }[] = []
ctx.provide('loader', {
entries: () => [{
options: { id: 'client-hmr' },
enableRuntime: async () => { updates.push('client-hmr') },
}],
entries: () => entries[Symbol.iterator](),
create: (options: { name: string }) => {
created.push(options.name)
entries.push({ options })
return Promise.resolve(options.name)
},
await: settle,
} as never)
return updates
return created
}
interface BashContribution {
@@ -92,13 +95,13 @@ describe('web-app runtime glue', () => {
} as never)
const enabledRows = provideHmrRow(ctx)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
await apply(ctx, new Config({ mode: 'development', printUrl: true, surfaceContext: true, trustedHosts: ['lab.internal'] }))
apply(ctx, new Config({ mode: 'development', printUrl: true, surfaceContext: true, trustedHosts: ['lab.internal'] }))
await ctx.plugin(SystemPrompt, { persona: '' })
// Settle the injected registrations.
await new Promise(resolve => setTimeout(resolve, 0))
expect(seat()).toBeDefined() // frontend-static claimed the fallback
expect(enabledRows).toEqual(['client-hmr'])
expect(enabledRows).toEqual(['@deepseek-ai/dsh-client-hmr'])
expect(ctx.get('webRuntime')).toEqual({
lanAddresses: ['192.168.1.5'],
trustedHosts: ['192.168.1.5', 'lab.internal'],
@@ -119,7 +122,7 @@ describe('web-app runtime glue', () => {
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
await apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: true, trustedHosts: [] }))
apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: true, trustedHosts: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
@@ -140,7 +143,7 @@ describe('web-app runtime glue', () => {
return () => {}
},
} as never)
await apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: false, trustedHosts: [] }))
apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: false, trustedHosts: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
const assembly = await ctx.systemPrompt.assemble()
@@ -155,12 +158,94 @@ describe('web-app runtime glue', () => {
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
await apply(ctx, new Config({ mode: 'production', printUrl: true, surfaceContext: true, trustedHosts: [] }))
apply(ctx, new Config({ mode: 'production', printUrl: true, surfaceContext: true, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).toHaveBeenCalledWith('dsh web: http://127.0.0.1:4567')
await ctx.fiber.dispose()
})
it('creates the client-hmr row exactly once across runtime reloads', async () => {
stageDist()
const ctx = new Context()
ctx.provide('httpServer', fakeHttpServer().server)
const created = provideHmrRow(ctx)
const mount = async (): Promise<() => Promise<void>> => {
const fiber = ctx.plugin((child: Context) => {
apply(child, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] }))
})
await fiber
await new Promise(resolve => setTimeout(resolve, 0))
return () => fiber.dispose()
}
const disposeFirst = await mount()
expect(created).toEqual(['@deepseek-ai/dsh-client-hmr'])
await disposeFirst()
// A reload generation must not duplicate the row the previous one created.
const disposeSecond = await mount()
expect(created).toEqual(['@deepseek-ai/dsh-client-hmr'])
await disposeSecond()
await ctx.fiber.dispose()
})
it('skips the dev row when the tree is disposed during settlement and logs a creation failure', async () => {
stageDist()
const raced = new Context()
raced.provide('httpServer', fakeHttpServer().server)
let release!: () => void
const settlement = new Promise<void>((resolve) => { release = resolve })
const created: string[] = []
const disposeLoader = raced.provide('loader', {
entries: () => [][Symbol.iterator](),
create: (options: { name: string }) => {
created.push(options.name)
return Promise.resolve(options.name)
},
await: () => settlement,
} as never)
apply(raced, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] }))
disposeLoader()
release()
await new Promise(resolve => setTimeout(resolve, 0))
expect(created).toEqual([])
await raced.fiber.dispose()
const failing = new Context()
failing.provide('httpServer', fakeHttpServer().server)
const failure = new Error('row creation failed')
failing.provide('loader', {
entries: () => [][Symbol.iterator](),
create: () => Promise.reject(failure),
await: () => Promise.resolve(),
} as never)
const errors: unknown[] = []
failing.logger.error = ((error: unknown) => { errors.push(error) }) as typeof failing.logger.error
apply(failing, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(errors).toEqual([failure])
await failing.fiber.dispose()
})
it('mounts no dev row in production and only warns without a Loader in development', async () => {
stageDist()
const prod = new Context()
prod.provide('httpServer', fakeHttpServer().server)
const created = provideHmrRow(prod)
apply(prod, new Config({ mode: 'production', printUrl: false, surfaceContext: false, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(created).toEqual([])
await prod.fiber.dispose()
const bare = new Context()
bare.provide('httpServer', fakeHttpServer().server)
const warnings: string[] = []
bare.logger.warn = ((message: unknown) => { warnings.push(String(message)) }) as typeof bare.logger.warn
apply(bare, new Config({ mode: 'development', printUrl: false, surfaceContext: false, trustedHosts: [] }))
expect(warnings).toEqual(['web-app: development mode without a Loader tree mounts no client-hmr row'])
// Let the vitest invariant host settle before tearing the root down.
await new Promise(resolve => setTimeout(resolve, 0))
await bare.fiber.dispose()
})
it('defers the URL line until Loader settlement and drops it on failure or teardown', async () => {
stageDist()
// Settlement path: the line waits for loader.await() so supervisors can
@@ -171,7 +256,7 @@ describe('web-app runtime glue', () => {
const settlement = new Promise<void>((resolve) => { release = resolve })
provideHmrRow(settled, () => settlement)
const log = vi.spyOn(console, 'log').mockImplementation(() => {})
await apply(settled, new Config({ mode: 'production', printUrl: true, surfaceContext: true, trustedHosts: [] }))
apply(settled, new Config({ mode: 'production', printUrl: true, surfaceContext: true, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
release!()
@@ -185,7 +270,7 @@ describe('web-app runtime glue', () => {
const failed = new Context()
failed.provide('httpServer', fakeHttpServer().server)
provideHmrRow(failed, async () => { throw new Error('boot failed') })
await apply(failed, new Config({ mode: 'production', printUrl: true, surfaceContext: true, trustedHosts: [] }))
apply(failed, new Config({ mode: 'production', printUrl: true, surfaceContext: true, trustedHosts: [] }))
await new Promise(resolve => setTimeout(resolve, 0))
expect(log).not.toHaveBeenCalled()
await failed.fiber.dispose()
@@ -201,7 +286,7 @@ describe('web-app runtime glue', () => {
let releaseTorn: () => void
const tornSettlement = new Promise<void>((resolve) => { releaseTorn = resolve })
provideHmrRow(torn, () => tornSettlement)
await apply(torn, new Config({ mode: 'production', printUrl: true, surfaceContext: true, trustedHosts: [] }))
apply(torn, new Config({ mode: 'production', printUrl: true, surfaceContext: true, trustedHosts: [] }))
await child.dispose() // the httpServer service goes away
releaseTorn!()
await new Promise(resolve => setTimeout(resolve, 0))
@@ -217,7 +302,7 @@ describe('web-app runtime glue', () => {
const { server } = fakeHttpServer()
Object.defineProperty(server, 'port', { get: () => undefined })
ctx.provide('httpServer', server)
await apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: true, trustedHosts: [] }))
apply(ctx, new Config({ mode: 'production', printUrl: false, surfaceContext: true, trustedHosts: [] }))
await ctx.plugin(SystemPrompt, { persona: '' })
await new Promise(resolve => setTimeout(resolve, 0))
await expect(ctx.systemPrompt.assemble()).rejects.toThrow('httpServer service missing')