From 1aadce9fe7eaa24f66a210e772acd701ed06fc20 Mon Sep 17 00:00:00 2001 From: Yichen Jiang Date: Sun, 12 Jul 2026 16:30:01 +0800 Subject: [PATCH] refactor(core): centralize DSH home resolution --- docs/config-catalog.md | 3 +- docs/cordis-catalog/services.md | 2 +- docs/module-graph.md | 11 +++++-- ...agent-session-identity-and-log-location.md | 4 +-- knip.json | 5 ++++ packages/README.md | 2 +- packages/bash/tool-bash/README.md | 2 +- packages/bash/tool-bash/package.json | 2 ++ packages/bash/tool-bash/src/index.ts | 11 ++++--- packages/bash/tool-bash/tsconfig.json | 3 ++ packages/core/agent-core/README.md | 2 +- packages/core/agent-core/package.json | 2 ++ packages/core/agent-core/src/index.ts | 14 ++++----- packages/core/agent-core/tsconfig.json | 3 ++ packages/skill/skill-local/README.md | 2 +- packages/skill/skill-local/package.json | 2 ++ packages/skill/skill-local/src/index.ts | 3 +- packages/skill/skill-local/tsconfig.json | 1 + packages/util/README.md | 3 ++ packages/util/home/README.md | 9 ++++++ packages/util/home/package.json | 30 +++++++++++++++++++ packages/util/home/src/index.ts | 23 ++++++++++++++ packages/util/home/tests/home.spec.ts | 26 ++++++++++++++++ packages/util/home/tsconfig.json | 9 ++++++ pnpm-lock.yaml | 15 ++++++++++ tsconfig.build.json | 1 + tsconfig.json | 1 + 27 files changed, 164 insertions(+), 27 deletions(-) create mode 100644 packages/util/home/README.md create mode 100644 packages/util/home/package.json create mode 100644 packages/util/home/src/index.ts create mode 100644 packages/util/home/tests/home.spec.ts create mode 100644 packages/util/home/tsconfig.json diff --git a/docs/config-catalog.md b/docs/config-catalog.md index b297595592..55b0551ac2 100644 --- a/docs/config-catalog.md +++ b/docs/config-catalog.md @@ -606,7 +606,7 @@ export interface Config { } ``` -Source: [`packages/skill/skill-local/src/index.ts:39`](../packages/skill/skill-local/src/index.ts) +Source: [`packages/skill/skill-local/src/index.ts:40`](../packages/skill/skill-local/src/index.ts) ## `@deepseek-ai/dsh-stdio-agent` @@ -1184,6 +1184,7 @@ Imported as libraries by other packages; a `cordis.yml` cannot load them. - `@deepseek-ai/dsh-acp-snapshot` ([`packages/support/acp-snapshot/src/index.ts`](../packages/support/acp-snapshot/src/index.ts)) - `@deepseek-ai/dsh-app-boot` ([`packages/ui/app-boot/src/index.ts`](../packages/ui/app-boot/src/index.ts)) - `@deepseek-ai/dsh-brand` ([`packages/util/brand/src/index.ts`](../packages/util/brand/src/index.ts)) +- `@deepseek-ai/dsh-home` ([`packages/util/home/src/index.ts`](../packages/util/home/src/index.ts)) - `@deepseek-ai/dsh-hook-protocol` ([`packages/hooks/hook-protocol/src/index.ts`](../packages/hooks/hook-protocol/src/index.ts)) - `@deepseek-ai/dsh-subagent-inprocess` ([`packages/subagent/subagent-inprocess/src/index.ts`](../packages/subagent/subagent-inprocess/src/index.ts)) - `@deepseek-ai/dsh-subagent-subprocess` ([`packages/subagent/subagent-subprocess/src/index.ts`](../packages/subagent/subagent-subprocess/src/index.ts)) diff --git a/docs/cordis-catalog/services.md b/docs/cordis-catalog/services.md index d25379adad..9094e3a351 100644 --- a/docs/cordis-catalog/services.md +++ b/docs/cordis-catalog/services.md @@ -93,7 +93,7 @@ list(): BashEnvVariableInfo[] Types: [ToolExecution](../core-data-structures/tools.md) -Source: [`packages/bash/tool-bash/src/index.ts:147`](../../packages/bash/tool-bash/src/index.ts) +Source: [`packages/bash/tool-bash/src/index.ts:146`](../../packages/bash/tool-bash/src/index.ts) ## `ctx.codeRuntime` — `CodeRuntime` (abstract seam) diff --git a/docs/module-graph.md b/docs/module-graph.md index 56c3a64f95..fe606ad78a 100644 --- a/docs/module-graph.md +++ b/docs/module-graph.md @@ -9,6 +9,7 @@ Inter-package dependencies among the `@deepseek-ai/dsh-*` harness packages, deri flowchart TD subgraph group_util["packages/util"] pkg_brand["brand"] + pkg_home["home"] pkg_timeout["timeout"] end subgraph group_llm["packages/llm"] @@ -133,6 +134,7 @@ flowchart TD pkg_fs_local --> pkg_fs pkg_fs_policy --> pkg_fs pkg_skill_local --> pkg_fs + pkg_skill_local --> pkg_home pkg_skill_local --> pkg_skill pkg_compact --> pkg_llm pkg_compact --> pkg_session @@ -188,6 +190,7 @@ flowchart TD pkg_agent_loop --> pkg_tools pkg_tool_bash --> pkg_agent pkg_tool_bash --> pkg_bash + pkg_tool_bash --> pkg_home pkg_tool_bash --> pkg_llm pkg_tool_bash --> pkg_sandbox pkg_tool_bash --> pkg_session_persistence @@ -244,6 +247,7 @@ flowchart TD pkg_tool_workflow --> pkg_workflow pkg_agent_core --> pkg_agent pkg_agent_core --> pkg_agent_loop + pkg_agent_core --> pkg_home pkg_agent_core --> pkg_invariants pkg_agent_core --> pkg_llm pkg_agent_core --> pkg_session @@ -309,6 +313,7 @@ flowchart TD | Package | Group | Depends on | | --- | --- | --- | | [`brand`](../packages/util/brand) | `util` | — | +| [`home`](../packages/util/home) | `util` | — | | [`timeout`](../packages/util/timeout) | `util` | — | | [`skill`](../packages/skill/skill) | `skill` | — | | [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | `subagent` | — | @@ -328,7 +333,7 @@ flowchart TD | [`bash`](../packages/bash/bash) | `bash` | [`brand`](../packages/util/brand), [`sandbox`](../packages/sandbox/sandbox), [`session`](../packages/core/session) | | [`fs-local`](../packages/fs/fs-local) | `fs` | [`fs`](../packages/fs/fs) | | [`fs-policy`](../packages/fs/fs-policy) | `fs` | [`fs`](../packages/fs/fs) | -| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`skill`](../packages/skill/skill) | +| [`skill-local`](../packages/skill/skill-local) | `skill` | [`fs`](../packages/fs/fs), [`home`](../packages/util/home), [`skill`](../packages/skill/skill) | | [`compact`](../packages/compact/compact) | `compact` | [`llm`](../packages/llm/llm), [`session`](../packages/core/session) | | [`web-fetch-local`](../packages/web/web-fetch-local) | `web` | [`timeout`](../packages/util/timeout), [`web`](../packages/web/web) | | [`web-search-deepseek`](../packages/web/web-search-deepseek) | `web` | [`web`](../packages/web/web) | @@ -349,7 +354,7 @@ flowchart TD | [`tools`](../packages/core/tools) | `core` | [`agent`](../packages/core/agent), [`code-runtime`](../packages/code-runtime/code-runtime), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`user-approval`](../packages/ui/user-approval) | | [`bash-sandbox`](../packages/bash/bash-sandbox) | `bash` | [`bash`](../packages/bash/bash), [`bash-local`](../packages/bash/bash-local), [`sandbox`](../packages/sandbox/sandbox) | | [`agent-loop`](../packages/core/agent-loop) | `core` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | -| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | +| [`tool-bash`](../packages/bash/tool-bash) | `bash` | [`agent`](../packages/core/agent), [`bash`](../packages/bash/bash), [`home`](../packages/util/home), [`llm`](../packages/llm/llm), [`sandbox`](../packages/sandbox/sandbox), [`session-persistence`](../packages/session-persistence/session-persistence), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`user-approval`](../packages/ui/user-approval) | | [`tool-fs`](../packages/fs/tool-fs) | `fs` | [`fs`](../packages/fs/fs), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-skill`](../packages/skill/tool-skill) | `skill` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`skill`](../packages/skill/skill), [`tools`](../packages/core/tools) | | [`subagent`](../packages/subagent/subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`tools`](../packages/core/tools) | @@ -362,7 +367,7 @@ flowchart TD | [`tool-ask-user`](../packages/ui/tool-ask-user) | `ui` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools), [`user-interaction`](../packages/ui/user-interaction) | | [`repeat-tool-guard`](../packages/guard/repeat-tool-guard) | `guard` | [`agent`](../packages/core/agent), [`tools`](../packages/core/tools) | | [`tool-workflow`](../packages/workflow/tool-workflow) | `workflow` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools), [`workflow`](../packages/workflow/workflow) | -| [`agent-core`](../packages/core/agent-core) | `core` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/bash/tool-bash), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) | +| [`agent-core`](../packages/core/agent-core) | `core` | [`agent`](../packages/core/agent), [`agent-loop`](../packages/core/agent-loop), [`home`](../packages/util/home), [`invariants`](../packages/support/invariants), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`skill`](../packages/skill/skill), [`skill-local`](../packages/skill/skill-local), [`system-prompt`](../packages/core/system-prompt), [`tool-bash`](../packages/bash/tool-bash), [`tool-skill`](../packages/skill/tool-skill), [`tools`](../packages/core/tools) | | [`subagent-acp`](../packages/subagent/subagent-acp) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`subagent-subprocess`](../packages/subagent/subagent-subprocess) | | [`subagent-inprocess`](../packages/subagent/subagent-inprocess) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`session`](../packages/core/session), [`subagent`](../packages/subagent/subagent), [`system-prompt`](../packages/core/system-prompt), [`tools`](../packages/core/tools) | | [`tool-subagent`](../packages/subagent/tool-subagent) | `subagent` | [`agent`](../packages/core/agent), [`llm`](../packages/llm/llm), [`subagent`](../packages/subagent/subagent), [`tools`](../packages/core/tools) | diff --git a/docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md b/docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md index 9dcd609b50..9127d70207 100644 --- a/docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md +++ b/docs/rfc/implemented/feature/2026-07-10-agent-session-identity-and-log-location.md @@ -31,7 +31,7 @@ The model-facing bash package owns a `ctx.bashEnv` registry. A contributor decla The registry rebuilds a trusted overlay for every foreground and background bash `ToolExecution`: -- `DSH_HOME` is always the absolute configured Harness home, resolved from tool-bash/agent-core `dshHome`, then ambient `$DSH_HOME`, then `~/.dsh`. +- `DSH_HOME` is always the absolute configured Harness home. The standalone [`@deepseek-ai/dsh-home`](../../../../packages/util/home/README.md) utility owns its precedence: explicit `dshHome`, then ambient `$DSH_HOME`, then `~/.dsh`. - `DSH_SHELL=1` is always present and identifies a model bash child managed by DeepSeek Harness. - `DSH_SESSION_ID` is present when the execution has an agent and equals `agent.session.header.id`. - The built-in persistence translator contributes `DSH_SESSION_JSONL` only when `ctx.sessionPersistence.locate(header)` returns `kind: 'jsonl'`. @@ -54,7 +54,7 @@ A fresh session receives its id before the first turn, so its first bash call ca Resume reuses the loaded header and therefore the same id and location. Fork and spawn create new session ids and locations. Parent and child calls resolve from their own `ToolExecution.agent`; each command receives an immutable snapshot even when calls overlap. A persistence service replacement affects later collections because the translator queries `ctx.get('sessionPersistence')` at execution time; the registry itself is effect-scoped and HMR-safe. -`dshHome` is session-independent deployment context. Agent-core routes one value to both tool-bash and local skill discovery; if top-level `dshHome` and `skills.local.dshHome` are both supplied and resolve differently, composition fails instead of exposing contradictory homes. Persistence may change independently without freezing its facts into the session prefix. +`dshHome` is session-independent deployment context. Agent-core resolves one value through `@deepseek-ai/dsh-home` and routes it to both tool-bash and local skill discovery; standalone consumers call the same resolver. If top-level `dshHome` and `skills.local.dshHome` are both supplied and resolve differently, composition fails instead of exposing contradictory homes. Persistence may change independently without freezing its facts into the session prefix. ## Testing diff --git a/knip.json b/knip.json index cf43b90e34..929b590874 100644 --- a/knip.json +++ b/knip.json @@ -31,6 +31,11 @@ "project": ["src/**/*.ts"], "ignoreDependencies": ["cordis"] }, + "packages/util/home": { + "entry": ["tests/**/*.spec.ts"], + "project": ["src/**/*.ts", "tests/**/*.ts"], + "ignoreDependencies": ["cordis"] + }, "packages/util/timeout": { "entry": ["tests/**/*.spec.ts"], "project": ["src/**/*.ts", "tests/**/*.ts"], diff --git a/packages/README.md b/packages/README.md index 35c4652a1f..5c6fedd7f4 100644 --- a/packages/README.md +++ b/packages/README.md @@ -27,7 +27,7 @@ Packages are grouped by modular role at `packages///`. The group dir | [`session-persistence/`](session-persistence/README.md) | Persistence capability family: the seam + JSONL/SQLite backends | Product — stable surface | | [`ui/`](ui/README.md) | Editor/client integration surfaces: ACP bridge, app packages, user-approval and user-interaction seams, ask-user tool | Product — stable surface | | [`support/`](support/README.md) | Dev/test/example infrastructure (invariants, replay adapter, subagent mock) | Support — lower compatibility expectations | -| [`util/`](util/README.md) | Low-level zero-dependency utilities shared across groups (the `Branded` primitive) | Support — small, stable, harness-dep-free | +| [`util/`](util/README.md) | Low-level zero-dependency utilities shared across groups (branding, Harness home resolution, timeout classification) | Support — small, stable, harness-dep-free | The split is the point: a package's group says whether it is part of the product API or support/test/example infrastructure, so release and removal decisions do not treat every package as an equal public contract. New packages join an existing group; adding a new top-level group is a deliberate act (extend the group READMEs and this table). diff --git a/packages/bash/tool-bash/README.md b/packages/bash/tool-bash/README.md index ccbb1c0033..d728788da8 100644 --- a/packages/bash/tool-bash/README.md +++ b/packages/bash/tool-bash/README.md @@ -24,7 +24,7 @@ The plugin also contributes the `tool:bash` prompt section (order 105) — the c ### Managed shell environment -Every foreground and background model bash call receives a newly collected trusted `DSH_*` environment. `DSH_HOME` is the absolute Harness home (`dshHome` config, then ambient `$DSH_HOME`, then `~/.dsh`) and `DSH_SHELL=1` identifies the managed child. Agent calls additionally receive `DSH_SESSION_ID=agent.session.header.id`; when the active persistence seam locates a JSONL artifact they also receive `DSH_SESSION_JSONL=`. The JSONL path is a location hint: it may not exist before the first flush or contain the current buffered turn, and it is not an authorization credential. +Every foreground and background model bash call receives a newly collected trusted `DSH_*` environment. `DSH_HOME` is the absolute Harness home resolved by [`@deepseek-ai/dsh-home`](../../util/home/README.md) (`dshHome` config, then ambient `$DSH_HOME`, then `~/.dsh`) and `DSH_SHELL=1` identifies the managed child. Agent calls additionally receive `DSH_SESSION_ID=agent.session.header.id`; when the active persistence seam locates a JSONL artifact they also receive `DSH_SESSION_JSONL=`. The JSONL path is a location hint: it may not exist before the first flush or contain the current buffered turn, and it is not an authorization credential. `ctx.bashEnv` owns collection. Other plugins can register an effect-scoped contributor with a stable name, declared keys/descriptions, and `resolve(execution: ToolExecution)`; duplicate ownership and undeclared runtime keys fail loudly, while `list()` enumerates declarations without executing providers. Harness built-ins reserve `DSH_HOME`, `DSH_SHELL`, and `DSH_SESSION_ID`; tool-bash's persistence translator owns `DSH_SESSION_JSONL` by reading the backend-neutral `sessionPersistence.locate()` seam. diff --git a/packages/bash/tool-bash/package.json b/packages/bash/tool-bash/package.json index 003aadad74..e41f08f56d 100644 --- a/packages/bash/tool-bash/package.json +++ b/packages/bash/tool-bash/package.json @@ -25,6 +25,7 @@ "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-user-approval": "^0.0.1", "@deepseek-ai/dsh-bash": "^0.0.1", + "@deepseek-ai/dsh-home": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session-persistence": "^0.0.1", "@deepseek-ai/dsh-sandbox": "^0.0.1", @@ -42,6 +43,7 @@ "@deepseek-ai/dsh-bash": "workspace:^", "@deepseek-ai/dsh-bash-local": "workspace:^", "@deepseek-ai/dsh-bash-sandbox": "workspace:^", + "@deepseek-ai/dsh-home": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-sandbox": "workspace:^", "@deepseek-ai/dsh-sandbox-local": "workspace:^", diff --git a/packages/bash/tool-bash/src/index.ts b/packages/bash/tool-bash/src/index.ts index 08493d79c0..712b381930 100644 --- a/packages/bash/tool-bash/src/index.ts +++ b/packages/bash/tool-bash/src/index.ts @@ -57,8 +57,7 @@ import { Service, type Context } from 'cordis' import z from 'schemastery' -import { homedir } from 'node:os' -import { isAbsolute, join, resolve as resolvePath } from 'node:path' +import { isAbsolute, resolve as resolvePath } from 'node:path' import { defineTool } from '@deepseek-ai/dsh-tools' import type { GenericCallView, TerminalCallView, ToolExecution, ToolResult, ToolResultView } from '@deepseek-ai/dsh-tools' import type { Agent } from '@deepseek-ai/dsh-agent' @@ -72,6 +71,7 @@ import type {} from '@deepseek-ai/dsh-user-approval' import type { SandboxMode } from '@deepseek-ai/dsh-sandbox' import { BashTaskId, DSH_ENV_PREFIX, OwnerToken, effectiveSandboxMode } from '@deepseek-ai/dsh-bash' import type { BashRunResult, BashTask, CollectedOutput, DshEnvironment, DshEnvironmentKey } from '@deepseek-ai/dsh-bash' +import { DSH_HOME_ENV, resolveDshHome } from '@deepseek-ai/dsh-home' declare module 'cordis' { interface Context { @@ -125,12 +125,11 @@ export interface BashEnvVariableInfo extends BashEnvVariable { key: DshEnvironmentKey } -const DSH_HOME_KEY = `${DSH_ENV_PREFIX}HOME` as const const DSH_SHELL_KEY = `${DSH_ENV_PREFIX}SHELL` as const const DSH_SESSION_ID_KEY = `${DSH_ENV_PREFIX}SESSION_ID` as const const DSH_SESSION_JSONL_KEY = `${DSH_ENV_PREFIX}SESSION_JSONL` as const const RESERVED_BASH_ENV_KEYS = new Set([ - DSH_HOME_KEY, + DSH_HOME_ENV, DSH_SHELL_KEY, DSH_SESSION_ID_KEY, ]) @@ -156,7 +155,7 @@ export class BashEnvRegistry extends Service { */ constructor(ctx: Context, config: Config = {}) { super(ctx, 'bashEnv') - this.dshHome = resolvePath(config.dshHome ?? process.env[DSH_HOME_KEY] ?? join(homedir(), '.dsh')) + this.dshHome = resolveDshHome(config.dshHome) } /** @@ -209,7 +208,7 @@ export class BashEnvRegistry extends Service { */ collect(execution: ToolExecution): DshEnvironment { const values: Record = { - [DSH_HOME_KEY]: this.dshHome, + [DSH_HOME_ENV]: this.dshHome, [DSH_SHELL_KEY]: '1', } if (execution.agent !== undefined) { diff --git a/packages/bash/tool-bash/tsconfig.json b/packages/bash/tool-bash/tsconfig.json index d3bd550386..5f8f4752b5 100644 --- a/packages/bash/tool-bash/tsconfig.json +++ b/packages/bash/tool-bash/tsconfig.json @@ -29,6 +29,9 @@ { "path": "../../bash/bash" }, + { + "path": "../../util/home" + }, { "path": "../../core/system-prompt" }, diff --git a/packages/core/agent-core/README.md b/packages/core/agent-core/README.md index ad63fdc5c0..233a126561 100644 --- a/packages/core/agent-core/README.md +++ b/packages/core/agent-core/README.md @@ -43,7 +43,7 @@ import type { Config } from '@deepseek-ai/dsh-agent-core' // so validation and defaulting can never drift from the owners. ``` -The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loop` (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`) — `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry; `dshHome` to tool-bash's managed environment and the local skill provider; and `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. +The bundle FORWARDS each field to the child that owns it: `agents` to `agent-loop` (default `[]`), so each app supplies its own pre-created agents — a stdio app pre-creates a `main`; the ACP app pre-creates none (it creates agents on demand at `session/new`) — `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry; and `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer. It resolves `dshHome` once through [`@deepseek-ai/dsh-home`](../../util/home/README.md) and forwards that absolute value to tool-bash's managed environment and local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. ## Why a code bundle, not a shared YAML include diff --git a/packages/core/agent-core/package.json b/packages/core/agent-core/package.json index 039a6ac505..749de25be9 100644 --- a/packages/core/agent-core/package.json +++ b/packages/core/agent-core/package.json @@ -26,6 +26,7 @@ "@deepseek-ai/dsh-agent": "^0.0.1", "@deepseek-ai/dsh-agent-loop": "^0.0.1", "@deepseek-ai/dsh-invariants": "^0.0.1", + "@deepseek-ai/dsh-home": "^0.0.1", "@deepseek-ai/dsh-llm": "^0.0.1", "@deepseek-ai/dsh-session": "^0.0.1", "@deepseek-ai/dsh-skill": "^0.0.1", @@ -41,6 +42,7 @@ "@deepseek-ai/dsh-agent": "workspace:^", "@deepseek-ai/dsh-agent-loop": "workspace:^", "@deepseek-ai/dsh-invariants": "workspace:^", + "@deepseek-ai/dsh-home": "workspace:^", "@deepseek-ai/dsh-llm": "workspace:^", "@deepseek-ai/dsh-session": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", diff --git a/packages/core/agent-core/src/index.ts b/packages/core/agent-core/src/index.ts index ba4904cbef..2c0c92a4bc 100644 --- a/packages/core/agent-core/src/index.ts +++ b/packages/core/agent-core/src/index.ts @@ -46,7 +46,6 @@ */ import type { Context } from 'cordis' -import { resolve as resolvePath } from 'node:path' import Timer from '@cordisjs/plugin-timer' import z from 'schemastery' import LlmService from '@deepseek-ai/dsh-llm' @@ -60,6 +59,7 @@ import * as invariants from '@deepseek-ai/dsh-invariants' import * as toolBash from '@deepseek-ai/dsh-tool-bash' import * as toolSkill from '@deepseek-ai/dsh-tool-skill' import AgentLoop, { type Config as AgentLoopConfig } from '@deepseek-ai/dsh-agent-loop' +import { resolveDshHome } from '@deepseek-ai/dsh-home' export const name = 'agent-core' @@ -127,10 +127,10 @@ export const Config = z.intersect([ export function apply(ctx: Context, config: Config): void { const nestedDshHome = config.skills?.local?.dshHome if (config.dshHome !== undefined && nestedDshHome !== undefined - && resolvePath(config.dshHome) !== resolvePath(nestedDshHome)) { + && resolveDshHome(config.dshHome) !== resolveDshHome(nestedDshHome)) { throw new Error('agent-core: dshHome and skills.local.dshHome must resolve to the same directory') } - const dshHome = config.dshHome ?? nestedDshHome + const dshHome = resolveDshHome(config.dshHome ?? nestedDshHome) ctx.plugin(Timer) ctx.plugin(LlmService) @@ -147,14 +147,10 @@ export function apply(ctx: Context, config: Config): void { }) ctx.plugin(ToolRegistry, config.tools ?? {}) ctx.plugin(SkillService, config.skills?.registry ?? {}) - ctx.plugin(SkillLocal, Object.assign( - {}, - config.skills?.local, - dshHome === undefined ? {} : { dshHome }, - )) + ctx.plugin(SkillLocal, Object.assign({}, config.skills?.local, { dshHome })) ctx.plugin(AgentRegistry) ctx.plugin(invariants) - ctx.plugin(toolBash, dshHome === undefined ? {} : { dshHome }) + ctx.plugin(toolBash, { dshHome }) ctx.plugin(toolSkill, config.skills?.tool ?? {}) ctx.plugin(AgentLoop, { agents: config.agents ?? [] }) } diff --git a/packages/core/agent-core/tsconfig.json b/packages/core/agent-core/tsconfig.json index 4fd0a81e97..974c2aecc0 100644 --- a/packages/core/agent-core/tsconfig.json +++ b/packages/core/agent-core/tsconfig.json @@ -50,6 +50,9 @@ { "path": "../../support/invariants" }, + { + "path": "../../util/home" + }, { "path": "../../bash/tool-bash" } diff --git a/packages/skill/skill-local/README.md b/packages/skill/skill-local/README.md index c885416ff5..4214c55505 100644 --- a/packages/skill/skill-local/README.md +++ b/packages/skill/skill-local/README.md @@ -12,7 +12,7 @@ Requires `ctx.skills` (`inject: ['skills']`). | Field | Default | Meaning | |---|---|---| -| `dshHome` | `$DSH_HOME` or `~/.dsh` | DeepSeek Harness config root; scans `skills` under this directory. | +| `dshHome` | `$DSH_HOME` or `~/.dsh` | DeepSeek Harness config root resolved by [`@deepseek-ai/dsh-home`](../../util/home/README.md); scans `skills` under this directory. | | `agentsHome` | `$DSH_AGENTS_HOME` or `~/.agents` | Shared agent config root scanned for compatible skills. | | `customSkillDirs` | `[]` | Additional local skill roots scanned after project roots and before user roots. | diff --git a/packages/skill/skill-local/package.json b/packages/skill/skill-local/package.json index dcacc5960a..1e19cf891a 100644 --- a/packages/skill/skill-local/package.json +++ b/packages/skill/skill-local/package.json @@ -23,6 +23,7 @@ "license": "BSD-3-Clause", "peerDependencies": { "@deepseek-ai/dsh-fs": "^0.0.1", + "@deepseek-ai/dsh-home": "^0.0.1", "@deepseek-ai/dsh-skill": "^0.0.1", "cordis": "^4.0.0-rc.6" }, @@ -32,6 +33,7 @@ }, "devDependencies": { "@deepseek-ai/dsh-fs": "workspace:^", + "@deepseek-ai/dsh-home": "workspace:^", "@deepseek-ai/dsh-skill": "workspace:^", "cordis": "^4.0.0-rc.6" } diff --git a/packages/skill/skill-local/src/index.ts b/packages/skill/skill-local/src/index.ts index 19a15f1de8..ee109fbb16 100644 --- a/packages/skill/skill-local/src/index.ts +++ b/packages/skill/skill-local/src/index.ts @@ -17,6 +17,7 @@ import z from 'schemastery' import type Schema from 'schemastery' import { parse as parseYaml } from 'yaml' import type { FileSystem, FsDirEntry, FsTarget } from '@deepseek-ai/dsh-fs' +import { resolveDshHome } from '@deepseek-ai/dsh-home' import { isSkillName, type SkillCandidate, @@ -92,7 +93,7 @@ export class LocalSkillProvider implements SkillProvider { private readonly customSkillDirs: string[] constructor(private readonly ctx: Context, config: Config = {}) { - this.dshHome = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh')) + this.dshHome = resolveDshHome(config.dshHome) this.agentsHome = resolve(config.agentsHome ?? process.env.DSH_AGENTS_HOME ?? join(homedir(), '.agents')) this.customSkillDirs = (config.customSkillDirs ?? []).map(root => resolve(root)) } diff --git a/packages/skill/skill-local/tsconfig.json b/packages/skill/skill-local/tsconfig.json index 018f0a4a50..f51147abce 100644 --- a/packages/skill/skill-local/tsconfig.json +++ b/packages/skill/skill-local/tsconfig.json @@ -9,6 +9,7 @@ { "path": "../../../vendor/cosmokit" }, { "path": "../../../vendor/cordis" }, { "path": "../../../vendor/schemastery" }, + { "path": "../../util/home" }, { "path": "../../fs/fs" }, { "path": "../skill" } ] diff --git a/packages/util/README.md b/packages/util/README.md index 45afe7b0a9..256cc36e67 100644 --- a/packages/util/README.md +++ b/packages/util/README.md @@ -5,8 +5,11 @@ Zero-dependency primitives shared across the other groups. A package lands here | Package | Role | |---|---| | `brand/` | The type-only `Branded` nominal-typing primitive (no runtime code, no harness deps) | +| `home/` | Canonical `DSH_HOME` resolution from explicit config, environment, or `~/.dsh` (no harness deps) | | `timeout/` | The timing/classification half of a timeout — `clampTimeout`/`deadline`/`timeoutOf`/`TimeoutReason` (pure functions, no harness deps); termination stays in each capability | `dsh-brand` is the canonical case: it owns ONLY the `Branded` helper, so a capability package can brand the ids it owns (`dsh-bash`'s `BashTaskId`/`OwnerToken`, `dsh-session`'s `SessionId`, …) by depending on `dsh-brand` alone, without pulling in an unrelated package just to reach `Branded`. +`dsh-home` gives every package the same configurable Harness home without assigning that cross-cutting fact to bash, skills, or a composition bundle. It resolves an explicit value before `$DSH_HOME`, falls back to `~/.dsh`, and returns an absolute path without caching, creating, or mutating anything. + `dsh-timeout` follows the same shape for the timeout family: `dsh-bash` and `dsh-web-fetch-local` each fuse a caller's cancellation with a deadline and later classify "timed out" vs "cancelled" by depending on `dsh-timeout` alone. It deliberately owns only the timing/classification half — the *termination* (SIGKILL a process group, tear down a fetch socket) stays in each capability, because no shared layer can own every capability's kill (see [the timeout-library RFC](../../docs/rfc/implemented/architecture/2026-07-06-timeout-deadline-library.md)). diff --git a/packages/util/home/README.md b/packages/util/home/README.md new file mode 100644 index 0000000000..14d8fec1f8 --- /dev/null +++ b/packages/util/home/README.md @@ -0,0 +1,9 @@ +# @deepseek-ai/dsh-home + +`@deepseek-ai/dsh-home` is the single owner of DeepSeek Harness home-directory resolution. `resolveDshHome(configured?)` returns an absolute path using this precedence: + +1. The explicit `configured` path. +2. The `DSH_HOME` environment variable. +3. The `.dsh` directory under the current user's home directory. + +The resolver reads its inputs at call time. It does not cache a result, create the directory, or mutate `process.env`; consumers keep ownership of their own configuration fields and pass the configured value when resolving the shared home. diff --git a/packages/util/home/package.json b/packages/util/home/package.json new file mode 100644 index 0000000000..efeaf4832c --- /dev/null +++ b/packages/util/home/package.json @@ -0,0 +1,30 @@ +{ + "name": "@deepseek-ai/dsh-home", + "description": "Canonical DeepSeek Harness home-directory resolver", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "lib/index.js", + "types": "lib/types/index.d.ts", + "exports": { + ".": { + "types": "./lib/types/index.d.ts", + "default": "./lib/index.js" + }, + "./src/*": "./src/*", + "./package.json": "./package.json" + }, + "files": [ + "lib/index.js", + "lib/types/**/*.d.ts", + "lib/types/**/*.d.ts.map", + "src" + ], + "license": "BSD-3-Clause", + "peerDependencies": { + "cordis": "^4.0.0-rc.6" + }, + "devDependencies": { + "cordis": "^4.0.0-rc.6" + } +} diff --git a/packages/util/home/src/index.ts b/packages/util/home/src/index.ts new file mode 100644 index 0000000000..4e3d56b54b --- /dev/null +++ b/packages/util/home/src/index.ts @@ -0,0 +1,23 @@ +/** + * Canonical DeepSeek Harness home-directory resolution. + * + * @module @deepseek-ai/dsh-home + */ + +import { homedir } from 'node:os' +import { join, resolve } from 'node:path' + +const DEFAULT_DSH_HOME_DIRNAME = '.dsh' + +/** Environment variable that overrides the default Harness home directory. */ +export const DSH_HOME_ENV = 'DSH_HOME' as const + +/** + * Resolve the DeepSeek Harness home directory without caching or mutating the environment. + * + * @param configured - Optional configured path, which takes precedence over the environment. + * @returns The absolute configured path, `$DSH_HOME`, or `~/.dsh`, in that order. + */ +export function resolveDshHome(configured?: string): string { + return resolve(configured ?? process.env[DSH_HOME_ENV] ?? join(homedir(), DEFAULT_DSH_HOME_DIRNAME)) +} diff --git a/packages/util/home/tests/home.spec.ts b/packages/util/home/tests/home.spec.ts new file mode 100644 index 0000000000..3ebde50bee --- /dev/null +++ b/packages/util/home/tests/home.spec.ts @@ -0,0 +1,26 @@ +import { homedir } from 'node:os' +import { join, resolve } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import { DSH_HOME_ENV, resolveDshHome } from '@deepseek-ai/dsh-home' + +afterEach(() => vi.unstubAllEnvs()) + +describe('resolveDshHome', () => { + it('prefers an explicit configured path and resolves it absolutely', () => { + vi.stubEnv(DSH_HOME_ENV, './environment-home') + + expect(resolveDshHome('./configured-home')).toBe(resolve('./configured-home')) + }) + + it('uses DSH_HOME when no configured path is supplied', () => { + vi.stubEnv(DSH_HOME_ENV, './environment-home') + + expect(resolveDshHome()).toBe(resolve('./environment-home')) + }) + + it('defaults to the .dsh directory under the user home', () => { + vi.stubEnv(DSH_HOME_ENV, undefined) + + expect(resolveDshHome()).toBe(join(homedir(), '.dsh')) + }) +}) diff --git a/packages/util/home/tsconfig.json b/packages/util/home/tsconfig.json new file mode 100644 index 0000000000..9770ef25d6 --- /dev/null +++ b/packages/util/home/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../../tsconfig.base.json", + "compilerOptions": { + "rootDir": "src", + "outDir": "lib/types" + }, + "include": ["src"], + "references": [] +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 688c6b1f0c..0e7451343c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -152,6 +152,9 @@ importers: '@deepseek-ai/dsh-bash-sandbox': specifier: workspace:^ version: link:../bash-sandbox + '@deepseek-ai/dsh-home': + specifier: workspace:^ + version: link:../../util/home '@deepseek-ai/dsh-llm': specifier: workspace:^ version: link:../../llm/llm @@ -311,6 +314,9 @@ importers: '@deepseek-ai/dsh-agent-loop': specifier: workspace:^ version: link:../agent-loop + '@deepseek-ai/dsh-home': + specifier: workspace:^ + version: link:../../util/home '@deepseek-ai/dsh-invariants': specifier: workspace:^ version: link:../../support/invariants @@ -773,6 +779,9 @@ importers: '@deepseek-ai/dsh-fs': specifier: workspace:^ version: link:../../fs/fs + '@deepseek-ai/dsh-home': + specifier: workspace:^ + version: link:../../util/home '@deepseek-ai/dsh-skill': specifier: workspace:^ version: link:../skill @@ -1350,6 +1359,12 @@ importers: specifier: ^4.0.0-rc.6 version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + packages/util/home: + devDependencies: + cordis: + specifier: ^4.0.0-rc.6 + version: 4.0.0-rc.6(@cordisjs/plugin-include@1.0.4)(@cordisjs/plugin-loader@1.0.0-rc.4) + packages/util/timeout: devDependencies: cordis: diff --git a/tsconfig.build.json b/tsconfig.build.json index 8b2967b2bf..caf749cd4b 100644 --- a/tsconfig.build.json +++ b/tsconfig.build.json @@ -11,6 +11,7 @@ { "path": "./vendor/hmr" }, { "path": "./vendor/logger-console" }, { "path": "./packages/util/brand" }, + { "path": "./packages/util/home" }, { "path": "./packages/util/timeout" }, { "path": "./packages/llm/llm" }, { "path": "./packages/core/session" }, diff --git a/tsconfig.json b/tsconfig.json index 70780e1c17..808d07a2e1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,6 +22,7 @@ { "path": "./vendor/hmr" }, { "path": "./vendor/logger-console" }, { "path": "./packages/util/brand" }, + { "path": "./packages/util/home" }, { "path": "./packages/util/timeout" }, { "path": "./packages/llm/llm" }, { "path": "./packages/core/session" },