refactor: apply repository naming contract

Apply the accepted pre-release package, service, type, directory, and role renames as one repository-wide change.
This commit is contained in:
Tianyi Cui
2026-08-13 00:36:22 +08:00
parent 101df7cf58
commit a2d0f7f411
3281 changed files with 21730 additions and 21592 deletions

View File

@@ -0,0 +1,6 @@
# Bilingual-pair consistency record (docs/i18n/README.md): the git blob hash of each
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write packages/util/launch-environment/README.md
README.md: 80ec4d59c38ee296e836f8cff52e3a89b0700ff5
README.zh.md: 5dc9bfd1fc70e5432ca9f62e81a7d7a23f4a48c9

View File

@@ -0,0 +1,36 @@
# dsh-launch-environment
English | [中文](README.zh.md)
This run's environment as one immutable snapshot that remembers **which layer supplied each value**. Consumers resolve user-facing values against it instead of `process.env`, because the layers are not equally trusted and a flattened view cannot tell them apart.
| Layer | Source id | What it is |
|---|---|---|
| Inherited process environment | `process` | What the launching shell, CI job, or container passed in — this run's explicit intent |
| `<invocation cwd>/.env` | `project-env` | The project the harness was launched in, which the product trusts to configure its own agent |
| `$DSH_HOME/.env` | `user-env` | The user's own machine-level defaults |
Values do also reach `process.env` — a user's `--config` tree and third-party libraries read it — but that flattened view is not the authority for anything the harness resolves.
## Resolving
`get(name)` searches every layer, most trusted first. `getFrom(name, sources)` searches only the named layers without changing that trust order.
**Omitting a layer is a refusal, not a demotion** — a caller that must never accept a layer leaves it out of the list, so no future reordering can let it back in. The provider adapters name all three, because the product trusts the project it runs in; the mechanism exists for the decisions where that is not true.
Names match the way the platform matches them: exactly on POSIX, case-insensitively on Windows. A case-sensitive lookup there would rank the wrong layer — a shell's `deepseek_api_key` and a project `.env`'s `DEEPSEEK_API_KEY` are one variable to the OS, and treating them as two would let the project win.
```ts
import type { Context } from '@deepseek-ai/cordis'
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
declare const ctx: Context
const endpoint = launchEnvironmentOf(ctx).get('DEEPSEEK_BASE_URL')?.value
```
`launchEnvironmentOf(ctx)` returns the launcher's snapshot when the product CLI booted the tree, and otherwise the inherited environment as the only layer. That fallback does not weaken the rules: an SDK host or a bare `cordis.yml` discovered no files, so everything it has really is the environment it was launched with.
## Known Limitations and Deferred Work
- **The snapshot is not a subprocess boundary** — every layer is also materialized into `process.env`, so ordinary project variables reach child processes under [`dsh-subprocess`](../../subprocess/subprocess/README.md)'s scrub. The product launcher's [`.env` contract](../../boot/app-boot/README.md#profiles) rejects bootstrap variables before materialization.
- **No per-workspace layer** — the project layer is the *invoking* directory, fixed at launch. A workspace selected later in the Web UI contributes nothing, deliberately: following it would let a model's own workspace change the harness environment mid-session.

View File

@@ -0,0 +1,36 @@
# dsh-launch-environment
[English](README.md) | 中文
把本次运行的环境冻结为一份不可变快照,并记住**每个值来自哪一层**。消费方用它而不是 `process.env` 解析面向用户的值,因为各层的可信程度并不相同,而压平后的视图无法区分它们。
| 层 | 来源 id | 它是什么 |
|---|---|---|
| 继承的进程环境 | `process` | 启动 shell、CI 任务或容器传入的东西——本次运行的明确意图 |
| `<invocation cwd>/.env` | `project-env` | harness 被启动于其中的项目;产品信任它配置自己的 agent(智能体) |
| `$DSH_HOME/.env` | `user-env` | 用户自己的机器级默认值 |
这些值同样会进入 `process.env`——用户自己的 `--config` 树和第三方库要读它——但那份压平的视图不是 harness 解析任何值的依据。
## 解析
`get(name)` 按可信度从高到低搜索所有层。`getFrom(name, sources)` 只搜索指定的层,不改变这一可信顺序。
**省略某一层是拒绝,不是降级**——绝不能接受某一层的调用方直接不把它列进去,后续任何重新排序都无法让它回来。提供方适配器三层全列,因为产品信任它所运行的项目;该机制是为那些「并非如此」的决策准备的。
变量名按平台自身的规则匹配:POSIX 上精确匹配,Windows 上不区分大小写。在 Windows 上做大小写敏感的查找会选错层——shell 里的 `deepseek_api_key` 与项目 `.env` 里的 `DEEPSEEK_API_KEY` 对操作系统而言是同一个变量,把它们当成两个就会让项目胜出。
```ts
import type { Context } from '@deepseek-ai/cordis'
import { launchEnvironmentOf } from '@deepseek-ai/dsh-launch-environment'
declare const ctx: Context
const endpoint = launchEnvironmentOf(ctx).get('DEEPSEEK_BASE_URL')?.value
```
当产品 CLI(命令行界面)启动了这棵树时,`launchEnvironmentOf(ctx)` 返回启动器的快照;否则返回只含继承环境的那一层。该回退并不削弱规则:SDK 宿主或裸 `cordis.yml` 从未发现过任何文件,因此它拥有的一切确实就是它被启动时的环境。
## 已知限制与暂缓事项
- **快照不是子进程边界**:每一层同样会被物化进 `process.env`,因此项目里的普通变量会按 [`dsh-subprocess`](../../subprocess/subprocess/README.md) 的清洗规则抵达子进程。产品启动器的 [`.env` 约定](../../boot/app-boot/README.md#profiles) 会在物化之前拒绝 bootstrap 变量。
- **没有按工作区划分的层**:项目层是*调用*目录,在启动时固定。之后在 Web UI 中选择的工作区不贡献任何内容,这是刻意的:跟随它等于让模型自己的工作区在会话中途改变 harness 的环境。

View File

@@ -0,0 +1,42 @@
{
"name": "@deepseek-ai/dsh-launch-environment",
"description": "Immutable DeepSeek Harness launch environment that records which layer supplied each value",
"version": "0.0.1-rc.2",
"publishConfig": {
"access": "restricted"
},
"repository": {
"type": "git",
"url": "git+https://github.com/deepseek-ai/deepseek-harness.git",
"directory": "packages/util/launch-environment"
},
"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"
],
"license": "BSD-3-Clause",
"peerDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
},
"devDependencies": {
"@deepseek-ai/dsh-invariants": "workspace:^",
"@deepseek-ai/cordis": "workspace:^"
}
}

