merge: update the MCP manager with master

This commit is contained in:
gengruilin committed 2026-10-20 15:45:33 +08:00
commit 4e796f736e
569 files changed
+13876 -2216

No files matched your search

@@ -442,6 +442,67 @@ describe('package payload constraints', () => {
const manifest = JSON.parse(readFileSync(new URL(`../${dir}/package.json`, import.meta.url), 'utf8')) as WorkspaceManifest['manifest']
expect(checkWorkspaceManifest({ dir, manifest })).toEqual([])
})
it.each([
{ './assets/*': './lib/assets/*' },
{ './assets/*': { default: './lib/assets/*' } },
])('publishes built package assets after the browser Client entries: %j', (assets) => {
expect(expectedDshPackageFiles({
exports: {
'./client': { types: './lib/types/client/index.d.ts', node: './lib/client.node.js', default: './lib/client.js' },
'./loader': './lib/loader.js',
...assets,
},
})).toEqual([
'lib/index.js', 'lib/client.js', 'lib/client.node.js', 'lib/assets', 'lib/loader.js', 'lib/types/**/*.d.ts',
])
})
it.each([
{},
{ './assets/*': './assets/*' },
{ './assets/*': { types: './lib/assets/*' } },
{ './assets/extra': './lib/assets/*' },
])('does not publish package assets without the exact asset export target: %j', (assets) => {
expect(expectedDshPackageFiles({ exports: { './client': './lib/client.js', ...assets } }))
.toEqual(['lib/index.js', 'lib/client.js', 'lib/types/**/*.d.ts'])
})
it('rejects a package-asset export whose payload omits the built assets', () => {
const dir = 'packages/client/probe'
const manifest = {
name: '@deepseek-ai/dsh-client-probe',
type: 'module',
main: 'lib/index.js',
types: 'lib/types/index.d.ts',
exports: {
'.': { types: './lib/types/index.d.ts', default: './lib/index.js' },
'./client': { types: './lib/types/client/index.d.ts', default: './lib/client.js' },
'./assets/*': './lib/assets/*',
},
files: ['lib/index.js', 'lib/client.js', 'lib/assets', 'lib/types/**/*.d.ts'],
peerDependencies: { '@deepseek-ai/cordis': 'workspace:~' },
devDependencies: { '@deepseek-ai/cordis': 'workspace:~' },
} satisfies WorkspaceManifest['manifest']
const payloadErrors = (candidate: WorkspaceManifest['manifest']): string[] =>
checkWorkspaceManifest({ dir, manifest: candidate }).filter(error => error.includes('package.json files must be'))
expect(payloadErrors(manifest)).toEqual([])
expect(payloadErrors({ ...manifest, files: manifest.files.filter(file => file !== 'lib/assets') })).toEqual([
expect.stringContaining('@deepseek-ai/dsh-client-probe: package.json files must be '
+ '["lib/index.js","lib/client.js","lib/assets","lib/types/**/*.d.ts"]'),
])
})
it.each(['ui-chat', 'ui-settings-account'])(
'accepts the published package assets for %s and rejects their omission', (name) => {
const dir = `packages/client/${name}`
const manifest = JSON.parse(readFileSync(new URL(`../${dir}/package.json`, import.meta.url), 'utf8')) as WorkspaceManifest['manifest']
expect(checkWorkspaceManifest({ dir, manifest })).toEqual([])
expect(checkWorkspaceManifest({ dir, manifest: {
...manifest, files: manifest.files!.filter(file => file !== 'lib/assets'),
} })).toEqual([expect.stringContaining('package.json files must be')])
},
)
})
it('publishes CLI runtime declarations and rejects a payload that omits them', () => {
+9 -2
View File
@@ -21,6 +21,11 @@ import { hasTypertRemoteNavigation, isForbiddenPublicationFile } from './publica
import type { DshBundleManifest } from '../packages/util/package-manifest/src/types.ts'
import { detailPath } from '../packages/util/plugin-detail/src/document.ts'
import { OPTIONAL_BUNDLES, bundlePatchFiles } from '../packages/boot/app-boot/src/profile.ts'
import {
PACKAGE_ASSET_EXPORT,
PACKAGE_ASSET_EXPORT_TARGET,
PACKAGE_ASSET_FILES_ENTRY,
} from '../packages/client/modules/src/assets.ts'
import { collectProjectReferenceFaceViolations } from './project-reference-faces.ts'
const root = resolve(import.meta.dirname, '..')
@@ -285,10 +290,11 @@ export function detailDocumentFiles(packageDir: string): readonly string[] {
}
/**
* Compute canonical publication patterns, including declared and exported icon paths, exported locale JSON resources, and detail documents.
* Compute canonical publication patterns, including declared and exported icon paths, exported locale JSON resources,
* detail documents, and the built package-asset directory when `exports["./assets/*"]` targets it.
* @param manifest - workspace package manifest.
* @param detailFiles - entries from {@link detailDocumentFiles}.
* @returns the icon, deduplicated locale targets, and detail documents followed by runtime and declaration payloads.
* @returns the icon, deduplicated locale targets, and detail documents followed by runtime, package-asset, and declaration payloads.
*/
export function expectedDshPackageFiles(manifest: PackageManifest, detailFiles: readonly string[] = []): readonly string[] {
const localeFiles = new Set<string>()
@@ -327,6 +333,7 @@ export function expectedDshPackageFiles(manifest: PackageManifest, detailFiles:
...browserClient === './lib/client.js' ? ['lib/client.js'] : [],
...browserClient === './lib/client.browser.js' ? ['lib/client.browser.js'] : [],
...exportCondition(manifest, './client', 'node') === './lib/client.node.js' ? ['lib/client.node.js'] : [],
...exportDefault(manifest, PACKAGE_ASSET_EXPORT) === PACKAGE_ASSET_EXPORT_TARGET ? [PACKAGE_ASSET_FILES_ENTRY] : [],
// runtime's shell-held loader subpath ships as its own bundle beside the client half.
...exportDefault(manifest, './loader') === './lib/loader.js' ? ['lib/loader.js'] : [],
// A store subpath ships its own bundle (single-entry builds; no shared chunk).
+32
View File
@@ -0,0 +1,32 @@
/**
* The type roots declare one wildcard image module per supported package
* asset extension, each with a string default export.
*/
import { join } from 'node:path'
import ts from 'typescript'
import { describe, expect, it } from 'vitest'
import { PACKAGE_ASSET_MEDIA_TYPES } from '../packages/client/modules/src/assets.ts'
const DECLARATIONS = join(import.meta.dirname, 'types/client-assets/index.d.ts')
describe('client asset type declarations', () => {
it('declare a string default export for exactly the supported extensions', () => {
const program = ts.createProgram({
rootNames: [DECLARATIONS],
options: { noEmit: true, noLib: true, types: [] },
})
const checker = program.getTypeChecker()
const declarations = program.getSourceFile(DECLARATIONS)
if (declarations === undefined) throw new Error(`${DECLARATIONS} is not in the program`)
const modules = checker.getAmbientModules()
.filter(module => module.declarations?.some(declaration => declaration.getSourceFile() === declarations))
expect(modules.map(module => JSON.parse(module.getName()) as string).sort())
.toEqual([...PACKAGE_ASSET_MEDIA_TYPES.keys()].map(extension => `*${extension}`).sort())
for (const module of modules) {
const defaultExport = checker.getExportsOfModule(module).find(symbol => symbol.getName() === 'default')
if (defaultExport === undefined) throw new Error(`${module.getName()} declares no default export`)
expect(checker.typeToString(checker.getTypeOfSymbol(defaultExport)), module.getName()).toBe('string')
}
})
})
+1
View File
@@ -38,6 +38,7 @@ const CLIENT_ARTIFACT_PATTERNS = [
'packages/*/*/lib/client.js.map',
'packages/*/*/lib/client.*.js',
'packages/*/*/lib/client.*.js.map',
'packages/*/*/lib/assets/**/*',
] as const
/** Public values embedded in one set of client artifacts. */
+509 -87
View File
@@ -1,124 +1,546 @@
/**
* Stylesheets and images enter client bundles through plugin loaders, which
* must register their physical files as watch dependencies.
* Stylesheets and package assets enter client bundles through plugin loaders,
* which register their physical files as watch dependencies. Package assets
* are emitted under `lib/assets/` with revisioned file names and referenced by document-relative URLs.
*/
import { mkdtemp, rm, writeFile } from 'node:fs/promises'
import { transform } from 'lightningcss'
import { runInNewContext } from 'node:vm'
import { existsSync } from 'node:fs'
import { mkdir, mkdtemp, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { describe, expect, it } from 'vitest'
import { dirname, join } from 'node:path'
import { build, type TsdownBundle, type UserConfig } from 'tsdown'
import { afterEach, describe, expect, it } from 'vitest'
import { packageAssetRevision } from '../packages/client/modules/src/assets.ts'
import { clientBundle } from '../packages/client/tsdown.client.ts'
interface EmittedAsset {
type: 'asset'
fileName: string
source: Uint8Array
originalFileName: string
}
interface PluginContext {
emitFile(file: EmittedAsset): string
addWatchFile(id: string): void
}
interface BundlePlugin {
name: string
buildStart?: (this: PluginContext) => void
resolveId?: (source: string, importer?: string) => string | null
load?: (this: { addWatchFile(id: string): void }, id: string) => Promise<string | null>
load?: (this: PluginContext, id: string) => string | null | Promise<string | null>
writeBundle?: (this: { meta: { watchMode: boolean } }, outputOptions: { dir?: string }, bundle: Record<string, unknown>) => Promise<void>
renderChunk?: (code: string, chunk: unknown, outputOptions: { format: string }) => { toString(): string } | null
}
function bundlePlugin(name: 'dsh-css-modules-inline' | 'dsh-css-global-inline' | 'dsh-css-text-inline' | 'dsh-image-inline'): BundlePlugin {
const configs = clientBundle(
join(import.meta.dirname, '../packages/client/ui-sidebar'),
['lib/types/index.js'],
)({ env: { DSH_BUILD_FACE: 'client' } })
const client = configs.find(config => config.platform === 'browser')
if (client === undefined) throw new Error('client config missing')
const plugins = (client as { plugins: BundlePlugin[] }).plugins
const plugin = plugins.find(candidate => candidate.name === name)
if (plugin === undefined) throw new Error(`${name} missing from client config`)
type PluginName = 'dsh-css-modules-inline' | 'dsh-css-global-inline' | 'dsh-css-text-inline' | 'dsh-package-asset'
const PACKAGE_NAME = '@deepseek-ai/dsh-client-asset-fixture'
const ASSET_EXPORTS = { './client': './lib/client.js', './assets/*': './lib/assets/*' }
const WHALE = Uint8Array.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a, 1, 2, 3])
const WHALE_REV = packageAssetRevision(WHALE)
const WHALE_URL = `plugins/@deepseek-ai/dsh-client-asset-fixture/assets/whale-${WHALE_REV}.png`
/** The resolver call identifier a compiled module emits before rendering. */
const RESOLVER_CALL = /__dshPackageAsset_[0-9a-f]{12}(?=\()/u
/** Read the resolver call identifier from one compiled module. */
function resolverCall(output: string | null): string {
const call = RESOLVER_CALL.exec(output ?? '')?.[0]
if (call === undefined) throw new Error('compiled module carries no resolver call')
return call
}
/**
* Evaluate the stylesheet text an emitted style module or inline-text module computes.
* @param output - emitted module source.
* @param resolve - stands in for the module loader's asset resolver.
* @returns the stylesheet text with each resolved reference.
*/
function stylesheetText(output: string | null, resolve: (reference: string) => string): string {
if (output === null) throw new Error('stylesheet module was not emitted')
const call = RESOLVER_CALL.exec(output)?.[0]
const body = output
.replace(/^export default (.*);$/mu, 'return typeof css === "undefined" ? $1 : css;')
.replace(/^export \{\};$/mu, 'return css;')
return runInNewContext(`(() => {\n${body}\n})()`, { ...call === undefined ? {} : { [call]: resolve }, document: undefined }) as string
}
const temporaryRoots: string[] = []
afterEach(async () => {
await Promise.all(temporaryRoots.splice(0).map(root => rm(root, { recursive: true, force: true })))
})
/** Create a spec-owned temporary directory, addressed by its real path. */
async function temporaryDirectory(prefix: string): Promise<string> {
const root = await realpath(await mkdtemp(join(tmpdir(), prefix)))
temporaryRoots.push(root)
return root
}
async function writeFiles(root: string, files: Readonly<Record<string, string | Uint8Array>>): Promise<void> {
for (const [path, content] of Object.entries(files)) {
await mkdir(dirname(join(root, path)), { recursive: true })
await writeFile(join(root, path), content)
}
}
/** Create a client package fixture whose manifest declares the given exports. */
async function assetPackage(
files: Readonly<Record<string, string | Uint8Array>>,
exports: Readonly<Record<string, string>> = ASSET_EXPORTS,
): Promise<string> {
const root = await temporaryDirectory('dsh-client-asset-')
await writeFiles(root, { 'package.json': JSON.stringify({ name: PACKAGE_NAME, type: 'module', exports }), ...files })
return root
}
/** Client-face configs of one package, as its tsdown.config.ts would return them. */
function clientFaceConfigs(packageDir: string, dualClient = false): UserConfig[] {
return clientBundle(packageDir, ['lib/types/index.js'], { dualClient })({ env: { DSH_BUILD_FACE: 'client' } })
}
function configPlugin(config: UserConfig | undefined, name: string): BundlePlugin {
if (config === undefined) throw new Error('config missing')
const plugin = (config as { plugins: BundlePlugin[] }).plugins.find(candidate => candidate.name === name)
if (plugin === undefined) throw new Error(`${name} missing from ${String(config.name)}`)
return plugin
}
/** Look up plugins of one browser config, so stylesheet and asset plugins share its per-build state. */
function browserPlugins(packageDir: string): (name: PluginName) => BundlePlugin {
const client = clientFaceConfigs(packageDir).find(config => config.platform === 'browser')
return name => configPlugin(client, name)
}
function bundlePlugin(name: PluginName): BundlePlugin {
return browserPlugins(join(import.meta.dirname, '../packages/client/ui-sidebar'))(name)
}
interface RecordingContext extends PluginContext {
readonly emitted: EmittedAsset[]
readonly watched: string[]
}
function recordingContext(): RecordingContext {
const emitted: EmittedAsset[] = []
const watched: string[] = []
return {
emitted,
watched,
emitFile(file) {
emitted.push(file)
return file.fileName
},
addWatchFile(id) { watched.push(id) },
}
}
/** Emitted files with hex sources, so Buffer and Uint8Array bytes compare equal. */
function emittedFiles(context: { readonly emitted: readonly EmittedAsset[] }): object[] {
return context.emitted.map(file => ({ ...file, source: Buffer.from(file.source).toString('hex') }))
}
function hooks(plugin: BundlePlugin): Required<Pick<BundlePlugin, 'resolveId' | 'load'>> {
if (plugin.resolveId === undefined || plugin.load === undefined) throw new Error(`${plugin.name} hooks are incomplete`)
return { resolveId: plugin.resolveId, load: plugin.load }
}
/** Resolve and load one import through a plugin. */
async function loadImport(plugin: BundlePlugin, context: PluginContext, source: string, importer: string): Promise<string | null> {
const { resolveId, load } = hooks(plugin)
const id = resolveId(source, importer)
if (typeof id !== 'string') throw new Error(`${plugin.name} did not resolve ${source}`)
return await load.call(context, id)
}
describe('client bundle CSS Modules', () => {
it('registers the source stylesheet as a watch dependency', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-client-css-watch-'))
try {
const stylesheet = join(root, 'Fixture.module.css')
const importer = join(root, 'index.ts')
await writeFile(stylesheet, '.root { color: red; }\n')
const plugin = bundlePlugin('dsh-css-modules-inline')
const virtualId = plugin.resolveId?.('./Fixture.module.css', importer)
if (typeof virtualId !== 'string' || plugin.load === undefined) {
throw new Error('CSS Modules plugin hooks are incomplete')
}
const watched: string[] = []
const root = await temporaryDirectory('dsh-client-css-watch-')
const stylesheet = join(root, 'Fixture.module.css')
await writeFile(stylesheet, '.root { color: red; }\n')
const context = recordingContext()
const output = await plugin.load.call({ addWatchFile: id => watched.push(id) }, virtualId)
const output = await loadImport(bundlePlugin('dsh-css-modules-inline'), context, './Fixture.module.css', join(root, 'index.ts'))
expect(watched).toEqual([stylesheet])
expect(output).toContain('data-plugin-css')
} finally {
await rm(root, { recursive: true, force: true })
}
expect(context.watched).toEqual([stylesheet])
expect(output).toContain('data-plugin-css')
})
})
describe('client bundle global CSS', () => {
it('compiles a side-effect stylesheet into a watched style injector', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-client-global-css-watch-'))
try {
const stylesheet = join(root, 'base.css')
const importer = join(root, 'index.ts')
await writeFile(stylesheet, 'body { color: red; }\n')
const plugin = bundlePlugin('dsh-css-global-inline')
const virtualId = plugin.resolveId?.('./base.css', importer)
if (typeof virtualId !== 'string' || plugin.load === undefined) {
throw new Error('global CSS plugin hooks are incomplete')
}
const watched: string[] = []
const root = await temporaryDirectory('dsh-client-global-css-watch-')
const stylesheet = join(root, 'base.css')
await writeFile(stylesheet, 'body { color: red; }\n')
const context = recordingContext()
const output = await plugin.load.call({ addWatchFile: id => watched.push(id) }, virtualId)
const output = await loadImport(bundlePlugin('dsh-css-global-inline'), context, './base.css', join(root, 'index.ts'))
expect(watched).toEqual([stylesheet])
expect(output).toContain('data-plugin-css')
expect(output).toContain('body{color:red}')
} finally {
await rm(root, { recursive: true, force: true })
}
expect(context.watched).toEqual([stylesheet])
expect(output).toContain('data-plugin-css')
expect(output).toContain('body{color:red}')
})
it('compiles inline stylesheets as watched text without a module side effect', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-client-inline-css-watch-'))
try {
const stylesheet = join(root, 'base.css')
const importer = join(root, 'index.ts')
await writeFile(stylesheet, 'body { color: red; }\n')
const plugin = bundlePlugin('dsh-css-text-inline')
const virtualId = plugin.resolveId?.('./base.css?inline', importer)
if (typeof virtualId !== 'string' || plugin.load === undefined) {
throw new Error('inline CSS plugin hooks are incomplete')
}
const watched: string[] = []
const root = await temporaryDirectory('dsh-client-inline-css-watch-')
const stylesheet = join(root, 'base.css')
await writeFile(stylesheet, 'body { color: red; }\n')
const context = recordingContext()
const output = await plugin.load.call({ addWatchFile: id => watched.push(id) }, virtualId)
const output = await loadImport(bundlePlugin('dsh-css-text-inline'), context, './base.css?inline', join(root, 'index.ts'))
expect(watched).toEqual([stylesheet])
expect(output).toContain('export default "body{color:red}"')
expect(output).not.toContain('data-plugin-css')
} finally {
await rm(root, { recursive: true, force: true })
}
expect(context.watched).toEqual([stylesheet])
expect(output).toContain('export default "body{color:red}"')
expect(output).not.toContain('data-plugin-css')
})
})
describe('client bundle images', () => {
it('embeds an imported image as a watched data URL and ignores other modules', async () => {
const root = await mkdtemp(join(tmpdir(), 'dsh-client-image-watch-'))
describe('client bundle package asset imports', () => {
it('emits an imported asset once per build and exports its revisioned URL', async () => {
const root = await assetPackage({ 'src/client/assets/icons/[email protected]': WHALE })
const plugin = browserPlugins(root)('dsh-package-asset')
const { resolveId, load } = hooks(plugin)
const asset = join(root, 'src/client/assets/icons/[email protected]')
const context = recordingContext()
const id = resolveId('./assets/icons/[email protected]', join(root, 'lib/types/client/index.js'))
if (id === null) throw new Error('asset import did not resolve')
const first = await load.call(context, id)
const second = await load.call(context, id)
const expected = `export default ${resolverCall(first)}("plugins/@deepseek-ai/dsh-client-asset-fixture/assets/icons/whale%402x-${WHALE_REV}.png");`
expect(id).toBe(asset)
expect([first, second]).toEqual([expected, expected])
expect(emittedFiles(context)).toEqual([{
type: 'asset',
fileName: `assets/icons/whale@2x-${WHALE_REV}.png`,
source: Buffer.from(WHALE).toString('hex'),
originalFileName: asset,
}])
expect(context.watched).toEqual([asset])
plugin.buildStart?.call(context)
expect(await load.call(context, id)).toBe(expected)
expect(context.emitted).toHaveLength(2)
})
it('renders each resolver call as the module loader require.asset in the CommonJS bundle only', async () => {
const root = await assetPackage({ 'src/client/assets/whale.png': WHALE })
const plugin = browserPlugins(root)('dsh-package-asset')
const { renderChunk } = plugin
if (renderChunk === undefined) throw new Error('asset plugin has no renderChunk hook')
const call = resolverCall(await hooks(plugin).load.call(recordingContext(), join(root, 'src/client/assets/whale.png')))
// Plugin data that only shares the fixed prefix stays as written.
const literal = 'const text = "__dshPackageAsset(";\n'
const code = `const a = ${call}("${WHALE_URL}");\nconst css = "x" + ${call}("${WHALE_URL}") + "y";\n${literal}`
expect(renderChunk(code, {}, { format: 'cjs' })?.toString())
.toBe(`const a = require.asset("${WHALE_URL}");\nconst css = "x" + require.asset("${WHALE_URL}") + "y";\n${literal}`)
expect(renderChunk(code, {}, { format: 'es' })).toBeNull()
expect(renderChunk('const a = 1;\n', {}, { format: 'cjs' })).toBeNull()
})
it('builds an asset import into require.asset and keeps plugin strings that share the prefix', async () => {
const root = await assetPackage({
'src/client/assets/whale.png': WHALE,
'lib/types/client/index.js': 'import whale from "./assets/whale.png"\nexport const url = whale\nexport const text = "__dshPackageAsset("\n',
})
const config = clientFaceConfigs(root).find(candidate => candidate.platform === 'browser')
if (config === undefined) throw new Error('browser config missing')
let builds: TsdownBundle[] = []
try {
const image = join(root, 'preview.svg')
const importer = join(root, 'index.ts')
await writeFile(image, '<svg/>')
const plugin = bundlePlugin('dsh-image-inline')
const id = plugin.resolveId?.('./preview.svg', importer)
if (typeof id !== 'string' || plugin.load === undefined) throw new Error('image plugin hooks are incomplete')
const watched: string[] = []
const output = await plugin.load.call({ addWatchFile: file => watched.push(file) }, id)
expect(watched).toEqual([image])
expect(output).toBe(`export default "data:image/svg+xml;base64,${Buffer.from('<svg/>').toString('base64')}";`)
expect(plugin.resolveId?.('./index.ts', importer)).toBeNull()
expect(await plugin.load.call({ addWatchFile: file => watched.push(file) }, importer)).toBeNull()
builds = await build({
...config, cwd: root, config: false, tsconfig: false,
write: false, clean: false, exports: false, report: false, logLevel: 'silent',
})
const output = builds.flatMap(bundle => bundle.chunks).find(chunk => chunk.type === 'chunk' && chunk.fileName === 'client.js')
const code = output?.type === 'chunk' ? output.code : ''
expect(code).toContain(`require.asset(${JSON.stringify(WHALE_URL)})`)
expect(code).not.toContain('__dshPackageAsset_')
expect(code).toContain('"__dshPackageAsset("')
} finally {
await rm(root, { recursive: true, force: true })
for (const bundle of builds) await bundle[Symbol.asyncDispose]()
}
})
it('ignores imports and modules that are not supported assets', async () => {
const root = await assetPackage({ 'src/client/index.ts': '', 'src/client/assets/font.woff2': 'font' })
const { resolveId, load } = hooks(browserPlugins(root)('dsh-package-asset'))
const importer = join(root, 'src/client/index.ts')
const context = recordingContext()
expect(resolveId('./index.ts', importer)).toBeNull()
expect(resolveId('./assets/font.woff2', importer)).toBeNull()
expect(resolveId('./assets/whale.png', undefined)).toBeNull()
expect(await load.call(context, importer)).toBeNull()
expect(await load.call(context, 'whale.png')).toBeNull()
expect(context.emitted).toEqual([])
})
it('rejects files outside the package src/client/assets directory', async () => {
const other = await assetPackage({ 'src/client/assets/whale.png': WHALE })
const root = await assetPackage({
'src/client/whale.png': WHALE,
'node_modules/dependency/package.json': JSON.stringify({ name: 'dependency' }),
'node_modules/dependency/whale.png': WHALE,
})
const plugin = browserPlugins(root)('dsh-package-asset')
const { load } = hooks(plugin)
const importer = join(root, 'src/client/index.ts')
const context = recordingContext()
await expect(loadImport(plugin, context, './whale.png', importer))
.rejects.toThrow(`${join(root, 'src/client/whale.png')} is outside src/client/assets; move the asset there`)
expect(() => load.call(context, join(other, 'src/client/assets/whale.png'))).toThrow('is outside src/client/assets')
await expect(loadImport(plugin, context, 'dependency/whale.png', importer)).rejects.toThrow('is outside src/client/assets')
expect(context.emitted).toEqual([])
})
it.each([
['is missing', { './client': './lib/client.js' }],
['targets another directory', { './assets/*': './src/client/assets/*' }],
])('rejects an asset while the asset export %s', async (_case, exports) => {
const root = await assetPackage({ 'src/client/assets/whale.png': WHALE }, exports)
const { load } = hooks(browserPlugins(root)('dsh-package-asset'))
expect(() => load.call(recordingContext(), join(root, 'src/client/assets/whale.png')))
.toThrow('needs package.json exports {"./assets/*":"./lib/assets/*"} and "lib/assets" in files')
})
it('rejects a missing asset file by name', async () => {
const root = await assetPackage({})
const asset = join(root, 'src/client/assets/missing.png')
const { load } = hooks(browserPlugins(root)('dsh-package-asset'))
expect(() => load.call(recordingContext(), asset)).toThrow(`${PACKAGE_NAME}: cannot resolve asset ${asset}`)
})
it.each([
'src/client/assets/100%.png', 'src/client/assets/icons#1/whale.png', 'src/client/assets/Node_Modules/whale.png',
])('rejects %s, which the asset route cannot serve', async (path) => {
const root = await assetPackage({ [path]: WHALE })
const { load } = hooks(browserPlugins(root)('dsh-package-asset'))
expect(() => load.call(recordingContext(), join(root, path)))
.toThrow(`${join(root, path)} has a node_modules path segment or one containing %, #, ?, \\ or NUL, which the asset route cannot serve; rename it`)
})
it('resolves every copy of a reference and tolerates a minifier dropping one', async () => {
const root = await assetPackage({
'src/client/assets/whale.png': WHALE,
'src/client/assets/old.png': WHALE,
'src/client/theme.css': '.a { background: url(./assets/old.png); background: url(./assets/whale.png); } .b { mask: url(./assets/whale.png); }',
})
const output = await loadImport(browserPlugins(root)('dsh-css-global-inline'), recordingContext(), './theme.css', join(root, 'src/client/index.ts'))
const css = stylesheetText(output, reference => `resolved:${reference}`)
expect(css.split(`url(resolved:${WHALE_URL})`)).toHaveLength(3)
// lightningcss keeps only the last background, so the placeholder of old.png is missing from the text.
expect(css).not.toContain('old-')
expect(css).not.toContain('__dshPackageAsset')
})
it('rewrites the urls inside image-set()', async () => {
const root = await assetPackage({
'src/client/assets/whale.png': WHALE,
'src/client/theme.css': '.a { background-image: image-set(url(./assets/whale.png) 1x, url("data:image/png;base64,AA==") 2x); }',
})
const context = recordingContext()
const output = await loadImport(browserPlugins(root)('dsh-css-global-inline'), context, './theme.css', join(root, 'src/client/index.ts'))
const css = stylesheetText(output, reference => `resolved:${reference}`)
expect(css).toContain(`resolved:${WHALE_URL}`)
expect(css).toContain('data:image/png;base64,AA==')
expect(context.emitted.map(file => file.fileName)).toEqual([`assets/whale-${WHALE_REV}.png`])
})
it('keeps literal placeholder-like text and resolves only the references the rewrite inserted', async () => {
const root = await assetPackage({
'src/client/assets/whale.png': WHALE,
'src/client/assets/__dshPackageAsset_logo.png': WHALE,
'src/client/theme.css': [
'.a::after { content: "__dshPackageAsset0__"; } .b { background: url(./assets/whale.png); }',
'.c { background: url(./assets/__dshPackageAsset_logo.png); }',
].join('\n'),
})
const output = await loadImport(browserPlugins(root)('dsh-css-global-inline'), recordingContext(), './theme.css', join(root, 'src/client/index.ts'))
const css = stylesheetText(output, reference => `resolved:${reference}`)
expect(css).toContain('content:"__dshPackageAsset0__"')
expect(css).toContain(`url(resolved:${WHALE_URL})`)
// A real file whose name shares the placeholder prefix is still a package asset.
expect(css).toContain(`url(resolved:plugins/@deepseek-ai/dsh-client-asset-fixture/assets/__dshPackageAsset_logo-${WHALE_REV}.png)`)
})
it('rejects an uppercase extension, which the asset import types do not declare', async () => {
const root = await assetPackage({ 'src/client/assets/whale.PNG': WHALE })
const { load } = hooks(browserPlugins(root)('dsh-package-asset'))
const asset = join(root, 'src/client/assets/whale.PNG')
expect(() => load.call(recordingContext(), asset)).toThrow(`${asset} has an uppercase extension`)
})
it('passes a file name lightningcss escapes through the resolver call, and rejects a fragment or query', async () => {
const root = await assetPackage({
'src/client/assets/whale(2x).png': WHALE,
'src/client/View.module.css': '.a { mask: url(\'./assets/whale(2x).png\'); }',
'src/client/Bad.module.css': '.b { mask: url(\'./assets/whale(2x).png#part\'); }',
})
const plugin = browserPlugins(root)('dsh-css-modules-inline')
const importer = join(root, 'src/client/index.ts')
const output = await loadImport(plugin, recordingContext(), './View.module.css', importer)
const reference = `plugins/@deepseek-ai/dsh-client-asset-fixture/assets/whale%282x%29-${WHALE_REV}.png`
// The served Web app returns the reference unchanged; the text must still parse as one url() declaration.
const css = stylesheetText(output, asset => asset)
expect(css).toContain(`url(${reference})`)
expect(transform({ filename: 'View.css', code: Buffer.from(css), minify: true }).code.toString()).toContain(`mask:url(${reference})`)
await expect(loadImport(plugin, recordingContext(), './Bad.module.css', importer))
.rejects.toThrow('a package asset url() takes no fragment or query')
})
it('rejects asset imports in the Node Client face', async () => {
const root = await assetPackage({})
const node = clientFaceConfigs(root, true).find(config => config.name === `${PACKAGE_NAME}/client-node`)
const { resolveId } = configPlugin(node, 'dsh-package-asset-browser-only')
if (resolveId === undefined) throw new Error('browser-only guard has no resolveId hook')
const importer = join(root, 'lib/types/client/index.js')
expect(() => resolveId('./assets/whale.png', importer)).toThrow(`${PACKAGE_NAME}: asset imports are browser-only (./assets/whale.png)`)
expect(resolveId('./index.js', importer)).toBeNull()
})
})
describe('client bundle stylesheet url()', () => {
it('rewrites relative CSS Modules urls and keeps urls that name no package file', async () => {
const root = await assetPackage({
'src/client/assets/whale.png': WHALE,
'src/client/View.module.css': [
'.whale { mask: url(\'./assets/whale.png\') center / 100% 100% no-repeat; }',
'@supports (mask-image: url(\'\')) { .empty { background: url(\'\'); } }',
'.data { background: url("data:image/svg+xml,%3Csvg%3E"); }',
'.fragment { mask: url(#mask); }',
'.rooted { background: url(/static/a.png); }',
'.remote { background: url(https://example.com/a.png); }',
].join('\n'),
})
const context = recordingContext()
const output = await loadImport(browserPlugins(root)('dsh-css-modules-inline'), context, './View.module.css', join(root, 'src/client/index.ts'))
expect(output).toContain(`url(" + ${resolverCall(output)}(${JSON.stringify(WHALE_URL)}) + ")`)
const css = stylesheetText(output, reference => `resolved:${reference}`)
expect(css).toContain(`url(resolved:${WHALE_URL})`)
for (const preserved of ['(mask-image:url(\'\'))', 'background:url()', 'url(data:image/svg+xml,%3Csvg%3E)', 'url(#mask)', 'url(/static/a.png)', 'url(https://example.com/a.png)']) {
expect(css).toContain(preserved)
}
expect(emittedFiles(context)).toEqual([{
type: 'asset',
fileName: `assets/whale-${WHALE_REV}.png`,
source: Buffer.from(WHALE).toString('hex'),
originalFileName: join(root, 'src/client/assets/whale.png'),
}])
})
it.each([
['dsh-css-global-inline', './theme.css'],
['dsh-css-text-inline', './theme.css?inline'],
] as const)('rewrites relative urls through %s', async (name, source) => {
const root = await assetPackage({
'src/client/assets/whale.png': WHALE,
'src/client/theme.css': 'body { background: url(./assets/whale.png); }',
})
const context = recordingContext()
const output = await loadImport(browserPlugins(root)(name), context, source, join(root, 'src/client/index.ts'))
expect(stylesheetText(output, reference => `resolved:${reference}`)).toContain(`url(resolved:${WHALE_URL})`)
expect(context.emitted.map(file => file.fileName)).toEqual([`assets/whale-${WHALE_REV}.png`])
})
it('leaves stylesheets outside the package src untouched', async () => {
const root = await assetPackage({
'node_modules/@fortune-sheet/react/dist/index.css': '.sheet { background: url("EwaAntH.gif"); }',
})
const context = recordingContext()
const output = await loadImport(
browserPlugins(root)('dsh-css-global-inline'),
context,
'../../node_modules/@fortune-sheet/react/dist/index.css',
join(root, 'src/client/index.ts'),
)
expect(output).toContain('url(EwaAntH.gif)')
expect(context.emitted).toEqual([])
})
it.each([
['./assets/font.woff2', 'is not a supported asset; supported extensions: .svg, .png, .webp, .jpg, .jpeg'],
['./whale.png', 'is outside src/client/assets; move the asset there'],
['./assets/missing.png', 'cannot resolve asset'],
])('rejects url(%s)', async (url, message) => {
const root = await assetPackage({
'src/client/whale.png': WHALE,
'src/client/assets/font.woff2': 'font',
'src/client/View.module.css': `.x { background: url(${url}); }`,
})
await expect(loadImport(browserPlugins(root)('dsh-css-modules-inline'), recordingContext(), './View.module.css', join(root, 'src/client/index.ts')))
.rejects.toThrow(message)
})
})
describe('client bundle asset output', () => {
it('prunes lib/assets files this build did not emit and the directories they leave empty', async () => {
const root = await assetPackage({
'lib/client.js': '',
'lib/assets/whale.png': WHALE,
'lib/assets/icons/kept.svg': '<svg/>',
'lib/assets/icons/stale.svg': '<svg/>',
'lib/assets/old/deep/stale.webp': 'webp',
})
const { writeBundle } = browserPlugins(root)('dsh-package-asset')
if (writeBundle === undefined) throw new Error('asset plugin has no writeBundle hook')
const out = join(root, 'lib')
const build = { meta: { watchMode: false } }
await writeBundle.call(build, { dir: out }, { 'client.js': {}, 'assets/whale.png': {}, 'assets/icons/kept.svg': {} })
expect(['client.js', 'assets/whale.png', 'assets/icons/kept.svg', 'assets/icons/stale.svg', 'assets/old']
.map(path => [path, existsSync(join(out, path))]))
.toEqual([
['client.js', true],
['assets/whale.png', true],
['assets/icons/kept.svg', true],
['assets/icons/stale.svg', false],
['assets/old', false],
])
await writeBundle.call(build, { dir: out }, { 'client.js': {} })
expect([existsSync(join(out, 'client.js')), existsSync(join(out, 'assets'))]).toEqual([true, false])
await writeBundle.call(build, { dir: out }, { 'client.js': {} })
})
it('keeps earlier lib/assets files in a watch rebuild for pages running the previous generation', async () => {
const root = await assetPackage({
'lib/client.js': '',
'lib/assets/whale-000000000000.png': WHALE,
})
const { writeBundle } = browserPlugins(root)('dsh-package-asset')
if (writeBundle === undefined) throw new Error('asset plugin has no writeBundle hook')
const out = join(root, 'lib')
await writeBundle.call({ meta: { watchMode: true } }, { dir: out }, { 'client.js': {}, 'assets/whale-111111111111.png': {} })
expect(existsSync(join(out, 'assets/whale-000000000000.png'))).toBe(true)
})
})
+1 -1
View File
@@ -1,7 +1,7 @@
{
"AGENTS.md": 2080,
"docs/AGENTS.md": 1430,
"docs/architecture.md": 2410,
"docs/architecture.md": 2430,
"docs/cordis-primer.md": 600,
"docs/testing.md": 1350,
"packages/AGENTS.md": 810,
@@ -0,0 +1,8 @@
/** Keep the authoring request snapshot stable without replacing native timers. */
export function apply(ctx) {
const original = Date.now
ctx.effect(() => {
Date.now = () => 1792108800000
return () => { Date.now = original }
}, 'authoring snapshot clock')
}
+5
View File
@@ -68,6 +68,7 @@ export const SERVICE_PAGE: Record<string, string> = {
mcpResources: 'mcp.md',
agentLoop: 'core.md',
mcpServers: 'mcp.md',
agentController: 'core.md',
agentDefaultModel: 'core.md',
agentPresets: 'core.md',
agents: 'core.md',
@@ -226,6 +227,7 @@ export const EVENT_SCOPE_PAGE: Record<string, string> = {
'plugin-manager': 'boot.md',
'plugin-market': 'boot.md',
'agent': 'core.md',
'agent-controller': 'core.md',
'agent-loop': 'core.md',
'agent-preset': 'core.md',
'api-session': 'session.md',
@@ -352,6 +354,9 @@ export const LINK_MAP: Readonly<Record<string, string>> = {
AllowedModelRoute: 'subagent.md',
SubagentModelSelectionSettings: 'subagent.md',
AgentOptions: 'core.md',
AdmissionDecision: 'core.md',
DeliveryMode: 'core.md',
DeliveryRequest: 'core.md',
TurnBoundaryProjection: 'core.md',
AgentStatus: 'core.md',
AssistantStreamFrame: 'core.md',
+8
View File
@@ -641,6 +641,14 @@ const SERVICE_ROLES: ServiceRole[] = [
consumers: ['agent-loop', 'acp', 'subagent'],
note: 'Owns live Agent handles, the create/resume factory seam, and process-local initiator propagation.',
},
{
key: 'agentController',
pkg: 'agent-controller',
title: 'Outside-message delivery',
mode: 'core',
consumers: ['api-session-controller'],
note: 'Prepares, admits through the agent-controller/admission stage, and appends messages that senders bring on someone\'s behalf, one at a time per Agent.',
},
{
key: 'agentDefaultModel',
pkg: 'agent-default-model',
-5
View File
@@ -751,9 +751,7 @@
"24334994394c1d4336d192b2f38986beadcd0f6b235108370c3a736c34eebe98": 1,
"33e462c9f8d9ef55f1b54a408f047caaa9467facc4fc88887fa5979fdce6c09f": 1,
"455072902745ebd120bdeaaebf1b00b2d747a300409066715375263bcc4e8a63": 1,
"4df26af834cb4f8d55bb6908736ddeb6971b4c4c1cbd5a9ace2f9cfe9c0b94e4": 1,
"6078d7e1390a0c3c94892edd74526a76e6cbbda8360656bf9f97d367371c0c4c": 1,
"71b9c7a18658556ab36c9af394f29dac5885fab402304b0b6f4d5127d6acdeff": 1,
"9117562f15005ef8ec18dbad9c29cdc723c4af2c6a9c217af3cd7d8380de999b": 1,
"98169406d3c872458f70f3f8f70abdd6eeaca296816ce91cdcf8392cfa6a77bf": 1,
"afecf8092cd8de095ffbe00a1df00816897e8205854fd33a9d2beeaee88eca95": 1,
@@ -2059,9 +2057,6 @@
"packages/web/tool-web/tests/spill.spec.ts": {
"ecf3420d023d28e1cbc687d438bb66ee49d55a4ae4cb0828dd0f59b926363903": 1
},
"packages/web/web-fetch-http/tests/fetch-http.spec.ts": {
"366f13ff1ff2522ad824a1d4333085e2c771c0b7ca22783aa41ca2e23233b259": 1
},
"packages/web/web-search-deepseek/tests/deepseek.spec.ts": {
"37c9617ff75d6a383f58dd63f939a88803e4ff5e1676bfe7e1c0cc340fc65014": 5,
"598014dcfe77101781e3c6a02709097e3b0151a335e909c44719fac08fdea433": 2
+16 -1
View File
@@ -75,17 +75,23 @@ SPAWN_NODE_COMMAND = (
)
LEGACY_CUSTOM_DISABLED_ROWS = (
"agent-instructions",
"deliverable-file-references",
"file-reference-local",
"goal",
"goal-round-driver",
"command-goal",
"plan-mode",
"present",
"skill",
"skill-filesystem",
"skill-office",
"workspace-dependencies",
"time-context",
"tool-ask-user",
"tool-fs",
"tool-fs-search",
"tool-goal",
"tool-schedule",
"tool-skill",
"tool-str-replace-editor",
"tool-subagent-control",
@@ -915,7 +921,16 @@ def _smoke_sdk_authoring_cached(base_url: str, executable: Path, update_snapshot
skill.parent.mkdir(parents=True)
skill.write_text("---\nname: office-docx\ndescription: CUSTOM_OFFICE_DOCX\n---\nUse the bundled Python for custom document work.\n")
patch = root / "skills.patch.yml"
patch.write_text(json.dumps([{"id": "skill-office", "disabled": True}] if mode == "disabled" else []))
# Fix the model-visible clock in this child only; native timers stay live.
patch_rows = [
{"insert": [{"id": "authoring-clock", "name": (
Path(__file__).resolve().parent / "fixtures/python-sdk-authoring-clock.mjs"
).as_uri()}]},
{"id": "time-context", "config": {"timeZone": "UTC"}},
]
if mode == "disabled":
patch_rows.append({"id": "skill-office", "disabled": True})
patch.write_text(json.dumps(patch_rows))
first = len(MockModelHandler.requests)
with DeepSeekHarness(
provider="deepseek-official", model="smoke-model", cwd=str(root),
@@ -8,7 +8,7 @@
}
},
"catalogs": [
"Query the packaged Python environment for Office authoring.Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).<system-reminder>\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n<available_skills>\n- `office`: Create, edit, inspect, or convert Office documents, spreadsheets, presentations, and PDFs. Load to choose the relevant workflow.\n- `office-docx`: Create, read, edit, and validate Word documents (.docx).\n- `office-export-pdf`: Convert an Office document to a requested PDF with LibreOffice Kit.\n- `office-pptx`: Create, read, edit, and validate PowerPoint presentations (.pptx).\n- `office-recalculate`: Refresh spreadsheet formula caches and verify calculated values with LibreOffice Kit.\n- `office-render`: Render requested document images or inspect visual layout with LibreOffice Kit.\n- `office-xlsx`: Read, create, edit, and validate Excel workbooks, data, and formulas (.xlsx).\n</available_skills>\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\nA user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.\n</system-reminder>"
"Query the packaged Python environment for Office authoring.Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).<system-reminder>\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n<available_skills>\n- `office`: Create, edit, inspect, or convert Office documents, spreadsheets, presentations, and PDFs. Load to choose the relevant workflow.\n- `office-docx`: Create, read, edit, and validate Word documents (.docx).\n- `office-export-pdf`: Convert an Office document to a requested PDF with LibreOffice Kit.\n- `office-pptx`: Create, read, edit, and validate PowerPoint presentations (.pptx).\n- `office-recalculate`: Refresh spreadsheet formula caches and verify calculated values with LibreOffice Kit.\n- `office-render`: Render requested document images or inspect visual layout with LibreOffice Kit.\n- `office-xlsx`: Read, create, edit, and validate Excel workbooks, data, and formulas (.xlsx).\n</available_skills>\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\nA user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.\n</system-reminder>Time sampled while preparing turn 1, step 1: 2026-10-16T00:00:00+00:00[UTC]\nBrowser time zone for this request: unavailable. Ask the user to clarify otherwise-unqualified dates and times.\nElapsed since the preceding model-visible message: unavailable."
],
"result": {
"python": "{{python}}",
@@ -8,7 +8,7 @@
}
},
"catalogs": [
"Query the packaged Python environment for Office authoring.Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).<system-reminder>\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n<available_skills>\n- `diagnose-windows-sandbox-acl`: Use on Windows for unexpected DSH sandbox access denials: workspace writes or listing fail, or an ordinarily readable path cannot be read. One bundled command inspects the path and every ancestor and repairs the ACL problems it proves in that same run. Expected confinement denials need no ACL repair.\n- `office`: Create, edit, inspect, or convert Office documents, spreadsheets, presentations, and PDFs. Load to choose the relevant workflow.\n- `office-docx`: Create, read, edit, and validate Word documents (.docx).\n- `office-export-pdf`: Convert an Office document to a requested PDF with LibreOffice Kit.\n- `office-pptx`: Create, read, edit, and validate PowerPoint presentations (.pptx).\n- `office-recalculate`: Refresh spreadsheet formula caches and verify calculated values with LibreOffice Kit.\n- `office-render`: Render requested document images or inspect visual layout with LibreOffice Kit.\n- `office-xlsx`: Read, create, edit, and validate Excel workbooks, data, and formulas (.xlsx).\n</available_skills>\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\nA user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.\n</system-reminder>"
"Query the packaged Python environment for Office authoring.Current runtime context. This snapshot supersedes earlier runtime-context snapshots.\n\nCurrent working directory: \"{{cwd}}\".\n\nCurrent DSH file policy: danger-full-access. The DSH file sandbox does not restrict file modifications by available operations.\n\nApproval prompts are disabled in this session: actions that require approval are rejected automatically — do not request sandbox escalation (do not set `sandbox_permissions`).<system-reminder>\nA skill is a reusable set of task-specific instructions. The following skills are available in this session:\n\n<available_skills>\n- `diagnose-windows-sandbox-acl`: Use on Windows for unexpected DSH sandbox access denials: workspace writes or listing fail, or an ordinarily readable path cannot be read. One bundled command inspects the path and every ancestor and repairs the ACL problems it proves in that same run. Expected confinement denials need no ACL repair.\n- `office`: Create, edit, inspect, or convert Office documents, spreadsheets, presentations, and PDFs. Load to choose the relevant workflow.\n- `office-docx`: Create, read, edit, and validate Word documents (.docx).\n- `office-export-pdf`: Convert an Office document to a requested PDF with LibreOffice Kit.\n- `office-pptx`: Create, read, edit, and validate PowerPoint presentations (.pptx).\n- `office-recalculate`: Refresh spreadsheet formula caches and verify calculated values with LibreOffice Kit.\n- `office-render`: Render requested document images or inspect visual layout with LibreOffice Kit.\n- `office-xlsx`: Read, create, edit, and validate Excel workbooks, data, and formulas (.xlsx).\n</available_skills>\n\nIf the user names a skill, or the task clearly matches a skill's description, call the `skill` tool with the exact skill name before taking task actions. Load all applicable skills, then follow their full instructions. This catalog contains summaries only; do not infer or follow a skill's instructions until it has been loaded.\nA user may also invoke a skill directly; its <skill_content> block then appears in this conversation. Follow it, and do not call the `skill` tool again for that skill.\n</system-reminder>Time sampled while preparing turn 1, step 1: 2026-10-16T00:00:00+00:00[UTC]\nBrowser time zone for this request: unavailable. Ask the user to clarify otherwise-unqualified dates and times.\nElapsed since the preceding model-visible message: unavailable."
],
"result": {
"python": "{{python}}",
@@ -30,7 +30,7 @@
"events": [
{
"type": "request/header",
"seq": 11,
"seq": 12,
"time": 0,
"data": {
"header": {
@@ -45,6 +45,7 @@
"maxTokens": true
},
"tools": [
"ask_user_question",
"create_goal",
"edit",
"exit_plan_mode",
@@ -56,8 +57,13 @@
"job_list",
"job_output",
"list_agents",
"present",
"read",
"read_image",
"schedule_create",
"schedule_delete",
"schedule_list",
"schedule_update",
"send_message",
"skill",
"subagent",
@@ -76,7 +82,7 @@
},
{
"type": "request/context",
"seq": 12,
"seq": 13,
"time": 0,
"data": {
"provider": "deepseek-official",
@@ -86,7 +92,7 @@
},
{
"type": "request/header",
"seq": 19,
"seq": 20,
"time": 0,
"data": {
"header": {
@@ -101,6 +107,7 @@
"maxTokens": true
},
"tools": [
"ask_user_question",
"create_goal",
"edit",
"exit_plan_mode",
@@ -112,8 +119,13 @@
"job_list",
"job_output",
"list_agents",
"present",
"read",
"read_image",
"schedule_create",
"schedule_delete",
"schedule_list",
"schedule_update",
"send_message",
"skill",
"snapshot_ping",
@@ -133,7 +145,7 @@
},
{
"type": "developer/message",
"seq": 20,
"seq": 21,
"time": 0,
"data": {
"turn": 1,
@@ -157,7 +169,7 @@
},
{
"type": "request/header",
"seq": 26,
"seq": 27,
"time": 0,
"data": {
"header": {
@@ -172,6 +184,7 @@
"maxTokens": true
},
"tools": [
"ask_user_question",
"create_goal",
"edit",
"exit_plan_mode",
@@ -183,8 +196,13 @@
"job_list",
"job_output",
"list_agents",
"present",
"read",
"read_image",
"schedule_create",
"schedule_delete",
"schedule_list",
"schedule_update",
"send_message",
"skill",
"subagent",
@@ -203,7 +221,7 @@
},
{
"type": "developer/message",
"seq": 27,
"seq": 28,
"time": 0,
"data": {
"turn": 1,
+15
View File
@@ -2526,6 +2526,21 @@
"doc": "docs/subsystems/session-projection.md",
"symbol": "ProjectionKeySelection",
"source": "packages/session/session-projection/src/index.ts"
},
{
"doc": "docs/subsystems/core.md",
"symbol": "DeliveryMode",
"source": "packages/core/agent-controller/src/types.ts"
},
{
"doc": "docs/subsystems/core.md",
"symbol": "DeliveryRequest",
"source": "packages/core/agent-controller/src/types.ts"
},
{
"doc": "docs/subsystems/core.md",
"symbol": "AdmissionDecision",
"source": "packages/core/agent-controller/src/types.ts"
}
]
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Package images under `src/client/assets/`: the client build preset replaces
* each import with the asset's document-relative, content-revisioned URL.
* The module names mirror the supported extensions of the client-modules
* asset format.
*/
declare module '*.svg' {
const url: string
export default url
}
declare module '*.png' {
const url: string
export default url
}
declare module '*.webp' {
const url: string
export default url
}
declare module '*.jpg' {
const url: string
export default url
}
declare module '*.jpeg' {
const url: string
export default url
}
+164
View File
@@ -10,9 +10,12 @@ import {
collectRuntimeLocalSourceSpecifiers,
collectRuntimeSourcePackageUses,
collectRuntimeSourceSpecifiers,
collectSourceAssetSpecifiers,
collectSourcePackageUses,
collectStylesheetAssetUrls,
fixClientPackageManifests,
readClientDeclarations,
readStylesheetAssetReferences,
type ClientDeclaration,
type ClientPackage,
type ClientPackageFacts,
@@ -33,10 +36,13 @@ function declaration(
name: short.startsWith('@') ? short : '@deepseek-ai/dsh-client-' + short,
manifest: 'packages/client/' + short.replace(/^.*\//, '') + '/package.json',
dynamic: true,
platform: 'web',
external: [],
inject: [],
runtimeSourceUses: {},
runtimeSourceSpecifiers: {},
assetExport: undefined,
assetReferences: [],
...fields,
}
}
@@ -285,6 +291,164 @@ describe('module requests', () => {
})
})
describe('package assets', () => {
const art = { file: 'packages/client/ui/src/client/View.tsx', reference: '"./assets/art.webp"' }
const mask = { file: 'packages/client/ui/src/client/View.module.css', reference: 'url("../assets/mask.png")' }
it('collects relative runtime module specifiers with a package-asset extension', () => {
expect([...collectSourceAssetSpecifiers('View.tsx', [
"import art from './assets/art.webp'",
"import back from '../assets/icons/back.SVG'",
"import './assets/side-effect.png'",
"export { default as photo } from './assets/photo.jpeg'",
"const lazy = () => import('./assets/lazy.jpg')",
"const legacy = require('./assets/legacy.svg')",
"import type Art from './assets/type-only.webp'",
"import styles from './View.module.css'",
"import font from './assets/font.woff2'",
"import raw from './assets/raw.svg?raw'",
"import shared from '@deepseek-ai/dsh-client-other/assets/shared.svg'",
"import rooted from '/assets/rooted.svg'",
].join('\n'))]).toEqual([
'./assets/art.webp',
'../assets/icons/back.SVG',
'./assets/side-effect.png',
'./assets/photo.jpeg',
'./assets/lazy.jpg',
'./assets/legacy.svg',
])
})
it.each(['View.module.css', 'global.css'])('collects relative url() values from %s', (path) => {
expect(collectStylesheetAssetUrls(path, [
".plain { background: url('./x.png'); }",
'.parent { mask: url("../assets/x.png") center / 100% no-repeat; }',
'.bare { background: url(./bare.svg); }',
".empty { background: url(''); }",
'.inline { background: url("data:image/svg+xml,%3Csvg%3E%3C/svg%3E"); }',
".fragment { filter: url('#frag'); }",
".rooted { background: url('/abs/x.png'); }",
'.network { background: url(//cdn.example.com/x.png); }',
".remote { background: url('https://example.com/x.png'); }",
"@supports (mask-image: url('')) { .nested { mask: url(./nested.svg); } }",
".set { background-image: image-set(url('./[email protected]') 1x, url('data:image/png;base64,AA==') 2x); }",
].join('\n'))).toEqual(['./x.png', '../assets/x.png', './bare.svg', './nested.svg', './[email protected]'])
})
it('rejects an unparsable stylesheet with its path', () => {
expect(() => collectStylesheetAssetUrls('src/View.module.css', '.a { composes: b; } .c .d { composes: a; }'))
.toThrow('verify-client-packages: cannot parse stylesheet src/View.module.css: The `composes` property')
})
it('reads url() references only from package stylesheets that Client sources import', () => {
const root = mkdtempSync(join(tmpdir(), 'client-package-assets-'))
roots.push(root)
const files: Record<string, string> = {
'packages/client/ui/src/client/View.module.css': ".view { mask: url('../assets/mask.png'); background: url(data:,x); }",
'packages/client/ui/src/client/global.css': '.global { background: url(./assets/global.svg); }',
'packages/client/ui/src/client/text.css': '.text { background: url("./assets/text.webp"); }',
'packages/client/ui/src/styles/static.css': '@font-face { src: url(./static.woff2); }',
'packages/client/ui/styles/outside.css': '.outside { background: url(./outside.png); }',
}
for (const [path, value] of Object.entries(files)) {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), value)
}
expect(readStylesheetAssetReferences(root, 'packages/client/ui', new Map([
['packages/client/ui/src/client/View.tsx', ['./View.module.css', './global.css', './assets/art.webp']],
['packages/client/ui/src/client/lazy.ts', ['./text.css?inline', '../../styles/outside.css', './View.module.css']],
['packages/client/ui/src/index.ts', ['katex/dist/katex.css', './client/missing.css.ts']],
]))).toEqual({
stylesheets: [
'packages/client/ui/src/client/View.module.css',
'packages/client/ui/src/client/global.css',
'packages/client/ui/src/client/text.css',
],
references: [
{ file: 'packages/client/ui/src/client/View.module.css', reference: 'url("../assets/mask.png")' },
{ file: 'packages/client/ui/src/client/global.css', reference: 'url("./assets/global.svg")' },
{ file: 'packages/client/ui/src/client/text.css', reference: 'url("./assets/text.webp")' },
],
})
})
it('reads the Web platform and the raw asset export from manifests', () => {
const root = mkdtempSync(join(tmpdir(), 'client-package-asset-manifests-'))
roots.push(root)
const files: Record<string, unknown> = {
'packages/g/web/package.json': {
name: '@f/web', dsh: { client: { platform: 'web' } }, exports: { './assets/*': './lib/assets/*' },
},
'packages/g/conditional/package.json': {
name: '@f/conditional', exports: { './assets/*': { default: './lib/assets/*' } },
},
'packages/g/none/package.json': { name: '@f/none', dsh: { client: { platform: 1 } }, exports: './lib/index.js' },
}
for (const [path, value] of Object.entries(files)) {
mkdirSync(dirname(join(root, path)), { recursive: true })
writeFileSync(join(root, path), JSON.stringify(value))
}
expect(readClientDeclarations(root).declarations.map(entry => [entry.name, entry.platform, entry.assetExport]))
.toEqual([
['@f/conditional', undefined, { default: './lib/assets/*' }],
['@f/none', undefined, undefined],
['@f/web', 'web', './lib/assets/*'],
])
})
it('accepts referenced assets with the export and unreferenced packages without it', () => {
expect(collectClientPackageViolations(facts([], {
declarations: [
declaration('ui', { assetExport: './lib/assets/*', assetReferences: [mask, art] }),
declaration('plain'),
declaration('shell', { dynamic: false, platform: undefined }),
],
}))).toEqual([])
})
it.each([
['./lib/assets/**', '"./lib/assets/**"'],
[{ default: './lib/assets/*' }, '{"default":"./lib/assets/*"}'],
[null, 'null'],
])('rejects the asset export target %j', (target, shown) => {
const ui = declaration('ui', { assetExport: target, assetReferences: [art] })
expect(collectClientPackageViolations(facts([], { declarations: [ui] }))).toEqual([
ui.manifest + ': exports["./assets/*"] is ' + shown + '; it must be exactly "./lib/assets/*"',
])
})
it.each([
['a statically linked package', { dynamic: false, platform: undefined }],
['a row for another platform', { platform: 'desktop' }],
['a row without a platform', { platform: undefined }],
] as const)('rejects the asset export on %s', (_label, fields) => {
const ui = declaration('ui', { ...fields, assetExport: './lib/assets/*', assetReferences: [art] })
expect(collectClientPackageViolations(facts([], { declarations: [ui] }))).toEqual([
ui.manifest + ': ' + ui.name + ' declares exports["./assets/*"] without a dsh.client row for platform "web";'
+ ' only dynamic Web Client packages publish package assets',
])
})
it('rejects a referenced asset without the export and names the first reference', () => {
const ui = declaration('ui', { assetReferences: [mask, art] })
expect(collectClientPackageViolations(facts([], { declarations: [ui] }))).toEqual([
ui.manifest + ': ' + ui.name + ' references package asset url("../assets/mask.png") in '
+ 'packages/client/ui/src/client/View.module.css but declares no exports["./assets/*"];'
+ ' add "./assets/*": "./lib/assets/*" and publish "lib/assets"',
])
})
it('rejects an unused asset export', () => {
const ui = declaration('ui', { assetExport: './lib/assets/*' })
expect(collectClientPackageViolations(facts([], { declarations: [ui] }))).toEqual([
ui.manifest + ': ' + ui.name + ' declares an unused ./assets/* export;'
+ ' no Client source or stylesheet it imports references a package asset',
])
})
})
describe('manifest declarations', () => {
it('reports malformed arrays without hiding other packages', () => {
const root = mkdtempSync(join(tmpdir(), 'client-packages-'))
+202 -15
View File
@@ -1,11 +1,18 @@
/**
* Verify client package modes and the synchronous browser module-request graph.
* Verify client package modes, the synchronous browser module-request graph, and package-asset publication.
*/
import { globSync, readFileSync, writeFileSync } from 'node:fs'
import { dirname, resolve, sep } from 'node:path'
import { dirname, posix, resolve, sep } from 'node:path'
import { pathToFileURL } from 'node:url'
import { transform } from 'lightningcss'
import ts from 'typescript'
import {
PACKAGE_ASSET_EXPORT,
PACKAGE_ASSET_EXPORT_TARGET,
PACKAGE_ASSET_FILES_ENTRY,
packageAssetMediaType,
} from '../packages/client/modules/src/assets.ts'
import { TypeScriptProject } from './ts-project.ts'
const GATE = 'verify-client-packages'
@@ -16,18 +23,38 @@ const PLATFORM_SOURCE = 'packages/client/web/src/platform.ts'
const PARSER_PRELOAD_SOURCE = 'packages/client/modules/src/index.ts'
const STATIC_PRESET_SOURCE = 'packages/client/tsdown.client.ts'
const CORDIS = '@deepseek-ai/cordis'
/** Stylesheet requests the dynamic build preset compiles: CSS Modules, global sheets, and `?inline` text. */
const STYLESHEET_REQUEST = /\.css(?:\?inline)?$/u
const INLINE_STYLESHEET_QUERY = /\?inline$/u
/** One source location whose reference makes the dynamic build preset emit a package asset. */
export interface AssetReference {
/** Repository-relative, `/`-separated source or stylesheet path. */
readonly file: string
/** The JSON-quoted module specifier, or `url("<value>")` for a stylesheet reference. */
readonly reference: string
}
/** One workspace package's browser-module declaration. */
export interface ClientDeclaration {
readonly name: string
readonly manifest: string
readonly dynamic: boolean
/** String `dsh.client.platform`; undefined without a row or a string platform. */
readonly platform: string | undefined
readonly external: readonly string[]
readonly runtimeSourceUses: Readonly<Record<string, readonly string[]>>
/** Exact runtime specifiers used to validate `dsh.client.external` declarations. */
readonly runtimeSourceSpecifiers: Readonly<Record<string, readonly string[]>>
/** Informational package dependencies declared by the row. */
readonly inject: readonly string[]
/** Raw `exports["./assets/*"]` value; undefined when the manifest declares no such subpath. */
readonly assetExport: unknown
/**
* Package-asset references in a dynamic Web row's Client sources under `src/` and in the package stylesheets they
* import, sorted by file and then reference; empty for every other package.
*/
readonly assetReferences: readonly AssetReference[]
}
/** One package directly under packages/client. */
@@ -112,6 +139,94 @@ export function collectRuntimeLocalSourceSpecifiers(path: string, source: string
return collectSourceFileUses(sourceFile, true, 'local', includeRootRelative)
}
/**
* Collect package-asset module specifiers retained by one production source file.
* @param path - File path used to select TypeScript's parser mode.
* @param source - Source text to inspect.
* @returns Relative runtime imports, re-exports, and requires whose extension has a package-asset media type.
*/
export function collectSourceAssetSpecifiers(path: string, source: string): Set<string> {
const sourceFile = ts.createSourceFile(path, source, ts.ScriptTarget.Latest, true)
return new Set([...collectSourceFileUses(sourceFile, true, 'local')].filter(isAssetSpecifier))
}
function isAssetSpecifier(specifier: string): boolean {
return packageAssetMediaType(specifier) !== undefined
}
/**
* Collect the relative `url()` values of one stylesheet, which the dynamic build preset resolves to package assets.
* @param path - Stylesheet path; a `.module.css` name enables CSS Modules syntax, as in the build preset.
* @param source - Stylesheet text.
* @returns `url()` values in source order, excluding empty values, fragments, root-relative paths, and URLs with a
* scheme such as `data:` or `https:`.
* @throws when lightningcss cannot parse the stylesheet; the message names `path`.
*/
export function collectStylesheetAssetUrls(path: string, source: string): string[] {
const urls: string[] = []
try {
transform({
filename: path,
code: Buffer.from(source),
cssModules: path.endsWith('.module.css'),
visitor: {
Url(url) {
if (isRelativeStylesheetUrl(url.url)) urls.push(url.url)
},
// lightningcss does not visit the urls inside image-set(), so its options are read here.
Image(image) {
if (image.type !== 'image-set') return
for (const option of image.value.options) {
if (option.image.type === 'url' && isRelativeStylesheetUrl(option.image.value.url)) urls.push(option.image.value.url)
}
},
},
})
} catch (error) {
throw new Error(
GATE + ': cannot parse stylesheet ' + path + ': ' + (error instanceof Error ? error.message : String(error)),
{ cause: error },
)
}
return urls
}
function isRelativeStylesheetUrl(url: string): boolean {
return url !== '' && !url.startsWith('#') && !url.startsWith('/') && !/^[a-z][a-z\d+.-]*:/iu.test(url)
}
/**
* Read package-asset references from the package stylesheets that Client sources import. The dynamic build preset
* compiles exactly these stylesheets; a stylesheet no Client source imports, such as a copied static payload, keeps
* its `url()` values unchanged.
* @param root - Absolute repository root.
* @param packageDirectory - Repository-relative, `/`-separated package directory.
* @param importers - Repository-relative, `/`-separated Client source paths and their runtime local module specifiers.
* @returns Imported `.css` and `.css?inline` stylesheets under the package's `src/`, in path order, and their references.
*/
export function readStylesheetAssetReferences(
root: string,
packageDirectory: string,
importers: ReadonlyMap<string, Iterable<string>>,
): { readonly stylesheets: readonly string[]; readonly references: readonly AssetReference[] } {
const sourcePrefix = packageDirectory + '/src/'
const imported = new Set<string>()
for (const [file, specifiers] of importers) {
for (const specifier of specifiers) {
if (!specifier.startsWith('.') || !STYLESHEET_REQUEST.test(specifier)) continue
const stylesheet = posix.join(posix.dirname(file), specifier.replace(INLINE_STYLESHEET_QUERY, ''))
if (stylesheet.startsWith(sourcePrefix)) imported.add(stylesheet)
}
}
const stylesheets = [...imported].sort()
return {
stylesheets,
references: stylesheets.flatMap(file =>
collectStylesheetAssetUrls(file, readFileSync(resolve(root, file), 'utf8'))
.map(url => ({ file, reference: 'url(' + JSON.stringify(url) + ')' }))),
}
}
function importCarriesRuntimeValue(node: ts.ImportDeclaration): boolean {
const clause = node.importClause
if (clause === undefined) return true
@@ -190,7 +305,7 @@ export function readClientDeclarations(root: string): ClientDeclarations {
/**
* Return every client package policy violation.
* @param facts - Package modes, manifests, source uses, and platform module lists.
* @param facts - Package modes, manifests, source uses, package-asset references, and platform module lists.
* @returns Stable self-contained diagnostics.
*/
export function collectClientPackageViolations(facts: ClientPackageFacts): string[] {
@@ -198,6 +313,7 @@ export function collectClientPackageViolations(facts: ClientPackageFacts): strin
...facts.malformed,
...collectModeViolations(facts),
...collectModuleViolations(facts),
...collectAssetViolations(facts),
].sort((left, right) => left.localeCompare(right))
}
@@ -440,9 +556,51 @@ function formatCycle(
return manifest + ': synchronous dsh.client.external cycle: ' + chain + ' ' + (entry?.from ?? '')
}
/** Whether the dynamic build preset builds this package's browser face, the only face that emits package assets. */
function buildsDynamicWebClient(declaration: ClientDeclaration): boolean {
return declaration.dynamic && declaration.platform === 'web'
}
function collectAssetViolations(facts: ClientPackageFacts): string[] {
const violations: string[] = []
const exportKey = 'exports[' + JSON.stringify(PACKAGE_ASSET_EXPORT) + ']'
for (const pkg of facts.declarations) {
const declared = pkg.assetExport !== undefined
const covered = buildsDynamicWebClient(pkg)
if (declared && pkg.assetExport !== PACKAGE_ASSET_EXPORT_TARGET) {
violations.push(
pkg.manifest + ': ' + exportKey + ' is ' + JSON.stringify(pkg.assetExport)
+ '; it must be exactly ' + JSON.stringify(PACKAGE_ASSET_EXPORT_TARGET),
)
}
if (declared && !covered) {
violations.push(
pkg.manifest + ': ' + pkg.name + ' declares ' + exportKey + ' without a dsh.client row for platform "web";'
+ ' only dynamic Web Client packages publish package assets',
)
}
if (!covered) continue
const first = pkg.assetReferences[0]
if (!declared && first !== undefined) {
violations.push(
pkg.manifest + ': ' + pkg.name + ' references package asset ' + first.reference + ' in ' + first.file
+ ' but declares no ' + exportKey + '; add ' + JSON.stringify(PACKAGE_ASSET_EXPORT) + ': '
+ JSON.stringify(PACKAGE_ASSET_EXPORT_TARGET) + ' and publish ' + JSON.stringify(PACKAGE_ASSET_FILES_ENTRY),
)
} else if (declared && first === undefined) {
violations.push(
pkg.manifest + ': ' + pkg.name + ' declares an unused ' + PACKAGE_ASSET_EXPORT + ' export;'
+ ' no Client source or stylesheet it imports references a package asset',
)
}
}
return violations
}
interface Manifest {
name?: unknown
dsh?: unknown
exports?: unknown
dependencies?: Record<string, string>
peerDependencies?: Record<string, string>
devDependencies?: Record<string, string>
@@ -457,27 +615,27 @@ function readDeclaration(
if (typeof manifest.name !== 'string') return undefined
const dsh = isRecord(manifest.dsh) ? manifest.dsh : undefined
const rawClient = dsh?.client
if (rawClient === undefined) {
return {
name: manifest.name, manifest: manifestPath, dynamic: false, external: [], inject: [],
runtimeSourceUses: {}, runtimeSourceSpecifiers: {},
}
}
const assetExport = isRecord(manifest.exports) ? manifest.exports[PACKAGE_ASSET_EXPORT] : undefined
const rowless = {
name: manifest.name, manifest: manifestPath, dynamic: false, platform: undefined, external: [], inject: [],
runtimeSourceUses: {}, runtimeSourceSpecifiers: {}, assetExport, assetReferences: [],
} satisfies ClientDeclaration
if (rawClient === undefined) return rowless
if (!isRecord(rawClient)) {
malformed.push(manifestPath + ': ' + manifest.name + ' dsh.client must be an object')
return {
name: manifest.name, manifest: manifestPath, dynamic: false, external: [], inject: [],
runtimeSourceUses: {}, runtimeSourceSpecifiers: {},
}
return rowless
}
return {
name: manifest.name,
manifest: manifestPath,
dynamic: true,
platform: typeof rawClient.platform === 'string' ? rawClient.platform : undefined,
external: stringArray(rawClient.external, manifest.name, manifestPath, 'external', malformed),
inject: stringArray(rawClient.inject, manifest.name, manifestPath, 'inject', malformed),
runtimeSourceUses: {},
runtimeSourceSpecifiers: {},
assetExport,
assetReferences: [],
}
}
@@ -563,9 +721,13 @@ async function readFacts(root: string): Promise<ClientPackageFacts> {
const staticLinkedPackages = await readStaticLinkedRoster(root)
const project = new TypeScriptProject(root, 'client')
const sourceFiles = project.sourceFiles()
let webStylesheets = 0
const declarations = bareDeclarations.map((declaration): ClientDeclaration => {
const runtimeSourceUses = new Map<string, Set<string>>()
const runtimeSourceSpecifiers = new Map<string, Set<string>>()
const assetReferences: AssetReference[] = []
const scansAssets = buildsDynamicWebClient(declaration)
const importers = new Map<string, ReadonlySet<string>>()
const sourcePrefix = dirname(declaration.manifest) + '/src/'
for (const sourceFile of sourceFiles) {
if (sourceFile.isDeclarationFile) continue
@@ -581,6 +743,24 @@ async function readFacts(root: string): Promise<ClientPackageFacts> {
locations.add(file)
runtimeSourceSpecifiers.set(specifier, locations)
}
if (!scansAssets) continue
const localSpecifiers = collectSourceFileUses(sourceFile, true, 'local')
importers.set(file, localSpecifiers)
for (const specifier of localSpecifiers) {
if (isAssetSpecifier(specifier)) assetReferences.push({ file, reference: JSON.stringify(specifier) })
}
}
if (scansAssets) {
// A Web row always has a browser half, so an empty source set means the Client program stopped covering it.
if (importers.size === 0) {
throw new Error(
GATE + ': the Client program loads no source under ' + sourcePrefix + ' for dynamic Web row '
+ declaration.name + '; its package-asset references cannot be read',
)
}
const stylesheets = readStylesheetAssetReferences(root, dirname(declaration.manifest), importers)
webStylesheets += stylesheets.stylesheets.length
assetReferences.push(...stylesheets.references)
}
return {
...declaration,
@@ -592,8 +772,13 @@ async function readFacts(root: string): Promise<ClientPackageFacts> {
[...runtimeSourceSpecifiers].sort(([left], [right]) => left.localeCompare(right))
.map(([specifier, locations]) => [specifier, [...locations].sort()]),
),
assetReferences: assetReferences.sort((left, right) =>
left.file.localeCompare(right.file) || left.reference.localeCompare(right.reference)),
}
})
if (webStylesheets === 0) {
throw new Error(GATE + ': no dynamic Web row imports a stylesheet from its src/ directory; the stylesheet scan read an empty corpus')
}
const byManifest = new Map(declarations.map(entry => [entry.manifest, entry]))
const packages: ClientPackage[] = []
@@ -701,10 +886,12 @@ async function main(): Promise<void> {
const dynamic = facts.packages.filter(pkg => pkg.dynamic).length
const requests = facts.declarations.reduce((total, pkg) => total + pkg.external.length, 0)
const assetExports = facts.declarations.filter(pkg => pkg.assetExport !== undefined).length
console.log(
GATE + ': ' + String(facts.packages.length) + ' client packages (' + String(dynamic) + ' dynamic, '
+ String(facts.packages.length - dynamic) + ' statically linked) satisfy package-mode and module-request rules; '
+ String(requests) + ' explicit external request(s).',
+ String(facts.packages.length - dynamic) + ' statically linked) satisfy package-mode, module-request, and '
+ 'package-asset rules; ' + String(requests) + ' explicit external request(s), ' + String(assetExports)
+ ' package-asset export(s).',
)
}
+1 -1
View File
@@ -1,7 +1,7 @@
import { describe, expect, it } from 'vitest'
import { findDispatcherViolations, scanRepository, DISPATCHER_OWNER } from './verify-no-bare-dispatcher.ts'
const FILE = 'packages/web/web-fetch-http/src/network.ts'
const FILE = 'packages/web/web-fetch-http/src/provider.ts'
function reasons(source: string, file = FILE): string[] {
return findDispatcherViolations(file, source).map(violation => violation.what)
+2 -3
View File
@@ -8,9 +8,8 @@
* agent silently bypassed every proxy.
*
* `proxyRouteFor(url)` from that package is the sanctioned way to ask where one request goes and to
* get the transport that answer assumed. A call site that genuinely owns its transport — because it
* carries per-request state the process-wide dispatcher cannot, as `web-fetch-http`'s address
* pinning does — says so with the marker below.
* get the transport that answer assumed. A call site that genuinely owns its transport, because it
* carries per-request state the process-wide dispatcher cannot, says so with the marker below.
*
* Discovery is syntax-aware, as `scripts/AGENTS.md` requires: a line-wise regex misses the
* `{ dispatcher }` shorthand and a `new Alias(...)` whose import renamed `Agent`, and both bypass the
@@ -707,6 +707,36 @@ describe('face-aware source classification', () => {
})).rejects.toThrow('Host export ./node cannot map ./other/node.js to a source entry')
})
it.each(['./lib/assets/*', { default: './lib/assets/*.js' }])(
'does not classify the package-asset export %j as Host code', async (target) => {
const root = writeWorkspace({
'packages/client/ui-probe/package.json': {
name: '@deepseek-ai/dsh-client-ui-probe',
dsh: { client: { platform: 'web' } },
exports: {
'.': { types: './lib/types/index.d.ts', default: './lib/index.js' },
'./client': { types: './lib/types/client/index.d.ts', default: './lib/client.js' },
'./assets/*': target,
},
...CORDIS_PEER,
},
'packages/client/ui-probe/src/index.ts': 'export function apply() {}\n',
'packages/client/ui-probe/src/client/index.ts': "import art from './assets/art.webp'\nexport const image = art\n",
})
const state = await readPackageDependencyState(root, policy())
expect(state.facts.map(fact => fact.manifestPath)).toEqual(['packages/client/ui-probe/package.json'])
expect(collectPackageDependencyViolations(state)).toEqual([])
},
)
it('scans other subpaths below ./assets as Host exports', async () => {
await expect(sourceFacts({ 'src/index.ts': 'export function apply() {}' }, {
exports: { './assets/index': './lib/assets/index.js' },
})).rejects.toThrow('Host export ./assets/index has no source entry')
})
it('fails when a managed Host package has no Host entry', async () => {
const root = mkdtempSync(join(tmpdir(), 'dsh-package-missing-host-'))
roots.push(root)
+7 -2
View File
@@ -10,6 +10,7 @@ import { dirname, extname, join, normalize, relative, resolve, sep } from 'node:
import { pathToFileURL } from 'node:url'
import ts from 'typescript'
import { WorkspaceTypertGenerator } from '../packages/typert/generator/src/workspace.ts'
import { PACKAGE_ASSET_EXPORT } from '../packages/client/modules/src/assets.ts'
import { writeModuleGraph } from './gen-module-graph.ts'
import {
hasClientDeclaration,
@@ -363,7 +364,10 @@ function generatedHostSources(root: string, packages: readonly WorkspacePackageM
return sources
}
/** Source-backed Node exports; generated Typert artifacts have no standalone source entry. */
/**
* Source-backed Node exports. Generated Typert artifacts have no standalone source entry; `./client`, `./src`, and
* package-asset subpaths publish no Host code.
*/
function hostSourceEntries(root: string, pkg: WorkspacePackageManifest): string[] {
const entry = resolve(root, pkg.dir, 'src/index.ts')
if (!existsSync(entry)) {
@@ -373,7 +377,8 @@ function hostSourceEntries(root: string, pkg: WorkspacePackageManifest): string[
const exports = pkg.manifest.exports
if (exports === null || typeof exports !== 'object') return [...entries]
for (const [subpath, declaration] of Object.entries(exports as Record<string, unknown>)) {
if (subpath === '.' || subpath === './package.json' || /^\.\/(?:client|src)(?:\/|$)/.test(subpath)) continue
if (subpath === '.' || subpath === './package.json' || subpath === PACKAGE_ASSET_EXPORT
|| /^\.\/(?:client|src)(?:\/|$)/.test(subpath)) continue
const types = declaration !== null && typeof declaration === 'object' && 'types' in declaration
? declaration.types
: undefined
@@ -97,6 +97,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
'packages/util/plugin-market-index': { kind: 'indirect', reason: 'Pure index validation; the plugin_market tool owns the entries it returns to the model.' },
'packages/util/time': { kind: 'indirect', reason: 'Pure zone validation; the consumer that records a canonical zone owns the model-visible line derived from it.' },
'packages/core/agent-default-model': { kind: 'indirect', reason: 'The service supplies a ModelSelection; request assembly and adapters own the model-visible request.' },
'packages/core/agent-controller': { kind: 'indirect', reason: 'The controller appends sender-prepared messages to the inbox; the Agent loop owns how they enter a model request.' },
'packages/llm/deepseek-llm-api-extensions': { kind: 'indirect', reason: 'The registry contributes model-hidden provider fields; dsh-llm-deepseek owns their wire placement.' },
'packages/preset/agent-preset': { kind: 'indirect', reason: 'The declaration selects plugins that own model-visible contributions.' },
'packages/typert/registry': { kind: 'none', reason: 'Runtime type registry; consumers (cordis_inspect, wire faces, gates) own any model-visible projection of registry contents.' },