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

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',
)