fix(web): keep host-plane services out of the preset, and pin the lane's skill roots
Moving the agent plane behind per-session presets took five rows with it that the host still owns, and the Web surface stopped booting: `host-apiproxy` injects `subagents`, so with the registry disabled here the entry never activated and `dsh web` died at plugin-tree load. The criterion is injection, not subject matter. A host row that injects a service resolves it before any session exists, so there is no agent to key by: `bash-env` (which `apps/cli/src/web.ts` injects to publish `DSH_WEB_URL`), the `subagents` registry and its spawn/fork backends (a process singleton whose cross-session queries the api-proxy serves, and whose provider names are globally unique), and `tool-subagent-report` (a continuable setup on that singleton, registered once per live session by a list that is not scope-aware) all stay host-plane. What a preset chooses is which delegation TOOLS it sees. The browser lane needs the second half: skill roots now resolve inside a preset, a subtree the lane's include patches cannot reach, so the row's documented environment fallback is pinned for the whole scaffold lifetime — presets mount when a session is created, not at boot. Without it a developer's real ~/.dsh/skills enters replay requests and goldens while CI sees none.
This commit is contained in:
@@ -15,20 +15,12 @@
|
|||||||
text: >-
|
text: >-
|
||||||
You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
|
You are a coding agent powered by the {{model}} model. Your working directory is {{cwd}}.
|
||||||
|
|
||||||
# `tool-bash` provides the `bashEnv` service, so it needs a realm even alone.
|
# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
||||||
- id: shell
|
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||||
name: cordis:group
|
# the criterion for host-plane ownership. `tool-bash` consumes that host
|
||||||
group: true
|
# registry from here.
|
||||||
isolate:
|
- id: tool-bash
|
||||||
bashEnv: true
|
name: '@deepseek-ai/dsh-tool-bash'
|
||||||
config:
|
|
||||||
# The registry and its consumer share the realm: a consumer left outside
|
|
||||||
# would resolve the host's `bashEnv`, which this plane no longer provides.
|
|
||||||
- id: bash-env
|
|
||||||
name: '@deepseek-ai/dsh-bash-env'
|
|
||||||
|
|
||||||
- id: tool-bash
|
|
||||||
name: '@deepseek-ai/dsh-tool-bash'
|
|
||||||
|
|
||||||
- id: tool-str-replace-editor
|
- id: tool-str-replace-editor
|
||||||
name: '@deepseek-ai/dsh-tool-str-replace-editor'
|
name: '@deepseek-ai/dsh-tool-str-replace-editor'
|
||||||
|
|||||||
@@ -31,22 +31,15 @@
|
|||||||
|
|
||||||
# ── shell ───────────────────────────────────────────────────────────────────
|
# ── shell ───────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
# `tool-bash` reads as a tool but provides the `bashEnv` service, so it needs a
|
# `bash-env` stays in the HOST composition: `apps/cli/src/web.ts` injects it to
|
||||||
# realm like any other provider. The executor behind it (`bash-sandbox`) stays
|
# publish `DSH_WEB_URL`/`DSH_WEB_MODE`, and a host row that injects a service is
|
||||||
# in the host composition, where the sandbox policy owns it.
|
# the criterion for host-plane ownership — injection resolves before any session
|
||||||
- id: shell
|
# exists, so there is no agent to key by. Behind a preset realm those variables
|
||||||
name: cordis:group
|
# never reached the model's shell at all. `tool-bash` consumes the host registry
|
||||||
group: true
|
# from here; the executor behind it (`bash-sandbox`) is host-plane too, where the
|
||||||
isolate:
|
# sandbox policy owns it.
|
||||||
bashEnv: true
|
- id: tool-bash
|
||||||
config:
|
name: '@deepseek-ai/dsh-tool-bash'
|
||||||
# The registry and its consumer share the realm: a consumer left outside
|
|
||||||
# would resolve the host's `bashEnv`, which this plane no longer provides.
|
|
||||||
- id: bash-env
|
|
||||||
name: '@deepseek-ai/dsh-bash-env'
|
|
||||||
|
|
||||||
- id: tool-bash
|
|
||||||
name: '@deepseek-ai/dsh-tool-bash'
|
|
||||||
|
|
||||||
# ── filesystem ──────────────────────────────────────────────────────────────
|
# ── filesystem ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -172,29 +165,21 @@
|
|||||||
|
|
||||||
# ── delegation and workflows ────────────────────────────────────────────────
|
# ── delegation and workflows ────────────────────────────────────────────────
|
||||||
|
|
||||||
# Every backend and every tool that reaches `subagents` or `workflows` shares
|
# The `subagents` registry and its spawn/fork backends live in the HOST
|
||||||
# one realm: a consumer left outside it would resolve the host's registry
|
# composition: the registry is a process singleton whose cross-session queries
|
||||||
# instead, which this preset does not populate.
|
# the api-proxy serves to the browser, and a provider name may only be
|
||||||
|
# registered once. This preset contributes the delegation TOOLS, which resolve
|
||||||
|
# that host registry.
|
||||||
|
#
|
||||||
|
# `workflows` is different — nothing outside an agent reads it — so every row
|
||||||
|
# that reaches it shares one entry-local realm here, and a consumer left
|
||||||
|
# outside would resolve a host registry this preset does not populate.
|
||||||
- id: delegation
|
- id: delegation
|
||||||
name: cordis:group
|
name: cordis:group
|
||||||
group: true
|
group: true
|
||||||
isolate:
|
isolate:
|
||||||
subagents: true
|
|
||||||
workflows: true
|
workflows: true
|
||||||
config:
|
config:
|
||||||
- id: subagent
|
|
||||||
name: '@deepseek-ai/dsh-subagent'
|
|
||||||
|
|
||||||
- id: subagent-spawn
|
|
||||||
name: '@deepseek-ai/dsh-subagent-spawn'
|
|
||||||
config:
|
|
||||||
providerName: spawn
|
|
||||||
|
|
||||||
- id: subagent-fork
|
|
||||||
name: '@deepseek-ai/dsh-subagent-fork'
|
|
||||||
config:
|
|
||||||
providerName: fork
|
|
||||||
|
|
||||||
- id: tool-subagent-control
|
- id: tool-subagent-control
|
||||||
name: '@deepseek-ai/dsh-tool-subagent-control'
|
name: '@deepseek-ai/dsh-tool-subagent-control'
|
||||||
|
|
||||||
@@ -215,8 +200,11 @@
|
|||||||
toolName: subagent_fork
|
toolName: subagent_fork
|
||||||
backgroundMode: continuable
|
backgroundMode: continuable
|
||||||
|
|
||||||
- id: tool-subagent-report
|
# `tool-subagent-report` is host-plane for the same reason as the registry,
|
||||||
name: '@deepseek-ai/dsh-tool-subagent-report'
|
# not because a preset may not want it: it registers a CONTINUABLE SETUP on
|
||||||
|
# that singleton rather than a tool this agent calls, and the setup list is
|
||||||
|
# not scope-aware — one copy per mounted preset means every child gets
|
||||||
|
# `report` registered once per live session, which throws on the second.
|
||||||
|
|
||||||
- id: workflow-workerthread
|
- id: workflow-workerthread
|
||||||
name: '@deepseek-ai/dsh-workflow-workerthread'
|
name: '@deepseek-ai/dsh-workflow-workerthread'
|
||||||
|
|||||||
@@ -240,6 +240,31 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
|||||||
// paths at load, and an in-process boot must NEVER touch the developer's
|
// paths at load, and an in-process boot must NEVER touch the developer's
|
||||||
// real ~/.dsh document or credential file.
|
// real ~/.dsh document or credential file.
|
||||||
const harnessHome = join(workspaceCwd, '.dsh-home')
|
const harnessHome = join(workspaceCwd, '.dsh-home')
|
||||||
|
// Skill discovery is model-visible input, and its roots now resolve inside a
|
||||||
|
// PRESET — a subtree this lane's include patches cannot reach, because the
|
||||||
|
// roster mounts it directly per session rather than as a row of the booted
|
||||||
|
// tree. The row's documented fallback is the environment, so pin that: the
|
||||||
|
// whole scaffold lifetime, not just the boot, since presets mount when a
|
||||||
|
// session is created. Without this a developer's real ~/.dsh/skills silently
|
||||||
|
// enters replay requests and goldens while CI sees none.
|
||||||
|
const skillRootEnvironment = {
|
||||||
|
DSH_HOME: join(workspaceCwd, '.dsh-home'),
|
||||||
|
DSH_AGENTS_HOME: join(workspaceCwd, '.agents-home'),
|
||||||
|
DSH_BUNDLED_SKILL_DIR: join(workspaceCwd, '.bundled-skills'),
|
||||||
|
}
|
||||||
|
const originalSkillRootEnvironment = Object.fromEntries(
|
||||||
|
Object.keys(skillRootEnvironment).map(key => [key, process.env[key]]),
|
||||||
|
)
|
||||||
|
let skillRootEnvironmentRestored = false
|
||||||
|
const restoreSkillRootEnvironment = (): void => {
|
||||||
|
if (skillRootEnvironmentRestored) return
|
||||||
|
skillRootEnvironmentRestored = true
|
||||||
|
for (const [key, value] of Object.entries(originalSkillRootEnvironment)) {
|
||||||
|
if (value === undefined) Reflect.deleteProperty(process.env, key)
|
||||||
|
else process.env[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Object.assign(process.env, skillRootEnvironment)
|
||||||
let persistenceRoot: string
|
let persistenceRoot: string
|
||||||
try {
|
try {
|
||||||
persistenceRoot = await mkdtemp(join(tmpdir(), 'dsh-web-e2e-sessions-'))
|
persistenceRoot = await mkdtemp(join(tmpdir(), 'dsh-web-e2e-sessions-'))
|
||||||
@@ -415,6 +440,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
|||||||
if (process.cwd() !== originalCwd) process.chdir(originalCwd)
|
if (process.cwd() !== originalCwd) process.chdir(originalCwd)
|
||||||
const cleanupFailures = await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot)
|
const cleanupFailures = await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot)
|
||||||
restoreCredentialEnvironment()
|
restoreCredentialEnvironment()
|
||||||
|
restoreSkillRootEnvironment()
|
||||||
if (cleanupFailures.length > 0) {
|
if (cleanupFailures.length > 0) {
|
||||||
throw new AggregateError([error, ...cleanupFailures], 'web scaffold setup failed and cleanup was incomplete')
|
throw new AggregateError([error, ...cleanupFailures], 'web scaffold setup failed and cleanup was incomplete')
|
||||||
}
|
}
|
||||||
@@ -462,6 +488,7 @@ export async function launchWebScaffold(options: LaunchOptions = {}): Promise<We
|
|||||||
failures.push(...await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot))
|
failures.push(...await cleanupScaffoldWorld(ctx, workspaceCwd, persistenceRoot))
|
||||||
} finally {
|
} finally {
|
||||||
restoreCredentialEnvironment()
|
restoreCredentialEnvironment()
|
||||||
|
restoreSkillRootEnvironment()
|
||||||
}
|
}
|
||||||
if (failures.length > 0) throw new AggregateError(failures, 'web scaffold teardown failed')
|
if (failures.length > 0) throw new AggregateError(failures, 'web scaffold teardown failed')
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -204,8 +204,11 @@
|
|||||||
# absent from a surface overlay would silently reappear the day someone reorders
|
# absent from a surface overlay would silently reappear the day someone reorders
|
||||||
# the composition.
|
# the composition.
|
||||||
|
|
||||||
- id: bash-env
|
# `bash-env` STAYS in the host plane: `apps/cli/src/web.ts` injects it to
|
||||||
disabled: true
|
# 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
|
||||||
|
# would never reach the model's shell at all.
|
||||||
|
|
||||||
- id: tool-bash
|
- id: tool-bash
|
||||||
disabled: true
|
disabled: true
|
||||||
@@ -261,14 +264,12 @@
|
|||||||
- id: tool-result-prune
|
- id: tool-result-prune
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
- id: subagent
|
# The subagent registry and its backends STAY in the host plane. `subagents` is
|
||||||
disabled: true
|
# a process singleton with a cross-session query surface (`listChildren`,
|
||||||
|
# `followup`) that the host api-proxy serves to the browser, and a provider
|
||||||
- id: subagent-spawn
|
# registers under a globally unique name, so a per-session copy would both
|
||||||
disabled: true
|
# starve that host row and collide on the second session. What a preset
|
||||||
|
# chooses is which delegation TOOLS its agent sees, below.
|
||||||
- id: subagent-fork
|
|
||||||
disabled: true
|
|
||||||
|
|
||||||
- id: tool-subagent-control
|
- id: tool-subagent-control
|
||||||
disabled: true
|
disabled: true
|
||||||
@@ -282,8 +283,11 @@
|
|||||||
- id: tool-subagent-fork
|
- id: tool-subagent-fork
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|
||||||
- id: tool-subagent-report
|
# `tool-subagent-report` is host-plane for the same reason as the registry, not
|
||||||
disabled: true
|
# because a preset may not want it: it registers a CONTINUABLE SETUP on that
|
||||||
|
# singleton rather than a tool this agent calls, and the setup list is not
|
||||||
|
# scope-aware — one copy per mounted preset means every child gets `report`
|
||||||
|
# registered once per live session, which throws on the second.
|
||||||
|
|
||||||
- id: workflow-workerthread
|
- id: workflow-workerthread
|
||||||
disabled: true
|
disabled: true
|
||||||
|
|||||||
Reference in New Issue
Block a user