feat(api): shared agent domain tool registry, proposals, and commit path #330

Merged
james merged 1 commit from 51-domain-tool-surface into main 2026-06-28 21:39:32 +00:00
Owner

Implements the shared domain tool registry for epic #47's agent (#51), per ADR-0029 and ADR-0030. The lib/agent/ module is framework-agnostic — the future PWA agent loop and MCP endpoint import it unchanged.

What's in it

  • 96 tools across every domain entity — profile, skills (sections tree + skills), education, jobs/contracts/positions/contributions, projects, organizations (+links +key-people), people (+children), notes. Coarse CRUD by default plus earned domain tools: find_people_at_org, link_person_to_org, add_note_to_person, merge_people. verb_noun naming; every write tool tagged safe_create | safe_update | destructive_update | destructive_delete.
  • Propose-then-confirm writes — write tools return a ProposedChange and persist it; they never mutate. The commit path is the only mutator: re-validates against current state, enforces a 15-min expiry, applies through the entity repository, and writes an append-only audit event.
  • Migrations 022_agent_proposals + 023_audit_events (+ repositories, entities, schema). Standalone for now; the agent-loop ticket will link proposals to conversations later.
  • Thin authenticated routes POST /api/agent/proposals/{id}/{commit,reject} (resolve the user from session/PAT via the existing identity reader).

Safety (ADR-0030)

  • No tool accepts a user_id — the acting identity comes only from request context; cross-user access is unrepresentable in the schema. Enforcement lives at the repository layer.
  • Cross-user commit (user B vs user A's proposal) → 404, not 403 (don't leak existence) — asserted in tests.
  • Apply-once via a DB pending→applied claim; stale proposals (out-of-band edit) → 409 proposal_stale; expired → 409 proposal_expired.

Verification (re-run locally)

  • @carol/api typecheck + lint — clean
  • openapi:check up to date · openapi:coverage 107 pairs · @carol/api-client regen + typecheck clean
  • Full test suite against BOTH engines: 1041 passed, 0 skipped — I ran an ephemeral Postgres so the new agent_proposals/audit_events migrations and per-engine tool suites are proven on Postgres too, not just SQLite.

