From a24d6377415fa8788a33eaf00450b1171ac30a40 Mon Sep 17 00:00:00 2001 From: Aaron Stannard Date: Thu, 29 Oct 2026 14:08:13 -0500 Subject: [PATCH 1/6] docs: screenshot pipeline, generators, CI checks and sidebar for the 0.28 docs refresh Re-taken screenshots from the 0.28.2 release image, the containerised capture stack, the config and tools reference generators, the rendered-page and link checks, the Mattermost compose example, and the sidebar entries for the new pages that the following PRs add. --- .claude/skills/netclaw-docs/SKILL.md | 121 ++- .github/workflows/deploy.yml | 19 + .github/workflows/pr-validation.yml | 38 + AGENTS.md | 2 +- CLAUDE.md | 78 +- astro.config.mjs | 110 ++- docker-compose.screenshots.yml | 110 +-- examples/mattermost-compose/.env.example | 14 + examples/mattermost-compose/.gitignore | 2 + examples/mattermost-compose/README.md | 26 + examples/mattermost-compose/compose.yaml | 99 ++ .../mattermost-compose/configure-netclaw.sh | 40 + examples/mattermost-compose/provision.sh | 105 +++ package.json | 4 +- public/_redirects | 2 + public/assets/approval-prompt.png | Bin 54256 -> 85593 bytes public/assets/mcp-icon.svg | 10 - public/assets/netclaw-horizontal-dark.svg | 26 - public/assets/netclaw-stacked-purple.svg | 26 - screenshots/.env.example | 12 + screenshots/.gitignore | 3 + screenshots/README.md | 177 +++- screenshots/capture.sh | 109 ++- screenshots/capture/Dockerfile | 48 + screenshots/capture/entrypoint.sh | 63 ++ screenshots/compose/configured.yml | 29 + screenshots/compose/docker-host.yml | 12 + screenshots/compose/forwarder.conf.template | 23 + screenshots/compose/forwarder.yml | 32 + screenshots/compose/ollama.yml | 31 + screenshots/compose/projects.yml | 25 + screenshots/compose/skillserver.yml | 31 + screenshots/lib.sh | 109 +++ screenshots/mattermost/approval.mjs | 95 ++ screenshots/mattermost/bootstrap.mjs | 13 +- screenshots/mattermost/capture-compose.mjs | 266 ++++++ screenshots/mattermost/capture.mjs | 16 +- .../mattermost/docker-compose.mattermost.yml | 114 ++- screenshots/mattermost/frame-approval.mjs | 34 + screenshots/mattermost/post-message.mjs | 44 + screenshots/mattermost/run.sh | 176 ++-- screenshots/mattermost/scripted-llm.mjs | 59 ++ screenshots/mattermost/tcp-forward.mjs | 14 + screenshots/mcp-server/Dockerfile | 3 +- screenshots/mcp-server/Dockerfile.oauth | 6 + screenshots/mcp-server/oauth_server.py | 246 +++++ screenshots/output/approval-prompt-tui.png | Bin 0 -> 177412 bytes screenshots/output/approvals-list-json.png | Bin 103476 -> 0 bytes screenshots/output/approvals-list.png | Bin 119090 -> 125994 bytes .../output/approvals-tui-revoke-confirm.png | Bin 60768 -> 47929 bytes .../output/approvals-tui-revoke-done.png | Bin 182036 -> 0 bytes screenshots/output/approvals-tui-selected.png | Bin 179050 -> 0 bytes screenshots/output/approvals-tui.png | Bin 179028 -> 164508 bytes screenshots/output/approvals.gif | Bin 423569 -> 0 bytes screenshots/output/chat-approval-prompt.png | Bin 0 -> 157147 bytes screenshots/output/chat-first-greeting.png | Bin 0 -> 169049 bytes screenshots/output/chat-intro-greeting.png | Bin 47300 -> 0 bytes screenshots/output/chat-profile-questions.png | Bin 63370 -> 0 bytes .../output/chat-profile-update-soul.md.png | Bin 62954 -> 0 bytes .../output/chat-tui-approval-prompt.png | Bin 0 -> 173076 bytes screenshots/output/chat-tui-idle.png | Bin 0 -> 145984 bytes screenshots/output/config-browser.png | Bin 196352 -> 0 bytes .../output/config-channel-audiences.png | Bin 0 -> 123491 bytes screenshots/output/config-channels-menu.png | Bin 112478 -> 80651 bytes screenshots/output/config-dashboard.png | Bin 135842 -> 128578 bytes screenshots/output/config-exposure.png | Bin 138634 -> 110445 bytes .../output/config-inbound-webhooks.png | Bin 165899 -> 102616 bytes screenshots/output/config-retention.png | Bin 0 -> 54905 bytes screenshots/output/config-search.png | Bin 77322 -> 60431 bytes screenshots/output/config-skills.png | Bin 109141 -> 108918 bytes screenshots/output/config-telemetry.png | Bin 123834 -> 0 bytes screenshots/output/config-workspaces.png | Bin 63180 -> 0 bytes screenshots/output/daemon-pair.png | Bin 0 -> 66799 bytes .../output/discord-setup-applications.png | Bin 150412 -> 118299 bytes .../output/discord-setup-authorize-dialog.png | Bin 96785 -> 79000 bytes ...scord-setup-installation-provided-link.png | Bin 212917 -> 0 bytes .../output/discord-setup-oauth-url.png | Bin 26079 -> 19588 bytes screenshots/output/docker-health.png | Bin 0 -> 110679 bytes screenshots/output/doctor.gif | Bin 342852 -> 0 bytes screenshots/output/doctor.png | Bin 650697 -> 449142 bytes screenshots/output/init-01-endpoint.png | Bin 61741 -> 60416 bytes screenshots/output/init-01-model-select.png | Bin 42168 -> 36362 bytes screenshots/output/init-01-provider-list.png | Bin 57860 -> 62773 bytes .../output/init-01-provider-ollama.png | Bin 61741 -> 55848 bytes screenshots/output/init-02-identity.png | Bin 72206 -> 61595 bytes .../output/init-02-security-posture.png | Bin 137721 -> 97505 bytes .../output/init-04-channel-audiences.png | Bin 39860 -> 0 bytes .../output/init-04-enabled-features.png | Bin 153126 -> 123546 bytes screenshots/output/init-05-health-check.png | Bin 49559 -> 90209 bytes .../output/init-07-external-skills.png | Bin 89933 -> 0 bytes screenshots/output/init-09-exposure.png | Bin 151728 -> 0 bytes screenshots/output/init-existing-menu.png | Bin 121433 -> 101575 bytes screenshots/output/init-start-over-scope.png | Bin 88581 -> 72023 bytes .../output/mattermost-bot-accounts.png | Bin 50483 -> 90188 bytes .../mattermost-compose-approval-resolved.png | Bin 0 -> 149266 bytes .../output/mattermost-compose-approval.png | Bin 0 -> 102519 bytes .../output/mattermost-compose-bot-account.png | Bin 0 -> 29654 bytes .../output/mattermost-compose-thread.png | Bin 0 -> 163975 bytes .../output/mattermost-conversation.png | Bin 107827 -> 222425 bytes screenshots/output/mattermost-create-bot.png | Bin 54535 -> 134609 bytes screenshots/output/mcp-tools-confirm.png | Bin 0 -> 112888 bytes screenshots/output/mcp-tools-personal.png | Bin 157121 -> 114708 bytes screenshots/output/mcp-tools-public.png | Bin 124436 -> 0 bytes .../output/mcp-tools-server-list-2.png | Bin 56921 -> 0 bytes screenshots/output/mcp-tools-server-list.png | Bin 56588 -> 31023 bytes .../output/mcp-tools-server2-personal.png | Bin 111088 -> 0 bytes screenshots/output/mcp-tools-team.png | Bin 123201 -> 106961 bytes screenshots/output/mcp-tools.gif | Bin 331764 -> 0 bytes screenshots/output/model-manager.png | Bin 94359 -> 90945 bytes screenshots/output/model-properties.png | Bin 0 -> 122176 bytes screenshots/output/model.gif | Bin 72634 -> 0 bytes .../output/provider-add-openai-compatible.png | Bin 0 -> 76494 bytes screenshots/output/provider-manager.png | Bin 113340 -> 121898 bytes screenshots/output/provider.gif | Bin 111281 -> 0 bytes .../output/reminder-builder-review.png | Bin 0 -> 78169 bytes screenshots/output/reminder-list.png | Bin 163843 -> 69965 bytes screenshots/output/reminder-run-approval.png | Bin 0 -> 226803 bytes screenshots/output/reminder.gif | Bin 93237 -> 245166 bytes screenshots/output/sessions-tui.png | Bin 0 -> 63279 bytes .../output/single-shot-chat-web-fetch.png | Bin 44758 -> 0 bytes .../output/single-shot-cli-execution.png | Bin 42438 -> 159790 bytes screenshots/output/skill-list.png | Bin 0 -> 126993 bytes screenshots/output/skill-sync.png | Bin 0 -> 64472 bytes .../output/slack-channel-integration.png | Bin 53367 -> 0 bytes screenshots/output/slack-install-app.png | Bin 41727 -> 34138 bytes screenshots/output/slash-unknown-command.png | Bin 0 -> 103283 bytes screenshots/output/stats.gif | Bin 119512 -> 0 bytes screenshots/output/stats.png | Bin 191797 -> 129884 bytes screenshots/output/status.gif | Bin 130847 -> 0 bytes screenshots/output/status.png | Bin 243910 -> 171512 bytes screenshots/output/update-check.png | Bin 0 -> 53494 bytes screenshots/output/webhooks-list.png | Bin 41963 -> 44770 bytes screenshots/output/webhooks.gif | Bin 30918 -> 0 bytes screenshots/playwright/Dockerfile | 16 + screenshots/scripts/docker-health-down.sh | 10 + screenshots/scripts/docker-health-up.sh | 33 + screenshots/scripts/lib-seed.sh | 34 + screenshots/scripts/mock-mattermost.py | 45 + screenshots/scripts/ollama-init.sh | 8 +- screenshots/scripts/redact-third-party.py | 65 ++ screenshots/scripts/seed-approvals.sh | 42 - screenshots/scripts/seed-assistant.sh | 37 + screenshots/scripts/seed-channel-audiences.sh | 22 + screenshots/scripts/seed-config-demo.sh | 32 + screenshots/scripts/seed-demo-install.sh | 81 ++ screenshots/scripts/seed-paired-device.sh | 10 + screenshots/scripts/seed-projects.sh | 28 + screenshots/scripts/seed-providers.sh | 31 + screenshots/scripts/seed-reminders.sh | 10 + screenshots/scripts/seed-skills.sh | 91 ++ screenshots/scripts/seed-webhooks.sh | 16 + screenshots/shoot-page.sh | 30 + screenshots/stack.sh | 111 +++ screenshots/tapes/approvals-list.tape | 18 + screenshots/tapes/approvals-tui.tape | 28 + screenshots/tapes/approvals.tape | 94 -- screenshots/tapes/channel-audiences.tape | 51 + screenshots/tapes/chat-approval.tape | 61 ++ screenshots/tapes/chat-first-greeting.tape | 44 + screenshots/tapes/chat-session.tape | 44 + screenshots/tapes/config.tape | 133 +++ screenshots/tapes/daemon-pair.tape | 32 + screenshots/tapes/docker-health.tape | 44 + screenshots/tapes/doctor.tape | 26 +- screenshots/tapes/help.tape | 2 +- screenshots/tapes/include/init-personal.tape | 31 + screenshots/tapes/include/seed-approvals.tape | 43 + screenshots/tapes/init-existing.tape | 35 + screenshots/tapes/init-team.tape | 50 + screenshots/tapes/init-wizard.tape | 250 ++--- screenshots/tapes/mcp-tools.tape | 47 +- screenshots/tapes/model-properties.tape | 35 + screenshots/tapes/model.tape | 28 +- screenshots/tapes/provider.tape | 57 +- screenshots/tapes/reminder-list.tape | 26 + screenshots/tapes/reminder-run.tape | 34 + screenshots/tapes/reminder.tape | 44 +- screenshots/tapes/sessions.tape | 79 ++ .../tapes/single-shot-cli-execution.tape | 31 + screenshots/tapes/skill-list.tape | 25 + screenshots/tapes/skill-sync.tape | 26 + screenshots/tapes/stats.tape | 72 +- screenshots/tapes/status.tape | 25 +- screenshots/tapes/update-check.tape | 24 + screenshots/tapes/webhooks.tape | 25 +- scripts/check-links.mjs | 75 ++ scripts/check-rendered-pages.mjs | 81 ++ scripts/config-reference-overrides.json | 890 ++++++++++++++++++ scripts/gen-config-reference.mjs | 374 ++++++++ scripts/gen-tools-reference.mjs | 355 +++++++ scripts/generate-social-images.mjs | 3 + scripts/screenshot-page.mjs | 90 +- scripts/tools-reference-overrides.json | 196 ++++ src/pages/index.astro | 24 +- src/styles/custom.css | 23 + src/version.json | 4 +- 196 files changed, 6191 insertions(+), 862 deletions(-) create mode 100644 examples/mattermost-compose/.env.example create mode 100644 examples/mattermost-compose/.gitignore create mode 100644 examples/mattermost-compose/README.md create mode 100644 examples/mattermost-compose/compose.yaml create mode 100755 examples/mattermost-compose/configure-netclaw.sh create mode 100755 examples/mattermost-compose/provision.sh delete mode 100644 public/assets/mcp-icon.svg delete mode 100644 public/assets/netclaw-horizontal-dark.svg delete mode 100644 public/assets/netclaw-stacked-purple.svg create mode 100644 screenshots/.env.example create mode 100644 screenshots/.gitignore create mode 100644 screenshots/capture/Dockerfile create mode 100755 screenshots/capture/entrypoint.sh create mode 100644 screenshots/compose/configured.yml create mode 100644 screenshots/compose/docker-host.yml create mode 100644 screenshots/compose/forwarder.conf.template create mode 100644 screenshots/compose/forwarder.yml create mode 100644 screenshots/compose/ollama.yml create mode 100644 screenshots/compose/projects.yml create mode 100644 screenshots/compose/skillserver.yml create mode 100755 screenshots/lib.sh create mode 100644 screenshots/mattermost/approval.mjs create mode 100644 screenshots/mattermost/capture-compose.mjs create mode 100644 screenshots/mattermost/frame-approval.mjs create mode 100644 screenshots/mattermost/post-message.mjs create mode 100644 screenshots/mattermost/scripted-llm.mjs create mode 100644 screenshots/mattermost/tcp-forward.mjs create mode 100644 screenshots/mcp-server/Dockerfile.oauth create mode 100644 screenshots/mcp-server/oauth_server.py create mode 100644 screenshots/output/approval-prompt-tui.png delete mode 100644 screenshots/output/approvals-list-json.png delete mode 100644 screenshots/output/approvals-tui-revoke-done.png delete mode 100644 screenshots/output/approvals-tui-selected.png delete mode 100644 screenshots/output/approvals.gif create mode 100644 screenshots/output/chat-approval-prompt.png create mode 100644 screenshots/output/chat-first-greeting.png delete mode 100644 screenshots/output/chat-intro-greeting.png delete mode 100644 screenshots/output/chat-profile-questions.png delete mode 100644 screenshots/output/chat-profile-update-soul.md.png create mode 100644 screenshots/output/chat-tui-approval-prompt.png create mode 100644 screenshots/output/chat-tui-idle.png delete mode 100644 screenshots/output/config-browser.png create mode 100644 screenshots/output/config-channel-audiences.png create mode 100644 screenshots/output/config-retention.png delete mode 100644 screenshots/output/config-telemetry.png delete mode 100644 screenshots/output/config-workspaces.png create mode 100644 screenshots/output/daemon-pair.png delete mode 100644 screenshots/output/discord-setup-installation-provided-link.png create mode 100644 screenshots/output/docker-health.png delete mode 100644 screenshots/output/doctor.gif delete mode 100644 screenshots/output/init-04-channel-audiences.png delete mode 100644 screenshots/output/init-07-external-skills.png delete mode 100644 screenshots/output/init-09-exposure.png create mode 100644 screenshots/output/mattermost-compose-approval-resolved.png create mode 100644 screenshots/output/mattermost-compose-approval.png create mode 100644 screenshots/output/mattermost-compose-bot-account.png create mode 100644 screenshots/output/mattermost-compose-thread.png create mode 100644 screenshots/output/mcp-tools-confirm.png delete mode 100644 screenshots/output/mcp-tools-public.png delete mode 100644 screenshots/output/mcp-tools-server-list-2.png delete mode 100644 screenshots/output/mcp-tools-server2-personal.png delete mode 100644 screenshots/output/mcp-tools.gif create mode 100644 screenshots/output/model-properties.png delete mode 100644 screenshots/output/model.gif create mode 100644 screenshots/output/provider-add-openai-compatible.png delete mode 100644 screenshots/output/provider.gif create mode 100644 screenshots/output/reminder-builder-review.png create mode 100644 screenshots/output/reminder-run-approval.png create mode 100644 screenshots/output/sessions-tui.png delete mode 100644 screenshots/output/single-shot-chat-web-fetch.png create mode 100644 screenshots/output/skill-list.png create mode 100644 screenshots/output/skill-sync.png delete mode 100644 screenshots/output/slack-channel-integration.png create mode 100644 screenshots/output/slash-unknown-command.png delete mode 100644 screenshots/output/stats.gif delete mode 100644 screenshots/output/status.gif create mode 100644 screenshots/output/update-check.png delete mode 100644 screenshots/output/webhooks.gif create mode 100644 screenshots/playwright/Dockerfile create mode 100755 screenshots/scripts/docker-health-down.sh create mode 100755 screenshots/scripts/docker-health-up.sh create mode 100755 screenshots/scripts/lib-seed.sh create mode 100755 screenshots/scripts/mock-mattermost.py create mode 100644 screenshots/scripts/redact-third-party.py delete mode 100755 screenshots/scripts/seed-approvals.sh create mode 100755 screenshots/scripts/seed-assistant.sh create mode 100755 screenshots/scripts/seed-channel-audiences.sh create mode 100755 screenshots/scripts/seed-config-demo.sh create mode 100755 screenshots/scripts/seed-demo-install.sh create mode 100755 screenshots/scripts/seed-paired-device.sh create mode 100644 screenshots/scripts/seed-projects.sh create mode 100755 screenshots/scripts/seed-providers.sh create mode 100644 screenshots/scripts/seed-reminders.sh create mode 100644 screenshots/scripts/seed-skills.sh create mode 100644 screenshots/scripts/seed-webhooks.sh create mode 100755 screenshots/shoot-page.sh create mode 100755 screenshots/stack.sh create mode 100644 screenshots/tapes/approvals-list.tape create mode 100644 screenshots/tapes/approvals-tui.tape delete mode 100644 screenshots/tapes/approvals.tape create mode 100644 screenshots/tapes/channel-audiences.tape create mode 100644 screenshots/tapes/chat-approval.tape create mode 100644 screenshots/tapes/chat-first-greeting.tape create mode 100644 screenshots/tapes/chat-session.tape create mode 100644 screenshots/tapes/config.tape create mode 100644 screenshots/tapes/daemon-pair.tape create mode 100644 screenshots/tapes/docker-health.tape create mode 100644 screenshots/tapes/include/init-personal.tape create mode 100644 screenshots/tapes/include/seed-approvals.tape create mode 100644 screenshots/tapes/init-existing.tape create mode 100644 screenshots/tapes/init-team.tape create mode 100644 screenshots/tapes/model-properties.tape create mode 100644 screenshots/tapes/reminder-list.tape create mode 100644 screenshots/tapes/reminder-run.tape create mode 100644 screenshots/tapes/sessions.tape create mode 100644 screenshots/tapes/single-shot-cli-execution.tape create mode 100644 screenshots/tapes/skill-list.tape create mode 100644 screenshots/tapes/skill-sync.tape create mode 100644 screenshots/tapes/update-check.tape create mode 100644 scripts/check-links.mjs create mode 100644 scripts/check-rendered-pages.mjs create mode 100644 scripts/config-reference-overrides.json create mode 100644 scripts/gen-config-reference.mjs create mode 100644 scripts/gen-tools-reference.mjs create mode 100644 scripts/tools-reference-overrides.json diff --git a/.claude/skills/netclaw-docs/SKILL.md b/.claude/skills/netclaw-docs/SKILL.md index 3a22ecb..1719657 100644 --- a/.claude/skills/netclaw-docs/SKILL.md +++ b/.claude/skills/netclaw-docs/SKILL.md @@ -3,64 +3,93 @@ name: netclaw-docs description: Voice and style contract for netclaw.dev docs (Astro + Starlight, dark-mode only). The how-it-should-sound layer on top of CLAUDE.md's authoring process. --- -# netclaw.dev Docs Voice +# netclaw.dev docs voice -Write like you're explaining netclaw to a sharp coworker who knows Linux, Docker, and Slack but has never touched netclaw. Confident, casual-technical, code-first. Open by telling the reader what the thing does and when they'd reach for it, then get out of the way and show commands. The published pages earn trust by being correct and dense, not by being long. CLAUDE.md owns the process (page types, templates, the critique/humanizer pipeline); this skill owns the voice. +Write like you're explaining netclaw to a sharp coworker who knows Linux, Docker, and Slack but has never touched netclaw. Confident, casual-technical, code-first. Open by saying what the thing does and when you'd reach for it, then show commands. The pages earn trust by being correct and dense, not long. CLAUDE.md owns the process (page types, templates, critique and humanizer steps). This skill owns how the page reads. -## Do - -- **Open with the payload.** First sentence states what the feature does and when you'd use it. No "In this section" runway. See `getting-started/installation.md`: "Netclaw has two components..." -- **Use direct imperative voice.** "Run `netclaw status`." "Store tokens with `netclaw secrets`." Never "You can run" or "Users may wish to." -- **Show, don't tell.** A config block, command, or screenshot beats a paragraph. If CLI help already explains a flag, show the output instead of rewriting it. -- **Tables for anything with a shape** - flags, config fields (Field / Type / Default / Description), scopes, error→cause→fix, mode comparisons. Prose is for the one idea a table can't hold. -- **Name the #1 mistake.** The best pages flag the common footgun in bold ("This is the number one setup mistake"). Lead readers away from the trap. -- **Use Starlight callouts** `:::caution` / `:::note` for breaking changes, footguns, and security boundaries. Reserve them for things that bite. -- **Comment real screenshots in, stub honest TODOs out.** `` and `` are the house pattern; never ship fake "coming soon" prose. -- **Close with cross-links.** End with Related pages + Resources (2+ external links to upstream docs/RFCs). +## Relationship to aarons-voice and humanizer -## Don't +For anything under `src/content/docs/`, this skill supersedes `aarons-voice` and both humanizer skills. It already contains the parts of `aarons-voice` that apply to product docs, so don't load it alongside this one. Deliberately not carried over: bare URLs (use markdown links with descriptive text), the no-bold rule (see Bold), first-person "I", exclamation points, and the email phrase list. Humanizer runs on prose only. Where it disagrees with this skill, this skill wins. -- No filler: "It is important to note that", "This section describes", "The following table shows", "In this guide we will". Cut the sentence; keep the table. -- No hedging: "you may want to", "it is recommended that", "it should be noted". State it. -- No rule-of-three padding ("fast, simple, and powerful") or negative parallelism ("not X, but Y") used for rhythm. -- No inflated symbolism, no "seamless / robust / leverage / delve". Plain verbs. -- Don't explain the obvious. If a reader of this audience already knows it, delete it. -- Don't restate code in prose right after showing it. +## Two registers -## Punctuation +- **Reference** (`cli/`, `configuration/`, `channels/`, `deployment/`, `observability/`, most `guides/`): "you" plus imperative. netclaw is the subject of behavior sentences ("netclaw resolves symlinks first"). No "we", no "I". Opinions stop at "use X unless Y". Exact beats charming. +- **Narrative** (design philosophy, comparisons, tutorials, the landing page): "we" is fine for the project's decisions and opinions. State opinions as opinions. A little roughness is fine. Still never "I". -Use the spaced hyphen ` - ` (space-hyphen-space), not the em-dash (`—`), for parenthetical asides and appositives. As of 2026-07-22 the docs drop the em-dash to match the house writing voice, where the em-dash reads as an AI tell and ` - ` is the connector that replaces it. Outbound email and marketing already ban the em-dash, so there's now one rule everywhere - no docs-versus-email split to remember. +A page is one or the other. If a reference page needs a "why", link to the narrative page. -Style notes for ` - `: it joins clauses mid-sentence only, never after a terminal `.`, `?`, or `!`. If the prior sentence ended, start a new one. Don't stack more than one per sentence, and skip it where a period, colon, comma, or parentheses read cleaner. +## Do -Migration: use ` - ` in every new page and in any page you edit. The ~50 legacy pages still contain em-dashes (`security-model.md` alone has ~38) and stay as they are until you edit the page for another reason, or a dedicated sweep converts them. Some channel pages use the double-hyphen ` -- ` instead. Normalize those to ` - ` when you touch them. +- **Open with the payload.** First sentence says what the feature does and when you'd use it. +- **Lead guides with the answer.** Put an "I want / do this" table first where it fits, then explain. +- **Use the words people search for.** Titles and headings say "turn off", "stop asking", "disable", not internal names. +- **Name the thing.** "Slack, Discord, or Mattermost", not "your chat app". +- **Show, don't tell.** A config block, command or screenshot beats a paragraph. Tables for anything with a shape. Prose is for the one idea a table can't hold, and never restates the table. +- **Name the number one mistake**, once per section at most. +- **Use `:::caution` and `:::note`** for breaking changes, footguns and security boundaries only. +- **One concept page, one guide, one reference per topic.** Other pages link instead of re-explaining. +- **Stub honestly.** `` and ``. Never ship "coming soon" prose. -## Per-page-type quick reference +## Don't -- **CLI** (`cli/`): one-line intent → `## Usage` → options table → examples with a screenshot each → Related Commands. -- **Tutorial** (`getting-started/`): what we're building → Prerequisites (with versions) → numbered Steps with exact commands + expected output → Verify It Works → Next Steps. -- **Guide** (`guides/`): the problem in one line → Before You Begin → steps (call out conditional branches) → Troubleshooting (symptom / cause / fix). -- **Concept** (`architecture/`, `security/`): 2–3 paragraph mental model → key-concept sections → Limitations. -- **Configuration** (`configuration/`): what routes through this → minimal JSON block → full fields table (Field/Type/Default) → invalid-config behavior → diagnosing-errors table. -- **Integration** (`channels/`): transport in one line → Prerequisites → third-party setup (app/tokens/scopes, scopes table) → Configure netclaw → Access control → Verify it works → Troubleshooting. -- **Operations** (`deployment/`): when you need a non-default mode → modes table → per-mode config → restart/validation behavior → `netclaw doctor` troubleshooting. +- No filler ("It is important to note", "This page covers", "The following table shows") and no hedging ("you may want to", "it is recommended"). +- No "No X, no Y, no Z" more than once per page. No slogan triads or punchline fragments for rhythm ("That's unrealistic."). +- No screenshot captions shaped like `Label - fragment; fragment`. A caption is one sentence. +- Cut: "surface" as a noun, "just" as a minimiser, "walks through", "the full breakdown", "out of the box" (say "by default"), "first-class", "at a glance", "worth knowing". +- Don't explain what this audience already knows. Don't restate code in prose after showing it. +- Don't sand good sentences flat to chase a clean score. Fix the tell and keep the sentence. A page that passes every check and reads like a compliance form is worse than one with a stray dash. -## Terminology & product facts +## Punctuation -- **netclaw** is lowercase in running prose. CLI is `netclaw`; daemon is `netclawd`. -- Commands and flags go in backticks: `netclaw status`, `--channel beta`. Config keys too: `Search.Backend`, `Daemon.ExposureMode`. -- **`netclaw init`** = focused 5-step first-run setup (provider, identity, security posture, features) that also powers factory reset. Don't pile post-install config into it. -- **`netclaw config`** = the re-entrant, menu-driven dashboard for everything else (channels, search, exposure, webhooks, skills); safe to re-run, autosaves on completion. -- Cross-link slugs: `/cli//`, `/channels//`, `/configuration//`, `/deployment//`, `/security//`, `/getting-started//`. Trailing slash, lowercase. -- Config lives in `~/.netclaw/config/netclaw.json`; secrets in `~/.netclaw/config/secrets.json` (encrypted at rest, written via `netclaw secrets set`). Env override prefix is `NETCLAW_` with `__` path separators. +- **Contractions everywhere:** it's, don't, you'll, can't. Spell out "do not" or "cannot" only inside a `:::caution` or a security boundary statement, where the stiffness is the point. +- **No semicolons in prose.** Write two sentences. +- **No exclamation points.** No emoji, except status glyphs that mirror real CLI output. +- **Sentence-case headings:** `## Related pages`, `## Before you begin`. Proper nouns and command names keep their case. +- **No em dashes at all.** Pick the replacement by position: + - List separators and table cells: ` - ` or a colon. An empty cell is `-`. + - Headings: a colon (`### 401 Unauthorized: secret mismatch`). Heading edits change anchors, so check inbound links. + - Prose: rewrite with a period, colon or comma first. Keep ` - ` for a real aside, about once per paragraph at most, never after a terminal `.`, `?` or `!`. + - Don't search-and-replace. Forty em dashes turned into forty spaced hyphens is a new tic. Normalize ` -- ` the same way. + +## Bold + +Four uses only: UI labels the reader looks for, one warning per section, Symptom / Cause / Fix labels, and a term where it's defined. No bolded thesis sentences. No bold-label bullet lists where a table or subheadings would do. + +## Structure + +- Every page ends with exactly `## Related pages` (three to five internal links) then `## Resources` (two or more external links). No "Next steps", "See also" or "Keep reading". Tutorials may put `## What's next` before them. +- Every screenshot has the instruction before it and a one-sentence caption after it that says something the alt text doesn't. +- Page-type shapes: + - **CLI:** intent, `## Usage`, options table, examples with a screenshot each. + - **Tutorial:** goal, prerequisites with versions, numbered steps with expected output, verify it works. + - **Guide:** problem in one line, before you begin, steps, troubleshooting (Symptom / Cause / Fix). + - **Concept:** two or three paragraphs of mental model, key sections, limitations. + - **Configuration:** what routes through it, minimal JSON, fields table (Field / Type / Default), invalid-config behavior. + - **Integration:** transport in one line, prerequisites, third-party setup, configure netclaw, access control, verify, troubleshooting. + - **Operations:** when you need a non-default mode, modes table, per-mode config, restart behavior, `netclaw doctor`. + +## Terminology and product facts (0.27.1) + +- netclaw is lowercase in running prose. The CLI is `netclaw`, the daemon is `netclawd`. Commands, flags and config keys go in backticks. +- `netclaw init` is first-run bootstrap only: provider, identity, security posture, then health check. That's four steps for Personal and five for Team or Public, which add a feature-selection step. Channels, search, exposure, webhooks and skill sources live in `netclaw config`, the re-entrant menu dashboard. +- Default base directory is `~/.netclaw`, or `$NETCLAW_HOME` if set. Config is `config/netclaw.json`, secrets are `config/secrets.json` (written with `netclaw secrets set`). Env overrides use the `NETCLAW_` prefix with `__` between path segments. +- `Ctrl+Q` quits every TUI. Escape is per screen: it never quits the config, provider, model, init or chat screens (in chat it denies a pending approval), but it does quit from the `netclaw mcp` server list. State keys per screen from the key-handling code, never as a global rule. +- There is no `netclaw daemon restart`. The daemon restarts itself, after draining sessions, when `netclaw.json` changes. Nothing else triggers it, so secrets and environment changes need a manual restart: + - native: `netclaw daemon stop`, then `netclaw daemon start` + - systemd user unit: `systemctl --user restart netclaw` + - Docker: `docker restart ` (the image's entrypoint supervises the daemon) +- Screenshots and command output come from the release container (`ghcr.io/netclaw-dev/netclaw:`), never a host binary. No private hostnames in samples or screenshots. Use `llm.example.internal` or a named public provider. +- Cross-link slugs are lowercase with a trailing slash: `/cli//`, `/channels//`, `/configuration//`. ## Pre-ship checklist -- [ ] Frontmatter `title` + `description` set. -- [ ] Opens with what/when, not runway. No filler or hedge phrases. -- [ ] Reference material is in tables; code/screenshots carry the weight. -- [ ] netclaw lowercase; commands and config keys backticked; cross-links use correct trailing-slash slugs. -- [ ] No placeholder prose - only `` comments for gaps; all referenced screenshots exist. -- [ ] 2+ external resource links. -- [ ] Every runnable snippet was actually run against the current release. -- [ ] `/humanizer` pass done; `npm run build` passes. +- [ ] Frontmatter `title` and `description` set. Headings in sentence case and built from search words. +- [ ] Opens with what and when. Guides lead with the answer. No filler, hedges or cut-list phrases. +- [ ] No em dashes, ` -- `, or semicolons in prose. ` - ` within budget. Contractions used. +- [ ] Reference page has no "we". No page has "I". Bold only in the four permitted uses. +- [ ] Tables carry reference material and aren't restated in prose. +- [ ] Every screenshot has an instruction before it and a one-sentence caption after it, and the file exists. +- [ ] Ends with `## Related pages` then `## Resources` (2+ external links). +- [ ] Every runnable snippet was run against the release container. No private hostnames. +- [ ] No placeholder prose. Only `` comments for gaps. +- [ ] `humanizer:humanizer` pass done on prose only. `npm run build` passes. diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index d107a59..366e4f6 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -31,6 +31,25 @@ jobs: - name: Install dependencies run: npm ci + - name: Cache Playwright browser + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + + # Mermaid diagrams render at build time through a headless browser. Without it + # the build still succeeds but pages with diagrams ship with an empty body + # (scripts/check-rendered-pages.mjs fails the build when that happens). + # The browser revision follows the playwright version pinned in package-lock.json. + - name: Install Playwright browser + run: | + if [ "${{ steps.playwright-cache.outputs.cache-hit }}" = "true" ]; then + npx playwright install-deps chromium-headless-shell + else + npx playwright install --with-deps chromium-headless-shell + fi + - name: Build site run: npm run build env: diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml index 3a590a5..f9f9339 100644 --- a/.github/workflows/pr-validation.yml +++ b/.github/workflows/pr-validation.yml @@ -22,6 +22,25 @@ jobs: - name: Install dependencies run: npm ci + - name: Cache Playwright browser + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + + # Mermaid diagrams render at build time through a headless browser. Without it + # the build still succeeds but pages with diagrams ship with an empty body + # (scripts/check-rendered-pages.mjs fails the build when that happens). + # The browser revision follows the playwright version pinned in package-lock.json. + - name: Install Playwright browser + run: | + if [ "${{ steps.playwright-cache.outputs.cache-hit }}" = "true" ]; then + npx playwright install-deps chromium-headless-shell + else + npx playwright install --with-deps chromium-headless-shell + fi + - name: Build site run: npm run build @@ -41,6 +60,25 @@ jobs: - name: Install dependencies run: npm ci + - name: Cache Playwright browser + id: playwright-cache + uses: actions/cache@v4 + with: + path: ~/.cache/ms-playwright + key: playwright-${{ runner.os }}-${{ hashFiles('package-lock.json') }} + + # Mermaid diagrams render at build time through a headless browser. Without it + # the build still succeeds but pages with diagrams ship with an empty body + # (scripts/check-rendered-pages.mjs fails the build when that happens). + # The browser revision follows the playwright version pinned in package-lock.json. + - name: Install Playwright browser + run: | + if [ "${{ steps.playwright-cache.outputs.cache-hit }}" = "true" ]; then + npx playwright install-deps chromium-headless-shell + else + npx playwright install --with-deps chromium-headless-shell + fi + - name: Build site run: npm run build diff --git a/AGENTS.md b/AGENTS.md index 29b4b45..be1e61b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,7 +4,7 @@ netclaw.dev documentation site — Astro 6.2 + Starlight, dark-mode only, deploy Two guides govern work here. Read both before writing or editing any doc page: -- **[CLAUDE.md](./CLAUDE.md)** — the authoring *process*: page types, per-type templates, the screenshot pipeline, the critique + `/humanizer` review steps, and build/preview commands. +- **[CLAUDE.md](./CLAUDE.md)** — the authoring *process*: page types, per-type templates, the screenshot pipeline, the critique + `humanizer:humanizer` review steps, and build/preview commands. - **[`.claude/skills/netclaw-docs/SKILL.md`](./.claude/skills/netclaw-docs/SKILL.md)** — the *voice and style* contract: the "less is more" house voice, punctuation policy, per-page-type quick reference, terminology, and the pre-ship checklist. CLAUDE.md owns *what* to write and *how to ship it*; the `netclaw-docs` skill owns *how it reads*. Use both. diff --git a/CLAUDE.md b/CLAUDE.md index 51815c4..867ade9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -26,9 +26,9 @@ Read the target markdown stub in `src/content/docs/` to get the title and descri Before asking the user anything, collect available information: -1. **Netclaw source repo** — Clone `https://github.com/netclaw-dev/netclaw` (or locate a local clone). Key paths relative to that repo root: +1. **Netclaw source repo** — The local clone is at `~/repositories/netclaw-dev/netclaw` (or clone `https://github.com/netclaw-dev/netclaw`). Check out the release tag the docs target (`src/version.json`). Key paths relative to that repo root: - `src/Netclaw.Cli/` — CLI commands, flags, help text - - `src/Netclaw.Configuration/` — Config schemas, defaults, validation + - `src/Netclaw.Configuration/` — Config schemas, defaults, validation. The JSON schema is `src/Netclaw.Configuration/Schemas/netclaw-config.v1.schema.json` (webhook routes: `webhook-route.v1.schema.json` in the same folder) - `src/Netclaw.Daemon/` — Daemon architecture, startup, health checks - `src/Netclaw.Channels.Slack/` and `Netclaw.Channels.Discord/` — Channel integrations - `src/Netclaw.Security/` — Security model, ACLs, approval gates @@ -37,6 +37,7 @@ Before asking the user anything, collect available information: - `docs/adr/` — Architecture decision records (tool schemas, context discovery, progressive tool disclosure) - `docs/runbooks/` — Operational runbooks (background jobs, behavior debugging, daemon upgrades, memory health, subagents, tool approval gates) - `docs/spec/configuration.md` — Configuration specification + - **Warning:** `docs/spec/SPEC-004-cli-contract.md` and `docs/spec/configuration.md` are stale in places. They list commands and keys that were never implemented. When they disagree with the C# source, the JSON schema, or real `--help` output from the release container, the source, schema and `--help` win. - `CLAUDE.md`, `PROJECT_CONTEXT.md`, `CONTRIBUTING.md` — Project context and conventions 2. **Screenshots** — Check `screenshots/output/` for assets matching the page topic. Map them to the page. Screenshots are served at `/screenshots/output/`. 3. **Landing page** — `src/pages/index.astro` contains feature descriptions, architecture overview, and product positioning that inform doc content. @@ -144,9 +145,13 @@ netclaw -## Related Commands +## Related pages -- [`netclaw `](/cli//) — +- [`netclaw `](/cli//) - + +## Resources + +- []() - ``` #### Tutorial Template @@ -180,13 +185,21 @@ description: "" ... -## Verify It Works +## Verify it works -## Next Steps +## What's next + +- []() - + +## Related pages -- []() — +- []() - + +## Resources + +- []() - ``` #### Guide Template @@ -199,7 +212,7 @@ description: "" -## Before You Begin +## Before you begin - @@ -211,7 +224,15 @@ description: "" ### - +**Symptom:** ... **Cause:** ... **Fix:** ... + +## Related pages + +- []() - + +## Resources + +- []() - ``` #### Concept Template @@ -231,6 +252,14 @@ description: "" ## Limitations - + +## Related pages + +- []() - + +## Resources + +- []() - ``` ### Step 5: Screenshot Integration @@ -239,19 +268,20 @@ When including screenshots: - Use `![alt text](/screenshots/output/.png)` for static screenshots - Prefer `.png` over `.gif` unless the animation is essential to understanding -- Write a caption sentence below each screenshot explaining what it shows +- Put the instruction before the screenshot and a one-sentence caption after it that says something the alt text doesn't +- **Captures come from the release container** (`ghcr.io/netclaw-dev/netclaw:`), never a host-installed binary. A host build can be an unreleased prototype. No private hostnames in samples or screenshots. - If a screenshot shows a multi-step flow (like `init`), break it into individual step screenshots: `init-01-provider-list.png`, `init-02-security-posture.png`, etc. -Available screenshot sets: -- `init-*` — Full `netclaw init` wizard flow (10+ steps) -- `status.*` — System status display -- `doctor.*` — Health check diagnostics -- `stats.*` — Usage statistics -- `provider.*` / `provider-manager.png` — Provider management -- `model.*` / `model-manager.png` — Model management -- `mcp-tools.*` / `mcp-tools-*.png` — MCP tool server management -- `webhooks.*` / `webhooks-list.png` — Webhook configuration -- `reminder.*` / `reminder-list.png` — Reminder management +Available screenshot sets (in `screenshots/output/`): +- `init-*` — `netclaw init` wizard steps (provider, identity, security posture, health check, start-over) +- `config-*` — `netclaw config` dashboard and areas (channels, search, exposure, browser, telemetry, webhooks, skills, workspaces) +- `status.*`, `doctor.*`, `stats.*` — system status, health checks, usage statistics +- `provider.*` / `provider-manager.png`, `model.*` / `model-manager.png` — provider and model management +- `mcp-tools.*` / `mcp-tools-*.png` — MCP server management +- `approvals.*` / `approvals-*.png` — tool approvals list and TUI +- `webhooks.*` / `webhooks-list.png`, `reminder.*` / `reminder-list.png` +- `chat-*`, `single-shot-*` — interactive chat and single-shot CLI runs +- `slack-*`, `discord-*`, `mattermost-*` — third-party channel setup and conversations (kept as is) ### Step 6: Critique (3 parallel agents) @@ -280,7 +310,7 @@ After writing the draft, spawn three review agents in parallel. Pass each the fu ### Step 7: Humanizer Pass -Run `/humanizer` on the drafted page. This is mandatory — no page ships without it. The humanizer catches AI writing patterns that slip through manual review: over-explaining, passive voice, hedge words, repetitive structure, and generic filler that makes documentation feel soulless. +Run `humanizer:humanizer` (the newer, rewrite-oriented plugin skill, not the older `humanizer`) on the drafted page. It runs on prose paragraphs only, never on tables, code blocks or frontmatter. Where it disagrees with the `netclaw-docs` skill, the skill wins (for example, ignore suggestions to add first person or tangents on reference pages). This is mandatory — no page ships without it. The humanizer catches AI writing patterns that slip through manual review: over-explaining, passive voice, hedge words, repetitive structure, and generic filler that makes documentation feel soulless. ### Step 8: Revise @@ -304,6 +334,8 @@ Before declaring the page done: - [ ] At least 2 external links to relevant resources - [ ] No robotic phrasing or unnecessary hedging - [ ] `npm run build` passes with no errors +- [ ] Screenshots and command output come from the release container, not a host binary +- [ ] No private hostnames in samples or screenshots - [ ] **Every runnable example actually runs.** Any `docker run`, `docker compose`, or shell snippet a reader is told to execute MUST be run locally against the current release first and verified crash-free — the container reaches `healthy` and `docker logs` shows no fatal error / `Aborted (core dumped)`. Never ship a command you haven't executed. (This is how the stale `Daemon.Host=0.0.0.0` Docker example shipped a guaranteed startup crash.) ## Autonomous Doc Writing Mode @@ -316,13 +348,13 @@ When invoked with a page slug (e.g., `cli/status`), run the full pipeline withou 4. Run `npm run build` to verify 5. Stage and commit the single page with message: `docs: write
/` -The netclaw source repo is expected at `~/repositories/stannardlabs/netclaw/`. If not found, check common locations or skip source-dependent content. +The netclaw source repo is expected at `~/repositories/netclaw-dev/netclaw`. If not found, check common locations or skip source-dependent content. ## Build & Preview ```bash npm run dev -- --host 0.0.0.0 # Dev server with Tailscale access -npm run build # Production build (43 pages) +npm run build # Production build (165 pages built) ``` ## Project Structure diff --git a/astro.config.mjs b/astro.config.mjs index 1e01f94..4d29df7 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,8 +4,13 @@ import rehypeMermaid from 'rehype-mermaid'; export default defineConfig({ site: 'https://netclaw.dev', + // Mirrored in public/_redirects (301 on Cloudflare Pages). These also work in `astro preview`. + redirects: { + '/guides/custom-subagents': '/skills/custom-subagents/', + '/guides/connecting-slack': '/channels/slack/', + }, markdown: { - rehypePlugins: [[rehypeMermaid, { strategy: 'inline-svg', dark: true }]], + rehypePlugins: [[rehypeMermaid, { strategy: 'inline-svg', dark: true, mermaidConfig: { htmlLabels: false, flowchart: { htmlLabels: false, wrappingWidth: 280 } } }]], }, vite: { server: { @@ -41,18 +46,24 @@ export default defineConfig({ items: [ { label: 'Installation', slug: 'getting-started/installation' }, { label: 'Quickstart', slug: 'getting-started/quickstart' }, - { label: 'Your First Conversation', slug: 'getting-started/first-conversation' }, + { label: 'Your first conversation', slug: 'getting-started/first-conversation' }, + { label: 'Build your first assistant', slug: 'getting-started/first-assistant' }, ], }, { - label: 'Architecture & Philosophy', + label: 'Guides', items: [ - { label: 'Design Philosophy', slug: 'architecture/design-philosophy' }, - { label: 'Overview', slug: 'architecture/overview' }, - { label: 'Sessions & Input Model', slug: 'architecture/sessions' }, - { label: 'Context Structure', slug: 'architecture/context-structure' }, - { label: 'Memory Model', slug: 'architecture/memory-model' }, - { label: 'Security Model', slug: 'architecture/security-model' }, + { label: 'Reduce or turn off approval prompts', slug: 'guides/reduce-approval-prompts' }, + { label: 'Run unattended', slug: 'guides/unattended-runs' }, + { label: 'Connect an OAuth MCP server', slug: 'guides/oauth-mcp-servers' }, + { label: 'Run netclaw with Mattermost on Docker Compose', slug: 'guides/mattermost-docker-compose' }, + { label: 'Upgrade netclaw', slug: 'guides/upgrading' }, + { label: 'Back up, restore and move netclaw', slug: 'guides/backup-and-restore' }, + { label: 'Use the CLI from another machine', slug: 'guides/pairing-remote-devices' }, + { label: 'MCP tool permissions', slug: 'guides/mcp-tool-permissions' }, + { label: 'Scripting headless sessions', slug: 'guides/scripting-sessions' }, + { label: 'Images, audio and other file types', slug: 'guides/multimodal-files' }, + { label: 'Migrating model configuration', slug: 'guides/migrating-model-config' }, ], }, { @@ -65,42 +76,56 @@ export default defineConfig({ ], }, { - label: 'Skills', + label: 'Skills & Agents', items: [ { label: 'Overview', slug: 'skills/overview' }, - { label: 'Native Manifest & Sync', slug: 'skills/native-manifest' }, - { label: 'Bundling Resources with Skills', slug: 'skills/bundled-resources' }, - { label: 'Skill Server', slug: 'skills/skill-server' }, + { label: 'Native manifest & sync', slug: 'skills/native-manifest' }, + { label: 'Bundling resources with skills', slug: 'skills/bundled-resources' }, + { label: 'Custom subagents', slug: 'skills/custom-subagents' }, + { label: 'Skill feeds', slug: 'skills/skill-feeds' }, + { label: 'External skills', slug: 'skills/external-skills' }, + { label: 'Skill server', slug: 'skills/skill-server' }, { label: 'skillserver CLI', slug: 'skills/skillserver-cli' }, - { label: 'External Skills', slug: 'skills/external-skills' }, - { label: 'Skill Feeds', slug: 'skills/skill-feeds' }, + { label: 'Distributing subagents', slug: 'skills/distributing-subagents' }, + ], + }, + { + label: 'Configuration', + items: [ + { label: 'Managed providers', slug: 'configuration/managed-providers' }, + { label: 'Self-hosted providers', slug: 'configuration/self-hosted-providers' }, + { label: 'Models', slug: 'configuration/models' }, + { label: 'Memory and embeddings', slug: 'configuration/memory' }, + { label: 'Search providers', slug: 'configuration/search-providers' }, + { label: 'MCP servers', slug: 'configuration/mcp-servers' }, + { label: 'Webhooks', slug: 'configuration/webhooks' }, + { label: 'Reminders', slug: 'configuration/reminders' }, + { label: 'Data retention', slug: 'configuration/data-retention' }, + { label: 'Configuration reference', slug: 'configuration/reference' }, ], }, { label: 'Security', items: [ - { label: 'Security Model', slug: 'security/security-model' }, + { label: 'Security model', slug: 'security/security-model' }, + { label: 'How tool approvals work', slug: 'security/approvals' }, { label: 'Hardening', slug: 'security/hardening' }, - { label: 'Secrets Management', slug: 'security/secrets' }, + { label: 'Secrets management', slug: 'security/secrets' }, ], }, { - label: 'Configuration', + label: 'Deployment', items: [ - { label: 'Managed Providers', slug: 'configuration/managed-providers' }, - { label: 'Self-Hosted Providers', slug: 'configuration/self-hosted-providers' }, - { label: 'Models', slug: 'configuration/models' }, - { label: 'Search Providers', slug: 'configuration/search-providers' }, - { label: 'MCP Servers', slug: 'configuration/mcp-servers' }, - { label: 'Webhooks', slug: 'configuration/webhooks' }, - { label: 'Reminders', slug: 'configuration/reminders' }, + { label: 'Docker', slug: 'deployment/docker' }, + { label: 'systemd', slug: 'deployment/systemd' }, + { label: 'Exposure modes', slug: 'deployment/exposure-modes' }, ], }, { label: 'Notifications & Observability', items: [ - { label: 'Operational Alerts', slug: 'observability/operational-alerts' }, - { label: 'Health Checks', slug: 'observability/health-checks' }, + { label: 'Operational alerts', slug: 'observability/operational-alerts' }, + { label: 'Health checks', slug: 'observability/health-checks' }, { label: 'OpenTelemetry', slug: 'observability/opentelemetry' }, ], }, @@ -115,9 +140,12 @@ export default defineConfig({ { label: 'status', slug: 'cli/status' }, { label: 'doctor', slug: 'cli/doctor' }, { label: 'stats', slug: 'cli/stats' }, + { label: 'daemon', slug: 'cli/daemon' }, + { label: 'update', slug: 'cli/update' }, { label: 'provider', slug: 'cli/provider' }, { label: 'model', slug: 'cli/model' }, - { label: 'mcp-tools', slug: 'cli/mcp-tools' }, + { label: 'memory', slug: 'cli/memory' }, + { label: 'mcp', slug: 'cli/mcp-tools' }, { label: 'webhooks', slug: 'cli/webhooks' }, { label: 'reminder', slug: 'cli/reminder' }, { label: 'skill', slug: 'cli/skill' }, @@ -126,27 +154,30 @@ export default defineConfig({ ], }, { - label: 'Deployment', + label: 'Reference', items: [ - { label: 'Docker', slug: 'deployment/docker' }, - { label: 'systemd', slug: 'deployment/systemd' }, - { label: 'Exposure Modes', slug: 'deployment/exposure-modes' }, + { label: 'Slash commands', slug: 'reference/slash-commands' }, + { label: 'Built-in tools', slug: 'reference/tools' }, + { label: 'Data directory layout', slug: 'reference/data-directory' }, + { label: 'Glossary', slug: 'reference/glossary' }, ], }, { - label: 'Guides', + label: 'Architecture & Philosophy', items: [ - { label: 'MCP Tool Permissions', slug: 'guides/mcp-tool-permissions' }, - { label: 'Migrating Model Configuration', slug: 'guides/migrating-model-config' }, - { label: 'Custom Subagents', slug: 'guides/custom-subagents' }, - { label: 'Images, Audio, and Other File Types', slug: 'guides/multimodal-files' }, - { label: 'Pairing Remote Devices', slug: 'guides/pairing-remote-devices' }, - { label: 'Scripting Headless Sessions', slug: 'guides/scripting-sessions' }, + { label: 'Design philosophy', slug: 'architecture/design-philosophy' }, + { label: 'Overview', slug: 'architecture/overview' }, + { label: 'Sessions & input model', slug: 'architecture/sessions' }, + { label: 'Context structure', slug: 'architecture/context-structure' }, + { label: 'Memory model', slug: 'architecture/memory-model' }, + { label: 'Security model', slug: 'architecture/security-model' }, ], }, { label: 'Troubleshooting', items: [ + { label: 'Troubleshooting by symptom', slug: 'troubleshooting/common-problems' }, + { label: 'FAQ', slug: 'troubleshooting/faq' }, { label: 'llama.cpp', slug: 'troubleshooting/llama-cpp' }, { label: 'vLLM', slug: 'troubleshooting/vllm' }, ], @@ -158,6 +189,7 @@ export default defineConfig({ { label: 'vs. Hermes Agent', slug: 'comparisons/hermes-agent' }, ], }, + { label: 'Changelog', link: '/changelog' }, ], }), ], diff --git a/docker-compose.screenshots.yml b/docker-compose.screenshots.yml index e97cd0b..716532e 100644 --- a/docker-compose.screenshots.yml +++ b/docker-compose.screenshots.yml @@ -1,74 +1,66 @@ -services: - ollama: - image: ollama/ollama:latest - restart: unless-stopped - ports: - - "11434:11434" - volumes: - - screenshot-ollama-data:/root/.ollama +# Base of the docs capture / verification stack. Do not run this file directly: +# use screenshots/stack.sh or screenshots/capture.sh, which assemble it with the +# overlays under screenshots/compose/ and set the variables below. +# +# base netns + netclawd (no model config: first-run state) + capture +# screenshots/compose/configured.yml adds Models.Definitions/Roles, the demo MCP server +# screenshots/compose/ollama.yml adds the hermetic Ollama inference backend +# +# Every service shares ONE network namespace (the `netns` container) so the daemon, +# the CLI inside the capture container and any helper all reach each other on +# 127.0.0.1, exactly like a real user's machine. Nothing is published to the host. +# No container_name and no host ports: run as many projects side by side as you like +# (COMPOSE_PROJECT_NAME=dr04 ...). - ollama-init: +x-netclaw-image: &netclaw-image ${NETCLAW_IMAGE:-ghcr.io/netclaw-dev/netclaw:${NETCLAW_VERSION:?set by screenshots/stack.sh}} + +services: + netns: + # Holds the shared network namespace and nothing else. image: curlimages/curl:8.13.0 - entrypoint: ["/bin/sh", "/usr/local/bin/ollama-init.sh"] + entrypoint: ["sleep", "infinity"] + stop_grace_period: 1s + + netclawd: + image: *netclaw-image + network_mode: "service:netns" environment: - OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen2:0.5b} - depends_on: - - ollama + NETCLAW_Daemon__Port: "5199" + NETCLAW_Daemon__DisableSelfUpdate: "true" volumes: - - ./screenshots/scripts/ollama-init.sh:/usr/local/bin/ollama-init.sh:ro - - mcp-demo: - build: ./screenshots/mcp-server - expose: - - "8080" + # The daemon runs as the non-root `netclaw` user; its home is /home/netclaw/.netclaw. + - netclaw-home:/home/netclaw/.netclaw healthcheck: - test: ["CMD", "python", "-c", "import socket; s=socket.create_connection(('127.0.0.1',8080),2); s.close()"] - interval: 5s + test: ["CMD", "curl", "-sf", "http://127.0.0.1:5199/api/health/ready"] + interval: 3s timeout: 5s - retries: 10 + retries: 40 start_period: 5s + stop_grace_period: 10s - netclawd: - # The published image is `netclaw` (not `netclawd`). Pinned to a release tag - # via NETCLAW_VERSION so screenshots track a known build; capture.sh sources - # NETCLAW_VERSION from src/version.json. Override NETCLAW_IMAGE to use :dev. - image: ${NETCLAW_IMAGE:-ghcr.io/netclaw-dev/netclaw:${NETCLAW_VERSION:-0.22.1}} - ports: - - "${NETCLAW_PORT:-5299}:5199" + # VHS + the netclaw CLI from the same release image as the daemon. Only started by + # `docker compose run` (profile `capture`); see screenshots/capture/Dockerfile. + capture: + profiles: ["capture"] + image: netclaw-docs-capture:${NETCLAW_IMAGE_TAG:?set by screenshots/stack.sh} + build: + context: ./screenshots/capture + args: + NETCLAW_IMAGE: *netclaw-image + network_mode: "service:netns" depends_on: - ollama-init: - condition: service_completed_successfully - mcp-demo: + netclawd: condition: service_healthy environment: - # NOTE (Phase 5): 0.22.1's `local` exposure mode binds loopback and aborts on a - # non-loopback Daemon.Host. The old host-reachability model here (Host=0.0.0.0 + - # published port + host-side `curl`/`netclaw` in capture.sh) crashes under 0.22.1. - # The Phase 5 re-shoot must drive the CLI via `docker exec` (loopback-local) or move - # to reverse-proxy mode + Daemon.TrustedProxies, and re-validate end to end. + HOST_UID: ${HOST_UID:-1000} + HOST_GID: ${HOST_GID:-1000} NETCLAW_Daemon__Port: "5199" - NETCLAW_Daemon__DisableSelfUpdate: "true" - NETCLAW_Providers__local-ollama__Type: ollama - NETCLAW_Providers__local-ollama__Endpoint: http://ollama:11434 - NETCLAW_Models__Main__Provider: local-ollama - NETCLAW_Models__Main__ModelId: ${OLLAMA_MODEL:-qwen2:0.5b} - NETCLAW_Models__Fallback__Provider: local-ollama - NETCLAW_Models__Fallback__ModelId: ${OLLAMA_MODEL:-qwen2:0.5b} - NETCLAW_Models__Compaction__Provider: local-ollama - NETCLAW_Models__Compaction__ModelId: ${OLLAMA_MODEL:-qwen2:0.5b} - NETCLAW_McpServers__demo-utilities__Transport: http - NETCLAW_McpServers__demo-utilities__Url: http://mcp-demo:8080/mcp - NETCLAW_McpServers__demo-utilities__Enabled: "true" + NETCLAW_CONTAINER_SUPERVISOR: entrypoint volumes: - # 0.22.1 runs as the non-root `netclaw` user; its home is /home/netclaw, not /root. - - screenshot-netclaw-home:/home/netclaw/.netclaw - healthcheck: - test: ["CMD", "curl", "-sf", "http://127.0.0.1:5199/api/health/ready"] - interval: 5s - timeout: 5s - retries: 30 - start_period: 10s + - netclaw-home:/home/netclaw/.netclaw + - ./screenshots/tapes:/tapes:ro + - ./screenshots/scripts:/work/screenshots/scripts:ro + - ${CAPTURE_OUT_DIR:-./screenshots/output}:/out volumes: - screenshot-ollama-data: - screenshot-netclaw-home: + netclaw-home: diff --git a/examples/mattermost-compose/.env.example b/examples/mattermost-compose/.env.example new file mode 100644 index 0000000..84bbcd5 --- /dev/null +++ b/examples/mattermost-compose/.env.example @@ -0,0 +1,14 @@ +# Copy to .env and fill in. Compose reads .env automatically. + +# Postgres password for the mattermost database user. +POSTGRES_PASSWORD=change-me-to-a-long-random-string + +# Where you open Mattermost in a browser. Use your real https URL later. +MATTERMOST_SITE_URL=http://localhost:8065 + +# Inference: any OpenAI-compatible endpoint (llama.cpp, vLLM, ...). The address works with or without a /v1 suffix. +# configure-netclaw.sh reads these. The compose file doesn't use them. +INFERENCE_BASE_URL=http://llm.example.internal:8080 +INFERENCE_MODEL_ID=your-model-id + +# provision.sh appends MM_CHANNEL_ID and MM_USER_ID here. Don't set them by hand. diff --git a/examples/mattermost-compose/.gitignore b/examples/mattermost-compose/.gitignore new file mode 100644 index 0000000..2cf01c2 --- /dev/null +++ b/examples/mattermost-compose/.gitignore @@ -0,0 +1,2 @@ +.env +.bot-token diff --git a/examples/mattermost-compose/README.md b/examples/mattermost-compose/README.md new file mode 100644 index 0000000..319442c --- /dev/null +++ b/examples/mattermost-compose/README.md @@ -0,0 +1,26 @@ +# netclaw + Mattermost on Docker Compose + +A private assistant stack: netclaw 0.28.2, Mattermost Team Edition 11.11.1 and +Postgres 17.11 on one Compose network. The full walkthrough, with expected +output at every step, is +[Run netclaw with Mattermost on Docker Compose](https://netclaw.dev/guides/mattermost-docker-compose/). + +| File | What it does | +|------|--------------| +| `compose.yaml` | The three services. netclaw publishes no ports and runs read-only with all capabilities dropped. | +| `.env.example` | Copy to `.env`. Passwords and the inference endpoint. | +| `provision.sh` | Creates the admin, a team, a private channel, one member and one bot with a token. Needs bash and curl. | +| `configure-netclaw.sh` | Adds the inference provider, sets the channel audience, stores the bot token in netclaw's vault and restarts. | + +```bash +cp .env.example .env && chmod 600 .env # edit POSTGRES_PASSWORD, INFERENCE_BASE_URL, INFERENCE_MODEL_ID +docker compose up -d --wait +./provision.sh # prints the generated passwords once +./configure-netclaw.sh +``` + +Then sign in at as the member user and mention the bot +in the `assistant` channel. + +Everything lives in named volumes. `docker compose down` keeps your data. +`docker compose down -v` deletes it. diff --git a/examples/mattermost-compose/compose.yaml b/examples/mattermost-compose/compose.yaml new file mode 100644 index 0000000..c9a3e90 --- /dev/null +++ b/examples/mattermost-compose/compose.yaml @@ -0,0 +1,99 @@ +# netclaw + Mattermost (Team Edition) + Postgres on one private network. +# Only Mattermost's web UI is published, and only on the host's loopback. + +services: + postgres: + image: postgres:17.11-alpine + restart: unless-stopped + environment: + POSTGRES_DB: mattermost + POSTGRES_USER: mattermost + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?Set POSTGRES_PASSWORD in .env} + volumes: + - postgres-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U mattermost -d mattermost"] + interval: 5s + timeout: 5s + retries: 12 + networks: [chat] + + mattermost: + image: mattermost/mattermost-team-edition:11.11.1 + restart: unless-stopped + depends_on: + postgres: + condition: service_healthy + environment: + MM_SQLSETTINGS_DRIVERNAME: postgres + MM_SQLSETTINGS_DATASOURCE: postgres://mattermost:${POSTGRES_PASSWORD}@postgres:5432/mattermost?sslmode=disable&connect_timeout=10 + MM_SERVICESETTINGS_SITEURL: ${MATTERMOST_SITE_URL:-http://localhost:8065} + # Bots and personal access tokens are off by default. netclaw needs both. + MM_SERVICESETTINGS_ENABLEBOTACCOUNTCREATION: "true" + MM_SERVICESETTINGS_ENABLEUSERACCESSTOKENS: "true" + # Nobody can self-register. Accounts come from provision.sh or an admin. + MM_TEAMSETTINGS_ENABLEOPENSERVER: "false" + # Local mode: lets the healthcheck use mmctl over a container-internal socket. + MM_SERVICESETTINGS_ENABLELOCALMODE: "true" + ports: + - "127.0.0.1:8065:8065" # host loopback only + volumes: + - mattermost-config:/mattermost/config + - mattermost-data:/mattermost/data + - mattermost-plugins:/mattermost/plugins + security_opt: + - no-new-privileges:true + healthcheck: + # The image has no shell or curl, so use mmctl. + test: ["CMD", "/mattermost/bin/mmctl", "--local", "system", "status"] + interval: 10s + timeout: 5s + retries: 18 + start_period: 30s + networks: [chat] + + netclaw: + image: ghcr.io/netclaw-dev/netclaw:0.27.1 + restart: unless-stopped + depends_on: + mattermost: + condition: service_healthy + # No ports: the daemon stays in the default `local` exposure mode (loopback). + # You drive it with `docker compose exec`, and Mattermost never calls it. + environment: + # The daemon is a single-file .NET binary that unpacks itself on start. + # The root filesystem is read-only, so unpack into a small exec tmpfs. + DOTNET_BUNDLE_EXTRACT_BASE_DIR: /var/netclaw-bundle + # Mattermost. The bot token is NOT here. It goes in netclaw's vault. + NETCLAW_Mattermost__Enabled: "true" + NETCLAW_Mattermost__ServerUrl: http://mattermost:8065 + NETCLAW_Mattermost__MentionOnly: "true" + NETCLAW_Mattermost__AllowDirectMessages: "true" + # Allow-lists live here and only here. Env entries merge with netclaw.json + # lists by position, so never set the same list in both places. + # Blank until provision.sh writes the IDs to .env. + NETCLAW_Mattermost__AllowedChannelIds__0: ${MM_CHANNEL_ID:-} + NETCLAW_Mattermost__AllowedUserIds__0: ${MM_USER_ID:-} + volumes: + - netclaw-home:/home/netclaw/.netclaw + # Hardening + user: "1654:1654" # start as the netclaw user, so the entrypoint never needs root + read_only: true # root filesystem is immutable + tmpfs: + - /tmp:rw,noexec,nosuid,size=64m # scratch space only + - /var/netclaw-bundle:rw,exec,nosuid,size=128m,uid=1654,gid=1654 # the unpacked daemon (43 MB), rebuilt on every start + cap_drop: [ALL] # no Linux capabilities + security_opt: + - no-new-privileges:true # no setuid escalation + pids_limit: 256 # fork-bomb ceiling + networks: [chat] + +volumes: + postgres-data: + mattermost-config: + mattermost-data: + mattermost-plugins: + netclaw-home: + +networks: + chat: diff --git a/examples/mattermost-compose/configure-netclaw.sh b/examples/mattermost-compose/configure-netclaw.sh new file mode 100755 index 0000000..b2b28b2 --- /dev/null +++ b/examples/mattermost-compose/configure-netclaw.sh @@ -0,0 +1,40 @@ +#!/usr/bin/env bash +# Finish wiring netclaw after provision.sh: apply the IDs from .env, add the +# inference provider, set the channel audience, and store the bot token in +# netclaw's encrypted vault. Run it once. +set -euo pipefail +cd "$(dirname "$0")" + +[ -f .bot-token ] || { echo "Run ./provision.sh first." >&2; exit 1; } +set -a; . ./.env; set +a +: "${MM_CHANNEL_ID:?missing from .env}" "${INFERENCE_BASE_URL:?missing from .env}" "${INFERENCE_MODEL_ID:?missing from .env}" + +nc() { docker compose exec -T netclaw netclaw "$@"; } + +# Recreate netclaw so it picks up MM_CHANNEL_ID and MM_USER_ID from .env. +docker compose up -d --wait netclaw + +nc provider add main openai-compatible --endpoint "$INFERENCE_BASE_URL" +nc model set main main "$INFERENCE_MODEL_ID" + +# Personal posture (what `netclaw init` writes for one operator) and a personal +# audience for the private channel, so the agent can ask to run shell commands. +# Each command still needs your approval. Editing netclaw.json makes the daemon +# restart itself. +docker compose exec -T -e CH="$MM_CHANNEL_ID" netclaw sh -c ' + f=$HOME/.netclaw/config/netclaw.json + jq --arg c "$CH" " + .Security.DeploymentPosture = \"Personal\" + | .Security.ShellExecutionMode = \"HostAllowed\" + | .Security.StrictDefaults = true + | .Tools.ShellMode = \"HostAllowed\" + | .Mattermost.ChannelAudiences[\$c] = \"personal\"" "$f" > /tmp/netclaw.json + cat /tmp/netclaw.json > "$f"; rm -f /tmp/netclaw.json' + +# Token goes in over stdin, so it never appears in a process list or in .env. +docker compose exec -T netclaw sh -c 'netclaw secrets set Mattermost.BotToken "$(cat)"' < .bot-token + +# Secrets changes need a restart. +docker compose restart netclaw +docker compose up -d --wait netclaw +nc status diff --git a/examples/mattermost-compose/provision.sh b/examples/mattermost-compose/provision.sh new file mode 100755 index 0000000..fffb176 --- /dev/null +++ b/examples/mattermost-compose/provision.sh @@ -0,0 +1,105 @@ +#!/usr/bin/env bash +# Provision a fresh Mattermost: admin, team, private channel, one human user, +# one bot account with an access token. Needs only bash and curl. +# +# ./provision.sh # Mattermost at http://localhost:8065 +# MM_URL=https://chat.example.com ./provision.sh +# +# Writes MM_CHANNEL_ID and MM_USER_ID to .env (compose reads them), the bot +# token to .bot-token (mode 600), and prints the generated passwords once. +# Passwords, the admin session token and request bodies reach curl over stdin +# or a private temp file, never on its command line, so `ps` can't show them. +set -euo pipefail +umask 077 # every file this script creates is readable by you only +cd "$(dirname "$0")" + +MM_URL="${MM_URL:-http://localhost:8065}" +TEAM_NAME="${TEAM_NAME:-home}" +CHANNEL_NAME="${CHANNEL_NAME:-assistant}" +BOT_NAME="${BOT_NAME:-netclaw}" +ADMIN_USER="${ADMIN_USER:-admin}" +ADMIN_EMAIL="${ADMIN_EMAIL:-admin@example.com}" +HUMAN_USER="${HUMAN_USER:-alice}" +HUMAN_EMAIL="${HUMAN_EMAIL:-alice@example.com}" + +# The values are pasted into JSON below, so keep them to plain characters. +for v in TEAM_NAME CHANNEL_NAME BOT_NAME ADMIN_USER HUMAN_USER; do + [[ "${!v}" =~ ^[a-z0-9_-]+$ ]] || { echo "$v must be lowercase letters, digits, - or _." >&2; exit 1; } +done +for v in ADMIN_EMAIL HUMAN_EMAIL; do + [[ "${!v}" =~ ^[A-Za-z0-9._+-]+@[A-Za-z0-9.-]+$ ]] || { echo "$v doesn't look like an email address." >&2; exit 1; } +done + +[ -e .bot-token ] && { echo "Refusing to run twice: .bot-token already exists." >&2; exit 1; } + +rand() { od -An -N12 -tx1 /dev/urandom | tr -d ' \n'; } +# First string value for a key in compact JSON. Mattermost returns compact JSON. +field() { sed -n "s/.*\"$1\":\"\([^\"]*\)\".*/\1/p" | head -n1; } +# Stop if an ID came back empty, so a changed response shape fails loudly. +need() { [ -n "$2" ] || { echo "Mattermost returned no $1. Run 'docker compose down -v' and start over." >&2; exit 1; }; } + +ADMIN_PASS="$(rand)" +HUMAN_PASS="$(rand)" + +# The admin token lives in this private file and curl reads it with -H @file. +HDR="$(mktemp)" +trap 'rm -f "$HDR"' EXIT + +api() { # api METHOD PATH [JSON] (sends the admin token once login has run) + local args=(-sS --fail-with-body -X "$1" "$MM_URL/api/v4$2" -H 'Content-Type: application/json') + [ ! -s "$HDR" ] || args+=(-H "@$HDR") + if [ -n "${3:-}" ]; then + printf '%s' "$3" | curl "${args[@]}" --data-binary @- + else + curl "${args[@]}" + fi +} + +echo "Waiting for Mattermost at $MM_URL ..." +until curl -sf "$MM_URL/api/v4/system/ping" >/dev/null; do sleep 2; done + +# The first account on a fresh server becomes the system admin. +api POST /users "{\"email\":\"$ADMIN_EMAIL\",\"username\":\"$ADMIN_USER\",\"password\":\"$ADMIN_PASS\"}" >/dev/null + +# Log in. The session token comes back in the Token response header. +TOKEN="$(printf '{"login_id":"%s","password":"%s"}' "$ADMIN_USER" "$ADMIN_PASS" \ + | curl -sS -D - -o /dev/null -X POST "$MM_URL/api/v4/users/login" \ + -H 'Content-Type: application/json' --data-binary @- \ + | tr -d '\r' | sed -n 's/^[Tt]oken: //p')" +[ -n "$TOKEN" ] || { echo "Login failed." >&2; exit 1; } +printf 'Authorization: Bearer %s\n' "$TOKEN" > "$HDR" + +TEAM_ID="$(api POST /teams "{\"name\":\"$TEAM_NAME\",\"display_name\":\"Home\",\"type\":\"I\"}" | field id)" +need team "$TEAM_ID" +CHANNEL_ID="$(api POST /channels "{\"team_id\":\"$TEAM_ID\",\"name\":\"$CHANNEL_NAME\",\"display_name\":\"Assistant\",\"type\":\"P\"}" | field id)" +need channel "$CHANNEL_ID" + +HUMAN_ID="$(api POST /users "{\"email\":\"$HUMAN_EMAIL\",\"username\":\"$HUMAN_USER\",\"password\":\"$HUMAN_PASS\"}" | field id)" +need user "$HUMAN_ID" +api POST "/teams/$TEAM_ID/members" "{\"team_id\":\"$TEAM_ID\",\"user_id\":\"$HUMAN_ID\"}" >/dev/null +api POST "/channels/$CHANNEL_ID/members" "{\"user_id\":\"$HUMAN_ID\"}" >/dev/null + +BOT_ID="$(api POST /bots "{\"username\":\"$BOT_NAME\",\"display_name\":\"netclaw\"}" | field user_id)" +need bot "$BOT_ID" +api POST "/teams/$TEAM_ID/members" "{\"team_id\":\"$TEAM_ID\",\"user_id\":\"$BOT_ID\"}" >/dev/null +api POST "/channels/$CHANNEL_ID/members" "{\"user_id\":\"$BOT_ID\"}" >/dev/null +BOT_TOKEN="$(api POST "/users/$BOT_ID/tokens" '{"description":"netclaw"}' | field token)" +need "bot token" "$BOT_TOKEN" + +printf '%s' "$BOT_TOKEN" > .bot-token +[ ! -s .env ] || [ -z "$(tail -c1 .env)" ] || echo >> .env # ensure a trailing newline +{ + echo "MM_CHANNEL_ID=$CHANNEL_ID" + echo "MM_USER_ID=$HUMAN_ID" +} >> .env + +cat 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 z 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

rm#mUHmHS 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 literal 54256 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%0A*X$57A2x`xUMI%3chSh@ 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 diff --git a/public/assets/mcp-icon.svg b/public/assets/mcp-icon.svg deleted file mode 100644 index 6748ea9..0000000 --- a/public/assets/mcp-icon.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/public/assets/netclaw-horizontal-dark.svg b/public/assets/netclaw-horizontal-dark.svg deleted file mode 100644 index c48c75e..0000000 --- a/public/assets/netclaw-horizontal-dark.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/public/assets/netclaw-stacked-purple.svg b/public/assets/netclaw-stacked-purple.svg deleted file mode 100644 index e189634..0000000 --- a/public/assets/netclaw-stacked-purple.svg +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/screenshots/.env.example b/screenshots/.env.example new file mode 100644 index 0000000..32a2313 --- /dev/null +++ b/screenshots/.env.example @@ -0,0 +1,12 @@ +# Copy to screenshots/.env (gitignored). Never commit a real hostname or model name. +# +# Real inference without leaking the endpoint (relay mode): set the real https:// base URL and +# the model id it serves. The forwarder (compose/forwarder.yml) presents it inside the stack as +# http://llm.example.internal:8081 with the model id demo-model. +# INFERENCE_UPSTREAM=https://inference.internal.example +# INFERENCE_UPSTREAM_MODEL=the-real-model-id +# +# Or point netclaw straight at an endpoint (external mode; it ends up in netclaw.json, so don't +# capture provider lists this way). Without either, the stack uses a tiny local Ollama model. +# INFERENCE_BASE_URL=http://llm.example.internal:8080 +# INFERENCE_MODEL_ID=your-model-id diff --git a/screenshots/.gitignore b/screenshots/.gitignore new file mode 100644 index 0000000..fcdbee0 --- /dev/null +++ b/screenshots/.gitignore @@ -0,0 +1,3 @@ +.env +.netclaw-screenshot-home/ +mattermost/.bootstrap*.json diff --git a/screenshots/README.md b/screenshots/README.md index 1a2379f..b3bbe5c 100644 --- a/screenshots/README.md +++ b/screenshots/README.md @@ -1,56 +1,169 @@ -# Screenshot capture pipeline +# Screenshot and verification stack -Source of truth for every image under `output/` (served at `/screenshots/output/`). +Everything that produces an image under `output/` (served at `/screenshots/output/`) or proves a command works runs in containers against the **released** Netclaw image. Docker is the only prerequisite. + +**Rule: never use a host `netclaw` binary.** Whatever is on your `PATH` may be a prototype or a dev build. The CLI in every recording and every verification command comes from `ghcr.io/netclaw-dev/netclaw:`. ## Layout | Path | What it is | |------|-----------| -| `capture.sh` | Orchestrator: brings up the Docker stack, runs every VHS tape, tears down | -| `tapes/*.tape` | [VHS](https://github.com/charmbracelet/vhs) recordings — one per CLI command/flow | -| `scripts/` | Helpers (`ollama-init.sh` model pull, `seed-approvals.sh` fixture seeding) | -| `mcp-server/` | Demo MCP server used by the `mcp-tools` capture | -| `output/` | Generated PNGs/GIFs (committed; consumed by the docs) | +| `stack.sh` | Bring up, reset, exec into and tear down a disposable daemon (verification, no recording) | +| `capture.sh` | Record VHS tapes inside the capture container against that daemon | +| `shoot-page.sh` | Screenshot a page of the locally built docs site in the Playwright container | +| `mattermost/run.sh` | Mattermost + daemon + Playwright, all in containers; regenerates `mattermost-*.png` | +| `tapes/*.tape` | [VHS](https://github.com/charmbracelet/vhs) recordings, one per CLI command or flow | +| `capture/` | Capture image: VHS (ttyd, ffmpeg, Chromium) plus the released CLI | +| `playwright/` | Playwright image matching the version locked in `package-lock.json` | +| `compose/` | Compose overlays (`configured.yml`, `ollama.yml`, `forwarder.yml`, `projects.yml`, `skillserver.yml`, `docker-host.yml`); the base is `../docker-compose.screenshots.yml` | +| `scripts/`, `mcp-server/` | Ollama model pull, the seed scripts tapes run out of sight (`seed-*.sh`), a Mattermost API fixture, the third-party redaction script, the demo MCP server | +| `lib.sh`, `.env.example` | Shared settings; copy `.env.example` to `.env` (gitignored) for real inference | + +## Pick the release + +The image tag comes from one place: `documentedVersion` in `src/version.json`. Override per run: + +```bash +NETCLAW_VERSION=0.27.1 ./screenshots/stack.sh up # a specific release +NETCLAW_IMAGE=ghcr.io/netclaw-dev/netclaw:dev ./screenshots/stack.sh up # any image reference +``` + +`src/version.json` may lag the release you're documenting, so pass `NETCLAW_VERSION=0.27.1` until it's bumped. Every command prints the image it used. + +## Verify a command against a clean daemon + +```bash +export COMPOSE_PROJECT_NAME=dr04 # your own name, see "Run several at once" +./screenshots/stack.sh up # clean ~/.netclaw, daemon healthy, provider + model + MCP configured +./screenshots/stack.sh exec netclaw doctor # runs as the netclaw user, like the daemon +./screenshots/stack.sh exec netclaw status +./screenshots/stack.sh daemon-log 100 # the daemon's log file (container stdout is empty) +./screenshots/stack.sh down # removes containers, network and volumes +``` + +Daemon states: + +| State | Command | What you get | +|-------|---------|--------------| +| configured (default) | `stack.sh up` | Provider `inference`, model roles and the demo MCP server `demo-utilities`, added with `netclaw provider add`, `netclaw model set` and `netclaw mcp add`, plus the Security section `netclaw init` writes. This is what a user's `netclaw.json` looks like after init. | +| first run | `stack.sh up --fresh` | Healthy daemon, empty `~/.netclaw`, no model. `netclaw status` says "daemon not configured". Use it for `netclaw init`. | +| bare | `stack.sh up --bare` | Like first run, plus the demo MCP server container. Tapes that seed their own config (`scripts/seed-*.sh`) use it. | + +`stack.sh reset [--fresh]` wipes `~/.netclaw` and restarts the daemon without rebuilding the rest of the stack. Run `./screenshots/stack.sh` with no arguments for the full command list. + +The configured state writes `netclaw.json` instead of setting `NETCLAW_*` env vars because the CLI refuses to run most commands without that file, and the `provider`, `model` and `mcp` TUIs edit it. The 0.25+ env shape still works if you need it for a throwaway daemon: + +```bash +docker run -d --name x -e NETCLAW_Providers__inference__Type=ollama \ + -e NETCLAW_Providers__inference__Endpoint=http://ollama:11434 \ + -e NETCLAW_Models__Definitions__main__Provider=inference \ + -e NETCLAW_Models__Definitions__main__ModelId=qwen2:0.5b \ + -e NETCLAW_Models__Roles__Main=main ghcr.io/netclaw-dev/netclaw:0.27.1 +``` + +The Mattermost flow below uses exactly this env shape. + +## Inference + +| Mode | When | How | +|------|------|-----| +| hermetic (default) | Shots that don't need a real answer | Ollama with `qwen2:0.5b` in the stack. The first `up` of a project pulls the model (about 350 MB), so allow a minute or two. | +| relay | You need real answers in a screenshot | Set `INFERENCE_UPSTREAM` and `INFERENCE_UPSTREAM_MODEL` in `screenshots/.env` (see below). The only mode that keeps the real endpoint and model name out of the config and the frame. | +| external | You need a real reply and nothing is captured that prints the endpoint | Set `INFERENCE_BASE_URL` (an OpenAI-compatible endpoint) and `INFERENCE_MODEL_ID` in the environment or in `screenshots/.env` | + +```bash +INFERENCE_BASE_URL=http://llm.example.internal:8080 INFERENCE_MODEL_ID=my-model \ + ./screenshots/stack.sh up +``` + +External mode writes the endpoint into the daemon's `netclaw.json`, so commands like `netclaw provider list` print it. Capture those screenshots in hermetic mode. + +### Real inference without leaking the endpoint + +Set `INFERENCE_UPSTREAM` (the real `https://` base URL) and `INFERENCE_UPSTREAM_MODEL` (the model id it serves) in the gitignored `screenshots/.env`. `lib.sh` then switches to relay mode and adds `compose/forwarder.yml`: an nginx container in the stack's network namespace relays port 8081 to the upstream and rewrites the upstream's model id to `demo-model` in every response, and `llm.example.internal` resolves to loopback for everything in the namespace. netclaw only ever sees `http://llm.example.internal:8081` and the model `demo-model`, so provider lists, `netclaw init` and chat status bars are safe to capture, and no real model file name reaches a status bar. Check the first frame of every chat tape anyway. + +This is the only relay, and both tape families use it. The seed scripts (`seed-assistant.sh`, `seed-providers.sh`) point their `lab-llama` provider at it, and the configured daemon (`stack.sh up`) registers it as provider `inference`. Ollama stays up alongside it, so `netclaw init` and the provider and model tapes still have a reachable local provider. Without `INFERENCE_UPSTREAM`, tapes that need a real answer (`stats`, `single-shot-cli-execution`, the chat and approval tapes) can't produce one: the 0.5B Ollama model can't call tools. + +## Standard tape settings + +Every terminal image is shown in a docs column about 650 px wide, so a capture wider than about 1000 px shrinks its text below what a reader can read without the lightbox. Use this block, and change only `Height` (crop by terminal height, not by editing the PNG): + +``` +Set Shell "bash" +Set Theme "Dracula" +Set FontSize 16 +Set Width 1000 +Set Height 540 +Set Padding 20 +Set TypingSpeed 40ms +``` -The daemon image and the demo Ollama model come from `../docker-compose.screenshots.yml`. +At `FontSize 16` a character cell is about 10 px wide, so 1000 px holds about 93 columns, and scaled into the column the text lands at about 11 px. Widen a tape only as far as a table needs it (`model.tape` uses 1200 for the long OpenAI-compatible provider label, `doctor.tape` 1700 for its longest check line) and make the page caption say the image can be enlarged. -## Regenerate everything (VHS / TUI shots) +The status-like tapes (`status`, `doctor`, `stats`, `sessions`, `model`, `provider`, `skill-list`, `skill-sync`, `webhooks`, `reminder-list`, `config`, `single-shot-cli-execution`) all start from `scripts/seed-demo-install.sh`, so the images agree on providers, roles, skills, webhook routes, reminders and the MCP server. Add new status-like captures to that family instead of seeding a separate install. + +## Record tapes ```bash -./screenshots/capture.sh +./screenshots/capture.sh status # one tape +./screenshots/capture.sh status doctor # a few +./screenshots/capture.sh # all tapes +OUT_DIR=/tmp/shots ./screenshots/capture.sh status # write elsewhere (default: screenshots/output/) ``` -The daemon image is **pinned to the documented release**: `capture.sh` reads -`src/version.json` and pins `ghcr.io/netclaw-dev/netclaw:`. To -capture against a different build, override before running: +`capture.sh` starts a stack, resets `~/.netclaw` before every tape, runs each tape inside the capture container, copies the results to the output directory owned by you, and always tears the stack down. It exits non-zero if VHS fails or a tape doesn't produce every `Output` and `Screenshot` file it declares. + +How the recording reaches the daemon: the capture container shares the daemon container's network namespace and `~/.netclaw` volume, so `netclaw status` typed in a tape talks to `127.0.0.1:5199` exactly like a real user's CLI. Nothing is published to the host, and no `docker exec` shows up in a recording. + +Tape conventions: + +- Paths are relative to `/work`, so `Output screenshots/output/status.gif` and `Screenshot screenshots/output/status.png` land in the output directory. +- VHS starts bash with a clean `> ` prompt. No hidden preamble is needed unless a tape has to stage state first (see `chat-approval.tape`). +- Put `# stack: fresh` in a tape to run it against the first-run daemon (see `init-wizard.tape`), or `# stack: bare` for the same empty home with the demo MCP server up. +- Setup that isn't the subject of the image runs between `Hide` and `Show`: `bash /work/screenshots/scripts/seed-*.sh` for config, or `Source "/tapes/include/init-personal.tape"` to run the real `netclaw init` wizard (Personal, Ollama). Wait for a seed to finish with `Wait+Screen@4m /seeded: .../` instead of a fixed sleep. +- `# image: 0.27.0` runs one tape against another release (only `update-check.tape` does, to be honestly one version behind). `# host-docker: true` gives a tape the host's Docker socket (only `docker-health.tape`, which filters everything it prints to its own container). +- Only `.png` files are published from a run. A tape still needs an `Output` line for VHS, and its GIF stays in the container. +- VHS applies `Set Width`/`Set Height` once, so size each tape for its tallest screen. `Screenshot` is slightly asynchronous: leave a `Sleep` after every one, and don't issue two in a row. +- Shared setup goes in `tapes/include/*.tape` and is pulled in with `Source "/tapes/include/.tape"`. `capture.sh` only runs top-level tapes. Hide setup with `Hide`/`Show`, and wait for a marker the typed command doesn't contain (`echo SE''EDED`, then `Wait+Screen /SEEDED/`). +- Tapes that need a Git workspace run `scripts/seed-projects.sh` first. It creates `/home/netclaw/projects/website` on a volume the daemon shares (`compose/projects.yml`) and declares it as the workspace root, so shell approval prompts offer "This repository". Skill captures use `scripts/seed-skills.sh`, which publishes demo skills to the stack's skill server (`compose/skillserver.yml`, port 8090) with the `skillserver` CLI baked into the capture image. +- Tapes that drive a real model (`chat-*`, `approvals-*`, `reminder-run`) must make each prompt self-limiting ("then stop, run nothing else") and wait on text the prompt didn't type, such as a tool result line. A model that wanders into an extra tool call leaves a prompt open and the wait times out. +- A trailing `Screenshot` is lost by VHS if nothing follows it, so `capture.sh` appends `Sleep 1s` to the copy it runs. You don't have to. +- In 0.27.1, `Escape` doesn't quit a TUI. Use `Ctrl+Q`. +- The init wizard prints "Step N of 5" until you pick a posture, then "of 4" for Personal. `init-wizard.tape` (Personal) and `init-team.tape` (Team, the fifth step) record that as shown. + +## Mattermost and other browser captures ```bash -NETCLAW_VERSION=0.22.1 ./screenshots/capture.sh # a specific release -NETCLAW_IMAGE=ghcr.io/netclaw-dev/netclaw:dev ./screenshots/capture.sh # bleeding edge -OLLAMA_MODEL=qwen2:0.5b ./screenshots/capture.sh # demo inference model +INFERENCE_BASE_URL=http://llm.example.internal:8080 INFERENCE_MODEL_ID=my-model \ + OUT_DIR=/tmp/shots ./screenshots/mattermost/run.sh ``` -Keep `src/version.json` in step with the release you profiled — the -`docs_version_check` workflow opens an issue when the docs fall behind. +Mattermost, the daemon and Playwright share one compose network, and the daemon reaches Mattermost at `http://mattermost:8065`. The flow is: bootstrap Mattermost (`bootstrap.mjs`), start the daemon, store the bot token with `netclaw secrets set Mattermost.BotToken` (no token in env or compose), enable the channel, post an `@netclaw` message as the test user (`post-message.mjs`) and wait for the reply, then capture the System Console, bot account and thread (`capture.mjs`). `run.sh` sets `Mattermost.CallbackUrl` (through a small forwarder sidecar, because the daemon listens on loopback only) and allow-lists the daemon host in Mattermost (`MM_SERVICESETTINGS_ALLOWEDUNTRUSTEDINTERNALCONNECTIONS`), so tool approvals render as buttons. `SCRIPTED_APPROVAL=1 ./screenshots/mattermost/run.sh` needs no model: `scripted-llm.mjs` answers with one `demo-utilities/http_fetch` MCP call (the demo MCP server, gated to `Approval` for the Personal audience; `SCRIPTED_TOOL=shell` requests a `shell_execute` call instead), `approval.mjs` captures the prompt and `frame-approval.mjs` frames it as `approval-prompt.png` (rounded dark card, purple glow, 2x). The normal flow needs external inference (a 0.5B model won't produce a presentable answer). The bootstrap file with the short-lived bot token is deleted on exit. -## Mattermost capture (web setup + live conversation) +The Compose-sample screenshots (`mattermost-compose-*.png`, plus the recaptured `mattermost-bot-accounts.png`, `mattermost-create-bot.png`, `mattermost-conversation.png` and the landing page's `public/assets/approval-prompt.png`) come from `mattermost/capture-compose.mjs`, which signs in to the stack in `examples/mattermost-compose/` after `provision.sh` and `configure-netclaw.sh` have run. It needs the admin and `alice` passwords that `provision.sh` printed, and a Git repository for the approval prompt (so "This repository" appears). Token IDs are overwritten in the page before each shot. The Slack and Discord redactions are `scripts/redact-third-party.py`. -The Mattermost shots aren't a VHS tape — they're browser screenshots driven by -Playwright. Everything lives in `mattermost/`: +Screenshot a page of the docs site you built locally: -| File | Role | -|------|------| -| `docker-compose.mattermost.yml` | the Mattermost (`mattermost-preview`) container | -| `bootstrap.mjs` | seeds admin / team / bot + token / channel / test user via the REST API → `.bootstrap.json` | -| `run.sh` | orchestrator: compose up → bootstrap → start the pinned netclaw image (inference pointed at an OpenAI-compatible endpoint) → post an `@netclaw` message → run `capture.mjs` → tear down | -| `capture.mjs` | Playwright: the System Console bot-accounts setting, the bot + its token, and the live `@netclaw` thread | +```bash +npm run build +./screenshots/shoot-page.sh cli/status # docs-logs/screenshots/cli-status.png +``` + +`shoot-page.sh` runs `scripts/screenshot-page.mjs` in the same Playwright image and serves `dist/` itself. To shoot a dev server instead, start it with `npm run dev -- --host 0.0.0.0` and set `SCREENSHOT_BASE_URL=http://host.docker.internal:4321`. + +## Run several at once -Regenerate them with: +Every container, network and volume is namespaced by `COMPOSE_PROJECT_NAME`. There are no fixed container names and no published ports, so two agents can run side by side: ```bash -./screenshots/mattermost/run.sh +COMPOSE_PROJECT_NAME=dr04 ./screenshots/stack.sh up +COMPOSE_PROJECT_NAME=dr07 ./screenshots/capture.sh status ``` -Point inference at your own endpoint/model with `SPARK_ENDPOINT` and `SPARK_MODEL_ID` -(the defaults match the local netclaw instance). `.bootstrap.json` holds a short-lived -bot token and is gitignored. +Use your own name every time, and `down` when you're done. The Mattermost flow appends `-mm` to the name, so it doesn't collide with a stack of the same name. Only built images are shared across projects (`netclaw-docs-capture:`, `netclaw-docs-playwright:`, `netclaw-docs-mcp-demo:1`), which is safe because they're deterministic. + +## Design notes + +- **Why the capture image starts from VHS.** The release image is `ubuntu:24.04` (glibc) and the CLI is a self-contained .NET single-file binary. The VHS image is Debian trixie (glibc 2.41) with ICU and libsodium already installed, so the CLI and its self-dropping launcher are copied out of the release image unmodified. The recorded CLI is byte-for-byte the released one. Putting VHS into the release image would have meant installing Chromium, ttyd and ffmpeg on an Ubuntu base that ships Chromium only as a snap. +- **One network namespace.** A tiny `netns` container owns the stack's network namespace, and the daemon, capture container, Ollama and demo MCP server join it. Everything talks over `127.0.0.1`, the daemon stays in its default loopback-only `local` exposure mode, and the `netclaw init` wizard's default Ollama endpoint (`http://localhost:11434`) just works. +- **Ownership.** The capture container runs the tape as the `netclaw` user (uid 1654, same as the daemon) so it can use the shared home, then copies the files out with your uid and gid. diff --git a/screenshots/capture.sh b/screenshots/capture.sh index 91f7238..af3f216 100755 --- a/screenshots/capture.sh +++ b/screenshots/capture.sh @@ -1,58 +1,73 @@ #!/bin/bash -# Orchestrates the screenshot capture pipeline. -# Brings up Docker Compose environment, runs VHS tapes, tears down. -set -eu +# Record VHS tapes inside the capture container against a disposable released daemon. +# Docker is the only prerequisite; the host netclaw is never used. +# +# ./screenshots/capture.sh run every tape +# ./screenshots/capture.sh status run one tape (screenshots/tapes/status.tape) +# ./screenshots/capture.sh status doctor +# +# Output goes to screenshots/output/ (override: OUT_DIR=/some/dir), owned by you. +# A tape starts from a clean ~/.netclaw. A tape whose first line comments contain +# `# stack: fresh` runs against the first-run state (no model config); `# stack: bare` is the +# same empty home plus the demo MCP server container, for tapes that seed their own config; +# all others run against the configured daemon. Exits non-zero if any tape fails; the stack is always +# torn down. Image, project name and inference settings: see stack.sh and README.md. +set -euo pipefail +# shellcheck source=lib.sh +. "$(dirname "${BASH_SOURCE[0]}")/lib.sh" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -TAPES_DIR="$SCRIPT_DIR/tapes" -OUTPUT_DIR="$SCRIPT_DIR/output" -COMPOSE_FILE="$PROJECT_ROOT/docker-compose.screenshots.yml" +OUT_DIR="${OUT_DIR:-$SHOTS_DIR/output}" +mkdir -p "$OUT_DIR" +export CAPTURE_OUT_DIR="$(cd "$OUT_DIR" && pwd)" -# Use a clean netclaw home for the init wizard so we get first-run screens -export NETCLAW_SCREENSHOT_HOME="$SCRIPT_DIR/.netclaw-screenshot-home" - -mkdir -p "$OUTPUT_DIR" +tapes=("$@") +if [ "${#tapes[@]}" -eq 0 ]; then + for f in "$SHOTS_DIR"/tapes/*.tape; do tapes+=("$(basename "$f" .tape)"); done +fi -NETCLAW_PORT="${NETCLAW_PORT:-5299}" +cleanup() { + echo "==> Tearing down project $COMPOSE_PROJECT_NAME" + "$SHOTS_DIR/stack.sh" down >/dev/null 2>&1 || true +} +trap cleanup EXIT -# Pin the daemon image to the version the docs are written against (src/version.json), -# unless the caller already set NETCLAW_VERSION or NETCLAW_IMAGE. Keeps screenshots -# in lockstep with the documented release. -if [ -z "${NETCLAW_VERSION:-}" ] && [ -f "$PROJECT_ROOT/src/version.json" ]; then - NETCLAW_VERSION="$(node -e "process.stdout.write(require('$PROJECT_ROOT/src/version.json').documentedVersion)" 2>/dev/null || true)" -fi -export NETCLAW_VERSION -echo "==> Capturing against netclaw ${NETCLAW_IMAGE:-ghcr.io/netclaw-dev/netclaw:${NETCLAW_VERSION:-0.22.1}}" +echo "==> Capturing against $NETCLAW_IMAGE ($INFERENCE_MODE inference), output $CAPTURE_OUT_DIR" -echo "==> Starting screenshot environment (building images if needed)..." -docker compose -f "$COMPOSE_FILE" up -d --build --wait +failed=() +for name in "${tapes[@]}"; do + tape="$SHOTS_DIR/tapes/$name.tape" + [ -f "$tape" ] || { echo "capture: no such tape: $name" >&2; exit 2; } + state=configured + if grep -qE '^#[[:space:]]*stack:[[:space:]]*fresh' "$tape"; then state=fresh; fi + if grep -qE '^#[[:space:]]*stack:[[:space:]]*bare' "$tape"; then state=bare; fi -echo "==> Waiting for netclawd health check on port $NETCLAW_PORT..." -for i in $(seq 1 60); do - if curl -sf "http://127.0.0.1:${NETCLAW_PORT}/api/health/ready" >/dev/null 2>&1; then - echo " netclawd is healthy." - break - fi - if [ "$i" -eq 60 ]; then - echo " ERROR: netclawd did not become healthy in time." - docker compose -f "$COMPOSE_FILE" logs netclawd - docker compose -f "$COMPOSE_FILE" down - exit 1 - fi - sleep 2 -done + # `# image: 0.27.0` runs this one tape against another release (update-check.tape needs an + # install that is genuinely one version behind). `# host-docker: true` lets the tape drive the + # host's Docker through its socket (docker-health.tape). + image_version="$(sed -nE 's/^#[[:space:]]*image:[[:space:]]*([0-9A-Za-z._-]+).*/\1/p' "$tape" | head -1)" + host_docker="" + if grep -qE '^#[[:space:]]*host-docker:[[:space:]]*true' "$tape"; then host_docker=1; fi -echo "==> Running VHS tapes..." -for tape in "$TAPES_DIR"/*.tape; do - if [ -f "$tape" ]; then - echo " Running: $(basename "$tape")" - vhs "$tape" || echo " WARNING: $(basename "$tape") failed" + echo "==> $name: clean daemon ($state)" + if ! ( + if [ -n "$image_version" ]; then + unset NETCLAW_IMAGE + export NETCLAW_VERSION="$image_version" + # shellcheck source=lib.sh + . "$SHOTS_DIR/lib.sh" + echo "==> $name: this tape pins image $NETCLAW_IMAGE" + fi + if [ -n "$host_docker" ]; then export CAPTURE_HOST_DOCKER=1; fi + # reset = wipe ~/.netclaw and (re)start the daemon; also creates the stack on first use. + "$SHOTS_DIR/stack.sh" reset $([ "$state" != configured ] && echo "--$state") && + dc "$state" run --rm --no-deps -T --build capture "$name" + ); then + failed+=("$name") fi done -echo "==> Tearing down environment..." -docker compose -f "$COMPOSE_FILE" down - -echo "==> Screenshots saved to: $OUTPUT_DIR" -ls -la "$OUTPUT_DIR"/*.png 2>/dev/null || echo " No PNG files generated." +if [ "${#failed[@]}" -gt 0 ]; then + echo "capture: FAILED tapes: ${failed[*]}" >&2 + exit 1 +fi +echo "==> Done: ${tapes[*]}" diff --git a/screenshots/capture/Dockerfile b/screenshots/capture/Dockerfile new file mode 100644 index 0000000..fce7e0b --- /dev/null +++ b/screenshots/capture/Dockerfile @@ -0,0 +1,48 @@ +# Capture image: VHS (ttyd, ffmpeg, headless chromium) + the released netclaw CLI. +# +# Why this shape: the release image (ubuntu:24.04, glibc) ships the CLI as a +# self-contained .NET single-file binary, and the VHS image (debian trixie, glibc 2.41, +# ICU and libsodium already present) runs it unmodified. So we take the VHS image and +# copy the CLI and its self-dropping launcher out of the release image. The CLI in a +# tape is therefore byte-for-byte the released one, never a host build. +# +# The container shares the daemon's network namespace and ~/.netclaw volume (see +# docker-compose.screenshots.yml), so `netclaw status` typed in a recording reaches the +# daemon over loopback the way a real user's CLI does. +ARG NETCLAW_IMAGE +ARG VHS_IMAGE=ghcr.io/charmbracelet/vhs:v0.12.1 + +FROM ${NETCLAW_IMAGE} AS netclaw + +FROM ${VHS_IMAGE} + +RUN apt-get update \ + && apt-get install -y --no-install-recommends gosu curl ca-certificates git jq \ + && rm -rf /var/lib/apt/lists/* + +# Same uid/gid as the daemon so the shared ~/.netclaw volume is readable and writable. +RUN groupadd --gid 1654 netclaw \ + && useradd --uid 1654 --gid netclaw --create-home netclaw \ + && mkdir -p /home/netclaw/.netclaw /work/screenshots/output \ + && chown -R netclaw:netclaw /home/netclaw /work + +# The skillserver CLI, used by the seed scripts to publish demo skills to the stack's skill server. +ARG SKILLSERVER_VERSION=0.4.1 +RUN arch="$(uname -m)"; case "$arch" in x86_64) rid=x64 ;; aarch64) rid=arm64 ;; esac \ + && curl -fsSL "https://github.com/netclaw-dev/skill-server/releases/download/${SKILLSERVER_VERSION}/skillserver-${SKILLSERVER_VERSION}-linux-${rid}.tar.gz" \ + | tar -xz -C /usr/local/bin \ + && chmod +x /usr/local/bin/skillserver + +COPY --from=netclaw /opt/netclaw/cli/netclaw /opt/netclaw/cli/netclaw +COPY --from=netclaw /usr/local/bin/netclaw /usr/local/bin/netclaw + +COPY entrypoint.sh /usr/local/bin/capture-entrypoint +RUN chmod +x /usr/local/bin/capture-entrypoint + +ENV HOME=/home/netclaw \ + NETCLAW_HOME=/home/netclaw/.netclaw \ + NETCLAW_CONTAINER_SUPERVISOR=entrypoint \ + VHS_NO_SANDBOX=true + +WORKDIR /work +ENTRYPOINT ["/usr/local/bin/capture-entrypoint"] diff --git a/screenshots/capture/entrypoint.sh b/screenshots/capture/entrypoint.sh new file mode 100755 index 0000000..a8d7993 --- /dev/null +++ b/screenshots/capture/entrypoint.sh @@ -0,0 +1,63 @@ +#!/bin/bash +# Runs inside the capture container. Arguments are tape names (without .tape). +# Each tape is recorded as the unprivileged `netclaw` user (same as the daemon), into a +# staging dir, then copied to /out owned by the invoking host user. +# Exits non-zero as soon as any tape fails or does not produce a file it declares. +set -uo pipefail + +STAGE=/work/screenshots/output +OUT=/out +HOST_UID="${HOST_UID:-0}" +HOST_GID="${HOST_GID:-0}" + +if [ "$#" -eq 0 ]; then + echo "capture: no tapes given" >&2 + exit 2 +fi + +# Tapes marked `# host-docker: true` get the host's Docker socket (compose/docker-host.yml). The +# netclaw user needs the socket's group to use it. +if [ -S /var/run/docker.sock ]; then + sock_gid="$(stat -c %g /var/run/docker.sock)" + getent group "$sock_gid" >/dev/null || groupadd --gid "$sock_gid" dockerhost + usermod -aG "$(getent group "$sock_gid" | cut -d: -f1)" netclaw +fi + +for name in "$@"; do + tape="/tapes/${name}.tape" + if [ ! -f "$tape" ]; then + echo "capture: no such tape: $name" >&2 + exit 2 + fi + + echo "==> Recording $name ($(netclaw --version))" + rm -rf "${STAGE:?}"/* + chown netclaw:netclaw "$STAGE" + + # VHS writes a Screenshot asynchronously, so one that is the LAST command in a tape is + # lost when the recording ends. Run a copy with a trailing Sleep so any tape is safe. + run_tape="/tmp/run-${name}.tape" + { cat "$tape"; printf '\nSleep 1s\n'; } > "$run_tape" + chmod 644 "$run_tape" + + if ! gosu netclaw vhs "$run_tape"; then + echo "capture: FAILED: vhs exited non-zero for $name" >&2 + exit 1 + fi + + # Every Output/Screenshot path the tape declares must exist. + missing=0 + while read -r path; do + if [ ! -s "/work/$path" ]; then + echo "capture: FAILED: $name did not produce $path" >&2 + missing=1 + fi + done < <(sed -nE 's/^[[:space:]]*(Output|Screenshot)[[:space:]]+"?([^" ]+)"?.*$/\2/p' "$tape" | grep -v '^#') + [ "$missing" -eq 0 ] || exit 1 + + # Only stills are published. A tape needs an Output line for VHS to run; its GIF stays in staging. + while IFS= read -r -d '' f; do + install -m 0644 -o "$HOST_UID" -g "$HOST_GID" "$f" "$OUT/$(basename "$f")" + echo " wrote $(basename "$f")" + done < <(find "$STAGE" -maxdepth 1 -type f -name '*.png' -print0) +done diff --git a/screenshots/compose/configured.yml b/screenshots/compose/configured.yml new file mode 100644 index 0000000..9303652 --- /dev/null +++ b/screenshots/compose/configured.yml @@ -0,0 +1,29 @@ +# Overlay: the "initialized" daemon. Adds the demo MCP server container. The daemon's +# provider, model roles and MCP entry are NOT env vars: stack.sh writes them with the +# real CLI (`netclaw provider add`, `netclaw model set`, `netclaw mcp add`) so +# ~/.netclaw/config/netclaw.json looks exactly like a user's after `netclaw init`, and +# the CLI's own config preflight and TUIs see it. +# Relative paths resolve against the directory of docker-compose.screenshots.yml. +services: + mcp-demo: + image: netclaw-docs-mcp-demo:1 + build: ./screenshots/mcp-server + network_mode: "service:netns" + depends_on: + - netns + healthcheck: + test: ["CMD", "python", "-c", "import socket; s=socket.create_connection(('127.0.0.1',8080),2); s.close()"] + interval: 3s + timeout: 5s + retries: 20 + start_period: 3s + + netclawd: + depends_on: + mcp-demo: + condition: service_healthy + environment: + # Real inference endpoints can be slow to first token. + NETCLAW_Session__TurnLlmTimeoutSeconds: "600" + NETCLAW_Session__FirstTokenTimeoutSeconds: "600" + NETCLAW_Session__PrefillTimeoutSeconds: "600" diff --git a/screenshots/compose/docker-host.yml b/screenshots/compose/docker-host.yml new file mode 100644 index 0000000..16e3794 --- /dev/null +++ b/screenshots/compose/docker-host.yml @@ -0,0 +1,12 @@ +# Overlay: lets a tape drive the host's Docker (docker-health.tape), by mounting the Docker socket +# and the host's docker client into the capture container. Added by lib.sh only for tapes marked +# `# host-docker: true`. The tape must filter everything it prints to its own container, so nothing +# else on the host shows up in the screenshot. +services: + capture: + environment: + NETCLAW_IMAGE: ${NETCLAW_IMAGE} + COMPOSE_PROJECT_NAME: ${COMPOSE_PROJECT_NAME} + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ${DOCKER_CLI_PATH:-/usr/bin/docker}:/usr/local/bin/docker:ro diff --git a/screenshots/compose/forwarder.conf.template b/screenshots/compose/forwarder.conf.template new file mode 100644 index 0000000..566f0f2 --- /dev/null +++ b/screenshots/compose/forwarder.conf.template @@ -0,0 +1,23 @@ +# Relays plain HTTP on :8081 to the real inference endpoint, so netclaw is configured with +# http://llm.example.internal:8081 and the real hostname never appears in netclaw.json, +# the TUI or a recording. The upstream's model id is rewritten to a neutral one in every +# response (so /v1/models lists the neutral id and `netclaw init` / `model discover` store it); +# requests keep the neutral id, which llama.cpp style servers accept for any model name. +# Rendered by the nginx image from INFERENCE_UPSTREAM, INFERENCE_UPSTREAM_HOST, +# INFERENCE_UPSTREAM_MODEL and INFERENCE_MODEL_ID (screenshots/.env, gitignored). +server { + listen 8081; + location / { + proxy_pass ${INFERENCE_UPSTREAM}; + proxy_http_version 1.1; + proxy_ssl_server_name on; + proxy_set_header Host ${INFERENCE_UPSTREAM_HOST}; + proxy_set_header Accept-Encoding ""; + proxy_buffering off; + proxy_read_timeout 900s; + proxy_send_timeout 900s; + sub_filter_types application/json text/event-stream; + sub_filter_once off; + sub_filter '${INFERENCE_UPSTREAM_MODEL}' '${INFERENCE_MODEL_ID}'; + } +} diff --git a/screenshots/compose/forwarder.yml b/screenshots/compose/forwarder.yml new file mode 100644 index 0000000..b2e849d --- /dev/null +++ b/screenshots/compose/forwarder.yml @@ -0,0 +1,32 @@ +# Overlay: keeps a real inference endpoint out of every frame. Added by lib.sh when +# INFERENCE_UPSTREAM is set (an https:// URL in screenshots/.env, together with +# INFERENCE_UPSTREAM_MODEL, the model id the upstream really serves). +# +# - `forwarder` joins the stack's network namespace and relays :8081 to the upstream, rewriting +# the upstream's model id to the neutral INFERENCE_MODEL_ID (default demo-model) in responses. +# - `extra_hosts` on the namespace owner maps llm.example.internal to loopback, and every +# container that shares the namespace (daemon, capture, helpers) inherits the entry. +# +# The configured endpoint is http://llm.example.internal:8081 and the model id is demo-model. +# Port 8081 because the demo MCP server owns 8080 in the shared network namespace. +services: + netns: + extra_hosts: + - "llm.example.internal:127.0.0.1" + + forwarder: + image: nginx:1.27-alpine + network_mode: "service:netns" + depends_on: + - netns + environment: + INFERENCE_UPSTREAM: ${INFERENCE_UPSTREAM:?set in screenshots/.env} + INFERENCE_UPSTREAM_HOST: ${INFERENCE_UPSTREAM_HOST:?set in screenshots/.env} + INFERENCE_UPSTREAM_MODEL: ${INFERENCE_UPSTREAM_MODEL:?set in screenshots/.env} + INFERENCE_MODEL_ID: ${INFERENCE_MODEL_ID:?set in screenshots/.env} + volumes: + - ./screenshots/compose/forwarder.conf.template:/etc/nginx/templates/default.conf.template:ro + + netclawd: + depends_on: + - forwarder diff --git a/screenshots/compose/ollama.yml b/screenshots/compose/ollama.yml new file mode 100644 index 0000000..ef2c1fa --- /dev/null +++ b/screenshots/compose/ollama.yml @@ -0,0 +1,31 @@ +# Overlay: hermetic inference. A tiny Ollama model, no external network use after the +# one-time model pull. Shares the stack's network namespace, so the endpoint is +# http://127.0.0.1:11434 (which is also what the `netclaw init` wizard defaults to). +# Relative paths resolve against the directory of docker-compose.screenshots.yml. +services: + ollama: + image: ollama/ollama:0.32.0 + network_mode: "service:netns" + depends_on: + - netns + volumes: + - ollama-data:/root/.ollama + + ollama-init: + image: curlimages/curl:8.13.0 + network_mode: "service:netns" + entrypoint: ["/bin/sh", "/usr/local/bin/ollama-init.sh"] + environment: + OLLAMA_MODEL: ${OLLAMA_MODEL:-qwen2:0.5b} + depends_on: + - ollama + volumes: + - ./screenshots/scripts/ollama-init.sh:/usr/local/bin/ollama-init.sh:ro + + netclawd: + depends_on: + ollama-init: + condition: service_completed_successfully + +volumes: + ollama-data: diff --git a/screenshots/compose/projects.yml b/screenshots/compose/projects.yml new file mode 100644 index 0000000..155ed9f --- /dev/null +++ b/screenshots/compose/projects.yml @@ -0,0 +1,25 @@ +# Overlay: a neutral projects directory shared by the daemon and the capture container, +# so a tape can create a small Git repository at /home/netclaw/projects/website that the +# daemon's shell tool can also see (approval prompts offer "This repository" only inside +# a Git repository). Created and owned by the netclaw uid (1654) by a one-shot init. +services: + projects-init: + image: alpine:3.20 + network_mode: "none" + command: ["sh", "-c", "mkdir -p /projects && chown 1654:1654 /projects"] + volumes: + - projects:/projects + + netclawd: + depends_on: + projects-init: + condition: service_completed_successfully + volumes: + - projects:/home/netclaw/projects + + capture: + volumes: + - projects:/home/netclaw/projects + +volumes: + projects: diff --git a/screenshots/compose/skillserver.yml b/screenshots/compose/skillserver.yml new file mode 100644 index 0000000..abdb677 --- /dev/null +++ b/screenshots/compose/skillserver.yml @@ -0,0 +1,31 @@ +# Overlay: a skill server on the stack's network namespace (port 8090, because the demo MCP +# server owns 8080), for the skill-list and skill-sync captures. It +# is seeded with a throwaway demo key (publishing needs one; reads stay open). Pinned to the release the docs describe. +services: + skillserver-init: + image: ghcr.io/netclaw-dev/skillserver:0.4.1 + user: "0:0" + network_mode: "none" + entrypoint: ["sh", "-c", "chown -R 1654:1654 /data"] + restart: "no" + volumes: + - skill-data:/data + + skillserver: + image: ghcr.io/netclaw-dev/skillserver:0.4.1 + network_mode: "service:netns" + depends_on: + netns: + condition: service_started + skillserver-init: + condition: service_completed_successfully + environment: + SKILLSERVER__DATAPATH: /data + SKILLSERVER__BASEURL: http://127.0.0.1:8090 + SKILLSERVER__APIKEY: demo-publish-key + ASPNETCORE_URLS: http://+:8090 + volumes: + - skill-data:/data + +volumes: + skill-data: diff --git a/screenshots/lib.sh b/screenshots/lib.sh new file mode 100755 index 0000000..5beee83 --- /dev/null +++ b/screenshots/lib.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# Shared helpers for stack.sh and capture.sh. Source it, don't run it. +# +# Resolves, in order: ./screenshots/.env (gitignored), then defaults. +# COMPOSE_PROJECT_NAME isolates containers, network and volumes (default netclaw-shots) +# NETCLAW_VERSION release tag; default = documentedVersion in src/version.json +# NETCLAW_IMAGE full image reference; wins over NETCLAW_VERSION +# INFERENCE_BASE_URL external OpenAI-compatible endpoint (unset = hermetic Ollama) +# INFERENCE_MODEL_ID model id served there (required with INFERENCE_BASE_URL) +# OLLAMA_MODEL hermetic model (default qwen2:0.5b) +# INFERENCE_UPSTREAM base URL of a real OpenAI-compatible server (relay mode). Adds the forwarder +# (compose/forwarder.yml) that answers to http://llm.example.internal:8081 +# inside the stack and rewrites the upstream's model id to `demo-model` in +# every response, so tapes get real answers while neither the real hostname +# nor the real model name reaches netclaw.json or a screenshot. Needs +# INFERENCE_UPSTREAM_MODEL (the id the upstream really serves). Ollama stays +# available alongside it, for the tapes that need a reachable local provider. + +SHOTS_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "$SHOTS_DIR/.." && pwd)" +BASE_COMPOSE="$PROJECT_ROOT/docker-compose.screenshots.yml" + +if [ -f "$SHOTS_DIR/.env" ]; then + set -a + # shellcheck disable=SC1091 + . "$SHOTS_DIR/.env" + set +a +fi + +export COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-netclaw-shots}" +export COMPOSE_PROGRESS="${COMPOSE_PROGRESS:-quiet}" # set COMPOSE_PROGRESS=plain to see build/pull output +# Compose project names must be lowercase alphanumerics, '-' or '_'. +case "$COMPOSE_PROJECT_NAME" in + *[!a-z0-9_-]* | "") echo "COMPOSE_PROJECT_NAME must match [a-z0-9_-]+ (got '$COMPOSE_PROJECT_NAME')" >&2; exit 2 ;; +esac + +# Version: one source of truth (src/version.json) unless overridden. +if [ -z "${NETCLAW_IMAGE:-}" ]; then + if [ -z "${NETCLAW_VERSION:-}" ]; then + NETCLAW_VERSION="$(sed -n 's/.*"documentedVersion": *"\([^"]*\)".*/\1/p' "$PROJECT_ROOT/src/version.json")" + [ -n "$NETCLAW_VERSION" ] || { echo "could not read documentedVersion from src/version.json" >&2; exit 2; } + fi + NETCLAW_IMAGE="ghcr.io/netclaw-dev/netclaw:${NETCLAW_VERSION}" +fi +export NETCLAW_IMAGE +export NETCLAW_VERSION="${NETCLAW_IMAGE##*:}" +export NETCLAW_IMAGE_TAG="$(printf '%s' "$NETCLAW_VERSION" | tr -c 'A-Za-z0-9_.-' '_')" + +# Inference: three modes. +# relay INFERENCE_UPSTREAM set: real answers through the forwarder, neutral hostname and model +# external INFERENCE_BASE_URL set: a real endpoint written into netclaw.json as is +# hermetic neither: Ollama with a tiny model, no external network use +if [ -n "${INFERENCE_UPSTREAM:-}" ]; then + : "${INFERENCE_UPSTREAM_MODEL:?INFERENCE_UPSTREAM_MODEL is required with INFERENCE_UPSTREAM (the model id the upstream serves)}" + INFERENCE_UPSTREAM_HOST="${INFERENCE_UPSTREAM#*://}"; INFERENCE_UPSTREAM_HOST="${INFERENCE_UPSTREAM_HOST%%/*}" + INFERENCE_BASE_URL="${INFERENCE_BASE_URL:-http://llm.example.internal:8081}" + INFERENCE_MODEL_ID="${INFERENCE_MODEL_ID:-demo-model}" + INFERENCE_MODE=relay + export INFERENCE_UPSTREAM INFERENCE_UPSTREAM_HOST INFERENCE_UPSTREAM_MODEL INFERENCE_MODEL_ID + export NC_INFERENCE_TYPE=openai-compatible + export NC_INFERENCE_ENDPOINT="$INFERENCE_BASE_URL" + export NC_INFERENCE_MODEL="$INFERENCE_MODEL_ID" +elif [ -n "${INFERENCE_BASE_URL:-}" ]; then + : "${INFERENCE_MODEL_ID:?INFERENCE_MODEL_ID is required when INFERENCE_BASE_URL is set}" + INFERENCE_MODE=external + export NC_INFERENCE_TYPE=openai-compatible + export NC_INFERENCE_ENDPOINT="$INFERENCE_BASE_URL" + export NC_INFERENCE_MODEL="$INFERENCE_MODEL_ID" +else + INFERENCE_MODE=hermetic + export NC_INFERENCE_TYPE=ollama + export NC_INFERENCE_ENDPOINT=http://127.0.0.1:11434 + export NC_INFERENCE_MODEL="${OLLAMA_MODEL:-qwen2:0.5b}" +fi + +# Playwright image/module version: the one locked in package-lock.json. +if [ -z "${PLAYWRIGHT_VERSION:-}" ]; then + PLAYWRIGHT_VERSION="$(grep -A1 '"node_modules/playwright":' "$PROJECT_ROOT/package-lock.json" | sed -n 's/.*"version": *"\([^"]*\)".*/\1/p')" +fi +export PLAYWRIGHT_VERSION + +export HOST_UID="${HOST_UID:-$(id -u)}" +export HOST_GID="${HOST_GID:-$(id -g)}" + +# compose_files +# bare: like configured (demo MCP server container up) but stack.sh does not seed any config +# configured: daemon with a provider, model roles and the demo MCP server +# fresh: daemon with no model config at all (first-run state for `netclaw init`) +# all: every overlay, for commands (exec/logs/down) that must see every service +compose_files() { + local state="$1" + local files=(-f "$BASE_COMPOSE") + if [ "$INFERENCE_MODE" != external ]; then files+=(-f "$SHOTS_DIR/compose/ollama.yml"); fi + if [ "$INFERENCE_MODE" = relay ]; then files+=(-f "$SHOTS_DIR/compose/forwarder.yml"); fi + if [ "$state" != fresh ]; then files+=(-f "$SHOTS_DIR/compose/configured.yml" -f "$SHOTS_DIR/compose/skillserver.yml"); fi + files+=(-f "$SHOTS_DIR/compose/projects.yml") + if [ -n "${CAPTURE_HOST_DOCKER:-}" ]; then files+=(-f "$SHOTS_DIR/compose/docker-host.yml"); fi + printf '%s\0' "${files[@]}" +} + +# dc +dc() { + local state="$1"; shift + local files=() + while IFS= read -r -d '' f; do files+=("$f"); done < <(compose_files "$state") + docker compose "${files[@]}" "$@" +} + +home_volume() { printf '%s_netclaw-home' "$COMPOSE_PROJECT_NAME"; } diff --git a/screenshots/mattermost/approval.mjs b/screenshots/mattermost/approval.mjs new file mode 100644 index 0000000..373e5b6 --- /dev/null +++ b/screenshots/mattermost/approval.mjs @@ -0,0 +1,95 @@ +#!/usr/bin/env node +// Captures the landing page's approval prompt (public/assets/approval-prompt.png): the thread +// post that netclaw sends when shell_execute needs approval, with its buttons, framed by +// frame-approval.mjs. Runs in the Playwright container (run.sh with SCRIPTED_APPROVAL=1). +// +// Env: BOOTSTRAP_FILE, OUT_DIR, REPLY_TIMEOUT_SECONDS (default 120) +import { chromium } from 'playwright'; +import { readFileSync, mkdirSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import { frame } from './frame-approval.mjs'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const boot = JSON.parse(readFileSync(process.env.BOOTSTRAP_FILE || join(__dirname, '.bootstrap.json'), 'utf8')); +const OUT = process.env.OUT_DIR || join(__dirname, '..', 'output'); +const TIMEOUT_MS = Number(process.env.REPLY_TIMEOUT_SECONDS || 120) * 1000; +const BASE = boot.serverUrl; +mkdirSync(OUT, { recursive: true }); + +// Mattermost's Onyx theme. +const ONYX = { + type: 'Onyx', sidebarBg: '#18181a', sidebarText: '#ffffff', sidebarUnreadText: '#ffffff', + sidebarTextHoverBg: '#25262a', sidebarTextActiveBorder: '#afbe5d', sidebarTextActiveColor: '#ffffff', + sidebarHeaderBg: '#1f1f1f', sidebarTeamBarBg: '#121212', sidebarHeaderTextColor: '#ffffff', + onlineIndicator: '#a4c940', awayIndicator: '#dcbd4e', dndIndicator: '#d24b4e', + mentionBg: '#ffffff', mentionColor: '#1f1f1f', centerChannelBg: '#1f1f1f', centerChannelColor: '#dddddd', + newMessageSeparator: '#cc8f00', linkColor: '#a4ffeb', buttonBg: '#a4ffeb', buttonColor: '#1f1f1f', + errorTextColor: '#ff6461', mentionHighlightBg: '#784098', mentionHighlightLink: '#a4ffeb', codeTheme: 'solarized-dark', +}; + +async function api(method, path, body, token) { + const res = await fetch(`${BASE}/api/v4${path}`, { + method, + headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}: ${(await res.text()).slice(0, 200)}`); + return res; +} + +const login = await api('POST', '/users/login', { login_id: boot.testuser.username, password: boot.testuser.password }); +const token = login.headers.get('Token'); +const me = await login.json(); +await api('PUT', `/users/${me.id}/preferences`, [ + { user_id: me.id, category: 'theme', name: '', value: JSON.stringify(ONYX) }, + { user_id: me.id, category: 'tutorial_step', name: me.id, value: '999' }, + { user_id: me.id, category: 'onboarding_task_list', name: 'onboarding_task_list_show', value: 'false' }, + { user_id: me.id, category: 'recommended_next_steps', name: 'hide', value: 'true' }, +], token); + +const root = await (await api('POST', '/posts', { channel_id: boot.channel.id, message: '@netclaw push the website changes' }, token)).json(); +const thread = async () => { + const d = await (await api('GET', `/posts/${root.id}/thread`, null, token)).json(); + return d.order.map(id => d.posts[id]).sort((a, b) => a.create_at - b.create_at); +}; +let prompt; +for (const t0 = Date.now(); Date.now() - t0 < TIMEOUT_MS && !prompt; await new Promise(r => setTimeout(r, 2000))) + prompt = (await thread()).find(p => p.user_id === boot.bot.userId && /Tool approval required/i.test(p.message)); +if (!prompt) { console.error('no approval prompt within the timeout'); process.exit(1); } +const actions = (prompt.props?.attachments?.[0]?.actions || []).map(a => a.name); +console.log('prompt text:\n' + prompt.message + '\nbuttons: ' + JSON.stringify(actions)); +if (!actions.length) { console.error('the prompt has no buttons; CallbackUrl is not taking effect'); process.exit(1); } + +const browser = await chromium.launch(); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 }, deviceScaleFactor: 2, colorScheme: 'dark' }); +await ctx.addInitScript(() => { try { localStorage.setItem('__landingPageSeen__', 'true'); localStorage.setItem('onboarding_complete', 'true'); } catch {} }); +const page = await ctx.newPage(); +await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' }); +for (const rx of [/view in browser/i, /continue in browser/i]) { + try { const l = page.getByText(rx).first(); if (await l.isVisible({ timeout: 1500 })) { await l.click(); break; } } catch {} +} +await page.waitForSelector('input[type="password"]', { timeout: 30000 }); +await page.locator('input[type="text"], input[type="email"]').first().fill(boot.testuser.username); +await page.locator('input[type="password"]').first().fill(boot.testuser.password); +await page.getByRole('button', { name: /log in|sign in/i }).first().click(); +await page.waitForURL(u => !/\/login/.test(new URL(u).pathname), { timeout: 20000 }); +await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); + +await page.goto(`${BASE}/${boot.team.name}/pl/${root.id}`, { waitUntil: 'networkidle' }); +await page.waitForTimeout(2500); +await page.getByRole('button', { name: /\d+ repl(y|ies)/i }).last().click(); +await page.waitForTimeout(2500); +await page.getByRole('button', { name: /expand/i }).first().click(); // widen the panel so the buttons fit one row +await page.waitForTimeout(1500); +await page.mouse.move(640, 880); +await page.addStyleTag({ content: '.NotificationSeparator{display:none !important}' }); // the "New Messages" line +await page.waitForTimeout(500); +const post = page.locator(`#sidebar-right #post_${prompt.id}, #sidebar-right [id$="${prompt.id}"]`).first(); +await post.waitFor({ timeout: 15000 }); +await page.screenshot({ path: join(OUT, '_approval-full.png') }); +const raw = join(OUT, '_approval-raw.png'); +await post.screenshot({ path: raw }); +await frame(browser, raw, join(OUT, 'approval-prompt.png')); +await browser.close(); +console.log(' ok approval-prompt.png'); diff --git a/screenshots/mattermost/bootstrap.mjs b/screenshots/mattermost/bootstrap.mjs index c6a8c35..4bb79b8 100644 --- a/screenshots/mattermost/bootstrap.mjs +++ b/screenshots/mattermost/bootstrap.mjs @@ -2,16 +2,19 @@ // Bootstraps a fresh mattermost-preview the same way the netclaw repo's Aspire // demo does: first user (auto system-admin), team, bot + access token, channel, // and a non-admin test user. Writes the resulting IDs/token/credentials to -// .bootstrap.json for run.sh (netclaw config) and capture.mjs (Playwright). +// BOOTSTRAP_FILE for run.sh (netclaw config) and capture.mjs (Playwright). // -// Usage: node bootstrap.mjs [serverUrl] (default http://localhost:8065) +// Runs inside the Playwright container (run.sh), on the same compose network as Mattermost. +// Usage: node bootstrap.mjs [serverUrl] (default http://mattermost:8065) +// Env: BOOTSTRAP_FILE where to write the result (default ./.bootstrap.json) import { writeFileSync } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; -const SERVER = (process.argv[2] || 'http://localhost:8065').replace(/\/$/, ''); +const SERVER = (process.argv[2] || 'http://mattermost:8065').replace(/\/$/, ''); const __dirname = dirname(fileURLToPath(import.meta.url)); +const BOOTSTRAP_FILE = process.env.BOOTSTRAP_FILE || join(__dirname, '.bootstrap.json'); const ADMIN = { email: 'admin@test.local', username: 'admin', password: 'Admin1234!' }; const USER = { email: 'testuser@test.local', username: 'testuser', password: 'TestUser1234!' }; @@ -97,9 +100,9 @@ async function main() { channel: { id: channel.id, name: channel.name }, bot: { userId: bot.user_id, username: bot.username, token: tok.token }, }; - writeFileSync(join(__dirname, '.bootstrap.json'), JSON.stringify(out, null, 2)); + writeFileSync(BOOTSTRAP_FILE, JSON.stringify(out, null, 2)); console.log(`\n✅ Bootstrap complete. channel=${channel.id} bot=${bot.user_id}`); - console.log(` Wrote ${join(__dirname, '.bootstrap.json')}`); + console.log(` Wrote ${BOOTSTRAP_FILE}`); } main().catch(err => { console.error('❌ Bootstrap failed:', err.message); process.exit(1); }); diff --git a/screenshots/mattermost/capture-compose.mjs b/screenshots/mattermost/capture-compose.mjs new file mode 100644 index 0000000..d181de5 --- /dev/null +++ b/screenshots/mattermost/capture-compose.mjs @@ -0,0 +1,266 @@ +#!/usr/bin/env node +// Captures the Mattermost screenshots for channels/mattermost.md and +// guides/mattermost-docker-compose.md from the stack in examples/mattermost-compose/ +// (provisioned with provision.sh, wired with configure-netclaw.sh). +// +// One consistent look: Chromium, 1280x800 viewport, device scale factor 2, the Onyx +// dark theme, no browser chrome (page screenshots, cropped to the relevant region). +// Token IDs on screen are overwritten in the page before the screenshot is taken. +// +// Usage (Playwright container or any Node with playwright installed): +// MM_URL=http://localhost:8065 ADMIN_PASSWORD=... ALICE_PASSWORD=... \ +// node capture-compose.mjs [step ...] +// Steps (default: all): admin bots thread approval +// Env: TEAM (home), CHANNEL (assistant), OUT_DIR (../output), +// REPO_DIR (directory the approval prompt runs in), REPLY_TIMEOUT_SECONDS (300) + +import { chromium } from 'playwright'; +import { mkdirSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const here = dirname(fileURLToPath(import.meta.url)); +const BASE = (process.env.MM_URL || 'http://localhost:8065').replace(/\/$/, ''); +const TEAM = process.env.TEAM || 'home'; +const CHANNEL = process.env.CHANNEL || 'assistant'; +const OUT = process.env.OUT_DIR || join(here, '..', 'output'); +const REPO_DIR = process.env.REPO_DIR || '~/.netclaw/projects/website'; +const TIMEOUT_MS = Number(process.env.REPLY_TIMEOUT_SECONDS || 300) * 1000; +const ADMIN = { username: 'admin', password: process.env.ADMIN_PASSWORD }; +const ALICE = { username: 'alice', password: process.env.ALICE_PASSWORD }; +if (!ADMIN.password || !ALICE.password) { console.error('ADMIN_PASSWORD and ALICE_PASSWORD are required'); process.exit(2); } +mkdirSync(OUT, { recursive: true }); + +const steps = new Set(process.argv.slice(2).length ? process.argv.slice(2) : ['admin', 'bots', 'thread', 'approval']); +const VIEWPORT = { width: 1280, height: 800 }; +const MASK = 'x'.repeat(26); + +// Mattermost's Onyx theme. +const ONYX = { + type: 'Onyx', sidebarBg: '#18181a', sidebarText: '#ffffff', sidebarUnreadText: '#ffffff', + sidebarTextHoverBg: '#25262a', sidebarTextActiveBorder: '#afbe5d', sidebarTextActiveColor: '#ffffff', + sidebarHeaderBg: '#1f1f1f', sidebarTeamBarBg: '#121212', sidebarHeaderTextColor: '#ffffff', + onlineIndicator: '#a4c940', awayIndicator: '#dcbd4e', dndIndicator: '#d24b4e', + mentionBg: '#ffffff', mentionColor: '#1f1f1f', centerChannelBg: '#1f1f1f', centerChannelColor: '#dddddd', + newMessageSeparator: '#cc8f00', linkColor: '#a4ffeb', buttonBg: '#a4ffeb', buttonColor: '#1f1f1f', + errorTextColor: '#ff6461', mentionHighlightBg: '#784098', mentionHighlightLink: '#a4ffeb', codeTheme: 'solarized-dark', +}; + +async function api(method, path, body, token) { + const res = await fetch(`${BASE}/api/v4${path}`, { + method, + headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}: ${(await res.text()).slice(0, 200)}`); + return res; +} + +async function apiLogin(user) { + const res = await api('POST', '/users/login', { login_id: user.username, password: user.password }); + const token = res.headers.get('Token'); + const me = await res.json(); + user.token = token; user.id = me.id; + await api('PUT', `/users/${me.id}/preferences`, [ + { user_id: me.id, category: 'theme', name: '', value: JSON.stringify(ONYX) }, + { user_id: me.id, category: 'tutorial_step', name: me.id, value: '999' }, + { user_id: me.id, category: 'onboarding_task_list', name: 'onboarding_task_list_show', value: 'false' }, + { user_id: me.id, category: 'recommended_next_steps', name: 'hide', value: 'true' }, + ], token).catch(e => console.warn(' preferences:', e.message)); + return token; +} + +async function newPage(browser, user, viewport = VIEWPORT) { + const ctx = await browser.newContext({ viewport, deviceScaleFactor: 2, colorScheme: 'dark' }); + await ctx.addInitScript(() => { + try { localStorage.setItem('__landingPageSeen__', 'true'); localStorage.setItem('onboarding_complete', 'true'); } catch {} + }); + const page = await ctx.newPage(); + await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' }); + for (const rx of [/view in browser/i, /continue in browser/i]) { + try { const l = page.getByText(rx).first(); if (await l.isVisible({ timeout: 1500 })) { await l.click(); break; } } catch {} + } + await page.waitForSelector('input[type="password"]', { timeout: 30000 }); + await page.locator('input[type="text"], input[type="email"]').first().fill(user.username); + await page.locator('input[type="password"]').first().fill(user.password); + await page.getByRole('button', { name: /log in|sign in/i }).first().click(); + await page.waitForURL(u => !/\/login/.test(new URL(u).pathname), { timeout: 20000 }); + await page.waitForLoadState('networkidle', { timeout: 15000 }).catch(() => {}); + return { ctx, page }; + +} + +async function dismissModals(page) { + for (const rx of [/view in browser/i, /no thanks/i, /skip/i, /got it/i, /not now/i]) { + for (const role of ['link', 'button']) { + try { + const el = page.getByRole(role, { name: rx }).first(); + if (await el.isVisible({ timeout: 300 })) { await el.click({ timeout: 1000 }); await page.waitForTimeout(300); } + } catch {} + } + } +} + +// The "Preview Mode: Email notifications have not been configured" banner is a stack detail. +// Dismiss it the way a person would, with its close button. +async function closeBanner(page) { + try { + const bar = page.locator('.announcement-bar').first(); + if (await bar.isVisible({ timeout: 1500 })) { + await bar.locator('.announcement-bar__close, .close, [aria-label="Close"]').first().click({ timeout: 2000 }); + await page.waitForTimeout(500); + } + } catch {} +} + +const maskTokenIds = page => page.evaluate(mask => { + // Overwrite anything shaped like a 26-character Mattermost ID inside a "Token ID" line. + const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT); + const hits = []; + while (walker.nextNode()) { + const n = walker.currentNode; + if (/^[a-z0-9]{26}$/.test(n.nodeValue.trim()) && /Token ID/i.test(n.parentElement?.parentElement?.textContent || '')) hits.push(n); + } + for (const n of hits) n.nodeValue = mask; + return hits.length; +}, MASK); + +const shot = async (page, name, clip) => { + await page.screenshot({ path: join(OUT, name), ...(clip ? { clip } : {}) }); + console.log(` ok ${name}`); +}; + +async function waitFor(fn, what) { + const start = Date.now(); + while (Date.now() - start < TIMEOUT_MS) { + const v = await fn(); + if (v) return v; + await new Promise(r => setTimeout(r, 2500)); + } + throw new Error(`timed out waiting for ${what}`); +} + +const browser = await chromium.launch(); +let failures = 0; +const attempt = async (name, fn) => { try { await fn(); } catch (e) { failures++; console.error(` FAIL ${name}: ${e.message}`); } }; + +await apiLogin(ADMIN); +await apiLogin(ALICE); +const channel = (await (await api('GET', `/teams/name/${TEAM}/channels/name/${CHANNEL}`, null, ALICE.token)).json()); + +if (steps.has('admin')) await attempt('admin', async () => { + const { ctx, page } = await newPage(browser, ADMIN); + await page.goto(`${BASE}/admin_console/integrations/bot_accounts`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(2000); + await dismissModals(page); + await closeBanner(page); + await shot(page, 'mattermost-bot-accounts.png', { x: 220, y: 44, width: 1060, height: 376 }); + await ctx.close(); +}); + +if (steps.has('bots')) await attempt('bots', async () => { + const { ctx, page } = await newPage(browser, ADMIN); + await page.goto(`${BASE}/${TEAM}/integrations/bots`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(2000); + await dismissModals(page); + await closeBanner(page); + console.log(` masked ${await maskTokenIds(page)} token id(s)`); + await shot(page, 'mattermost-create-bot.png', { x: 16, y: 76, width: 1240, height: 610 }); + await shot(page, 'mattermost-compose-bot-account.png', { x: 290, y: 372, width: 940, height: 150 }); + await ctx.close(); +}); + +const botUserId = async () => (await (await api('GET', '/users/username/netclaw', null, ALICE.token)).json()).id; + +async function postAsAlice(message) { + const res = await api('POST', '/posts', { channel_id: channel.id, message }, ALICE.token); + return (await res.json()); +} + +// Start each conversation from an empty channel so only that conversation is on screen. +async function clearChannel() { + const data = await (await api('GET', `/channels/${channel.id}/posts?per_page=200`, null, ADMIN.token)).json(); + for (const id of data.order) { + const p = data.posts[id]; + if (p.type === '' && !p.root_id) await api('DELETE', `/posts/${id}`, null, ADMIN.token).catch(() => {}); + } + await new Promise(r => setTimeout(r, 1000)); +} + +async function threadPosts(rootId) { + const data = await (await api('GET', `/posts/${rootId}/thread`, null, ALICE.token)).json(); + return data.order.map(id => data.posts[id]).sort((a, b) => a.create_at - b.create_at); +} + +async function openThread(page, rootId) { + await page.goto(`${BASE}/${TEAM}/pl/${rootId}`, { waitUntil: 'networkidle' }); + await page.waitForTimeout(2500); + await dismissModals(page); + await closeBanner(page); + // The permalink only highlights the post. Open the thread panel like a person would. + await page.getByRole('button', { name: /\d+ repl(y|ies)/i }).last().click(); + await page.waitForTimeout(2500); + await page.mouse.move(640, 790); +} + +if (steps.has('thread')) await attempt('thread', async () => { + const bot = await botUserId(); + await clearChannel(); + const root = await postAsAlice('@netclaw reply with a one-sentence greeting'); + await waitFor(async () => (await threadPosts(root.id)).some(p => p.user_id === bot && p.message), 'the bot reply'); + const { ctx, page } = await newPage(browser, ALICE); + await openThread(page, root.id); + await shot(page, 'mattermost-conversation.png'); + await shot(page, 'mattermost-compose-thread.png', { x: 270, y: 45, width: 965, height: 755 }); + await ctx.close(); +}); + +if (steps.has('approval')) await attempt('approval', async () => { + const bot = await botUserId(); + await clearChannel(); + const root = await postAsAlice(`@netclaw Use the shell to run exactly this, nothing else: git -C ${REPO_DIR} push origin main`); + const prompt = await waitFor(async () => (await threadPosts(root.id)).find(p => p.user_id === bot && /Tool approval required/i.test(p.message)), 'the approval prompt'); + console.log('--- prompt text ---\n' + prompt.message + '\n-------------------'); + const { ctx, page } = await newPage(browser, ALICE, { width: 1280, height: 900 }); + await openThread(page, root.id); + // Widen the thread panel so the command doesn't wrap. + await page.getByRole('button', { name: /expand/i }).first().click(); + await page.waitForTimeout(1500); + await page.mouse.move(640, 880); + const rhs = page.locator('#sidebar-right'); + const rb = await rhs.boundingBox(); + const posts = rhs.locator('.post'); + const fb = await posts.first().boundingBox(); + const lb = await posts.last().boundingBox(); + console.log('rhs', JSON.stringify(rb), 'first', JSON.stringify(fb), 'last', JSON.stringify(lb)); + await page.screenshot({ path: join(OUT, '_pending-full.png') }); + // Landing-page crop (public/assets/approval-prompt.png): just the pending prompt post. + await page.screenshot({ path: join(OUT, 'approval-prompt.png'), clip: { x: rb.x + 2, y: lb.y + 8, width: rb.width - 4, height: 270 } }); + console.log(' ok approval-prompt.png'); + // The guide's image: alice's request, the pending prompt with all six choices, and her + // one-letter reply typed in the thread box. A prompt resolves the instant the reply is + // sent (the post is edited in place), so this is the last frame that still shows the choices. + await page.locator('#reply_textbox').click(); + await page.keyboard.type('A'); + await page.waitForTimeout(400); + const y0 = fb.y - 24; + await page.screenshot({ path: join(OUT, 'mattermost-compose-approval.png'), clip: { x: rb.x + 2, y: y0, width: rb.width - 4, height: rb.y + rb.height - 22 - y0 } }); + console.log(' ok mattermost-compose-approval.png'); + await page.keyboard.press('Enter'); + await waitFor(async () => { + const posts = await threadPosts(root.id); + const last = posts[posts.length - 1]; + return posts.some(p => /Tool approval resolved/i.test(p.message)) && last.user_id === bot && !/Tool approval/i.test(last.message); + }, 'the approval to resolve and the bot to finish'); + await page.waitForTimeout(2500); + await page.mouse.move(640, 880); + const top = (await posts.first().boundingBox()).y - 24; + const boxTop = (await page.locator('#reply_textbox').boundingBox()).y; + await page.screenshot({ path: join(OUT, 'mattermost-compose-approval-resolved.png'), clip: { x: rb.x + 2, y: top, width: rb.width - 4, height: boxTop - 8 - top } }); + console.log(' ok mattermost-compose-approval-resolved.png'); + await ctx.close(); +}); + +await browser.close(); +if (failures) { console.error(`${failures} step(s) failed`); process.exit(1); } +console.log('Done.'); diff --git a/screenshots/mattermost/capture.mjs b/screenshots/mattermost/capture.mjs index f042351..59fd316 100644 --- a/screenshots/mattermost/capture.mjs +++ b/screenshots/mattermost/capture.mjs @@ -1,10 +1,12 @@ #!/usr/bin/env node // Drives Playwright against the bootstrapped Mattermost to capture the // channels/mattermost.md screenshots: the System Console settings, the bot -// account, and a live netclaw conversation. Reads .bootstrap.json. +// account, and a live netclaw conversation. Runs in the Playwright container (run.sh). // -// Each shot is independent (try/catch) so one failure doesn't lose the rest. +// Each shot is independent (try/catch) so one failure doesn't lose the rest; the +// process exits non-zero if any shot failed. // Usage: node capture.mjs +// Env: BOOTSTRAP_FILE (default ./.bootstrap.json), OUT_DIR (default ../output) import { chromium } from 'playwright'; import { readFileSync, mkdirSync } from 'fs'; @@ -12,10 +14,11 @@ import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; const __dirname = dirname(fileURLToPath(import.meta.url)); -const boot = JSON.parse(readFileSync(join(__dirname, '.bootstrap.json'), 'utf8')); -const OUT = join(__dirname, '..', 'output'); +const boot = JSON.parse(readFileSync(process.env.BOOTSTRAP_FILE || join(__dirname, '.bootstrap.json'), 'utf8')); +const OUT = process.env.OUT_DIR || join(__dirname, '..', 'output'); mkdirSync(OUT, { recursive: true }); const BASE = boot.serverUrl; +let failures = 0; const shot = (page, name) => page.screenshot({ path: join(OUT, name) }).then(() => console.log(` ✓ ${name}`)); @@ -86,7 +89,7 @@ try { await dismissModals(page); await shot(page, 'mattermost-create-bot.png'); await ctx.close(); -} catch (e) { console.error(' ✗ admin shots:', e.message); } +} catch (e) { failures++; console.error(' ✗ admin shots:', e.message); } // 3: live conversation as the test user try { @@ -118,7 +121,8 @@ try { await page.waitForTimeout(1000); await shot(page, 'mattermost-conversation.png'); await ctx.close(); -} catch (e) { console.error(' ✗ conversation shot:', e.message); } +} catch (e) { failures++; console.error(' ✗ conversation shot:', e.message); } await browser.close(); +if (failures) { console.error(`${failures} capture step(s) failed`); process.exit(1); } console.log('Done.'); diff --git a/screenshots/mattermost/docker-compose.mattermost.yml b/screenshots/mattermost/docker-compose.mattermost.yml index 2769691..963e5db 100644 --- a/screenshots/mattermost/docker-compose.mattermost.yml +++ b/screenshots/mattermost/docker-compose.mattermost.yml @@ -1,24 +1,106 @@ -# Mattermost server for capturing the channels/mattermost.md screenshots. -# Brought up by run.sh, which then bootstraps it (bootstrap.mjs), starts the -# pinned netclaw image against it, and drives Playwright (capture.mjs). +# Mattermost capture topology. Everything runs in containers on one compose network; +# nothing is published to the host and there are no fixed container names, so projects +# do not collide. Driven by run.sh (do not run directly). # -# mattermost-preview is an all-in-one testing image (bundled Postgres). amd64-only. +# mattermost all-in-one testing image (bundled Postgres, amd64 only) +# netclawd the released daemon; reaches Mattermost at http://mattermost:8065 +# playwright bootstrap + browser captures, reaching Mattermost by the same name +# +# Models use the 0.25+ Models.Definitions / Models.Roles env shape. The bot token is +# NOT an env var: run.sh stores it with `netclaw secrets set Mattermost.BotToken`. +# run.sh sets Mattermost.CallbackUrl to http://netclawd:5299/api/mattermost/actions (a forwarder +# sidecar, because the daemon listens on loopback only) so approvals +# render as buttons. Mattermost refuses integration callbacks to private addresses, so it +# allow-lists the netclawd host below. services: mattermost: - image: mattermost/mattermost-preview:latest - ports: - - "8065:8065" + image: ${MATTERMOST_IMAGE:-mattermost/mattermost-preview:11.7.7} environment: - MM_SERVICESETTINGS_SITEURL: http://localhost:8065 + MM_SERVICESETTINGS_SITEURL: http://mattermost:8065 MM_TEAMSETTINGS_ENABLEOPENSERVER: "true" MM_SERVICESETTINGS_ENABLETESTING: "true" - # NOTE: EnableBotAccountCreation / EnableUserAccessTokens are intentionally - # NOT set here. bootstrap.mjs flips them via the System Console config API so - # the captured screenshot shows them as editable toggles (an env var would - # render them locked: "set through an environment variable"). + # Button callbacks go to the daemon on the private compose network; Mattermost blocks that + # unless the host is listed. + MM_SERVICESETTINGS_ALLOWEDUNTRUSTEDINTERNALCONNECTIONS: netclawd + # EnableBotAccountCreation / EnableUserAccessTokens are intentionally NOT set here. + # bootstrap.mjs flips them through the config API so the captured System Console + # page shows editable toggles (an env var would render them locked). + # No healthcheck: the 11.x preview image ships neither curl nor wget. bootstrap.mjs + # polls /api/v4/system/ping until the server is ready. + + netclawd: + image: ${NETCLAW_IMAGE:?set by run.sh} + depends_on: + - mattermost + environment: + NETCLAW_Daemon__DisableSelfUpdate: "true" + NETCLAW_Providers__inference__Type: ${NC_INFERENCE_TYPE:?set by run.sh} + NETCLAW_Providers__inference__Endpoint: ${NC_INFERENCE_ENDPOINT:?set by run.sh} + NETCLAW_Models__Definitions__main__Provider: inference + NETCLAW_Models__Definitions__main__ModelId: ${NC_INFERENCE_MODEL:?set by run.sh} + NETCLAW_Models__Roles__Main: main + NETCLAW_Models__Roles__Fallback: main + NETCLAW_Models__Roles__Compaction: main + # Real inference endpoints can be slow to first token. + NETCLAW_Session__TurnLlmTimeoutSeconds: "600" + NETCLAW_Session__FirstTokenTimeoutSeconds: "600" + NETCLAW_Session__PrefillTimeoutSeconds: "600" + volumes: + - netclaw-home:/home/netclaw/.netclaw healthcheck: - test: ["CMD", "curl", "-sf", "http://localhost:8065/api/v4/system/ping"] - interval: 5s + test: ["CMD", "curl", "-sf", "http://127.0.0.1:5199/api/health/ready"] + interval: 3s timeout: 5s - retries: 48 - start_period: 20s + retries: 40 + start_period: 5s + + # Shares netclawd's network namespace so it can reach the loopback-only daemon listener and + # re-expose it on :5299 for Mattermost's button callbacks. + callbackfwd: + image: netclaw-docs-playwright:${PLAYWRIGHT_VERSION:?set by run.sh} + network_mode: service:netclawd + user: "${HOST_UID:-1000}:${HOST_GID:-1000}" + command: ["node", "/work/tcp-forward.mjs"] + depends_on: + - netclawd + volumes: + - .:/work:ro + + # Only started by run.sh with SCRIPTED_APPROVAL=1: answers with one scripted shell_execute call (no real model). + scriptedllm: + profiles: ["scripted"] + image: netclaw-docs-playwright:${PLAYWRIGHT_VERSION:?set by run.sh} + user: "${HOST_UID:-1000}:${HOST_GID:-1000}" + command: ["node", "/work/scripted-llm.mjs"] + environment: + SCRIPTED_TOOL: ${SCRIPTED_TOOL:-mcp} + SCRIPTED_COMMAND: ${SCRIPTED_COMMAND:-git push origin main} + volumes: + - .:/work:ro + + # The stack's demo MCP server (screenshots/mcp-server); approval.sh-style runs register it + # with netclawd and gate http_fetch behind approval. + mcp-demo: + profiles: ["scripted"] + image: netclaw-docs-mcp-demo:1 + build: + context: ../mcp-server + + playwright: + profiles: ["tools"] + image: netclaw-docs-playwright:${PLAYWRIGHT_VERSION:?set by run.sh} + build: + context: ../playwright + args: + PLAYWRIGHT_VERSION: ${PLAYWRIGHT_VERSION} + # Run as the invoking user so files written to the mounts are yours, not root's. + user: "${HOST_UID:-1000}:${HOST_GID:-1000}" + environment: + BOOTSTRAP_FILE: /work/.bootstrap.${COMPOSE_PROJECT_NAME}.json + OUT_DIR: /out + volumes: + - .:/work + - ${CAPTURE_OUT_DIR:?set by run.sh}:/out + +volumes: + netclaw-home: diff --git a/screenshots/mattermost/frame-approval.mjs b/screenshots/mattermost/frame-approval.mjs new file mode 100644 index 0000000..2796afc --- /dev/null +++ b/screenshots/mattermost/frame-approval.mjs @@ -0,0 +1,34 @@ +#!/usr/bin/env node +// Frames a raw crop of the approval prompt the way the landing page shows it: a rounded dark +// card with padding and a soft purple glow on a near-black background (colours sampled from the +// original Slack-era image). Output is a PNG at 2x. +// +// Library: import { frame } from './frame-approval.mjs'; await frame(browser, rawPath, outPath, { bg }) +// CLI: node frame-approval.mjs raw.png out.png [cardBackgroundCss] +import { readFileSync } from 'fs'; +import { fileURLToPath } from 'url'; + +export async function frame(browser, rawPath, outPath, { cardBg = '#1f1f1f', dsf = 2 } = {}) { + const b64 = readFileSync(rawPath).toString('base64'); + const ctx = await browser.newContext({ deviceScaleFactor: dsf, viewport: { width: 1400, height: 900 } }); + const page = await ctx.newPage(); + await page.setContent(`

`); + await page.waitForFunction(() => document.getElementById('shot').complete); + // Show the raw capture at its CSS size (it was taken at the same scale factor). + await page.evaluate(d => { const i = document.getElementById('shot'); i.style.width = (i.naturalWidth / d) + 'px'; }, dsf); + await page.locator('#stage').screenshot({ path: outPath }); + await ctx.close(); +} + +if (process.argv[1] === fileURLToPath(import.meta.url)) { + const { chromium } = await import('playwright'); + const browser = await chromium.launch(); + await frame(browser, process.argv[2], process.argv[3], process.argv[4] ? { cardBg: process.argv[4] } : {}); + await browser.close(); +} diff --git a/screenshots/mattermost/post-message.mjs b/screenshots/mattermost/post-message.mjs new file mode 100644 index 0000000..c985103 --- /dev/null +++ b/screenshots/mattermost/post-message.mjs @@ -0,0 +1,44 @@ +#!/usr/bin/env node +// Posts an @netclaw message as the test user, then waits until the bot replies. +// Exits non-zero if no reply arrives in time (the daemon or inference is broken). +// Runs in the Playwright container (run.sh). +// +// Usage: node post-message.mjs ["message text"] +// Env: BOOTSTRAP_FILE (default ./.bootstrap.json), REPLY_TIMEOUT_SECONDS (default 300) + +import { readFileSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const boot = JSON.parse(readFileSync(process.env.BOOTSTRAP_FILE || join(__dirname, '.bootstrap.json'), 'utf8')); +const MESSAGE = process.argv[2] || '@netclaw hey! In one sentence, what are you and what can you help with?'; +const TIMEOUT_MS = Number(process.env.REPLY_TIMEOUT_SECONDS || 300) * 1000; + +async function api(method, path, body, token) { + const res = await fetch(`${boot.serverUrl}${path}`, { + method, + headers: { 'Content-Type': 'application/json', ...(token ? { Authorization: `Bearer ${token}` } : {}) }, + body: body ? JSON.stringify(body) : undefined, + }); + if (!res.ok) throw new Error(`${method} ${path} -> ${res.status}: ${(await res.text()).slice(0, 200)}`); + return res; +} + +const login = await api('POST', '/api/v4/users/login', { login_id: boot.testuser.username, password: boot.testuser.password }); +const userToken = login.headers.get('Token'); +await api('POST', '/api/v4/posts', { channel_id: boot.channel.id, message: MESSAGE }, userToken); +console.log(`posted: ${MESSAGE}`); + +const start = Date.now(); +while (Date.now() - start < TIMEOUT_MS) { + const data = await (await api('GET', `/api/v4/channels/${boot.channel.id}/posts`, null, boot.bot.token)).json(); + const reply = data.order.map(id => data.posts[id]).find(p => p.user_id === boot.bot.userId && p.root_id && p.message); + if (reply) { + console.log(`bot replied after ${Math.round((Date.now() - start) / 1000)}s: ${reply.message.slice(0, 200).replace(/\s+/g, ' ')}`); + process.exit(0); + } + await new Promise(r => setTimeout(r, 3000)); +} +console.error(`no bot reply within ${TIMEOUT_MS / 1000}s`); +process.exit(1); diff --git a/screenshots/mattermost/run.sh b/screenshots/mattermost/run.sh index e7c8d89..3a89804 100755 --- a/screenshots/mattermost/run.sh +++ b/screenshots/mattermost/run.sh @@ -1,83 +1,137 @@ #!/bin/bash -# Orchestrates the Mattermost screenshot capture end to end: -# 1. brings up the Mattermost container -# 2. bootstraps it (admin/team/bot/token/channel/test user) -> .bootstrap.json -# 3. starts the pinned netclaw daemon against it, with inference pointed at -# an OpenAI-compatible endpoint (spark2 by default) -# 4. posts an @netclaw message as the test user so the bot replies -# 5. drives Playwright (capture.mjs) to shoot the System Console + live thread -# 6. tears everything down +# Regenerate the Mattermost screenshots, entirely in containers (Docker is the only +# prerequisite): Mattermost, the released netclaw daemon and Playwright share one +# compose network. Needs real inference so the bot can answer: # -# The daemon uses `--network host` so its loopback control plane is reachable -# from the host, and Mattermost is reached at http://localhost:8065. +# INFERENCE_BASE_URL=http://llm.example.internal:8080 INFERENCE_MODEL_ID= \ +# ./screenshots/mattermost/run.sh # -# Overridable env: -# NETCLAW_IMAGE default ghcr.io/netclaw-dev/netclaw:0.22.1 -# SPARK_ENDPOINT OpenAI-compatible inference endpoint (default spark2) -# SPARK_MODEL_ID model id served there (matches the local netclaw instance) +# Landing-page approval prompt (public/assets/approval-prompt.png) needs no model: set +# SCRIPTED_APPROVAL=1 and a scripted endpoint (scripted-llm.mjs) requests one demo-utilities http_fetch call +# (SCRIPTED_TOOL=shell for a shell_execute call instead); +# the run then captures the prompt with its buttons, framed by frame-approval.mjs, into OUT_DIR. +# +# (or put INFERENCE_UPSTREAM and INFERENCE_UPSTREAM_MODEL in screenshots/.env, gitignored). Other knobs: +# COMPOSE_PROJECT_NAME prefix for this run; "-mm" is appended (default netclaw-shots-mm) +# NETCLAW_VERSION / NETCLAW_IMAGE daemon image (default: src/version.json) +# OUT_DIR where PNGs go (default screenshots/output) +# REPLY_TIMEOUT_SECONDS how long to wait for the bot's reply (default 300) +# +# Steps: compose up Mattermost -> bootstrap (admin, team, bot, channel, test user) -> +# start netclawd -> store the bot token as an encrypted secret and enable the Mattermost +# channel -> post an @netclaw message as the test user and wait for the reply -> +# Playwright captures -> tear everything down. Exits non-zero on any failure. set -euo pipefail -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -cd "$SCRIPT_DIR" +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +COMPOSE_PROJECT_NAME="${COMPOSE_PROJECT_NAME:-netclaw-shots}-mm" +export COMPOSE_PROJECT_NAME +# shellcheck source=../lib.sh +. "$HERE/../lib.sh" + +if [ -n "${SCRIPTED_APPROVAL:-}" ]; then + export NC_INFERENCE_TYPE=openai-compatible NC_INFERENCE_ENDPOINT=http://scriptedllm:8080/v1 NC_INFERENCE_MODEL=demo-model +elif [ "$INFERENCE_MODE" = relay ]; then + # This flow runs on its own compose network without the forwarder, and no frame shows the + # daemon's configuration, so the daemon talks to the upstream directly. + export NC_INFERENCE_ENDPOINT="$INFERENCE_UPSTREAM" NC_INFERENCE_MODEL="$INFERENCE_UPSTREAM_MODEL" +elif [ "$INFERENCE_MODE" != external ]; then + echo "mattermost: set INFERENCE_UPSTREAM (or INFERENCE_BASE_URL and INFERENCE_MODEL_ID); the bot needs a real model to answer" >&2 + exit 2 +fi -NETCLAW_IMAGE="${NETCLAW_IMAGE:-ghcr.io/netclaw-dev/netclaw:0.22.1}" -SPARK_ENDPOINT="${SPARK_ENDPOINT:-https://spark2.testlab.petabridge.net}" -SPARK_MODEL_ID="${SPARK_MODEL_ID:-Qwen/Qwen3.6-35B-A3B-FP8}" -SERVER="http://localhost:8065" +OUT_DIR="${OUT_DIR:-$SHOTS_DIR/output}" +mkdir -p "$OUT_DIR" +export CAPTURE_OUT_DIR="$(cd "$OUT_DIR" && pwd)" +BOOTSTRAP=".bootstrap.${COMPOSE_PROJECT_NAME}.json" + +mm() { docker compose -f "$HERE/docker-compose.mattermost.yml" "$@"; } +pw() { mm run --rm --no-deps -T playwright "$@"; } +nc() { mm exec -T -u netclaw netclawd netclaw "$@"; } cleanup() { - echo "==> Tearing down" - docker rm -f netclaw-mm-shot >/dev/null 2>&1 || true - docker compose -f docker-compose.mattermost.yml down -v >/dev/null 2>&1 || true - rm -f .bootstrap.json + if [ -n "${KEEP_STACK:-}" ]; then echo "==> KEEP_STACK set: leaving project $COMPOSE_PROJECT_NAME running"; return; fi + echo "==> Tearing down project $COMPOSE_PROJECT_NAME" + mm --profile scripted --profile tools down -v --remove-orphans >/dev/null 2>&1 || true + rm -f "$HERE/$BOOTSTRAP" } trap cleanup EXIT -echo "==> Bringing up Mattermost" -docker compose -f docker-compose.mattermost.yml up -d +echo "==> Bringing up Mattermost (${MATTERMOST_IMAGE:-mattermost/mattermost-preview:11.7.7}) and building the Playwright image ($PLAYWRIGHT_VERSION)" +mm up -d mattermost +mm build playwright +if [ -n "${SCRIPTED_APPROVAL:-}" ]; then + export SCRIPTED_TOOL="${SCRIPTED_TOOL:-mcp}" # mcp (default) or shell + mm --profile scripted up -d --build scriptedllm mcp-demo +fi echo "==> Bootstrapping Mattermost" -node bootstrap.mjs "$SERVER" +pw node /work/bootstrap.mjs http://mattermost:8065 + +BOT_TOKEN="$(pw node -p "require('/work/$BOOTSTRAP').bot.token")" +CHANNEL_ID="$(pw node -p "require('/work/$BOOTSTRAP').channel.id")" -BOT_TOKEN="$(node -e "process.stdout.write(require('./.bootstrap.json').bot.token)")" -CHANNEL_ID="$(node -e "process.stdout.write(require('./.bootstrap.json').channel.id)")" +echo "==> Starting netclawd ($NETCLAW_IMAGE), models via NETCLAW_Models__Definitions/Roles env" +mm up -d --wait netclawd +mm up -d callbackfwd -echo "==> Starting netclaw ($NETCLAW_IMAGE) against Mattermost + $SPARK_ENDPOINT" -docker rm -f netclaw-mm-shot >/dev/null 2>&1 || true -docker run -d --name netclaw-mm-shot --network host \ - -e NETCLAW_Daemon__Host=127.0.0.1 -e NETCLAW_Daemon__Port=5299 \ - -e NETCLAW_Daemon__ExposureMode=local -e NETCLAW_Daemon__DisableSelfUpdate=true \ - -e NETCLAW_Session__TurnLlmTimeoutSeconds=600 \ - -e NETCLAW_Session__FirstTokenTimeoutSeconds=600 \ - -e NETCLAW_Session__PrefillTimeoutSeconds=600 \ - -e NETCLAW_Mattermost__Enabled=true \ - -e NETCLAW_Mattermost__ServerUrl="$SERVER" \ - -e NETCLAW_Mattermost__BotToken="$BOT_TOKEN" \ - -e NETCLAW_Mattermost__DefaultChannelId="$CHANNEL_ID" \ - -e NETCLAW_Mattermost__MentionOnly=true \ - -e "NETCLAW_Mattermost__ChannelAudiences__${CHANNEL_ID}=team" \ - -e NETCLAW_Providers__spark__Type=openai-compatible \ - -e NETCLAW_Providers__spark__Endpoint="$SPARK_ENDPOINT" \ - -e NETCLAW_Models__Main__Provider=spark -e NETCLAW_Models__Main__ModelId="$SPARK_MODEL_ID" \ - -e NETCLAW_Models__Fallback__Provider=spark -e NETCLAW_Models__Fallback__ModelId="$SPARK_MODEL_ID" \ - -e NETCLAW_Models__Compaction__Provider=spark -e NETCLAW_Models__Compaction__ModelId="$SPARK_MODEL_ID" \ - "$NETCLAW_IMAGE" +echo "==> Storing the bot token as an encrypted secret and enabling the Mattermost channel" +nc secrets set Mattermost.BotToken "$BOT_TOKEN" >/dev/null +# The scripted approval run needs shell_execute, which the "team" audience doesn't get. +AUDIENCE="${CHANNEL_AUDIENCE:-$([ -n "${SCRIPTED_APPROVAL:-}" ] && echo personal || echo team)}" +mm exec -T -u netclaw -e SERVER=http://mattermost:8065 -e CH="$CHANNEL_ID" -e AUD="$AUDIENCE" netclawd sh -c ' + f=/home/netclaw/.netclaw/config/netclaw.json + [ -f "$f" ] || echo "{}" > "$f" + jq --arg s "$SERVER" --arg c "$CH" ".configVersion = 1 + | .Security = {DeploymentPosture: \"Personal\", ShellExecutionMode: \"HostAllowed\", StrictDefaults: true} + | .Mattermost = {Enabled: true, ServerUrl: \$s, CallbackUrl: \"http://netclawd:5299/api/mattermost/actions\", DefaultChannelId: \$c, MentionOnly: true, ChannelAudiences: {(\$c): \$a}}" --arg a "$AUD" \ + "$f" > "$f.tmp" && mv "$f.tmp" "$f"' -echo "==> Waiting for daemon health" -for i in $(seq 1 30); do - curl -sf http://127.0.0.1:5299/api/health/ready >/dev/null 2>&1 && break +echo "==> Waiting for the Mattermost channel to connect" +connected=0 +for _ in $(seq 1 45); do + if mm exec -T -u netclaw netclawd sh -c 'cat /home/netclaw/.netclaw/logs/daemon-*.log' 2>/dev/null \ + | grep -i 'mattermost' | grep -iE 'connected|started' | tail -n 3 | grep -q .; then connected=1; break; fi sleep 2 done +if [ "$connected" -ne 1 ]; then + echo "mattermost: netclawd never reported a Mattermost connection" >&2 + mm exec -T -u netclaw netclawd sh -c 'grep -i mattermost /home/netclaw/.netclaw/logs/daemon-*.log | tail -n 20' >&2 || true + exit 1 +fi +mm exec -T -u netclaw netclawd sh -c 'grep -i mattermost /home/netclaw/.netclaw/logs/daemon-*.log | tail -n 5' + +if [ -n "${SCRIPTED_APPROVAL:-}" ] && [ "$SCRIPTED_TOOL" = mcp ]; then + echo "==> Registering the demo MCP server and gating its tools behind approval" + nc mcp add --transport http demo-utilities http://mcp-demo:8080/mcp >/dev/null + mm exec -T -u netclaw netclawd sh -c ' + f=/home/netclaw/.netclaw/config/netclaw.json + jq ".Tools.AudienceProfiles.Personal.ApprovalPolicy.McpServerDefaults[\"demo-utilities\"] = \"Approval\"" "$f" > "$f.tmp" && mv "$f.tmp" "$f"' + sleep 12 # the daemon restarts itself when netclaw.json changes + for _ in $(seq 1 30); do nc status 2>/dev/null | grep -q 'mcp:demo-utilities: healthy' && break; sleep 2; done +fi +if [ -n "${SCRIPTED_APPROVAL:-}" ]; then + # A Git repository for the shell call to run in, so the prompt looks like real project work. + mm exec -T -u netclaw netclawd sh -c ' + mkdir -p ~/projects/website && cd ~/projects/website && git init -q -b main . \ + && git config user.name alice && git config user.email alice@example.com \ + && echo "# website" > README.md && git add . && git commit -q -m "Initial commit" \ + && git remote add origin git@example.com:acme/website.git' + echo "==> Capturing the approval prompt" + pw node /work/approval.mjs || { + echo "==> approval capture failed; scripted endpoint and daemon logs follow" >&2 + mm logs --tail 20 scriptedllm >&2 || true + mm exec -T -u netclaw netclawd sh -c 'tail -n 40 /home/netclaw/.netclaw/logs/daemon-*.log' >&2 || true + exit 1 + } + echo "==> Done. approval-prompt.png written to $CAPTURE_OUT_DIR" + exit 0 +fi -echo "==> Posting an @netclaw message as the test user" -TOKEN="$(curl -s -i -X POST "$SERVER/api/v4/users/login" -H 'Content-Type: application/json' \ - -d '{"login_id":"testuser","password":"TestUser1234!"}' | grep -i '^token:' | awk '{print $2}' | tr -d '\r')" -curl -s -X POST "$SERVER/api/v4/posts" -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \ - -d "{\"channel_id\":\"$CHANNEL_ID\",\"message\":\"@netclaw hey! In one sentence, what are you and what can you help with?\"}" >/dev/null -echo " posted; waiting for the bot to reply" -sleep 25 +echo "==> Posting an @netclaw message and waiting for the reply" +pw node /work/post-message.mjs echo "==> Capturing screenshots (Playwright)" -node capture.mjs +pw node /work/capture.mjs -echo "==> Done. Shots written to ../output/mattermost-*.png" +echo "==> Done. Shots written to $CAPTURE_OUT_DIR/mattermost-*.png" diff --git a/screenshots/mattermost/scripted-llm.mjs b/screenshots/mattermost/scripted-llm.mjs new file mode 100644 index 0000000..550765d --- /dev/null +++ b/screenshots/mattermost/scripted-llm.mjs @@ -0,0 +1,59 @@ +#!/usr/bin/env node +// A tiny OpenAI-compatible endpoint that answers every first turn with one scripted +// shell_execute tool call, so the approval prompt shows up without a real model. +// Runs in the Playwright image (approval.sh). After the tool result it replies "Done." +// +// Env: PORT (8080), SCRIPTED_TOOL (mcp|shell, default mcp), SCRIPTED_COMMAND (shell command), +// SCRIPTED_MCP_URL (URL the MCP call fetches) +import { createServer } from 'http'; + +const COMMAND = process.env.SCRIPTED_COMMAND || 'git push origin main'; +const KIND = process.env.SCRIPTED_TOOL || 'mcp'; +const MCP_URL = process.env.SCRIPTED_MCP_URL || 'https://status.example.com/api/v1/incidents'; +const PORT = Number(process.env.PORT || 8080); +const id = () => 'chatcmpl-' + Math.random().toString(36).slice(2, 10); + +function completion(body) { + const tools = body.tools || []; + const shell = KIND === 'shell' && tools.find(t => (t.function?.name || t.name) === 'shell_execute'); + const mcp = KIND === 'mcp' && tools.find(t => /http_fetch/.test(t.function?.name || t.name || '')); + const answered = (body.messages || []).some(m => m.role === 'tool'); + const calledMcp = (body.messages || []).some(m => (m.tool_calls || []).some(c => /http_fetch/.test(c.function?.name || ''))); + const loader = KIND === 'mcp' && !mcp && !answered && tools.find(t => (t.function?.name || t.name) === 'load_tool'); + if (loader) { + // MCP tools are deferred: the first turn loads the tool, the next one calls it. + return { message: { role: 'assistant', content: null, tool_calls: [{ id: 'call_ap0', type: 'function', function: { name: 'load_tool', arguments: JSON.stringify({ name: 'demo-utilities/http_fetch', _rationale: 'Load the HTTP fetch tool.' }) } }] }, finish: 'tool_calls' }; + } + if (mcp && !calledMcp) { + const name = mcp.function?.name || mcp.name; + return { message: { role: 'assistant', content: null, tool_calls: [{ id: 'call_ap1', type: 'function', function: { name, arguments: JSON.stringify({ url: MCP_URL, method: 'POST', _rationale: 'Open a status page incident for the website deploy.' }) } }] }, finish: 'tool_calls' }; + } + if (shell && !answered) { + return { message: { role: 'assistant', content: null, tool_calls: [{ id: 'call_ap1', type: 'function', function: { name: 'shell_execute', arguments: JSON.stringify({ command: COMMAND, _rationale: 'Push the pending website changes to origin.' }) } }] }, finish: 'tool_calls' }; + } + return { message: { role: 'assistant', content: 'Done.' }, finish: 'stop' }; +} + +createServer((req, res) => { + let raw = ''; + req.on('data', c => (raw += c)); + req.on('end', () => { + if (req.method === 'GET') { res.setHeader('Content-Type', 'application/json'); return res.end(JSON.stringify({ object: 'list', data: [{ id: 'demo-model', object: 'model' }] })); } + let body = {}; + try { body = JSON.parse(raw || '{}'); } catch {} + const { message, finish } = completion(body); + console.log(`${req.method} ${req.url} stream=${!!body.stream} -> ${finish}`); + const base = { id: id(), object: 'chat.completion', created: Math.floor(Date.now() / 1000), model: 'demo-model' }; + if (body.stream) { + res.writeHead(200, { 'Content-Type': 'text/event-stream' }); + const delta = { ...message }; + if (delta.tool_calls) delta.tool_calls = delta.tool_calls.map((t, index) => ({ index, ...t })); + res.write(`data: ${JSON.stringify({ ...base, object: 'chat.completion.chunk', choices: [{ index: 0, delta, finish_reason: null }] })}\n\n`); + res.write(`data: ${JSON.stringify({ ...base, object: 'chat.completion.chunk', choices: [{ index: 0, delta: {}, finish_reason: finish }], usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 } })}\n\n`); + res.end('data: [DONE]\n\n'); + } else { + res.setHeader('Content-Type', 'application/json'); + res.end(JSON.stringify({ ...base, choices: [{ index: 0, message, finish_reason: finish }], usage: { prompt_tokens: 10, completion_tokens: 10, total_tokens: 20 } })); + } + }); +}).listen(PORT, () => console.log(`scripted llm on :${PORT}`)); diff --git a/screenshots/mattermost/tcp-forward.mjs b/screenshots/mattermost/tcp-forward.mjs new file mode 100644 index 0000000..491e148 --- /dev/null +++ b/screenshots/mattermost/tcp-forward.mjs @@ -0,0 +1,14 @@ +#!/usr/bin/env node +// Forwards 0.0.0.0:LISTEN to 127.0.0.1:TARGET inside the netclawd network namespace. +// The daemon's default `local` exposure mode binds loopback only, so Mattermost (another +// container) can't reach /api/mattermost/actions directly. This sidecar is the "forwarder" +// the Mattermost channel docs describe; the daemon still sees loopback traffic. +// Env: LISTEN (5299), TARGET (5199) +import net from 'net'; +const LISTEN = Number(process.env.LISTEN || 5299), TARGET = Number(process.env.TARGET || 5199); +net.createServer(c => { + const u = net.connect(TARGET, '127.0.0.1'); + c.pipe(u).pipe(c); + const end = () => { c.destroy(); u.destroy(); }; + c.on('error', end); u.on('error', end); +}).listen(LISTEN, '0.0.0.0', () => console.log(`forwarding :${LISTEN} -> 127.0.0.1:${TARGET}`)); diff --git a/screenshots/mcp-server/Dockerfile b/screenshots/mcp-server/Dockerfile index 76ae002..e60adb3 100644 --- a/screenshots/mcp-server/Dockerfile +++ b/screenshots/mcp-server/Dockerfile @@ -1,6 +1,7 @@ FROM python:3.12-slim WORKDIR /app -RUN pip install --no-cache-dir "mcp[cli]" +# server.py uses the 1.x FastMCP API; mcp 2.x renamed it. +RUN pip install --no-cache-dir "mcp[cli]<2" COPY server.py . EXPOSE 8080 CMD ["python", "server.py"] diff --git a/screenshots/mcp-server/Dockerfile.oauth b/screenshots/mcp-server/Dockerfile.oauth new file mode 100644 index 0000000..60c30f0 --- /dev/null +++ b/screenshots/mcp-server/Dockerfile.oauth @@ -0,0 +1,6 @@ +FROM python:3.12-slim +WORKDIR /app +RUN pip install --no-cache-dir "mcp[cli]<2" +COPY oauth_server.py . +EXPOSE 8080 +CMD ["python", "oauth_server.py"] diff --git a/screenshots/mcp-server/oauth_server.py b/screenshots/mcp-server/oauth_server.py new file mode 100644 index 0000000..64fc900 --- /dev/null +++ b/screenshots/mcp-server/oauth_server.py @@ -0,0 +1,246 @@ +"""OAuth-protected demo MCP server for testing `netclaw mcp auth`. + +One process plays both roles: the MCP resource server at /mcp and a minimal +OAuth 2.1 authorization server (authorization code + PKCE, refresh tokens with +rotation). The authorize endpoint auto-approves, so a script can follow the +redirect with curl instead of a browser. + +MODE (environment variable): + confidential Pre-registered client only. No dynamic registration. The token + endpoint rejects a wrong or missing client secret. + public Dynamic client registration (RFC 7591), public client, PKCE only. + anonymous Like public, but initialize and tools/list work without a token. + Only tools/call returns 401 (models servers that allow anonymous + catalog discovery). + +Other settings: BASE_URL (public URL of this server), CLIENT_ID, CLIENT_SECRET, +TOKEN_TTL (access token lifetime in seconds, default 3600). +""" + +import base64 +import hashlib +import json +import os +import secrets +import time +from urllib.parse import urlencode + +import uvicorn +from mcp.server.fastmcp import FastMCP +from starlette.requests import Request +from starlette.responses import JSONResponse, RedirectResponse, Response + +MODE = os.environ.get("MODE", "confidential") +BASE_URL = os.environ.get("BASE_URL", "http://localhost:8080").rstrip("/") +CLIENT_ID = os.environ.get("CLIENT_ID", "demo-client") +CLIENT_SECRET = os.environ.get("CLIENT_SECRET", "demo-secret") +TOKEN_TTL = int(os.environ.get("TOKEN_TTL", "3600")) + +mcp = FastMCP("oauth-demo", stateless_http=True, host="0.0.0.0", port=8080) + + +@mcp.tool() +def whoami() -> str: + """Return the identity this server associates with the access token.""" + return "demo-user" + + +@mcp.tool() +def generate_uuid() -> str: + """Generate a random UUID v4.""" + import uuid + return str(uuid.uuid4()) + + +@mcp.prompt() +def review_checklist(topic: str) -> str: + """Walk through a short review checklist for a topic.""" + return f"Review {topic}: list the risks, the owner of each risk, and the next action." + + +codes: dict[str, dict] = {} +access_tokens: dict[str, float] = {} +refresh_tokens: dict[str, dict] = {} +dynamic_clients: dict[str, dict] = {} + + +def log(msg: str) -> None: + print(f"[oauth-demo] {msg}", flush=True) + + +def metadata_url() -> str: + return f"{BASE_URL}/.well-known/oauth-protected-resource/mcp" + + +@mcp.custom_route("/.well-known/oauth-protected-resource/mcp", ["GET"]) +@mcp.custom_route("/.well-known/oauth-protected-resource", ["GET"]) +async def protected_resource(_: Request) -> Response: + return JSONResponse({ + "resource": f"{BASE_URL}/mcp", + "authorization_servers": [BASE_URL], + "bearer_methods_supported": ["header"], + }) + + +@mcp.custom_route("/.well-known/oauth-authorization-server", ["GET"]) +async def as_metadata(_: Request) -> Response: + doc = { + "issuer": BASE_URL, + "authorization_endpoint": f"{BASE_URL}/authorize", + "token_endpoint": f"{BASE_URL}/token", + "response_types_supported": ["code"], + "grant_types_supported": ["authorization_code", "refresh_token"], + "code_challenge_methods_supported": ["S256"], + "token_endpoint_auth_methods_supported": + ["client_secret_post", "client_secret_basic"] if MODE == "confidential" else ["none"], + } + if MODE != "confidential": + doc["registration_endpoint"] = f"{BASE_URL}/register" + return JSONResponse(doc) + + +@mcp.custom_route("/register", ["POST"]) +async def register(request: Request) -> Response: + if MODE == "confidential": + return JSONResponse({"error": "registration_not_supported"}, status_code=404) + body = await request.json() + client_id = "dyn-" + secrets.token_hex(6) + dynamic_clients[client_id] = {"redirect_uris": body.get("redirect_uris", [])} + log(f"registered dynamic client {client_id}") + return JSONResponse({ + "client_id": client_id, + "redirect_uris": body.get("redirect_uris", []), + "token_endpoint_auth_method": "none", + "grant_types": ["authorization_code", "refresh_token"], + "response_types": ["code"], + }, status_code=201) + + +def known_client(client_id: str, redirect_uri: str) -> bool: + if MODE == "confidential": + return client_id == CLIENT_ID and redirect_uri.endswith("/api/mcp/oauth/callback") + return client_id in dynamic_clients and redirect_uri in dynamic_clients[client_id]["redirect_uris"] + + +@mcp.custom_route("/authorize", ["GET"]) +async def authorize(request: Request) -> Response: + q = request.query_params + client_id, redirect_uri = q.get("client_id", ""), q.get("redirect_uri", "") + if not known_client(client_id, redirect_uri): + log(f"authorize rejected: unknown client {client_id} or redirect {redirect_uri}") + return JSONResponse({"error": "invalid_request", + "error_description": "unknown client or redirect_uri"}, status_code=400) + if not q.get("code_challenge"): + return JSONResponse({"error": "invalid_request", "error_description": "PKCE required"}, status_code=400) + code = secrets.token_urlsafe(16) + codes[code] = {"client_id": client_id, "redirect_uri": redirect_uri, + "challenge": q["code_challenge"], "scope": q.get("scope", "")} + log(f"authorize ok for {client_id}, scope='{q.get('scope', '')}'") + params = {"code": code} + if q.get("state"): + params["state"] = q["state"] + return RedirectResponse(f"{redirect_uri}?{urlencode(params)}", status_code=302) + + +def client_credentials(request: Request, form: dict) -> tuple[str, str | None]: + auth = request.headers.get("authorization", "") + if auth.lower().startswith("basic "): + raw = base64.b64decode(auth[6:]).decode() + cid, _, sec = raw.partition(":") + return cid, sec + return form.get("client_id", ""), form.get("client_secret") + + +@mcp.custom_route("/token", ["POST"]) +async def token(request: Request) -> Response: + form = dict((await request.form()).items()) + cid, sec = client_credentials(request, form) + grant = form.get("grant_type") + log(f"token request grant={grant} client={cid} secret_presented={sec is not None}") + if MODE == "confidential" and (cid != CLIENT_ID or sec != CLIENT_SECRET): + return JSONResponse({"error": "invalid_client", + "error_description": "client authentication failed"}, status_code=401) + if grant == "authorization_code": + info = codes.pop(form.get("code", ""), None) + if info is None or info["client_id"] != cid: + return JSONResponse({"error": "invalid_grant"}, status_code=400) + verifier = form.get("code_verifier", "") + digest = base64.urlsafe_b64encode(hashlib.sha256(verifier.encode()).digest()).rstrip(b"=").decode() + if digest != info["challenge"]: + return JSONResponse({"error": "invalid_grant", "error_description": "PKCE mismatch"}, status_code=400) + return issue(cid, info["scope"]) + if grant == "refresh_token": + info = refresh_tokens.pop(form.get("refresh_token", ""), None) + if info is None or info["client_id"] != cid: + log("refresh rejected: unknown or already-used refresh token") + return JSONResponse({"error": "invalid_grant"}, status_code=400) + return issue(cid, info["scope"]) + return JSONResponse({"error": "unsupported_grant_type"}, status_code=400) + + +def issue(client_id: str, scope: str) -> Response: + access = "at-" + secrets.token_urlsafe(16) + refresh = "rt-" + secrets.token_urlsafe(16) + access_tokens[access] = time.time() + TOKEN_TTL + refresh_tokens[refresh] = {"client_id": client_id, "scope": scope} + log(f"issued access token (ttl {TOKEN_TTL}s) and refresh token") + return JSONResponse({"access_token": access, "token_type": "Bearer", "expires_in": TOKEN_TTL, + "refresh_token": refresh, "scope": scope}) + + +def valid_bearer(headers: dict) -> bool: + auth = headers.get(b"authorization", b"").decode() + if not auth.lower().startswith("bearer "): + return False + expires = access_tokens.get(auth[7:]) + return expires is not None and expires > time.time() + + +class Gate: + """Return 401 with an RFC 9728 challenge when /mcp has no valid bearer token.""" + + def __init__(self, app): + self.app = app + + async def __call__(self, scope, receive, send): + if scope["type"] != "http" or scope["path"].rstrip("/") != "/mcp": + return await self.app(scope, receive, send) + + headers = dict(scope["headers"]) + body = b"" + while True: + message = await receive() + body += message.get("body", b"") + if not message.get("more_body"): + break + + methods = [] + try: + parsed = json.loads(body) if body else {} + methods = [m.get("method") for m in (parsed if isinstance(parsed, list) else [parsed])] + except ValueError: + pass + + needs_token = MODE != "anonymous" or "tools/call" in methods + ok = valid_bearer(headers) + log(f"/mcp {scope['method']} methods={methods} bearer_valid={ok}") + if needs_token and not ok: + resp = Response(status_code=401, headers={ + "WWW-Authenticate": f'Bearer resource_metadata="{metadata_url()}"'}) + return await resp(scope, receive, send) + + replayed = False + + async def replay(): + nonlocal replayed + if not replayed: + replayed = True + return {"type": "http.request", "body": body, "more_body": False} + return await receive() + + return await self.app(scope, replay, send) + + +if __name__ == "__main__": + log(f"mode={MODE} base_url={BASE_URL}") + uvicorn.run(Gate(mcp.streamable_http_app()), host="0.0.0.0", port=8080, log_level="warning") diff --git a/screenshots/output/approval-prompt-tui.png b/screenshots/output/approval-prompt-tui.png new file mode 100644 index 0000000000000000000000000000000000000000..ef76f7a7aabcfd548782bd45138f78eecaf9052d GIT binary patch literal 177412 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