diff --git a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml index 4288bcae58..38d2409b9d 100644 --- a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml +++ b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.i18n.yaml @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md index 45ac032db0..e06dbc85f2 100644 --- a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md +++ b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.md @@ -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 diff --git a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md index e835325b0d..6c6a128f12 100644 --- a/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md +++ b/.agents/notes/implemented/architecture/2026-08-04-configuration-source-ownership.zh.md @@ -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 diff --git a/scripts/verify-config-source-ownership.ts b/scripts/verify-config-source-ownership.ts index 1abf269b83..ffc849bba3 100644 --- a/scripts/verify-config-source-ownership.ts +++ b/scripts/verify-config-source-ownership.ts @@ -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> = { - // 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', )