Merge origin/master into worktree/ci-under-minute

This commit is contained in:
Tianyi Cui
2026-07-21 22:51:07 +08:00
40 changed files with 217 additions and 301 deletions

View File

@@ -26,7 +26,7 @@ The plugin also contributes the `tool:bash` prompt section (order 105): check th
### Managed shell environment
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=<absolute target path>`. 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-paths`](../../util/paths/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=<absolute target path>`. 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.

View File

@@ -29,12 +29,12 @@
"peerDependencies": {
"@deepseek-ai/dsh-agent": "^0.0.1",
"@deepseek-ai/dsh-bash": "^0.0.1",
"@deepseek-ai/dsh-home": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"@deepseek-ai/dsh-sandbox": "^0.0.1",
"@deepseek-ai/dsh-session-persistence": "^0.0.1",
"@deepseek-ai/dsh-sandbox-policy": "^0.0.1",
"@deepseek-ai/dsh-session-persistence": "^0.0.1",
"@deepseek-ai/dsh-system-prompt": "^0.0.1",
"@deepseek-ai/dsh-tasks": "^0.0.1",
"@deepseek-ai/dsh-tools": "^0.0.1",
@@ -50,9 +50,9 @@
"@deepseek-ai/dsh-agent-loop-testkit": "workspace:^",
"@deepseek-ai/dsh-bash": "workspace:^",
"@deepseek-ai/dsh-bash-local": "workspace:^",
"@deepseek-ai/dsh-home": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-sandbox": "workspace:^",
"@deepseek-ai/dsh-sandbox-policy": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",

View File

@@ -23,7 +23,7 @@ import { ESCALATION_TARGETS, approveEscalation, validateEscalationArgs } from '@
import { effectiveSandboxMode } from '@deepseek-ai/dsh-sandbox-policy'
import { DSH_ENV_PREFIX } from '@deepseek-ai/dsh-bash'
import type { DshEnvironment, DshEnvironmentKey } from '@deepseek-ai/dsh-bash'
import { DSH_HOME_ENV, resolveDshHome } from '@deepseek-ai/dsh-home'
import { DSH_HOME_ENV, resolveDshHome } from '@deepseek-ai/dsh-paths'
import { processOutcome } from './background.ts'
import { parseExitStatus, renderProcessRead, renderResult } from './render.ts'

View File

@@ -33,7 +33,7 @@
"path": "../../bash/bash"
},
{
"path": "../../util/home"
"path": "../../util/paths"
},
{
"path": "../../tasks/tasks"

View File

@@ -9,7 +9,7 @@ import { lstat, stat } from 'node:fs/promises'
import { dirname, isAbsolute, join, relative, resolve } from 'node:path'
import type { FileSystem, FsInfo, FsPathInfo, FsTarget, FsVersion } from '@deepseek-ai/dsh-fs'
import { assertNever } from '@deepseek-ai/dsh-llm'
import { DEFAULT_DSH_HOME_DISPLAY, defaultDshHome } from '@deepseek-ai/dsh-paths'
import { dshHomeDisplay } from '@deepseek-ai/dsh-paths'
import { resolveConfig, resolveDiscoveryConfig, type ResolvedConfig } from './config.ts'
import { renderWorkspaceContext, type RenderedWorkspaceContext } from './render.ts'
@@ -469,5 +469,5 @@ export async function readScopeInstruction(
}
function userGlobalDisplayPath(dshHome: string): string {
return dshHome === resolve(defaultDshHome()) ? `${DEFAULT_DSH_HOME_DISPLAY}/AGENTS.md` : '$DSH_HOME/AGENTS.md'
return `${dshHomeDisplay(dshHome)}/AGENTS.md`
}

View File

@@ -55,7 +55,7 @@ import type { Config } from '@deepseek-ai/dsh-agent-spine-demo'
// workspaceContext requires { maxBytes } or false; the other owner schemas supply defaults.
```
The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — TUI and headless apps pre-create `main`, while the ACP app creates agents on demand at `session/new`; `llmRetry` to the bounded retry policy; `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); `invariants` to the invariant service; and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. A `goals` object opts into the persisted domain, model tools, and same-session driver while forwarding `goals.domain` and `goals.tool` to their owners; omission or `false` leaves the stack absent so headless callers retain one-turn settlement. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, and set `toolTasks: false` to retain the task service for foreground producers without exposing `task_output` / `task_list` / `task_kill`. 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 enabled local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bash producer; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields.
The bundle FORWARDS each field to the child that owns it: `agents` and `maxParallelToolCalls` to `agent-loop` (`agents` defaults to `[]`; the cap defaults there), so each app supplies its own pre-created agents — TUI and headless apps pre-create `main`, while the ACP app creates agents on demand at `session/new`; `llmRetry` to the bounded retry policy; `persona` and `toolOrder` to `dsh-system-prompt`; `tools` to the tool registry for its presentation mode; `skills.registry`, `skills.local`, and `skills.tool` to the skill registry, local provider, and model-facing consumer; the required `workspaceContext` choice to `dsh-workspace-context` (`{ maxBytes }` enables loading and `false` disables it); `invariants` to the invariant service; and `toolBash`/`toolTasks` to the two model-facing tool plugins the bundle owns. A `goals` object opts into the persisted domain, model tools, and same-session driver while forwarding `goals.domain` and `goals.tool` to their owners; omission or `false` leaves the stack absent so headless callers retain one-turn settlement. Set `skills.enabled: false` to omit both the local provider and model-facing skill tool, and set `toolTasks: false` to retain the task service for foreground producers without exposing `task_output` / `task_list` / `task_kill`. It resolves `dshHome` once through [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) and forwards that absolute value to tool-bash's managed environment and enabled local skill discovery. An absent top-level `dshHome` adopts `skills.local.dshHome`; supplying both with different resolved paths fails loudly. `toolBash.enableRunInBackground` controls only the bash producer; independently loaded producers keep their own config. Workspace instructions register before the skill catalog so their session-prefix message renders first. App packages use `pickSpineConfig()` to copy only these bundle-owned fields.
For example, `{ invariants: { enabled: true, package_allowlist: ['^@deepseek-ai/dsh-'], package_blocklist: ['agent-loop$'] } }` keeps the package-owned companions mounted but suppresses the blocked owner. Blocklist matches override allowlist matches; see [`dsh-invariants`](../../support/invariants/README.md) for regex and lifecycle rules.

View File

@@ -32,9 +32,9 @@
"@deepseek-ai/dsh-agent-loop": "^0.0.1",
"@deepseek-ai/dsh-goal": "^0.0.1",
"@deepseek-ai/dsh-goal-session": "^0.0.1",
"@deepseek-ai/dsh-home": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-llm": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"@deepseek-ai/dsh-llm-retry": "^0.0.1",
"@deepseek-ai/dsh-scope": "^0.0.1",
"@deepseek-ai/dsh-session": "^0.0.1",
@@ -57,9 +57,9 @@
"@deepseek-ai/dsh-goal": "workspace:^",
"@deepseek-ai/dsh-goal-session": "workspace:^",
"@deepseek-ai/dsh-fs-local": "workspace:^",
"@deepseek-ai/dsh-home": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-llm": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-llm-retry": "workspace:^",
"@deepseek-ai/dsh-scope": "workspace:^",
"@deepseek-ai/dsh-session": "workspace:^",

View File

@@ -33,7 +33,7 @@ import * as toolSkill from '@deepseek-ai/dsh-tool-skill'
import * as toolTasks from '@deepseek-ai/dsh-tool-tasks'
import AgentLoop, { type Config as AgentLoopConfig } from '@deepseek-ai/dsh-agent-loop'
import * as llmRetry from '@deepseek-ai/dsh-llm-retry'
import { resolveDshHome } from '@deepseek-ai/dsh-home'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
export const name = 'agent-spine-demo'

View File

@@ -63,7 +63,7 @@
"path": "../../support/invariants"
},
{
"path": "../../util/home"
"path": "../../util/paths"
},
{
"path": "../../bash/tool-bash"

View File

@@ -7,7 +7,7 @@ Launcher-side telemetry primitives for the dsh-sdk toolchain. This is a plain li
| `SecretRedactor` | Conservative safety backstop: replaces secret-shaped values (secret-like keys, known token shapes, PEM blocks, URL credentials, high-entropy opaque tokens) with a placeholder in both parsed values (`redactValue`) and raw text (`redactText`). Never drops a field or line. |
| `ConsentResolver` | Parses (never boots) a project `cordis.yml` and reads the telemetry entry's enabled/disabled state as consent; `DO_NOT_TRACK`/CI env force a hard opt-out. |
| `buildTelemetryPayload` | Assembles `{command, durationMs, success, cordisYmlContent, packageJsonContent}`, running the redactor over the full `cordis.yml` and `package.json` text. Never reads `.env`; `package.json` ships only alongside a `cordis.yml`, so a command run in a non-SDK directory never uploads that directory's unrelated manifest. |
| `getOrCreateAnonymousId` | Random UUID persisted in a per-user GLOBAL config file (never in the project, never derived from git). |
| `getOrCreateAnonymousId` | Random UUID persisted in the harness home resolved by [`@deepseek-ai/dsh-paths`](../../util/paths/README.md) (`$DSH_HOME` > `~/.dsh`), scoped to that home rather than the machine, never derived from git. |
| `TelemetryReporter` | Fire-and-forget send: `report()` never blocks or throws; delivery resolves on every path; `flush()` optionally drains in-flight sends within a cap. |
Consent is carried by the telemetry entry in `cordis.yml`, so disabling telemetry is disabling that entry. Telemetry reports by default and is off only when a present telemetry entry is explicitly `disabled`: a missing `cordis.yml` (first `create`), an enabled entry, or a `cordis.yml` with no telemetry entry all report. `DO_NOT_TRACK`/CI always deny. The no-config and absent-entry defaults are configurable on `ConsentResolver`.

View File

@@ -32,11 +32,13 @@
"peerDependencies": {
"@deepseek-ai/dsh-brand": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
"devDependencies": {
"@deepseek-ai/dsh-brand": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"cordis": "^4.0.0-rc.7"
}
}

View File

@@ -1,63 +1,50 @@
/**
* Per-machine anonymous telemetry id.
* Per-harness-home anonymous telemetry id.
*
* The id is a random UUID persisted in a per-user GLOBAL config file — never in
* the project, and never derived from the git remote, repository URL, or any
* other identifying source (a derived id would make "anonymous" a fiction). The
* same id is reused across projects on one machine so telemetry counts machines,
* not repositories.
* The id is a random UUID persisted directly in the harness home resolved by
* {@link resolveDshHome} (`$DSH_HOME` > `~/.dsh`), and never derived from the
* git remote, repository URL, or any other identifying source (a derived id
* would make "anonymous" a fiction). The id is scoped to the harness home, not
* the machine: every command sharing one `$DSH_HOME` reuses the same id, so the
* default `~/.dsh` counts per-OS-user home directories, while a relocated
* `$DSH_HOME` moves the id with the rest of the harness data — the single-root
* convention this package shares, not a telemetry-specific policy.
*
* @module @deepseek-ai/dsh-telemetry/anonymous-id
*/
import { randomUUID } from 'node:crypto'
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import { homedir } from 'node:os'
import { dirname, join } from 'node:path'
import type { Branded } from '@deepseek-ai/dsh-brand'
import { resolveDshHome } from '@deepseek-ai/dsh-paths'
/** A machine-scoped anonymous telemetry id (random UUID v4). */
/** A harness-home-scoped anonymous telemetry id (random UUID v4). */
export type AnonymousId = Branded<'AnonymousId'>
/** Config directory name owned by the DeepSeek Harness across tools. */
const CONFIG_NAMESPACE = 'deepseek-harness'
/** Default file, inside the global config dir, storing the anonymous id. */
/** Default file, inside the harness home, storing the anonymous id. */
export const ANONYMOUS_ID_FILE_NAME = 'telemetry.json'
const UUID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
/** Ambient seams for locating and generating the id; every field has a default. */
export interface AnonymousIdOptions {
/** Environment consulted for `DSH_CONFIG_HOME`/`XDG_CONFIG_HOME`/`APPDATA`; defaults to `process.env`. */
/** Environment consulted for `DSH_HOME`; defaults to `process.env`. */
env?: NodeJS.ProcessEnv
/** Platform string used to pick the Windows path; defaults to `process.platform`. */
platform?: NodeJS.Platform
/** Home directory resolver; defaults to `os.homedir`. */
homeDir?: () => string
/** UUID generator; defaults to `crypto.randomUUID` (test seam). */
randomUUID?: () => string
}
/**
* Resolve the per-user global config directory for harness tooling.
* Precedence: `DSH_CONFIG_HOME` (explicit override) > `XDG_CONFIG_HOME` >
* platform default (`%APPDATA%` on Windows, else `~/.config`).
* @param options - environment, platform, and home-directory seams.
* @returns absolute config directory path for the harness namespace.
* Resolve the single-root harness home that stores the anonymous id.
* Delegates to {@link resolveDshHome} so telemetry shares the harness's one
* home-resolution policy (`DSH_HOME` > `~/.dsh`) instead of maintaining a
* second config-directory convention.
* @param options - environment seam.
* @returns absolute harness home path.
*/
export function globalConfigDir(options: AnonymousIdOptions = {}): string {
const env = options.env ?? process.env
const platform = options.platform ?? process.platform
const home = options.homeDir ?? homedir
if (env.DSH_CONFIG_HOME !== undefined && env.DSH_CONFIG_HOME.length > 0) return env.DSH_CONFIG_HOME
if (env.XDG_CONFIG_HOME !== undefined && env.XDG_CONFIG_HOME.length > 0) {
return join(env.XDG_CONFIG_HOME, CONFIG_NAMESPACE)
}
if (platform === 'win32' && env.APPDATA !== undefined && env.APPDATA.length > 0) {
return join(env.APPDATA, CONFIG_NAMESPACE)
}
return join(home(), '.config', CONFIG_NAMESPACE)
return resolveDshHome(undefined, options.env ?? process.env)
}
/** Read a valid persisted id from the store, or `undefined` when absent/corrupt. */
@@ -84,11 +71,11 @@ async function readPersistedId(file: string): Promise<AnonymousId | undefined> {
}
/**
* Return the machine's anonymous id, creating and persisting one on first use.
* Return the harness home's anonymous id, creating and persisting one on first use.
* Persistence is best-effort: a write failure still returns a usable id for the
* current run so telemetry is never blocked by config-dir permissions.
* @param options - config-location and UUID-generation seams.
* @returns the stable per-machine anonymous id.
* @returns the stable per-harness-home anonymous id.
*/
export async function getOrCreateAnonymousId(options: AnonymousIdOptions = {}): Promise<AnonymousId> {
const file = join(globalConfigDir(options), ANONYMOUS_ID_FILE_NAME)

View File

@@ -1,6 +1,7 @@
import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { isAbsolute, join, resolve } from 'node:path'
import { defaultDshHome } from '@deepseek-ai/dsh-paths'
import { afterEach, describe, expect, it } from 'vitest'
import {
ANONYMOUS_ID_FILE_NAME,
@@ -23,37 +24,26 @@ afterEach(async () => {
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i
describe('globalConfigDir', () => {
it('prefers an explicit DSH_CONFIG_HOME override', () => {
expect(globalConfigDir({ env: { DSH_CONFIG_HOME: '/custom/dsh' } })).toBe('/custom/dsh')
it('prefers an explicit DSH_HOME override', () => {
expect(globalConfigDir({ env: { DSH_HOME: '/custom/dsh' } })).toBe('/custom/dsh')
})
it('falls back to XDG_CONFIG_HOME under the harness namespace', () => {
expect(globalConfigDir({ env: { XDG_CONFIG_HOME: '/xdg' } })).toBe(join('/xdg', 'deepseek-harness'))
})
it('uses %APPDATA% on Windows', () => {
expect(globalConfigDir({ env: { APPDATA: 'C:/Users/x/AppData/Roaming' }, platform: 'win32' }))
.toBe(join('C:/Users/x/AppData/Roaming', 'deepseek-harness'))
})
it('falls back to ~/.config on Windows without APPDATA and on posix', () => {
const home = () => '/home/dev'
expect(globalConfigDir({ env: {}, platform: 'win32', homeDir: home }))
.toBe(join('/home/dev', '.config', 'deepseek-harness'))
expect(globalConfigDir({ env: {}, platform: 'linux', homeDir: home }))
.toBe(join('/home/dev', '.config', 'deepseek-harness'))
it('falls back to ~/.dsh when DSH_HOME is unset', () => {
expect(globalConfigDir({ env: {} })).toBe(resolve(defaultDshHome()))
})
it('reads process.env by default', () => {
// No override supplied: the call must not throw and must return an absolute path.
expect(globalConfigDir()).toContain('deepseek-harness')
// The ambient DSH_HOME is unknown here, so assert only the invariant the
// resolver guarantees rather than a specific location.
expect(isAbsolute(globalConfigDir())).toBe(true)
})
})
describe('getOrCreateAnonymousId', () => {
it('creates, persists, and returns a UUID on first use', async () => {
const dir = await tempDir()
const id = await getOrCreateAnonymousId({ env: { DSH_CONFIG_HOME: dir } })
const id = await getOrCreateAnonymousId({ env: { DSH_HOME: dir } })
expect(id).toMatch(UUID)
const stored: unknown = JSON.parse(await readFile(join(dir, ANONYMOUS_ID_FILE_NAME), 'utf8'))
expect(stored).toEqual({ anonymousId: id })
@@ -61,15 +51,15 @@ describe('getOrCreateAnonymousId', () => {
it('returns the same persisted id on subsequent calls', async () => {
const dir = await tempDir()
const first = await getOrCreateAnonymousId({ env: { DSH_CONFIG_HOME: dir } })
const second = await getOrCreateAnonymousId({ env: { DSH_CONFIG_HOME: dir } })
const first = await getOrCreateAnonymousId({ env: { DSH_HOME: dir } })
const second = await getOrCreateAnonymousId({ env: { DSH_HOME: dir } })
expect(second).toBe(first)
})
it('uses the injected UUID generator', async () => {
const dir = await tempDir()
const id = await getOrCreateAnonymousId({
env: { DSH_CONFIG_HOME: dir },
env: { DSH_HOME: dir },
randomUUID: () => '00000000-0000-4000-8000-000000000000',
})
expect(id).toBe('00000000-0000-4000-8000-000000000000')
@@ -78,23 +68,23 @@ describe('getOrCreateAnonymousId', () => {
it('regenerates when the stored file is corrupt JSON', async () => {
const dir = await tempDir()
await writeFile(join(dir, ANONYMOUS_ID_FILE_NAME), 'not json', 'utf8')
const id = await getOrCreateAnonymousId({ env: { DSH_CONFIG_HOME: dir } })
const id = await getOrCreateAnonymousId({ env: { DSH_HOME: dir } })
expect(id).toMatch(UUID)
})
it('regenerates when the stored value is not a valid UUID or object', async () => {
const dir = await tempDir()
await writeFile(join(dir, ANONYMOUS_ID_FILE_NAME), JSON.stringify({ anonymousId: 'nope' }), 'utf8')
expect(await getOrCreateAnonymousId({ env: { DSH_CONFIG_HOME: dir } })).toMatch(UUID)
expect(await getOrCreateAnonymousId({ env: { DSH_HOME: dir } })).toMatch(UUID)
await writeFile(join(dir, ANONYMOUS_ID_FILE_NAME), '123', 'utf8')
expect(await getOrCreateAnonymousId({ env: { DSH_CONFIG_HOME: dir } })).toMatch(UUID)
expect(await getOrCreateAnonymousId({ env: { DSH_HOME: dir } })).toMatch(UUID)
})
it('returns a usable id even when persistence fails', async () => {
const dir = await tempDir()
// A regular file where a directory is expected makes mkdir/writeFile fail.
await writeFile(join(dir, 'blocker'), 'x', 'utf8')
const id = await getOrCreateAnonymousId({ env: { DSH_CONFIG_HOME: join(dir, 'blocker') } })
const id = await getOrCreateAnonymousId({ env: { DSH_HOME: join(dir, 'blocker') } })
expect(id).toMatch(UUID)
})
})

View File

@@ -9,6 +9,7 @@
],
"references": [
{ "path": "../../util/brand" },
{ "path": "../../util/paths" },
{ "path": "../../support/invariants" }
]
}

View File

@@ -12,7 +12,7 @@ Requires `ctx.skills` (`inject: ['skills']`).
| Field | Default | Meaning |
|---|---|---|
| `dshHome` | `$DSH_HOME` or `~/.dsh` | DeepSeek Harness config root resolved by [`@deepseek-ai/dsh-home`](../../util/home/README.md); scans `skills` under this directory. |
| `dshHome` | `$DSH_HOME` or `~/.dsh` | DeepSeek Harness config root resolved by [`@deepseek-ai/dsh-paths`](../../util/paths/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. |

View File

@@ -28,8 +28,8 @@
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-fs": "^0.0.1",
"@deepseek-ai/dsh-home": "^0.0.1",
"@deepseek-ai/dsh-invariants": "^0.0.1",
"@deepseek-ai/dsh-paths": "^0.0.1",
"@deepseek-ai/dsh-skill": "^0.0.1",
"cordis": "^4.0.0-rc.7"
},
@@ -39,8 +39,8 @@
},
"devDependencies": {
"@deepseek-ai/dsh-fs": "workspace:^",
"@deepseek-ai/dsh-home": "workspace:^",
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/dsh-paths": "workspace:^",
"@deepseek-ai/dsh-skill": "workspace:^",
"cordis": "^4.0.0-rc.7"
}

View File

@@ -17,7 +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 { resolveDshHome } from '@deepseek-ai/dsh-paths'
import {
isSkillName,
type SkillCandidate,

View File

@@ -6,26 +6,12 @@
},
"include": ["src"],
"references": [
{
"path": "../../../vendor/cosmokit"
},
{
"path": "../../../vendor/cordis"
},
{
"path": "../../../vendor/schemastery"
},
{
"path": "../../util/home"
},
{
"path": "../../fs/fs"
},
{
"path": "../skill"
},
{
"path": "../../support/invariants"
}
{ "path": "../../../vendor/cosmokit" },
{ "path": "../../../vendor/cordis" },
{ "path": "../../../vendor/schemastery" },
{ "path": "../../fs/fs" },
{ "path": "../../util/paths" },
{ "path": "../skill" },
{ "path": "../../support/invariants" }
]
}

View File

@@ -5,14 +5,13 @@ Zero-dependency primitives shared across the other groups. A package lands here
| Package | Role |
|---|---|
| `brand/` | The type-only `Branded<B>` nominal-typing primitive (no runtime code, no harness deps) |
| `home/` | Canonical `DSH_HOME` resolution from explicit config, environment, or `~/.dsh` (no harness deps) |
| `paths/` | Shared filesystem path constants and helpers for harness user data |
| `paths/` | Canonical single-root `DSH_HOME` resolution plus shared filesystem path constants and helpers for harness user data (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 |
| `retention/` | Bounded model-facing output — `ItemRetainer`/`TextRetainer` + neutral notice helpers (pure, no harness deps); business semantics stay in each tool |
`dsh-brand` is the canonical case: it owns ONLY the `Branded<B>` helper, so a capability package can brand the ids it owns (`dsh-tasks`'s `TaskId`, `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-paths` gives every package the same configurable Harness home without assigning that cross-cutting fact to bash, skills, telemetry, 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. The harness keeps all user data under one root.
`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 Agent Note](../../.agents/notes/implemented/architecture/2026-07-06-timeout-deadline-library.md)).

