diff --git a/.gitattributes b/.gitattributes index 8085800..342df04 100644 --- a/.gitattributes +++ b/.gitattributes @@ -14,3 +14,8 @@ manifests.json text eol=lf netlify.toml text eol=lf _headers text eol=lf _redirects text eol=lf + +# Manifest mirrors are byte-exact copies of upstream responses; any eol +# normalization would make the checkout differ from the fetched bytes and +# _refresh_mirrors would see phantom changes on Windows checkouts. +mirrors/** -text diff --git a/.github/workflows/federation-sync.yml b/.github/workflows/federation-sync.yml index acda435..69b0088 100644 --- a/.github/workflows/federation-sync.yml +++ b/.github/workflows/federation-sync.yml @@ -1,9 +1,15 @@ name: federation-sync # Fetches every publisher's .well-known/install-manifests.json, merges -# into manifests.json, and opens a PR with the diff (if any). The PR is -# created with the `auto-sync-federation` label and `--auto --squash` -# enabled, so it merges itself the moment required checks go green. +# into manifests.json, refreshes the committed manifest mirrors +# (mirrors/manifests/), rebuilds registry/ pages from those mirrors, and +# opens a PR with the combined diff (if any). The PR is created with the +# `auto-sync-federation` label and `--auto --squash` enabled, so it +# merges itself the moment required checks go green. +# +# Manifest content, its mirror, and the rebuilt pages land in ONE +# commit — syncing manifests without rebuilding pages is what kept +# redding the registry drift check (PRs #54/#55/#58). # # Scheduled daily; can also be triggered manually via the Actions UI. @@ -41,10 +47,14 @@ jobs: - name: Run federation sync (live fetch) run: python scripts/sync_from_publishers.py --allow-network + - name: Refresh manifest mirrors + rebuild registry pages + run: python scripts/build_registry_pages.py --refresh + - name: Check for diff id: diff run: | - if git diff --quiet manifests.json; then + # git status (not diff) so brand-new mirror/page files count. + if [ -z "$(git status --porcelain -- manifests.json mirrors registry)" ]; then echo "changed=false" >> "$GITHUB_OUTPUT" else echo "changed=true" >> "$GITHUB_OUTPUT" @@ -69,12 +79,12 @@ jobs: git config user.name "yep-federation-bot" git config user.email "yep-federation-bot@users.noreply.github.com" git checkout -b "$BRANCH" - git add manifests.json + git add manifests.json mirrors registry git commit -m "auto: federation sync $(date -u +%Y-%m-%d)" git push origin "$BRANCH" PR_URL=$(gh pr create \ --title "auto: federation sync $(date -u +%Y-%m-%d)" \ - --body "Automated federation pull. Diff is the result of fetching every publisher in publishers.json and re-rendering manifests.json. Merging this updates the public registry. See federation-sync workflow for details." \ + --body "Automated federation pull. Diff is the result of fetching every publisher in publishers.json, re-rendering manifests.json, refreshing the committed manifest mirrors, and rebuilding registry pages from them. Merging this updates the public registry. See federation-sync workflow for details." \ --label "auto-sync-federation" \ --base main \ --head "$BRANCH") diff --git a/mirrors/manifests/deep-research.json b/mirrors/manifests/deep-research.json new file mode 100644 index 0000000..e3352f7 --- /dev/null +++ b/mirrors/manifests/deep-research.json @@ -0,0 +1,284 @@ +{ + "manifest_version": "0.4", + "tool": { + "namespace": "drknowhow", + "id": "deep-research", + "version": "0.2.0", + "name": "deep_research", + "summary": "Protocol-first, gated, multi-agent literature investigation with no-fabrication enforced via verbatim quote spans.", + "description": "deep_research is an agent-runtime-agnostic workflow that turns an empirical research question into a citation-grade synthesis. Two human-in-the-loop gates (protocol pre-registration, Pass-2 spend) bracket a four-role subagent crew (Scout / Skeptic / Methodologist / Synthesizer). Every claim a synthesis ships must be backed by a row in the research_evidence table with a verbatim quote_span; claims without their quote get cut. v0.2.0 adds the Python reference implementation: a stdlib-urllib scholar adapter (OpenAlex / Semantic Scholar / PubMed / arXiv / Europe PMC / Crossref / Unpaywall) and a python-docx + matplotlib synthesis builder with pluggable upload. Core stays stdlib-only; the docx builder is gated behind the optional `[viz]` extra.", + "homepage": "https://github.com/drknowhow/deep-research", + "author": { + "name": "Dimitri T", + "url": "https://yepgent.com" + }, + "license": "Apache-2.0", + "tags": [ + "research", + "literature-review", + "meta-analysis", + "agents", + "subagent-fan-out", + "no-fabrication", + "skill-bundle" + ] + }, + "runtime": { + "kind": "python-module", + "install": { + "method": "git", + "url": "https://github.com/drknowhow/deep-research", + "ref": "v0.2.0", + "layout": "skill-bundle" + }, + "entrypoint": { + "command": ["python", "-m", "deep_research"] + } + }, + "scopes": [ + { + "resource": "net.outbound.scholarly", + "actions": ["read"], + "rationale": "Calls public scholarly APIs (OpenAlex, Semantic Scholar, PubMed, arXiv, Europe PMC, Crossref, Unpaywall) and fetches open-access PDFs to extract verbatim quote spans." + }, + { + "resource": "db.research_tables", + "actions": ["read", "write"], + "rationale": "Persists the project row, every query (research_searches), and every claim with provenance (research_evidence). These three append-only tables are the audit trail." + } + ], + "actions": [ + { + "name": "start_project", + "summary": "Create a new research project row in 'planned' status with the user-supplied question.", + "docs": { + "goal": "Open a new research investigation.", + "inputs_brief": "name (slug), title, question", + "outputs_brief": "research_project_id (uuid)", + "errors_brief": "schema_missing, db_unreachable", + "example": "start_project(name='cholesterol-acm', title='Primary-prevention LDL lowering and all-cause mortality', question='Does pharmacological LDL lowering reduce ACM in strict primary prevention?')" + }, + "invocation": { + "kind": "stdin-json" + }, + "input": { + "type": "object", + "required": ["name", "title", "question"], + "properties": { + "name": {"type": "string", "pattern": "^[a-z0-9-]+$"}, + "title": {"type": "string", "minLength": 1}, + "question": {"type": "string", "minLength": 1} + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": ["research_project_id", "status"], + "properties": { + "research_project_id": {"type": "string", "format": "uuid"}, + "status": {"type": "string", "const": "planned"} + } + } + }, + "side_effects": "write", + "idempotent": false, + "scopes_used": ["db.research_tables"], + "error_envelope": "standard" + }, + { + "name": "submit_protocol", + "summary": "Attach the pre-registered protocol JSON to a project and move status to protocol_gated.", + "docs": { + "goal": "Lock the search/inclusion/effect/analysis plan before any paid searches.", + "inputs_brief": "research_project_id, protocol (PICO + queries + inclusion/exclusion + analysis_plan + gate_thresholds)", + "outputs_brief": "status='protocol_gated', awaiting_human_approval=true", + "errors_brief": "project_not_found, protocol_invalid", + "example": "submit_protocol(research_project_id=..., protocol={...})" + }, + "invocation": {"kind": "stdin-json"}, + "input": { + "type": "object", + "required": ["research_project_id", "protocol"], + "properties": { + "research_project_id": {"type": "string", "format": "uuid"}, + "protocol": {"type": "object"} + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": ["status", "awaiting_human_approval"], + "properties": { + "status": {"type": "string"}, + "awaiting_human_approval": {"type": "boolean"} + } + } + }, + "side_effects": "write", + "idempotent": true, + "scopes_used": ["db.research_tables"], + "error_envelope": "standard" + }, + { + "name": "approve_gate", + "summary": "Append a human-decision row to gate_log and transition the project to the next phase.", + "docs": { + "goal": "Record a Gate 1 or Gate 2 decision and advance state.", + "inputs_brief": "research_project_id, gate ('protocol'|'pass2'), decision ('approve'|'revise'|'abort'), notes", + "outputs_brief": "new status", + "errors_brief": "project_not_found, wrong_phase, unknown_gate", + "example": "approve_gate(research_project_id=..., gate='protocol', decision='approve')" + }, + "invocation": {"kind": "stdin-json"}, + "input": { + "type": "object", + "required": ["research_project_id", "gate", "decision"], + "properties": { + "research_project_id": {"type": "string", "format": "uuid"}, + "gate": {"type": "string", "enum": ["protocol", "pass2"]}, + "decision": {"type": "string", "enum": ["approve", "revise", "abort"]}, + "notes": {"type": "string"} + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": ["status"], + "properties": {"status": {"type": "string"}} + } + }, + "side_effects": "write", + "idempotent": false, + "scopes_used": ["db.research_tables"], + "error_envelope": "standard" + }, + { + "name": "run_pass1", + "summary": "Fan out Scout / Skeptic / Methodologist subagents over the approved protocol. Writes research_searches and research_evidence rows.", + "docs": { + "goal": "Build the Pass-1 candidate corpus from abstracts.", + "inputs_brief": "research_project_id", + "outputs_brief": "scout_run, skeptic_run, methodologist_run summaries; corpus rollup", + "errors_brief": "wrong_phase, scholarly_api_unreachable, subagent_cap_exceeded", + "example": "run_pass1(research_project_id=...)" + }, + "invocation": {"kind": "stdin-json"}, + "input": { + "type": "object", + "required": ["research_project_id"], + "properties": { + "research_project_id": {"type": "string", "format": "uuid"} + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": ["scout", "skeptic", "methodologist", "rollup"], + "properties": { + "scout": {"type": "object"}, + "skeptic": {"type": "object"}, + "methodologist": {"type": "object"}, + "rollup": {"type": "object"} + } + } + }, + "side_effects": "write", + "idempotent": false, + "scopes_used": ["net.outbound.scholarly", "db.research_tables"], + "error_envelope": "standard" + }, + { + "name": "run_pass2", + "summary": "Retrieve full text for the approved candidate set and stage it for the Synthesizer.", + "docs": { + "goal": "Pull OA PDFs and extract to text.", + "inputs_brief": "research_project_id", + "outputs_brief": "n_retrieved, n_paywalled, n_unavailable, candidate ids", + "errors_brief": "wrong_phase, pdf_extract_failed", + "example": "run_pass2(research_project_id=...)" + }, + "invocation": {"kind": "stdin-json"}, + "input": { + "type": "object", + "required": ["research_project_id"], + "properties": { + "research_project_id": {"type": "string", "format": "uuid"} + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "properties": { + "n_retrieved": {"type": "integer"}, + "n_paywalled": {"type": "integer"}, + "n_unavailable": {"type": "integer"} + } + } + }, + "side_effects": "write", + "idempotent": false, + "scopes_used": ["net.outbound.scholarly", "db.research_tables"], + "error_envelope": "standard" + }, + { + "name": "synthesize", + "summary": "Run the Synthesizer subagent. Produces a structured document; enforces verbatim-quote-or-cut on every claim.", + "docs": { + "goal": "Produce the no-fabrication synthesis document.", + "inputs_brief": "research_project_id", + "outputs_brief": "doc_artifact_id, n_cited, n_supports, n_refutes, n_mixed, complete", + "errors_brief": "wrong_phase, fabrication_detected, doc_build_failed", + "example": "synthesize(research_project_id=...)" + }, + "invocation": {"kind": "stdin-json"}, + "input": { + "type": "object", + "required": ["research_project_id"], + "properties": { + "research_project_id": {"type": "string", "format": "uuid"} + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": ["doc_artifact_id", "n_cited", "complete"], + "properties": { + "doc_artifact_id": {"type": "string"}, + "n_cited": {"type": "integer"}, + "n_supports": {"type": "integer"}, + "n_refutes": {"type": "integer"}, + "n_mixed": {"type": "integer"}, + "complete": {"type": "boolean"} + } + } + }, + "side_effects": "write", + "idempotent": false, + "scopes_used": ["db.research_tables"], + "error_envelope": "standard" + } + ], + "smoke": { + "kind": "shell", + "command": ["python", "-c", "import deep_research; print(deep_research.__version__)"], + "timeout_seconds": 15, + "success": { + "exit_code": 0, + "stdout_regex": "^0\\." + } + }, + "kill_switch": { + "kind": "manual", + "instructions": "deep_research holds no credentials and persists nothing outside the research_* tables the host application provisions. To revoke: (1) drop or revoke INSERT/UPDATE/DELETE on research_projects, research_searches, research_evidence in your application's database; (2) remove the deep_research module from your agent's runtime PYTHONPATH or skill registry. No vendor account to close." + }, + "support": { + "issues_url": "https://github.com/drknowhow/deep-research/issues", + "docs_url": "https://github.com/drknowhow/deep-research#readme" + } +} diff --git a/mirrors/manifests/muninn-blog-publish.json b/mirrors/manifests/muninn-blog-publish.json new file mode 100644 index 0000000..f00d37e --- /dev/null +++ b/mirrors/manifests/muninn-blog-publish.json @@ -0,0 +1,299 @@ +{ + "manifest_version": "0.4", + "tool": { + "id": "muninn-blog-publish", + "version": "0.1.0", + "name": "Muninn blog_publish", + "summary": "Publish HTML pages to austegard.com via GitHub Pages, optionally update the Atom feed, optionally announce on Bluesky with a follow-up engagement-link commit. Encoded as a flowing DAG so the bsky chain is detached and partial-failure-tolerant.", + "description": "Encoded as a flowing graph: page-commit \u2192 wait-for-deploy \u2192 (when feed_entry given) feed-update; bsky-announce + engagement-link-commit run as detached side-effects. Caller gets the page URL the moment GH Pages serves it; bsky failures land in `flow.detached_failures` and never bubble up as publish failures. The 300-grapheme bsky cap is enforced as a `validate=` gate BEFORE any createRecord call. Same primary credential as `perch_publish` (GH_TOKEN), same secondary credentials as `bsky_card` (MUNINN_BSKY_HANDLE, MUNINN_BSKY_APP_PASSWORD) \u2014 third publishing target after perch_publish and (planned) whtwnd. Issue #5 calls this out as the test for whether v0.4 `writes[]` generalises across multiple publishing surfaces in the same agent.", + "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/blog_publish.py", + "author": { + "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)", + "url": "https://muninn.austegard.com" + }, + "license": "MIT", + "tags": [ + "publishing", + "github-pages", + "atom-feed", + "blog", + "bluesky", + "flowing" + ] + }, + "runtime": { + "kind": "python-module", + "install": { + "method": "preinstalled", + "locator": { + "kind": "python-module", + "module": "muninn_utils.blog_publish" + } + }, + "entrypoint": { + "command": [ + "python", + "-m", + "muninn_utils.blog_publish" + ] + } + }, + "env": [ + { + "name": "GH_TOKEN", + "prompt": "GitHub personal access token. Needs write access to the publish-target repo (oaustegard/austegard.com by default). Same coarse credential as perch_publish, issue_close, perch_triage, verify_patch \u2014 share-by-default is intentional. Falls back to GITHUB_TOKEN if GH_TOKEN is not set.", + "secret": true, + "required": true, + "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$", + "obtain_url": "https://github.com/settings/personal-access-tokens" + }, + { + "name": "GITHUB_TOKEN", + "prompt": "Optional fallback for GH_TOKEN. The source reads `os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')`, so either name works. Same scope/sensitivity as GH_TOKEN.", + "secret": true, + "required": false, + "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$", + "obtain_url": "https://github.com/settings/personal-access-tokens" + }, + { + "name": "MUNINN_BSKY_HANDLE", + "prompt": "The Bluesky handle to post the announcement as, e.g. 'austegard.com'. Optional \u2014 leave blank to skip the bsky chain entirely (the page-publish path still runs).", + "secret": false, + "required": false, + "validation_regex": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$", + "obtain_url": "https://bsky.app/settings/account" + }, + { + "name": "MUNINN_BSKY_APP_PASSWORD", + "prompt": "App password for the Bluesky handle above. Format is four hyphen-separated four-character groups. Treat as a secret. Required only if MUNINN_BSKY_HANDLE is set; otherwise leave blank. Bsky app passwords cannot be programmatically revoked.", + "secret": true, + "required": false, + "validation_regex": "^[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$", + "obtain_url": "https://bsky.app/settings/app-passwords" + } + ], + "scopes": [ + { + "resource": "github.repo.contents", + "actions": [ + "read", + "write" + ], + "rationale": "Reads the existing feed file (when a feed_entry is supplied), writes the new page commit, optionally commits the updated feed, optionally appends a follow-up engagement-link commit referencing the bsky post. Lands directly on the publish branch with no PR.", + "provider_scope": "github-pat (coarse; full account write access)" + }, + { + "resource": "atproto.repo", + "actions": [ + "write" + ], + "rationale": "When MUNINN_BSKY_HANDLE+PASSWORD are set, creates an app.bsky.feed.post record on the user's repo announcing the published page. Detached side-effect: failure does not abort the publish.", + "provider_scope": "bsky-app-password (coarse; full repo write except DMs)" + }, + { + "resource": "net.outbound", + "actions": [ + "read", + "write" + ], + "rationale": "Talks to api.github.com for git operations, polls the eventually-served page URL on austegard.com to confirm GitHub Pages deploy, and (when bsky configured) talks to bsky.social for the announce.", + "provider_scope": "api.github.com, austegard.com, bsky.social" + } + ], + "actions": [ + { + "name": "publish_and_announce", + "summary": "Commit a page, wait for GH Pages to serve it, optionally update the Atom feed, optionally announce on Bluesky with a follow-up engagement-link commit.", + "description": "Synchronous nodes: page-commit, wait-for-deploy, feed-update (when feed_entry given). Detached nodes: bsky-announce (when bsky auth supplied), engagement-link-commit (depends on bsky-announce success). The bsky_text input is validated against the 300-grapheme cap (using the bsky_limit utility) BEFORE the bsky chain fires; an over-cap text aborts only the bsky chain, not the page publish. The `deployed` boolean reflects whether the page URL was served within the deploy budget; `bsky_post` is null when the bsky chain was skipped or failed; `detached_failures` lists any background failures.", + "docs": { + "goal": "Publish an HTML page and optionally announce it on Bluesky with engagement linking.", + "inputs_brief": "path (req), content (req), bsky_text (req if announcing), feed_entry (optional), repo (default oaustegard/austegard.com)", + "outputs_brief": "{page_url, commit_sha, feed_sha, deployed, bsky_post, update_sha, detached_failures}", + "errors_brief": "auth_invalid, commit_failed, deploy_timeout, bsky_text_too_long, target_unreachable", + "example": "publish_and_announce path='blog/post.html' content='...' bsky_text='New post: ...'" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "publish-and-announce" + ] + }, + "input": { + "type": "object", + "required": [ + "path", + "content" + ], + "additionalProperties": false, + "properties": { + "path": { + "type": "string", + "minLength": 1 + }, + "content": { + "type": "string", + "minLength": 1 + }, + "bsky_text": { + "type": "string", + "maxLength": 2000 + }, + "feed_entry": { + "type": "object" + }, + "repo": { + "type": "string", + "pattern": "^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$", + "default": "oaustegard/austegard.com" + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "page_url", + "commit_sha", + "deployed" + ], + "properties": { + "page_url": { + "type": "string", + "format": "uri" + }, + "commit_sha": { + "type": "string", + "pattern": "^[0-9a-f]{40}$" + }, + "feed_sha": { + "type": [ + "string", + "null" + ] + }, + "deployed": { + "type": "boolean" + }, + "bsky_post": { + "type": [ + "object", + "null" + ], + "properties": { + "uri": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri" + }, + "rkey": { + "type": "string" + } + } + }, + "update_sha": { + "type": [ + "string", + "null" + ] + }, + "detached_failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "error": { + "type": "string" + } + } + } + } + } + } + }, + "side_effects": "destructive", + "idempotent": false, + "scopes_used": [ + "github.repo.contents", + "atproto.repo", + "net.outbound" + ], + "error_envelope": "standard", + "runtime_telemetry": {} + } + ], + "data_boundary": { + "reads": [ + { + "resource": "github.repo.contents", + "sensitivity": "low" + } + ], + "transmits": [ + { + "to": "api.github.com", + "fields": [ + "env.GH_TOKEN", + "input.content", + "input.path" + ], + "purpose": "publish page commit + feed-update commit + engagement-link commit", + "third_party_retention": "none-per-vendor-tos", + "vendor_tos_url": "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement" + }, + { + "to": "bsky.social", + "fields": [ + "input.bsky_text", + "computed.page_url" + ], + "purpose": "announce published page; PDS federates publicly to the network firehose", + "third_party_retention": "persistent-indefinite" + }, + { + "to_kind": "agent-supplied", + "to_constraint": "GitHub Pages domain for the input.repo (e.g. austegard.com, muninn.austegard.com). HEAD request only; no body sent. Used to confirm the page has been served before returning.", + "fields": [ + "computed.page_url" + ], + "purpose": "poll page URL to confirm GH Pages deploy", + "third_party_retention": "unknown" + } + ], + "persists": [] + }, + "smoke": { + "kind": "shell", + "command": [ + "python", + "-c", + "import os, json, urllib.request\ntoken = os.environ['GH_TOKEN']\nreq = urllib.request.Request('https://api.github.com/repos/oaustegard/austegard.com', headers={'Authorization': f'token {token}', 'Accept': 'application/vnd.github+json', 'User-Agent': 'blog-publish-smoke'})\nd = json.loads(urllib.request.urlopen(req).read())\nassert d['permissions']['push'], 'token lacks write access to publish-target repo'\nprint('OK: write access confirmed to', d['name'])\n" + ], + "timeout_seconds": 10, + "success": { + "exit_code": 0, + "stdout_regex": "OK: write access confirmed to austegard\\.com" + } + }, + "kill_switch": { + "kind": "manual", + "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/blog-publish/REVOKE.md" + }, + "cost": { + "install_fee_cents": 0, + "monthly_fee_cents": 0, + "usage_model": "external" + }, + "support": { + "issues_url": "https://github.com/oaustegard/muninn-utilities/issues", + "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/blog_publish.py" + } +} diff --git a/mirrors/manifests/muninn-bsky-card.json b/mirrors/manifests/muninn-bsky-card.json new file mode 100644 index 0000000..ff66fd4 --- /dev/null +++ b/mirrors/manifests/muninn-bsky-card.json @@ -0,0 +1,398 @@ +{ + "manifest_version": "0.4", + "tool": { + "id": "muninn-bsky-card", + "version": "0.1.0", + "name": "Muninn Bluesky Card", + "summary": "Compose and publish Bluesky posts with rich link-card embeds (Open Graph preview). Python module; app-password auth; reads arbitrary URLs to extract OG metadata, then posts via the authenticated PDS.", + "description": "Library that an orchestrating agent calls to share a URL on Bluesky with a proper link card. Workflow: fetch the target URL, extract Open Graph tags, upload the card thumbnail as a blob to the user's PDS, compose UTF-8 facets for any inline links, and create the post via com.atproto.repo.createRecord. Auth is a Bluesky app password (created at bsky.app/settings/app-passwords); the password grants write access to everything except DMs and account deletion. The tool itself stores nothing; ephemeral session JWTs live only in the calling process's memory. First in the consumer-test series for install-manifest-spec v0.3 \u2014 see the muninns-inbox discussion #1 thread for the findings the writeup surfaced. Manifest moved here (from muninns-inbox/manifests/) per issue #5 \u2014 the round-1 venue mistake. Two entry shapes: the library API takes a pre-resolved `auth` dict (`handle`, `did`, `access_jwt`) from the caller, while the `python -m muninn_utils.bsky_card` CLI entrypoint resolves a session from BSKY_HANDLE + BSKY_APP_PASSWORD (see env).", + "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/bsky_card.py", + "author": { + "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)", + "url": "https://muninn.austegard.com" + }, + "license": "MIT", + "tags": [ + "bluesky", + "atproto", + "social", + "posting", + "link-card" + ] + }, + "runtime": { + "kind": "python-module", + "install": { + "method": "preinstalled", + "locator": { + "kind": "python-module", + "module": "muninn_utils.bsky_card" + } + }, + "entrypoint": { + "command": [ + "python", + "-m", + "muninn_utils.bsky_card" + ] + } + }, + "env": [ + { + "name": "BSKY_HANDLE", + "prompt": "The Bluesky handle the CLI authenticates as, e.g. 'austegard.com'. Required by the `python -m muninn_utils.bsky_card` entrypoint (whoami / post-link / delete-post). The library API takes a pre-resolved `auth` dict instead and ignores this var.", + "secret": false, + "required": true, + "validation_regex": "^[a-z0-9]([a-z0-9-]*[a-z0-9])?(\\.[a-z0-9]([a-z0-9-]*[a-z0-9])?)+$", + "obtain_url": "https://bsky.app/settings/account" + }, + { + "name": "BSKY_APP_PASSWORD", + "prompt": "App password for BSKY_HANDLE. Format is four hyphen-separated four-character groups. Treat as a secret. Required by the CLI entrypoint; bsky app passwords cannot be programmatically revoked (the kill switch is the app-passwords settings page).", + "secret": true, + "required": true, + "validation_regex": "^[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}-[a-z0-9]{4}$", + "obtain_url": "https://bsky.app/settings/app-passwords" + }, + { + "name": "BSKY_PDS", + "prompt": "Optional PDS base URL override. Defaults to https://bsky.social. Set only when authenticating against a self-hosted or non-default PDS.", + "secret": false, + "required": false, + "validation_regex": "^https?://.+", + "obtain_url": "https://atproto.com/guides/self-hosting" + } + ], + "scopes": [ + { + "resource": "atproto.repo", + "actions": [ + "read", + "write" + ], + "rationale": "Creates and deletes records (app.bsky.feed.post) in the user's repo, and uploads blob attachments for card thumbnails.", + "provider_scope": "app-password (coarse)" + }, + { + "resource": "atproto.identity", + "actions": [ + "read" + ], + "rationale": "Resolves the user's handle to a DID at startup so the smoke test can confirm auth resolves to the expected account.", + "provider_scope": "app-password (coarse)" + }, + { + "resource": "net.outbound", + "actions": [ + "read" + ], + "rationale": "Two outbound destinations: (1) the configured PDS (default bsky.social) for atproto operations, and (2) ARBITRARY user-supplied URLs fetched server-side to extract Open Graph metadata. The second is the wider blast radius \u2014 any URL the agent shares is fetched by this tool.", + "provider_scope": "*" + } + ], + "actions": [ + { + "name": "whoami", + "summary": "Authenticate, resolve handle to DID, and return both. Read-only.", + "description": "Calls com.atproto.server.createSession with the configured handle + app password, then com.atproto.identity.resolveHandle to confirm the handle resolves to the same DID the session returned. Used as the smoke test.", + "docs": { + "goal": "Verify auth and confirm the configured handle resolves to a real DID.", + "inputs_brief": "(none)", + "outputs_brief": "{handle, did, pds}", + "errors_brief": "auth_invalid, handle_not_found, network_unreachable", + "example": "whoami" + }, + "invocation": { + "kind": "subcommand", + "argv_template": [ + "whoami" + ] + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "handle", + "did" + ], + "properties": { + "handle": { + "type": "string" + }, + "did": { + "type": "string", + "pattern": "^did:" + }, + "pds": { + "type": "string", + "format": "uri" + } + } + } + }, + "side_effects": "read", + "idempotent": true, + "scopes_used": [ + "atproto.identity" + ], + "error_envelope": "standard", + "examples": [ + { + "description": "Healthy auth.", + "input": {}, + "output": { + "handle": "austegard.com", + "did": "did:plc:abc123...", + "pds": "https://bsky.social" + } + } + ], + "runtime_telemetry": {} + }, + { + "name": "post_link", + "summary": "Post a URL to Bluesky with an automatically-generated link card.", + "description": "Fetches the URL, extracts Open Graph (og:title / og:description / og:image), uploads the image as a blob to the user's PDS, computes UTF-8 facets for any inline links in the post text, and creates an app.bsky.feed.post record with an external embed. Destructive (a posted message is publicly visible immediately and federates to the wider AppView) but reversible via delete_post on the returned URI.", + "docs": { + "goal": "Share a URL on Bluesky with a proper card preview.", + "inputs_brief": "text (\u2264300 graphemes), url, og_overrides? (manual title/description/image), languages? (BCP-47 array)", + "outputs_brief": "{uri, cid, url}", + "errors_brief": "text_too_long, url_unreachable, blob_upload_failed, auth_invalid, rate_limited", + "example": "post_link text='New post on the manifest spec' url='https://muninn.austegard.com/perch/...'" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "post-link" + ] + }, + "input": { + "type": "object", + "required": [ + "text", + "url" + ], + "additionalProperties": false, + "properties": { + "text": { + "type": "string", + "minLength": 1, + "description": "Post text. Library enforces 300-grapheme cap (NOT len(); emoji and combining marks count differently)." + }, + "url": { + "type": "string", + "format": "uri" + }, + "og_overrides": { + "type": "object", + "additionalProperties": false, + "properties": { + "title": { + "type": "string", + "maxLength": 300 + }, + "description": { + "type": "string", + "maxLength": 1000 + }, + "image": { + "type": "string", + "format": "uri" + } + } + }, + "languages": { + "type": "array", + "items": { + "type": "string", + "pattern": "^[a-z]{2,3}(-[A-Z]{2})?$" + }, + "maxItems": 4, + "default": [ + "en" + ] + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "uri", + "cid" + ], + "properties": { + "uri": { + "type": "string", + "pattern": "^at://" + }, + "cid": { + "type": "string" + }, + "url": { + "type": "string", + "format": "uri", + "description": "Convenience: https://bsky.app/profile/{handle}/post/{rkey}" + } + } + } + }, + "side_effects": "destructive", + "idempotent": false, + "scopes_used": [ + "atproto.repo", + "net.outbound" + ], + "error_envelope": "standard", + "examples": [ + { + "description": "Share a blog post.", + "input": { + "text": "I wrote a manifest for one of my own tools. Findings in the muninns-inbox thread.", + "url": "https://github.com/oaustegard/muninns-inbox/discussions/1" + }, + "output": { + "uri": "at://did:plc:abc.../app.bsky.feed.post/3l5...", + "cid": "bafy...", + "url": "https://bsky.app/profile/austegard.com/post/3l5..." + } + } + ], + "runtime_telemetry": {} + }, + { + "name": "delete_post", + "summary": "Delete a previously-created post by AT-URI.", + "description": "Calls com.atproto.repo.deleteRecord on a post URI returned by post_link. Idempotent (deleting a non-existent record is a no-op at the AppView level). Network-propagation lag means the post may remain visible briefly after delete returns.", + "docs": { + "goal": "Retract a Bluesky post by AT-URI.", + "inputs_brief": "uri (at:// URI from post_link)", + "outputs_brief": "{uri, deleted: true}", + "errors_brief": "uri_invalid, not_owned, auth_invalid", + "example": "delete-post uri='at://did:plc:abc.../app.bsky.feed.post/3l5...'" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "delete-post" + ] + }, + "input": { + "type": "object", + "required": [ + "uri" + ], + "additionalProperties": false, + "properties": { + "uri": { + "type": "string", + "pattern": "^at://" + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "uri", + "deleted" + ], + "properties": { + "uri": { + "type": "string" + }, + "deleted": { + "type": "boolean" + } + } + } + }, + "side_effects": "destructive", + "idempotent": true, + "scopes_used": [ + "atproto.repo" + ], + "error_envelope": "standard", + "runtime_telemetry": {} + } + ], + "verify": { + "sla": { + "p50_latency_ms": 1200, + "p95_latency_ms": 4000, + "error_rate_max": 0.05 + }, + "schedule": { + "cadence": "weekly", + "on_install": false + } + }, + "data_boundary": { + "reads": [ + { + "resource": "atproto.repo.posts", + "sensitivity": "low" + }, + { + "resource": "external.url_content", + "sensitivity": "low" + } + ], + "transmits": [ + { + "to": "bsky.social", + "fields": [ + "atproto.repo.posts/text", + "atproto.repo.posts/embed.external.uri", + "atproto.repo.posts/embed.external.title", + "atproto.repo.posts/embed.external.description", + "atproto.repo.posts/embed.external.thumb" + ], + "purpose": "publish post + OG-card thumbnail blob to user's PDS; PDS federates publicly to the network firehose. Posts are public by design.", + "third_party_retention": "persistent-indefinite" + }, + { + "to_kind": "agent-supplied", + "to_constraint": "URLs the caller asks to share (post_link.url) and the og:image URL extracted from that page. Both fetched server-side by this tool to extract Open Graph metadata and thumbnail bytes.", + "fields": [ + "input.url", + "fetched.og_image_url" + ], + "purpose": "fetch OG metadata and thumbnail image from the caller-supplied URL", + "third_party_retention": "unknown" + } + ], + "persists": [], + "retention": { + "tool_local_days": 0 + } + }, + "smoke": { + "kind": "action-call", + "action": "whoami", + "arguments": {}, + "timeout_seconds": 10, + "success": { + "no_error_field": true, + "json_pointer_equals": { + "/handle": "${BSKY_HANDLE}" + } + } + }, + "kill_switch": { + "kind": "url", + "url": "https://bsky.app/settings/app-passwords" + }, + "cost": { + "install_fee_cents": 0, + "monthly_fee_cents": 0, + "usage_model": "none" + }, + "support": { + "issues_url": "https://github.com/oaustegard/muninn-utilities/issues", + "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/bsky_card.py" + } +} diff --git a/mirrors/manifests/muninn-bsky-limit.json b/mirrors/manifests/muninn-bsky-limit.json new file mode 100644 index 0000000..ca23ca7 --- /dev/null +++ b/mirrors/manifests/muninn-bsky-limit.json @@ -0,0 +1,231 @@ +{ + "manifest_version": "0.4", + "tool": { + "id": "muninn-bsky-limit", + "version": "0.1.0", + "name": "Muninn bsky_limit", + "summary": "Bluesky 300-grapheme length checker and truncator. len() lies on emoji and ZWJ sequences; this counts graphemes correctly and truncates at the last whitespace boundary.", + "description": "Two-function helper used by anything that posts to Bluesky. fits(text, limit=300) returns whether the text fits the AppView's grapheme cap; truncate(text, limit=300, suffix='\u2026') walks back to the last whitespace under the cap and appends the suffix. Pure compute: no I/O, no environment, no state. Depends on the `grapheme` PyPI package, which the module pip-installs at import time if missing (the only side-effect surface). Manifested here as the shared-credential / shared-dependency reuse case for Bluesky utilities (bsky_card, blog_publish, whtwnd) and as the v0.3-spec stress-test of 'a tool whose only declarable scope is compute.local'.", + "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/bsky_limit.py", + "author": { + "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)", + "url": "https://muninn.austegard.com" + }, + "license": "MIT", + "tags": [ + "bluesky", + "atproto", + "grapheme", + "text-utility", + "length-check" + ] + }, + "runtime": { + "kind": "python-module", + "install": { + "method": "preinstalled", + "locator": { + "kind": "python-module", + "module": "muninn_utils.bsky_limit" + } + }, + "entrypoint": { + "command": [ + "python", + "-m", + "muninn_utils.bsky_limit" + ] + } + }, + "scopes": [ + { + "resource": "compute.local", + "actions": [ + "read" + ], + "rationale": "Pure string computation. The only non-pure surface is a one-time pip install of `grapheme` if absent at import time." + } + ], + "actions": [ + { + "name": "fits", + "summary": "Return whether the given text fits the Bluesky 300-grapheme post limit.", + "description": "Counts graphemes (not codepoints, not bytes) using the `grapheme` package. The default limit is 300 to match Bluesky's AppView; callers may override for other surfaces.", + "docs": { + "goal": "Check whether text fits Bluesky's 300-grapheme cap.", + "inputs_brief": "text (req), limit (int, default 300)", + "outputs_brief": "{fits: bool, length: int}", + "errors_brief": "(none \u2014 pure compute)", + "example": "fits text='Hello \ud83d\udc4b' limit=300" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "fits" + ] + }, + "input": { + "type": "object", + "required": [ + "text" + ], + "additionalProperties": false, + "properties": { + "text": { + "type": "string" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 10000, + "default": 300 + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "fits", + "length" + ], + "properties": { + "fits": { + "type": "boolean" + }, + "length": { + "type": "integer", + "minimum": 0 + } + } + } + }, + "side_effects": "none", + "idempotent": true, + "scopes_used": [ + "compute.local" + ], + "error_envelope": "standard", + "examples": [ + { + "description": "Plain ASCII fits.", + "input": { + "text": "Hello world" + }, + "output": { + "fits": true, + "length": 11 + } + }, + { + "description": "Emoji counts as one grapheme, not four bytes.", + "input": { + "text": "\ud83d\udc4b" + }, + "output": { + "fits": true, + "length": 1 + } + } + ], + "runtime_telemetry": {} + }, + { + "name": "truncate", + "summary": "Truncate text to fit the grapheme limit, walking back to the last whitespace boundary if possible.", + "description": "If the text already fits, return it unchanged. Otherwise slice to (limit - len(suffix)) graphemes, walk back to the last space/newline/tab, and append the suffix. Returns at most `limit` graphemes total.", + "docs": { + "goal": "Trim text to fit Bluesky's grapheme cap without breaking words.", + "inputs_brief": "text (req), limit (int, default 300), suffix (string, default '\u2026')", + "outputs_brief": "{text: string, length: int, truncated: bool}", + "errors_brief": "(none \u2014 pure compute)", + "example": "truncate text='very long...' limit=50" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "truncate" + ] + }, + "input": { + "type": "object", + "required": [ + "text" + ], + "additionalProperties": false, + "properties": { + "text": { + "type": "string" + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 10000, + "default": 300 + }, + "suffix": { + "type": "string", + "default": "\u2026" + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "text", + "length", + "truncated" + ], + "properties": { + "text": { + "type": "string" + }, + "length": { + "type": "integer", + "minimum": 0 + }, + "truncated": { + "type": "boolean" + } + } + } + }, + "side_effects": "none", + "idempotent": true, + "scopes_used": [ + "compute.local" + ], + "error_envelope": "standard", + "runtime_telemetry": {} + } + ], + "smoke": { + "kind": "shell", + "command": [ + "python", + "-c", + "from muninn_utils.bsky_limit import fits, truncate\nassert fits('hello') is True\nassert fits('x' * 301) is False\nassert truncate('x' * 400).endswith('\u2026')\nprint('OK')\n" + ], + "timeout_seconds": 10, + "success": { + "exit_code": 0, + "stdout_regex": "^OK$" + } + }, + "kill_switch": { + "kind": "manual", + "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/bsky-limit/REVOKE.md" + }, + "cost": { + "install_fee_cents": 0, + "monthly_fee_cents": 0, + "usage_model": "none" + }, + "support": { + "issues_url": "https://github.com/oaustegard/muninn-utilities/issues", + "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/bsky_limit.py" + } +} diff --git a/mirrors/manifests/muninn-issue-close.json b/mirrors/manifests/muninn-issue-close.json new file mode 100644 index 0000000..d6113c7 --- /dev/null +++ b/mirrors/manifests/muninn-issue-close.json @@ -0,0 +1,287 @@ +{ + "manifest_version": "0.4", + "tool": { + "id": "muninn-issue-close", + "version": "0.1.0", + "name": "Muninn issue_close", + "summary": "Close a GitHub issue with a learning synthesis. Posts the synthesis as a closing comment, then writes it as a `decision` memory tagged with the issue number \u2014 encoded as a flowing DAG so the close ack returns the moment GitHub returns 2xx, while the memory write happens detached.", + "description": "Two-artifact close: GitHub issue gets the implementation log (the closing comment), Muninn's memory store gets the behavioral learning (the decision memory). Wraps a `flowing` graph so synthesis-text validation runs BEFORE any GitHub call fires, the close-issue ack is the terminal node (returns immediately on 2xx), and the memory-store + optional pending-test-verification run as detached side-effects whose failure populates `flow.detached_failures` rather than bubbling up. Same shape as `perch_publish` (single primary credential, structured comment) but writes to a different surface (issues, not pages) and with a paired Turso write per close. Issue #5 calls this out as the credential-reuse-with-perch_publish test.", + "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/issue_close.py", + "author": { + "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)", + "url": "https://muninn.austegard.com" + }, + "license": "MIT", + "tags": [ + "github", + "issues", + "decision-memory", + "learning-synthesis", + "flowing" + ] + }, + "runtime": { + "kind": "python-module", + "install": { + "method": "preinstalled", + "locator": { + "kind": "python-module", + "module": "muninn_utils.issue_close" + } + }, + "entrypoint": { + "command": [ + "python", + "-m", + "muninn_utils.issue_close" + ] + } + }, + "env": [ + { + "name": "GH_TOKEN", + "prompt": "GitHub personal access token. Needs write access (issues scope) to the target repo. Classic PAT with repo scope works; fine-grained PAT with explicit per-repo Issues:write is preferred. The same token is used by perch_publish, blog_publish, perch_triage, and verify_patch \u2014 share-by-default is intentional.", + "secret": true, + "required": true, + "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$", + "obtain_url": "https://github.com/settings/personal-access-tokens" + }, + { + "name": "GITHUB_TOKEN", + "prompt": "Optional fallback for GH_TOKEN. The source reads `os.environ.get('GH_TOKEN') or os.environ.get('GITHUB_TOKEN')`.", + "secret": true, + "required": false, + "validation_regex": "^(ghp_[A-Za-z0-9]{36}|github_pat_[A-Za-z0-9_]+)$", + "obtain_url": "https://github.com/settings/personal-access-tokens" + }, + { + "name": "TURSO_TOKEN", + "prompt": "Turso libSQL auth token for the Muninn memory database. The decision memory is written here. Required.", + "secret": true, + "required": true, + "obtain_url": "https://app.turso.tech/" + }, + { + "name": "TURSO_URL", + "prompt": "Hostname of the Muninn memory libSQL database, e.g. 'mydb-username.turso.io'.", + "secret": false, + "required": true, + "validation_regex": "^[a-z0-9-]+\\.[a-z0-9-]+\\.turso\\.io$" + } + ], + "scopes": [ + { + "resource": "github.issues", + "actions": [ + "read", + "write" + ], + "rationale": "Posts a closing comment and toggles state to closed on the target issue. Reads the current issue state to decide whether the close is a no-op.", + "provider_scope": "github-pat (coarse; full account write access)" + }, + { + "resource": "memory.tracking", + "actions": [ + "write" + ], + "rationale": "Stores the learning synthesis as a `decision`-typed memory tagged `issue-N` and the short-form repo identifier so that future recall can find it from either side of the issue/memory pair.", + "provider_scope": "turso-libsql-token (coarse; full DB access)" + }, + { + "resource": "net.outbound", + "actions": [ + "read", + "write" + ], + "rationale": "Talks to api.github.com (REST) for issue read/comment/close and to the configured Turso libSQL host for the memory write. No other outbound destinations.", + "provider_scope": "api.github.com, *.turso.io" + } + ], + "actions": [ + { + "name": "close", + "summary": "Validate synthesis, close the GitHub issue with it as the closing comment, and store the decision memory in the background.", + "description": "Synchronously: validates the synthesis is non-empty, posts it as a comment on the issue, closes the issue. The close-ack (issue_url + comment_url) is returned immediately. Detached: writes a `decision`-type memory with the synthesis body and tags `issue-N`, ``, plus any extra_tags. If pending_test=true, an additional detached node verifies the pending-test contract on the freshly-stored memory. Detached failures populate the returned `detached_failures` array; they do NOT raise. Idempotent at the GitHub layer (closing an already-closed issue returns the same shape) but NOT at the memory layer (each close stores a new memory).", + "docs": { + "goal": "Close a GitHub issue with a learning synthesis and persist the synthesis as a decision memory.", + "inputs_brief": "number (req), synthesis (req), repo (default oaustegard/claude-skills), pending_test (bool), extra_tags (string[])", + "outputs_brief": "{issue_url, comment_url, memory_id, pending_test_applied, detached_failures}", + "errors_brief": "synthesis_empty, issue_not_found, auth_invalid, close_failed", + "example": "close number=619 synthesis='Pattern X works because Y' repo=oaustegard/claude-skills" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "close" + ] + }, + "input": { + "type": "object", + "required": [ + "number", + "synthesis" + ], + "additionalProperties": false, + "properties": { + "number": { + "type": "integer", + "minimum": 1 + }, + "synthesis": { + "type": "string", + "minLength": 1 + }, + "repo": { + "type": "string", + "pattern": "^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$", + "default": "oaustegard/claude-skills" + }, + "pending_test": { + "type": "boolean", + "default": false + }, + "extra_tags": { + "type": "array", + "items": { + "type": "string", + "minLength": 1 + }, + "maxItems": 16, + "default": [] + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "issue_url", + "comment_url", + "memory_id" + ], + "properties": { + "issue_url": { + "type": "string", + "format": "uri" + }, + "comment_url": { + "type": "string", + "format": "uri" + }, + "memory_id": { + "type": "string" + }, + "pending_test_applied": { + "type": "boolean" + }, + "detached_failures": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "error": { + "type": "string" + } + } + } + } + } + } + }, + "side_effects": "destructive", + "idempotent": false, + "scopes_used": [ + "github.issues", + "memory.tracking", + "net.outbound" + ], + "error_envelope": "standard", + "examples": [ + { + "description": "Close issue #619 with a synthesis.", + "input": { + "number": 619, + "synthesis": "Refactor pattern X works because of Y. Constraint: Z.", + "repo": "oaustegard/claude-skills", + "extra_tags": [ + "flowing", + "refactor" + ] + }, + "output": { + "issue_url": "https://github.com/oaustegard/claude-skills/issues/619", + "comment_url": "https://github.com/oaustegard/claude-skills/issues/619#issuecomment-12345", + "memory_id": "abc12345", + "pending_test_applied": false, + "detached_failures": [] + } + } + ], + "runtime_telemetry": {} + } + ], + "data_boundary": { + "reads": [ + { + "resource": "github.issues", + "sensitivity": "low" + } + ], + "transmits": [ + { + "to": "api.github.com", + "fields": [ + "env.GH_TOKEN", + "input.synthesis" + ], + "purpose": "post closing comment + toggle issue state", + "third_party_retention": "none-per-vendor-tos", + "vendor_tos_url": "https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement" + } + ], + "persists": [ + { + "where": "tool_local", + "fields": [ + "synthesis", + "issue_number", + "repo", + "extra_tags" + ] + } + ], + "retention": { + "tool_local_days": 365 + } + }, + "smoke": { + "kind": "shell", + "command": [ + "python", + "-c", + "import os, json, urllib.request\ntoken = os.environ['GH_TOKEN']\nreq = urllib.request.Request('https://api.github.com/repos/oaustegard/claude-skills', headers={'Authorization': f'token {token}', 'Accept': 'application/vnd.github+json', 'User-Agent': 'issue-close-smoke'})\nd = json.loads(urllib.request.urlopen(req).read())\nassert d['permissions']['push'], 'token lacks write access (needed for issue close)'\nprint('OK: write access confirmed to', d['name'])\n" + ], + "timeout_seconds": 10, + "success": { + "exit_code": 0, + "stdout_regex": "OK: write access confirmed to claude-skills" + } + }, + "kill_switch": { + "kind": "manual", + "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/issue-close/REVOKE.md" + }, + "cost": { + "install_fee_cents": 0, + "monthly_fee_cents": 0, + "usage_model": "external" + }, + "support": { + "issues_url": "https://github.com/oaustegard/muninn-utilities/issues", + "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/issue_close.py" + } +} diff --git a/mirrors/manifests/muninn-memory-tfidf.json b/mirrors/manifests/muninn-memory-tfidf.json new file mode 100644 index 0000000..ace9694 --- /dev/null +++ b/mirrors/manifests/muninn-memory-tfidf.json @@ -0,0 +1,189 @@ +{ + "manifest_version": "0.4", + "tool": { + "id": "muninn-memory-tfidf", + "version": "0.1.0", + "name": "Muninn memory_tfidf", + "summary": "TF-IDF index over Muninn's memory summaries. Read-only similarity search, near-duplicate detection, clustering, and outlier identification across the memory store.", + "description": "Loads all memory summaries from Turso once, builds a sklearn TfidfVectorizer + cosine-similarity matrix, and exposes four read-only queries: duplicates() (pairs above a similarity threshold), similar(id) (top-N for a given memory), clusters() (connected components above threshold), outliers() (memories with low max-similarity to anything else). Build is in-memory and ephemeral; nothing persists between calls in the tool's own storage. Authored as the deliberate **minimum-honest-manifest** consumer test for install-manifest-spec v0.3 \u2014 every required field is present, nothing optional is added past what the tool actually does.", + "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/memory_tfidf.py", + "author": { + "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)", + "url": "https://muninn.austegard.com" + }, + "license": "MIT", + "tags": [ + "tf-idf", + "similarity", + "clustering", + "memory", + "read-only" + ] + }, + "runtime": { + "kind": "python-module", + "install": { + "method": "preinstalled", + "locator": { + "kind": "python-module", + "module": "muninn_utils.memory_tfidf" + } + }, + "entrypoint": { + "command": [ + "python", + "-m", + "muninn_utils.memory_tfidf" + ] + } + }, + "env": [ + { + "name": "TURSO_TOKEN", + "prompt": "Turso libSQL auth token for the memory database. Read-only access is sufficient \u2014 this tool never writes. The token is a coarse credential; treat as a secret.", + "secret": true, + "required": true, + "obtain_url": "https://app.turso.tech/" + }, + { + "name": "TURSO_URL", + "prompt": "Hostname of the Turso libSQL database, e.g. 'mydb-username.turso.io'. The tool reads memories via the libSQL HTTP pipeline endpoint.", + "secret": false, + "required": true, + "validation_regex": "^[a-z0-9-]+\\.[a-z0-9-]+\\.turso\\.io$" + } + ], + "scopes": [ + { + "resource": "memory.tracking", + "actions": [ + "read" + ], + "rationale": "Reads all memory summaries (id, summary, tags, type) from the Turso DB to build the index. Never writes.", + "provider_scope": "turso-libsql-token (coarse; full DB access)" + }, + { + "resource": "net.outbound", + "actions": [ + "read" + ], + "rationale": "Talks to the configured Turso libSQL host for the initial fetch. No other outbound destinations.", + "provider_scope": "*.turso.io" + } + ], + "actions": [ + { + "name": "build_and_query", + "summary": "Fetch all memory summaries, build the TF-IDF index, and return a query result (duplicates / similar / clusters / outliers).", + "description": "Single combined action because the build is the expensive step (one SELECT over the full memory table plus the TfidfVectorizer fit). All four query modes share the same matrix; the action takes a `mode` discriminant and the parameters relevant to that mode. The matrix is not cached between invocations \u2014 each call rebuilds. Read-only at every layer.", + "docs": { + "goal": "Run a TF-IDF similarity query over the memory store.", + "inputs_brief": "mode (duplicates|similar|clusters|outliers), threshold (float), id (for similar), n (for similar/outliers)", + "outputs_brief": "{mode: string, results: array, build_time_ms: number, total_memories: int}", + "errors_brief": "tracking_unconfigured (TURSO_* not set), tracking_unreachable, mode_unknown, id_not_found", + "example": "build_and_query mode=duplicates threshold=0.8" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "build-and-query" + ] + }, + "input": { + "type": "object", + "required": [ + "mode" + ], + "additionalProperties": false, + "properties": { + "mode": { + "type": "string", + "enum": [ + "duplicates", + "similar", + "clusters", + "outliers" + ] + }, + "threshold": { + "type": "number", + "minimum": 0, + "maximum": 1, + "default": 0.8 + }, + "id": { + "type": "string", + "description": "Memory id; required when mode=similar." + }, + "n": { + "type": "integer", + "minimum": 1, + "maximum": 200, + "default": 5 + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "mode", + "results", + "build_time_ms", + "total_memories" + ], + "properties": { + "mode": { + "type": "string" + }, + "results": { + "type": "array" + }, + "build_time_ms": { + "type": "number" + }, + "total_memories": { + "type": "integer", + "minimum": 0 + } + } + } + }, + "side_effects": "read", + "idempotent": true, + "scopes_used": [ + "memory.tracking", + "net.outbound" + ], + "error_envelope": "standard", + "runtime_telemetry": {} + } + ], + "smoke": { + "kind": "shell", + "command": [ + "python", + "-c", + "from muninn_utils.memory_tfidf import MemoryIndex\nidx = MemoryIndex()\nidx.build(memories=[{'id': 'a', 'summary': 'apple banana', 'tags': []}, {'id': 'b', 'summary': 'banana cherry', 'tags': []}])\nassert len(idx.ids) == 2\nprint('OK')\n" + ], + "timeout_seconds": 10, + "success": { + "exit_code": 0, + "stdout_regex": "^OK$" + } + }, + "kill_switch": { + "kind": "manual", + "instructions_url": "https://github.com/oaustegard/muninn-utilities/blob/main/manifests/memory-tfidf/REVOKE.md" + }, + "cost": { + "install_fee_cents": 0, + "monthly_fee_cents": 0, + "usage_model": "none" + }, + "support": { + "issues_url": "https://github.com/oaustegard/muninn-utilities/issues", + "docs_url": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/memory_tfidf.py" + } +} diff --git a/mirrors/manifests/muninn-news-watch.json b/mirrors/manifests/muninn-news-watch.json new file mode 100644 index 0000000..ab9de7a --- /dev/null +++ b/mirrors/manifests/muninn-news-watch.json @@ -0,0 +1,415 @@ +{ + "manifest_version": "0.4", + "tool": { + "id": "muninn-news-watch", + "version": "0.1.0", + "name": "Muninn news_watch", + "summary": "Watch claude.com/blog for new posts during Daily Perch. Pure parsing + watermark state; HTTP fetching is delegated to the caller's web_fetch tool (claude.com WAFs raw container egress).", + "description": "Five functions: parse_claude_blog(content) extracts post links, dates and categories from rendered blog-index content; filter_new(posts, last_seen) returns posts strictly newer than the watermark; get_last_seen/set_last_seen read and write a single ISO date in Turso config (key 'claude-blog-last-seen-iso', category 'ops'); format_for_report(new_posts) renders HTML
  • rows for the perch report. First run (last_seen is None) returns no new posts so the seed run doesn't alert on already-historical content; the watermark advances even when no new posts are found to avoid re-scanning the same back-window. The tool itself does NOT make outbound HTTP fetches against claude.com — that's the caller's web_fetch (claude.com 403s raw HTTP from container egress); the tool only parses content the caller already fetched and reads/writes the watermark in Turso.", + "homepage": "https://github.com/oaustegard/muninn-utilities/blob/main/muninn_utils/news_watch.py", + "author": { + "name": "Muninn (raven of memory; agent operating on behalf of Oskar Austegard)", + "url": "https://muninn.austegard.com" + }, + "license": "MIT", + "tags": [ + "news", + "watermark", + "claude-blog", + "parsing", + "perch" + ] + }, + "runtime": { + "kind": "python-module", + "install": { + "method": "preinstalled", + "locator": { + "kind": "python-module", + "module": "muninn_utils.news_watch" + } + }, + "entrypoint": { + "command": [ + "python", + "-m", + "muninn_utils.news_watch" + ] + } + }, + "env": [ + { + "name": "TURSO_TOKEN", + "prompt": "Turso libSQL auth token for the Muninn memory database. Required for the watermark read/write (get_last_seen / set_last_seen route through `scripts.config_get`/`config_set`, which uses these credentials). Treat as a secret.", + "secret": true, + "required": true, + "obtain_url": "https://app.turso.tech/" + }, + { + "name": "TURSO_URL", + "prompt": "Hostname of the Muninn memory libSQL database, e.g. 'mydb-username.turso.io'.", + "secret": false, + "required": true, + "validation_regex": "^[a-z0-9-]+\\.[a-z0-9-]+\\.turso\\.io$" + } + ], + "scopes": [ + { + "resource": "memory.tracking", + "actions": [ + "read", + "write" + ], + "rationale": "Reads and writes a single watermark value in the Turso config table (key 'claude-blog-last-seen-iso', category 'ops') so consecutive perch runs only surface posts newer than the last-seen date.", + "provider_scope": "turso-libsql-token (coarse; full DB access)" + }, + { + "resource": "net.outbound", + "actions": [ + "read", + "write" + ], + "rationale": "Talks to the configured Turso libSQL host for watermark state. No direct HTTP fetches against claude.com or any caller-supplied URL — fetching is the caller's responsibility via web_fetch.", + "provider_scope": "*.turso.io" + } + ], + "actions": [ + { + "name": "parse_claude_blog", + "summary": "Extract blog posts from the rendered content of claude.com/blog. Pure parse — no I/O.", + "description": "Regex-based extractor that anchors on links to /blog/, walks the surrounding 600-char window backwards to find the nearest date (Month D, YYYY), and matches standalone category labels against a known list. Returns post dicts de-duplicated by URL, sorted newest-first by date, filtered to those with a parseable date.", + "docs": { + "goal": "Parse a fetched copy of claude.com/blog into structured post records.", + "inputs_brief": "content (req: rendered blog-index page; markdown or HTML — the regex shape matches either)", + "outputs_brief": "{posts: [{url, title, date (YYYY-MM-DD), category}]}", + "errors_brief": "(none — returns an empty list on parse miss)", + "example": "parse_claude_blog content='...blog page text...'" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "parse-claude-blog" + ] + }, + "input": { + "type": "object", + "required": [ + "content" + ], + "additionalProperties": false, + "properties": { + "content": { + "type": "string" + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "posts" + ], + "properties": { + "posts": { + "type": "array", + "items": { + "type": "object", + "required": [ + "url", + "title", + "date" + ], + "properties": { + "url": { + "type": "string", + "format": "uri" + }, + "title": { + "type": "string" + }, + "date": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, + "category": { + "type": [ + "string", + "null" + ] + } + } + } + } + } + } + }, + "side_effects": "none", + "idempotent": true, + "scopes_used": [], + "error_envelope": "standard", + "runtime_telemetry": {} + }, + { + "name": "filter_new", + "summary": "Return posts strictly newer than the watermark. Read-only, pure compute.", + "description": "Drops posts whose ISO date is <= last_seen. When last_seen is null (first run) returns an empty list — the seed run does not alert on historical content; the caller still advances set_last_seen so the next run alerts on anything published in between.", + "docs": { + "goal": "Filter parsed posts down to those newer than the watermark.", + "inputs_brief": "posts (req: from parse_claude_blog), last_seen (ISO date or null)", + "outputs_brief": "{new_posts: [...]}", + "errors_brief": "(none — pure compute)", + "example": "filter_new posts=[...] last_seen='2026-05-20'" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "filter-new" + ] + }, + "input": { + "type": "object", + "required": [ + "posts" + ], + "additionalProperties": false, + "properties": { + "posts": { + "type": "array" + }, + "last_seen": { + "type": [ + "string", + "null" + ], + "default": null + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "new_posts" + ], + "properties": { + "new_posts": { + "type": "array" + } + } + } + }, + "side_effects": "none", + "idempotent": true, + "scopes_used": [], + "error_envelope": "standard", + "runtime_telemetry": {} + }, + { + "name": "get_last_seen", + "summary": "Read the last-seen ISO date watermark from Turso config.", + "description": "Calls scripts.config_get('claude-blog-last-seen-iso'). Returns the stored ISO date, or null when the key is unset (first run).", + "docs": { + "goal": "Fetch the watermark for the next perch run.", + "inputs_brief": "key (optional, default 'claude-blog-last-seen-iso')", + "outputs_brief": "{last_seen: string|null}", + "errors_brief": "tracking_unconfigured", + "example": "get_last_seen" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "get-last-seen" + ] + }, + "input": { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "type": "string", + "default": "claude-blog-last-seen-iso" + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "last_seen" + ], + "properties": { + "last_seen": { + "type": [ + "string", + "null" + ] + } + } + } + }, + "side_effects": "read", + "idempotent": true, + "scopes_used": [ + "memory.tracking", + "net.outbound" + ], + "error_envelope": "standard", + "runtime_telemetry": {} + }, + { + "name": "set_last_seen", + "summary": "Write the last-seen ISO date watermark to Turso config under category 'ops'.", + "description": "Calls scripts.config_set(key, iso, 'ops'). Idempotent at the storage layer (re-setting to the same value is a no-op semantically; the underlying upsert touches the row).", + "docs": { + "goal": "Advance the watermark to the supplied ISO date.", + "inputs_brief": "iso (req: YYYY-MM-DD), key (optional, default 'claude-blog-last-seen-iso')", + "outputs_brief": "{stored: bool}", + "errors_brief": "tracking_unconfigured, iso_invalid", + "example": "set_last_seen iso='2026-05-27'" + }, + "invocation": { + "kind": "stdin-json", + "argv_template": [ + "set-last-seen" + ] + }, + "input": { + "type": "object", + "required": [ + "iso" + ], + "additionalProperties": false, + "properties": { + "iso": { + "type": "string", + "pattern": "^\\d{4}-\\d{2}-\\d{2}$" + }, + "key": { + "type": "string", + "default": "claude-blog-last-seen-iso" + } + } + }, + "output": { + "format": "json", + "schema": { + "type": "object", + "required": [ + "stored" + ], + "properties": { + "stored": { + "type": "boolean" + } + } + } + }, + "side_effects": "write", + "idempotent": true, + "scopes_used": [ + "memory.tracking", + "net.outbound" + ], + "error_envelope": "standard", + "runtime_telemetry": {} + }, + { + "name": "format_for_report", + "summary": "Render new posts as HTML
  • rows for the perch report. Pure compute, read-only.", + "description": "Returns inner HTML (no