You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Commit 93cec69
Browse filesBrowse the repository at this point in the historyBrowse files
@@ -75,7 +75,7 @@ config.json.example / .env.example # examples only, never real secrets
75
75
| Auth multi-key (A) |`OPENCODE_API_KEYS` canonical, `API_KEYS` legacy alias (merge; empty never blocks) |`API_KEYS`|`(none, merges with API_KEY)`|
76
76
| Fallback proxies |`OPENCODE_UPSTREAM_PROXIES` / `OPENCODE_UPSTREAM_PROXY_STRATEGY` / `OPENCODE_UPSTREAM_PROXY_COOLDOWN_MS` / `OPENCODE_UPSTREAM_PROXY_NO_PROXY` (legacy raw env `UPSTREAM_*` merged in **both** entries) |`UPSTREAM_*` short forms |`(none)` / `failover-rr` / `300000` / `localhost,127.0.0.1,::1`|
77
77
78
-
-`DISABLE_TOOLS` resolution: `OPENCODE_DISABLE_TOOLS > DISABLE_TOOLS > file > true` via `resolveDisableTools` (`src/config/proxy-config.ts:76-86`); covered by `tests/env-alias.test.js`. Never `??`-chain booleans by hand; call the helper. Note: `true` still resolves `true` — the free-tier strip (`selectPromptToolOverrides`, `src/proxy.ts:75-91`) is a post-resolution prompt-body filter for `isFreeTierSuspectModel` only (guard text + output markup stripping stay active).
78
+
-`DISABLE_TOOLS` resolution: `OPENCODE_DISABLE_TOOLS > DISABLE_TOOLS > file > true` via `resolveDisableTools` (`src/config/proxy-config.ts:76-86`); covered by `tests/env-alias.test.js`. Never `??`-chain booleans by hand; call the helper. Note: `true` still resolves `true` — the free-tier strip (`selectPromptToolOverrides`, `src/proxy.ts:75-91`) is a post-resolution prompt-body filter for `isFreeTierSuspectModel` only (guard text + output markup stripping stay active). The default backend `read` permission is an inert ordered gate (see §5), so free-tier requests still carry `read`; explicit allowlist `read` remains a real allow.
79
79
- Out-of-matrix backend identity (do NOT add to the six-way matrix): `OPENCODE_CLIENT` is read directly in `src/backend/manager.ts:41-45` (allowlist `cli/desktop/acp/app`, default `cli`, foreign values overwritten), never enters `ProxyConfig`/`buildProxyConfig`; documented in `docs/configuration.md` backend-identity subsection + `CHANGELOG.md`. Same for the `OPENCODE_API_KEY="public"` strip and best-effort jail `git init`.
80
80
-`RETRY`: strict-merged by `resolveRetryCountSetting` (options > `OPENCODE_PROXY_RETRY_MAX_RETRIES` > legacy `RETRY_MAX_RETRIES` > file > `3`; partial numerics like `3abc`/`2.9` are invalid and fall through — never `parseInt`-truncated), then clamped by `resolveMaxRetries` (`src/retry/policy.ts:51-58`, clamp 0–5, total attempts `1+n`, `2s×2ⁿ⁻¹` + 25% jitter, `retry-after` clamped to 30s). Covered by `tests/env-alias.test.js` + `tests/retry-policy.test.js`.
81
81
-**Single merge contract:** every key goes through the pure resolvers in `src/config/proxy-config.ts` (`resolveStringSetting` / `resolveBoolSetting` / `resolveIntSetting` / `resolvePortSetting` / `resolveDurationSetting` / `resolveRetryCountSetting` / `resolveListSetting` / `resolveEnumSetting` / `resolveUrlSetting`, plus parsers `parseStrictInteger` / `parseStrictPort` / `parseHttpUrl` / `parseToolNameList`). `index.ts` and `buildProxyConfig` share them, so never re-implement a local `parseBool`/`parsePort`/list parser in either entry — add a resolver there instead. Recognized bool tokens are exactly `1/true/yes/y/on` and `0/false/no/n/off` (case-insensitive, trimmed); everything else is *unset* and falls through (no `Boolean()` coercion).
@@ -87,6 +87,7 @@ config.json.example / .env.example # examples only, never real secrets
87
87
-`DISABLE_TOOLS=true` default. External client `tools` go through the proxy bridge and are namespaced (`external__*`, never registered as OpenCode built-ins).
88
88
- Internal allowlist applies **only when the request carries no `tools`**; empty/unmatched allowlist falls back to all-builtins-disabled (safe). Request-level `opencode.internal_allowed_tools` can only narrow: `effective = server ∩ request`.
89
89
-`OPENCODE_INTERNAL_WEB_FETCH_ENABLED=true` is a legacy shortcut for allowlist `web_fetch` when no explicit allowlist is configured.
90
+
- Backend `read` permission default is an **inert ordered gate**, not plain `deny`/`allow`: `{ '*': 'deny', [FREE_TIER_READ_GATE_PATH]: 'allow' }` with `FREE_TIER_READ_GATE_PATH = '/__opencode_free_tier_gate_never__\u0000'` (`src/backend/backend-permission.ts`). OpenCode `Permission.disabled()` selects the last rule by **tool name** (`findLast`) then checks whether that rule's pattern is exactly `*`; the trailing non-`*` NUL sentinel allow keeps `read` in the upstream `tools[]` (Zen free tier otherwise returns 403 `FreeTierError`). Execution-time path matching still denies every real path via the leading `*: deny`, and the terminal NUL means no real path can match the sentinel even after `Wildcard.match` backslash normalization. This is an upstream gate token, not execution permission — explicit allowlist `read` still emits a real `'allow'`, and `bash` stays `deny` by default. Do not "simplify" it to `deny` (re-breaks free tier) or `allow` (real read execution).
90
91
- Never log tool outputs; `OPENCODE_INTERNAL_TOOL_METRICS_ENABLED` logs mode/hits/downgrade reasons only.
@@ -109,6 +110,8 @@ config.json.example / .env.example # examples only, never real secrets
109
110
110
111
-`tsconfig.json` includes only `index.ts` + `src/**/*` (tests excluded): `tsc` will NOT catch test-side API misuse. `tests/*.test.js` are JS run through `@swc/jest` (`jest.config.cjs`) importing `../src/*.js` paths mapped to `.ts`. Keep `import { jest } from '@jest/globals'` + `jest.unstable_mockModule` + top-level `await import(...)` patterns as-is (ESM order-sensitive).
111
112
-`tests/env-alias.test.js` mirrors the `index.ts` merge (it cannot import `index.ts`, which boots the server): the `DISABLE_TOOLS` chain plus the port/url/bool/policy-list/retry/upstream-strategy layer arrays. Update the mirrors when a layer array changes; the shared resolvers themselves are imported from `src/config/proxy-config.ts` and tested directly.
113
+
-`tests/free-tier-gate.test.js` locks the cross-module free-tier contract: `selectPromptToolOverrides` drops every `false` entry (all-false maps omitted) for suspect models, and `buildBackendPermission` keeps `read` visible via the ordered NUL-sentinel gate while the leading `*: deny` keeps real paths denied. This is a data-level contract only — source/live verification is still required when bumping the backend. Update both halves together — either regression re-breaks Zen free tier.
114
+
-`tests/dockerfile-pin.test.js` locks `ARG OPENCODE_VERSION=1.18.35` (build-only, not a runtime `ENV`). Bumping the pin requires updating the Dockerfile `ARG OPENCODE_VERSION`, `PINNED_VERSION`, and current version references, re-running `tests/free-tier-gate.test.js`, `tests/backend-permission.test.js`, and `tests/dockerfile-pin.test.js`, checking the target version's source for `Permission.disabled()` findLast semantics (last rule by tool name, pattern exactly `*`), plus a live Zen check that `read` stays in upstream `tools[]`; see `docs/docker.md`.
112
115
-`tests/test-integration.sh` and `tests/test-streaming-real.sh` need a real backend/credentials and are not CI gates; do not "fix" them into the default `npm test` path.
113
116
-`custom-bin/opencode` is a local-only binary; `Dockerfile` intentionally has no `custom-bin` logic (stock `npm i -g opencode-ai`). Do not reintroduce arch-specific binary coupling without an explicit request.
114
117
@@ -117,7 +120,7 @@ config.json.example / .env.example # examples only, never real secrets
117
120
-`src/proxy.ts:createApp` is a god closure (~800 lines) assembling `AppContext` (50+ fields, `src/types/context.ts`); the four routes (`chat`/`responses`/`messages`/`interactions`, ~3200 lines combined) duplicate the preflight → stream/non-stream → retry → usage/cleanup template. Prefer **pure moves + thin wrappers** over behavior changes.
118
121
- Shared unknown-guards (`asRecord`, `toErrorMessage`) live in `src/utils/guards.ts` — import from there, do not add new copies. Exception: `src/routes/system.ts` and `src/stream/collector.ts` keep their own narrower `toErrorMessage` variant (see `guards.ts` header); do not "unify" it.
119
122
- Pure layers to preserve: `src/retry/policy.ts`, `src/converters/anthropic.ts`, `src/errors/upstream.ts` (no Express/SDK deps). Route error exits must keep their wire shapes: chat non-stream JSON, responses-stream `response.failed` SSE + `[DONE]`, messages-stream `error` event without `[DONE]`, interactions-stream `interaction.completed` / `error` event without `[DONE]`.
120
-
- High-risk no-touch list without explicit approval + regression tests: timeout/retry constants, `transformUpstreamError` mappings, `isTransientUpstreamError` matchers, parser ambiguity policy, prompt guard text/order, model alias rules, policy evaluation order, backend spawn/jail/HOME isolation, collector `finish==='tool'`/idle-exemption logic, bool-fallthrough semantics.
123
+
- High-risk no-touch list without explicit approval + regression tests: timeout/retry constants, `transformUpstreamError` mappings, `isTransientUpstreamError` matchers, parser ambiguity policy, prompt guard text/order, model alias rules, policy evaluation order, backend permission gate ordering/sentinel, backend spawn/jail/HOME isolation, collector `finish==='tool'`/idle-exemption logic, bool-fallthrough semantics.
-**Unified env/file/default config merge (Phase 3A)**: `index.ts` and `buildProxyConfig` now share the strict resolvers in `src/config/proxy-config.ts` (`resolve*Setting`, `parseStrictInteger/Port`, `parseHttpUrl`, `parseToolNameList`); lookup stays env canonical > env legacy alias > file short key > hardcoded default with invalid values falling through. Library `resolveDisableTools` default is now `true` (was `false`), matching prod/docs; library consumers relying on the old tools-enabled default must now set `DISABLE_TOOLS=false` explicitly. Bool tokens are `1/true/yes/y/on` vs `0/false/no/n/off` everywhere. Docker/Compose keep empty defaults so code owns values; `entrypoint.sh` validates `OPENCODE_PROXY_PORT`/`PORT` and `OPENCODE_SERVER_PORT` against `1..65535` and unsets invalid proxy ports so `config.json` still applies.
21
+
-**Docker backend pin (`opencode-ai@1.18.35`)**: the runtime image now installs `opencode-ai@${OPENCODE_VERSION}` from `ARG OPENCODE_VERSION=1.18.35` (build-only ARG, never a runtime `ENV`), keeping the Zen free-tier `read` gate effective because it depends on the pinned backend's `Permission.disabled()` findLast semantics. Bump checklist (update the Dockerfile `ARG OPENCODE_VERSION`, `PINNED_VERSION`, and current version references, rerun the three gate tests, check the target source's `Permission.disabled()` findLast/pattern-`*` semantics, live Zen `tools[]` check) lives in `docs/docker.md`; no runtime config-matrix change. 镜像后端固定 `opencode-ai@1.18.35`(仅构建 ARG,不作为运行时 `ENV`);升级时按 `docs/docker.md` 清单同步更新 Dockerfile `ARG OPENCODE_VERSION`、`PINNED_VERSION` 及当前版本引用、重跑三个闸门测试、检查目标版本源码的 findLast 语义并做真实 Zen `tools[]` 验证,无运行时配置矩阵变化。
21
22
22
23
### Added
23
24
@@ -32,6 +33,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
32
33
33
34
### Fixed
34
35
36
+
- **Zen free-tier `read` gate (`FreeTierError` 403)**: the default backend `read` permission is now an inert ordered gate (`{ '*': 'deny', [FREE_TIER_READ_GATE_PATH]: 'allow' }`, NUL-terminated sentinel) instead of plain `deny`, so OpenCode `Permission.disabled()`'s findLast-by-tool-name check keeps `read` in the upstream `tools[]` (the free tier requires it) while execution-time path matching still denies every real path; the NUL sentinel closes the `Wildcard.match` backslash-normalization bypass, `bash` remains `deny`, and explicit allowlist `read` still emits a real `allow`. The data-level contract is locked by `tests/free-tier-gate.test.js` + `tests/backend-permission.test.js`; source/live verification is still required when bumping the backend pin because unit tests cannot execute OpenCode's installed backend. 默认后端 `read` 权限改为有序惰性闸门(NUL 哨兵 allow 让 `read` 保留在上游 `tools[]`),真实路径仍全部拒绝,`bash` 默认仍为 deny,显式 allowlist `read` 仍是真 allow;该数据层契约由 `tests/free-tier-gate.test.js` 与 `tests/backend-permission.test.js` 锁定,升级后端固定版本时仍需源码与真实 Zen 验证(单测无法执行镜像内安装的后端)。
35
37
- **Backend tool-hang + per-request dispose fixes**: the proxy no longer `PUT config {activeModel}` on every request — native `ConfigHttpApi.update` disposes the whole backend project instance unconditionally (no changed-check), aborting in-flight sessions into the `Aborted` retry-loop storm; `ensureActiveModel` (`src/backend/active-model.ts`) PUTs only on actual model change per backend client (failures never cached, existing catch semantics preserved). Separately, the headless backend can no longer `ask` (nobody approves, so prompts hung until the 180s timeout) nor silently execute unlisted tools (the free-tier strip omits the prompt tools map, so the backend fell back to agent defaults = all on): backend `opencode.json` now carries a generated deny-all `permission` map (`src/backend/backend-permission.ts`, single source for `entrypoint.sh` via `dist` with static fallback and `src/backend/manager.ts` isolated path) allowing only explicitly configured internal tools, with `external_directory` always jailed to `/home/node/project/**`. 后端不再每请求 dispose(模型变更才 PUT activeModel);headless 后端不再 `ask` 卡死、不再静默执行未授权工具(deny-all,仅放行明确配置的 internal 工具)。Locked by `tests/active-model.test.js` + `tests/backend-permission.test.js` + `tests/tool-protocol-parity.test.js`.
36
38
- **Anonymous first-party backend posture + Zen free-tier gate handling**: spawned backend pins `OPENCODE_CLIENT` to a known first-party value (`cli` default; `desktop`/`acp`/`app` pass through), strips the `OPENCODE_API_KEY="public"` footgun (anonymous uses empty), and git-inits the jail workspace best-effort (5s timeout, warn-and-continue) as defense-in-depth for session-dir sensitivity. Prompt `tools` maps have `false` entries stripped (true-only sent; all-false omitted) only for likely-free `opencode` models (`-free` suffix, `big-pickle`, `union-alpha`) to dodge the `FreeTierError` 403 (any `false` gates); stripped tools fall back to agent defaults server-side while paid/other providers keep the exact hard-disable map. The gate (`...used from within OpenCode` / `FreeTierError`) now surfaces as 403 `permission_denied` fail-fast (no retry, no proxy fallback) with false-positive guards for plain quota text. Jail stdout no longer prints absolute paths; interactions route gains text-level tool-markup stripping parity with chat/responses/messages. New `tests/backend-identity.test.js` / `tests/first-party-gate.test.js` / `tests/prompt-tool-overrides.test.js` / `tests/session-ids.test.js`. No new proxy knob (no config matrix change).
37
39
-**Translator parity fixes (N×N matrix review)**: chat array `content` parts no longer silently dropped (extract `text`/`content`); `responses``incomplete` with `content_filter` reason round-trips instead of collapsing to `length` (non-stream + stream); `is_error` now round-trips via the `ERROR: ` text prefix on chat/messages/responses tool legs (strict `true`/`"true"` check; success text literally starting with `ERROR: ` round-trips as `is_error` by design); named `custom` tool declarations are kept so `custom_tool_call` invocations always have a matching declaration; ID generation centralized on `makeId(prefix)` (`toolu_` fallback separator canonicalized, prefix-only checks unaffected). N×N 翻译矩阵对齐修复。New `tests/translator-phase-fixes.test.js` (13 cases).
0 commit comments