View File

@@ -1,21 +0,0 @@
# @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.
## Model Experience
Indirectly, through `dsh-tool-bash`, which exposes the resolved path to model bash as `DSH_HOME` without adding a prompt section.
#### KV Cache effect
No direct invalidation; the named consumer owns any request-prefix changes.
## Known Limitations and Deferred Work
- **Resolution only** — the resolver makes a path absolute but does not create it, check access, or canonicalize symlinks; each consumer owns those filesystem decisions.

View File

@@ -1,37 +0,0 @@
{
"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"
},
"./invariant": {
"types": "./lib/types/invariant.d.ts",
"default": "./lib/invariant.js"
},
"./src/*": "./src/*",
"./package.json": "./package.json"
},
"files": [
"lib/index.js",
"lib/invariant.js",
"lib/types/**/*.d.ts",
"lib/types/**/*.d.ts.map",
"src"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "^0.0.1",
"cordis": "^4.0.0-rc.6"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"cordis": "^4.0.0-rc.6"
}
}

View File

@@ -1,23 +0,0 @@
/**
* 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))
}

View File

@@ -1,30 +0,0 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-home`.
* @module @deepseek-ai/dsh-home/invariant
*/
/* jscpd:ignore-start */
import type { Context } from 'cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-home'
/** Cordis companion plugin name. */
export const name = 'home-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: this pure utility owns no event stream or mutable runtime data; its value
* algebra is enforced by unit tests.
*/
const install: InvariantInstaller = () => {}
/**
* Register this package's invariant companion.
* @param ctx - Cordis context carrying the invariant service.
* @returns the installed registration's disposer after setup succeeds.
*/
export const apply = (ctx: Context): Promise<() => void> =>
Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install))
/* jscpd:ignore-end */