View File

@@ -0,0 +1,124 @@
/**
* Immutable launch-time environment snapshot that records which layer
* supplied each value. Harness consumers resolve through it instead of a flattened
* `process.env`; launchers may still materialize accepted values for config
* expressions and third-party libraries.
* @module @deepseek-ai/dsh-launch-environment
*/
import type { Context } from '@deepseek-ai/cordis'
/**
* Which layer supplied a value, from most to least trusted: the environment
* this process inherited, the invoking directory's `.env`, the Harness home's
* `.env`.
*/
export type LaunchEnvironmentSource = 'process' | 'project-env' | 'user-env'
/** Layer order, most trusted first. */
const SOURCE_ORDER: readonly LaunchEnvironmentSource[] = ['process', 'project-env', 'user-env']
/** One resolved variable and the layer it came from. */
export interface LaunchEnvironmentEntry {
/** The value as the layer supplied it; may be empty, which each owner judges for itself. */
value: string
/** The layer that supplied it. */
source: LaunchEnvironmentSource
/** Absolute path of the file that supplied it; absent for `process`. */
path?: string
}
/**
* The frozen environment of one launch. Construct through
* {@link createLaunchEnvironmentSnapshot}; nothing mutates it afterwards, so a
* later `chdir`, workspace switch, or resumed session observes the same
* values a consumer resolved at boot.
*/
export interface LaunchEnvironmentSnapshot {
/**
* Resolve one name across every layer, most trusted first.
* @param name - the variable name.
* @returns the winning entry, or `undefined` when no layer supplies it.
*/
get(name: string): LaunchEnvironmentEntry | undefined
/**
* Resolve one name only from `sources`, retaining canonical trust order;
* omitted layers are unreachable.
* @param name - the variable name.
* @param sources - the layers allowed in the canonical trust order.
* @returns the first matching entry, or `undefined`.
*/
getFrom(name: string, sources: readonly LaunchEnvironmentSource[]): LaunchEnvironmentEntry | undefined
}
/**
* The map key one variable name resolves under. Windows treats environment
* names case-insensitively; every other platform does not.
* @param name - the variable name as written.
* @returns the key to store and look up by.
*/
function lookupKey(name: string): string {
/* v8 ignore next -- native Windows coverage exercises the folding arm; POSIX covers the exact one */
return process.platform === 'win32' ? name.toUpperCase() : name
}
/** One layer's raw contents, as {@link createLaunchEnvironmentSnapshot} receives them. */
export interface LaunchEnvironmentLayerInput {
source: LaunchEnvironmentSource
/** Absolute path of the file behind this layer; omit for `process`. */
path?: string
values: Readonly<Record<string, string>>
}
/**
* Build the snapshot from each layer's contents.
* @param layers - the layers in any order; the result searches them by canonical trust order.
* @returns the immutable snapshot.
*/
export function createLaunchEnvironmentSnapshot(layers: readonly LaunchEnvironmentLayerInput[]): LaunchEnvironmentSnapshot {
// Copy every layer so later mutations cannot change the snapshot. Fold names
// on Windows so case variants cannot split precedence; POSIX remains exact.
const bySource = new Map<LaunchEnvironmentSource, { path?: string; values: Map<string, string> }>()
for (const layer of layers) {
bySource.set(layer.source, {
...layer.path === undefined ? {} : { path: layer.path },
values: new Map(Object.entries(layer.values).map(([name, value]) => [lookupKey(name), value])),
})
}
const getFrom = (name: string, sources: readonly LaunchEnvironmentSource[]): LaunchEnvironmentEntry | undefined => {
const key = lookupKey(name)
for (const source of SOURCE_ORDER) {
if (!sources.includes(source)) continue
const layer = bySource.get(source)
const value = layer?.values.get(key)
if (value === undefined) continue
return { value, source, ...layer?.path === undefined ? {} : { path: layer.path } }
}
return undefined
}
return {
get: name => getFrom(name, SOURCE_ORDER),
getFrom,
}
}
/** Context slot the launcher fills with this run's snapshot before any config entry mounts. */
export const DSH_LAUNCH_ENVIRONMENT_KEY = 'launchEnvironment'
/**
* Return the launcher's snapshot, or the inherited environment as the sole
* layer when the host provided none.
* @param ctx - the consuming plugin's context.
* @returns the snapshot to resolve user-facing values against.
*/
export function launchEnvironmentOf(ctx: Context): LaunchEnvironmentSnapshot {
return ctx.get(DSH_LAUNCH_ENVIRONMENT_KEY)
?? createLaunchEnvironmentSnapshot([{ source: 'process', values: process.env as Record<string, string> }])
}
declare module '@deepseek-ai/cordis' {
interface Context {
/** Launcher-owned snapshot of this run's environment; absent in compositions the product CLI did not boot. */
launchEnvironment?: LaunchEnvironmentSnapshot
}
}

View File

@@ -0,0 +1,30 @@
/**
* Package-owned invariant companion for `@deepseek-ai/dsh-launch-environment`.
* @module @deepseek-ai/dsh-launch-environment/invariant
*/
/* jscpd:ignore-start */
import type { Context } from '@deepseek-ai/cordis'
import type { InvariantInstaller } from '@deepseek-ai/dsh-invariants'
const PACKAGE_NAME = '@deepseek-ai/dsh-launch-environment'
/** Cordis companion plugin name. */
export const name = 'launch-environment-invariant'
/** Service required before the companion can reserve package ownership. */
export const inject = ['invariants']
/**
* No runtime invariant: the snapshot is frozen before any fiber starts and this package owns no
* event stream or mutable runtime data; its lookup and rejection rules are 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

@@ -0,0 +1,69 @@
import { describe, expect, it, vi } from 'vitest'
import { Context } from '@deepseek-ai/cordis'
import {
createLaunchEnvironmentSnapshot, DSH_LAUNCH_ENVIRONMENT_KEY, launchEnvironmentOf,
} from '../src/index.ts'
const layered = createLaunchEnvironmentSnapshot([
{ source: 'process', values: { SHARED: 'from-process', ONLY_PROCESS: 'p' } },
{ source: 'project-env', path: '/work/.env', values: { SHARED: 'from-project', ONLY_PROJECT: 'j' } },
{ source: 'user-env', path: '/home/.dsh/.env', values: { SHARED: 'from-user', ONLY_USER: 'u' } },
])
describe('createLaunchEnvironmentSnapshot', () => {
it('resolves across every layer, most trusted first, and reports the winning source', () => {
expect(layered.get('SHARED')).toEqual({ value: 'from-process', source: 'process' })
expect(layered.get('ONLY_PROJECT')).toEqual({ value: 'j', source: 'project-env', path: '/work/.env' })
expect(layered.get('ONLY_USER')).toEqual({ value: 'u', source: 'user-env', path: '/home/.dsh/.env' })
expect(layered.get('ABSENT')).toBeUndefined()
})
it('filters layers without changing their trust order', () => {
// The point of getFrom: a routing field that must never come from a
// project directory cannot be reached by reordering, only by listing it.
expect(layered.getFrom('ONLY_PROJECT', ['process', 'user-env'])).toBeUndefined()
expect(layered.getFrom('SHARED', ['user-env', 'process']))
.toEqual({ value: 'from-process', source: 'process' })
expect(layered.getFrom('SHARED', [])).toBeUndefined()
})
it('copies each layer, so a later mutation of the source object cannot change it', () => {
const values: Record<string, string> = { KEY: 'first' }
const snapshot = createLaunchEnvironmentSnapshot([{ source: 'process', values }])
values.KEY = 'second'
values.LATE = 'added'
expect(snapshot.get('KEY')).toEqual({ value: 'first', source: 'process' })
expect(snapshot.get('LATE')).toBeUndefined()
})
it('keeps an empty value as a present value, for its owner to judge', () => {
const snapshot = createLaunchEnvironmentSnapshot([{ source: 'process', values: { EMPTY: '' } }])
expect(snapshot.get('EMPTY')).toEqual({ value: '', source: 'process' })
})
it('orders lookups canonically regardless of construction order', () => {
const reversed = createLaunchEnvironmentSnapshot([
{ source: 'user-env', path: '/u', values: { K: 'u' } },
{ source: 'process', values: { K: 'p' } },
])
expect(reversed.get('K')).toEqual({ value: 'p', source: 'process' })
})
})
describe('launchEnvironmentOf', () => {
it('returns the launcher snapshot when the product CLI provided one', () => {
const ctx = new Context()
ctx.provide(DSH_LAUNCH_ENVIRONMENT_KEY, layered)
expect(launchEnvironmentOf(ctx)).toBe(layered)
})
it('falls back to the inherited environment as the only layer', () => {
vi.stubEnv('DSH_ENV_SPEC_FALLBACK', 'ambient')
try {
const snapshot = launchEnvironmentOf(new Context())
expect(snapshot.get('DSH_ENV_SPEC_FALLBACK')).toEqual({ value: 'ambient', source: 'process' })
} finally {
vi.unstubAllEnvs()
}
})
})

View File

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