fix(config): trust the invoking project, and stop leaking what it must not decide
Review found five real defects in the configuration-source work, all confirmed against the code rather than argued: 1. The note claimed --config outranks settings.yaml. It does not: the settings seam registers a plugin's cordis entry config as the `base` layer and the user section layers over it, and the seam cannot tell a shipped value from a --config one. The note now states shipped reality and names --config-replace as the lever for a deployment that must win. Separately, a literal `apiKey` in settings outranked both the environment and .credentials.yaml — the field is removed, so configuration carries a reference and nothing else. 2. DEEPSEEK_SEARCH_BASE_URL was functionally deleted: the shipped inline went away without the provider learning to read it. It now resolves from the environment snapshot, as the README always claimed. 3. The bootstrap deny list missed the interpreter start-up hooks. BASH_ENV is the sharpest: `bash -c` sources it on every bash tool call, so a project .env could run a file of its choosing before every command. The list now covers BASH_ENV and its per-language siblings, the Git hook commands, and the remaining preload and CA variables, organised by what a variable does rather than which runtime owns it. 4. YAML parse errors quoted the offending source line — which in a credentials document is the secret — into boot stderr and the watcher's logger. Only the error code and position are reported now, in credentials-local and settings-local alike, pinned by a test that asserts the secret is absent. 5. 0600 governed only files the harness wrote. A hand-created 0644 document was read normally. POSIX now checks the mode before reading contents, at boot and on every reload; Windows has no mode to inspect and is skipped rather than faked. The project a session is launched in is trusted by default, with no prompt and no stored trust record: it may supply its own endpoint, ordinary variables, and a key ranked below the managed store. Trust stops at the harness itself — a discovered file still cannot set DSH_PERMISSION_MODE, PATH, BASH_ENV, or the rest, because those take effect with no user action, before any turn, outside the permission policy and the sandbox.
This commit is contained in:
@@ -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 packages/web/web-search-deepseek/README.md
|
||||
README.md: 9046934de209ed0787efa50332e5be16bfdf55c6
|
||||
README.zh.md: 94e01daba69cecd2f5c3c6680979ee5fd66d7cdd
|
||||
README.md: 95340314fe08d0963b899f4a1d704a98f85963a5
|
||||
README.zh.md: efd02af805faa96781654b4a4a0dd69a6b8ed3e4
|
||||
|
||||
@@ -20,7 +20,7 @@ It reuses the `DEEPSEEK_API_KEY` credential reference (no new secret) but **not*
|
||||
|---|---|---|
|
||||
| `apiKey` | omitted | Literal DeepSeek API key. Prefer `apiKeyEnv` so no secret enters configuration; a non-empty literal wins. |
|
||||
| `apiKeyEnv` | `DEEPSEEK_API_KEY` | Credential reference resolved for each search through `ctx.credentials`, or from the process environment when that seam is absent. A missing value fails the call as `WEB_PROVIDER_CREDENTIAL_MISSING`. |
|
||||
| `baseURL` | `https://api.deepseek.com/anthropic/v1` | Anthropic-compatible endpoint base; `/messages` is appended. Use a separate env var such as `$DEEPSEEK_SEARCH_BASE_URL` when overriding it; do not reuse `$DEEPSEEK_BASE_URL`, which belongs to the chat-completions LLM adapter. An unparseable value makes the provider unavailable. |
|
||||
| `baseURL` | `https://api.deepseek.com/anthropic/v1` | Anthropic-compatible endpoint base; `/messages` is appended. Falls back to `$DEEPSEEK_SEARCH_BASE_URL` from any environment layer; do not reuse `$DEEPSEEK_BASE_URL`, which belongs to the chat-completions LLM adapter. An unparseable value makes the provider unavailable. |
|
||||
| `model` | `deepseek-v4-flash` | Anthropic-format model name. |
|
||||
| `apiVersion` | `2023-06-01` | `anthropic-version` header value. |
|
||||
| `maxTokens` | `4096` | Positive-integer upper bound on generated tokens for the Messages request. |
|
||||
@@ -31,7 +31,7 @@ It reuses the `DEEPSEEK_API_KEY` credential reference (no new secret) but **not*
|
||||
name: '@deepseek-ai/dsh-web-search-deepseek'
|
||||
config:
|
||||
apiKeyEnv: DEEPSEEK_API_KEY
|
||||
baseURL: !!js process.env.DEEPSEEK_SEARCH_BASE_URL
|
||||
baseURL: https://gateway.internal/anthropic/v1
|
||||
```
|
||||
|
||||
## Mapping
|
||||
|
||||
@@ -20,7 +20,7 @@ Exa 和 Perplexity 提供专用搜索端点,DeepSeek 则没有。该提供方
|
||||
|---|---|---|
|
||||
| `apiKey` | 未设置 | DeepSeek API 密钥字面值。优先使用 `apiKeyEnv`,避免密钥进入配置;非空字面值优先。 |
|
||||
| `apiKeyEnv` | `DEEPSEEK_API_KEY` | 每次搜索都会通过 `ctx.credentials` 解析该凭据引用;没有该 seam 时则从进程环境解析。值缺失时,调用以 `WEB_PROVIDER_CREDENTIAL_MISSING` 失败。 |
|
||||
| `baseURL` | `https://api.deepseek.com/anthropic/v1` | Anthropic 兼容端点基址;追加 `/messages`。覆盖时使用 `$DEEPSEEK_SEARCH_BASE_URL` 等独立环境变量;禁止复用属于 chat-completions LLM 适配器的 `$DEEPSEEK_BASE_URL`。无法解析时提供方不可用。 |
|
||||
| `baseURL` | `https://api.deepseek.com/anthropic/v1` | Anthropic 兼容端点基址;追加 `/messages`。缺省时回退到任一环境层中的 `$DEEPSEEK_SEARCH_BASE_URL`;禁止复用属于 chat-completions LLM 适配器的 `$DEEPSEEK_BASE_URL`。无法解析时提供方不可用。 |
|
||||
| `model` | `deepseek-v4-flash` | Anthropic 格式模型名称。 |
|
||||
| `apiVersion` | `2023-06-01` | `anthropic-version` 标头值。 |
|
||||
| `maxTokens` | `4096` | Messages 请求生成 token 的正整数上限。 |
|
||||
@@ -31,7 +31,7 @@ Exa 和 Perplexity 提供专用搜索端点,DeepSeek 则没有。该提供方
|
||||
name: '@deepseek-ai/dsh-web-search-deepseek'
|
||||
config:
|
||||
apiKeyEnv: DEEPSEEK_API_KEY
|
||||
baseURL: !!js process.env.DEEPSEEK_SEARCH_BASE_URL
|
||||
baseURL: https://gateway.internal/anthropic/v1
|
||||
```
|
||||
|
||||
## 映射
|
||||
|
||||
@@ -68,6 +68,14 @@ export const Config: z<Config> = z.object({
|
||||
maxUses: z.number().step(1).min(1),
|
||||
})
|
||||
|
||||
/**
|
||||
* Environment variable naming this provider's endpoint. Deliberately distinct
|
||||
* from `$DEEPSEEK_BASE_URL`, which belongs to the chat-completions adapter:
|
||||
* search speaks the Anthropic-compatible Messages API, so one variable cannot
|
||||
* serve both.
|
||||
*/
|
||||
const SEARCH_BASE_URL_ENV = 'DEEPSEEK_SEARCH_BASE_URL'
|
||||
|
||||
/** Register the DeepSeek search provider with `ctx.web`. */
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
const maxTokens = config.maxTokens ?? DEEPSEEK_DEFAULT_MAX_TOKENS
|
||||
@@ -81,13 +89,14 @@ export function apply(ctx: Context, config: Config): void {
|
||||
resolveApiKey: async () => {
|
||||
const credentials = ctx.get('credentials')
|
||||
if (credentials !== undefined) return (await credentials.resolve(apiKeyEnv))?.value
|
||||
// Without the seam the launching environment is the whole credential
|
||||
// plane — but only that layer, never a discovered project file.
|
||||
const inherited = environmentOf(ctx).getFrom(apiKeyEnv, ['process'])
|
||||
return inherited !== undefined && inherited.value.length > 0 ? inherited.value : undefined
|
||||
// Without the seam the environment is the whole credential plane.
|
||||
const ambient = environmentOf(ctx).getFrom(apiKeyEnv, ['process', 'project-env', 'user-env'])
|
||||
return ambient !== undefined && ambient.value.length > 0 ? ambient.value : undefined
|
||||
},
|
||||
apiKeyEnv,
|
||||
baseURL: config.baseURL ?? DEEPSEEK_DEFAULT_BASE_URL,
|
||||
baseURL: config.baseURL
|
||||
?? environmentOf(ctx).getFrom(SEARCH_BASE_URL_ENV, ['process', 'project-env', 'user-env'])?.value
|
||||
?? DEEPSEEK_DEFAULT_BASE_URL,
|
||||
model: config.model ?? DEEPSEEK_DEFAULT_MODEL,
|
||||
apiVersion: config.apiVersion ?? DEEPSEEK_DEFAULT_API_VERSION,
|
||||
maxTokens,
|
||||
|
||||
@@ -59,10 +59,9 @@ export const Config: z<Config> = z.object({
|
||||
/** Register the Exa search provider with `ctx.web`. */
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.web.registerSearchProvider(new ExaSearchProvider({
|
||||
// Only the launching shell and the user's own `.env` may name this key:
|
||||
// a project directory can be written by the model, and a substituted key
|
||||
// would route every request through an account someone else reads.
|
||||
apiKey: config.apiKey ?? environmentOf(ctx).getFrom('EXA_API_KEY', ['process', 'user-env'])?.value ?? '',
|
||||
// Every environment layer may name this key: the product trusts the
|
||||
// project it is launched in, and the managed store is not involved here.
|
||||
apiKey: config.apiKey ?? environmentOf(ctx).getFrom('EXA_API_KEY', ['process', 'project-env', 'user-env'])?.value ?? '',
|
||||
baseURL: config.baseURL ?? EXA_DEFAULT_BASE_URL,
|
||||
searchType: config.searchType ?? EXA_DEFAULT_SEARCH_TYPE,
|
||||
highlightsPerResult: config.highlightsPerResult ?? EXA_DEFAULT_HIGHLIGHTS_PER_RESULT,
|
||||
|
||||
@@ -53,10 +53,9 @@ export const Config: z<Config> = z.object({
|
||||
/** Register the Perplexity search provider with `ctx.web`. */
|
||||
export function apply(ctx: Context, config: Config): void {
|
||||
ctx.web.registerSearchProvider(new PerplexitySearchProvider({
|
||||
// Only the launching shell and the user's own `.env` may name this key:
|
||||
// a project directory can be written by the model, and a substituted key
|
||||
// would route every request through an account someone else reads.
|
||||
apiKey: config.apiKey ?? environmentOf(ctx).getFrom('PERPLEXITY_API_KEY', ['process', 'user-env'])?.value ?? '',
|
||||
// Every environment layer may name this key: the product trusts the
|
||||
// project it is launched in, and the managed store is not involved here.
|
||||
apiKey: config.apiKey ?? environmentOf(ctx).getFrom('PERPLEXITY_API_KEY', ['process', 'project-env', 'user-env'])?.value ?? '',
|
||||
baseURL: config.baseURL ?? PERPLEXITY_DEFAULT_BASE_URL,
|
||||
model: config.model ?? PERPLEXITY_DEFAULT_MODEL,
|
||||
maxTokens: config.maxTokens ?? PERPLEXITY_DEFAULT_MAX_TOKENS,
|
||||
|
||||
Reference in New Issue
Block a user