View File

@@ -1,26 +0,0 @@
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'))
})
})

View File

@@ -1,13 +0,0 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"rootDir": "src",
"outDir": "lib/types"
},
"include": ["src"],
"references": [
{
"path": "../../support/invariants"
}
]
}

View File

@@ -4,6 +4,10 @@ Shared filesystem path helpers for DeepSeek Harness user data.
## DSH home
`resolveDshHome()` resolves the single-root DeepSeek Harness home. Precedence, highest first: an explicit configured path, `$DSH_HOME`, then `~/.dsh`. The harness keeps all user data under one root.
`dshHomeDisplay()` names an active root symbolically for user-facing paths: `~/.dsh` for the default home, `$DSH_HOME` for any configured home. It never leaks an absolute machine path.
`DSH_HOME_DIR_NAME` owns the default user-data directory name: `.dsh`.
`defaultDshHome()` returns the default DeepSeek Harness home by joining the operating-system home directory with `.dsh`, using Node's platform path rules.

View File

@@ -36,12 +36,30 @@ export function expandHomePath(path: string): string {
}
/**
* Resolve an explicitly configured, environment-selected, or default DSH home.
* Resolve the single-root DeepSeek Harness home.
*
* Precedence, highest first: an explicit configured path, `$DSH_HOME`, then
* `~/.dsh`. The harness keeps all user data under one root. An empty or
* whitespace-only `$DSH_HOME` is treated as unset, so a blank override never
* resolves the home to the current working directory.
* @param configured - explicit harness-home override, which has highest precedence.
* @param env - environment mapping used to read `DSH_HOME`.
* @returns the normalized absolute harness home path.
*/
export function resolveDshHome(configured?: string, env: Record<string, string | undefined> = process.env): string {
const selected = configured ?? env[DSH_HOME_ENV] ?? defaultDshHome()
const fromEnv = env[DSH_HOME_ENV]
const selected = configured ?? (fromEnv !== undefined && fromEnv.trim().length > 0 ? fromEnv : defaultDshHome())
return resolve(expandHomePath(selected))
}
/**
* Describe a resolved harness home symbolically for user-facing display.
*
* It never returns an absolute machine path: the default home is labelled
* `~/.dsh`, and any configured home is labelled `$DSH_HOME`.
* @param resolvedHome - the absolute path returned by {@link resolveDshHome}.
* @returns `~/.dsh` for the default home, otherwise `$DSH_HOME`.
*/
export function dshHomeDisplay(resolvedHome: string): string {
return resolvedHome === resolve(defaultDshHome()) ? DEFAULT_DSH_HOME_DISPLAY : `$${DSH_HOME_ENV}`
}

