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
+
+
+
+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
+
-- **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"