fix(gateway): provider recipes, stdio takes allow-list override, proposal review CLI

- #2657: stdio MCP takesHoldersAllowList is operator-overridable via
  GBRAIN_MCP_TAKES_HOLDERS (comma-separated). Fail-closed ['world']
  default preserved; empty/garbage values never widen.
- #2689 (drift-guard half): pin every anthropic-recipe model id (chat +
  expansion + alias targets) to CANONICAL_PRICING so the allowlist can
  never rot ahead of / behind the canonical pricing table again. The
  Claude 5 allowlist entries and the honest MODEL_NOT_USABLE labeling
  for configured models already landed on master.
- #2790: calendar-to-brain recipe backfill + weekly cron now end a week
  AHEAD of today so the lookahead window ('knows who you're meeting
  tomorrow') actually has upcoming events to read; cron guide updated.
- #1467: take-proposal review surface — new src/core/takes-proposals.ts
  (list/accept/reject; accept promotes to the canonical takes fence
  under the per-page lock + DB mirror + promoted_row_num audit), wired
  as contract-first ops takes_proposals_list / takes_proposal_resolve
  (resolve is localOnly) and 'gbrain takes propose [--accept N|--reject N]'.
  propose_takes phase model now resolves models.propose_takes config
  (alias-aware) and threads the resolved id to the extractor + model_id
  column.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-07-21 14:37:55 -07:00
co-authored by Claude Fable 5
parent 0612b0daa8
commit 2d4f980a42
13 changed files with 645 additions and 13 deletions
+34
View File
@@ -0,0 +1,34 @@
/**
* #2657 — stdio MCP takes-holder allow-list operator override.
*
* The stdio transport hardcoded takesHoldersAllowList to ['world'] with no
* escape hatch, making non-world takes unreachable for stdio MCP agents.
* GBRAIN_MCP_TAKES_HOLDERS (comma-separated) is the opt-in widening; the
* fail-closed ['world'] default is preserved when unset/empty/garbage.
*/
import { describe, test, expect } from 'bun:test';
import { resolveStdioTakesHolders } from '../src/mcp/server.ts';
describe('resolveStdioTakesHolders (#2657)', () => {
test('unset → fail-closed default ["world"]', () => {
expect(resolveStdioTakesHolders(undefined)).toEqual(['world']);
});
test('empty / whitespace-only → default, never an empty allow-list', () => {
expect(resolveStdioTakesHolders('')).toEqual(['world']);
expect(resolveStdioTakesHolders(' ')).toEqual(['world']);
expect(resolveStdioTakesHolders(', ,')).toEqual(['world']);
});
test('comma-split with trimming', () => {
expect(resolveStdioTakesHolders('world, brain , people/alice-example')).toEqual([
'world',
'brain',
'people/alice-example',
]);
});
test('single non-world holder is honored (the unreachable-takes case)', () => {
expect(resolveStdioTakesHolders('brain')).toEqual(['brain']);
});
});
+26
View File
@@ -41,12 +41,16 @@ interface CapturedSql {
function buildMockEngine(opts: {
pages: Page[];
existingProposals?: Set<string>; // composite-key strings already in take_proposals
config?: Record<string, string>; // #1467: models.propose_takes resolution
}): { engine: BrainEngine; captured: CapturedSql[] } {
const captured: CapturedSql[] = [];
const existing = opts.existingProposals ?? new Set<string>();
const engine = {
kind: 'pglite',
async getConfig(key: string) {
return opts.config?.[key] ?? null;
},
async listPages() {
return opts.pages;
},
@@ -267,6 +271,28 @@ describe('runPhaseProposeTakes — phase integration', () => {
expect(inserts[0]!.params[9]).toBe('market'); // domain
});
test('#1467: models.propose_takes config routes the extractor model + model_id column', async () => {
const pages = [buildPage({ slug: 'wiki/model-routing', body: 'Configured models should reach the extractor.' })];
const { engine, captured } = buildMockEngine({
pages,
config: { 'models.propose_takes': 'anthropic:claude-sonnet-5' },
});
let seenModelHint: string | undefined;
const extractor: ProposeTakesExtractor = async (input) => {
seenModelHint = input.modelHint;
return [{ claim_text: 'Config routing works', kind: 'take', holder: 'brain', weight: 0.6 }];
};
const result = await runPhaseProposeTakes(buildCtx(engine), { extractor });
expect(result.status).toBe('ok');
// The RESOLVED model (config key, no explicit opts.model) reaches the
// extractor AND lands in the take_proposals.model_id audit column.
expect(seenModelHint).toBe('anthropic:claude-sonnet-5');
const inserts = captured.filter(c => c.sql.includes('INSERT INTO take_proposals'));
expect(inserts).toHaveLength(1);
expect(inserts[0]!.params[11]).toBe('anthropic:claude-sonnet-5'); // model_id
});
test('cache hit: page already in take_proposals is skipped', async () => {
const body = 'A page that was already processed.';
const pages = [buildPage({ slug: 'wiki/old-page', body })];
+34
View File
@@ -0,0 +1,34 @@
/**
* #2689 — recipe allowlist ↔ canonical pricing drift guard.
*
* The Anthropic chat allowlist once predated the canonical pricing table
* (pricing knew claude-sonnet-5; the recipe rejected it), so a config-set
* model the whole system "knew" still failed the chat-client probe. This
* guard pins the invariant the other way around: every model id the
* anthropic recipe accepts (chat + expansion) MUST resolve in
* CANONICAL_PRICING, so an allowlist addition can never outrun pricing and
* a pricing-known Anthropic model missing from the allowlist shows up in
* review as an explicit divergence, not silent rot.
*/
import { describe, test, expect } from 'bun:test';
import { anthropic } from '../src/core/ai/recipes/anthropic.ts';
import { canonicalLookup } from '../src/core/model-pricing.ts';
describe('anthropic recipe ↔ canonical pricing (#2689 drift guard)', () => {
const recipeIds = new Set<string>([
...(anthropic.touchpoints.chat?.models ?? []),
...(anthropic.touchpoints.expansion?.models ?? []),
// Alias targets must be priced too — they're what actually hits the wire.
...Object.values(anthropic.aliases ?? {}),
]);
test('recipe declares at least one chat model', () => {
expect(recipeIds.size).toBeGreaterThan(0);
});
for (const id of recipeIds) {
test(`recipe model "${id}" is priced in CANONICAL_PRICING`, () => {
expect(canonicalLookup(`anthropic:${id}`)).toBeTruthy();
});
}
});
+143
View File
@@ -0,0 +1,143 @@
/**
* #1467 — take-proposal review queue (list / accept / reject).
*
* Real PGLite engine (in-memory) + a temp brain dir. Pins the queue→fence
* promotion contract: list shows pending proposals (holder allow-list
* respected), accept writes the canonical markdown fence + DB take +
* verdict row with promoted_row_num, reject records the verdict, and a
* second resolve on the same id fails loudly.
*/
import { describe, test, expect, beforeAll, afterAll } from 'bun:test';
import { mkdtempSync, rmSync, readFileSync, existsSync } from 'node:fs';
import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { PGLiteEngine } from '../src/core/pglite-engine.ts';
import { listTakeProposals, resolveTakeProposal } from '../src/core/takes-proposals.ts';
let engine: PGLiteEngine;
let brainDir: string;
async function insertProposal(opts: {
slug: string;
claim: string;
holder?: string;
status?: string;
hash?: string;
}): Promise<number> {
const rows = await engine.executeRaw<{ id: number }>(
`INSERT INTO take_proposals
(source_id, page_slug, content_hash, prompt_version, proposal_run_id,
status, claim_text, kind, holder, weight, domain, model_id)
VALUES ('default', $1, $2, 'test-v1', 'run-test',
$3, $4, 'take', $5, 0.6, NULL, 'anthropic:claude-sonnet-4-6')
RETURNING id`,
[opts.slug, opts.hash ?? `hash-${opts.slug}-${opts.claim.length}`, opts.status ?? 'pending', opts.claim, opts.holder ?? 'brain'],
);
return rows[0]!.id;
}
beforeAll(async () => {
engine = new PGLiteEngine();
await engine.connect({});
await engine.initSchema();
brainDir = mkdtempSync(join(tmpdir(), 'gbrain-proposals-'));
await engine.putPage('people/alice-example', {
title: 'Alice Example',
type: 'person' as const,
compiled_truth: 'Alice is a strong founder.\n',
});
});
afterAll(async () => {
await engine.disconnect();
rmSync(brainDir, { recursive: true, force: true });
});
describe('listTakeProposals (#1467)', () => {
test('lists pending by default, respects holder allow-list', async () => {
const brainId = await insertProposal({ slug: 'people/alice-example', claim: 'Alice will raise a Series A', holder: 'brain', hash: 'h-list-1' });
await insertProposal({ slug: 'people/alice-example', claim: 'Consensus view of Alice', holder: 'world', hash: 'h-list-2' });
await insertProposal({ slug: 'people/alice-example', claim: 'Already rejected', status: 'rejected', hash: 'h-list-3' });
const all = await listTakeProposals(engine, { sourceId: 'default' });
expect(all.map((r) => r.id)).toContain(brainId);
expect(all.every((r) => r.status === 'pending')).toBe(true);
expect(all.some((r) => r.claim_text === 'Already rejected')).toBe(false);
// MCP-style allow-list: only 'world' holders visible.
const worldOnly = await listTakeProposals(engine, { sourceId: 'default', takesHoldersAllowList: ['world'] });
expect(worldOnly.length).toBeGreaterThan(0);
expect(worldOnly.every((r) => r.holder === 'world')).toBe(true);
// Fail-closed: empty allow-list → nothing, not everything.
expect(await listTakeProposals(engine, { sourceId: 'default', takesHoldersAllowList: [] })).toEqual([]);
// Source isolation: a different source sees nothing.
expect(await listTakeProposals(engine, { sourceId: 'other-source' })).toEqual([]);
});
});
describe('resolveTakeProposal (#1467)', () => {
test('accept promotes to markdown fence + DB take + verdict row', async () => {
const id = await insertProposal({ slug: 'people/alice-example', claim: 'Alice ships weekly', hash: 'h-accept' });
const result = await resolveTakeProposal(engine, { id, verdict: 'accept', brainDir, sourceId: 'default', actedBy: 'test' });
expect(result.status).toBe('accepted');
expect(result.promoted_row_num).toBeGreaterThanOrEqual(1);
// Markdown fence written (markdown is canonical).
const mdPath = join(brainDir, 'people/alice-example.md');
expect(existsSync(mdPath)).toBe(true);
expect(readFileSync(mdPath, 'utf-8')).toContain('Alice ships weekly');
// DB mirror.
const takes = await engine.listTakes({ page_slug: 'people/alice-example' });
const promoted = takes.find((t) => t.claim === 'Alice ships weekly');
expect(promoted).toBeTruthy();
expect(promoted!.row_num).toBe(result.promoted_row_num!);
// Verdict row.
const rows = await engine.executeRaw<{ status: string; promoted_row_num: number; acted_by: string }>(
`SELECT status, promoted_row_num, acted_by FROM take_proposals WHERE id = $1`,
[id],
);
expect(rows[0]!.status).toBe('accepted');
expect(rows[0]!.promoted_row_num).toBe(result.promoted_row_num!);
expect(rows[0]!.acted_by).toBe('test');
// Double-resolve fails loudly.
await expect(
resolveTakeProposal(engine, { id, verdict: 'reject', sourceId: 'default' }),
).rejects.toThrow(/already accepted/);
});
test('reject records the verdict without touching the fence', async () => {
const id = await insertProposal({ slug: 'people/alice-example', claim: 'Overreaching claim', hash: 'h-reject' });
const result = await resolveTakeProposal(engine, { id, verdict: 'reject', sourceId: 'default' });
expect(result.status).toBe('rejected');
const rows = await engine.executeRaw<{ status: string }>(
`SELECT status FROM take_proposals WHERE id = $1`, [id],
);
expect(rows[0]!.status).toBe('rejected');
expect(readFileSync(join(brainDir, 'people/alice-example.md'), 'utf-8')).not.toContain('Overreaching claim');
});
test('accept without a brain dir fails loudly (markdown is canonical)', async () => {
const id = await insertProposal({ slug: 'people/alice-example', claim: 'No dir claim', hash: 'h-nodir' });
await expect(
resolveTakeProposal(engine, { id, verdict: 'accept', sourceId: 'default' }),
).rejects.toThrow(/NO_BRAIN_DIR/);
});
test('unknown or out-of-scope id fails loudly', async () => {
await expect(
resolveTakeProposal(engine, { id: 9_999_999, verdict: 'reject', sourceId: 'default' }),
).rejects.toThrow(/PROPOSAL_NOT_FOUND/);
// Source isolation: an id that exists is invisible from another source.
const id = await insertProposal({ slug: 'people/alice-example', claim: 'Scoped claim', hash: 'h-scope' });
await expect(
resolveTakeProposal(engine, { id, verdict: 'reject', sourceId: 'other-source' }),
).rejects.toThrow(/PROPOSAL_NOT_FOUND/);
});
});