From 1b7cc3cedbb332e4b7f9c9999153c719df59f57c Mon Sep 17 00:00:00 2001 From: Michael Martello Date: Thu, 26 Mar 2026 06:23:26 +0000 Subject: [PATCH] feat(openclaw): update sandbox with Slack support, expanded policies, WS proxy patch - policy.yaml: Add network policies for Anthropic, OpenAI, Google, Telegram, Slack (REST + Socket Mode), GitHub, npm/PyPI, Brave Search, OpenRouter, LinkedIn. Document the Slack wildcard gotcha and tls:skip requirement. - openclaw-ws-proxy-patch.js: New file. Monkey-patches the ws WebSocket constructor to tunnel Slack Socket Mode connections through the OpenShell CONNECT proxy. Required until native WS proxy support lands (see #387). - openclaw-slack-manifest.yaml: New file. Complete Slack app manifest with 23 bot scopes and 12 events for one-click app creation. - Dockerfile: Bump openclaw to @latest, add WS proxy patch and Slack manifest, set NODE_OPTIONS to auto-load the patch. - openclaw-start.sh: Rewrite as a full lifecycle manager with subcommands (start, stop, status, logs, pair). Uses nohup for gateway persistence across SSH disconnects. - README.md: Document all new features, policy coverage, Slack setup guide, WS proxy explanation, and troubleshooting section. --- sandboxes/openclaw/Dockerfile | 19 +- sandboxes/openclaw/README.md | 101 +++++-- .../openclaw/openclaw-slack-manifest.yaml | 99 +++++++ sandboxes/openclaw/openclaw-start.sh | 119 +++++++-- sandboxes/openclaw/openclaw-ws-proxy-patch.js | 126 +++++++++ sandboxes/openclaw/policy.yaml | 250 ++++++++++++------ 6 files changed, 602 insertions(+), 112 deletions(-) create mode 100644 sandboxes/openclaw/openclaw-slack-manifest.yaml create mode 100644 sandboxes/openclaw/openclaw-ws-proxy-patch.js diff --git a/sandboxes/openclaw/Dockerfile b/sandboxes/openclaw/Dockerfile index a851461..51a138d 100644 --- a/sandboxes/openclaw/Dockerfile +++ b/sandboxes/openclaw/Dockerfile @@ -5,7 +5,9 @@ # OpenClaw sandbox image for OpenShell # -# Builds on the community base sandbox and adds OpenClaw. +# Builds on the community base sandbox and adds OpenClaw with Slack +# Socket Mode support (WebSocket proxy patch) and expanded network policies. +# # Build: docker build -t openshell-openclaw --build-arg BASE_IMAGE=openshell-base . # Run: openshell sandbox create --from openclaw @@ -14,13 +16,22 @@ FROM ${BASE_IMAGE} USER root -# Install OpenClaw CLI (pinned to fix GHSA-rchv-x836-w7xp, GHSA-6mgf-v5j7-45cr, -# GHSA-5wcw-8jjv-m286) -RUN npm install -g openclaw@2026.3.11 +# Install OpenClaw CLI +RUN npm install -g openclaw@latest # Copy sandbox policy COPY policy.yaml /etc/openshell/policy.yaml +# Copy the WebSocket proxy patch (required for Slack Socket Mode behind +# the OpenShell CONNECT proxy — see README.md for details) +COPY openclaw-ws-proxy-patch.js /sandbox/openclaw-ws-proxy-patch.js + +# Copy the Slack app manifest for one-click app creation +COPY openclaw-slack-manifest.yaml /sandbox/openclaw-slack-manifest.yaml + +# Auto-load the WS proxy patch in every Node.js process +ENV NODE_OPTIONS="--require /sandbox/openclaw-ws-proxy-patch.js" + # Copy the OpenClaw startup helper COPY openclaw-start.sh /usr/local/bin/openclaw-start RUN chmod +x /usr/local/bin/openclaw-start diff --git a/sandboxes/openclaw/README.md b/sandboxes/openclaw/README.md index 510a5d7..a3808a2 100644 --- a/sandboxes/openclaw/README.md +++ b/sandboxes/openclaw/README.md @@ -4,10 +4,13 @@ OpenShell sandbox image pre-configured with [OpenClaw](https://github.com/opencl ## What's Included -- **OpenClaw CLI** -- Agent orchestration and gateway management -- **OpenClaw Gateway** -- Local gateway for agent-to-tool communication -- **Node.js 22** -- Runtime required by the OpenClaw gateway -- **openclaw-start** -- Helper script that onboards and starts the gateway automatically +| File | Purpose | +|---|---| +| `Dockerfile` | Builds the sandbox image with OpenClaw + WS proxy patch | +| `policy.yaml` | Network policies for 12 services (LLMs, Slack, GitHub, …) | +| `openclaw-ws-proxy-patch.js` | Monkey-patches `ws` to tunnel Slack WebSockets through the CONNECT proxy | +| `openclaw-slack-manifest.yaml` | One-click Slack app manifest with all 23 scopes and 12 events | +| `openclaw-start.sh` | Gateway lifecycle manager (start / stop / status / logs / pair) | ## Build @@ -18,40 +21,108 @@ docker build -t openshell-openclaw . To build against a specific base image: ```bash -docker build -t openshell-openclaw --build-arg BASE_IMAGE=ghcr.io/nvidia/openshell-community/sandboxes/base:latest . +docker build -t openshell-openclaw \ + --build-arg BASE_IMAGE=ghcr.io/nvidia/openshell-community/sandboxes/base:latest . ``` ## Usage ### Create a sandbox -```bash -openshell sandbox create --from openclaw -``` - -### With port forwarding (to access the OpenClaw UI) - ```bash openshell sandbox create --from openclaw --forward 18789 -- openclaw-start ``` -This runs the `openclaw-start` helper which: +This runs `openclaw-start` which: 1. Runs `openclaw onboard` to configure the environment -2. Starts the OpenClaw gateway in the background +2. Starts the OpenClaw gateway under `nohup` (survives SSH disconnects) 3. Prints the gateway URL (with auth token if available) Access the UI at `http://127.0.0.1:18789/`. -### Manual startup +### Gateway management -If you prefer to start OpenClaw manually inside the sandbox: +Once inside the sandbox: + +```bash +openclaw-start # start (or restart) the gateway +openclaw-start stop # stop the gateway +openclaw-start status # check if running +openclaw-start logs # tail the gateway log +openclaw-start pair CODE # approve a Slack/Telegram pairing request +``` + +### Manual startup ```bash openclaw onboard openclaw gateway run ``` +## Network Policy Coverage + +The included `policy.yaml` covers: + +| Category | Services | +|---|---| +| **LLM providers** | Anthropic, OpenAI, Google (Gemini/Vertex) | +| **Messaging** | Telegram, Slack REST API, Slack Socket Mode (WebSocket) | +| **Code** | GitHub (git + REST API), npm, PyPI | +| **Search** | Brave Search, OpenRouter | +| **Web** | LinkedIn | + +All policies default to `enforcement: audit` except Slack WebSocket which uses `enforcement: enforce`. + +### Slack wildcard gotcha + +Do **not** add a `*.slack.com` wildcard to the `slack` policy. On OpenShell ≤ 0.0.15, a wildcard match takes priority over the `slack_websocket` policy's `tls: skip` setting, causing the L7 proxy to intercept the TLS handshake and break Socket Mode. + +## WebSocket Proxy Patch + +OpenShell routes all outbound traffic through an HTTP CONNECT proxy. The Slack SDK's `ws` library does not natively support CONNECT proxies for WebSocket connections. The `openclaw-ws-proxy-patch.js` file monkey-patches the `ws` module to: + +1. Detect connections to `wss-primary.slack.com` and `wss-backup.slack.com` +2. Establish a CONNECT tunnel through the proxy +3. Perform TLS over the tunnel + +The patch is auto-loaded via `NODE_OPTIONS="--require /sandbox/openclaw-ws-proxy-patch.js"` set in the Dockerfile. + +> **Related:** [OpenShell #387](https://github.com/NVIDIA/OpenShell-Community/issues/387) — feature request for native WebSocket proxy support. + +## Slack Setup + +1. Go to [api.slack.com/apps](https://api.slack.com/apps) +2. Click **Create New App** → **From an app manifest** +3. Paste `openclaw-slack-manifest.yaml` +4. Generate an **App-Level Token** with `connections:write` scope +5. Install to your workspace and copy the **Bot User OAuth Token** +6. Pass both tokens during `openclaw onboard` + +## Troubleshooting + +### DNS resolution fails inside the sandbox + +OpenShell's CoreDNS may not forward external queries by default. Patch the Corefile to add `forward . 8.8.8.8 8.8.4.4`: + +```bash +openshell doctor exec -- kubectl patch configmap coredns -n kube-system \ + --type merge -p '{"data":{"Corefile":"......"}}' +openshell doctor exec -- kubectl rollout restart deployment coredns -n kube-system +``` + +### Slack Socket Mode disconnects or times out + +- Verify `tls: skip` is set on `wss-primary.slack.com` and `wss-backup.slack.com` in `policy.yaml` +- Confirm `NODE_OPTIONS` includes `--require /sandbox/openclaw-ws-proxy-patch.js` +- Check logs: `openclaw-start logs | grep ws-proxy-patch` + +### Gateway won't start + +- Run `openclaw-start status` to check for zombie processes +- Run `openclaw-start stop` then `openclaw-start` to restart cleanly +- Inspect the log: `cat /sandbox/.openclaw/gateway.log` + ## Configuration OpenClaw stores its configuration in `~/.openclaw/openclaw.json` inside the sandbox. The config is generated during `openclaw onboard`. diff --git a/sandboxes/openclaw/openclaw-slack-manifest.yaml b/sandboxes/openclaw/openclaw-slack-manifest.yaml new file mode 100644 index 0000000..67664e0 --- /dev/null +++ b/sandboxes/openclaw/openclaw-slack-manifest.yaml @@ -0,0 +1,99 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Slack App Manifest for OpenClaw +# +# This manifest configures all scopes, events, and settings required +# for OpenClaw's Slack integration with Socket Mode. +# +# Usage: +# 1. Go to https://api.slack.com/apps +# 2. Click "Create New App" > "From an app manifest" +# 3. Select your workspace +# 4. Paste this YAML and confirm +# 5. After creation, generate an App-Level Token with connections:write scope +# 6. Install to workspace and copy the Bot User OAuth Token +# +# To update an existing app: +# 1. Go to https://api.slack.com/apps > select your app +# 2. Click "App Manifest" in the left sidebar +# 3. Paste this YAML and save + +_metadata: + major_version: 1 + minor_version: 1 + +display_information: + name: OpenClaw Agent + description: Autonomous AI agent powered by OpenClaw, running inside an OpenShell sandbox. + background_color: "#1a1a2e" + +features: + app_home: + home_tab_enabled: true + messages_tab_enabled: true + messages_tab_read_only_enabled: false + bot_user: + display_name: openclaw-agent + always_online: true + +oauth_config: + scopes: + bot: + # Core messaging + - chat:write + - chat:write.customize + - im:history + - im:read + - im:write + + # Channel access + - channels:history + - channels:read + - groups:history + - groups:read + + # Group DMs + - mpim:history + - mpim:read + - mpim:write + + # Mentions & reactions + - app_mentions:read + - reactions:read + - reactions:write + + # Files + - files:read + - files:write + + # Pins + - pins:read + - pins:write + + # Other + - assistant:write + - commands + - emoji:read + - users:read + +settings: + event_subscriptions: + bot_events: + - app_mention + - message.channels + - message.groups + - message.im + - message.mpim + - reaction_added + - reaction_removed + - member_joined_channel + - member_left_channel + - channel_rename + - pin_added + - pin_removed + interactivity: + is_enabled: true + org_deploy_enabled: false + socket_mode_enabled: true + token_rotation_enabled: false diff --git a/sandboxes/openclaw/openclaw-start.sh b/sandboxes/openclaw/openclaw-start.sh index 8afa919..90badb6 100644 --- a/sandboxes/openclaw/openclaw-start.sh +++ b/sandboxes/openclaw/openclaw-start.sh @@ -3,26 +3,111 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -# openclaw-start — Configure OpenClaw and start the gateway. -# Designed for OpenShell sandboxes. +# openclaw-start — Manage the OpenClaw gateway inside an OpenShell sandbox. # # Usage: -# openshell sandbox create --from openclaw --forward 18789 -- openclaw-start +# openclaw-start Onboard (if needed) and start the gateway +# openclaw-start pair CODE Approve a Slack/Telegram pairing request +# openclaw-start stop Stop the running gateway +# openclaw-start status Check whether the gateway is running +# openclaw-start logs Tail the gateway log +# +# The gateway runs under nohup so it survives SSH disconnects. +# Use `openclaw gateway run` (foreground), NOT `openclaw gateway start` +# which invokes launchd/systemd service managers unavailable in sandboxes. + set -euo pipefail -openclaw onboard +LOGFILE="/sandbox/.openclaw/gateway.log" +CONFIG_FILE="${HOME}/.openclaw/openclaw.json" -nohup openclaw gateway run > /tmp/gateway.log 2>&1 & +mkdir -p /sandbox/.openclaw -CONFIG_FILE="${HOME}/.openclaw/openclaw.json" -token=$(grep -o '"token"\s*:\s*"[^"]*"' "${CONFIG_FILE}" 2>/dev/null | head -1 | cut -d'"' -f4 || true) - -echo "" -echo "OpenClaw gateway starting in background." -echo " Logs: /tmp/gateway.log" -if [ -n "${token}" ]; then - echo " UI: http://127.0.0.1:18789/?token=${token}" -else - echo " UI: http://127.0.0.1:18789/" -fi -echo "" +stop_gateway() { + pkill -f "openclaw-gateway" 2>/dev/null || true + pkill -f "openclaw gateway" 2>/dev/null || true + sleep 1 + pkill -9 -f "openclaw-gateway" 2>/dev/null || true +} + +case "${1:-start}" in + pair) + if [ -z "${2:-}" ]; then + echo "Usage: openclaw-start pair " + exit 1 + fi + openclaw pairing approve slack "$2" + ;; + + stop) + echo "Stopping openclaw gateway..." + stop_gateway + echo "Stopped." + ;; + + status) + if pgrep -f "openclaw-gateway" >/dev/null 2>&1; then + PID=$(pgrep -f "openclaw-gateway" | head -1) + echo "Gateway is running (PID $PID)" + echo "Log: $LOGFILE" + exit 0 + else + echo "Gateway is NOT running" + exit 1 + fi + ;; + + logs) + if [ -f "$LOGFILE" ]; then + tail -f "$LOGFILE" + else + echo "No log file found at $LOGFILE" + exit 1 + fi + ;; + + start|"") + # Run onboard wizard (generates config if missing) + openclaw onboard + + # Stop any existing gateway first + stop_gateway + + echo "Starting openclaw gateway..." + nohup openclaw gateway run --allow-unconfigured > "$LOGFILE" 2>&1 & + + sleep 3 + + if pgrep -f "openclaw-gateway" >/dev/null 2>&1; then + PID=$(pgrep -f "openclaw-gateway" | head -1) + token=$(grep -o '"token"\s*:\s*"[^"]*"' "${CONFIG_FILE}" 2>/dev/null | head -1 | cut -d'"' -f4 || true) + + echo "" + echo "OpenClaw gateway started (PID $PID)" + echo " Logs: $LOGFILE" + if [ -n "${token:-}" ]; then + echo " UI: http://127.0.0.1:18789/?token=${token}" + else + echo " UI: http://127.0.0.1:18789/" + fi + echo "" + + # Report patch / Slack status from early log output + if grep -q "ws-proxy-patch.*active" "$LOGFILE" 2>/dev/null; then + echo "[OK] WebSocket proxy patch active" + fi + if grep -q "socket mode connected" "$LOGFILE" 2>/dev/null; then + echo "[OK] Slack Socket Mode connected" + fi + else + echo "Gateway failed to start. Log output:" + cat "$LOGFILE" 2>/dev/null + exit 1 + fi + ;; + + *) + echo "Usage: openclaw-start {start|stop|status|logs|pair }" + exit 1 + ;; +esac diff --git a/sandboxes/openclaw/openclaw-ws-proxy-patch.js b/sandboxes/openclaw/openclaw-ws-proxy-patch.js new file mode 100644 index 0000000..a7bd7cc --- /dev/null +++ b/sandboxes/openclaw/openclaw-ws-proxy-patch.js @@ -0,0 +1,126 @@ +// SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +/** + * OpenClaw Slack Socket Mode proxy patch. + * + * Monkey-patches the `ws` WebSocket constructor to inject a custom + * https.Agent that tunnels through the HTTP CONNECT proxy for Slack + * WebSocket hosts. + */ + +'use strict'; + +const proxyUrl = + process.env.HTTPS_PROXY || + process.env.HTTP_PROXY || + process.env.https_proxy || + process.env.http_proxy; + +if (proxyUrl) { + try { + const http = require('http'); + const https = require('https'); + const tls = require('tls'); + const { URL } = require('url'); + + const parsed = new URL(proxyUrl); + const proxyHost = parsed.hostname; + const proxyPort = parseInt(parsed.port, 10) || 3128; + + const origHttpRequest = http.request; + + /** + * Custom https.Agent that establishes a CONNECT tunnel through the + * sandbox proxy, then does TLS over the tunnel. + * + * Uses the official (options, callback) signature of + * Agent.prototype.createConnection, which supports async creation. + */ + class ConnectProxyAgent extends https.Agent { + createConnection(options, callback) { + const host = options.host || options.hostname || options.servername; + const port = options.port || 443; + + const proxyReq = origHttpRequest({ + host: proxyHost, + port: proxyPort, + method: 'CONNECT', + path: `${host}:${port}`, + headers: { Host: `${host}:${port}` }, + }); + + proxyReq.on('connect', (res, socket, head) => { + if (res.statusCode !== 200) { + socket.destroy(); + callback(new Error(`CONNECT proxy returned ${res.statusCode}`)); + return; + } + + // Wrap the raw tunnel in TLS (the agent is responsible for TLS) + const tlsSocket = tls.connect({ + socket: socket, + servername: host, + rejectUnauthorized: options.rejectUnauthorized !== false, + }); + + tlsSocket.on('secureConnect', () => callback(null, tlsSocket)); + tlsSocket.on('error', (err) => callback(err)); + }); + + proxyReq.on('error', (err) => callback(err)); + proxyReq.end(); + } + } + + // One shared agent instance (keeps connections alive) + const slackProxyAgent = new ConnectProxyAgent({ keepAlive: true }); + + const Module = require('module'); + const origLoad = Module._load; + Module._load = function (request, parent, isMain) { + const result = origLoad.call(this, request, parent, isMain); + + if (request === 'ws' && result && result.WebSocket && !result.__proxyPatched) { + const OrigWebSocket = result.WebSocket; + + class PatchedWebSocket extends OrigWebSocket { + constructor(address, protocols, options) { + if (typeof protocols === 'object' && !Array.isArray(protocols) && + protocols !== null && options === undefined) { + options = protocols; + protocols = undefined; + } + if (!options) options = {}; + + if (typeof address === 'string' && + address.startsWith('wss://') && + (address.includes('wss-primary.slack.com') || + address.includes('wss-backup.slack.com'))) { + // Inject our proxy agent instead of createConnection + options.agent = slackProxyAgent; + console.log( + `[ws-proxy-patch] Routing ${address.substring(0, 60)}... through proxy` + ); + } + + if (protocols !== undefined) { + super(address, protocols, options); + } else { + super(address, options); + } + } + } + + result.WebSocket = PatchedWebSocket; + result.__proxyPatched = true; + } + + return result; + }; + + console.log(`[ws-proxy-patch] Slack WebSocket proxy active → ${proxyHost}:${proxyPort}`); + } catch (err) { + console.error('[ws-proxy-patch] Failed to initialize:', err.message); + } +} diff --git a/sandboxes/openclaw/policy.yaml b/sandboxes/openclaw/policy.yaml index a91da84..ec453ad 100644 --- a/sandboxes/openclaw/policy.yaml +++ b/sandboxes/openclaw/policy.yaml @@ -1,9 +1,16 @@ # SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 -version: 1 +# OpenClaw sandbox network & filesystem policy +# +# Covers: Anthropic, OpenAI, Google, Telegram, Slack (REST + Socket Mode), +# GitHub, npm/PyPI registries, Brave Search, OpenRouter, LinkedIn. +# +# Apply: openshell policy set --policy policy.yaml --wait +# Verify: openshell logs --level warn --since 5m +# Reload: Edit this file, re-run `openshell policy set` -# --- Sandbox setup configuration (queried once at startup) --- +version: 1 filesystem_policy: include_workdir: true @@ -15,6 +22,7 @@ filesystem_policy: - /app - /etc - /var/log + - /opt/openclaw-config read_write: - /sandbox - /tmp @@ -27,101 +35,191 @@ process: run_as_user: sandbox run_as_group: sandbox -# --- Network policies (queried per-CONNECT request) --- -# -# Each named policy maps a set of allowed (binary, endpoint) pairs. -# Binary identity is resolved via /proc/net/tcp inode lookup + /proc/{pid}/exe. -# Ancestors (/proc/{pid}/status PPid walk) and cmdline paths are also matched. -# SHA256 integrity is enforced in Rust via trust-on-first-use, not here. - network_policies: - claude_code: - name: claude_code + + # ═══ LLM Providers ═══════════════════════════════════ + + anthropic: + name: anthropic endpoints: - - { host: api.anthropic.com, port: 443, protocol: rest, enforcement: enforce, access: full, tls: terminate } - - { host: statsig.anthropic.com, port: 443 } - - { host: sentry.io, port: 443 } - - { host: raw.githubusercontent.com, port: 443 } - - { host: platform.claude.com, port: 443 } + - host: api.anthropic.com + port: 443 + tls: skip + enforcement: audit + - host: statsig.anthropic.com + port: 443 + enforcement: audit + - host: sentry.io + port: 443 + enforcement: audit + - host: platform.claude.com + port: 443 + enforcement: audit binaries: + - { path: /usr/bin/node } - { path: /usr/local/bin/claude } + + openai: + name: openai + endpoints: + - host: api.openai.com + port: 443 + tls: skip + enforcement: audit + binaries: + - { path: /usr/bin/node } + + google: + name: google + endpoints: + - host: "*.googleapis.com" + port: 443 + enforcement: audit + - host: "*.google.com" + port: 443 + enforcement: audit + - host: "*.googleusercontent.com" + port: 443 + enforcement: audit + binaries: + - { path: /usr/bin/node } + - { path: /usr/bin/curl } + + # ═══ Messaging Channels ═════════════════════════════ + + telegram: + name: telegram + endpoints: + - host: "*.telegram.org" + port: 443 + enforcement: audit + binaries: - { path: /usr/bin/node } - gitlab: - name: gitlab + + # Slack REST API — explicit per-subdomain entries. + # + # ⚠️ Do NOT use a *.slack.com wildcard here. On OpenShell ≤ 0.0.15 a + # wildcard match takes priority over the more-specific slack_websocket + # policy below, causing WS connections to hit the wrong TLS mode and + # break the Socket Mode frame stream. + slack: + name: slack endpoints: - - { host: gitlab.com, port: 443 } - - { host: gitlab.mycorp.com, port: 443 } + - host: slack.com + port: 443 + enforcement: audit + - host: api.slack.com + port: 443 + enforcement: audit + - host: files.slack.com + port: 443 + enforcement: audit + - host: hooks.slack.com + port: 443 + enforcement: audit + - host: edgeapi.slack.com + port: 443 + enforcement: audit binaries: - - { path: /usr/bin/glab } + - { path: /usr/bin/node } + + # Separate policy for Slack WebSocket (Socket Mode). + # + # tls: skip forces raw TCP passthrough — without it the L7 proxy + # intercepts the TLS handshake and breaks the WebSocket upgrade. + # enforcement: enforce because this carries live Socket Mode traffic. + slack_websocket: + name: slack_websocket + endpoints: + - host: wss-primary.slack.com + port: 443 + tls: skip + enforcement: enforce + - host: wss-backup.slack.com + port: 443 + tls: skip + enforcement: enforce + binaries: + - { path: /usr/bin/node } + - { path: /usr/bin/curl } + + # ═══ Code & Package Registries ══════════════════════ + github: name: github endpoints: - host: github.com port: 443 - protocol: rest - tls: terminate - enforcement: enforce - rules: - # Git Smart HTTP read-only: allow clone, fetch, pull - # Discovery (query string is included in path matching) - - allow: - method: GET - path: "/**/info/refs*" - # Data transfer for reads (all repos) - - allow: - method: POST - path: "/**/git-upload-pack" - # Data transfer for writes (uncomment and scope to specific repos) - # - allow: - # method: POST - # path: "///git-receive-pack" + enforcement: audit + - host: api.github.com + port: 443 + enforcement: audit + - host: "*.ghcr.io" + port: 443 + enforcement: audit + - host: formulae.brew.sh + port: 443 + enforcement: audit + - host: "*.githubusercontent.com" + port: 443 + enforcement: audit binaries: + - { path: /usr/bin/node } - { path: /usr/bin/git } - nvidia: - name: nvidia + - { path: /usr/bin/gh } + - { path: /usr/bin/curl } + + registries: + name: registries endpoints: - - { host: integrate.api.nvidia.com, port: 443 } + - host: registry.npmjs.org + port: 443 + enforcement: audit + - host: pypi.org + port: 443 + enforcement: audit + - host: files.pythonhosted.org + port: 443 + enforcement: audit binaries: + - { path: /usr/bin/node } + - { path: /usr/bin/npm } + - { path: /usr/bin/pip } + - { path: /usr/bin/pip3 } - { path: /usr/bin/curl } - - { path: /bin/bash } - - { path: /usr/local/bin/opencode } - nvidia_web: - name: nvidia_web + + # ═══ Web Search & Fetch ═════════════════════════════ + + brave: + name: brave-search endpoints: - - { host: nvidia.com, port: 443 } - - { host: www.nvidia.com, port: 443 } + - host: api.search.brave.com + port: 443 + enforcement: audit binaries: + - { path: /usr/bin/node } - { path: /usr/bin/curl } - # --- GitHub REST API (L7, TLS-terminated) --- - # Read-only by default. Uncomment and scope write rules to specific repos. - github_rest_api: - name: github-rest-api + openrouter: + name: openrouter endpoints: - - host: api.github.com + - host: openrouter.ai port: 443 - protocol: rest - tls: terminate - enforcement: enforce - rules: - # Read-only access to all GitHub API paths - - allow: - method: GET - path: "/**" - - allow: - method: HEAD - path: "/**" - - allow: - method: OPTIONS - path: "/**" - # GraphQL API (used by gh CLI for most operations) - # - allow: - # method: POST - # path: "/graphql" - # Per-repo write access (uncomment and replace /) - # - allow: - # method: "*" - # path: "/repos///**" + enforcement: audit binaries: - - { path: /usr/local/bin/claude } - - { path: /usr/bin/gh } + - { path: /usr/bin/node } + + # Per-site web access for web_fetch / web_search tools. + # TLD wildcards (*.com) cause OPA errors — use subdomain wildcards. + linkedin: + name: linkedin + endpoints: + - host: "*.linkedin.com" + port: 443 + enforcement: audit + - host: linkedin.com + port: 443 + enforcement: audit + binaries: + - { path: /usr/bin/node } + - { path: /usr/bin/curl }