Skip to content

Commit 73122e6

Browse files
committed
fix(free-tier): strip false entries instead of only all-false maps
Live verification round 2: ANY false entry in the tools map gates (single-false and mixed false+true both 403; {} / omitted / true-only pass). Free-tier suspects now send true-only maps; paid/other tiers unchanged. Also confirmed: any permission-deny content gates (wildcard or explicit), plain dirs pass (git-ness not required), benign files pass.
1 parent 3d1e306 commit 73122e6

2 files changed

Lines changed: 27 additions & 22 deletions

File tree

‎src/proxy.ts‎

Lines changed: 16 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -53,14 +53,14 @@ export { normalizeBool, resolveDisableTools, withTimeout };
5353

5454
/**
5555
* Stage-5 (Zen free-tier gate): the upstream free tier answers prompts whose
56-
* `tools` map disables EVERYTHING (all-false) with `FreeTierError` 403 —
57-
* verified live against the same backend/session shape where an omitted map
58-
* or any-true map returns 200. The omission applies ONLY to likely-free
59-
* Zen models (see isFreeTierSuspectModel): paid/other providers keep the
60-
* exact old behavior so their hard-disable posture never softens. With the
61-
* map omitted the tools-disabled guard text already in the system prompt
62-
* carries the posture, and with no tool definitions the model cannot emit
63-
* tool calls. Pure (module-level for direct unit testing).
56+
* `tools` map contains ANY `false` entry with `FreeTierError` 403 — verified
57+
* live (single-false, all-false, and mixed false+true maps all gate; {},
58+
* omitted, and true-only maps pass). The stripping applies ONLY to
59+
* likely-free Zen models (see isFreeTierSuspectModel): paid/other providers
60+
* keep the exact old behavior so their hard-disable posture never softens.
61+
* Stripped tools fall back to agent defaults server-side while the guard
62+
* text still narrows usage; backend ask-defaults remain the backstop.
63+
* Pure (module-level for direct unit testing).
6464
*/
6565
export function isFreeTierSuspectModel(providerID: unknown, modelID: unknown): boolean {
6666
if (typeof providerID !== 'string' || providerID !== 'opencode') return false;
@@ -80,11 +80,14 @@ export function selectPromptToolOverrides(
8080
if (!overrides || typeof overrides !== 'object' || Array.isArray(overrides)) return null;
8181
const entries = Object.entries(overrides as Record<string, unknown>);
8282
if (entries.length === 0) return null;
83-
if (entries.some(([, value]) => value === true)) return overrides as Record<string, boolean>;
84-
// All-false: omit only for free-tier suspects; everywhere else the map is
85-
// the hard-disable mechanism and must be preserved verbatim.
86-
if (isFreeTierSuspectModel(providerID, modelID)) return null;
87-
return overrides as Record<string, boolean>;
83+
if (!isFreeTierSuspectModel(providerID, modelID)) {
84+
return overrides as Record<string, boolean>;
85+
}
86+
const enabled: Record<string, boolean> = {};
87+
for (const [key, value] of entries) {
88+
if (value === true) enabled[key] = true;
89+
}
90+
return Object.keys(enabled).length > 0 ? enabled : null;
8891
}
8992

9093
export function createApp(config: ProxyConfig): CreateAppResult {

‎tests/prompt-tool-overrides.test.js‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import { selectPromptToolOverrides, isFreeTierSuspectModel } from '../src/proxy.js';
22

3-
// Stage-5: Zen's free tier answers all-false tools maps with FreeTierError
4-
// 403 (verified live); omitted or any-true maps return 200. The omission
5-
// applies ONLY to likely-free Zen models — paid/other providers keep the
6-
// exact old behavior (map always sent) so hard-disable never softens there.
3+
// Stage-5: Zen's free tier answers tools maps containing ANY `false` with
4+
// FreeTierError 403 (verified live: single/all-false/mixed all gate; {},
5+
// omitted, and true-only maps pass). The stripping applies ONLY to
6+
// likely-free Zen models — paid/other providers keep the exact old behavior
7+
// (map always sent) so hard-disable never softens there.
78
describe('isFreeTierSuspectModel', () => {
89
test('matches opencode free-suffixed models', () => {
910
expect(isFreeTierSuspectModel('opencode', 'muse-spark-1.3-contributor-free')).toBe(true);
@@ -29,16 +30,17 @@ describe('selectPromptToolOverrides', () => {
2930
const FREE = ['opencode', 'muse-spark-1.3-contributor-free'];
3031
const PAID = ['opencode', 'kimi-k2.5'];
3132

32-
test('passes through maps with at least one enabled tool (any tier)', () => {
33+
test('passes maps through verbatim for paid/other tiers', () => {
3334
const mixed = { bash: false, read: true };
34-
expect(selectPromptToolOverrides(mixed, ...FREE)).toBe(mixed);
3535
expect(selectPromptToolOverrides(mixed, ...PAID)).toBe(mixed);
36+
const allFalse = { bash: false };
37+
expect(selectPromptToolOverrides(allFalse, ...PAID)).toBe(allFalse);
3638
});
3739

38-
test('drops all-false maps only for free-tier suspects', () => {
40+
test('strips false entries for free-tier suspects (any false gates)', () => {
41+
expect(selectPromptToolOverrides({ bash: false, read: true }, ...FREE)).toEqual({ read: true });
3942
expect(selectPromptToolOverrides({ bash: false, read: false }, ...FREE)).toBeNull();
40-
const paidMap = { bash: false };
41-
expect(selectPromptToolOverrides(paidMap, ...PAID)).toBe(paidMap);
43+
expect(selectPromptToolOverrides({ bash: true }, ...FREE)).toEqual({ bash: true });
4244
});
4345

4446
test('drops empty/non-object inputs (previous call-site behavior)', () => {

0 commit comments

Comments
 (0)