feat(hal-mcp): artifact-callable Edifice tools - #83
Conversation
…o bytes, readOnlyHint (#82) Today's Edifice tools assume a Claude-Code-side consumer that can follow a signed URL and tolerate permission prompts. A Claude artifact can do neither (CSP blocks all outbound fetch/img except connector calls; every load-time tool call fires its own Allow/Deny dialog). Three changes make hal-mcp callable from inside an artifact. Changes: - Add get_mission_context(mission_id): returns the full context.json-shaped payload inline (header + observations[] + notes[] + photos[] metadata), zero signed URLs. Shape ported field-for-field from build_context.py. - Add get_mission_photo(photo_id, max_width?): returns one photo as a resized base64 JPEG (magick-wasm @0.0.41), ~300 KB target, image content block. - Add annotations.readOnlyHint:true to 12 read tools; readOnlyHint:false on push_mission_context so no cache layer replays it. - Extract get_mission_with_assets' fetch logic into fetchMissionRaw() — contract unchanged, shared by both tools. - Tests: inline shape (no signed_url), tools/list readOnlyHint, single-field no-clobber proof, get_mission_photo error paths. deno check + 68 Deno tests + make test (157 pytest) green offline. No edifice_* schema change. Unblocks bluegreen-marketplace#50 Phases 2-3. Live acceptance criteria (context shape, photo <400KB decodable JPEG, tools/list annotations) to be verified post-deploy against reference mission 3f2bd270-fd5f-4d09-a64e-063a66348dc8. Fixes #82 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
🔍 Comprehensive PR ReviewPR: #83 SummaryThe Python→TypeScript port ( Verdict: Note: This PR is correctly a draft per explicit instruction — none of the below blocks anything today, but the two HIGH bugs (#1, #2) ship latent production failure modes the offline test suite can't exercise, and should be fixed before this leaves draft.
🟠 High Issues1.
|
| Issue | Location | Suggestion |
|---|---|---|
get_mission_context description overclaims context.json parity (omits building 2D-map block, likely intentional) |
index.ts:744-765 |
Tighten docstring to name fields explicitly |
max_width accepts non-positive values, surfaces raw wasm exception |
index.ts:657 |
z.number().int().positive().optional() |
get_mission_photo's "no storage_path" branch untested |
index.ts:787 |
Clone the "unknown photo_id" test pattern |
"ported field-for-field" comment is an unverifiable cross-repo claim (one visible zone-field asymmetry, may be intentional) |
index.ts:377 |
Add pointer to docs/cross-repo-log.md sync note |
docs/architecture.md mermaid diagram already stale pre-PR, widened by 2 more tools |
docs/architecture.md:32-38 |
Pre-existing drift — flag as standalone doc-hygiene follow-up, out of this PR's scope |
✅ What's Good
buildHeader/buildObservationsAndNotesverified field-for-field accurate againstbuild_context.py.fetchMissionRaw()extraction is behavior-preserving — full 68-test suite passes, including 9 pre-existing tests unmodified.- All 13 touched
readOnlyHintcall sites carry the correct value. push_mission_contextno-clobber test genuinely proves the claim by capturing the actual.update()payload.- Zero-signed-URL guarantee in
get_mission_contextis enforced by a real runtime assertion, not just a shape check. - Cross-repo hygiene (
docs/cross-repo-log.md,.claude/STATUS.md) is complete and accurate. - New inline comments consistently explain why, matching this repo's comment bar.
📋 Suggested Follow-up Issues
| Issue Title | Priority | Related Finding |
|---|---|---|
| Re-sync docs/architecture.md tool count/list with prd.md (pre-existing drift) | P3 | LOW issue — architecture.md |
Next Steps
- Fix the 5 HIGH issues before this PR leaves draft — all have concrete, low-effort patches above.
- Review the 3 MEDIUM issues (all low-effort, single-file changes).
- Consider the 5 LOW issues for this PR or a follow-up pass.
- Confirm the
Migration smoke-testCI check (in-progress at scope time) has resolved.
Reviewed by 5-agent comprehensive PR review
Full artifact: artifacts/runs/dfefba2e640229da358a4c1b1fbd91dc/review/consolidated-review.md
Fixed: - get_mission_context now surfaces notes/photos/annotations fetch errors instead of silently returning a payload that looks like a clean, empty mission (HIGH) - ensureMagick() resets its memoized promise on rejection so a transient CDN blip doesn't permanently break get_mission_photo for the isolate's lifetime (HIGH) - readOnlyHint: true added to list_stages and get_document_file, closing the PR's own readOnlyHint sweep gap (HIGH) - docs/prd.md tool count self-contradiction (27 vs 30) fixed, with a changelog bullet for the artifact-callable Edifice tools (HIGH) - README.md tool count, Edifice tool list, and readOnlyHint mention brought in sync with prd.md (MEDIUM) - OBSERVATION_TYPE comment restated as a general invariant so it doesn't misleadingly imply devis is covered (MEDIUM) - max_width now rejects non-positive values at the schema boundary instead of surfacing a raw magick-wasm exception (LOW) - get_mission_context docstring no longer overclaims exact context.json parity (building 2D-map block is out of scope) (LOW) - "ported field-for-field" comment now points to the docs/cross-repo-log.md sync note (LOW) Tests added: - tools/list readOnlyHint check rewritten as a structural invariant (every list_*/get_* tool) instead of a hardcoded 12-name list - get_mission_context: suivi_chantier and devis project_type branches (previously 0% covered) - get_mission_context: building-present path (residence/adresse/ type_batiment/description_batiment + cleanAddress) - get_mission_photo: missing storage_path error branch Skipped: - docs/architecture.md tool count/list — pre-existing staleness predating this PR by at least one prior increment; review's own recommendation is a dedicated follow-up doc-hygiene pass, not a partial touch-up bundled into this PR Validation: deno check clean, 72/72 Deno tests pass (68 pre-existing + 4 new), make test (type-check + offline pytest) clean.
⚡ Self-Fix Report (Aggressive)Status: COMPLETE Fixes Applied (12 total)
View all fixes
Tests Added
Skipped (1)
Suggested Follow-up Issues
Validation✅ Lint: N/A — no Self-fix by Archon · aggressive mode · fixes pushed to |
Summary
Makes
hal-mcpcallable from inside a Claude artifact (blocked downstream consumer: bluegreen-marketplace#50, Phases 2-3). Three changes, all insupabase/functions/hal-mcp/index.ts:get_mission_context(mission_id)— returns the full mission context inline (flat header +observations[]+notes[]+photos[]metadata), same shapebuild_context.pyproduces, zero signed URLs. An artifact's CSP blocks all outboundfetch/<img>against Supabase Storage — connector calls are its only network path, so a signed URL is dead weight there.get_mission_photo(photo_id, max_width?)— server-side resize + JPEG re-encode (@imagemagick/magick-wasm, the Deno-Edge-Function-compatible option since nativesharpdoesn't run there), returned as base64 image bytes inline (~1200px, q70, target ≈300KB, under the artifact's 16 MiB page ceiling).readOnlyHintannotations —trueon 12 read tools (list_edifice_missions,read_edifice_mission,get_mission_context,get_mission_photo,list_tasks,list_sprints,list_documents,get_document,list_projects,list_contacts,list_companies,whoami), explicitfalseonpush_mission_context. Works around anthropics/claude-code#57398 (closed, not planned) — unannotated tools fire an Allow/Deny dialog per call with no "Always allow", so a mission-on-load artifact was previously unusable.Also adds a test proving
push_mission_context's partial-update path doesn't clobber sibling fields (behavior was already correct; this closes the acceptance criterion that asks for proof by test, not by reading).Changes
supabase/functions/hal-mcp/index.ts— extractedfetchMissionRaw()(shared raw-fetch helper, no behavior change toget_mission_with_assets), addedget_mission_context+get_mission_photo, addedannotationsto 13registerToolcalls, portedbuildHeader/buildObservationsAndNotesfrombuild_context.pysupabase/functions/hal-mcp/deno.json/deno.lock— added@imagemagick/magick-wasm@0.0.41(wasm fetched from jsDelivr at runtime, memoized per isolate — no local asset, noconfig.tomlchange needed)supabase/functions/hal-mcp/index.test.ts— 5 new tests:get_mission_contextshape/no-URL assertion,get_mission_photoerror paths (unknownphoto_id, storage download failure),tools/listreadOnlyHintassertions,push_mission_contextsingle-field no-clobber proofdocs/prd.md— added the two new tools to the Edifice tool listdocs/cross-repo-log.md,.claude/STATUS.md— cross-repo hygiene entriesNot in scope (per issue)
plugins/hal/.mcp.jsoninbluegreen-marketplace— different repo, tracked via thedocs/cross-repo-log.mdentry as a downstream follow-up once this function is deployed.push_mission_context— issue phrases this as "Consider," not an acceptance criterion; current counts-only response is unchanged.push_mission_context's update logic — confirmed already correct; only a proof test + annotation were added.get_mission_photoif a resized photo exceeds 400KB — fixed resize+encode call per the issue's stated target/ceiling.Validation
deno check --config=deno.json index.ts(bothhal-mcpandedifice-map-generator) — pass, no errorsdeno test(hal-mcp): 68 passed / 0 failed, including the 5 new tests; all 9 pre-existingpush_mission_context+get_mission_with_assetstests pass unmodified (confirmsfetchMissionRaw()extraction is a pure refactor)make test(deno check×2 Edge Functions +uv run pytest tests/ -q -m "not live"): 157 passed, 17 deselecteduv run ruff check hal tests scripts: all checks passedDeliberately unverified (flagged, not claimed)
Two acceptance criteria from the issue require a live deploy against shared prod (
zgkvbjqlvebttbnkklpo) and were not exercised in this PR — deploy needs explicit user go-ahead per this repo's session rules:get_mission_context('3f2bd270-fd5f-4d09-a64e-063a66348dc8')returning the reference mission's 10 notes / 12 photos with zero URL fields — covered by a mocked-shape unit test only.get_mission_photoreturning a real photo under 400 KB, decodable as JPEG — the resize/encode path itself is not unit-tested offline (it needs the ~14.6 MB wasm module, which would makemake testnetwork-dependent); only the tool's error paths (unknownphoto_id, storage download failure) are unit-tested.tools/listshowingreadOnlyHint: truelive on the deployed function — unit-verified via a mockedtools/listcall; live re-check still pending.plugins/hal/.mcp.jsondownstream — deploy not run yet.Fixes #82