Merge remote-tracking branch 'origin/master' into cross-family-fs-sandbox
# Conflicts: # docs/config-catalog.md # docs/cordis-catalog/services.md # docs/module-graph.md # docs/rfc/implemented/feature/2026-07-06-sandbox.md # examples/acp-agent/tests/snapshots/advanced-toolchain/system-prompt.golden.md # examples/acp-agent/tests/snapshots/advanced-toolchain/tool-schemas.golden.json # examples/acp-agent/tests/snapshots/both-mode-turn/system-prompt.golden.md # examples/acp-agent/tests/snapshots/both-mode-turn/tool-schemas.golden.json # examples/acp-agent/tests/snapshots/code-mode-turn/system-prompt.golden.md # examples/acp-agent/tests/snapshots/escalation-approved/session.jsonl # examples/acp-agent/tests/snapshots/escalation-rejected/session.jsonl # examples/acp-agent/tests/snapshots/hook-cc-pretool-ask/session.jsonl # examples/acp-agent/tests/snapshots/permission-switching/session.jsonl # examples/acp-agent/tests/snapshots/permission-switching/system-prompt.golden.md # examples/acp-agent/tests/snapshots/permission-switching/tool-schemas.golden.json # examples/acp-agent/tests/snapshots/skill-load/tool-schemas.golden.json # examples/acp-agent/tests/snapshots/text-turn/tool-schemas.golden.json # examples/acp-agent/tests/snapshots/workspace-edit/system-prompt.golden.md # examples/acp-agent/tests/snapshots/workspace-edit/tool-schemas.golden.json # packages/bash/bash-sandbox/src/index.ts # packages/bash/bash-sandbox/tests/bwrap.e2e.ts # packages/bash/bash-sandbox/tests/sandbox.spec.ts # packages/bash/bash-sandbox/tests/seatbelt.e2e.ts # packages/bash/bash/src/index.ts # packages/bash/tool-bash/package.json # packages/bash/tool-bash/src/index.ts # packages/bash/tool-bash/src/render.ts # packages/bash/tool-bash/tests/tools.spec.ts # packages/bash/tool-bash/tsconfig.json # pnpm-lock.yaml # scripts/verify-package-readme-model-experience.ts
This commit is contained in:
@@ -116,11 +116,31 @@ const dshWorkerPackageFiles = [
|
||||
'src',
|
||||
] as const
|
||||
|
||||
const packageFileExtras: Readonly<Record<string, readonly string[]>> = {
|
||||
'@deepseek-ai/dsh-helper': ['lib/assets'],
|
||||
'@deepseek-ai/dsh-scripts': [
|
||||
'lib/dev/tsdown-config.js',
|
||||
'lib/local-plugin-loader-hooks.js',
|
||||
'lib/assets',
|
||||
],
|
||||
}
|
||||
|
||||
function sameStringList(actual: readonly string[] | undefined, expected: readonly string[]): boolean {
|
||||
return !!actual && actual.length === expected.length && actual.every((value, index) => value === expected[index])
|
||||
}
|
||||
|
||||
function expectedDshPackageFiles(manifest: PackageManifest): readonly string[] {
|
||||
const extras = manifest.name ? packageFileExtras[manifest.name] ?? [] : []
|
||||
if (extras.length > 0) {
|
||||
return [
|
||||
'lib/index.js',
|
||||
...manifest.bin ? ['lib/bin.js'] : [],
|
||||
...extras,
|
||||
'lib/types/**/*.d.ts',
|
||||
'lib/types/**/*.d.ts.map',
|
||||
'src',
|
||||
]
|
||||
}
|
||||
if (manifest.bin) return dshBinPackageFiles
|
||||
// A declared "./worker" subpath export sanctions the one extra runtime
|
||||
// bundle a worker-thread entry needs (and NodeNext/publint then validate
|
||||
|
||||
@@ -46,8 +46,6 @@ export const LINK_MAP: Record<string, string> = {
|
||||
BashExecRequest: 'bash.md',
|
||||
BashExecSpec: 'bash.md',
|
||||
BashRunResult: 'bash.md',
|
||||
BashTask: 'bash.md',
|
||||
BashTaskRead: 'bash.md',
|
||||
ConfinedArgv: 'sandbox.md',
|
||||
SandboxMode: 'sandbox.md',
|
||||
SandboxPolicy: 'sandbox.md',
|
||||
|
||||
@@ -64,6 +64,8 @@ const GROUP_ORDER = [
|
||||
'skill',
|
||||
'compact',
|
||||
'subagent',
|
||||
'tasks',
|
||||
'workflow',
|
||||
'web',
|
||||
'todo',
|
||||
'cordis',
|
||||
@@ -240,6 +242,14 @@ const SERVICE_ROLES: ServiceRole[] = [
|
||||
consumers: ['tool-subagent'],
|
||||
note: 'Providers implement transports; tool-subagent exposes one configured provider as a model-facing tool name.',
|
||||
},
|
||||
{
|
||||
key: 'tasks',
|
||||
pkg: 'tasks',
|
||||
title: 'Background task registry',
|
||||
mode: 'core',
|
||||
consumers: ['tool-bash', 'tool-subagent', 'tool-tasks'],
|
||||
note: 'Producers (tool-bash background commands, tool-subagent background delegations) register running work; tool-tasks is the model-facing control surface that reads, lists, and kills it.',
|
||||
},
|
||||
{
|
||||
key: 'web',
|
||||
pkg: 'web',
|
||||
|
||||
@@ -22,11 +22,13 @@ import SubagentService from '@deepseek-ai/dsh-subagent'
|
||||
import * as SubagentMock from '@deepseek-ai/dsh-subagent-mock'
|
||||
import SkillService from '@deepseek-ai/dsh-skill'
|
||||
import * as SkillLocal from '@deepseek-ai/dsh-skill-local'
|
||||
import TaskService from '@deepseek-ai/dsh-tasks'
|
||||
import * as ToolAskUser from '@deepseek-ai/dsh-tool-ask-user'
|
||||
import * as ToolBash from '@deepseek-ai/dsh-tool-bash'
|
||||
import * as ToolCordis from '@deepseek-ai/dsh-tool-cordis'
|
||||
import * as ToolFs from '@deepseek-ai/dsh-tool-fs'
|
||||
import * as ToolSkill from '@deepseek-ai/dsh-tool-skill'
|
||||
import * as ToolTasks from '@deepseek-ai/dsh-tool-tasks'
|
||||
import * as ToolTodo from '@deepseek-ai/dsh-tool-todo'
|
||||
import * as ToolSubagent from '@deepseek-ai/dsh-tool-subagent'
|
||||
import * as ToolWeb from '@deepseek-ai/dsh-tool-web'
|
||||
@@ -111,14 +113,14 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
pkg: '@deepseek-ai/dsh-tool-bash',
|
||||
dir: 'tool-bash',
|
||||
source: 'packages/bash/tool-bash/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.bash'],
|
||||
writes: ['tool/call', 'tool/result', 'context/message via agent.inject() for background completion notices'],
|
||||
requires: ['ctx.tools', 'ctx.bash', 'ctx.tasks at call time for run_in_background'],
|
||||
writes: ['tool/call', 'tool/result'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(LocalBashExecutor)
|
||||
await ctx.plugin(ToolBash)
|
||||
},
|
||||
note:
|
||||
'The bash/bash_output/bash_kill tools are model-facing consumers of the bash executor seam.',
|
||||
'The bash tool is the model-facing consumer of the bash executor seam. A `run_in_background` run registers with the generic `ctx.tasks` runtime and is collected/stopped through the `task_*` tools from `@deepseek-ai/dsh-tool-tasks`; the `enableRunInBackground` config (default true) removes the parameter entirely when disabled.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-cordis',
|
||||
@@ -178,6 +180,19 @@ const TOOL_PACKAGES: ToolPackage[] = [
|
||||
note:
|
||||
'The registered tool name is the load-time `toolName` config (default `subagent`); the schema above is that default. The shipped example agents load this package once per subagent backend, so the model additionally sees `subagent_fork` (bound to the fork backend) with an identical schema — see `examples/coding-agent/cordis.yml` and `examples/acp-agent/cordis.yml`.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-tasks',
|
||||
dir: 'tool-tasks',
|
||||
source: 'packages/tasks/tool-tasks/src/index.ts',
|
||||
requires: ['ctx.tools', 'ctx.tasks', 'ctx.systemPrompt'],
|
||||
writes: ['tool/call', 'tool/result', 'context/message via agent.inject() for background completion notices'],
|
||||
async mount(ctx) {
|
||||
await ctx.plugin(TaskService)
|
||||
await ctx.plugin(ToolTasks)
|
||||
},
|
||||
note:
|
||||
'The kind-agnostic background-task control surface: a background bash command and a background subagent are read, listed, and killed through the same three tools. Loading the plugin attaches the control surface that arms producers\' `ctx.tasks.start()`.',
|
||||
},
|
||||
{
|
||||
pkg: '@deepseek-ai/dsh-tool-todo',
|
||||
dir: 'tool-todo',
|
||||
|
||||
@@ -88,8 +88,15 @@
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashRunResult", "source": "packages/bash/bash/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashSandboxInfo", "source": "packages/bash/bash/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "CollectedOutput", "source": "packages/bash/bash/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashTask", "source": "packages/bash/bash/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashTaskRead", "source": "packages/bash/bash/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashProcess", "source": "packages/bash/bash/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/bash.md", "symbol": "BashProcessRead", "source": "packages/bash/bash/src/types.ts" },
|
||||
|
||||
{ "doc": "docs/core-data-structures/tasks.md", "symbol": "TaskKindMap", "source": "packages/tasks/tasks/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/tasks.md", "symbol": "TaskStart", "source": "packages/tasks/tasks/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/tasks.md", "symbol": "TaskHooks", "source": "packages/tasks/tasks/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/tasks.md", "symbol": "TaskOutcome", "source": "packages/tasks/tasks/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/tasks.md", "symbol": "TaskSnapshot", "source": "packages/tasks/tasks/src/types.ts" },
|
||||
{ "doc": "docs/core-data-structures/tasks.md", "symbol": "TaskRead", "source": "packages/tasks/tasks/src/types.ts" },
|
||||
|
||||
{ "doc": "docs/core-data-structures/sandbox.md", "symbol": "SandboxMode", "source": "packages/sandbox/sandbox/src/index.ts" },
|
||||
{ "doc": "docs/core-data-structures/sandbox.md", "symbol": "ConfinedSandboxMode", "source": "packages/sandbox/sandbox/src/index.ts" },
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
* re-exports keep their docs at the declaring contract. Unknown forms fail closed.
|
||||
*/
|
||||
|
||||
import { existsSync, globSync } from 'node:fs'
|
||||
import { resolve } from 'node:path'
|
||||
import { existsSync, globSync, readFileSync } from 'node:fs'
|
||||
import { relative, resolve, sep } from 'node:path'
|
||||
import ts from 'typescript'
|
||||
import { checkParams, checkReturns, parseJsDoc, parseTags, pointer, rawJsDoc } from './jsdoc.ts'
|
||||
|
||||
@@ -389,7 +389,13 @@ function checkDecl(
|
||||
* @param w - the walk state violations append to.
|
||||
* @param ambient - whether this scope is ambient (`declare` namespace or a declaration file), where members export implicitly.
|
||||
*/
|
||||
function checkScope(statements: readonly ts.Statement[], prefix: string, w: Walk, ambient: boolean): void {
|
||||
function checkScope(
|
||||
statements: readonly ts.Statement[],
|
||||
prefix: string,
|
||||
w: Walk,
|
||||
ambient: boolean,
|
||||
allowedNames?: ReadonlySet<string>,
|
||||
): void {
|
||||
const byName = new Map<string, ts.Statement[]>()
|
||||
const overloadSigs = new Set<string>()
|
||||
const add = (name: string, stmt: ts.Statement): void => {
|
||||
@@ -455,7 +461,20 @@ function checkScope(statements: readonly ts.Statement[], prefix: string, w: Walk
|
||||
}
|
||||
continue
|
||||
}
|
||||
if (isExported(stmt) || (ambient && !ts.isImportDeclaration(stmt))) request(stmt, null)
|
||||
if (isExported(stmt) || (ambient && !ts.isImportDeclaration(stmt))) {
|
||||
if (allowedNames === undefined) {
|
||||
request(stmt, null)
|
||||
} else if (ts.isVariableStatement(stmt)) {
|
||||
for (const declaration of stmt.declarationList.declarations) {
|
||||
if (ts.isIdentifier(declaration.name) && allowedNames.has(declaration.name.text)) {
|
||||
request(stmt, declaration.name.text)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const name = declarationName(stmt) ?? 'default'
|
||||
if (allowedNames.has(name)) request(stmt, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const stmt of statements) {
|
||||
const only = requested.get(stmt)
|
||||
@@ -463,6 +482,67 @@ function checkScope(statements: readonly ts.Statement[], prefix: string, w: Walk
|
||||
}
|
||||
}
|
||||
|
||||
function exportedTargets(value: unknown): string[] {
|
||||
if (typeof value === 'string') return [value]
|
||||
if (!value || typeof value !== 'object') return []
|
||||
return Object.values(value).flatMap(exportedTargets)
|
||||
}
|
||||
|
||||
function sourceEntry(target: string): string | undefined {
|
||||
if (target.startsWith('./lib/types/') && target.endsWith('.d.ts')) {
|
||||
return `src/${target.slice('./lib/types/'.length, -'.d.ts'.length)}.ts`
|
||||
}
|
||||
if (target.startsWith('./lib/') && target.endsWith('.js')) {
|
||||
return `src/${target.slice('./lib/'.length, -'.js'.length)}.ts`
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
function declarationName(declaration: ts.Node): string | undefined {
|
||||
const name = (declaration as ts.NamedDeclaration).name
|
||||
if (name && ts.isIdentifier(name)) return name.text
|
||||
return undefined
|
||||
}
|
||||
|
||||
/** Resolve the declarations reachable through packages that do not export src/*. */
|
||||
function restrictedPublicNames(
|
||||
scanRoot: string,
|
||||
rels: readonly string[],
|
||||
program: ts.Program,
|
||||
checker: ts.TypeChecker,
|
||||
): { restrictedPackages: Set<string>; namesByFile: Map<string, Set<string>> } {
|
||||
const restrictedPackages = new Set<string>()
|
||||
const namesByFile = new Map<string, Set<string>>()
|
||||
const packages = new Set(rels.map(rel => rel.split('/').slice(0, 3).join('/')))
|
||||
for (const packageDir of packages) {
|
||||
const manifestPath = resolve(scanRoot, packageDir, 'package.json')
|
||||
if (!existsSync(manifestPath)) continue
|
||||
const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { exports?: Record<string, unknown> }
|
||||
if (!manifest.exports || manifest.exports['./src/*'] !== undefined) continue
|
||||
restrictedPackages.add(packageDir)
|
||||
const entries = new Set(Object.values(manifest.exports).flatMap(exportedTargets).flatMap((target) => {
|
||||
const entry = sourceEntry(target)
|
||||
return entry ? [`${packageDir}/${entry}`] : []
|
||||
}))
|
||||
for (const entry of entries) {
|
||||
const source = program.getSourceFile(resolve(scanRoot, entry))
|
||||
const moduleSymbol = source && checker.getSymbolAtLocation(source)
|
||||
if (!source || !moduleSymbol) continue
|
||||
for (const exported of checker.getExportsOfModule(moduleSymbol)) {
|
||||
const target = (exported.flags & ts.SymbolFlags.Alias) !== 0 ? checker.getAliasedSymbol(exported) : exported
|
||||
for (const declaration of target.declarations ?? []) {
|
||||
const name = declarationName(declaration)
|
||||
const file = declaration.getSourceFile().fileName
|
||||
const rel = relative(scanRoot, file).split(sep).join('/')
|
||||
if (!name || !rel.startsWith(`${packageDir}/src/`)) continue
|
||||
namesByFile.set(rel, new Set([...(namesByFile.get(rel) ?? []), name]))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return { restrictedPackages, namesByFile }
|
||||
}
|
||||
|
||||
/**
|
||||
* Compiler options for the walk's program.
|
||||
*
|
||||
@@ -495,15 +575,26 @@ function loadCompilerOptions(scanRoot: string): ts.CompilerOptions {
|
||||
*/
|
||||
export function collectExportJsdocViolations(scanRoot: string = root): string[] {
|
||||
const violations: string[] = []
|
||||
const rels = globSync('packages/*/*/src/**/*.ts', { cwd: scanRoot }).sort()
|
||||
const rels = globSync('packages/*/*/src/**/*.ts', { cwd: scanRoot })
|
||||
.map(path => path.split(sep).join('/'))
|
||||
.sort()
|
||||
const program = ts.createProgram(rels.map(rel => resolve(scanRoot, rel)), loadCompilerOptions(scanRoot))
|
||||
const checker = program.getTypeChecker()
|
||||
const { restrictedPackages, namesByFile } = restrictedPublicNames(scanRoot, rels, program, checker)
|
||||
for (const rel of rels) {
|
||||
const sf = program.getSourceFile(resolve(scanRoot, rel))
|
||||
if (!sf) continue // program root files always resolve; guard for narrowing
|
||||
// A script-style declaration file (no imports/exports) is one big ambient
|
||||
// scope; a module-style .d.ts still honors explicit export modifiers.
|
||||
checkScope(sf.statements, '', { rel, sf, text: sf.text, checker, violations }, sf.isDeclarationFile && !ts.isExternalModule(sf))
|
||||
const packageDir = rel.split('/').slice(0, 3).join('/')
|
||||
const allowedNames = restrictedPackages.has(packageDir) ? namesByFile.get(rel) ?? new Set<string>() : undefined
|
||||
checkScope(
|
||||
sf.statements,
|
||||
'',
|
||||
{ rel, sf, text: sf.text, checker, violations },
|
||||
sf.isDeclarationFile && !ts.isExternalModule(sf),
|
||||
allowedNames,
|
||||
)
|
||||
}
|
||||
return violations
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/llm/llm': { kind: 'none', reason: 'The adapter registry forwards already-assembled requests unchanged.' },
|
||||
'packages/sandbox/sandbox-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-bash-sandbox and dsh-tool-bash.' },
|
||||
'packages/sandbox/sandbox-policy': { kind: 'indirect', reason: 'The policy service holds the mode dsh-tool-bash and dsh-tool-fs render in their denial markers.' },
|
||||
'packages/sdk/create-sdk': { kind: 'indirect', reason: 'The initializer only writes project files; selected runtime plugins provide the generated project model surface.' },
|
||||
'packages/sdk/helper': { kind: 'none', reason: 'The project domain edits files and registers no live agent or model surface.' },
|
||||
'packages/sdk/scripts': { kind: 'indirect', reason: 'The launcher delegates model context to the loaded project plugin tree.' },
|
||||
'packages/session-query/session-query': { kind: 'none', reason: 'The trusted query service exposes cloned records only to callers and registers no model surface.' },
|
||||
'packages/skill/skill': { kind: 'indirect', reason: 'The provider registry delegates model rendering to dsh-tool-skill.' },
|
||||
'packages/skill/skill-local': { kind: 'indirect', reason: 'The provider backend delegates model rendering to dsh-tool-skill.' },
|
||||
@@ -60,6 +63,7 @@ const SENTENCE_MODEL_EXPERIENCE: Readonly<Record<string, SentenceContract>> = {
|
||||
'packages/support/loader-smoke': { kind: 'none', reason: 'The test harness observes child-process streams without changing live requests.' },
|
||||
'packages/support/llm-replay': { kind: 'none', reason: 'The keyless adapter invokes no provider model.' },
|
||||
'packages/support/subagent-mock': { kind: 'indirect', reason: 'Only dsh-tool-subagent renders its configured test outcome.' },
|
||||
'packages/tasks/tasks': { kind: 'indirect', reason: 'Producer and control-surface plugins own all model rendering over the task registry.' },
|
||||
'packages/examples/acp-demo': { kind: 'indirect', reason: 'The app bundle delegates request composition to dsh-agent-spine-demo and dsh-acp.' },
|
||||
'packages/ui/app-boot': { kind: 'indirect', reason: 'Only the loaded plugin tree contributes model context.' },
|
||||
'packages/examples/jsonrpc-demo': { kind: 'indirect', reason: 'Only the externally configured plugin tree contributes model context.' },
|
||||
|
||||
Reference in New Issue
Block a user