mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-19 05:01:23 +02:00
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:
parent
dccd9c5c4e
commit
4be5e29bd3
7 changed files with 164 additions and 37 deletions
|
|
@ -1,6 +1,7 @@
|
|||
import { describe, it, expect } from 'vitest';
|
||||
import { resolvePolicy, setAiPolicy, DEFAULT_AI_POLICY } from './policy';
|
||||
import { registerTools } from '../tools/registry';
|
||||
import { AI_PROPOSABLE_TOOL_NAMES } from '@mana/shared-ai';
|
||||
import type { Actor } from '../events/actor';
|
||||
|
||||
const AI: Actor = { kind: 'ai', missionId: 'm', iterationId: 'i', rationale: 'r' };
|
||||
|
|
@ -57,4 +58,10 @@ describe('resolvePolicy', () => {
|
|||
restore();
|
||||
expect(resolvePolicy('create_task', AI)).toBe('propose');
|
||||
});
|
||||
|
||||
it('every shared-ai proposable tool maps to propose in DEFAULT_AI_POLICY', () => {
|
||||
for (const name of AI_PROPOSABLE_TOOL_NAMES) {
|
||||
expect(DEFAULT_AI_POLICY.tools[name], `${name} should be 'propose'`).toBe('propose');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
import { getTool } from '../tools/registry';
|
||||
import type { Actor } from '../events/actor';
|
||||
import { AI_PROPOSABLE_TOOL_NAMES } from '@mana/shared-ai';
|
||||
|
||||
export type PolicyDecision = 'auto' | 'propose' | 'deny';
|
||||
|
||||
|
|
@ -33,33 +34,34 @@ export interface AiPolicy {
|
|||
readonly defaultForAi: PolicyDecision;
|
||||
}
|
||||
|
||||
// ── Auto-executed tools (read-only / append-only self-state) ──────────
|
||||
// Kept here as the canonical local-only list — policies that don't mutate
|
||||
// user-visible records are webapp-specific and don't need to travel
|
||||
// through @mana/shared-ai.
|
||||
const AUTO_TOOLS: Record<string, 'auto'> = {
|
||||
get_task_stats: 'auto',
|
||||
list_tasks: 'auto',
|
||||
get_todays_events: 'auto',
|
||||
get_drink_progress: 'auto',
|
||||
nutrition_summary: 'auto',
|
||||
get_places: 'auto',
|
||||
location_log: 'auto',
|
||||
// Append-only self-state logs: AI proposing "did you drink water?" +
|
||||
// user confirming + AI logging it should not require a second approval.
|
||||
log_drink: 'auto',
|
||||
log_meal: 'auto',
|
||||
};
|
||||
|
||||
// ── Proposable tools derived from the shared canonical list ───────────
|
||||
// Keeps the webapp policy and mana-ai's `AI_AVAILABLE_TOOLS` from drifting.
|
||||
// Adding a new proposable tool → append to AI_PROPOSABLE_TOOL_NAMES in
|
||||
// @mana/shared-ai and both sides pick it up automatically.
|
||||
const PROPOSE_TOOLS: Record<string, 'propose'> = Object.fromEntries(
|
||||
AI_PROPOSABLE_TOOL_NAMES.map((name) => [name, 'propose'] as const)
|
||||
);
|
||||
|
||||
export const DEFAULT_AI_POLICY: AiPolicy = {
|
||||
tools: {
|
||||
// ── Read-only / harmless → auto ───────────────────────
|
||||
get_task_stats: 'auto',
|
||||
list_tasks: 'auto',
|
||||
get_todays_events: 'auto',
|
||||
get_drink_progress: 'auto',
|
||||
nutrition_summary: 'auto',
|
||||
get_places: 'auto',
|
||||
location_log: 'auto',
|
||||
|
||||
// ── Append-only self-state logs → auto ────────────────
|
||||
// These are fast-feedback user-logged values (drink, meal). The AI
|
||||
// proposing "did you drink water?" then the user confirming + AI
|
||||
// logging it should not require a second approval step.
|
||||
log_drink: 'auto',
|
||||
log_meal: 'auto',
|
||||
|
||||
// ── Mutating user-visible records → propose ───────────
|
||||
create_task: 'propose',
|
||||
complete_task: 'propose',
|
||||
complete_tasks_by_title: 'propose',
|
||||
create_event: 'propose',
|
||||
create_place: 'propose',
|
||||
visit_place: 'propose',
|
||||
undo_drink: 'propose',
|
||||
},
|
||||
tools: { ...AUTO_TOOLS, ...PROPOSE_TOOLS },
|
||||
defaultForAi: 'propose',
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue