cleanup(config): remove textual process env audit

The gate treated a literal process.env substring search as repository-wide source-ownership enforcement. It missed equivalent syntax while matching comments and strings, so the allowlist projected a security guarantee the implementation could not provide.

Remove the scanner and its allowlist. Keep the independently useful shipped-config inline tripwire, and narrow both the module contract and bilingual Agent Note to its actual source-shape claim.
This commit is contained in:
Tianyi Cui
2026-08-07 21:05:04 +08:00
parent dfaf3c288e
commit 55fca161e6
4 changed files with 10 additions and 86 deletions
@@ -2,5 +2,5 @@
# 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 .agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md
2026-08-04-configuration-source-ownership.md: 45ac032db0b60e0c8ce5a8c96ad2cf9cd847e14a
2026-08-04-configuration-source-ownership.zh.md: e835325b0d87410e6513f08cf0777a1713deb8cd
2026-08-04-configuration-source-ownership.md: e06dbc85f2307fa8a50fba13000f42306d69d9bf
2026-08-04-configuration-source-ownership.zh.md: 6c6a128f1279a271f583e0bf4bcd27d0e5b81162
@@ -48,7 +48,7 @@ The line is that these take effect with no user action, before any turn, outside
**`packages/util/environment` owns the snapshot**, deliberately as a utility rather than a three-package capability seam. The snapshot is frozen before Cordis starts and injected once by the launcher, so there is no runtime implementation to swap; consumers need types and pure functions, which a `util/` package gives them without depending on a UI package. `environmentOf(ctx)` returns the launcher's snapshot, or the inherited environment as the only layer — an SDK host or bare `cordis.yml` discovered no files, so its single layer really is what it was launched with, and the same trusted lookups keep working there unchanged.
**`verify-config-source-ownership`** keeps both rules: no unregistered `process.env` read under `packages/*/*/src` (26 allowlisted, each with the reason it is a process fact), and no `apiKey`/`baseURL`/`headers` inlined from the environment in shipped Cordis configuration. Removing those inlines is what makes the deployment tier meaningful — with the shipped tree silent on `baseURL`, a present value means a human or deployment set it.
**`verify-config-source-ownership`** is a narrow tripwire for the ordinary single-line form of an `apiKey`/`baseURL`/`headers` environment inline in shipped Cordis configuration. Removing those inlines is what makes the deployment tier meaningful — with the shipped tree silent on `baseURL`, a present value means a human or deployment set it. Adapters own actual resolution; the gate makes no repository-wide claim about `process.env` access.
## Consequences
@@ -50,7 +50,7 @@ inherited process environment (read-only, wins)
**`packages/util/environment` 拥有该快照**,刻意做成 utility 而不是三包能力 seam。快照在 Cordis 启动前就冻结,并由启动器一次性注入,因此不存在需要切换的运行时实现;消费方需要的只是类型和纯函数,而 `util/` 包能提供这些且不必依赖 UI 包。`environmentOf(ctx)` 返回启动器的快照,或者返回只含继承环境的那一层——SDK 宿主或裸 `cordis.yml` 从未发现过任何文件,它那唯一一层确实就是它被启动时的环境,因此同样的受信查询在那里原样继续工作。
**`verify-config-source-ownership`** 守住这两条规则:`packages/*/*/src` 下没有未登记的 `process.env` 读取(26 处在 allowlist 中,各自写明它为何是进程事实),以及已交付 Cordis 配置中不得从环境内联 `apiKey`/`baseURL`/`headers`。删除这些内联正是「部署层」得以成立的原因——已交付配置树对 `baseURL` 保持沉默之后,「有值」就意味着「人或部署设过它」。
**`verify-config-source-ownership`** 仅作为一道窄门禁,检查已交付 Cordis 配置中从环境内联 `apiKey`/`baseURL`/`headers` 的普通单行写法。删除这些内联正是「部署层」得以成立的原因——已交付配置树对 `baseURL` 保持沉默之后,「有值」就意味着「人或部署设过它」。实际解析由适配器负责;该门禁不声称覆盖仓库范围内的 `process.env` 访问。
## Consequences
+6 -82
View File
@@ -1,20 +1,8 @@
/**
* Gate: every user-facing value has one owner, and no shipped file smuggles a
* second one in.
*
* Two rules, both about the same failure — a value reaching the harness
* through a path nobody ranked:
*
* 1. Production package source does not read `process.env` directly. A
* credential belongs to `ctx.credentials`, a user-configurable value to the
* environment snapshot plus its owner's resolve step, and a real
* process-launch fact to the app bootstrap. Each remaining read is listed
* below with the reason it is one of those.
* 2. Shipped Cordis configuration does not inline a credential or an endpoint
* from the environment. Doing so re-creates the layer the snapshot exists
* to rank: `apiKey: !!js process.env.X` and `baseURL: !!js process.env.X`
* bypass both the credential seam and the endpoint ladder, and a project
* file could then decide where a key is sent.
* Gate: shipped Cordis configuration does not use the ordinary inline form
* for a credential or endpoint from the environment. This narrow source-shape
* lint prevents checked-in composition from bypassing the credential seam and
* endpoint ladder; adapters remain responsible for actual value resolution.
* @module scripts/verify-config-source-ownership
*/
@@ -23,58 +11,6 @@ import { resolve, sep } from 'node:path'
const ROOT = resolve(import.meta.dirname, '..')
/**
* Production package sources allowed to read `process.env`, each with the
* reason it is a process fact rather than a user-configurable value. Adding a
* row is a deliberate act: state which of the three owners it belongs to and
* why it cannot go there.
*/
const ENV_READ_ALLOWLIST: Readonly<Record<string, string>> = {
// The environment plane itself.
'packages/util/environment/src/index.ts': 'defines the snapshot; the inherited environment is its input',
'packages/ui/app-boot/src/index.ts': 'the app bootstrap that builds the snapshot and reads $DSH_SNAPSHOT',
'packages/util/paths/src/index.ts': 'resolves $DSH_HOME before any snapshot exists',
// Process-launch facts owned by the boundary that spawns or is spawned.
'packages/subprocess/subprocess/src/index.ts': 'scrubs the parent environment for children',
'packages/workflow/workflow-workerthread/src/host.ts': 'passes the parent environment to a worker thread',
'packages/ui/tui/src/index.ts': 'reads $COLORTERM, a terminal capability of this process',
'packages/lsp/lsp-local/src/index.ts': 'passes the parent environment to a language server it spawns',
'packages/cordis/repository-plugin/src/index.ts': 'resolves an MCP manifest against the spawning environment',
'packages/host/directory-picker-native/src/win32-dialog-host.ts': 'builds the child environment for the dialog worker it spawns',
'packages/host/directory-picker-native/src/win32-dialog-worker.ts': 'the spawned worker reads the title its parent passed on the env channel',
'packages/bash/pwsh-local/src/resolve.ts': 'locates pwsh through $ProgramFiles and $SystemRoot, Windows install layout rather than user configuration',
// Bootstrap-only DSH_* switches, which no discovered file may set.
'packages/skill/skill-local/src/index.ts': 'reads $DSH_AGENTS_HOME and $DSH_BUNDLED_SKILL_DIR, both bootstrap-only',
'packages/web/web/src/index.ts': 'reads $DSH_WEB_SEARCH_PROVIDER and $DSH_WEB_FETCH_PROVIDER, both bootstrap-only',
'packages/host/apiproxy/src/native-path-opener.ts': 'reads the WSL interop markers of this process to pick an opener',
'packages/host/directory-picker-auto/src/index.ts': 'reads launch facts (display, SSH) of this process',
'packages/host/directory-picker-auto/src/resolve.ts': 'reads launch facts (display, SSH) of this process',
// Telemetry identity and consent, resolved once per process at bootstrap.
'packages/telemetry/session-telemetry-otel/src/user-id.ts': 'derives a machine identity from process facts',
'packages/sdk/telemetry/src/consent-resolver.ts': 'reads the SDK bootstrap consent switch',
'packages/sdk/telemetry/src/anonymous-id.ts': 'derives a machine identity from process facts',
// SDK and example bins: their own app bootstrap, outside the product CLI.
'packages/sdk/sdk-client/src/client.ts': 'SDK host bootstrap',
'packages/sdk/helper/src/features/builtin/provider.ts': 'SDK scaffolding reads the developer environment',
'packages/sdk/helper/src/features/builtin/app.ts': 'SDK scaffolding reads the developer environment',
'packages/sdk/helper/src/package-managers/package-manager.ts': 'detects the invoking package manager',
'packages/sdk/create-sdk/src/create-wizard.ts': 'SDK scaffolding reads the developer environment',
'packages/examples/jsonrpc-demo/src/bin.ts': 'demo bin bootstrap',
'packages/examples/acp-demo/src/bin.ts': 'demo bin bootstrap',
// Test and replay infrastructure.
'packages/support/loader-smoke/src/index.ts': 'test launcher composing a child environment',
'packages/support/llm-replay/src/index.ts': 'replay fixture switch',
'packages/support/acp-snapshot/src/launcher.ts': 'snapshot launcher composing a child environment',
// Browser bundle: `process.env` is replaced at build time, never read at runtime.
'packages/client/runtime/src/client/contract/store.ts': 'build-time constant folded by the bundler',
}
/** Shipped Cordis configuration these rules apply to. */
const SHIPPED_CONFIG_GLOBS = [
'apps/*/config/*.yml',
@@ -95,17 +31,6 @@ const INLINE_DENY = /^\s*(apiKey|baseURL|apiKeyEnv|authToken|headers)\s*:\s*!!js
const failures: string[] = []
for (const file of globSync('packages/*/*/src/**/*.ts', { cwd: ROOT })) {
const rel = file.split(sep).join('/')
if (!readFileSync(resolve(ROOT, rel), 'utf8').includes('process.env')) continue
if (rel in ENV_READ_ALLOWLIST) continue
failures.push(
`${rel}: reads process.env directly. A credential belongs to ctx.credentials, a user-configurable`
+ ' value to environmentOf(ctx) plus its owner\'s resolve step, and a process-launch fact to the app'
+ ' bootstrap. If it is genuinely one of those, add it to ENV_READ_ALLOWLIST with the reason.',
)
}
for (const glob of SHIPPED_CONFIG_GLOBS) {
for (const file of globSync(glob, { cwd: ROOT })) {
const rel = file.split(sep).join('/')
@@ -126,8 +51,7 @@ if (failures.length > 0) {
process.exit(1)
}
const allowed = Object.keys(ENV_READ_ALLOWLIST).length
process.stdout.write(
`verify-config-source-ownership: no unregistered process.env reads (${String(allowed)} allowlisted)`
+ ' and no credential or endpoint inlined in shipped configuration.\n',
'verify-config-source-ownership: no credential or endpoint uses the ordinary inline environment form'
+ ' in shipped configuration.\n',
)