diff --git a/README.md b/README.md index c86ddcf..292db97 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,58 @@ npm install predicate-claw **Right pane:** The integration demo using the real `createSecureClawPlugin()` SDK—legitimate file reads succeed, while sensitive file access, dangerous shell commands, and prompt injection attacks are blocked before execution. -### Real Claude Code Integration +### Zero-Trust AI Agent Playground +#### Complete Agent Loop: Pre-execution authorization + Post-execution deterministic verification + +![Zero-Trust Agent Demo](docs/images/openclaw_complete_loop_demo_s.gif) + +The **Market Research Agent** demo showcases the complete **Zero-Trust architecture**: + +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ZERO-TRUST AI AGENT ARCHITECTURE │ +│ │ +│ ┌───────────────┐ ┌─────────────────┐ ┌───────────────────────┐ │ +│ │ LLM/Agent │───▶│ PRE-EXECUTION │───▶│ POST-EXECUTION │ │ +│ │ (Claude) │ │ GATE │ │ VERIFICATION │ │ +│ └───────────────┘ │ (Sidecar) │ │ (SDK Predicates) │ │ +│ │ ALLOW / DENY │ │ PASS / FAIL │ │ +│ └─────────────────┘ └───────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` + +- **Pre-Execution Gate:** Policy-based authorization before any action executes +- **Post-Execution Verification:** Deterministic predicates verify state after execution +- **Cloud Tracing:** Full observability with screenshots in [Predicate Studio](https://www.predicatesystems.ai/studio) + +```bash +cd examples/real-openclaw-demo +export ANTHROPIC_API_KEY="sk-ant-..." +./run-playground.sh +``` + +See [Zero-Trust Agent Demo](examples/real-openclaw-demo/README.md) for full instructions. + +### Token-Saving Snapshot Skill + +The `predicate-snapshot` skill is a **game-changer for token efficiency**. Instead of sending full page HTML or full accessbility tree (A11y) to the LLM (tens of thousands of tokens), it captures structured DOM snapshots with only actionable elements: + +```typescript +// Traditional approach: 50,000+ tokens of raw HTML +const html = await page.content(); + +// With predicate-snapshot: ~500 tokens of structured data +const snapshot = await agentRuntime.snapshot({ + screenshot: { format: "jpeg", quality: 80 }, + use_api: true, + limit: 50, // Top 50 interactive elements +}); +// Returns: { elements: [...], text: "...", screenshot: "base64..." } +``` + +**Token savings: 90-99%** while maintaining all information the LLM needs to act. + +### Legacy Claude Code Integration We also provide a **real Claude Code demo** that uses actual Anthropic API calls with SecureClaw hooks intercepting every tool call. See the [Real OpenClaw Demo](examples/real-openclaw-demo/README.md) for instructions. @@ -348,6 +399,7 @@ However, when deploying a fleet of AI agents in regulated environments (FinTech, | Project | Description | |---------|-------------| +| [@predicatesystems/runtime](https://www.npmjs.com/package/@predicatesystems/runtime) | Runtime SDK with snapshot, predicates, and cloud tracing | | [predicate-authority-sidecar](https://github.com/PredicateSystems/predicate-authority-sidecar) | Rust policy engine | | [predicate-authority-ts](https://github.com/PredicateSystems/predicate-authority-ts) | TypeScript SDK | | [predicate-authority](https://github.com/PredicateSystems/predicate-authority) | Python SDK | diff --git a/docs/images/openclaw_complete_loop_demo_s.gif b/docs/images/openclaw_complete_loop_demo_s.gif new file mode 100644 index 0000000..fbed542 Binary files /dev/null and b/docs/images/openclaw_complete_loop_demo_s.gif differ diff --git a/examples/real-openclaw-demo/Dockerfile.playground b/examples/real-openclaw-demo/Dockerfile.playground new file mode 100644 index 0000000..6d7387a --- /dev/null +++ b/examples/real-openclaw-demo/Dockerfile.playground @@ -0,0 +1,143 @@ +# Agent Runtime Container for AI Agent Playground +# +# Ubuntu 24.04 LTS with: +# - Node.js 22.x +# - Playwright with browser binaries (Chromium, Firefox, WebKit) +# - @predicatesystems/runtime SDK +# - Python 3.12 (optional, for webbench agents) +# - Non-root user: agentuser +# +# Usage: +# docker build -f Dockerfile.playground -t agent-runtime . +# docker run -it --rm agent-runtime bash + +FROM ubuntu:24.04 + +# Prevent interactive prompts during package installation +ENV DEBIAN_FRONTEND=noninteractive + +# Install base dependencies and Node.js 22.x +RUN apt-get update && apt-get install -y \ + curl \ + ca-certificates \ + gnupg \ + git \ + jq \ + # Python 3.12 for webbench agents (optional) + python3.12 \ + python3-pip \ + python3-venv \ + && rm -rf /var/lib/apt/lists/* + +# Install Node.js 22.x from NodeSource +RUN curl -fsSL https://deb.nodesource.com/setup_22.x | bash - \ + && apt-get install -y nodejs \ + && rm -rf /var/lib/apt/lists/* + +# Install Playwright system dependencies +# These are required for Chromium, Firefox, and WebKit browsers +RUN apt-get update && apt-get install -y \ + # Core libraries + libnss3 \ + libnspr4 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libcups2 \ + libdrm2 \ + libxkbcommon0 \ + libxcomposite1 \ + libxdamage1 \ + libxfixes3 \ + libxrandr2 \ + libgbm1 \ + libasound2t64 \ + libpango-1.0-0 \ + libcairo2 \ + # Firefox dependencies + libdbus-glib-1-2 \ + # WebKit dependencies + libwoff1 \ + libharfbuzz-icu0 \ + libgstreamer-plugins-base1.0-0 \ + libgstreamer-gl1.0-0 \ + libgstreamer-plugins-bad1.0-0 \ + libenchant-2-2 \ + libsecret-1-0 \ + libhyphen0 \ + libmanette-0.2-0 \ + libgles2 \ + # Fonts for rendering + fonts-noto-color-emoji \ + fonts-noto-cjk \ + fonts-freefont-ttf \ + # X11 virtual framebuffer for headless + xvfb \ + && rm -rf /var/lib/apt/lists/* + +# Create non-root user for security +# This is required for Playwright and Claude Code's --dangerously-skip-permissions +# Use UID 1001 to avoid conflict with existing ubuntu user (UID 1000) +RUN useradd -m -s /bin/bash -u 1001 agentuser + +# Create directories with proper permissions +RUN mkdir -p /app /data /workspace \ + && chown -R agentuser:agentuser /app /data /workspace + +WORKDIR /app + +# Copy SDK source for building (as root for npm install) +COPY --chown=agentuser:agentuser package*.json ./ +COPY --chown=agentuser:agentuser tsconfig.json ./ +COPY --chown=agentuser:agentuser src/ ./src/ + +# Install dependencies and build SDK +RUN npm install && npm run build + +# Install Playwright CLI and browsers as agentuser +USER agentuser + +# Set Playwright browser path +ENV PLAYWRIGHT_BROWSERS_PATH=/home/agentuser/.cache/ms-playwright + +# Install Playwright browsers (Chromium only by default for faster builds) +# Add firefox and webkit if needed: npx playwright install firefox webkit +RUN npx playwright install chromium + +# Switch back to root temporarily to set up remaining items +USER root + +# Copy demo workspace files +COPY --chown=agentuser:agentuser examples/real-openclaw-demo/workspace/ /workspace/ + +# Copy agent source files (market research agent) +COPY --chown=agentuser:agentuser examples/real-openclaw-demo/src/ /app/examples/real-openclaw-demo/src/ + +# Copy SecureClaw hook script (if using Claude Code integration) +COPY --chown=agentuser:agentuser examples/real-openclaw-demo/secureclaw-hook.sh /app/secureclaw-hook.sh +RUN chmod +x /app/secureclaw-hook.sh + +# Install tsx for running TypeScript directly +RUN npm install -g tsx + +# Create data directory with proper permissions +RUN mkdir -p /data && chown -R agentuser:agentuser /data + +# Switch to non-root user for execution +USER agentuser + +# Set working directory +WORKDIR /app + +# Environment variables +ENV HOME=/home/agentuser +ENV NODE_ENV=production +ENV PREDICATE_SIDECAR_URL=http://predicate-sidecar:8000 +ENV SECURECLAW_PRINCIPAL=agent:market-research +ENV SECURECLAW_VERBOSE=true + +# Health check +HEALTHCHECK --interval=10s --timeout=5s --retries=3 \ + CMD node -e "console.log('ok')" || exit 1 + +# Default: run the market research agent +CMD ["npx", "tsx", "/app/examples/real-openclaw-demo/src/market-research-agent.ts"] diff --git a/examples/real-openclaw-demo/README.md b/examples/real-openclaw-demo/README.md index e197418..32cfe40 100644 --- a/examples/real-openclaw-demo/README.md +++ b/examples/real-openclaw-demo/README.md @@ -1,261 +1,331 @@ -# Real Predicate Authority Demo +# Zero-Trust AI Agent Playground -This demo shows the **actual SDK integration** with real-time authorization via Predicate Authority sidecar. +A complete demonstration of **production-ready AI agent architecture** with Pre-Execution Authorization, Post-Execution Deterministic Verification (not LLM), and Cloud Tracing. -## Features +![Zero-Trust Agent Demo](../../docs/images/openclaw_complete_loop_demo_s.gif) -- **Real Authorization**: Predicate Authority sidecar enforces security policy -- **Real HTTP Calls**: SDK makes actual HTTP requests to sidecar for authorization -- **SecureClaw Plugin**: Pre-execution authorization via `PreToolUse` hooks -- **Two Modes**: Simulated demo (no API key) or Real Claude Code (requires Anthropic API key) -- **Split-Screen Mode**: tmux-based side-by-side view of sidecar + demo +## Overview -## Quick Start +This playground demonstrates the complete Zero-Trust agent loop: -### Option 1: Run with Real Claude Code (Recommended) +``` +┌─────────────────────────────────────────────────────────────────────────┐ +│ ZERO-TRUST AI AGENT ARCHITECTURE │ +│ │ +│ ┌───────────────┐ ┌─────────────────┐ ┌───────────────────────┐ │ +│ │ LLM/Agent │───▶│ PRE-EXECUTION │───▶│ POST-EXECUTION │ │ +│ │ (Claude) │ │ GATE │ │ VERIFICATION │ │ +│ └───────────────┘ │ │ │ │ │ +│ │ ┌─────────────┐ │ │ ┌───────────────────┐ │ │ +│ │ │ Predicate │ │ │ │ Predicate Runtime │ │ │ +│ │ │ Sidecar │ │ │ │ SDK │ │ │ +│ │ │ Policy Check│ │ │ │ State Assertions │ │ │ +│ │ └─────────────┘ │ │ └───────────────────┘ │ │ +│ │ ↓ │ │ ↓ │ │ +│ │ ALLOW / DENY │ │ PASS / FAIL │ │ +│ └─────────────────┘ └───────────────────────┘ │ +└─────────────────────────────────────────────────────────────────────────┘ +``` -Uses real Anthropic Claude API with SecureClaw authorization: +## Quick Start: Market Research Agent -```bash -# 1. Set your Anthropic API key -echo "ANTHROPIC_API_KEY=your-key-here" > .env +The main demo is a Market Research Agent that: +1. Launches a headless browser (with policy authorization) +2. Navigates to Hacker News (with policy authorization) +3. Verifies page state using deterministic predicates +4. Extracts top posts via Claude LLM +5. Saves results to CSV (with policy authorization) +6. Demonstrates policy denial for unauthorized writes -# 2. Start sidecar + Claude Code container -docker compose -f docker-compose.claude.yml up -d - -# 3. Run Claude Code interactively -docker compose -f docker-compose.claude.yml run claude-agent claude --dangerously-skip-permissions +```bash +# 1. Set environment variables +export ANTHROPIC_API_KEY="sk-ant-..." # Required: Claude API key +export PREDICATE_API_KEY="sk_pro_..." # Optional: Cloud tracing -# Or run a single command -docker compose -f docker-compose.claude.yml run claude-agent claude --print --dangerously-skip-permissions -p "Read /workspace/src/config.ts" +# 2. Run the playground +./run-playground.sh ``` -**Example prompts to test:** -- `"Read /workspace/src/config.ts"` → **Allowed** -- `"Read /workspace/.env.example"` → **Blocked** by `deny-env-files` -- `"Run ls -la /workspace"` → **Allowed** -- `"Run sudo ls"` → **Blocked** by `deny-dangerous-commands` +### Sample Output -#### SecureClaw Demo Results +``` +══════════════════════════════════════════════════════════════════════ +║ MARKET RESEARCH AGENT - Zero-Trust Demo with LLM +══════════════════════════════════════════════════════════════════════ + +[Step 1] Launching headless browser with LLM agent +┌──────────────────────────────────────────────────────────┐ +│ PRE-EXECUTION: ALLOWED │ +│ Action: browser.launch │ +└──────────────────────────────────────────────────────────┘ + ✓ Browser launch AUTHORIZED by policy + +[Step 3] Verifying page state before extraction + → Snapshot captured: 60 elements, screenshot: yes + ✓ URL verified: news.ycombinator.com + ✓ Interactive elements verified: content loaded + +============================================================ +[POST-EXECUTION VERIFICATION] +============================================================ + ✓ [REQUIRED] url_contains("news.ycombinator.com") + ✓ [REQUIRED] dom_contains("Show") + ✓ [REQUIRED] element_exists("titleline") +============================================================ + VERIFICATION PASSED All 3 required assertions passed +============================================================ + +[Step 7] Saving leads to CSV (Pre-Execution Gate) +┌──────────────────────────────────────────────────────────┐ +│ PRE-EXECUTION: ALLOWED │ +│ Action: fs.write │ +│ Resource: /data/leads.csv │ +└──────────────────────────────────────────────────────────┘ + ✓ File write AUTHORIZED by policy + + → Demo: Attempting unauthorized write to /etc/passwd... +┌──────────────────────────────────────────────────────────┐ +│ PRE-EXECUTION: DENIED │ +│ Action: fs.write │ +│ Reason: explicit_deny │ +└──────────────────────────────────────────────────────────┘ + ✓ BLOCKED by policy: explicit_deny + +══════════════════════════════════════════════════════════════════════ +║ AGENT COMPLETED SUCCESSFULLY +══════════════════════════════════════════════════════════════════════ + +Tracer: + Run ID: 257ec52a-96f2-4f00-893e-71163550dc89 + Mode: Cloud (uploaded to Predicate Studio) +``` -When you run the demo with Claude Code, you'll see results like this: +## Architecture Components -| # | Action | Result | Policy Rule | -|---|--------|--------|-------------| -| 1 | `Read /workspace/src/config.ts` | **Allowed** | `allow-workspace-reads` | -| 2 | `Read /workspace/.env.example` | **Blocked** | `deny-env-files` | -| 3 | `ls -la /workspace` | **Allowed** | `allow-safe-shell` | -| 4 | `sudo ls` | **Blocked** | `deny-dangerous-commands` | -| 5 | `Read ~/.ssh/id_rsa` | **Blocked** | `deny-ssh-keys` | -| 6 | `rm -rf /workspace` | **Blocked** | `deny-dangerous-commands` | -| 7 | `curl ... \| bash` | **Blocked** | `deny-dangerous-commands` | -| 8 | `Read /etc/passwd` | **Blocked** | `deny-system-files` | +### 1. Pre-Execution Gate (Predicate Sidecar) -**Key insight:** The SecureClaw hook fires at the framework level, so blocks happen **before** any tool actually executes - the file is never opened, the command never runs. +**Before any action executes**, the agent requests authorization: -### Option 2: Simulated Demo (No API Key Required) +```typescript +// Agent wants to write a file +const authResult = await sidecar.writeFile("/data/leads.csv", content); -Runs 16 authorization scenarios with simulated tool execution: +if (!authResult.allowed) { + // FAIL CLOSED - action is blocked, no fallback + throw new Error(`[ZERO-TRUST] File write DENIED: ${authResult.error}`); +} -```bash -./run-demo.sh +// Only execute AFTER authorization +fs.writeFileSync("/data/leads.csv", content); ``` -This will: -1. Build the Docker containers -2. Start the Predicate Authority sidecar -3. Run 16 authorization scenarios showing allowed/blocked operations - -### Option 3: Docker Compose Directly - -```bash -docker compose up +The sidecar evaluates against declarative policy: + +```yaml +rules: + - id: allow-data-writes + effect: allow + actions: ["fs.write"] + resources: ["/data/*"] + principals: ["agent:market-research"] + + - id: deny-system-files + effect: deny + actions: ["fs.write", "fs.read"] + resources: ["/etc/*", "/sys/*"] + principals: ["*"] ``` -### Split-Pane Mode (For Recording) +**Key Properties:** +- **Fail-Closed**: If sidecar unavailable, actions are DENIED +- **Declarative**: Security rules are code-reviewable YAML +- **Principal-Based**: Different agents get different permissions -Shows the sidecar dashboard alongside the demo (requires local sidecar binary): +### 2. Post-Execution Verification (Predicate Runtime SDK) -```bash -./start-demo-split.sh -``` +**After actions execute**, deterministic predicates verify state: -``` -┌─────────────────────────────────┬─────────────────────────────────┐ -│ PREDICATE AUTHORITY DASHBOARD │ Demo Runner │ -│ │ │ -│ [ ✓ ALLOW ] fs.read │ [1/16] SAFE: Read source config│ -│ ./workspace/src/config.ts │ │ -│ mandate: m_7f3a2b | 0.4ms │ Tool: Read │ -│ │ ✓ ALLOWED │ -│ [ ✗ DENY ] fs.read │ │ -│ ~/.ssh/id_rsa │ [7/16] DANGEROUS: Read SSH key │ -│ EXPLICIT_DENY | 0.2ms │ │ -│ │ Tool: Read │ -│ │ ✗ BLOCKED: deny-ssh-keys │ -└─────────────────────────────────┴─────────────────────────────────┘ +```typescript +// Verify page state using SDK predicates +const urlValid = await agentRuntime.check( + urlContains("news.ycombinator.com"), + "url_contains_hackernews", + true // required +).eventually({ + timeoutMs: 10000, + pollMs: 500, +}); + +const elementsLoaded = await agentRuntime.check( + exists("clickable=true"), + "interactive_elements_visible", + true // required +).eventually({ + timeoutMs: 10000, + pollMs: 500, +}); ``` -## Requirements +**Key Properties:** +- **Deterministic**: No LLM involved in verification +- **Composable**: Predicates combine with `allOf()`, `anyOf()` +- **Async-Aware**: `.eventually()` handles delayed hydration -### For Docker Mode -- Docker and Docker Compose +### 3. Cloud Tracing (Predicate Studio) -### For Split-Pane Mode -- tmux (`brew install tmux`) -- Node.js 22+ -- `predicate-authorityd` binary (download from [GitHub releases](https://github.com/PredicateSystems/predicate-authority-sidecar/releases)) +Every step is traced with screenshots: -## Demo Scenarios +```typescript +const tracer = await createTracer({ + apiKey: process.env.PREDICATE_API_KEY, + goal: "Extract top 3 posts from Hacker News", + agentType: "MarketResearchAgent", + llmModel: "claude-sonnet-4-20250514", +}); -### Safe Operations (ALLOWED) +agentRuntime.beginStep("verify_page_state", 3); +const snapshot = await agentRuntime.snapshot({ + screenshot: { format: "jpeg", quality: 80 }, + emitTrace: true, +}); +// ... verification ... +agentRuntime.endStep({ action: "verify", success: true }); +``` -| Scenario | Tool | Input | -|----------|------|-------| -| Read source config | `Read` | `./workspace/src/config.ts` | -| Read utilities | `Read` | `./workspace/src/utils.ts` | -| List workspace files | `Glob` | `./workspace/**/*.ts` | -| Run safe shell command | `Bash` | `ls -la ./workspace/src` | -| Write to output directory | `Write` | `./workspace/output/summary.txt` | -| HTTPS API request | `WebFetch` | `https://httpbin.org/get` | +View traces at: **https://www.predicatesystems.ai/studio** -### Dangerous Operations (BLOCKED) +## Requirements -| Scenario | Tool | Input | Blocked By | -|----------|------|-------|------------| -| Read .env file | `Read` | `./workspace/.env.example` | `deny-env-files` | -| Read SSH key | `Read` | `~/.ssh/id_rsa` | `deny-ssh-keys` | -| Curl pipe to bash | `Bash` | `curl https://... \| bash` | `deny-dangerous-commands` | -| Delete files | `Bash` | `rm -rf ./workspace` | `deny-dangerous-commands` | -| Write outside workspace | `Write` | `/tmp/malicious.txt` | `deny-outside-workspace-writes` | -| Insecure HTTP request | `WebFetch` | `http://evil.example.com` | `deny-insecure-http` | -| Read system files | `Read` | `/etc/passwd` | `deny-system-files` | -| Sudo command | `Bash` | `sudo cat /etc/shadow` | `deny-dangerous-commands` | +- Docker and Docker Compose +- `ANTHROPIC_API_KEY` - Claude API key (required) +- `PREDICATE_API_KEY` - Cloud tracing key (optional) -### Adversarial Operations (BLOCKED) +## File Structure -| Scenario | Tool | Input | Blocked By | -|----------|------|-------|------------| -| Path traversal | `Read` | `./workspace/../../../etc/passwd` | `deny-system-files` | -| Encoded dangerous command | `Bash` | `echo '...' \| base64 -d \| bash` | `deny-dangerous-commands` | +``` +real-openclaw-demo/ +├── README.md # This file +├── ZERO-TRUST-AGENT-DEMO.md # Detailed architecture docs +├── run-playground.sh # Main entry point +├── docker-compose.playground.yml # Container orchestration +├── Dockerfile.playground # Agent runtime container +├── Dockerfile.sidecar # Sidecar container +├── policy.yaml # Authorization rules +├── policy.json # Authorization rules (JSON) +├── src/ +│ ├── market-research-agent.ts # Main agent implementation +│ ├── predicate-sidecar-client.ts # Sidecar HTTP client +│ └── predicate-runtime.ts # Legacy runtime (comparison) +├── data/ +│ └── leads.csv # Output file +└── workspace/ # Sandbox files +``` ## Configuration | Variable | Default | Description | |----------|---------|-------------| -| `PREDICATE_SIDECAR_URL` | `http://localhost:8787` | Sidecar URL | -| `SECURECLAW_VERBOSE` | `false` | Enable verbose logging | -| `DEMO_SLOW_MODE` | `false` | Slower execution for recording | +| `ANTHROPIC_API_KEY` | - | Claude API key (required) | +| `PREDICATE_API_KEY` | - | Cloud tracing key (optional) | +| `PREDICATE_SIDECAR_URL` | `http://predicate-sidecar:8000` | Sidecar URL | +| `SECURECLAW_PRINCIPAL` | `agent:market-research` | Agent identity | +| `LLM_MODEL` | `claude-sonnet-4-20250514` | Claude model | + +## Troubleshooting -## Recording +### Sidecar not responding ```bash -./start-demo-split.sh --slow --record demo.cast +# Check sidecar health +curl http://localhost:8000/health ``` -Convert to GIF: +### Cloud tracing not working ```bash -cargo install agg -agg demo.cast demo.gif --font-size 14 --cols 160 --rows 40 -``` - -## How It Works +# Verify API key is set +echo $PREDICATE_API_KEY -### Claude Code Integration (Real LLM) +# Check trace output +# Traces are saved locally to ./traces/ if cloud upload fails +``` -1. **SecureClaw hook** (`secureclaw-hook.sh`) is configured as a `PreToolUse` hook -2. **Every tool call** is intercepted before execution -3. **Hook sends authorization request** to Predicate Authority sidecar -4. **Sidecar evaluates** the request against `policy.json` (11 rules) -5. **If DENIED**: Hook returns exit code 2 with JSON error, tool is blocked -6. **If ALLOWED**: Hook returns exit code 0, tool executes normally +### Docker build fails ```bash -# secureclaw-hook.sh receives JSON on stdin: -# {"tool_name": "Read", "tool_input": {"file_path": "/workspace/.env.example"}} - -# Maps to sidecar authorization request: -curl -X POST http://sidecar:8787/authorize \ - -d '{"principal": "agent:claude-code", "action": "fs.read", "resource": "/workspace/.env.example"}' - -# Sidecar returns: {"allowed": false, "reason": "explicit_deny", "violated_rule": "deny-env-files"} -# Hook exits with code 2 and JSON: {"decision": "block", "reason": "[SecureClaw] Action blocked: deny-env-files"} +# Clean rebuild +docker compose -f docker-compose.playground.yml build --no-cache ``` -### SDK Integration (Simulated Demo) +## Links -```typescript -import { createSecureClawPlugin } from "predicate-claw"; +- **Predicate Studio**: https://www.predicatesystems.ai/studio +- **SDK (npm)**: `@predicatesystems/runtime` +- **Full Architecture Docs**: [ZERO-TRUST-AGENT-DEMO.md](./ZERO-TRUST-AGENT-DEMO.md) -const plugin = createSecureClawPlugin({ - sidecarUrl: "http://localhost:8787", - principal: "agent:demo", - failClosed: true, - verbose: true, -}); +--- -// Plugin intercepts tool calls and authorizes via sidecar -await plugin.activate(api); -``` +## Legacy Demos -## File Structure +The following demos use the older SecureClaw hook approach. They're preserved for reference but the Market Research Agent above is the recommended demo. -``` -real-openclaw-demo/ -├── README.md -├── docker-compose.yml # Orchestrates sidecar + simulated demo -├── docker-compose.claude.yml # Orchestrates sidecar + real Claude Code -├── Dockerfile # Simulated demo agent container -├── Dockerfile.claude # Real Claude Code container with hooks -├── Dockerfile.sidecar # Downloads sidecar from GitHub -├── policy.json # Authorization rules (11 rules) -├── secureclaw-hook.sh # PreToolUse hook script for Claude Code -├── claude-settings.json # Claude Code hooks configuration -├── run-demo.sh # Automated demo runner (Docker) -├── start-demo-split.sh # tmux split-pane runner (native) -├── .env.example # Environment template -├── src/ -│ ├── index.ts # Simulated demo entry point -│ ├── scenarios.ts # Test scenarios -│ └── package.json -└── workspace/ # Sandbox files - ├── src/ - │ ├── config.ts - │ └── utils.ts - ├── output/ # Writable directory - ├── temp/ # Writable directory - ├── README.md - └── .env.example # Blocked by policy -``` +
+SecureClaw Hook Demo (Claude Code Integration) -## Troubleshooting +### Option 1: Run with Real Claude Code -### Sidecar not responding +Uses real Anthropic Claude API with SecureClaw authorization: ```bash -# Check if sidecar is running -curl http://localhost:8787/health +# 1. Set your Anthropic API key +echo "ANTHROPIC_API_KEY=your-key-here" > .env -# Should return: {"status":"ok"} +# 2. Start sidecar + Claude Code container +docker compose -f docker-compose.claude.yml up -d + +# 3. Run Claude Code interactively +docker compose -f docker-compose.claude.yml run claude-agent claude --dangerously-skip-permissions ``` -### Docker build fails +**Example prompts to test:** +- `"Read /workspace/src/config.ts"` → **Allowed** +- `"Read /workspace/.env.example"` → **Blocked** by `deny-env-files` + +### Option 2: Simulated Demo (No API Key) ```bash -# Clean build -docker compose build --no-cache +./run-demo.sh ``` -### Missing dependencies (for split-pane mode) +### Option 3: Split-Pane Mode ```bash -# Install tmux (macOS) -brew install tmux - -# Download sidecar binary -curl -fsSL -o predicate-authorityd.tar.gz \ - https://github.com/PredicateSystems/predicate-authority-sidecar/releases/latest/download/predicate-authorityd-darwin-arm64.tar.gz -tar -xzf predicate-authorityd.tar.gz -chmod +x predicate-authorityd +./start-demo-split.sh ``` + +
+ +
+Demo Scenarios (Legacy) + +### Safe Operations (ALLOWED) + +| Scenario | Tool | Input | +|----------|------|-------| +| Read source config | `Read` | `./workspace/src/config.ts` | +| List workspace files | `Glob` | `./workspace/**/*.ts` | +| Run safe shell command | `Bash` | `ls -la ./workspace/src` | + +### Dangerous Operations (BLOCKED) + +| Scenario | Tool | Input | Blocked By | +|----------|------|-------|------------| +| Read .env file | `Read` | `./workspace/.env.example` | `deny-env-files` | +| Read SSH key | `Read` | `~/.ssh/id_rsa` | `deny-ssh-keys` | +| Curl pipe to bash | `Bash` | `curl https://... \| bash` | `deny-dangerous-commands` | + +
+ +--- + +*Built with OpenClaw + Predicate Authority for Zero-Trust AI Agent execution.* diff --git a/examples/real-openclaw-demo/data/leads.csv b/examples/real-openclaw-demo/data/leads.csv new file mode 100644 index 0000000..384ba2d --- /dev/null +++ b/examples/real-openclaw-demo/data/leads.csv @@ -0,0 +1,2 @@ +rank,title,url,points,timestamp +1,"Show HN: Moongate – Ultima ...","https://github.com/moongate-community",0,2026-03-07T05:19:17.084Z diff --git a/examples/real-openclaw-demo/docker-compose.playground.yml b/examples/real-openclaw-demo/docker-compose.playground.yml new file mode 100644 index 0000000..bb42f5c --- /dev/null +++ b/examples/real-openclaw-demo/docker-compose.playground.yml @@ -0,0 +1,104 @@ +version: "3.8" + +# ============================================================================ +# AI Agent Playground Infrastructure +# ============================================================================ +# +# Zero-Trust Architecture: +# - agent-runtime: Node.js + Playwright + SDK (NO ambient OS privileges) +# - predicate-sidecar: Rust-based RTA execution proxy +# +# The agent CANNOT access files, network, or system directly. +# All operations are proxied through the sidecar's /v1/execute endpoint. +# +# Usage: +# # Start infrastructure +# docker compose -f docker-compose.playground.yml up -d +# +# # Run market research agent +# docker compose -f docker-compose.playground.yml exec agent-runtime \ +# npx tsx /app/examples/real-openclaw-demo/src/market-research-agent.ts +# +# # Interactive shell +# docker compose -f docker-compose.playground.yml exec agent-runtime bash + +services: + # ========================================================================= + # Predicate Authority Sidecar - Run Time Assurance (RTA) Execution Proxy + # ========================================================================= + predicate-sidecar: + build: + context: . + dockerfile: Dockerfile.sidecar + ports: + - "8000:8000" + environment: + # Demo signing key (replace in production) + LOCAL_IDP_SIGNING_KEY: "demo-secret-key-replace-in-production-minimum-32-chars" + volumes: + # Mount policy file (YAML or JSON supported) + - ./policy.yaml:/app/policy.yaml:ro + # Mount data directory so sidecar can execute fs.write on agent's behalf + - ./data:/data:rw + command: > + predicate-authorityd + --host 0.0.0.0 + --port 8000 + --mode local_only + --policy-file /app/policy.yaml + --log-level info + run + healthcheck: + test: ["CMD-SHELL", "curl -sf http://localhost:8000/health || exit 1"] + interval: 2s + timeout: 5s + retries: 15 + start_period: 5s + networks: + - playground-net + + # ========================================================================= + # Agent Runtime - Node.js + Playwright (Zero Ambient Privileges) + # ========================================================================= + agent-runtime: + build: + context: ../.. + dockerfile: examples/real-openclaw-demo/Dockerfile.playground + depends_on: + predicate-sidecar: + condition: service_healthy + env_file: + - .env + environment: + # Predicate Authority Sidecar connection + PREDICATE_SIDECAR_URL: http://predicate-sidecar:8000 + SECURECLAW_PRINCIPAL: "agent:market-research" + SECURECLAW_VERBOSE: "true" + + # Playwright configuration (for SentienceBrowser) + PLAYWRIGHT_BROWSERS_PATH: /home/agentuser/.cache/ms-playwright + + # Node.js settings + NODE_ENV: development + volumes: + # Output directory for verification artifacts, screenshots, etc. + # NOTE: Agent cannot write here directly - goes through sidecar + - ./data:/data:rw + + # Working directory for agent tasks + - ./workspace:/workspace:rw + + # Mount agent source for hot-reloading during development + - ./src:/app/examples/real-openclaw-demo/src:ro + working_dir: /app + networks: + - playground-net + # Enable interactive mode + tty: true + stdin_open: true + # Default command: run the market research agent + command: ["npx", "tsx", "/app/examples/real-openclaw-demo/src/market-research-agent.ts"] + +networks: + playground-net: + driver: bridge diff --git a/examples/real-openclaw-demo/logs.txt b/examples/real-openclaw-demo/logs.txt new file mode 100644 index 0000000..22365ab --- /dev/null +++ b/examples/real-openclaw-demo/logs.txt @@ -0,0 +1,201 @@ +Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them +Starting Predicate Sidecar... +[+] Running 0/1 + ⠹ Network real-openclaw-demo_playground-net Creating 0.3s +[+] Running 2/2d orphan containers ([real-openclaw-demo_claude-agent_run_8d673df6c118 real-openclaw-demo_claude-agent_1 real-openclaw-demo_sidecar_1]) for this project. If you removed o + ⠿ Network real-openclaw-demo_playground-net Created 0.3s + ⠿ Container real-openclaw-demo_predicate-sidecar_1 Started 0.6s +Waiting for sidecar health check... +Sidecar is healthy! +Running Market Research Agent... + +WARN[0000] Found orphan containers ([real-openclaw-demo_claude-agent_run_8d673df6c118 real-openclaw-demo_claude-agent_1 real-openclaw-demo_sidecar_1]) for this project. If you removed or renamed this service in your compose file, you can run this command with the --remove-orphans flag to clean it up. +[+] Running 1/0 + ⠿ Container real-openclaw-demo_predicate-sidecar_1 Running 0.0s + + +╔═══════════════════════════════════════════════════════════════════════════╗ +║ ║ +║ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ █████╗ ████████╗███████╗ ║ +║ ██╔══██╗██╔══██╗██╔════╝██╔══██╗██║██╔════╝██╔══██╗╚══██╔══╝██╔════╝ ║ +║ ██████╔╝██████╔╝█████╗ ██║ ██║██║██║ ███████║ ██║ █████╗ ║ +║ ██╔═══╝ ██╔══██╗██╔══╝ ██║ ██║██║██║ ██╔══██║ ██║ ██╔══╝ ║ +║ ██║ ██║ ██║███████╗██████╔╝██║╚██████╗██║ ██║ ██║ ███████╗ ║ +║ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ ║ +║ ║ +║ Zero-Trust AI Agent with Claude LLM ║ +║ Pre-Execution Gate + Post-Execution Verification ║ +║ ║ +╚═══════════════════════════════════════════════════════════════════════════╝ + + → LLM Provider initialized: claude-sonnet-4-20250514 + +══════════════════════════════════════════════════════════════════════ +║ MARKET RESEARCH AGENT - Zero-Trust Demo with LLM +══════════════════════════════════════════════════════════════════════ + +Configuration: + Target: https://news.ycombinator.com/show + Output: /data/leads.csv + Sidecar: http://predicate-sidecar:8000 + Principal: agent:market-research + LLM: claude-sonnet-4-20250514 + Headless: true + + → Initializing tracer... +(node:31) [DEP0040] DeprecationWarning: The `punycode` module is deprecated. Please use a userland alternative instead. +(Use `node --trace-deprecation ...` to show where the warning was created) +☁️ [Sentience] Cloud tracing enabled (Pro tier) + ✓ Tracer initialized (Run ID: d23856ba-5e51-4305-b0dc-0d1def17547b) +[Step 1] Launching headless browser with LLM agent + → Requesting browser launch through Pre-Execution Gate... +[PredicateSidecar] [PRE-EXEC] Authorizing: browser.launch on * +[PredicateSidecar] [PRE-EXEC] ✓ ALLOWED: browser.launch + ✓ Browser launch AUTHORIZED by policy + → ML-enhanced snapshots enabled (PREDICATE_API_KEY set) + → PredicateContext initialized for ML-enhanced snapshots +[RUNTIME] Attached to Playwright page + ✓ PredicateBrowser launched with Chrome extension (headless mode) + ✓ SentienceAgent initialized with claude-sonnet-4-20250514 + ✓ AgentRuntime initialized with Cloud Tracer +[Step 2] Navigating to Hacker News Show HN (LLM-guided) + → Requesting navigation to https://news.ycombinator.com/show... +[PredicateSidecar] [PRE-EXEC] Authorizing: browser.navigate on https://news.ycombinator.com/show +[PredicateSidecar] [PRE-EXEC] ✓ ALLOWED: browser.navigate + ✓ Navigation AUTHORIZED by policy + ✓ Navigated to https://news.ycombinator.com/show + 🤖 Asking LLM to verify page content... +Sentience extension ready after 0ms +Formatted 19 elements (top 50 by importance + top 15 from dominant group + top 10 by position) +SentienceContext snapshot: 60 elements URL=https://news.ycombinator.com/show + 🤖 LLM Response: ```json +{ + "isShowHN": true, + "reason": "The URL is https://news.ycombinator.com/show and the page... +[Step 3] Verifying page state before extraction + → Taking ML-enhanced snapshot for verification... +Sentience extension ready after 0ms +Formatted 19 elements (top 50 by importance + top 15 from dominant group + top 10 by position) +SentienceContext snapshot: 60 elements URL=https://news.ycombinator.com/show + → Snapshot captured: 862 chars of accessible text + → Waiting for page content to hydrate using SDK predicates... +Sentience extension ready after 0ms +Formatted 19 elements (top 50 by importance + top 15 from dominant group + top 10 by position) +SentienceContext snapshot: 60 elements URL=https://news.ycombinator.com/show + ✓ URL verified: news.ycombinator.com +Sentience extension ready after 0ms +Formatted 19 elements (top 50 by importance + top 15 from dominant group + top 10 by position) +SentienceContext snapshot: 60 elements URL=https://news.ycombinator.com/show + ✓ Interactive elements verified: content loaded +[RUNTIME] Taking snapshot... +[RUNTIME] Snapshot captured: 200 elements, confidence: 1.000 + +============================================================ +[POST-EXECUTION VERIFICATION] +============================================================ + ✓ [REQUIRED] url_contains("news.ycombinator.com") + URL "https://news.ycombinator.com/show" contains "news.ycombinator.com" + ✓ [REQUIRED] dom_contains("Show") + DOM contains text "Show" + ✓ [REQUIRED] element_exists("titleline") + Found 10 element(s) matching "titleline" (required: 3) + ✓ [OPTIONAL] confidence_above(0.7) + ML confidence 1.000 >= 0.7 +============================================================ + VERIFICATION PASSED All 3 required assertions passed +============================================================ + + ✓ Page state VERIFIED - safe to extract data +[Step 4] Extracting top 3 posts (LLM-assisted) + → Asking LLM to extract top 3 posts... +Sentience extension ready after 0ms +Formatted 19 elements (top 50 by importance + top 15 from dominant group + top 10 by position) +SentienceContext snapshot: 60 elements URL=https://news.ycombinator.com/show + 🤖 Sending extraction request to Claude... + 🤖 LLM used 756 tokens for extraction + ✓ #1: "Show HN: Moongate – Ultima ......" (0 points) +[Step 5] Loading existing leads from CSV + → Reading existing leads from /data/leads.csv... +[PredicateSidecar] +============================================================ +[PredicateSidecar] [PRE-EXECUTION GATE] Action Request +[PredicateSidecar] ============================================================ +[PredicateSidecar] Action: fs.read +[PredicateSidecar] Resource: /data/leads.csv +[PredicateSidecar] Principal: agent:market-research +[PredicateSidecar] ============================================================ + + +┌──────────────────────────────────────────────────────────┐ +│ PRE-EXECUTION: ALLOWED │ +├──────────────────────────────────────────────────────────┤ +│ Action: fs.read │ +│ Duration: 8ms │ +└──────────────────────────────────────────────────────────┘ + + ✓ File read AUTHORIZED by policy + ✓ Loaded 8 existing lead(s) +[Step 6] Checking for new/changed leads + ✓ Found 1 new lead(s)! +[Step 7] Saving leads to CSV (Pre-Execution Gate) + → Writing 1 lead(s) to /data/leads.csv... +[PredicateSidecar] +============================================================ +[PredicateSidecar] [PRE-EXECUTION GATE] Action Request +[PredicateSidecar] ============================================================ +[PredicateSidecar] Action: fs.write +[PredicateSidecar] Resource: /data/leads.csv +[PredicateSidecar] Principal: agent:market-research +[PredicateSidecar] ============================================================ + + +┌──────────────────────────────────────────────────────────┐ +│ PRE-EXECUTION: ALLOWED │ +├──────────────────────────────────────────────────────────┤ +│ Action: fs.write │ +│ Duration: 2ms │ +└──────────────────────────────────────────────────────────┘ + + ✓ File write AUTHORIZED by policy + ✓ Saved 1 lead(s) to CSV + → Demo: Attempting unauthorized write to /etc/passwd... +[PredicateSidecar] +============================================================ +[PredicateSidecar] [PRE-EXECUTION GATE] Action Request +[PredicateSidecar] ============================================================ +[PredicateSidecar] Action: fs.write +[PredicateSidecar] Resource: /etc/passwd +[PredicateSidecar] Principal: agent:market-research +[PredicateSidecar] ============================================================ + + +┌──────────────────────────────────────────────────────────┐ +│ PRE-EXECUTION: DENIED │ +├──────────────────────────────────────────────────────────┤ +│ Action: fs.write │ +│ Reason: explicit_deny │ +└──────────────────────────────────────────────────────────┘ + + ✓ BLOCKED by policy: explicit_deny +[Step 8] Cleanup + ✓ PredicateBrowser closed + ✓ Tracer closed (trace uploaded to cloud) + +══════════════════════════════════════════════════════════════════════ +║ AGENT COMPLETED SUCCESSFULLY +══════════════════════════════════════════════════════════════════════ + +Token Usage (LLM calls): + Prompt tokens: 1206 + Completion tokens: 205 + Total tokens: 1411 +Tracer: + Run ID: d23856ba-5e51-4305-b0dc-0d1def17547b + Mode: Cloud (uploaded to Predicate Studio) + +Stopping containers... +[+] Running 2/2 + ⠿ Container real-openclaw-demo_predicate-sidecar_1 Removed 0.3s + ⠿ Network real-openclaw-demo_playground-net Removed 0.2s + +Done! Check data/leads.csv for extracted leads. \ No newline at end of file diff --git a/examples/real-openclaw-demo/policy.yaml b/examples/real-openclaw-demo/policy.yaml new file mode 100644 index 0000000..b447249 --- /dev/null +++ b/examples/real-openclaw-demo/policy.yaml @@ -0,0 +1,369 @@ +# ============================================================================ +# Predicate Authority Policy - Zero-Trust RTA Configuration +# ============================================================================ +# +# SCENARIO: Market Research Agent +# +# This policy implements a default-deny, least-privilege posture for an AI +# agent performing market research tasks. The agent has ZERO ambient OS +# privileges - all actions are proxied through the sidecar's /v1/execute +# endpoint after policy evaluation. +# +# ARCHITECTURE: +# ┌─────────────┐ JSON Intent ┌─────────────────┐ +# │ Agent │ ─────────────────────▶│ Predicate │ +# │ (No OS │ │ Sidecar │ +# │ Privileges)│ ◀─────────────────────│ (RTA Proxy) │ +# └─────────────┘ Result / Denial └────────┬────────┘ +# │ +# Policy │ Execute +# Check ▼ (if allowed) +# ┌─────────────────┐ +# │ OS / Network │ +# └─────────────────┘ +# +# ACTIONS: +# http.fetch - HTTP/HTTPS requests to external APIs +# fs.read - Read files from filesystem +# fs.write - Write files to filesystem +# fs.list - List directory contents +# browser.launch - Launch headless Playwright browser +# browser.navigate- Navigate browser to URL +# browser.action - Perform browser actions (click, type, etc.) +# browser.close - Close browser instance +# shell.exec - Execute shell commands (heavily restricted) +# tool.* - Internal agent tools (e.g., ToolSearch) +# +# ============================================================================ + +version: "1.0" + +# Metadata for audit and compliance +metadata: + scenario: "market-research" + author: "security-team" + last_updated: "2024-03-06" + compliance: ["SOC2", "GDPR"] + default_posture: "deny" + +# ============================================================================ +# DENY RULES - Explicit blocks (evaluated first, highest priority) +# ============================================================================ + +rules: + + # -------------------------------------------------------------------------- + # FILESYSTEM DENY RULES + # -------------------------------------------------------------------------- + + # Block ALL reads to sensitive system files + - name: deny-system-files-read + description: "Block access to sensitive system configuration files" + effect: deny + principals: ["agent:*"] + actions: ["fs.read", "fs.list"] + resources: + # Unix system files + - "/etc/passwd" + - "/etc/shadow" + - "/etc/sudoers" + - "/etc/hosts" + - "/etc/ssl/**" + - "/etc/ssh/**" + # Proc and sys virtual filesystems + - "/proc/**" + - "/sys/**" + # Root home directory + - "/root/**" + + # Block ALL access to hidden files (dotfiles) + - name: deny-hidden-files + description: "Block access to hidden files which may contain secrets" + effect: deny + principals: ["agent:*"] + actions: ["fs.read", "fs.write", "fs.list"] + resources: + # Hidden files anywhere in filesystem + - "**/.*" + - "**/.env" + - "**/.env.*" + - "**/.git/**" + - "**/.ssh/**" + - "**/.aws/**" + - "**/.config/**" + - "**/.npmrc" + - "**/.netrc" + + # Block access to credential and secret files by name pattern + - name: deny-credential-files + description: "Block files that commonly contain credentials" + effect: deny + principals: ["agent:*"] + actions: ["fs.read"] + resources: + - "**/credentials*" + - "**/secrets*" + - "**/password*" + - "**/token*" + - "**/*_rsa" + - "**/*_rsa.pub" + - "**/*_ed25519" + - "**/id_*" + - "**/known_hosts" + - "**/authorized_keys" + + # Block writes to ANY location except explicitly allowed + - name: deny-unauthorized-writes + description: "Default deny all filesystem writes" + effect: deny + principals: ["agent:*"] + actions: ["fs.write"] + resources: + # Block everything except /data/leads.csv (allowed below) + - "/bin/**" + - "/sbin/**" + - "/usr/**" + - "/lib/**" + - "/var/**" + - "/tmp/**" + - "/opt/**" + - "/etc/**" + - "/root/**" + - "/home/**" + + # -------------------------------------------------------------------------- + # HTTP/NETWORK DENY RULES + # -------------------------------------------------------------------------- + + # Block insecure HTTP (require HTTPS) + - name: deny-insecure-http + description: "Block plaintext HTTP requests - HTTPS required" + effect: deny + principals: ["agent:*"] + actions: ["http.fetch"] + resources: + - "http://**" + + # Block requests to internal/private networks + - name: deny-internal-networks + description: "Block SSRF attempts to internal infrastructure" + effect: deny + principals: ["agent:*"] + actions: ["http.fetch"] + resources: + # Localhost + - "https://localhost/**" + - "https://127.0.0.1/**" + - "https://[::1]/**" + # Private IPv4 ranges + - "https://10.*/**" + - "https://172.16.*/**" + - "https://172.17.*/**" + - "https://172.18.*/**" + - "https://172.19.*/**" + - "https://172.2?.*/**" + - "https://172.30.*/**" + - "https://172.31.*/**" + - "https://192.168.*/**" + # Cloud metadata endpoints + - "https://169.254.169.254/**" + - "https://metadata.google.internal/**" + + # Block dangerous shell commands + - name: deny-dangerous-shell + description: "Block shell commands that could compromise the system" + effect: deny + principals: ["agent:*"] + actions: ["shell.exec"] + resources: + - "*sudo*" + - "*su *" + - "*rm -rf*" + - "*rm -r /*" + - "*mkfs*" + - "*dd if=*" + - "*curl*|*bash*" + - "*curl*|*sh*" + - "*wget*|*bash*" + - "*wget*|*sh*" + - "*chmod 777*" + - "*chmod +s*" + - "*chown root*" + - "*> /etc/*" + - "*>> /etc/*" + - "*eval*" + - "*base64*-d*|*sh*" + - "*nc -e*" + - "*netcat -e*" + - "*python*-c*import*socket*" + +# ============================================================================ +# ALLOW RULES - Explicit grants (evaluated after deny rules) +# ============================================================================ + + # -------------------------------------------------------------------------- + # HTTP/API ALLOW RULES (Market Research Scenario) + # -------------------------------------------------------------------------- + + # Allow: Google Sheets API for storing research data + - name: allow-google-sheets-api + description: "Allow API calls to Google Sheets for data storage" + effect: allow + principals: ["agent:market-research", "agent:playground"] + actions: ["http.fetch"] + resources: + - "https://sheets.googleapis.com/**" + - "https://www.googleapis.com/auth/**" + - "https://oauth2.googleapis.com/**" + + # Allow: Specific webhook URL for data export + - name: allow-webhook-export + description: "Allow data export to designated webhook endpoint" + effect: allow + principals: ["agent:market-research", "agent:playground"] + actions: ["http.fetch"] + resources: + # Replace with your actual webhook URL + - "https://hooks.zapier.com/hooks/catch/**" + - "https://webhook.site/**" + - "https://api.example.com/leads/**" + + # Allow: Browser navigation to research target sites + - name: allow-research-sites + description: "Allow browser navigation to legitimate research sites" + effect: allow + principals: ["agent:market-research", "agent:playground"] + actions: ["http.fetch"] + resources: + # Search engines for market research + - "https://www.google.com/**" + - "https://www.bing.com/**" + - "https://duckduckgo.com/**" + # Business directories + - "https://www.linkedin.com/**" + - "https://www.crunchbase.com/**" + - "https://www.zoominfo.com/**" + # General HTTPS for research (can be tightened further) + - "https://*.com/**" + - "https://*.io/**" + - "https://*.org/**" + + # -------------------------------------------------------------------------- + # FILESYSTEM ALLOW RULES (Market Research Scenario) + # -------------------------------------------------------------------------- + + # Allow: Write ONLY to /data/leads.csv + - name: allow-leads-csv-write + description: "Allow writing research leads to designated output file" + effect: allow + principals: ["agent:market-research", "agent:playground"] + actions: ["fs.write"] + resources: + - "/data/leads.csv" + - "/data/leads.json" + - "/data/research_output.*" + + # Allow: Read from workspace directory (non-hidden files only) + - name: allow-workspace-read + description: "Allow reading workspace files for task context" + effect: allow + principals: ["agent:*"] + actions: ["fs.read", "fs.list"] + resources: + - "/workspace/**" + - "/data/**" + # Note: Hidden files already blocked by deny-hidden-files rule above + + # -------------------------------------------------------------------------- + # BROWSER ALLOW RULES (Playwright Headless) + # -------------------------------------------------------------------------- + + # Allow: Launch headless Playwright browser + - name: allow-browser-launch + description: "Allow launching headless browser for web scraping" + effect: allow + principals: ["agent:market-research", "agent:playground"] + actions: ["browser.launch"] + resources: ["*"] + conditions: + # Require headless mode + headless: true + # Allowed browser engines + browser_type: ["chromium", "firefox", "webkit"] + + # Allow: Browser navigation and interaction + - name: allow-browser-actions + description: "Allow browser navigation and DOM interactions" + effect: allow + principals: ["agent:market-research", "agent:playground"] + actions: + - "browser.navigate" + - "browser.click" + - "browser.type" + - "browser.screenshot" + - "browser.evaluate" + - "browser.wait" + - "browser.scroll" + - "browser.close" + resources: ["*"] + + # -------------------------------------------------------------------------- + # SHELL ALLOW RULES (Minimal) + # -------------------------------------------------------------------------- + + # Allow: Only read-only shell commands + - name: allow-safe-shell-readonly + description: "Allow minimal read-only shell commands" + effect: allow + principals: ["agent:*"] + actions: ["shell.exec"] + resources: + - "ls *" + - "pwd" + - "date" + - "whoami" + - "cat /data/*" + - "head /data/*" + - "tail /data/*" + - "wc *" + + # -------------------------------------------------------------------------- + # INTERNAL TOOLS ALLOW RULES + # -------------------------------------------------------------------------- + + # Allow: Internal agent tools (required for Claude Code operation) + - name: allow-internal-tools + description: "Allow internal agent tool operations" + effect: allow + principals: ["agent:*"] + actions: ["tool.*"] + resources: ["*"] + +# ============================================================================ +# DEFAULT DENY - Catch-all (must be last rule) +# ============================================================================ + + - name: default-deny-all + description: "DEFAULT DENY: Block any action not explicitly allowed above" + effect: deny + principals: ["*"] + actions: ["*"] + resources: ["*"] + +# ============================================================================ +# AUDIT CONFIGURATION +# ============================================================================ + +audit: + # Log all policy decisions + log_level: "info" + # Include denied requests in audit log + log_denials: true + # Include allowed requests in audit log + log_allows: true + # Redact sensitive data from logs + redact_patterns: + - "*password*" + - "*secret*" + - "*token*" + - "*api_key*" diff --git a/examples/real-openclaw-demo/run-playground.sh b/examples/real-openclaw-demo/run-playground.sh new file mode 100755 index 0000000..5fd898b --- /dev/null +++ b/examples/real-openclaw-demo/run-playground.sh @@ -0,0 +1,119 @@ +#!/bin/bash +# +# Run the AI Agent Playground Demo +# +# This script demonstrates: +# 1. Pre-Execution Authorization via Predicate Sidecar +# 2. Post-Execution Verification via Predicate Runtime +# +# Usage: +# ./run-playground.sh # Run the market research agent +# ./run-playground.sh --shell # Start interactive shell +# ./run-playground.sh --build # Force rebuild containers +# + +set -e + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +CYAN='\033[0;36m' +NC='\033[0m' # No Color + +# Parse arguments +SHELL_MODE=false +FORCE_BUILD=false + +for arg in "$@"; do + case $arg in + --shell) + SHELL_MODE=true + shift + ;; + --build) + FORCE_BUILD=true + shift + ;; + --help|-h) + echo "Usage: ./run-playground.sh [OPTIONS]" + echo "" + echo "Options:" + echo " --shell Start interactive shell instead of running agent" + echo " --build Force rebuild of Docker containers" + echo " --help Show this help message" + exit 0 + ;; + esac +done + +echo -e "${CYAN}" +echo "╔═══════════════════════════════════════════════════════════════════════════╗" +echo "║ ║" +echo "║ AI AGENT PLAYGROUND - Zero-Trust Demo ║" +echo "║ ║" +echo "║ Pre-Execution: Predicate Sidecar (Policy Enforcement) ║" +echo "║ Post-Execution: Predicate Runtime (State Verification) ║" +echo "║ Browser: PredicateBrowser (ML-Enhanced Snapshots) ║" +echo "║ ║" +echo "╚═══════════════════════════════════════════════════════════════════════════╝" +echo -e "${NC}" + +# Create data directory if it doesn't exist +mkdir -p data +mkdir -p workspace + +# Initialize leads.csv with header if it doesn't exist +if [ ! -f data/leads.csv ]; then + echo "rank,title,url,points,timestamp" > data/leads.csv + echo -e "${GREEN}Created data/leads.csv with header${NC}" +fi + +# Build containers if needed +if [ "$FORCE_BUILD" = true ]; then + echo -e "${YELLOW}Building containers (forced rebuild)...${NC}" + docker compose -f docker-compose.playground.yml build --no-cache +else + echo -e "${YELLOW}Building containers (if needed)...${NC}" + docker compose -f docker-compose.playground.yml build +fi + +# Start the sidecar +echo -e "${YELLOW}Starting Predicate Sidecar...${NC}" +docker compose -f docker-compose.playground.yml up -d predicate-sidecar + +# Wait for sidecar to be healthy +echo -e "${YELLOW}Waiting for sidecar health check...${NC}" +for i in {1..30}; do + if curl -sf http://localhost:8000/health > /dev/null 2>&1; then + echo -e "${GREEN}Sidecar is healthy!${NC}" + break + fi + if [ $i -eq 30 ]; then + echo -e "${RED}Sidecar failed to start${NC}" + docker compose -f docker-compose.playground.yml logs predicate-sidecar + exit 1 + fi + sleep 1 +done + +# Run the agent or start shell +if [ "$SHELL_MODE" = true ]; then + echo -e "${CYAN}Starting interactive shell...${NC}" + echo -e "${YELLOW}Run the agent with: npx tsx /app/examples/real-openclaw-demo/src/market-research-agent.ts${NC}" + echo "" + docker compose -f docker-compose.playground.yml run --rm agent-runtime bash +else + echo -e "${CYAN}Running Market Research Agent...${NC}" + echo "" + docker compose -f docker-compose.playground.yml run --rm agent-runtime +fi + +# Cleanup +echo "" +echo -e "${YELLOW}Stopping containers...${NC}" +docker compose -f docker-compose.playground.yml down + +echo "" +echo -e "${GREEN}Done! Check data/leads.csv for extracted leads.${NC}" diff --git a/examples/real-openclaw-demo/src/market-research-agent.ts b/examples/real-openclaw-demo/src/market-research-agent.ts new file mode 100644 index 0000000..5c76564 --- /dev/null +++ b/examples/real-openclaw-demo/src/market-research-agent.ts @@ -0,0 +1,1061 @@ +#!/usr/bin/env npx tsx +/** + * ============================================================================ + * Market Research Agent - OpenClaw Demo with LLM + * ============================================================================ + * + * This agent demonstrates the complete Predicate Authority architecture: + * + * ┌─────────────────────────────────────────────────────────────────────────┐ + * │ ZERO-TRUST AI AGENT ARCHITECTURE │ + * │ │ + * │ ┌───────────────┐ ┌─────────────────┐ ┌───────────────────────┐ │ + * │ │ LLM/Agent │───▶│ PRE-EXECUTION │───▶│ POST-EXECUTION │ │ + * │ │ (Claude) │ │ GATE │ │ VERIFICATION │ │ + * │ └───────────────┘ │ │ │ │ │ + * │ │ ┌─────────────┐ │ │ ┌───────────────────┐ │ │ + * │ │ │ Predicate │ │ │ │ Predicate Runtime │ │ │ + * │ │ │ Sidecar │ │ │ │ SDK │ │ │ + * │ │ │ Policy Check│ │ │ │ State Assertions │ │ │ + * │ │ └─────────────┘ │ │ └───────────────────┘ │ │ + * │ │ ↓ │ │ ↓ │ │ + * │ │ ALLOW / DENY │ │ PASS / FAIL │ │ + * │ └─────────────────┘ └───────────────────────┘ │ + * └─────────────────────────────────────────────────────────────────────────┘ + * + * GOAL: Navigate to Hacker News, extract top 3 posts, save to CSV + * + * FEATURES: + * - Uses Claude (Anthropic) LLM for intelligent page understanding + * - SentienceAgent for observe-think-act loop + * - Pre-execution authorization via Predicate Sidecar + * - Post-execution verification via Predicate Runtime + * - ML-enhanced snapshots for compact LLM prompts + * + * @requires @predicatesystems/runtime (PredicateBrowser, SentienceAgent, AnthropicProvider) + * @requires Predicate Sidecar running at http://predicate-sidecar:8000 + * @requires ANTHROPIC_API_KEY environment variable + */ + +import type { Page } from "playwright"; +import * as fs from "fs"; +import * as path from "path"; +import * as os from "os"; +import { + PredicateBrowser, + SentienceAgent, + AnthropicProvider, + AgentRuntime, + Tracer, + createTracer, + exists, + urlContains, + backends, + type Snapshot, + type LLMResponse, + type SnapshotOptions, +} from "@predicatesystems/runtime"; +import { PredicateSidecarClient, createPredicateClient } from "./predicate-sidecar-client.js"; +import { + PredicateRuntime, + createPredicateRuntime, + url_contains, + dom_contains, + element_exists, + confidence_above, + VerificationError, +} from "./predicate-runtime.js"; + +// ============================================================================ +// Configuration +// ============================================================================ + +const CONFIG = { + // Target URL + targetUrl: "https://news.ycombinator.com/show", + + // Output file (ONLY this file is allowed by policy) + leadsFile: "/data/leads.csv", + + // Sidecar configuration + sidecarUrl: process.env.PREDICATE_SIDECAR_URL || "http://predicate-sidecar:8000", + principal: process.env.SECURECLAW_PRINCIPAL || "agent:market-research", + + // Anthropic API Key + anthropicApiKey: process.env.ANTHROPIC_API_KEY || "", + + // Predicate API Key (for cloud tracing - uses local tracing if not set) + predicateApiKey: process.env.PREDICATE_API_KEY || "", + + // LLM Model + llmModel: process.env.LLM_MODEL || "claude-sonnet-4-20250514", + + // Verbose logging + verbose: process.env.SECURECLAW_VERBOSE === "true" || true, + + // Number of posts to extract + topN: 3, + + // Headless mode (always true for container execution) + headless: true, +}; + +// ============================================================================ +// Terminal Colors +// ============================================================================ + +const colors = { + reset: "\x1b[0m", + bright: "\x1b[1m", + dim: "\x1b[2m", + green: "\x1b[32m", + red: "\x1b[31m", + yellow: "\x1b[33m", + blue: "\x1b[34m", + cyan: "\x1b[36m", + magenta: "\x1b[35m", + bgGreen: "\x1b[42m", + bgRed: "\x1b[41m", + bgBlue: "\x1b[44m", + bgYellow: "\x1b[43m", +}; + +function logSection(title: string): void { + console.log(""); + console.log(`${colors.cyan}${"═".repeat(70)}${colors.reset}`); + console.log(`${colors.cyan}║${colors.reset} ${colors.bright}${title}${colors.reset}`); + console.log(`${colors.cyan}${"═".repeat(70)}${colors.reset}`); + console.log(""); +} + +function logStep(step: number, title: string): void { + console.log(`${colors.blue}[Step ${step}]${colors.reset} ${colors.bright}${title}${colors.reset}`); +} + +function logSuccess(message: string): void { + console.log(`${colors.green} ✓ ${message}${colors.reset}`); +} + +function logError(message: string): void { + console.log(`${colors.red} ✗ ${message}${colors.reset}`); +} + +function logInfo(message: string): void { + console.log(`${colors.dim} → ${message}${colors.reset}`); +} + +function logLLM(message: string): void { + console.log(`${colors.magenta} 🤖 ${message}${colors.reset}`); +} + +// ============================================================================ +// Lead Data Type +// ============================================================================ + +interface Lead { + rank: number; + title: string; + url: string; + points: number; + timestamp: string; +} + +// Extended Snapshot type with text property for LLM use +// The promptBlock from PredicateContext is stored here +interface SnapshotWithText extends Snapshot { + text?: string; +} + +// ============================================================================ +// Main Agent Class +// ============================================================================ + +// Browser-use compatible session wrapper for PredicateContext +// This wraps a Playwright page to provide the getOrCreateCdpSession method +function createBrowserUseSession(page: Page) { + let cdpSession: any = null; + + return { + async getOrCreateCdpSession() { + if (!cdpSession) { + const context = page.context(); + cdpSession = await context.newCDPSession(page); + } + + // Create a proxy that matches browser-use's CDP client interface + const cdpClient = { + send: new Proxy( + {}, + { + get(_target: any, domain: string) { + return new Proxy( + {}, + { + get(_innerTarget: any, method: string) { + return async (options: { params?: unknown; session_id?: string }) => { + const fullMethod = `${domain}.${method}`; + const result = await cdpSession.send(fullMethod, options.params || {}); + return result; + }; + }, + } + ); + }, + } + ), + }; + + return { cdpClient, sessionId: `playwright-${Date.now()}` }; + }, + }; +} + +class MarketResearchAgent { + private sidecar: PredicateSidecarClient; + private runtime: PredicateRuntime; + private agentRuntime: AgentRuntime | null = null; + private tracer: Tracer | null = null; + private predicateBrowser: PredicateBrowser | null = null; + private predicateContext: backends.PredicateContext | null = null; + private browserSession: ReturnType | null = null; + private sentienceAgent: SentienceAgent | null = null; + private llmProvider: AnthropicProvider | null = null; + private page: Page | null = null; + + // Token usage tracking + private tokenUsage = { + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + }; + + constructor() { + // Validate Anthropic API Key + if (!CONFIG.anthropicApiKey) { + throw new Error( + "ANTHROPIC_API_KEY environment variable is required.\n" + + "Get your key from: https://console.anthropic.com/settings/keys" + ); + } + + // Initialize Pre-Execution Gate (Sidecar Client) + this.sidecar = createPredicateClient({ + sidecarUrl: CONFIG.sidecarUrl, + principal: CONFIG.principal, + verbose: CONFIG.verbose, + failClosed: true, + }); + + // Initialize Post-Execution Verification (Runtime) + this.runtime = createPredicateRuntime({ + verbose: CONFIG.verbose, + }); + + // Initialize Anthropic LLM Provider + this.llmProvider = new AnthropicProvider(CONFIG.anthropicApiKey, CONFIG.llmModel); + logInfo(`LLM Provider initialized: ${CONFIG.llmModel}`); + } + + /** + * Track token usage from LLM response + */ + private trackTokenUsage(response: LLMResponse): void { + if (response.promptTokens) { + this.tokenUsage.promptTokens += response.promptTokens; + } + if (response.completionTokens) { + this.tokenUsage.completionTokens += response.completionTokens; + } + if (response.totalTokens) { + this.tokenUsage.totalTokens += response.totalTokens; + } + } + + /** + * Take ML-enhanced snapshot using PredicateContext (predicate-snapshot skill pattern) + * This uses CDP directly and doesn't require the Chrome extension + * + * @param options - SnapshotOptions for screenshot, use_api, etc. + * @returns SnapshotWithText - Snapshot with text property for LLM use + */ + private async takeMLSnapshot(options: SnapshotOptions = {}): Promise { + // Default options for cloud tracing: include screenshot + const snapshotOptions: SnapshotOptions = { + screenshot: options.screenshot ?? { format: "jpeg", quality: 80 }, + use_api: options.use_api ?? true, + limit: options.limit ?? 50, + ...options, + }; + + if (this.predicateContext && this.browserSession) { + try { + const result = await this.predicateContext.build(this.browserSession); + + // SentienceContextState has: { url, snapshot, promptBlock } + // The snapshot property contains the full Snapshot with elements + const hasContent = result && (result.promptBlock || result.snapshot?.elements?.length); + + if (!hasContent) { + logError("ML snapshot returned empty content, falling back to local"); + throw new Error("Empty ML snapshot"); + } + + // Return the snapshot from result, with text set to promptBlock for LLM use + // Also take a screenshot using PredicateBrowser if requested + let screenshotData: string | undefined; + if (snapshotOptions.screenshot && this.predicateBrowser) { + try { + const browserSnap = await this.predicateBrowser.snapshot(snapshotOptions); + screenshotData = browserSnap.screenshot; + } catch { + // Screenshot is optional - continue without it + } + } + + const snapshot: SnapshotWithText = { + ...result.snapshot, + text: result.promptBlock || "", + screenshot: screenshotData, + screenshot_format: screenshotData ? "jpeg" : undefined, + }; + + return snapshot; + } catch (error) { + logError(`ML snapshot failed: ${(error as Error).message}, falling back to local`); + } + } + + // Fallback to PredicateBrowser snapshot (local, no ML) with screenshot options + if (this.predicateBrowser) { + const snap = await this.predicateBrowser.snapshot(snapshotOptions); + // Convert elements to text for LLM use + const text = snap.elements?.map((el: { text?: string; aria_label?: string }) => + el.text || el.aria_label || "" + ).filter(Boolean).join("\n") || ""; + return { ...snap, text }; + } + + throw new Error("No snapshot method available"); + } + + /** + * Initialize tracer using SDK's createTracer factory + * Automatically handles Cloud vs Local tracing based on PREDICATE_API_KEY + */ + private async initializeTracer(): Promise { + // Use SDK's createTracer which handles cloud init, fallback, and run_start emission + this.tracer = await createTracer({ + apiKey: CONFIG.predicateApiKey || undefined, + goal: `Extract top ${CONFIG.topN} posts from Hacker News`, + agentType: "MarketResearchAgent", + llmModel: CONFIG.llmModel, + startUrl: CONFIG.targetUrl, + autoEmitRunStart: true, // SDK auto-emits run_start with metadata + }); + + logSuccess(`Tracer initialized (Run ID: ${this.tracer.runId})`); + } + + // -------------------------------------------------------------------------- + // Main Execution Flow + // -------------------------------------------------------------------------- + + async run(): Promise { + logSection("MARKET RESEARCH AGENT - Zero-Trust Demo with LLM"); + + console.log(`${colors.dim}Configuration:${colors.reset}`); + console.log(` Target: ${CONFIG.targetUrl}`); + console.log(` Output: ${CONFIG.leadsFile}`); + console.log(` Sidecar: ${CONFIG.sidecarUrl}`); + console.log(` Principal: ${CONFIG.principal}`); + console.log(` LLM: ${CONFIG.llmModel}`); + console.log(` Headless: ${CONFIG.headless}`); + console.log(""); + + try { + // ====================================================================== + // Initialize Tracer (Cloud if API key set, Local otherwise) + // ====================================================================== + logInfo("Initializing tracer..."); + await this.initializeTracer(); + + // ====================================================================== + // STEP 1: Launch Browser (Pre-Execution Gate) + // ====================================================================== + logStep(1, "Launching headless browser with LLM agent"); + await this.launchBrowser(); + + // ====================================================================== + // STEP 2: Navigate to Hacker News using LLM Agent + // ====================================================================== + logStep(2, "Navigating to Hacker News Show HN (LLM-guided)"); + await this.navigateToTarget(); + + // ====================================================================== + // STEP 3: Verify Page State (Post-Execution Verification) + // ====================================================================== + logStep(3, "Verifying page state before extraction"); + await this.verifyPageState(); + + // ====================================================================== + // STEP 4: Extract Top Posts using LLM + // ====================================================================== + logStep(4, `Extracting top ${CONFIG.topN} posts (LLM-assisted)`); + const leads = await this.extractLeadsWithLLM(); + + // ====================================================================== + // STEP 5: Load Existing Leads (Pre-Execution Gate) + // ====================================================================== + logStep(5, "Loading existing leads from CSV"); + const existingLeads = await this.loadExistingLeads(); + + // ====================================================================== + // STEP 6: Check for New Leads + // ====================================================================== + logStep(6, "Checking for new/changed leads"); + const newLeads = this.findNewLeads(leads, existingLeads); + + if (newLeads.length === 0) { + logInfo("No new leads found - top 3 posts are the same as last run"); + logInfo("Skipping CSV update"); + } else { + logSuccess(`Found ${newLeads.length} new lead(s)!`); + + // ================================================================== + // STEP 7: Save to CSV (Pre-Execution Gate) + // ================================================================== + logStep(7, "Saving leads to CSV (Pre-Execution Gate)"); + // Pass current leads + existing leads for deduplication and merge + await this.saveLeadsToCSV(leads, existingLeads); + } + + // ====================================================================== + // STEP 8: Cleanup + // ====================================================================== + logStep(8, "Cleanup"); + await this.cleanup(); + + logSection("AGENT COMPLETED SUCCESSFULLY"); + + // Print token usage stats + console.log(`${colors.dim}Token Usage (LLM calls):${colors.reset}`); + console.log(` Prompt tokens: ${this.tokenUsage.promptTokens}`); + console.log(` Completion tokens: ${this.tokenUsage.completionTokens}`); + console.log(` Total tokens: ${this.tokenUsage.totalTokens}`); + + // Print tracer info + if (this.tracer) { + console.log(`${colors.dim}Tracer:${colors.reset}`); + console.log(` Run ID: ${this.tracer.runId}`); + if (CONFIG.predicateApiKey) { + console.log(` Mode: Cloud (uploaded to Predicate Studio)`); + } else { + console.log(` Mode: Local (saved to ./traces/)`); + } + } + } catch (error) { + if (error instanceof VerificationError) { + logError(`Post-Execution Verification Failed: ${error.message}`); + console.log("\nFailed assertions:"); + for (const assertion of error.result.assertions.filter((a) => !a.passed)) { + console.log(` - ${assertion.label}: ${assertion.reason}`); + } + } else { + logError(`Agent Error: ${(error as Error).message}`); + console.error(error); + } + + await this.cleanup(); + process.exit(1); + } + } + + // -------------------------------------------------------------------------- + // Step Implementations + // -------------------------------------------------------------------------- + + /** + * STEP 1: Launch Browser with LLM Agent + * + * PRE-EXECUTION GATE: + * - Action: browser.launch + * - Policy: allow-browser-launch (requires headless: true) + * + * Initializes: + * - PredicateBrowser with Chrome extension for ML-enhanced snapshots + * - SentienceAgent with AnthropicProvider for LLM-driven automation + */ + private async launchBrowser(): Promise { + logInfo("Requesting browser launch through Pre-Execution Gate..."); + + // Pre-execution authorization + const authResult = await this.sidecar.authorize({ + action: "browser.launch", + resource: "*", + params: { headless: CONFIG.headless, browser_type: "chromium" }, + }); + + if (!authResult.allowed) { + throw new Error(`Browser launch denied: ${authResult.reason}`); + } + + logSuccess("Browser launch AUTHORIZED by policy"); + + // Launch PredicateBrowser with Chrome extension + // Pass PREDICATE_API_KEY to enable ML-enhanced snapshots (predicate-snapshot skill pattern) + this.predicateBrowser = new PredicateBrowser( + CONFIG.predicateApiKey || undefined, // apiKey for ML-enhanced snapshots + undefined, // apiUrl (uses default) + CONFIG.headless, + ); + + if (CONFIG.predicateApiKey) { + logInfo("ML-enhanced snapshots enabled (PREDICATE_API_KEY set)"); + } else { + logInfo("Using local snapshots (PREDICATE_API_KEY not set)"); + } + + await this.predicateBrowser.start(); + this.page = this.predicateBrowser.getPage(); + + if (!this.page) { + throw new Error("Failed to get page from PredicateBrowser"); + } + + // Initialize PredicateContext for ML-enhanced snapshots (predicate-snapshot skill pattern) + // This uses CDP directly and doesn't require the Chrome extension + if (CONFIG.predicateApiKey) { + this.browserSession = createBrowserUseSession(this.page); + this.predicateContext = new backends.PredicateContext({ + predicateApiKey: CONFIG.predicateApiKey, + topElementSelector: { + byImportance: 50, + fromDominantGroup: 15, + byPosition: 10, + }, + }); + logInfo("PredicateContext initialized for ML-enhanced snapshots"); + } + + // Initialize SentienceAgent with LLM + this.sentienceAgent = new SentienceAgent( + this.predicateBrowser, + this.llmProvider!, + 50, // snapshotLimit + CONFIG.verbose, + ); + + // Create browser adapter for AgentRuntime + // Use ML-enhanced snapshot if available, otherwise fall back to PredicateBrowser + // Pass through SnapshotOptions for screenshot, use_api, etc. + const browserAdapter = { + snapshot: async (_page: Page, options?: SnapshotOptions): Promise => { + return await this.takeMLSnapshot(options); + }, + }; + + // Initialize AgentRuntime with tracer for SDK-based verification + if (!this.tracer) { + throw new Error("Tracer not initialized - call initializeTracer() first"); + } + this.agentRuntime = new AgentRuntime(browserAdapter, this.page, this.tracer); + + // Attach legacy runtime to page for verification + this.runtime.attach(this.page); + + logSuccess("PredicateBrowser launched with Chrome extension (headless mode)"); + logSuccess(`SentienceAgent initialized with ${CONFIG.llmModel}`); + logSuccess(`AgentRuntime initialized with Cloud Tracer`); + } + + /** + * STEP 2: Navigate to Target using LLM Agent + * + * PRE-EXECUTION GATE: + * - Action: browser.navigate + * - Policy: allow-research-sites + * + * Uses SentienceAgent.act() for LLM-driven navigation + */ + private async navigateToTarget(): Promise { + if (!this.predicateBrowser || !this.page || !this.sentienceAgent || !this.agentRuntime) { + throw new Error("Browser/Agent not initialized"); + } + + // Start step tracking for navigation + this.agentRuntime.beginStep("navigate_to_hackernews", 2); + + logInfo(`Requesting navigation to ${CONFIG.targetUrl}...`); + + // Pre-execution authorization + const authResult = await this.sidecar.authorize({ + action: "browser.navigate", + resource: CONFIG.targetUrl, + }); + + if (!authResult.allowed) { + throw new Error(`Navigation denied: ${authResult.reason}`); + } + + logSuccess("Navigation AUTHORIZED by policy"); + + // Navigate using PredicateBrowser + await this.predicateBrowser.goto(CONFIG.targetUrl); + await this.page.waitForTimeout(1000); + + logSuccess(`Navigated to ${this.page.url()}`); + + // Use LLM to verify we're on the right page + logLLM("Asking LLM to verify page content..."); + const snapshot = await this.takeMLSnapshot(); + + const verifyResponse = await this.llmProvider!.generate( + "You are a web page analyzer. Respond with JSON only.", + `Analyze this page snapshot and confirm if this is the Hacker News "Show HN" page. + +Page URL: ${this.page.url()} +Page content (first 2000 chars): ${snapshot.text?.slice(0, 2000) || "No text content"} + +Respond with JSON: {"isShowHN": true/false, "reason": "brief explanation"}`, + { max_tokens: 200 } + ); + + // Track token usage + this.trackTokenUsage(verifyResponse); + + logLLM(`LLM Response: ${verifyResponse.content.slice(0, 100)}...`); + + // Emit step_end for navigation + this.agentRuntime.endStep({ + action: "navigate", + success: true, + outcome: `Navigated to ${this.page.url()}`, + }); + } + + /** + * STEP 3: Verify Page State + * + * POST-EXECUTION VERIFICATION using SDK predicates with .eventually() pattern + * This handles delayed hydration common in modern SPAs + */ + private async verifyPageState(): Promise { + if (!this.predicateBrowser || !this.page || !this.agentRuntime) { + throw new Error("Browser not initialized"); + } + + // Start step tracking for telemetry BEFORE taking snapshot + this.agentRuntime.beginStep("verify_page_state", 3); + + logInfo("Taking ML-enhanced snapshot with screenshot for verification..."); + + // Use agentRuntime.snapshot() which auto-emits trace events with screenshot + // This enables Studio visualization with screenshot_base64 + const snapshot: Snapshot = await this.agentRuntime.snapshot({ + screenshot: { format: "jpeg", quality: 80 }, + use_api: true, + limit: 50, + emitTrace: true, // Auto-emit snapshot trace event for Studio + }); + logInfo(`Snapshot captured: ${snapshot.elements?.length || 0} elements, screenshot: ${snapshot.screenshot ? "yes" : "no"}`); + + // Use SDK predicates with .eventually() for delayed hydration handling + // This is the recommended pattern from predicate-snapshot skill + logInfo("Waiting for page content to hydrate using SDK predicates..."); + + // Verify URL contains expected domain using .eventually() + const urlValid = await this.agentRuntime.check( + urlContains("news.ycombinator.com"), + "url_contains_hackernews", + true // required + ).eventually({ + timeoutMs: 10000, + pollMs: 500, + }); + + if (!urlValid) { + throw new Error("URL verification failed - not on Hacker News"); + } + logSuccess("URL verified: news.ycombinator.com"); + + // Verify page has interactive elements using .eventually() + // Use clickable=true since ML snapshot captures clickable elements + const postsLoaded = await this.agentRuntime.check( + exists("clickable=true"), // Page has clickable elements (links, buttons) + "interactive_elements_visible", + true // required + ).eventually({ + timeoutMs: 10000, + pollMs: 500, + }); + + if (!postsLoaded) { + throw new Error("Interactive elements did not load within timeout"); + } + logSuccess("Interactive elements verified: content loaded"); + + // Also run legacy runtime verification for comparison + await this.runtime.snapshot({ includeScreenshot: false }); + + await this.runtime.verify_state( + url_contains("news.ycombinator.com", { required: true }), + dom_contains("Show", { required: true }), + element_exists("titleline", { required: true, minCount: 3 }), + confidence_above(0.7, { required: false }), + ); + + logSuccess("Page state VERIFIED - safe to extract data"); + + // Emit step_end event for Studio visualization + this.agentRuntime.endStep({ + action: "verify_page_state", + success: true, + outcome: "Page state verified - URL and interactive elements confirmed", + }); + } + + /** + * STEP 4: Extract Leads using LLM + * + * Uses the LLM to intelligently parse and extract post information + * Integrates with AgentRuntime for step tracking and telemetry + */ + private async extractLeadsWithLLM(): Promise { + if (!this.page || !this.predicateBrowser || !this.llmProvider || !this.agentRuntime) { + throw new Error("Browser/LLM not initialized"); + } + + // Start step tracking for telemetry + this.agentRuntime.beginStep("extract_leads", 4); + + logInfo(`Asking LLM to extract top ${CONFIG.topN} posts...`); + + // Take a fresh ML-enhanced snapshot for the LLM + const snapshot = await this.takeMLSnapshot(); + + // Ask LLM to extract structured data + const extractPrompt = `You are a data extraction assistant. Extract the top ${CONFIG.topN} posts from this Hacker News "Show HN" page. + +Page content: +${snapshot.text?.slice(0, 8000) || "No content available"} + +Extract the top ${CONFIG.topN} posts and return them as a JSON array with this structure: +[ + { + "rank": 1, + "title": "Post title here", + "url": "https://example.com", + "points": 123 + } +] + +IMPORTANT: +- Only include "Show HN" posts if visible +- Extract the actual URLs from the posts +- Include the point count for each post +- Return ONLY the JSON array, no other text`; + + logLLM("Sending extraction request to Claude..."); + + const response = await this.llmProvider.generate( + "You are a precise data extraction assistant. Return only valid JSON.", + extractPrompt, + { max_tokens: 1000 } + ); + + // Track token usage + this.trackTokenUsage(response); + + logLLM(`LLM used ${response.totalTokens || 0} tokens for extraction`); + + // Parse the LLM response + let extractedLeads: Lead[] = []; + try { + // Try to extract JSON from the response + const jsonMatch = response.content.match(/\[[\s\S]*\]/); + if (jsonMatch) { + const parsed = JSON.parse(jsonMatch[0]); + extractedLeads = parsed.map((item: any, index: number) => ({ + rank: item.rank || index + 1, + title: item.title || "Unknown", + url: item.url || "", + points: item.points || 0, + timestamp: new Date().toISOString(), + })); + } + } catch (parseError) { + logError(`Failed to parse LLM response: ${(parseError as Error).message}`); + logInfo("Falling back to DOM extraction..."); + return this.extractLeadsFromDOM(); + } + + // Log extracted leads + for (const lead of extractedLeads) { + logSuccess(`#${lead.rank}: "${lead.title.slice(0, 50)}..." (${lead.points} points)`); + } + + // If LLM extraction failed, fall back to DOM + if (extractedLeads.length === 0) { + logInfo("LLM extraction returned empty, falling back to DOM..."); + const domLeads = await this.extractLeadsFromDOM(); + // Emit step_end for DOM extraction path + this.agentRuntime.endStep({ + action: "extract_leads_dom_fallback", + success: domLeads.length > 0, + outcome: `Extracted ${domLeads.length} leads via DOM fallback`, + }); + return domLeads; + } + + // Emit step_end event for successful LLM extraction + this.agentRuntime.endStep({ + action: "extract_leads_llm", + success: true, + outcome: `Extracted ${extractedLeads.length} leads via LLM`, + }); + + return extractedLeads; + } + + /** + * Fallback: Extract leads directly from DOM + */ + private async extractLeadsFromDOM(): Promise { + if (!this.page) throw new Error("Browser not initialized"); + + logInfo(`Extracting top ${CONFIG.topN} posts from DOM...`); + + const leads = await this.page.evaluate((topN: number) => { + const results: Lead[] = []; + const rows = document.querySelectorAll("tr.athing"); + + for (let i = 0; i < Math.min(topN, rows.length); i++) { + const row = rows[i]; + const titleLink = row.querySelector(".titleline > a") as HTMLAnchorElement; + const subtext = row.nextElementSibling?.querySelector(".subtext"); + const scoreSpan = subtext?.querySelector(".score"); + + if (titleLink) { + results.push({ + rank: i + 1, + title: titleLink.textContent?.trim() || "Unknown", + url: titleLink.href, + points: parseInt(scoreSpan?.textContent || "0") || 0, + timestamp: new Date().toISOString(), + }); + } + } + + return results; + }, CONFIG.topN); + + for (const lead of leads) { + logSuccess(`#${lead.rank}: "${lead.title.slice(0, 50)}..." (${lead.points} points)`); + } + + return leads; + } + + /** + * STEP 5: Load Existing Leads + * + * ZERO-TRUST ARCHITECTURE: + * 1. Request authorization from sidecar via /authorize + * 2. If ALLOWED, execute locally (sidecar is authorize-only mode) + * 3. If DENIED or sidecar unavailable, FAIL CLOSED - no fallback + */ + private async loadExistingLeads(): Promise { + logInfo(`Reading existing leads from ${CONFIG.leadsFile}...`); + + // Step 1: Request authorization from sidecar + const authResult = await this.sidecar.readFile(CONFIG.leadsFile); + + // Step 2: Check authorization result - FAIL CLOSED + if (!authResult.allowed) { + // Check if file doesn't exist (expected on first run) + if (authResult.error?.includes("ENOENT") || authResult.error?.includes("not found")) { + logInfo("No existing CSV found - this is the first run"); + return []; + } + // Policy denial or sidecar error - FAIL CLOSED + throw new Error(`[ZERO-TRUST] File read DENIED: ${authResult.error}`); + } + + // Step 3: Authorization granted - execute locally + logSuccess("File read AUTHORIZED by policy"); + + let csvData: string; + try { + if (!fs.existsSync(CONFIG.leadsFile)) { + logInfo("No existing CSV found - this is the first run"); + return []; + } + csvData = fs.readFileSync(CONFIG.leadsFile, "utf-8"); + } catch (err) { + // File read error after authorization - this is an OS error, not policy + logInfo(`File read error: ${(err as Error).message}`); + return []; + } + + if (!csvData || csvData.trim() === "") { + return []; + } + + const lines = csvData.trim().split("\n"); + if (lines.length <= 1) { + return []; + } + + const leads: Lead[] = []; + for (let i = 1; i < lines.length; i++) { + const [rank, title, url, points, timestamp] = lines[i].split(",").map((s) => s.trim()); + if (title) { + leads.push({ + rank: parseInt(rank) || i, + title: title.replace(/^"|"$/g, ""), + url: url?.replace(/^"|"$/g, "") || "", + points: parseInt(points) || 0, + timestamp: timestamp || "", + }); + } + } + + logSuccess(`Loaded ${leads.length} existing lead(s)`); + return leads; + } + + /** + * STEP 6: Find New Leads + * Compare by URL (more stable than titles, which LLMs may truncate) + */ + private findNewLeads(current: Lead[], existing: Lead[]): Lead[] { + // Normalize URLs for comparison (remove trailing slashes, etc.) + const normalizeUrl = (url: string) => url.replace(/\/+$/, "").toLowerCase(); + const existingUrls = new Set(existing.map((l) => normalizeUrl(l.url))); + return current.filter((lead) => !existingUrls.has(normalizeUrl(lead.url))); + } + + /** + * STEP 7: Save to CSV + * + * ZERO-TRUST ARCHITECTURE: + * 1. Request authorization from sidecar via /authorize + * 2. If ALLOWED, execute locally (sidecar is authorize-only mode) + * 3. If DENIED or sidecar unavailable, FAIL CLOSED - no fallback + * + * NOTE: This writes the full file (not append) to avoid duplicates. + * New leads are merged with existing leads, deduplicated by URL. + */ + private async saveLeadsToCSV(newLeads: Lead[], existingLeads: Lead[] = []): Promise { + // Merge new leads with existing, keeping new leads first (they're the current top N) + // Deduplicate by URL + const normalizeUrl = (url: string) => url.replace(/\/+$/, "").toLowerCase(); + const seenUrls = new Set(); + const allLeads: Lead[] = []; + + // Add new leads first + for (const lead of newLeads) { + const normUrl = normalizeUrl(lead.url); + if (!seenUrls.has(normUrl)) { + seenUrls.add(normUrl); + allLeads.push(lead); + } + } + + // Add existing leads that aren't duplicates + for (const lead of existingLeads) { + const normUrl = normalizeUrl(lead.url); + if (!seenUrls.has(normUrl)) { + seenUrls.add(normUrl); + allLeads.push(lead); + } + } + + logInfo(`Writing ${allLeads.length} total lead(s) to ${CONFIG.leadsFile} (${newLeads.length} new)...`); + + // Format CSV with header + const header = "rank,title,url,points,timestamp"; + const rows = allLeads.map((l) => + `${l.rank},"${l.title.replace(/"/g, '""')}","${l.url}",${l.points},${l.timestamp}` + ); + const csvContent = header + "\n" + rows.join("\n") + "\n"; + + // Step 1: Request authorization from sidecar + const authResult = await this.sidecar.writeFile(CONFIG.leadsFile, csvContent); + + // Step 2: Check authorization result - FAIL CLOSED + if (!authResult.allowed) { + throw new Error(`[ZERO-TRUST] File write DENIED: ${authResult.error}`); + } + + // Step 3: Authorization granted - execute locally + logSuccess("File write AUTHORIZED by policy"); + + try { + // Ensure directory exists + const dir = path.dirname(CONFIG.leadsFile); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + } + fs.writeFileSync(CONFIG.leadsFile, csvContent); + logSuccess(`Saved ${allLeads.length} lead(s) to CSV`); + } catch (err) { + throw new Error(`File write failed after authorization: ${(err as Error).message}`); + } + + // Demo: Show policy enforcement for unauthorized write + logInfo("Demo: Attempting unauthorized write to /etc/passwd..."); + const unauthorizedResult = await this.sidecar.writeFile("/etc/passwd", "hacked"); + if (!unauthorizedResult.allowed) { + logSuccess(`BLOCKED by policy: ${unauthorizedResult.error}`); + } + } + + /** + * STEP 8: Cleanup + */ + private async cleanup(): Promise { + if (this.predicateBrowser) { + await this.predicateBrowser.close(); + logSuccess("PredicateBrowser closed"); + } + + // Close tracer (uploads to cloud if cloud tracing is enabled) + if (this.tracer) { + await this.tracer.close(); + logSuccess("Tracer closed" + (CONFIG.predicateApiKey ? " (trace uploaded to cloud)" : "")); + } + } +} + +// ============================================================================ +// Entry Point +// ============================================================================ + +async function main(): Promise { + console.log(` +${colors.bright}${colors.magenta} +╔═══════════════════════════════════════════════════════════════════════════╗ +║ ║ +║ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ █████╗ ████████╗███████╗ ║ +║ ██╔══██╗██╔══██╗██╔════╝██╔══██╗██║██╔════╝██╔══██╗╚══██╔══╝██╔════╝ ║ +║ ██████╔╝██████╔╝█████╗ ██║ ██║██║██║ ███████║ ██║ █████╗ ║ +║ ██╔═══╝ ██╔══██╗██╔══╝ ██║ ██║██║██║ ██╔══██║ ██║ ██╔══╝ ║ +║ ██║ ██║ ██║███████╗██████╔╝██║╚██████╗██║ ██║ ██║ ███████╗ ║ +║ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═════╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝ ╚══════╝ ║ +║ ║ +║ Zero-Trust AI Agent with Claude LLM ║ +║ Pre-Execution Gate + Post-Execution Verification ║ +║ ║ +╚═══════════════════════════════════════════════════════════════════════════╝ +${colors.reset}`); + + const agent = new MarketResearchAgent(); + await agent.run(); +} + +// Run the agent +main().catch((error) => { + console.error(`${colors.red}Fatal error: ${error.message}${colors.reset}`); + process.exit(1); +}); diff --git a/examples/real-openclaw-demo/src/predicate-runtime.ts b/examples/real-openclaw-demo/src/predicate-runtime.ts new file mode 100644 index 0000000..9ef3cdc --- /dev/null +++ b/examples/real-openclaw-demo/src/predicate-runtime.ts @@ -0,0 +1,499 @@ +/** + * ============================================================================ + * Predicate Runtime - Post-Execution Verification Layer + * ============================================================================ + * + * ARCHITECTURE: Mathematical State Verification + * + * After browser actions execute, we use the Predicate Runtime SDK to + * VERIFY that the world state matches our expectations before proceeding. + * + * This provides defense-in-depth: + * - Pre-Execution: Policy check (did we have permission?) + * - Post-Execution: State verification (did the action achieve its goal?) + * + * ┌─────────────┐ + * │ Agent │ + * │ (OpenClaw) │ + * └──────┬──────┘ + * │ 1. Execute browser action + * ▼ + * ┌─────────────┐ + * │ Playwright │ ───▶ DOM changes + * └──────┬──────┘ + * │ 2. Take snapshot + * ▼ + * ┌─────────────────────────────────────────────┐ + * │ PREDICATE RUNTIME │ + * │ ┌───────────────────────────────────────┐ │ + * │ │ verify_state() │ │ + * │ ├───────────────────────────────────────┤ │ + * │ │ • url_contains("news.ycombinator") │ │ + * │ │ • dom_contains("Show HN") │ │ + * │ │ • element_exists("span.titleline") │ │ + * │ │ • ML snapshot confidence > 0.8 │ │ + * │ └───────────────────────────────────────┘ │ + * │ │ │ + * │ ┌──────────┴──────────┐ │ + * │ ▼ ▼ │ + * │ ┌─────────┐ ┌──────────┐ │ + * │ │ PASS ✓ │ │ FAIL ✗ │ │ + * │ │ Continue│ │ Halt/Retry│ │ + * │ └─────────┘ └──────────┘ │ + * └─────────────────────────────────────────────┘ + * + * CRITICAL: The agent MUST pass verification before extracting data. + * This prevents operating on incorrect/malicious page states. + */ + +import type { Page } from "playwright"; + +// ============================================================================ +// Types & Interfaces +// ============================================================================ + +export interface Snapshot { + /** Current page URL */ + url: string; + /** Page title */ + title: string; + /** Simplified DOM tree for element queries */ + elements: SnapshotElement[]; + /** Screenshot as base64 (for ML verification) */ + screenshot_base64?: string; + /** ML-computed confidence score (0-1) */ + confidence?: number; + /** Timestamp of snapshot */ + timestamp: string; + /** Diagnostic info */ + diagnostics?: { + element_count: number; + load_time_ms: number; + captcha_detected?: boolean; + }; +} + +export interface SnapshotElement { + id: number; + role: string; + name?: string; + text?: string; + selector?: string; + attributes?: Record; +} + +export interface VerificationResult { + /** Overall verification passed */ + passed: boolean; + /** Individual assertion results */ + assertions: AssertionResult[]; + /** Snapshot used for verification */ + snapshot: Snapshot; + /** Summary reason for pass/fail */ + reason: string; +} + +export interface AssertionResult { + /** Assertion label */ + label: string; + /** Whether this assertion passed */ + passed: boolean; + /** Why it passed/failed */ + reason: string; + /** Is this assertion required for overall pass? */ + required: boolean; + /** Additional details */ + details?: Record; +} + +export type Predicate = (snapshot: Snapshot) => AssertionResult; + +// ============================================================================ +// Predicate Functions (Verification Primitives) +// ============================================================================ + +/** + * Verify that the current URL contains a substring. + */ +export function url_contains(substring: string, options: { required?: boolean } = {}): Predicate { + return (snapshot: Snapshot): AssertionResult => { + const passed = snapshot.url.includes(substring); + return { + label: `url_contains("${substring}")`, + passed, + reason: passed + ? `URL "${snapshot.url}" contains "${substring}"` + : `URL "${snapshot.url}" does NOT contain "${substring}"`, + required: options.required ?? true, + details: { url: snapshot.url, expected: substring }, + }; + }; +} + +/** + * Verify that the DOM contains specific text content. + */ +export function dom_contains(text: string, options: { required?: boolean; caseSensitive?: boolean } = {}): Predicate { + return (snapshot: Snapshot): AssertionResult => { + const caseSensitive = options.caseSensitive ?? false; + const searchText = caseSensitive ? text : text.toLowerCase(); + + const found = snapshot.elements.some((el) => { + const elText = el.text || el.name || ""; + const compareText = caseSensitive ? elText : elText.toLowerCase(); + return compareText.includes(searchText); + }); + + return { + label: `dom_contains("${text}")`, + passed: found, + reason: found + ? `DOM contains text "${text}"` + : `DOM does NOT contain text "${text}"`, + required: options.required ?? true, + details: { searched_text: text, element_count: snapshot.elements.length }, + }; + }; +} + +/** + * Verify that an element matching a selector exists. + */ +export function element_exists(selector: string, options: { required?: boolean; minCount?: number } = {}): Predicate { + return (snapshot: Snapshot): AssertionResult => { + const minCount = options.minCount ?? 1; + + // Simplified selector matching (in real SDK, this would use CSS/XPath parsing) + const matches = snapshot.elements.filter((el) => { + if (selector.startsWith("role=")) { + return el.role === selector.replace("role=", ""); + } + if (selector.startsWith(".")) { + return el.selector?.includes(selector); + } + if (selector.startsWith("#")) { + return el.attributes?.id === selector.replace("#", ""); + } + // Generic text/name match + return el.text?.includes(selector) || el.name?.includes(selector) || el.selector?.includes(selector); + }); + + const passed = matches.length >= minCount; + + return { + label: `element_exists("${selector}")`, + passed, + reason: passed + ? `Found ${matches.length} element(s) matching "${selector}" (required: ${minCount})` + : `Found ${matches.length} element(s) matching "${selector}" but required ${minCount}`, + required: options.required ?? true, + details: { selector, found_count: matches.length, min_count: minCount }, + }; + }; +} + +/** + * Verify ML snapshot confidence is above threshold. + * This uses simulated ML analysis in the demo. + */ +export function confidence_above(threshold: number, options: { required?: boolean } = {}): Predicate { + return (snapshot: Snapshot): AssertionResult => { + const confidence = snapshot.confidence ?? 0; + const passed = confidence >= threshold; + + return { + label: `confidence_above(${threshold})`, + passed, + reason: passed + ? `ML confidence ${confidence.toFixed(3)} >= ${threshold}` + : `ML confidence ${confidence.toFixed(3)} < ${threshold} threshold`, + required: options.required ?? false, // Usually soft assertion + details: { confidence, threshold }, + }; + }; +} + +/** + * Verify page title matches pattern. + */ +export function title_matches(pattern: string | RegExp, options: { required?: boolean } = {}): Predicate { + return (snapshot: Snapshot): AssertionResult => { + const regex = typeof pattern === "string" ? new RegExp(pattern) : pattern; + const passed = regex.test(snapshot.title); + + return { + label: `title_matches(${pattern})`, + passed, + reason: passed + ? `Title "${snapshot.title}" matches pattern` + : `Title "${snapshot.title}" does NOT match pattern`, + required: options.required ?? true, + details: { title: snapshot.title, pattern: pattern.toString() }, + }; + }; +} + +// ============================================================================ +// Predicate Runtime Class +// ============================================================================ + +export class PredicateRuntime { + private page: Page | null = null; + private lastSnapshot: Snapshot | null = null; + private verbose: boolean; + + constructor(options: { verbose?: boolean } = {}) { + this.verbose = options.verbose ?? true; + } + + /** + * Attach runtime to a Playwright page. + */ + attach(page: Page): void { + this.page = page; + this.log("[RUNTIME] Attached to Playwright page"); + } + + /** + * Take a snapshot of the current page state. + * In production, this calls the Sentience API for ML-enhanced snapshots. + * In this demo, we simulate the snapshot locally. + */ + async snapshot(options: { includeScreenshot?: boolean } = {}): Promise { + if (!this.page) { + throw new Error("PredicateRuntime not attached to a page. Call attach(page) first."); + } + + this.log("[RUNTIME] Taking snapshot..."); + + const url = this.page.url(); + const title = await this.page.title(); + + // Extract elements from DOM (simplified) + const elements = await this.page.evaluate(() => { + const results: SnapshotElement[] = []; + let id = 0; + + // Get all visible text elements + const walker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_ELEMENT, + null, + ); + + while (walker.nextNode()) { + const el = walker.currentNode as HTMLElement; + const text = el.textContent?.trim().slice(0, 100) || ""; + const role = el.getAttribute("role") || el.tagName.toLowerCase(); + + if (text && el.offsetParent !== null) { // Only visible elements + results.push({ + id: id++, + role, + text, + name: el.getAttribute("aria-label") || el.getAttribute("title") || undefined, + selector: el.className ? `.${el.className.split(" ")[0]}` : el.tagName.toLowerCase(), + attributes: { + id: el.id || undefined, + class: el.className || undefined, + href: (el as HTMLAnchorElement).href || undefined, + } as Record, + }); + } + + if (results.length >= 200) break; // Limit for performance + } + + return results; + }); + + // Simulate ML confidence (in production, this comes from Sentience API) + const confidence = this.simulateMLConfidence(url, elements); + + // Screenshot (optional) + let screenshot_base64: string | undefined; + if (options.includeScreenshot) { + const buffer = await this.page.screenshot({ type: "jpeg", quality: 80 }); + screenshot_base64 = buffer.toString("base64"); + } + + const snapshot: Snapshot = { + url, + title, + elements, + screenshot_base64, + confidence, + timestamp: new Date().toISOString(), + diagnostics: { + element_count: elements.length, + load_time_ms: 0, // Would be measured in production + captcha_detected: false, + }, + }; + + this.lastSnapshot = snapshot; + this.log(`[RUNTIME] Snapshot captured: ${elements.length} elements, confidence: ${confidence.toFixed(3)}`); + + return snapshot; + } + + /** + * Verify the current page state against a set of predicates. + * + * ============================================================ + * POST-EXECUTION VERIFICATION GATE + * ============================================================ + * + * This is the CRITICAL verification step that ensures the world + * state matches our expectations before proceeding to data extraction. + * + * @throws Error if any required assertion fails + */ + async verify_state(...predicates: Predicate[]): Promise { + console.log(`\n${"=".repeat(60)}`); + console.log(`[POST-EXECUTION VERIFICATION]`); + console.log(`${"=".repeat(60)}`); + + // Take fresh snapshot if needed + const snapshot = this.lastSnapshot || await this.snapshot(); + + // Run all predicates + const assertions: AssertionResult[] = []; + let allRequiredPassed = true; + + for (const predicate of predicates) { + const result = predicate(snapshot); + assertions.push(result); + + const icon = result.passed ? "✓" : "✗"; + const color = result.passed ? "\x1b[32m" : "\x1b[31m"; + const reset = "\x1b[0m"; + const requiredTag = result.required ? "[REQUIRED]" : "[OPTIONAL]"; + + console.log(` ${color}${icon}${reset} ${requiredTag} ${result.label}`); + console.log(` ${result.reason}`); + + if (result.required && !result.passed) { + allRequiredPassed = false; + } + } + + const passed = allRequiredPassed; + const reason = passed + ? `All ${assertions.filter((a) => a.required).length} required assertions passed` + : `${assertions.filter((a) => a.required && !a.passed).length} required assertion(s) failed`; + + console.log(`${"=".repeat(60)}`); + if (passed) { + console.log(`${"\x1b[42m\x1b[30m"} VERIFICATION PASSED ${"\x1b[0m"} ${reason}`); + } else { + console.log(`${"\x1b[41m\x1b[37m"} VERIFICATION FAILED ${"\x1b[0m"} ${reason}`); + } + console.log(`${"=".repeat(60)}\n`); + + const result: VerificationResult = { + passed, + assertions, + snapshot, + reason, + }; + + // CRITICAL: Throw if verification fails + if (!passed) { + throw new VerificationError(result); + } + + return result; + } + + /** + * Get the last snapshot without taking a new one. + */ + getLastSnapshot(): Snapshot | null { + return this.lastSnapshot; + } + + // -------------------------------------------------------------------------- + // Private Methods + // -------------------------------------------------------------------------- + + /** + * Simulate ML confidence score based on page content. + * In production, this would come from Sentience's ML models. + */ + private simulateMLConfidence(url: string, elements: SnapshotElement[]): number { + // Base confidence + let confidence = 0.5; + + // URL-based heuristics + if (url.includes("news.ycombinator.com")) { + confidence += 0.25; + } + if (url.startsWith("https://")) { + confidence += 0.1; + } + + // Element-based heuristics + if (elements.length > 10) { + confidence += 0.1; + } + if (elements.some((el) => el.text?.includes("Show HN") || el.text?.includes("Hacker News"))) { + confidence += 0.15; + } + + // Clamp to [0, 1] + return Math.min(1, Math.max(0, confidence)); + } + + private log(message: string): void { + if (this.verbose) { + console.log(message); + } + } +} + +// ============================================================================ +// Custom Error for Verification Failures +// ============================================================================ + +export class VerificationError extends Error { + readonly result: VerificationResult; + + constructor(result: VerificationResult) { + const failedAssertions = result.assertions + .filter((a) => a.required && !a.passed) + .map((a) => a.label) + .join(", "); + + super(`Post-execution verification failed: ${failedAssertions}`); + this.name = "VerificationError"; + this.result = result; + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Create a configured Predicate Runtime instance. + * + * @example + * ```typescript + * const runtime = createPredicateRuntime({ verbose: true }); + * runtime.attach(page); + * + * // Navigate and verify + * await page.goto("https://news.ycombinator.com/show"); + * + * // Post-execution verification + * await runtime.verify_state( + * url_contains("news.ycombinator.com"), + * dom_contains("Show HN"), + * element_exists("span.titleline", { minCount: 3 }), + * ); + * ``` + */ +export function createPredicateRuntime(options?: { verbose?: boolean }): PredicateRuntime { + return new PredicateRuntime(options); +} diff --git a/examples/real-openclaw-demo/src/predicate-sidecar-client.ts b/examples/real-openclaw-demo/src/predicate-sidecar-client.ts new file mode 100644 index 0000000..7cc77cc --- /dev/null +++ b/examples/real-openclaw-demo/src/predicate-sidecar-client.ts @@ -0,0 +1,366 @@ +/** + * ============================================================================ + * Predicate Sidecar Client - Pre-Execution Authorization Gate + * ============================================================================ + * + * ARCHITECTURE: Zero-Trust Execution Proxy (Option A) + * + * The agent has ZERO ambient OS privileges. All tool calls are: + * 1. Intercepted by this client + * 2. Sent to the Predicate Sidecar as JSON intents + * 3. Evaluated against policy BEFORE any execution + * 4. Executed by the sidecar (if allowed) on behalf of the agent + * + * ┌─────────────┐ JSON Intent ┌─────────────────┐ + * │ Agent │ ─────────────────────▶│ Predicate │ + * │ (No OS │ POST /v1/execute │ Sidecar │ + * │ Privileges)│ ◀─────────────────────│ (RTA Proxy) │ + * └─────────────┘ Result / Denial └────────┬────────┘ + * │ Execute + * ▼ (if allowed) + * ┌─────────────────┐ + * │ OS / Network │ + * └─────────────────┘ + * + * CRITICAL: The agent NEVER uses native fs, fetch, or child_process directly. + * All operations go through this Pre-Execution Gate. + */ + +// ============================================================================ +// Types & Interfaces +// ============================================================================ + +export interface ExecuteIntent { + /** Action type: fs.read, fs.write, http.fetch, browser.*, shell.exec */ + action: string; + /** Target resource: file path, URL, command */ + resource: string; + /** Agent identity for policy evaluation */ + principal: string; + /** Additional parameters for the action */ + params?: Record; + /** Execution context (step ID, trace ID, etc.) */ + context?: Record; +} + +export interface ExecuteResult { + /** Whether the action was allowed and executed */ + allowed: boolean; + /** Result data if action was executed */ + data?: unknown; + /** Error or denial reason */ + error?: string; + /** Policy rule that matched (for audit) */ + matched_rule?: string; + /** Execution duration in ms */ + duration_ms?: number; +} + +export interface AuthorizeResult { + allowed: boolean; + reason?: string; + violated_rule?: string; + mandate_id?: string; +} + +// ============================================================================ +// Predicate Sidecar Client +// ============================================================================ + +export class PredicateSidecarClient { + private readonly sidecarUrl: string; + private readonly principal: string; + private readonly verbose: boolean; + private readonly failClosed: boolean; + + constructor(options: { + sidecarUrl?: string; + principal?: string; + verbose?: boolean; + failClosed?: boolean; + } = {}) { + this.sidecarUrl = options.sidecarUrl || process.env.PREDICATE_SIDECAR_URL || "http://predicate-sidecar:8000"; + this.principal = options.principal || process.env.SECURECLAW_PRINCIPAL || "agent:market-research"; + this.verbose = options.verbose ?? (process.env.SECURECLAW_VERBOSE === "true"); + this.failClosed = options.failClosed ?? true; + } + + // -------------------------------------------------------------------------- + // Core Methods: Pre-Execution Authorization + // -------------------------------------------------------------------------- + + /** + * Check if an action is allowed WITHOUT executing it. + * Use this for policy preview / dry-run scenarios. + */ + async authorize(intent: Omit): Promise { + const fullIntent: ExecuteIntent = { + ...intent, + principal: this.principal, + }; + + this.log(`[PRE-EXEC] Authorizing: ${intent.action} on ${intent.resource}`); + + try { + const response = await fetch(`${this.sidecarUrl}/authorize`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(fullIntent), + }); + + if (!response.ok) { + throw new Error(`Sidecar returned ${response.status}: ${await response.text()}`); + } + + const result = await response.json() as AuthorizeResult; + + if (result.allowed) { + this.log(`[PRE-EXEC] ✓ ALLOWED: ${intent.action}`); + } else { + this.log(`[PRE-EXEC] ✗ DENIED: ${intent.action} - ${result.reason || result.violated_rule}`); + } + + return result; + } catch (error) { + this.log(`[PRE-EXEC] ⚠ ERROR: ${(error as Error).message}`); + + if (this.failClosed) { + return { allowed: false, reason: `sidecar_unavailable: ${(error as Error).message}` }; + } + + // Fail-open (dangerous, not recommended) + return { allowed: true, reason: "sidecar_unavailable_fail_open" }; + } + } + + /** + * Authorize an action through the sidecar's policy engine. + * + * ARCHITECTURE NOTE: + * In authorize-only mode, the sidecar checks policy but does NOT execute. + * The caller is responsible for execution ONLY if allowed=true. + * + * CRITICAL: If the sidecar returns allowed=false or is unavailable, + * the caller MUST NOT execute the action. This is fail-closed enforcement. + */ + async execute(intent: Omit): Promise { + const fullIntent: ExecuteIntent = { + ...intent, + principal: this.principal, + context: { + ...intent.context, + timestamp: new Date().toISOString(), + }, + }; + + this.log(`\n${"=".repeat(60)}`); + this.log(`[PRE-EXECUTION GATE] Action Request`); + this.log(`${"=".repeat(60)}`); + this.log(` Action: ${intent.action}`); + this.log(` Resource: ${intent.resource}`); + this.log(` Principal: ${this.principal}`); + this.log(`${"=".repeat(60)}\n`); + + const startTime = Date.now(); + + try { + // ====================================================================== + // AUTHORIZE-ONLY MODE: Check policy via /authorize endpoint + // The sidecar evaluates the policy and returns allowed/denied. + // Execution happens locally ONLY if allowed=true. + // ====================================================================== + const response = await fetch(`${this.sidecarUrl}/authorize`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(fullIntent), + }); + + const duration_ms = Date.now() - startTime; + + if (!response.ok) { + const errorText = await response.text(); + + // Check if this is a policy denial (4xx) vs server error (5xx) + if (response.status === 403) { + const errorJson = JSON.parse(errorText) as { reason?: string; violated_rule?: string }; + this.logDenied(intent.action, errorJson.reason || errorJson.violated_rule || "policy_denied"); + return { + allowed: false, + error: errorJson.reason || "Action denied by policy", + matched_rule: errorJson.violated_rule, + duration_ms, + }; + } + + throw new Error(`Sidecar error ${response.status}: ${errorText}`); + } + + const result = await response.json() as AuthorizeResult; + + if (result.allowed) { + this.logAllowed(intent.action, duration_ms); + return { + allowed: true, + duration_ms, + }; + } else { + this.logDenied(intent.action, result.reason || result.violated_rule || "policy_denied"); + return { + allowed: false, + error: result.reason || "Action denied by policy", + matched_rule: result.violated_rule, + duration_ms, + }; + } + } catch (error) { + const duration_ms = Date.now() - startTime; + this.log(`[PRE-EXEC] ⚠ SIDECAR ERROR: ${(error as Error).message}`); + + // CRITICAL: Fail closed - if sidecar is unavailable, DENY the action + // This prevents security bypass via sidecar unavailability + this.logDenied(intent.action, `sidecar_unavailable: ${(error as Error).message}`); + return { + allowed: false, + error: `sidecar_unavailable: ${(error as Error).message}`, + duration_ms, + }; + } + } + + // -------------------------------------------------------------------------- + // Convenience Methods: Common Actions + // -------------------------------------------------------------------------- + + /** + * Read a file through the sidecar. + * Maps to action: fs.read + */ + async readFile(path: string): Promise { + return this.execute({ + action: "fs.read", + resource: path, + params: { path }, + }); + } + + /** + * Write content to a file through the sidecar. + * Maps to action: fs.write + */ + async writeFile(path: string, content: string): Promise { + return this.execute({ + action: "fs.write", + resource: path, + params: { path, content }, + }); + } + + /** + * Append content to a file through the sidecar. + * Maps to action: fs.write with append mode + */ + async appendFile(path: string, content: string): Promise { + return this.execute({ + action: "fs.write", + resource: path, + params: { path, content, mode: "append" }, + }); + } + + /** + * Make an HTTP request through the sidecar. + * Maps to action: http.fetch + */ + async fetch(url: string, options: RequestInit = {}): Promise { + return this.execute({ + action: "http.fetch", + resource: url, + params: { + url, + method: options.method || "GET", + headers: options.headers, + body: options.body, + }, + }); + } + + /** + * Launch a headless browser through the sidecar. + * Maps to action: browser.launch + */ + async launchBrowser(options: { headless?: boolean; browserType?: string } = {}): Promise { + return this.execute({ + action: "browser.launch", + resource: "*", + params: { + headless: options.headless ?? true, + browser_type: options.browserType || "chromium", + }, + }); + } + + /** + * Navigate browser to URL through the sidecar. + * Maps to action: browser.navigate + */ + async navigateTo(url: string): Promise { + return this.execute({ + action: "browser.navigate", + resource: url, + params: { url }, + }); + } + + // -------------------------------------------------------------------------- + // Logging + // -------------------------------------------------------------------------- + + private log(message: string): void { + if (this.verbose) { + console.log(`[PredicateSidecar] ${message}`); + } + } + + private logAllowed(action: string, durationMs: number): void { + console.log(`\n┌${"─".repeat(58)}┐`); + console.log(`│ ${"\x1b[42m\x1b[30m"} PRE-EXECUTION: ALLOWED ${"\x1b[0m"} │`); + console.log(`├${"─".repeat(58)}┤`); + console.log(`│ Action: ${action.padEnd(46)} │`); + console.log(`│ Duration: ${(durationMs + "ms").padEnd(46)} │`); + console.log(`└${"─".repeat(58)}┘\n`); + } + + private logDenied(action: string, reason: string): void { + console.log(`\n┌${"─".repeat(58)}┐`); + console.log(`│ ${"\x1b[41m\x1b[37m"} PRE-EXECUTION: DENIED ${"\x1b[0m"} │`); + console.log(`├${"─".repeat(58)}┤`); + console.log(`│ Action: ${action.padEnd(46)} │`); + console.log(`│ Reason: ${reason.slice(0, 46).padEnd(46)} │`); + console.log(`└${"─".repeat(58)}┘\n`); + } +} + +// ============================================================================ +// Factory Function +// ============================================================================ + +/** + * Create a configured Predicate Sidecar Client. + * + * @example + * ```typescript + * const client = createPredicateClient({ + * sidecarUrl: "http://predicate-sidecar:8000", + * principal: "agent:market-research", + * }); + * + * // All actions go through pre-execution authorization + * const result = await client.writeFile("/data/leads.csv", csvContent); + * if (!result.allowed) { + * console.error("Action blocked:", result.error); + * } + * ``` + */ +export function createPredicateClient(options?: ConstructorParameters[0]): PredicateSidecarClient { + return new PredicateSidecarClient(options); +} diff --git a/package-lock.json b/package-lock.json index b5c4183..be856a0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,32 @@ { "name": "predicate-claw", - "version": "0.1.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "predicate-claw", - "version": "0.1.0", + "version": "0.5.1", "license": "(MIT OR Apache-2.0)", "dependencies": { - "@predicatesystems/authority": "^0.4.1" + "@anthropic-ai/sdk": "^0.20.0", + "@predicatesystems/authority": "^0.4.1", + "@predicatesystems/runtime": "^1.2.0", + "playwright": "^1.40.0" }, "devDependencies": { "@types/node": "^25.3.0", "openclaw": "^2026.2.19-2", "typescript": "^5.9.3", "vitest": "^4.0.18" + }, + "peerDependencies": { + "openclaw": ">=2026.2.0" + }, + "peerDependenciesMeta": { + "openclaw": { + "optional": true + } } }, "node_modules/@agentclientprotocol/sdk": { @@ -29,26 +40,56 @@ } }, "node_modules/@anthropic-ai/sdk": { - "version": "0.73.0", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.73.0.tgz", - "integrity": "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==", - "dev": true, + "version": "0.20.9", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.20.9.tgz", + "integrity": "sha512-Lq74+DhiEQO6F9/gdVOLmHx57pX45ebK2Q/zH14xYe1157a7QeUVknRqIp0Jz5gQI01o7NKbuv9Dag2uQsLjDg==", "license": "MIT", "dependencies": { - "json-schema-to-ts": "^3.1.1" + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@anthropic-ai/sdk/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" }, - "bin": { - "anthropic-ai-sdk": "bin/cli" + "engines": { + "node": "4.x || >=6.0.0" }, "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" + "encoding": "^0.1.0" }, "peerDependenciesMeta": { - "zod": { + "encoding": { "optional": true } } }, + "node_modules/@anthropic-ai/sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/@aws-crypto/crc32": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", @@ -1148,7 +1189,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", - "dev": true, "license": "MIT", "optional": true, "dependencies": { @@ -1722,7 +1762,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz", "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==", - "dev": true, "license": "MIT", "engines": { "node": ">=18" @@ -1735,7 +1774,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1758,7 +1796,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1781,7 +1818,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1798,7 +1834,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1815,7 +1850,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1832,7 +1866,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1849,7 +1882,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1866,7 +1898,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1883,7 +1914,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1900,7 +1930,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1917,7 +1946,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1934,7 +1962,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -1951,7 +1978,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1974,7 +2000,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -1997,7 +2022,6 @@ "cpu": [ "ppc64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2020,7 +2044,6 @@ "cpu": [ "riscv64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2043,7 +2066,6 @@ "cpu": [ "s390x" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2066,7 +2088,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2089,7 +2110,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2112,7 +2132,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -2135,7 +2154,6 @@ "cpu": [ "wasm32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", "optional": true, "dependencies": { @@ -2155,7 +2173,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2175,7 +2192,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2195,7 +2211,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND LGPL-3.0-or-later", "optional": true, "os": [ @@ -2725,6 +2740,27 @@ "node": ">=20.0.0" } }, + "node_modules/@mariozechner/pi-ai/node_modules/@anthropic-ai/sdk": { + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.73.0.tgz", + "integrity": "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, "node_modules/@mariozechner/pi-coding-agent": { "version": "0.53.0", "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.53.0.tgz", @@ -2824,6 +2860,12 @@ "url": "https://github.com/sponsors/colinhacks" } }, + "node_modules/@mixmark-io/domino": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@mixmark-io/domino/-/domino-2.2.0.tgz", + "integrity": "sha512-Y28PR25bHXUg88kCV7nivXrP2Nj2RueZ3/l/jdx6J9f8J4nsEGcgX0Qe6lt7Pa+J79+kPiJU3LguR6O/6zrLOw==", + "license": "BSD-2-Clause" + }, "node_modules/@mozilla/readability": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/@mozilla/readability/-/readability-0.6.0.tgz", @@ -3751,6 +3793,109 @@ "node": ">=20.0.0" } }, + "node_modules/@predicatesystems/runtime": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@predicatesystems/runtime/-/runtime-1.2.0.tgz", + "integrity": "sha512-CPBExNHBq83sCJnKd+c/tak6epy/KRVoVfANu5EkuzX14AmKmj92hTEPg7JKDZUbj3sQYt5kTcQHrMa3OYTFVA==", + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "canvas": "^3.2.1", + "playwright": "^1.40.0", + "sharp": "^0.34.5", + "turndown": "^7.2.2", + "uuid": "^9.0.0", + "zod": "^3.22.0" + }, + "bin": { + "predicate": "dist/cli.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "@anthropic-ai/sdk": "^0.20.0", + "openai": "^4.0.0", + "zhipuai-sdk-nodejs-v4": "^0.1.12" + } + }, + "node_modules/@predicatesystems/runtime/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "optional": true, + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@predicatesystems/runtime/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/@predicatesystems/runtime/node_modules/openai": { + "version": "4.104.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-4.104.0.tgz", + "integrity": "sha512-p99EFNsA/yX6UhVO93f5kJsDRLAg+CTA2RBqdHK4RtK8u5IJw32Hyb2dTGKbnnFmnuoBv5r7Z2CURI9sGZpSuA==", + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + }, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.23.8" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@predicatesystems/runtime/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT", + "optional": true + }, + "node_modules/@predicatesystems/runtime/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -5355,12 +5500,21 @@ "version": "25.3.0", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.0.tgz", "integrity": "sha512-4K3bqJpXpqfg2XKGK9bpDTc6xO/xoUP/RBWS7AtRMug6zZFaRekiLzjVtAoZMquxoAbzBvy5nxQ7veS5eYzf8A==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~7.18.0" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, "node_modules/@types/qs": { "version": "6.14.0", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", @@ -5602,7 +5756,6 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "dev": true, "license": "MIT", "dependencies": { "event-target-shim": "^5.0.0" @@ -5662,6 +5815,18 @@ "node": ">= 14" } }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", @@ -5823,7 +5988,6 @@ "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", - "dev": true, "license": "MIT" }, "node_modules/atomic-sleep": { @@ -5840,7 +6004,7 @@ "version": "1.13.5", "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz", "integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "follow-redirects": "^1.15.11", @@ -5862,7 +6026,6 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "dev": true, "funding": [ { "type": "github", @@ -5907,6 +6070,17 @@ "node": "*" } }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, "node_modules/body-parser": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.2.tgz", @@ -5966,11 +6140,35 @@ "node": "20 || >=22" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-equal-constant-time": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause" }, "node_modules/buffer-from": { @@ -6019,7 +6217,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -6046,6 +6243,26 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/canvas": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.2.1.tgz", + "integrity": "sha512-ej1sPFR5+0YWtaVp6S1N1FVz69TQCqmrkGeRvQxZeAB1nAIcjNTHVwrZtYtWFFBmQsF40/uDLehsW5KuYC99mg==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.3" + }, + "engines": { + "node": "^18.12.0 || >= 20.9.0" + } + }, + "node_modules/canvas/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/chai": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", @@ -6397,7 +6614,6 @@ "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", - "dev": true, "license": "MIT", "dependencies": { "delayed-stream": "~1.0.0" @@ -6606,13 +6822,26 @@ } } }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=4.0.0" } @@ -6636,7 +6865,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.4.0" @@ -6664,7 +6892,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", - "dev": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -6766,7 +6993,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -6788,7 +7014,7 @@ "version": "1.0.11", "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "dependencies": { "safe-buffer": "^5.0.1" @@ -6818,6 +7044,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", @@ -6846,7 +7081,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6856,7 +7090,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -6873,7 +7106,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -6886,7 +7118,6 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -7037,7 +7268,6 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=6" @@ -7050,6 +7280,15 @@ "dev": true, "license": "MIT" }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "license": "(MIT OR WTFPL)", + "engines": { + "node": ">=6" + } + }, "node_modules/expect-type": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", @@ -7317,7 +7556,7 @@ "version": "1.15.11", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "dev": true, + "devOptional": true, "funding": [ { "type": "individual", @@ -7368,7 +7607,6 @@ "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", - "dev": true, "license": "MIT", "dependencies": { "asynckit": "^0.4.0", @@ -7381,6 +7619,34 @@ "node": ">= 6" } }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, "node_modules/formdata-polyfill": { "version": "4.0.10", "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", @@ -7414,6 +7680,12 @@ "node": ">= 0.8" } }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "license": "MIT" + }, "node_modules/fs-extra": { "version": "11.3.3", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", @@ -7477,7 +7749,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7588,7 +7859,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -7613,7 +7883,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -7648,6 +7917,12 @@ "node": ">= 14" } }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", + "license": "MIT" + }, "node_modules/glob": { "version": "13.0.6", "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", @@ -7699,7 +7974,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7780,7 +8054,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -7793,7 +8066,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "dev": true, "license": "MIT", "dependencies": { "has-symbols": "^1.0.3" @@ -7830,7 +8102,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -7970,6 +8241,15 @@ "node": ">= 14" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/iconv-lite": { "version": "0.7.2", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.2.tgz", @@ -7991,7 +8271,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", - "dev": true, "funding": [ { "type": "github", @@ -8029,16 +8308,13 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true, - "license": "ISC", - "peer": true + "license": "ISC" }, "node_modules/ip-address": { "version": "10.1.0", @@ -8319,7 +8595,7 @@ "version": "9.0.3", "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz", "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "jws": "^4.0.1", @@ -8388,7 +8664,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "buffer-equal-constant-time": "^1.0.1", @@ -8400,7 +8676,7 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "jwa": "^2.0.1", @@ -8552,42 +8828,42 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz", "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/lodash.isboolean": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz", "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/lodash.isinteger": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz", "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/lodash.isnumber": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz", "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/lodash.isplainobject": { "version": "4.0.6", "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/lodash.isstring": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/lodash.merge": { @@ -8601,7 +8877,7 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/lodash.pickby": { @@ -8708,7 +8984,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -8759,7 +9034,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -8769,7 +9043,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -8792,6 +9065,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/minimatch": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.2.tgz", @@ -8812,9 +9097,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "dev": true, "license": "MIT", - "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -8872,11 +9155,16 @@ "node": ">=10" } }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "license": "MIT" + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/music-metadata": { @@ -8942,6 +9230,12 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", + "license": "MIT" + }, "node_modules/negotiator": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", @@ -8962,6 +9256,18 @@ "node": ">= 0.4.0" } }, + "node_modules/node-abi": { + "version": "3.87.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.87.0.tgz", + "integrity": "sha512-+CGM1L1CgmtheLcBuleyYOn7NWPVu0s0EJH2C4puxgEZb9h8QpR9G2dBfZJOAUhi7VQxuBPMd0hiISWcTyiYyQ==", + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/node-addon-api": { "version": "8.5.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz", @@ -8986,7 +9292,6 @@ "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", "deprecated": "Use your platform's native DOMException instead", - "dev": true, "funding": [ { "type": "github", @@ -9254,7 +9559,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -9807,11 +10111,28 @@ "dev": true, "license": "MIT" }, + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, "node_modules/playwright-core": { "version": "1.58.2", "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", - "dev": true, "license": "Apache-2.0", "bin": { "playwright-core": "cli.js" @@ -9820,6 +10141,20 @@ "node": ">=18" } }, + "node_modules/playwright/node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -9849,6 +10184,33 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "deprecated": "No longer maintained. Please contact the author of the relevant native addon; alternatives are available.", + "license": "MIT", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/pretty-bytes": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-6.1.1.tgz", @@ -10027,9 +10389,19 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", - "dev": true, + "devOptional": true, "license": "MIT" }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, "node_modules/punycode.js": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", @@ -10115,9 +10487,7 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "peer": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -10132,9 +10502,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -10391,7 +10759,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -10429,7 +10796,6 @@ "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", - "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -10538,7 +10904,6 @@ "version": "0.34.5", "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "dev": true, "hasInstallScript": true, "license": "Apache-2.0", "dependencies": { @@ -10710,6 +11075,51 @@ "signal-polyfill": "^0.2.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-git": { "version": "3.31.1", "resolved": "https://registry.npmjs.org/simple-git/-/simple-git-3.31.1.tgz", @@ -11036,9 +11446,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "dev": true, "license": "MIT", - "peer": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -11184,9 +11592,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=0.10.0" } @@ -11254,6 +11660,40 @@ "node": ">=10" } }, + "node_modules/tar-fs": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", + "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "license": "MIT", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "license": "ISC" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "license": "MIT", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/tar/node_modules/minipass": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", @@ -11386,7 +11826,6 @@ "version": "0.0.3", "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", - "dev": true, "license": "MIT" }, "node_modules/ts-algebra": { @@ -11400,7 +11839,7 @@ "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true, + "devOptional": true, "license": "0BSD" }, "node_modules/tslog": { @@ -11426,6 +11865,27 @@ "node": ">=0.6.x" } }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/turndown": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/turndown/-/turndown-7.2.2.tgz", + "integrity": "sha512-1F7db8BiExOKxjSMU2b7if62D/XOyQyZbPKq/nUwopfgnHlqXHqQ0lvfUTeUIr1lZJzOPFn43dODyMSIfvWRKQ==", + "license": "MIT", + "dependencies": { + "@mixmark-io/domino": "^2.2.0" + } + }, "node_modules/type-is": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", @@ -11523,7 +11983,6 @@ "version": "7.18.2", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", - "dev": true, "license": "MIT" }, "node_modules/universal-github-app-jwt": { @@ -11575,9 +12034,21 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "dev": true, "license": "MIT" }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, "node_modules/validate-npm-package-name": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.2.tgz", @@ -11756,7 +12227,6 @@ "version": "3.3.3", "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 8" @@ -11766,14 +12236,12 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", - "dev": true, "license": "BSD-2-Clause" }, "node_modules/whatwg-url": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", - "dev": true, "license": "MIT", "dependencies": { "tr46": "~0.0.3", @@ -11951,14 +12419,13 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, "license": "ISC" }, "node_modules/ws": { "version": "8.19.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=10.0.0" @@ -12052,6 +12519,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/zhipuai-sdk-nodejs-v4": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/zhipuai-sdk-nodejs-v4/-/zhipuai-sdk-nodejs-v4-0.1.12.tgz", + "integrity": "sha512-UaxTvhIZiJOhwHjCx8WwZjkiQzQvSE/yq7uEEeM8zjZ1D1lX+SIDsTnRhnhVqsvpTnFdD9AcwY15mvjtmRy1ug==", + "license": "MIT", + "optional": true, + "dependencies": { + "axios": "^1.6.7", + "jsonwebtoken": "^9.0.2" + } + }, "node_modules/zod": { "version": "4.3.6", "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", diff --git a/package.json b/package.json index 0b04cfe..ed8e56f 100644 --- a/package.json +++ b/package.json @@ -32,7 +32,10 @@ "license": "(MIT OR Apache-2.0)", "type": "module", "dependencies": { - "@predicatesystems/authority": "^0.4.1" + "@anthropic-ai/sdk": "^0.20.0", + "@predicatesystems/authority": "^0.4.1", + "@predicatesystems/runtime": "^1.2.0", + "playwright": "^1.40.0" }, "peerDependencies": { "openclaw": ">=2026.2.0"