diff --git a/.github/workflows/pr-validation.yml b/.github/workflows/pr-validation.yml new file mode 100644 index 0000000..f37f65d --- /dev/null +++ b/.github/workflows/pr-validation.yml @@ -0,0 +1,103 @@ +name: pr_validation + +on: + pull_request: + branches: + - dev + +jobs: + build-site: + name: "Build Astro" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build site + run: npm run build + + link-validation: + name: "Link and URL Validation" + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build site + run: npm run build + + - name: Install link-validator + run: | + INSTALL_DIR="${GITHUB_WORKSPACE}/link-validator-bin" + mkdir -p "$INSTALL_DIR" + + echo "Downloading link-validator v0.1.1..." + curl -L https://github.com/Aaronontheweb/link-validator/releases/download/v0.1.1/link-validator-linux-x64.tar.gz -o link-validator.tar.gz + tar -xzf link-validator.tar.gz -C "$INSTALL_DIR" + chmod +x "$INSTALL_DIR/link-validator" + + "$INSTALL_DIR/link-validator" --version + echo "LINK_VALIDATOR_PATH=$INSTALL_DIR/link-validator" >> $GITHUB_ENV + + - name: Serve site and validate links + run: | + # Start Astro preview server in background + npx astro preview --port 4321 & + SERVER_PID=$! + + # Wait for server to be ready + echo "Waiting for Astro preview server to start..." + for i in $(seq 1 30); do + if curl -sf http://localhost:4321 > /dev/null 2>&1; then + echo "Server is ready." + break + fi + sleep 1 + done + + if ! curl -sf http://localhost:4321 > /dev/null 2>&1; then + echo "Failed to start preview server" + kill $SERVER_PID 2>/dev/null + exit 1 + fi + + # Run link validator + echo "Running link validator..." + $LINK_VALIDATOR_PATH --url http://localhost:4321 \ + --output link-validation-report.md \ + --max-external-retries 3 || VALIDATION_EXIT_CODE=$? + + # Display report + echo "Link validation report:" + cat link-validation-report.md + + # Cleanup + kill $SERVER_PID 2>/dev/null + + exit ${VALIDATION_EXIT_CODE:-0} + + - name: Upload link validation report + uses: actions/upload-artifact@v4 + if: always() + with: + name: link-validation-report + path: link-validation-report.md + retention-days: 30 diff --git a/.gitignore b/.gitignore index d3a76a1..938b447 100644 --- a/.gitignore +++ b/.gitignore @@ -11,5 +11,15 @@ public/screenshots/output/ # Screenshot pipeline temp screenshots/.netclaw-screenshot-home/ +# Doc writing pipeline logs and screenshots +docs-logs/ + +# OpenProse runtime state +.prose/runs/ +.prose/agents/ + +# Local Claude settings and skills +.claude/ + # OS files .DS_Store diff --git a/.prose/write-doc-page.prose b/.prose/write-doc-page.prose new file mode 100644 index 0000000..00d03cc --- /dev/null +++ b/.prose/write-doc-page.prose @@ -0,0 +1,214 @@ +# Write Documentation Page +# Full pipeline: research → draft → parallel critique → humanizer → revise → visual verify → commit +# +# Usage: +# prose run .prose/write-doc-page.prose PAGE=cli/status +# prose run .prose/write-doc-page.prose PAGE=observability/opentelemetry + +input PAGE: "Documentation page slug (e.g. cli/status, observability/opentelemetry)" + +# --- Agents --- + +agent researcher: + model: sonnet + prompt: "You are a source code researcher. You read code, specs, and docs to extract facts. Be thorough — report file paths, line numbers, and exact values. No speculation." + +agent writer: + model: opus + prompt: """ + You are a technical documentation writer for netclaw.dev. + Writing principles — less is more: + - Be minimal. Only include what people actually need. + - One sentence that teaches something beats three that elaborate. + - Code examples are worth more than prose. Show, don't tell. + - Tables over prose for reference material. + - Visuals over text — if a screenshot explains it better, use it. + - No filler paragraphs. No padding. No hedging. + - Every sentence should teach something or help the reader do something. Delete the rest. + - Tone: confident, casual-technical, like explaining to a coworker. + """ + +agent critic: + model: sonnet + prompt: "You are a documentation reviewer. Be specific and actionable. Report findings as bullet lists with exact text to change." + +agent visual-reviewer: + model: sonnet + prompt: "You are a visual design reviewer for documentation pages. Check rendered screenshots for layout issues, broken elements, missing content, and readability problems." + +# --- Stage 1: Gather Source Material --- + +let source = session: researcher + prompt: """ + Gather all source material for the netclaw documentation page: **{PAGE}** + + Read CLAUDE.md for the full authoring workflow, page type classification, and templates. + + Then collect from the netclaw source repo (expected at ~/repositories/stannardlabs/netclaw/): + 1. Source code relevant to this page topic (CLI commands, config schemas, services, handlers) + 2. Specs in docs/spec/ that cover this topic + 3. ADRs in docs/adr/ if relevant + 4. Runbooks in docs/runbooks/ if relevant + 5. PROJECT_CONTEXT.md and CONTRIBUTING.md for overall context + + Also collect from this repo: + 6. The page stub in src/content/docs/{PAGE}.md (title, description) + 7. Screenshots in screenshots/output/ matching this topic + 8. Landing page src/pages/index.astro for product positioning + 9. Any already-written docs that should be cross-referenced + + Report: + - What you found (with file paths and key details) + - What screenshots are available + - What gaps remain that can't be filled from source material + """ + +# --- Stage 2: Write Draft --- + +session: writer + prompt: """ + Write the documentation page for **{PAGE}** on the netclaw.dev site. + + Follow the Documentation Authoring Workflow in CLAUDE.md exactly: + - Identify the page type (Step 1) + - Use the correct template for that page type (Step 4) + - Integrate screenshots where they exist (Step 5) + - Write to the file: src/content/docs/{PAGE}.md + + Source material from the research phase is in your context. Use it. + + Skip the interview step — this is autonomous. If critical info is missing, + leave a comment. + + Remember: less is more. Minimal. Show don't tell. Visuals over text. No filler. + """ + context: source + +# --- Stage 3: Parallel Critique --- + +parallel: + technical = session: critic + prompt: """ + **Technical Accuracy Review** for src/content/docs/{PAGE}.md + + Read the drafted page and compare every claim against the netclaw source code + at ~/repositories/stannardlabs/netclaw/. + + Check: + - Are flag names, default values, and behavior descriptions correct? + - Do code examples actually work? + - Do command output descriptions match what screenshots show? + - Are there any claims not supported by the source? + - Is anything technically wrong or misleading? + + Report findings as a bullet list. Be specific — include what's wrong and what's correct. + """ + context: source + + empathy = session: critic + prompt: """ + **Reader Empathy Review** for src/content/docs/{PAGE}.md + + Read the drafted page as a sysadmin or developer who just installed netclaw. + + Check: + - What would a reader find missing or confusing? + - Are there unstated prerequisites? + - Would a reader know what to do next? + - Identify 2-4 external links that would genuinely help (official docs for third-party tools, relevant guides) + - Flag any jargon not defined or linked + - Are there unanswered questions a reader would have? + + Report findings as a bullet list with specific, actionable suggestions. + """ + context: source + + humanizer = session: critic + prompt: """ + **Humanizer Review** for src/content/docs/{PAGE}.md + + Read the drafted page and flag AI writing patterns: + - Robotic phrasing: "It is important to note...", "This section describes..." + - Unnecessary hedging: "you may want to", "it is recommended that" + - Passive voice where active is better + - Over-explaining the obvious + - Repetitive sentence structure + - Generic filler that teaches nothing + - AI vocabulary: ensure, leverage, utilize, comprehensive, robust, seamless + - Rule of three overuse, em dash overuse + - Inline-header vertical lists (bold word + colon pattern) + + The tone should be confident and casual-technical, like explaining to a coworker. + + For each finding, include the exact text and a suggested rewrite. + """ + +# --- Stage 4: Revise --- + +let revised = session: writer + prompt: """ + Revise src/content/docs/{PAGE}.md incorporating ALL feedback from the three reviewers. + + In a single pass: + 1. Fix every technical inaccuracy flagged + 2. Add missing context, prerequisites, and external links the empathy reviewer identified + 3. Rewrite every robotic or hedging phrase the humanizer flagged + 4. Ensure cross-links to other netclaw docs use correct slugs + 5. Verify the page still follows its template from CLAUDE.md + + Then do a final anti-AI pass: read the page aloud in your head. Does it sound like + a person wrote it? If not, fix what's off. + + Write the final version to src/content/docs/{PAGE}.md. + """ + context: [technical, empathy, humanizer, source] + +# --- Stage 5: Visual Verification --- + +let visual = session: visual-reviewer + prompt: """ + Verify the rendered documentation page for **{PAGE}** looks correct. + + Steps: + 1. Run: npm run build + 2. If build fails, fix the issue in src/content/docs/{PAGE}.md and rebuild. + 3. Use Playwright MCP tools (browser_navigate, browser_screenshot) to verify the page: + a. Navigate to http://localhost:4321/{PAGE}/ + b. Take a screenshot + c. Check the rendered page: + - Does it render correctly? No broken layout, missing sections, or garbled text? + - Do tables render properly? + - Are screenshots/images displaying? + - Is the content readable against the dark background? + - Does the sidebar show the correct section highlighted? + 4. If there are visual issues, fix them and re-check. + + If the dev server isn't running or Playwright MCP isn't available, skip visual + verification — the build check is sufficient. Do not hang or wait. + + Report what you see. If everything looks good, say so. + """ + context: revised + +# --- Stage 6: Commit --- + +session: writer + prompt: """ + Final steps for **{PAGE}**: + + 1. Run npm run build one final time to confirm no errors. + 2. Run through the quality checklist from CLAUDE.md Step 9: + - Frontmatter title and description set + - Correct template for page type + - All referenced screenshots exist + - No placeholder text (Content coming soon, TODO, TBD) + - Cross-links use correct slugs + - Code examples are complete and copy-pasteable + - At least 2 external links + - No robotic phrasing or hedging + 3. Stage ONLY the documentation page: git add src/content/docs/{PAGE}.md + 4. Commit: git commit -m "docs: write {PAGE}" + + If any checklist item fails, fix it before committing. + """ + context: visual diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..4e372d9 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,338 @@ +# netclaw.dev Documentation Site + +Astro 6.2 + Starlight 0.38.4 static site. Dark-mode only. Deployed to Cloudflare Pages. + +## Documentation Authoring Workflow + +Each doc page follows a repeatable process. When asked to write a doc page, execute these steps in order. + +### Step 1: Identify the Page + +Read the target markdown stub in `src/content/docs/` to get the title and description. Determine the page type: + +| Page Type | Directories | Approach | +|-----------|------------|----------| +| **CLI Reference** | `cli/` | Self-serve from screenshots + interview for non-obvious behavior | +| **Tutorial** | `getting-started/` | Interview-heavy — need the golden path, prerequisites, expected output | +| **Guide** | `guides/` | Mix — problem statement is self-evident, solution steps need interview | +| **Concept** | `architecture/`, `security/` | Interview-heavy — design decisions, rationale, threat models | +| **Configuration** | `configuration/` | Mix — schema/options from interview, examples from screenshots | +| **Integration** | `channels/` | Interview-heavy — third-party setup steps, OAuth flows, permissions | +| **Operations** | `deployment/` | Interview-heavy — runtime requirements, environment variables, health checks | + +### Step 2: Gather What You Can + +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: + - `src/Netclaw.Cli/` — CLI commands, flags, help text + - `src/Netclaw.Configuration/` — Config schemas, defaults, validation + - `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 + - `src/Netclaw.Providers/` — Provider abstraction, model routing + - `docs/spec/` — Formal specs (runtime boundaries, session lifecycle, ACL policy, CLI contract, onboarding, model providers, MCP integration, daemon architecture) + - `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 + - `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. +4. **Existing docs** — Read related pages that are already written for context and cross-referencing. +5. **Skill server repo** — Clone `https://github.com/netclaw-dev/skill-server` (or locate a local clone). May have relevant API docs or README content for skills-related pages. + +Read relevant source files and specs BEFORE interviewing. Document what you found and what gaps remain before proceeding to interview. + +### Step 3: Interview Mode + +Ask the user **targeted, specific questions** to fill the gaps from Step 2. Rules: + +- **Batch questions** — ask 3-7 questions at once, grouped by topic. Don't ask one at a time. +- **Show your work** — tell the user what you already know (from screenshots/landing page) so they don't repeat themselves. +- **Ask for the non-obvious** — don't ask "what does `netclaw status` do?" when you have a screenshot. Ask "what happens when status shows a degraded provider?" or "what's the most common mistake users make here?" +- **Ask for sequencing** — for tutorials/guides, ask "what order should a user do these steps?" and "what should they see at each checkpoint?" +- **Ask for failure modes** — "what goes wrong?" and "what's the fix?" are often the most valuable content. +- **Ask for scope boundaries** — "what should this page NOT cover?" prevents scope creep. +- **One round preferred** — aim to get everything in one interview round. A second round is OK if answers reveal new gaps. + +#### Interview Templates by Page Type + +**CLI Reference:** +- What flags/options aren't visible in the screenshots? +- What's the most common use case vs. advanced use cases? +- Any gotchas or non-obvious behavior? +- What other commands is this typically used with? + +**Tutorial:** +- What are the prerequisites (installed software, accounts, API keys)? +- Walk me through the exact sequence — what does the user type, what do they see? +- Where do users most commonly get stuck? +- What does "success" look like at the end? + +**Guide:** +- What problem is this solving? When would a user need this? +- What's the step-by-step? Any conditional branches (if X, do Y)? +- Are there multiple valid approaches? Which do we recommend and why? +- What are the failure modes and how do you recover? + +**Concept:** +- What's the mental model the user should have? +- What design decisions were made and why? +- What are the boundaries/limitations? +- How does this relate to other parts of the system? + +**Configuration:** +- What's the config file format and location? +- What are all the fields, their types, defaults, and valid ranges? +- What's a minimal config vs. a production config? +- What happens when config is invalid? + +**Integration:** +- What third-party setup is required (apps, bots, tokens, permissions)? +- What's the OAuth/auth flow? +- What permissions/scopes are needed and why? +- How do you verify the integration is working? + +### Step 4: Write the Page + +**Writing principles — less is more:** +- Be minimal. Only include what people actually need. If it's obvious, don't explain it. +- Prefer a short page that covers the essentials over a long page that covers everything. +- One sentence that teaches something beats three sentences that elaborate. +- Code examples are worth more than prose. Show, don't tell. +- If the CLI help text already explains a flag well, don't rewrite it — just show the help output. +- No filler paragraphs. No "In this section we will discuss..." No padding. +- Tables over prose for reference material (flags, config fields, alert types). +- Every sentence should either teach something or help the reader do something. Delete the rest. +- **Visuals over text** — If a screenshot, diagram, or image explains something better than prose, use it. A screenshot of the TUI output is worth more than three paragraphs describing it. Architecture pages should have diagrams. CLI pages should have screenshots. Don't describe what you can show. + +Write for a technical audience that's new to netclaw but experienced with their platform (Linux, Docker, Slack, etc.). + +#### CLI Reference Template + +```markdown +--- +title: "netclaw " +description: "" +--- + + + +## Usage + +\`\`\`bash +netclaw [options] +\`\`\` + +## Options + +| Flag | Description | Default | +|------|-------------|---------| +| ... | ... | ... | + +## Examples + +### + +\`\`\`bash +netclaw +\`\`\` + +![](/screenshots/output/.png) + + + +## Related Commands + +- [`netclaw `](/cli//) — +``` + +#### Tutorial Template + +```markdown +--- +title: "" +description: "" +--- + + + +## Prerequisites + +- +- + +## Steps + +### 1. + + + +\`\`\`bash + +\`\`\` + + + +### 2. + +... + +## Verify It Works + + + +## Next Steps + +- []() — +``` + +#### Guide Template + +```markdown +--- +title: "" +description: "" +--- + + + +## Before You Begin + +- + +## + +... + +## Troubleshooting + +### + + +``` + +#### Concept Template + +```markdown +--- +title: "" +description: "" +--- + +<2-3 paragraph overview: what this is, why it matters, mental model> + +## + +... + +## Limitations + +- +``` + +### Step 5: Screenshot Integration + +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 +- 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 + +### Step 6: Critique (3 parallel agents) + +After writing the draft, spawn three review agents in parallel. Pass each the full draft content. + +**Agent 1 — Technical Accuracy:** +- Compare every claim against the source code and specs +- Flag any incorrect flag names, default values, or behavior descriptions +- Verify all code examples would actually work +- Check that command output descriptions match what the screenshots show + +**Agent 2 — Reader Empathy:** +- What would a human reader find missing or confusing? +- Are there prerequisites the page assumes but doesn't state? +- Would a reader know what to do next after finishing this page? +- Identify 2-4 external links that would be useful (official docs for third-party tools, relevant RFCs, related tutorials) +- Flag any jargon that isn't defined on this page or linked to a page that defines it + +**Agent 3 — Humanizer:** +- Does this read like a person wrote it or like a manual was generated? +- Flag robotic phrasing: "It is important to note that...", "This section describes...", "The following table shows..." +- Flag unnecessary hedging: "you may want to", "it is recommended that" +- Prefer direct voice: "Run `netclaw status`" not "You can run `netclaw status`" +- Check for variety in sentence structure — not every paragraph should start the same way +- The tone should be confident and casual-technical, like explaining to a coworker + +### 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. + +### Step 8: Revise + +Incorporate all critique feedback and humanizer output in a single pass: +- Fix technical inaccuracies +- Add missing context, prerequisites, and "what's next" links +- Insert external resource links where the reader empathy agent identified gaps +- Rewrite robotic or hedging language +- Ensure cross-links to other netclaw docs use correct slugs + +### Step 9: Quality Check + +Before declaring the page done: + +- [ ] Frontmatter `title` and `description` are set +- [ ] Page follows the correct template for its type +- [ ] All referenced screenshots exist in `screenshots/output/` +- [ ] No placeholder text ("Content coming soon", "TODO", "TBD") +- [ ] Cross-links to related pages use correct slugs +- [ ] Code examples are complete and copy-pasteable +- [ ] At least 2 external links to relevant resources +- [ ] No robotic phrasing or unnecessary hedging +- [ ] `npm run build` passes with no errors + +## Autonomous Doc Writing Mode + +When invoked with a page slug (e.g., `cli/status`), run the full pipeline without user interaction: + +1. Skip Step 3 (Interview) — use only source material from Step 2 +2. If critical information is missing, write the best page you can and leave a `` comment +3. Run all critique agents (Step 6) and revise (Step 7) +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. + +## Build & Preview + +```bash +npm run dev -- --host 0.0.0.0 # Dev server with Tailscale access +npm run build # Production build (43 pages) +``` + +## Project Structure + +``` +src/ + content/docs/ # Markdown documentation pages + pages/ # index.astro (landing page) + components/ # Starlight overrides (Header, Footer, ThemeSelect) + styles/ # custom.css (brand tokens) + assets/ # Logo SVG for Starlight +public/ + assets/ # Static assets (images, favicons) + screenshots/ # Copied from repo root at build time +screenshots/ # VHS capture pipeline output (source of truth) +``` diff --git a/astro.config.mjs b/astro.config.mjs index 397caa6..2d9adca 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,8 +1,12 @@ import { defineConfig } from 'astro/config'; import starlight from '@astrojs/starlight'; +import rehypeMermaid from 'rehype-mermaid'; export default defineConfig({ site: 'https://netclaw.dev', + markdown: { + rehypePlugins: [[rehypeMermaid, { strategy: 'inline-svg', dark: true }]], + }, vite: { server: { allowedHosts: ['.ts.net'], @@ -65,13 +69,22 @@ export default defineConfig({ { label: 'Configuration', items: [ - { label: 'Providers', slug: 'configuration/providers' }, + { label: 'Managed Providers', slug: 'configuration/managed-providers' }, + { label: 'Self-Hosted Providers', slug: 'configuration/self-hosted-providers' }, { label: 'Models', slug: 'configuration/models' }, { label: 'MCP Servers', slug: 'configuration/mcp-servers' }, { label: 'Webhooks', slug: 'configuration/webhooks' }, { label: 'Reminders', slug: 'configuration/reminders' }, ], }, + { + label: 'Notifications & Observability', + items: [ + { label: 'Operational Alerts', slug: 'observability/operational-alerts' }, + { label: 'Health Checks', slug: 'observability/health-checks' }, + { label: 'OpenTelemetry', slug: 'observability/opentelemetry' }, + ], + }, { label: 'CLI Reference', items: [ @@ -96,7 +109,7 @@ export default defineConfig({ items: [ { label: 'Docker', slug: 'deployment/docker' }, { label: 'systemd', slug: 'deployment/systemd' }, - { label: 'Reverse Proxy', slug: 'deployment/reverse-proxy' }, + { label: 'Exposure Modes', slug: 'deployment/exposure-modes' }, ], }, { @@ -109,7 +122,6 @@ export default defineConfig({ { label: 'Guides', items: [ - { label: 'Setting Up Ollama', slug: 'guides/setting-up-ollama' }, { label: 'Connecting Slack', slug: 'guides/connecting-slack' }, { label: 'MCP Tool Permissions', slug: 'guides/mcp-tool-permissions' }, { label: 'Pairing Remote Devices', slug: 'guides/pairing-remote-devices' }, diff --git a/docs-queue.md b/docs-queue.md new file mode 100644 index 0000000..127c371 --- /dev/null +++ b/docs-queue.md @@ -0,0 +1,70 @@ +# Documentation Page Queue + +Pages marked `[x]` are complete. Autonomous pages can be written without user input. + +## Batch 1: CLI Reference (autonomous) + +- [x] `cli/overview` +- [x] `cli/init` +- [x] `cli/chat` +- [x] `cli/sessions` +- [x] `cli/status` +- [x] `cli/doctor` +- [x] `cli/stats` +- [x] `cli/provider` +- [x] `cli/model` +- [x] `cli/mcp-tools` +- [x] `cli/webhooks` +- [x] `cli/reminder` +- [x] `cli/skill` +- [x] `cli/secrets` + +## Batch 2: Security (autonomous) + +- [x] `security/security-model` +- [x] `security/hardening` +- [x] `security/secrets` + +## Batch 3: Configuration (autonomous) + +- [x] `configuration/managed-providers` +- [x] `configuration/self-hosted-providers` +- [x] `configuration/models` +- [x] `configuration/mcp-servers` +- [x] `configuration/webhooks` +- [x] `configuration/reminders` + +## Batch 4: Observability (autonomous) + +- [x] `observability/operational-alerts` +- [x] `observability/opentelemetry` + +## Batch 5: Channels & Troubleshooting (autonomous) + +- [x] `channels/troubleshooting` + +## Batch 6: Deployment (autonomous) + +- [x] `deployment/docker` +- [x] `deployment/systemd` +- [x] `deployment/exposure-modes` + +## Batch 7: Architecture & Guides (autonomous with review) + +- [x] `architecture/overview` +- [x] `architecture/security-model` +- [x] `channels/slack` +- [x] `channels/discord` +- [x] `skills/overview` +- [x] `skills/external-skills` +- [x] `guides/connecting-slack` +- [x] `guides/mcp-tool-permissions` +- [x] `guides/pairing-remote-devices` + +## Batch 8: Needs Interview + +- [ ] `getting-started/installation` +- [ ] `getting-started/quickstart` +- [ ] `getting-started/first-conversation` +- [ ] `skills/skill-server` +- [ ] `skills/skill-feeds` diff --git a/package-lock.json b/package-lock.json index 9c6ecf1..07cfb4d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,12 +10,27 @@ "dependencies": { "@astrojs/starlight": "^0.38.4", "astro": "^6.2.0", + "mermaid": "^11.14.0", + "rehype-mermaid": "^3.0.0", "sharp": "^0.34.0" }, "devDependencies": { "playwright": "^1.59.1" } }, + "node_modules/@antfu/install-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@antfu/install-pkg/-/install-pkg-1.1.0.tgz", + "integrity": "sha512-MGQsmw10ZyI+EJo45CdSER4zEb+p31LpDAFp2Z3gkSd1yqVZGi0Ebx++YTEMonJy4oChEMLsxZ64j8FH6sSqtQ==", + "license": "MIT", + "dependencies": { + "package-manager-detector": "^1.3.0", + "tinyexec": "^1.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, "node_modules/@astrojs/compiler": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-4.0.0.tgz", @@ -221,6 +236,12 @@ "node": ">=6.9.0" } }, + "node_modules/@braintree/sanitize-url": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-7.1.2.tgz", + "integrity": "sha512-jigsZK+sMF/cuiB7sERuo9V7N9jx+dhmHHnQyDSVdpZwVutaBu7WvNYqMDLSgFgfB30n452TP3vjDAvFC973mA==", + "license": "MIT" + }, "node_modules/@capsizecss/unpack": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", @@ -233,6 +254,43 @@ "node": ">=18" } }, + "node_modules/@chevrotain/cst-dts-gen": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/cst-dts-gen/-/cst-dts-gen-12.0.0.tgz", + "integrity": "sha512-fSL4KXjTl7cDgf0B5Rip9Q05BOrYvkJV/RrBTE/bKDN096E4hN/ySpcBK5B24T76dlQ2i32Zc3PAE27jFnFrKg==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/gast": "12.0.0", + "@chevrotain/types": "12.0.0" + } + }, + "node_modules/@chevrotain/gast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/gast/-/gast-12.0.0.tgz", + "integrity": "sha512-1ne/m3XsIT8aEdrvT33so0GUC+wkctpUPK6zU9IlOyJLUbR0rg4G7ZiApiJbggpgPir9ERy3FRjT6T7lpgetnQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/types": "12.0.0" + } + }, + "node_modules/@chevrotain/regexp-to-ast": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/regexp-to-ast/-/regexp-to-ast-12.0.0.tgz", + "integrity": "sha512-p+EW9MaJwgaHguhoqwOtx/FwuGr+DnNn857sXWOi/mClXIkPGl3rn7hGNWvo31HA3vyeQxjqe+H36yZJwYU8cA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/types": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/types/-/types-12.0.0.tgz", + "integrity": "sha512-S+04vjFQKeuYw0/eW3U52LkAHQsB1ASxsPGsLPUyQgrZ2iNNibQrsidruDzjEX2JYfespXMG0eZmXlhA6z7nWA==", + "license": "Apache-2.0" + }, + "node_modules/@chevrotain/utils": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/@chevrotain/utils/-/utils-12.0.0.tgz", + "integrity": "sha512-lB59uJoaGIfOOL9knQqQRfhl9g7x8/wqFkp13zTdkRu1huG9kg6IJs1O8hqj9rs6h7orGxHJUKb+mX3rPbWGhA==", + "license": "Apache-2.0" + }, "node_modules/@clack/core": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.3.0.tgz", @@ -818,6 +876,32 @@ "@expressive-code/core": "^0.41.7" } }, + "node_modules/@fortawesome/fontawesome-free": { + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.7.2.tgz", + "integrity": "sha512-JUOtgFW6k9u4Y+xeIaEiLr3+cjoUPiAuLXoyKOJSia6Duzb7pq+A76P9ZdPDoAoxHdHzq6gE9/jKBGXlZT8FbA==", + "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", + "engines": { + "node": ">=6" + } + }, + "node_modules/@iconify/types": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@iconify/types/-/types-2.0.0.tgz", + "integrity": "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==", + "license": "MIT" + }, + "node_modules/@iconify/utils": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@iconify/utils/-/utils-3.1.1.tgz", + "integrity": "sha512-MwzoDtw9rO1x+qfgLTV/IVXsHDBqeYZoMIQC8SfxfYSlaSUG+oWiAcoiB1yajAda6mqblm4/1/w2E8tRu7a7Tw==", + "license": "MIT", + "dependencies": { + "@antfu/install-pkg": "^1.1.0", + "@iconify/types": "^2.0.0", + "mlly": "^1.8.2" + } + }, "node_modules/@img/colour": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", @@ -1326,6 +1410,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/@mermaid-js/parser": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@mermaid-js/parser/-/parser-1.1.0.tgz", + "integrity": "sha512-gxK9ZX2+Fex5zu8LhRQoMeMPEHbc73UKZ0FQ54YrQtUxE1VVhMwzeNtKRPAu5aXks4FasbMe4xB4bWrmq6Jlxw==", + "license": "MIT", + "dependencies": { + "langium": "^4.0.0" + } + }, "node_modules/@oslojs/encoding": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", @@ -1882,6 +1975,259 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, + "node_modules/@types/d3": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/@types/d3/-/d3-7.4.3.tgz", + "integrity": "sha512-lZXZ9ckh5R8uiFVt8ogUNf+pIrK4EsWrx2Np75WvF/eTpJ0FMHNhjXk8CKEx/+gpHbNQyJWehbFaTvqmHWB3ww==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/d3-axis": "*", + "@types/d3-brush": "*", + "@types/d3-chord": "*", + "@types/d3-color": "*", + "@types/d3-contour": "*", + "@types/d3-delaunay": "*", + "@types/d3-dispatch": "*", + "@types/d3-drag": "*", + "@types/d3-dsv": "*", + "@types/d3-ease": "*", + "@types/d3-fetch": "*", + "@types/d3-force": "*", + "@types/d3-format": "*", + "@types/d3-geo": "*", + "@types/d3-hierarchy": "*", + "@types/d3-interpolate": "*", + "@types/d3-path": "*", + "@types/d3-polygon": "*", + "@types/d3-quadtree": "*", + "@types/d3-random": "*", + "@types/d3-scale": "*", + "@types/d3-scale-chromatic": "*", + "@types/d3-selection": "*", + "@types/d3-shape": "*", + "@types/d3-time": "*", + "@types/d3-time-format": "*", + "@types/d3-timer": "*", + "@types/d3-transition": "*", + "@types/d3-zoom": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-axis": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-axis/-/d3-axis-3.0.6.tgz", + "integrity": "sha512-pYeijfZuBd87T0hGn0FO1vQ/cgLk6E1ALJjfkC0oJ8cbwkZl3TpgS8bVBLZN+2jjGgg38epgxb2zmoGtSfvgMw==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-brush": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-brush/-/d3-brush-3.0.6.tgz", + "integrity": "sha512-nH60IZNNxEcrh6L1ZSMNA28rj27ut/2ZmI3r96Zd+1jrZD++zD3LsMIjWlvg4AYrHn/Pqz4CF3veCxGjtbqt7A==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-chord": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-chord/-/d3-chord-3.0.6.tgz", + "integrity": "sha512-LFYWWd8nwfwEmTZG9PfQxd17HbNPksHBiJHaKuY1XeqscXacsS2tyoo6OdRsjf+NQYeB6XrNL3a25E3gH69lcg==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-contour": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-contour/-/d3-contour-3.0.6.tgz", + "integrity": "sha512-BjzLgXGnCWjUSYGfH1cpdo41/hgdWETu4YxpezoztawmqsvCeep+8QGfiY6YbDvfgHz/DkjeIkkZVJavB4a3rg==", + "license": "MIT", + "dependencies": { + "@types/d3-array": "*", + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-ZMaSKu4THYCU6sV64Lhg6qjf1orxBthaC161plr5KuPHo3CNm8DTHiLw/5Eq2b6TsNP0W0iJrUOFscY6Q450Hw==", + "license": "MIT" + }, + "node_modules/@types/d3-dispatch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dispatch/-/d3-dispatch-3.0.7.tgz", + "integrity": "sha512-5o9OIAdKkhN1QItV2oqaE5KMIiXAvDWBDPrD85e58Qlz1c1kI/J0NcqbEG88CoTwJrYe7ntUCVfeUl2UJKbWgA==", + "license": "MIT" + }, + "node_modules/@types/d3-drag": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-drag/-/d3-drag-3.0.7.tgz", + "integrity": "sha512-HE3jVKlzU9AaMazNufooRJ5ZpWmLIoc90A37WU2JMmeq28w1FQqCZswHZ3xR+SuxYftzHq6WU6KJHvqxKzTxxQ==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-dsv": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-dsv/-/d3-dsv-3.0.7.tgz", + "integrity": "sha512-n6QBF9/+XASqcKK6waudgL0pf/S5XHPPI8APyMLLUHd8NqouBGLsU8MgtO7NINGtPBtk9Kko/W4ea0oAspwh9g==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-fetch": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@types/d3-fetch/-/d3-fetch-3.0.7.tgz", + "integrity": "sha512-fTAfNmxSb9SOWNB9IoG5c8Hg6R+AzUHDRlsXsDZsNp6sxAEOP0tkP3gKkNSO/qmHPoBFTxNrjDprVHDQDvo5aA==", + "license": "MIT", + "dependencies": { + "@types/d3-dsv": "*" + } + }, + "node_modules/@types/d3-force": { + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/@types/d3-force/-/d3-force-3.0.10.tgz", + "integrity": "sha512-ZYeSaCF3p73RdOKcjj+swRlZfnYpK1EbaDiYICEEp5Q6sUiqFaFQ9qgoshp5CzIyyb/yD09kD9o2zEltCexlgw==", + "license": "MIT" + }, + "node_modules/@types/d3-format": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-format/-/d3-format-3.0.4.tgz", + "integrity": "sha512-fALi2aI6shfg7vM5KiR1wNJnZ7r6UuggVqtDA+xiEdPZQwy/trcQaHnwShLuLdta2rTymCNpxYTiMZX/e09F4g==", + "license": "MIT" + }, + "node_modules/@types/d3-geo": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-geo/-/d3-geo-3.1.0.tgz", + "integrity": "sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==", + "license": "MIT", + "dependencies": { + "@types/geojson": "*" + } + }, + "node_modules/@types/d3-hierarchy": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-hierarchy/-/d3-hierarchy-3.1.7.tgz", + "integrity": "sha512-tJFtNoYBtRtkNysX1Xq4sxtjK8YgoWUNpIiUee0/jHGRwqvzYxkq0hGVbbOGSz+JgFxxRu4K8nb3YpG3CMARtg==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-polygon": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-polygon/-/d3-polygon-3.0.2.tgz", + "integrity": "sha512-ZuWOtMaHCkN9xoeEMr1ubW2nGWsp4nIql+OPQRstu4ypeZ+zk3YKqQT0CXVe/PYqrKpZAi+J9mTs05TKwjXSRA==", + "license": "MIT" + }, + "node_modules/@types/d3-quadtree": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@types/d3-quadtree/-/d3-quadtree-3.0.6.tgz", + "integrity": "sha512-oUzyO1/Zm6rsxKRHA1vH0NEDG58HrT5icx/azi9MF1TWdtttWl0UIUsjEQBBh+SIkrpd21ZjEv7ptxWys1ncsg==", + "license": "MIT" + }, + "node_modules/@types/d3-random": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-random/-/d3-random-3.0.3.tgz", + "integrity": "sha512-Imagg1vJ3y76Y2ea0871wpabqp613+8/r0mCLEBfdtqC7xMSfj9idOnmBYyMoULfHePJyxMAw3nWhJxzc+LFwQ==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-iWMJgwkK7yTRmWqRB5plb1kadXyQ5Sj8V/zYlFGMUBbIPKQScw+Dku9cAAMgJG+z5GYDoMjWGLVOvjghDEFnKQ==", + "license": "MIT" + }, + "node_modules/@types/d3-selection": { + "version": "3.0.11", + "resolved": "https://registry.npmjs.org/@types/d3-selection/-/d3-selection-3.0.11.tgz", + "integrity": "sha512-bhAXu23DJWsrI45xafYpkQ4NtcKMwWnAC/vKrd2l+nxMFuvOT3XMYTIj2opv8vq8AO5Yh7Qac/nSeP/3zjTK0w==", + "license": "MIT" + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-time-format": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@types/d3-time-format/-/d3-time-format-4.0.3.tgz", + "integrity": "sha512-5xg9rC+wWL8kdDj153qZcsJ0FWiFt0J5RB6LYUNZjwSnesfblqrI/bJ1wBdJ8OQfncgbJG5+2F+qfqnqyzYxyg==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/d3-transition": { + "version": "3.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-transition/-/d3-transition-3.0.9.tgz", + "integrity": "sha512-uZS5shfxzO3rGlu0cC3bjmMFKsXv+SmZZcgp0KD22ts4uGXp5EVYGzu/0YdwZeKmddhcAccYtREJKkPfXkZuCg==", + "license": "MIT", + "dependencies": { + "@types/d3-selection": "*" + } + }, + "node_modules/@types/d3-zoom": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/@types/d3-zoom/-/d3-zoom-3.0.8.tgz", + "integrity": "sha512-iqMC4/YlFCSlO8+2Ii1GGGliCAY4XdeG748w5vQUbevlbDu0zSjH/+jojorQVBK/se0j6DUFNPBGSqD3YWYnDw==", + "license": "MIT", + "dependencies": { + "@types/d3-interpolate": "*", + "@types/d3-selection": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -1906,6 +2252,12 @@ "@types/estree": "*" } }, + "node_modules/@types/geojson": { + "version": "7946.0.16", + "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", + "integrity": "sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==", + "license": "MIT" + }, "node_modules/@types/hast": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", @@ -1969,6 +2321,13 @@ "@types/node": "*" } }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "license": "MIT", + "optional": true + }, "node_modules/@types/unist": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", @@ -1981,6 +2340,16 @@ "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "license": "ISC" }, + "node_modules/@upsetjs/venn.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@upsetjs/venn.js/-/venn.js-2.0.0.tgz", + "integrity": "sha512-WbBhLrooyePuQ1VZxrJjtLvTc4NVfpOyKx0sKqioq9bX1C1m7Jgykkn8gLrtwumBioXIqam8DLxp88Adbue6Hw==", + "license": "MIT", + "optionalDependencies": { + "d3-selection": "^3.0.0", + "d3-transition": "^3.0.1" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", @@ -2256,6 +2625,34 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chevrotain": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/chevrotain/-/chevrotain-12.0.0.tgz", + "integrity": "sha512-csJvb+6kEiQaqo1woTdSAuOWdN0WTLIydkKrBnS+V5gZz0oqBrp4kQ35519QgK6TpBThiG3V1vNSHlIkv4AglQ==", + "license": "Apache-2.0", + "dependencies": { + "@chevrotain/cst-dts-gen": "12.0.0", + "@chevrotain/gast": "12.0.0", + "@chevrotain/regexp-to-ast": "12.0.0", + "@chevrotain/types": "12.0.0", + "@chevrotain/utils": "12.0.0" + }, + "engines": { + "node": ">=22.0.0" + } + }, + "node_modules/chevrotain-allstar": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/chevrotain-allstar/-/chevrotain-allstar-0.4.3.tgz", + "integrity": "sha512-2X4mkroolSMKqW+H22pyPMUVDqYZzPhephTmg/NODKb1IGYPHfxfhcW0EjS7wcPJNbze2i4vBWT7zT5FKF2lrQ==", + "license": "MIT", + "dependencies": { + "lodash-es": "^4.18.1" + }, + "peerDependencies": { + "chevrotain": "^12.0.0" + } + }, "node_modules/chokidar": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-5.0.0.tgz", @@ -2333,6 +2730,12 @@ "node": ">= 18" } }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "license": "MIT" + }, "node_modules/cookie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", @@ -2352,6 +2755,15 @@ "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", "license": "MIT" }, + "node_modules/cose-base": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-1.0.3.tgz", + "integrity": "sha512-s9whTXInMSgAp/NVXVNuVxVKzGH2qck3aQlVHxDCdAEPgtMKwc4Wq6/QKhgdEdgbLSi9rBTAcPoRa6JpiG4ksg==", + "license": "MIT", + "dependencies": { + "layout-base": "^1.0.0" + } + }, "node_modules/crossws": { "version": "0.3.5", "resolved": "https://registry.npmjs.org/crossws/-/crossws-0.3.5.tgz", @@ -2399,69 +2811,583 @@ "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "license": "MIT", "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "license": "CC0-1.0" + }, + "node_modules/cytoscape": { + "version": "3.33.3", + "resolved": "https://registry.npmjs.org/cytoscape/-/cytoscape-3.33.3.tgz", + "integrity": "sha512-Gej7U+OKR+LZ8kvX7rb2HhCYJ0IhvEFsnkud4SB1PR+BUY/TsSO0dmOW59WEVLu51b1Rm+gQRKoz4bLYxGSZ2g==", + "license": "MIT", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/cytoscape-cose-bilkent": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cytoscape-cose-bilkent/-/cytoscape-cose-bilkent-4.1.0.tgz", + "integrity": "sha512-wgQlVIUJF13Quxiv5e1gstZ08rnZj2XaLHGoFMYXz7SkNfCDOOteKBE6SYRfA9WxxI/iBc3ajfDoc6hb/MRAHQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^1.0.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cytoscape-fcose/-/cytoscape-fcose-2.2.0.tgz", + "integrity": "sha512-ki1/VuRIHFCzxWNrsshHYPs6L7TvLu3DL+TyIGEsRcvVERmxokbf5Gdk7mFxZnTdiGtnA4cfSmjZJMviqSuZrQ==", + "license": "MIT", + "dependencies": { + "cose-base": "^2.2.0" + }, + "peerDependencies": { + "cytoscape": "^3.2.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/cose-base": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/cose-base/-/cose-base-2.2.0.tgz", + "integrity": "sha512-AzlgcsCbUMymkADOJtQm3wO9S3ltPfYOFD5033keQn9NJzIbtnZj+UdBJe7DYml/8TdbtHJW3j58SOnKhWY/5g==", + "license": "MIT", + "dependencies": { + "layout-base": "^2.0.0" + } + }, + "node_modules/cytoscape-fcose/node_modules/layout-base": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-2.0.1.tgz", + "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==", + "license": "MIT" + }, + "node_modules/d3": { + "version": "7.9.0", + "resolved": "https://registry.npmjs.org/d3/-/d3-7.9.0.tgz", + "integrity": "sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==", + "license": "ISC", + "dependencies": { + "d3-array": "3", + "d3-axis": "3", + "d3-brush": "3", + "d3-chord": "3", + "d3-color": "3", + "d3-contour": "4", + "d3-delaunay": "6", + "d3-dispatch": "3", + "d3-drag": "3", + "d3-dsv": "3", + "d3-ease": "3", + "d3-fetch": "3", + "d3-force": "3", + "d3-format": "3", + "d3-geo": "3", + "d3-hierarchy": "3", + "d3-interpolate": "3", + "d3-path": "3", + "d3-polygon": "3", + "d3-quadtree": "3", + "d3-random": "3", + "d3-scale": "4", + "d3-scale-chromatic": "3", + "d3-selection": "3", + "d3-shape": "3", + "d3-time": "3", + "d3-time-format": "4", + "d3-timer": "3", + "d3-transition": "3", + "d3-zoom": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-axis": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-axis/-/d3-axis-3.0.0.tgz", + "integrity": "sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-brush": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-brush/-/d3-brush-3.0.0.tgz", + "integrity": "sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "3", + "d3-transition": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-chord": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-chord/-/d3-chord-3.0.1.tgz", + "integrity": "sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==", + "license": "ISC", + "dependencies": { + "d3-path": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-contour": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-contour/-/d3-contour-4.0.2.tgz", + "integrity": "sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==", + "license": "ISC", + "dependencies": { + "d3-array": "^3.2.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-delaunay": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/d3-delaunay/-/d3-delaunay-6.0.4.tgz", + "integrity": "sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==", + "license": "ISC", + "dependencies": { + "delaunator": "5" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dispatch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-3.0.1.tgz", + "integrity": "sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-drag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-drag/-/d3-drag-3.0.0.tgz", + "integrity": "sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-selection": "3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-dsv/-/d3-dsv-3.0.1.tgz", + "integrity": "sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==", + "license": "ISC", + "dependencies": { + "commander": "7", + "iconv-lite": "0.6", + "rw": "1" + }, + "bin": { + "csv2json": "bin/dsv2json.js", + "csv2tsv": "bin/dsv2dsv.js", + "dsv2dsv": "bin/dsv2dsv.js", + "dsv2json": "bin/dsv2json.js", + "json2csv": "bin/json2dsv.js", + "json2dsv": "bin/json2dsv.js", + "json2tsv": "bin/json2dsv.js", + "tsv2csv": "bin/dsv2dsv.js", + "tsv2json": "bin/dsv2json.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-dsv/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-fetch": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-fetch/-/d3-fetch-3.0.1.tgz", + "integrity": "sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==", + "license": "ISC", + "dependencies": { + "d3-dsv": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-force": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-3.0.0.tgz", + "integrity": "sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==", + "license": "ISC", + "dependencies": { + "d3-dispatch": "1 - 3", + "d3-quadtree": "1 - 3", + "d3-timer": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-geo": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-3.1.1.tgz", + "integrity": "sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2.5.0 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-hierarchy": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-hierarchy/-/d3-hierarchy-3.1.2.tgz", + "integrity": "sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-polygon": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-polygon/-/d3-polygon-3.0.1.tgz", + "integrity": "sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-quadtree": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-3.0.1.tgz", + "integrity": "sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-random": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-random/-/d3-random-3.0.1.tgz", + "integrity": "sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-sankey": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/d3-sankey/-/d3-sankey-0.12.3.tgz", + "integrity": "sha512-nQhsBRmM19Ax5xEIPLMY9ZmJ/cDvd1BG3UVvt5h3WRxKg5zGRbvnteTyWAbzeSvlh3tW7ZEmq4VwR5mB3tutmQ==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-array": "1 - 2", + "d3-shape": "^1.2.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-array": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.12.1.tgz", + "integrity": "sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==", + "license": "BSD-3-Clause", + "dependencies": { + "internmap": "^1.0.0" + } + }, + "node_modules/d3-sankey/node_modules/d3-path": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-1.0.9.tgz", + "integrity": "sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==", + "license": "BSD-3-Clause" + }, + "node_modules/d3-sankey/node_modules/d3-shape": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-1.3.7.tgz", + "integrity": "sha512-EUkvKjqPFUAZyOlhY5gzCxCeI0Aep04LwIRpsZ/mLFelJiUfnK56jo5JMDSE7yyP2kLSb6LtF+S5chMk7uqPqw==", + "license": "BSD-3-Clause", + "dependencies": { + "d3-path": "1" + } + }, + "node_modules/d3-sankey/node_modules/internmap": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-1.0.1.tgz", + "integrity": "sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==", + "license": "ISC" + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale-chromatic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-scale-chromatic/-/d3-scale-chromatic-3.1.0.tgz", + "integrity": "sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3", + "d3-interpolate": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-selection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-3.0.0.tgz", + "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + "node": ">=12" } }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" }, - "funding": { - "url": "https://github.com/sponsors/fb55" + "engines": { + "node": ">=12" } }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" }, "engines": { - "node": ">=4" + "node": ">=12" } }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "license": "MIT", + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-transition": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-3.0.1.tgz", + "integrity": "sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==", + "license": "ISC", "dependencies": { - "css-tree": "~2.2.0" + "d3-color": "1 - 3", + "d3-dispatch": "1 - 3", + "d3-ease": "1 - 3", + "d3-interpolate": "1 - 3", + "d3-timer": "1 - 3" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=12" + }, + "peerDependencies": { + "d3-selection": "2 - 3" } }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "license": "MIT", + "node_modules/d3-zoom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-zoom/-/d3-zoom-3.0.0.tgz", + "integrity": "sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==", + "license": "ISC", "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" + "d3-dispatch": "1 - 3", + "d3-drag": "2 - 3", + "d3-interpolate": "1 - 3", + "d3-selection": "2 - 3", + "d3-transition": "2 - 3" }, "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" + "node": ">=12" } }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "license": "CC0-1.0" + "node_modules/dagre-d3-es": { + "version": "7.0.14", + "resolved": "https://registry.npmjs.org/dagre-d3-es/-/dagre-d3-es-7.0.14.tgz", + "integrity": "sha512-P4rFMVq9ESWqmOgK+dlXvOtLwYg0i7u0HBGJER0LZDJT2VHIPAMZ/riPxqJceWMStH5+E61QxFra9kIS3AqdMg==", + "license": "MIT", + "dependencies": { + "d3": "^7.9.0", + "lodash-es": "^4.17.21" + } + }, + "node_modules/dayjs": { + "version": "1.11.20", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.20.tgz", + "integrity": "sha512-YbwwqR/uYpeoP4pu043q+LTDLFBLApUP6VxRihdfNTqu4ubqMlGDLd6ErXhEgsyvY0K6nCs7nggYumAN+9uEuQ==", + "license": "MIT" }, "node_modules/debug": { "version": "4.4.3", @@ -2499,6 +3425,15 @@ "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, + "node_modules/delaunator": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/delaunator/-/delaunator-5.1.0.tgz", + "integrity": "sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==", + "license": "ISC", + "dependencies": { + "robust-predicates": "^3.0.2" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -2623,6 +3558,15 @@ "url": "https://github.com/fb55/domhandler?sponsor=1" } }, + "node_modules/dompurify": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.2.tgz", + "integrity": "sha512-lHeS9SA/IKeIFFyYciHBr2n0v1VMPlSj843HdLOwjb2OxNwdq9Xykxqhk+FE42MzAdHvInbAolSE4mhahPpjXA==", + "license": "(MPL-2.0 OR Apache-2.0)", + "optionalDependencies": { + "@types/trusted-types": "^2.0.7" + } + }, "node_modules/domutils": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", @@ -2972,6 +3916,12 @@ "uncrypto": "^0.1.3" } }, + "node_modules/hachure-fill": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/hachure-fill/-/hachure-fill-0.5.2.tgz", + "integrity": "sha512-3GKBOn+m2LX9iq+JC1064cSFprJY4jL1jCXTcpnfER5HYE2l/4EfWSGzkPa/ZDBmYI0ZOEj5VHV/eKnPGkHuOg==", + "license": "MIT" + }, "node_modules/hast-util-embedded": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", @@ -3005,6 +3955,21 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-from-dom": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/hast-util-from-dom/-/hast-util-from-dom-5.0.1.tgz", + "integrity": "sha512-N+LqofjR2zuzTjCPzyDUdSshy4Ma6li7p/c3pA78uTwzFgENbgbUrm2ugwsOdcjI1muO+o6Dgzp9p8WHtn/39Q==", + "license": "ISC", + "dependencies": { + "@types/hast": "^3.0.0", + "hastscript": "^9.0.0", + "web-namespaces": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-from-html": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/hast-util-from-html/-/hast-util-from-html-2.0.3.tgz", @@ -3023,6 +3988,22 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hast-util-from-html-isomorphic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hast-util-from-html-isomorphic/-/hast-util-from-html-isomorphic-2.0.0.tgz", + "integrity": "sha512-zJfpXq44yff2hmE0XmwEOzdWin5xwH+QIhMLOScpX91e/NSGPsAzNCvLQDIEPyO2TXi+lBmU6hjLIhV8MwP2kw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-dom": "^5.0.0", + "hast-util-from-html": "^2.0.0", + "unist-util-remove-position": "^5.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/hast-util-from-parse5": { "version": "8.0.3", "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", @@ -3392,12 +4373,33 @@ "@babel/runtime": "^7.23.2" } }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/inline-style-parser": { "version": "0.2.7", "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", "license": "MIT" }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/iron-webcrypto": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/iron-webcrypto/-/iron-webcrypto-1.2.1.tgz", @@ -3538,6 +4540,36 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/katex": { + "version": "0.16.45", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.45.tgz", + "integrity": "sha512-pQpZbdBu7wCTmQUh7ufPmLr0pFoObnGUoL/yhtwJDgmmQpbkg/0HSVti25Fu4rmd1oCR6NGWe9vqTWuWv3GcNA==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, + "node_modules/katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/khroma": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/khroma/-/khroma-2.1.0.tgz", + "integrity": "sha512-Ls993zuzfayK269Svk9hzpeGUKob/sIgZzyHYdjQoAdQetRKpOLj+k/QQQ/6Qi0Yz65mlROrfd+Ev+1+7dz9Kw==" + }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -3547,6 +4579,36 @@ "node": ">= 8" } }, + "node_modules/langium": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/langium/-/langium-4.2.3.tgz", + "integrity": "sha512-sOPIi4hISFnY7twwV97ca1TsxpBtXq0URu/LL1AvxwccPG/RIBBlKS7a/f/EL6w8lTNaS0EFs/F+IdSOaqYpng==", + "license": "MIT", + "dependencies": { + "@chevrotain/regexp-to-ast": "~12.0.0", + "chevrotain": "~12.0.0", + "chevrotain-allstar": "~0.4.3", + "vscode-languageserver": "~9.0.1", + "vscode-languageserver-textdocument": "~1.0.11", + "vscode-uri": "~3.1.0" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/layout-base": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/layout-base/-/layout-base-1.0.2.tgz", + "integrity": "sha512-8h2oVEZNktL4BH2JCOI90iD1yXwL6iNW7KcCKT2QZgQJR2vbqDsldCTPRU9NifTCqHZci57XvQQ15YTu+sTYPg==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz", + "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==", + "license": "MIT" + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -3608,6 +4670,18 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/marked": { + "version": "16.4.2", + "resolved": "https://registry.npmjs.org/marked/-/marked-16.4.2.tgz", + "integrity": "sha512-TI3V8YYWvkVf3KJe1dRkpnjs68JUPyEa5vjKrp1XEEJUAOaQc+Qj+L1qWbPd0SJuAdQkFU0h73sXXqwDYxsiDA==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/mdast-util-definitions": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/mdast-util-definitions/-/mdast-util-definitions-6.0.0.tgz", @@ -3937,6 +5011,57 @@ "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", "license": "CC0-1.0" }, + "node_modules/mermaid": { + "version": "11.14.0", + "resolved": "https://registry.npmjs.org/mermaid/-/mermaid-11.14.0.tgz", + "integrity": "sha512-GSGloRsBs+JINmmhl0JDwjpuezCsHB4WGI4NASHxL3fHo3o/BRXTxhDLKnln8/Q0lRFRyDdEjmk1/d5Sn1Xz8g==", + "license": "MIT", + "dependencies": { + "@braintree/sanitize-url": "^7.1.1", + "@iconify/utils": "^3.0.2", + "@mermaid-js/parser": "^1.1.0", + "@types/d3": "^7.4.3", + "@upsetjs/venn.js": "^2.0.0", + "cytoscape": "^3.33.1", + "cytoscape-cose-bilkent": "^4.1.0", + "cytoscape-fcose": "^2.2.0", + "d3": "^7.9.0", + "d3-sankey": "^0.12.3", + "dagre-d3-es": "7.0.14", + "dayjs": "^1.11.19", + "dompurify": "^3.3.1", + "katex": "^0.16.25", + "khroma": "^2.1.0", + "lodash-es": "^4.17.23", + "marked": "^16.3.0", + "roughjs": "^4.6.6", + "stylis": "^4.3.6", + "ts-dedent": "^2.2.0", + "uuid": "^11.1.0" + } + }, + "node_modules/mermaid-isomorphic": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mermaid-isomorphic/-/mermaid-isomorphic-3.1.0.tgz", + "integrity": "sha512-mzrvfEVjnJIkJlEqxp3eMuR1wS0TeLCH1VK5E/T5yzWaBwI3JqjJuw70yUIThSCDJ5bRs6O3rgfp00oBAbvSeQ==", + "license": "MIT", + "dependencies": { + "@fortawesome/fontawesome-free": "^6.0.0", + "katex": "^0.16.0", + "mermaid": "^11.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "playwright": "1" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + } + } + }, "node_modules/micromark": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", @@ -4673,6 +5798,27 @@ ], "license": "MIT" }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "license": "MIT", + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, + "node_modules/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, "node_modules/mrmime": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", @@ -4927,6 +6073,18 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, + "node_modules/path-data-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/path-data-parser/-/path-data-parser-0.1.0.tgz", + "integrity": "sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==", + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, "node_modules/piccolore": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/piccolore/-/piccolore-0.1.3.tgz", @@ -4951,11 +6109,22 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, "node_modules/playwright": { "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "playwright-core": "1.59.1" @@ -4974,7 +6143,7 @@ "version": "1.59.1", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -4983,6 +6152,22 @@ "node": ">=18" } }, + "node_modules/points-on-curve": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/points-on-curve/-/points-on-curve-0.2.0.tgz", + "integrity": "sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==", + "license": "MIT" + }, + "node_modules/points-on-path": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/points-on-path/-/points-on-path-0.2.1.tgz", + "integrity": "sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==", + "license": "MIT", + "dependencies": { + "path-data-parser": "0.1.0", + "points-on-curve": "0.2.0" + } + }, "node_modules/postcss": { "version": "8.5.13", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.13.tgz", @@ -5217,6 +6402,34 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/rehype-mermaid": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/rehype-mermaid/-/rehype-mermaid-3.0.0.tgz", + "integrity": "sha512-fxrD5E4Fa1WXUjmjNDvLOMT4XB1WaxcfycFIWiYU0yEMQhcTDElc9aDFnbDFRLxG1Cfo1I3mfD5kg4sjlWaB+Q==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "hast-util-from-html-isomorphic": "^2.0.0", + "hast-util-to-text": "^4.0.0", + "mermaid-isomorphic": "^3.0.0", + "mini-svg-data-uri": "^1.0.0", + "space-separated-tokens": "^2.0.0", + "unified": "^11.0.0", + "unist-util-visit-parents": "^6.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/remcohaszing" + }, + "peerDependencies": { + "playwright": "1" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": true + } + } + }, "node_modules/rehype-parse": { "version": "9.0.1", "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", @@ -5449,6 +6662,12 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/robust-predicates": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/robust-predicates/-/robust-predicates-3.0.3.tgz", + "integrity": "sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==", + "license": "Unlicense" + }, "node_modules/rollup": { "version": "4.60.2", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", @@ -5493,6 +6712,30 @@ "fsevents": "~2.3.2" } }, + "node_modules/roughjs": { + "version": "4.6.6", + "resolved": "https://registry.npmjs.org/roughjs/-/roughjs-4.6.6.tgz", + "integrity": "sha512-ZUz/69+SYpFN/g/lUlo2FXcIjRkSu3nDarreVdGGndHEBJ6cXPdKguS8JGxwj5HA5xIbVKSmLgr5b3AWxtRfvQ==", + "license": "MIT", + "dependencies": { + "hachure-fill": "^0.5.2", + "path-data-parser": "^0.1.0", + "points-on-curve": "^0.2.0", + "points-on-path": "^0.2.1" + } + }, + "node_modules/rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==", + "license": "BSD-3-Clause" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, "node_modules/sax": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", @@ -5680,6 +6923,12 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/stylis": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.4.0.tgz", + "integrity": "sha512-5Z9ZpRzfuH6l/UAvCPAPUo3665Nk2wLaZU3x+TLHKVzIz33+sbJqbtrYoC3KD4/uVOr2Zp+L0LySezP9OHV9yA==", + "license": "MIT" + }, "node_modules/svgo": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", @@ -5765,6 +7014,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/ts-dedent": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ts-dedent/-/ts-dedent-2.2.0.tgz", + "integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==", + "license": "MIT", + "engines": { + "node": ">=6.10" + } + }, "node_modules/tsconfck": { "version": "3.1.6", "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", @@ -6084,6 +7342,19 @@ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", "license": "MIT" }, + "node_modules/uuid": { + "version": "11.1.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.1.tgz", + "integrity": "sha512-vIYxrBCC/N/K+Js3qSN88go7kIfNPssr/hHCesKCQNAjmgvYS2oqr69kIufEG+O4+PfezOH4EbIeHCfFov8ZgQ==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -6233,6 +7504,55 @@ } } }, + "node_modules/vscode-jsonrpc": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.0.tgz", + "integrity": "sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/vscode-languageserver": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/vscode-languageserver/-/vscode-languageserver-9.0.1.tgz", + "integrity": "sha512-woByF3PDpkHFUreUa7Hos7+pUWdeWMXRd26+ZX2A8cFx6v/JPTtd4/uN0/jB6XQHYaOlHbio03NTHCqrgG5n7g==", + "license": "MIT", + "dependencies": { + "vscode-languageserver-protocol": "3.17.5" + }, + "bin": { + "installServerIntoExtension": "bin/installServerIntoExtension" + } + }, + "node_modules/vscode-languageserver-protocol": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.17.5.tgz", + "integrity": "sha512-mb1bvRJN8SVznADSGWM9u/b07H7Ecg0I3OgXDuLdn307rl/J3A9YD6/eYOssqhecL27hK1IPZAsaqh00i/Jljg==", + "license": "MIT", + "dependencies": { + "vscode-jsonrpc": "8.2.0", + "vscode-languageserver-types": "3.17.5" + } + }, + "node_modules/vscode-languageserver-textdocument": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/vscode-languageserver-textdocument/-/vscode-languageserver-textdocument-1.0.12.tgz", + "integrity": "sha512-cxWNPesCnQCcMPeenjKKsOCKQZ/L6Tv19DTRIGuLWe32lyzWhihGVJ/rcckZXJxfdKCFvRLS3fpBIsV/ZGX4zA==", + "license": "MIT" + }, + "node_modules/vscode-languageserver-types": { + "version": "3.17.5", + "resolved": "https://registry.npmjs.org/vscode-languageserver-types/-/vscode-languageserver-types-3.17.5.tgz", + "integrity": "sha512-Ld1VelNuX9pdF39h2Hgaeb5hEZM2Z3jUrrMgWQAu82jMtZp7p3vJT3BzToKtZI7NgQssZje5o0zryOrhQvzQAg==", + "license": "MIT" + }, + "node_modules/vscode-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", + "integrity": "sha512-/BpdSx+yCQGnCvecbyXdxHDkuk55/G3xwnC0GqY4gmQ3j+A+g8kzzgB4Nk/SINjqn6+waqw3EgbVF2QKExkRxQ==", + "license": "MIT" + }, "node_modules/web-namespaces": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", diff --git a/package.json b/package.json index 2d888f4..671c929 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,10 @@ "copy-screenshots": "mkdir -p public/screenshots/output && rsync -a --delete screenshots/output/ public/screenshots/output/" }, "dependencies": { - "astro": "^6.2.0", "@astrojs/starlight": "^0.38.4", + "astro": "^6.2.0", + "mermaid": "^11.14.0", + "rehype-mermaid": "^3.0.0", "sharp": "^0.34.0" }, "devDependencies": { diff --git a/screenshots/output/chat-intro-greeting.png b/screenshots/output/chat-intro-greeting.png new file mode 100644 index 0000000..a4502ad Binary files /dev/null and b/screenshots/output/chat-intro-greeting.png differ diff --git a/screenshots/output/chat-profile-questions.png b/screenshots/output/chat-profile-questions.png new file mode 100644 index 0000000..8023a5c Binary files /dev/null and b/screenshots/output/chat-profile-questions.png differ diff --git a/screenshots/output/chat-profile-update-soul.md.png b/screenshots/output/chat-profile-update-soul.md.png new file mode 100644 index 0000000..456808d Binary files /dev/null and b/screenshots/output/chat-profile-update-soul.md.png differ diff --git a/screenshots/output/chat-session-start.png b/screenshots/output/chat-session-start.png new file mode 100644 index 0000000..37338aa Binary files /dev/null and b/screenshots/output/chat-session-start.png differ diff --git a/screenshots/output/discord-setup-applications.png b/screenshots/output/discord-setup-applications.png new file mode 100644 index 0000000..f75b187 Binary files /dev/null and b/screenshots/output/discord-setup-applications.png differ diff --git a/screenshots/output/discord-setup-bot-intents.png b/screenshots/output/discord-setup-bot-intents.png new file mode 100644 index 0000000..2c36e8c Binary files /dev/null and b/screenshots/output/discord-setup-bot-intents.png differ diff --git a/screenshots/output/discord-setup-bot-permissions.png b/screenshots/output/discord-setup-bot-permissions.png new file mode 100644 index 0000000..9993758 Binary files /dev/null and b/screenshots/output/discord-setup-bot-permissions.png differ diff --git a/screenshots/output/discord-setup-bot-settings.png b/screenshots/output/discord-setup-bot-settings.png new file mode 100644 index 0000000..d34e4f7 Binary files /dev/null and b/screenshots/output/discord-setup-bot-settings.png differ diff --git a/screenshots/output/discord-setup-create-app.png b/screenshots/output/discord-setup-create-app.png new file mode 100644 index 0000000..16121f5 Binary files /dev/null and b/screenshots/output/discord-setup-create-app.png differ diff --git a/screenshots/output/discord-setup-install-dialog.png b/screenshots/output/discord-setup-install-dialog.png new file mode 100644 index 0000000..e1cc8b0 Binary files /dev/null and b/screenshots/output/discord-setup-install-dialog.png differ diff --git a/screenshots/output/discord-setup-installation-none.png b/screenshots/output/discord-setup-installation-none.png new file mode 100644 index 0000000..d39597e Binary files /dev/null and b/screenshots/output/discord-setup-installation-none.png differ diff --git a/screenshots/output/discord-setup-installation-provided-link.png b/screenshots/output/discord-setup-installation-provided-link.png new file mode 100644 index 0000000..3e6c7fb Binary files /dev/null and b/screenshots/output/discord-setup-installation-provided-link.png differ diff --git a/screenshots/output/discord-setup-oauth-scopes.png b/screenshots/output/discord-setup-oauth-scopes.png new file mode 100644 index 0000000..46ee5c1 Binary files /dev/null and b/screenshots/output/discord-setup-oauth-scopes.png differ diff --git a/screenshots/output/discord-setup-oauth-url.png b/screenshots/output/discord-setup-oauth-url.png new file mode 100644 index 0000000..215d250 Binary files /dev/null and b/screenshots/output/discord-setup-oauth-url.png differ diff --git a/screenshots/output/doctor.gif b/screenshots/output/doctor.gif new file mode 100644 index 0000000..d713b02 Binary files /dev/null and b/screenshots/output/doctor.gif differ diff --git a/screenshots/output/init-step1-provider-endpoint.png b/screenshots/output/init-step1-provider-endpoint.png new file mode 100644 index 0000000..4859e86 Binary files /dev/null and b/screenshots/output/init-step1-provider-endpoint.png differ diff --git a/screenshots/output/init-step1-provider-model.png b/screenshots/output/init-step1-provider-model.png new file mode 100644 index 0000000..ecd08e7 Binary files /dev/null and b/screenshots/output/init-step1-provider-model.png differ diff --git a/screenshots/output/init-step10-healthcheck.png b/screenshots/output/init-step10-healthcheck.png new file mode 100644 index 0000000..4573923 Binary files /dev/null and b/screenshots/output/init-step10-healthcheck.png differ diff --git a/screenshots/output/init-step2-security-posture.png b/screenshots/output/init-step2-security-posture.png new file mode 100644 index 0000000..4b8b40e Binary files /dev/null and b/screenshots/output/init-step2-security-posture.png differ diff --git a/screenshots/output/init-step3-channels.png b/screenshots/output/init-step3-channels.png new file mode 100644 index 0000000..4ab97f4 Binary files /dev/null and b/screenshots/output/init-step3-channels.png differ diff --git a/screenshots/output/init-step3-slack-app-token.png b/screenshots/output/init-step3-slack-app-token.png new file mode 100644 index 0000000..e782c83 Binary files /dev/null and b/screenshots/output/init-step3-slack-app-token.png differ diff --git a/screenshots/output/init-step3-slack-bot-token.png b/screenshots/output/init-step3-slack-bot-token.png new file mode 100644 index 0000000..b6a747b Binary files /dev/null and b/screenshots/output/init-step3-slack-bot-token.png differ diff --git a/screenshots/output/init-step3-slack-channel-names.png b/screenshots/output/init-step3-slack-channel-names.png new file mode 100644 index 0000000..44a8ca2 Binary files /dev/null and b/screenshots/output/init-step3-slack-channel-names.png differ diff --git a/screenshots/output/init-step7-communication-style.png b/screenshots/output/init-step7-communication-style.png new file mode 100644 index 0000000..c90d81e Binary files /dev/null and b/screenshots/output/init-step7-communication-style.png differ diff --git a/screenshots/output/init-step7-your-name.png b/screenshots/output/init-step7-your-name.png new file mode 100644 index 0000000..ae906c2 Binary files /dev/null and b/screenshots/output/init-step7-your-name.png differ diff --git a/screenshots/output/init-step8-skill-feeds-prompt.png b/screenshots/output/init-step8-skill-feeds-prompt.png new file mode 100644 index 0000000..f966a5b Binary files /dev/null and b/screenshots/output/init-step8-skill-feeds-prompt.png differ diff --git a/screenshots/output/init-step9-exposure-mode.png b/screenshots/output/init-step9-exposure-mode.png new file mode 100644 index 0000000..947e151 Binary files /dev/null and b/screenshots/output/init-step9-exposure-mode.png differ diff --git a/screenshots/output/mcp-tools-server-list-2.png b/screenshots/output/mcp-tools-server-list-2.png new file mode 100644 index 0000000..0797d99 Binary files /dev/null and b/screenshots/output/mcp-tools-server-list-2.png differ diff --git a/screenshots/output/mcp-tools-server2-personal.png b/screenshots/output/mcp-tools-server2-personal.png new file mode 100644 index 0000000..3d95516 Binary files /dev/null and b/screenshots/output/mcp-tools-server2-personal.png differ diff --git a/screenshots/output/mcp-tools.gif b/screenshots/output/mcp-tools.gif new file mode 100644 index 0000000..2900c5f Binary files /dev/null and b/screenshots/output/mcp-tools.gif differ diff --git a/screenshots/output/model.gif b/screenshots/output/model.gif new file mode 100644 index 0000000..a629cf6 Binary files /dev/null and b/screenshots/output/model.gif differ diff --git a/screenshots/output/provider.gif b/screenshots/output/provider.gif new file mode 100644 index 0000000..8de1fce Binary files /dev/null and b/screenshots/output/provider.gif differ diff --git a/screenshots/output/reminder.gif b/screenshots/output/reminder.gif new file mode 100644 index 0000000..d4fd796 Binary files /dev/null and b/screenshots/output/reminder.gif differ diff --git a/screenshots/output/single-shot-chat-web-fetch.png b/screenshots/output/single-shot-chat-web-fetch.png new file mode 100644 index 0000000..7aa7db5 Binary files /dev/null and b/screenshots/output/single-shot-chat-web-fetch.png differ diff --git a/screenshots/output/single-shot-cli-execution.png b/screenshots/output/single-shot-cli-execution.png new file mode 100644 index 0000000..efe878c Binary files /dev/null and b/screenshots/output/single-shot-cli-execution.png differ diff --git a/screenshots/output/slack-channel-integration.png b/screenshots/output/slack-channel-integration.png new file mode 100644 index 0000000..c7d2abf Binary files /dev/null and b/screenshots/output/slack-channel-integration.png differ diff --git a/screenshots/output/slack-generate-app-token.png b/screenshots/output/slack-generate-app-token.png new file mode 100644 index 0000000..74e8a5f Binary files /dev/null and b/screenshots/output/slack-generate-app-token.png differ diff --git a/screenshots/output/slack-install-app.png b/screenshots/output/slack-install-app.png new file mode 100644 index 0000000..f5b1b07 Binary files /dev/null and b/screenshots/output/slack-install-app.png differ diff --git a/screenshots/output/slack-your-apps.png b/screenshots/output/slack-your-apps.png new file mode 100644 index 0000000..a651a2e Binary files /dev/null and b/screenshots/output/slack-your-apps.png differ diff --git a/screenshots/output/stats.gif b/screenshots/output/stats.gif new file mode 100644 index 0000000..7c9df47 Binary files /dev/null and b/screenshots/output/stats.gif differ diff --git a/screenshots/output/status.gif b/screenshots/output/status.gif new file mode 100644 index 0000000..4f7c742 Binary files /dev/null and b/screenshots/output/status.gif differ diff --git a/screenshots/output/webhooks.gif b/screenshots/output/webhooks.gif new file mode 100644 index 0000000..0e9b8fe Binary files /dev/null and b/screenshots/output/webhooks.gif differ diff --git a/scripts/screenshot-page.mjs b/scripts/screenshot-page.mjs new file mode 100755 index 0000000..9c2cdf3 --- /dev/null +++ b/scripts/screenshot-page.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +// Capture a full-page screenshot of a rendered documentation page. +// +// Usage: node scripts/screenshot-page.mjs +// Example: node scripts/screenshot-page.mjs cli/status +// +// Requires: npm run build (must be run first) +// Output: docs-logs/screenshots/.png +// Timeout: 60 seconds total, then kills everything and exits non-zero. + +import { chromium } from 'playwright'; +import { spawn } from 'child_process'; +import { mkdirSync, existsSync } from 'fs'; +import { dirname, join } from 'path'; +import { fileURLToPath } from 'url'; +import { createServer } from 'net'; + +const TIMEOUT_MS = 60_000; + +const __dirname = dirname(fileURLToPath(import.meta.url)); +const projectDir = join(__dirname, '..'); +const slug = process.argv[2]; + +if (!slug) { + console.error('Usage: node scripts/screenshot-page.mjs '); + console.error('Example: node scripts/screenshot-page.mjs cli/status'); + process.exit(1); +} + +const outputDir = join(projectDir, 'docs-logs', 'screenshots'); +const outputFile = join(outputDir, slug.replace(/\//g, '-') + '.png'); +mkdirSync(outputDir, { recursive: true }); + +const distDir = join(projectDir, 'dist'); +if (!existsSync(distDir)) { + console.error('dist/ not found — run npm run build first'); + process.exit(1); +} + +function findFreePort() { + return new Promise((resolve, reject) => { + const srv = createServer(); + srv.listen(0, () => { + const port = srv.address().port; + srv.close(() => resolve(port)); + }); + srv.on('error', reject); + }); +} + +let preview; +const killTimer = setTimeout(() => { + console.error('Screenshot timed out after 60s'); + if (preview) preview.kill('SIGKILL'); + process.exit(1); +}, TIMEOUT_MS); + +try { + const port = await findFreePort(); + const url = `http://localhost:${port}/${slug}/`; + + preview = spawn('npx', ['astro', 'preview', '--port', String(port)], { + cwd: projectDir, + stdio: ['ignore', 'pipe', 'pipe'], + }); + + const start = Date.now(); + while (Date.now() - start < 15_000) { + try { + const res = await fetch(url); + if (res.ok) break; + } catch {} + await new Promise(r => setTimeout(r, 300)); + } + + const browser = await chromium.launch(); + const page = await browser.newPage({ viewport: { width: 1280, height: 900 } }); + await page.goto(url, { waitUntil: 'networkidle', timeout: 15_000 }); + await page.waitForTimeout(500); + await page.screenshot({ path: outputFile, fullPage: true }); + await browser.close(); + + console.log(outputFile); +} catch (err) { + console.error('Screenshot failed:', err.message); + process.exit(1); +} finally { + clearTimeout(killTimer); + if (preview) preview.kill('SIGTERM'); +} diff --git a/scripts/write-docs.sh b/scripts/write-docs.sh new file mode 100755 index 0000000..28d271a --- /dev/null +++ b/scripts/write-docs.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Write documentation pages autonomously using Claude Code + OpenProse. +# Each page gets its own context window (fresh session) running the +# write-doc-page.prose workflow. +# +# Usage: +# ./scripts/write-docs.sh # Run all autonomous batches (1-7) +# ./scripts/write-docs.sh --batch 1 # Run batch 1 only (CLI Reference) +# ./scripts/write-docs.sh --pages cli/status cli/doctor # Run specific pages +# ./scripts/write-docs.sh --dry-run # Print pages without running + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" +QUEUE_FILE="$PROJECT_DIR/docs-queue.md" +LOG_DIR="$PROJECT_DIR/docs-logs" +PROSE_FILE="$PROJECT_DIR/.prose/write-doc-page.prose" + +mkdir -p "$LOG_DIR" + +# All autonomous pages by batch +BATCH_1=( + cli/overview cli/init cli/chat cli/sessions cli/status cli/doctor cli/stats + cli/provider cli/model cli/mcp-tools cli/webhooks cli/reminder cli/skill cli/secrets +) +BATCH_2=(security/security-model security/hardening security/secrets) +BATCH_3=( + configuration/managed-providers configuration/self-hosted-providers + configuration/models configuration/mcp-servers configuration/webhooks configuration/reminders +) +BATCH_4=(observability/operational-alerts observability/opentelemetry) +BATCH_5=(channels/troubleshooting) +BATCH_6=(deployment/docker deployment/systemd deployment/exposure-modes) +BATCH_7=( + architecture/overview architecture/security-model + channels/slack channels/discord + skills/overview skills/external-skills + guides/connecting-slack guides/mcp-tool-permissions guides/pairing-remote-devices +) + +DRY_RUN=false +PAGES=() + +# Parse arguments +while [[ $# -gt 0 ]]; do + case "$1" in + --batch) + shift + case "$1" in + 1) PAGES+=("${BATCH_1[@]}") ;; + 2) PAGES+=("${BATCH_2[@]}") ;; + 3) PAGES+=("${BATCH_3[@]}") ;; + 4) PAGES+=("${BATCH_4[@]}") ;; + 5) PAGES+=("${BATCH_5[@]}") ;; + 6) PAGES+=("${BATCH_6[@]}") ;; + 7) PAGES+=("${BATCH_7[@]}") ;; + *) echo "Unknown batch: $1"; exit 1 ;; + esac + shift + ;; + --pages) + shift + while [[ $# -gt 0 && ! "$1" =~ ^-- ]]; do + PAGES+=("$1") + shift + done + ;; + --dry-run) + DRY_RUN=true + shift + ;; + *) + echo "Unknown argument: $1" + echo "Usage: $0 [--batch N] [--pages page1 page2 ...] [--dry-run]" + exit 1 + ;; + esac +done + +# Default: all autonomous batches +if [[ ${#PAGES[@]} -eq 0 ]]; then + PAGES+=("${BATCH_1[@]}" "${BATCH_2[@]}" "${BATCH_3[@]}" "${BATCH_4[@]}" "${BATCH_5[@]}" "${BATCH_6[@]}" "${BATCH_7[@]}") +fi + +# Check if page is already done (checked off in queue) +is_done() { + local page="$1" + grep -qP "^\- \[x\] \`${page}\`" "$QUEUE_FILE" 2>/dev/null +} + +# Mark page as done in queue +mark_done() { + local page="$1" + sed -i "s/^- \[ \] \`${page//\//\\/}\`/- [x] \`${page//\//\\/}\`/" "$QUEUE_FILE" +} + +echo "=== netclaw.dev doc writer ===" +echo "Pages to process: ${#PAGES[@]}" +echo "" + +completed=0 +skipped=0 +failed=0 + +for page in "${PAGES[@]}"; do + if is_done "$page"; then + echo "[SKIP] $page (already done)" + skipped=$((skipped + 1)) + continue + fi + + if $DRY_RUN; then + echo "[DRY ] $page" + continue + fi + + echo "[RUN ] $page" + log_file="$LOG_DIR/$(echo "$page" | tr '/' '-').log" + + if claude --print --dangerously-skip-permissions -p "prose run .prose/write-doc-page.prose PAGE=$page" > "$log_file" 2>&1; then + # Check if the page was actually written (no longer "Content coming soon") + page_file="$PROJECT_DIR/src/content/docs/${page}.md" + if [[ -f "$page_file" ]] && ! grep -q "Content coming soon" "$page_file"; then + mark_done "$page" + echo "[DONE] $page" + completed=$((completed + 1)) + else + echo "[FAIL] $page (page not written or still has placeholder)" + failed=$((failed + 1)) + fi + else + echo "[FAIL] $page (claude exited non-zero)" + failed=$((failed + 1)) + fi +done + +echo "" +echo "=== Summary ===" +echo "Completed: $completed" +echo "Skipped: $skipped" +echo "Failed: $failed" +echo "Logs: $LOG_DIR/" diff --git a/src/assets/netclaw-icon-purple.svg b/src/assets/netclaw-icon-purple.svg index f2eb112..7325617 100644 --- a/src/assets/netclaw-icon-purple.svg +++ b/src/assets/netclaw-icon-purple.svg @@ -1,6 +1,5 @@ - diff --git a/src/content/docs/architecture/overview.md b/src/content/docs/architecture/overview.md index 915e71b..1d12537 100644 --- a/src/content/docs/architecture/overview.md +++ b/src/content/docs/architecture/overview.md @@ -3,4 +3,190 @@ title: "Architecture Overview" description: "Daemon + CLI architecture, Akka.NET, and design principles." --- -Content coming soon. +Netclaw is a single-process .NET 10 application that runs as an always-on autonomous agent. It takes input from Slack, Discord, webhooks, timers, and a local TUI, and treats all of them the same way: as messages routed into persistent conversation actors. + +Two binaries ship together: `netclawd` (the daemon) and `netclaw` (the CLI). The daemon does everything. The CLI is a thin client that renders the daemon's output in a terminal. If the daemon isn't running, the CLI can't do anything interesting. + +## Two binaries + +### `netclawd` — the daemon + +The daemon is an ASP.NET Core application that owns all agent logic: Akka.NET actor system, LLM sessions, tool execution, channel adapters (Slack/Discord), SignalR hub, REST API, and SQLite persistence. + +It binds to `http://127.0.0.1:5199` (loopback only) by default. A PID file at `~/.netclaw/netclaw.pid` and a lock file enforce single-instance — one daemon per machine. + +### `netclaw` — the CLI + +A Termina-based TUI. No actor system, no persistence, no tool execution. It connects to the daemon over SignalR and renders what comes back. + +The CLI binary includes a `daemon` subcommand (`netclaw daemon start`, `netclaw daemon stop`) for managing the `netclawd` process. Think of `netclaw` as both the management interface and the interactive shell for a daemon that runs independently. + +The split matters: restart the CLI without interrupting conversations. Run the daemon in Docker and the CLI on your host. Swap the CLI out entirely for the web UI or a channel adapter. + +![Netclaw status output showing a running daemon](/screenshots/output/status.png) + +`netclaw status` showing daemon state, active connectors, uptime, and current model. + +## Communication + +The CLI talks to the daemon over two protocols: + +| Protocol | Path | Purpose | +|----------|------|---------| +| **SignalR** | `/hub/session` | Real-time session interaction: create sessions, send messages, receive streaming output | +| **REST** | `/api/*` | Non-session operations: health, stats, device pairing, MCP queries, shutdown | + +SignalR carries the interactive traffic. The client sends commands (`CreateSession`, `SendMessage`, `CompactSession`) and receives typed `SessionOutputDto` events back as a stream. REST handles everything that doesn't need real-time delivery. + +## Akka.NET internals + +[Akka.NET](https://getakka.net/) is the concurrency and persistence layer. Every conversation is an actor. Every channel adapter is an actor. Background jobs, reminders, tool approvals — all actors. + +### Key actors + +| Actor | Role | +|-------|------| +| `LlmSessionActor` | One per conversation thread, keyed by channel + thread identifier. Persistent, with three states: Ready, Processing, Compacting. | +| `SessionManager` | Routes messages to the right session actor via child-per-entity routing (no lookup table). | +| `ReminderManagerActor` | Scheduled tasks, backed by Akka.Reminders + SQLite. | +| `BackgroundJobManagerActor` | Async shell execution. | +| `SubAgentActor` | Child of a session actor. Handles delegated work (research, code analysis, summarization). | +| `ModelCapabilityActor` | Singleton that caches model capabilities at startup. | +| `ToolApprovalActor` | Approval gates for restricted tools. | + +### Why actors? + +Each conversation needs its own state, its own message queue, and crash isolation from other conversations. Actors give you that without thread management. A session actor processes one turn at a time, persists events to SQLite, and recovers its full history on restart. If one conversation crashes, the rest keep running. + +Persistence uses [Akka.Persistence.Sql](https://getakka.net/articles/persistence/overview.html) with SQLite. Events: `TurnRecorded`, `SessionTitleSet`, `SessionCompacted`. Serialization is [Google Protobuf](https://protobuf.dev/) via the `NetclawProtobufSerializer`. + +## Three boundaries + +Three logical boundaries divide the daemon (from the [runtime spec](https://github.com/netclaw-dev/netclaw/blob/main/docs/spec/SPEC-001-runtime-boundaries.md)): + +At the edge, the **gateway** receives transport events (Slack message, webhook POST, CLI input), runs policy checks, and converts them to actor commands. Nothing reaches the session layer before policy runs. + +The **session** layer owns conversation state, turn lifecycle, compaction, and persistence. Sessions communicate outward only by broadcasting messages — they have no idea whether they're talking to Slack, Discord, or the CLI. + +**Subscribers** (channel adapters and UI clients) consume those broadcasts via pub/sub. They render output and nothing else. They can't read actor internals or influence model behavior. + +Data flows one direction: gateway sends commands to sessions, sessions broadcast to subscribers. Policy errors close the gate. + +## Everything is just input + +Every input source — Slack message, webhook POST, timer, CLI chat, web UI — produces a message routed to a session actor with context-specific instructions. The actor doesn't know or care where it came from; turns process identically regardless of source. + +Adding a new channel adapter requires no changes to the session layer. + +## Channel adapters + +Slack and Discord adapters run inside the daemon as [hosted services](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/host/hosted-services). They're in-process actors that maintain persistent connections to the platform APIs, not external bridges. + +| Channel | Connection | Actors | +|---------|-----------|--------| +| **Slack** | [Socket Mode](https://api.slack.com/apis/socket-mode) via SlackNet | `SlackGatewayActor`, `SlackConversationActor`, `SlackThreadBindingActor` | +| **Discord** | WebSocket via Discord.Net | `DiscordGatewayActor`, `DiscordConversationActor`, `DiscordSessionBindingActor` | + +An [Akka.Streams](https://getakka.net/articles/streams/introduction.html) `SessionPipeline` bridges channel adapters to the actor system. + +## LLM providers + +Netclaw uses [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/ai-extensions) (`IChatClient`) as the provider abstraction. Supported providers: + +| Provider | Type key | Notes | +|----------|----------|-------| +| **Ollama** | `ollama` | Code default. Native integration, queries `/api/show` for capabilities. | +| **OpenRouter** | `openrouter` | Recommended for cloud models. Widest model catalog. | +| **Anthropic** | `anthropic` | Direct API. | +| **OpenAI** | `openai` | Direct API. | +| **OpenAI-compatible** | `openai-compatible` | Generic self-hosted endpoint. | + +At startup, the daemon queries the provider to detect context window size, vision support, and tool use capabilities. If the direct query fails, it falls back to the OpenRouter catalog, then HuggingFace metadata, then assumes text-only. + +See [Models](/configuration/models/) for configuration and [Managed Providers](/configuration/managed-providers/) / [Self-hosted Providers](/configuration/self-hosted-providers/) for provider setup. + +## Session lifecycle + +Each conversation thread maps to one persistent actor. In Slack, the key is `{channelId}/{threadTs}`; Discord uses a similar channel + thread mapping. + +**States:** Ready → Processing → (threshold check) → Ready or Compacting → Ready. + +When a session's context grows past the compaction threshold, the actor compacts in phases: clear stale tool results (Phase 1), drop old messages via extractive reduction keeping the N most recent (Phase 2), then optionally generate observation notes from the discarded window via a best-effort LLM call (Phase 3). If the LLM call fails, extractive-only results stand. Context shrinks, and the session returns to Ready. + +## Tool system + +Tools are C# methods registered at compile time via a Roslyn incremental source generator. The generator produces JSON schema and typed deserializers — no runtime reflection. `ToolRegistry` holds the catalog; `IToolExecutor` runs them. + +Access control is layered: `ToolAccessPolicy` (which tools are available), `ToolPathPolicy` (filesystem boundaries), `ShellCommandPolicy` (allowed shell commands). Feature gates control progressive exposure. + +### MCP integration + +External tool servers connect via the [Model Context Protocol](https://modelcontextprotocol.io/). `McpClientManager` handles server lifecycle including OAuth 2.1 flows. Sessions see only server summaries until a tool is explicitly requested via `search_tools` (progressive disclosure). See [MCP Servers](/configuration/mcp-servers/) for configuration. + +## Supporting systems + +The daemon keeps a [SQLite](https://www.sqlite.org/docs.html)-backed memory store in `~/.netclaw/netclaw.db`. Facts persist across sessions; a background `MemoryCurationEngine` handles pruning and deduplication. + +**Skills** are markdown files in `~/.netclaw/skills/` that extend what the agent knows how to do. Built-ins ship as embedded resources; extras sync from a CDN feed at startup. + +For delegated work — research, code analysis, summarization — a session spawns a **subagent** as a child actor. Definitions live in `~/.netclaw/agents/*.md` with YAML frontmatter (built-ins: research-assistant, code-analyst, summarizer). Max 10 tool iterations per subagent. + +Scheduled tasks go through `ReminderManagerActor`. See [Reminders](/configuration/reminders/). + +## Configuration + +Layered, with later sources winning: + +1. `~/.netclaw/config/netclaw.json` — base config +2. `~/.netclaw/config/secrets.json` — encrypted credentials (ASP.NET Core Data Protection) +3. `NETCLAW_*` environment variables — highest priority + +A `ConfigWatcherService` monitors `netclaw.json` with a 500ms debounce. Valid changes trigger a coordinated restart: close ingress, drain active sessions, persist state, restart, then warm active sessions back up. Active CLI sessions reconnect automatically after the restart completes. + +## Deployment + +The daemon runs on anything .NET 10 supports, from a Raspberry Pi to a cloud VM. Ships as a self-contained tarball: `netclaw-{version}-{os}-{arch}.tar.gz`. + +| Method | Guide | +|--------|-------| +| **systemd** (recommended for Linux) | [systemd Service](/deployment/systemd/) | +| **Docker** | [Docker Deployment](/deployment/docker/) | +| **Direct** | `netclaw daemon start` (detached process) | + +Docker runs only the daemon; the CLI stays on the host. For remote access beyond loopback, see [Exposure Modes](/deployment/exposure-modes/). + +## Design principles + +None of these are aspirational — they're constraints the codebase enforces. + +1. **Gall's Law first** — ship a simple working system before building a generalized framework. +2. **Actor transport boundary** — channel adapters consume broadcasts via pub/sub and never drive model internals directly. +3. **Serialization boundary** — Netclaw controls its own persistence schema. Never persist framework-external chat types. +4. **Security boundary** — default deny, explicit allow, fail closed. See [Security Model](/security/security-model/). +5. **Everything is just input** — every source (Slack, webhook, CLI, timer) produces a message routed to a session actor. No special-casing by channel. + +## Limitations + +No clustering or multi-node distribution. One daemon, one machine, one actor system. + +SQLite is the only persistence backend. Good for single-node; won't scale to multi-instance. + +Security is in Phase 1: all connections are loopback-only with `LocalOperator` trust (full access). Bearer token auth and device pairing for remote access are planned for a future release. + +Binary updates require a daemon restart. No hot code reload. + +## What to read next + +Setting up for the first time? Go to [Models](/configuration/models/) to configure your LLM provider. + +For production deployment, see [systemd Service](/deployment/systemd/) or [Docker Deployment](/deployment/docker/). + +For the security model, see [Security Model](/security/security-model/) — covers ACLs, trust context, and audience scoping. + +## Resources + +- [Akka.NET documentation](https://getakka.net/) — the actor model framework behind netclaw's concurrency +- [Microsoft.Extensions.AI](https://learn.microsoft.com/en-us/dotnet/ai/ai-extensions) — the `IChatClient` abstraction netclaw uses to talk to LLM providers +- [Model Context Protocol specification](https://modelcontextprotocol.io/) — standard for external tool server integration +- [SignalR documentation](https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction) — real-time communication between CLI and daemon +- [SQLite documentation](https://www.sqlite.org/docs.html) — the persistence and memory backend diff --git a/src/content/docs/architecture/security-model.md b/src/content/docs/architecture/security-model.md index ea52e4f..d979e7a 100644 --- a/src/content/docs/architecture/security-model.md +++ b/src/content/docs/architecture/security-model.md @@ -3,4 +3,134 @@ title: "Security Architecture" description: "ACL evaluation, audience scoping, and trust context." --- -Content coming soon. +Netclaw gives an LLM shell access, file access, and the ability to talk to external services. Without strong guardrails, that's a loaded weapon pointed at your infrastructure. The security architecture exists to make the agent useful without making it dangerous. + +All of this is enforced automatically from daemon startup — no additional setup required for the defaults to protect you. + +The core bet: **explicit grants are safer than implicit restrictions.** Rather than trying to enumerate everything dangerous and blocking it, netclaw starts from zero permissions and requires you to opt in to each capability. This inverts the typical "sandbox" approach — instead of poking holes in a wall, you're building up from nothing. + +## Design Decisions + +### Default-deny over default-allow + +Most tools start permissive and add restrictions. Netclaw does the opposite. A freshly initialized daemon with no config file binds to loopback, disables shell access, and limits tools to basic file operations in a temporary session directory that's wiped on session end. You build up from there. + +Misconfiguration fails safe. Forget to add a tool grant? The tool is invisible. Typo in a channel ID? That channel gets nothing. Config file corrupted? Daemon won't start. + +### Audience as the trust primitive + +Rather than per-user ACLs (which require identity infrastructure most self-hosted deployments don't have), netclaw classifies trust by **channel type**. A message from the TUI (terminal interface, launched by `netclaw chat`) is from the operator sitting at the machine — high trust. A message from Slack could be anyone in the workspace — lower trust. An unknown channel gets the lowest trust. + +Three audiences, ordered by trust: + +``` +Personal > Team > Public +``` + +![Security posture selection during netclaw init](/screenshots/output/init-02-security-posture.png) + +Posture selection during `netclaw init` — the choice that establishes the baseline trust tier for your deployment. + +This maps to how people actually deploy: you trust yourself on your own machine, partially trust your coworkers in Slack, and don't trust unknown sources at all. The exact channel-to-audience mapping and per-audience permission tables are in the [Security Model reference](/security/security-model/#trust-audiences). + +### Four layers, not one + +A single permission check is a single point of failure. Netclaw stacks four independent [defense-in-depth](https://csrc.nist.gov/glossary/term/defense_in_depth) layers: + +``` +┌─────────────────────────────────┐ +│ 1. Operation Hard Deny │ ← unconditional, not overridable +├─────────────────────────────────┤ +│ 2. Resource Hard Deny │ ← path-based, symlink-aware +├─────────────────────────────────┤ +│ 3. Tool Access Grant │ ← audience-scoped allowlist +├─────────────────────────────────┤ +│ 4. Approval Gate │ ← human in the loop +└─────────────────────────────────┘ +``` + +Each layer can only deny — none can override a denial from a layer above it. Layer 1 blocks `rm -rf /` regardless of audience or approval status. Layer 2 blocks credential access even for Personal. Layer 3 controls what the model can see. Layer 4 adds human oversight for high-risk operations. + +This is a conceptual model — the actual enforcement runs across both the gateway (for inbound message ACLs) and the session layer (for tool execution policy). What matters: no tool executes without passing all four checks, regardless of where in the code they run. See the [reference page](/security/security-model/#four-layer-invocation-stack) for exact deny lists and configuration syntax. + +### Policy before dispatch + +Inbound message ACL checks run in the gateway boundary, before messages reach the session layer. Tool execution policy (shell denies, path checks, approval gates) runs in the session's tool executor, synchronously before any tool fires. The [three-boundary design](/architecture/overview/#three-boundaries) (gateway → session → subscriber) enforces this structurally — the gateway is the only ingress point for messages, and the tool executor is the only execution path for tools. + +### Human-in-the-loop as a layer, not a crutch + +Approval gates sit at Layer 4 — after hard denies and access grants have already filtered. The human only sees requests that are structurally permitted but operationally risky. You're not approving every `ls` — you're approving `git push` to a remote. + +The approval system extracts verb-chain patterns from shell commands. A verb-chain is the leading command tokens (`git push`, `docker compose up`) without paths or flags — so approving `git push` covers `git push origin main` and `git push --force-with-lease`. Compound commands (`cmd1 && cmd2`) are split and each segment is checked independently. + +Channels that don't support interactive prompts (scheduled reminders, webhooks, headless automation) auto-deny tools that require approval — unless those tools have been persistently pre-approved or appear on the safe-list for non-interactive execution. + +## Trust Context Model + +Every inbound message carries a trust context assembled from: + +| Signal | Source | Purpose | +|--------|--------|---------| +| **Principal classification** | Channel adapter | Who is speaking? (operator, team member, external, automation) | +| **Transport authenticity** | Connection type | Can we verify the sender? (local process, verified, unverified) | +| **Payload taint** | Content origin | Where did this data come from? (trusted, community, public) | +| **Audience** | Channel type | What trust level does this channel get? | + +The audience resolves the final permission set. The other signals flow through to audit logs for post-hoc review. + +## MCP Security: Two-Gate Model + +External tool servers connecting via the [Model Context Protocol](https://modelcontextprotocol.io/) (MCP) are doubly gated: + +1. **Server allowlist** — the MCP server must be explicitly permitted for the audience +2. **Tool allowlist** — if per-tool grants are configured for that server, only listed tools are available + +When no per-tool grants are configured for a server, all tools on that server are accessible (subject to the server-level gate). Configure `McpServerToolGrants` to restrict individual tools — see [`netclaw mcp permissions`](/cli/mcp-tools/) for the operational details. + +Netclaw doesn't load MCP tools into the model's context until explicitly searched via `search_tools`. Granted-but-unused tools don't consume context window or influence behavior — the model doesn't know they exist until it asks. + +## Self-Configuration Boundaries + +The agent can modify its own personality, instructions, scheduled tasks, and project registry — but security policy is off-limits. ACL rules, exposure mode, tool grants, and audience mappings are read-only from the agent's perspective. + +This is enforced at two levels: the `config_write` tool category (one of seven grant categories in the [ACL policy](/security/security-model/#per-audience-permissions)) requires explicit grants, and the resource hard-deny layer blocks direct file access to config paths regardless of tool grants. + +## Threat Model Scope + +What the security model protects against: + +- The LLM deciding to run destructive commands +- Prompt injection via tool output or file contents attempting privilege escalation +- Overly broad tool access from misconfiguration +- Credential leakage through command output +- Unauthorized network exposure + +What it explicitly does **not** protect against: + +- A malicious operator with direct machine access (they can edit config files) +- Side-channel attacks against the LLM provider's API +- Denial of service against the daemon process itself +- Vulnerabilities in third-party MCP servers (netclaw gates access, not behavior) + +## Limitations + +- Prompt injection detection is regex-based, not semantic — novel phrasings can evade it +- Approval gates require an interactive channel — headless sessions (scheduled jobs, reminders, webhooks with no connected user) auto-deny gated tools unless persistently pre-approved +- No per-user identity within a channel — all Slack users in an allowed channel get the same audience +- Secret redaction catches known patterns only — custom formats need custom path deny rules +- No formal sandboxing (seccomp, namespaces) for shell execution — defense is policy-based, not kernel-based + +## Related + +- [Security Model reference](/security/security-model/) — operational details: exact deny lists, configuration syntax, per-audience permissions +- [Hardening](/security/hardening/) — production lockdown beyond the defaults +- [Architecture Overview](/architecture/overview/) — the three-boundary design that enforces policy-before-dispatch +- [`netclaw init`](/cli/init/) — where you first choose your security posture +- [`netclaw doctor`](/cli/doctor/) — verify what's actually being enforced at runtime + +## External Resources + +- [OWASP LLM Top 10](https://genai.owasp.org/llm-top-10/) — the attack taxonomy netclaw's security model is designed against +- [NIST AI Risk Management Framework](https://www.nist.gov/artificial-intelligence/ai-risk-management-framework) — federal guidance on AI system risk management +- [Principle of Least Privilege (POLP)](https://csrc.nist.gov/glossary/term/least_privilege) — the access control philosophy behind default-deny +- [Defense in Depth (NIST)](https://csrc.nist.gov/glossary/term/defense_in_depth) — the layered-security model behind the four-layer stack diff --git a/src/content/docs/channels/discord.md b/src/content/docs/channels/discord.md index 2b4210f..a9d70dd 100644 --- a/src/content/docs/channels/discord.md +++ b/src/content/docs/channels/discord.md @@ -3,4 +3,292 @@ title: "Discord" description: "Connect Netclaw to your Discord server." --- -Content coming soon. +Netclaw connects to Discord over the [Gateway WebSocket API](https://discord.com/developers/docs/events/gateway) -- outbound connections only, no public URLs needed. You create a Discord bot, give netclaw one token, and it shows up in your server. + +## Prerequisites + +- Netclaw installed and initialized ([`netclaw init`](/cli/init/)) +- A Discord server where you have the "Manage Server" permission +- [Developer Mode](https://support.discord.com/hc/en-us/articles/206346498) enabled in Discord (for copying IDs) + +## Create a Discord bot + +Head to [discord.com/developers/applications](https://discord.com/developers/applications) and create a new application. + +![Discord Applications page](/screenshots/output/discord-setup-applications.png) + +### 1. Create the application + +Click **New Application**, give it a name, and hit Create. + +![New Application dialog](/screenshots/output/discord-setup-create-app.png) + +### 2. Copy the bot token + +Go to **Bot** in the left sidebar. Click **Reset Token** to generate a bot token and copy it immediately -- Discord only shows it once. + +![Bot settings page](/screenshots/output/discord-setup-bot-settings.png) + +If you lose the token, you can always reset it here, but you'll need to update your netclaw config with the new value. + +### 3. Enable Message Content Intent + +Scroll down on the Bot page to **Privileged Gateway Intents** and enable **Message Content**. Without this, netclaw receives message events but can't read their text. + +![Privileged Gateway Intents with Message Content enabled](/screenshots/output/discord-setup-bot-intents.png) + +### 4. Set up OAuth2 scopes and permissions + +Go to **OAuth2 > URL Generator**. Check these scopes: + +- `bot` +- `applications.commands` + +![OAuth2 scopes selection](/screenshots/output/discord-setup-oauth-scopes.png) + +Then select bot permissions: + +![Bot permissions checklist](/screenshots/output/discord-setup-bot-permissions.png) + +**Required permissions:** Send Messages, Create Public Threads, Send Messages in Threads, Manage Threads, Embed Links, Read Message History, Add Reactions. + +Manage Threads is needed so netclaw can rename threads with session titles as conversations progress. + +### 5. Install the bot to your server + +Copy the generated URL at the bottom of the page. + +![Generated OAuth2 URL](/screenshots/output/discord-setup-oauth-url.png) + +Open it in your browser. Discord shows an authorization dialog -- select your server and click **Authorize**. + +![OAuth install approval dialog](/screenshots/output/discord-setup-install-dialog.png) + +The bot appears in your server's member list once the daemon is running. + +### 6. Set Installation to None + +Under **Installation** in the left sidebar, set the install method to **None**. This prevents users from installing the bot to other servers through Discord's app directory. + +![Installation settings set to None](/screenshots/output/discord-setup-installation-none.png) + +If you want others to install via a link you control, use **Discord Provided Link** instead. + +![Installation with Discord Provided Link](/screenshots/output/discord-setup-installation-provided-link.png) + +## Configure netclaw + +Easiest path: [`netclaw init`](/cli/init/). Step 3 handles channel selection and token entry. + +![Channel selection during netclaw init](/screenshots/output/init-03-channels.png) + +Pick Discord, paste your bot token, done. + +For manual setup, store the token with [`netclaw secrets`](/cli/secrets/): + +```bash +netclaw secrets set Discord.BotToken your-bot-token +``` + +Then enable Discord in `~/.netclaw/config/netclaw.json`: + +```json +{ + "Discord": { + "Enabled": true, + "DefaultChannelId": "123456789012345678" + } +} +``` + +Environment variables work too: + +```bash +export NETCLAW_Discord__BotToken="your-bot-token" +export NETCLAW_Discord__Enabled="true" +``` + +### All config fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Enabled` | bool | `false` | Turn on Discord | +| `BotToken` | string | -- | Bot token from the Developer Portal. Store with `netclaw secrets set`. | +| `DefaultChannelId` | string | -- | Channel ID for the default channel | +| `MentionOnly` | bool | `true` | Only respond when @-mentioned | +| `AllowDirectMessages` | bool | `false` | Accept DMs | +| `MentionRequiredInDm` | bool | `false` | Require @-mention even in DMs | +| `AllowedChannelIds` | string[] | `[]` | Channel allow-list. Empty + no default = all channel messages denied | +| `AllowedUserIds` | string[] | `[]` | User allow-list. Empty = everyone in allowed channels is accepted | +| `ChannelAudiences` | object | `{}` | Per-channel [audience](/security/security-model/) overrides. Keys are channel IDs or `"dm"`. Values: `"personal"`, `"team"`, `"public"`. | + +The token lives in `~/.netclaw/config/secrets.json` (encrypted at rest). + +## Access control + +Discord ACL is **default-deny**. Three settings decide who talks to the bot and where. + +### Channels + +The bot only responds in channels that pass the allow-list: + +- `DefaultChannelId` allows one channel +- `AllowedChannelIds` allows multiple +- **If both are empty, every channel message is denied.** This is the number one setup mistake. + +```json +{ + "Discord": { + "DefaultChannelId": "123456789012345678", + "AllowedChannelIds": ["234567890123456789", "345678901234567890"] + } +} +``` + +Finding channel IDs: right-click a channel in Discord with Developer Mode on, click "Copy Channel ID." + +### Users + +`AllowedUserIds` restricts who gets responses: + +- Empty (default) -- everyone in allowed channels is accepted +- Non-empty -- only listed user IDs get responses, everyone else is silently dropped + +Finding user IDs: right-click a user in Discord with Developer Mode on, click "Copy User ID." + +Users in `AllowedUserIds` are treated as `TrustedInternal` by the [security model](/security/security-model/). Everyone else is `UntrustedExternal`. + +### Direct messages + +DMs are off by default: + +```json +{ + "Discord": { + "AllowDirectMessages": true + } +} +``` + +With DMs on, users can just type normally -- `MentionRequiredInDm` defaults to `false`, so no @-mention needed. Each user gets a single long-running DM session (unlike channels, where each root message starts a new session). + +:::caution +With `AllowDirectMessages: true` and `AllowedUserIds` empty, any server member can DM the bot. Lock down `AllowedUserIds` if that's not what you want. +::: + +### Audience overrides + +Override the default audience per-channel with `ChannelAudiences`: + +```json +{ + "Discord": { + "ChannelAudiences": { + "123456789012345678": "team", + "dm": "personal" + } + } +} +``` + +[Security Model](/security/security-model/) has the full breakdown on how audiences map to tools and permissions. + +## Behavior in Discord + +### Threads and sessions + +When someone messages the bot in a regular channel, netclaw creates a public thread on its first reply and continues the conversation there. The thread starts as "Netclaw" and netclaw renames it once the LLM generates a session title. + +Thread replies don't need a @-mention -- if there's an active session in the thread, the bot responds to everything. + +Idle conversations are freed from memory after 2 hours (individual sessions after 1 hour, unless an approval request is pending). On daemon restart, up to 200 messages of thread history are backfilled so in-progress conversations resume. + +### Mention behavior + +`MentionOnly: true` (the default) means the bot ignores messages that don't @-mention it. Two exceptions: + +- **Thread replies** -- if a thread already has an active session, the bot responds without needing a mention +- **Daemon restart recovery** -- if the daemon restarts and a user continues a thread, the session is re-created from the thread's message history + +Netclaw strips the @-mention before passing text to the LLM. + +### Message formatting + +Discord natively renders markdown -- bold, italic, code blocks, headers, lists, links. Responses longer than 2,000 characters are split at newline boundaries. + +### Tool approval + +When a tool call needs approval, netclaw posts an interactive button prompt in the thread. The prompt shows the tool name, action, and pattern(s), with four buttons: **Approve once** (green), **Approve for this chat**, **Approve always**, and **Deny** (red). Only the user who triggered the request can approve. + +If posting the button prompt fails, netclaw falls back to a text prompt where you reply with `A`, `B`, `C`, or `D`. + +After a decision, netclaw updates the original message in-place with a checkmark or denied icon. + +### Reminders + +Reminder targets for Discord users accept several formats: + +- `<@123456789012345678>` or `<@!123456789012345678>` -- standard Discord mention +- `@123456789012345678` -- shorthand +- `123456789012345678` -- raw user ID +- `dm:123456789012345678` -- explicit DM channel + +### Proactive messaging + +Discord does not have proactive messaging tools yet. Unlike Slack's `send_slack_message` and `lookup_slack_user`, there are no equivalent tools for initiating conversations in Discord channels or looking up users by name. Reminders work, but the bot can't start new threads on its own. + +### Ignored messages + +The bot drops: empty messages (no text, no attachments), other bots' messages, its own messages, DMs when `AllowDirectMessages` is off, and un-mentioned channel messages when `MentionOnly` is on and there's no active thread. + +Messages over 4,000 characters are truncated. + +## Verify it works + +Restart the daemon and check status: + +```bash +netclaw daemon stop && netclaw daemon start +netclaw status +``` + +Discord should show `connected`. If it shows `disabled` or `disconnected`, check the [troubleshooting guide](/channels/troubleshooting/). + +Then @-mention the bot in an allowed channel. If it creates a thread and responds, you're set. + +:::note +`netclaw doctor` doesn't validate Discord tokens yet. For now, use `netclaw status` and check the daemon logs. +::: + +## Troubleshooting + +Common problems and fixes are in [Channel Troubleshooting](/channels/troubleshooting/). The hits: + +- **Connected but silent** -- `AllowedChannelIds` is empty and no `DefaultChannelId` is set, so all traffic gets denied +- **Invalid bot token** -- HTTP 401 on startup means the token is wrong or was regenerated in the Developer Portal +- **Gateway disconnected** -- the daemon couldn't establish a connection. Usually a network issue or [Discord outage](https://discordstatus.com/). +- **Message Content privileged intent not enabled** -- turn it on under Privileged Gateway Intents in the [Developer Portal](https://discord.com/developers/applications) + +## Next steps + +- [Configure audiences and approval gates](/security/security-model/) to control what tools are available in each channel +- [Set up systemd](/deployment/systemd/) so the daemon stays running after reboots +- [Add Slack](/channels/slack/) if your team uses both + +## Related pages + +- [`netclaw init`](/cli/init/) -- Discord setup at step 3 +- [`netclaw secrets`](/cli/secrets/) -- token management +- [`netclaw status`](/cli/status/) -- primary diagnostic tool for Discord +- [Security Model](/security/security-model/) -- audiences and approval gates +- [Channel Troubleshooting](/channels/troubleshooting/) -- error codes and debug logging +- [Slack](/channels/slack/) -- sibling channel integration + +## External resources + +- [Discord Developer Portal](https://discord.com/developers/applications) -- create and manage your bot +- [Discord Developer Docs: Getting Started](https://discord.com/developers/docs/getting-started) -- bot setup walkthrough +- [Discord: Privileged Intents](https://discord.com/developers/docs/events/gateway#privileged-intents) -- Message Content intent setup +- [Discord: Bot Permissions](https://discord.com/developers/docs/topics/permissions) -- permission reference +- [Discord: Enable Developer Mode](https://support.discord.com/hc/en-us/articles/206346498) -- how to copy channel and user IDs +- [Discord Status](https://discordstatus.com/) -- check for outages diff --git a/src/content/docs/channels/slack.md b/src/content/docs/channels/slack.md index 80cfaa9..69b4d80 100644 --- a/src/content/docs/channels/slack.md +++ b/src/content/docs/channels/slack.md @@ -3,4 +3,319 @@ title: "Slack" description: "Connect Netclaw to your Slack workspace." --- -Content coming soon. +Netclaw talks to Slack over [Socket Mode](https://api.slack.com/apis/socket-mode) -- outbound WebSocket connections only. No public URLs, no ingress rules, no reverse proxies. You create a Slack app, give netclaw two tokens, and it shows up in your workspace as a bot. + +## Prerequisites + +- Netclaw installed and initialized ([`netclaw init`](/cli/init/)) +- A Slack workspace where you can install apps (some orgs restrict this to workspace admins) + +## Create a Slack app + +The fastest path: create from a manifest. Go to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App**. + +![Slack Your Apps page](/screenshots/output/slack-your-apps.png) + +Select **From a manifest**, pick your workspace, and paste this: + +```json +{ + "display_information": { + "name": "Netclaw", + "description": "AI assistant powered by Netclaw", + "background_color": "#512BD4" + }, + "features": { + "bot_user": { + "display_name": "Netclaw", + "always_online": true + } + }, + "oauth_config": { + "scopes": { + "bot": [ + "app_mentions:read", + "channels:history", + "channels:read", + "chat:write", + "chat:write.customize", + "files:read", + "files:write", + "groups:history", + "groups:read", + "im:history", + "im:read", + "im:write", + "mpim:history", + "mpim:read", + "users:read" + ] + } + }, + "settings": { + "event_subscriptions": { + "bot_events": [ + "app_mention", + "message.channels", + "message.groups", + "message.im", + "message.mpim" + ] + }, + "interactivity": { + "is_enabled": true + }, + "org_deploy_enabled": false, + "socket_mode_enabled": true, + "token_rotation_enabled": false + } +} +``` + +Change the `name` and `display_name` to whatever you want your bot to be called. + +After creating the app, you need two tokens: + +1. **App-Level Token** — Settings > Basic Information > App-Level Tokens. Click **Generate Token and Scopes**, name it anything, and add the `connections:write` scope. + +![Generate an app-level token dialog](/screenshots/output/slack-generate-app-token.png) + +Click **Generate** and copy the `xapp-...` token. + +2. **Bot Token** — Go to **Install App** in the sidebar and click **Install to {Your Workspace}**. + +![Slack Install App page](/screenshots/output/slack-install-app.png) + +After approving, copy the `xoxb-...` Bot User OAuth Token from the OAuth & Permissions page. + +Then invite the bot to each channel where it should respond: `/invite @YourBotName` + +### What the scopes do + +| Scope | Why | +|-------|-----| +| `app_mentions:read` | Receive @-mention events | +| `channels:history` | Read message history in public channels | +| `channels:read` | Resolve channel names to IDs, list public channels | +| `chat:write` | Post messages and replies in threads | +| `chat:write.customize` | Post with custom display name/avatar | +| `files:read` | Download files shared in conversations | +| `files:write` | Upload files (agent output, attachments) | +| `groups:history` | Read message history in private channels | +| `groups:read` | List private channels the bot is in | +| `im:history` | Read DM history for thread context | +| `im:read` | List DM conversations | +| `im:write` | Open DM conversations for proactive messaging | +| `mpim:history` | Read group DM history | +| `mpim:read` | List group DM conversations | +| `users:read` | Look up users by name or email for `lookup_slack_user` | + +## Configure netclaw + +Easiest path: [`netclaw init`](/cli/init/). Step 3 handles channel selection and token entry. + +![Channel selection during netclaw init](/screenshots/output/init-03-channels.png) + +Pick Slack, paste your tokens, done. + +For manual setup, store tokens with [`netclaw secrets`](/cli/secrets/): + +```bash +netclaw secrets set Slack.BotToken xoxb-your-bot-token +netclaw secrets set Slack.AppToken xapp-your-app-token +``` + +Then enable Slack and point it at a channel in `~/.netclaw/config/netclaw.json`: + +```json +{ + "Slack": { + "Enabled": true, + "DefaultChannelName": "general" + } +} +``` + +Environment variables work too: + +```bash +export NETCLAW_Slack__BotToken="xoxb-..." +export NETCLAW_Slack__AppToken="xapp-..." +``` + +### All config fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Enabled` | bool | `false` | Turn on Slack | +| `SocketMode` | bool | `true` | Must be `true`. Only Socket Mode is supported. | +| `BotToken` | string | -- | Bot User OAuth Token (`xoxb-...`). Store with `netclaw secrets set`. | +| `AppToken` | string | -- | App-Level Token (`xapp-...`). Required for Socket Mode. Store with `netclaw secrets set`. | +| `DefaultChannelName` | string | -- | Channel name, resolved to an ID at startup | +| `DefaultChannelId` | string | -- | Channel ID directly (use instead of name if you prefer) | +| `MentionOnly` | bool | `true` | Only respond when @-mentioned | +| `AllowDirectMessages` | bool | `false` | Accept DMs | +| `MentionRequiredInDm` | bool | `false` | Require @-mention even in DMs | +| `AllowedChannelIds` | string[] | `[]` | Channel allow-list. Empty + no default = all channels denied | +| `AllowedUserIds` | string[] | `[]` | User allow-list. Empty = everyone in allowed channels is accepted | +| `ChannelAudiences` | object | `{}` | Per-channel [audience](/security/security-model/) overrides. Keys are channel IDs or `"dm"`. Values: `"personal"`, `"team"`, `"public"`. | + +Tokens live in `~/.netclaw/config/secrets.json` (encrypted at rest). + +## Access control + +Slack ACL is **default-deny**. Three settings decide who talks to the bot and where. + +### Channels + +The bot only responds in channels that pass the allow-list: + +- `DefaultChannelName` or `DefaultChannelId` allows one channel +- `AllowedChannelIds` allows multiple +- **If all three are empty, every channel message is denied.** This is the number one setup mistake. + +```json +{ + "Slack": { + "DefaultChannelName": "openclaw", + "AllowedChannelIds": ["C0123456789", "C9876543210"] + } +} +``` + +Finding channel IDs: right-click a channel name in Slack, "View channel details," scroll to the bottom. [Slack's help article](https://slack.com/help/articles/221769328) has screenshots. + +### Users + +`AllowedUserIds` restricts who gets responses: + +- Empty (default) -- everyone in allowed channels is accepted +- Non-empty -- only listed user IDs get responses, everyone else is silently dropped + +Finding user IDs: click a user's profile in Slack, open the three-dot menu, "Copy member ID." [Slack's help article](https://slack.com/help/articles/360003534892) has screenshots. + +Users in `AllowedUserIds` are treated as `TrustedInternal` by the [security model](/security/security-model/). Everyone else is `UntrustedExternal`. + +### Direct messages + +DMs are off by default: + +```json +{ + "Slack": { + "AllowDirectMessages": true + } +} +``` + +With DMs on, users can just type normally -- `MentionRequiredInDm` defaults to `false`, so no @-mention needed. + +:::caution +With `AllowDirectMessages: true` and `AllowedUserIds` empty, any workspace member can DM the bot. Lock down `AllowedUserIds` if that's not what you want. +::: + +### Audience overrides + +Audience is resolved per-message: `Team` for DMs and channels in your allow-list, `Public` for everything else. Override with `ChannelAudiences`: + +```json +{ + "Slack": { + "ChannelAudiences": { + "C0123456789": "team", + "dm": "personal" + } + } +} +``` + +[Security Model](/security/security-model/) has the full breakdown on how audiences map to tools and permissions. + +## Behavior in Slack + +### Threads and sessions + +Each Slack thread is its own isolated session, and the bot always replies in-thread. Idle sessions are checkpointed and freed from memory after 1 hour. + +On daemon restart, thread history is backfilled so in-progress conversations pick up where they left off. + +### Mention behavior + +`MentionOnly: true` (the default) means the bot ignores messages that don't @-mention it. Two exceptions: + +- **Thread replies** -- if a thread already has an active session, the bot responds to everything in that thread without needing a mention +- **File shares** -- attached files bypass the mention check entirely, with or without an active thread + +Netclaw strips the @-mention before passing text to the LLM. + +### Message formatting + +Netclaw converts LLM markdown to Slack [Block Kit](https://api.slack.com/block-kit): headers, code blocks, blockquotes, lists, bold, italic, strikethrough, inline code, links. Everything renders natively in Slack. + +### Tool approval + +When a tool call needs approval, netclaw posts a Block Kit prompt right in the thread: + +![Tool approval prompt in Slack showing Approve once, Approve for this chat, Approve always, and Deny buttons](/assets/approval-prompt.png) + +Shows the tool name, the exact command, and four buttons. Only the user who triggered the request can approve. System-initiated tool calls (`VerifiedAutomation`) can be approved by anyone in the thread. + +Typing a letter in the thread works too: `A` = Approve once, `B` = Approve for this chat, `C` = Approve always, `D` = Deny. + +### Proactive messaging + +The LLM can initiate conversations through two built-in tools: + +| Tool | What it does | +|------|-------------| +| `send_slack_message` | Posts a top-level message to a channel or DM. Takes `channel_id` or `user_id`. Respects ACL. | +| `lookup_slack_user` | Searches users by name, display name, or email. Returns up to 10 matches. Filtered to `AllowedUserIds` if set. Cached 5 minutes. | + +### Ignored messages + +The bot drops: empty messages (no text, no files), hidden messages, other bots' messages, its own messages, messages with unsupported subtypes (edits, joins, bot subtype messages), DMs when `AllowDirectMessages` is off, and un-mentioned channel messages when `MentionOnly` is on and there's no active thread. + +## Verify it works + +Restart the daemon and check status: + +```bash +netclaw daemon stop && netclaw daemon start +netclaw status +``` + +Slack should show `connected`. If it doesn't, run `netclaw doctor` -- it checks token validity and ACL config. + + + +Then @-mention the bot in an allowed channel. If it responds, you're set. + +## Troubleshooting + +Common problems and fixes are in [Channel Troubleshooting](/channels/troubleshooting/). The hits: + +- **Connected but silent** -- `AllowedChannelIds` is empty and no default channel is set, so all traffic gets denied +- **Works in some channels, not others** -- channel missing from `AllowedChannelIds`, or the bot hasn't been invited +- **Socket Mode keeps disconnecting** -- the `xapp-...` App-Level Token may have expired or been revoked + +## Next steps + +- [Configure audiences and approval gates](/security/security-model/) to control what tools are available in each channel +- [Set up systemd](/deployment/systemd/) so the daemon stays running after reboots +- [Run `netclaw doctor`](/cli/doctor/) to verify token health and ACL config + +## Related pages + +- [`netclaw init`](/cli/init/) -- Slack setup at step 3 +- [`netclaw secrets`](/cli/secrets/) -- token management +- [`netclaw doctor`](/cli/doctor/) -- Slack auth and ACL diagnostics +- [Security Model](/security/security-model/) -- audiences and approval gates +- [Channel Troubleshooting](/channels/troubleshooting/) -- error codes and debug logging + +## External resources + +- [Slack API: Socket Mode](https://api.slack.com/apis/socket-mode) -- how Socket Mode connections work +- [Slack API: Bot Token Scopes](https://api.slack.com/scopes) -- scope reference +- [Slack: Block Kit](https://api.slack.com/block-kit) -- message formatting +- [Slack: Finding IDs](https://slack.com/help/articles/221769328) -- channel and user IDs for ACL config +- [Slack: Finding User IDs](https://slack.com/help/articles/360003534892) -- step-by-step for copying member IDs diff --git a/src/content/docs/channels/troubleshooting.md b/src/content/docs/channels/troubleshooting.md index bb8264c..21b8f9c 100644 --- a/src/content/docs/channels/troubleshooting.md +++ b/src/content/docs/channels/troubleshooting.md @@ -1,6 +1,310 @@ --- title: "Channel Troubleshooting" -description: "Common issues with Slack and Discord channel setup." +description: "Diagnose and fix common Slack and Discord channel issues." --- -Content coming soon. +Netclaw not responding in Slack or Discord? Two commands will tell you what's wrong most of the time: + +```bash +netclaw status # live connector health (requires running daemon) +netclaw doctor # offline config and credential checks +``` + +Daemon logs live at `~/.netclaw/logs/`, or `journalctl -u netclaw` if you're running it as a systemd service. + +## Symptom Triage + +| What you're seeing | Jump to | +|---|---| +| Bot shows "disconnected" in `netclaw status` | [Authentication Errors](#authentication-errors) | +| Bot connected but never replies | [ACL and Permissions](#acl-and-permissions) | +| Bot replies to some messages but not others | [Bot Not Responding to Messages (MentionOnly)](#bot-not-responding-to-messages-mentiononly) | +| Bot was working, then stopped | [Connectivity](#connectivity) | +| Bot processes messages but replies never appear | [Message Delivery](#message-delivery) | + +![netclaw doctor running diagnostic checks including Slack Auth and Slack ACL](/screenshots/output/doctor.png) + +Doctor only checks Slack right now (Auth and ACL). For Discord, you're looking at `netclaw status` and daemon logs. + +## Authentication Errors + +### Slack: Invalid or Expired Bot Token + +`netclaw status` shows Slack as `disconnected`. Doctor's Slack Auth check fails with one of these: + +| Error | Meaning | +|-------|---------| +| `invalid_auth` | Bot token is invalid. Check your Slack app's Bot User OAuth Token. | +| `token_revoked` | Token has been revoked. Generate a new one. | +| `token_expired` | Token has expired. Generate a new one. | +| `account_inactive` | Bot account is deactivated. | +| `not_authed` | No authentication token provided. | +| `missing_scope` | Bot token lacks `channels:read` scope. | + +If you're not sure which token is bad, regenerate both from the [Slack App Management page](https://api.slack.com/apps): + +```bash +netclaw secrets set Slack.BotToken xoxb-your-new-token +netclaw secrets set Slack.AppToken xapp-your-new-token +netclaw daemon stop && netclaw daemon start +``` + +### Slack: No Bot Token Found + +Doctor reports "Slack is enabled but no bot token found." + +Run [`netclaw init`](/cli/init/) or set the token directly: + +```bash +netclaw secrets set Slack.BotToken xoxb-your-token +``` + +### Discord: Invalid Bot Token + +`netclaw status` shows Discord as `disconnected`. Daemon logs show an authentication error at startup. + +| HTTP Code | Meaning | +|-----------|---------| +| 401 | Bot token is invalid. Check your Discord application's bot token. | +| 403 (code 50001) | Bot lacks access. Ensure it has been invited to the server. | +| 403 | Access denied. Check bot permissions. | +| 404 | Resource not found. Check the ID is correct. | + +Discord uses a single bot token (not a `xoxb-`/`xapp-` pair like Slack). Regenerate it from the [Discord Developer Portal](https://discord.com/developers/applications): + +```bash +netclaw secrets set Discord.BotToken your-new-token +netclaw daemon stop && netclaw daemon start +``` + +### Discord: Connection Timeout at Startup + +Daemon start hangs, then Discord shows `disconnected`. Logs show a 30-second timeout waiting for the gateway READY event. + +The bot token may be valid but the bot hasn't been invited to any server, so Discord has nothing to initialize. Verify the bot has been added to your server with the correct OAuth2 scopes. Use Discord's [URL Generator](https://discord.com/developers/docs/topics/oauth2#bot-authorization-flow) to create an invite link with `bot` and `applications.commands` scopes. + +## ACL and Permissions + +### All Channel Traffic Denied + +Netclaw receives messages but never responds. `netclaw status` counters show `recv > 0`, `dropped > 0`, `routed = 0`. Logs show `channel_not_allowed`. + +This is the number one gotcha. When `AllowedChannelIds` is empty and no `DefaultChannelId`/`DefaultChannelName` is set, netclaw denies all channel traffic. + +Set a default channel or list specific channels in `~/.netclaw/config/netclaw.json`: + +```json +{ + "Slack": { + "DefaultChannelId": "C0123456789" + } +} +``` + +Or allow specific channels: + +```json +{ + "Slack": { + "AllowedChannelIds": ["C0123456789", "C9876543210"] + } +} +``` + +After editing config, restart the daemon: `netclaw daemon stop && netclaw daemon start` + +To find Slack channel IDs, right-click the channel name, select "View channel details," and scroll to the bottom. For Discord, enable Developer Mode in Settings > App Settings > Advanced, then right-click any channel and select "Copy Channel ID." See [Slack: Finding IDs](https://slack.com/help/articles/221769328) and [Discord: Finding IDs](https://support.discord.com/hc/en-us/articles/206346498). + +[`netclaw doctor`](/cli/doctor/) catches this one automatically. + +### User Not Allowed + +Specific users get no response. Logs show `user_not_allowed`. + +Add the user's ID to `AllowedUserIds`, or clear the list to allow all users: + +```json +{ + "Slack": { + "AllowedUserIds": ["U0123456789", "U9876543210"] + } +} +``` + +### DMs Not Working + +Direct messages to the bot get no response, but channel messages work fine. Logs show `DmNotAllowed`. + +`AllowDirectMessages` defaults to `false`. Turn it on: + +```json +{ + "Slack": { + "AllowDirectMessages": true + } +} +``` + +Doctor warns if you enable DMs with an empty `AllowedUserIds` list, since any workspace member can then DM the bot. + +### Bot Not Responding to Messages (MentionOnly) + +Bot is connected and healthy but only responds to some messages. Logs show `ChannelMentionRequired`. + +`MentionOnly` defaults to `true`, so the bot ignores messages that don't @-mention it. Either @-mention the bot every time, or turn it off: + +**Slack:** + +```json +{ + "Slack": { + "MentionOnly": false + } +} +``` + +**Discord:** + +```json +{ + "Discord": { + "MentionOnly": false, + "MentionRequiredInDm": false + } +} +``` + +`MentionRequiredInDm` controls whether DMs also need a mention (defaults to `false`). + +## Connectivity + +### Slack Socket Mode Disconnected + +`netclaw status` shows Slack as `disconnected`. Was working, then stopped. + +Restart the daemon: + +```bash +netclaw daemon stop && netclaw daemon start +``` + +If it keeps disconnecting, check the [Slack Status page](https://status.slack.com/) and your network. Make sure the App Token (`xapp-...`) is still valid; Socket Mode requires it. + +Only Socket Mode is supported. Setting `SocketMode: false` in config throws an `InvalidOperationException` at startup. + +### Discord Gateway Disconnected + +`netclaw status` shows Discord as `disconnected`. + +Restart the daemon: `netclaw daemon stop && netclaw daemon start`. If it keeps dropping, check the [Discord Status page](https://discordstatus.com/) and verify the bot token. + +### Rate Limiting + +Replies are delayed or fail intermittently. Logs show `rate_limited` (Slack) or HTTP 429 (Discord). + +Netclaw retries on its own. If rate limiting is sustained, check whether another integration shares the same bot token's rate limit budget. + +## Message Delivery + +### Slack: Reply Not Posted + +Bot processes the message (logs show session activity) but the reply never appears in Slack. + +| Error | Meaning | +|-------|---------| +| `not_in_channel` | Bot hasn't been invited to the channel. Invite it with `/invite @botname`. | +| `channel_not_found` | Channel ID is wrong or the channel was deleted. | +| `missing_scope` | Bot token lacks the required OAuth scope for posting. | +| `no_permission` | Bot doesn't have permission to post in this channel. | +| `msg_too_long` | Response exceeded Slack's message size limit. | +| `invalid_blocks` | Malformed Block Kit payload. Usually a formatting edge case in the response. | +| `too_many_attachments` | Response has too many attachments. | + +For permission errors: add the bot to the channel and make sure it has `chat:write` scope. Check your app's OAuth scopes in the [Slack API dashboard](https://api.slack.com/apps). + +### Discord: Reply Not Posted + +Bot processes the message but no reply appears in Discord. + +Check daemon logs for HTTP error codes. Usually the bot is missing `Send Messages` permission in the target channel. Fix it in your Discord server's channel permission settings. + +### Bot Reply Loop + +Bot responds to its own messages, creating an infinite loop. High `events.filtered{reason="bot_message"}` counter alongside high `events.received`. + +Netclaw filters its own messages to prevent loops, so this is rare. Restart the daemon. If the loop comes back, bot user ID detection probably failed at startup. Look for errors around the `slack.Auth.Test()` call in the logs. + +## Debug Logging + +Still stuck? Turn on debug logging in `~/.netclaw/config/netclaw.json`: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Debug" + }, + "Console": { + "Enabled": true + } + } +} +``` + +Restart the daemon, trigger the failing interaction, and look for these patterns: + +**Healthy message flow (Slack):** +1. `Routing Slack event ... to conversation ...` +2. `Routing Slack event ... to session thread actor` +3. `Accepted inbound Slack message for session queue` +4. `Received user message` +5. `Posted Slack reply message` + +If the chain stalls at step 3, something in ACL config is rejecting the message. If it stalls at step 5, the reply couldn't be posted. Check the error tables above. + +**Quick triage commands:** + +```bash +# Daemon process status +netclaw daemon status + +# Recent crash logs +ls -lt ~/.netclaw/logs/crash-*.log + +# Recent session activity (SQLite) +sqlite3 ~/.netclaw/netclaw.db \ + "SELECT persistence_id, MAX(created), MAX(sequence_number) \ + FROM journal GROUP BY persistence_id \ + ORDER BY MAX(created) DESC LIMIT 10;" +``` + +## OTLP Diagnostic Patterns + +If [OpenTelemetry](/observability/opentelemetry/) is enabled, channel metrics tell the story faster than logs: + +| Pattern | Metrics | Likely Cause | +|---------|---------|--------------| +| Messages received, none routed | `events.received > 0`, `events.routed = 0` | ACL blocking all traffic | +| Messages routed, no replies | `messages.enqueued > 0`, `replies.posted = 0` | Outbound delivery failure | +| High drop rate | `events.dropped` spiking with `reason=channel_not_allowed` | Channel not in allow list | +| Suspected loop | High `events.filtered{reason="bot_message"}` + high `events.received` | Bot message filter working, but volume suggests upstream issue | + +Channel metrics use the namespace `netclaw.channel.slack.*` and `netclaw.channel.discord.*`. + +## Related Pages + +- [`netclaw doctor`](/cli/doctor/) — offline diagnostics +- [`netclaw status`](/cli/status/) — live connector health and message counters +- [`netclaw secrets`](/cli/secrets/) — manage encrypted tokens +- [`netclaw init`](/cli/init/) — first-run wizard for channel setup +- [OpenTelemetry](/observability/opentelemetry/) — OTLP metrics reference + +## External Resources + +- [Slack API: Bot tokens and permissions](https://api.slack.com/authentication/token-types#bot) — token types and required scopes +- [Slack API: Socket Mode](https://api.slack.com/apis/socket-mode) — how Socket Mode connections work +- [Slack: Finding Channel and User IDs](https://slack.com/help/articles/221769328) — locate IDs for ACL config +- [Discord Developer Docs: Getting Started](https://discord.com/developers/docs/getting-started) — bot setup and permissions +- [Discord: Finding IDs](https://support.discord.com/hc/en-us/articles/206346498) — enable Developer Mode to copy IDs +- [Slack Status](https://status.slack.com/) — check for Slack API outages +- [Discord Status](https://discordstatus.com/) — check for Discord API outages diff --git a/src/content/docs/cli/chat.md b/src/content/docs/cli/chat.md index 92156e7..6add81e 100644 --- a/src/content/docs/cli/chat.md +++ b/src/content/docs/cli/chat.md @@ -3,4 +3,192 @@ title: "netclaw chat" description: "Interactive chat and headless prompt mode." --- -Content coming soon. +Talk to your agent. `netclaw chat` opens a terminal UI for interactive conversations. `netclaw chat -p` sends a single prompt and exits — the scriptable version. + +Both modes require a running daemon. If it's not up, start it with `netclaw daemon start` (or run [`netclaw init`](/cli/init/) which starts it for you). The CLI is a thin [SignalR](https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction) client — rendering only. Inference, tool execution, and session state all live in the daemon. + +## Usage + +```bash +netclaw chat [options] [prompt] +``` + +## Options + +| Flag | Alias | Description | Default | +|------|-------|-------------|---------| +| `--resume ` | `-r` | Resume an existing session by ID or name | New session | +| `--prompt` | `-p` | Headless mode — send a single prompt, stream output, exit | Interactive TUI | +| `--json` | — | Output structured JSON envelope (headless only) | Plain text | +| `--help` | `-h` | Print help and exit | — | + +## Interactive TUI + +```bash +netclaw chat +``` + +The TUI has three panels: + +1. **Chat history** — scrollable, streams responses as they arrive +2. **Input** — text area that auto-sizes from 3 to 8 rows, keeps 100-message history +3. **Status bar** — keyboard hints, connection state, active model, token usage + +### Keyboard shortcuts + +| Key | Action | +|-----|--------| +| `Enter` | Send message | +| `Ctrl+Enter` | Insert newline | +| `PgUp` / `PgDn` / Mouse wheel | Scroll chat history | +| `Ctrl+Q` | Quit | +| `Escape` | Quit (ignored while generating) | + +### Status bar + +``` +[key hints] | [connection status] | [model ID] | [usage] +``` + +Status bar color reflects connection state: green = Ready/Connected, yellow = Connecting/Generating, red = Disconnected/Failed. + +Usage reads like `in=142 out=87 (12% ctx)` — input tokens, output tokens, percentage of the model's [context window](https://platform.openai.com/docs/concepts#context-window) used. + +### Tool approval + +When a tool needs approval, the input panel swaps the text area for a selection list: + +| Option | Effect | +|--------|--------| +| **Approve once** | This invocation only | +| **Approve for this chat** | Session-scoped — resets when you quit | +| **Approve always** | Persists to [`~/.netclaw/config/tool-approvals.json`](/cli/mcp-tools/) | +| **Deny** | Block this invocation | + +Arrow keys to select, `Enter` to confirm. + +### Reconnection + +If the daemon disconnects, the status bar turns red and the TUI retries with backoff (1s, 2s, 5s, 10s). Anything you type while disconnected queues up and sends on reconnect. + +### Session resume + +```bash +netclaw chat --resume abc123 +``` + +Recent messages replay as grayed-out history so you have context from where you left off. Find session IDs with [`netclaw sessions`](/cli/sessions/). You can use daemon-assigned IDs or pass your own human-readable name — `--resume daily-standup` creates a named session if it doesn't exist. + +## Headless mode + +```bash +netclaw chat -p "summarize today's alerts" +``` + +Sends one prompt, streams the response to stdout, exits when the turn completes (including any tool calls the agent makes along the way). + +### Output format + +| Prefix | Meaning | +|--------|---------| +| *(none)* | Streamed assistant text | +| `[tool:call] name(args)` | Tool invocation | +| `[tool:result] name → result` | Tool result | +| `[usage] in=N out=N total=N cached=N prompt_ms=N tok_s=N` | Token usage and timing | +| `[file] fileName → filePath` | File written by the agent | +| `[subagent:start] name (N tools)` | [Sub-agent](/architecture/overview/) spawned | +| `[subagent:done] name (success/failed, Xs)` | Sub-agent completed | +| `[compaction] N → M messages (keep=K, context=X/Y tokens)` | [Context compaction](/architecture/overview/) — the daemon summarized older messages to free up token space | + +Errors go to stderr as `[error] message`. Thinking tokens are logged to `~/.netclaw/logs/` but not written to stdout. + +### JSON output + +```bash +netclaw chat -p --json "list running services" +``` + +Outputs a single JSON object to stdout. In JSON mode, the prefixed lines above are suppressed — you get one clean object: + +```json +{ + "sessionId": "a1b2c3d4", + "response": "Here are the running services...", + "toolCalls": [ + { + "callId": "call_01", + "toolName": "shell_execute", + "argumentsJson": "{\"command\":\"systemctl list-units\"}" + } + ], + "usage": { + "inputTokens": 1420, + "outputTokens": 387, + "totalTokens": 1807, + "cachedInputTokens": 980, + "reasoningTokens": 0, + "promptMs": 142.3, + "predictedPerSecond": 48.2 + }, + "ttftMs": 342.7, + "totalMs": 2841.0 +} +``` + +Null fields are omitted — if there are no tool calls, `toolCalls` won't appear. `ttftMs` is client-side time-to-first-token; `totalMs` is the full round-trip. + +### Named sessions in headless mode + +```bash +netclaw chat -p --resume daily-report "what happened since yesterday?" +``` + +Same `--resume` ID = same conversation, with full history carried forward. + +```bash +# First turn: set context +netclaw chat -p --resume deploy-check "I'm about to deploy v2.3 to production" + +# Second turn: ask a follow-up (same session, full history) +netclaw chat -p --resume deploy-check "run the pre-deploy checklist" +``` + +### Tool approval in headless mode + +Approval-gated tools are **automatically denied** in headless mode. If your script needs a tool like shell execution, set its approval policy to `Auto` in your [tool configuration](/cli/mcp-tools/) beforehand. + +## Examples + +### Quick question + +```bash +netclaw chat -p "what's the status of the postgres backup job?" +``` + +### Pipe output into other tools + +```bash +netclaw chat -p --json "list all open incidents" | jq '.response' +``` + +### Scripted multi-turn session + +```bash +SESSION="weekly-review-$(date +%Y%m%d)" + +netclaw chat -p --resume "$SESSION" "pull this week's metrics from grafana" +netclaw chat -p --resume "$SESSION" "compare against last week and flag anomalies" +netclaw chat -p --resume "$SESSION" --json "write a summary for the team" | jq -r '.response' > report.md +``` + +## Related commands + +- [`netclaw sessions`](/cli/sessions/) — Browse and resume previous chat sessions +- [`netclaw init`](/cli/init/) — First-run setup (configures the daemon that chat connects to) +- [`netclaw status`](/cli/status/) — Check daemon health before starting a chat +- [`netclaw mcp`](/cli/mcp-tools/) — Manage MCP tool servers and approval policies + +## Resources + +- [SignalR documentation](https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction) — the real-time protocol between CLI and daemon +- [jq manual](https://jqlang.github.io/jq/manual/) — handy for wrangling `--json` output in scripts diff --git a/src/content/docs/cli/doctor.md b/src/content/docs/cli/doctor.md index c64f9ca..1546921 100644 --- a/src/content/docs/cli/doctor.md +++ b/src/content/docs/cli/doctor.md @@ -1,6 +1,203 @@ --- title: "netclaw doctor" -description: "Run configuration diagnostics." +description: "Run configuration diagnostics and auto-fix common issues." --- -Content coming soon. +`netclaw doctor` runs 16 checks against your config files, secrets, crash logs, memory state, and connectivity. It catches things [`netclaw status`](/cli/status/) won't: broken JSON schemas, unencrypted secrets, misconfigured webhooks, stale crash logs. Doesn't need a running daemon. + +## Usage + +```bash +netclaw doctor [options] +``` + +## Options + +| Flag | Description | Default | +|------|-------------|---------| +| `--fix` | Apply safe automatic fixes | Off | +| `--dry-run` | Show planned fixes without writing (implies `--fix`) | Off | +| `--yes`, `-y` | Skip confirmation prompt when applying fixes | Off | +| `--format ` | Output format | `text` | + +## Output + +```bash +netclaw doctor +``` + +![netclaw doctor running all 16 diagnostic checks](/screenshots/output/doctor.png) + +Checks print as `PASS`, `WARN`, or `FAIL`: + +``` +[PASS] Config Schema: Config matches schema v1. +[WARN] Telemetry: Telemetry is enabled without Telemetry:Otlp:Endpoint; default endpoint will be used. + fix: Set 'Telemetry:Otlp:Endpoint' (e.g. http://127.0.0.1:4317). +[WARN] Secrets JSON: secrets.json has 2 unencrypted value(s) (3 encrypted). + fix: Re-set secrets via 'netclaw secrets set ' to encrypt them. +``` + +Failures and warnings include a `fix:` line telling you what to do. Pass `--fix` to let doctor handle the ones it can repair automatically. + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | All checks passed | +| `1` | One or more checks failed | +| `2` | Warnings only, no failures | + +Note: the [CLI overview](/cli/overview/) documents exit code `2` as "usage/argument error" for most commands. `doctor` is an exception — it uses `2` specifically for warnings-only results. + +Skipped checks (Slack checks when Slack is disabled, etc.) don't affect the exit code. + +Wire it into a script: + +```bash +netclaw doctor > /dev/null 2>&1 +rc=$? +case $rc in + 0) echo "clean" ;; + 2) echo "warnings — review with 'netclaw doctor'" ;; + *) echo "failures — run 'netclaw doctor --fix'" ;; +esac +``` + +## Checks + +Doctor runs these in order. Checks that don't apply to your config get skipped. + +Config files live under `~/.netclaw/config/`: `netclaw.json` for settings, `secrets.json` for credentials. + +| Check | What it validates | +|-------|-------------------| +| **Config Schema** | `netclaw.json` parses, has valid `configVersion`, matches JSON schema | +| **Tool Audience Profiles** | Public/team/personal audience profiles don't grant excessive tool access | +| **Security Policy** | `DeploymentPosture` is set; errors if null with `StrictDefaults` disabled | +| **Slack ACL** | Warns if no channel allow-list and no default channel are configured; warns if DMs enabled with no `AllowedUserIds` | +| **Telemetry** | Warns if [OTLP](https://opentelemetry.io/docs/specs/otel/protocol/) endpoint is absent; errors if present but not a valid absolute URI (skipped if telemetry disabled) | +| **Secrets JSON** | File exists, valid JSON, correct Unix permissions (`600`), values are encrypted | +| **Slack Auth** | Live API call to verify [bot token](https://api.slack.com/authentication/token-types#bot) works | +| **SQLite Provisioning** | Inspects the most recent crash log for SQLite-related failures | +| **Daemon Crash Logs** | Checks for crash files in the last 7 days (in `~/.netclaw/logs/`) | +| **Memory Checkpoint Health** | Opens memory DB, checks pending checkpoint backlog | +| **mcp-servers** | Validates MCP server config and live status | +| **Context Window** | `Models.Main.ContextWindow` is set and valid; warns if absent (defaults to 32,768 tokens) | +| **Update** | Checks for a newer netclaw version (5s network timeout) | +| **Webhook Format** | Flags Slack webhooks using Generic format instead of Slack format | +| **Inbound Webhook Routes** | Validates route JSON files in `~/.netclaw/config/webhooks/` | +| **exposure-mode** | Validates exposure mode matches running services (`tailscaled`, `cloudflared`, etc.) | + +The **mcp-servers** check hits the daemon API for live MCP status but falls back to offline config probes if the daemon isn't running. **Slack Auth** requires network access and will fail in air-gapped environments. + +## Auto-fix + +`--fix` handles these issues in `~/.netclaw/config/netclaw.json`: + +- Missing `configVersion` (inserts `1`) +- Slack enabled with no channel allow-list *and* no default channel (inserts empty `AllowedChannelIds`) +- Telemetry enabled with no OTLP endpoint (inserts `http://127.0.0.1:4317`) +- Slack webhooks using Generic format (corrects to `"Slack"`) +- Schema mismatches: enum int-to-string conversion, removing disallowed properties, inserting missing required properties with schema defaults + +Unencrypted secrets, invalid bot tokens, and missing external services need manual work. The `fix:` lines in doctor's output tell you what to do. + +Before writing anything, `--fix` shows a diff: + +``` + --- before + +++ after + - "configVersion": null, + + "configVersion": 1, +``` + +Then prompts `Apply these fixes? [y/N]:`. + +### Preview without writing + +```bash +netclaw doctor --dry-run +``` + +Same diff output, nothing written. Good for CI. + +## JSON output + +```bash +netclaw doctor --format json +``` + +```json +{ + "exitCode": 0, + "checks": [ + { + "name": "Config Schema", + "severity": "pass", + "message": "Config matches schema v1.", + "remediation": null + } + ], + "fix": { + "requested": false, + "dryRun": false, + "changedFiles": 0, + "files": [] + } +} +``` + +Severity values are lowercase: `"pass"`, `"warning"`, `"error"`. Parse with [jq](https://jqlang.github.io/jq/manual/): + +```bash +# List all failures +netclaw doctor --format json | jq '[.checks[] | select(.severity == "error")]' + +# Count warnings +netclaw doctor --format json | jq '[.checks[] | select(.severity == "warning")] | length' +``` + +## Common workflows + +### Post-init verification + +After running [`netclaw init`](/cli/init/), run doctor right away: + +```bash +netclaw doctor +``` + +Doctor checks things that init's built-in health check skips: crash logs, memory state, webhook routes. + +### Crash triage + +When the daemon crashes or behaves unexpectedly: + +```bash +netclaw doctor +``` + +**Daemon Crash Logs** and **SQLite Provisioning** scan for recent crash files and storage-related failures. Start there before digging through raw logs. + +### CI health gate + +```bash +netclaw doctor --format json --dry-run | jq -e '.exitCode == 0' +``` + +`--dry-run` ensures no files are written — safe for read-only CI runners. Exit code still reflects real check results, so unfixed issues cause a non-zero exit. + +## Related commands + +- [`netclaw status`](/cli/status/) — live runtime health (requires the daemon; doctor doesn't) +- [`netclaw init`](/cli/init/) — first-run wizard. Run doctor right after +- [`netclaw mcp-tools`](/cli/mcp-tools/) — manage the MCP tool servers doctor validates +- [`netclaw secrets`](/cli/secrets/) — manage the secrets doctor checks for encryption + +## Resources + +- [jq manual](https://jqlang.github.io/jq/manual/) — filter and transform `--format json` output +- [jq download](https://jqlang.github.io/jq/download/) — install jq if you don't have it +- [OpenTelemetry collector docs](https://opentelemetry.io/docs/collector/) — if the Telemetry check flags your OTLP endpoint +- [Slack bot token types](https://api.slack.com/authentication/token-types#bot) — if the Slack Auth check fails diff --git a/src/content/docs/cli/init.md b/src/content/docs/cli/init.md index 3df179a..5a41650 100644 --- a/src/content/docs/cli/init.md +++ b/src/content/docs/cli/init.md @@ -1,6 +1,182 @@ --- title: "netclaw init" -description: "First-run setup wizard." +description: "Interactive setup wizard for providers, channels, security, and network exposure." --- -Content coming soon. +First-run wizard. Configures your provider, security policy, channels, identity, skills, and network exposure in one pass, then starts the daemon. + +## Usage + +```bash +netclaw init +``` + +## Options + +| Flag | Description | Default | +|------|-------------|---------| +| *(none)* | Launch the interactive wizard | — | + +## Before you begin + +Have these ready before starting: + +- **LLM provider credentials** — an [OpenRouter](https://openrouter.ai/docs) API key (easiest) or [Ollama](https://ollama.com/download) installed locally +- **Slack tokens** (if using Slack) — Bot Token (`xoxb-...`) + App Token (`xapp-...`). See the [Slack quickstart](https://api.slack.com/start/quickstart) +- **Discord bot token** (if using Discord) — from the [Discord developer portal](https://discord.com/developers/docs/getting-started) + +No other dependencies needed. + +## Wizard steps + +The wizard adapts to your choices — steps get skipped based on your security posture and feature selections. + +### 1. LLM Provider + +Pick a provider, enter credentials, select a model. + +![Provider selection list](/screenshots/output/init-01-provider-list.png) + +Netclaw supports many providers out of the box — pick one to start with here, and use [`netclaw provider`](/cli/provider/) to add more later. Self-hosted providers like Ollama need an endpoint URL: + +![Endpoint configuration](/screenshots/output/init-01-endpoint.png) + +![Ollama provider configuration](/screenshots/output/init-01-provider-ollama.png) + +Once credentials pass a connectivity check, you pick a default model: + +![Model selection](/screenshots/output/init-01-model-select.png) + +### 2. Security posture + +![Security posture selection](/screenshots/output/init-02-security-posture.png) + +| Posture | Trust Level | Shell Access | +|---------|-------------|--------------| +| **Personal** | Single-user, high trust | Enabled with approval gates | +| **Team** | Multi-user, medium trust | Off by default | +| **Public** | Untrusted users, low trust | Off | + +Every new shell command needs your sign-off the first time. You can override per-channel later. + +### 3. Feature selection + +Enable or disable memory, search, skills, scheduling, sub-agents, and webhooks. Skipped in Personal mode (everything on by default). + +### 4. Channels + +![Channel picker](/screenshots/output/init-03-channels.png) + +Slack, Discord, or both. Each one opens a sub-step for tokens and workspace config. + +Slack needs a Bot Token (`xoxb-...`) and App Token (`xapp-...`) for [Socket Mode](https://api.slack.com/apis/socket-mode). It tests connectivity before moving on. + +### 5. Web search + +![Search toggle](/screenshots/output/init-04-search.png) + +Enable web search so netclaw can pull live results during conversations. + +### 6. Browser automation + +![Browser automation toggle](/screenshots/output/init-05-browser.png) + +Activate browser automation — page fetching, screenshots, form interaction. + +### 7. Identity + +![Owner/user identity](/screenshots/output/init-06-identity-user.png) + +The owner identity determines who gets operator-level access. + +![Agent name](/screenshots/output/init-06-identity-name.png) + +![Personality style](/screenshots/output/init-06-identity-style.png) + +![Timezone](/screenshots/output/init-06-identity-timezone.png) + +![Webhook URL](/screenshots/output/init-06-identity-webhook.png) + +![Workspace configuration](/screenshots/output/init-06-identity-workspaces.png) + +Give your agent a name, pick a personality style (shapes tone in chat), set a timezone for scheduling, and optionally wire up a webhook URL for outbound notifications. The screenshots walk you through each field. + +### 8. External skills + +![Custom skills path](/screenshots/output/init-07-custom-skills-path.png) + +Point to a local directory with custom skill definitions. + +![External skills](/screenshots/output/init-07-external-skills.png) + +Add remote skill servers or additional skill packages. + +### 9. Skill feeds + +![Skill feeds](/screenshots/output/init-08-skill-feeds.png) + +Subscribe to skill feeds — curated skill collections from the community or your org. + +### 10. Network exposure + +![Exposure mode selection](/screenshots/output/init-09-exposure.png) + +| Mode | Reachability | Requires | +|------|-------------|----------| +| `local` | Loopback only (this machine) | Nothing | +| `tailscale-serve` | Your [Tailscale](https://tailscale.com/kb/) tailnet | `tailscaled` running | +| `tailscale-funnel` | Public internet via Tailscale | `tailscaled` running | +| `cloudflare-tunnel` | Public internet via [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) | `cloudflared` running | + +The internet-facing modes (`tailscale-funnel`, `cloudflare-tunnel`) make you type an explicit confirmation. They're not kidding about the warning. + +![Webhook configuration](/screenshots/output/init-09-webhooks.png) + +If you enabled webhooks, you'll configure inbound routes here. + +### 11. Health check + +![Health check running](/screenshots/output/init-10-health-check.png) + +Validates config files, tests provider connectivity, verifies channel tokens, checks tunnel prerequisites. + +![Health check complete](/screenshots/output/init-10-health-check-complete.png) + +All green? The daemon starts automatically. + +If something fails, the wizard tells you which check broke and suggests running `netclaw doctor` for detailed diagnostics. + +## What it creates + +| File | Purpose | +|------|---------| +| `~/.netclaw/config/netclaw.json` | Main configuration (includes security posture) | +| `~/.netclaw/config/secrets.json` | Encrypted credentials | +| `~/.netclaw/identity/` | Agent identity and personality | + +## After init + +```bash +# Verify everything is healthy +netclaw doctor + +# Check daemon status +netclaw status + +# Start your first conversation +netclaw chat +``` + +## Related commands + +- [`netclaw doctor`](/cli/doctor/) — Diagnose config and connectivity issues post-setup +- [`netclaw status`](/cli/status/) — Check daemon health and connector states +- [`netclaw provider`](/cli/provider/) — Change or add providers without re-running init +- [`netclaw model`](/cli/model/) — Reassign model roles without re-running init + +## Resources + +- [OpenRouter documentation](https://openrouter.ai/docs) — API keys, model catalog, rate limits +- [Slack Socket Mode](https://api.slack.com/apis/socket-mode) — How netclaw connects to Slack without a public endpoint +- [Tailscale Funnel documentation](https://tailscale.com/kb/1223/funnel/) — Exposing services to the public internet via Tailscale +- [Cloudflare Tunnel documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — Exposing services via Cloudflare's network diff --git a/src/content/docs/cli/mcp-tools.md b/src/content/docs/cli/mcp-tools.md index 4ef926f..6263eb9 100644 --- a/src/content/docs/cli/mcp-tools.md +++ b/src/content/docs/cli/mcp-tools.md @@ -3,4 +3,256 @@ title: "netclaw mcp" description: "Manage MCP servers and tool permissions." --- -Content coming soon. +Manage [Model Context Protocol](https://modelcontextprotocol.io/) servers and control which tools each audience can access. Run `netclaw mcp permissions` for an interactive TUI, or use subcommands to script server setup. + +Netclaw has three audiences — **Personal**, **Team**, and **Public** — each with independent tool grants and approval policies. New servers are **fail-closed**: all tools are blocked for every audience until you grant them. + +## Usage + +```bash +netclaw mcp permissions # launch permissions TUI +netclaw mcp [options] # CLI mode +``` + +## Permissions TUI + +```bash +netclaw mcp permissions +``` + +Requires a running daemon (`netclaw run` or `netclaw daemon start`). + +### Server list + +![MCP Permissions server list showing three connected servers: browser_playwright (22 tools), memorizer (21 tools), notion (14 tools)](/screenshots/output/mcp-tools-server-list.png) + +The TUI probes each configured server and shows connection status and tool count. Select a server to manage its tool grants. + +### Tool grid + +![Tool grid for browser_playwright showing all tools granted for the Personal audience with Auto approval](/screenshots/output/mcp-tools-personal.png) + +The Personal audience with all tools granted and Auto approval. Each row is a discovered tool — `[✓]` means granted, `[ ]` means blocked. The approval column shows whether the tool runs automatically (`Auto`), requires user confirmation (`Approval`), or is hard-blocked (`Deny`). + +Switch audiences with `←`/`→` on the Audience row. The same server can have completely different permissions per audience: + +![Team audience with server disabled and no tools granted](/screenshots/output/mcp-tools-team.png) + +Team audience — server not enabled, no tools granted. This is the default for new servers. + +![Public audience with server disabled and no tools granted](/screenshots/output/mcp-tools-public.png) + +Public audience — same default lockdown. + +### Keybindings + +| Key | Action | +|-----|--------| +| `↑` / `↓` | Navigate rows | +| `←` / `→` | Change audience or cycle approval mode | +| `Space` | Toggle tool grant on/off | +| `A` | Toggle all tools on/off | +| `E` | Toggle server enabled for current audience | +| `M` | Cycle server default approval mode | +| `P` | Cycle per-tool approval override | +| `Enter` | Done (prompts to save if there are unsaved changes) | +| `Esc` | Back to server list (quits from server list) | +| `Ctrl+Q` | Quit (discards unsaved changes) | + +Changes are saved to `~/.netclaw/config/netclaw.json`. Restart the daemon to apply. + +## Subcommands + +### `mcp add` + +```bash +# stdio transport (default) +netclaw mcp add [command] [-- args...] + +# HTTP or SSE transport +netclaw mcp add --transport http +``` + +| Flag | Description | Default | +|------|-------------|---------| +| `--transport`, `-t` | `stdio`, `http`, or `sse` | `stdio` | +| `--env KEY=VALUE` | Environment variable (repeatable) | — | +| `--header "Key: Value"` | HTTP header (repeatable) | — | +| `--client-id ` | OAuth client ID for HTTP/SSE servers | — | +| `--scope ` | OAuth scopes (space-separated) | — | +| `--grant-all` | Skip per-tool grants (all tools visible), but still write approval defaults | — | + +Server config goes to `~/.netclaw/config/netclaw.json`. Credentials (`--env` values, `--header` values) go to [`secrets.json`](/cli/secrets/). + +After adding a server, all tool grants are empty and the approval default is `Approval` for Personal and Team, `Deny` for Public. Restart the daemon to load the new server, then grant tools: + +``` +Next: run netclaw mcp permissions to grant tools and adjust approvals for 'notion'. +``` + +The server name you provide here is what appears as `mcp:` in [`netclaw status`](/cli/status/) output. + +#### Examples + +```bash +# Add a stdio MCP server +netclaw mcp add memorizer npx -y @anthropic/memorizer-mcp + +# Add an HTTP server with OAuth +netclaw mcp add notion https://mcp.notion.com --transport http \ + --client-id abc123 --scope "read write" + +# Add a server with environment variables +netclaw mcp add my-server npx my-mcp-server \ + --env API_KEY=sk-123 --env REGION=us-east-1 + +# CI/scripting — skip per-tool grant setup +netclaw mcp add my-server npx my-mcp-server --grant-all +``` + +### `mcp list` + +```bash +netclaw mcp list +``` + +``` +Name Transport Enabled Status +browser_playwright stdio yes connected (22 tools) +memorizer stdio yes connected (21 tools) +notion http yes awaiting auth — run: netclaw mcp auth notion +``` + +Shows static config plus live status from the daemon. Without a running daemon, all statuses show as unavailable. + +| Status | Meaning | +|--------|---------| +| `connected (N tools)` | Server is up, N tools discovered | +| `awaiting auth` | OAuth required — run `netclaw mcp auth ` | +| `auth failed` | OAuth token expired or invalid | +| `unreachable` | Server process crashed or network error — run [`netclaw doctor`](/cli/doctor/) to diagnose | +| `disabled` | Disabled via `netclaw mcp disable` | + +### `mcp get` + +```bash +netclaw mcp get +``` + +Shows full server config: transport, command/URL, enabled state, OAuth settings, and environment variables. Credential values are redacted (`***REDACTED***`). + +### `mcp remove` + +```bash +netclaw mcp remove +``` + +Removes the server from both `netclaw.json` and `secrets.json`. Tool grant and approval policy entries for this server are not cleaned up automatically — if you re-add a server with the same name, old permission settings will still apply. + +### `mcp enable` / `mcp disable` + +```bash +netclaw mcp enable +netclaw mcp disable +``` + +Toggle a server without removing its config. Disabled servers aren't loaded by the daemon — their tools won't appear in `netclaw mcp permissions` or be available in sessions. + +### `mcp auth` + +```bash +netclaw mcp auth +``` + +Starts the OAuth flow for an HTTP or SSE server. Opens your browser to the authorization page and prints the URL to the terminal. In headless environments, copy the printed URL manually. Times out after 5 minutes. + +Only needed for HTTP and SSE servers that require OAuth. + +### `mcp tools` + +```bash +netclaw mcp tools [--audience ] [--snapshot] [--grant ] [--revoke ] +``` + +View and script tool grants from the CLI. For interactive editing, use `netclaw mcp permissions`. + +| Flag | Description | +|------|-------------| +| `--audience ` | Filter to `personal`, `team`, or `public` | +| `--snapshot` | Populate grants from currently discovered tools (requires daemon) | +| `--grant ` | Comma-separated tool names to grant (requires `--audience`) | +| `--revoke ` | Comma-separated tool names to revoke (requires `--audience`) | + +Without `--grant`/`--revoke`, displays a table: `✓` = granted, `-` = blocked, `✱` = no per-tool filtering (all tools pass through). + +```bash +# See what's granted for team +netclaw mcp tools memorizer --audience team + +# Grant specific tools for personal use +netclaw mcp tools browser_playwright --audience personal \ + --grant "browser_navigate,browser_click" + +# Snapshot all discovered tools into the grants config +netclaw mcp tools memorizer --snapshot +``` + +## How permissions work + +Tool grants control which tools are visible to each audience. An ungranted tool doesn't exist from the model's perspective — configure grants with `netclaw mcp permissions` or `netclaw mcp tools`. + +Approval policy controls whether granted tools run automatically or need human confirmation: + +| Mode | Behavior | +|------|----------| +| `Auto` | Tool runs without asking | +| `Approval` | Prompts for confirmation before executing | +| `Deny` | Hard-blocked even if granted | + +Approval overrides are per-tool: you can set the server default to `Auto` but require `Approval` for destructive tools like `delete` or `drop_table`. + +Default policies for new servers: + +| Audience | Tool Grants | Approval Default | +|----------|-------------|------------------| +| Personal | Empty (all blocked) | Approval | +| Team | Empty (all blocked) | Approval | +| Public | Empty (all blocked) | Deny | + +## Examples + +```bash +# Full setup: add a server, grant tools, configure approvals +netclaw mcp add notion https://mcp.notion.com --transport http +netclaw mcp permissions # grant tools in the TUI + +# Check what's connected +netclaw mcp list + +# Temporarily disable a server +netclaw mcp disable notion + +# Re-enable it +netclaw mcp enable notion + +# Remove a server you no longer need +netclaw mcp remove old-server +``` + +## What's next + +After adding a server and granting permissions, restart the daemon and run [`netclaw status`](/cli/status/) to confirm the MCP connector shows healthy. If a server reports `unreachable` or `auth failed`, run [`netclaw doctor`](/cli/doctor/) to diagnose. + +## Related commands + +- [`netclaw init`](/cli/init/) — adds browser automation MCP servers during onboarding +- [`netclaw status`](/cli/status/) — shows MCP server connectivity at runtime +- [`netclaw doctor`](/cli/doctor/) — diagnoses MCP server connection issues +- [`netclaw secrets`](/cli/secrets/) — manage encrypted credentials used by MCP servers + +## Resources + +- [Model Context Protocol specification](https://spec.modelcontextprotocol.io/) — the protocol netclaw implements for tool servers +- [MCP server registry](https://github.com/modelcontextprotocol/servers) — community-maintained list of MCP servers +- [Notion MCP server](https://github.com/makenotion/notion-mcp-server) — Notion's official MCP integration +- [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) — build your own MCP server diff --git a/src/content/docs/cli/model.md b/src/content/docs/cli/model.md index b3cdab7..9e6a0ab 100644 --- a/src/content/docs/cli/model.md +++ b/src/content/docs/cli/model.md @@ -1,6 +1,190 @@ --- title: "netclaw model" -description: "Manage model assignments." +description: "Manage model assignments across roles." --- -Content coming soon. +Assign LLM models to roles: Main, Fallback, and Compaction. Run `netclaw model` for an interactive TUI, or use subcommands to script it. + +You need at least one provider configured first. If you haven't added one, see [`netclaw provider`](/cli/provider/). + +## Usage + +```bash +netclaw model # launch TUI +netclaw model [options] # CLI mode +``` + +## Model roles + +Three roles, only Main is required: + +| Role | Purpose | When unset | +|------|---------|------------| +| **Main** | Primary model for all interactions | Required — cannot be cleared | +| **Fallback** | Automatic failover when Main is unavailable (rate limits, network errors, provider outages) | Falls back to Main | +| **Compaction** | Context summarization with a cheaper/faster model | Falls back to Main | + +Role names are case-insensitive. + +Compaction runs automatically when a session's context approaches the model's token limit. + +## Model Manager TUI + +```bash +netclaw model +``` + +![Model Manager TUI showing role assignments with Main configured and Fallback/Compaction unset](/screenshots/output/model-manager.png) + +Select a role to reassign it, or use the hotkeys: + +| Key | Action | +|-----|--------| +| `↑` / `↓` | Navigate roles | +| `Enter` | Assign model to selected role | +| `D` | Discover available models from a provider | +| `C` | Clear optional role (Fallback or Compaction) | +| `Esc` | Back / Quit (from role overview) | +| `Ctrl+Q` | Quit from any screen | + +`Enter` opens the assignment flow: pick a provider, discover its models, confirm. Discovery times out after 20 seconds and shows up to 30 results. If yours isn't listed, pick "Enter model ID manually..." and type it in (e.g., `qwen3:30b`, `llama3.2:latest`). + +## Subcommands + +### `model list` + +```bash +netclaw model list +``` + +``` +Role Provider Model ID Context Window +Main remote-gpu qwen3:30b 32,768 tokens +Fallback remote-gpu qwen3:8b (default) +Compaction (not set) +``` + +Context window shows `(default)` unless you've set an explicit `--context-window` value. + +This reads from config, not from the running daemon. With no models configured, it prompts you to run `model set` or open the TUI. + +### `model set` + +```bash +netclaw model set [--context-window ] +``` + +| Flag | Description | Default | +|------|-------------|---------| +| `--context-window ` | Override context window size (positive integer) | Provider-detected | + +Use this when a local model doesn't report its context window (discovery shows `-`), or to cap usage below the model's actual maximum. + +The provider must already exist in your config. If it doesn't, the error lists your configured providers: + +``` +Error: Provider 'my-cloud' not found in configuration. +Configured providers: remote-gpu, my-anthropic +``` + +Shrinking Main's context window prints a warning because existing sessions with longer histories may fail until compacted. + +Restart the daemon for changes to take effect. + +### `model discover` + +```bash +netclaw model discover +``` + +The provider must be reachable. This queries its API live: + +``` +Model ID Context Window Cost (in/out per 1M) +claude-opus-4-1 200,000 $15.00 / $75.00 +claude-sonnet-4 200,000 $3.00 / $15.00 +gpt-4-turbo 128,000 $10.00 / $30.00 +gpt-4o 128,000 $5.00 / $15.00 + +4 model(s) found. +``` + +Cost and context window columns show `-` when the provider doesn't report them (common with Ollama and [OpenAI-compatible endpoints](/cli/provider/)). + +### `model clear` + +```bash +netclaw model clear +``` + +Clears Fallback or Compaction. Cannot clear Main: + +``` +Error: Cannot clear the main model role. Use `netclaw model set main` to change it instead. +``` + +Cleared roles are removed from the config file entirely. + +## Examples + +```bash +# Set main model on a remote Ollama server +netclaw model set main remote-gpu qwen3:30b --context-window 32768 + +# Add a smaller fallback model on the same provider +netclaw model set fallback remote-gpu qwen3:8b + +# Use a cloud model for compaction +netclaw model set compaction my-anthropic claude-sonnet-4 + +# See what models an Ollama server has available +netclaw model discover my-ollama + +# Remove the fallback assignment +netclaw model clear fallback +``` + +## Configuration + +Assignments live in `~/.netclaw/config/netclaw.json` under the `Models` key: + +```json +{ + "Models": { + "Main": { + "Provider": "remote-gpu", + "ModelId": "qwen3:30b", + "ContextWindow": 32768 + }, + "Fallback": { + "Provider": "remote-gpu", + "ModelId": "qwen3:8b", + "ContextWindow": 32768 + } + } +} +``` + +Restart the daemon afterward. + +## Exit codes + +`0` on success, `1` on invalid arguments, unknown provider, or validation failure. + +## What's next + +After setting your models, run [`netclaw status`](/cli/status/) to confirm the daemon picked them up. + +## Related commands + +- [`netclaw provider`](/cli/provider/) — add providers before assigning models +- [`netclaw status`](/cli/status/) — see which model the daemon is actually using +- [`netclaw doctor`](/cli/doctor/) — checks model config validity +- [`netclaw init`](/cli/init/) — initial model setup during onboarding + +## Resources + +- [Ollama model library](https://ollama.com/library) — browse models for local inference +- [Anthropic models](https://docs.anthropic.com/en/docs/about-claude/models) — Claude specs, pricing, context windows +- [OpenRouter catalog](https://openrouter.ai/models) — compare across providers +- [OpenAI models](https://platform.openai.com/docs/models) — GPT specs, pricing, context windows diff --git a/src/content/docs/cli/overview.md b/src/content/docs/cli/overview.md index ea5fa38..31d61c9 100644 --- a/src/content/docs/cli/overview.md +++ b/src/content/docs/cli/overview.md @@ -3,4 +3,133 @@ title: "CLI Overview" description: "Netclaw command-line interface reference." --- -Content coming soon. +The `netclaw` CLI controls your agent — configuration, diagnostics, chat, daemon lifecycle. + +```bash +netclaw [options] +``` + +The **Daemon** column in each table indicates whether the netclaw daemon (the background process that manages sessions, connectors, and model routing) must be running for that command to work. + +## Commands + +### Chat & Sessions + +| Command | Description | Daemon | TUI | +|---------|-------------|--------|-----| +| [`chat`](/cli/chat/) | Interactive terminal UI chat | Required | Yes | +| `chat -p ` | Headless single-prompt mode (supports `--resume` / `-r`, `--json`) | Required | No | +| [`sessions`](/cli/sessions/) | Browse and resume recent sessions | Required | Yes | +| `sessions --once` | List sessions without the terminal UI | Required | No | +| `sessions --json` | JSON output (implies `--once`) | Required | No | + +### First Run & Diagnostics + +| Command | Description | Daemon | TUI | +|---------|-------------|--------|-----| +| [`init`](/cli/init/) | First-run setup wizard (starts the daemon for you) | Not required | Yes | +| [`doctor`](/cli/doctor/) | Config and connectivity diagnostics; `--fix` for auto-repair | Optional | No | +| [`status`](/cli/status/) | Daemon runtime health, connector status, model info | Required | No | +| [`stats`](/cli/stats/) | Token usage, session counts, memory stats; `--tui` for dashboard | Required | No | + +### Daemon Management + +| Command | Description | +|---------|-------------| +| `daemon start` | Start daemon as a background process | +| `daemon stop` | Stop daemon gracefully | +| `daemon status` | Show daemon process status | +| `daemon install` | Install systemd user service (Linux) | +| `daemon uninstall` | Remove systemd user service (Linux) | +| `daemon pair` | Generate a pairing code (run on the host machine) | +| `daemon devices` | List paired devices | +| `daemon devices revoke ` | Revoke a paired device by name | +| `pair ` | Pair this device with a remote daemon using a pairing code (run on the remote device) | + +See the [Pairing Remote Devices](/guides/pairing-remote-devices/) guide for the full walkthrough. + +### Configuration + +| Command | Description | Daemon | TUI | +|---------|-------------|--------|-----| +| [`provider`](/cli/provider/) | Manage LLM providers | No | Yes (bare invocation) | +| [`model`](/cli/model/) | Manage model role assignments | No | Yes (bare invocation) | +| [`mcp`](/cli/mcp-tools/) | Manage [MCP](https://spec.modelcontextprotocol.io/) (Model Context Protocol) server profiles and tool permissions | Optional | Partial | +| [`webhooks`](/cli/webhooks/) | Manage inbound webhook routes | No | No | +| [`secrets`](/cli/secrets/) | Store encrypted secrets via `secrets set `. See [Secrets](/security/secrets/). | No | No | +| [`reminder`](/cli/reminder/) | Manage scheduled reminders | Required | Yes (`ui` or `tui` subcommand) | +| [`skill`](/cli/skill/) | Manage skills and external skill sources. See [Skills](/skills/overview/). | No | No | + +See [Configuration](/configuration/managed-providers/) for field references and config file details. + +### Housekeeping + +| Command | Description | +|---------|-------------| +| `update` | Check for and install CLI updates; `--check` to check only | +| `version` / `--version` / `-V` | Print version, commit hash, and build timestamp | + +## First Run + +```bash +# First-time setup (starts the daemon automatically) +netclaw init + +# Check everything is wired up +netclaw doctor + +# Verify the daemon is healthy +netclaw status +``` + +If `status` shows errors, run `netclaw doctor --fix` to auto-repair common issues. + +```bash +# Start chatting +netclaw chat +``` + +Once you're up and running, head to [First Conversation](/getting-started/first-conversation/) for a walkthrough of your first chat session. + +![netclaw status output](/screenshots/output/status.png) + +`netclaw status` shows the daemon's health, active model, and connector states. + +## Config Files + +| File | Purpose | +|------|---------| +| `~/.netclaw/config/netclaw.json` | Main configuration | +| `~/.netclaw/config/secrets.json` | Encrypted credentials overlay | +| `NETCLAW_*` env vars | Highest-priority overrides | + +See [Configuration](/configuration/managed-providers/) for field references. + +## Exit Codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | Validation, policy, or runtime failure | +| `2` | Usage or argument error | + +## Background Update Check + +Non-TUI commands silently check for updates on startup. TUI commands skip this — run `netclaw update --check` to check manually. + +## Version Output + +``` +netclaw 0.16.2 (commit 5f3f0ee, built 2026-04-30T03:18:25Z) +``` + +## Related + +- [Quickstart](/getting-started/quickstart/) — Get netclaw running from scratch +- [Installation](/getting-started/installation/) — Platform-specific install instructions +- [First Conversation](/getting-started/first-conversation/) — Walk through your first chat session +- [systemd deployment](/deployment/systemd/) — Run the daemon as a system service +- [Docker deployment](/deployment/docker/) — Containerized daemon setup +- [.NET CLI tools documentation](https://learn.microsoft.com/en-us/dotnet/core/tools/) — Background on .NET global tools, which is how netclaw is distributed +- [systemd user services](https://wiki.archlinux.org/title/Systemd/User) — Reference for `daemon install` / `daemon uninstall` on Linux +- [Model Context Protocol spec](https://spec.modelcontextprotocol.io/) — MCP specification diff --git a/src/content/docs/cli/provider.md b/src/content/docs/cli/provider.md index dbac248..08e119d 100644 --- a/src/content/docs/cli/provider.md +++ b/src/content/docs/cli/provider.md @@ -3,4 +3,147 @@ title: "netclaw provider" description: "Manage LLM providers." --- -Content coming soon. +Manage the LLM providers that netclaw talks to. Run `netclaw provider` for an interactive TUI, or use subcommands to script provider setup. + +If you haven't run `netclaw init` yet, start there — it configures your first provider. + +## Usage + +```bash +netclaw provider # launch TUI +netclaw provider [options] # CLI mode +``` + +## Provider Manager TUI + +![Provider Manager TUI showing configured providers with health status](/screenshots/output/provider-manager.png) + +On launch, the TUI probes every configured provider and shows health status: + +| Indicator | Meaning | +|-----------|---------| +| `✓` | Healthy, models discovered | +| `⚠` | Unreachable or auth failure | +| `…` | Probe in progress | + +Select a provider to view details (type, auth, endpoint, model count) or take action: + +| Key | Action | +|-----|--------| +| `↑` / `↓` | Navigate | +| `Enter` | Select / open details | +| `K` | Update API key (details view) | +| `R` | Remove provider (details view) | +| `V` | Re-validate connection (details view) | +| `Esc` | Back / quit | + +The sentinel row `+ Add new provider...` starts an interactive add flow. Netclaw validates connectivity with a 20-second timeout and reports how many models it found. + +OpenAI OAuth is only available through this TUI flow — select Add, choose OpenAI, then pick "ChatGPT Subscription" to authenticate with your existing account. + +## Subcommands + +### `provider list` + +```bash +netclaw provider list +``` + +``` +Name Provider Auth Endpoint +my-anthropic Anthropic ApiKey https://api.anthropic.com +my-ollama Ollama None http://localhost:11434 +``` + +This shows static config only — no live health probing. Open the TUI to see real-time provider health. + +### `provider add` + +```bash +netclaw provider add [--api-key ] [--endpoint ] +``` + +| Flag | Description | Default | +|------|-------------|---------| +| `--api-key ` | API key for the provider | Prompted if required | +| `--endpoint ` | Custom endpoint URL | Provider default | + +Provider type and endpoint are stored in `~/.netclaw/config/netclaw.json`. Credentials are encrypted in [`secrets.json`](/cli/secrets/). Restart the daemon after adding a provider so it picks up the new config. + +### `provider remove` + +```bash +netclaw provider remove +``` + +Netclaw blocks removal if any [model role](/cli/model/) (Main, Fallback, or Compaction) references the provider: + +``` +Error: Cannot remove provider 'my-anthropic' — referenced by model role(s): Main, Fallback +Run `netclaw model set` to reassign these roles first, or `netclaw model clear` for optional roles. +``` + +Reassign models first with [`netclaw model`](/cli/model/), then remove. + +## Provider types + +Pick Anthropic or OpenAI for hosted models, Ollama for fully local inference, or OpenRouter for access to models from multiple vendors through a single key. + +| Type | Display Name | Default Endpoint | Auth | +|------|-------------|-----------------|------| +| `ollama` | Ollama | `http://localhost:11434` | None | +| `openai-compatible` | llama.cpp / vLLM | `http://localhost:11434` | None | +| `openai` | OpenAI | `https://api.openai.com` | OAuth or API key | +| `anthropic` | Anthropic | `https://api.anthropic.com` | API key | +| `openrouter` | OpenRouter | `https://openrouter.ai/api/v1` | API key | + +## Examples + +```bash +# Local Ollama on a remote GPU server +netclaw provider add my-ollama ollama --endpoint http://my-gpu-server:11434 + +# Anthropic with an API key +netclaw provider add my-anthropic anthropic --api-key sk-ant-... + +# OpenAI with an API key +netclaw provider add my-openai openai --api-key sk-proj-... + +# OpenRouter +netclaw provider add my-openrouter openrouter --api-key sk-or-... + +# llama.cpp or vLLM behind an OpenAI-compatible endpoint +netclaw provider add my-llama openai-compatible --endpoint http://localhost:8080 + +# Remove a provider +netclaw provider remove my-ollama +``` + +After adding a provider, assign it to a model role with [`netclaw model set`](/cli/model/). + +## Override API keys with environment variables + +Skip config files entirely by setting an environment variable: + +```bash +export NETCLAW_Providers__my-anthropic__ApiKey="sk-ant-..." +``` + +Double underscores (`__`) separate config path segments. The `NETCLAW_` prefix is required. + +## Related commands + +- [`netclaw init`](/cli/init/) — set up your initial provider during first-run +- [`netclaw model`](/cli/model/) — assign providers to model roles +- [`netclaw doctor`](/cli/doctor/) — validate provider connectivity and config health +- [`netclaw status`](/cli/status/) — check the active provider and model at runtime +- [`netclaw secrets`](/cli/secrets/) — manage encrypted credentials in `secrets.json` + +## Resources + +- [Anthropic API keys](https://console.anthropic.com/settings/keys) — create and manage Anthropic keys +- [OpenAI API keys](https://platform.openai.com/api-keys) — create and manage OpenAI keys +- [OpenRouter keys](https://openrouter.ai/keys) — create and manage OpenRouter keys +- [OpenRouter model catalog](https://openrouter.ai/models) — browse available models +- [Ollama](https://ollama.com/) — install and run local models +- [llama.cpp server docs](https://github.com/ggml-org/llama.cpp/blob/master/tools/server/README.md) — set up an OpenAI-compatible local server diff --git a/src/content/docs/cli/reminder.md b/src/content/docs/cli/reminder.md index 2fc7997..69c06d5 100644 --- a/src/content/docs/cli/reminder.md +++ b/src/content/docs/cli/reminder.md @@ -1,6 +1,201 @@ --- title: "netclaw reminder" -description: "Manage scheduled reminders." +description: "Schedule autonomous agent sessions that fire once, on an interval, or via cron." --- -Content coming soon. +`netclaw reminder` schedules autonomous agent sessions. A reminder fires on a schedule (once, on an interval, or via [cron](https://crontab.guru/)), runs a session with your instructions, and optionally delivers results to a Slack channel. + +All subcommands except `validate` require the daemon to be running. Start it with `netclaw daemon start` or [`netclaw init`](/cli/init/). + +## Usage + +```bash +netclaw reminder [options] +``` + +## Subcommands + +| Subcommand | Description | +|-----------|-------------| +| `list` | List all active reminders | +| `create` | Create or update a reminder | +| `ui` / `tui` | Interactive reminder builder | +| `show ` | Show reminder details | +| `history ` | Show execution history | +| `enable ` | Enable a reminder | +| `disable ` | Disable a reminder | +| `cancel ` | Cancel (soft delete — disables, keeps definition) | +| `delete ` | Permanently delete a reminder and its history | +| `import ` | Import a reminder from a JSON file | +| `validate ` | Validate a reminder file offline (no daemon needed) | + +## Interactive TUI + +```bash +netclaw reminder ui +``` + +The Reminder Builder walks you through six steps: title, schedule type, schedule value, instructions, delivery guidance, and review. + +![The netclaw reminder TUI builder walking through the creation flow](/screenshots/output/reminder.gif) + +Navigate with arrow keys and Enter. Esc goes back a step (or quits on the first step). Ctrl+Q quits from anywhere. Ctrl+Enter adds newlines in multi-line fields. + +## `list` + +```bash +netclaw reminder list +``` + +![netclaw reminder list showing two reminders — a recurring cron reminder and a one-shot reminder](/screenshots/output/reminder-list.png) + +## `create` + +```bash +netclaw reminder create "" [options] +``` + +| Positional | Description | +|-----------|-------------| +| `id` | Stable identifier, kebab-case slug (e.g. `daily-standup`) | +| `scheduleType` | `once`, `interval`, or `cron` | +| `schedule` | Duration or cron expression (see [Schedule types](#schedule-types)) | +| `prompt` | Instructions for what netclaw should do when the reminder fires | + +### Options + +| Flag | Description | Default | +|------|-------------|---------| +| `--name ` | Human-readable title | Same as `id` | +| `--delivery ` | `none` or `channel` | `none` | +| `--transport ` | Transport for channel delivery (e.g. `slack`) | — | +| `--address ` | Target for channel delivery (`#channel`, `@user`, or ID) | — | +| `--expires-in ` | Auto-disable after duration (e.g. `24h`, `7d`) | — | + +If a reminder with the given ID already exists, it gets updated (upsert). + +Channel delivery requires [Slack to be configured](/cli/init/) in netclaw. Without it, the reminder runs but has nowhere to post results. + +### Schedule types + +| Type | What it does | Examples | +|------|-------------|----------| +| `once` | Fires one time, then auto-disables | `30m`, `2h`, or an [ISO 8601](https://en.wikipedia.org/wiki/ISO_8601) timestamp like `2026-06-01T18:00:00Z` | +| `interval` | Repeats on a fixed duration | `15m`, `2h`, `1d` | +| `cron` | Fires on a [cron schedule](https://crontab.guru/) (5-field, UTC) | `0 */6 * * *`, `0 9 * * MON-FRI` | + +### Examples + +#### Recurring cron reminder + +```bash +netclaw reminder create akka-nuget-daily cron "0 15 * * MON-FRI" \ + "Check Akka.NET NuGet download counts. Compare to yesterday and flag anything unusual." \ + --name "Akka NuGet Daily Downloads Report" +``` + +#### One-shot with Slack delivery + +```bash +netclaw reminder create stir-trek-talk once "2026-05-01T18:45:00Z" \ + "Remind me to head to the Stir Trek talk room" \ + --name "Head to Stir Trek talk room" \ + --delivery channel --transport slack --address "#general" +``` + +#### Interval health check + +```bash +netclaw reminder create infra-check interval "6h" \ + "Run netclaw doctor and summarize any degraded services." +``` + +## `show` + +```bash +netclaw reminder show +``` + +Shows the full JSON definition for a reminder — the same format `import` expects. + +## `history` + +```bash +netclaw reminder history [--last N] +``` + +| Flag | Description | Default | +|------|-------------|---------| +| `--last ` | Number of history records to return | `20` | + +Output is a table with columns: `fired_at`, `status` (`ok`/`failed`), `duration_ms`, and `session_id`. Use the session ID to dig into what the agent did with [`netclaw sessions`](/cli/sessions/). + +## `enable` / `disable` + +```bash +netclaw reminder enable +netclaw reminder disable +``` + +Disabled reminders keep their definition and history — they just stop firing. + +## `cancel` vs `delete` + +```bash +netclaw reminder cancel +netclaw reminder delete +``` + +`cancel` disables a reminder but leaves the definition and history intact, so you can re-enable it later. `delete` removes everything permanently. + +## `import` + +```bash +netclaw reminder import [--replace|--upsert] +``` + +Imports a reminder from a JSON file. The file format matches `show` output — grab a reminder's JSON, edit it, and import it back. + +| Mode | Description | +|------|-------------| +| _(default)_ | Create only — fails if the reminder already exists | +| `--upsert` | Create or update | +| `--replace` | Replace the existing reminder entirely | + +The file is validated before import. Run `validate` first to check for errors without hitting the daemon. + +## `validate` + +```bash +netclaw reminder validate +``` + +Checks a reminder JSON file for schema errors offline. Schedule-type-specific rules: + +- `once` reminders need a `fireAtMs` timestamp +- `interval` reminders need `intervalTicks` +- `cron` reminders need a valid `cronExpression` (standard [5-field format](https://en.wikipedia.org/wiki/Cron#Cron_expression)) + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | Reminder not found, validation error, daemon unreachable, or other failure | + +## What's next + +Run [`netclaw reminder list`](#list) to confirm your reminder appears with the right schedule and next fire time. After it fires, [`netclaw reminder history `](#history) shows whether it succeeded. Grab the session ID from the history output and pass it to [`netclaw sessions`](/cli/sessions/) to see the full run. + +## Related commands + +- [`netclaw status`](/cli/status/) — confirms daemon is running before using reminders +- [`netclaw sessions`](/cli/sessions/) — inspect what happened during a reminder session +- [`netclaw stats`](/cli/stats/) — usage statistics including reminder execution counts +- [`netclaw doctor`](/cli/doctor/) — health checks for the daemon and its subsystems + +## Resources + +- [Crontab.guru](https://crontab.guru/) — visual editor for cron schedule expressions +- [Cron expression syntax (Wikipedia)](https://en.wikipedia.org/wiki/Cron#Cron_expression) — reference for the standard 5-field cron format +- [ISO 8601 (Wikipedia)](https://en.wikipedia.org/wiki/ISO_8601) — timestamp format for one-shot reminder schedules diff --git a/src/content/docs/cli/secrets.md b/src/content/docs/cli/secrets.md index 547539d..fda8db0 100644 --- a/src/content/docs/cli/secrets.md +++ b/src/content/docs/cli/secrets.md @@ -3,4 +3,144 @@ title: "netclaw secrets" description: "Manage encrypted secrets." --- -Content coming soon. +Store API keys, tokens, and credentials in an encrypted local vault. Values are encrypted at rest using [ASP.NET Data Protection](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction) — no plaintext ever touches disk. + +If you haven't run [`netclaw init`](/cli/init/) yet, start there — the wizard stores your initial credentials automatically. Use `secrets set` afterward to rotate keys, add new providers, or script credential injection without re-running the wizard. + +## Usage + +```bash +netclaw secrets set +``` + +`set` is the only subcommand — there's no `get`, `list`, or `delete`. Secrets are write-only by design. To remove a key, edit `~/.netclaw/config/secrets.json` directly. + +## Arguments + +| Argument | Description | Required | +|----------|-------------|----------| +| `` | Dotted key path (e.g., `Slack.BotToken`) | Yes | +| `` | The secret value to encrypt and store | Yes | + +## Key Paths + +Keys use dotted paths matching the `netclaw.json` hierarchy: + +| Key Path | What It Stores | +|----------|---------------| +| `Slack.BotToken` | Slack Bot User OAuth Token (`xoxb-...`) | +| `Slack.AppToken` | Slack App-Level Token (`xapp-...`) | +| `Providers.openrouter.ApiKey` | OpenRouter API key | +| `Providers.ollama.ApiKey` | Ollama API key (if auth enabled) | +| `Search.BraveApiKey` | Brave Search API key | + +This isn't exhaustive — any dotted path that corresponds to a field in `netclaw.json` works. See [Configuration](/configuration/managed-providers/) for the full schema. + +## Examples + +### Store a Slack Bot Token + +```bash +netclaw secrets set Slack.BotToken xoxb-1234567890-abcdef +``` + +``` +Set Slack.BotToken (encrypted). +``` + +### Store a Provider API Key + +```bash +netclaw secrets set Providers.openrouter.ApiKey sk-or-v1-your-key-here +``` + +``` +Set Providers.openrouter.ApiKey (encrypted). +``` + +### Store Multiple Secrets + +```bash +netclaw secrets set Slack.BotToken xoxb-... +netclaw secrets set Slack.AppToken xapp-... +netclaw secrets set Providers.openrouter.ApiKey sk-or-v1-... +``` + +## How Encryption Works + +1. The value is encrypted via ASP.NET Data Protection +2. The ciphertext is written to `secrets.json` with an `ENC:` prefix +3. File permissions are set to `600` on Unix + +The raw `secrets.json` looks like this: + +```json +{ + "Slack": { + "BotToken": "ENC:CfDJ8N2x...long-base64-string..." + }, + "Providers": { + "openrouter": { + "ApiKey": "ENC:CfDJ8K9y...long-base64-string..." + } + } +} +``` + +Encryption keys live in `~/.netclaw/keys/`, separate from the secrets file. Both are required to decrypt — one is useless without the other. + +## Storage Locations + +| Path | Contents | +|------|----------| +| `~/.netclaw/config/secrets.json` | Encrypted credential values | +| `~/.netclaw/keys/` | Data Protection key material | + +Both paths are created automatically on first use of `netclaw secrets set` or during `netclaw init`. + +## Configuration Layering + +Secrets participate in the standard config priority chain (highest priority wins): + +``` +NETCLAW_* env vars → secrets.json → netclaw.json + (highest) (lowest) +``` + +Environment variables override secrets, which override `netclaw.json`. Use env vars in CI or containers while keeping `secrets.json` for local development. + +## Output Redaction + +Even if a secret leaks into a tool's output at runtime, netclaw's output redactor catches it before it reaches the LLM. It recognizes common secret patterns: API key prefixes (`sk-*`, `xox*-*`, `ghp_*`, `AKIA*`), `Authorization` headers, connection strings with embedded passwords, JWT tokens, and PEM private key blocks. + +Secrets are encrypted at rest, decrypted only when needed, and redacted from any tool output before it reaches the LLM. + +## Troubleshooting + +### Permission denied writing secrets.json + +The file requires `chmod 600`. If permissions drifted: + +```bash +chmod 600 ~/.netclaw/config/secrets.json +``` + +### Lost encryption keys + +If `~/.netclaw/keys/` is deleted, existing encrypted values in `secrets.json` cannot be recovered. You'll need to re-run `netclaw secrets set` for each credential. Always back up `~/.netclaw/keys/` alongside `secrets.json`. + +### Secrets not taking effect + +An environment variable with the same path will override `secrets.json`. Run [`netclaw doctor`](/cli/doctor/) to diagnose config issues. + +## Related Commands + +- [`netclaw init`](/cli/init/) — Stores initial credentials during first-run setup +- [`netclaw doctor`](/cli/doctor/) — Diagnoses config and connectivity issues +- [`netclaw provider`](/cli/provider/) — Manages providers that consume API key secrets + +## Further Reading + +- [ASP.NET Data Protection overview](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction) — The encryption framework netclaw builds on +- [ASP.NET Data Protection key management](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-management) — How encryption keys are stored and rotated +- [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html) — General best practices for credential storage diff --git a/src/content/docs/cli/sessions.md b/src/content/docs/cli/sessions.md index f3064dc..1ac5644 100644 --- a/src/content/docs/cli/sessions.md +++ b/src/content/docs/cli/sessions.md @@ -3,4 +3,162 @@ title: "netclaw sessions" description: "Browse and resume chat sessions." --- -Content coming soon. +Pick up where you left off. `netclaw sessions` opens a terminal UI (TUI) listing your recent sessions — scroll, hit Enter, and you're back in the conversation. For scripts, `--once` and `--json` dump session data to stdout instead. + +Requires a running daemon. If it's not up, start it with `netclaw daemon start` (or run [`netclaw init`](/cli/init/) which starts it for you). + +This command only lists sessions — there's no delete or cleanup here. Session lifecycle is managed by the daemon's retention policy. + +## Usage + +```bash +netclaw sessions [options] +``` + +## Options + +| Flag | Description | Default | +|------|-------------|---------| +| `--once` | List sessions to stdout and exit (no TUI) | TUI mode | +| `--json` | Output as JSON array (implies `--once`) | Plain text | + +## TUI mode + +```bash +netclaw sessions +``` + +Full-screen session browser. Each entry shows: + +``` +[cli] Deploy troubleshooting (8 turns, 23m ago) +[slack] Weekly standup recap (3 turns, 2h ago) +[cli] Config migration (12 turns, 1d ago) +``` + +The channel tag (`cli`, `slack`, etc.) shows where the session originated. The daemon returns up to 50 of your most recent sessions. + +### Keyboard shortcuts + +| Key | Action | +|-----|--------| +| `↑` / `K` | Move selection up | +| `↓` / `J` | Move selection down | +| `Enter` | Resume selected session (opens chat) | +| `N` | Start a new [`netclaw chat`](/cli/chat/) session | +| `Ctrl+Q` | Quit | +| `Escape` | Quit | + +Enter drops you straight into [`netclaw chat`](/cli/chat/) with that session's full history loaded. When no sessions exist, you'll see "No sessions found. Press Enter to start a new chat." + +## Single-shot mode + +### Plain text output + +```bash +netclaw sessions --once +``` + +Lists all sessions to stdout, one per line: + +``` +a1b2c3d4 Deploy troubleshooting [active] turns=8 last=2026-05-03 14:22 +e5f6g7h8 Weekly standup recap [inactive] turns=3 last=2026-05-03 11:45 +``` + +**Active** means the session is held in memory and responds instantly. **Inactive** means it's persisted to disk — resuming one replays its history, which takes a moment. + +The session ID in the first column is what you pass to [`netclaw chat --resume`](/cli/chat/#session-resume): + +```bash +netclaw sessions --once | head -1 | awk '{print $1}' | xargs netclaw chat --resume +``` + +### JSON output + +```bash +netclaw sessions --json +``` + +Outputs a JSON array of session objects: + +```json +[ + { + "persistenceId": "session-a1b2c3d4", + "channel": "cli", + "title": "Deploy troubleshooting", + "status": "active", + "turnCount": 8, + "createdAt": 1746268800000, + "lastActivity": 1746282120000, + "logPath": "/home/user/.netclaw/logs/sessions/a1b2c3d4" + } +] +``` + +#### JSON fields + +| Field | Type | Description | +|-------|------|-------------| +| `persistenceId` | string | Full internal persistence ID (prefixed with `session-`) | +| `channel` | string | Origin channel (`cli`, `slack`, `discord`, etc.) | +| `title` | string? | Auto-generated or null for untitled sessions | +| `description` | string? | Session description, if set | +| `status` | string | `active` (in memory) or `inactive` (persisted to disk) | +| `turnCount` | int | Number of completed conversation turns | +| `createdAt` | long | Creation time (Unix milliseconds) | +| `lastActivity` | long | Last activity time (Unix milliseconds) | +| `logPath` | string? | Path to per-session log directory | +| `lastInputTokens` | long? | Input tokens from the last recorded turn | + +Note that `sessionId` is not included in the JSON — it's computed at runtime. To derive the short ID from `persistenceId`, strip the `session-` prefix: + +```bash +# Requires jq +netclaw sessions --json | jq -r '.[0].persistenceId | ltrimstr("session-")' +``` + +Active sessions have their [actor](https://getakka.net/articles/concepts/actors.html) materialized in memory — they respond without any startup cost. Inactive sessions are persisted to disk and fully recoverable; resuming one replays its history, which takes a moment depending on the number of turns. + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Sessions listed successfully | +| `1` | Daemon unavailable or request failed | + +In TUI mode, a failed daemon connection shows "Failed to connect to daemon. Is it running?" In `--once` mode, the error looks different: + +``` +[FAIL] sessions: unable to reach daemon at : +run 'netclaw daemon start' and retry. +``` + +## Examples + +### Find sessions with many turns + +```bash +# Requires jq +netclaw sessions --json | jq '[.[] | select(.turnCount > 10)] | sort_by(-.lastActivity)' +``` + +### Resume the most recently active session + +```bash +# Requires jq — derives session ID by stripping the "session-" prefix +netclaw chat --resume "$(netclaw sessions --json | jq -r '.[0].persistenceId | ltrimstr("session-")')" +``` + +## Related commands + +- [`netclaw chat`](/cli/chat/) — Pass `--resume ` to jump straight into a specific session +- [`netclaw chat -p`](/cli/chat/#headless-mode) — Headless mode with `--resume` for scripted multi-turn conversations +- [`netclaw status`](/cli/status/) — Check daemon health if sessions can't connect +- [`netclaw stats`](/cli/stats/) — Token usage and session count metrics + +## Resources + +- [jq manual](https://jqlang.github.io/jq/manual/) — filter and transform `--json` output +- [Akka.NET persistence](https://getakka.net/articles/persistence/architecture.html) — how session state survives daemon restarts diff --git a/src/content/docs/cli/skill.md b/src/content/docs/cli/skill.md index 860ba92..ea21437 100644 --- a/src/content/docs/cli/skill.md +++ b/src/content/docs/cli/skill.md @@ -1,6 +1,281 @@ --- title: "netclaw skill" -description: "Manage skills and external sources." +description: "Manage skills and external skill sources." --- -Content coming soon. +Manage skills from the CLI — list what's installed, validate new ones, wire up external sources. None of these commands need a running daemon. + +Skills are markdown files following the [AgentSkills.io](https://agentskills.io) format: a `SKILL.md` with YAML frontmatter describing what the skill does, what tools it needs, and how to invoke it. At runtime, the agent sees a compressed skill index on every session and reads full skill content on demand — skills inject procedural knowledge into context without bloating the base prompt. + +## Usage + +```bash +netclaw skill [subcommand] [options] +``` + +Running `netclaw skill` with no subcommand defaults to `list`. + +## Subcommands + +| Subcommand | Description | +|------------|-------------| +| `list` | List all discovered skills (default) | +| `show ` | Show skill details and full content | +| `validate ` | Validate a SKILL.md file's frontmatter | +| `remove ` | Remove a native skill | +| `issues` | Show only scanner issues | +| `search ` | Search skills by name or description | +| `source list` | List configured external sources | +| `source add --path ` | Add a custom external source | +| `source add --well-known ` | Add a well-known source | +| `source remove ` | Remove an external source | +| `source enable ` | Enable an external source | +| `source disable ` | Disable an external source | + +## Skill classifications + +Three categories: + +| Source | Location | Mutable? | +|--------|----------|----------| +| `system` | `~/.netclaw/skills/.system/` | No — managed by the daemon | +| `native` | `~/.netclaw/skills/` | Yes — user-created | +| `external` | Configured source directories | Manage at source | + +For a deeper look at how skills fit into netclaw's architecture, see [Skills Overview](/skills/overview/). + +## Listing skills + +```bash +netclaw skill list +``` + +``` +NAME SOURCE VERSION STATUS +commit native 1.0.0 ok +review-pr native 1.2.0 ok +akka-net-best-practices external - ok +modern-csharp-standards external - ok +session-context system - ok +my-broken-skill ? - MissingDescription + +5 skill(s), 1 issue(s) +``` + +Skills with problems show their issue kind in the STATUS column. `netclaw skill issues` gives you the full details. + +## Showing skill details + +```bash +netclaw skill show commit +``` + +``` +Name: commit +Display: Commit Changes +Source: native +Version: 1.0.0 +Category: git +License: MIT +Path: /home/user/.netclaw/skills/commit/SKILL.md +Flat file: False +Tools: bash git +Description: Create well-structured git commits with conventional commit messages. + +--- content --- +(full SKILL.md content follows) +``` + +## Validating a skill file + +Catch frontmatter problems before deploying: + +```bash +netclaw skill validate ./my-skill/SKILL.md +``` + +``` +[OK] Valid skill file. + Name: my-skill + Description: Does something useful. + Version: 1.0.0 + License: MIT +``` + +Once validated, drop the skill into `~/.netclaw/skills/` (directory-based or flat file) and it'll appear on the next `netclaw skill list`. + +A failing file tells you exactly what's wrong: + +```bash +netclaw skill validate ./broken-skill.md +``` + +``` +[FAIL] MissingDescription: Skill file missing required 'description' field. +``` + +## Searching skills + +```bash +netclaw skill search "testing" +``` + +``` +NAME SOURCE VERSION DESCRIPTION +snapshot-testing external - Verify test output using snapshot... +integration-tests external - Testcontainers patterns for .NET... + +2 match(es) +``` + +## Removing a skill + +```bash +netclaw skill remove my-old-skill +``` + +Only native skills can be removed — system and external skills are read-only here. + +## Managing external sources + +Point netclaw at skill directories from other tools (Claude Code, Open Code) or shared team paths. + +Note: [Skill feeds](/skills/skill-feeds/) are a separate concept — server-based skill repositories synced by the daemon. This section covers local directory sources only. + +### List sources + +```bash +netclaw skill source list +``` + +``` +NAME ENABLED TYPE PATH / WELL-KNOWN +claude-skills yes well-known claude-code +team-shared yes path /opt/skills/team +``` + +When no sources are configured, netclaw checks for well-known directories and nudges you: + +``` +No external skill sources configured. + +Detected well-known skill directories on disk: + claude-code /home/user/.claude/skills +Add one with: netclaw skill source add --well-known +``` + +### Add a well-known source + +```bash +netclaw skill source add my-claude --well-known claude-code +``` + +The aliases map to these paths: + +| Alias | Scans | +|-------|-------| +| `claude-code` | `~/.claude/skills/`, `~/.claude/commands/`, plus installed plugin marketplaces | +| `open-code` | `~/.open-code/skills/` | + +During `netclaw init`, you're prompted to configure external skill sources: + +![External skills configuration during init](/screenshots/output/init-07-external-skills.png) + +The init wizard detects well-known directories automatically. + +### Add a custom path source + +```bash +netclaw skill source add team-skills --path /opt/company/skills +``` + +The directory must exist. Netclaw scans for both directory-based skills (`skill-name/SKILL.md`) and flat files (`skill-name.md`). + +### Enable / disable a source + +```bash +netclaw skill source disable team-skills +netclaw skill source enable team-skills +``` + +Disabled sources stay in config but won't be scanned. + +### Remove a source + +```bash +netclaw skill source remove team-skills +``` + +Removes the config entry only — skill files in the directory are untouched. + +## Skill file format + +Two layouts work: + +**Directory-based:** + +``` +my-skill/ + SKILL.md + scripts/ + helper.sh + references/ + api-docs.md + assets/ + diagram.png +``` + +**Flat file:** + +``` +my-skill.md +``` + +The SKILL.md frontmatter: + +```yaml +--- +name: my-skill +description: One-line description of what this skill does. +metadata: + version: 1.0.0 +license: MIT +compatibility: ["netclaw", "claude-code"] +allowed-tools: "bash git" +disable-model-invocation: false +user-invocable: true +argument-hint: "" +--- + +# My Skill + +Skill instructions go here... +``` + +The display name is derived from the first `#` heading in the markdown body, falling back to title-casing the skill name. + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Identifier (must match directory/file name) | +| `description` | Yes | One-line summary | +| `metadata.version` | No | Semver version (nested under `metadata:`) | +| `license` | No | SPDX license identifier | +| `compatibility` | No | Which agents can use this skill | +| `allowed-tools` | No | Space-delimited list of tools the skill needs | +| `disable-model-invocation` | No | When `true`, prevents the model from auto-invoking this skill | +| `user-invocable` | No | When `true`, users can invoke the skill directly (e.g. `/commit`) | +| `argument-hint` | No | Hint text shown in command completion | + +## Related commands + +- [`netclaw init`](/cli/init/) — configures external skill sources during onboarding +- [`netclaw mcp`](/cli/mcp-tools/) — manage tool servers that skills reference in `allowed-tools` +- [`netclaw status`](/cli/status/) — shows loaded skill count at runtime + +## Resources + +- [AgentSkills.io](https://agentskills.io) — the SKILL.md format specification +- [Cloudflare Agent Skills Discovery RFC](https://github.com/cloudflare/agent-skills-spec) — discovery protocol for skill feeds +- [netclaw skill-server](https://github.com/netclaw-dev/skill-server) — self-hosted skill registry for organizations +- [Skills Overview](/skills/overview/) — how skills work at runtime +- [Skill Feeds](/skills/skill-feeds/) — server-synced skill repositories diff --git a/src/content/docs/cli/stats.md b/src/content/docs/cli/stats.md index f286eed..93e365c 100644 --- a/src/content/docs/cli/stats.md +++ b/src/content/docs/cli/stats.md @@ -3,4 +3,168 @@ title: "netclaw stats" description: "View usage activity statistics." --- -Content coming soon. +`netclaw stats` tells you where your tokens are going, how many sessions are running, and whether memory is healthy. It also covers channel activity, webhook deliveries, and reminders. `--json` for scripts, `--tui` for a live dashboard. + +## Usage + +```bash +netclaw stats [options] +netclaw stats skills [options] +``` + +Requires a running daemon. If it's not up, start it with `netclaw daemon start` (or run [`netclaw init`](/cli/init/) which starts it for you). + +## Options + +| Flag | Description | Default | +|------|-------------|---------| +| `--days ` | Show trailing N-day daily breakdown | None (omits daily table) | +| `--all` | Show all-time daily breakdown | Off | +| `--tui` | Visual TUI dashboard | Off | +| `--json` | Alias for `--format json` | — | +| `--format ` | Output format | `text` | + +## Output + +```bash +netclaw stats +``` + +![netclaw stats showing token usage, sessions, memory, channel activity, webhooks, and reminders](/screenshots/output/stats.png) + +Without `--days` or `--all`, you get process-lifetime counters plus current snapshot data. No daily table. + +| Section | What it shows | +|---------|---------------| +| **this process** | Token consumption (in/out), turns completed, [memories](/architecture/overview/) formed/recalled, [skills](/skills/overview/) loaded | +| **sessions** | Total, active, and all-time turn count | +| **memory** | Status (`healthy`, `degraded`, `unavailable`), anchor/document/record/edge counts, pending checkpoints | +| **skills** | Total available skill count | +| **\** | Per-channel events (recv/routed/dropped) and replies (posted/rejected/failed) | +| **webhooks** | Route counts, delivery stats, rejection breakdown by HTTP status | +| **reminders** | Scheduled, active, and failed counts | + +The "this process" counters live in the daemon process's memory and reset when the daemon restarts. Everything else (sessions, memory, daily breakdown) is persisted to SQLite and survives restarts. + +If memory status shows `degraded` or `unavailable`, run [`netclaw doctor --fix`](/cli/doctor/) to fix common issues automatically. + +### Daily breakdown + +Add `--days` to see a per-day table of token usage and activity: + +```bash +# Last 14 days +netclaw stats --days 14 + +# All time +netclaw stats --all +``` + +Columns: date, input/output tokens, turns, sessions, memories formed/recalled, skills loaded. When more than one day of data is returned, a totals row appears at the bottom. + +## TUI dashboard + +```bash +netclaw stats --tui +``` + +Opens a full-screen dashboard. `--tui` always shows an ASCII bar chart of daily token usage, defaulting to the last 7 days. Pass `--days N` to change the window: + +```bash +netclaw stats --tui --days 30 +``` + +The lower half of the dashboard shows the Daily Token Usage bar chart by default. Memory, per-channel, and Webhooks panels take its place when there's no daily breakdown data. + +`Q`, `Escape`, or `Ctrl+C` to quit. + +## Skill usage stats + +`stats skills` shows per-skill load counts broken down by day and invocation method (e.g., `direct`, `auto`, `scheduled`): + +```bash +# Last 7 days (default) +netclaw stats skills + +# Last 30 days +netclaw stats skills --days 30 + +# All time +netclaw stats skills --all +``` + +Each day shows total loads, a method breakdown, and per-skill counts (top 15). `--tui` doesn't work with `stats skills`. + +## JSON output + +```bash +netclaw stats --json +``` + +```bash +# Total input tokens since daemon started +netclaw stats --json | jq '.tokens.inputTokensTotal' + +# Daily token usage for the last 7 days +netclaw stats --days 7 --json | jq '.dailyBreakdown[] | {date, inputTokens, outputTokens}' + +# Active session count +netclaw stats --json | jq '.sessions.activeSessions' + +# Memory health status +netclaw stats --json | jq -r '.memory.status' +``` + +`stats skills --json` returns a separate shape with daily skill load breakdowns: + +```bash +netclaw stats skills --json | jq '.daily[] | {date, totalLoads}' +``` + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | Daemon unreachable or error response | + +## Daemon endpoint + +`stats` queries `http://127.0.0.1:5199` by default. Override with: + +- `NETCLAW_DAEMON_ENDPOINT` environment variable +- `~/.netclaw/client/config.json` + +See [`netclaw status`](/cli/status/) for more on endpoint configuration. + +## When the daemon isn't running + +You'll see one of two errors: + +**Connection refused** — the daemon process isn't running at all: + +``` +[FAIL] stats: unable to reach daemon at http://127.0.0.1:5199: Connection refused + fix: run `netclaw daemon start` and retry. +``` + +**503 response** — the daemon is up but not ready (still starting, or something broke): + +``` +[FAIL] stats: daemon returned 503 from http://127.0.0.1:5199 + fix: run `netclaw daemon start` and retry. +``` + +For 503s, [`netclaw doctor --fix`](/cli/doctor/) can often sort out what's wrong without a restart. + +## Related commands + +- [`netclaw status`](/cli/status/) — live health check for daemon, connectors, and model +- [`netclaw doctor`](/cli/doctor/) — config diagnostics with `--fix` for auto-repair +- [`netclaw sessions`](/cli/sessions/) — if stats show unexpected session counts, dig into individual conversations here +- [`netclaw init`](/cli/init/) — first-run setup (starts the daemon for you) + +## Resources + +- [jq manual](https://jqlang.github.io/jq/manual/) — for wrangling `--json` output +- [SQLite documentation](https://www.sqlite.org/docs.html) — daily breakdown data is persisted here diff --git a/src/content/docs/cli/status.md b/src/content/docs/cli/status.md index 732b5c7..1003f43 100644 --- a/src/content/docs/cli/status.md +++ b/src/content/docs/cli/status.md @@ -3,4 +3,142 @@ title: "netclaw status" description: "Query daemon runtime status." --- -Content coming soon. +`netclaw status` queries the running daemon and prints overall health, connector states, the active model, persistence, telemetry counters, and whether an update is available. Run it when something feels off, or wire it into a health check script. + +## Usage + +```bash +netclaw status [options] +``` + +Requires a running daemon. If it's not up, start it with `netclaw daemon start` (or run [`netclaw init`](/cli/init/) which starts it for you). + +## Options + +| Flag | Description | Default | +|------|-------------|---------| +| `--format ` | Output format | `text` | +| `--json` | Alias for `--format json` | — | + +## Output + +```bash +netclaw status +``` + +![netclaw status showing healthy daemon with connectors](/screenshots/output/status.png) + +| Section | What it tells you | +|---------|-------------------| +| **overall** | `healthy` or `degraded` | +| **version** | Build version, commit hash, build timestamp | +| **daemon** | PID, uptime, endpoint URL | +| **persistence** | Storage provider (e.g., SQLite) | +| **memory** | Memory provider and status — returns `unavailable`, `healthy`, or `degraded` | +| **telemetry** | Enabled/disabled, [OpenTelemetry Protocol](https://opentelemetry.io/docs/specs/otlp/) (OTLP) endpoint if active | +| **counters** | Per-channel message stats: `recv`, `routed`, `dropped`, `replied`, `rejected`, `reply_failed` | +| **model** | Active model name, provider, context window, input/output modalities | +| **connectors** | Each connector's key, status, and enabled/disabled state | +| **update** | Whether a newer version is available | + +### Overall status logic + +`overall` reflects connector health: + +- **healthy** — all enabled connectors are healthy +- **degraded** — any enabled connector is `disconnected`, `degraded`, `auth-required`, or `auth-failed` + +### Connector statuses + +Channel connectors (Slack, Discord) report: `healthy`, `degraded`, `disconnected`, `disabled`, `unknown`. + +MCP connectors — keyed as `mcp:` from your configured [MCP tool servers](/cli/mcp-tools/) — add: `auth-required` and `auth-failed`. + +Disabled connectors still appear in the list with `(disabled)` so you can tell they're intentionally off, not broken. + +If any connector shows `degraded` or worse, run [`netclaw doctor --fix`](/cli/doctor/) to auto-repair common issues. If that doesn't resolve it, check the specific connector's configuration. + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Healthy | +| `2` | Degraded | +| `1` | Daemon unreachable or error response | + +Exit code `2` for degraded (rather than `1`) lets scripts distinguish "something's wrong but running" from "down entirely": + +```bash +netclaw status > /dev/null 2>&1 +rc=$? +case $rc in + 0) echo "healthy" ;; + 2) echo "degraded — check connectors" ;; + *) echo "down or unreachable" ;; +esac +``` + +Note: the [CLI overview](/cli/overview/) documents exit code `2` as "usage/argument error" for most commands. `status` is an exception — it uses `2` specifically for degraded health. + +## JSON output + +```bash +netclaw status --json +``` + +Returns the full status response as JSON. + +The JSON includes fields not shown in text output — notably `reminders` (with `scheduledCount`, `activeExecutions`, `failedCount`). Also, the `version` text section maps to the `.build` key in JSON, so use `jq '.build'` rather than `jq '.version'`: + +```bash +# Check overall health +netclaw status --json | jq -r '.overall' + +# Get version info (note: `.build`, not `.version`) +netclaw status --json | jq '.build' + +# List unhealthy connectors +netclaw status --json | jq '[.connectors[] | select(.enabled and .status != "healthy")]' +``` + +## When the daemon isn't running + +If the daemon is unreachable, you'll see one of: + +``` +[FAIL] status: unable to reach daemon at http://127.0.0.1:5199: Connection refused +fix: run 'netclaw daemon start' and retry. +``` + +``` +[FAIL] status: daemon returned 503 from http://127.0.0.1:5199 +fix: run 'netclaw daemon status' and 'netclaw daemon start'. +``` + +The distinction: + +| Command | What it checks | Daemon required | +|---------|---------------|-----------------| +| `netclaw daemon status` | PID file — is the process running? | No | +| `netclaw status` | Live health query — are connectors, model, persistence all working? | Yes | + +If you're not sure the process is alive, start with `daemon status`. Once it's up, `status` gives the full picture. + +## Daemon endpoint + +`status` queries `http://127.0.0.1:5199/api/health/status` by default. Override with: + +- `NETCLAW_DAEMON_ENDPOINT` +- `~/.netclaw/client/config.json` + +## Related commands + +- [`netclaw doctor`](/cli/doctor/) — Diagnostics with auto-repair (`--fix`) +- [`netclaw stats`](/cli/stats/) — Token usage, session counts, and memory metrics +- [`netclaw chat`](/cli/chat/) — Start a conversation (check status first if connections fail) +- [`netclaw init`](/cli/init/) — First-run setup that starts the daemon for you + +## Resources + +- [jq manual](https://jqlang.github.io/jq/manual/) — for wrangling `--json` output +- [OpenTelemetry collector docs](https://opentelemetry.io/docs/collector/) — if you're sending telemetry to an OTLP endpoint diff --git a/src/content/docs/cli/webhooks.md b/src/content/docs/cli/webhooks.md index a64389f..e7800d7 100644 --- a/src/content/docs/cli/webhooks.md +++ b/src/content/docs/cli/webhooks.md @@ -1,6 +1,248 @@ --- title: "netclaw webhooks" -description: "Manage webhook routes." +description: "Configure inbound HTTP endpoints that trigger netclaw sessions from GitHub, GitLab, and other services." --- -Content coming soon. +`netclaw webhooks` manages inbound webhook routes. External services POST to a route, netclaw verifies the signature, runs a session with the route's prompt, and posts results to Slack if configured. + +Routes live as individual JSON files in `~/.netclaw/config/webhooks/`. CLI management (`list`, `set`, `validate`, etc.) doesn't need a running daemon. Actually receiving webhook requests does — start the daemon with `netclaw daemon start` or [`netclaw init`](/cli/init/). + +## Usage + +```bash +netclaw webhooks [subcommand] [options] +``` + +## `list` + +```bash +netclaw webhooks list [--json] [--all] +``` + +| Flag | Description | Default | +|------|-------------|---------| +| `--json` | JSON output | Off | +| `--all` | Include disabled routes | Off | + +![netclaw webhooks list showing no routes configured and the route directory path](/screenshots/output/webhooks-list.png) + +With no routes configured, you see the storage path. Once routes exist, the table shows status, audience, verification kind, and delivery requirement per route. + +## `show` + +```bash +netclaw webhooks show [--json] [--show-secret] +``` + +Displays full configuration for a single route. Also validates the route file — exits with code 1 if the route has schema errors. + +| Flag | Description | Default | +|------|-------------|---------| +| `--json` | JSON output | Off | +| `--show-secret` | Reveal the verification secret | Off | + +Secrets are redacted by default. Pass `--show-secret` when you need to copy the secret to a third-party service. + +## `set` + +Creates or updates a route. Route names must be lowercase alphanumeric with single dashes between segments (`github-issues`, `deploy-v2`). Regex: `^[a-z0-9]+(?:-[a-z0-9]+)*$`. + +```bash +netclaw webhooks set [options] +``` + +### Prompt and secret + +New routes require a prompt (choose one) and a secret (choose one): + +| Flag | Description | +|------|-------------| +| `--prompt ` | Prompt text injected into webhook sessions | +| `--prompt-file ` | Read prompt from a file | +| `--secret ` | Verification secret — **visible in shell history**; prefer `--secret-file` or `--secret-env` | +| `--secret-file ` | Read secret from a file | +| `--secret-env ` | Read secret from an environment variable | + +### Verification + +| Flag | Description | Default | +|------|-------------|---------| +| `--verification-kind ` | `hmac` or `header-secret` | `hmac` | +| `--signature-header ` | Header containing the [HMAC](https://en.wikipedia.org/wiki/HMAC) signature | — | +| `--signature-prefix ` | Prefix on the signature value (e.g. `sha256=`) | — | +| `--secret-header ` | Header containing the secret (header-secret mode) | — | +| `--event-header ` | Header with the event type name | — | +| `--delivery-header ` | Header with the unique delivery ID (for deduplication) | — | + +### Behavior + +`--audience` controls which [tool audience](/architecture/overview/) the webhook session runs under. `public` gets the most restricted tool access, `personal` gets the most permissive. + +| Flag | Description | Default | +|------|-------------|---------| +| `--events ` | Comma-separated event type allow-list (empty = all) | All | +| `--audience ` | `public`, `team`, or `personal` | `public` | +| `--max-body ` | Maximum request body size | `1048576` (1 MB) | +| `--rate-limit ` | Requests per minute | `30` | +| `--enabled` / `--disabled` | Enable or disable the route | Enabled | + +### Notifications + +When a notification target is configured, the agent posts results to that Slack channel. This requires [Slack to be configured](/cli/init/) in netclaw. + +| Flag | Description | Default | +|------|-------------|---------| +| `--notify-instructions ` | Instructions for agent notification behavior | — | +| `--notify-instructions-file ` | Read notification instructions from a file | — | +| `--delivery-required` / `--no-delivery-required` | Require notification delivery | `true` | +| `--notification-channel ` | Slack channel ID for notifications | — | + +### Modifiers + +| Flag | Description | +|------|-------------| +| `--dry-run` | Validate and print the route without saving | +| `--create-only` | Fail if the route already exists | +| `--update-only` | Fail if the route doesn't exist | + +### Examples + +#### GitHub issues route + +```bash +netclaw webhooks set github-issues \ + --prompt "Triage this GitHub issue. Summarize it and suggest a priority label." \ + --secret-env GITHUB_WEBHOOK_SECRET \ + --verification-kind hmac \ + --signature-header X-Hub-Signature-256 \ + --signature-prefix "sha256=" \ + --event-header X-GitHub-Event \ + --delivery-header X-GitHub-Delivery \ + --events "issues,issue_comment" \ + --audience team \ + --notification-channel C0123SLACK +``` + +#### Preview without saving + +```bash +netclaw webhooks set github-issues --rate-limit 10 --dry-run +``` + +#### Create-only (fail if exists) + +```bash +netclaw webhooks set deploy-notify \ + --prompt "Summarize this deployment and post a status update." \ + --secret-env DEPLOY_SECRET \ + --create-only +``` + +## `delete` + +```bash +netclaw webhooks delete [--force | -f] +``` + +Prompts for confirmation unless `--force` is passed. + +## `validate` + +```bash +netclaw webhooks validate +``` + +Checks the route file for syntax errors and schema violations. These are the same checks [`netclaw doctor`](/cli/doctor/) runs across all routes automatically. The backtick-quoted names below are JSON fields in the route file: + +- Route name must match `^[a-z0-9]+(?:-[a-z0-9]+)*$` +- `Prompt` is required and non-empty +- `Verification` section is required with a non-empty `Secret` +- `MaxBodyBytes` and `RateLimitPerMinute` must be >= 1 +- `Events` list can't contain blank entries +- If `DeliveryRequired` is true and `NotifyInstructions` is set, `NotificationTarget` is required +- If `NotificationTarget.Kind` is `Slack`, `NotificationTarget.ChannelId` is required + +## Route files + +Each route is stored as `~/.netclaw/config/webhooks/.json`. A GitHub Issues route looks like this: + +```json +{ + "Enabled": true, + "Prompt": "Triage this GitHub issue. Summarize it and suggest a priority label.", + "Verification": { + "Kind": "Hmac", + "HmacAlgorithm": "Sha256", + "Secret": "whsec_abc123...", + "SignatureHeaderName": "X-Hub-Signature-256", + "SignaturePrefix": "sha256=", + "EventHeaderName": "X-GitHub-Event", + "DeliveryIdHeaderName": "X-GitHub-Delivery" + }, + "Events": ["issues", "issue_comment"], + "Audience": "Team", + "MaxBodyBytes": 1048576, + "RateLimitPerMinute": 30, + "DeliveryRequired": true, + "NotifyInstructions": "Post a summary to the triage channel.", + "NotificationTarget": { + "Kind": "Slack", + "ChannelId": "C0123SLACK" + } +} +``` + +Note the casing difference: CLI flags use lowercase (`--verification-kind hmac`, `--audience team`) while route files use PascalCase (`"Kind": "Hmac"`, `"Audience": "Team"`). + +Route files contain secrets in plaintext. Keep `~/.netclaw/config/webhooks/` mode `700` and don't commit these files to source control. + +## Request lifecycle + +When a POST hits `/api/webhooks/{routeName}`: + +1. The daemon loads the route file (hot-reloaded per request — no restart needed after changes) +2. Verifies the request using the route's HMAC signature or header secret +3. Checks rate limits and body size constraints +4. Filters by event type if the route has an allow-list +5. Creates a new `Webhook` session with the route's prompt injected as context +6. If a notification target is configured, the agent posts results to that Slack channel. Without a notification target, the session still runs — output is stored in the session log + +Rejected requests return the appropriate HTTP status. Missing or malformed route files return 404. To see delivery and rejection counts, run [`netclaw stats`](/cli/stats/). + +## Finding your webhook URL + +After creating a route, you need the full URL to give your external service. Run [`netclaw status`](/cli/status/) — the output includes your webhook base URL (set during [`netclaw init`](/cli/init/)). Your route's endpoint is: + +``` +/api/webhooks/ +``` + +The webhook URL depends on how you've exposed your daemon. [Tailscale Serve](https://tailscale.com/kb/1312/serve) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) are the two supported options for making your daemon reachable from external services. + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Success | +| `1` | Route not found, validation error, or other failure | + +## What's next + +After setting up a route, restart the daemon and run [`netclaw status`](/cli/status/) to confirm the webhook endpoint is healthy. Then paste the full webhook URL into your external service's webhook settings. + +To debug webhook issues: [`netclaw stats`](/cli/stats/) shows delivery counts and rejection breakdowns, and [`netclaw doctor`](/cli/doctor/) validates all route files. + +## Related commands + +- [`netclaw doctor`](/cli/doctor/) — validates all webhook route files and flags format issues +- [`netclaw stats`](/cli/stats/) — webhook delivery counts and rejection breakdowns +- [`netclaw init`](/cli/init/) — sets up webhook URL identity during onboarding +- [`netclaw status`](/cli/status/) — live daemon health including webhook endpoint availability + +## Resources + +- [GitHub webhook documentation](https://docs.github.com/en/webhooks) — setting up webhooks on the GitHub side +- [GitLab webhook documentation](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html) — GitLab's webhook configuration +- [HMAC signature verification](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries) — how GitHub's `X-Hub-Signature-256` works +- [Tailscale Serve](https://tailscale.com/kb/1312/serve) — expose your webhook endpoint without a public IP +- [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — alternative to Tailscale for public webhook ingress diff --git a/src/content/docs/configuration/managed-providers.md b/src/content/docs/configuration/managed-providers.md new file mode 100644 index 0000000..3e0092b --- /dev/null +++ b/src/content/docs/configuration/managed-providers.md @@ -0,0 +1,222 @@ +--- +title: Managed Providers +description: Configure cloud-hosted LLM providers like OpenRouter, Anthropic, and OpenAI. +--- + +Managed providers are cloud-hosted LLM services that netclaw connects to over HTTPS. Each requires an API key or OAuth token — OpenRouter, Anthropic, and OpenAI are supported. + +Provider config lives in two files. Non-secret fields (type, endpoint, auth method) go in `~/.netclaw/config/netclaw.json`. Credentials go in `~/.netclaw/config/secrets.json`, which is [encrypted at rest](/security/secrets/) — write plaintext values and netclaw encrypts them on first read. Environment variables override both. + +If you're setting up netclaw for the first time, [`netclaw init`](/cli/init/) handles provider selection interactively. This page covers manual configuration and the details behind each provider type. + +For self-hosted inference (Ollama, llama.cpp, vLLM), see [Self-Hosted Providers](/configuration/self-hosted-providers/). + +## Provider Summary + +| Type | Display Name | Default Endpoint | Auth | Key Source | +|------|-------------|-----------------|------|------------| +| `openrouter` | OpenRouter | `https://openrouter.ai/api/v1` | API key | [openrouter.ai/keys](https://openrouter.ai/keys) | +| `anthropic` | Anthropic | `https://api.anthropic.com` | API key | [console.anthropic.com](https://console.anthropic.com/settings/keys) | +| `openai` | OpenAI | `https://api.openai.com` | OAuth PKCE or API key | [platform.openai.com](https://platform.openai.com/api-keys) | + +## Configuration Schema + +Each provider is a named entry under the `Providers` section. The key is a name you choose — you can have multiple entries of the same type (e.g., two Anthropic providers with different keys). + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Type` | string | `"ollama"` | Provider SDK: `openrouter`, `anthropic`, or `openai` for managed providers. Always set this explicitly. | +| `Endpoint` | string | `""` | Base URL. Leave empty to use the provider's default | +| `ApiKey` | string? | `null` | API key. Store in `secrets.json`, never `netclaw.json` | +| `AuthMethod` | enum | `None` | `None`, `ApiKey`, `OAuthDevice`, or `OAuthPkce` | + +## Config File Examples + +### `netclaw.json` — non-secret fields + +```json +{ + "Providers": { + "openrouter": { + "Type": "openrouter", + "Endpoint": "https://openrouter.ai/api/v1" + }, + "my-anthropic": { + "Type": "anthropic" + }, + "my-openai": { + "Type": "openai", + "AuthMethod": "OAuthPkce" + } + } +} +``` + +### `secrets.json` — encrypted credentials + +```json +{ + "Providers": { + "openrouter": { "ApiKey": "sk-or-v1-..." }, + "my-anthropic": { "ApiKey": "sk-ant-..." } + } +} +``` + +OpenAI with OAuth doesn't need an entry in `secrets.json` — tokens are managed automatically after the OAuth flow. + +You can also manage credentials with [`netclaw secrets set`](/cli/secrets/) instead of editing `secrets.json` directly. + +After adding a provider, assign it to a [model role](/configuration/models/) — Main, Fallback, or Compaction — with [`netclaw model set`](/cli/model/). + +## OpenRouter + +One API key, hundreds of models. OpenRouter proxies requests to Anthropic, OpenAI, Meta, Mistral, and others, so you don't need a separate account with each vendor. + +```bash +netclaw provider add openrouter openrouter --api-key sk-or-v1-... +``` + +Or configure manually in `netclaw.json`: + +```json +{ + "Providers": { + "openrouter": { + "Type": "openrouter" + } + } +} +``` + +The endpoint defaults to `https://openrouter.ai/api/v1` — only override it if you're using a proxy. + +Get your key at [openrouter.ai/keys](https://openrouter.ai/keys). Browse available models in the [OpenRouter model catalog](https://openrouter.ai/models). + +## Anthropic + +Connects directly to Claude models via the Anthropic API. + +```bash +netclaw provider add my-anthropic anthropic --api-key sk-ant-... +``` + +The endpoint defaults to `https://api.anthropic.com`. Netclaw sends the `x-api-key` header and `anthropic-version: 2023-06-01` on probe requests. + +Get your key at [console.anthropic.com/settings/keys](https://console.anthropic.com/settings/keys). See the [Anthropic API docs](https://docs.anthropic.com/en/api/getting-started) for model versions and usage limits. + +## OpenAI + +OpenAI supports two auth methods: [OAuth PKCE](https://datatracker.ietf.org/doc/html/rfc7636) (Proof Key for Code Exchange, uses your ChatGPT subscription) and API key (uses platform.openai.com credits). + +### ChatGPT Subscription (OAuth) + +If you already pay for ChatGPT Plus or Pro, this is the way to go — no API keys to manage. Setup happens inside the TUI, which opens a browser window for the OAuth flow: + +```bash +netclaw provider # opens Provider Manager +``` + +Select **+ Add new provider**, choose **OpenAI**, then pick **ChatGPT Subscription (recommended)**. + +You can also start the OAuth device flow from the CLI without the TUI: + +```bash +netclaw provider add my-openai openai --auth oauth-device +``` + +OAuth requires a browser — if you're on a headless server over SSH, use the API key method instead. + +OAuth tokens expire periodically. When they do, open `netclaw provider`, select the unhealthy provider, and re-authenticate. + +Because OAuth tokens can't call OpenAI's `/v1/models` endpoint, netclaw uses a curated model list: + +| Category | Models | +|----------|--------| +| Frontier | gpt-5.4, gpt-5, gpt-5-mini, gpt-5-nano, gpt-4.1, gpt-4.1-mini, gpt-4.1-nano | +| Reasoning | o3, o3-mini, o4-mini | +| Codex | gpt-5.3-codex, gpt-5.2-codex, gpt-5-codex | + +This list is updated with each netclaw release. + +### API Key + +```bash +netclaw provider add my-openai openai --api-key sk-proj-... +``` + +Get your key at [platform.openai.com/api-keys](https://platform.openai.com/api-keys). With API key auth, netclaw discovers available models automatically via `/v1/models`. + +## Provider Manager TUI + +![Provider Manager TUI showing configured providers with health status](/screenshots/output/provider-manager.png) + +`netclaw provider` probes each configured provider on startup and shows whether it's reachable and authenticated: + +| Indicator | Meaning | +|-----------|---------| +| `✓` | Healthy — models discovered | +| `⚠` | Unreachable or auth failure | +| `…` | Probe in progress | + +Select a provider to manage it: + +| Key | Action | +|-----|--------| +| `K` | Update API key | +| `R` | Remove provider | +| `V` | Re-validate connection | + +## Environment Variable Overrides + +Skip config files entirely by setting environment variables with the `NETCLAW_` prefix. Double underscores (`__`) separate path segments, following the [.NET configuration convention](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider): + +```bash +# Override a provider's API key +export NETCLAW_Providers__openrouter__ApiKey="sk-or-v1-..." + +# Point a model role at a provider +export NETCLAW_Models__Main__Provider="openrouter" +export NETCLAW_Models__Main__ModelId="anthropic/claude-sonnet-4" +``` + +Environment variables take highest priority, overriding both `netclaw.json` and `secrets.json`. On Linux, variable names are case-sensitive — `NETCLAW_Providers__openrouter__ApiKey` won't match `NETCLAW_PROVIDERS__OPENROUTER__APIKEY`. + +## Probe and Health Checks + +When the TUI or daemon starts, netclaw probes each provider by hitting its model-listing endpoint. Each probe has a 10-second HTTP timeout. + +Common probe errors: + +| HTTP Status | Message | +|-------------|---------| +| 401 | Invalid credentials — double-check your provider key | +| 403 | Access denied — credentials may lack model-listing permissions | +| 404 | Models API not found — service may be down | +| 429 | Rate limited — wait a moment and try again | +| 5xx | Provider returned an error — may be experiencing issues | +| Connection failure | Network issue — check endpoint and connectivity | + +To test your configuration right now, run [`netclaw doctor`](/cli/doctor/) for a full config and connectivity diagnostic, or open `netclaw provider` to see live health status. + +## Caveats + +- Changing providers requires a daemon restart. +- Missing credentials are a startup error, not a warning — the daemon won't start if a configured provider has no API key and no `NETCLAW_Providers____ApiKey` env var. +- Each [model role](/configuration/models/) uses exactly one provider. There's no automatic failover or per-channel provider selection. Change the active provider with [`netclaw model set`](/cli/model/). + +## Related Pages + +- [`netclaw provider`](/cli/provider/) — CLI reference for all provider commands +- [`netclaw model`](/cli/model/) — assign providers to Main, Fallback, and Compaction roles +- [Secrets Management](/security/secrets/) — how credentials are encrypted and stored +- [Self-Hosted Providers](/configuration/self-hosted-providers/) — Ollama and openai-compatible setup +- [Models](/configuration/models/) — model role configuration + +## Resources + +- [OpenRouter API docs](https://openrouter.ai/docs/api-reference/overview) — request format, rate limits, model routing +- [Anthropic API docs](https://docs.anthropic.com/en/api/getting-started) — authentication, model versions, usage limits +- [OpenAI API docs](https://platform.openai.com/docs/api-reference) — endpoints, authentication, model capabilities +- [.NET environment variable configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider) — the double-underscore nesting convention +- [RFC 7636 — OAuth PKCE](https://datatracker.ietf.org/doc/html/rfc7636) — the code exchange flow OpenAI uses diff --git a/src/content/docs/configuration/mcp-servers.md b/src/content/docs/configuration/mcp-servers.md index bb3b663..edd1a1e 100644 --- a/src/content/docs/configuration/mcp-servers.md +++ b/src/content/docs/configuration/mcp-servers.md @@ -3,4 +3,294 @@ title: "MCP Servers" description: "Add and manage MCP tool servers." --- -Content coming soon. +Netclaw connects to [Model Context Protocol](https://modelcontextprotocol.io/) servers to give the LLM tools: browsing the web, querying Notion, storing memories, whatever the MCP server exposes. Server definitions live in the `McpServers` section of `~/.netclaw/config/netclaw.json`, with credentials split into [`secrets.json`](/security/secrets/). + +At startup, the daemon loads all enabled servers, discovers their tools, and enforces [per-audience permissions](#per-audience-access-control) on which tools each session can call. + +The CLI writes this JSON for you (`netclaw mcp add`, `netclaw mcp permissions`), but direct editing works fine too. For the full CLI reference, see [`netclaw mcp`](/cli/mcp-tools/). + +## Configuration schema + +Each entry under `McpServers` is keyed by server name: + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Transport` | string | `"stdio"` | `"stdio"`, `"sse"`, or `"http"` | +| `Command` | string? | `null` | Executable for stdio transport | +| `Arguments` | string[]? | `null` | Args passed to the stdio command | +| `Url` | string? | `null` | Endpoint URL for sse/http transport | +| `EnvironmentVariables` | object? | `null` | Env overlay for the stdio process | +| `Headers` | object? | `null` | Additional HTTP headers for http/sse transport | +| `Enabled` | bool | `true` | Whether the server is loaded at startup | +| `GrantCategory` | string? | `null` | ACL grant category; defaults to `mcp:{serverName}`. Set this to group multiple servers under a single permission grant — e.g., both `browser_playwright` and `browser_chrome_devtools` share `browser_automation` so they can be toggled together. | +| `OAuthClientId` | string? | `null` | Static OAuth client ID (servers without dynamic registration) | +| `OAuthScope` | string? | `null` | OAuth scope override (space-separated) | + +Sensitive values (`EnvironmentVariables` entries, `Headers` with tokens) belong in `secrets.json`, not `netclaw.json`. The CLI handles this split for you when you use `netclaw mcp add --env` or `--header`. + +## Transports + +### stdio + +Launches a local process. The daemon starts it on startup and kills it on shutdown. + +```json +{ + "McpServers": { + "memorizer": { + "Transport": "stdio", + "Command": "uvx", + "Arguments": ["memorizer-mcp"] + } + } +} +``` + +[`uvx`](https://docs.astral.sh/uv/) is the runner from the `uv` Python package manager — it downloads and executes MCP servers published as Python packages. `Command` must be on the daemon's `PATH`. + +### http + +Connects to a remote MCP server over HTTP with streamable transport. + +```json +{ + "McpServers": { + "notion": { + "Transport": "http", + "Url": "https://mcp.notion.com", + "OAuthClientId": "abc123", + "OAuthScope": "read write" + } + } +} +``` + +If the server needs OAuth, run `netclaw mcp auth ` after adding it. + +### sse + +Server-Sent Events transport — use this for MCP servers that expose an SSE endpoint instead of streamable HTTP. Some older or self-hosted servers only support SSE. + +```json +{ + "McpServers": { + "my-server": { + "Transport": "sse", + "Url": "https://mcp.example.com/sse", + "Headers": { + "Authorization": "Bearer your-api-token-here" + } + } + } +} +``` + +The `Authorization` header above is a placeholder. Put actual tokens in [`secrets.json`](/security/secrets/) instead of `netclaw.json`, or use `netclaw mcp add --header` which does the split for you. + +## Minimal config + +One stdio server with all defaults: + +```json +{ + "McpServers": { + "memorizer": { + "Command": "uvx", + "Arguments": ["memorizer-mcp"] + } + } +} +``` + +`Transport` defaults to `"stdio"`, `Enabled` defaults to `true`, and `GrantCategory` defaults to `mcp:memorizer`. + +## Production config + +**`~/.netclaw/config/netclaw.json`:** + +```json +{ + "McpServers": { + "browser_playwright": { + "Transport": "stdio", + "Command": "npx", + "Arguments": [ + "-y", "@playwright/mcp@latest", + "--isolated", "--headless", + "--image-responses", "omit", + "--snapshot-mode", "none", + "--browser", "chromium" + ], + "GrantCategory": "browser_automation" + }, + "memorizer": { + "Transport": "stdio", + "Command": "uvx", + "Arguments": ["memorizer-mcp"] + }, + "notion": { + "Transport": "http", + "Url": "https://mcp.notion.com", + "OAuthClientId": "abc123", + "OAuthScope": "read write" + } + } +} +``` + +The `--browser chromium` value is set by `netclaw init` based on which browsers are installed. `netclaw init` may also set `EnvironmentVariables` for the browser profiles depending on your environment. + +`Enabled: true` is omitted throughout — it's the default. + +**`~/.netclaw/config/secrets.json`** (encrypted at rest): + +```json +{ + "McpServers": { + "memorizer": { + "EnvironmentVariables": { + "API_KEY": "ENC:CfDJ8N2x...encrypted..." + } + } + } +} +``` + +## Built-in browser automation profiles + +`netclaw init` offers two browser automation backends. Both use stdio transport and share the `browser_automation` grant category: + +| Profile | Package | Runtime Requirement | +|---------|---------|---------------------| +| `browser_playwright` | `@playwright/mcp@latest` | Node.js + Playwright browser | +| `browser_chrome_devtools` | `chrome-devtools-mcp@latest` | Node.js + local Chrome | + +Both are regular MCP server entries. Edit them in `netclaw.json` like any other server. + +## OAuth flow + +HTTP and SSE servers can authenticate via OAuth: + +1. Run `netclaw mcp auth ` — attempts to open your browser to the authorization page +2. Complete the flow in the browser (or copy the printed URL in headless environments) +3. The CLI polls for completion, timing out after 5 minutes + +Set `OAuthClientId` and `OAuthScope` for servers that don't support [dynamic client registration](https://datatracker.ietf.org/doc/html/rfc7591). stdio servers cannot use OAuth. + +## Per-audience access control + +Netclaw gates MCP server access per [audience](/security/security-model/#trust-audiences) (Personal, Team, Public) in the `Tools.AudienceProfiles` section of `netclaw.json`: + +| Audience | Default `McpServersMode` | Default behavior | +|----------|--------------------------|------------------| +| Personal | `"All"` | All servers allowed | +| Team | `"Allowlist"` | Must list servers in `AllowedMcpServers` | +| Public | `"Allowlist"` | Must list servers in `AllowedMcpServers` | + +Each audience profile controls three things: which servers are visible, which of those servers' tools can be invoked, and whether each tool runs on autopilot, needs confirmation, or is hard-blocked. + +```json +{ + "Tools": { + "AudienceProfiles": { + "Personal": { + "McpServersMode": "All", + "McpServerToolGrants": { + "memorizer": ["search_memories", "store_memory"] + }, + "ApprovalPolicy": { + "DefaultMode": "Auto", + "McpServerDefaults": { "memorizer": "Approval" }, + "ToolOverrides": { "memorizer/delete_memory": "Deny" } + } + }, + "Team": { + "McpServersMode": "Allowlist", + "AllowedMcpServers": ["memorizer"], + "McpServerToolGrants": { "memorizer": ["search_memories"] }, + "ApprovalPolicy": { "DefaultMode": "Approval" } + } + } + } +} +``` + +Omitting `McpServerToolGrants` for a server passes all of that server's tools through (if server-level access is allowed). To grant everything quickly from the CLI, use `netclaw mcp add --grant-all`. + +New servers are fail-closed (blocked by default): all tools blocked for every audience until you grant them. Use `netclaw mcp permissions` to manage grants interactively — the daemon must be running first. + +![MCP Permissions server list showing three connected servers with tool counts](/screenshots/output/mcp-tools-server-list.png) + +Each server shows its connection status and discovered tool count. Select one to configure per-audience grants and approval policies. + +## Daemon status codes + +After the daemon loads your MCP config, `netclaw mcp list` shows live status: + +| Status | Meaning | Action | +|--------|---------|--------| +| `connected (N tools)` | Server up, N tools discovered | None | +| `awaiting auth` | OAuth required | Run `netclaw mcp auth ` | +| `auth failed` | OAuth rejected or credentials invalid | Re-run `netclaw mcp auth ` | +| `unreachable` | Process crashed or network error | Run [`netclaw doctor`](/cli/doctor/) | +| `disabled` | Toggled off | Run `netclaw mcp enable ` when ready | + +## Environment variable overrides + +Override any server field with `NETCLAW_` environment variables. Double underscores separate path segments per the [.NET configuration convention](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider): + +```bash +export NETCLAW_McpServers__notion__Enabled="true" +export NETCLAW_McpServers__notion__Url="https://mcp.notion.com" +export NETCLAW_McpServers__notion__Transport="http" +``` + +These take highest priority, overriding anything in `netclaw.json`. On Linux, variable names are case-sensitive. + +## Validation errors + +`netclaw doctor` validates MCP server config at startup: + +| Condition | Result | +|-----------|--------| +| `Transport` not `stdio`, `sse`, or `http` | Config error | +| stdio transport with empty `Command` | Config error | +| sse/http transport with empty `Url` | Config error | +| Server `unreachable` at runtime | Warning (partial) or Error (all servers down) | +| OAuth auth failed | Error — re-run `netclaw mcp auth ` | + +Invalid config prevents that server from loading, but the rest still start fine. + +## Applying changes + +Config changes take effect after a daemon restart: + +```bash +netclaw daemon restart +netclaw mcp list # shows config + live status +netclaw status # shows MCP connector health +``` + +## Setup sequence + +1. Add a server: `netclaw mcp add ` +2. Restart the daemon: `netclaw daemon restart` +3. Grant tool permissions: `netclaw mcp permissions` (requires a running daemon) +4. Authenticate (if HTTP/SSE with OAuth): `netclaw mcp auth ` +5. Verify: `netclaw mcp list` + +## Related pages + +- [`netclaw mcp`](/cli/mcp-tools/) — CLI reference for all MCP subcommands (add, list, remove, permissions, auth) +- [`netclaw init`](/cli/init/) — adds browser automation MCP servers during onboarding +- [Secrets Management](/security/secrets/) — how credentials in `secrets.json` are encrypted +- [Models](/configuration/models/) — configure which LLMs use these tools +- [Security Model](/security/security-model/) — audience definitions and trust levels + +## Resources + +- [Model Context Protocol specification](https://spec.modelcontextprotocol.io/) — the protocol netclaw implements +- [MCP server registry](https://github.com/modelcontextprotocol/servers) — community-maintained list of MCP servers +- [uv documentation](https://docs.astral.sh/uv/) — the Python package manager behind `uvx` +- [.NET environment variable configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider) — the double-underscore nesting convention +- [OAuth 2.0 Dynamic Client Registration (RFC 7591)](https://datatracker.ietf.org/doc/html/rfc7591) — for servers that support automatic client registration diff --git a/src/content/docs/configuration/models.md b/src/content/docs/configuration/models.md index 05d042f..315777f 100644 --- a/src/content/docs/configuration/models.md +++ b/src/content/docs/configuration/models.md @@ -3,4 +3,181 @@ title: "Models" description: "Configure Main, Fallback, and Compaction model slots." --- -Content coming soon. +Netclaw assigns LLMs to three roles in the `Models` section of `~/.netclaw/config/netclaw.json`: **Main**, **Fallback**, and **Compaction**. Only Main is required. The other two route to Main when unset. + +Before assigning models, you need at least one provider configured. See [Managed Providers](/configuration/managed-providers/) or [Self-Hosted Providers](/configuration/self-hosted-providers/). + +For CLI commands that manage models interactively, see [`netclaw model`](/cli/model/). + +## Model roles + +| Role | Purpose | Required? | +|------|---------|-----------| +| **Main** | Primary model for all interactions | Yes — defaults to `qwen3:30b` on `local-ollama` | +| **Fallback** | Automatic failover when Main is unavailable | No — routes to Main when unset | +| **Compaction** | Cheaper/faster model for context summarization | No — routes to Main when unset | + +![Model Manager TUI showing role assignments](/screenshots/output/model-manager.png) + +## Configuration schema + +Each role is an object under `Models` with these fields: + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Provider` | string | `"local-ollama"` | Key into the `Providers` dictionary | +| `ModelId` | string | `"qwen3:30b"` | Model identifier as used by the provider API | +| `ContextWindow` | int? | `null` | Clamps the runtime context window in tokens; takes precedence over provider-reported value | +| `Provenance` | enum? | `null` | Read-only. Set by the CLI: `"Live"` (discovered from provider), `"Defaults"` (curated defaults), or `"Manual"` (`model set`) | +| `InputModalities` | flags enum? | `null` | Override input modalities, e.g. `"Text, Image"`. Values: `Text`, `Image`, `Audio`, `Video` | +| `OutputModalities` | flags enum? | `null` | Override output modalities (same values, e.g. `"Text"`) | + +The schema enforces `additionalProperties: false`, so only `Main`, `Fallback`, and `Compaction` are valid role names. + +## Minimal config + +With Ollama running locally and `qwen3:30b` pulled, this is all you need. The `Provider` value must match a key you've defined under `Providers` in the same config file. + +```json +{ + "Models": { + "Main": { + "Provider": "local-ollama", + "ModelId": "qwen3:30b" + } + } +} +``` + +Context window is auto-detected from Ollama. + +## Production config + +Here's a more realistic setup: 30B for Main, 8B for Fallback (resilience if the big model goes down), same 8B for Compaction (summarization doesn't need a big model): + +```json +{ + "Models": { + "Main": { + "Provider": "remote-gpu", + "ModelId": "qwen3:30b", + "ContextWindow": 32768 + }, + "Fallback": { + "Provider": "remote-gpu", + "ModelId": "qwen3:8b", + "ContextWindow": 32768 + }, + "Compaction": { + "Provider": "remote-gpu", + "ModelId": "qwen3:8b" + } + } +} +``` + + + +## Context window resolution + +Config takes precedence over anything the provider reports: + +1. `ContextWindow` value in config (highest priority) +2. Provider-detected value (via `/api/show`, `/v1/models`, etc.) +3. Default: 32,768 tokens + +Any role with an explicit `ContextWindow` must set it to at least 4,096 tokens. If Main's `ContextWindow` exceeds what the provider reports, the daemon refuses to start and tells you why. + +## Capability detection + +Netclaw auto-detects what a model supports (context window, modalities) by walking this list until something answers: + +1. Built-in static catalog (covers well-known models with zero network cost) +2. Ollama `/api/show` — only when the provider type is `ollama` +3. OpenAI-compatible `/v1/models` metadata — only when the provider type is `openai-compatible` +4. [OpenRouter](https://openrouter.ai/models) public catalog +5. [HuggingFace](https://huggingface.co/models) capability resolver +6. Text-only defaults (32,768 token context window) + +If your provider misreports capabilities (say, an Ollama model supports vision but detection shows text-only), set `InputModalities` or `OutputModalities` in config to override detection. + +## Failover + +When Fallback is configured, netclaw wraps both models in a failover layer. If Main throws after exhausting retries, the request goes to Fallback automatically. + +Retries happen first: 3 attempts with exponential backoff (1s base, 30s max, ±25% jitter). Retried errors: network failures, HTTP 408/429/5xx, `TaskCanceledException`, `TimeoutException`. Only after all retries fail does failover kick in. + +There's a catch with streaming. Failover only applies if Main fails before the first chunk reaches the caller. Once a chunk has been emitted, failures propagate directly. Splicing two model responses together mid-stream would produce garbage, so netclaw doesn't try. + +| Event | Alert Level | +|-------|-------------| +| Main fails, Fallback takes over | `provider.failover` — Warning | +| Both Main and Fallback fail | `provider.unreachable` — Critical | + +If Fallback is not configured, failed retries on Main surface the error directly. + +## Compaction + +Compaction is for background LLM work: summarizing conversation context when it grows too long, generating session titles, extracting memories. These don't need your best model. An 8B handles them fine and saves compute for actual conversations. + +Compaction fires when context reaches 75% of the context window. You can tune this with `Session.CompactionThreshold`. + +## Environment variable overrides + +You can override any model field with `NETCLAW_` environment variables. Double underscores separate path segments, following the [.NET configuration convention](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider): + +```bash +export NETCLAW_Models__Main__Provider="openrouter" +export NETCLAW_Models__Main__ModelId="anthropic/claude-sonnet-4" +export NETCLAW_Models__Main__ContextWindow="200000" +``` + +These take highest priority, overriding anything in `netclaw.json`. On Linux, variable names are case-sensitive. + +## Validation errors + +| Condition | Result | +|-----------|--------| +| Main `Provider` or `ModelId` is empty | Startup fails | +| `ContextWindow` < 4,096 on any role | Startup fails | +| Main `ContextWindow` exceeds provider-reported value | Startup fails with descriptive error | +| Unknown role name in `Models` | Config schema rejects it | +| Provider key doesn't exist in `Providers` | `netclaw model set` rejects it; lists configured providers | + +## Applying changes + +All model config changes require a daemon restart: + + + +```bash +netclaw daemon restart +``` + +Verify models are picked up: + +```bash +netclaw model list # reads from config +netclaw status # shows what the running daemon is using +``` + +## Typical setup sequence + +1. Configure a provider ([Managed Providers](/configuration/managed-providers/) or [Self-Hosted Providers](/configuration/self-hosted-providers/)) +2. Assign models to roles (this page, or [`netclaw model set`](/cli/model/)) +3. Restart the daemon +4. Verify with `netclaw status` + +## Related pages + +- [`netclaw model`](/cli/model/) — CLI reference for model management (TUI, `set`, `discover`, `list`, `clear`) +- [Managed Providers](/configuration/managed-providers/) — configure cloud providers (OpenRouter, Anthropic, OpenAI) +- [Self-Hosted Providers](/configuration/self-hosted-providers/) — configure Ollama, llama.cpp, vLLM +- [Secrets Management](/security/secrets/) — how credentials are encrypted and stored + +## Resources + +- [Ollama model library](https://ollama.com/library) — browse models for local inference +- [Ollama modelfile parameters](https://github.com/ollama/ollama/blob/main/docs/modelfile.md#parameter) — context window and other model-level settings +- [OpenRouter model catalog](https://openrouter.ai/models) — compare models across providers with pricing +- [.NET environment variable configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider) — the double-underscore nesting convention diff --git a/src/content/docs/configuration/providers.md b/src/content/docs/configuration/providers.md deleted file mode 100644 index 6c097cf..0000000 --- a/src/content/docs/configuration/providers.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "Providers" -description: "Configure LLM providers: Ollama, OpenAI, Anthropic, OpenRouter." ---- - -Content coming soon. diff --git a/src/content/docs/configuration/reminders.md b/src/content/docs/configuration/reminders.md index 9644fd4..8641460 100644 --- a/src/content/docs/configuration/reminders.md +++ b/src/content/docs/configuration/reminders.md @@ -3,4 +3,218 @@ title: "Reminders" description: "Schedule recurring and one-time reminder prompts." --- -Content coming soon. +Reminders are autonomous agent sessions that fire on a schedule. You define what the agent should do, pick a schedule (once, interval, or cron), and optionally send results to Slack. The daemon handles the rest. + +The scheduling subsystem is enabled by default. For CLI commands that create and manage reminders, see [`netclaw reminder`](/cli/reminder/). + +## Global configuration + +Toggle the scheduling subsystem in `~/.netclaw/config/netclaw.json`: + +```json +{ + "Scheduling": { + "Enabled": true + } +} +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Enabled` | bool | `true` | When `false`, all reminder scheduling stops and the LLM tools (`set_reminder`, `cancel_reminder`, `list_reminders`, `get_reminder_history`) are unregistered. | + +Disabling doesn't delete existing reminders — it just stops them from firing. Re-enable and they pick up where they left off. + +## Filesystem layout + +Reminder data lives alongside other netclaw state: + +``` +~/.netclaw/schedules/reminders/ +├── daily-standup.json # reminder definition +├── daily-standup.history.jsonl # execution history (append-only) +├── infra-check.json +└── infra-check.history.jsonl +``` + +Each reminder gets two files: a JSON definition and a JSONL history log. The filename is the reminder ID (URL-encoded if it contains special characters). History is capped at 500 entries per reminder — oldest entries are trimmed automatically. + +## Reminder definition schema + +This is the JSON format used by `netclaw reminder show`, `import`, and `validate`: + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `id` | string | Yes | Kebab-case slug, max 50 characters | +| `title` | string | Yes | Human-readable name | +| `instructions` | string | Yes | Prompt for the agent session when the reminder fires | +| `schedule.type` | enum | Yes | `OneShot`, `Interval`, or `Cron` | +| `schedule.fireAtMs` | long | OneShot | Unix timestamp in milliseconds. Validated at import time. | +| `schedule.intervalTicks` | long | Interval | .NET Ticks (1 tick = 100ns). Auto-populated by the CLI from duration strings — you don't set this manually. | +| `schedule.cronExpression` | string | Cron | Standard 5-field cron expression (UTC) | +| `schedule.originalExpression` | string | No | Original human input (e.g. `"6h"`, `"0 */6 * * *"`) | +| `delivery.kind` | enum | Yes | `CurrentSession`, `Channel`, or `None` | +| `delivery.transport` | string | Channel | `"slack"` | +| `delivery.address` | string | Channel | Slack channel ID or user ID | +| `delivery.sessionId` | string | CurrentSession | Auto-populated by the daemon from the creating session context | +| `deliveryRequired` | bool | No | Default `true`. Marks execution failed if delivery doesn't happen. | +| `deliveryInstructions` | string | No | Guidance for what to include in the delivery message | +| `audience` | enum | No | `Personal`, `Team`, or `Public`. Inherited from the creating session. Controls which tools the reminder session can access — see [MCP Servers](/configuration/mcp-servers/). | +| `enabled` | bool | No | Default `true` | +| `expiresAtMs` | long | No | Auto-disable after this timestamp. Not valid on OneShot reminders. | +| `createdBy` | string | No | `"llm-tool"`, `"cli"`, `"system"` | +| `createdAtMs` | long | No | Unix ms creation timestamp | +| `updatedAtMs` | long | No | Unix ms last-update timestamp | + +### Minimal definition + +```json +{ + "id": "infra-check", + "title": "Infrastructure Health Check", + "instructions": "Run netclaw doctor and summarize any degraded services.", + "schedule": { + "type": "Interval", + "intervalTicks": 216000000000, + "originalExpression": "6h" + }, + "delivery": { + "kind": "None" + } +} +``` + +### Channel delivery definition + +```json +{ + "id": "daily-standup", + "title": "Daily Standup Summary", + "instructions": "Check yesterday's reminder history and git commits. Post a standup summary.", + "schedule": { + "type": "Cron", + "cronExpression": "0 9 * * MON-FRI", + "originalExpression": "0 9 * * MON-FRI" + }, + "delivery": { + "kind": "Channel", + "transport": "slack", + "address": "C12345678" + }, + "deliveryRequired": true, + "deliveryInstructions": "Post the summary to the standup channel. Keep it under 5 bullet points.", + "audience": "Team" +} +``` + +To load a definition from a file: `netclaw reminder validate ` to check for errors, then `netclaw reminder import ` to register it with the daemon. + +## Schedule types + +| Type | Fires | Minimum | Format | +|------|-------|---------|--------| +| `OneShot` | Once, then auto-disables | 60 seconds from now | Relative duration (`30m`, `2h`) or ISO 8601 timestamp | +| `Interval` | Repeats on a fixed duration | 60 seconds | Duration string (`15m`, `6h`, `1d`) | +| `Cron` | On a cron schedule | N/A | 5-field cron expression, UTC | + +The 60-second minimum is enforced for all schedule types. Cron expressions are always evaluated in UTC. + +Interval reminders fire their first execution one interval after creation (or after being re-enabled) — not at a fixed wall-clock time. Imported definitions with an explicit `fireAtMs` will use that timestamp for the first fire instead. + +For more detail on creating reminders with each schedule type, see [`netclaw reminder create`](/cli/reminder/#create). + +## Delivery modes + +| Kind | What happens | Requirements | +|------|-------------|--------------| +| `None` | Agent runs silently. Results recorded in history only. | Nothing | +| `Channel` | Agent posts results to a Slack channel via `send_slack_message` (called automatically by the agent) | `transport` + `address` + [Slack configured](/cli/init/) | +| `CurrentSession` | Re-enters the originating conversation (Slack thread, TUI, etc.) | Active session context at creation time | + +When `deliveryRequired` is `true` (the default) and delivery kind is `Channel`, the execution is marked **failed** if the agent doesn't post to the target channel. Each failed execution emits a `ReminderExecutionFailed` warning alert. After 5 consecutive failures, the reminder is auto-disabled and a `ReminderAutoDisabled` critical alert fires. + +For `CurrentSession` delivery, if the originating session is no longer active, delivery times out after 300 seconds and the execution is marked failed. + +For Slack delivery, use `netclaw reminder create --delivery channel` or set `delivery.kind` to `"Channel"` in the JSON directly. + +## Runtime behavior + +| Setting | Value | Configurable? | +|---------|-------|---------------| +| Execution timeout | 300 seconds | No | +| Max concurrent executions | 3 | No | +| History retention | 500 entries per reminder | No | +| Overflow handling | FIFO queue (unbounded) | — | + +When all 3 slots are busy, incoming fires queue and run as slots open. + +Recurring reminders get an automatic instruction appended: if the agent determines the reminder's purpose has been permanently fulfilled, it should call `cancel_reminder` to stop future fires. + +## Auto-disable and expiration + +Two mechanisms stop reminders automatically. + +### Consecutive failures + +After 5 failed executions in a row, the reminder is disabled and a `ReminderAutoDisabled` critical alert fires via your [notification webhooks](/configuration/webhooks/#outbound-notification-webhooks). A single successful execution resets the failure counter. + +Fix the underlying issue (usually a missing Slack channel or invalid delivery target), then re-enable: + +```bash +netclaw reminder enable +``` + +### Expiration + +Set `expiresAtMs` (or `--expires-in` on the CLI) to auto-disable a recurring reminder after a deadline. The reminder fires normally until the expiration time, then disables on the next scheduled fire. Not applicable to one-shot reminders. + +## LLM tools + +The agent manages reminders through four tools, all requiring the `scheduling` grant (grants control which LLM tools are available — see [MCP Servers](/configuration/mcp-servers/) for grant configuration): + +| Tool | Description | +|------|-------------| +| `set_reminder` | Create or update (upsert) a reminder | +| `cancel_reminder` | Disable a reminder (definition preserved) | +| `list_reminders` | List active or all reminders | +| `get_reminder_history` | Return execution history (default 20, max 100) | + +These tools are only registered when `Scheduling.Enabled` is `true`. + +## Stale one-shot cleanup + +On daemon startup, the reconciliation process permanently deletes any one-shot reminder that already fired — the only time a definition disappears without an explicit `delete` command. Expired recurring reminders are disabled (not deleted) during reconciliation. + +## Troubleshooting + +### Reminder fires but delivery fails + +Verify that Slack is still configured and the target channel exists. Run `netclaw reminder history ` — if you see repeated `failed` statuses, the auto-disable countdown is ticking. Fix the channel, then `netclaw reminder enable `. + +### Reminder doesn't fire + +First verify the daemon is running (`netclaw status`). Then check `Scheduling.Enabled` in your config and run `netclaw reminder show ` to confirm `enabled` is `true`. + +### Cron fires at unexpected times + +Cron expressions are evaluated in UTC, not local time. A `0 9 * * *` schedule fires at 9:00 UTC. Adjust your expression for your timezone offset. + +## What's next + +Run `netclaw reminder list` to confirm your reminder appears with the right schedule and next fire time. After it fires, `netclaw reminder history ` shows whether it succeeded — grab the session ID from that output and pass it to [`netclaw sessions`](/cli/sessions/) to see the full execution log. + +## Related pages + +- [`netclaw reminder`](/cli/reminder/) — CLI reference for all reminder subcommands +- [Webhooks](/configuration/webhooks/) — outbound alerts including `ReminderAutoDisabled` +- [Operational Alerts](/observability/operational-alerts/) — full alert type reference including `ReminderExecutionFailed` and `ReminderAutoDisabled` +- [`netclaw sessions`](/cli/sessions/) — inspect what happened during a reminder execution +- [`netclaw doctor`](/cli/doctor/) — validates daemon subsystems including scheduling + +## Resources + +- [Crontab.guru](https://crontab.guru/) — visual editor for cron schedule expressions +- [Cron expression syntax (Wikipedia)](https://en.wikipedia.org/wiki/Cron#Cron_expression) — reference for the standard 5-field cron format +- [ISO 8601 (Wikipedia)](https://en.wikipedia.org/wiki/ISO_8601) — timestamp format for one-shot schedules +- [Locate your Slack channel ID](https://slack.com/help/articles/221769328-Locate-your-Slack-URL-or-ID) — find channel IDs for delivery targets +- [Slack App Permissions](https://api.slack.com/scopes) — required scopes for channel delivery diff --git a/src/content/docs/configuration/self-hosted-providers.md b/src/content/docs/configuration/self-hosted-providers.md new file mode 100644 index 0000000..ae61bc5 --- /dev/null +++ b/src/content/docs/configuration/self-hosted-providers.md @@ -0,0 +1,250 @@ +--- +title: Self-Hosted Providers +description: Configure self-hosted inference servers like Ollama, llama.cpp, and vLLM. +--- + +Self-hosted providers run inference on your own hardware. No API keys, no data leaving your network. Netclaw has two provider types for this: native Ollama integration and an OpenAI-compatible mode that works with any server exposing a `/v1/chat/completions` endpoint — llama.cpp, vLLM, Lemonade, or anything else OpenAI-compatible. + +Config goes in `~/.netclaw/config/netclaw.json`. Self-hosted providers don't need credentials unless you're running an authenticated endpoint. Environment variables with the `NETCLAW_` prefix override file-based config. + +`netclaw init` handles provider setup interactively if you're starting fresh. This page covers manual configuration. + +For cloud-hosted providers (OpenRouter, Anthropic, OpenAI), see [Managed Providers](/configuration/managed-providers/). + +## Before You Start + +- [Ollama](https://ollama.com/) or another inference server installed and running +- `netclaw init` completed (or you're configuring manually for the first time) +- The netclaw daemon running (`netclaw daemon start`) + +## Provider Summary + +| Type | Display Name | Default Endpoint | Auth | Use Case | +|------|-------------|-----------------|------|----------| +| `ollama` | Ollama | `http://localhost:11434` | None | Ollama servers (auto-detects model capabilities) | +| `openai-compatible` | llama.cpp / vLLM | `http://localhost:11434` | Optional Bearer token | Anything exposing `/v1/chat/completions` | + +## Configuration Schema + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Type` | string | `"ollama"` | Provider SDK: `ollama` or `openai-compatible` | +| `Endpoint` | string | varies by type | Base URL of the inference server | +| `ApiKey` | string? | `null` | Optional Bearer token for authenticated endpoints | + +## Ollama + +The default provider. Netclaw discovers models via `/api/tags` and detects capabilities per-model through `/api/show`. + +Ollama must have at least one model pulled before netclaw can use it — an empty model list is treated as a probe failure. Pull a model first: + +```bash +ollama pull qwen3:30b +``` + +Browse available models at the [Ollama model library](https://ollama.com/library). + +### Local Ollama + +```json +{ + "Providers": { + "local": { + "Type": "ollama", + "Endpoint": "http://localhost:11434" + } + }, + "Models": { + "Main": { "Provider": "local", "ModelId": "qwen3:30b" } + } +} +``` + +If Ollama is running locally, no other config is needed. + +### Remote Ollama Instance + +Point to any machine on your network: + +```json +{ + "Providers": { + "gpu-box": { + "Type": "ollama", + "Endpoint": "http://192.168.1.50:11434" + } + } +} +``` + +### Capability Detection + +Netclaw queries `/api/show` for each model and inspects the architecture metadata: + +| Capability | Detection Method | Example Models | +|-----------|-----------------|----------------| +| Context window | `{arch}.context_length` field | All models | +| Vision | `{arch}.vision.block_count` field | llava, llama3.2-vision | + +Models without native tool calling still work — netclaw falls back to structured prompting for tool use. + +![Ollama provider setup during the init wizard](/screenshots/output/init-01-provider-ollama.png) + +The init wizard auto-discovers models pulled in your local Ollama instance and lets you assign them to roles. + +## OpenAI-Compatible (llama.cpp, vLLM, Lemonade) + +For any inference server with a `/v1/chat/completions` endpoint — llama.cpp, vLLM, Lemonade, or anything else OpenAI-compatible. Netclaw discovers models via `/v1/models` and streams completions with tool calling. + +### Basic Setup + +```json +{ + "Providers": { + "llama-server": { + "Type": "openai-compatible", + "Endpoint": "http://localhost:8080" + } + }, + "Models": { + "Main": { "Provider": "llama-server", "ModelId": "my-model" } + } +} +``` + +Note: the netclaw default endpoint for `openai-compatible` is `http://localhost:11434`, but llama-server defaults to port 8080 — specify the endpoint explicitly when using llama.cpp. + +### With Authentication + +Some deployments protect the API with a Bearer token. Store tokens in `secrets.json` to keep credentials out of version-controlled config: + +```json +{ + "Providers": { + "vllm-cluster": { "ApiKey": "your-token-here" } + } +} +``` + +The main config in `netclaw.json` just references the provider without the key: + +```json +{ + "Providers": { + "vllm-cluster": { + "Type": "openai-compatible", + "Endpoint": "https://inference.internal:8443" + } + } +} +``` + +### llama.cpp Server Example + +Start llama-server, then point netclaw at it: + +```bash +# Start llama-server (defaults to port 8080) +llama-server -m ./models/qwen3-30b-q4_k_m.gguf --port 8080 + +# Configure netclaw +netclaw provider add llama-local openai-compatible --endpoint http://localhost:8080 +``` + +## Multi-Provider Setup + +Mix provider types freely. Here's Ollama handling Main with a llama.cpp instance as Fallback: + +```json +{ + "Providers": { + "ollama-local": { + "Type": "ollama", + "Endpoint": "http://localhost:11434" + }, + "llama-gpu": { + "Type": "openai-compatible", + "Endpoint": "http://localhost:8080" + } + }, + "Models": { + "Main": { "Provider": "ollama-local", "ModelId": "qwen3:30b" }, + "Fallback": { "Provider": "llama-gpu", "ModelId": "qwen3:14b" } + } +} +``` + +Assign models to roles with [`netclaw model set`](/cli/model/). + +## Environment Variable Overrides + +```bash +# Point at a remote Ollama instance +export NETCLAW_Providers__local__Type="ollama" +export NETCLAW_Providers__local__Endpoint="http://gpu-server:11434" + +# Override model assignment +export NETCLAW_Models__Main__Provider="local" +export NETCLAW_Models__Main__ModelId="qwen3:30b" +``` + +Environment variables follow the [.NET configuration convention](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider) — double underscores separate path segments. + +## Health Checks + +Netclaw probes each provider on startup — `/api/tags` for Ollama, `/v1/models` for openai-compatible. Each probe times out after 10 seconds. + +Common issues: + +| Symptom | Cause | Fix | +|---------|-------|-----| +| Connection refused | Server not running | Start Ollama (`ollama serve`) or llama-server | +| Empty model list | No models pulled | Run `ollama pull ` | +| Timeout | Server overloaded or wrong port | Check endpoint URL and server logs | +| 401 Unauthorized | Token required | Add `ApiKey` to provider config | + +Run [`netclaw doctor`](/cli/doctor/) for a full connectivity diagnostic, or open `netclaw provider` to see live health status. + +## Applying Changes + +After editing `netclaw.json`, restart the daemon for changes to take effect: + +```bash +netclaw daemon restart +``` + +Verify your provider is healthy: + +```bash +netclaw provider # check health indicators in the TUI +netclaw doctor # full diagnostic including provider probes +``` + +## Provider Manager TUI + +![Provider Manager TUI showing configured providers with health status](/screenshots/output/provider-manager.png) + +Self-hosted entries show `✓` when reachable with models discovered, or `⚠` when the server is down or returns errors. Select a provider to manage endpoints, re-probe, or remove it. + +## Limitations + +- Changing providers requires a daemon restart. +- Tool calling quality varies between models. Qwen3 30B+ and Llama 3.1 70B+ handle it well; smaller models often choke on complex tool schemas. +- The openai-compatible provider sends standard OpenAI tool-calling format. Servers that don't implement tool calling will fall back to structured prompting. + +## See Also + +- [`netclaw provider`](/cli/provider/) — manage providers from the CLI or TUI +- [`netclaw model`](/cli/model/) — assign models to Main, Fallback, and Compaction roles +- [Managed Providers](/configuration/managed-providers/) — cloud providers when you want someone else to run the GPUs +- [Models](/configuration/models/) — deep dive on model role configuration and routing +- [Secrets Management](/security/secrets/) — how netclaw encrypts and stores credentials at rest + +## Resources + +- [Ollama](https://ollama.com/) — install and run local models +- [Ollama model library](https://ollama.com/library) — browse available models +- [Ollama API reference](https://github.com/ollama/ollama/blob/main/docs/api.md) — model management, tags, and show endpoints +- [llama.cpp server docs](https://github.com/ggml-org/llama.cpp/blob/master/examples/server/README.md) — flags, endpoints, performance tuning +- [vLLM OpenAI-compatible serving](https://docs.vllm.ai/en/latest/serving/openai_compatible_server.html) — model parallelism and quantization options +- [.NET environment variable configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider) — the double-underscore nesting convention diff --git a/src/content/docs/configuration/webhooks.md b/src/content/docs/configuration/webhooks.md index 5e23ddf..89bfa50 100644 --- a/src/content/docs/configuration/webhooks.md +++ b/src/content/docs/configuration/webhooks.md @@ -3,4 +3,299 @@ title: "Webhooks" description: "Configure webhook routes for event-driven automation." --- -Content coming soon. +Inbound webhook routes let external services POST to your daemon and kick off autonomous agent sessions. The daemon verifies the signature, spawns a session with the route's prompt and the inbound payload, and optionally delivers results to a notification target. + +External services POST JSON to `/api/webhooks/`. The daemon verifies the signature, checks event filters, and starts an autonomous agent session using the route's prompt. Each route is a standalone JSON file in `~/.netclaw/config/webhooks/`. + +![netclaw webhooks list showing the route directory path](/screenshots/output/webhooks-list.png) + +All CLI route management works offline -- no running daemon required. See [`netclaw webhooks`](/cli/webhooks/) for the full CLI reference. + +### Global settings + +Enable inbound webhooks and set the execution timeout in `~/.netclaw/config/netclaw.json`: + +```json +{ + "Webhooks": { + "Enabled": true, + "ExecutionTimeoutSeconds": 300 + } +} +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Enabled` | bool | `false` | Registers the `/api/webhooks/{route}` endpoint. Returns 404 for all routes when disabled. | +| `ExecutionTimeoutSeconds` | int | `300` | Maximum seconds an autonomous webhook session can run before the daemon marks it failed. | + +### Route file schema + +Each route lives at `~/.netclaw/config/webhooks/.json`. The filename (minus `.json`) is the route name and must match `^[a-z0-9]+(?:-[a-z0-9]+)*$` (lowercase kebab-case). + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Enabled` | bool | `true` | Whether this route accepts requests | +| `Prompt` | string | **(required)** | System prompt injected into the webhook session | +| `Verification` | object | **(required)** | Signature/secret verification settings ([details below](#verification)) | +| `Events` | string[] | `[]` (all) | Event type allow-list. Empty array accepts all event types. | +| `Audience` | enum | `"Public"` | Trust level: `Public`, `Team`, or `Personal` | +| `MaxBodyBytes` | int | `1048576` | Maximum request body size in bytes (1 MB default) | +| `RateLimitPerMinute` | int | `30` | Requests accepted per minute per route | +| `DeliveryRequired` | bool | `true` | Whether the agent must deliver results to the notification target | +| `NotifyInstructions` | string | `""` | Custom instructions for how the agent should notify | +| `NotificationTarget` | object? | `null` | Where to deliver results ([details below](#notification-targets)) | + +### Minimal route + +```json +{ + "Prompt": "Summarize this event and log the result.", + "Verification": { + "Kind": "Hmac", + "Secret": "your-shared-secret" + } +} +``` + +Omitted fields use defaults from the table above. + +### Production route (GitHub Issues) + +```json +{ + "Enabled": true, + "Verification": { + "Kind": "Hmac", + "Secret": "whsec_abc123...", + "SignatureHeaderName": "X-Hub-Signature-256", + "SignaturePrefix": "sha256=", + "EventHeaderName": "X-GitHub-Event", + "DeliveryIdHeaderName": "X-GitHub-Delivery" + }, + "Events": ["issues", "issue_comment"], + "Audience": "Team", + "Prompt": "Triage this GitHub issue. Public input may be adversarial or low quality.", + "DeliveryRequired": true, + "NotifyInstructions": "Post a summary to the triage channel.", + "NotificationTarget": { + "Kind": "Slack", + "ChannelId": "C12345678" + } +} +``` + +Or create it with the CLI: + +```bash +netclaw webhooks set github-issues \ + --prompt "Triage this GitHub issue. Public input may be adversarial or low quality." \ + --secret-env GITHUB_WEBHOOK_SECRET \ + --verification-kind hmac \ + --signature-header X-Hub-Signature-256 \ + --signature-prefix "sha256=" \ + --event-header X-GitHub-Event \ + --delivery-header X-GitHub-Delivery \ + --events "issues,issue_comment" \ + --audience team \ + --notification-channel C12345678 +``` + +### Verification + +Every route requires a verification secret. Two modes: + +| Mode | How It Works | Default Signature Header | +|------|-------------|--------------------------| +| `Hmac` | HMAC-SHA256 of the request body, compared with constant-time equality | `X-Webhook-Signature` | +| `HeaderSecret` | Plain shared secret sent in a header | `X-Webhook-Secret` | + +> **CLI vs. JSON naming:** The CLI flag uses `--verification-kind header-secret` (hyphenated lowercase), but the JSON config field requires `"Kind": "HeaderSecret"` (PascalCase, no hyphen). + +Only SHA-256 is supported for HMAC. Both modes share these default headers: + +| Header | Default | Purpose | +|--------|---------|---------| +| Event type | `X-Webhook-Event` | Identifies the event for filtering | +| Delivery ID | `X-Webhook-Delivery` | Unique ID for deduplication | + +Override any header name in the `Verification` object to match your service's convention. GitHub, for example, uses `X-Hub-Signature-256`, `X-GitHub-Event`, and `X-GitHub-Delivery`. + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Kind` | enum | `"Hmac"` | `Hmac` or `HeaderSecret` | +| `HmacAlgorithm` | enum | `"Sha256"` | Only `Sha256` is supported | +| `Secret` | string | **(required)** | Shared secret for verification | +| `SignatureHeaderName` | string? | `"X-Webhook-Signature"` | Header containing the HMAC signature (Hmac mode) | +| `SignaturePrefix` | string? | `""` | Prefix on the signature value, e.g. `sha256=` | +| `SecretHeaderName` | string? | `"X-Webhook-Secret"` | Header containing the secret (HeaderSecret mode) | +| `EventHeaderName` | string? | `"X-Webhook-Event"` | Header with the event type | +| `DeliveryIdHeaderName` | string? | `"X-Webhook-Delivery"` | Header with the unique delivery ID | + +### Audience and trust levels + +The `Audience` field controls which tool permissions the webhook session gets: + +| Audience | Tool Access | +|----------|-------------| +| `Public` | Most restricted -- external untrusted input | +| `Team` | Moderate -- trusted collaborators | +| `Personal` | Full access -- your own services | + +Default is `Public`. Use this for anything internet-facing (GitHub, GitLab). Reserve `Personal` for internal services you fully control. + +### Notification targets + +When `NotificationTarget` is set, the agent posts results to that channel. Only Slack is supported: + +```json +{ + "NotificationTarget": { + "Kind": "Slack", + "ChannelId": "C12345678" + } +} +``` + +| Field | Type | Description | +|-------|------|-------------| +| `Kind` | enum | `Slack` (only option) | +| `ChannelId` | string | Slack channel ID (required when Kind is Slack) | + +To find your Slack channel ID, see [Locate your Slack URL or ID](https://slack.com/help/articles/221769328-Locate-your-Slack-URL-or-ID). + +When `DeliveryRequired` is `true` and the route has notification instructions -- either explicit `NotifyInstructions` or auto-generated from a `NotificationTarget` -- the agent *must* call `send_slack_message` during the session. If it doesn't, the run is marked failed. When `DeliveryRequired` is `false`, the agent's session prompt tells it that notification is optional and can be skipped if there's nothing actionable. + +Routes without a `NotificationTarget` and without `NotifyInstructions` don't enforce delivery at all, regardless of the `DeliveryRequired` flag. + +### Ingress pipeline + +Requests to `/api/webhooks/{route}` go through these checks in order: + +| Step | Check | Failure Response | +|------|-------|-----------------| +| 1 | `Webhooks.Enabled` | 404 (entire webhook system is off) | +| 2 | Route lookup | 404 | +| 3 | Body size | 413 Payload Too Large | +| 4 | JSON validation | 400 Bad Request | +| 5 | Signature/secret verification | 401 Unauthorized | +| 6 | Event type filter | 202 (ignored) | +| 7 | Delivery ID dedup | 202 (ignored) | +| 8 | Rate limit | 429 + `Retry-After` header | +| 9 | Dispatch | 202 Accepted | + +After dispatch, the agent session runs asynchronously -- the 202 response returns immediately without waiting for the session to complete. + +Accepted response body: + +```json +{ + "status": "accepted", + "route": "github-issues", + "eventType": "issues", + "deliveryId": "abc-123", + "sessionId": "webhook/github-issues/abc-123" +} +``` + +The `deliveryId` field is `null` when the sender doesn't include a delivery ID header. The daemon generates a synthetic ID internally for session tracking, but it isn't returned in the response. + +### Hot-reload + +Route files are re-read from disk on each request (the daemon checks `LastWriteTime`). Edit a route file, and the next request picks up the change. The daemon removes invalid files from the catalog immediately and triggers a `webhook.route.invalid` alert. + +No daemon restart needed for route changes. Global `Webhooks.Enabled` and `ExecutionTimeoutSeconds` changes *do* require a restart. + +### Rate limiting and deduplication + +- **Rate limit window:** 1 minute (sliding). Configurable per route via `RateLimitPerMinute`. +- **Dedup window:** 1 hour. Deliveries with the same ID within this window are silently ignored (202). +- Session ID format: `webhook//` + +### Validation rules + +`netclaw webhooks validate ` and [`netclaw doctor`](/cli/doctor/) both run these checks: + +| Rule | Error If | +|------|----------| +| Route name | Doesn't match `^[a-z0-9]+(?:-[a-z0-9]+)*$` | +| `Prompt` | Empty or missing | +| `Verification.Secret` | Empty or missing | +| `MaxBodyBytes` | Less than 1 | +| `RateLimitPerMinute` | Less than 1 | +| `Events` entries | Contains blank strings | +| `DeliveryRequired` + `NotifyInstructions` | `DeliveryRequired` is `true` AND `NotifyInstructions` is non-empty AND `NotificationTarget` is `null` (all three conditions simultaneously) | +| `NotificationTarget.Kind = Slack` | Missing `ChannelId` | + +### Security + +Route files contain plaintext secrets. Treat `~/.netclaw/config/webhooks/` the same way you treat [`secrets.json`](/security/secrets/): + +- Keep directory permissions at `700` +- Don't commit route files to source control +- The agent is [hard-denied](/security/secrets/#agent-isolation) from reading this directory +- Prefer `--secret-file` or `--secret-env` over `--secret` when creating routes via CLI (avoids shell history exposure) + +## Setup + +1. Enable webhooks in `netclaw.json` (or toggle during [`netclaw init`](/cli/init/)) +2. Create a route: `netclaw webhooks set --prompt "..." --secret-env SECRET_VAR` +3. Restart the daemon to pick up the `Webhooks.Enabled` change: `netclaw daemon stop && netclaw daemon start` +4. Copy the webhook URL from [`netclaw status`](/cli/status/) and paste it into your external service +5. Send a test event and check [`netclaw stats`](/cli/stats/) for delivery counts -- look for the `webhook.received` counter + +![Init wizard showing the inbound webhooks toggle](/screenshots/output/init-09-webhooks.png) + +You only need to restart when first enabling `Webhooks.Enabled`. After that, route changes are [hot-reloaded](#hot-reload) on each request. + +## Troubleshooting + +### 401 Unauthorized -- secret mismatch + +The HMAC signature or header secret doesn't match. Double-check that the secret in your route file matches what the external service is sending. For HMAC, also verify `SignaturePrefix` matches (e.g., GitHub sends `sha256=` before the hex digest). + +### 401 Unauthorized -- wrong signature header + +The daemon is reading the signature from a different header than the one your service sends. Set `SignatureHeaderName` in the route's `Verification` block to match your service (e.g., `X-Hub-Signature-256` for GitHub). + +### 404 Not Found + +Either `Webhooks.Enabled` is `false` in `netclaw.json`, or no route file matches the URL path. Run `netclaw webhooks list` to see loaded routes, and check that `Webhooks.Enabled` is `true`. + +### 413 Payload Too Large + +The request body exceeds the route's `MaxBodyBytes` (default 1 MB). Increase it in the route file if the payloads are legitimately large. + +## Finding your webhook URL + +Run [`netclaw status`](/cli/status/) to see the webhook base URL. Your route's full endpoint is: + +``` +/api/webhooks/ +``` + +The base URL depends on how you expose the daemon. [Tailscale Serve](https://tailscale.com/kb/1312/serve) and [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) are the two supported ingress options. + +## Limitations + +- Notification targets are Slack-only. Discord, email, and generic webhook-to-notification bridges aren't supported yet. +- Route secrets are stored in plaintext JSON (not in the encrypted `secrets.json` vault). +- Only SHA-256 is supported for HMAC verification. +- No webhook request logging or replay. Failed sessions are visible via [`netclaw stats`](/cli/stats/) but the original payloads aren't stored. + +## Related pages + +- [`netclaw webhooks`](/cli/webhooks/) -- CLI reference for route management (list, show, set, delete, validate) +- [Secrets Management](/security/secrets/) -- encrypted credential storage and agent isolation +- [Security Model](/security/security-model/) -- audience definitions and trust levels +- [`netclaw doctor`](/cli/doctor/) -- validates all webhook route files +- [`netclaw stats`](/cli/stats/) -- delivery counts and rejection breakdowns + +## Resources + +- [GitHub webhook documentation](https://docs.github.com/en/webhooks) -- setting up webhooks on the GitHub side +- [GitLab webhook documentation](https://docs.gitlab.com/ee/user/project/integrations/webhooks.html) -- setting up webhooks on the GitLab side +- [HMAC signature verification](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries) -- how GitHub's `X-Hub-Signature-256` works +- [Tailscale Serve](https://tailscale.com/kb/1312/serve) -- expose your webhook endpoint without a public IP +- [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) -- alternative to Tailscale for public webhook ingress +- [Locate your Slack channel ID](https://slack.com/help/articles/221769328-Locate-your-Slack-URL-or-ID) -- find the channel ID for notification targets diff --git a/src/content/docs/deployment/docker.md b/src/content/docs/deployment/docker.md index 75f0a02..58e4376 100644 --- a/src/content/docs/deployment/docker.md +++ b/src/content/docs/deployment/docker.md @@ -1,6 +1,245 @@ --- title: "Docker Deployment" -description: "Run Netclaw in Docker containers." +description: "Run the netclaw daemon in a Docker container with persistent config, health checks, and zero-downtime upgrades." --- -Content coming soon. +The netclaw Docker image runs the daemon (`netclawd`) in a supervised container. The CLI stays on your host and connects over HTTP at `127.0.0.1:5199`. Your interactive terminal stays outside the container; the daemon runs headless. + +## Before you begin + +- [Docker Engine](https://docs.docker.com/engine/install/) 20.10+ or Docker Desktop +- A provider API key (OpenRouter, Anthropic, OpenAI, etc.) or a reachable [Ollama](https://ollama.com/) instance +- An initialized `~/.netclaw` directory — run `netclaw init` on the host first, or bind-mount a pre-configured one. If you don't have the CLI yet, see [Installation](/getting-started/installation/). + +## Quick start + +```bash +docker run -d \ + --name netclaw \ + -v ~/.netclaw:/root/.netclaw \ + -p 127.0.0.1:5199:5199 \ + ghcr.io/netclaw-dev/netclaw +``` + +The port binding is loopback-only (`127.0.0.1:5199`) because the health check endpoint is unauthenticated — don't expose it to the network. The volume mount persists identity, config, credentials, session state, and logs across restarts. Self-update is disabled in the image; the image tag _is_ the version. Update availability checks still run, so you'll know when a new release exists. + +**Tags:** `:latest` tracks the most recent release. Pin to a version tag (e.g., `:1.2.3`) in production. + +Verify from the host: + +```bash +netclaw status +``` + +The CLI defaults to `http://127.0.0.1:5199`, so no configuration is needed for local Docker. For remote daemons, see [Exposure Modes](/deployment/exposure-modes/). + +### First run with Docker only + +If you don't have `netclaw init` on the host, run it inside the container: + +```bash +docker exec -it netclaw netclaw init +``` + +The interactive wizard works the same way over `docker exec -it`. + +## Configuration via environment variables + +Pass provider credentials and model config as `NETCLAW_`-prefixed environment variables. Double underscores separate path segments, following the [.NET configuration convention](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider). Env vars take highest priority, overriding both `netclaw.json` and `secrets.json`. + +```bash +docker run -d \ + --name netclaw \ + -v ~/.netclaw:/root/.netclaw \ + -p 127.0.0.1:5199:5199 \ + -e NETCLAW_Providers__openrouter__Type=openrouter \ + -e NETCLAW_Providers__openrouter__ApiKey=sk-or-v1-... \ + -e NETCLAW_Models__Main__Provider=openrouter \ + -e NETCLAW_Models__Main__ModelId=anthropic/claude-sonnet-4 \ + ghcr.io/netclaw-dev/netclaw +``` + +Keep secrets out of config files — inject them at runtime. The `ModelId` value must match a valid identifier from the provider API; see [Models](/configuration/models/) for the full schema and available model IDs. + +## Docker Compose + +For anything beyond quick testing, use Compose. This example pairs netclaw with a local Ollama instance and uses named volumes (unlike the bind mount in the quick start) for easier lifecycle management: + +```yaml +services: + netclaw: + image: ghcr.io/netclaw-dev/netclaw + container_name: netclaw + restart: unless-stopped + depends_on: + - ollama + ports: + - "127.0.0.1:5199:5199" + volumes: + - netclaw-home:/root/.netclaw + environment: + NETCLAW_Providers__local-ollama__Type: ollama + NETCLAW_Providers__local-ollama__Endpoint: http://ollama:11434 + NETCLAW_Models__Main__Provider: local-ollama + NETCLAW_Models__Main__ModelId: qwen3:30b + + ollama: + image: ollama/ollama:latest + container_name: ollama + ports: + - "127.0.0.1:11434:11434" + volumes: + - ollama-data:/root/.ollama + +volumes: + netclaw-home: + ollama-data: +``` + +```bash +docker compose up -d +``` + +Pull the model into Ollama before netclaw can use it: + +```bash +docker exec ollama ollama pull qwen3:30b +``` + +Netclaw references Ollama by service name (`http://ollama:11434`) since Compose puts both containers on the same network. + +### Docker socket access + +If you want the agent to manage Docker containers as part of its tool use, mount the socket: + +```bash +-v /var/run/docker.sock:/var/run/docker.sock +``` + +Add this to the `volumes` section of the netclaw service in your Compose file or to the `docker run` command. + +## Volume layout + +Everything the daemon persists lives under `/root/.netclaw`: + +``` +/root/.netclaw/ +├── client/config.json # CLI endpoint state +├── config/ +│ ├── netclaw.json # Daemon settings +│ └── secrets.json # Credentials +├── identity/ # Agent personality (SOUL.md, AGENTS.md, TOOLING.md) +├── sessions/ # Conversation history +├── keys/ # Key material +├── projects/ +├── environment/ +├── schedules/ +└── logs/ # crash-*.log, session logs +``` + +Back up this volume before upgrades. `config/` and `identity/` are the critical directories; everything else can be regenerated. + +## Health checks + +The image has a built-in health check that polls every 15 seconds with a 30-second startup grace period: + +``` +HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 + CMD curl -sf http://127.0.0.1:5199/api/health/ready || exit 1 +``` + +| Endpoint | Auth | Purpose | +|----------|------|---------| +| `GET /api/health/ready` | None | Returns `200 OK` when the daemon is ready. Use this for orchestrators and load balancers. | +| `GET /api/health/status` | Required | Returns detailed runtime status. | + +Check container health from the host: + +```bash +docker inspect --format='{{.State.Health.Status}}' netclaw +``` + +## Process supervision + +The entrypoint script is a PID 1 supervisor. If the daemon exits (config-update restart, crash, `netclaw init` wizard completion), the entrypoint waits 2 seconds and restarts it. The container stays alive, so `docker exec` sessions survive daemon restarts. + +`docker stop` sends SIGTERM, which the entrypoint forwards to the daemon for a clean shutdown. Docker's default 10-second stop timeout is plenty. + +## Upgrading + +Self-update is disabled in the image (`NETCLAW_Daemon__DisableSelfUpdate=true`), so upgrades mean pulling a new image. Schema migrations are forward-only with no automatic rollback. + +```bash +# Pull the new version +docker pull ghcr.io/netclaw-dev/netclaw:latest + +# Stop the old container (volume stays) +docker stop netclaw && docker rm netclaw + +# Start with the new image +docker run -d \ + --name netclaw \ + -v ~/.netclaw:/root/.netclaw \ + -p 127.0.0.1:5199:5199 \ + ghcr.io/netclaw-dev/netclaw:latest + +# Wait for readiness +until curl -sf http://127.0.0.1:5199/api/health/ready; do sleep 2; done +echo "Daemon is ready" +``` + +With Compose: + +```bash +docker compose pull +docker compose up -d +``` + +To rollback, stop the container and start with the previous image tag. If the new version already ran a schema migration, restore the volume from backup to roll back. + +## Image details + +| Property | Value | +|----------|-------| +| Registry | `ghcr.io/netclaw-dev/netclaw` | +| Base | `ubuntu:24.04` | +| Architectures | `linux/amd64`, `linux/arm64` | +| Port | `5199` | +| Volume | `/root/.netclaw` | +| License | Apache-2.0 | + +Built on Ubuntu 24.04 (not a minimal runtime), the image ships with `git`, `jq`, `sqlite3`, `python3`, `curl`, `wget`, `gh`, and more. Operators can `apt-get install` additional tools if the agent needs them. + +## Troubleshooting + +### Container starts but CLI can't connect + +Confirm the port mapping binds to `127.0.0.1` and nothing else is on port 5199: + +```bash +ss -tlnp | grep 5199 +docker logs netclaw +``` + +### Container keeps restarting + +The entrypoint restarts the daemon on every exit, and that's by design. If it's a crash loop, check `docker logs netclaw` for the cause. Common culprits: missing provider config, invalid API key, or a required field missing from `netclaw.json`. + +### Health check failing + +The 30-second start period gives the daemon time to initialize. If it's still unhealthy after that, the daemon isn't starting. Check logs and run `netclaw doctor` from the host, or `docker exec netclaw netclaw doctor` inside the container. + +## Related pages + +- [Models](/configuration/models/) — model slot configuration +- [Exposure Modes](/deployment/exposure-modes/) — remote access via Tailscale or Cloudflare Tunnel +- [systemd Service](/deployment/systemd/) — bare-metal Linux alternative +- [OpenTelemetry](/observability/opentelemetry/) — daemon metrics and log export + +## Resources + +- [Docker Engine installation guide](https://docs.docker.com/engine/install/) — platform-specific install instructions +- [Docker Compose documentation](https://docs.docker.com/compose/) — multi-container orchestration +- [GitHub Container Registry (GHCR)](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry) — pulling and authenticating with ghcr.io +- [.NET environment variable configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider) — the double-underscore nesting convention +- [Ollama model library](https://ollama.com/library) — browse available models for local inference diff --git a/src/content/docs/deployment/exposure-modes.md b/src/content/docs/deployment/exposure-modes.md new file mode 100644 index 0000000..c97312e --- /dev/null +++ b/src/content/docs/deployment/exposure-modes.md @@ -0,0 +1,253 @@ +--- +title: Exposure Modes +description: Configure how netclaw is exposed to the network — local, Tailscale Serve, Tailscale Funnel, or Cloudflare Tunnel. +--- + +Exposure mode controls how the daemon is reachable over the network. Most setups stay on local mode. You only need a tunnel mode when something external (GitHub webhooks, CI, a second machine) needs to reach the daemon. + +## Before you begin + +- Netclaw installed and initialized (`netclaw init`). See [Installation](/getting-started/installation/) if needed. +- For Tailscale modes: [`tailscaled` installed and running](https://tailscale.com/download). +- For Cloudflare Tunnel: [`cloudflared` installed and configured](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/). + +## Modes + +| Mode | Config value | Required process | Reachability | Risk | +|------|-------------|-----------------|-------------|------| +| **Local** | `local` | None | Loopback only | Lowest | +| **Tailscale Serve** | `tailscale-serve` | `tailscaled` | Your Tailscale network (called a [tailnet](https://tailscale.com/kb/1136/tailnet)), HTTPS | Low | +| **Tailscale Funnel** | `tailscale-funnel` | `tailscaled` | Public internet via Tailscale | High | +| **Cloudflare Tunnel** | `cloudflare-tunnel` | `cloudflared` | Public internet via Cloudflare | High | + +The `netclaw init` wizard covers this at step 9: + +![Exposure mode selection in the netclaw init wizard, with Local highlighted as the recommended option](/screenshots/output/init-09-exposure.png) + +Options marked with a warning triangle expose the daemon to the public internet. Tailscale Serve is the recommended remote mode: tailnet-only access, no public exposure. + +## Configuration + +Set the mode in the `Daemon` section of `~/.netclaw/config/netclaw.json`. + +### Local (default) + +No `Daemon` section needed. The daemon binds to `127.0.0.1:5199` and is only reachable from the same machine. + +```json +{} +``` + +### Tailscale Serve + +```json +{ + "Daemon": { + "ExposureMode": "tailscale-serve" + } +} +``` + +Tailscale Serve creates an HTTPS endpoint on your tailnet that proxies to the daemon's local port. Only devices on your tailnet can reach it. Then run [`tailscale serve`](https://tailscale.com/kb/1242/tailscale-serve) to proxy your tailnet hostname to `127.0.0.1:5199`. + +### Tailscale Funnel + +```json +{ + "Daemon": { + "ExposureMode": "tailscale-funnel" + } +} +``` + +Funnel extends Serve to the public internet. Anyone with the URL can reach the daemon, though netclaw's device authentication still applies. Configure it with [`tailscale funnel`](https://tailscale.com/kb/1223/funnel). Only use Funnel when you need public internet access — Tailscale Serve covers everything else. + +### Cloudflare Tunnel + +```json +{ + "Daemon": { + "ExposureMode": "cloudflare-tunnel" + } +} +``` + +Routes traffic through Cloudflare's network to the daemon. Set up `cloudflared` with a [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/get-started/) pointed at `127.0.0.1:5199`, and pair it with a [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/) policy to restrict who can connect. + + + +### Full Daemon section + +| Field | Type | Default | Notes | +|-------|------|---------|-------| +| `Host` | string | `127.0.0.1` | IP address the daemon binds to | +| `Port` | int | `5199` | TCP port | +| `ExposureMode` | string | `local` | `local`, `tailscale-serve`, `tailscale-funnel`, or `cloudflare-tunnel` | + +```json +{ + "Daemon": { + "Host": "127.0.0.1", + "Port": 5199, + "ExposureMode": "tailscale-serve" + } +} +``` + +Case-insensitive — `tailscale-serve`, `TailscaleServe`, and `TAILSCALE-SERVE` all work. + +**Docker users:** if you switch to a tunnel mode, update your container's port binding to match the `Host` and `Port` values here. See [Docker Deployment](/deployment/docker/) for details. + +### Environment variables + +Override any field with `NETCLAW_Daemon__` prefixed env vars: + +```bash +NETCLAW_Daemon__ExposureMode=tailscale-serve +NETCLAW_Daemon__Host=127.0.0.1 +NETCLAW_Daemon__Port=5199 +``` + +Double underscores separate path segments, following the [.NET configuration convention](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider). + +### Restart required + +`Host`, `Port`, and `ExposureMode` require a daemon restart — they aren't hot-reloaded. Other config changes trigger an automatic restart; the daemon drains active sessions and restarts itself. + +```bash +# systemd +systemctl --user restart netclaw + +# Docker +docker restart netclaw +``` + +## Inbound webhooks + +Tunnel modes make [inbound webhooks](/configuration/webhooks/) possible. External services like GitHub or CI systems can trigger autonomous runs via HTTP POST. The `netclaw init` wizard asks about this right after exposure mode selection: + +![Inbound webhook toggle in the init wizard](/screenshots/output/init-09-webhooks.png) + +They do nothing in local mode. + +## Device pairing + +Non-local modes require at least one paired device or an alternative remote authentication scheme. Without one, the daemon refuses to start — remote clients have no way to authenticate. + +The init wizard handles this: it generates a bootstrap device token when you pick a tunnel mode, writing it to `~/.netclaw/config/devices.json` and `~/.netclaw/config/secrets.json`. For manual setup, pair a device before starting the daemon: + +```bash +netclaw daemon pair +``` + +## Startup validation + +If the mode requires `tailscaled` or `cloudflared` and that process isn't running, you'll see this in the daemon logs (`~/.netclaw/logs/daemon.log` or `journalctl --user -u netclaw` for systemd): + +``` +Daemon startup aborted: ExposureMode is 'tailscale-serve' but the required +tunnel process 'tailscaled' is not running. Start 'tailscaled' before starting +Netclaw, or set ExposureMode to 'local' in netclaw.json. +``` + +If no paired devices exist and no alternative auth scheme is configured: + +``` +Daemon startup aborted: ExposureMode is 'tailscale-serve' but no paired devices +exist and no alternative remote authentication scheme is configured. Pair a device +with 'netclaw daemon pair' or configure another remote auth scheme before starting +Netclaw. +``` + +Both are fatal. The daemon won't start until you fix the underlying issue. + +## Troubleshooting + +### Start here: `netclaw doctor` + +Run [`netclaw doctor`](/cli/doctor/) first — it has a dedicated `exposure-mode` check. + +![Netclaw doctor output showing health check diagnostics](/screenshots/output/doctor.png) + +### Tunnel process not running + +Startup aborted with "required tunnel process is not running." + +Start the required process first: + +```bash +# Tailscale modes +sudo systemctl start tailscaled + +# Cloudflare Tunnel +sudo systemctl start cloudflared +``` + +Or switch to local mode if you don't need remote access: + +```json +{ + "Daemon": { + "ExposureMode": "local" + } +} +``` + +### No paired devices + +Startup aborted with "no paired devices exist and no alternative remote authentication scheme is configured." + +Pair a device: + +```bash +netclaw daemon pair +``` + +Or re-run the init wizard, which generates a bootstrap token automatically when you select a tunnel mode: + +```bash +netclaw init +``` + +### Non-loopback bind in local mode + +`netclaw doctor` reports a warning: "ExposureMode is 'local' but bind address is not loopback." + +The daemon is reachable beyond localhost without tunnel protection. Either bind to loopback: + +```json +{ + "Daemon": { + "Host": "127.0.0.1" + } +} +``` + +Or switch to the exposure mode that reflects how the daemon is actually reachable: + +```json +{ + "Daemon": { + "Host": "127.0.0.1", + "ExposureMode": "tailscale-serve" + } +} +``` + + + +## Related pages + +- [Docker Deployment](/deployment/docker/) — containerized daemon with loopback port binding +- [systemd Service](/deployment/systemd/) — bare-metal Linux service management +- [Webhooks](/configuration/webhooks/) — inbound webhook route configuration +- [Models](/configuration/models/) — model slot configuration +- [`netclaw doctor`](/cli/doctor/) — built-in health check diagnostics + +## Resources + +- [Tailscale Serve documentation](https://tailscale.com/kb/1242/tailscale-serve) — set up HTTPS endpoints on your tailnet +- [Tailscale Funnel documentation](https://tailscale.com/kb/1223/funnel) — expose tailnet services to the public internet +- [Cloudflare Tunnel documentation](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) — route traffic through Cloudflare to your origin +- [Cloudflare Access documentation](https://developers.cloudflare.com/cloudflare-one/policies/access/) — identity-aware access policies for tunneled services +- [Tailscale download](https://tailscale.com/download) — install Tailscale on your platform diff --git a/src/content/docs/deployment/reverse-proxy.md b/src/content/docs/deployment/reverse-proxy.md deleted file mode 100644 index 8cf8598..0000000 --- a/src/content/docs/deployment/reverse-proxy.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "Reverse Proxy" -description: "Configure Nginx or Caddy as a reverse proxy." ---- - -Content coming soon. diff --git a/src/content/docs/deployment/systemd.md b/src/content/docs/deployment/systemd.md index 09dc216..9e42de6 100644 --- a/src/content/docs/deployment/systemd.md +++ b/src/content/docs/deployment/systemd.md @@ -3,4 +3,332 @@ title: "systemd Service" description: "Run Netclaw as a systemd service on Linux." --- -Content coming soon. +The netclaw daemon (`netclawd`) runs as a systemd user service — no `sudo`, no root, just your own account. One CLI command sets everything up. + +## Before you begin + +- Linux with systemd **236+** (Ubuntu 20.04+, Debian 11+, Fedora 36+, RHEL 8+, etc.) — check with `systemctl --version` +- `dbus-user-session` installed — required for `systemctl --user` on headless/minimal servers (e.g., `sudo apt install dbus-user-session` on Debian/Ubuntu, then re-login) +- Netclaw installed — see [Installation](/getting-started/installation/) if you don't have it yet +- An initialized `~/.netclaw` directory — run `netclaw init` first +- A configured provider and model — see [Models](/configuration/models/) + +## Install the service + +```bash +netclaw daemon install +``` + +This writes a unit file to `~/.config/systemd/user/netclaw.service`, enables it, and runs `loginctl enable-linger $USER` so the service survives logout. + +Start it: + +```bash +systemctl --user start netclaw +``` + +Verify: + +```bash +netclaw status +``` + +![Netclaw status output showing a running daemon](/screenshots/output/status.png) + +## The unit file + +`netclaw daemon install` generates this at `~/.config/systemd/user/netclaw.service`: + +```ini +[Unit] +Description=Netclaw Daemon +After=network.target + +[Service] +Type=simple +ExecStart=/path/to/netclawd +ExecStop=/path/to/netclaw daemon stop +Restart=always +RestartSec=5 +Environment=DOTNET_ENVIRONMENT=Production + +[Install] +WantedBy=default.target +``` + +The `/path/to/` placeholders are resolved to actual binary locations by `netclaw daemon install` — the literal string never gets written. `ExecStop` uses the CLI's `daemon stop` command instead of a raw signal, which lets the daemon fire shutdown webhooks and drain active sessions before exiting. After `ExecStop` completes, systemd sends SIGTERM with a 10-second grace period. If the process is still alive after that, SIGKILL finishes it. In practice the daemon shuts down well within that window. + +### Manual installation + +If you prefer to create the service file yourself, adjust the `ExecStart` and `ExecStop` paths to wherever your binaries actually live: + +```bash +mkdir -p ~/.config/systemd/user + +cat > ~/.config/systemd/user/netclaw.service << 'EOF' +[Unit] +Description=Netclaw Daemon +After=network.target + +[Service] +Type=simple +ExecStart=/path/to/netclawd +ExecStop=/path/to/netclaw daemon stop +Restart=always +RestartSec=5 +Environment=DOTNET_ENVIRONMENT=Production + +[Install] +WantedBy=default.target +EOF + +systemctl --user daemon-reload +systemctl --user enable netclaw.service +loginctl enable-linger $USER +systemctl --user start netclaw +``` + +The `%h` specifier (expands to your home directory) also works in ExecStart/ExecStop paths if you prefer relative-to-home references. + +## Service management + +| Action | Command | +|--------|---------| +| Start | `systemctl --user start netclaw` | +| Stop | `systemctl --user stop netclaw` | +| Restart | `systemctl --user restart netclaw` | +| Status | `systemctl --user status netclaw` | +| Logs (journal) | `journalctl --user -u netclaw` | +| Enable on boot | `systemctl --user enable netclaw` | +| Disable on boot | `systemctl --user disable netclaw` | + +The CLI commands `netclaw daemon start`, `netclaw daemon stop`, and `netclaw daemon status` also work, regardless of whether systemd is managing the process. + +### Lingering + +systemd kills user services when you log out. [Lingering](https://www.freedesktop.org/software/systemd/man/latest/loginctl.html) prevents that by keeping your user slice alive. + +```bash +# Check if lingering is enabled +ls /var/lib/systemd/linger/$USER + +# Enable it (netclaw daemon install does this automatically) +loginctl enable-linger $USER +``` + +If your daemon stops every time you disconnect SSH, lingering isn't enabled. + +## Configuration + +Config lives at `~/.netclaw/config/netclaw.json`. To relocate the base directory, set `NETCLAW_HOME` — useful when you want data on a separate volume or need to run multiple instances with isolated state. Add it to the `[Service]` section of the unit file: + +```ini +[Service] +Environment=NETCLAW_HOME=/data/netclaw +``` + +### Key paths + +| Path | Contents | +|------|----------| +| `~/.netclaw/config/netclaw.json` | Daemon settings | +| `~/.netclaw/config/secrets.json` | Provider API keys and credentials | +| `~/.netclaw/netclaw.db` | SQLite database (sessions, stats) | +| `~/.netclaw/logs/daemon.log` | Rolling log file | +| `~/.netclaw/netclaw.pid` | PID file | +| `~/.netclaw/netclaw.lock` | Singleton lock (OS-backed exclusive lock) | + +### Network binding + +Default binding is `127.0.0.1:5199`, loopback only. + +```json +{ + "Daemon": { + "Host": "127.0.0.1", + "Port": 5199 + } +} +``` + +To bind a different address or port via environment variables, add them to the unit file: + +```ini +[Service] +Environment=NETCLAW_Daemon__Host=127.0.0.1 +Environment=NETCLAW_Daemon__Port=5200 +``` + +Then reload and restart: + +```bash +systemctl --user daemon-reload +systemctl --user restart netclaw +``` + +For remote access via Tailscale or Cloudflare Tunnel, see [Exposure Modes](/deployment/exposure-modes/). + +### Config reload + +A file watcher on `netclaw.json` picks up changes and triggers a graceful drain-and-restart automatically. One exception: `Daemon.Host`, `Daemon.Port`, and `Daemon.ExposureMode` require a manual `systemctl --user restart netclaw` — the file watcher ignores these fields because changing the listener binding mid-flight isn't safe. + +## Health checks + +The daemon exposes two HTTP endpoints: + +| Endpoint | Auth | Purpose | +|----------|------|---------| +| `GET /api/health/ready` | None | Returns `200 OK` when the daemon is ready | +| `GET /api/health/status` | Required | Detailed runtime status | + +Smoke test: + +```bash +curl -sf http://127.0.0.1:5199/api/health/ready && echo "OK" +``` + +To block systemd from reporting the service as "started" until the daemon is actually ready, add an `ExecStartPost` readiness gate to your `[Service]` section. This delays dependent services until netclaw is genuinely accepting connections: + +```ini +ExecStartPost=/bin/sh -c 'until curl -sf http://127.0.0.1:5199/api/health/ready; do sleep 2; done' +``` + +For richer metrics and log export, see [OpenTelemetry](/observability/opentelemetry/). + +## Logging + +Logs go to `~/.netclaw/logs/daemon.log` as a rolling file — `journalctl --user -u netclaw` captures stdout/stderr from the process, but the structured application logs live in the file. Change the log level via `Logging:LogLevel:Default` in `netclaw.json`: + +```json +{ + "Logging": { + "LogLevel": { + "Default": "Information" + } + } +} +``` + +Valid levels: `Trace`, `Debug`, `Information`, `Warning`, `Error`, `Critical`. + +Follow the log in real time: + +```bash +tail -f ~/.netclaw/logs/daemon.log +``` + +## Upgrading + +Schema migrations are forward-only with no automatic rollback, so back up before upgrading. + +Unlike the Docker deployment, bare-metal installs can self-update — the daemon periodically checks for new releases and applies them. After an update, the process exits and systemd's `Restart=always` brings it back on the new version. For manual upgrades: + +```bash +# 1. Stop the daemon +systemctl --user stop netclaw + +# 2. Back up the database +cp ~/.netclaw/netclaw.db ~/.netclaw/netclaw.db.bak.$(date +%s) + +# 3. Replace binaries (method depends on how you installed) +# For manual installs, download and extract the new release. +# For package manager installs, update the package. + +# 4. Start the daemon +systemctl --user start netclaw + +# 5. Verify +until curl -sf http://127.0.0.1:5199/api/health/ready; do sleep 2; done +echo "Daemon is ready" +``` + +To rollback: stop the daemon, restore the database backup, put the old binaries back, and start again. + +## Uninstalling + +```bash +netclaw daemon uninstall +``` + +Your data in `~/.netclaw` stays untouched. + +To remove manually: + +```bash +systemctl --user stop netclaw +systemctl --user disable netclaw +rm ~/.config/systemd/user/netclaw.service +systemctl --user daemon-reload +``` + +## Troubleshooting + +### Start here: `netclaw doctor` + +Before diving into specific symptoms, run [`netclaw doctor`](/cli/doctor/). It checks provider connectivity, config validity, daemon health, and common misconfigurations in one pass. + +![Netclaw doctor output showing health check diagnostics](/screenshots/output/doctor.png) + +### Daemon stops after SSH disconnect + +Lingering isn't enabled. Fix it with `loginctl enable-linger $USER` and verify with `ls /var/lib/systemd/linger/`. + +### "Failed to connect to bus" when running systemctl + +`XDG_RUNTIME_DIR` isn't set. Common when running `systemctl --user` from cron or a non-login shell. On headless servers, also make sure `dbus-user-session` is installed (`sudo apt install dbus-user-session`). Export the runtime dir manually: + +```bash +export XDG_RUNTIME_DIR=/run/user/$(id -u) +systemctl --user status netclaw +``` + +### Service starts but CLI can't connect + +Check that the daemon is actually listening: + +```bash +ss -tlnp | grep 5199 +``` + +If nothing shows, check the daemon log: + +```bash +tail -20 ~/.netclaw/logs/daemon.log +``` + +Common causes: port conflict (another process on 5199), JSON syntax error in `netclaw.json`, or missing provider configuration. + +### "Daemon already running" when starting + +The lock file at `~/.netclaw/netclaw.lock` is held by another process — either a daemon is already running or a previous instance didn't exit cleanly. Check for it: + +```bash +pgrep -a netclawd +``` + +If nothing shows, the lock file is stale. Remove it and restart: + +```bash +systemctl --user stop netclaw +rm ~/.netclaw/netclaw.lock +systemctl --user start netclaw +``` + +### Service fails on older systemd versions + +User-level services require systemd 236+. Check with `systemctl --version`. On older systems, skip systemd and run `netclaw daemon start` directly — it detaches as a background process. + +## Related pages + +- [Docker Deployment](/deployment/docker/) — containerized alternative +- [Exposure Modes](/deployment/exposure-modes/) — remote access via Tailscale or Cloudflare Tunnel +- [Models](/configuration/models/) — model slot configuration +- [OpenTelemetry](/observability/opentelemetry/) — metrics and log export +- [`netclaw doctor`](/cli/doctor/) — built-in health check diagnostics + +## Resources + +- [systemd user services documentation](https://www.freedesktop.org/software/systemd/man/latest/systemd.service.html) — full unit file reference +- [systemd user services on ArchWiki](https://wiki.archlinux.org/title/Systemd/User) — best practical guide for user-level systemd +- [loginctl enable-linger](https://www.freedesktop.org/software/systemd/man/latest/loginctl.html) — why lingering matters for headless services +- [.NET environment variable configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider) — the double-underscore nesting convention used by `NETCLAW_` env vars diff --git a/src/content/docs/getting-started/first-conversation.md b/src/content/docs/getting-started/first-conversation.md index fdb3653..2e6acfb 100644 --- a/src/content/docs/getting-started/first-conversation.md +++ b/src/content/docs/getting-started/first-conversation.md @@ -3,4 +3,93 @@ title: "Your First Conversation" description: "Start your first chat session with Netclaw." --- -Content coming soon. +You've installed Netclaw, run `netclaw init`, and the daemon is up. Open a chat. + +## Prerequisites + +- Netclaw [installed](/getting-started/installation/) with a running daemon +- At least one [provider configured](/cli/provider/) (done during `netclaw init`) + +## 1. Open a chat + +```bash +netclaw chat +``` + +![Netclaw Chat TUI on startup](/screenshots/output/chat-session-start.png) + +The TUI launches into a new session. The status bar shows your active model and "Generating..." because Netclaw is already composing its opening message. + +## 2. Meet your agent + +Netclaw introduces itself without any prompt from you. It reads your identity file (created during `netclaw init`) and greets you by name. + +![Netclaw greeting and profile check](/screenshots/output/chat-intro-greeting.png) + +The `file_read` tool call in the output means Netclaw pulled your identity from `~/.netclaw/identity/SOUL.md` to check what it already knows about you. Then it asks what you want to use it for. + +## 3. Teach it about you + +Tell Netclaw about your work. It asks follow-up questions to build your profile. + +![Multi-turn profile conversation](/screenshots/output/chat-profile-questions.png) + +In this example, the conversation went: + +1. "I need help with administrative tasks for NetclawCorp." +2. Netclaw asks about team size and role +3. "5 people" / "COO. We use Google Workspace, Slack, Xero, and Invoicely." + +Give it as much context as you want. Your role, team, tools, workflows. It remembers everything across sessions. + +## 4. Profile saved + +Netclaw writes your profile to `SOUL.md`, the identity file that persists across sessions. + +![Profile written to SOUL.md](/screenshots/output/chat-profile-update-soul.md.png) + +The `file_write` line confirms your profile landed at `~/.netclaw/identity/SOUL.md`. Every future session loads this file automatically, so you never re-introduce yourself. + +`SOUL.md` grows over time. As you work with Netclaw, it updates the file with new preferences and context it picks up from your conversations. + +## 5. Do something real + +Profile bootstrapping done. Try a tool-assisted task. + +### Ask a live question + +``` +what is the current temperature in Chicago? +``` + +![Web fetch tool in action](/screenshots/output/single-shot-chat-web-fetch.png) + +Netclaw calls `web_fetch`, pulls weather data, and returns a plain-language answer. Tool calls appear inline so you can see exactly what happened behind the scenes. + +### Headless mode + +Skip the TUI for quick one-offs. The `-p` flag sends a single prompt and exits: + +```bash +netclaw chat -p "can you tell me if there's an MCP server for Xero?" +``` + +![Headless mode with tool call output](/screenshots/output/single-shot-cli-execution.png) + +Tool calls and results stream to stdout, then the process exits. Good for scripting and quick terminal lookups. + +## Where to go from here + +At this point, Netclaw has your profile and a working tool stack. A few directions worth exploring: + +- Resume this session later: `Ctrl+Q` to close, [`netclaw sessions`](/cli/sessions/) to pick it back up +- Give Netclaw more tools via [`netclaw mcp`](/cli/mcp-tools/) — MCP servers for databases, APIs, file systems, whatever you need +- Wire it into Slack or Discord so your team can talk to the same agent ([Channels](/channels/slack/)) +- [`netclaw chat`](/cli/chat/) has the full reference: keyboard shortcuts, JSON output, named sessions, scripted multi-turn workflows + +## Resources + +- [`netclaw chat` reference](/cli/chat/) — flags, modes, keyboard shortcuts +- [Security Model](/security/security-model/) — how tool access and permissions work +- [MCP Tools](https://modelcontextprotocol.io/specification/2025-03-26/server/tools) — the protocol behind tool execution +- [SignalR documentation](https://learn.microsoft.com/en-us/aspnet/core/signalr/introduction) — real-time protocol between CLI and daemon diff --git a/src/content/docs/getting-started/installation.md b/src/content/docs/getting-started/installation.md index 849d15c..dace13c 100644 --- a/src/content/docs/getting-started/installation.md +++ b/src/content/docs/getting-started/installation.md @@ -3,4 +3,122 @@ title: "Installation" description: "Install Netclaw on your system." --- -Content coming soon. +Netclaw has two components: a **CLI** (`netclaw`) and a **daemon** (`netclawd`). The CLI is a thin client — all inference, tool execution, and session state live in the daemon. Install both on the same machine to get started; for remote setups, see [Pairing Remote Devices](/guides/pairing-remote-devices/). + +## Linux (recommended) + +```bash +curl -sSL https://releases.netclaw.dev/install.sh | bash +``` + +Installs both CLI and daemon to `~/.netclaw/bin` and adds it to your PATH. Supports x86_64 and ARM64. + +```bash +# CLI only +curl -sSL https://releases.netclaw.dev/install.sh | bash -s -- cli + +# Daemon only +curl -sSL https://releases.netclaw.dev/install.sh | bash -s -- daemon + +# Pin a version +NETCLAW_VERSION=0.1.0 curl -sSL https://releases.netclaw.dev/install.sh | bash + +# Custom install directory +INSTALL_DIR=/opt/netclaw curl -sSL https://releases.netclaw.dev/install.sh | bash +``` + +## Windows + +```powershell +iwr -useb https://releases.netclaw.dev/install.ps1 | iex +``` + +Installs to `%LOCALAPPDATA%\Programs\netclaw`. To install a specific component or version: + +```powershell +$script = Join-Path $env:TEMP "netclaw-install.ps1" +iwr -useb https://releases.netclaw.dev/install.ps1 -OutFile $script +& $script -Component cli # CLI only +& $script -Component daemon # Daemon only +& $script -Version 0.1.0 # Pinned version +``` + +## Docker + +Run the daemon as a container — useful for servers, homelab setups, or if you don't want to install anything on the host. + +```bash +docker run -d \ + --name netclaw \ + -v ~/.netclaw:/root/.netclaw \ + -p 127.0.0.1:5199:5199 \ + ghcr.io/netclaw-dev/netclaw +``` + +| Setting | Value | +|---------|-------| +| Image | `ghcr.io/netclaw-dev/netclaw` | +| Architectures | `linux/amd64`, `linux/arm64` | +| Port | 5199 | +| Volume | `/root/.netclaw` (config, identity, sessions, logs) | + +The container runs the daemon only. You still need the CLI installed on whatever machine you're talking to it from — install it with the Linux or Windows script above (`bash -s -- cli`), then [pair](/guides/pairing-remote-devices/) the CLI to the container's daemon. + +Pass provider credentials as environment variables: + +```bash +docker run -d \ + --name netclaw \ + -v ~/.netclaw:/root/.netclaw \ + -p 127.0.0.1:5199:5199 \ + -e NETCLAW_Providers__openrouter__Type=openrouter \ + -e NETCLAW_Providers__openrouter__ApiKey=sk-or-v1-... \ + -e NETCLAW_Models__Main__Provider=openrouter \ + -e NETCLAW_Models__Main__ModelId=anthropic/claude-sonnet-4 \ + ghcr.io/netclaw-dev/netclaw +``` + +For Docker Compose setups (including bundling Ollama), see [Docker Deployment](/deployment/docker/). + +## Build from source + +Requires [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0). + +```bash +git clone https://github.com/netclaw-dev/netclaw.git +cd netclaw +dotnet publish src/Netclaw.Cli/Netclaw.Cli.csproj -c Release -o ./out +dotnet publish src/Netclaw.Daemon/Netclaw.Daemon.csproj -c Release -o ./out +export PATH="$PWD/out:$PATH" +``` + +## Verify the install + +```bash +netclaw --version +``` + +You should see the version, commit hash, and build timestamp. + +## Next step + +```bash +netclaw init +``` + +The [`init` wizard](/cli/init/) walks you through provider setup, security posture, channels, identity, and network exposure — then starts the daemon. See the [Quickstart](/getting-started/quickstart/) for the full walkthrough. + +## Updating + +```bash +netclaw update # check for and install updates +netclaw update --check # check only, don't install +``` + +Self-update is disabled in the Docker image — update by pulling a new image tag instead. + +## Resources + +- [.NET 10 SDK](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) — required for building from source +- [Docker Engine](https://docs.docker.com/engine/install/) — container runtime for the Docker install method +- [Docker Deployment](/deployment/docker/) — Docker Compose, Ollama sidecar, and production container configuration diff --git a/src/content/docs/getting-started/quickstart.md b/src/content/docs/getting-started/quickstart.md index 48a3c0f..0d167f0 100644 --- a/src/content/docs/getting-started/quickstart.md +++ b/src/content/docs/getting-started/quickstart.md @@ -3,4 +3,99 @@ title: "Quickstart" description: "Get Netclaw running in minutes with the init wizard." --- -Content coming soon. +Install netclaw, run the setup wizard, talk to your agent. Takes about 5 minutes. + +## 1. Install + +```bash +curl -sSL https://releases.netclaw.dev/install.sh | bash +``` + +See [Installation](/getting-started/installation/) for Windows, Docker, and build-from-source options. + +## 2. Run the setup wizard + +```bash +netclaw init +``` + +The wizard walks you through everything. Here's what to expect. + +### Pick a provider + +![Provider endpoint configuration](/screenshots/output/init-step1-provider-endpoint.png) + +Choose an LLM provider and enter credentials. Self-hosted providers like Ollama need an endpoint URL. Once credentials pass a connectivity check, you pick a default model: + +![Model selection](/screenshots/output/init-step1-provider-model.png) + +Use [`netclaw provider`](/cli/provider/) to add more providers later. + +### Set your security posture + +![Security posture selection](/screenshots/output/init-step2-security-posture.png) + +Pick how much you trust the environment. **Personal** is single-user with full tool access. **Team** and **Public** are progressively more restrictive. See [Security Model](/security/security-model/) for details. + +### Connect channels (optional) + +![Channel selection](/screenshots/output/init-step3-channels.png) + +Wire up Slack, Discord, or both. Each channel needs a token — the wizard prompts for them and tests connectivity before moving on. + +![Slack bot token entry](/screenshots/output/init-step3-slack-bot-token.png) + +Tokens are masked and stored encrypted. See the [Slack](/channels/slack/) or [Discord](/channels/discord/) pages for full setup guides including app creation. + +### Set your identity + +![Your name](/screenshots/output/init-step7-your-name.png) + +Tell netclaw who you are. This sets the owner identity for the agent. + +![Communication style](/screenshots/output/init-step7-communication-style.png) + +Pick a personality style — this shapes how the agent talks to you. + +### Choose network exposure + +![Exposure mode selection](/screenshots/output/init-step9-exposure-mode.png) + +**Local** (default) means the daemon only listens on loopback. Tailscale and Cloudflare Tunnel options make it reachable from other machines. See [Exposure Modes](/deployment/exposure-modes/) and [Pairing Remote Devices](/guides/pairing-remote-devices/). + +### Health check + +![Health check results](/screenshots/output/init-step10-healthcheck.png) + +The wizard validates your config — provider connectivity, channel tokens, network setup. All green means the daemon starts automatically. + +## 3. Start chatting + +```bash +netclaw chat +``` + +![First chat session](/screenshots/output/chat-session-start.png) + +The agent introduces itself and kicks off a personality bootstrapping conversation — it asks about your work, your tools, and what you need help with. This builds your profile so future conversations have context. + +See [Your First Conversation](/getting-started/first-conversation/) for the full walkthrough of what happens next. + +### Headless mode + +Don't need the TUI? Send a one-shot prompt from your terminal: + +```bash +netclaw chat -p "what's the weather in Chicago?" +``` + +![Headless CLI execution](/screenshots/output/single-shot-cli-execution.png) + +Pipe output to other tools with `--json`. See [`netclaw chat`](/cli/chat/) for the full reference. + +## What's next + +- [Your First Conversation](/getting-started/first-conversation/) — personality bootstrapping and your first real interaction +- [`netclaw doctor`](/cli/doctor/) — diagnose issues if something didn't work +- [`netclaw status`](/cli/status/) — check daemon health and connector states +- [Slack](/channels/slack/) / [Discord](/channels/discord/) — full channel setup guides diff --git a/src/content/docs/guides/connecting-slack.md b/src/content/docs/guides/connecting-slack.md index e5c8b75..238c15a 100644 --- a/src/content/docs/guides/connecting-slack.md +++ b/src/content/docs/guides/connecting-slack.md @@ -1,6 +1,198 @@ --- title: "Connecting Slack" -description: "Step-by-step Slack bot setup and configuration." +description: "Get netclaw responding in your Slack workspace, from app creation to first message." --- -Content coming soon. +You've got netclaw installed but it's not talking to Slack yet. This page covers the whole setup -- creating a Slack app, wiring up tokens, and verifying the connection. + +The [Slack channel reference](/channels/slack/) has the full config field list, ACL details, and message behavior docs. This page just gets you connected. + +## Before You Begin + +- Netclaw installed and running ([`netclaw init`](/cli/init/) completed, or at least the daemon is up) +- Permission to install apps in your Slack workspace (some orgs restrict this to admins) +- A channel in mind where the bot should respond + +## 1. Create a Slack App + +Go to [api.slack.com/apps](https://api.slack.com/apps) and click **Create New App**. "From scratch" is fine. + +### Enable Socket Mode + +Under **Settings > Socket Mode**, flip the toggle on. Generate an **App-Level Token** when prompted -- give it the `connections:write` scope. Copy the `xapp-...` token somewhere safe. + +[Socket Mode](https://api.slack.com/apis/socket-mode) is an outbound WebSocket connection -- netclaw dials out, so you don't need a public URL or any ingress rules. + +### Add Bot Token Scopes + +Under **OAuth & Permissions > Scopes**, add these bot token scopes: + +| Scope | Why | +|-------|-----| +| `app_mentions:read` | Receive @-mention events | +| `channels:history` | Read channel messages for thread backfill | +| `channels:read` | Resolve channel names to IDs | +| `chat:write` | Post messages and replies | +| `files:read` | Access shared file content | +| `users:read` | User lookup | + +If you plan to use private channels, also add `groups:history` and `groups:read`. + +### Install to Workspace + +Click **Install to Workspace** under OAuth & Permissions. After authorizing, copy the **Bot User OAuth Token** (`xoxb-...`). + +### Invite the Bot + +In each Slack channel where netclaw should respond, run: + +``` +/invite @yourbot +``` + +The bot won't auto-join channels -- you have to invite it. + +## 2. Configure Netclaw + +The `netclaw init` wizard is fastest. Manual config gives you more control. + +### Fast Path: `netclaw init` + +If you haven't run `netclaw init` yet, or want to reconfigure, this handles everything. Step 4 of the wizard covers channel setup -- paste your tokens, pick Slack, done. + +![Channel selection during netclaw init](/screenshots/output/init-03-channels.png) + +Paste both tokens when prompted -- the wizard stores them encrypted and enables Slack. + +### Manual Path + +Store tokens with [`netclaw secrets`](/cli/secrets/): + +```bash +netclaw secrets set Slack.BotToken xoxb-your-bot-token +netclaw secrets set Slack.AppToken xapp-your-app-token +``` + +Then enable Slack in `~/.netclaw/config/netclaw.json` and set at least one allowed channel: + +```json +{ + "Slack": { + "Enabled": true, + "DefaultChannelName": "general" + } +} +``` + +Channel names go without the `#` prefix. + +Environment variables also work: + +```bash +export NETCLAW_Slack__BotToken="xoxb-..." +export NETCLAW_Slack__AppToken="xapp-..." +``` + +:::caution[Don't skip channel configuration] +Netclaw uses **default-deny access control**. If you don't set `DefaultChannelName`, `DefaultChannelId`, or `AllowedChannelIds`, the bot connects to Slack but silently ignores every message. This is the most common setup mistake. +::: + +## 3. Set Allowed Channels + +If you used the manual path above, you still need to configure which channels the bot monitors. Set at least one: + +| Field | What it does | +|-------|-------------| +| `DefaultChannelName` | Allow a single channel by name (resolved to an ID at startup) | +| `DefaultChannelId` | Same thing, but by ID | +| `AllowedChannelIds` | Allow multiple channels by ID | + +```json +{ + "Slack": { + "Enabled": true, + "DefaultChannelName": "eng-claw", + "AllowedChannelIds": ["C0123456789", "C9876543210"] + } +} +``` + +To find a channel's ID: right-click the channel name in Slack > "View channel details" > scroll to the bottom. [Slack's help article](https://slack.com/help/articles/221769328) has screenshots. + +The [Slack channel reference](/channels/slack/#access-control) covers user allow-lists, DM settings, and audience overrides -- worth reading before you go to production. + +## 4. Restart and Verify + +Restart the daemon to pick up the new config: + +```bash +netclaw daemon stop && netclaw daemon start +``` + +Check the connection: + +```bash +netclaw status +``` + +![System status showing channel health](/screenshots/output/status.png) + +Slack should show `connected`. If it doesn't, run the diagnostics: + +```bash +netclaw doctor +``` + +![Doctor output showing Slack Auth and Slack ACL checks](/screenshots/output/doctor.png) + +`netclaw doctor` validates your token against Slack's `auth.test` API and warns if no channel allow-list or default channel is configured. + +@-mention the bot in an allowed channel. If it responds, you're done. + +## Troubleshooting + +### Bot connects but never responds + +This is by far the most common problem. Tokens are configured, `netclaw status` shows `connected`, but the bot ignores every message. + +Almost always, it's the default-deny access control: `AllowedChannelIds` is empty and no `DefaultChannelName`/`DefaultChannelId` is set, so every message gets dropped. + +Set `DefaultChannelName` or add channel IDs to `AllowedChannelIds`, then restart the daemon. + +### `DefaultChannelName` is set but bot still ignores messages + +If the channel name can't be resolved at startup (typo, or the bot hasn't been `/invite`d to that channel), netclaw falls back to having no default channel -- which means the default-deny access control kicks in. Check the daemon logs for a channel resolution warning, fix the name, and restart. + +### Bot works in some channels but not others + +The channel is either missing from `AllowedChannelIds` or the bot hasn't been `/invite`d there. Add the channel ID and run `/invite @yourbot`. + +### Socket Mode keeps disconnecting + +The `xapp-...` App-Level Token has probably expired or been revoked. Generate a new one under Settings > Socket Mode in your [Slack app config](https://api.slack.com/apps), then update it: + +```bash +netclaw secrets set Slack.AppToken xapp-new-token +``` + +### `netclaw doctor` fails the Slack Auth check + +The `xoxb-...` Bot Token is invalid -- either it expired or the app was uninstalled from the workspace. Reinstall the app under OAuth & Permissions, copy the fresh Bot Token, and update it: + +```bash +netclaw secrets set Slack.BotToken xoxb-new-token +``` + +## Next Steps + +- [Slack channel reference](/channels/slack/) -- all config fields, ACL details, message behavior, proactive messaging +- [Security model](/security/security-model/) -- how audiences and approval gates work +- [`netclaw doctor`](/cli/doctor/) -- run this periodically to catch token expiry and ACL drift +- [Discord channel reference](/channels/discord/) -- setting up the other supported channel + +## External Resources + +- [Slack API: Socket Mode](https://api.slack.com/apis/socket-mode) -- how outbound WebSocket connections work +- [Slack: Getting Started with Socket Mode](https://api.slack.com/start/quickstart) -- quickstart walkthrough from Slack +- [Slack API: Bot Token Scopes](https://api.slack.com/scopes) -- full scope reference +- [Slack: Finding Channel IDs](https://slack.com/help/articles/221769328) -- where to find IDs for ACL config diff --git a/src/content/docs/guides/mcp-tool-permissions.md b/src/content/docs/guides/mcp-tool-permissions.md index 8479497..1c7145f 100644 --- a/src/content/docs/guides/mcp-tool-permissions.md +++ b/src/content/docs/guides/mcp-tool-permissions.md @@ -3,4 +3,262 @@ title: "MCP Tool Permissions" description: "Configure tool audience grants for Personal, Team, and Public." --- -Content coming soon. +You've added MCP servers to netclaw but the tools aren't showing up in Team or Public sessions. That's by design -- new servers start fully locked down. + +For the full TUI and CLI reference, see [`netclaw mcp`](/cli/mcp-tools/). For the config schema, see [MCP Servers](/configuration/mcp-servers/). + +## Before You Begin + +- At least one MCP server added (`netclaw mcp add`) and the daemon running -- run `netclaw mcp list` to confirm your servers are connected +- Familiarity with netclaw's three [audiences](/security/security-model/#trust-audiences): Personal (TUI, SignalR web client), Team (Slack), Public (unknown channels) + +## How Audience Defaults Work + +Each audience starts with different MCP access: + +| Audience | Servers allowed | Tools granted | +|----------|----------------|---------------| +| **Personal** | All | All | +| **Team** | None (allowlist, empty) | None | +| **Public** | None (allowlist, empty) | None | + +No audience has approval gates configured by default -- all granted tools run automatically. `netclaw init` recommends adding `shell_execute: Approval` for the Personal audience, but that's an opt-in step during setup. + +Personal gets everything out of the box. Team and Public get nothing -- you opt in server by server, tool by tool. + +## Grant Permissions with the TUI + +```bash +netclaw mcp permissions +``` + +Use the TUI for interactive setup when you want to explore what's available. + +### Pick a server + +![MCP Permissions server list showing three connected servers](/screenshots/output/mcp-tools-server-list.png) + +The server list shows connection status and tool count for each configured server. Select one to manage its grants. + +### Grant tools per audience + +![Personal audience with all tools granted and Auto approval](/screenshots/output/mcp-tools-personal.png) + +Personal audience -- all tools granted, Auto approval mode. Use `←`/`→` on the Audience row to switch between Personal, Team, and Public. + +![Team audience with server disabled and no tools granted](/screenshots/output/mcp-tools-team.png) + +Team audience -- server not enabled, nothing granted. Same defaults apply to Public. + +To open up a server for Team: + +1. Press `E` to enable the server for this audience +2. Press `A` to toggle all tools on, or `Space` on individual tools +3. Press `M` to cycle the server's default approval mode (Auto / Approval / Deny) +4. Press `P` on any row to set a per-tool approval override +5. Press `Enter` to save + +Changes write to `~/.netclaw/config/netclaw.json`. Run `netclaw daemon restart` to apply. + +## Grant Permissions via JSON + +For automation or version-controlled config, edit `netclaw.json` directly. + +### Enable a server for an audience + +Add the server name to `AllowedMcpServers`: + +```json +{ + "Tools": { + "AudienceProfiles": { + "Team": { + "McpServersMode": "Allowlist", + "AllowedMcpServers": ["memorizer", "notion"] + } + } + } +} +``` + +### Grant specific tools + +Use `McpServerToolGrants` to control which of a server's tools are visible. Omit a server from this map to pass all its tools through: + +```json +{ + "Tools": { + "AudienceProfiles": { + "Team": { + "McpServersMode": "Allowlist", + "AllowedMcpServers": ["notion"], + "McpServerToolGrants": { + "notion": ["notion-search", "notion-fetch", "notion-create-pages"] + } + } + } + } +} +``` + +Team users see only `notion-search`, `notion-fetch`, and `notion-create-pages`. Everything else on the Notion server is invisible to the model. + +## Grant Permissions via CLI + +Skip the TUI for quick one-off changes: + +```bash +# Grant specific tools for team +netclaw mcp tools notion --audience team \ + --grant "notion-search,notion-fetch,notion-create-pages" + +# Snapshot all currently discovered tools into grants +netclaw mcp tools memorizer --snapshot +``` + +## Set Approval Policies + +Approval policies control whether granted tools run automatically or need human confirmation: + +| Mode | Behavior | +|------|----------| +| `Auto` | Runs immediately, no prompt | +| `Approval` | Human must confirm before execution | +| `Deny` | Always blocked, no prompt offered | + +### Precedence + +Netclaw resolves the effective mode in order: + +1. **Exact tool override** -- `ToolOverrides["notion/notion-delete-page"]` +2. **Server default** -- `McpServerDefaults["notion"]` +3. **Audience default** -- `DefaultMode` + +First match wins. + +### Example: Auto for reads, Approval for writes + +```json +{ + "Tools": { + "AudienceProfiles": { + "Personal": { + "ApprovalPolicy": { + "DefaultMode": "Auto", + "McpServerDefaults": { + "notion": "Auto" + }, + "ToolOverrides": { + "notion/notion-delete-page": "Approval", + "notion/notion-update-page": "Approval", + "shell_execute": "Approval" + } + } + } + } + } +} +``` + +MCP tool override keys use the format `"{serverName}/{toolName}"` -- e.g., `"notion/notion-delete-page"`. + +### Server defaults for new tools + +Newly discovered tools on a server inherit its `McpServerDefaults` entry automatically: + +```json +"McpServerDefaults": { + "browser_playwright": "Approval" +} +``` + +Every tool on `browser_playwright` now requires approval unless you add an explicit `ToolOverrides` entry for it. + +## Verify It Works + +After changing permissions, restart the daemon and confirm: + +```bash +netclaw daemon restart +netclaw mcp tools notion --audience team +``` + +The output lists every tool the Team audience can see on the `notion` server, along with the effective approval mode for each. + +## Persistent Approvals + +When a user picks "Approve always" at an approval prompt, the decision persists to `~/.netclaw/config/tool-approvals.json`. These approvals survive daemon restarts. + +To revoke a persistent approval, edit the file directly: + +```json +{ + "audiences": { + "personal": { + "shell_execute": ["git push", "npm install"] + } + } +} +``` + +Remove entries you no longer want auto-approved. `netclaw doctor` warns about stale approvals for disabled audiences. + +If `tool-approvals.json` becomes corrupt, netclaw quarantines it to `tool-approvals.json.invalid` and starts with an empty store. Fail-closed -- no approvals carry over until you fix the file. + +## Headless Mode and Approval Gates + +Approval gates only work on interactive channels. Non-interactive sessions auto-deny all gated tools immediately -- there's no human to ask. + +| Channel | Supports approval? | +|---------|-------------------| +| TUI (`netclaw chat`) | Yes | +| Slack | Yes | +| SignalR (web client) | Yes | +| Headless (`netclaw chat -p`) | No -- auto-deny | +| Reminders | No -- auto-deny | +| Webhooks | No -- auto-deny | + +If your reminders, webhooks, or headless sessions need a tool, that tool must be set to `Auto` approval mode or it won't execute. + +Interactive channels that don't respond within 5 minutes also auto-deny. The model receives a generic tool-execution error with no indication that approval was the cause. + +## Troubleshooting + +### Tools blocked for Team/Public even after granting + +Check three things in order: + +1. Is the server in `AllowedMcpServers` for the audience (or `McpServersMode` set to `"All"`)? +2. Does `McpServerToolGrants` list the tool, or is the server omitted entirely (which passes all tools through)? +3. Is the tool set to `Deny` in `ToolOverrides`? A denied tool is blocked even if granted. + +Run `netclaw mcp tools --audience team` to see exactly what's granted. + +### `netclaw mcp list` shows `awaiting auth` + +The server needs authentication before you can grant tools. Run `netclaw mcp auth ` to complete the OAuth or token flow, then retry your grants. + +### Approval prompts never appear in automation + +Expected behavior. Headless, reminders, and webhooks auto-deny all approval-gated tools. Set those tools to `Auto` for the relevant audience, or accept that they won't run unattended. + +### "Approve always" not working after daemon restart + +Check that `~/.netclaw/config/tool-approvals.json` exists and is valid JSON. If it was quarantined (you'll see a `.invalid` file alongside it), the original was corrupt. Inspect the quarantined copy and recreate the approvals you need. + +### `netclaw doctor` warns about stale approvals + +You have persistent approvals for an audience or server that's been disabled. Clean up `tool-approvals.json` by removing entries for servers or audiences you no longer use. + +## Next Steps + +- [`netclaw mcp`](/cli/mcp-tools/) -- full TUI keybinding reference and CLI subcommands +- [MCP Servers config](/configuration/mcp-servers/) -- server schema, transports, OAuth setup +- [Security model](/security/security-model/) -- the four-layer invocation stack and audience system +- [Hardening](/security/hardening/) -- production lockdown recommendations including MCP tool policy + +## External Resources + +- [Model Context Protocol specification](https://spec.modelcontextprotocol.io/) -- the wire protocol behind MCP tool servers +- [MCP server registry](https://github.com/modelcontextprotocol/servers) -- community-maintained list of available MCP servers +- [Notion MCP server](https://github.com/makenotion/notion-mcp-server) -- official Notion MCP server for search, fetch, and page management diff --git a/src/content/docs/guides/pairing-remote-devices.md b/src/content/docs/guides/pairing-remote-devices.md index 510d435..1e5da70 100644 --- a/src/content/docs/guides/pairing-remote-devices.md +++ b/src/content/docs/guides/pairing-remote-devices.md @@ -3,4 +3,216 @@ title: "Pairing Remote Devices" description: "Pair remote CLI clients with your Netclaw daemon." --- -Content coming soon. +Your netclaw daemon runs on one machine but you want to use the CLI from another -- a laptop, a second server, a container you can't shell into. Pairing is a two-command handshake: generate a time-limited code on the daemon, exchange it from the remote device, and the client gets a bearer token for all future requests. + +## Before You Begin + +- Netclaw installed on both machines ([`netclaw init`](/cli/init/) completed on the daemon host). The remote device only needs the `netclaw` binary -- you don't need to run `netclaw init` on it. Pairing replaces init for client-only machines. +- The daemon's exposure mode set to something other than `local` -- remote pairing doesn't work over loopback. The default daemon port is **5199**; make sure your firewall allows it. +- Network connectivity between the two machines (same tailnet, tunnel, or direct) + +## 1. Set an Exposure Mode + +By default, the daemon only listens on loopback. You need to change this before any remote device can connect. + +During `netclaw init`, Step 9 handles this: + +![Network exposure mode selection during netclaw init](/screenshots/output/init-09-exposure.png) + +| Mode | Config value | Requires | Who can reach it | +|------|-------------|----------|-----------------| +| Local | `local` | Nothing | Loopback only (default) | +| Tailscale Serve | `tailscale-serve` | `tailscaled` running | Same [tailnet](https://tailscale.com/kb/1136/tailnet) | +| Tailscale Funnel | `tailscale-funnel` | `tailscaled` running | [Public internet](https://tailscale.com/kb/1223/funnel) | +| Cloudflare Tunnel | `cloudflare-tunnel` | `cloudflared` running | Internet via [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) | + +To change the mode after init, edit `~/.netclaw/config/netclaw.json`: + +```json +{ + "Daemon": { + "ExposureMode": "tailscale-serve" + } +} +``` + +For tunnel setup details -- Tailscale Serve commands, Cloudflare Tunnel configuration, and how each mode binds the daemon -- see [Exposure Modes](/deployment/exposure-modes/). + +Restart the daemon after changing exposure mode (`netclaw daemon stop && netclaw daemon start`, or `systemctl restart netclaw` if you're [using systemd](/deployment/systemd/)). + +:::caution[Non-local modes require at least one paired device] +If you set a non-local exposure mode, the daemon requires at least one paired device at startup (unless another remote auth scheme is configured). This is a chicken-and-egg situation: you need to pair your first device while still in `local` mode, then switch. The `netclaw init` wizard handles this automatically -- manual setup requires pairing first from the daemon host. +::: + +## 2. Generate a Pairing Code (Daemon Side) + +On the machine running the daemon: + +```bash +netclaw daemon pair +``` + +Output: + +``` +Pairing code: ABCD-EF23 +Expires at: 14:32:15 (local time) + +On the remote device, run: + netclaw pair http://my-server:5199 +``` + +Codes expire after **5 minutes** and are single-use. Generating a new code replaces any previous one -- only one active at a time. + +The character set (`23456789ABCDEFGHJKLMNPQRSTUVWXYZ`) deliberately excludes `0`/`O`/`1`/`I`/`L` to avoid misreads. + +### Docker + +If the daemon runs in a container, the pairing code is logged at `Information` level: + +```bash +docker logs | grep "Pairing code" +``` + +No need to exec into the container. + +## 3. Pair the Remote Device (Client Side) + +On the remote machine: + +```bash +netclaw pair http://my-server:5199 +``` + +For Tailscale and Cloudflare Tunnel modes, the endpoint URL differs -- check [Exposure Modes](/deployment/exposure-modes/) for the correct format for each mode. + +The CLI prompts for two things: + +``` +Pairing code (XXXX-XXXX): ABCD-EF23 +Device name [my-laptop]: +``` + +Device name defaults to the machine's hostname. Hit Enter to accept the default, or type a custom name. + +On success: + +- The CLI saves the bearer token to `~/.netclaw/config/secrets.json` +- The daemon endpoint is written to `~/.netclaw/client/config.json` +- `netclaw chat`, `netclaw status`, and all other remote commands work from here + +If a device with the same name already exists on the daemon, the exchange returns HTTP 409. Revoke the old device first (see below). + +There's no limit to how many devices you can pair -- add as many as you need. + +## 4. Verify the Connection + +From the newly paired device: + +```bash +netclaw status +``` + +If you get system status back, pairing worked. The CLI attaches the bearer token automatically from here on. + +## Managing Paired Devices + +Run these from the daemon host. + +### List devices + +```bash +netclaw daemon devices +``` + +Shows Name, Created, and Last Used for each paired device. Prints `No paired devices.` if none exist. + +### Revoke a device + +```bash +netclaw daemon devices revoke +``` + +After revocation, the device gets 401 on its next request. Use this when a device is lost, retired, or you need to re-pair with the same name. + +## How Tokens Work + +- Raw tokens never hit disk on the daemon side -- it stores a SHA256 hash with a per-device salt in `~/.netclaw/config/devices.json` (file permissions `600` on Linux) +- Clients keep the raw token in `~/.netclaw/config/secrets.json` +- Loopback connections skip bearer auth entirely -- if you're on the daemon host, you don't need to pair + +### Endpoint Resolution + +When the CLI connects, it checks these in order: + +1. `NETCLAW_DAEMON_ENDPOINT` environment variable +2. `~/.netclaw/client/config.json` (written by `netclaw pair`) +3. Default: `http://127.0.0.1:5199` + +Override with the env var when you need to switch between multiple daemons without re-pairing. + +## Exchange Endpoint Security + +The pairing endpoint has three layers of brute-force protection: + +| Layer | Behavior | +|-------|----------| +| Rate limiter | 5 attempts/minute per IP | +| Fail2ban-style guard | 10 failures in 15 min blocks the IP for 15 min | +| No-code-pending gate | Returns 404 when no code is active | + +With a 32-character alphabet, 8-character codes, 5-minute expiry, and 5 attempts/minute -- brute force isn't happening. + +## Troubleshooting + +### "Authentication failed: the daemon rejected the bearer token" + +The token was revoked or the daemon's device store was reset. Re-pair: + +```bash +netclaw pair +``` + +Run `netclaw daemon pair` on the daemon host to get a fresh code. + +### Pairing code expired + +Codes last 5 minutes. Generate a new one with `netclaw daemon pair` and try again. + +### HTTP 409 — device name already exists + +A device with that name is already paired. Either pick a different name during pairing, or revoke the existing one first: + +```bash +netclaw daemon devices revoke +``` + +### Can't connect to the daemon at all + +Check the basics: + +1. Is the daemon running? (`netclaw daemon start` on the host) +2. Is the exposure mode set to something other than `local`? +3. Can you reach the endpoint from the remote machine? (`curl http://my-server:5199/api/health/ready`) +4. Is a firewall blocking port 5199? + +Run [`netclaw doctor`](/cli/doctor/) on the daemon host -- it includes exposure-mode health checks. + +### IP blocked after too many failed attempts + +Wait 15 minutes, or fix the issue from a different IP. The fail2ban guard auto-expires after 15 minutes. + +## Next Steps + +- [`netclaw chat`](/cli/chat/) -- start talking to your agent from the paired device +- [`netclaw status`](/cli/status/) -- check daemon connectivity and endpoint URL +- [`netclaw doctor`](/cli/doctor/) -- diagnose exposure mode and connectivity issues +- [Exposure Modes](/deployment/exposure-modes/) -- all `Daemon.ExposureMode` options and tunnel setup +- [`netclaw init`](/cli/init/) -- the setup wizard handles bootstrap pairing during non-local setup + +## External Resources + +- [Tailscale Serve](https://tailscale.com/kb/1242/tailscale-serve) -- expose local services to your tailnet +- [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) -- expose local services to the public internet via Tailscale +- [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) -- route traffic to your daemon through Cloudflare's network +- [Tailscale: What is a tailnet?](https://tailscale.com/kb/1136/tailnet) -- networking concepts for the Tailscale Serve mode diff --git a/src/content/docs/guides/setting-up-ollama.md b/src/content/docs/guides/setting-up-ollama.md deleted file mode 100644 index 8cff1f2..0000000 --- a/src/content/docs/guides/setting-up-ollama.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -title: "Setting Up Ollama" -description: "Install and configure Ollama for local LLM inference." ---- - -Content coming soon. diff --git a/src/content/docs/observability/health-checks.md b/src/content/docs/observability/health-checks.md new file mode 100644 index 0000000..c837414 --- /dev/null +++ b/src/content/docs/observability/health-checks.md @@ -0,0 +1,205 @@ +--- +title: "Health Checks" +description: "HTTP health endpoints for monitoring and orchestration." +--- + +The daemon exposes two HTTP health endpoints for container orchestration, load balancers, and monitoring systems. + +## Endpoints + +| Endpoint | Auth | Purpose | +|----------|------|---------| +| `GET /api/health/ready` | None | Liveness/readiness probe | +| `GET /api/health/status` | Required | Full subsystem status | + +Both listen on the daemon's configured port (default `5199`). + +## Readiness probe + +```bash +curl -sf http://127.0.0.1:5199/api/health/ready +``` + +Returns `200 OK` with body `healthy` when the daemon is accepting requests. No authentication, no JSON — just a string. Use this for Docker HEALTHCHECK, Kubernetes liveness probes, and load balancer health checks. + +Any non-200 response (or connection refused) means the daemon isn't ready. + +### Docker HEALTHCHECK + +The official container image uses this: + +```dockerfile +HEALTHCHECK --interval=15s --timeout=5s --start-period=30s --retries=3 \ + CMD curl -sf http://127.0.0.1:5199/api/health/ready || exit 1 +``` + +The 30-second start period gives the daemon time to initialize providers and connect channels before the first probe fires. + +### Kubernetes + +```yaml +livenessProbe: + httpGet: + path: /api/health/ready + port: 5199 + initialDelaySeconds: 30 + periodSeconds: 15 + timeoutSeconds: 5 + failureThreshold: 3 +``` + +### Systemd + +For systemd-managed daemons, use a `ExecStartPost` check or a watchdog timer: + +```ini +[Service] +ExecStartPost=/bin/sh -c 'until curl -sf http://127.0.0.1:5199/api/health/ready; do sleep 2; done' +``` + +## Status endpoint + +```bash +curl -s http://127.0.0.1:5199/api/health/status \ + -H "Authorization: Bearer $(netclaw auth token)" +``` + +Returns a JSON object with the state of every subsystem. This is the same data that [`netclaw status`](/cli/status/) displays — the CLI just formats it as a table. + +Requires authentication (bearer token or loopback origin). Returns `401` without valid credentials. + +### Response structure + +```json +{ + "overall": "healthy", + "build": { + "version": "0.4.2", + "commitHash": "a1b2c3d", + "buildTimestamp": "2026-05-01T12:00:00Z" + }, + "process": { + "pid": 1234, + "startedAtUtc": "2026-05-05T08:00:00Z", + "uptimeSeconds": 3600 + }, + "connectors": [ + { + "key": "slack", + "displayName": "Slack", + "enabled": true, + "status": "healthy", + "message": null + }, + { + "key": "discord", + "displayName": "Discord", + "enabled": true, + "status": "disconnected", + "message": "Gateway timeout" + }, + { + "key": "mcp:github", + "displayName": "GitHub MCP", + "enabled": true, + "status": "healthy", + "message": null + } + ], + "model": { + "modelId": "anthropic/claude-sonnet-4", + "displayName": "Claude Sonnet 4", + "provider": "openrouter", + "inputModalities": ["text", "image"], + "outputModalities": ["text"], + "contextWindow": 200000 + }, + "persistence": { + "provider": "sqlite" + }, + "memory": { + "provider": "sqlite", + "status": "healthy", + "databasePath": "/root/.netclaw/memory/netclaw-memory.db", + "pendingCheckpoints": 0 + }, + "reminders": { + "scheduledCount": 3, + "activeExecutions": 0, + "failedCount": 0 + }, + "telemetry": { + "enabled": true, + "otlpEndpoint": "http://localhost:4317" + }, + "update": { + "state": "up-to-date", + "available": false, + "currentVersion": "0.4.2", + "latestVersion": "0.4.2" + } +} +``` + +### Overall status + +The `overall` field is computed from connector states: + +| Overall | Condition | +|---------|-----------| +| `healthy` | All enabled connectors are healthy | +| `degraded` | Any enabled connector is `disconnected`, `degraded`, `auth-required`, or `auth-failed` | + +The HTTP status code is always `200` — parse the `overall` field to determine health. This avoids false-positive container restarts when a single channel has a transient disconnect. + +### Connector statuses + +Each connector (Slack, Discord, MCP servers) reports one of: + +| Status | Meaning | +|--------|---------| +| `healthy` | Connected and operational | +| `degraded` | Partially functional (e.g., reconnecting) | +| `disconnected` | Connection lost | +| `auth-required` | Needs OAuth flow (MCP servers) | +| `auth-failed` | Credentials rejected | +| `disabled` | Turned off in config | + +### Memory status + +| Status | Meaning | +|--------|---------| +| `healthy` | Database accessible, checkpoint backlog ≤ 25 | +| `degraded` | Checkpoint backlog growing (> 25 pending) | +| `unavailable` | Database unreachable | + +## Monitoring integration + +### Prometheus / Grafana + +Poll `/api/health/status` on an interval and extract metrics: + +```bash +# Simple availability check (no auth needed) +curl -sf http://127.0.0.1:5199/api/health/ready && echo 1 || echo 0 +``` + +For richer metrics, use the [OpenTelemetry](/observability/opentelemetry/) integration which exports to OTLP directly. + +### Operational alerts + +When a connector transitions to `disconnected` or `auth-failed`, the daemon fires an [operational alert](/observability/operational-alerts/) to configured webhook targets. You don't need to poll the status endpoint to detect failures — alerts push to you. + +## Related pages + +- [`netclaw status`](/cli/status/) — CLI that formats this endpoint's response as a table +- [`netclaw doctor`](/cli/doctor/) — offline diagnostics for configuration issues +- [Operational Alerts](/observability/operational-alerts/) — push notifications on health state changes +- [OpenTelemetry](/observability/opentelemetry/) — metrics and traces export +- [Docker Deployment](/deployment/docker/) — container health check configuration + +## Resources + +- [ASP.NET Health Checks](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks) — the framework behind the endpoints +- [Kubernetes Probes](https://kubernetes.io/docs/tasks/configure-pod-container/configure-liveness-readiness-startup-probes/) — configuring liveness and readiness probes +- [Docker HEALTHCHECK](https://docs.docker.com/reference/dockerfile/#healthcheck) — container health check instruction reference diff --git a/src/content/docs/observability/opentelemetry.md b/src/content/docs/observability/opentelemetry.md new file mode 100644 index 0000000..21edeea --- /dev/null +++ b/src/content/docs/observability/opentelemetry.md @@ -0,0 +1,138 @@ +--- +title: OpenTelemetry +description: Export metrics and logs from netclaw via OTLP for monitoring token usage, session activity, and system health. +--- + +Netclaw can push metrics and structured logs to any [OpenTelemetry](https://opentelemetry.io/) Protocol (OTLP) compatible collector. Flip it on, point it at your collector, and your existing backend (Grafana, Datadog, Honeycomb, whatever speaks OTLP) gets per-channel message flow metrics, token consumption counters, and full daemon logs. + +## Configuration + +Merge a `Telemetry` block into your existing `~/.netclaw/config/netclaw.json`: + +```json +{ + "Telemetry": { + "Enabled": true, + "Otlp": { + "Endpoint": "http://127.0.0.1:4317" + } + } +} +``` + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Enabled` | bool | `false` | Turns on the OTLP export pipeline | +| `Otlp:Endpoint` | string | `http://127.0.0.1:4317` | OTLP collector endpoint (gRPC) | + +Netclaw uses gRPC OTLP on port 4317, not HTTP/Protobuf (4318). If your collector only accepts HTTP OTLP, you'll get silent failures. + +Environment variable overrides follow the [.NET double-underscore convention](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider): + +```bash +export NETCLAW_Telemetry__Enabled="true" +export NETCLAW_Telemetry__Otlp__Endpoint="http://127.0.0.1:4317" +``` + +Telemetry config changes need a daemon restart. Run [`netclaw doctor`](/cli/doctor/) first to catch config errors before you bounce the daemon: + +```bash +netclaw doctor +netclaw daemon stop && netclaw daemon start +``` + +### Validation + +If `Otlp:Endpoint` isn't a valid absolute URI, the daemon refuses to start, even with telemetry disabled: + +``` +Telemetry:Otlp:Endpoint must be an absolute URI. +``` + +[`netclaw doctor`](/cli/doctor/) catches this before you hit it at startup. It validates the endpoint format and warns when telemetry is on but no explicit endpoint is set. + +## What gets exported + +The OTel resource service name is `netclawd` (hardcoded, not configurable). + +Every log line the daemon produces goes to your collector, with full formatting and scope data (`IncludeFormattedMessage`, `IncludeScopes`, `ParseStateValues` all enabled). Two meters cover metrics: one for session-level token usage, one for per-channel message flow. Full reference below. + +Distributed tracing is off for now. The cross-actor model produces disconnected spans with no meaningful causality chain, so it's more noise than signal. + +## Metrics reference + +### Session metrics (`Netclaw.Sessions`) + +Token consumption and turn tracking across all sessions. + +| Metric | Type | Description | +|--------|------|-------------| +| `netclaw.session.tokens.input` | Counter | Input tokens consumed | +| `netclaw.session.tokens.output` | Counter | Output tokens consumed | +| `netclaw.session.turns.completed` | Counter | Conversation turns completed | + +These are aggregate totals across all models and providers, with no per-model or per-provider attribute breakdowns. + +### Channel metrics (`Netclaw.Channels`) + +Per-channel message pipeline metrics. Each metric name is prefixed with `netclaw.channel.{channel-type}` where channel type is one of: `slack`, `tui`, `headless`, `signalr`, `reminder`, `webhook`, `discord`. + +| Metric suffix | Type | Attributes | Description | +|---------------|------|------------|-------------| +| `.events.received` | Counter | `kind` | Inbound events received | +| `.events.dropped` | Counter | `reason` | Events dropped before processing | +| `.events.filtered` | Counter | `reason` | Events filtered by policy | +| `.events.routed` | Counter | `kind` | Events that reached conversation routing | +| `.messages.enqueued` | Counter | — | Messages accepted into session queue | +| `.replies.posted` | Counter | — | Successful replies sent | +| `.replies.rejected` | Counter | `error_code` | Rejected reply attempts | +| `.replies.failed` | Counter | — | Failed reply attempts | +| `.reply.duration.ms` | Histogram | — | Reply post latency in milliseconds (includes both successful and failed attempts) | + +> `Netclaw.Webhooks` (the meter behind [`netclaw stats`](/cli/stats/)) is not wired into OTLP. The `netclaw.channel.webhook.*` metrics above cover message flow through the webhook channel; per-route delivery stats only show up in `netclaw stats`. + +## Diagnostic queries + +| Symptom | What to check | +|---------|---------------| +| No replies | `events.received` > 0 but `replies.posted` = 0. Events arrive, nothing comes back. | +| Looping agent | `turns.completed` climbing without corresponding `replies.posted` | +| Policy dropping messages | `events.dropped` with `reason` attribute showing the cause | + +## Collector setup + +Point the endpoint at any [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) that accepts gRPC OTLP. A minimal local setup with Docker: + +```bash +docker run -d --name otel-collector \ + -p 4317:4317 \ + otel/opentelemetry-collector-contrib:latest +``` + +Route to your backend (Prometheus, Grafana Cloud, Datadog, etc.) via the collector's [exporter configuration](https://opentelemetry.io/docs/collector/configuration/#exporters). + +For a quick local stack, [Grafana OTel-LGTM](https://github.com/grafana/docker-otel-lgtm) bundles the collector, Prometheus, Loki, and Grafana in one container. Good for kicking the tires before committing to a production backend. + +## Verifying the pipeline + +After restarting the daemon with telemetry enabled: + +1. [`netclaw status`](/cli/status/) should show the telemetry row as `enabled` with your endpoint +2. [`netclaw doctor`](/cli/doctor/) validates the endpoint URI format +3. Send a test message through any channel and look for `netclaw.channel.*` metrics in your collector + +Once data is flowing, build dashboards around the [diagnostic queries](#diagnostic-queries) above. [Operational Alerts](/observability/operational-alerts/) covers netclaw's built-in webhook notifications, which work alongside OTel-based alerting. + +## Related pages + +- [Operational Alerts](/observability/operational-alerts/) for outbound webhook notifications +- [`netclaw status`](/cli/status/) shows the telemetry row and OTLP endpoint +- [`netclaw doctor`](/cli/doctor/) validates OTLP endpoint configuration +- [`netclaw stats`](/cli/stats/) has in-process counters, including webhook metrics that aren't in OTLP + +## Resources + +- [OpenTelemetry Collector documentation](https://opentelemetry.io/docs/collector/) covers setup, configuration, and deployment +- [OTLP specification](https://opentelemetry.io/docs/specs/otlp/) describes the wire protocol netclaw uses +- [Grafana OTel-LGTM Docker image](https://github.com/grafana/docker-otel-lgtm) is an all-in-one local observability stack +- [.NET environment variable configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider) explains the double-underscore nesting convention diff --git a/src/content/docs/observability/operational-alerts.md b/src/content/docs/observability/operational-alerts.md new file mode 100644 index 0000000..97ed147 --- /dev/null +++ b/src/content/docs/observability/operational-alerts.md @@ -0,0 +1,154 @@ +--- +title: Operational Alerts +description: Configure outbound webhook notifications for daemon lifecycle events, provider failures, channel disconnects, and more. +--- + +Netclaw emits operational alerts when something happens that you or your ops tooling should know about — a provider going down, a channel disconnecting, the daemon crashing. Netclaw delivers these as outbound webhook POSTs to URLs you configure, with native Slack Block Kit formatting or a generic JSON envelope. + +## Quick Start + +Add a `Notifications` block to `~/.netclaw/config/netclaw.json` and restart the daemon: + +```json +{ + "Notifications": { + "Webhooks": [ + { + "Url": "https://hooks.slack.com/services/T00/B00/xxx", + "Name": "ops-slack", + "Format": "Slack" + } + ] + } +} +``` + +You'll get a `daemon.started` alert on the next restart, which doubles as confirmation that delivery works. + +## Alert Types + +| Type String | Severity | What Happened | +|-------------|----------|---------------| +| `daemon.started` | Info | Daemon finished startup. Includes PID. | +| `daemon.stopping` | Info | Graceful shutdown initiated. Includes reason. | +| `daemon.crashing` | Critical | Unhandled exception — the process is going down. | +| `update.available` | Info | A newer netclaw binary exists in the release feed. | +| `provider.failover` | Warning | Primary LLM provider failed; traffic moved to fallback. | +| `provider.unreachable` | Critical | All configured LLM providers are unavailable. | +| `channel.disconnected` | Warning | Slack or Discord connection lost. | +| `mcp.auth.expired` | Warning | MCP OAuth token expired and refresh was rejected. | +| `mcp.server.disconnected` | Warning | Connection to an MCP server dropped. | +| `webhook.received` | Info | A valid inbound webhook delivery was accepted and queued. | +| `webhook.route.invalid` | Warning | A webhook route file is missing or invalid. | +| `reminder.execution.failed` | Warning | A scheduled reminder failed to execute. | +| `reminder.auto.disabled` | Critical | Reminder disabled after repeated consecutive failures. | +| `reminder.schema.dropped` | Warning | Invalid reminder definitions were dropped at startup. | + +`provider.auth.expired` is defined but not currently emitted. + +All configured destinations receive all alert types — there's no per-destination filtering. + +## Configuring Alert Destinations + +Add outbound webhook targets in `~/.netclaw/config/netclaw.json`. Merge the `Notifications` block into your existing config if one is already there. + +```json +{ + "Notifications": { + "Webhooks": [ + { + "Url": "https://hooks.slack.com/services/T00/B00/xxx", + "Name": "ops-slack", + "Format": "Slack" + }, + { + "Url": "https://your-monitoring.example.com/alerts", + "Name": "pagerduty-relay", + "Format": "Generic", + "Headers": { + "Authorization": "Bearer your-token" + } + } + ], + "DeduplicationWindowSeconds": 300, + "MaxRetries": 2, + "TimeoutSeconds": 10 + } +} +``` + +Restart the daemon after editing `netclaw.json` for changes to take effect. + +Each webhook target has: + +| Field | Required | Description | +|-------|----------|-------------| +| `Url` | Yes | Endpoint to POST alerts to | +| `Name` | Yes | Human-readable label for logs | +| `Format` | No | `Slack` or `Generic` (default). URLs containing `hooks.slack.com` auto-detect as Slack. | +| `Headers` | No | Custom HTTP headers (auth tokens, API keys) | + +Top-level notification settings: + +| Field | Default | Description | +|-------|---------|-------------| +| `DeduplicationWindowSeconds` | 300 | Suppress duplicate alerts within this window ([details below](#delivery-behavior)) | +| `MaxRetries` | 2 | Retry attempts for failed deliveries | +| `TimeoutSeconds` | 10 | HTTP timeout per delivery attempt | + +## Payload Formats + +### Generic JSON + +Every alert arrives as a JSON POST with this envelope: + +```json +{ + "alertId": "550e8400-e29b-41d4-a716-446655440000", + "type": "provider.unreachable", + "severity": "critical", + "summary": "All LLM providers are unreachable", + "timestamp": "2026-05-02T14:30:00Z", + "source": "netclaw", + "hostname": "claw-prod-01", + "context": { + "lastProvider": "anthropic", + "errorCount": "5" + } +} +``` + +The `context` object varies by alert type — it carries whatever extra detail is relevant to that event. + +### Slack Block Kit + +When `Format` is `Slack`, netclaw sends [Block Kit](https://api.slack.com/block-kit) messages with severity-colored headers and structured fields: + +- 🔴 Critical alerts +- ⚠️ Warnings +- ℹ️ Info events + +Each message includes a `text` fallback for notification previews. The `blocks` payload has the header, summary, metadata (severity, type, timestamp, hostname), and alert-specific context. + +## Delivery Behavior + +**Deduplication** — Identical alerts (same type and source) within the deduplication window are suppressed. This prevents notification storms when a provider flaps or a connection drops and reconnects rapidly. + +**Retries with backoff** — Failed deliveries retry with exponential backoff and jitter. With the default `MaxRetries` of 2: + +| Attempt | Base Delay | Range (with ±25% jitter) | +|---------|-----------|---------------------| +| 1 | 1s | 0.75s – 1.25s | +| 2 | 2s | 1.5s – 2.5s | + +Backoff caps at 30 seconds for higher retry counts. Netclaw doesn't retry client errors (4xx) — only server errors (5xx) and timeouts trigger retries. + +**Bounded queue** — Netclaw buffers alerts in a 256-slot in-memory queue. If the queue fills (all webhook targets are slow or down), new alerts drop rather than applying backpressure to the daemon. + +## Further Reading + +- [Slack Incoming Webhooks setup guide](https://api.slack.com/messaging/webhooks) — Create the webhook URL you'll paste into the `Url` field +- [Slack Block Kit documentation](https://api.slack.com/block-kit) — Reference for the structured message format netclaw uses for Slack alerts +- [PagerDuty Events API v2](https://developer.pagerduty.com/docs/events-api-v2/overview/) — Set up a PagerDuty service to receive Generic-format alerts +- [OpenTelemetry](/observability/opentelemetry/) — Export metrics via OpenTelemetry Protocol (OTLP) for dashboards and long-term monitoring +- [netclaw status](/cli/status/) — Check current system health interactively diff --git a/src/content/docs/security/hardening.md b/src/content/docs/security/hardening.md index bb31946..0aa4d79 100644 --- a/src/content/docs/security/hardening.md +++ b/src/content/docs/security/hardening.md @@ -3,4 +3,325 @@ title: "Hardening" description: "Lock down MCP permissions and network exposure." --- -Content coming soon. +The [security model](/security/security-model/) covers what netclaw enforces by default: default-deny, [audience](/security/security-model/#trust-audiences) scoping, a four-layer invocation stack. This page covers what *you* should do on top of that, especially if you're running it for a team, exposing it to the internet, or leaving it unattended. + +Sections are independent. Work through the ones that apply to your deployment. + +All configuration lives in `~/.netclaw/config/netclaw.json` unless noted otherwise. Values merge with built-in defaults, so you only need to specify what you're changing. + +## File Permissions + +Netclaw stores credentials and webhook secrets in plaintext JSON files. Lock them down: + +```bash +# Encrypted secrets — only the daemon user should read this +chmod 600 ~/.netclaw/config/secrets.json + +# Webhook route files contain HMAC secrets +chmod 700 ~/.netclaw/config/webhooks/ + +# Encryption keys for secrets at rest +chmod 700 ~/.netclaw/keys/ +``` + +[`netclaw doctor`](/cli/doctor/) checks `secrets.json` permissions and flags unencrypted values. Run it after any manual config edits. + +## Shell Access + +Shell access is the biggest risk surface you can control. Three modes in `~/.netclaw/config/netclaw.json`: + +| Mode | Behavior | +|------|----------| +| `Off` | Shell completely disabled. No `shell_execute` tool available. | +| `SandboxOnly` | Shell runs in a sandboxed environment. | +| `HostAllowed` | Shell runs directly on the host. Approval gates are your only guardrail. | + + + +Set the mode explicitly: + +```json +{ + "Security": { + "ShellExecutionMode": "Off" + } +} +``` + +**For Team and Public [postures](/security/security-model/#deployment-postures), shell is off by default.** Only Personal posture enables it, and only with approval gates on `shell_execute`. If you're running Personal posture but don't need shell, turn it off. + +### Custom Hard-Deny Patterns + +The built-in hard-deny list blocks `sudo`, `rm -rf ~/`, `kill`, fork bombs, and commands that would stop the daemon. Add your own patterns for anything dangerous in your environment: + +```json +{ + "Tools": { + "HardDenyPatterns": [ + "docker rm", + "kubectl delete namespace", + "terraform destroy" + ] + } +} +``` + +Custom patterns augment the defaults, they don't replace them. Netclaw tokenizes compound commands (`&&`, `||`, `;`, `|`) and checks each segment independently, so `echo hello && docker rm foo` still triggers the deny. + +## MCP Tool Permissions + +New MCP servers start with zero tool grants for all audiences, safe by default. The risk comes from granting too much. + +Audit your grants per audience: + +```bash +netclaw mcp permissions +``` + +![MCP tool grants for Personal audience showing all tools enabled](/screenshots/output/mcp-tools-personal.png) + +Personal audience with all tools granted. Compare against the locked-down Team and Public defaults: + +![Team audience with server disabled](/screenshots/output/mcp-tools-team.png) + +Team audience: server disabled, no tools granted. + +Grant the minimum tools each audience actually needs. Don't enable everything for Team because it's faster to configure. For destructive MCP tools (`delete`, `drop`, `write`), set approval mode to `Approval` or `Deny`. Per-tool overrides let you keep the server default on `Auto` while gating the dangerous ones: + +```json +{ + "Tools": { + "AudienceProfiles": { + "Personal": { + "ApprovalPolicy": { + "DefaultMode": "Auto", + "ToolOverrides": { + "shell_execute": "Approval", + "notion/notion-delete-page": "Approval" + } + } + } + } + } +} +``` + +Tool override keys use the format `{serverName}/{toolName}`. + +Review `~/.netclaw/config/tool-approvals.json` periodically and prune stale "approve always" decisions. + +See [`netclaw mcp`](/cli/mcp-tools/) for full details on the permissions TUI and CLI. + +## Network Exposure + +The daemon binds to `127.0.0.1:5199` by default. Keep it that way unless you have a reason not to. + +| Mode | Scope | Risk | +|------|-------|------| +| `local` | Loopback only | Minimal — only local processes can connect | +| `tailscale-serve` | Your tailnet | Low — [Tailscale identity](https://tailscale.com/kb/1312/serve) gates access | +| `tailscale-funnel` | Public internet | High — anyone on the internet can reach it via [Tailscale Funnel](https://tailscale.com/kb/1223/funnel) | +| `cloudflare-tunnel` | Public internet | High — requires a [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/) policy | + +![Exposure mode selection during netclaw init](/screenshots/output/init-09-exposure.png) + +The exposure selection during `netclaw init`. Internet-facing modes force explicit confirmation. + +If you need remote access, prefer `tailscale-serve`. It limits access to your tailnet. Funnel exposes you to the public internet — a completely different threat model. + +For Docker deployments, bind to loopback explicitly: + +```bash +docker run -p 127.0.0.1:5199:5199 ... +``` + +Omitting `127.0.0.1` binds to all interfaces. Any machine on your network can reach the daemon. + +Changing exposure mode requires a daemon restart. It's excluded from hot-reload on purpose. + +```bash +netclaw daemon stop && netclaw daemon start +``` + + + +## Slack Channel Restrictions + +Out of the box, Slack uses `MentionOnly: true` (netclaw only responds when @mentioned) and `AllowDirectMessages: false`. Lock down channel and user access on top of that. + +### Channel Allowlist + +With no channel allowlist and no default channel set, netclaw denies all channel traffic. Explicitly list the channels it should respond in: + +```json +{ + "Slack": { + "AllowedChannelIds": ["C0123ABCDEF", "C0456GHIJKL"], + "MentionOnly": true + } +} +``` + +To find a Slack channel ID: right-click the channel name in Slack, select "View channel details," and look at the bottom of the panel. Or see [Slack's guide to finding IDs](https://slack.com/help/articles/221769328-Locate-your-Slack-URL-or-ID). + +### User Allowlist + +Restrict which users can invoke netclaw: + +```json +{ + "Slack": { + "AllowedUserIds": ["U0123ABCDEF", "U0456GHIJKL"] + } +} +``` + +User IDs follow the same pattern — click a user's profile in Slack and find the ID under "More." + +### DMs and Per-Channel Audiences + +Keep DMs off unless you need them. If you enable DMs, restrict which users can DM: + +```json +{ + "Slack": { + "AllowDirectMessages": true, + "AllowedUserIds": ["U0123ABCDEF"] + } +} +``` + +You can also override the [audience](/security/security-model/#trust-audiences) per channel. Useful if you want a specific Slack channel to get Personal-level tool access while everything else stays at Team: + +```json +{ + "Slack": { + "ChannelAudiences": { + "C0123ABCDEF": "personal", + "dm": "team" + } + } +} +``` + +The `"dm"` key is reserved. It maps all direct messages to the specified audience. + +Invalid audience values in `ChannelAudiences` result in a deny (fail-closed). + +[`netclaw doctor`](/cli/doctor/) warns if Slack is enabled with no channel allowlist and no default channel configured, or if DMs are enabled without an `AllowedUserIds` list. + +## Approval Gates + +The last layer before a tool actually runs. Configure per audience: + +```json +{ + "Tools": { + "AudienceProfiles": { + "Personal": { + "ApprovalPolicy": { + "DefaultMode": "Auto", + "ToolOverrides": { + "shell_execute": "Approval" + } + } + }, + "Team": { + "ApprovalPolicy": { + "DefaultMode": "Approval" + } + }, + "Public": { + "ApprovalPolicy": { + "DefaultMode": "Deny" + } + } + } + } +} +``` + +- Headless sessions (reminders, webhooks, `netclaw chat -p "prompt"`) auto-deny all gated tools. There's no human to ask. +- No response within 5 minutes means deny. +- "Approve always" persists to `~/.netclaw/config/tool-approvals.json`. Revoke by editing the file directly. + +If nobody is watching the approval prompts in production, set `DefaultMode: "Deny"` for Team and Public. Auto-deny is safer than a 5-minute timeout nobody sees. + +## Webhook Security + +Each webhook route has its own HMAC secret, audience, body size limit, and rate limit: + +```json +{ + "Verification": { + "Kind": "Hmac", + "HmacAlgorithm": "Sha256", + "Secret": "whsec_...", + "SignatureHeaderName": "X-Hub-Signature-256", + "SignaturePrefix": "sha256=" + }, + "Audience": "Public", + "MaxBodyBytes": 1048576, + "RateLimitPerMinute": 10 +} +``` + +Set the audience to the minimum the webhook actually needs. Most should run as `Public` (fewest tools, session-scoped filesystem, wiped on end). Only use `Team` or `Personal` if the webhook prompt genuinely requires those tools. + +`RateLimitPerMinute` applies per webhook route, not globally across all routes. Lower it from the default 30 if the source won't fire that often — GitHub sends roughly one webhook per event, so 10/min is plenty. Keep `MaxBodyBytes` at 1 MB or lower unless you know the payloads are larger. + +Route files contain secrets in plaintext. Keep `~/.netclaw/config/webhooks/` at mode `700` (see [File Permissions](#file-permissions)). + +See [`netclaw webhooks`](/cli/webhooks/) for route setup and HMAC verification details. + +## Doctor Checks + +After making changes, validate everything: + +```bash +netclaw doctor +``` + +![netclaw doctor running 16 diagnostic checks](/screenshots/output/doctor.png) + +Security-relevant checks include: + +| Check | What it catches | +|-------|----------------| +| **Security Policy** | Missing `DeploymentPosture` | +| **Secrets JSON** | Wrong file permissions, unencrypted values | +| **Slack ACL** | No channel allowlist, DMs enabled without user allowlist | +| **Tool Audience Profiles** | Overly permissive tool grants | +| **exposure-mode** | Internet-reachable exposure without valid auth policy | +| **Inbound Webhook Routes** | Schema errors in route files | + +Wire it into your deployment pipeline: + +```bash +netclaw doctor --format json | jq -e '.exitCode == 0' +``` + +## Limitations + +- Prompt injection detection is regex-based. It catches known patterns (role resets, exfiltration attempts, invisible Unicode) but novel phrasings, Base64 encoding, synonym substitution, and non-English attacks can evade it. Treat it as a tripwire, not a firewall. +- Tool grants are per-audience, not per-channel. `ChannelAudiences` overrides which audience a channel maps to, but you can't give one Slack channel different tools than another channel with the same audience. +- Secret redaction catches `sk-*`, Slack tokens (`xox[baprs]-*`), `ghp_*`, AWS access keys (`AKIA*`), JWTs, PEM blocks, and common JSON key names. Custom secret formats won't be redacted. Use hard-deny path rules to block file access instead. +- Approval gates only work on interactive channels. Headless mode, reminders, and webhooks auto-deny all gated tools. Design automation workflows with that in mind. + + +## Related Pages + +- [Security Model](/security/security-model/) -- default-deny architecture, audiences, trust layers +- [`netclaw init`](/cli/init/) -- posture and exposure selection wizard +- [`netclaw mcp`](/cli/mcp-tools/) -- MCP tool permissions TUI +- [`netclaw doctor`](/cli/doctor/) -- configuration diagnostics +- [`netclaw webhooks`](/cli/webhooks/) -- webhook route management and HMAC verification +- [`netclaw secrets`](/cli/secrets/) -- encrypted credential storage + +## Further Reading + +- [OWASP LLM Top 10](https://genai.owasp.org/llm-top-10/) -- prompt injection, insecure output handling, and other LLM attack vectors +- [Tailscale ACLs](https://tailscale.com/kb/1018/acls/) -- network-level access control for `tailscale-serve` deployments +- [Cloudflare Access policies](https://developers.cloudflare.com/cloudflare-one/policies/access/) -- IdP-based access control for `cloudflare-tunnel` deployments +- [GitHub webhook security](https://docs.github.com/en/webhooks/using-webhooks/best-practices-for-using-webhooks) -- best practices for HMAC verification and secret rotation +- [Slack: Locate your URL or ID](https://slack.com/help/articles/221769328-Locate-your-Slack-URL-or-ID) -- finding channel and user IDs for allowlist configuration diff --git a/src/content/docs/security/secrets.md b/src/content/docs/security/secrets.md index e90f185..5c54356 100644 --- a/src/content/docs/security/secrets.md +++ b/src/content/docs/security/secrets.md @@ -3,4 +3,154 @@ title: "Secrets Management" description: "Encrypted credential storage with netclaw secrets." --- -Content coming soon. +Netclaw encrypts credentials at rest using [ASP.NET Data Protection](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction). No plaintext secret ever touches disk. Key material lives in `~/.netclaw/keys/`, separate from `secrets.json` — copying the secrets file alone is insufficient to decrypt its values. + +:::note +The goal isn't bulletproof encryption — anyone with shell access to the machine can decrypt these values. The goal is preventing the *agent* from accidentally reading raw credentials during tool use. Encryption at rest means `file_read` on `secrets.json` returns ciphertext, not your API keys. +::: + +Three layers protect credentials independently: encryption at rest keeps `secrets.json` opaque, ACL denial blocks the agent from reading specific secret files, and output redaction scrubs known secret patterns before the LLM sees them. If one layer fails, the other two still hold. + +If you're adding credentials for the first time, start with [`netclaw init`](/cli/init/). To add or rotate keys on an existing install, see [`netclaw secrets`](/cli/secrets/). + +## Encryption Architecture + +Netclaw wraps the [ASP.NET Data Protection API](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-management) with an application name of `Netclaw` and a purpose string that scopes encryption to `Netclaw.Secrets.v1` so keys can't be cross-used between subsystems. `DataProtectionSecretsProtector` calls `IDataProtector.Protect()` and `Unprotect()` — standard .NET cryptography, not a custom implementation. + +Secrets are encrypted the moment you run `netclaw secrets set` or complete `netclaw init`. Encrypted values are prefixed with `ENC:`: + +```json +{ + "Slack": { + "BotToken": "ENC:CfDJ8N2x...long-base64-string..." + }, + "Providers": { + "openrouter": { + "ApiKey": "ENC:CfDJ8K9y...long-base64-string..." + } + } +} +``` + +Re-encrypting an already-encrypted value is safe — the writer checks for the `ENC:` prefix and skips values that already have it. + +## Storage Layout + +| Path | Contents | Permissions | +|------|----------|-------------| +| `~/.netclaw/config/secrets.json` | Encrypted credential values | `chmod 600` (Unix) | +| `~/.netclaw/keys/` | Data Protection key material | Owner-only access (Unix) | + +Both paths are required for decryption. `secrets.json` without `~/.netclaw/keys/` is locked ciphertext, and the keys without the secrets file have nothing to decrypt. Back up both together. + +On Windows, netclaw relies on user-profile ACLs instead of Unix file modes. Check file properties in the Security tab to verify only your user account has access. + +## Configuration Layering + +Secrets slot into the standard config priority chain. Highest priority wins: + +| Priority | Source | Use Case | +|----------|--------|----------| +| 1 (highest) | `NETCLAW_*` environment variables | CI/CD, containers, temporary overrides | +| 2 | `secrets.json` (encrypted) | Persistent credentials | +| 3 (lowest) | `netclaw.json` (plaintext) | Non-sensitive configuration | + +Environment variable names use the `NETCLAW_` prefix with double underscores for nesting, following the [.NET configuration convention](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider): `NETCLAW_Slack__BotToken` maps to `Slack.BotToken`. Case matters on Linux. + +## Agent Isolation + +The [security model's](/security/security-model/) resource hard-deny layer blocks the agent from accessing its own credential storage. These paths are denied for both read and write operations: + +- `~/.netclaw/config/secrets.json` — the encrypted credential file +- `~/.netclaw/keys/` — encryption key material +- `~/.netclaw/config/webhooks/` — webhook route configurations with HMAC secrets + +Symlinks are resolved before path checks, so `ln -s ~/.netclaw/keys/ ./sneaky` won't bypass the policy. If the agent tries to access a denied path, the operation is blocked and the tool call fails. The agent can *use* the credentials (they're injected into provider connections at startup) but can never read, modify, or exfiltrate the raw values. + +Note that `netclaw.json` itself is readable by the agent — only files containing secrets are denied. + +## Output Redaction + +Even with encryption and path denial, a secret can leak through tool output — an API key echoed in a curl response, a token appearing in a log. `SecretOutputRedactor` scrubs tool output before the LLM sees it, replacing matches with `***REDACTED***`. + +Patterns it catches: + +| Pattern | Examples | +|---------|----------| +| Provider API keys | `sk-*`, `ghp_*`, `AKIA*` (AWS) | +| Slack tokens | `xoxb-*`, `xoxp-*`, `xoxa-*`, `xoxr-*`, `xoxs-*` | +| Auth headers | `Authorization: Bearer ` | +| Connection strings | `Password=...;`, `Pwd=...;` | +| JWT tokens | Three-segment base64 starting with `eyJ` | +| PEM private keys | `-----BEGIN * PRIVATE KEY-----` blocks | +| JSON secret fields | Keys matching `api_key`, `token`, `secret`, `password`, `authorization`, `access_token`, `refresh_token`, `client_secret`, `signing_key`, `private_key`, `connection_string`, `credential` | +| Environment variables | `API_KEY=value`, `TOKEN=value`, `PASSWORD=value`, etc. | + +This is regex-based pattern matching, not semantic analysis. Custom or unusual secret formats won't be caught. + +## Write-Only Vault + +There is no `secrets get`, `secrets list`, or `secrets delete` command. Write secrets in, never read them back. This eliminates exfiltration via CLI: even if an attacker triggers a prompt injection, there's no netclaw command to dump credentials. + +To remove a secret, delete the key from `~/.netclaw/config/secrets.json` directly. The agent can't touch this file, but you can. To verify secrets are properly stored, run [`netclaw doctor`](/cli/doctor/). + +## Doctor Integration + +[`netclaw doctor`](/cli/doctor/) validates the secrets subsystem on every run: + +| Check | What It Catches | +|-------|----------------| +| File existence | `secrets.json` missing (warning, not error) | +| JSON validity | Malformed JSON that would prevent config loading | +| File permissions | Group or other read/write bits set on Unix (should be `600`) | +| Encryption status | Plaintext values that should be encrypted | + +If doctor flags unencrypted values, you'll need the original plaintext to re-encrypt: run `netclaw secrets set ` for each one. + +## Threat Model + +| Threat | Mitigation | +|--------|-----------| +| Secrets at rest on disk | AES encryption via Data Protection API; `chmod 600` file permissions | +| Agent reads its own credentials | Resource hard-deny on `secrets.json`, `keys/`, and `webhooks/` | +| Secret leaks into tool stdout | Regex-based output redaction before LLM ingestion | +| Secret exposed via CLI command | Write-only vault with no read/list/dump commands | +| Encryption keys copied to another machine | Keys and secrets both required — `secrets.json` alone is useless | +| Permission drift after manual edits | `netclaw doctor` flags incorrect file modes | + +## Recovery + +Back up `~/.netclaw/keys/` alongside `secrets.json`. Without the keys directory, encrypted values are unrecoverable. + +If `~/.netclaw/keys/` is deleted or corrupted, re-run `netclaw secrets set` for each credential. There's no other recovery path. + +If file permissions have drifted, reset them and verify: + +```bash +chmod 600 ~/.netclaw/config/secrets.json +netclaw doctor +``` + +If a secret isn't taking effect, check for environment variable overrides. `NETCLAW_Slack__BotToken` silently wins over the `secrets.json` value. `netclaw doctor` helps diagnose layering issues. + +## Limitations + +- Secrets are portable only if you copy both `~/.netclaw/keys/` and `~/.netclaw/config/secrets.json` together. The keys directory contains the Data Protection key ring needed to decrypt values. +- No built-in key rotation. Re-running `secrets set` overwrites the previous value, but there's no automated rotation schedule. +- Windows relies on user-profile ACLs rather than explicit `chmod`, so permission auditing is less straightforward. +- Single-machine vault, not a team secrets manager. No integration with external secret stores (Vault, AWS Secrets Manager, etc.). + +## Related Pages + +- [`netclaw secrets`](/cli/secrets/) — CLI command reference for setting secrets +- [`netclaw init`](/cli/init/) — stores initial credentials during first-run setup +- [`netclaw doctor`](/cli/doctor/) — validates secrets encryption and file permissions +- [Security Model](/security/security-model/) — default-deny architecture and the invocation stack +- [Hardening](/security/hardening/) — file permission lockdown and operational best practices + +## Further Reading + +- [ASP.NET Data Protection overview](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/introduction) — the encryption framework netclaw builds on +- [ASP.NET Data Protection key management](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/implementation/key-management) — how encryption keys are stored and rotated +- [.NET environment variable configuration](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers#environment-variable-configuration-provider) — the double-underscore nesting convention +- [OWASP Secrets Management Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html) — general best practices for credential storage diff --git a/src/content/docs/security/security-model.md b/src/content/docs/security/security-model.md index d2607b7..c4a303c 100644 --- a/src/content/docs/security/security-model.md +++ b/src/content/docs/security/security-model.md @@ -3,4 +3,216 @@ title: "Security Model" description: "Default-deny ACL, audience scoping, and trust levels." --- -Content coming soon. +Netclaw is **default-deny**. Every tool call, file access, and shell command starts blocked and must be explicitly permitted. Any policy failure — invalid config, engine exception, unrecognized channel — results in deny. + +*Audiences* control what's allowed. A *four-layer invocation stack* enforces it. You pick a deployment posture during [`netclaw init`](/cli/init/), and it governs every permission decision the daemon makes. + +## Trust Audiences + +Netclaw classifies each inbound message into one of three audiences based on the channel it arrived on: + +| Channel | Audience | +|---------|----------| +| TUI (`netclaw chat`), SignalR (real-time web transport), headless (no UI, daemon-only), console, manual | **Personal** | +| Slack, Discord, reminders, timers | **Team** | +| Unknown channel type | **Public** | + +Personal is the most permissive, Public the most restrictive. Unknown channels resolve to Public. If netclaw can't identify who's talking, it assumes the worst. + +## Deployment Postures + +During [`netclaw init`](/cli/init/), you pick a deployment posture that sets the baseline trust level: + +![Security posture selection during netclaw init](/screenshots/output/init-02-security-posture.png) + +The posture selection screen during `netclaw init`. Personal mode enables shell access with approval gates; Team and Public disable shell entirely. + +| Posture | Shell Access | Default Behavior | +|---------|-------------|-----------------| +| **Personal** | Enabled (with [approval gates](#approval-gates)) | All tools available, all MCP servers, unrestricted filesystem | +| **Team** | Off | Allowlisted tools only, no MCP servers, session-scoped filesystem (temp directory per session, wiped on end) | +| **Public** | Off | Minimal tool set, no MCP servers, session-scoped filesystem (temp directory per session, wiped on end) | + +With no config file at all, netclaw defaults to Public posture with shell disabled — the most restrictive option. + +Posture and audience are related but distinct. Posture is a deployment-wide setting you choose once. Audience is resolved per-message based on channel. A Team-posture deployment still classifies TUI sessions as Personal audience. + +## Per-Audience Permissions + +Tools, filesystem access, and attachment policies differ by audience: + +| Audience | Tools | MCP Servers | Filesystem | Attachment Types | +|----------|-------|-------------|------------|-----------------| +| **Personal** | All | All | Unrestricted | Image, PDF, Document, Archive, Media, Other | +| **Team** | `file_read`, `attach_file` | None | Session-scoped only | Image, PDF, Document, Archive, Media | +| **Public** | `file_read`, `file_write`, `attach_file` | None | Session-scoped only | Images only | + +Public having `file_write` while Team doesn't looks backwards — Public's `file_write` is restricted to the session-scoped temp directory (which is wiped on session end), so the blast radius is minimal. Team omits it because Team sessions are longer-lived and shared across users. + +MCP server permissions are managed separately per audience through [`netclaw mcp permissions`](/cli/mcp-tools/). New MCP servers start with zero tool grants for all audiences — you must explicitly enable them. + +## Four-Layer Invocation Stack + +Tool calls pass through four layers in sequence. If any layer rejects, execution stops. + +``` +┌─────────────────────────────────┐ +│ 1. Operation Hard Deny │ ← blocks dangerous commands +├─────────────────────────────────┤ +│ 2. Resource Hard Deny │ ← blocks protected file paths +├─────────────────────────────────┤ +│ 3. Tool Access Grant │ ← audience-based allowlist +├─────────────────────────────────┤ +│ 4. Approval Gate │ ← human confirms execution +└─────────────────────────────────┘ +``` + +### Layer 1: Operation Hard Deny + +Shell commands hit a hard-deny list first. Blocked regardless of audience or approval status: + +| Category | Blocked | +|----------|---------| +| Self-destructive | `netclaw daemon stop`, `netclaw daemon kill`, `systemctl stop netclaw`, `systemctl kill netclaw`, `kill`, `killall`, `pkill` | +| Privilege escalation | `sudo`, `su`, `doas` | +| System destruction | `rm -rf /`, `rm -rf ~/`, `mkfs*` | +| Fork bombs | `:(){ :\|:& };:`, `:(){:\|:&};:` | + +Add your own patterns in `~/.netclaw/config/netclaw.json`: + +```json +{ + "Tools": { + "HardDenyPatterns": ["docker rm", "kubectl delete namespace"] + } +} +``` + +### Layer 2: Resource Hard Deny + +File access is checked against protected paths. Read and write have separate deny lists: + +| Operation | Denied paths | +|-----------|-------------| +| **Read** | `~/.netclaw/config/secrets.json`, `~/.netclaw/keys/`, `~/.netclaw/config/webhooks/` | +| **Write** | `~/.netclaw/config/secrets.json`, `~/.netclaw/keys/`, SQLite DB, PID file, lock file, restart manifest | + +`~/.netclaw/config/netclaw.json` is intentionally **not** denied — the agent can read (but not write) the main config file. + +Netclaw resolves symlinks before checking, so `ln -s ~/.netclaw/keys/ ./sneaky` won't bypass the policy. + +### Layer 3: Tool Access Grant + +The audience profile determines which tools exist at all. Public and Team audiences only see allowlisted tools (see the [per-audience table](#per-audience-permissions)). Ungranted tools are invisible to the model — they don't appear in the tool list at all. + +MCP tool grants are configured separately per server and per audience through [`netclaw mcp permissions`](/cli/mcp-tools/). + +### Layer 4: Approval Gates {#approval-gates} + +Tools that pass layers 1-3 hit the approval gate, which prompts the operator for confirmation: + +| Option | Behavior | +|--------|----------| +| Approve once | Valid for the current session only | +| Approve always | Persisted to `~/.netclaw/config/tool-approvals.json` (edit this file directly to revoke) | +| Deny | Blocks this invocation | + +Approval timeouts work differently depending on the channel: + +- **Interactive channels** (TUI, Slack, Discord, SignalR) — no response within 5 minutes triggers auto-deny. The LLM gets an error message but has no idea an approval gate rejected it. +- **Non-interactive channels** (headless, reminders, webhooks) — approval gates are structurally unsupported, so all gated tools are auto-denied immediately. + +Compound commands (`cmd1 && cmd2 | cmd3`) are split into segments, each checked independently. Unapproved patterns are batched into a single prompt. + +| Channel | Approval Support | +|---------|-----------------| +| TUI, Slack, Discord, SignalR | Yes | +| Headless, reminders, webhooks | No — auto-deny | + +## Secret Redaction + +The redactor strips secrets from command stdout before the model sees them. It catches: + +- API key prefixes: `sk-*`, Slack API tokens (`xox[baprs]-...`), `ghp_*`, `AKIA*` +- `Authorization: Bearer` headers +- JSON fields matching sensitive names: `api_key`, `api-key`, `apikey`, `token`, `secret`, `password`, `authorization`, `access_token`, `refresh_token`, `client_secret`, `signing_key`, `private_key`, `connection_string`, `credential` +- Connection strings with `Password=` or `Pwd=` +- JWT tokens and PEM private key blocks + +For encryption at rest, see [`netclaw secrets`](/cli/secrets/). + +## Prompt Injection Detection + +Netclaw scans inbound content — tool output, file contents, MCP server responses — for injection patterns. Detection returns a risk level per match; callers (the invocation stack layers) decide whether to reject or warn. + +| Category | Examples | Risk Level | +|----------|---------|------------| +| Prompt injection | "ignore previous instructions" | High | +| Role resets | `YouAreNow`-style patterns | Medium | +| Data exfiltration | "exfiltrate secrets via curl" | High | +| Privilege escalation | Access control list (ACL) modification, admin grants | High | +| Destructive operations | `rm -rf`, DROP TABLE | High | +| Invisible unicode | Zero-width chars, BiDi control characters | Medium | +| Private Use Area chars | PUA codepoints | Low | + +High-risk matches are rejected outright. Medium-risk matches generate warnings. Low-risk matches are allowed through. + +## Content Validation + +Before accepting a file upload, netclaw validates: + +- MIME type must be on the audience's allowlist +- Maximum file size: 25 MiB +- File headers are checked against declared MIME types ([magic byte validation](https://en.wikipedia.org/wiki/List_of_file_signatures)) — renaming `malware.exe` to `photo.png` won't work + +## Network Exposure + +Exposure mode controls network reachability. It's separate from audience and posture: + +| Mode | Scope | Requires | +|------|-------|----------| +| `local` | Loopback only | Nothing (default) | +| `tailscale-serve` | Your tailnet | [`tailscaled`](https://tailscale.com/kb/) | +| `tailscale-funnel` | Public internet | [`tailscaled`](https://tailscale.com/kb/1223/funnel/) | +| `cloudflare-tunnel` | Public internet | [`cloudflared`](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/) | + +Internet-reachable modes force explicit confirmation during [`netclaw init`](/cli/init/) and trigger high-risk diagnostic warnings. Changing exposure mode requires a daemon restart — not hot-reloaded. + +A Personal-posture deployment exposed via Tailscale Funnel is reachable from the internet but still applies Personal audience rules to TUI sessions and Team rules to Slack. Exposure and audience are orthogonal. + +## Fail-Closed Guarantees + +When in doubt, netclaw denies: + +- Invalid ACL schema in config → daemon refuses to start +- Policy engine throws an exception → deny +- Unknown channel type → Public audience (most restrictive) +- No config file → Public posture, shell off +- Approval gate timeout → auto-deny +- MCP server with no tool grants → all tools blocked + +There is no permissive mode — access must be explicitly granted. + +## Limitations + +- Prompt injection detection uses regex pattern matching, not semantic analysis — novel phrasings can evade it +- Approval gates require an interactive channel — headless, reminder, and webhook sessions auto-deny all gated tools +- Secret redaction catches known patterns only — custom secret formats need custom hard-deny path rules +- Content validation checks magic bytes but doesn't deep-scan file contents for embedded threats +- Per-channel audience overrides exist ([`ChannelAudiences` config](/security/hardening/)) but require manual channel ID mapping — there's no UI for it yet + + + +## Related Pages + +- [`netclaw init`](/cli/init/) — posture selection and network exposure configuration +- [`netclaw secrets`](/cli/secrets/) — encrypted credential storage and output redaction +- [`netclaw mcp`](/cli/mcp-tools/) — per-audience MCP tool permissions +- [`netclaw webhooks`](/cli/webhooks/) — HMAC verification and audience assignment for inbound webhooks +- [Hardening](/security/hardening/) — additional lockdown recommendations for production deployments + +## Further Reading + +- [OWASP LLM Top 10](https://genai.owasp.org/llm-top-10/) — common attack vectors for LLM applications +- [NIST AI Risk Management Framework](https://www.nist.gov/artificial-intelligence/ai-risk-management-framework) — federal guidance on AI system security +- [Model Context Protocol specification](https://spec.modelcontextprotocol.io/) — the protocol netclaw uses for tool server integration diff --git a/src/content/docs/skills/external-skills.md b/src/content/docs/skills/external-skills.md index 128fc40..db66d6e 100644 --- a/src/content/docs/skills/external-skills.md +++ b/src/content/docs/skills/external-skills.md @@ -3,4 +3,175 @@ title: "External Skills" description: "Adding custom skill paths and external sources." --- -Content coming soon. +External skills let netclaw read skill directories owned by other AI tools — Claude Code, Open Code, or any arbitrary directory on disk. Point netclaw at an existing directory and it scans those skills alongside its own. No copying required. + +Skills must follow the [SKILL.md format](https://agentskills.io) (frontmatter with `name` and `description` fields, markdown body). See [Skills Overview](/skills/overview/) for details on the format and lifecycle. + +## Quick Start + +Already have Claude Code installed? The [`netclaw init`](/cli/init/) wizard detects it and configures the source automatically. To add it manually: + +```bash +netclaw skill source add claude-code --well-known claude-code +``` + +That's it. The daemon picks up the change via its file watcher — no restart needed. + +## Before You Begin + +- Netclaw is installed and `netclaw init` has been run (or you're comfortable editing `netclaw.json` directly — it lives at `~/.netclaw/config/netclaw.json` by default) +- The external directory you want to add exists on disk (netclaw logs a warning for missing paths but still configures the source) + +## Well-Known Sources + +Well-known aliases expand to standard paths: + +| Alias | Resolves to | +|-------|-------------| +| `claude-code` | `~/.claude/skills/`, `~/.claude/commands/`, plus marketplace paths discovered dynamically (`~/.claude/plugins/marketplaces/*/skills/`) | +| `open-code` | `~/.open-code/skills/` | + +The `claude-code` alias scans marketplace plugin directories alphabetically for stable precedence. Marketplace paths are discovered at runtime — if you install a new plugin, it gets picked up on the next scan. Missing one of those paths is fine — the alias resolves whichever ones exist. + +## Configuration + +External sources live under `ExternalSkills.Sources` in `netclaw.json`: + +```json +{ + "ExternalSkills": { + "Sources": [ + { + "Name": "claude-code", + "WellKnown": "claude-code", + "Enabled": true, + "AllowSymlinks": true + }, + { + "Name": "team-skills", + "Path": "/opt/skills/shared", + "Enabled": true, + "AllowSymlinks": true + } + ] + } +} +``` + +### Source fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Name` | string | — | Unique identifier for this source | +| `Path` | string | null | Absolute path to a skill directory (mutually exclusive with `WellKnown`) | +| `WellKnown` | string | null | Well-known alias (mutually exclusive with `Path`) | +| `Enabled` | bool | `true` | Whether the source is active | +| `AllowSymlinks` | bool | `false` | Follow symlinks within the directory. Well-known sources like `claude-code` default to `true` when added via CLI or init wizard. | + +Each source must set either `Path` or `WellKnown`, not both. + +## Managing Sources via CLI + +Add and remove sources without editing `netclaw.json`: + +```bash +# List configured sources +netclaw skill source list + +# Add a well-known source +netclaw skill source add claude-code --well-known claude-code + +# Add a custom path +netclaw skill source add team-skills --path /opt/skills/shared + +# Disable a source without removing it +netclaw skill source disable team-skills + +# Re-enable +netclaw skill source enable team-skills + +# Remove entirely +netclaw skill source remove team-skills +``` + +All `netclaw skill source` commands work without the daemon running. CLI changes are picked up by a running daemon automatically via its file watcher. + +## Auto-Detection During Init + +The [`netclaw init`](/cli/init/) wizard detects Claude Code and Open Code installations automatically: + +![External skills configuration during init](/screenshots/output/init-07-external-skills.png) + +Detected sources get enabled by default. Next, the wizard prompts for custom paths: + +![Custom skills path input](/screenshots/output/init-07-custom-skills-path.png) + +A symlink toggle follows. Leave it off unless your setup requires it (shared filesystems, monorepo layouts with linked skill directories). + +## Precedence + +When multiple sources define a skill with the same name: + +``` +native skills > server feeds > external sources +``` + +[Native skills](/skills/overview/) are skills authored directly through netclaw's agent. [Server feeds](/skills/skill-feeds/) are remotely-synced skill repositories. External sources always have lowest priority. + +Within external sources, order in the `Sources` array determines which wins — higher-priority sources go first. Collisions are logged; run `netclaw skill issues` to see them. + +## Runtime Behavior + +The daemon watches every resolved external path for changes. When a `*.md` file changes, it rescans after a 500ms debounce — so multiple rapid writes only trigger one rescan. Drop a new skill file into an external directory and it's available on the next agent turn. No restart needed. + +The daemon logs a warning for missing directories at startup but keeps running. + +## Security + +Well-known sources (like `claude-code`) have `AllowSymlinks: true` set automatically because their standard paths include symlinked marketplace plugins. Custom sources default to `AllowSymlinks: false` — opt in per source if you trust the targets. + +External skills loaded from disk go through: + +- **Frontmatter validation** — `name` and `description` are required; malformed YAML is rejected +- **Symlink/path safety** — blocked unless `AllowSymlinks: true` for that source +- **[Tool access policies](/security/security-model/)** — same restrictions as native skills; the `allowed-tools` frontmatter field is informational only and doesn't grant tool access + +External skills do NOT go through the prompt injection content scanner (that only runs on skills authored through the `skill_manage` tool). The assumption is that external skill files are user-curated. + +## Validating External Skills + +Validate skills before loading them: + +```bash +# Validate a single skill +netclaw skill validate /opt/skills/shared/deploy/SKILL.md + +# See all scanning/validation issues across all sources +netclaw skill issues +``` + +## Troubleshooting + +### Source shows 0 skills after adding + +The path doesn't contain valid `*.md` files with [SKILL.md frontmatter](https://agentskills.io). Run `netclaw skill validate ` on a file to see what's wrong. Common causes: missing `name` or `description` in frontmatter, or the directory has subdirectories but no top-level markdown files. + +### "Symlink blocked" warnings in logs + +The source has `AllowSymlinks: false` and the directory contains symlinks. Either restructure the directory or set `AllowSymlinks: true` if you trust where the links point. + +### Skills not updating after file changes + +Filesystem events are debounced by 500ms. If changes still aren't showing up, check that the daemon is running (`netclaw status`) and the source is enabled (`netclaw skill source list`). + +## What to Read Next + +- [Skills Overview](/skills/overview/) — skill format, source types, and the full lifecycle +- [Skill Feeds](/skills/skill-feeds/) — server-synced skill repositories +- [`netclaw skill`](/cli/skill/) — full CLI reference for skill management + +## Resources + +- [AgentSkills.io](https://agentskills.io) — the SKILL.md format specification +- [Claude Code documentation](https://docs.anthropic.com/en/docs/claude-code) — Claude Code setup and commands reference +- [FileSystemWatcher docs](https://learn.microsoft.com/en-us/dotnet/api/system.io.filesystemwatcher) — the .NET file watching API netclaw uses diff --git a/src/content/docs/skills/overview.md b/src/content/docs/skills/overview.md index ac2e062..8ac54c0 100644 --- a/src/content/docs/skills/overview.md +++ b/src/content/docs/skills/overview.md @@ -3,4 +3,189 @@ title: "Skills Overview" description: "What skills are and how Netclaw uses them." --- -Content coming soon. +Skills are markdown files that teach the agent *how to do things*. Rather than hard-coding every behavior into the daemon, you write procedural knowledge as plain text and the agent loads it on demand. + +Think of them as cheat sheets. A `commit` skill encodes your team's commit conventions; a `review-pr` skill walks through your code review checklist. The agent sees a one-line-per-skill index on every turn (just names and descriptions), then reads the full content only when it needs it. A large skill library costs almost nothing in context. + +All skills follow the [AgentSkills.io](https://agentskills.io) format: a `SKILL.md` file with YAML frontmatter. The format is shared across multiple agent platforms, so skills you write for netclaw can work in Claude Code too. + +## How skills reach the agent + +The lifecycle has four phases: + +1. **Sync** — at startup, the daemon pulls system skills from a CDN and (optionally) syncs skills from private [skill servers](/skills/skill-server/). These land in read-only directories under `~/.netclaw/skills/`. + +2. **Scan** — the daemon walks all skill directories (native, server feeds, external), validates frontmatter, and merges results. Native skills take precedence over server feeds, which take precedence over external sources. Netclaw logs name collisions from lower-precedence sources as issues and skips the duplicates. Run `netclaw skill issues` to see them. + +3. **Index** — the daemon compresses the merged list into a one-line-per-skill summary and injects it into the agent's system prompt. The agent sees skill names and descriptions but not the full instructions. + +4. **Load** — when the agent needs a skill, it calls the `skill_load` tool (an internal tool the agent uses to fetch skill content) to read the full `SKILL.md` body and get a listing of resource files (scripts, references, assets). Users can also invoke skills directly via `/skill-name` slash commands. + +Changes to skill files trigger a rescan automatically. A file watcher monitors native and external source directories with a 500ms debounce, so edits take effect without a restart. Server feed directories are not watched — they update on the configured sync interval. + +## Skill file format + +Two layouts work: + +**Directory-based** — for skills with supporting resources: + +``` +commit/ + SKILL.md # Required — instructions + frontmatter + scripts/ # Optional — helper scripts + references/ # Optional — reference material + assets/ # Optional — images, templates +``` + +**Flat file** — for self-contained skills: + +``` +commit.md +``` + +The file name (or directory name) must match the `name` field in the frontmatter for native skills. External sources are more relaxed — the frontmatter `name` is treated as canonical. + +```yaml +--- +name: commit +description: Create well-structured git commits with conventional commit messages. +metadata: + version: 1.0.0 +license: MIT +allowed-tools: "bash git" +--- + +# Commit Changes + +Instructions for the agent go here... +``` + +### Frontmatter fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | Lowercase identifier (must match directory/file name for native skills) | +| `description` | Yes | One-line summary (max 1024 chars per the [AgentSkills.io](https://agentskills.io) spec) | +| `metadata.version` | No | [Semver](https://semver.org) version string | +| `license` | No | [SPDX license identifier](https://spdx.org/licenses/) | +| `compatibility` | No | Which agent platforms can use this skill | +| `allowed-tools` | No | Space-delimited list of tools the skill needs (informational — does not grant access) | +| `disable-model-invocation` | No | When `true`, excluded from the LLM index (still invocable via `/name`) | +| `invocable` | No | Default `true`; when `false`, hidden from slash commands | +| `argument-hint` | No | Hint text shown after the slash command in completions | +| `metadata.subagent` | No | Routes execution to a named subagent instead of returning skill content | + +The display name shown in `netclaw skill list` comes from the first `#` heading in the markdown body, falling back to title-casing the skill name. + +## Where skills come from + +Four source types, each with different trust and management: + +| Source | Location | Managed by | Mutable? | +|--------|----------|-----------|----------| +| **system** | `~/.netclaw/skills/.system/` | Daemon (CDN sync) | No | +| **native** | `~/.netclaw/skills/` | You | Yes | +| **server feeds** | `~/.netclaw/skills/.server-feeds/` | Private [skill servers](/skills/skill-server/) | No | +| **external** | Configured directories | Source tool (Claude Code, etc.) | At source | + +Drop a `SKILL.md` into `~/.netclaw/skills/` and it appears on the next scan — that's a native skill. Create, edit, and remove them with [`netclaw skill`](/cli/skill/). Run `netclaw skill validate` to check a skill's frontmatter before using it. + +System skills ship from a CDN feed and are read-only. Built-in skills like `netclaw-identity` and `netclaw-diagnostics` land here on first run. For a fully offline setup, set `SkillSync.DisableSystemSkillSync: true` in your `netclaw.json` (see [Configuration](/configuration/models/) for file location). + + + +External skills let netclaw read skill directories from other AI tools. The [`netclaw init`](/cli/init/) wizard detects well-known directories automatically: + +![External skills configuration during init](/screenshots/output/init-07-external-skills.png) + +The init wizard detects Claude Code's skill directory and offers to wire it up as an external source. + +Well-known aliases expand to standard paths: + +| Alias | Scans | +|-------|-------| +| `claude-code` | `~/.claude/skills/`, `~/.claude/commands/`, `~/.claude/plugins/marketplaces/*/skills/` | +| `open-code` | `~/.open-code/skills/` | + +Server feed skills sync from private [skill servers](/skills/skill-server/) that implement the [Cloudflare Agent Skills Discovery RFC](https://github.com/cloudflare/agent-skills-spec). These are self-hosted registries that distribute skills across your organization. Configure them during init or add them later: + +![Skill feeds configuration during init](/screenshots/output/init-08-skill-feeds.png) + +Configure skill feeds during init or add them later via `netclaw skill feed add`. See the [netclaw skill-server](https://github.com/netclaw-dev/skill-server) reference implementation for setting up your own. + +## Precedence + +When multiple sources define a skill with the same name, the highest-precedence source wins: + +``` +native > server feeds > external +``` + +A native skill named `commit` shadows any `commit` from a server feed or external source. You can always override shared skills with your own version. + +## Security + +Skills are just text, but text that lands in an agent's instructions can carry prompt injection. Netclaw runs a content scanner on both write operations (create, edit, sync) and at load time (every `skill_load` call scans before returning content): + +| Verdict | Risk | Action | +|---------|------|--------| +| **Allowed** | None or low risk detected | Skill proceeds normally | +| **Warning** | Medium | Skill loads, but the event is logged | +| **Rejected** | High (prompt injection detected) | Skill is blocked; `netclaw skill issues` surfaces the rejection | + +Additional guardrails: + +- Symlinks are blocked by default in native and system directories. External sources can opt in via `AllowSymlinks: true` in config. +- Sessions with a [public trust audience](/security/security-model/) cannot load skills. `skill_load` also returns an error when `SkillSync.Enabled` is `false`. +- Skills run through the same [tool access policies](/security/security-model/) as everything else. The `allowed-tools` frontmatter field is informational — actual tool grants are controlled by the security policy. + +## Subagent routing + +Instead of returning instructions, a skill can hand off to a subagent. Add `metadata.subagent` to the frontmatter: + +```yaml +--- +name: research +description: Deep research on a topic using multiple sources. +metadata: + subagent: research-assistant +--- +``` + +When `skill_load` hits this skill, it spawns the named subagent with the skill's body as a system prompt overlay. The caller passes a `task` parameter to the spawned subagent. The target subagent must be registered and user-facing — internal-only subagents cannot be routed to. + +## Configuration + +Two config keys in `netclaw.json` control skill loading: + +| Key | Default | Description | +|-----|---------|-------------| +| `SkillSync.Enabled` | `true` | Blocks the agent from loading skills via `skill_load` when `false` | +| `SkillSync.DisableSystemSkillSync` | `false` | Skip CDN sync for system skills | + +External sources and skill feeds have their own config sections. See [External Skills](/skills/external-skills/) and [Skill Feeds](/skills/skill-feeds/) for details. + +## Tracking skill usage + +Run [`netclaw stats skills`](/cli/stats/) to see which skills the agent loads most often. Good for spotting which skills pull their weight and which to cut. + +## Limitations + +- No built-in skill marketplace or discovery beyond what your configured sources provide. +- Skill content scanning uses regex-based heuristics. It catches common prompt injection patterns but is not a comprehensive security boundary. +- The `allowed-tools` frontmatter field is informational. It does not grant tool access — that is controlled entirely by the [security policy](/security/security-model/). +- Skills are text-only. They cannot register tools, modify daemon behavior, or execute code directly. +- No versioning or rollback for native skills. If you overwrite a skill, the old version is gone. + +## What to read next + +- [`netclaw skill`](/cli/skill/) — CLI reference for managing skills (list, show, validate, search, source management) +- [External Skills](/skills/external-skills/) — configuring external skill directories +- [Skill Feeds](/skills/skill-feeds/) — subscribing to server-synced skill repositories +- [Skill Server](/skills/skill-server/) — running a private skill registry for your org + +## Resources + +- [AgentSkills.io](https://agentskills.io) — the SKILL.md format specification +- [Cloudflare Agent Skills Discovery RFC](https://github.com/cloudflare/agent-skills-spec) — the protocol behind skill feeds and server-based distribution +- [netclaw skill-server](https://github.com/netclaw-dev/skill-server) — reference implementation of a self-hosted skill registry diff --git a/src/content/docs/skills/skill-feeds.md b/src/content/docs/skills/skill-feeds.md index 9ce7420..2eb5d9f 100644 --- a/src/content/docs/skills/skill-feeds.md +++ b/src/content/docs/skills/skill-feeds.md @@ -1,6 +1,211 @@ --- title: "Skill Feeds" -description: "Subscribing to community skill feeds." +description: "Subscribing to private skill server feeds." --- -Content coming soon. +Skill feeds connect your netclaw daemon to private skill servers. The daemon discovers available skills via the [Cloudflare Agent Skills Discovery RFC](https://github.com/cloudflare/agent-skills-spec) protocol, downloads them, verifies integrity via SHA-256 digests, and makes them available to the agent — all automatically. + +Skills follow the [AgentSkills.io](https://agentskills.io) open standard format. Both standards are vendor-neutral and supported across multiple agent platforms, so skills published to a feed work anywhere that speaks the protocol. + +## How It Works + +```mermaid +flowchart TD + A[Daemon starts] --> B[Sync all feeds in parallel] + B --> C{For each feed} + C --> D[Fetch /.well-known/agent-skills/index.json] + D --> E[Diff against local sync state] + E --> F{New or changed skills?} + F -->|No| G[Skip — already up to date] + F -->|Yes| H[Download skill files] + H --> I[Verify SHA-256 digest] + I -->|Mismatch| J[Reject — log warning] + I -->|OK| K[Content scan for prompt injection] + K -->|Rejected| J + K -->|Allowed| L[Write to ~/.netclaw/skills/.server-feeds/] + L --> M[Update sync state] + G --> N[Rescan all skill directories] + M --> N + J --> N + N --> O[Update agent skill index] + O --> P[Sleep SyncIntervalMinutes] + P --> B +``` + +Each feed syncs independently. A failing server never blocks other feeds or daemon startup. On failure, the daemon falls back to on-disk skills from the last successful sync. + +## Add a Feed During Init + +The [`netclaw init`](/cli/init/) wizard includes a skill feeds step: + +![Skill feeds configuration during init](/screenshots/output/init-step8-skill-feeds-prompt.png) + +The wizard probes the URL, fetches the RFC index, reports the skill count (or shows the error), and suggests a name based on the hostname. Add as many feeds as you need. + +## Manual Configuration + +Add a feed after init: + +```bash +netclaw skill feed add corp-skills --url https://skills.corp.com +``` + +Or edit `~/.netclaw/config/netclaw.json` directly: + +```json +{ + "SkillFeeds": { + "SyncIntervalMinutes": 60, + "Feeds": [ + { + "Name": "corp-skills", + "Url": "https://skills.corp.com", + "Enabled": true, + "TimeoutSeconds": 30 + } + ] + } +} +``` + +## Feed Config Fields + +| Field | Type | Default | Description | +|-------|------|---------|-------------| +| `Name` | string | — | Filesystem-safe identifier for this feed | +| `Url` | string | — | Base URL; daemon appends `/.well-known/agent-skills/index.json` | +| `Enabled` | bool | `true` | Toggle without removing the entry | +| `TimeoutSeconds` | int | `30` | HTTP timeout for this feed | + +Top-level `SkillFeeds` settings: + +| Field | Default | Description | +|-------|---------|-------------| +| `SyncIntervalMinutes` | `60` | Periodic sync interval. Set to `0` for startup-only sync. | + +## Sync Behavior + +- **Startup sync** runs immediately with no jitter. Network failures are logged but never block the daemon from starting. +- **Periodic sync** fires every `SyncIntervalMinutes`. The first periodic tick adds 0-5 minutes of random jitter to stagger multiple instances hitting the same server. +- **Startup-only mode**: set `SyncIntervalMinutes: 0` to disable periodic sync entirely. Skills only update when the daemon restarts. +- **Isolation**: each feed syncs in its own background task. One slow or broken server has zero impact on the others. +- **Fallback**: on any failure, the daemon keeps serving the last successfully synced skills from disk. + +## Writing Skills for a Feed + +Any skill server publishes skills that follow the [AgentSkills.io](https://agentskills.io) format. Here's what you need to know. + +### Directory Layout + +``` +my-skill/ + SKILL.md # Required + references/ # Optional — reference material + scripts/ # Optional — helper scripts + assets/ # Optional — images, templates +``` + +### SKILL.md Format + +```yaml +--- +name: deploy-staging +description: Deploy the current branch to the staging environment. +version: 1.2.0 +category: devops +allowed-tools: "bash docker" +--- + +# Deploy to Staging + +Prerequisites: +- Docker CLI authenticated to the registry +- VPN connected to staging network + +## Steps + +1. Build the container image +2. Push to the staging registry +3. Trigger the deployment webhook +``` + +### Frontmatter Fields + +| Field | Required | Description | +|-------|----------|-------------| +| `name` | Yes | 1-64 chars, lowercase alphanumeric + hyphens (`^[a-z0-9]+(-[a-z0-9]+)*$`) | +| `description` | Yes | One-line summary visible to agents during discovery | +| `version` | Yes (for feeds) | [Semver](https://semver.org) string — bumping triggers a new publish | +| `category` | No | Aids search and filtering | +| `license` | No | [SPDX identifier](https://spdx.org/licenses/) | +| `compatibility` | No | Which agent platforms can use this | +| `allowed-tools` | No | Space-delimited tool list (informational only, does not grant access) | +| `invocable` | No | Default `true`; `false` hides from slash commands | +| `disable-model-invocation` | No | `true` excludes from LLM index (still invocable via `/name`) | +| `argument-hint` | No | Hint shown after the slash command name | +| `metadata.subagent` | No | Routes to a named subagent instead of returning content | + +The `version` field is required for feed publishing. The server uses it to detect changes — bump the version to trigger downstream syncs. + +## Security + +Every skill downloaded from a feed goes through the content scanner before it reaches the agent: + +| Verdict | Action | +|---------|--------| +| **Allowed** | Skill proceeds normally | +| **Warning** | Skill loads, event is logged | +| **Rejected** | Skill blocked; surfaces in `netclaw skill issues` | + +Resource files (`references/`, `scripts/`) are also scanned. SHA-256 digest verification runs before scanning — a tampered file is rejected before the content scanner even sees it. + +Server feed skills are stored as read-only in `~/.netclaw/skills/.server-feeds/{feed-name}/`. They cannot be modified locally. + +### Precedence + +``` +native skills > server feeds > external sources +``` + +A native skill with the same name always wins. To override a feed skill, create a native skill with the same `name` — your local version takes priority. + +### Disabling All Skills + +Two related config keys control broader skill behavior: + +| Key | Effect | +|-----|--------| +| `SkillSync.Enabled: false` | Blocks the agent from loading *any* skills via `skill_load` | +| `SkillSync.DisableSystemSkillSync: false` | Controls the built-in CDN feed (separate from private feeds) | + +## Troubleshooting + +### Feed shows 0 skills after adding + +Run `netclaw skill feed list` to confirm the feed is enabled. Then check the daemon logs — the most common cause is the server not serving `/.well-known/agent-skills/index.json` at the expected path. Verify with: + +```bash +curl -s https://skills.corp.com/.well-known/agent-skills/index.json | head +``` + +### Skills not updating + +Periodic sync fires every `SyncIntervalMinutes` (default 60). If you need immediate updates, restart the daemon — startup sync runs without delay. Also check that the skill's `version` was bumped on the server side. + +### "Content rejected" in skill issues + +The content scanner flagged prompt injection patterns in the skill body. Run `netclaw skill issues` for details. Work with the skill author to revise the content, or file an issue on the skill server if you believe it's a false positive. + + +## Related Pages + +- [Skills Overview](/skills/overview/) — format, lifecycle, and precedence rules +- [External Skills](/skills/external-skills/) — local skill directories from other tools +- [Skill Server](/skills/skill-server/) — running your own server that publishes feeds +- [`netclaw skill`](/cli/skill/) — CLI reference for skill management + +## External Resources + +- [AgentSkills.io](https://agentskills.io) — the SKILL.md format specification +- [Cloudflare Agent Skills Discovery RFC v0.2.0](https://github.com/cloudflare/agent-skills-spec) — the discovery protocol behind skill feeds +- [netclaw-dev/skill-server](https://github.com/netclaw-dev/skill-server) — reference implementation of a private skill server diff --git a/src/content/docs/skills/skill-server.md b/src/content/docs/skills/skill-server.md index 838095e..ca49c25 100644 --- a/src/content/docs/skills/skill-server.md +++ b/src/content/docs/skills/skill-server.md @@ -3,4 +3,314 @@ title: "Skill Server" description: "Running and connecting a skill server." --- -Content coming soon. +SkillServer is a self-hosted skill registry — a private NuGet feed or npm registry, but for [SKILL.md](https://agentskills.io) files. Host it behind your firewall, publish proprietary skills, and netclaw instances on the network sync from it automatically. + +**Source code and releases: [github.com/netclaw-dev/skill-server](https://github.com/netclaw-dev/skill-server)** + +It implements two open standards: + +- **[Cloudflare Agent Skills Discovery RFC v0.2.0](https://github.com/cloudflare/agent-skills-spec)** — discovery via `/.well-known/agent-skills/index.json` +- **[AgentSkills.io](https://agentskills.io)** — the SKILL.md format for skill definitions + +Any agent that supports these standards can consume skills from your server, not just netclaw. + +## What it does + +- Stores versioned skills in content-addressable blob storage (SHA-256) +- Serves a discovery index that agents poll on an interval +- API key auth on writes; reads are open (agents fetch skills without credentials) +- Single container, no external dependencies — just SQLite + filesystem + +## Deploy with Docker + +Pull the image: + +```bash +docker pull ghcr.io/netclaw-dev/skillserver:latest +``` + +Available for `linux/amd64` and `linux/arm64`. + +### Docker Compose + +```yaml +services: + skill-server: + image: ghcr.io/netclaw-dev/skillserver:latest + ports: + - "8080:8080" + volumes: + - skill-data:/data + environment: + - SKILLSERVER__DATAPATH=/data + - SKILLSERVER__BASEURL=http://localhost:8080 + - SKILLSERVER__APIKEY=${SKILLSERVER_APIKEY:-} + - ASPNETCORE_URLS=http://+:8080 + +volumes: + skill-data: +``` + +Start it: + +```bash +export SKILLSERVER_APIKEY="sk-$(openssl rand -base64 32)" +echo "Save this key: $SKILLSERVER_APIKEY" +docker compose up -d +``` + +Verify it's running: + +```bash +curl http://localhost:8080/health +``` + +The bootstrap API key is hashed and stored on first startup. Save the raw value — it cannot be recovered from the server. + +## Configuration + +All configuration is via environment variables: + +| Variable | Default | Description | +|----------|---------|-------------| +| `SKILLSERVER__DATAPATH` | `./data` | SQLite database + blob storage directory | +| `SKILLSERVER__BASEURL` | `http://localhost:8080` | Base URL for absolute URLs in discovery responses | +| `SKILLSERVER__APIKEY` | *(none)* | Bootstrap API key, seeded on first run if no keys exist in DB | +| `ASPNETCORE_URLS` | `http://+:8080` | Listen address and port | + +All state lives in `SKILLSERVER__DATAPATH`. Back up that volume and you have everything. + +### Production + +- Put a reverse proxy (Caddy, nginx, Traefik) in front for TLS +- Set `SKILLSERVER__BASEURL` to your public URL (e.g., `https://skills.internal.example.com`) so discovery responses have correct absolute URLs +- Mount `/data` to persistent storage — if the volume is lost, you'll need to re-publish all skills + +## API key management + +Reads are open. Writes (publish, delete, key management) require a `Bearer` token. + +### Bootstrap + +Set `SKILLSERVER__APIKEY` before the first run. The server hashes it and stores it as the "bootstrap" key. Once any key exists in the database, this environment variable is ignored on subsequent starts. + +### Create additional keys + +```bash +curl -X POST http://localhost:8080/api-keys \ + -H "Authorization: Bearer sk-your-bootstrap-key" \ + -H "Content-Type: application/json" \ + -d '{"label": "ci-deploy"}' +``` + +The response contains the raw key once. Store it in a secret manager or password vault. + +### List and revoke + +```bash +# List (never shows raw keys) +curl http://localhost:8080/api-keys \ + -H "Authorization: Bearer sk-your-key" + +# Revoke +curl -X DELETE http://localhost:8080/api-keys/2 \ + -H "Authorization: Bearer sk-your-key" +``` + +You cannot delete the last remaining key. + +### Key format + +Format: `sk-{random}` (256 bits entropy, base64url-encoded). Stored as SHA-256 hashes, compared in constant time. Raw keys never touch disk. + +## Publishing skills (skillserver CLI) + +The `skillserver` CLI handles publishing and management from your terminal or CI. + +### Install + +```bash +# Standalone binary (no .NET runtime required) +curl -fsSL https://raw.githubusercontent.com/netclaw-dev/skill-server/dev/scripts/install-skillserver.sh | bash + +# Or as a .NET global tool +dotnet tool install --global Netclaw.SkillServer.Cli +``` + +Standalone binaries are available for linux-x64, linux-arm64, osx-arm64, and win-x64. + +### Configure + +```bash +skillserver config init +``` + +Prompts for server URL and API key. Saves to `~/.skillserver/config.json`. + +For CI, use environment variables instead: + +```bash +export SKILLSERVER_URL=https://skills.internal.example.com +export SKILLSERVER_API_KEY=sk-your-ci-key +``` + +Priority: CLI flags > environment variables > config file. + +### Publish + +A skill is a directory containing a `SKILL.md` with YAML frontmatter: + +``` +my-skill/ + SKILL.md # Required — name, description, version in frontmatter + resources/ # Optional — supporting files +``` + +Name validation: 1-64 chars, lowercase alphanumeric + hyphens (`^[a-z0-9]+(-[a-z0-9]+)*$`). + +```bash +# Single skill +skillserver publish ./my-skill + +# All skills in a directory +skillserver publish-all ./skills + +# Dry run (validates without uploading) +skillserver publish-all ./skills --dry-run + +# Force re-publish (deletes existing version first) +skillserver publish ./my-skill --force +``` + +Publishing a version that already exists returns 409 Conflict and the CLI skips it. This makes `publish-all` idempotent — safe to run repeatedly in CI. + +### Other commands + +```bash +# List and search +skillserver list +skillserver list --search kubernetes + +# Version history +skillserver versions my-skill + +# Verify local matches server +skillserver verify ./my-skill + +# Delete a version +skillserver delete my-skill 1.0.0 --yes + +# Manage API keys +skillserver api-key create --label "CI Pipeline" +skillserver api-key list +skillserver api-key delete 7 +``` + +### Global options + +| Option | Description | +|--------|-------------| +| `--server-url ` | Override server URL | +| `--api-key ` | Override API key | +| `--output text\|json` | Output format | +| `--verbose, -v` | Show HTTP request/response details | + +## CI/CD integration + +A GitHub Actions workflow that publishes on push and validates on PR. This example uses Tailscale to reach a server on a private network: + +```yaml +name: Publish Skills +on: + push: + branches: [master] + pull_request: + branches: [master] + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Connect to Tailscale + uses: tailscale/github-action@v2 + with: + oauth-client-id: ${{ secrets.TS_OAUTH_CLIENT_ID }} + oauth-secret: ${{ secrets.TS_OAUTH_SECRET }} + + - name: Install skillserver CLI + run: curl -fsSL https://raw.githubusercontent.com/netclaw-dev/skill-server/dev/scripts/install-skillserver.sh | bash + + - name: Publish skills + if: github.event_name == 'push' + env: + SKILLSERVER_URL: ${{ secrets.SKILLSERVER_URL }} + SKILLSERVER_API_KEY: ${{ secrets.SKILLSERVER_API_KEY }} + run: skillserver publish-all ./skills + + - name: Validate skills (PRs only) + if: github.event_name == 'pull_request' + env: + SKILLSERVER_URL: ${{ secrets.SKILLSERVER_URL }} + SKILLSERVER_API_KEY: ${{ secrets.SKILLSERVER_API_KEY }} + run: skillserver publish-all ./skills --dry-run +``` + +PR builds validate with `--dry-run`. Pushes to master publish for real. Existing versions return 409 and are skipped — only new or bumped versions actually upload. + +## Connecting netclaw instances + +Add a skill server as a feed source: + +```bash +netclaw skill feed add my-server --url http://skills.internal.example.com/manifest.json +``` + +The daemon syncs on a periodic interval. Skills land in `~/.netclaw/skills/.server-feeds/` (read-only). See [Skill Feeds](/skills/skill-feeds/) for sync intervals, authentication, and selective sync options. + +## API reference + +### Discovery (no auth) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/.well-known/agent-skills/index.json` | RFC-compliant skill discovery index | +| GET | `/manifest.json` | NetClaw-compatible manifest | +| GET | `/health` | Health check | + +### Skills (reads open, writes require auth) + +| Method | Path | Description | +|--------|------|-------------| +| GET | `/skills` | List all (`?q=`, `?skip=`, `?take=` supported) | +| GET | `/skills/{name}` | All versions of a skill | +| GET | `/skills/{name}/latest` | Latest version metadata | +| GET | `/skills/{name}/{version}` | Specific version metadata | +| GET | `/skills/{name}/{version}/SKILL.md` | Download skill content | +| GET | `/skills/{name}/{version}/{*path}` | Download resource files | +| POST | `/skills/check-updates` | Batch update check (up to 100 skills) | +| POST | `/skills` | Upload new version (multipart/form-data) | +| DELETE | `/skills/{name}/{version}` | Delete a version | + +### API keys (all require auth) + +| Method | Path | Description | +|--------|------|-------------| +| POST | `/api-keys` | Create a key (returns raw key once) | +| GET | `/api-keys` | List keys (hashed, never shows raw) | +| DELETE | `/api-keys/{id}` | Revoke a key | + +## Related pages + +- [Skills Overview](/skills/overview/) — format, lifecycle, source types +- [Skill Feeds](/skills/skill-feeds/) — configuring netclaw to consume from skill servers +- [External Skills](/skills/external-skills/) — local skill directories +- [Security Model](/security/security-model/) — content scanning and trust model + +## External resources + +- [AgentSkills.io](https://agentskills.io) — the SKILL.md format spec +- [Cloudflare Agent Skills Discovery RFC](https://github.com/cloudflare/agent-skills-spec) — the discovery protocol +- [netclaw-dev/skill-server on GitHub](https://github.com/netclaw-dev/skill-server) — source, issues, releases +- [Tailscale GitHub Action](https://github.com/tailscale/github-action) — CI/CD access to private network servers