Merge remote-tracking branch 'origin/master' into docs/post-v3-release-proofreading
# Conflicts: # .agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.i18n.yaml # .agents/notes/implemented/feature/2026-07-20-dsh-cli-personal-config.zh.md # .agents/notes/implemented/feature/2026-07-25-session-list-browsing-and-manual-order.i18n.yaml # .agents/notes/implemented/feature/2026-07-25-session-list-browsing-and-manual-order.zh.md # .agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.i18n.yaml # .agents/notes/implemented/feature/2026-07-25-workspace-ui-product-flow.zh.md # .agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.i18n.yaml # .agents/notes/implemented/feature/2026-07-26-code-dispatch-ui-foundation.zh.md # docs/user/develop/basic/index.i18n.yaml # docs/user/develop/basic/index.zh.md # docs/user/guide/config.i18n.yaml # docs/user/guide/config.zh.md # docs/user/guide/providers.i18n.yaml # docs/user/guide/providers.zh.md # packages/bundle/base/README.i18n.yaml # packages/bundle/base/README.zh.md # packages/host/apiproxy/README.i18n.yaml # packages/host/apiproxy/README.zh.md
This commit is contained in:
@@ -60,6 +60,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_sandbox_policy
|
||||
plugin_dsh_base_bash_sandbox["bash-sandbox<br/>@deepseek-ai/dsh-bash-sandbox"]
|
||||
cfg --> plugin_dsh_base_bash_sandbox
|
||||
plugin_dsh_base_pwsh_sandbox["pwsh-sandbox<br/>@deepseek-ai/dsh-pwsh-sandbox"]
|
||||
cfg --> plugin_dsh_base_pwsh_sandbox
|
||||
plugin_dsh_base_approval["approval<br/>@deepseek-ai/dsh-user-approval"]
|
||||
cfg --> plugin_dsh_base_approval
|
||||
plugin_dsh_base_permission["permission<br/>@deepseek-ai/dsh-permission"]
|
||||
@@ -68,6 +70,8 @@ flowchart LR
|
||||
cfg --> plugin_dsh_base_bash_env
|
||||
plugin_dsh_base_tool_bash["tool-bash<br/>@deepseek-ai/dsh-tool-bash"]
|
||||
cfg --> plugin_dsh_base_tool_bash
|
||||
plugin_dsh_base_tool_pwsh["tool-pwsh<br/>@deepseek-ai/dsh-tool-pwsh"]
|
||||
cfg --> plugin_dsh_base_tool_pwsh
|
||||
plugin_dsh_base_tool_tasks["tool-tasks<br/>@deepseek-ai/dsh-tool-tasks"]
|
||||
cfg --> plugin_dsh_base_tool_tasks
|
||||
plugin_dsh_base_fs_policy["fs-policy<br/>@deepseek-ai/dsh-fs-policy"]
|
||||
@@ -194,10 +198,12 @@ flowchart LR
|
||||
| `sandbox` | `@deepseek-ai/dsh-sandbox-local` |
|
||||
| `sandbox-policy` | `@deepseek-ai/dsh-sandbox-policy` |
|
||||
| `bash-sandbox` | `@deepseek-ai/dsh-bash-sandbox` |
|
||||
| `pwsh-sandbox` | `@deepseek-ai/dsh-pwsh-sandbox` |
|
||||
| `approval` | `@deepseek-ai/dsh-user-approval` |
|
||||
| `permission` | `@deepseek-ai/dsh-permission` |
|
||||
| `bash-env` | `@deepseek-ai/dsh-bash-env` |
|
||||
| `tool-bash` | `@deepseek-ai/dsh-tool-bash` |
|
||||
| `tool-pwsh` | `@deepseek-ai/dsh-tool-pwsh` |
|
||||
| `tool-tasks` | `@deepseek-ai/dsh-tool-tasks` |
|
||||
| `fs-policy` | `@deepseek-ai/dsh-fs-policy` |
|
||||
| `tool-fs` | `@deepseek-ai/dsh-tool-fs` |
|
||||
|
||||
@@ -45,11 +45,16 @@
|
||||
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||
# sandbox policy owns it.
|
||||
# never reached the model's shell at all. Both shell tools consume the host
|
||||
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
||||
# host-plane too.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -39,11 +39,16 @@
|
||||
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||
# sandbox policy owns it.
|
||||
# never reached the model's shell at all. Both shell tools consume the host
|
||||
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
||||
# host-plane too.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -25,13 +25,13 @@ Two planes, and the choice is not about how "agent-related" something feels —
|
||||
|
||||
A preset is a directory holding one `agent.cordis.yml`, optionally beside a `preset.yml` carrying display metadata — `name` and `description` (and, for shipped presets, a roster `order`). Write the metadata too: a preset without it shows up in every picker as its bare directory name.
|
||||
|
||||
Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. Both roots are configuration rather than fixed locations, though, and no call reports them — `authorable` says only whether a writable one exists — so take the path you actually read or edit from `list()` or `resolve()`, which is also where `copy()` reports what it just created.
|
||||
Locally authored presets live one directory per preset under `${DSH_HOME:-$HOME/.dsh}/.agent-presets/`, and the shipped set sits beside the deployment's own config. Use those when the user asks where to look. A deployment can configure other roots, so the path you read or edit comes from `list()` or `resolve()` — which is also where `copy()` reports what it just created.
|
||||
|
||||
## The roster service
|
||||
|
||||
`ctx.agentPresets` owns discovery, authoring, and mounting. You reach it by mounting a temporary plugin that injects it and registers a tool for yourself — `cordis_mount` returns only the mount acknowledgement, so a registered tool is how a service answer gets back to you, and it becomes callable on your next step.
|
||||
|
||||
Read `cordis_inspect what:"api" name:"agentPresets"` for the current signatures before writing the code. The four calls this skill relies on:
|
||||
Read `cordis_inspect what:"api" name:"agentPresets"` for the current signatures before writing the code. What this skill relies on:
|
||||
|
||||
- `list()` — every preset with its `id`, `trust` (`system` for the shipped set, `user` for authored ones), and the absolute `path` of its composition file. This is how you locate any composition without knowing the install layout; the directory is that path's parent.
|
||||
- `read(id)` — one preset's composition text, without a file tool or a path.
|
||||
|
||||
@@ -38,11 +38,16 @@
|
||||
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||
# the criterion for host-plane ownership — injection resolves before any session
|
||||
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||
# sandbox policy owns it.
|
||||
# never reached the model's shell at all. Both shell tools consume the host
|
||||
# registry from here; their executors (`bash-sandbox`/`pwsh-sandbox`) are
|
||||
# host-plane too.
|
||||
- id: tool-bash
|
||||
name: '@deepseek-ai/dsh-tool-bash'
|
||||
disabled: !!js process.platform === 'win32'
|
||||
|
||||
- id: tool-pwsh
|
||||
name: '@deepseek-ai/dsh-tool-pwsh'
|
||||
disabled: !!js process.platform !== 'win32'
|
||||
|
||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type ConfigDumpLayer,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { homePatchPath, prepareProfile, PROFILE_ROOT_FILENAME } from './profile-boot.ts'
|
||||
import { resolveWindowsShellLayer } from './windows-shell.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
@@ -34,12 +33,6 @@ export function runDumpConfig(profile: string, defaultOnly: boolean, patches: re
|
||||
label: layer.packageName,
|
||||
patches: layer.patches,
|
||||
}))
|
||||
// The win32 shell platform layer rides between bundles and user layers,
|
||||
// exactly where the boot applies it.
|
||||
const windowsShellLayer = resolveWindowsShellLayer(process.platform, loaded.layers, NAME)
|
||||
if (windowsShellLayer !== undefined) {
|
||||
layers.push({ label: windowsShellLayer.label, patches: windowsShellLayer.patches })
|
||||
}
|
||||
if (!defaultOnly) {
|
||||
if (existsSync(loaded.patchPath)) {
|
||||
layers.push({ label: loaded.patchPath, patches: loaded.patches })
|
||||
|
||||
@@ -29,17 +29,14 @@ import {
|
||||
watchUserPatches,
|
||||
type Profile,
|
||||
} from '@deepseek-ai/dsh-app-boot'
|
||||
import { dshHomePath, resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
|
||||
|
||||
/** Shipped agent-preset root: beside this app's own config, in both source and built layouts. */
|
||||
const SHIPPED_PRESET_ROOT = fileURLToPath(new URL('../config/agent-presets/', import.meta.url))
|
||||
|
||||
/** Harness-home directory holding locally authored agent presets. */
|
||||
const USER_PRESET_DIR = '.agent-presets'
|
||||
import { DSH_ENVIRONMENT_KEY, type EnvironmentSnapshot } from '@deepseek-ai/dsh-environment'
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import { createProcessShutdown, type ProcessShutdown } from './process-shutdown.ts'
|
||||
import { resolveWindowsShellLayer } from './windows-shell.ts'
|
||||
|
||||
const NAME = 'dsh'
|
||||
|
||||
@@ -110,8 +107,6 @@ interface ComposedProfile {
|
||||
profile: Profile
|
||||
/** Bundle layers concatenated — the part below the user layers on a live reload. */
|
||||
bundlePatches: PatchOptions[]
|
||||
/** The win32 shell platform layer (the base bundle's `windows.cordis.patch.yml`), between bundles and user layers. */
|
||||
windowsShellPatches: PatchOptions[]
|
||||
/** The home-level user layer (`$DSH_HOME/cordis.patch.yml`), applied after the profile's own. */
|
||||
homePatches: PatchOptions[]
|
||||
/** Layers above the user layers on a live reload: `--patch` overlays and the telemetry switch. */
|
||||
@@ -127,7 +122,6 @@ interface ComposedProfile {
|
||||
function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
return [
|
||||
...composed.bundlePatches,
|
||||
...composed.windowsShellPatches,
|
||||
...composed.profile.patches,
|
||||
...composed.homePatches,
|
||||
...composed.overlays,
|
||||
@@ -136,10 +130,10 @@ function allPatches(composed: ComposedProfile): PatchOptions[] {
|
||||
|
||||
/**
|
||||
* Load `name` and compose its effective patch stack: bundle layers in
|
||||
* `dsh.profile.bundles` order, the win32 shell platform layer (when the host
|
||||
* is Windows), the profile's user layer, the home-level user layer
|
||||
* (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply to
|
||||
* every profile, so it outranks the per-profile layer), `--patch` overlays,
|
||||
* `dsh.profile.bundles` order (the base bundle gates the shell stacks by
|
||||
* platform on its own rows), the profile's user layer, the home-level user
|
||||
* layer (`$DSH_HOME/cordis.patch.yml` — machine-local preferences that apply
|
||||
* to every profile, so it outranks the per-profile layer), `--patch` overlays,
|
||||
* then the telemetry switch.
|
||||
* @param name - the profile name.
|
||||
* @param patchFiles - `--patch` overlay paths, in argv order.
|
||||
@@ -153,28 +147,27 @@ function composeProfile(
|
||||
const homePatches = loadOptionalPatches(NAME, homePatchPath()) ?? []
|
||||
const overlays = patchFiles.flatMap(file => loadOverlayPatches(NAME, resolve(file)))
|
||||
const bundlePatches = profile.layers.flatMap(layer => layer.patches)
|
||||
const windowsShellPatches = resolveWindowsShellLayer(process.platform, profile.layers, NAME)?.patches ?? []
|
||||
const rows = new Map<string, EntryOptions>()
|
||||
for (const row of composeEntries([bundlePatches, windowsShellPatches, profile.patches, homePatches, overlays])) {
|
||||
for (const row of composeEntries([bundlePatches, profile.patches, homePatches, overlays])) {
|
||||
if (typeof row.id === 'string') rows.set(row.id, row)
|
||||
}
|
||||
const composedOverlays = [...overlays]
|
||||
// Preset roots belong to every dsh composition that mounts the roster.
|
||||
// The SHIPPED root is the part of the roster only this app can resolve: it
|
||||
// sits beside this app's own config, in both the source and built layouts.
|
||||
// The writable root the roster appends is `dsh-agent-presets`' own, so a
|
||||
// launcher that never reaches this patch still finds a person's presets.
|
||||
if (rows.has('agent-presets')) {
|
||||
composedOverlays.push({
|
||||
id: 'agent-presets',
|
||||
config: {
|
||||
...(rows.get('agent-presets')?.config ?? {}) as Record<string, unknown>,
|
||||
roots: [
|
||||
{ path: SHIPPED_PRESET_ROOT, trust: 'system' },
|
||||
{ path: dshHomePath(USER_PRESET_DIR), trust: 'user' },
|
||||
],
|
||||
roots: [{ path: SHIPPED_PRESET_ROOT, trust: 'system' }],
|
||||
},
|
||||
})
|
||||
}
|
||||
const telemetryPatch = resolveTelemetryPatch(process.env.DSH_TELEMETRY_DISABLED, rows.has(TELEMETRY_ROW_ID))
|
||||
if (telemetryPatch !== undefined) composedOverlays.push(telemetryPatch)
|
||||
return { profile, bundlePatches, windowsShellPatches, homePatches, overlays: composedOverlays, rows }
|
||||
return { profile, bundlePatches, homePatches, overlays: composedOverlays, rows }
|
||||
}
|
||||
|
||||
/** Options for {@link runProfile}. */
|
||||
@@ -246,7 +239,6 @@ export async function runProfile(options: RunProfileOptions): Promise<{ ctx: Con
|
||||
// removing the override could never revert the row to the bundle default.
|
||||
const composeLive = (): PatchOptions[] => structuredClone([
|
||||
...composed.bundlePatches,
|
||||
...composed.windowsShellPatches,
|
||||
...loadOptionalPatches(NAME, composed.profile.patchPath) ?? [],
|
||||
...loadOptionalPatches(NAME, homePatchPath()) ?? [],
|
||||
...composed.overlays,
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
/**
|
||||
* The Windows shell platform layer: on win32 hosts the shipped profile
|
||||
* compositions swap the POSIX-only bash stack for the sandbox-confined
|
||||
* PowerShell stack (`@deepseek-ai/dsh-pwsh-sandbox` +
|
||||
* `@deepseek-ai/dsh-tool-pwsh`). The layer is the base bundle's
|
||||
* `windows.cordis.patch.yml`, injected by the launcher between the bundle
|
||||
* layers and the user layers so a user patch can still override it — the
|
||||
* only override channel is composition config, like every other roster
|
||||
* decision. POSIX hosts never receive the layer.
|
||||
* @module @deepseek-ai/dsh/windows-shell
|
||||
*/
|
||||
|
||||
import { join } from 'node:path'
|
||||
import type { PatchOptions } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { loadOverlayPatches, type ProfileLayer } from '@deepseek-ai/dsh-app-boot'
|
||||
|
||||
/** The base bundle whose package carries the Windows shell patch. */
|
||||
export const BASE_BUNDLE = '@deepseek-ai/dsh-base'
|
||||
|
||||
/** The Windows shell patch filename inside the base bundle package. */
|
||||
export const WINDOWS_SHELL_PATCH_FILENAME = 'windows.cordis.patch.yml'
|
||||
|
||||
/** One Windows shell platform layer: its patch file and parsed patches. */
|
||||
export interface WindowsShellLayer {
|
||||
/** The patch file path, used as the config-dump provenance label. */
|
||||
label: string
|
||||
/** The parsed patch entries, applied after the bundle layers. */
|
||||
patches: PatchOptions[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve the Windows shell platform layer for a profile composition.
|
||||
* @param platform - the host platform (`process.platform` at call sites).
|
||||
* @param layers - the profile's bundle layers, in application order.
|
||||
* @param binName - the diagnostic prefix on thrown errors (`dsh`).
|
||||
* @returns the pwsh layer on win32, else `undefined`. A custom profile that
|
||||
* mounts no base bundle is skipped (it owns its shell stack); a base
|
||||
* bundle whose Windows shell patch is missing fails loud in
|
||||
* {@link loadOverlayPatches} — the shipped package always carries it, so
|
||||
* a miss is a broken installation.
|
||||
*/
|
||||
export function resolveWindowsShellLayer(
|
||||
platform: NodeJS.Platform,
|
||||
layers: readonly ProfileLayer[],
|
||||
binName: string,
|
||||
): WindowsShellLayer | undefined {
|
||||
if (platform !== 'win32') return undefined
|
||||
const base = layers.find(layer => layer.packageName === BASE_BUNDLE)
|
||||
if (base === undefined) return undefined
|
||||
const label = join(base.packageDir, WINDOWS_SHELL_PATCH_FILENAME)
|
||||
return { label, patches: loadOverlayPatches(binName, label) }
|
||||
}
|
||||
@@ -96,7 +96,11 @@ async function bootWeb(settingsFile: string, extra: PatchOptions[] = []): Promis
|
||||
// document overrides.
|
||||
{
|
||||
id: 'agent-presets',
|
||||
config: { default: 'standard', roots: [{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' }] },
|
||||
config: {
|
||||
default: 'standard',
|
||||
roots: [{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' }],
|
||||
includeUserRoot: false,
|
||||
},
|
||||
},
|
||||
...extra,
|
||||
]
|
||||
@@ -442,6 +446,7 @@ describe('product subagent rows in user presets', () => {
|
||||
{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' },
|
||||
{ path: userRoot, trust: 'user' },
|
||||
],
|
||||
includeUserRoot: false,
|
||||
},
|
||||
}])
|
||||
}, 120_000)
|
||||
@@ -624,6 +629,66 @@ describe('a delegated child', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('a launcher that configures no writable root', () => {
|
||||
// The claim this default exists for, asserted through the real shipped
|
||||
// bundles rather than a hand-built context: `apps/cli` patches in only the
|
||||
// system root, and a person's own presets are found anyway because the
|
||||
// roster derives `<dshHome>/.agent-presets` itself. `$DSH_HOME` is pointed
|
||||
// at a temp home BEFORE boot — the derived root is resolved when the plugin
|
||||
// is constructed, and an unpinned run would read the developer's own.
|
||||
let derivedCtx: Context
|
||||
let previousHome: string | undefined
|
||||
|
||||
beforeAll(async () => {
|
||||
const home = await mkdtemp(join(tmpdir(), 'dsh-preset-derived-'))
|
||||
previousHome = process.env.DSH_HOME
|
||||
process.env.DSH_HOME = home
|
||||
await mkdir(join(home, '.agent-presets', 'derived-mine'), { recursive: true })
|
||||
await writeFile(
|
||||
join(home, '.agent-presets', 'derived-mine', 'agent.cordis.yml'),
|
||||
'- id: tool-todo\n name: \'@deepseek-ai/dsh-tool-todo\'\n config:\n allowParallelInProgress: true\n',
|
||||
)
|
||||
const settingsFile = join(await mkdtemp(join(tmpdir(), 'dsh-preset-derived-settings-')), 'settings.yaml')
|
||||
await writeFile(settingsFile, '{}\n')
|
||||
// Only the shipped root, exactly what `composeProfile` supplies; the
|
||||
// writable one is the roster's own default rather than this patch's job.
|
||||
derivedCtx = await bootWeb(settingsFile, [{
|
||||
id: 'agent-presets',
|
||||
config: {
|
||||
default: 'standard',
|
||||
roots: [{ path: join(CONFIG_DIR, 'agent-presets'), trust: 'system' }],
|
||||
includeUserRoot: true,
|
||||
},
|
||||
}])
|
||||
}, 120_000)
|
||||
|
||||
afterAll(async () => {
|
||||
if (previousHome === undefined) delete process.env.DSH_HOME
|
||||
else process.env.DSH_HOME = previousHome
|
||||
await derivedCtx.fiber.dispose()
|
||||
})
|
||||
|
||||
it('discovers and mounts a preset the person authored under the harness home', async () => {
|
||||
const listed = await derivedCtx.agentPresets.list()
|
||||
|
||||
const mine = listed.find(preset => preset.id === 'derived-mine')
|
||||
expect(mine).toMatchObject({ trust: 'user' })
|
||||
// Omitted rather than undefined: a healthy row carries no `broken` key.
|
||||
expect(mine?.broken).toBeUndefined()
|
||||
expect(derivedCtx.agentPresets.authorable).toBe(true)
|
||||
|
||||
const handle = await derivedCtx.agents.create({
|
||||
sessionId: SessionId('preset-derived-root'),
|
||||
setup: agentCtx => derivedCtx.agentPresets.mount(agentCtx, 'derived-mine').then(() => undefined),
|
||||
})
|
||||
try {
|
||||
expect(toolNames(derivedCtx, handle.agent)).toContain('todo_write')
|
||||
} finally {
|
||||
await handle.dispose()
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('authoring a preset on the shipped composition', () => {
|
||||
let authorCtx: Context
|
||||
let userRoot: string
|
||||
@@ -642,6 +707,7 @@ describe('authoring a preset on the shipped composition', () => {
|
||||
// nothing is the normal first-run state.
|
||||
{ path: userRoot, trust: 'user' },
|
||||
],
|
||||
includeUserRoot: false,
|
||||
},
|
||||
}])
|
||||
})
|
||||
|
||||
@@ -1,73 +1,38 @@
|
||||
/**
|
||||
* The shipped shell composition: the base bundle gates both shell stacks by
|
||||
* platform on its own rows (`disabled: !!js process.platform`), so exactly
|
||||
* one shell stack mounts per host and no separate platform layer exists —
|
||||
* the launcher applies nothing beyond the bundle layers. The spec composes
|
||||
* the REAL shipped bundle layers (dsh-base + dsh-web-app resolved from the
|
||||
* app installation anchor) through the boot's patch algorithm and pins the
|
||||
* effective per-platform roster, the preset-level gates that keep tool-bash
|
||||
* out of win32 sessions and tool-pwsh out of POSIX sessions, and the
|
||||
* cold-start resolution closure for the pwsh rows' bare plugin names.
|
||||
*/
|
||||
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { mkdtempSync, writeFileSync, rmSync, mkdirSync, readFileSync } from 'node:fs'
|
||||
import { mkdtempSync, rmSync, readFileSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import type { ProfileLayer } from '@deepseek-ai/dsh-app-boot'
|
||||
import yaml from 'js-yaml'
|
||||
import { entryListSchema } from '@deepseek-ai/cordis-plugin-include'
|
||||
import { evaluate } from '@deepseek-ai/cordis-plugin-loader'
|
||||
import { composeEntries, initProfile, loadProfile, PROFILES_DIR } from '@deepseek-ai/dsh-app-boot'
|
||||
import {
|
||||
BASE_BUNDLE,
|
||||
resolveWindowsShellLayer,
|
||||
WINDOWS_SHELL_PATCH_FILENAME,
|
||||
} from '../src/windows-shell.ts'
|
||||
|
||||
const WINDOWS_PATCH = `- id: bash-sandbox
|
||||
disabled: true
|
||||
- insert:
|
||||
- id: pwsh-sandbox
|
||||
name: '@deepseek-ai/dsh-pwsh-sandbox'
|
||||
`
|
||||
|
||||
/** One fake bundle layer rooted in a temp directory. */
|
||||
function fakeLayer(packageName: string, dir: string): ProfileLayer {
|
||||
return { packageName, packageDir: dir, patchPath: join(dir, 'cordis.patch.yml'), patches: [] }
|
||||
}
|
||||
|
||||
/** A base bundle layer whose package carries the Windows shell patch. */
|
||||
function baseLayerWithPatch(dir: string): ProfileLayer {
|
||||
writeFileSync(join(dir, WINDOWS_SHELL_PATCH_FILENAME), WINDOWS_PATCH)
|
||||
return fakeLayer(BASE_BUNDLE, dir)
|
||||
}
|
||||
|
||||
describe('resolveWindowsShellLayer', () => {
|
||||
let base: string
|
||||
afterEach(() => { if (base !== undefined) rmSync(base, { recursive: true, force: true }) })
|
||||
const tempBase = (): string => {
|
||||
base = mkdtempSync(join(tmpdir(), 'dsh-windows-shell-'))
|
||||
return base
|
||||
/**
|
||||
* The effective disabled state of one row on one platform: a `!!js` expression
|
||||
* evaluates with a platform-scoped `process` so both outcomes pin on any host.
|
||||
*/
|
||||
function disabledOn(row: { disabled?: unknown }, platform: 'win32' | 'linux'): boolean {
|
||||
const value = row.disabled
|
||||
if (value !== null && typeof value === 'object' && '__jsExpr' in value) {
|
||||
return Boolean(evaluate({ process: { platform } }, (value as { __jsExpr: string }).__jsExpr))
|
||||
}
|
||||
return value === true
|
||||
}
|
||||
|
||||
it('never applies on POSIX hosts', () => {
|
||||
expect(resolveWindowsShellLayer('linux', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
|
||||
expect(resolveWindowsShellLayer('darwin', [baseLayerWithPatch(tempBase())], 'dsh')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('defaults Windows hosts to the pwsh platform layer', () => {
|
||||
const layer = resolveWindowsShellLayer('win32', [baseLayerWithPatch(tempBase())], 'dsh')
|
||||
expect(layer).toBeDefined()
|
||||
expect(layer?.label.endsWith(WINDOWS_SHELL_PATCH_FILENAME)).toBe(true)
|
||||
expect(layer?.patches).toEqual([
|
||||
{ id: 'bash-sandbox', disabled: true },
|
||||
{ insert: [{ id: 'pwsh-sandbox', name: '@deepseek-ai/dsh-pwsh-sandbox' }] },
|
||||
])
|
||||
})
|
||||
|
||||
it('skips custom profiles without a base bundle', () => {
|
||||
const other = fakeLayer('@deepseek-ai/dsh-custom', tempBase())
|
||||
expect(resolveWindowsShellLayer('win32', [other], 'dsh')).toBeUndefined()
|
||||
})
|
||||
|
||||
it('fails loud when the base bundle ships no Windows shell patch', () => {
|
||||
const base = tempBase()
|
||||
mkdirSync(base, { recursive: true })
|
||||
// The overlay loader owns the fail-loud contract: the caller named this
|
||||
// file, so its absence is a misconfiguration, not "no overlay".
|
||||
expect(() => resolveWindowsShellLayer('win32', [fakeLayer(BASE_BUNDLE, base)], 'dsh'))
|
||||
.toThrow(/dsh: failed to read overlay .*windows\.cordis\.patch\.yml/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('the shipped Windows composition (real bundle layers)', () => {
|
||||
describe('the shipped shell composition (real bundle layers)', () => {
|
||||
let home: string
|
||||
afterEach(() => { if (home !== undefined) rmSync(home, { recursive: true, force: true }) })
|
||||
// The app installation anchor, mirroring profile-boot.ts: the bundle layers
|
||||
@@ -75,65 +40,98 @@ describe('the shipped Windows composition (real bundle layers)', () => {
|
||||
// suite composes the shipped patch files, not test fixtures.
|
||||
const anchor = fileURLToPath(new URL('../package.json', import.meta.url))
|
||||
|
||||
it('composes the win32 confined roster through the real patch layers', () => {
|
||||
it('composes the confined pwsh roster on win32 and the bash roster on POSIX from the same rows', () => {
|
||||
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
|
||||
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
|
||||
const profile = loadProfile('dsh', 'web', anchor, home)
|
||||
const warnings: string[] = []
|
||||
const win32 = resolveWindowsShellLayer('win32', profile.layers, 'dsh')
|
||||
expect(win32).toBeDefined()
|
||||
const rows = composeEntries(
|
||||
[...profile.layers.map(layer => layer.patches), win32!.patches],
|
||||
profile.layers.map(layer => layer.patches),
|
||||
message => warnings.push(message),
|
||||
)
|
||||
const byId = new Map(rows.map(row => [row.id, row]))
|
||||
// Only the POSIX bash stack leaves the roster: the permission surface
|
||||
// (sandbox/sandbox-policy/fs-sandbox, permission, approval) stays enabled
|
||||
// exactly as on POSIX — the confined pwsh executor is what changes.
|
||||
for (const id of ['bash-sandbox', 'tool-bash']) {
|
||||
expect(byId.get(id)?.disabled, `row ${id}`).toBe(true)
|
||||
// One shared patch set, two rosters: the shell stacks gate themselves.
|
||||
for (const id of ['bash-sandbox', 'pwsh-sandbox', 'tool-bash', 'tool-pwsh']) {
|
||||
expect(byId.has(id), `row ${id}`).toBe(true)
|
||||
}
|
||||
expect(disabledOn(byId.get('bash-sandbox')!, 'win32'), 'bash-sandbox on win32').toBe(true)
|
||||
expect(disabledOn(byId.get('bash-sandbox')!, 'linux'), 'bash-sandbox on linux').toBe(false)
|
||||
expect(disabledOn(byId.get('pwsh-sandbox')!, 'win32'), 'pwsh-sandbox on win32').toBe(false)
|
||||
expect(disabledOn(byId.get('pwsh-sandbox')!, 'linux'), 'pwsh-sandbox on linux').toBe(true)
|
||||
// Host shell-tool rows are disabled on every platform; sessions mount
|
||||
// their own rows instead.
|
||||
expect(byId.get('tool-bash')?.disabled).toBe(true)
|
||||
expect(byId.get('tool-pwsh')?.disabled).toBe(true)
|
||||
// The permission surface never moves: the sandbox/policy rows, the
|
||||
// permission switcher, fs-sandbox, and the approval service stay enabled
|
||||
// exactly as on POSIX — the confined pwsh executor is what changes.
|
||||
for (const id of ['permission', 'ui-permission', 'sandbox', 'sandbox-policy', 'fs-sandbox', 'approval']) {
|
||||
expect(byId.get(id)?.disabled, `row ${id}`).not.toBe(true)
|
||||
}
|
||||
for (const id of ['pwsh-sandbox', 'tool-pwsh']) {
|
||||
expect(byId.has(id), `inserted row ${id}`).toBe(true)
|
||||
}
|
||||
// The launcher's cold-start module fallback BFS-links the apps/cli
|
||||
// dependency closure into the profile's node_modules (the pwsh-local
|
||||
// precedent), so every inserted bare plugin must resolve from there.
|
||||
// dependency closure into the profile's node_modules, so every bare
|
||||
// plugin name in the base patch must resolve from there.
|
||||
const cliManifest = JSON.parse(readFileSync(anchor, 'utf8')) as { dependencies?: Record<string, string> }
|
||||
for (const name of ['@deepseek-ai/dsh-pwsh-sandbox', '@deepseek-ai/dsh-tool-pwsh']) {
|
||||
expect(cliManifest.dependencies?.[name], `cold-start closure must reach ${name}`).toBeDefined()
|
||||
}
|
||||
// The patch touches only base-owned rows plus inserts, so the full web
|
||||
// profile composes without any no-match warning.
|
||||
expect(warnings).toEqual([])
|
||||
})
|
||||
|
||||
it('leaves POSIX untouched and base-only profiles compose without warnings', () => {
|
||||
it('base-only profiles carry both stacks with the same platform gating', () => {
|
||||
home = mkdtempSync(join(tmpdir(), 'dsh-windows-home-'))
|
||||
initProfile(join(home, PROFILES_DIR, 'web'), ['@deepseek-ai/dsh-base', '@deepseek-ai/dsh-web-app'])
|
||||
const profile = loadProfile('dsh', 'web', anchor, home)
|
||||
// POSIX: no platform layer, the bash stack stays enabled.
|
||||
const posixRows = composeEntries(profile.layers.map(layer => layer.patches))
|
||||
const posixById = new Map(posixRows.map(row => [row.id, row]))
|
||||
expect(posixById.get('bash-sandbox')?.disabled).not.toBe(true)
|
||||
expect(posixById.has('pwsh-local')).toBe(false)
|
||||
expect(posixById.has('pwsh-sandbox')).toBe(false)
|
||||
|
||||
// A base-only custom profile (the DEFAULT_PROFILE_BUNDLES template): the
|
||||
// patch touches only base-owned rows (bash-sandbox/tool-bash) plus its
|
||||
// inserts, so the composition produces no no-match warning.
|
||||
initProfile(join(home, PROFILES_DIR, 'base-only'), ['@deepseek-ai/dsh-base'])
|
||||
const baseOnly = loadProfile('dsh', 'base-only', anchor, home)
|
||||
const baseWarnings: string[] = []
|
||||
const win32 = resolveWindowsShellLayer('win32', baseOnly.layers, 'dsh')
|
||||
expect(win32).toBeDefined()
|
||||
composeEntries(
|
||||
[...baseOnly.layers.map(layer => layer.patches), win32!.patches],
|
||||
message => baseWarnings.push(message),
|
||||
const profile = loadProfile('dsh', 'base-only', anchor, home)
|
||||
const warnings: string[] = []
|
||||
const rows = composeEntries(
|
||||
profile.layers.map(layer => layer.patches),
|
||||
message => warnings.push(message),
|
||||
)
|
||||
expect(baseWarnings).toEqual([])
|
||||
const byId = new Map(rows.map(row => [row.id, row]))
|
||||
for (const id of ['bash-sandbox', 'tool-bash', 'pwsh-sandbox', 'tool-pwsh']) {
|
||||
expect(byId.has(id), `row ${id}`).toBe(true)
|
||||
}
|
||||
// No web overlay: the tool rows keep their own gating too.
|
||||
expect(disabledOn(byId.get('tool-bash')!, 'win32'), 'tool-bash on win32').toBe(true)
|
||||
expect(disabledOn(byId.get('tool-bash')!, 'linux'), 'tool-bash on linux').toBe(false)
|
||||
expect(disabledOn(byId.get('tool-pwsh')!, 'win32'), 'tool-pwsh on win32').toBe(false)
|
||||
expect(disabledOn(byId.get('tool-pwsh')!, 'linux'), 'tool-pwsh on linux').toBe(true)
|
||||
expect(warnings).toEqual([])
|
||||
})
|
||||
})
|
||||
|
||||
describe('shipped agent presets gate both shell tools by platform', () => {
|
||||
const presetRoot = resolve(fileURLToPath(new URL('../package.json', import.meta.url)), '..', 'config', 'agent-presets')
|
||||
|
||||
it.each(['standard', 'code', 'cordis'])('preset %s gates its shell tool rows by platform', (preset) => {
|
||||
const entries: unknown = yaml.load(
|
||||
readFileSync(join(presetRoot, preset, 'agent.cordis.yml'), 'utf8'),
|
||||
{ schema: entryListSchema },
|
||||
)
|
||||
if (!Array.isArray(entries)) throw new TypeError(`preset ${preset} must parse to an entry array`)
|
||||
for (const [id, win32] of [['tool-bash', true], ['tool-pwsh', false]] as const) {
|
||||
const row = entries.find((entry): entry is Record<string, unknown> => (
|
||||
typeof entry === 'object' && entry !== null && (entry as Record<string, unknown>).id === id
|
||||
))
|
||||
if (row === undefined) throw new TypeError(`preset ${preset} must mount ${id}`)
|
||||
expect(row.disabled).toMatchObject({ __jsExpr: expect.any(String) as string })
|
||||
// A platform-scoped context pins both outcomes on every host.
|
||||
const expression = (row.disabled as { __jsExpr: string }).__jsExpr
|
||||
expect(Boolean(evaluate({ process: { platform: 'win32' } }, expression)), `${id} on win32`).toBe(win32)
|
||||
expect(Boolean(evaluate({ process: { platform: 'linux' } }, expression)), `${id} on linux`).toBe(!win32)
|
||||
}
|
||||
})
|
||||
|
||||
it('minimal mounts no shell tool row at all (its shell is the PTY stack)', () => {
|
||||
const entries: unknown = yaml.load(
|
||||
readFileSync(join(presetRoot, 'minimal', 'agent.cordis.yml'), 'utf8'),
|
||||
{ schema: entryListSchema },
|
||||
)
|
||||
if (!Array.isArray(entries)) throw new TypeError('minimal preset must parse to an entry array')
|
||||
for (const id of ['tool-bash', 'tool-pwsh']) {
|
||||
expect(entries.some(entry => (
|
||||
typeof entry === 'object' && entry !== null && (entry as Record<string, unknown>).id === id
|
||||
)), `${id} must be absent from minimal`).toBe(false)
|
||||
}
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user