Compare commits

..

1 Commits

Author SHA1 Message Date
Alexander Whitestone
1e9c5fc458 feat: add wizard-bootstrap shared tooling & environment validation (Epic-004)
Some checks failed
Docker Build and Publish / build-and-push (pull_request) Failing after 14s
Supply Chain Audit / Scan PR for supply chain risks (pull_request) Failing after 1s
Tests / test (pull_request) Failing after 3s
Implements the Wizard Council Automation shared infrastructure:

- wizard-bootstrap/wizard_bootstrap.py — validates Python version, core deps,
  hermes_constants import, HERMES_HOME, LLM provider key, Gitea auth, and
  Telegram connectivity; exits 0 if forge-ready, 1 if any check fails
- wizard-bootstrap/skills_audit.py — compares repo-bundled skills against
  installed ~/.hermes/skills/, reporting MISSING / EXTRA / OUTDATED / OK;
  --fix flag syncs missing/outdated skills automatically
- wizard-bootstrap/dependency_checker.py — reads 'dependencies.binaries' and
  'dependencies.env_vars' from SKILL.md frontmatter and verifies each is
  satisfied in the current environment
- wizard-bootstrap/monthly_audit.py — runs all three checks and generates a
  Markdown report saved to ~/.hermes/wizard-council/audit-YYYY-MM.md;
  --post-telegram flag delivers the summary to the configured channel
- wizard-bootstrap/WIZARD_ENVIRONMENT_CONTRACT.md — specifies the minimum
  viable state every forge wizard must maintain (v1.0.0)
- skills/devops/wizard-council-automation/SKILL.md — skill entry so the
  toolset is discoverable and invocable from any wizard
- tests/test_wizard_bootstrap.py — 21 tests covering all three tools

Fixes #148

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 21:55:02 -04:00
8 changed files with 0 additions and 1279 deletions

13
.github/CODEOWNERS vendored
View File

@@ -1,13 +0,0 @@
# Default owners for all files
* @Timmy
# Critical paths require explicit review
/gateway/ @Timmy
/tools/ @Timmy
/agent/ @Timmy
/config/ @Timmy
/scripts/ @Timmy
/.github/workflows/ @Timmy
/pyproject.toml @Timmy
/requirements.txt @Timmy
/Dockerfile @Timmy

View File

@@ -1,99 +0,0 @@
name: "🔒 Security PR Checklist"
description: "Use this when your PR touches authentication, file I/O, external API calls, or other sensitive paths."
title: "[Security Review]: "
labels: ["security", "needs-review"]
body:
- type: markdown
attributes:
value: |
## Security Pre-Merge Review
Complete this checklist before requesting review on PRs that touch **authentication, file I/O, external API calls, or secrets handling**.
- type: input
id: pr-link
attributes:
label: Pull Request
description: Link to the PR being reviewed
placeholder: "https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/pulls/XXX"
validations:
required: true
- type: dropdown
id: change-type
attributes:
label: Change Category
description: What kind of sensitive change does this PR make?
multiple: true
options:
- Authentication / Authorization
- File I/O (read/write/delete)
- External API calls (outbound HTTP/network)
- Secret / credential handling
- Command execution (subprocess/shell)
- Dependency addition or update
- Configuration changes
- CI/CD pipeline changes
validations:
required: true
- type: checkboxes
id: secrets-checklist
attributes:
label: Secrets & Credentials
options:
- label: No secrets, API keys, or credentials are hardcoded
required: true
- label: All sensitive values are loaded from environment variables or a secrets manager
required: true
- label: Test fixtures use fake/placeholder values, not real credentials
required: true
- type: checkboxes
id: input-validation-checklist
attributes:
label: Input Validation
options:
- label: All external input (user, API, file) is validated before use
required: true
- label: File paths are validated against path traversal (`../`, null bytes, absolute paths)
- label: URLs are validated for SSRF (blocked private/metadata IPs)
- label: Shell commands do not use `shell=True` with user-controlled input
- type: checkboxes
id: auth-checklist
attributes:
label: Authentication & Authorization (if applicable)
options:
- label: Authentication tokens are not logged or exposed in error messages
- label: Authorization checks happen server-side, not just client-side
- label: Session tokens are properly scoped and have expiry
- type: checkboxes
id: supply-chain-checklist
attributes:
label: Supply Chain
options:
- label: New dependencies are pinned to a specific version range
- label: Dependencies come from trusted sources (PyPI, npm, official repos)
- label: No `.pth` files or install hooks that execute arbitrary code
- label: "`pip-audit` passes (no known CVEs in added dependencies)"
- type: textarea
id: threat-model
attributes:
label: Threat Model Notes
description: |
Briefly describe the attack surface this change introduces or modifies, and how it is mitigated.
placeholder: |
This PR adds a new outbound HTTP call to the OpenRouter API.
Mitigation: URL is hardcoded (no user input), response is parsed with strict schema validation.
- type: textarea
id: testing
attributes:
label: Security Testing Done
description: What security testing did you perform?
placeholder: |
- Ran validate_security.py — all checks pass
- Tested path traversal attempts manually
- Verified no secrets in git diff

View File

