feat(shared-ai): canonical proposable-tool list + drift guard on mana-ai

Makes the webapp's AI policy and the server's tool allow-list physically
impossible to drift. Adds the missing entries the guard caught on first
run: `complete_tasks_by_title`, `visit_place`, `undo_drink` now have
parameter schemas server-side too.

- `packages/shared-ai/src/policy/proposable-tools.ts`
  - `AI_PROPOSABLE_TOOL_NAMES` as `const` array + literal union type
  - `AI_PROPOSABLE_TOOL_SET` for set-membership checks
- Webapp `DEFAULT_AI_POLICY` derives its `propose` entries from the
  shared list via `Object.fromEntries(...)` — adding a tool there is now
  a one-line change in `@mana/shared-ai`
- mana-ai `AI_AVAILABLE_TOOLS`: module-load assertion compares its
  hardcoded names against `AI_PROPOSABLE_TOOL_SET` and throws with a
  pointed error on drift (extras in one direction, missing in the
  other). Service refuses to start on mismatch — better than silent
  degradation.
- Bun test (`tools.test.ts`) runs the same contract plus sanity checks
  (non-empty description, required params carry docs). Vitest policy
  test adds the symmetric check on the webapp side.

All three runtimes now green: webapp 66/66, shared-ai 2/2,
mana-ai 9/9 Bun tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Till JS 2026-04-15 00:52:38 +02:00
parent dccd9c5c4e
commit 4be5e29bd3
7 changed files with 164 additions and 37 deletions

View file

@ -29,3 +29,9 @@ export type {
ResolvedInput,
} from './planner';
export { buildPlannerPrompt, parsePlannerResponse } from './planner';
export {
AI_PROPOSABLE_TOOL_NAMES,
AI_PROPOSABLE_TOOL_SET,
type AiProposableToolName,
} from './policy';

View file

@ -0,0 +1,5 @@
export {
AI_PROPOSABLE_TOOL_NAMES,
AI_PROPOSABLE_TOOL_SET,
type AiProposableToolName,
} from './proposable-tools';

View file

@ -0,0 +1,31 @@
/**
* Canonical list of tool names the AI is allowed to *propose*.
*
* Both the webapp's `DEFAULT_AI_POLICY` and the server-side
* `AI_AVAILABLE_TOOLS` list in `services/mana-ai/` derive from here.
* Adding a new proposable tool:
*
* 1. Append its name to {@link AI_PROPOSABLE_TOOL_NAMES}
* 2. Add the tool with its params to `AI_AVAILABLE_TOOLS` in mana-ai
* (the contract test below ensures step 2 isn't forgotten)
* 3. The webapp's `DEFAULT_AI_POLICY` picks it up automatically
*
* Tools NOT in this list default to `'propose'` only if the per-tool
* policy map lacks an explicit entry. Most `auto` / `deny` decisions
* stay hardcoded in the webapp policy this shared list only covers
* the tools the *server-side* planner actively proposes.
*/
export const AI_PROPOSABLE_TOOL_NAMES = [
'create_task',
'complete_task',
'complete_tasks_by_title',
'create_event',
'create_place',
'visit_place',
'undo_drink',
] as const;
export type AiProposableToolName = (typeof AI_PROPOSABLE_TOOL_NAMES)[number];
export const AI_PROPOSABLE_TOOL_SET: ReadonlySet<string> = new Set(AI_PROPOSABLE_TOOL_NAMES);