Notes

  • Person notes have no create_person_note — the earned add_note_to_person domain tool is the create path (per ADR-0030's "domain tool wins where it fits"); update/delete/list/get remain.
  • merge_people re-points children and drops self-referencing rows; a rare UNIQUE collision on already-shared links surfaces as a commit error → re-propose (not silently swallowed).
  • No new env var (PROPOSAL_TTL_MS is a 15-min code constant).

Closes #51

🤖 Generated with Claude Code

Implements the shared domain tool registry for epic #47's agent (#51), per **ADR-0029** and **ADR-0030**. The `lib/agent/` module is framework-agnostic — the future PWA agent loop and MCP endpoint import it unchanged. ## What's in it - **96 tools** across every domain entity — profile, skills (sections tree + skills), education, jobs/contracts/positions/contributions, projects, organizations (+links +key-people), people (+children), notes. Coarse CRUD by default plus earned domain tools: `find_people_at_org`, `link_person_to_org`, `add_note_to_person`, `merge_people`. `verb_noun` naming; every write tool tagged `safe_create | safe_update | destructive_update | destructive_delete`. - **Propose-then-confirm writes** — write tools return a `ProposedChange` and persist it; they never mutate. The **commit path is the only mutator**: re-validates against current state, enforces a 15-min expiry, applies through the entity repository, and writes an append-only **audit event**. - **Migrations** `022_agent_proposals` + `023_audit_events` (+ repositories, entities, schema). Standalone for now; the agent-loop ticket will link proposals to conversations later. - **Thin authenticated routes** `POST /api/agent/proposals/{id}/{commit,reject}` (resolve the user from session/PAT via the existing identity reader). ## Safety (ADR-0030) - **No tool accepts a `user_id`** — the acting identity comes only from request context; cross-user access is unrepresentable in the schema. Enforcement lives at the repository layer. - **Cross-user commit (user B vs user A's proposal) → 404, not 403** (don't leak existence) — asserted in tests. - Apply-once via a DB `pending→applied` claim; stale proposals (out-of-band edit) → `409 proposal_stale`; expired → `409 proposal_expired`. ## Verification (re-run locally) - `@carol/api` typecheck + lint — clean - `openapi:check` up to date · `openapi:coverage` **107 pairs** · `@carol/api-client` regen + typecheck clean - **Full test suite against BOTH engines: 1041 passed, 0 skipped** — I ran an ephemeral Postgres so the new `agent_proposals`/`audit_events` migrations and per-engine tool suites are proven on Postgres too, not just SQLite. ## Notes - Person notes have no `create_person_note` — the earned `add_note_to_person` domain tool is the create path (per ADR-0030's "domain tool wins where it fits"); update/delete/list/get remain. - `merge_people` re-points children and drops self-referencing rows; a rare UNIQUE collision on already-shared links surfaces as a commit error → re-propose (not silently swallowed). - No new env var (`PROPOSAL_TTL_MS` is a 15-min code constant). Closes #51 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(api): shared agent domain tool registry, proposals, and commit path
All checks were successful
Commits / Conventional Commits (pull_request) Successful in 11s
PR / OSV-Scanner (pull_request) Successful in 1m36s
PR / pnpm audit (pull_request) Successful in 2m2s
PR / Static analysis (pull_request) Successful in 2m16s
PR / Typecheck (pull_request) Successful in 2m44s
PR / Client (web export smoke) (pull_request) Successful in 2m54s
PR / Package age policy (soft) (pull_request) Successful in 37s
PR / Test (sqlite) (pull_request) Successful in 3m7s
PR / Test (postgres) (pull_request) Successful in 3m15s
PR / Build (pull_request) Successful in 3m23s
PR / OpenAPI (pull_request) Successful in 3m36s
Secrets / gitleaks (pull_request) Successful in 47s
PR / Lint (pull_request) Successful in 3m52s
PR / Trivy (image) (pull_request) Successful in 2m8s
PR / Coverage (soft) (pull_request) Successful in 3m19s
6b3898e4f2
Implements the shared domain tool registry for epic #47's agent (#51),
per ADR-0029 and ADR-0030: read + write tools for every domain entity,
write tools that return ProposedChange objects instead of mutating, a
single user-scoped commit path (the only mutator) with re-validation and
a 15-minute expiry, and an append-only audit trail.

- 96 tools across profile, skills, education, jobs/contracts/positions/
  contributions, projects, organizations (+links +key-people), people
  (+children), notes — coarse CRUD plus earned domain tools
  (find_people_at_org, link_person_to_org, add_note_to_person,
  merge_people). verb_noun naming; safe/destructive classification tags.
- Migrations 022 (agent_proposals) + 023 (audit_events), repositories,
  and the framework-agnostic lib/agent/ module the future PWA loop and
  MCP endpoint will import unchanged.
- Thin authenticated POST /api/agent/proposals/{id}/{commit,reject}
  routes. Cross-user commit (user B vs user A's proposal) returns 404,
  not 403. No tool accepts a user_id — the acting identity comes only
  from request context; scoping is enforced at the repository layer.

Both-engine tests (SQLite + Postgres) pass; openapi check/coverage and
api-client regen green.

Closes #51

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

📊 Test coverage

Patch coverage: no testable lines changed.

Overall (app/, lib/, db/, excluding UI per ADR-0019):

Metric Value Soft target
Lines 78.0% ≥ 50%
Branches 69.3% ⚠️ ≥ 75%
Functions 78.2% informational

Soft thresholds per ADR-0019. Coverage is informational and does not block merge.

<!-- coverage-comment --> ## 📊 Test coverage **Patch coverage:** no testable lines changed. **Overall** (`app/`, `lib/`, `db/`, excluding UI per ADR-0019): | Metric | Value | Soft target | |---|---|---| | Lines | 78.0% ✅ | ≥ 50% | | Branches | 69.3% ⚠️ | ≥ 75% | | Functions | 78.2% | informational | Soft thresholds per [ADR-0019](docs/adr/0019-coverage-soft-targets.md). Coverage is informational and does not block merge.
james merged commit 46026b7672 into main 2026-06-28 21:39:32 +00:00
james deleted branch 51-domain-tool-surface 2026-06-28 21:39:32 +00:00
Sign in to join this conversation.
No description provided.