mednotes-opencode 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.opencode/agents/med-chat-triager.md +204 -0
- package/.opencode/agents/med-flashcard-maker.md +63 -0
- package/.opencode/agents/med-knowledge-architect.md +230 -0
- package/.opencode/agents/med-link-graph-curator.md +177 -0
- package/.opencode/agents/med-publish-guard.md +62 -0
- package/.opencode/commands/flashcards.md +25 -0
- package/.opencode/commands/mednotes/create.md +25 -0
- package/.opencode/commands/mednotes/enrich.md +27 -0
- package/.opencode/commands/mednotes/fix-wiki.md +27 -0
- package/.opencode/commands/mednotes/history.md +22 -0
- package/.opencode/commands/mednotes/link-body.md +25 -0
- package/.opencode/commands/mednotes/link-related.md +27 -0
- package/.opencode/commands/mednotes/link.md +27 -0
- package/.opencode/commands/mednotes/pdf-library.md +27 -0
- package/.opencode/commands/mednotes/process-chats.md +23 -0
- package/.opencode/commands/mednotes/setup.md +21 -0
- package/.opencode/commands/mednotes/status.md +27 -0
- package/.opencode/commands/mednotes/telemetry.md +27 -0
- package/.opencode/commands/report.md +26 -0
- package/.opencode/mednotes/AGENTS.md +57 -0
- package/.opencode/mednotes/agents/med-chat-triager.md +197 -0
- package/.opencode/mednotes/agents/med-flashcard-maker.md +56 -0
- package/.opencode/mednotes/agents/med-knowledge-architect.md +224 -0
- package/.opencode/mednotes/agents/med-link-graph-curator.md +171 -0
- package/.opencode/mednotes/agents/med-publish-guard.md +55 -0
- package/.opencode/mednotes/contracts/.gitkeep +1 -0
- package/.opencode/mednotes/contracts/agents.json +116 -0
- package/.opencode/mednotes/contracts/opencode-plugin.json +70 -0
- package/.opencode/mednotes/docs/agent-prompt-hardening.md +567 -0
- package/.opencode/mednotes/docs/agent-role-contracts.md +94 -0
- package/.opencode/mednotes/docs/anki-mcp-twenty-rules.md +214 -0
- package/.opencode/mednotes/docs/anki-templates/README.md +39 -0
- package/.opencode/mednotes/docs/anki-templates/cloze.back.html +23 -0
- package/.opencode/mednotes/docs/anki-templates/cloze.front.html +14 -0
- package/.opencode/mednotes/docs/anki-templates/qa.back.html +24 -0
- package/.opencode/mednotes/docs/anki-templates/qa.front.html +14 -0
- package/.opencode/mednotes/docs/anki-templates/style.css +182 -0
- package/.opencode/mednotes/docs/atomicity-splitting-policy.md +113 -0
- package/.opencode/mednotes/docs/extension-docs.md +40 -0
- package/.opencode/mednotes/docs/flashcard-ingestion.md +278 -0
- package/.opencode/mednotes/docs/knowledge-architect.md +208 -0
- package/.opencode/mednotes/docs/merge-policy.md +110 -0
- package/.opencode/mednotes/docs/public-vocabulary.md +104 -0
- package/.opencode/mednotes/docs/semantic-linker.md +141 -0
- package/.opencode/mednotes/docs/taxonomy-policy.md +90 -0
- package/.opencode/mednotes/docs/triage-policy.md +187 -0
- package/.opencode/mednotes/docs/vault-version-control.md +758 -0
- package/.opencode/mednotes/docs/vocabulary-db-recovery.md +58 -0
- package/.opencode/mednotes/docs/workflow-output-contract.md +779 -0
- package/.opencode/mednotes/hooks/hooks.json +79 -0
- package/.opencode/mednotes/package-lock.json +6361 -0
- package/.opencode/mednotes/package.json +15 -0
- package/.opencode/mednotes/pyproject.toml +48 -0
- package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.cmd +13 -0
- package/.opencode/mednotes/scripts/bootstrap_windows_python_uv.ps1 +172 -0
- package/.opencode/mednotes/scripts/enrich_notes.py +23 -0
- package/.opencode/mednotes/scripts/full_reset_windows_python_uv.cmd +13 -0
- package/.opencode/mednotes/scripts/hooks/antigravity_hook_status.mjs +212 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/antigravity.mjs +169 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/harness_payload.mjs +103 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/anki_preflight.mjs +214 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/cli.mjs +143 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/diagnostics.mjs +11 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/domain/agent_directive_core.mjs +160 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/fsm_directive.mjs +1470 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/hook_errors.mjs +120 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/retention.mjs +114 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/runtime.mjs +174 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/telemetry_capture.mjs +511 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook/vault_guard.mjs +624 -0
- package/.opencode/mednotes/scripts/hooks/mednotes_hook.mjs +5 -0
- package/.opencode/mednotes/scripts/mednotes/_runtime_paths.py +24 -0
- package/.opencode/mednotes/scripts/mednotes/anki_model_validator.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/capture_extension_diff.py +1562 -0
- package/.opencode/mednotes/scripts/mednotes/feedback_report.py +16 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_index.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_pipeline.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_report.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/flashcard_sources.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/obsidian/README.md +6 -0
- package/.opencode/mednotes/scripts/mednotes/obsidian_note_utils.py +20 -0
- package/.opencode/mednotes/scripts/mednotes/pdf_library/cli.py +16 -0
- package/.opencode/mednotes/scripts/mednotes/project_fsm.py +229 -0
- package/.opencode/mednotes/scripts/mednotes/setup_telemetry_email.py +404 -0
- package/.opencode/mednotes/scripts/mednotes/sync_anki_twenty_rules.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/sync_opencode_user_config.py +36 -0
- package/.opencode/mednotes/scripts/mednotes/wiki/cli.py +20 -0
- package/.opencode/mednotes/scripts/mednotes/wiki_graph.py +18 -0
- package/.opencode/mednotes/scripts/mednotes/wiki_tree.py +134 -0
- package/.opencode/mednotes/scripts/reset_windows_python_uv.ps1 +625 -0
- package/.opencode/mednotes/scripts/run_python.mjs +109 -0
- package/.opencode/mednotes/scripts/vault/vault_commit.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_commit.sh +18 -0
- package/.opencode/mednotes/scripts/vault/vault_git.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_git.py +3107 -0
- package/.opencode/mednotes/scripts/vault/vault_git.sh +18 -0
- package/.opencode/mednotes/scripts/vault/vault_precommit.ps1 +19 -0
- package/.opencode/mednotes/scripts/vault/vault_precommit.sh +18 -0
- package/.opencode/mednotes/skills/THIRD_PARTY_NOTICES.md +45 -0
- package/.opencode/mednotes/skills/create-medical-flashcards/SKILL.md +113 -0
- package/.opencode/mednotes/skills/create-medical-note/SKILL.md +90 -0
- package/.opencode/mednotes/skills/enrich-medical-note/SKILL.md +120 -0
- package/.opencode/mednotes/skills/fix-medical-wiki/SKILL.md +559 -0
- package/.opencode/mednotes/skills/link-medical-wiki/SKILL.md +224 -0
- package/.opencode/mednotes/skills/obsidian-cli/SKILL.md +118 -0
- package/.opencode/mednotes/skills/obsidian-markdown/SKILL.md +207 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/EMBEDS.md +63 -0
- package/.opencode/mednotes/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
- package/.opencode/mednotes/skills/obsidian-ops/SKILL.md +136 -0
- package/.opencode/mednotes/skills/pdf-library/SKILL.md +45 -0
- package/.opencode/mednotes/skills/process-medical-chats/SKILL.md +246 -0
- package/.opencode/mednotes/skills/workflow-report/SKILL.md +100 -0
- package/.opencode/mednotes/src/mednotes/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/domains/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/README.md +26 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/build_demo_apkg.py +177 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/contracts.py +385 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/flashcards_machine.py +522 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/fsm.py +817 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/index.py +630 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/install_models.py +445 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/model.py +359 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_links.py +135 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/obsidian_note_utils.py +546 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/pipeline.py +580 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/report.py +510 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/sources.py +682 -0
- package/.opencode/mednotes/src/mednotes/domains/flashcards/sync_rules.py +184 -0
- package/.opencode/mednotes/src/mednotes/domains/history/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/history/history_fsm.py +852 -0
- package/.opencode/mednotes/src/mednotes/domains/history/history_machine.py +453 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/__init__.py +7 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/setup_fsm.py +808 -0
- package/.opencode/mednotes/src/mednotes/domains/setup/setup_machine.py +973 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/README.md +64 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/api.py +668 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/batch_state.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/atomicity/atomicity.py +877 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/body_link/body_linker.py +1562 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/effect_adapters.py +949 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/effects/fix_wiki_runtime_adapters.py +433 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/coverage.py +413 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph.py +396 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/graph/graph_fixes.py +161 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/hygiene/hygiene.py +483 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/anchors.py +185 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/__init__.py +0 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/cache.py +223 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/config.py +131 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/download.py +224 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/frontmatter.py +59 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/insert.py +227 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/core/local_import.py +54 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/__init__.py +42 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_profiles.py +99 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/web_search.py +203 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/illustrate/sources/wikimedia.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_db_adapter.mjs +434 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_node_runtime.py +274 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/markdown/markdown_query.py +227 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/artifacts.py +605 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/canonical_merge.py +277 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/markdown_zones.py +85 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/meaning_planner.py +307 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_iter.py +67 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_merge.py +278 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_plan.py +409 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_policy.py +22 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/__init__.py +79 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/fixes.py +264 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/frontmatter.py +435 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/models.py +208 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/prompts.py +37 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/tables.py +236 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/note_style/validate.py +404 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/provenance.py +478 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/raw_chats.py +273 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/notes/sources_backfill.py +235 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/__init__.py +10 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/anchors.py +16 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/captions.py +47 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cli.py +179 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/cloud.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/config.py +196 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/context_packets.py +76 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/db.py +81 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/doctor.py +102 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/figure_ids.py +42 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ingest.py +326 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/insert.py +316 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/mentions.py +57 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/ocr.py +71 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/paths.py +35 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/pdf_engine.py +77 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/schema.py +155 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/search.py +188 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/app.py +89 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/image_backend.py +29 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/pdf/tui/state.py +65 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish.py +1139 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_receipts.py +365 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/publish/publish_recovery.py +240 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_behavior_corpus.py +2069 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_report_validation.py +4448 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/agent_run_audit.py +852 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/architect_prompt_eval.py +341 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/body_linker_eval.py +240 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_output_validation.py +175 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/curator_prompt_eval.py +865 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/quality/triager_prompt_eval.py +1295 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes.py +1920 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/related_notes/related_notes_headless.py +1186 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/plan_attestation.py +148 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_receipts.py +360 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_runtime.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_task_runner.py +2470 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/style/style.py +1952 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/subagents/agents.py +1767 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/alias_projection.py +331 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/link_terms.py +151 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/llm_disambiguation.py +182 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/__init__.py +116 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/audit.py +201 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/migration.py +314 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/normalize.py +72 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/policy.py +135 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/resolve.py +413 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/schema.py +157 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/taxonomy/status.py +137 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_bootstrap.py +509 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_curator_batch.py +1115 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_ingestion.py +632 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_map.py +930 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/vocabulary/vocabulary_recovery.py +1388 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/cli.py +6665 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/common.py +69 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/config.py +210 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/__init__.py +74 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_report.py +242 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agent_run_audit.py +196 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/agents.py +601 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/curator.py +256 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/effect_payloads.py +519 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/happy_path.py +190 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_git.py +110 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/link_runtime_artifact.py +52 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/note_plan.py +75 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/paths.py +114 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/public_report.py +53 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/publish.py +111 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/raw_coverage.py +217 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes.py +136 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_headless.py +153 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/related_notes_runtime.py +395 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/schema_registry.py +637 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/specialist.py +432 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/status.py +62 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/style_rewrite.py +568 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/vocabulary_ingestion.py +223 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_blockers.py +510 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_guardrails.py +637 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_outcomes.py +121 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/contracts/workflow_receipts.py +100 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/__main__.py +4 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/cli.py +275 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/__init__.py +2 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/candidates.py +193 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/cli.py +189 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/gemini.py +220 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/inputs.py +120 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/models.py +34 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/parsing.py +48 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/prompts.py +216 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/quality.py +54 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/reporting.py +24 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/runner.py +433 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/utils.py +39 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/enrich/workflow/vault_guard_bridge.py +17 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_context_packets.py +454 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_decision_projection.py +133 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_effects.py +1260 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_fsm.py +2768 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_machine.py +1588 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_plan.py +306 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_primary_objective.py +316 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_problem.py +153 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_receipt_evidence.py +306 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_states.py +290 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/fix_wiki_user_report.py +342 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/fix_wiki/health.py +6332 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_fsm.py +1119 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_git.py +638 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_machine.py +1106 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_retry_governance.py +374 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_runtime_result.py +485 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/link_triggers.py +183 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/linking.py +2758 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/reference_repair.py +718 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link/related_notes_fsm.py +1855 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/link_related/link_related_machine.py +834 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/__init__.py +1 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_fsm.py +1592 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_machine.py +3097 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_primary_objective.py +28 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/flows/process_chats/process_chats_runtime_result.py +185 -0
- package/.opencode/mednotes/src/mednotes/domains/wiki/performance.py +97 -0
- package/.opencode/mednotes/src/mednotes/kernel/__init__.py +6 -0
- package/.opencode/mednotes/src/mednotes/kernel/agent_directive.py +336 -0
- package/.opencode/mednotes/src/mednotes/kernel/base.py +51 -0
- package/.opencode/mednotes/src/mednotes/kernel/blockers.py +39 -0
- package/.opencode/mednotes/src/mednotes/kernel/effect_executor.py +55 -0
- package/.opencode/mednotes/src/mednotes/kernel/effect_intent.py +69 -0
- package/.opencode/mednotes/src/mednotes/kernel/effects.py +160 -0
- package/.opencode/mednotes/src/mednotes/kernel/errors.py +38 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_event.py +35 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_model.py +55 -0
- package/.opencode/mednotes/src/mednotes/kernel/fsm_transition_result.py +75 -0
- package/.opencode/mednotes/src/mednotes/kernel/guardrails.py +188 -0
- package/.opencode/mednotes/src/mednotes/kernel/progress.py +319 -0
- package/.opencode/mednotes/src/mednotes/kernel/public_report.py +346 -0
- package/.opencode/mednotes/src/mednotes/kernel/state_machine.py +164 -0
- package/.opencode/mednotes/src/mednotes/kernel/workflow.py +619 -0
- package/.opencode/mednotes/src/mednotes/platform/__init__.py +5 -0
- package/.opencode/mednotes/src/mednotes/platform/backup_policy.py +382 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/__init__.py +62 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/cli.py +275 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/contracts.py +83 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/core.py +4168 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/integrity.py +989 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/operational_contract.py +2293 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry.py +875 -0
- package/.opencode/mednotes/src/mednotes/platform/feedback/telemetry_config.py +65 -0
- package/.opencode/mednotes/src/mednotes/platform/opencode_runtime_config.py +182 -0
- package/.opencode/mednotes/src/mednotes/platform/paths/__init__.py +1560 -0
- package/.opencode/mednotes/src/mednotes/platform/secrets.py +89 -0
- package/.opencode/mednotes/src/mednotes/platform/user_config.py +103 -0
- package/.opencode/mednotes/src/mednotes/platform/vault_guard.py +214 -0
- package/.opencode/mednotes/uv.lock +932 -0
- package/.opencode/mednotes.generated.json +395 -0
- package/.opencode/opencode.json +31 -0
- package/.opencode/plugins/mednotes-fsm.mjs +7 -0
- package/.opencode/plugins/mednotes_hook/adapters/antigravity.mjs +169 -0
- package/.opencode/plugins/mednotes_hook/adapters/harness_payload.mjs +103 -0
- package/.opencode/plugins/mednotes_hook/adapters/opencode_plugin.mjs +341 -0
- package/.opencode/plugins/mednotes_hook/adapters/opencode_user_config_sync.mjs +177 -0
- package/.opencode/plugins/mednotes_hook/anki_preflight.mjs +214 -0
- package/.opencode/plugins/mednotes_hook/cli.mjs +143 -0
- package/.opencode/plugins/mednotes_hook/diagnostics.mjs +11 -0
- package/.opencode/plugins/mednotes_hook/domain/agent_directive_core.mjs +160 -0
- package/.opencode/plugins/mednotes_hook/fsm_directive.mjs +1470 -0
- package/.opencode/plugins/mednotes_hook/hook_errors.mjs +120 -0
- package/.opencode/plugins/mednotes_hook/retention.mjs +114 -0
- package/.opencode/plugins/mednotes_hook/runtime.mjs +174 -0
- package/.opencode/plugins/mednotes_hook/telemetry_capture.mjs +511 -0
- package/.opencode/plugins/mednotes_hook/vault_guard.mjs +624 -0
- package/AGENTS.md +57 -0
- package/README.md +194 -0
- package/adapters/antigravity/agents.json +80 -0
- package/adapters/antigravity/templates/med-chat-triager.md +214 -0
- package/adapters/antigravity/templates/med-flashcard-maker.md +72 -0
- package/adapters/antigravity/templates/med-knowledge-architect.md +241 -0
- package/adapters/antigravity/templates/med-link-graph-curator.md +187 -0
- package/adapters/antigravity/templates/med-publish-guard.md +71 -0
- package/adapters/gemini-cli/gemini-extension.json +14 -0
- package/adapters/gemini-cli/package.json +15 -0
- package/adapters/gemini-cli/pyproject.toml +48 -0
- package/bin/mednotes-opencode.mjs +155 -0
- package/contracts/agents.json +116 -0
- package/core/agents/med-chat-triager.md +197 -0
- package/core/agents/med-flashcard-maker.md +56 -0
- package/core/agents/med-knowledge-architect.md +224 -0
- package/core/agents/med-link-graph-curator.md +171 -0
- package/core/agents/med-publish-guard.md +55 -0
- package/core/commands/flashcards.toml +22 -0
- package/core/commands/mednotes/create.toml +22 -0
- package/core/commands/mednotes/enrich.toml +24 -0
- package/core/commands/mednotes/fix-wiki.toml +24 -0
- package/core/commands/mednotes/history.toml +19 -0
- package/core/commands/mednotes/link-body.toml +22 -0
- package/core/commands/mednotes/link-related.toml +24 -0
- package/core/commands/mednotes/link.toml +24 -0
- package/core/commands/mednotes/pdf-library.toml +24 -0
- package/core/commands/mednotes/process-chats.toml +20 -0
- package/core/commands/mednotes/setup.toml +18 -0
- package/core/commands/mednotes/status.toml +24 -0
- package/core/commands/mednotes/telemetry.toml +24 -0
- package/core/commands/report.toml +23 -0
- package/core/skills/THIRD_PARTY_NOTICES.md +45 -0
- package/core/skills/create-medical-flashcards/SKILL.md +113 -0
- package/core/skills/create-medical-note/SKILL.md +90 -0
- package/core/skills/enrich-medical-note/SKILL.md +120 -0
- package/core/skills/fix-medical-wiki/SKILL.md +559 -0
- package/core/skills/link-medical-wiki/SKILL.md +224 -0
- package/core/skills/obsidian-cli/SKILL.md +118 -0
- package/core/skills/obsidian-markdown/SKILL.md +207 -0
- package/core/skills/obsidian-markdown/references/CALLOUTS.md +58 -0
- package/core/skills/obsidian-markdown/references/EMBEDS.md +63 -0
- package/core/skills/obsidian-markdown/references/PROPERTIES.md +61 -0
- package/core/skills/obsidian-ops/SKILL.md +136 -0
- package/core/skills/pdf-library/SKILL.md +45 -0
- package/core/skills/process-medical-chats/SKILL.md +246 -0
- package/core/skills/workflow-report/SKILL.md +100 -0
- package/package.json +45 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""specialist."""
|
package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/plan_attestation.py
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Workbench attestation for subagent plans."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import hashlib
|
|
5
|
+
import hmac
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import secrets
|
|
9
|
+
from datetime import UTC, datetime
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
14
|
+
|
|
15
|
+
from mednotes.domains.wiki.common import MissingPathError, ValidationError
|
|
16
|
+
from mednotes.domains.wiki.config import _user_state_dir
|
|
17
|
+
from mednotes.domains.wiki.contracts.agents import SubagentBatchPlan, SubagentPlanAttestation
|
|
18
|
+
from mednotes.kernel.base import contract_error
|
|
19
|
+
|
|
20
|
+
SUBAGENT_PLAN_ATTESTATION_SCHEMA = "medical-notes-workbench.subagent-plan-attestation.v1"
|
|
21
|
+
SUBAGENT_PLAN_ATTESTATION_KIND = "workbench_hmac_sha256.v1"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def canonical_subagent_plan_payload(payload: dict[str, Any]) -> dict[str, Any]:
|
|
25
|
+
return {key: value for key, value in payload.items() if key != "plan_attestation"}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def subagent_plan_hash(payload: dict[str, Any]) -> str:
|
|
29
|
+
unsigned = canonical_subagent_plan_payload(payload)
|
|
30
|
+
encoded = json.dumps(unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
31
|
+
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _subagent_plan_attestation_key_path() -> Path:
|
|
35
|
+
configured = os.getenv("MEDNOTES_SUBAGENT_PLAN_ATTESTATION_KEY_PATH", "").strip()
|
|
36
|
+
if configured:
|
|
37
|
+
return Path(configured).expanduser()
|
|
38
|
+
return _user_state_dir() / "subagent-plan-attestation.key"
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _subagent_plan_attestation_key(*, create: bool) -> bytes:
|
|
42
|
+
configured = os.getenv("MEDNOTES_SUBAGENT_PLAN_ATTESTATION_KEY", "").strip()
|
|
43
|
+
if configured:
|
|
44
|
+
return configured.encode("utf-8")
|
|
45
|
+
key_path = _subagent_plan_attestation_key_path()
|
|
46
|
+
if key_path.exists():
|
|
47
|
+
return key_path.read_bytes().strip()
|
|
48
|
+
if not create:
|
|
49
|
+
raise MissingPathError(f"subagent plan attestation key not found: {key_path}")
|
|
50
|
+
key_path.parent.mkdir(parents=True, exist_ok=True)
|
|
51
|
+
key = secrets.token_hex(32).encode("ascii")
|
|
52
|
+
tmp_path = key_path.with_name(f"{key_path.name}.tmp")
|
|
53
|
+
tmp_path.write_bytes(key + b"\n")
|
|
54
|
+
try:
|
|
55
|
+
os.chmod(tmp_path, 0o600)
|
|
56
|
+
except OSError:
|
|
57
|
+
pass
|
|
58
|
+
os.replace(tmp_path, key_path)
|
|
59
|
+
return key
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _attestation_signing_payload(payload: dict[str, Any]) -> bytes:
|
|
63
|
+
unsigned = {key: value for key, value in payload.items() if key != "signature"}
|
|
64
|
+
return json.dumps(unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _attestation_signature(payload: dict[str, Any], *, create_key: bool) -> str:
|
|
68
|
+
digest = hmac.new(
|
|
69
|
+
_subagent_plan_attestation_key(create=create_key),
|
|
70
|
+
_attestation_signing_payload(payload),
|
|
71
|
+
hashlib.sha256,
|
|
72
|
+
).hexdigest()
|
|
73
|
+
return f"hmac-sha256:{digest}"
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _verify_attestation_signature(payload: dict[str, Any]) -> bool:
|
|
77
|
+
try:
|
|
78
|
+
expected = _attestation_signature(payload, create_key=False)
|
|
79
|
+
except MissingPathError:
|
|
80
|
+
return False
|
|
81
|
+
return hmac.compare_digest(str(payload.get("signature") or ""), expected)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _typed_subagent_plan_for_attestation(payload: dict[str, Any]) -> SubagentBatchPlan:
|
|
85
|
+
"""Validate the full plan before its identity fields participate in signing."""
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
return SubagentBatchPlan.model_validate(payload)
|
|
89
|
+
except PydanticValidationError as exc:
|
|
90
|
+
raise contract_error(exc, prefix="subagent plan attestation payload invalid") from exc
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def build_subagent_plan_attestation(payload: dict[str, Any]) -> dict[str, Any]:
|
|
94
|
+
plan = _typed_subagent_plan_for_attestation(payload)
|
|
95
|
+
attestation_payload: dict[str, Any] = {
|
|
96
|
+
"schema": SUBAGENT_PLAN_ATTESTATION_SCHEMA,
|
|
97
|
+
"phase": plan.phase,
|
|
98
|
+
"plan_schema": plan.schema_,
|
|
99
|
+
"plan_hash": subagent_plan_hash(payload),
|
|
100
|
+
"attestation_kind": SUBAGENT_PLAN_ATTESTATION_KIND,
|
|
101
|
+
"created_by": "plan-subagents",
|
|
102
|
+
"issued_at": datetime.now(UTC).isoformat(timespec="seconds"),
|
|
103
|
+
"nonce": secrets.token_hex(16),
|
|
104
|
+
}
|
|
105
|
+
attestation_payload["signature"] = _attestation_signature(attestation_payload, create_key=True)
|
|
106
|
+
try:
|
|
107
|
+
attestation = SubagentPlanAttestation.model_validate(attestation_payload)
|
|
108
|
+
except PydanticValidationError as exc:
|
|
109
|
+
raise contract_error(exc, prefix="subagent plan attestation invalid") from exc
|
|
110
|
+
return attestation.model_dump(mode="json", by_alias=True)
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def attach_subagent_plan_attestation(payload: dict[str, Any]) -> dict[str, Any]:
|
|
114
|
+
attested = canonical_subagent_plan_payload(payload)
|
|
115
|
+
attested["plan_attestation"] = build_subagent_plan_attestation(attested)
|
|
116
|
+
return attested
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def validate_subagent_plan_attestation(payload: dict[str, Any]) -> str:
|
|
120
|
+
raw_attestation = payload.get("plan_attestation")
|
|
121
|
+
if isinstance(raw_attestation, dict):
|
|
122
|
+
expected_hash = subagent_plan_hash(payload)
|
|
123
|
+
if str(raw_attestation.get("plan_hash") or "") != expected_hash:
|
|
124
|
+
raise ValidationError("subagent plan attestation invalid: plan_hash")
|
|
125
|
+
plan = _typed_subagent_plan_for_attestation(payload)
|
|
126
|
+
if plan.plan_attestation is None:
|
|
127
|
+
raise ValidationError("subagent plan attestation required")
|
|
128
|
+
attestation = plan.plan_attestation
|
|
129
|
+
expected_hash = subagent_plan_hash(payload)
|
|
130
|
+
if attestation.plan_hash != expected_hash:
|
|
131
|
+
raise ValidationError("subagent plan attestation invalid: plan_hash")
|
|
132
|
+
if attestation.phase != plan.phase:
|
|
133
|
+
raise ValidationError("subagent plan attestation invalid: phase")
|
|
134
|
+
if attestation.plan_schema != plan.schema_:
|
|
135
|
+
raise ValidationError("subagent plan attestation invalid: plan_schema")
|
|
136
|
+
if attestation.attestation_kind != SUBAGENT_PLAN_ATTESTATION_KIND:
|
|
137
|
+
raise ValidationError("subagent plan attestation invalid: attestation_kind")
|
|
138
|
+
if not _verify_attestation_signature(attestation.to_payload()):
|
|
139
|
+
raise ValidationError("subagent plan attestation invalid: signature")
|
|
140
|
+
return expected_hash
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def subagent_plan_attestation_blocked_reason(exc: Exception) -> str:
|
|
144
|
+
return (
|
|
145
|
+
"subagent_plan_attestation_required"
|
|
146
|
+
if "attestation required" in str(exc).lower()
|
|
147
|
+
else "subagent_plan_attestation_invalid"
|
|
148
|
+
)
|
package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_receipts.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""Workbench attestation for specialist task run receipts."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import binascii
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import secrets
|
|
10
|
+
from datetime import UTC, datetime
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Literal
|
|
13
|
+
|
|
14
|
+
from cryptography.exceptions import InvalidSignature
|
|
15
|
+
from cryptography.hazmat.primitives import serialization
|
|
16
|
+
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey, Ed25519PublicKey
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
from pydantic import ValidationError as PydanticValidationError
|
|
19
|
+
|
|
20
|
+
from mednotes.domains.wiki.common import MissingPathError, ValidationError
|
|
21
|
+
from mednotes.domains.wiki.config import _user_state_dir
|
|
22
|
+
from mednotes.domains.wiki.contracts.specialist import (
|
|
23
|
+
SpecialistHarness,
|
|
24
|
+
SpecialistModelEvidence,
|
|
25
|
+
SpecialistOutputAttestationReference,
|
|
26
|
+
SpecialistOutputReceiptReference,
|
|
27
|
+
SpecialistQualityReviewStatus,
|
|
28
|
+
SpecialistRunStatus,
|
|
29
|
+
SpecialistTaskPhase,
|
|
30
|
+
SpecialistTaskRunReceipt,
|
|
31
|
+
SpecialistTaskRunReceiptAttestation,
|
|
32
|
+
SpecialistValidationStatus,
|
|
33
|
+
)
|
|
34
|
+
from mednotes.kernel.base import ContractModel, JsonObject, JsonObjectAdapter, contract_error
|
|
35
|
+
|
|
36
|
+
SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_SCHEMA = (
|
|
37
|
+
"medical-notes-workbench.specialist-task-run-receipt-attestation.v1"
|
|
38
|
+
)
|
|
39
|
+
SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_KIND = "workbench_ed25519.v1"
|
|
40
|
+
SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_CREATED_BY = "specialist-task-runner"
|
|
41
|
+
_PRIVATE_KEY_ENV = "MEDNOTES_SPECIALIST_TASK_RECEIPT_ATTESTATION_PRIVATE_KEY"
|
|
42
|
+
_PRIVATE_KEY_PATH_ENV = "MEDNOTES_SPECIALIST_TASK_RECEIPT_ATTESTATION_PRIVATE_KEY_PATH"
|
|
43
|
+
_PUBLIC_KEY_ENV = "MEDNOTES_SPECIALIST_TASK_RECEIPT_ATTESTATION_PUBLIC_KEY"
|
|
44
|
+
_PUBLIC_KEY_PATH_ENV = "MEDNOTES_SPECIALIST_TASK_RECEIPT_ATTESTATION_PUBLIC_KEY_PATH"
|
|
45
|
+
_DEFAULT_PRIVATE_KEY_NAME = "specialist-task-receipt-attestation.ed25519.private.key"
|
|
46
|
+
_DEFAULT_PUBLIC_KEY_NAME = "specialist-task-receipt-attestation.ed25519.public.key"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class _UnsignedSpecialistTaskRunReceipt(ContractModel):
|
|
50
|
+
"""Closed pre-signing view of the receipt fields bound into attestation."""
|
|
51
|
+
|
|
52
|
+
schema_id: Literal["medical-notes-workbench.specialist-task-run-receipt.v1"] = Field(
|
|
53
|
+
default="medical-notes-workbench.specialist-task-run-receipt.v1",
|
|
54
|
+
alias="schema",
|
|
55
|
+
)
|
|
56
|
+
work_id: str = Field(min_length=1)
|
|
57
|
+
phase: SpecialistTaskPhase
|
|
58
|
+
harness: SpecialistHarness
|
|
59
|
+
adapter: str = Field(min_length=1)
|
|
60
|
+
requested_agent: str = Field(min_length=1)
|
|
61
|
+
requested_model_policy: str = Field(min_length=1)
|
|
62
|
+
requested_model: str = Field(min_length=1)
|
|
63
|
+
observed_model: str = ""
|
|
64
|
+
model_evidence: SpecialistModelEvidence | None = None
|
|
65
|
+
input_packet_path: str = Field(min_length=1)
|
|
66
|
+
input_packet_sha256: str = Field(pattern=r"^sha256:[0-9a-f]{64}$")
|
|
67
|
+
output_path: str = ""
|
|
68
|
+
output_sha256: str = ""
|
|
69
|
+
status: SpecialistRunStatus
|
|
70
|
+
validation_status: SpecialistValidationStatus
|
|
71
|
+
quality_review_status: SpecialistQualityReviewStatus
|
|
72
|
+
parent_session_id: str = ""
|
|
73
|
+
specialist_session_id: str = ""
|
|
74
|
+
transcript_artifact_path: str = ""
|
|
75
|
+
transcript_artifact_sha256: str = ""
|
|
76
|
+
error_context: JsonObject = Field(default_factory=dict)
|
|
77
|
+
next_action: str = ""
|
|
78
|
+
specialist_output_receipt: SpecialistOutputReceiptReference | None = None
|
|
79
|
+
specialist_output_attestation: SpecialistOutputAttestationReference | None = None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _unsigned_receipt_for_attestation(payload: JsonObject) -> _UnsignedSpecialistTaskRunReceipt:
|
|
83
|
+
try:
|
|
84
|
+
return _UnsignedSpecialistTaskRunReceipt.model_validate(payload)
|
|
85
|
+
except PydanticValidationError as exc:
|
|
86
|
+
raise contract_error(exc, prefix="specialist task run receipt payload invalid") from exc
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _receipt_without_attestation(payload: JsonObject) -> JsonObject:
|
|
90
|
+
return JsonObjectAdapter.validate_python(
|
|
91
|
+
{key: value for key, value in payload.items() if key != "receipt_attestation"}
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def specialist_task_run_receipt_hash(payload: JsonObject) -> str:
|
|
96
|
+
encoded = json.dumps(
|
|
97
|
+
_receipt_without_attestation(payload),
|
|
98
|
+
ensure_ascii=False,
|
|
99
|
+
sort_keys=True,
|
|
100
|
+
separators=(",", ":"),
|
|
101
|
+
).encode("utf-8")
|
|
102
|
+
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _base64_decode_key(raw: str, *, label: str) -> bytes:
|
|
106
|
+
compact = raw.strip()
|
|
107
|
+
if not compact:
|
|
108
|
+
raise ValidationError(f"specialist task run receipt attestation {label} required")
|
|
109
|
+
try:
|
|
110
|
+
return base64.b64decode(compact, validate=True)
|
|
111
|
+
except (ValueError, binascii.Error) as exc:
|
|
112
|
+
raise ValidationError(f"specialist task run receipt attestation {label} must be base64") from exc
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _key_bytes_from_env_or_path(
|
|
116
|
+
*,
|
|
117
|
+
env_name: str,
|
|
118
|
+
path_env_name: str,
|
|
119
|
+
label: str,
|
|
120
|
+
) -> bytes | None:
|
|
121
|
+
configured = os.getenv(env_name, "").strip()
|
|
122
|
+
if configured:
|
|
123
|
+
return _base64_decode_key(configured, label=label)
|
|
124
|
+
configured_path = os.getenv(path_env_name, "").strip()
|
|
125
|
+
if configured_path:
|
|
126
|
+
key_path = Path(configured_path).expanduser()
|
|
127
|
+
if not key_path.exists():
|
|
128
|
+
raise MissingPathError(f"specialist task run receipt attestation {label} not found: {key_path}")
|
|
129
|
+
return _base64_decode_key(key_path.read_text(encoding="utf-8"), label=label)
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _local_private_key_path() -> Path:
|
|
134
|
+
return _user_state_dir() / _DEFAULT_PRIVATE_KEY_NAME
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _local_public_key_path() -> Path:
|
|
138
|
+
return _user_state_dir() / _DEFAULT_PUBLIC_KEY_NAME
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _write_local_key(path: Path, key_bytes: bytes) -> None:
|
|
142
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
143
|
+
encoded = base64.b64encode(key_bytes) + b"\n"
|
|
144
|
+
tmp_path = path.with_name(f"{path.name}.tmp")
|
|
145
|
+
tmp_path.write_bytes(encoded)
|
|
146
|
+
try:
|
|
147
|
+
os.chmod(tmp_path, 0o600)
|
|
148
|
+
except OSError:
|
|
149
|
+
pass
|
|
150
|
+
os.replace(tmp_path, path)
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _private_key_raw_bytes(private_key: Ed25519PrivateKey) -> bytes:
|
|
154
|
+
return private_key.private_bytes(
|
|
155
|
+
encoding=serialization.Encoding.Raw,
|
|
156
|
+
format=serialization.PrivateFormat.Raw,
|
|
157
|
+
encryption_algorithm=serialization.NoEncryption(),
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _public_key_raw_bytes(public_key: Ed25519PublicKey) -> bytes:
|
|
162
|
+
return public_key.public_bytes(
|
|
163
|
+
encoding=serialization.Encoding.Raw,
|
|
164
|
+
format=serialization.PublicFormat.Raw,
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _create_local_key_pair() -> bytes:
|
|
169
|
+
private_key = Ed25519PrivateKey.generate()
|
|
170
|
+
private_bytes = _private_key_raw_bytes(private_key)
|
|
171
|
+
_write_local_key(_local_private_key_path(), private_bytes)
|
|
172
|
+
_write_local_key(_local_public_key_path(), _public_key_raw_bytes(private_key.public_key()))
|
|
173
|
+
return private_bytes
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def _local_private_key_bytes(*, create: bool) -> bytes:
|
|
177
|
+
private_path = _local_private_key_path()
|
|
178
|
+
if private_path.exists():
|
|
179
|
+
return _base64_decode_key(private_path.read_text(encoding="utf-8"), label="private signing key")
|
|
180
|
+
if create:
|
|
181
|
+
return _create_local_key_pair()
|
|
182
|
+
raise MissingPathError(f"specialist task run receipt attestation private signing key not found: {private_path}")
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _derive_local_public_key_from_private() -> bytes | None:
|
|
186
|
+
private_path = _local_private_key_path()
|
|
187
|
+
if not private_path.exists():
|
|
188
|
+
return None
|
|
189
|
+
private_bytes = _base64_decode_key(private_path.read_text(encoding="utf-8"), label="private signing key")
|
|
190
|
+
try:
|
|
191
|
+
private_key = Ed25519PrivateKey.from_private_bytes(private_bytes)
|
|
192
|
+
except ValueError as exc:
|
|
193
|
+
raise ValidationError("specialist task run receipt attestation private signing key invalid") from exc
|
|
194
|
+
public_bytes = _public_key_raw_bytes(private_key.public_key())
|
|
195
|
+
_write_local_key(_local_public_key_path(), public_bytes)
|
|
196
|
+
return public_bytes
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def _local_public_key_bytes() -> bytes:
|
|
200
|
+
public_path = _local_public_key_path()
|
|
201
|
+
if public_path.exists():
|
|
202
|
+
return _base64_decode_key(public_path.read_text(encoding="utf-8"), label="trusted public key")
|
|
203
|
+
derived = _derive_local_public_key_from_private()
|
|
204
|
+
if derived is not None:
|
|
205
|
+
return derived
|
|
206
|
+
raise MissingPathError(f"specialist task run receipt attestation trusted public key not found: {public_path}")
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _private_key() -> Ed25519PrivateKey:
|
|
210
|
+
key_bytes = _key_bytes_from_env_or_path(
|
|
211
|
+
env_name=_PRIVATE_KEY_ENV,
|
|
212
|
+
path_env_name=_PRIVATE_KEY_PATH_ENV,
|
|
213
|
+
label="private signing key",
|
|
214
|
+
) or _local_private_key_bytes(create=True)
|
|
215
|
+
try:
|
|
216
|
+
return Ed25519PrivateKey.from_private_bytes(key_bytes)
|
|
217
|
+
except ValueError as exc:
|
|
218
|
+
raise ValidationError("specialist task run receipt attestation private signing key invalid") from exc
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def _public_key() -> Ed25519PublicKey:
|
|
222
|
+
key_bytes = _key_bytes_from_env_or_path(
|
|
223
|
+
env_name=_PUBLIC_KEY_ENV,
|
|
224
|
+
path_env_name=_PUBLIC_KEY_PATH_ENV,
|
|
225
|
+
label="trusted public key",
|
|
226
|
+
) or _local_public_key_bytes()
|
|
227
|
+
try:
|
|
228
|
+
return Ed25519PublicKey.from_public_bytes(key_bytes)
|
|
229
|
+
except ValueError as exc:
|
|
230
|
+
raise ValidationError("specialist task run receipt attestation trusted public key invalid") from exc
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _public_key_bytes(public_key: Ed25519PublicKey) -> bytes:
|
|
234
|
+
return _public_key_raw_bytes(public_key)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def _public_key_id(public_key: Ed25519PublicKey) -> str:
|
|
238
|
+
return "sha256:" + hashlib.sha256(_public_key_bytes(public_key)).hexdigest()
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _attestation_signing_payload(payload: JsonObject) -> bytes:
|
|
242
|
+
unsigned = {key: value for key, value in payload.items() if key != "signature"}
|
|
243
|
+
return json.dumps(unsigned, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode("utf-8")
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _attestation_signature(payload: JsonObject, private_key: Ed25519PrivateKey) -> str:
|
|
247
|
+
signature = private_key.sign(_attestation_signing_payload(payload))
|
|
248
|
+
return "ed25519:" + base64.urlsafe_b64encode(signature).decode("ascii")
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def _signature_bytes(signature: str) -> bytes:
|
|
252
|
+
prefix = "ed25519:"
|
|
253
|
+
if not signature.startswith(prefix):
|
|
254
|
+
raise ValidationError("specialist task run receipt attestation invalid: signature_kind")
|
|
255
|
+
try:
|
|
256
|
+
return base64.urlsafe_b64decode(signature[len(prefix):].encode("ascii"))
|
|
257
|
+
except ValueError as exc:
|
|
258
|
+
raise ValidationError("specialist task run receipt attestation invalid: signature_encoding") from exc
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def attach_specialist_task_run_receipt_attestation(payload: JsonObject) -> JsonObject:
|
|
262
|
+
private_key = _private_key()
|
|
263
|
+
public_key = private_key.public_key()
|
|
264
|
+
attested = JsonObjectAdapter.validate_python(dict(payload))
|
|
265
|
+
attested.pop("receipt_attestation", None)
|
|
266
|
+
receipt = _unsigned_receipt_for_attestation(attested)
|
|
267
|
+
attestation_payload = JsonObjectAdapter.validate_python({
|
|
268
|
+
"schema": SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_SCHEMA,
|
|
269
|
+
"attestation_kind": SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_KIND,
|
|
270
|
+
"created_by": SPECIALIST_TASK_RUN_RECEIPT_ATTESTATION_CREATED_BY,
|
|
271
|
+
"receipt_schema": receipt.schema_id,
|
|
272
|
+
"receipt_hash": specialist_task_run_receipt_hash(attested),
|
|
273
|
+
"work_id": receipt.work_id,
|
|
274
|
+
"phase": receipt.phase,
|
|
275
|
+
"harness": receipt.harness.value,
|
|
276
|
+
"adapter": receipt.adapter,
|
|
277
|
+
"key_id": _public_key_id(public_key),
|
|
278
|
+
"nonce": secrets.token_hex(16),
|
|
279
|
+
"issued_at": datetime.now(UTC).replace(microsecond=0).isoformat(),
|
|
280
|
+
})
|
|
281
|
+
attestation_payload["signature"] = _attestation_signature(attestation_payload, private_key)
|
|
282
|
+
try:
|
|
283
|
+
attestation = SpecialistTaskRunReceiptAttestation.model_validate(attestation_payload)
|
|
284
|
+
except PydanticValidationError as exc:
|
|
285
|
+
raise contract_error(exc, prefix="specialist task run receipt attestation invalid") from exc
|
|
286
|
+
attested["receipt_attestation"] = attestation.to_payload()
|
|
287
|
+
return attested
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def _validate_receipt_artifact(
|
|
291
|
+
*,
|
|
292
|
+
path_value: str,
|
|
293
|
+
sha_value: str,
|
|
294
|
+
path_field: str,
|
|
295
|
+
sha_field: str,
|
|
296
|
+
) -> None:
|
|
297
|
+
if not path_value:
|
|
298
|
+
raise ValidationError(f"specialist task run receipt artifact invalid: {path_field} required")
|
|
299
|
+
if not sha_value:
|
|
300
|
+
raise ValidationError(f"specialist task run receipt artifact invalid: {sha_field} required")
|
|
301
|
+
artifact_path = Path(path_value)
|
|
302
|
+
if not artifact_path.exists():
|
|
303
|
+
raise ValidationError(f"specialist task run receipt artifact invalid: {path_field} not found")
|
|
304
|
+
content = artifact_path.read_bytes()
|
|
305
|
+
if len(content.strip()) <= 2:
|
|
306
|
+
raise ValidationError(f"specialist task run receipt artifact invalid: {path_field} is empty")
|
|
307
|
+
actual = "sha256:" + hashlib.sha256(content).hexdigest()
|
|
308
|
+
if actual != sha_value:
|
|
309
|
+
raise ValidationError(f"specialist task run receipt artifact invalid: {sha_field}")
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def validate_specialist_task_run_receipt_attestation(
|
|
313
|
+
payload: JsonObject,
|
|
314
|
+
*,
|
|
315
|
+
require_artifacts: bool = True,
|
|
316
|
+
) -> None:
|
|
317
|
+
try:
|
|
318
|
+
receipt = SpecialistTaskRunReceipt.from_operation_payload(payload)
|
|
319
|
+
except PydanticValidationError as exc:
|
|
320
|
+
raise contract_error(exc, prefix="specialist task run receipt invalid") from exc
|
|
321
|
+
if receipt.receipt_attestation is None:
|
|
322
|
+
raise ValidationError("specialist task run receipt receipt_attestation required")
|
|
323
|
+
attestation = receipt.receipt_attestation
|
|
324
|
+
raw_attestation = attestation.to_payload()
|
|
325
|
+
expected_hash = specialist_task_run_receipt_hash(payload)
|
|
326
|
+
if attestation.receipt_hash != expected_hash:
|
|
327
|
+
raise ValidationError("specialist task run receipt attestation invalid: receipt_hash")
|
|
328
|
+
if attestation.receipt_schema != receipt.schema_id:
|
|
329
|
+
raise ValidationError("specialist task run receipt attestation invalid: receipt_schema")
|
|
330
|
+
if attestation.work_id != receipt.work_id:
|
|
331
|
+
raise ValidationError("specialist task run receipt attestation invalid: work_id")
|
|
332
|
+
if attestation.phase != receipt.phase:
|
|
333
|
+
raise ValidationError("specialist task run receipt attestation invalid: phase")
|
|
334
|
+
if attestation.harness != receipt.harness:
|
|
335
|
+
raise ValidationError("specialist task run receipt attestation invalid: harness")
|
|
336
|
+
if attestation.adapter != receipt.adapter:
|
|
337
|
+
raise ValidationError("specialist task run receipt attestation invalid: adapter")
|
|
338
|
+
try:
|
|
339
|
+
public_key = _public_key()
|
|
340
|
+
except (MissingPathError, ValidationError) as exc:
|
|
341
|
+
raise ValidationError(f"specialist task run receipt attestation invalid: {exc}") from exc
|
|
342
|
+
if attestation.key_id != _public_key_id(public_key):
|
|
343
|
+
raise ValidationError("specialist task run receipt attestation invalid: key_id")
|
|
344
|
+
try:
|
|
345
|
+
public_key.verify(_signature_bytes(attestation.signature), _attestation_signing_payload(raw_attestation))
|
|
346
|
+
except InvalidSignature as err:
|
|
347
|
+
raise ValidationError("specialist task run receipt attestation invalid: signature") from err
|
|
348
|
+
if require_artifacts and receipt.status == SpecialistRunStatus.COMPLETED:
|
|
349
|
+
_validate_receipt_artifact(
|
|
350
|
+
path_value=receipt.input_packet_path,
|
|
351
|
+
sha_value=receipt.input_packet_sha256,
|
|
352
|
+
path_field="input_packet_path",
|
|
353
|
+
sha_field="input_packet_sha256",
|
|
354
|
+
)
|
|
355
|
+
_validate_receipt_artifact(
|
|
356
|
+
path_value=receipt.transcript_artifact_path,
|
|
357
|
+
sha_value=receipt.transcript_artifact_sha256,
|
|
358
|
+
path_field="transcript_artifact_path",
|
|
359
|
+
sha_field="transcript_artifact_sha256",
|
|
360
|
+
)
|
package/.opencode/mednotes/src/mednotes/domains/wiki/capabilities/specialist/specialist_runtime.py
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""Runtime trust helpers for Workbench-mediated specialist calls."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import os
|
|
5
|
+
import shutil
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
_TRUSTED_GEMINI_BINARY_NAMES = frozenset({"gemini", "gemini.cmd", "gemini.exe"})
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def specialist_dev_escape_enabled() -> bool:
|
|
12
|
+
return os.environ.get("MEDNOTES_ALLOW_DEV_ESCAPE", "").strip() == "1"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def gemini_binary_identity(binary: str) -> str:
|
|
16
|
+
value = binary.strip()
|
|
17
|
+
if not value:
|
|
18
|
+
return ""
|
|
19
|
+
if "/" in value or "\\" in value:
|
|
20
|
+
return str(Path(value).expanduser().resolve(strict=False))
|
|
21
|
+
resolved = shutil.which(value)
|
|
22
|
+
return str(Path(resolved).resolve(strict=False)) if resolved else value
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def gemini_binary_is_public_trusted(binary: str) -> bool:
|
|
26
|
+
value = binary.strip()
|
|
27
|
+
if not value:
|
|
28
|
+
return False
|
|
29
|
+
if value in _TRUSTED_GEMINI_BINARY_NAMES:
|
|
30
|
+
return True
|
|
31
|
+
default = shutil.which("gemini")
|
|
32
|
+
if not default:
|
|
33
|
+
return False
|
|
34
|
+
default_identity = str(Path(default).resolve(strict=False))
|
|
35
|
+
return gemini_binary_identity(value) == default_identity
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def gemini_binary_override_block_reason(binary: str) -> str:
|
|
39
|
+
if gemini_binary_is_public_trusted(binary):
|
|
40
|
+
return ""
|
|
41
|
+
if specialist_dev_escape_enabled():
|
|
42
|
+
return ""
|
|
43
|
+
return "specialist_runner_untrusted_gemini_binary"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def transcript_command_untrusted_gemini_binary(command: object) -> str:
|
|
47
|
+
if not isinstance(command, list) or not command:
|
|
48
|
+
return "missing_command"
|
|
49
|
+
binary = command[0]
|
|
50
|
+
if not isinstance(binary, str) or not binary.strip():
|
|
51
|
+
return "missing_binary"
|
|
52
|
+
return "" if gemini_binary_is_public_trusted(binary) else binary.strip()
|