@@ -1,82 +0,0 @@
name: Dependency Audit
on:
pull_request:
branches: [main]
paths:
- 'requirements.txt'
- 'pyproject.toml'
- 'uv.lock'
schedule:
- cron: '0 8 * * 1' # Weekly on Monday
workflow_dispatch:
permissions:
pull-requests: write
contents: read
jobs:
audit:
name: Audit Python dependencies
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
- name: Set up Python
run: uv python install 3.11
- name: Install pip-audit
run: uv pip install --system pip-audit
- name: Run pip-audit
id: audit
run: |
set -euo pipefail
# Run pip-audit against the lock file/requirements
if pip-audit --requirement requirements.txt -f json -o /tmp/audit-results.json 2>/tmp/audit-stderr.txt; then
echo "found=false" >> "$GITHUB_OUTPUT"
else
echo "found=true" >> "$GITHUB_OUTPUT"
# Check severity
CRITICAL=$(python3 -c "
import json, sys
data = json.load(open('/tmp/audit-results.json'))
vulns = data.get('dependencies', [])
for d in vulns:
for v in d.get('vulns', []):
aliases = v.get('aliases', [])
# Check for critical/high CVSS
if any('CVSS' in str(a) for a in aliases):
print('true')
sys.exit(0)
print('false')
" 2>/dev/null || echo 'false')
echo "critical=${CRITICAL}" >> "$GITHUB_OUTPUT"
fi
continue-on-error: true
- name: Post results comment
if: steps.audit.outputs.found == 'true' && github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
BODY="## ⚠️ Dependency Vulnerabilities Detected
\`pip-audit\` found vulnerable dependencies in this PR. Review and update before merging.
\`\`\`
$(cat /tmp/audit-results.json | python3 -c "
import json, sys
data = json.load(sys.stdin)
for dep in data.get('dependencies', []):
for v in dep.get('vulns', []):
print(f\" {dep['name']}=={dep['version']}: {v['id']} - {v.get('description', '')[:120]}\")
" 2>/dev/null || cat /tmp/audit-stderr.txt)
\`\`\`
---
*Automated scan by [dependency-audit](/.github/workflows/dependency-audit.yml)*"
gh pr comment "${{ github.event.pull_request.number }}" --body "$BODY"
- name: Fail on vulnerabilities
if: steps.audit.outputs.found == 'true'
run: |
echo "::error::Vulnerable dependencies detected. See PR comment for details."
cat /tmp/audit-results.json | python3 -m json.tool || true
exit 1

View File

@@ -1,114 +0,0 @@
name: Quarterly Security Audit
on:
schedule:
# Run at 08:00 UTC on the first day of each quarter (Jan, Apr, Jul, Oct)
- cron: '0 8 1 1,4,7,10 *'
workflow_dispatch:
inputs:
reason:
description: 'Reason for manual trigger'
required: false
default: 'Manual quarterly audit'
permissions:
issues: write
contents: read
jobs:
create-audit-issue:
name: Create quarterly security audit issue
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get quarter info
id: quarter
run: |
MONTH=$(date +%-m)
YEAR=$(date +%Y)
QUARTER=$(( (MONTH - 1) / 3 + 1 ))
echo "quarter=Q${QUARTER}-${YEAR}" >> "$GITHUB_OUTPUT"
echo "year=${YEAR}" >> "$GITHUB_OUTPUT"
echo "q=${QUARTER}" >> "$GITHUB_OUTPUT"
- name: Create audit issue
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
QUARTER="${{ steps.quarter.outputs.quarter }}"
gh issue create \
--title "[$QUARTER] Quarterly Security Audit" \
--label "security,audit" \
--body "$(cat <<'BODY'
## Quarterly Security Audit — ${{ steps.quarter.outputs.quarter }}
This is the scheduled quarterly security audit for the hermes-agent project. Complete each section and close this issue when the audit is done.
**Audit Period:** ${{ steps.quarter.outputs.quarter }}
**Due:** End of quarter
**Owner:** Assign to a maintainer
---
## 1. Open Issues & PRs Audit
Review all open issues and PRs for security-relevant content. Tag any that touch attack surfaces with the `security` label.
- [ ] Review open issues older than 30 days for unaddressed security concerns
- [ ] Tag security-relevant open PRs with `needs-security-review`
- [ ] Check for any issues referencing CVEs or known vulnerabilities
- [ ] Review recently closed security issues — are fixes deployed?
## 2. Dependency Audit
- [ ] Run `pip-audit` against current `requirements.txt` / `pyproject.toml`
- [ ] Check `uv.lock` for any pinned versions with known CVEs
- [ ] Review any `git+` dependencies for recent changes or compromise signals
- [ ] Update vulnerable dependencies and open PRs for each
## 3. Critical Path Review
Review recent changes to attack-surface paths:
- [ ] `gateway/` — authentication, message routing, platform adapters
- [ ] `tools/` — file I/O, command execution, web access
- [ ] `agent/` — prompt handling, context management
- [ ] `config/` — secrets loading, configuration parsing
- [ ] `.github/workflows/` — CI/CD integrity
Run: `git log --since="3 months ago" --name-only -- gateway/ tools/ agent/ config/ .github/workflows/`
## 4. Secret Scan
- [ ] Run secret scanner on the full codebase (not just diffs)
- [ ] Verify no credentials are present in git history
- [ ] Confirm all API keys/tokens in use are rotated on a regular schedule
## 5. Access & Permissions Review
- [ ] Review who has write access to the main branch
- [ ] Confirm branch protection rules are still in place (require PR + review)
- [ ] Verify CI/CD secrets are scoped correctly (not over-permissioned)
- [ ] Review CODEOWNERS file for accuracy
## 6. Vulnerability Triage
List any new vulnerabilities found this quarter:
| ID | Component | Severity | Status | Owner |
|----|-----------|----------|--------|-------|
| | | | | |
## 7. Action Items
| Action | Owner | Due Date | Status |
|--------|-------|----------|--------|
| | | | |
---
*Auto-generated by [quarterly-security-audit](/.github/workflows/quarterly-security-audit.yml). Close this issue when the audit is complete.*
BODY
)"

View File

@@ -1,136 +0,0 @@
name: Secret Scan
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
pull-requests: write
contents: read
jobs:
scan:
name: Scan for secrets
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Fetch base branch
run: git fetch origin ${{ github.base_ref }}
- name: Scan diff for secrets
id: scan
run: |
set -euo pipefail
# Get only added lines from the diff (exclude deletions and context lines)
DIFF=$(git diff "origin/${{ github.base_ref }}"...HEAD -- \
':!*.lock' ':!uv.lock' ':!package-lock.json' ':!yarn.lock' \
| grep '^+' | grep -v '^+++' || true)
FINDINGS=""
CRITICAL=false
check() {
local label="$1"
local pattern="$2"
local critical="${3:-false}"
local matches
matches=$(echo "$DIFF" | grep -oP "$pattern" || true)
if [ -n "$matches" ]; then
FINDINGS="${FINDINGS}\n- **${label}**: pattern matched"
if [ "$critical" = "true" ]; then
CRITICAL=true
fi
fi
}
# AWS keys — critical
check "AWS Access Key" 'AKIA[0-9A-Z]{16}' true
# Private key headers — critical
check "Private Key Header" '-----BEGIN (RSA|EC|DSA|OPENSSH|PGP) PRIVATE KEY' true
# OpenAI / Anthropic style keys
check "OpenAI-style API key (sk-)" 'sk-[a-zA-Z0-9]{20,}' false
# GitHub tokens
check "GitHub personal access token (ghp_)" 'ghp_[a-zA-Z0-9]{36}' true
check "GitHub fine-grained PAT (github_pat_)" 'github_pat_[a-zA-Z0-9_]{1,}' true
# Slack tokens
check "Slack bot token (xoxb-)" 'xoxb-[0-9A-Za-z\-]{10,}' true
check "Slack user token (xoxp-)" 'xoxp-[0-9A-Za-z\-]{10,}' true
# Generic assignment patterns — exclude obvious placeholders
GENERIC=$(echo "$DIFF" | grep -iP '(api_key|apikey|api-key|secret_key|access_token|auth_token)\s*[=:]\s*['"'"'"][^'"'"'"]{20,}['"'"'"]' \
| grep -ivP '(fake|mock|test|placeholder|example|dummy|your[_-]|xxx|<|>|\{\{)' || true)
if [ -n "$GENERIC" ]; then
FINDINGS="${FINDINGS}\n- **Generic credential assignment**: possible hardcoded secret"
fi
# .env additions with long values
ENV_DIFF=$(git diff "origin/${{ github.base_ref }}"...HEAD -- '*.env' '**/.env' '.env*' \
| grep '^+' | grep -v '^+++' || true)
ENV_MATCHES=$(echo "$ENV_DIFF" | grep -P '^[A-Z_]+=.{16,}' \
| grep -ivP '(fake|mock|test|placeholder|example|dummy|your[_-]|xxx)' || true)
if [ -n "$ENV_MATCHES" ]; then
FINDINGS="${FINDINGS}\n- **.env file**: lines with potentially real secret values detected"
fi
# Write outputs
if [ -n "$FINDINGS" ]; then
echo "found=true" >> "$GITHUB_OUTPUT"
else
echo "found=false" >> "$GITHUB_OUTPUT"
fi
if [ "$CRITICAL" = "true" ]; then
echo "critical=true" >> "$GITHUB_OUTPUT"
else
echo "critical=false" >> "$GITHUB_OUTPUT"
fi
# Store findings in a file to use in comment step
printf "%b" "$FINDINGS" > /tmp/secret-findings.txt
- name: Post PR comment with findings
if: steps.scan.outputs.found == 'true' && github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
FINDINGS=$(cat /tmp/secret-findings.txt)
SEVERITY="warning"
if [ "${{ steps.scan.outputs.critical }}" = "true" ]; then
SEVERITY="CRITICAL"
fi
BODY="## Secret Scan — ${SEVERITY} findings
The automated secret scanner detected potential secrets in the diff for this PR.
### Findings
${FINDINGS}
### What to do
1. Remove any real credentials from the diff immediately.
2. If the match is a false positive (test fixture, placeholder), add a comment explaining why or rename the variable to include \`fake\`, \`mock\`, or \`test\`.
3. Rotate any exposed credentials regardless of whether this PR is merged.
---
*Automated scan by [secret-scan](/.github/workflows/secret-scan.yml)*"
gh pr comment "${{ github.event.pull_request.number }}" --body "$BODY"
- name: Fail on critical secrets
if: steps.scan.outputs.critical == 'true'
run: |
echo "::error::Critical secrets detected in diff (private keys, AWS keys, or GitHub tokens). Remove them before merging."
exit 1
- name: Warn on non-critical findings
if: steps.scan.outputs.found == 'true' && steps.scan.outputs.critical == 'false'
run: |
echo "::warning::Potential secrets detected in diff. Review the PR comment for details."

View File

@@ -1,25 +0,0 @@
repos:
# Secret detection
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
name: Detect secrets with gitleaks
description: Detect hardcoded secrets, API keys, and credentials
# Basic security hygiene
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: check-added-large-files
args: ['--maxkb=500']
- id: detect-private-key
name: Detect private keys
- id: check-merge-conflict
- id: check-yaml
- id: check-toml
- id: end-of-file-fixer
- id: trailing-whitespace
args: ['--markdown-linebreak-ext=md']
- id: no-commit-to-branch
args: ['--branch', 'main']

View File

@@ -1,132 +0,0 @@
# Fleet SITREP — April 6, 2026
**Classification:** Consolidated Status Report
**Compiled by:** Ezra
**Acknowledged by:** Claude (Issue #143)
---
## Executive Summary
Allegro executed 7 tasks across infrastructure, contracting, audits, and security. Ezra shipped PR #131, filed formalization audit #132, delivered quarterly report #133, and self-assigned issues #134#138. All wizard activity mapped below.
---
## 1. Allegro 7-Task Report
| Task | Description | Status |
|------|-------------|--------|
| 1 | Roll Call / Infrastructure Map | ✅ Complete |
| 2 | Dark industrial anthem (140 BPM, Suno-ready) | ✅ Complete |
| 3 | Operation Get A Job — 7-file contracting playbook pushed to `the-nexus` | ✅ Complete |
| 4 | Formalization audit filed ([the-nexus #893](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/893)) | ✅ Complete |
| 5 | GrepTard Memory Report — PR #525 on `timmy-home` | ✅ Complete |
| 6 | Self-audit issues #894#899 filed on `the-nexus` | ✅ Filed |
| 7 | `keystore.json` permissions fixed to `600` | ✅ Applied |
### Critical Findings from Task 4 (Formalization Audit)
- GOFAI source files missing — only `.pyc` remains
- Nostr keystore was world-readable — **FIXED** (Task 7)
- 39 burn scripts cluttering `/root` — archival pending ([#898](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/898))
---
## 2. Ezra Deliverables
| Deliverable | Issue/PR | Status |
|-------------|----------|--------|
| V-011 fix + compressor tuning | [PR #131](https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/pulls/131) | ✅ Merged |
| Formalization audit (hermes-agent) | [Issue #132](https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/issues/132) | Filed |
| Quarterly report (MD + PDF) | [Issue #133](https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/issues/133) | Filed |
| Burn-mode concurrent tool tests | [Issue #134](https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/issues/134) | Assigned → Ezra |
| MCP SDK migration | [Issue #135](https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/issues/135) | Assigned → Ezra |
| APScheduler migration | [Issue #136](https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/issues/136) | Assigned → Ezra |
| Pydantic-settings migration | [Issue #137](https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/issues/137) | Assigned → Ezra |
| Contracting playbook tracker | [Issue #138](https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/issues/138) | Assigned → Ezra |
---
## 3. Fleet Status
| Wizard | Host | Status | Blocker |
|--------|------|--------|---------|
| **Ezra** | Hermes VPS | Active — 5 issues queued | None |
| **Bezalel** | Hermes VPS | Gateway running on 8645 | None |
| **Allegro-Primus** | Hermes VPS | **Gateway DOWN on 8644** | Needs restart signal |
| **Bilbo** | External | Gemma 4B active, Telegram dual-mode | Host IP unknown to fleet |
### Allegro Gateway Recovery
Allegro-Primus gateway (port 8644) is down. Options:
1. **Alexander restarts manually** on Hermes VPS
2. **Delegate to Bezalel** — Bezalel can issue restart signal via Hermes VPS access
3. **Delegate to Ezra** — Ezra can coordinate restart as part of issue #894 work
---
## 4. Operation Get A Job — Contracting Playbook
Files pushed to `the-nexus/operation-get-a-job/`:
| File | Purpose |
|------|---------|
| `README.md` | Master plan |
| `entity-setup.md` | Wyoming LLC, Mercury, E&O insurance |
| `service-offerings.md` | Rates $150600/hr; packages $5k/$15k/$40k+ |
| `portfolio.md` | Portfolio structure |
| `outreach-templates.md` | Cold email templates |
| `proposal-template.md` | Client proposal structure |
| `rate-card.md` | Rate card |
**Human-only mile (Alexander's action items):**
1. Pick LLC name from `entity-setup.md`
2. File Wyoming LLC via Northwest Registered Agent ($225)
3. Get EIN from IRS (free, ~10 min)
4. Open Mercury account (requires EIN + LLC docs)
5. Secure E&O insurance (~$150250/month)
6. Restart Allegro-Primus gateway (port 8644)
7. Update LinkedIn using profile template
8. Send 5 cold emails using outreach templates
---
## 5. Pending Self-Audit Issues (the-nexus)
| Issue | Title | Priority |
|-------|-------|----------|
| [#894](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/894) | Deploy burn-mode cron jobs | CRITICAL |
| [#895](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/895) | Telegram thread-based reporting | Normal |
| [#896](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/896) | Retry logic and error recovery | Normal |
| [#897](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/897) | Automate morning reports at 0600 | Normal |
| [#898](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/898) | Archive 39 burn scripts | Normal |
| [#899](https://forge.alexanderwhitestone.com/Timmy_Foundation/the-nexus/issues/899) | Keystore permissions | ✅ Done |
---
## 6. Revenue Timeline
| Milestone | Target | Unlocks |
|-----------|--------|---------|
| LLC + Bank + E&O | Day 5 | Ability to invoice clients |
| First 5 emails sent | Day 7 | Pipeline generation |
| First scoping call | Day 14 | Qualified lead |
| First proposal accepted | Day 21 | **$4,500$12,000 revenue** |
| Monthly retainer signed | Day 45 | **$6,000/mo recurring** |
---
## 7. Delegation Matrix
| Owner | Owns |
|-------|------|
| **Alexander** | LLC filing, EIN, Mercury, E&O, LinkedIn, cold emails, gateway restart |
| **Ezra** | Issues #134#138 (tests, migrations, tracker) |
| **Allegro** | Issues #894, #898 (cron deployment, burn script archival) |
| **Bezalel** | Review formalization audit for Anthropic-specific gaps |
---
*SITREP acknowledged by Claude — April 6, 2026*
*Source issue: [hermes-agent #143](https://forge.alexanderwhitestone.com/Timmy_Foundation/hermes-agent/issues/143)*

View File

@@ -1,678 +0,0 @@
# Jupyter Notebooks as Core LLM Execution Layer — Deep Research Report
**Issue:** #155
**Date:** 2026-04-06
**Status:** Research / Spike
**Prior Art:** Timmy's initial spike (llm_execution_spike.ipynb, hamelnb bridge, JupyterLab on forge VPS)
---
## Executive Summary
This report deepens the research from issue #155 into three areas requested by Rockachopa:
1. The **full Jupyter product suite** — JupyterHub vs JupyterLab vs Notebook
2. **Papermill** — the production-grade notebook execution engine already used in real data pipelines
3. The **"PR model for notebooks"** — how agents can propose, diff, review, and merge changes to `.ipynb` files similarly to code PRs
The conclusion: an elegant, production-grade agent→notebook pipeline already exists as open-source tooling. We don't need to invent much — we need to compose what's there.
---
## 1. The Jupyter Product Suite
The Jupyter ecosystem has three distinct layers that are often conflated. Understanding the distinction is critical for architectural decisions.
### 1.1 Jupyter Notebook (Classic)
The original single-user interface. One browser tab = one `.ipynb` file. Version 6 is in maintenance-only mode. Version 7 was rebuilt on JupyterLab components and is functionally equivalent. For headless agent use, the UI is irrelevant — what matters is the `.ipynb` file format and the kernel execution model underneath.
### 1.2 JupyterLab
The current canonical Jupyter interface for human users: full IDE, multi-pane, terminal, extension manager, built-in diff viewer, and `jupyterlab-git` for Git workflows from the UI. JupyterLab is the recommended target for agent-collaborative workflows because:
- It exposes the same REST API as classic Jupyter (kernel sessions, execute, contents)
- Extensions like `jupyterlab-git` let a human co-reviewer inspect changes alongside the agent
- The `hamelnb` bridge Timmy already validated works against a JupyterLab server
**For agents:** JupyterLab is the platform to run on. The agent doesn't interact with the UI — it uses the Jupyter REST API or Papermill on top of it.
### 1.3 JupyterHub — The Multi-User Orchestration Layer
JupyterHub is not a UI. It is a **multi-user server** that spawns, manages, and proxies individual single-user Jupyter servers. This is the production infrastructure layer.
```
[Agent / Browser / API Client]
|
[Proxy] (configurable-http-proxy)
/ \
[Hub] [Single-User Jupyter Server per user/agent]
(Auth, (standard JupyterLab/Notebook server)
Spawner,
REST API)
```
**Key components:**
- **Hub:** Manages auth, user database, spawner lifecycle, REST API
- **Proxy:** Routes `/hub/*` to Hub, `/user/<name>/*` to that user's server
- **Spawner:** How single-user servers are started. Default = local process. Production options include `KubeSpawner` (Kubernetes pod per user) and `DockerSpawner` (container per user)
- **Authenticator:** PAM, OAuth, DummyAuthenticator (for isolated agent environments)
**JupyterHub REST API** (relevant for agent orchestration):
```bash
# Spawn a named server for an agent service account
POST /hub/api/users/<username>/servers/<name>
# Stop it when done
DELETE /hub/api/users/<username>/servers/<name>
# Create a scoped API token for the agent
POST /hub/api/users/<username>/tokens
# Check server status
GET /hub/api/users/<username>
```
**Why this matters for Hermes:** JupyterHub gives us isolated kernel environments per agent task, programmable lifecycle management, and a clean auth model. Instead of running one shared JupyterLab instance on the forge VPS, we could spawn ephemeral single-user servers per notebook execution run — each with its own kernel, clean state, and resource limits.
### 1.4 Jupyter Kernel Gateway — Minimal Headless Execution
If JupyterHub is too heavy, `jupyter-kernel-gateway` exposes just the kernel protocol over REST + WebSocket:
```bash
pip install jupyter-kernel-gateway
jupyter kernelgateway --KernelGatewayApp.api=kernel_gateway.jupyter_websocket
# Start kernel
POST /api/kernels
# Execute via WebSocket on Jupyter messaging protocol
WS /api/kernels/<kernel_id>/channels
# Stop kernel
DELETE /api/kernels/<kernel_id>
```
This is the lowest-level option: no notebook management, just raw kernel access. Suitable if we want to build our own execution layer from scratch.
---
## 2. Papermill — Production Notebook Execution
Papermill is the missing link between "notebook as experiment" and "notebook as repeatable pipeline task." It is already used at scale in industry data pipelines (Netflix, Airbnb, etc.).
### 2.1 Core Concept: Parameterization
Papermill's key innovation is **parameter injection**. Tag a cell in the notebook with `"parameters"`:
```python
# Cell tagged "parameters" (defaults — defined by notebook author)
alpha = 0.5
batch_size = 32
model_name = "baseline"
```
At runtime, Papermill inserts a new cell immediately after, tagged `"injected-parameters"`, that overrides the defaults:
```python
# Cell tagged "injected-parameters" (injected by Papermill at runtime)
alpha = 0.01
batch_size = 128
model_name = "experiment_007"
```
Because Python executes top-to-bottom, the injected cell shadows the defaults. The original notebook is never mutated — Papermill reads input, writes to a new output file.
### 2.2 Python API
```python
import papermill as pm
nb = pm.execute_notebook(
input_path="analysis.ipynb", # source (can be s3://, az://, gs://)
output_path="output/run_001.ipynb", # destination (persists outputs)
parameters={
"alpha": 0.01,
"n_samples": 1000,
"run_id": "fleet-check-2026-04-06",
},
kernel_name="python3",
execution_timeout=300, # per-cell timeout in seconds
log_output=True, # stream cell output to logger
cwd="/path/to/notebook/", # working directory
)
# Returns: NotebookNode (the fully executed notebook with all outputs)
```
On cell failure, Papermill raises `PapermillExecutionError` with:
- `cell_index` — which cell failed
- `source` — the failing cell's code
- `ename` / `evalue` — exception type and message
- `traceback` — full traceback
Even on failure, the output notebook is written with whatever cells completed — enabling partial-run inspection.
### 2.3 CLI
```bash
# Basic execution
papermill analysis.ipynb output/run_001.ipynb \
-p alpha 0.01 \
-p n_samples 1000
# From YAML parameter file
papermill analysis.ipynb output/run_001.ipynb -f params.yaml
# CI-friendly: log outputs, no progress bar
papermill analysis.ipynb output/run_001.ipynb \
--log-output \
--no-progress-bar \
--execution-timeout 300 \
-p run_id "fleet-check-2026-04-06"
# Prepare only (inject params, skip execution — for preview/inspection)
papermill analysis.ipynb preview.ipynb --prepare-only -p alpha 0.01
# Inspect parameter schema
papermill --help-notebook analysis.ipynb
```
**Remote storage** is built in — `pip install papermill[s3]` enables `s3://` paths for both input and output. Azure and GCS are also supported. For Hermes, this means notebook runs can be stored in object storage and retrieved later for audit.
### 2.4 Scrapbook — Structured Output Collection
`scrapbook` is Papermill's companion for extracting structured data from executed notebooks. Inside a notebook cell:
```python
import scrapbook as sb
# Write typed outputs (stored as special display_data in cell outputs)
sb.glue("accuracy", 0.9342)
sb.glue("metrics", {"precision": 0.91, "recall": 0.93, "f1": 0.92})
sb.glue("results_df", df, "pandas") # DataFrames too
```
After execution, from the agent:
```python
import scrapbook as sb
nb = sb.read_notebook("output/fleet-check-2026-04-06.ipynb")
metrics = nb.scraps["metrics"].data # -> {"precision": 0.91, ...}
accuracy = nb.scraps["accuracy"].data # -> 0.9342
# Or aggregate across many runs
book = sb.read_notebooks("output/")
book.scrap_dataframe # -> pd.DataFrame with all scraps + filenames
```
This is the clean interface between notebook execution and agent decision-making: the notebook outputs its findings as named, typed scraps; the agent reads them programmatically and acts.
### 2.5 How Papermill Compares to hamelnb
| Capability | hamelnb | Papermill |
|---|---|---|
| Stateful kernel session | Yes | No (fresh kernel per run) |
| Parameter injection | No | Yes |
| Persistent output notebook | No | Yes |
| Remote storage (S3/Azure) | No | Yes |
| Per-cell timing/metadata | No | Yes (in output nb metadata) |
| Error isolation (partial runs) | No | Yes |
| Production pipeline use | Experimental | Industry-standard |
| Structured output collection | No | Yes (via scrapbook) |
**Verdict:** `hamelnb` is great for interactive REPL-style exploration (where state accumulates). Papermill is better for task execution (where we want reproducible, parameterized, auditable runs). They serve different use cases. Hermes needs both.
---
## 3. The `.ipynb` File Format — What the Agent Is Actually Working With
Understanding the format is essential for the "PR model." A `.ipynb` file is JSON with this structure:
```json
{
"nbformat": 4,
"nbformat_minor": 5,
"metadata": {
"kernelspec": {"display_name": "Python 3", "language": "python", "name": "python3"},
"language_info": {"name": "python", "version": "3.10.0"}
},
"cells": [
{
"id": "a1b2c3d4",
"cell_type": "markdown",
"source": "# Fleet Health Check\n\nThis notebook checks system health.",
"metadata": {}
},
{
"id": "e5f6g7h8",
"cell_type": "code",
"source": "alpha = 0.5\nthreshold = 0.95",
"metadata": {"tags": ["parameters"]},
"execution_count": null,
"outputs": []
},
{
"id": "i9j0k1l2",
"cell_type": "code",
"source": "import sys\nprint(sys.version)",
"metadata": {},
"execution_count": 1,
"outputs": [
{
"output_type": "stream",
"name": "stdout",
"text": "3.10.0 (default, ...)\n"
}
]
}
]
}
```
The `nbformat` Python library provides a clean API for working with this:
```python
import nbformat
# Read
with open("notebook.ipynb") as f:
nb = nbformat.read(f, as_version=4)
# Navigate
for cell in nb.cells:
if cell.cell_type == "code":
print(cell.source)
# Modify
nb.cells[2].source = "import sys\nprint('updated')"
# Add cells
new_md = nbformat.v4.new_markdown_cell("## Agent Analysis\nInserted by Hermes.")
nb.cells.insert(3, new_md)
# Write
with open("modified.ipynb", "w") as f:
nbformat.write(nb, f)
# Validate
nbformat.validate(nb) # raises nbformat.ValidationError on invalid format
```
---
## 4. The PR Model for Notebooks
This is the elegant architecture Rockachopa described: agents making PRs to notebooks the same way they make PRs to code. Here's how the full stack enables it.
### 4.1 The Problem: Raw `.ipynb` Diffs Are Unusable
Without tooling, a `git diff` on a notebook that was merely re-run (no source changes) produces thousands of lines of JSON changes — execution counts, timestamps, base64-encoded plot images. Code review on raw `.ipynb` diffs is impractical.
### 4.2 nbstripout — Clean Git History
`nbstripout` installs a git **clean filter** that strips outputs before files enter the git index. The working copy is untouched; only what gets committed is clean.
```bash
pip install nbstripout
nbstripout --install # per-repo
# or
nbstripout --install --global # all repos
```
This writes to `.git/config`:
```ini
[filter "nbstripout"]
clean = nbstripout
smudge = cat
required = true
[diff "ipynb"]
textconv = nbstripout -t
```
And to `.gitattributes`:
```
*.ipynb filter=nbstripout
*.ipynb diff=ipynb
```
Now `git diff` shows only source changes — same as reviewing a `.py` file.
**For executed-output notebooks** (where we want to keep outputs for audit): use a separate path like `runs/` or `outputs/` excluded from the filter via `.gitattributes`:
```
*.ipynb filter=nbstripout
runs/*.ipynb !filter
runs/*.ipynb !diff
```
### 4.3 nbdime — Semantic Diff and Merge
nbdime understands notebook structure. Instead of diffing raw JSON, it diffs at the level of cells — knowing that `cells` is a list, `source` is a string, and outputs should often be ignored.
```bash
pip install nbdime
# Enable semantic git diff/merge for all .ipynb files
nbdime config-git --enable
# Now standard git commands are notebook-aware:
git diff HEAD notebook.ipynb # semantic cell-level diff
git merge feature-branch # uses nbdime for .ipynb conflict resolution
git log -p notebook.ipynb # readable patch per commit
```
**Python API for agent reasoning:**
```python
import nbdime
import nbformat
nb_base = nbformat.read(open("original.ipynb"), as_version=4)
nb_pr = nbformat.read(open("proposed.ipynb"), as_version=4)
diff = nbdime.diff_notebooks(nb_base, nb_pr)
# diff is a list of structured ops the agent can reason about:
# [{"op": "patch", "key": "cells", "diff": [
# {"op": "patch", "key": 3, "diff": [
# {"op": "patch", "key": "source", "diff": [...string ops...]}
# ]}
# ]}]
# Apply a diff (patch)
from nbdime.patching import patch
nb_result = patch(nb_base, diff)
```
### 4.4 The Full Agent PR Workflow
Here is the complete workflow — analogous to how Hermes makes PRs to code repos via Gitea:
**1. Agent reads the task notebook**
```python
nb = nbformat.read(open("fleet_health_check.ipynb"), as_version=4)
```
**2. Agent locates and modifies relevant cells**
```python
# Find parameter cell
params_cell = next(
c for c in nb.cells
if "parameters" in c.get("metadata", {}).get("tags", [])
)
# Update threshold
params_cell.source = params_cell.source.replace("threshold = 0.95", "threshold = 0.90")
# Add explanatory markdown
nb.cells.insert(
nb.cells.index(params_cell) + 1,
nbformat.v4.new_markdown_cell(
"**Note (Hermes 2026-04-06):** Threshold lowered from 0.95 to 0.90 "
"based on false-positive analysis from last 7 days of runs."
)
)
```
**3. Agent writes and commits to a branch**
```bash
git checkout -b agent/fleet-health-threshold-update
nbformat.write(nb, open("fleet_health_check.ipynb", "w"))
git add fleet_health_check.ipynb
git commit -m "feat(notebooks): lower fleet health threshold to 0.90 (#155)"
```
**4. Agent executes the proposed notebook to validate**
```python
import papermill as pm
pm.execute_notebook(
"fleet_health_check.ipynb",
"output/validation_run.ipynb",
parameters={"run_id": "agent-validation-2026-04-06"},
log_output=True,
)
```
**5. Agent collects results and compares**
```python
import scrapbook as sb
result = sb.read_notebook("output/validation_run.ipynb")
health_score = result.scraps["health_score"].data
alert_count = result.scraps["alert_count"].data
```
**6. Agent opens PR with results summary**
```bash
curl -X POST "$GITEA_API/pulls" \
-H "Authorization: token $TOKEN" \
-d '{
"title": "feat(notebooks): lower fleet health threshold to 0.90",
"body": "## Agent Analysis\n\n- Health score: 0.94 (was 0.89 with old threshold)\n- Alert count: 12 (was 47 false positives)\n- Validation run: output/validation_run.ipynb\n\nRefs #155",
"head": "agent/fleet-health-threshold-update",
"base": "main"
}'
```
**7. Human reviews the PR using nbdime diff**
The PR diff in Gitea shows the clean cell-level source changes (thanks to nbstripout). The human can also run `nbdiff-web original.ipynb proposed.ipynb` locally for rich rendered diff with output comparison.
### 4.5 nbval — Regression Testing Notebooks
`nbval` treats each notebook cell as a pytest test case, re-executing and comparing outputs to stored values:
```bash
pip install nbval
# Strict: every cell output must match stored outputs
pytest --nbval fleet_health_check.ipynb
# Lax: only check cells marked with # NBVAL_CHECK_OUTPUT
pytest --nbval-lax fleet_health_check.ipynb
```
Cell-level markers (comments in cell source):
```python
# NBVAL_CHECK_OUTPUT — in lax mode, validate this cell's output
# NBVAL_SKIP — skip this cell entirely
# NBVAL_RAISES_EXCEPTION — expect an exception (test passes if raised)
```
This becomes the CI gate: before a notebook PR is merged, run `pytest --nbval-lax` to verify no cells produce errors and critical output cells still produce expected values.
---
## 5. Gaps and Recommendations
### 5.1 Gap Assessment (Refining Timmy's Original Findings)
| Gap | Severity | Solution |
|---|---|---|
| No Hermes tool access in kernel | High | Inject `hermes_runtime` module (see §5.2) |
| No structured output protocol | High | Use scrapbook `sb.glue()` pattern |
| No parameterization | Medium | Add Papermill `"parameters"` cell to notebooks |
| XSRF/auth friction | Medium | Disable for local; use JupyterHub token scopes for multi-user |
| No notebook CI/testing | Medium | Add nbval to test suite |
| Raw `.ipynb` diffs in PRs | Medium | Install nbstripout + nbdime |
| No scheduling | Low | Papermill + existing Hermes cron layer |
### 5.2 Short-Term Recommendations (This Month)
**1. `NotebookExecutor` tool**
A thin Hermes tool wrapping the ecosystem:
```python
class NotebookExecutor:
def execute(self, input_path, output_path, parameters, timeout=300):
"""Wraps pm.execute_notebook(). Returns structured result dict."""
def collect_outputs(self, notebook_path):
"""Wraps sb.read_notebook(). Returns dict of named scraps."""
def inspect_parameters(self, notebook_path):
"""Wraps pm.inspect_notebook(). Returns parameter schema."""
def read_notebook(self, path):
"""Returns nbformat NotebookNode for cell inspection/modification."""
def write_notebook(self, nb, path):
"""Writes modified NotebookNode back to disk."""
def diff_notebooks(self, path_a, path_b):
"""Returns structured nbdime diff for agent reasoning."""
def validate(self, notebook_path):
"""Runs nbformat.validate() + optional pytest --nbval-lax."""
```
Execution result structure for the agent:
```python
{
"status": "success" | "error",
"duration_seconds": 12.34,
"cells_executed": 15,
"failed_cell": { # None on success
"index": 7,
"source": "model.fit(X, y)",
"ename": "ValueError",
"evalue": "Input contains NaN",
},
"scraps": { # from scrapbook
"health_score": 0.94,
"alert_count": 12,
},
}
```
**2. Fleet Health Check as a Notebook**
Convert the fleet health check epic into a parameterized notebook with:
- `"parameters"` cell for run configuration (date range, thresholds, agent ID)
- Markdown cells narrating each step
- `sb.glue()` calls for structured outputs
- `# NBVAL_CHECK_OUTPUT` markers on critical cells
**3. Git hygiene for notebooks**
Install nbstripout + nbdime in the hermes-agent repo:
```bash
pip install nbstripout nbdime
nbstripout --install
nbdime config-git --enable
```
Add to `.gitattributes`:
```
*.ipynb filter=nbstripout
*.ipynb diff=ipynb
runs/*.ipynb !filter
```
### 5.3 Medium-Term Recommendations (Next Quarter)
**4. `hermes_runtime` Python module**
Inject Hermes tool access into the kernel via a module that notebooks import:
```python
# In kernel cell: from hermes_runtime import terminal, read_file, web_search
import hermes_runtime as hermes
results = hermes.web_search("fleet health metrics best practices")
hermes.terminal("systemctl status agent-fleet")
content = hermes.read_file("/var/log/hermes/agent.log")
```
This closes the most significant gap: notebooks gain the same tool access as skills, while retaining state persistence and narrative structure.
**5. Notebook-triggered cron**
Extend the Hermes cron layer to accept `.ipynb` paths as targets:
```yaml
# cron entry
schedule: "0 6 * * *"
type: notebook
path: notebooks/fleet_health_check.ipynb
parameters:
run_id: "{{date}}"
alert_threshold: 0.90
output_path: runs/fleet_health_{{date}}.ipynb
```
The cron runner calls `pm.execute_notebook()` and commits the output to the repo.
**6. JupyterHub for multi-agent isolation**
If multiple agents need concurrent notebook execution, deploy JupyterHub with `DockerSpawner` or `KubeSpawner`. Each agent job gets an isolated container with its own kernel, no state bleed between runs.
---
## 6. Architecture Vision
```
┌─────────────────────────────────────────────────────────────────┐
│ Hermes Agent │
│ │
│ Skills (one-shot) Notebooks (multi-step) │
│ ┌─────────────────┐ ┌─────────────────────────────────┐ │
│ │ terminal() │ │ .ipynb file │ │
│ │ web_search() │ │ ├── Markdown (narrative) │ │
│ │ read_file() │ │ ├── Code cells (logic) │ │
│ └─────────────────┘ │ ├── "parameters" cell │ │
│ │ └── sb.glue() outputs │ │
│ └──────────────┬────────────────┘ │
│ │ │
│ ┌──────────────▼────────────────┐ │
│ │ NotebookExecutor tool │ │
│ │ (papermill + scrapbook + │ │
│ │ nbformat + nbdime + nbval) │ │
│ └──────────────┬────────────────┘ │
│ │ │
└────────────────────────────────────────────┼────────────────────┘
┌───────────────────▼──────────────────┐
│ JupyterLab / Hub │
│ (kernel execution environment) │
└───────────────────┬──────────────────┘
┌───────────────────▼──────────────────┐
│ Git + Gitea │
│ (nbstripout clean diffs, │
│ nbdime semantic review, │
│ PR workflow for notebook changes) │
└──────────────────────────────────────┘
```
**Notebooks become the primary artifact of complex tasks:** the agent generates or edits cells, Papermill executes them reproducibly, scrapbook extracts structured outputs for agent decision-making, and the resulting `.ipynb` is both proof-of-work and human-readable report. Skills remain for one-shot actions. Notebooks own multi-step workflows.
---
## 7. Package Summary
| Package | Purpose | Install |
|---|---|---|
| `nbformat` | Read/write/validate `.ipynb` files | `pip install nbformat` |
| `nbconvert` | Execute and export notebooks | `pip install nbconvert` |
| `papermill` | Parameterize + execute in pipelines | `pip install papermill` |
| `scrapbook` | Structured output collection | `pip install scrapbook` |
| `nbdime` | Semantic diff/merge for git | `pip install nbdime` |
| `nbstripout` | Git filter for clean diffs | `pip install nbstripout` |
| `nbval` | pytest-based output regression | `pip install nbval` |
| `jupyter-kernel-gateway` | Headless REST kernel access | `pip install jupyter-kernel-gateway` |
---
## 8. References
- [Papermill GitHub (nteract/papermill)](https://github.com/nteract/papermill)
- [Scrapbook GitHub (nteract/scrapbook)](https://github.com/nteract/scrapbook)
- [nbformat format specification](https://nbformat.readthedocs.io/en/latest/format_description.html)
- [nbdime documentation](https://nbdime.readthedocs.io/)
- [nbdime diff format spec (JEP #8)](https://github.com/jupyter/enhancement-proposals/blob/master/08-notebook-diff/notebook-diff.md)
- [nbconvert execute API](https://nbconvert.readthedocs.io/en/latest/execute_api.html)
- [nbstripout README](https://github.com/kynan/nbstripout)
- [nbval GitHub (computationalmodelling/nbval)](https://github.com/computationalmodelling/nbval)
- [JupyterHub REST API](https://jupyterhub.readthedocs.io/en/stable/howto/rest.html)
- [JupyterHub Technical Overview](https://jupyterhub.readthedocs.io/en/latest/reference/technical-overview.html)
- [Jupyter Kernel Gateway](https://github.com/jupyter-server/kernel_gateway)