View File

@@ -1,10 +1,11 @@
import { homedir } from 'node:os'
import { join } from 'node:path'
import { join, resolve } from 'node:path'
import { describe, expect, it } from 'vitest'
import {
DEFAULT_DSH_HOME_DISPLAY,
DSH_HOME_DIR_NAME,
defaultDshHome,
dshHomeDisplay,
expandHomePath,
resolveDshHome,
} from '@deepseek-ai/dsh-paths'
@@ -24,11 +25,21 @@ describe('dsh path helpers', () => {
expect(expandHomePath('~other/.dsh')).toBe('~other/.dsh')
})
it('resolves explicit DSH home before environment and default locations', () => {
it('resolves explicit path before DSH_HOME and the default', () => {
const envHome = join(homedir(), 'env-dsh')
expect(resolveDshHome(undefined, { DSH_HOME: '~/env-dsh' })).toBe(envHome)
expect(resolveDshHome('/tmp/explicit-dsh', { DSH_HOME: '~/env-dsh' })).toBe('/tmp/explicit-dsh')
expect(resolveDshHome(undefined, { DSH_HOME: '~/env-dsh' })).toBe(envHome)
expect(resolveDshHome(undefined, {})).toBe(defaultDshHome())
})
it('treats an empty or whitespace-only DSH_HOME as unset', () => {
expect(resolveDshHome(undefined, { DSH_HOME: '' })).toBe(defaultDshHome())
expect(resolveDshHome(undefined, { DSH_HOME: ' ' })).toBe(defaultDshHome())
})
it('labels a resolved home by whether it is the default root', () => {
expect(dshHomeDisplay(resolve(defaultDshHome()))).toBe('~/.dsh')
expect(dshHomeDisplay('/some/other/root')).toBe('$DSH_HOME')
})
})