Skill: davida-ps/clawsec-nanoclaw (version 0.0.6)

--- name: clawsec-nanoclaw version: 0.0.6 description: Use when checking for security vulnerabilities in NanoClaw skills, before installing new skills, or when asked about security advisories affecting the bot --- # ClawSec for NanoClaw Security advisory monitoring that protects your WhatsApp bot from known vulnerabilities in skills and dependencies. ## Overview ClawSec provides MCP tools that check installed skills against a curated feed of security advisories. It prevents installation of vulnerable skills, includes exploitability context for triage, and alerts you to issues in existing ones. **Core principle:** Check before you install. Monitor what's running. ## When to Use Use ClawSec tools when: - Installing a new skill (check safety first) - User asks "are my skills secure?" - Investigating suspicious behavior - Regular security audits - After receiving security notifications Do NOT use for: - Code review (use other tools) - Performance issues (different concern) - General debugging ## MCP Tools Available ### Pre-Installation Check ```typescript // Before installing any skill const safety = await tools.clawsec_check_skill_safety({ skillName: 'new-skill', skillVersion: '1.0.0' // optional }); if (!safety.safe) { // Show user the risks before proceeding console.warn(`Security issues: ${safety.advisories.map(a => a.id)}`); } ``` ### Security Audit ```typescript // Check all installed skills (defaults to ~/.claude/skills in the container) const result = await tools.clawsec_check_advisories({ installRoot: '/home/node/.claude/skills' // optional }); if (result.matches.some((m) => m.advisory.severity === 'critical' || m.advisory.exploitability_score === 'high' )) { // Alert user immediately console.error('Urgent advisories found!'); } ``` ### Browse Advisories ```typescript // List advisories with filters const advisories = await tools.clawsec_list_advisories({ severity: 'high', // optional exploitabilityScore: 'high' // optional }); ``` ## Quick Reference | Task | Tool | Key Parameter | |------|------|---------------| | Pre-install check | `clawsec_check_skill_safety` | `skillName` | | Audit all skills | `clawsec_check_advisories` | `installRoot` (optional) | | Browse feed | `clawsec_list_advisories` | `severity`, `type`, `exploitabilityScore` (optional) | | Verify package signature | `clawsec_verify_skill_package` | `packagePath` | | Refresh advisory cache | `clawsec_refresh_cache` | (none) | | Check file integrity | `clawsec_check_integrity` | `mode`, `autoRestore` (optional) | | Approve file change | `clawsec_approve_change` | `path` | | View baseline status | `clawsec_integrity_status` | `path` (optional) | | Verify audit log | `clawsec_verify_audit` | (none) | ## Common Patterns ### Pattern 1: Safe Skill Installation ```typescript // ALWAYS check before installing const safety = await tools.clawsec_check_skill_safety({ skillName: userRequestedSkill }); if (safety.safe) { // Proceed with installation await installSkill(userRequestedSkill); } else { // Show user the risks and get confirmation await showSecurityWarning(safety.advisories); if (await getUserConfirmation()) { await installSkill(userRequestedSkill); } } ``` ### Pattern 2: Periodic Security Check ```typescript // Add to scheduled tasks schedule_task({ prompt: "Check advisories using clawsec_check_advisories and alert when critical or high-exploitability matches appear", schedule_type: "cron", schedule_value: "0 9 * * *" // Daily at 9am }); ``` ### Pattern 3: User Security Query ``` User: "Are my skills secure?" You: I'll check installed skills for known vulnerabilities. [Use clawsec_check_advisories] Response: ✅ No urgent issues found. - 2 low-severity/low-exploitability advisories - All skills up to date ``` ## Common Mistakes ### ❌ Installing without checking ```typescript // DON'T await installSkill('untrusted-skill'); ``` ```typescript // DO const safety = await tools.clawsec_check_skill_safety({ skillName: 'untrusted-skill' }); if (safety.safe) await installSkill('untrusted-skill'); ``` ### ❌ Ignoring exploitability context ```typescript // DON'T: Use severity only if (advisory.severity === 'high') { notifyNow(advisory); } ``` ```typescript // DO: Use exploitability + severity if ( advisory.exploitability_score === 'high' || advisory.severity === 'critical' ) { notifyNow(advisory); } ``` ### ❌ Skipping critical severity ```typescript // DON'T: Ignore high exploitability in medium severity advisories if (advisory.severity === 'critical') alert(); ``` ```typescript // DO: Prioritize exploitability and severity together if (advisory.exploitability_score === 'high' || advisory.severity === 'critical') { // Alert immediately } ``` ## Implementation Details **Feed Source**: https://clawsec.prompt.security/advisories/feed.json This signed feed is consolidated. NanoClaw receives NVD CVEs, approved community advisories, and provisional GHSA-without-CVE advisories through the same default URL. **Update Frequency**: Every 6 hours (automatic) **Signature Verification**: Ed25519 signed feeds **Package Verification Policy**: pinned key only, bounded package/signature paths **Cache Location**: `/workspace/project/data/clawsec-advisory-cache.json` See [INSTALL.md](./INSTALL.md) for setup and [docs/](./docs/) for advanced usage. ## Real-World Impact - Prevents installation of skills with known RCE vulnerabilities - Alerts to supply chain attacks in dependencies - Provides actionable remediation steps - Zero false positives (curated feed only) ## Release Artifact Verification For standalone installs, verify the signed release manifest before trusting `SKILL.md`, `skill.json`, or the archive. The `skill.json` file is the package metadata/SBOM source, and the release pipeline signs `checksums.json` with the ClawSec release key. ```bash set -euo pipefail SKILL_NAME="clawsec-nanoclaw" VERSION="0.0.6" REPO="prompt-security/clawsec" TAG="${SKILL_NAME}-v${VERSION}" BASE="https://github.com/${REPO}/releases/download/${TAG}" ZIP_NAME="${SKILL_NAME}-v${VERSION}.zip" TMP_DIR="$(mktemp -d)" trap 'rm -rf "$TMP_DIR"' EXIT RELEASE_PUBKEY_SHA256=[REDACTED_SECRET] curl -fsSL "$BASE/checksums.json" -o "$TMP_DIR/checksums.json" curl -fsSL "$BASE/checksums.sig" -o "$TMP_DIR/checksums.sig" curl -fsSL "$BASE/signing-public.pem" -o "$TMP_DIR/signing-public.pem" curl -fsSL "$BASE/$ZIP_NAME" -o "$TMP_DIR/$ZIP_NAME" curl -fsSL "$BASE/SKILL.md" -o "$TMP_DIR/SKILL.md" curl -fsSL "$BASE/skill.json" -o "$TMP_DIR/skill.json" ACTUAL_PUBKEY_SHA256="$(openssl pkey -pubin -in "$TMP_DIR/signing-public.pem" -outform DER | shasum -a 256 | awk '{print $1}')" if [ "$ACTUAL_PUBKEY_SHA256" != "$RELEASE_PUBKEY_SHA256" ]; then echo "ERROR: signing-public.pem fingerprint mismatch" >&2 exit 1 fi openssl base64 -d -A -in "$TMP_DIR/checksums.sig" -out "$TMP_DIR/checksums.sig.bin" openssl pkeyutl -verify -rawin -pubin \ -inkey "$TMP_DIR/signing-public.pem" \ -sigfile "$TMP_DIR/checksums.sig.bin" \ -in "$TMP_DIR/checksums.json" >/dev/null hash_file() { if command -v shasum >/dev/null 2>&1; then shasum -a 256 "$1" | awk '{print $1}' else sha256sum "$1" | awk '{print $1}' fi } verify_manifest_file() { asset="$1" path="$2" expected="$(jq -r --arg asset "$asset" '.files[$asset].sha256 // empty' "$TMP_DIR/checksums.json")" if [ -z "$expected" ]; then echo "ERROR: checksums.json missing $asset" >&2 exit 1 fi actual="$(hash_file "$path")" if [ "$actual" != "$expected" ]; then echo "ERROR: checksum mismatch for $asset" >&2 exit 1 fi } expected_archive="$(jq -r '.archive.sha256 // empty' "$TMP_DIR/checksums.json")" if [ -z "$expected_archive" ]; then echo "ERROR: checksums.json missing archive.sha256" >&2 exit 1 fi actual_archive="$(hash_file "$TMP_DIR/$ZIP_NAME")" if [ "$actual_archive" != "$expected_archive" ]; then echo "ERROR: archive checksum mismatch" >&2 exit 1 fi verify_manifest_file "SKILL.md" "$TMP_DIR/SKILL.md" verify_manifest_file "skill.json" "$TMP_DIR/skill.json" echo "Signed release manifest, archive, SKILL.md, and skill.json verified." ``` Only install or extract the archive after this verification succeeds.

path: CHANGELOG.md
content: # Changelog

## [0.0.6] - 2026-05-24

### Changed
- Documented that NanoClaw consumes the consolidated signed advisory feed containing NVD CVEs, approved community advisories, and provisional GHSA-without-CVE records.
- Added advisory metadata typing for GHSA lifecycle fields used by the consolidated feed.

## [0.0.5] - 2026-05-14

### Security
- Added explicit signed release artifact verification instructions for standalone installs, including `checksums.json`, `checksums.sig`, `signing-public.pem`, archive hash verification, and `SKILL.md`/`skill.json` checksum checks.

All notable changes to the ClawSec NanoClaw compatibility skill will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [0.0.4] - 2026-04-16

### Changed

- Moved signature-related local file reads into `lib/local_file_io.ts` and kept network fetch logic isolated in `lib/signatures.ts`.

### Security

- Reduced static false-positive exfiltration signals by separating local file I/O and remote fetch code paths.

## [0.0.3] - 2026-03-09

### Security

- Removed runtime public-key override from host-side package signature verification; verification now always uses the pinned ClawSec key.
- Removed unsigned-package override path in host-side verification flow.
- Added strict package/signature path policy for signature verification (`/tmp`, `/var/tmp`, `/workspace/ipc`, `/workspace/project/data`, `/workspace/project/tmp`, `/workspace/project/downloads`) with absolute-path, extension, symlink, and realpath boundary checks.
- Added policy-bound path enforcement for integrity approvals: approvals now require normalized paths that are explicitly present in non-ignored integrity policy targets.

### Changed

- Updated MCP signature verification tool docs and behavior to align with bounded path policy and pinned-key-only verification.
- Added regression tests for signature-verification and integrity-approval hardening invariants.

## [0.0.2] - 2026-02-28

### Added

- Exploitability-aware advisory output in NanoClaw MCP tools (`exploitability_score`, `exploitability_rationale`).
- Exploitability filtering (`exploitabilityScore`) for `clawsec_list_advisories`.

### Changed

- Updated NanoClaw advisory sorting and pre-install safety recommendation logic to prioritize exploitability context.
- Updated NanoClaw integration docs to match current host/container integration points (`src/ipc.ts`, `src/index.ts`) and current cache schema.
- Removed duplicate exploitability normalization logic from MCP advisory tools and now reuse `normalizeExploitabilityScore` from `lib/risk.ts`.
- Reused `matchesAffectedSpecifier` from `lib/advisories.ts` in MCP advisory tools to keep skill/version matching logic centralized and consistent.


path: INSTALL.md
content: # ClawSec for NanoClaw - Installation Guide

This guide shows how to add ClawSec security monitoring to your NanoClaw deployment.

## Overview

ClawSec provides security advisory monitoring for NanoClaw through:
- **MCP Tools**: Agents can check for vulnerabilities via `clawsec_check_advisories`
- **Advisory Feed**: Automatic monitoring of https://clawsec.prompt.security/advisories/feed.json
- **Signature Verification**: Ed25519-signed feeds ensure integrity
- **Exploitability Context**: Advisories include exploitability score and rationale for triage

## Prerequisites

- NanoClaw >= 0.1.0
- Node.js >= 18.0.0
- Write access to NanoClaw installation directory

## Installation Steps

### 1. Copy Skill Files

Copy the `clawsec-nanoclaw` skill directory to your NanoClaw installation:

```bash
# From the ClawSec repository
cp -r skills/clawsec-nanoclaw /path/to/your/nanoclaw/skills/
```

### 2. Integrate MCP Tools

Add the ClawSec MCP tools to your NanoClaw container agent runner.

**File**: `container/agent-runner/src/ipc-mcp-stdio.ts`

```typescript
// Add these imports at the top to register all ClawSec MCP tools:

// Advisory tools: clawsec_check_advisories, clawsec_check_skill_safety,
//                 clawsec_list_advisories, clawsec_refresh_cache
import '../../../skills/clawsec-nanoclaw/mcp-tools/advisory-tools.js';

// Signature verification: clawsec_verify_skill_package
import '../../../skills/clawsec-nanoclaw/mcp-tools/signature-verification.js';

// Integrity monitoring: clawsec_check_integrity, clawsec_approve_change,
//                       clawsec_integrity_status, clawsec_verify_audit
import '../../../skills/clawsec-nanoclaw/mcp-tools/integrity-tools.js';
```

Each file calls `server.tool()` directly to register its tools. The `server`,
`writeIpcFile`, `TASKS_DIR`, and `groupFolder` variables must be available in
the scope where these files are imported (they are declared as ambient globals
in each tool file).

### 3. Integrate IPC Handlers

Add the host-side IPC handlers for ClawSec operations.

**File**: `src/ipc.ts`

```typescript
// Add these imports at the top
import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
import { SkillSignatureVerifier } from '../skills/clawsec-nanoclaw/host-services/skill-signature-handler.js';

// Initialize these once in host startup and pass through deps
const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger);
const signatureVerifier = new SkillSignatureVerifier();

// In processTaskIpc switch:
case 'refresh_advisory_cache':
case 'verify_skill_signature':
  await handleAdvisoryIpc(
    data,
    { advisoryCacheManager, signatureVerifier },
    logger,
    sourceGroup
  );
  break;
default:
  // existing task handling
}
```

### 4. Start Advisory Cache Service

Add the advisory cache manager to your host services.

**File**: `src/index.ts` (or your main entry point)

```typescript
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';

// Start the service when your host process starts
async function main() {
  // ... your existing initialization ...

  // Initialize cache manager and prime it at startup
  const advisoryCacheManager = new AdvisoryCacheManager('/workspace/project/data', logger);
  await advisoryCacheManager.initialize();

  // Recommended refresh cadence (6h)
  setInterval(() => {
    advisoryCacheManager.refresh().catch((error) => {
      logger.error({ error }, 'Periodic advisory cache refresh failed');
    });
  }, 6 * 60 * 60 * 1000);

  // ... rest of your startup ...
}
```

### 5. Restart NanoClaw

Restart your NanoClaw instance to load the new MCP tools and services:

```bash
# Stop NanoClaw
docker-compose down

# Start with new configuration
docker-compose up -d
```

## Verification

Test that ClawSec is working:

### 1. Check MCP Tools Available

From within a NanoClaw agent session, the following tools should be available:

**Advisory Tools** (mcp-tools/advisory-tools.ts):
- `clawsec_check_advisories` - Scan installed skills for vulnerabilities
- `clawsec_check_skill_safety` - Pre-installation safety check
- `clawsec_list_advisories` - List all advisories with filtering
- `clawsec_refresh_cache` - Request immediate advisory cache refresh

**Signature Verification** (mcp-tools/signature-verification.ts):
- `clawsec_verify_skill_package` - Verify Ed25519 signature on skill packages
  - Uses pinned ClawSec public key (no runtime key override)
  - Accepts staged package/signature paths only under `/tmp`, `/var/tmp`, `/workspace/ipc`, `/workspace/project/data`, `/workspace/project/tmp`, `/workspace/project/downloads`

**Integrity Monitoring** (mcp-tools/integrity-tools.ts):
- `clawsec_check_integrity` - Check protected files for unauthorized changes
- `clawsec_approve_change` - Approve intentional file modification as new baseline
- `clawsec_integrity_status` - View current baseline status
- `clawsec_verify_audit` - Verify audit log hash chain integrity

### 2. Test Advisory Checking

Ask your NanoClaw agent:
```
Check if any of my installed skills have security advisories
```

The agent should use the `clawsec_check_advisories` tool and report results.

### 3. Check Advisory Cache

Verify the cache file was created:
```bash
cat /workspace/project/data/clawsec-advisory-cache.json
```

You should see:
- `feed`: Array of advisories
- `fetchedAt`: Timestamp of last update
- `verified`: Should be `true`
- `publicKeyFingerprint`: SHA-256 fingerprint of the pinned signing key

## Usage Examples

### Agent Commands

Once installed, your NanoClaw agents can:

**Check for vulnerabilities:**
```
Scan my installed skills for security issues
```

**Pre-installation check:**
```
Is it safe to install skill-name@1.0.0?
```

**List all advisories:**
```
Show me all ClawSec security advisories
```

### Manual Tool Invocation

You can also call the MCP tools directly from agent code:

```typescript
// Check all installed skills
const result = await tools.clawsec_check_advisories({
  installRoot: '/home/node/.claude/skills'
});

// Check specific skill before installation
const safetyCheck = await tools.clawsec_check_skill_safety({
  skillName: 'risky-skill',
  skillVersion: '1.0.0'
});
```

## Configuration

### Cache Location

Default: `/workspace/project/data/clawsec-advisory-cache.json`

To change, pass a different data directory path to `new AdvisoryCacheManager(dataDir, logger)`.

### Refresh Interval

Default: 6 hours

To change, update the `setInterval(...)` duration (in milliseconds) in host startup.

### Feed URL

Default: `https://clawsec.prompt.security/advisories/feed.json`

To use a mirror or custom feed, update `FEED_URL` in `skills/clawsec-nanoclaw/host-services/advisory-cache.ts`.

## Platform-Specific Advisories

ClawSec advisories can target specific platforms:

- **`platforms: ["nanoclaw"]`**: Only affects NanoClaw
- **`platforms: ["openclaw"]`**: Only affects OpenClaw/MoltBot
- **`platforms: ["openclaw", "nanoclaw"]`**: Affects both
- **No `platforms` field**: Applies to all platforms

Platform metadata is preserved in advisory records and can be filtered by your policy layer.

## Security

### Signature Verification

All advisory feeds are Ed25519 signed. The public key is pinned in:
```
skills/clawsec-nanoclaw/advisories/feed-signing-public.pem
```

Feeds failing signature verification are rejected.

### Cache Integrity

The advisory cache includes:
- Cryptographic signature of feed contents
- Verification status
- Timestamp of last successful fetch

Never manually edit the cache file - it will break signature verification.

## Troubleshooting

### Tools Not Appearing

**Problem**: MCP tools not showing up in agent

**Solution**:
1. Check that you added the import and registration in `ipc-mcp-stdio.ts`
2. Restart the container
3. Check container logs for import errors

### Cache Not Updating

**Problem**: Advisory cache is empty or stale

**Solution**:
1. Check that `AdvisoryCacheManager.initialize()` is called in your host entry point
2. Verify network access to `clawsec.prompt.security`
3. Check host logs for fetch errors
4. Manually trigger: `curl https://clawsec.prompt.security/advisories/feed.json`

### Signature Verification Failing

**Problem**: Cache shows `"verified": false`

**Solution**:
1. Ensure public key file exists at correct path
2. Check file permissions (should be readable)
3. Verify feed URL is correct (not using HTTP instead of HTTPS)
4. Check for corrupted downloads (try clearing cache and refetching)

### IPC Communication Issues

**Problem**: Tools return errors about IPC

**Solution**:
1. Verify IPC handlers are registered in `src/ipc.ts`
2. Check that IPC directory exists and is writable
3. Ensure host process is running
4. Check host logs for handler errors

## Uninstallation

To remove ClawSec from NanoClaw:

1. Remove MCP tool registration from `ipc-mcp-stdio.ts`
2. Remove IPC handler registration from `src/ipc.ts`
3. Remove `AdvisoryCacheManager` initialization from host entry point
4. Delete the skill directory: `rm -rf skills/clawsec-nanoclaw`
5. Delete the cache file: `rm /workspace/project/data/clawsec-advisory-cache.json`
6. Restart NanoClaw

## Support

- **Documentation**: https://clawsec.prompt.security/
- **Issues**: https://github.com/prompt-security/clawsec/issues
- **Security**: [REDACTED_SECRET]

## License

AGPL-3.0-or-later

---

**Questions?** Open an issue or check the main ClawSec documentation.


path: README.md
content: # ClawSec for NanoClaw

ClawSec now supports NanoClaw, a containerized WhatsApp bot powered by Claude agents.

## What Changed

### Advisory Feed Monitoring
- **NVD CVE Pipeline**: Now monitors for NanoClaw-specific keywords
  - "NanoClaw", "WhatsApp-bot", "baileys" (WhatsApp library)
  - Container-related vulnerabilities
- **Platform Targeting**: Advisories can specify `platforms: ["nanoclaw"]` for NanoClaw-specific issues

### Keywords Added
The CVE monitoring now includes:
- `NanoClaw` - Direct product name
- `WhatsApp-bot` - Core functionality
- `baileys` - WhatsApp client library dependency

## Advisory Schema

Advisories now support optional `platforms` field:

```json
{
  "id": "CVE-2026-XXXXX",
  "platforms": ["openclaw", "nanoclaw"],
  "severity": "critical",
  "type": "prompt_injection",
  "affected": ["skill-name@1.0.0"],
  "action": "Update to version 1.0.1"
}
```

**Platform values:**
- `"openclaw"` - Affects OpenClaw/ClawdBot/MoltBot only
- `"nanoclaw"` - Affects NanoClaw only
- `["openclaw", "nanoclaw"]` - Affects both platforms
- (empty/missing) - Applies to all platforms (backward compatible)

## ClawSec NanoClaw Skill

ClawSec provides a complete security skill for NanoClaw deployments:

**Location**: `skills/clawsec-nanoclaw/`

### Features

- **9 MCP Tools** for agents to manage security:
  - `clawsec_check_advisories` - Scan installed skills for vulnerabilities
  - `clawsec_check_skill_safety` - Pre-installation safety checks
  - `clawsec_list_advisories` - Browse advisory feed with filtering
  - `clawsec_refresh_cache` - Request immediate advisory cache refresh
  - `clawsec_verify_skill_package` - Verify Ed25519 signatures on skill packages
  - `clawsec_check_integrity` - Check protected files for unauthorized changes
  - `clawsec_approve_change` - Approve intentional file modifications
  - `clawsec_integrity_status` - View file baseline status
  - `clawsec_verify_audit` - Verify audit log hash chain

- **Advisory Cache Service**: Host-managed feed fetching with signature validation
- **Signature Verification**: Ed25519-signed feeds ensure integrity
- **Exploitability Context**: Surfaces `exploitability_score` and rationale to reduce alert fatigue
- **IPC Communication**: Container-safe host communication

### Installation

1. Copy the skill to your NanoClaw deployment:
   ```bash
   cp -r skills/clawsec-nanoclaw /path/to/nanoclaw/skills/
   ```

2. Follow the detailed guide at `skills/clawsec-nanoclaw/INSTALL.md`

### Quick Integration

The skill integrates into three places:

**1. MCP Tools** (container):
```typescript
// container/agent-runner/src/ipc-mcp-stdio.ts
import '../../../skills/clawsec-nanoclaw/mcp-tools/advisory-tools.js';
```

**2. IPC Handlers** (host):
```typescript
// src/ipc.ts
import { handleAdvisoryIpc } from '../skills/clawsec-nanoclaw/host-services/ipc-handlers.js';
```

**3. Cache Service** (host):
```typescript
// src/index.ts
import { AdvisoryCacheManager } from '../skills/clawsec-nanoclaw/host-services/advisory-cache.js';
```

### Advisory Feed

NanoClaw consumes the same feed as OpenClaw:
```
https://clawsec.prompt.security/advisories/feed.json
```

The feed is Ed25519 signed and automatically fetched by the cache service.

## Team Credits

This integration was developed by a team of 8 specialized agents coordinated to adapt ClawSec for NanoClaw:

- **pioneer-repo-scout** - ClawSec architecture analysis
- **pioneer-nanoclaw-scout** - NanoClaw architecture analysis
- **architect** - Integration design and coordination
- **advisory-specialist** - Advisory feed integration
- **integrity-specialist** - File integrity design
- **installer-specialist** - Signature verification implementation
- **tester** - Test infrastructure and validation
- **documenter** - Documentation

Total contribution: 3000+ lines of code and comprehensive design documents.

## What's Included

The `clawsec-nanoclaw` skill provides:

- **1,730 lines** of production-ready TypeScript code
- **MCP Tools** (350 lines): Agent-facing vulnerability checking
- **Advisory Cache** (492 lines): Automatic feed fetching and caching
- **Signature Verification** (387 lines): Ed25519 signature validation
- **Advisory Matching** (289 lines): Skill-to-vulnerability correlation
- **IPC Handlers** (212 lines): Container-to-host communication
- **Complete Documentation**: Installation guide, usage examples, troubleshooting

## Future Enhancements

Planned features for future releases:
- File integrity monitoring (soul-guardian adaptation for containers)
- Real-time advisory alerts via WebSocket
- WhatsApp-native security alert formatting
- Behavioral analysis and anomaly detection
- Custom/private advisory feed support

## Documentation

- [Skill Documentation](skills/clawsec-nanoclaw/SKILL.md) - Features and architecture
- [Installation Guide](skills/clawsec-nanoclaw/INSTALL.md) - Detailed setup instructions
- [ClawSec Main README](README.md) - Overall ClawSec documentation
- [Security & Signing](../../wiki/security-signing-runbook.md) - Signature verification details

## Support

- **Issues**: https://github.com/prompt-security/clawsec/issues
- **Security**: [REDACTED_SECRET]
- NanoClaw Repository: https://github.com/qwibitai/nanoclaw


path: docs/INTEGRITY.md
content: # File Integrity Monitoring for NanoClaw

ClawSec's file integrity monitoring protects critical NanoClaw configuration files from unauthorized modification.

## What It Does

**Protects Critical Files:**
- `registered_groups.json` - Prevents unauthorized group access
- `CLAUDE.md` files - Protects agent instructions
- Container/host code - Alerts on unexpected changes

**How It Works:**
1. **Baseline**: Stores SHA-256 hashes of approved file states
2. **Monitoring**: Periodically checks files for changes (drift)
3. **Restore**: Automatically reverts critical files to approved versions
4. **Audit**: Maintains tamper-evident log of all operations

## Quick Start

### Step 1: Verify Installation

Check that integrity monitoring is available:

```bash
# From container
ls /workspace/project/skills/clawsec-nanoclaw/guardian/
# Should show: policy.json, integrity-monitor.ts
```

### Step 2: Initialize Baselines

The first time integrity monitoring runs, it creates baselines automatically:

```typescript
// Agent calls this (happens automatically on first integrity check)
await tools.clawsec_check_integrity();
```

This creates:
```
/workspace/project/data/soul-guardian/
├── baselines.json       # SHA-256 hashes
├── approved/            # File snapshots
│   ├── registered_groups.json
│   └── CLAUDE.md
├── patches/             # Diffs (empty initially)
├── quarantine/          # Tampered files (empty initially)
└── audit.jsonl          # Event log
```

### Step 3: Enable Scheduled Monitoring

Add to main group's scheduled tasks:

```typescript
schedule_task({
  prompt: `
    Check file integrity with clawsec_check_integrity.
    If drift detected and files restored, send WhatsApp message:
    "⚠️ SECURITY ALERT

    Unauthorized changes detected and automatically reverted:
    [list files that were restored]

    Review details: /workspace/project/data/soul-guardian/patches/"
  `,
  schedule_type: 'cron',
  schedule_value: '*/30 * * * *',  // Every 30 minutes
  context_mode: 'isolated'
});
```

That's it! Integrity monitoring is now active.

## MCP Tools Reference

### 1. `clawsec_check_integrity`

Check all protected files for unauthorized changes.

**Parameters:**
- `mode` (optional): `'check'` (default) or `'status'`
  - `check`: Detect drift and auto-restore
  - `status`: View baselines only (no drift detection)
- `autoRestore` (optional): `true` (default) or `false`
  - If `false`, drift is detected but not auto-fixed

**Output:**
```json
{
  "success": true,
  "timestamp": "2026-02-25T12:00:00Z",
  "drift_detected": false,
  "files": [
    {
      "path": "/workspace/project/data/registered_groups.json",
      "status": "ok",
      "mode": "restore",
      "expected_sha": "abc123...",
      "found_sha": "abc123..."
    }
  ],
  "summary": {
    "total": 3,
    "ok": 3,
    "drifted": 0,
    "restored": 0,
    "alerted": 0,
    "errors": 0
  }
}
```

**Example:**
```typescript
const result = await tools.clawsec_check_integrity();

if (result.drift_detected) {
  console.log('⚠️ Drift detected!');
  for (const file of result.files) {
    if (file.status === 'restored') {
      console.log(`✅ Restored: ${file.path}`);
      console.log(`  Diff: ${file.patch_path}`);
    } else if (file.status === 'drifted') {
      console.log(`⚠️ Changed: ${file.path} (alert only)`);
    }
  }
}
```

### 2. `clawsec_approve_change`

Approve an intentional file modification as the new baseline.

**When to use:**
- After legitimately updating CLAUDE.md
- After adding/removing groups in registered_groups.json
- After any intentional change to protected files

**Parameters:**
- `path` (required): Absolute path to file
- `note` (optional): Explanation for audit log

**Output:**
```json
{
  "success": true,
  "path": "/workspace/group/CLAUDE.md",
  "approved_at": "2026-02-25T12:00:00Z",
  "approved_by": "agent",
  "note": "Added new skill instructions"
}
```

**Example:**
```typescript
// After editing CLAUDE.md
await tools.clawsec_approve_change({
  path: '/workspace/group/CLAUDE.md',
  note: 'Updated agent instructions for new skill'
});

console.log('✅ Change approved - new baseline created');
```

### 3. `clawsec_integrity_status`

View current baseline status without checking for drift.

**Parameters:**
- `path` (optional): Specific file, or all if omitted

**Output:**
```json
{
  "success": true,
  "baseline_age": "2026-02-25T10:00:00Z",
  "files": [
    {
      "path": "/workspace/project/data/registered_groups.json",
      "mode": "restore",
      "priority": "critical",
      "has_baseline": true,
      "baseline_sha": "abc123...",
      "approved_at": "2026-02-25T10:00:00Z",
      "snapshot_exists": true
    }
  ]
}
```

**Example:**
```typescript
const status = await tools.clawsec_integrity_status();

console.log('Protected files:');
for (const file of status.files) {
  console.log(`- ${file.path} (${file.mode}, ${file.priority})`);
  console.log(`  Last approved: ${file.approved_at}`);
}
```

### 4. `clawsec_verify_audit`

Verify audit log hash chain integrity.

**No parameters.**

**Output:**
```json
{
  "success": true,
  "valid": true,
  "entries": 42,
  "errors": []
}
```

**Example:**
```typescript
const verification = await tools.clawsec_verify_audit();

if (!verification.valid) {
  console.log('🚨 CRITICAL: Audit log has been tampered with!');
  console.log('Errors:', verification.errors);
} else {
  console.log(`✅ Audit log verified (${verification.entries} entries)`);
}
```

## Protected Files Policy

### Critical Priority (Auto-Restore)

**`/workspace/project/data/registered_groups.json`**
- **Risk**: Tampering grants unauthorized group access
- **Action**: Immediate auto-restore + alert

**`/workspace/group/CLAUDE.md`**
- **Risk**: Modifies agent behavior
- **Action**: Immediate auto-restore + alert

**`/workspace/project/groups/global/CLAUDE.md`**
- **Risk**: Affects all groups
- **Action**: Immediate auto-restore + alert

### Medium Priority (Alert Only)

**Container code** (`/workspace/project/container/**/*.ts`)
- **Risk**: Unexpected code changes
- **Action**: Alert for review (no auto-restore)

**Host code** (`/workspace/project/host/**/*.ts`)
- **Risk**: Unexpected code changes
- **Action**: Alert for review (no auto-restore)

### Ignored

**IPC files** (`/workspace/ipc/**/*`)
- Changes are expected and frequent

**Conversations** (`/workspace/group/conversations/**/*`)
- Changes are expected and frequent

## Workflow Examples

### Scenario 1: Scheduled Monitoring

**Setup:**
```typescript
schedule_task({
  prompt: 'Run clawsec_check_integrity and alert on drift',
  schedule_type: 'cron',
  schedule_value: '*/30 * * * *'
});
```

**What happens:**
1. Every 30 minutes, agent checks integrity
2. If drift detected in critical files:
   - Files auto-restored to baseline
   - Tampered versions quarantined
   - Diff patch generated
   - User alerted via WhatsApp
3. If drift in non-critical files:
   - Alert only, no auto-restore

### Scenario 2: Updating Agent Instructions

**Workflow:**
```typescript
// 1. Edit CLAUDE.md
fs.writeFileSync('/workspace/group/CLAUDE.md', newInstructions);

// 2. Test changes
// ... verify agent behaves correctly ...

// 3. Approve changes
await tools.clawsec_approve_change({
  path: '/workspace/group/CLAUDE.md',
  note: 'Added instructions for new weather skill'
});

// 4. Future integrity checks will use this new baseline
```

### Scenario 3: Adding a New Group

**Workflow:**
```typescript
// 1. Add group to registered_groups.json
const groups = JSON.parse(fs.readFileSync('/workspace/project/data/registered_groups.json'));
groups['new-jid'] = { name: 'Family', folder: 'family', trigger: '@Andy' };
fs.writeFileSync('/workspace/project/data/registered_groups.json', JSON.stringify(groups, null, 2));

// 2. Approve the change
await tools.clawsec_approve_change({
  path: '/workspace/project/data/registered_groups.json',
  note: 'Added family group'
});
```

### Scenario 4: Investigating Drift

**When drift is detected:**
```typescript
const result = await tools.clawsec_check_integrity();

if (result.drift_detected) {
  for (const file of result.files) {
    if (file.status === 'restored') {
      // Critical file was auto-restored
      console.log(`🔧 Auto-restored: ${file.path}`);
      console.log(`📄 Diff: ${file.patch_path}`);
      console.log(`📦 Quarantine: ${file.quarantine_path}`);

      // Review the diff
      const diff = fs.readFileSync(file.patch_path, 'utf-8');
      console.log('Changes that were reverted:');
      console.log(diff);
    }
  }
}
```

## Security Model

### Threat Model

**Protects Against:**
- Unauthorized file modifications
- Group hijacking (via registered_groups.json tampering)
- Agent instruction poisoning (via CLAUDE.md changes)
- Accidental file corruption

**Does NOT Protect Against:**
- Attacker with full host access (can modify baselines)
- Simultaneous baseline + file modification
- Malicious scheduled tasks that approve their own changes

### Baseline Storage

**Location:** `/workspace/project/data/soul-guardian/`

**Access Control:**
- Baselines written only by host process
- Containers access via IPC only
- No container can modify its own baselines

**Integrity:**
- SHA-256 hashes (industry standard)
- Hash-chained audit log (tamper-evident)
- Atomic file operations (safe restores)

### Audit Log

**Format:** JSONL with hash chaining

**Each entry includes:**
```json
{
  "ts": "2026-02-25T12:00:00Z",
  "event": "drift",
  "actor": "agent",
  "path": "/workspace/group/CLAUDE.md",
  "expected_sha": "abc123...",
  "found_sha": "def456...",
  "chain": {
    "prev": "previous_entry_hash",
    "hash": "this_entry_hash"
  }
}
```

**Chain calculation:**
```
hash = SHA-256(prev_hash + '\n' + canonical_json(entry_without_chain))
```

This makes tampering detectable: changing any entry breaks the chain.

## Troubleshooting

### Integrity Check Fails

**Symptom:** `clawsec_check_integrity` returns `success: false`

**Causes:**
1. IntegrityService not initialized
2. Policy file missing
3. Baselines corrupted

**Solution:**
```bash
# Check service status
ls /workspace/project/data/soul-guardian/

# If missing, reinitialize
rm -rf /workspace/project/data/soul-guardian/
# Next integrity check will recreate baselines
```

### False Positives (Legitimate Changes Flagged)

**Symptom:** File keeps getting restored even though changes are legitimate

**Cause:** Baseline not updated after intentional changes

**Solution:**
```typescript
await tools.clawsec_approve_change({
  path: '/path/to/file',
  note: 'Legitimate change'
});
```

### Audit Chain Broken

**Symptom:** `clawsec_verify_audit` returns `valid: false`

**Causes:**
1. Audit log manually edited
2. Filesystem corruption
3. Security breach

**Solution:**
```typescript
const verification = await tools.clawsec_verify_audit();
console.log('Errors:', verification.errors);

// If corruption, backup and reset
cp /workspace/project/data/soul-guardian/audit.jsonl /tmp/audit-backup.jsonl
rm /workspace/project/data/soul-guardian/audit.jsonl
// Audit log will restart on next operation
```

### High Disk Usage

**Symptom:** `/workspace/project/data/soul-guardian/` grows large

**Causes:**
- Many drift events generate patches
- Quarantine files accumulate

**Solution:**
```bash
# Clean old patches (older than 30 days)
find /workspace/project/data/soul-guardian/patches/ -mtime +30 -delete

# Clean quarantine (after review)
rm /workspace/project/data/soul-guardian/quarantine/*
```

## Performance

**Overhead:**
- Baseline check: ~10ms per file
- SHA-256 computation: ~1ms per KB
- Restore operation: ~20ms per file

**Typical deployment:**
- 3-5 protected files
- 30-minute check interval
- < 0.1% CPU usage
- < 5MB disk usage

## Advanced Topics

### Custom Policy

While the default policy is pinned by the skill, you can fork it:

```bash
cp /workspace/project/skills/clawsec-nanoclaw/guardian/policy.json /workspace/project/data/custom-policy.json
```

Edit and reinitialize:
```typescript
// Update IntegrityMonitor initialization
new IntegrityMonitor({
  policyPath: '/workspace/project/data/custom-policy.json',
  stateDir: '/workspace/project/data/soul-guardian'
});
```

### Manual Baseline Export

```bash
# Export current baselines
cp /workspace/project/data/soul-guardian/baselines.json /tmp/baselines-backup.json

# Export approved snapshots
tar -czf /tmp/approved-snapshots.tar.gz /workspace/project/data/soul-guardian/approved/
```

### Baseline Import (Disaster Recovery)

```bash
# Restore baselines
cp /tmp/baselines-backup.json /workspace/project/data/soul-guardian/baselines.json

# Restore snapshots
tar -xzf /tmp/approved-snapshots.tar.gz -C /workspace/project/data/soul-guardian/
```

## FAQ

**Q: Can I disable auto-restore for testing?**

A: Yes, use `autoRestore: false`:
```typescript
await tools.clawsec_check_integrity({ autoRestore: false });
```

**Q: How do I protect additional files?**

A: Edit `policy.json` and add targets:
```json
{
  "path": "/workspace/group/my-config.json",
  "mode": "restore",
  "priority": "high",
  "description": "My custom config"
}
```

**Q: What happens if both baseline and file are modified?**

A: The most recent baseline wins. Always approve legitimate changes immediately.

**Q: Can I run integrity checks on-demand?**

A: Yes, just call `clawsec_check_integrity` from any agent.

**Q: Is the audit log encrypted?**

A: No, but it's hash-chained for tamper detection. Encryption can be added in Phase 3.

## Support

- **Documentation**: https://clawsec.prompt.security/
- **Issues**: https://github.com/prompt-security/clawsec/issues
- **Security Reports**: [REDACTED_SECRET]

---

**Ready to protect your NanoClaw deployment? Start with the [Quick Start](#quick-start) guide above.**


path: docs/SKILL_SIGNING.md
content: # Skill Package Signing and Verification

This document explains how ClawSec signs skill packages and how NanoClaw agents verify signatures before installation.

---

## Table of Contents

1. [Overview](#overview)
2. [For Skill Publishers: How to Sign Packages](#for-skill-publishers-how-to-sign-packages)
3. [For NanoClaw Agents: How to Verify Signatures](#for-nanoclaw-agents-how-to-verify-signatures)
4. [Security Properties](#security-properties)
5. [Key Management](#key-management)
6. [Troubleshooting](#troubleshooting)

---

## Overview

Skill signature verification prevents **supply chain attacks** by ensuring skill packages haven't been tampered with during distribution. ClawSec uses **Ed25519 digital signatures** to sign skill packages, and NanoClaw agents verify these signatures before installation.

### Why Signature Verification?

Without signature verification, an attacker could:
- **Replace** a legitimate skill package with a malicious one during download
- **Modify** package contents to inject backdoors or steal data
- **Distribute** trojan skills that appear legitimate but contain malware

Signature verification ensures:
- ✅ **Authenticity**: Package comes from ClawSec (or trusted publisher)
- ✅ **Integrity**: Package hasn't been modified since signing
- ✅ **Non-repudiation**: Signer can't deny signing the package

---

## For Skill Publishers: How to Sign Packages

### Prerequisites

- OpenSSL 1.1.1+ (for Ed25519 support)
- Private Ed25519 signing key (generate once, keep secure)
- Skill package ready for distribution

### Step 1: Generate Ed25519 Keypair (One-Time Setup)

```bash
# Generate private key (KEEP THIS SECRET!)
openssl genpkey -algorithm ED25519 -out clawsec-signing-private.pem

# Extract public key (share this with users)
openssl pkey -in clawsec-signing-private.pem -pubout -out clawsec-signing-public.pem

# Secure the private key
chmod 600 clawsec-signing-private.pem
```

**⚠️ CRITICAL**: Never commit the private key to version control! Store it securely:
- Local machine: `~/.ssh/clawsec-signing-private.pem` with `chmod 600`
- CI/CD: GitHub Secrets, AWS Secrets Manager, or similar
- Team: 1Password, Vault, or hardware security module (HSM)

### Step 2: Package Your Skill

```bash
# Create skill package (tarball or zip)
tar -czf my-skill-1.0.0.tar.gz -C skills/my-skill .

# Or as a zip file
zip -r my-skill-1.0.0.zip skills/my-skill/
```

### Step 3: Sign the Package

```bash
# Create detached Ed25519 signature
openssl dgst -sha512 -sign clawsec-signing-private.pem \
  -out my-skill-1.0.0.tar.gz.sig \
  my-skill-1.0.0.tar.gz

# Verify the signature was created
ls -lh my-skill-1.0.0.tar.gz.sig
# Should show a ~64-byte file
```

**Signature Format**: Detached Ed25519 signature, base64-encoded, stored in `.sig` file.

### Step 4: Distribute Package + Signature

Distribute **both** files together:
- `my-skill-1.0.0.tar.gz` (the skill package)
- `my-skill-1.0.0.tar.gz.sig` (the signature)

Users will verify the signature against your public key before installation.

### Step 5: Publish Public Key

Share your public key with users via:
- **Pinned in repository**: Commit `clawsec-signing-public.pem` to your repo
- **Website**: Host at `https://yoursite.com/clawsec-signing-public.pem`
- **DNS TXT record**: Publish as base64-encoded TXT record
- **Skill metadata**: Embed in `skill.json`

---

## For NanoClaw Agents: How to Verify Signatures

### Quick Start

```typescript
// Verify a downloaded skill package before installation
const verification = await tools.clawsec_verify_skill_package({
  packagePath: '/tmp/my-skill-1.0.0.tar.gz'
  // signaturePath auto-detected as /tmp/my-skill-1.0.0.tar.gz.sig
});

const result = JSON.parse(verification.content[0].text);

if (!result.valid) {
  console.log('⚠️ SIGNATURE VERIFICATION FAILED!');
  console.log(`Reason: ${result.reason || result.error}`);
  console.log('DO NOT install this package.');
  return;
}

console.log(`✓ Signature valid (signer: ${result.signer})`);
console.log(`Package hash: ${result.packageInfo.sha256}`);
console.log('Safe to proceed with installation.');
```

### MCP Tool: `clawsec_verify_skill_package`

**Parameters:**
- `packagePath` (required): Absolute path to skill package (`.tar.gz`, `.tar`, `.tgz`, or `.zip`)
- `signaturePath` (optional): Path to signature file (auto-detects `.sig` if omitted)

Path policy:
- Files must be under one of: `/tmp`, `/var/tmp`, `/workspace/ipc`, `/workspace/project/data`, `/workspace/project/tmp`, `/workspace/project/downloads`
- Symlinks are rejected
- Signatures must use `.sig`

**Returns:**
```typescript
{
  success: boolean,           // Operation completed without errors
  valid: boolean,             // Signature is cryptographically valid
  recommendation: string,     // "install" | "block" | "review"
  signer: string,             // "clawsec"
  algorithm: "Ed25519",       // Signature algorithm
  verifiedAt: string,         // ISO timestamp
  packageInfo: {
    size: number,             // Package file size in bytes
    sha256: string            // SHA-256 hash of package
  },
  error?: string              // Error message if failed
}
```

### Usage Patterns

#### Pattern 1: Basic Pre-Installation Check

```typescript
async function installSkill(packagePath: string) {
  // Verify signature first
  const verification = await tools.clawsec_verify_skill_package({ packagePath });
  const result = JSON.parse(verification.content[0].text);

  if (result.recommendation === 'block') {
    throw new Error(`Cannot install: ${result.reason || result.error}`);
  }

  // Signature valid - proceed with extraction
  extractPackage(packagePath, '/workspace/project/skills/');
}
```

#### Pattern 2: Combined Security Checks

```typescript
async function installSkillSafely(packagePath: string, skillName: string) {
  // Step 1: Verify signature
  const sigVerify = await tools.clawsec_verify_skill_package({ packagePath });
  const sigResult = JSON.parse(sigVerify.content[0].text);

  if (!sigResult.valid) {
    throw new Error(`Signature invalid: ${sigResult.reason}`);
  }

  // Step 2: Check advisories
  const advisory = await tools.clawsec_check_skill_safety({ skillName });
  const advResult = JSON.parse(advisory.content[0].text);

  if (!advResult.safe) {
    throw new Error(`Known vulnerabilities: ${advResult.advisories.map(a => a.id).join(', ')}`);
  }

  // Both checks passed - safe to install
  extractPackage(packagePath, '/workspace/project/skills/');
  console.log(`✓ Installed ${skillName} (verified + no advisories)`);
}
```

#### Pattern 3: Download and Verify Workflow

```typescript
async function downloadAndInstallSkill(url: string) {
  const packagePath = `/tmp/${Date.now()}-skill.tar.gz`;
  const signaturePath = `${packagePath}.sig`;

  // Download package
  await fetch(url).then(r => r.arrayBuffer()).then(buf => {
    fs.writeFileSync(packagePath, Buffer.from(buf));
  });

  // Download signature
  await fetch(`${url}.sig`).then(r => r.text()).then(sig => {
    fs.writeFileSync(signaturePath, sig);
  });

  // Verify before installation
  const verification = await tools.clawsec_verify_skill_package({
    packagePath,
    signaturePath
  });

  const result = JSON.parse(verification.content[0].text);

  if (!result.valid) {
    fs.unlinkSync(packagePath);     // Delete tampered file
    fs.unlinkSync(signaturePath);
    throw new Error('Signature verification failed');
  }

  // Install verified package
  extractPackage(packagePath, '/workspace/project/skills/');

  // Cleanup
  fs.unlinkSync(packagePath);
  fs.unlinkSync(signaturePath);
}
```

### Error Handling

```typescript
const verification = await tools.clawsec_verify_skill_package({ packagePath });
const result = JSON.parse(verification.content[0].text);

// Check result.success first (operation completed)
if (!result.success) {
  console.error('Verification operation failed:', result.error);
  // Reasons: file not found, service unavailable, timeout
  return;
}

// Then check result.valid (signature cryptographically valid)
if (!result.valid) {
  console.error('Invalid signature:', result.reason);
  // Reasons: signature mismatch, tampered package, invalid format
  return;
}

// Finally check recommendation
switch (result.recommendation) {
  case 'install':
    console.log('✓ Safe to install');
    break;
  case 'block':
    console.error('⛔ Installation blocked');
    break;
  case 'review':
    console.warn('⚠️ Manual review recommended');
    break;
}
```

---

## Security Properties

### What Signature Verification Prevents

✅ **Prevents:**
- **Tampering**: Detecting if package contents were modified after signing
- **MITM attacks**: Detecting if package was swapped during download
- **Malicious mirrors**: Ensuring package comes from trusted source
- **Accidental corruption**: Detecting file corruption during transfer

### What Signature Verification Does NOT Prevent

❌ **Does Not Prevent:**
- **Malicious signed packages**: If the publisher's key is compromised
- **Zero-day vulnerabilities**: Bugs unknown to the publisher
- **Social engineering**: Convincing users to trust malicious publishers
- **Time-of-check-to-time-of-use**: Package modified after verification

**Defense in Depth**: Combine signature verification with:
1. **Advisory checking** (`clawsec_check_skill_safety`)
2. **Code review** (manual inspection of skill code)
3. **Sandboxing** (run skills in isolated containers)
4. **Monitoring** (detect suspicious behavior at runtime)

### Trust Model

Signature verification relies on **trust in the public key**:

```
┌─────────────────────────────────────────────────┐
│ You trust ClawSec's public key                  │
│          ↓                                      │
│ ClawSec signs package with private key          │
│          ↓                                      │
│ You verify signature with ClawSec's public key  │
│          ↓                                      │
│ Signature valid → Package is authentic         │
└─────────────────────────────────────────────────┘
```

**Key Question**: How do you establish trust in the public key?
- **Pinned in repository**: Public key committed to ClawSec repo (trust GitHub)
- **HTTPS website**: Download from `https://clawsec.prompt.security/` (trust TLS/CA)
- **Out-of-band verification**: Compare key fingerprint via phone, Signal, etc.
- **Web of Trust**: Multiple trusted sources publish the same key

---

## Key Management

### ClawSec's Pinned Public Key

**Location**: `/workspace/project/skills/clawsec-nanoclaw/advisories/feed-signing-public.pem`

This is the **same key** used for advisory feed verification, providing a single trust anchor for all ClawSec security operations.

**Key Fingerprint** (for manual verification):
```bash
# Compute fingerprint of pinned key
openssl pkey -pubin -in feed-signing-public.pem -outform DER | \
  openssl dgst -sha256 -binary | base64
# Expected: <will be filled in after key generation>
```

### Public Key Policy

The verifier always uses the pinned ClawSec public key from this skill package.
Runtime public-key overrides are intentionally not supported.

### Key Rotation

If ClawSec's signing key is compromised or needs rotation:

1. **Generate new keypair** (keep private key secure)
2. **Sign all packages** with new key
3. **Publish new public key** to all distribution channels
4. **Update pinned key** in `/workspace/project/skills/clawsec-nanoclaw/advisories/`
5. **Deprecate old key** after transition period (e.g., 90 days)

During transition, support **dual signatures**:
- `package.tar.gz.sig` (old key)
- `package.tar.gz.sig2` (new key)

Agents can verify with either key during the overlap period.

---

## Troubleshooting

### Error: "Signature file not found"

**Cause**: Missing `.sig` file or incorrect path.

**Solution**:
```bash
# Check if signature exists
ls -l /tmp/skill.tar.gz.sig

# If missing, download signature
curl -o /tmp/skill.tar.gz.sig https://example.com/skill.tar.gz.sig

# Or specify explicit path
clawsec_verify_skill_package({
  packagePath: '/tmp/skill.tar.gz',
  signaturePath: '/tmp/custom-signature.sig'
})
```

### Error: "Signature verification failed"

**Cause**: Package was tampered with, or signature doesn't match package.

**Solution**:
```bash
# Re-download package and signature
curl -o /tmp/skill.tar.gz https://example.com/skill.tar.gz
curl -o /tmp/skill.tar.gz.sig https://example.com/skill.tar.gz.sig

# Verify manually with OpenSSL
openssl dgst -sha512 -verify clawsec-signing-public.pem \
  -signature /tmp/skill.tar.gz.sig /tmp/skill.tar.gz
# Should output: "Verified OK"
```

### Error: "Invalid PEM format"

**Cause**: Public key file is corrupted or not in PEM format.

**Solution**:
```bash
# Check public key format
head -1 /path/to/public-key.pem
# Should output: "-----BEGIN PUBLIC KEY-----"

# Re-download public key
curl -o clawsec-signing-public.pem \
  https://clawsec.prompt.security/clawsec-signing-public.pem
```

### Error: "Package file not found"

**Cause**: Incorrect path or file doesn't exist.

**Solution**:
```bash
# Use absolute paths (required)
clawsec_verify_skill_package({
  packagePath: '/tmp/skill.tar.gz'  // ✓ Absolute
  // packagePath: './skill.tar.gz' // ✗ Relative (won't work)
})

# Verify file exists
stat /tmp/skill.tar.gz
```

### Verification Times Out (>5s)

**Cause**: Large package (>50MB) or slow disk I/O.

**Solution**:
```bash
# Check package size
ls -lh /tmp/skill.tar.gz

# For very large packages, verification can take time
# Consider splitting into smaller skill modules
```

---

## Appendix: Signature File Format

ClawSec uses **Ed25519 detached signatures** in raw binary format, base64-encoded.

**File Structure**:
```
my-skill-1.0.0.tar.gz.sig:
  Line 1: base64-encoded signature (88 characters)
```

**Example**:
```
MEQCIDxyz...ABC123==
```

**Properties**:
- Algorithm: Ed25519 (EdDSA with Curve25519)
- Signature size: 64 bytes (88 characters base64)
- Hash function: SHA-512 (internal to Ed25519)
- Format: Raw binary, base64-encoded

**Verification Algorithm**:
1. Decode base64 signature → 64-byte binary
2. Hash package with SHA-512
3. Verify Ed25519 signature(hash, publicKey) → boolean

---

## References

- [Ed25519 Specification (RFC 8032)](https://tools.ietf.org/html/rfc8032)
- [OpenSSL Ed25519 Documentation](https://www.openssl.org/docs/man3.0/man7/Ed25519.html)
- [ClawSec Security Architecture](https://clawsec.prompt.security/docs/architecture)
- [Supply Chain Attack Prevention](https://owasp.org/www-community/attacks/Supply_Chain_Attack)

---

**Document Version**: 1.0.0
**Last Updated**: 2026-02-25
**Maintainer**: ClawSec Security Team


path: guardian/integrity-monitor.ts
content: /**
 * File Integrity Monitor for NanoClaw
 *
 * TypeScript port of ClawSec's soul-guardian with NanoClaw-specific adaptations.
 *
 * Key Features:
 * - SHA-256 baseline tracking for protected files
 * - Drift detection with unified diff generation
 * - Auto-restore for critical files (with quarantine)
 * - Hash-chained tamper-evident audit log
 * - Per-file policy (restore/alert/ignore modes)
 *
 * Security Model:
 * - Baselines stored on host only (containers access via IPC)
 * - Atomic file operations for restores
 * - Refuses to operate on symlinks
 * - Hash-chained audit log prevents tampering
 */

import fs from 'fs';
import path from 'path';
import crypto from 'crypto';
// glob is available when running in the NanoClaw host environment.
// For type checking in the clawsec repo, we declare a minimal interface.
// eslint-disable-next-line @typescript-eslint/no-namespace
declare namespace glob {
  function sync(pattern: string, options?: { nodir?: boolean }): string[];
}

// ============================================================================
// Types
// ============================================================================

export interface PolicyTarget {
  path?: string;
  pattern?: string;
  mode: 'restore' | 'alert' | 'ignore';
  priority: 'critical' | 'high' | 'medium' | 'low';
  description: string;
}

export interface Policy {
  version: number;
  description: string;
  nanoclaw_version: string;
  targets: PolicyTarget[];
  notes?: string[];
}

export interface FileBaseline {
  sha256: string;
  approved_at: string;
  approved_by: string;
  mode: 'restore' | 'alert' | 'ignore';
  priority: string;
}

export interface BaselinesManifest {
  schema_version: string;
  algorithm: 'sha256';
  created_at: string;
  files: Record<string, FileBaseline>;
}

export interface AuditEntry {
  ts: string;
  event: 'init' | 'drift' | 'restore' | 'approve' | 'error';
  actor: string;
  note?: string;
  path: string;
  mode?: string;
  expected_sha?: string;
  found_sha?: string;
  patch_path?: string;
  quarantine_path?: string;
  error?: string;
  chain?: {
    prev: string;
    hash: string;
  };
}

export interface DriftedFile {
  path: string;
  mode: 'restore' | 'alert';
  expected_sha: string;
  found_sha: string;
  patch_path: string;
  restored: boolean;
  quarantine_path?: string;
  error?: string;
}

export interface CheckResult {
  success: boolean;
  timestamp: string;
  drift_detected: boolean;
  files: Array<{
    path: string;
    status: 'ok' | 'drifted' | 'restored' | 'error';
    mode: string;
    expected_sha?: string;
    found_sha?: string;
    patch_path?: string;
    quarantine_path?: string;
    error?: string;
  }>;
  summary: {
    total: number;
    ok: number;
    drifted: number;
    restored: number;
    alerted: number;
    errors: number;
  };
}

export interface IntegrityMonitorOptions {
  policyPath: string;
  stateDir: string;
}

// ============================================================================
// Constants
// ============================================================================

const CHAIN_GENESIS = '0'.repeat(64);

// ============================================================================
// Utility Functions
// ============================================================================

function utcNowIso(): string {
  return new Date().toISOString();
}

function sha256Hex(data: Buffer | string): string {
  const hash = crypto.createHash('sha256');
  hash.update(data);
  return hash.digest('hex');
}

function sha256File(filePath: string): string {
  const data = fs.readFileSync(filePath);
  return sha256Hex(data);
}

function isSymlink(filePath: string): boolean {
  try {
    const stats = fs.lstatSync(filePath);
    return stats.isSymbolicLink();
  } catch {
    return false;
  }
}

function refuseSymlink(filePath: string): void {
  if (isSymlink(filePath)) {
    throw new Error(`Refusing to operate on symlink: ${filePath}`);
  }
}

function ensureDir(dirPath: string): void {
  fs.mkdirSync(dirPath, { recursive: true });
}

function atomicWrite(filePath: string, data: string | Buffer): void {
  ensureDir(path.dirname(filePath));
  const tmpPath = `${filePath}.tmp.${Date.now()}`;
  fs.writeFileSync(tmpPath, data);
  fs.renameSync(tmpPath, filePath);
}

function unifiedDiff(oldText: string, newText: string, oldLabel: string, newLabel: string): string {
  // Simple unified diff implementation
  const oldLines = oldText.split('\n');
  const newLines = newText.split('\n');

  const lines: string[] = [];
  lines.push(`--- ${oldLabel}`);
  lines.push(`+++ ${newLabel}`);
  lines.push(`@@ -1,${oldLines.length} +1,${newLines.length} @@`);

  for (let i = 0; i < Math.max(oldLines.length, newLines.length); i++) {
    if (i < oldLines.length && i < newLines.length) {
      if (oldLines[i] !== newLines[i]) {
        lines.push(`-${oldLines[i]}`);
        lines.push(`+${newLines[i]}`);
      } else {
        lines.push(` ${oldLines[i]}`);
      }
    } else if (i < oldLines.length) {
      lines.push(`-${oldLines[i]}`);
    } else {
      lines.push(`+${newLines[i]}`);
    }
  }

  return lines.join('\n');
}

function safePatchTag(tag: string): string {
  return tag.replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 40) || 'patch';
}

// ============================================================================
// Integrity Monitor Class
// ============================================================================

export class IntegrityMonitor {
  private policyPath: string;
  private stateDir: string;
  private baselinesPath: string;
  private auditPath: string;
  private approvedDir: string;
  private patchesDir: string;
  private quarantineDir: string;

  private policy: Policy | null = null;
  private baselines: BaselinesManifest | null = null;

  constructor(options: IntegrityMonitorOptions) {
    this.policyPath = options.policyPath;
    this.stateDir = options.stateDir;
    this.baselinesPath = path.join(this.stateDir, 'baselines.json');
    this.auditPath = path.join(this.stateDir, 'audit.jsonl');
    this.approvedDir = path.join(this.stateDir, 'approved');
    this.patchesDir = path.join(this.stateDir, 'patches');
    this.quarantineDir = path.join(this.stateDir, 'quarantine');
  }

  // --------------------------------------------------------------------------
  // Initialization
  // --------------------------------------------------------------------------

  async init(actor: string = 'system', note: string = 'initial baseline'): Promise<void> {
    ensureDir(this.stateDir);
    ensureDir(this.approvedDir);
    ensureDir(this.patchesDir);
    ensureDir(this.quarantineDir);

    // Load policy
    this.policy = this.loadPolicy();

    // Load or create baselines
    this.baselines = this.loadBaselines();

    // Resolve targets and initialize missing baselines
    const targets = this.resolveTargets();
    let initialized = false;

    for (const target of targets) {
      if (target.mode === 'ignore') continue;

      try {
        if (!fs.existsSync(target.path)) continue;

        refuseSymlink(target.path);

        // Check if already has baseline
        if (this.baselines.files[target.path]) continue;

        // Create baseline
        const sha = sha256File(target.path);
        const snapshot = path.join(this.approvedDir, path.basename(target.path));
        fs.copyFileSync(target.path, snapshot);

        this.baselines.files[target.path] = {
          sha256: sha,
          approved_at: utcNowIso(),
          approved_by: actor,
          mode: target.mode,
          priority: target.priority
        };

        this.appendAudit({
          ts: utcNowIso(),
          event: 'init',
          actor,
          note,
          path: target.path,
          mode: target.mode,
          expected_sha: sha
        });

        initialized = true;
      } catch (error) {
        console.error(`Failed to initialize baseline for ${target.path}:`, error);
      }
    }

    if (initialized) {
      this.saveBaselines();
    }
  }

  // --------------------------------------------------------------------------
  // Policy Management
  // --------------------------------------------------------------------------

  private loadPolicy(): Policy {
    const raw = fs.readFileSync(this.policyPath, 'utf-8');
    return JSON.parse(raw);
  }

  private resolveTargets(): Array<{ path: string; mode: 'restore' | 'alert' | 'ignore'; priority: string }> {
    if (!this.policy) throw new Error('Policy not loaded');

    const targets: Array<{ path: string; mode: 'restore' | 'alert' | 'ignore'; priority: string }> = [];

    for (const target of this.policy.targets) {
      if (target.path) {
        // Direct path
        targets.push({
          path: path.resolve(target.path),
          mode: target.mode,
          priority: target.priority
        });
      } else if (target.pattern) {
        // Glob pattern
        try {
          const matches = glob.sync(target.pattern, { nodir: true });
          for (const match of matches) {
            targets.push({
              path: path.resolve(match),
              mode: target.mode,
              priority: target.priority
            });
          }
        } catch (error) {
          console.error(`Failed to expand pattern ${target.pattern}:`, error);
        }
      }
    }

    return targets;
  }

  private normalizeBaselines(manifest: BaselinesManifest): BaselinesManifest {
    const normalizedFiles: Record<string, FileBaseline> = {};
    for (const [filePath, baseline] of Object.entries(manifest.files || {})) {
      normalizedFiles[path.resolve(filePath)] = baseline;
    }

    return {
      ...manifest,
      files: normalizedFiles,
    };
  }

  // --------------------------------------------------------------------------
  // Baseline Management
  // --------------------------------------------------------------------------

  private loadBaselines(): BaselinesManifest {
    if (fs.existsSync(this.baselinesPath)) {
      const raw = fs.readFileSync(this.baselinesPath, 'utf-8');
      return this.normalizeBaselines(JSON.parse(raw));
    }

    return {
      schema_version: '1',
      algorithm: 'sha256',
      created_at: utcNowIso(),
      files: {}
    };
  }

  private saveBaselines(): void {
    const data = JSON.stringify(this.baselines, null, 2);
    atomicWrite(this.baselinesPath, data);
  }

  // --------------------------------------------------------------------------
  // Audit Log with Hash Chaining
  // --------------------------------------------------------------------------

  private getLastAuditHash(): string {
    if (!fs.existsSync(this.auditPath)) {
      return CHAIN_GENESIS;
    }

    const content = fs.readFileSync(this.auditPath, 'utf-8');
    const lines = content.trim().split('\n').filter(l => l.trim());

    if (lines.length === 0) {
      return CHAIN_GENESIS;
    }

    try {
      const lastEntry = JSON.parse(lines[lines.length - 1]);
      return lastEntry.chain?.hash || CHAIN_GENESIS;
    } catch {
      return CHAIN_GENESIS;
    }
  }

  private appendAudit(entry: Omit<AuditEntry, 'chain'>): void {
    ensureDir(path.dirname(this.auditPath));

    const prevHash = this.getLastAuditHash();

    // Compute current hash
    const entryWithoutChain = { ...entry };
    const payload = prevHash + '\n' + JSON.stringify(entryWithoutChain, Object.keys(entryWithoutChain).sort());
    const currentHash = sha256Hex(payload);

    const record: AuditEntry = {
      ...entry,
      chain: {
        prev: prevHash,
        hash: currentHash
      }
    };

    fs.appendFileSync(this.auditPath, JSON.stringify(record) + '\n');
  }

  // --------------------------------------------------------------------------
  // Drift Detection
  // --------------------------------------------------------------------------

  async checkIntegrity(autoRestore: boolean = true, actor: string = 'agent'): Promise<CheckResult> {
    if (!this.baselines) {
      throw new Error('Baselines not loaded. Call init() first.');
    }

    const result: CheckResult = {
      success: true,
      timestamp: utcNowIso(),
      drift_detected: false,
      files: [],
      summary: {
        total: 0,
        ok: 0,
        drifted: 0,
        restored: 0,
        alerted: 0,
        errors: 0
      }
    };

    for (const [filePath, baseline] of Object.entries(this.baselines.files)) {
      result.summary.total++;

      try {
        if (!fs.existsSync(filePath)) {
          result.files.push({
            path: filePath,
            status: 'error',
            mode: baseline.mode,
            error: 'File not found'
          });
          result.summary.errors++;

          this.appendAudit({
            ts: utcNowIso(),
            event: 'error',
            actor,
            path: filePath,
            error: 'File not found'
          });

          continue;
        }

        refuseSymlink(filePath);

        const currentSha = sha256File(filePath);

        if (currentSha === baseline.sha256) {
          // No drift
          result.files.push({
            path: filePath,
            status: 'ok',
            mode: baseline.mode,
            expected_sha: baseline.sha256,
            found_sha: currentSha
          });
          result.summary.ok++;
          continue;
        }

        // Drift detected
        result.drift_detected = true;
        result.summary.drifted++;

        // Generate diff
        const snapshot = path.join(this.approvedDir, path.basename(filePath));
        const oldText = fs.existsSync(snapshot) ? fs.readFileSync(snapshot, 'utf-8') : '';
        const newText = fs.readFileSync(filePath, 'utf-8');
        const diff = unifiedDiff(oldText, newText, `approved/${path.basename(filePath)}`, path.basename(filePath));

        const patchPath = path.join(
          this.patchesDir,
          `${new Date().toISOString().replace(/[:.]/g, '-')}-drift-${safePatchTag(path.basename(filePath))}.patch`
        );
        fs.writeFileSync(patchPath, diff);

        this.appendAudit({
          ts: utcNowIso(),
          event: 'drift',
          actor,
          path: filePath,
          mode: baseline.mode,
          expected_sha: baseline.sha256,
          found_sha: currentSha,
          patch_path: patchPath
        });

        // Handle based on mode
        if (baseline.mode === 'restore' && autoRestore) {
          // Auto-restore
          try {
            const quarantinePath = path.join(
              this.quarantineDir,
              `${safePatchTag(path.basename(filePath))}.${Date.now()}.quarantine`
            );
            fs.copyFileSync(filePath, quarantinePath);

            if (fs.existsSync(snapshot)) {
              atomicWrite(filePath, fs.readFileSync(snapshot));
            }

            this.appendAudit({
              ts: utcNowIso(),
              event: 'restore',
              actor,
              path: filePath,
              mode: baseline.mode,
              quarantine_path: quarantinePath
            });

            result.files.push({
              path: filePath,
              status: 'restored',
              mode: baseline.mode,
              expected_sha: baseline.sha256,
              found_sha: currentSha,
              patch_path: patchPath,
              quarantine_path: quarantinePath
            });
            result.summary.restored++;
          } catch (error) {
            result.files.push({
              path: filePath,
              status: 'error',
              mode: baseline.mode,
              expected_sha: baseline.sha256,
              found_sha: currentSha,
              patch_path: patchPath,
              error: `Restore failed: ${error instanceof Error ? error.message : String(error)}`
            });
            result.summary.errors++;
          }
        } else {
          // Alert only
          result.files.push({
            path: filePath,
            status: 'drifted',
            mode: baseline.mode,
            expected_sha: baseline.sha256,
            found_sha: currentSha,
            patch_path: patchPath
          });
          result.summary.alerted++;
        }

      } catch (error) {
        result.files.push({
          path: filePath,
          status: 'error',
          mode: baseline.mode,
          error: error instanceof Error ? error.message : String(error)
        });
        result.summary.errors++;

        this.appendAudit({
          ts: utcNowIso(),
          event: 'error',
          actor,
          path: filePath,
          error: error instanceof Error ? error.message : String(error)
        });
      }
    }

    return result;
  }

  // --------------------------------------------------------------------------
  // Approve Changes
  // --------------------------------------------------------------------------

  async approveChange(filePath: string, actor: string, note: string = ''): Promise<void> {
    if (!this.baselines) {
      throw new Error('Baselines not loaded');
    }

    const normalizedFilePath = path.resolve(filePath);

    if (!fs.existsSync(normalizedFilePath)) {
      throw new Error(`File not found: ${normalizedFilePath}`);
    }

    refuseSymlink(normalizedFilePath);

    const targets = this.resolveTargets();
    const target = targets.find(t => t.path === normalizedFilePath);
    if (!target || target.mode === 'ignore') {
      throw new Error(`File ${normalizedFilePath} not in policy`);
    }

    const previousSha = this.baselines.files[normalizedFilePath]?.sha256;
    const currentSha = sha256File(normalizedFilePath);

    // Generate diff
    const snapshot = path.join(this.approvedDir, path.basename(normalizedFilePath));
    const oldText = fs.existsSync(snapshot) ? fs.readFileSync(snapshot, 'utf-8') : '';
    const newText = fs.readFileSync(normalizedFilePath, 'utf-8');
    const diff = unifiedDiff(
      oldText,
      newText,
      `approved/${path.basename(normalizedFilePath)}`,
      path.basename(normalizedFilePath)
    );

    const patchPath = path.join(
      this.patchesDir,
      `${new Date().toISOString().replace(/[:.]/g, '-')}-approve-${safePatchTag(path.basename(normalizedFilePath))}.patch`
    );
    fs.writeFileSync(patchPath, diff);

    // Update baseline
    if (!this.baselines.files[normalizedFilePath]) {
      this.baselines.files[normalizedFilePath] = {
        sha256: currentSha,
        approved_at: utcNowIso(),
        approved_by: actor,
        mode: target.mode,
        priority: target.priority
      };
    } else {
      this.baselines.files[normalizedFilePath].sha256 = currentSha;
      this.baselines.files[normalizedFilePath].approved_at = utcNowIso();
      this.baselines.files[normalizedFilePath].approved_by = actor;
    }

    // Update snapshot
    fs.copyFileSync(normalizedFilePath, snapshot);

    // Save and audit
    this.saveBaselines();

    this.appendAudit({
      ts: utcNowIso(),
      event: 'approve',
      actor,
      note,
      path: normalizedFilePath,
      expected_sha: previousSha,
      found_sha: currentSha,
      patch_path: patchPath
    });
  }

  // --------------------------------------------------------------------------
  // Status and Verification
  // --------------------------------------------------------------------------

  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  getStatus(filePath?: string): any {
    if (!this.baselines) {
      throw new Error('Baselines not loaded');
    }

    const normalizedFilePath = filePath ? path.resolve(filePath) : null;
    const files = normalizedFilePath
      ? { [normalizedFilePath]: this.baselines.files[normalizedFilePath] }
      : this.baselines.files;

    return {
      baseline_age: this.baselines.created_at,
      files: Object.entries(files).map(([path, baseline]) => ({
        path,
        mode: baseline?.mode,
        priority: baseline?.priority,
        has_baseline: !!baseline,
        baseline_sha: baseline?.sha256,
        approved_at: baseline?.approved_at,
        snapshot_exists: fs.existsSync(this.approvedDir + '/' + path.split('/').pop())
      }))
    };
  }

  verifyAuditChain(): { valid: boolean; entries: number; errors: string[] } {
    if (!fs.existsSync(this.auditPath)) {
      return { valid: true, entries: 0, errors: [] };
    }

    const content = fs.readFileSync(this.auditPath, 'utf-8');
    const lines = content.trim().split('\n').filter(l => l.trim());

    const errors: string[] = [];
    let prevHash = CHAIN_GENESIS;

    for (let i = 0; i < lines.length; i++) {
      try {
        const entry: AuditEntry = JSON.parse(lines[i]);

        if (entry.chain?.prev !== prevHash) {
          errors.push(`Line ${i + 1}: Chain break (expected prev=${prevHash}, got=${entry.chain?.prev})`);
        }

        const entryWithoutChain = { ...entry };
        delete entryWithoutChain.chain;
        const payload = prevHash + '\n' + JSON.stringify(entryWithoutChain, Object.keys(entryWithoutChain).sort());
        const expectedHash = sha256Hex(payload);

        if (entry.chain?.hash !== expectedHash) {
          errors.push(`Line ${i + 1}: Hash mismatch`);
        }

        prevHash = entry.chain?.hash || CHAIN_GENESIS;
      } catch (error) {
        errors.push(`Line ${i + 1}: Parse error - ${error}`);
      }
    }

    return {
      valid: errors.length === 0,
      entries: lines.length,
      errors
    };
  }
}


path: guardian/policy.json
content: {
  "version": 1,
  "description": "NanoClaw file integrity monitoring policy",
  "nanoclaw_version": "0.1.0",
  "targets": [
    {
      "path": "/workspace/project/data/registered_groups.json",
      "mode": "restore",
      "priority": "critical",
      "description": "Group registration config - prevents unauthorized group access"
    },
    {
      "path": "/workspace/group/CLAUDE.md",
      "mode": "restore",
      "priority": "high",
      "description": "Group-specific agent instructions"
    },
    {
      "path": "/workspace/project/groups/global/CLAUDE.md",
      "mode": "restore",
      "priority": "high",
      "description": "Global agent instructions shared across all groups"
    },
    {
      "pattern": "/workspace/project/container/**/*.ts",
      "mode": "alert",
      "priority": "medium",
      "description": "Container runtime code - alert on changes for awareness"
    },
    {
      "pattern": "/workspace/project/host/**/*.ts",
      "mode": "alert",
      "priority": "medium",
      "description": "Host process code - alert on changes for awareness"
    },
    {
      "pattern": "/workspace/ipc/**/*",
      "mode": "ignore",
      "priority": "low",
      "description": "IPC files change constantly - ignore"
    },
    {
      "pattern": "/workspace/group/conversations/**/*",
      "mode": "ignore",
      "priority": "low",
      "description": "Chat history - expected to change frequently"
    }
  ],
  "notes": [
    "Mode 'restore': Auto-restore file to approved baseline on drift + alert user",
    "Mode 'alert': Alert user about drift but do not auto-restore",
    "Mode 'ignore': No monitoring, file changes are expected",
    "Patterns use glob syntax with ** for recursive matching"
  ]
}


path: host-services/advisory-cache.ts
content: /**
 * ClawSec Advisory Cache Manager for NanoClaw
 *
 * Manages fetching, verifying, and caching the ClawSec advisory feed.
 * Runs on the host side (not in container).
 *
 * Security:
 * - Ed25519 signature verification using Node.js crypto
 * - Fail-closed policy: invalid signature = reject feed
 * - TLS 1.2+ enforcement with certificate validation
 * - Public key embedded (not user-modifiable)
 * - Cache stored in host-managed directory
 */

import crypto from 'node:crypto';
import fs from 'node:fs/promises';
import https from 'node:https';
import path from 'node:path';
import { evaluateAdvisoryRisk } from '../lib/risk.js';

// ClawSec public key (from clawsec-signing-public.pem)
const PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
MCowBQYDK2VwAyEAS7nijfMcUoOBCj4yOXJX+GYGv2pFl2Yaha1P4v5Cm6A=
-----END PUBLIC KEY-----`;

const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
const FEED_URL = 'https://clawsec.prompt.security/advisories/feed.json';
const FETCH_TIMEOUT_MS = 10000;

export interface Advisory {
  id: string;
  severity: string;
  type?: string;
  title?: string;
  description?: string;
  action?: string;
  published?: string;
  updated?: string;
  exploitability_score?: 'high' | 'medium' | 'low' | 'unknown' | string;
  exploitability_rationale?: string;
  affected: string[];
}

export interface FeedPayload {
  version: string;
  updated?: string;
  advisories: Advisory[];
}

export interface AdvisoryCache {
  feed: FeedPayload;
  fetchedAt: string;
  verified: boolean;
  publicKeyFingerprint: string;
}

interface Logger {
  info(msg: string | object, ...args: unknown[]): void;
  error(msg: string | object, ...args: unknown[]): void;
  warn(msg: string | object, ...args: unknown[]): void;
}

export class AdvisoryCacheManager {
  private cache: AdvisoryCache | null = null;
  private refreshPromise: Promise<void> | null = null;
  private cacheFile: string;
  private logger: Logger;

  constructor(dataDir: string, logger: Logger) {
    this.cacheFile = path.join(dataDir, 'clawsec-advisory-cache.json');
    this.logger = logger;
  }

  /**
   * Initialize cache manager. Loads cache from disk and refreshes if stale.
   */
  async initialize(): Promise<void> {
    await this.loadCacheFromDisk();

    if (!this.cache || this.isCacheStale()) {
      try {
        await this.refresh();
      } catch (error) {
        this.logger.error({ error }, 'Failed to initialize advisory cache');
        // Continue with stale cache if available
      }
    }
  }

  /**
   * Refresh advisory cache from remote feed.
   * Thread-safe: prevents concurrent refreshes.
   */
  async refresh(): Promise<void> {
    // Prevent concurrent refreshes
    if (this.refreshPromise) {
      return this.refreshPromise;
    }

    this.refreshPromise = this._doRefresh();
    try {
      await this.refreshPromise;
    } finally {
      this.refreshPromise = null;
    }
  }

  /**
   * Get current cache. Returns null if cache is stale or missing.
   */
  getCache(): AdvisoryCache | null {
    if (!this.cache || this.isCacheStale()) {
      return null;
    }
    return this.cache;
  }

  /**
   * Get cache even if stale (for fallback scenarios)
   */
  getCacheAllowStale(): AdvisoryCache | null {
    return this.cache;
  }

  private async _doRefresh(): Promise<void> {
    try {
      this.logger.info('Refreshing advisory cache from ClawSec feed');

      const feed = await this.fetchAndVerifyFeed();
      const fingerprint = this.calculateKeyFingerprint();

      this.cache = {
        feed,
        fetchedAt: new Date().toISOString(),
        verified: true,
        publicKeyFingerprint: fingerprint,
      };

      await this.saveCacheToDisk();
      this.logger.info({
        advisories: feed.advisories.length,
        updated: feed.updated,
      }, 'Advisory cache refreshed successfully');
    } catch (error) {
      this.logger.error({ error }, 'Failed to refresh advisory cache');
      throw error;
    }
  }

  private isCacheStale(): boolean {
    if (!this.cache) return true;
    const age = Date.now() - Date.parse(this.cache.fetchedAt);
    return age > CACHE_TTL_MS;
  }

  private async fetchAndVerifyFeed(): Promise<FeedPayload> {
    // Fetch feed and signature in parallel
    const [payloadRaw, signatureRaw] = await Promise.all([
      this.secureFetch(FEED_URL),
      this.secureFetch(`${FEED_URL}.sig`),
    ]);

    // Verify Ed25519 signature
    if (!this.verifySignature(payloadRaw, signatureRaw)) {
      throw new Error('Feed signature verification failed (Ed25519)');
    }

    // Parse and validate
    const feed = JSON.parse(payloadRaw) as FeedPayload;
    if (!this.isValidFeed(feed)) {
      throw new Error('Invalid feed format');
    }

    return feed;
  }

  private async secureFetch(url: string): Promise<string> {
    return new Promise((resolve, reject) => {
      // Create secure HTTPS agent with TLS 1.2+ enforcement
      const agent = new https.Agent({
        minVersion: 'TLSv1.2',
        rejectUnauthorized: true,
        ciphers: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256',
      });

      const req = https.get(url, {
        agent,
        timeout: FETCH_TIMEOUT_MS,
        headers: {
          'User-Agent': 'NanoClaw/1.0',
          'Accept': 'application/json,text/plain',
        },
      }, (res) => {
        if (res.statusCode !== 200) {
          reject(new Error(`HTTP ${res.statusCode} from ${url}`));
          return;
        }

        let data = '';
        res.on('data', (chunk) => { data += chunk; });
        res.on('end', () => resolve(data));
        res.on('error', reject);
      });

      req.on('error', reject);
      req.on('timeout', () => {
        req.destroy();
        reject(new Error(`Timeout fetching ${url}`));
      });
    });
  }

  private verifySignature(payload: string, signatureBase64: string): boolean {
    try {
      // Decode base64 signature
      const trimmed = signatureBase64.trim();
      let encoded = trimmed;

      // Handle JSON-wrapped signature: {"signature": "base64..."}
      if (trimmed.startsWith('{')) {
        try {
          const parsed = JSON.parse(trimmed);
          if (typeof parsed.signature === 'string') {
            encoded = parsed.signature;
          }
        } catch {
          // Not JSON, use as-is
        }
      }

      const normalized = encoded.replace(/\s+/g, '');
      const sigBuffer = Buffer.from(normalized, 'base64');

      // Verify Ed25519 signature using Node.js crypto
      const publicKey = crypto.createPublicKey(PUBLIC_KEY_PEM);
      return crypto.verify(
        null, // algorithm null = Ed25519 raw mode
        Buffer.from(payload, 'utf8'),
        publicKey,
        sigBuffer
      );
    } catch (error) {
      this.logger.warn({ error }, 'Signature verification failed');
      return false;
    }
  }

  private isValidFeed(feed: unknown): feed is FeedPayload {
    if (typeof feed !== 'object' || !feed) return false;
    const f = feed as FeedPayload;

    if (typeof f.version !== 'string' || !f.version.trim()) return false;
    if (!Array.isArray(f.advisories)) return false;

    // Validate each advisory
    return f.advisories.every((a: unknown) => {
      if (typeof a !== 'object' || !a) return false;
      const advisory = a as Advisory;

      return (
        typeof advisory.id === 'string' &&
        advisory.id.trim() !== '' &&
        typeof advisory.severity === 'string' &&
        advisory.severity.trim() !== '' &&
        Array.isArray(advisory.affected) &&
        advisory.affected.every(
          (affected) => typeof affected === 'string' && affected.trim() !== ''
        )
      );
    });
  }

  private calculateKeyFingerprint(): string {
    const publicKey = crypto.createPublicKey(PUBLIC_KEY_PEM);
    const der = publicKey.export({ type: 'spki', format: 'der' });
    return crypto.createHash('sha256').update(der).digest('hex');
  }

  private async loadCacheFromDisk(): Promise<void> {
    try {
      const data = await fs.readFile(this.cacheFile, 'utf8');
      const parsed = JSON.parse(data) as AdvisoryCache;

      // Validate cache structure
      if (this.isValidCache(parsed)) {
        this.cache = parsed;
        this.logger.info({
          age: Date.now() - Date.parse(parsed.fetchedAt),
          advisories: parsed.feed.advisories.length,
        }, 'Loaded advisory cache from disk');
      } else {
        this.logger.warn('Invalid cache format on disk, discarding');
        this.cache = null;
      }
    } catch {
      this.cache = null;
    }
  }

  private isValidCache(cache: unknown): cache is AdvisoryCache {
    if (typeof cache !== 'object' || !cache) return false;
    const c = cache as AdvisoryCache;

    return (
      this.isValidFeed(c.feed) &&
      typeof c.fetchedAt === 'string' &&
      typeof c.verified === 'boolean' &&
      typeof c.publicKeyFingerprint === 'string'
    );
  }

  private async saveCacheToDisk(): Promise<void> {
    if (!this.cache) return;

    try {
      await fs.mkdir(path.dirname(this.cacheFile), { recursive: true });

      // Atomic write: temp file then rename
      const tempFile = `${this.cacheFile}.tmp`;
      await fs.writeFile(tempFile, JSON.stringify(this.cache, null, 2), 'utf8');
      await fs.rename(tempFile, this.cacheFile);

      this.logger.info({ path: this.cacheFile }, 'Advisory cache saved to disk');
    } catch (error) {
      this.logger.error({ error }, 'Failed to save advisory cache to disk');
      throw error;
    }
  }
}

/**
 * Helper: Match advisories against installed skills
 */
export function findAdvisoryMatches(
  advisories: Advisory[],
  skills: Array<{ name: string; version: string | null; dirName: string }>
): Array<{
  advisory: Advisory;
  skill: { name: string; version: string | null; dirName: string };
  matchedAffected: string[];
}> {
  const matches: Array<{
    advisory: Advisory;
    skill: { name: string; version: string | null; dirName: string };
    matchedAffected: string[];
  }> = [];

  for (const advisory of advisories) {
    for (const skill of skills) {
      const matchedAffected: string[] = [];

      for (const affected of advisory.affected) {
        // Parse affected specifier: skill-name or skill-name@version
        const atIndex = affected.lastIndexOf('@');
        const affectedName = atIndex > 0 ? affected.slice(0, atIndex) : affected;
        const _affectedVersion = atIndex > 0 ? affected.slice(atIndex + 1) : '*';

        // Match by name or directory name
        if (affectedName === skill.name || affectedName === skill.dirName) {
          // TODO: implement version range matching
          matchedAffected.push(affected);
        }
      }

      if (matchedAffected.length > 0) {
        matches.push({ advisory, skill, matchedAffected });
      }
    }
  }

  return matches;
}

/**
 * Helper: Evaluate safety recommendation for a skill
 */
export function evaluateSkillSafety(advisories: Advisory[]): {
  safe: boolean;
  recommendation: 'install' | 'block' | 'review';
  reason: string;
} {
  return evaluateAdvisoryRisk(advisories);
}


path: host-services/integrity-handler.ts
content: /* eslint-disable @typescript-eslint/no-explicit-any */
/**
 * ClawSec File Integrity Monitoring IPC Handler for NanoClaw Host
 *
 * Add these handlers to /workspace/project/src/ipc.ts
 *
 * This processes integrity monitoring requests from agents running in containers.
 */

import fs from 'fs';
import path from 'path';
import { IntegrityMonitor } from '../guardian/integrity-monitor';

// ============================================================================
// Integrity Service (Singleton)
// ============================================================================

export class IntegrityService {
  private monitor: IntegrityMonitor | null = null;
  private initialized = false;

  async initialize(): Promise<void> {
    if (this.initialized) return;

    try {
      this.monitor = new IntegrityMonitor({
        policyPath: '/workspace/project/skills/clawsec-nanoclaw/guardian/policy.json',
        stateDir: '/workspace/project/data/soul-guardian'
      });

      // Initialize baselines on first run
      await this.monitor.init('system', 'initial baseline');

      this.initialized = true;
      console.log('[IntegrityService] Initialized successfully');
    } catch (error) {
      console.error('[IntegrityService] Initialization failed:', error);
      throw error;
    }
  }

  getMonitor(): IntegrityMonitor {
    if (!this.monitor) {
      throw new Error('IntegrityService not initialized');
    }
    return this.monitor;
  }

  isInitialized(): boolean {
    return this.initialized;
  }
}

// Global singleton instance
let integrityServiceInstance: IntegrityService | null = null;

export function getIntegrityService(): IntegrityService {
  if (!integrityServiceInstance) {
    integrityServiceInstance = new IntegrityService();
  }
  return integrityServiceInstance;
}

// ============================================================================
// IPC Handler Integration
// ============================================================================

/**
 * Add this to the IpcDeps interface in /workspace/project/src/ipc.ts:
 *
 * export interface IpcDeps {
 *   // ... existing deps
 *   integrityService?: IntegrityService;
 * }
 */

/**
 * Add these cases to the switch statement in processTaskIpc:
 */

export async function handleIntegrityIpc(
  task: any,
  deps: { integrityService?: IntegrityService },
  logger: any
): Promise<void> {
  const { type, requestId, groupFolder: _groupFolder } = task;

  if (!deps.integrityService) {
    logger.warn({ task }, 'IntegrityService not available');
    if (requestId) {
      writeResult(requestId, {
        success: false,
        error: 'IntegrityService not initialized'
      });
    }
    return;
  }

  const service = deps.integrityService;

  if (!service.isInitialized()) {
    try {
      await service.initialize();
    } catch (error) {
      logger.error({ error }, 'Failed to initialize IntegrityService');
      if (requestId) {
        writeResult(requestId, {
          success: false,
          error: `Initialization failed: ${error instanceof Error ? error.message : String(error)}`
        });
      }
      return;
    }
  }

  switch (type) {
    case 'integrity_check':
      await handleIntegrityCheck(task, service, logger);
      break;

    case 'integrity_approve':
      await handleIntegrityApprove(task, service, logger);
      break;

    case 'integrity_status':
      await handleIntegrityStatus(task, service, logger);
      break;

    case 'integrity_verify_audit':
      await handleIntegrityVerifyAudit(task, service, logger);
      break;

    default:
      logger.warn({ type }, 'Unknown integrity task type');
  }
}

// ============================================================================
// Individual Handlers
// ============================================================================

async function handleIntegrityCheck(
  task: any,
  service: IntegrityService,
  logger: any
): Promise<void> {
  const { requestId, mode, autoRestore, groupFolder } = task;

  logger.info({ requestId, groupFolder }, 'Processing integrity_check');

  try {
    const monitor = service.getMonitor();

    if (mode === 'status') {
      // Status mode: just return baseline info
      const status = monitor.getStatus();
      writeResult(requestId, {
        success: true,
        mode: 'status',
        ...status
      });
    } else {
      // Check mode: detect drift and optionally restore
      const result = await monitor.checkIntegrity(autoRestore !== false, 'agent');

      writeResult(requestId, result);

      if (result.drift_detected) {
        logger.warn(
          { requestId, drifted: result.summary.drifted, restored: result.summary.restored },
          'Integrity drift detected'
        );
      } else {
        logger.info({ requestId }, 'Integrity check passed');
      }
    }
  } catch (error) {
    logger.error({ error, requestId }, 'Integrity check failed');
    writeResult(requestId, {
      success: false,
      error: error instanceof Error ? error.message : String(error)
    });
  }
}

async function handleIntegrityApprove(
  task: any,
  service: IntegrityService,
  logger: any
): Promise<void> {
  const { requestId, path: filePath, note, approvedBy, groupFolder } = task;

  logger.info({ requestId, filePath, groupFolder }, 'Processing integrity_approve');

  try {
    const monitor = service.getMonitor();

    await monitor.approveChange(filePath, approvedBy || 'agent', note || '');

    writeResult(requestId, {
      success: true,
      path: filePath,
      approved_at: new Date().toISOString(),
      approved_by: approvedBy,
      note
    });

    logger.info({ requestId, filePath }, 'File change approved');
  } catch (error) {
    logger.error({ error, requestId, filePath }, 'Approve change failed');
    writeResult(requestId, {
      success: false,
      error: error instanceof Error ? error.message : String(error),
      path: filePath
    });
  }
}

async function handleIntegrityStatus(
  task: any,
  service: IntegrityService,
  logger: any
): Promise<void> {
  const { requestId, path: filePath, groupFolder } = task;

  logger.info({ requestId, filePath, groupFolder }, 'Processing integrity_status');

  try {
    const monitor = service.getMonitor();
    const status = monitor.getStatus(filePath);

    writeResult(requestId, {
      success: true,
      ...status
    });

    logger.info({ requestId }, 'Status retrieved');
  } catch (error) {
    logger.error({ error, requestId }, 'Status check failed');
    writeResult(requestId, {
      success: false,
      error: error instanceof Error ? error.message : String(error)
    });
  }
}

async function handleIntegrityVerifyAudit(
  task: any,
  service: IntegrityService,
  logger: any
): Promise<void> {
  const { requestId, groupFolder } = task;

  logger.info({ requestId, groupFolder }, 'Processing integrity_verify_audit');

  try {
    const monitor = service.getMonitor();
    const verification = monitor.verifyAuditChain();

    writeResult(requestId, {
      success: true,
      ...verification
    });

    if (!verification.valid) {
      logger.error({ requestId, errors: verification.errors }, 'Audit chain verification failed');
    } else {
      logger.info({ requestId, entries: verification.entries }, 'Audit chain verified');
    }
  } catch (error) {
    logger.error({ error, requestId }, 'Audit verification failed');
    writeResult(requestId, {
      success: false,
      error: error instanceof Error ? error.message : String(error)
    });
  }
}

// ============================================================================
// Helper Functions
// ============================================================================

function writeResult(requestId: string, result: any): void {
  const resultDir = '/workspace/ipc/clawsec_results';

  // Ensure directory exists
  if (!fs.existsSync(resultDir)) {
    fs.mkdirSync(resultDir, { recursive: true });
  }

  const resultPath = path.join(resultDir, `${requestId}.json`);
  fs.writeFileSync(resultPath, JSON.stringify(result, null, 2));
}

// ============================================================================
// Integration Instructions
// ============================================================================

/**
 * To integrate into NanoClaw host process:
 *
 * 1. Add IntegrityService to IpcDeps in src/ipc.ts:
 *
 *    import { IntegrityService, getIntegrityService } from '../skills/clawsec-nanoclaw/host-services/integrity-handler';
 *
 *    export interface IpcDeps {
 *      // ... existing deps
 *      integrityService?: IntegrityService;
 *    }
 *
 * 2. Initialize in main.ts:
 *
 *    const integrityService = getIntegrityService();
 *    await integrityService.initialize();
 *
 *    const ipcDeps: IpcDeps = {
 *      // ... existing deps
 *      integrityService
 *    };
 *
 * 3. Add handler calls in processTaskIpc switch statement:
 *
 *    case 'integrity_check':
 *    case 'integrity_approve':
 *    case 'integrity_status':
 *    case 'integrity_verify_audit':
 *      await handleIntegrityIpc(task, deps, logger);
 *      break;
 *
 * 4. Ensure /workspace/ipc/clawsec_results/ directory exists and is writable
 *
 * 5. Ensure /workspace/project/data/soul-guardian/ directory exists and is writable
 */

// Example scheduled task for continuous monitoring:
//
// schedule_task({
//   prompt: `
//     Run clawsec_check_integrity to check for file tampering.
//     If drift_detected is true and files were restored, send alert:
//     "SECURITY: Unauthorized changes detected and reverted in:
//     [list restored files with their paths]
//     Review patches in /workspace/project/data/soul-guardian/patches/"
//   `,
//   schedule_type: 'cron',
//   schedule_value: '*/30 * * * *',  // Every 30 minutes
//   context_mode: 'isolated'
// });


path: host-services/ipc-handlers.ts
content: /**
 * ClawSec Advisory Feed IPC Handler Additions for NanoClaw
 *
 * Add this case to the switch statement in /workspace/project/src/ipc.ts
 * inside the processTaskIpc function.
 *
 * This handler processes advisory cache refresh requests from agents.
 */

import { AdvisoryCacheManager } from './advisory-cache';
import { SkillSignatureVerifier } from './skill-signature-handler';

// Add to IpcDeps interface:
export interface IpcDeps {
  advisoryCacheManager?: AdvisoryCacheManager;
  signatureVerifier?: SkillSignatureVerifier;
}

interface IpcLogger {
  info(obj: Record<string, unknown>, msg?: string): void;
  warn(obj: Record<string, unknown>, msg?: string): void;
  error(obj: Record<string, unknown>, msg?: string): void;
}

// eslint-disable-next-line @typescript-eslint/no-explicit-any
type IpcTask = Record<string, any>;

/**
 * Placeholder for the host-side writeResponse function.
 * The actual implementation lives in the NanoClaw host process.
 */
declare function writeResponse(requestId: string, data: Record<string, unknown>): Promise<void>;

/**
 * Handle advisory and signature IPC tasks.
 *
 * In the host process, call this from the processTaskIpc switch statement
 * for the 'refresh_advisory_cache' and 'verify_skill_signature' cases.
 */
export async function handleAdvisoryIpc(
  task: IpcTask,
  deps: IpcDeps,
  logger: IpcLogger,
  sourceGroup: string,
): Promise<void> {
  switch (task.type) {
    case 'refresh_advisory_cache':
      // Any group can request cache refresh (rate-limited by cache manager)
      logger.info({ sourceGroup }, 'Advisory cache refresh requested via IPC');
      if (deps.advisoryCacheManager) {
        try {
          await deps.advisoryCacheManager.refresh();
          logger.info({ sourceGroup }, 'Advisory cache refreshed successfully');
        } catch (error) {
          logger.error({ error, sourceGroup }, 'Advisory cache refresh failed');
        }
      } else {
        logger.warn({ sourceGroup }, 'Advisory cache manager not initialized');
      }
      break;

    case 'verify_skill_signature': {
      // Skill signature verification (Phase 1)
      const { requestId, packagePath, signaturePath } = task;

      logger.info({ sourceGroup, requestId, packagePath }, 'Verifying skill signature');

      try {
        if (!deps.signatureVerifier) {
          throw new Error('Signature verification service not available');
        }

        const result = await deps.signatureVerifier.verify({
          packagePath,
          signaturePath,
        });

        await writeResponse(requestId, {
          success: true,
          message: result.valid ? 'Signature valid' : 'Signature invalid',
          data: result,
        });

        logger.info(
          { sourceGroup, requestId, valid: result.valid, signer: result.signer },
          'Signature verification completed'
        );
      } catch (error: unknown) {
        const err = error as Error & { code?: string };
        logger.error({ error, sourceGroup, requestId, packagePath }, 'Signature verification failed');

        const errorCode = err.code || 'CRYPTO_ERROR';
        await writeResponse(requestId, {
          success: false,
          message: err.message || 'Verification failed',
          error: {
            code: errorCode,
            details: error
          }
        });
      }
      break;
    }
  }
}


path: host-services/skill-signature-handler.ts
content: /**
 * Skill Signature Verification Handler for NanoClaw
 *
 * Verifies Ed25519 signatures on skill packages to prevent supply chain attacks.
 * Uses the same pinned public key as advisory feed verification.
 */

import fs from 'fs';
import path from 'path';
import {
  verifyDetachedSignatureWithDetails,
  loadPublicKey,
  sha256File,
  SecurityPolicyError
} from '../lib/signatures.js';

/**
 * Default location of ClawSec's pinned public key (same as advisory feed)
 */
const DEFAULT_PUBLIC_KEY_PATH = path.join(
  __dirname,
  '../advisories/feed-signing-public.pem'
);

/**
 * Verification result interface
 */
export interface VerificationResult {
  valid: boolean;
  signer: string | null;
  packageHash: string;
  verifiedAt: string;
  algorithm: 'Ed25519';
  error?: string;
}

/**
 * Verification parameters interface
 */
export interface VerifyParams {
  packagePath: string;
  signaturePath: string;
}

const ALLOWED_PACKAGE_ROOTS = [
  '/tmp',
  '/var/tmp',
  '/workspace/ipc',
  '/workspace/project/data',
  '/workspace/project/tmp',
  '/workspace/project/downloads',
] as const;

const ALLOWED_PACKAGE_EXTENSIONS = ['.zip', '.tar', '.tgz', '.tar.gz'] as const;

function isWithinAllowedRoots(filePath: string): boolean {
  return ALLOWED_PACKAGE_ROOTS.some((root) => filePath === root || filePath.startsWith(`${root}/`));
}

function hasAllowedPackageExtension(filePath: string): boolean {
  return ALLOWED_PACKAGE_EXTENSIONS.some((ext) => filePath.endsWith(ext));
}

function normalizeAndValidatePath(rawPath: string, kind: 'package' | 'signature'): string {
  if (!path.isAbsolute(rawPath)) {
    throw new SecurityPolicyError(`${kind} path must be absolute`);
  }

  const resolved = path.resolve(rawPath);
  if (!isWithinAllowedRoots(resolved)) {
    throw new SecurityPolicyError(
      `${kind} path must be under allowed roots: ${ALLOWED_PACKAGE_ROOTS.join(', ')}`
    );
  }

  if (kind === 'package' && !hasAllowedPackageExtension(resolved)) {
    throw new SecurityPolicyError(
      `package path must use one of: ${ALLOWED_PACKAGE_EXTENSIONS.join(', ')}`
    );
  }

  if (kind === 'signature' && !resolved.endsWith('.sig')) {
    throw new SecurityPolicyError('signature path must end with .sig');
  }

  return resolved;
}

function ensureExistingRegularFile(filePath: string, kind: 'package' | 'signature'): string {
  if (!fs.existsSync(filePath)) {
    throw new SecurityPolicyError(`${kind} file not found: ${filePath}`);
  }

  const stat = fs.lstatSync(filePath);
  if (stat.isSymbolicLink()) {
    throw new SecurityPolicyError(`${kind} path cannot be a symlink`);
  }
  if (!stat.isFile()) {
    throw new SecurityPolicyError(`${kind} path must be a regular file`);
  }

  const realPath = fs.realpathSync(filePath);
  if (!isWithinAllowedRoots(realPath)) {
    throw new SecurityPolicyError(`${kind} real path escapes allowed roots`);
  }

  return realPath;
}

function validatePackagePath(rawPackagePath: string): string {
  const resolved = normalizeAndValidatePath(rawPackagePath, 'package');
  return ensureExistingRegularFile(resolved, 'package');
}

function validateSignaturePath(rawSignaturePath: string): string {
  const resolved = normalizeAndValidatePath(rawSignaturePath, 'signature');
  return ensureExistingRegularFile(resolved, 'signature');
}

/**
 * Service class for skill package signature verification
 */
export class SkillSignatureVerifier {
  private publicKeyPath: string;
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  private logger: any;

  constructor(
    publicKeyPath: string = DEFAULT_PUBLIC_KEY_PATH,
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    logger?: any
  ) {
    this.publicKeyPath = publicKeyPath;
    this.logger = logger || console;
  }

  /**
   * Verify Ed25519 signature of a skill package
   */
  async verify(params: VerifyParams): Promise<VerificationResult> {
    const {
      packagePath,
      signaturePath,
    } = params;

    let validatedPackagePath: string;
    let validatedSignaturePath: string;
    try {
      validatedPackagePath = validatePackagePath(packagePath);
      validatedSignaturePath = validateSignaturePath(signaturePath);
    } catch (error) {
      return {
        valid: false,
        signer: null,
        packageHash: '',
        verifiedAt: new Date().toISOString(),
        algorithm: 'Ed25519',
        error: error instanceof Error ? error.message : String(error),
      };
    }

    // Load pinned ClawSec key only
    let keyPem: string;
    try {
      if (!fs.existsSync(this.publicKeyPath)) {
        return {
          valid: false,
          signer: null,
          packageHash: '',
          verifiedAt: new Date().toISOString(),
          algorithm: 'Ed25519',
          error: `Public key file not found: ${this.publicKeyPath}`
        };
      }

      keyPem = fs.readFileSync(this.publicKeyPath, 'utf8');
      loadPublicKey(keyPem); // Validate pinned key
    } catch (error) {
      if (error instanceof SecurityPolicyError) {
        return {
          valid: false,
          signer: null,
          packageHash: '',
          verifiedAt: new Date().toISOString(),
          algorithm: 'Ed25519',
          error: error.message
        };
      }
      return {
        valid: false,
        signer: null,
        packageHash: '',
        verifiedAt: new Date().toISOString(),
        algorithm: 'Ed25519',
        error: `Failed to load public key: ${error instanceof Error ? error.message : String(error)}`
      };
    }

    // Compute package hash (always, for integrity tracking)
    let packageHash: string;
    try {
      packageHash = sha256File(validatedPackagePath);
    } catch (error) {
      return {
        valid: false,
        signer: null,
        packageHash: '',
        verifiedAt: new Date().toISOString(),
        algorithm: 'Ed25519',
        error: `Failed to compute package hash: ${error instanceof Error ? error.message : String(error)}`
      };
    }

    // Verify signature
    const verificationResult = verifyDetachedSignatureWithDetails(
      validatedPackagePath,
      validatedSignaturePath,
      keyPem
    );

    // Return structured result
    return {
      valid: verificationResult.valid,
      signer: verificationResult.valid ? 'clawsec' : null,
      packageHash,
      verifiedAt: new Date().toISOString(),
      algorithm: 'Ed25519',
      error: verificationResult.error
    };
  }

  /**
   * Get public key fingerprint for auditing
   */
  getPublicKeyFingerprint(): string {
    try {
      const keyPem = fs.readFileSync(this.publicKeyPath, 'utf8');
      const keyObject = loadPublicKey(keyPem);
      const _keyDer = keyObject.export({ type: 'spki', format: 'der' });
      return `sha256:${sha256File(this.publicKeyPath).substring(0, 16)}`;
    } catch (error) {
      this.logger.error({ error }, 'Failed to compute public key fingerprint');
      return 'unknown';
    }
  }
}

/**
 * Error codes for IPC responses
 */
export const ErrorCodes = {
  SIGNATURE_INVALID: 'SIGNATURE_INVALID',
  FILE_NOT_FOUND: 'FILE_NOT_FOUND',
  CRYPTO_ERROR: 'CRYPTO_ERROR',
  SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE'
} as const;

/**
 * Map verification errors to standard error codes
 */
export function mapErrorCode(error: string): string {
  if (error.includes('not found')) {
    return ErrorCodes.FILE_NOT_FOUND;
  }
  if (error.includes('Invalid signature') || error.includes('verification failed')) {
    return ErrorCodes.SIGNATURE_INVALID;
  }
  if (error.includes('public key') || error.includes('PEM')) {
    return ErrorCodes.CRYPTO_ERROR;
  }
  return ErrorCodes.CRYPTO_ERROR;
}


path: lib/advisories.ts
content: /**
 * Advisory Feed Loading and Matching for NanoClaw
 * Ported from ClawSec's feed.mjs with fail-closed verification
 */

import fs from 'fs/promises';
import path from 'path';
import {
  Advisory,
  AdvisoryFeed,
  AdvisoryMatch,
  AffectedSpecifier,
  SignatureVerificationOptions,
} from './types.js';
import {
  verifySignedPayload,
  parseChecksumsManifest,
  verifyChecksums,
  fetchText,
  defaultChecksumsUrl,
  SecurityPolicyError,
} from './signatures.js';

const DEFAULT_FEED_URL = 'https://clawsec.prompt.security/advisories/feed.json';

/**
 * Validates that a payload is a valid advisory feed.
 */
export function isValidFeedPayload(raw: unknown): raw is AdvisoryFeed {
  if (typeof raw !== 'object' || raw === null) return false;
  const obj = raw as Record<string, unknown>;

  if (typeof obj.version !== 'string' || !obj.version.trim()) return false;
  if (!Array.isArray(obj.advisories)) return false;

  for (const advisory of obj.advisories) {
    if (typeof advisory !== 'object' || advisory === null) return false;
    const adv = advisory as Record<string, unknown>;

    if (typeof adv.id !== 'string' || !adv.id.trim()) return false;
    if (typeof adv.severity !== 'string' || !adv.severity.trim()) return false;
    if (!Array.isArray(adv.affected)) return false;
    if (!adv.affected.every((entry) => typeof entry === 'string' && entry.trim())) return false;
  }

  return true;
}

/**
 * Parses an affected specifier like "skill-name@version-spec".
 */
export function parseAffectedSpecifier(rawSpecifier: string): AffectedSpecifier | null {
  const specifier = rawSpecifier.trim();
  if (!specifier) return null;

  const atIndex = specifier.lastIndexOf('@');
  if (atIndex <= 0) {
    return { name: specifier, versionSpec: '*' };
  }

  return {
    name: specifier.slice(0, atIndex),
    versionSpec: specifier.slice(atIndex + 1),
  };
}

/**
 * Normalizes a skill name for comparison.
 */
export function normalizeSkillName(name: string): string {
  return name.toLowerCase().trim().replace(/[^a-z0-9-]/g, '');
}

/**
 * Checks if a version matches a version specifier.
 * Supports: exact match, semver range (^, ~, *), wildcards
 */
export function versionMatches(version: string, versionSpec: string): boolean {
  const v = version.trim();
  const spec = versionSpec.trim();

  // Wildcard matches everything
  if (spec === '*' || spec === '') return true;

  // Exact match
  if (v === spec) return true;

  // Parse semver components
  const parseVersion = (ver: string): number[] => {
    const match = ver.match(/^(\d+)\.(\d+)\.(\d+)/);
    if (!match) return [];
    return [parseInt(match[1], 10), parseInt(match[2], 10), parseInt(match[3], 10)];
  };

  const vParts = parseVersion(v);
  const specParts = parseVersion(spec.replace(/^[~^]/, ''));

  if (vParts.length === 0 || specParts.length === 0) return false;

  // Caret range (^1.2.3): compatible with 1.x.x where x >= 2.3
  if (spec.startsWith('^')) {
    if (vParts[0] !== specParts[0]) return false;
    if (vParts[0] === 0) {
      // ^0.2.3 means 0.2.x where x >= 3
      if (vParts[1] !== specParts[1]) return false;
      return vParts[2] >= specParts[2];
    }
    // ^1.2.3 means 1.x.x where x.x >= 2.3
    if (vParts[1] > specParts[1]) return true;
    if (vParts[1] < specParts[1]) return false;
    return vParts[2] >= specParts[2];
  }

  // Tilde range (~1.2.3): patch-level compatibility (1.2.x where x >= 3)
  if (spec.startsWith('~')) {
    if (vParts[0] !== specParts[0]) return false;
    if (vParts[1] !== specParts[1]) return false;
    return vParts[2] >= specParts[2];
  }

  return false;
}

/**
 * Checks whether an affected specifier matches a skill name/version.
 * Optionally matches against a skill directory name as alias.
 */
export function matchesAffectedSpecifier(
  affected: string,
  skillName: string,
  skillVersion: string | null,
  skillDirName?: string
): boolean {
  const parsed = parseAffectedSpecifier(affected);
  if (!parsed) return false;

  const normalizedTarget = normalizeSkillName(parsed.name);
  const normalizedSkillName = normalizeSkillName(skillName);
  const normalizedDirName = skillDirName ? normalizeSkillName(skillDirName) : null;

  if (normalizedTarget !== normalizedSkillName && normalizedTarget !== normalizedDirName) {
    return false;
  }

  if (!skillVersion) {
    return true;
  }

  return versionMatches(skillVersion, parsed.versionSpec);
}

/**
 * Loads advisory feed from a remote URL with signature verification.
 */
export async function loadRemoteFeed(
  feedUrl: string,
  options: SignatureVerificationOptions
): Promise<AdvisoryFeed | null> {
  const signatureUrl = options.signatureUrl || `${feedUrl}.sig`;
  const checksumsUrl = options.checksumsUrl || defaultChecksumsUrl(feedUrl);
  const checksumsSignatureUrl = options.checksumsSignatureUrl || `${checksumsUrl}.sig`;
  const publicKeyPem = options.publicKeyPem;
  const checksumsPublicKeyPem = options.checksumsPublicKeyPem || publicKeyPem;
  const allowUnsigned = options.allowUnsigned || false;
  const verifyChecksumManifest = options.verifyChecksumManifest !== false;

  try {
    const payloadRaw = await fetchText(feedUrl);
    if (!payloadRaw) return null;

    if (!allowUnsigned) {
      const signatureRaw = await fetchText(signatureUrl);
      if (!signatureRaw) return null;

      if (!verifySignedPayload(payloadRaw, signatureRaw, publicKeyPem)) {
        return null;
      }

      // Verify checksum manifest if available
      if (verifyChecksumManifest) {
        const checksumsRaw = await fetchText(checksumsUrl);
        const checksumsSignatureRaw = await fetchText(checksumsSignatureUrl);

        // Only proceed if BOTH checksum files are present
        if (checksumsRaw && checksumsSignatureRaw) {
          if (!verifySignedPayload(checksumsRaw, checksumsSignatureRaw, checksumsPublicKeyPem)) {
            return null; // Fail-closed: invalid signature
          }

          const checksumsManifest = parseChecksumsManifest(checksumsRaw);
          const checksumFeedEntry = feedUrl.split('/').pop() || 'feed.json';
          const checksumSignatureEntry = signatureUrl.split('/').pop() || 'feed.json.sig';
          verifyChecksums(checksumsManifest, {
            [checksumFeedEntry]: payloadRaw,
            [checksumSignatureEntry]: signatureRaw,
          });
        }
        // If checksum files missing: continue without checksum verification
        // (feed signature was already verified above)
      }
    }

    try {
      const payload = JSON.parse(payloadRaw);
      if (!isValidFeedPayload(payload)) return null;
      return payload;
    } catch {
      return null;
    }
  } catch (error) {
    // Security policy violations return null to allow graceful fallback to local feed
    if (error instanceof SecurityPolicyError) {
      return null;
    }
    // Re-throw unexpected errors
    throw error;
  }
}

/**
 * Loads advisory feed from a local file with signature verification.
 */
export async function loadLocalFeed(
  feedPath: string,
  options: SignatureVerificationOptions
): Promise<AdvisoryFeed> {
  const signaturePath = options.signatureUrl || `${feedPath}.sig`;
  const checksumsPath = options.checksumsUrl || path.join(path.dirname(feedPath), 'checksums.json');
  const checksumsSignaturePath = options.checksumsSignatureUrl || `${checksumsPath}.sig`;
  const publicKeyPem = options.publicKeyPem;
  const checksumsPublicKeyPem = options.checksumsPublicKeyPem || publicKeyPem;
  const allowUnsigned = options.allowUnsigned || false;
  const verifyChecksumManifest = options.verifyChecksumManifest !== false;

  const payloadRaw = await fs.readFile(feedPath, 'utf8');

  if (!allowUnsigned) {
    const signatureRaw = await fs.readFile(signaturePath, 'utf8');
    if (!verifySignedPayload(payloadRaw, signatureRaw, publicKeyPem)) {
      throw new Error(`Feed signature verification failed for local feed: ${feedPath}`);
    }

    if (verifyChecksumManifest) {
      const checksumsRaw = await fs.readFile(checksumsPath, 'utf8');
      const checksumsSignatureRaw = await fs.readFile(checksumsSignaturePath, 'utf8');

      if (!verifySignedPayload(checksumsRaw, checksumsSignatureRaw, checksumsPublicKeyPem)) {
        throw new Error(`Checksum manifest signature verification failed: ${checksumsPath}`);
      }

      const checksumsManifest = parseChecksumsManifest(checksumsRaw);
      const checksumFeedEntry = path.basename(feedPath);
      const checksumSignatureEntry = path.basename(signaturePath);
      verifyChecksums(checksumsManifest, {
        [checksumFeedEntry]: payloadRaw,
        [checksumSignatureEntry]: signatureRaw,
      });
    }
  }

  const payload = JSON.parse(payloadRaw);
  if (!isValidFeedPayload(payload)) {
    throw new Error(`Invalid advisory feed format: ${feedPath}`);
  }
  return payload;
}

/**
 * Loads advisory feed from remote or falls back to local.
 */
export async function loadFeed(
  feedUrl: string = DEFAULT_FEED_URL,
  localFeedPath: string,
  publicKeyPem: string,
  allowUnsigned: boolean = false
): Promise<{ feed: AdvisoryFeed; source: string }> {
  const options: SignatureVerificationOptions = {
    publicKeyPem,
    allowUnsigned,
    verifyChecksumManifest: true,
  };

  // Try remote feed first
  const remoteFeed = await loadRemoteFeed(feedUrl, options);
  if (remoteFeed) {
    return { feed: remoteFeed, source: `remote:${feedUrl}` };
  }

  // Fall back to local feed
  const localFeed = await loadLocalFeed(localFeedPath, options);
  return { feed: localFeed, source: `local:${localFeedPath}` };
}

/**
 * Checks if an advisory looks high-risk.
 */
export function advisoryLooksHighRisk(advisory: Advisory): boolean {
  const type = advisory.type.toLowerCase();
  const severity = advisory.severity.toLowerCase();
  const exploitability = (advisory.exploitability_score || 'unknown').toLowerCase();
  const combined = `${advisory.title} ${advisory.description} ${advisory.action}`.toLowerCase();

  if (type.includes('malicious')) return true;
  if (severity === 'critical') return true;
  if (exploitability === 'high') return true;
  if (/\b(malicious|exfiltrate|exfiltration|backdoor|trojan|stealer|credential theft)\b/.test(combined)) return true;
  if (/\b(remove|uninstall|disable|do not use|quarantine)\b/.test(combined)) return true;

  return false;
}

/**
 * Finds advisory matches for a skill.
 */
export function findAdvisoryMatches(
  feed: AdvisoryFeed,
  skillName: string,
  version: string | null
): AdvisoryMatch[] {
  const matches: AdvisoryMatch[] = [];

  for (const advisory of feed.advisories) {
    const affected = advisory.affected || [];
    if (affected.length === 0) continue;

    for (const specifier of affected) {
      if (!matchesAffectedSpecifier(specifier, skillName, version)) {
        continue;
      }

      // Match found
      matches.push({
        advisory,
        matchedSpecifier: specifier,
        isHighRisk: advisoryLooksHighRisk(advisory),
      });
      break; // Only count each advisory once
    }
  }

  return matches;
}

/**
 * Removes duplicate strings from an array.
 */
export function uniqueStrings(arr: string[]): string[] {
  return Array.from(new Set(arr));
}


path: lib/local_file_io.ts
content: import fs from 'fs';

export function fileExists(filePath: string): boolean {
  return fs.existsSync(filePath);
}

export function loadBinaryFile(filePath: string): Buffer {
  return fs.readFileSync(filePath);
}

export function loadUtf8File(filePath: string): string {
  return fs.readFileSync(filePath, 'utf8');
}


path: lib/risk.ts
content: /**
 * Shared advisory risk evaluation for NanoClaw host + MCP layers.
 */

export type SkillSafetyRecommendation = 'install' | 'block' | 'review';

export interface AdvisoryRiskInput {
  severity?: string;
  type?: string;
  action?: string;
  exploitability_score?: string;
}

export interface AdvisoryRiskEvaluation {
  safe: boolean;
  recommendation: SkillSafetyRecommendation;
  reason: string;
}

export function normalizeExploitabilityScore(score: unknown): 'high' | 'medium' | 'low' | 'unknown' {
  const value = String(score || '').toLowerCase().trim();
  if (value === 'high' || value === 'medium' || value === 'low') {
    return value;
  }
  return 'unknown';
}

export function evaluateAdvisoryRisk(advisories: AdvisoryRiskInput[]): AdvisoryRiskEvaluation {
  if (advisories.length === 0) {
    return { safe: true, recommendation: 'install', reason: 'No advisories found' };
  }

  const hasMalicious = advisories.some((a) => String(a.type || '').toLowerCase().includes('malicious'));
  const hasRemoveAction = advisories.some((a) =>
    /\b(remove|uninstall|disable|quarantine|block)\b/i.test(String(a.action || ''))
  );
  const hasCritical = advisories.some((a) => String(a.severity || '').toLowerCase() === 'critical');
  const hasHigh = advisories.some((a) => String(a.severity || '').toLowerCase() === 'high');
  const hasHighExploitability = advisories.some(
    (a) => normalizeExploitabilityScore(a.exploitability_score) === 'high'
  );

  if (hasMalicious || hasRemoveAction) {
    return {
      safe: false,
      recommendation: 'block',
      reason: 'Malicious skill or removal recommended by ClawSec',
    };
  }

  if (hasCritical && hasHighExploitability) {
    return {
      safe: false,
      recommendation: 'block',
      reason: 'Critical advisory with high exploitability context - do not install',
    };
  }

  if (hasCritical) {
    return {
      safe: false,
      recommendation: 'block',
      reason: 'Critical security advisory - do not install',
    };
  }

  if (hasHighExploitability) {
    return {
      safe: false,
      recommendation: 'review',
      reason: 'High exploitability advisory - urgent user review strongly recommended',
    };
  }

  if (hasHigh) {
    return {
      safe: false,
      recommendation: 'review',
      reason: 'High severity advisory - user review strongly recommended',
    };
  }

  return {
    safe: false,
    recommendation: 'review',
    reason: 'Advisory found - review details before installing',
  };
}


path: lib/signatures.ts
content: /**
 * Ed25519 Signature Verification for NanoClaw
 * Ported from ClawSec's feed.mjs
 */

import crypto from 'crypto';
import https from 'https';
import { ChecksumsManifest } from './types.js';
import { fileExists, loadBinaryFile, loadUtf8File } from './local_file_io.js';

/**
 * Allowed domains for feed/signature fetching.
 * Only connections to these domains are permitted for security.
 */
const ALLOWED_DOMAINS = [
  'clawsec.prompt.security',
  'prompt.security',
  'raw.githubusercontent.com',
  'github.com',
];

/**
 * Custom error class for security policy violations.
 * These errors should always propagate and never be silently caught.
 */
export class SecurityPolicyError extends Error {
  constructor(message: string) {
    super(message);
    this.name = 'SecurityPolicyError';
  }
}

/**
 * Creates a secure HTTPS agent with TLS 1.2+ enforcement and certificate validation.
 */
function createSecureAgent(): https.Agent {
  return new https.Agent({
    // Enforce minimum TLS 1.2 (eliminate TLS 1.0, 1.1)
    minVersion: 'TLSv1.2',
    // Ensure certificate validation is enabled (reject unauthorized certificates)
    rejectUnauthorized: true,
    // Use strong cipher suites
    ciphers: 'TLS_AES_128_GCM_SHA256:TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256',
  });
}

/**
 * Validates that a URL is from an allowed domain.
 */
function isAllowedDomain(url: string): boolean {
  try {
    const parsed = new URL(url);

    // Only allow HTTPS protocol
    if (parsed.protocol !== 'https:') {
      return false;
    }

    const hostname = parsed.hostname.toLowerCase();

    // Check if hostname matches any allowed domain
    return ALLOWED_DOMAINS.some(
      (allowed) => hostname === allowed || hostname.endsWith(`.${allowed}`)
    );
  } catch {
    return false;
  }
}

/**
 * Secure wrapper around fetch with TLS enforcement and domain validation.
 */
export async function secureFetch(url: string, options: RequestInit = {}): Promise<Response> {
  // Validate domain before making request
  if (!isAllowedDomain(url)) {
    throw new SecurityPolicyError(
      `Security policy violation: URL domain not allowed. ` +
      `Only connections to ${ALLOWED_DOMAINS.join(', ')} are permitted. ` +
      `Blocked: ${url}`
    );
  }

  // Use secure HTTPS agent with TLS 1.2+ enforcement
  const agent = createSecureAgent();

  return fetch(url, {
    ...options,
    // @ts-expect-error - agent is supported in Node.js fetch
    agent,
  });
}

/**
 * Decodes a signature from various formats (base64 string or JSON).
 */
function decodeSignature(signatureRaw: string): Buffer | null {
  const trimmed = signatureRaw.trim();
  if (!trimmed) return null;

  let encoded = trimmed;
  if (trimmed.startsWith('{')) {
    try {
      const parsed = JSON.parse(trimmed);
      if (typeof parsed === 'object' && parsed !== null && typeof parsed.signature === 'string') {
        encoded = parsed.signature;
      }
    } catch {
      return null;
    }
  }

  const normalized = encoded.replace(/\s+/g, '');
  if (!normalized) return null;

  try {
    return Buffer.from(normalized, 'base64');
  } catch {
    return null;
  }
}

/**
 * Verifies an Ed25519 signature for a payload.
 */
export function verifySignedPayload(
  payloadRaw: string,
  signatureRaw: string,
  publicKeyPem: string
): boolean {
  const signature = decodeSignature(signatureRaw);
  if (!signature) return false;

  const keyPem = publicKeyPem.trim();
  if (!keyPem) return false;

  try {
    const publicKey = crypto.createPublicKey(keyPem);
    return crypto.verify(null, Buffer.from(payloadRaw, 'utf8'), publicKey, signature);
  } catch {
    return false;
  }
}

/**
 * Computes SHA-256 hash of content.
 */
export function sha256Hex(content: string | Buffer): string {
  return crypto.createHash('sha256').update(content).digest('hex');
}

/**
 * Computes SHA-256 hash of a file.
 * Convenience wrapper for file-based integrity monitoring and package verification.
 */
export function sha256File(filePath: string): string {
  const data = loadBinaryFile(filePath);
  return sha256Hex(data);
}

/**
 * Loads and validates an Ed25519 public key from PEM format.
 * @throws {SecurityPolicyError} if PEM format is invalid
 */
export function loadPublicKey(pemString: string): crypto.KeyObject {
  const trimmed = pemString.trim();
  if (!trimmed.startsWith('-----BEGIN PUBLIC KEY-----')) {
    throw new SecurityPolicyError('Invalid PEM format: must start with -----BEGIN PUBLIC KEY-----');
  }

  try {
    return crypto.createPublicKey(trimmed);
  } catch (error) {
    throw new SecurityPolicyError(
      `Failed to load public key: ${error instanceof Error ? error.message : String(error)}`
    );
  }
}

/**
 * Verifies Ed25519 detached signature for a file.
 * Matches the API of verify_detached_ed25519.mjs from OpenClaw.
 *
 * @param dataPath - Path to the file to verify
 * @param signaturePath - Path to the detached signature file (.sig)
 * @param publicKeyPem - Ed25519 public key in PEM format
 * @returns true if signature is valid, false otherwise
 */
export function verifyDetachedSignature(
  dataPath: string,
  signaturePath: string,
  publicKeyPem: string
): boolean {
  try {
    const data = loadBinaryFile(dataPath);
    const signatureRaw = loadUtf8File(signaturePath);
    const signature = decodeSignature(signatureRaw);

    if (!signature) return false;

    const publicKey = crypto.createPublicKey(publicKeyPem.trim());
    return crypto.verify(null, data, publicKey, signature);
  } catch {
    return false;
  }
}

/**
 * Verifies detached signature with detailed error information.
 * Useful for debugging signature verification failures.
 *
 * @param dataPath - Path to the file to verify
 * @param signaturePath - Path to the detached signature file (.sig)
 * @param publicKeyPem - Ed25519 public key in PEM format
 * @returns Object with valid flag and optional error message
 */
export function verifyDetachedSignatureWithDetails(
  dataPath: string,
  signaturePath: string,
  publicKeyPem: string
): { valid: boolean; error?: string } {
  try {
    if (!fileExists(dataPath)) {
      return { valid: false, error: 'Data file not found' };
    }
    if (!fileExists(signaturePath)) {
      return { valid: false, error: 'Signature file not found' };
    }

    const data = loadBinaryFile(dataPath);
    const signatureRaw = loadUtf8File(signaturePath);
    const signature = decodeSignature(signatureRaw);

    if (!signature) {
      return { valid: false, error: 'Invalid signature format' };
    }

    const publicKey = crypto.createPublicKey(publicKeyPem.trim());
    const valid = crypto.verify(null, data, publicKey, signature);

    return { valid, error: valid ? undefined : 'Signature verification failed' };
  } catch (error) {
    return {
      valid: false,
      error: `Verification error: ${error instanceof Error ? error.message : String(error)}`
    };
  }
}

/**
 * Verifies multiple files against expected hashes.
 * Returns list of files that don't match their expected hashes.
 *
 * @param files - Map of file paths to expected SHA-256 hashes
 * @returns Array of mismatches with path, expected, and actual hashes
 */
export function verifyFileHashes(
  files: Record<string, string>
): { path: string; expected: string; actual: string }[] {
  const mismatches = [];

  for (const [path, expectedHash] of Object.entries(files)) {
    try {
      const actualHash = sha256File(path);
      if (actualHash !== expectedHash) {
        mismatches.push({ path, expected: expectedHash, actual: actualHash });
      }
    } catch (error) {
      // File missing or unreadable
      mismatches.push({
        path,
        expected: expectedHash,
        actual: `ERROR: ${error instanceof Error ? error.message : String(error)}`
      });
    }
  }

  return mismatches;
}

/**
 * Extracts SHA-256 value from various formats.
 */
function extractSha256Value(value: unknown): string | null {
  if (typeof value === 'string') {
    const normalized = value.trim().toLowerCase();
    return /^[a-f0-9]{64}$/.test(normalized) ? normalized : null;
  }

  if (typeof value === 'object' && value !== null && 'sha256' in value) {
    const sha256 = (value as { sha256: unknown }).sha256;
    if (typeof sha256 === 'string') {
      const normalized = sha256.trim().toLowerCase();
      return /^[a-f0-9]{64}$/.test(normalized) ? normalized : null;
    }
  }

  return null;
}

/**
 * Parses a checksums manifest JSON.
 */
export function parseChecksumsManifest(manifestRaw: string): ChecksumsManifest {
  let parsed: unknown;
  try {
    parsed = JSON.parse(manifestRaw);
  } catch {
    throw new Error('Checksum manifest is not valid JSON');
  }

  if (typeof parsed !== 'object' || parsed === null) {
    throw new Error('Checksum manifest must be an object');
  }

  const obj = parsed as Record<string, unknown>;

  const algorithmRaw = typeof obj.algorithm === 'string' ? obj.algorithm.trim().toLowerCase() : 'sha256';
  if (algorithmRaw !== 'sha256') {
    throw new Error(`Unsupported checksum manifest algorithm: ${algorithmRaw || '(empty)'}`);
  }

  // Support legacy manifest formats
  const schemaVersion = (
    typeof obj.schema_version === 'string' ? obj.schema_version.trim() :
    typeof obj.version === 'string' ? obj.version.trim() :
    typeof obj.generated_at === 'string' ? obj.generated_at.trim() :
    '1'
  );

  if (!schemaVersion) {
    throw new Error('Checksum manifest missing schema_version');
  }

  if (typeof obj.files !== 'object' || obj.files === null) {
    throw new Error('Checksum manifest missing files object');
  }

  const files: Record<string, string> = {};
  for (const [key, value] of Object.entries(obj.files)) {
    if (!key.trim()) continue;
    const digest = extractSha256Value(value);
    if (!digest) {
      throw new Error(`Invalid checksum digest entry for ${key}`);
    }
    files[key] = digest;
  }

  if (Object.keys(files).length === 0) {
    throw new Error('Checksum manifest has no usable file digests');
  }

  return {
    schema_version: schemaVersion,
    algorithm: 'sha256',
    files,
  };
}

/**
 * Normalizes a checksum entry name for matching.
 */
function normalizeChecksumEntryName(entryName: string): string {
  return entryName
    .trim()
    .replace(/\\/g, '/')
    .replace(/^(?:\.\/)+/, '')
    .replace(/^\/+/, '');
}

/**
 * Resolves a checksum manifest entry by name.
 */
function resolveChecksumManifestEntry(
  files: Record<string, string>,
  entryName: string
): { key: string; digest: string } | null {
  const normalizedEntry = normalizeChecksumEntryName(entryName);
  if (!normalizedEntry) return null;

  // Try direct match and common variations
  const directCandidates = [
    normalizedEntry,
    normalizedEntry.split('/').pop() || '',
    `advisories/${normalizedEntry.split('/').pop() || ''}`,
  ].filter((c, i, a) => c && a.indexOf(c) === i);

  for (const candidate of directCandidates) {
    if (candidate in files) {
      return { key: candidate, digest: files[candidate] };
    }
  }

  // Try basename matching
  const basename = normalizedEntry.split('/').pop() || '';
  if (!basename) return null;

  const basenameMatches = Object.entries(files).filter(([key]) => {
    const normalizedKey = normalizeChecksumEntryName(key);
    return normalizedKey.split('/').pop() === basename;
  });

  if (basenameMatches.length > 1) {
    throw new Error(
      `Checksum manifest entry is ambiguous for ${entryName}; ` +
      `multiple manifest keys share basename ${basename}`
    );
  }

  if (basenameMatches.length === 1) {
    const [resolvedKey, digest] = basenameMatches[0];
    return { key: resolvedKey, digest };
  }

  return null;
}

/**
 * Verifies checksums for expected entries.
 */
export function verifyChecksums(
  manifest: ChecksumsManifest,
  expectedEntries: Record<string, string | Buffer>
): void {
  for (const [entryName, entryContent] of Object.entries(expectedEntries)) {
    if (!entryName) continue;

    const resolved = resolveChecksumManifestEntry(manifest.files, entryName);
    if (!resolved) {
      throw new Error(`Checksum manifest missing required entry: ${entryName}`);
    }

    const actualDigest = sha256Hex(entryContent);
    if (actualDigest !== resolved.digest) {
      throw new Error(`Checksum mismatch for ${entryName} (manifest key: ${resolved.key})`);
    }
  }
}

/**
 * Fetches text from a URL with timeout.
 */
export async function fetchText(url: string, timeoutMs: number = 10000): Promise<string | null> {
  const controller = new AbortController();
  const timeout = setTimeout(() => controller.abort(), timeoutMs);

  try {
    const response = await secureFetch(url, {
      method: 'GET',
      signal: controller.signal,
      headers: { accept: 'application/json,text/plain;q=0.9,*/*;q=0.8' },
    });
    if (!response.ok) return null;
    return await response.text();
  } catch (error) {
    // Re-throw security policy violations - these should never be silently caught
    if (error instanceof SecurityPolicyError) {
      throw error;
    }
    // Network errors, timeouts, etc. return null (graceful degradation)
    return null;
  } finally {
    clearTimeout(timeout);
  }
}

/**
 * Default checksums URL from feed URL.
 */
export function defaultChecksumsUrl(feedUrl: string): string {
  try {
    return new URL('checksums.json', feedUrl).toString();
  } catch {
    const fallbackBase = feedUrl.replace(/\/?[^/]*$/, '');
    return `${fallbackBase}/checksums.json`;
  }
}

/**
 * Safely extracts the basename from a URL or file path.
 */
function _safeBasename(urlOrPath: string, fallback: string): string {
  try {
    const parsed = new URL(urlOrPath);
    const pathname = parsed.pathname;
    const lastSlash = pathname.lastIndexOf('/');
    if (lastSlash >= 0 && lastSlash < pathname.length - 1) {
      return pathname.slice(lastSlash + 1);
    }
  } catch {
    const normalized = urlOrPath.trim();
    const lastSlash = normalized.lastIndexOf('/');
    if (lastSlash >= 0 && lastSlash < normalized.length - 1) {
      return normalized.slice(lastSlash + 1);
    }
  }
  return fallback;
}


path: lib/types.ts
content: /**
 * TypeScript types for NanoClaw Skill Installer
 * Adapted from ClawSec's guarded skill installer
 */

export interface Advisory {
  id: string;
  ghsa_id?: string;
  cve_id?: string | null;
  status?: 'active' | 'matured' | 'stale' | string;
  stale?: boolean;
  source_feed?: string;
  severity: 'critical' | 'high' | 'medium' | 'low';
  type: 'vulnerable_skill' | 'malicious_skill' | 'prompt_injection' | string;
  title: string;
  description: string;
  affected: string[]; // e.g., ["skill-name@1.0.0", "skill-name@1.0.1"]
  action: string;
  published: string;
  references: string[];
  cvss_score?: number;
  cvss_vector?: string | null;
  nvd_url?: string;
  github_advisory_url?: string;
  platforms?: string[];
  exploitability_score?: 'high' | 'medium' | 'low' | 'unknown';
  exploitability_rationale?: string;
  source?: string;
  github_issue_url?: string;
  reporter?: {
    agent_name?: string;
    opener_type?: string;
  };
}

export interface AdvisoryFeed {
  version: string;
  updated: string;
  description: string;
  advisories: Advisory[];
}

export interface AdvisoryMatch {
  advisory: Advisory;
  matchedSpecifier: string;
  isHighRisk: boolean;
}

export interface ReputationResult {
  score: number; // 0-100
  warnings: string[];
  virusTotalFlags: string[];
  safe: boolean;
}

export interface SkillMetadata {
  slug: string;
  name: string;
  version: string;
  description: string;
  author: string;
  created: string;
  updated: string;
  downloads: number;
}

export interface InspectSkillResult {
  skill: SkillMetadata;
  reputation: ReputationResult;
  advisories: AdvisoryMatch[];
  overallStatus: 'safe' | 'reputation_warning' | 'advisory_warning' | 'blocked';
}

export interface SkillInstallRequest {
  request_id: string;
  user_jid: string;
  group_jid: string;
  skill_slug: string;
  skill_version: string | null;
  reputation_score: number;
  reputation_warnings: string[];
  advisories: AdvisoryMatch[];
  created_at: number; // Unix timestamp
  expires_at: number; // Unix timestamp
  status: 'pending' | 'confirmed' | 'expired' | 'cancelled';
  confirmed_at: number | null;
}

export interface ChecksumsManifest {
  schema_version: string;
  algorithm: 'sha256';
  files: Record<string, string>; // filename -> hex digest
}

export interface SignatureVerificationOptions {
  signatureUrl?: string;
  checksumsUrl?: string;
  checksumsSignatureUrl?: string;
  publicKeyPem: string;
  checksumsPublicKeyPem?: string;
  allowUnsigned?: boolean;
  verifyChecksumManifest?: boolean;
}

export interface AffectedSpecifier {
  name: string;
  versionSpec: string; // e.g., "1.0.0", "^1.0.0", "*"
}

// MCP Tool Request/Response Types

export interface InspectSkillRequest {
  slug: string;
  version?: string;
}

export interface RequestSkillInstallRequest {
  slug: string;
  version?: string;
  target_group_jid?: string;
}

export interface RequestSkillInstallResponse {
  request_id: string;
  status: 'safe' | 'reputation_warning' | 'advisory_warning' | 'blocked';
  reputation?: ReputationResult;
  advisories?: AdvisoryMatch[];
  message: string;
}

export interface ConfirmSkillInstallRequest {
  request_id: string;
  acknowledge_reputation?: boolean;
  acknowledge_advisories?: boolean;
}

export interface ConfirmSkillInstallResponse {
  status: 'installed' | 'failed';
  installed_path?: string;
  error?: string;
}

export interface ListSkillsRequest {
  target_group_jid?: string;
}

export interface ListSkillsResponse {
  skills: Array<{
    slug: string;
    version: string;
    installed_at: string;
    path: string;
  }>;
}

export interface RemoveSkillRequest {
  slug: string;
  target_group_jid?: string;
}

export interface RemoveSkillResponse {
  status: 'removed' | 'not_found';
  message: string;
}

// IPC Task Types

export interface IpcSkillInstallRequest {
  type: 'skill_install_request';
  slug: string;
  version?: string;
  target_group_jid?: string;
  user_jid: string;
  group_folder: string;
  timestamp: string;
}

export interface IpcSkillInstallConfirm {
  type: 'skill_install_confirm';
  request_id: string;
  acknowledge_reputation: boolean;
  acknowledge_advisories: boolean;
  user_jid: string;
  group_folder: string;
  timestamp: string;
}

export interface IpcSkillRemove {
  type: 'skill_remove';
  slug: string;
  target_group_jid?: string;
  user_jid: string;
  group_folder: string;
  timestamp: string;
}

// Database Schema

export interface SkillInstallRequestRow {
  request_id: string;
  user_jid: string;
  group_jid: string;
  skill_slug: string;
  skill_version: string | null;
  reputation_score: number;
  reputation_warnings_json: string; // JSON array
  advisories_json: string; // JSON array
  created_at: number;
  expires_at: number;
  status: 'pending' | 'confirmed' | 'expired' | 'cancelled';
  confirmed_at: number | null;
}

export interface InstalledSkillRow {
  slug: string;
  version: string;
  installed_at: string;
  installed_by: string; // user_jid
  path: string;
  metadata_json: string; // SkillMetadata as JSON
}

// Skill Signature Verification Types (Phase 1)

/**
 * IPC request for skill signature verification
 */
export interface VerifySkillSignatureRequest {
  type: 'verify_skill_signature';
  requestId: string;
  groupFolder: string;
  timestamp: string;
  packagePath: string;
  signaturePath: string;
}

/**
 * IPC response for skill signature verification
 */
export interface VerifySkillSignatureResponse {
  success: boolean;
  message: string;
  data?: {
    valid: boolean;
    signer: string;            // 'clawsec' or custom signer identifier
    packageHash: string;       // SHA-256 of package
    verifiedAt: string;        // ISO timestamp
    algorithm: 'Ed25519';
  };
  error?: {
    code: 'SIGNATURE_INVALID' | 'FILE_NOT_FOUND' | 'CRYPTO_ERROR' | 'SERVICE_UNAVAILABLE';
    details?: unknown;
  };
}

/**
 * MCP tool parameters for package verification
 */
export interface VerifySkillPackageParams {
  packagePath: string;
  signaturePath?: string;     // Optional: auto-detects .sig if omitted
}


path: mcp-tools/advisory-tools.ts
content: /* eslint-disable @typescript-eslint/no-explicit-any */
/**
 * ClawSec Advisory Feed MCP Tools for NanoClaw
 *
 * Add these tools to /workspace/project/container/agent-runner/src/ipc-mcp-stdio.ts
 *
 * These tools run in the container context and read from the host-managed
 * advisory cache at /workspace/project/data/clawsec-advisory-cache.json
 */

import fs from 'fs';
import path from 'path';
import { z } from 'zod';
import { evaluateAdvisoryRisk, normalizeExploitabilityScore } from '../lib/risk.js';
import { matchesAffectedSpecifier } from '../lib/advisories.js';

// These variables are provided by the host environment (ipc-mcp-stdio.ts)
// when this code is integrated into the NanoClaw container agent.
declare const server: { tool: (...args: any[]) => void };
declare function writeIpcFile(dir: string, data: any): void;
declare const TASKS_DIR: string;
declare const groupFolder: string;
const CACHE_FILE = '/workspace/project/data/clawsec-advisory-cache.json';

const severityOrder: Record<string, number> = { critical: 0, high: 1, medium: 2, low: 3 };
const exploitabilityOrder: Record<string, number> = { high: 0, medium: 1, low: 2, unknown: 3 };

/**
 * Discover installed skills in a directory
 */
async function discoverInstalledSkills(installRoot: string): Promise<Array<{
  name: string;
  version: string | null;
  dirName: string;
}>> {
  const skills: Array<{ name: string; version: string | null; dirName: string }> = [];

  try {
    const entries = fs.readdirSync(installRoot, { withFileTypes: true });

    for (const entry of entries) {
      if (!entry.isDirectory()) continue;

      const skillJsonPath = path.join(installRoot, entry.name, 'skill.json');
      try {
        const raw = fs.readFileSync(skillJsonPath, 'utf8');
        const parsed = JSON.parse(raw);
        skills.push({
          name: parsed.name || entry.name,
          version: parsed.version || null,
          dirName: entry.name,
        });
      } catch {
        // Skill without skill.json, use directory name
        skills.push({
          name: entry.name,
          version: null,
          dirName: entry.name,
        });
      }
    }
  } catch {
    // Return empty if directory doesn't exist
  }

  return skills;
}

/**
 * Find advisory matches for installed skills
 */
function findAdvisoryMatches(
  advisories: any[],
  skills: Array<{ name: string; version: string | null; dirName: string }>
): Array<{
  advisory: any;
  skill: { name: string; version: string | null; dirName: string };
  matchedAffected: string[];
}> {
  const matches: Array<{
    advisory: any;
    skill: { name: string; version: string | null; dirName: string };
    matchedAffected: string[];
  }> = [];

  for (const advisory of advisories) {
    for (const skill of skills) {
      const matchedAffected: string[] = [];

      for (const affected of advisory.affected || []) {
        if (matchesAffectedSpecifier(affected, skill.name, skill.version, skill.dirName)) {
          matchedAffected.push(affected);
        }
      }

      if (matchedAffected.length > 0) {
        matches.push({ advisory, skill, matchedAffected });
      }
    }
  }

  return matches;
}

// Add these tools to the server:

server.tool(
  'clawsec_check_advisories',
  'Check ClawSec advisory feed for security issues affecting installed skills. Returns list of matching advisories with details. Use this to scan for known vulnerabilities, malicious skills, or deprecated packages.',
  {
    installRoot: z.string().optional().describe('Skills installation directory (default: ~/.claude/skills)'),
    forceRefresh: z.boolean().optional().describe('Force cache refresh before checking (causes 1-2 second delay)'),
  },
  async (args) => {
    // Request cache refresh if needed
    if (args.forceRefresh) {
      writeIpcFile(TASKS_DIR, {
        type: 'refresh_advisory_cache',
        groupFolder,
        timestamp: new Date().toISOString(),
      });
      // Wait for refresh (async, best-effort)
      await new Promise(resolve => setTimeout(resolve, 2000));
    }

    // Read cache from shared mount
    try {
      const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
      const installRoot = args.installRoot || path.join(process.env.HOME || '~', '.claude', 'skills');

      // Discover installed skills
      const skills = await discoverInstalledSkills(installRoot);

      // Find matches
      const matches = findAdvisoryMatches(cacheData.feed.advisories, skills);

      // Calculate cache age
      const cacheAge = Date.now() - Date.parse(cacheData.fetchedAt);
      const cacheAgeMinutes = Math.floor(cacheAge / 60000);

      const result = {
        success: true,
        feedUpdated: cacheData.feed.updated || null,
        totalAdvisories: cacheData.feed.advisories.length,
        installedSkills: skills.length,
        matches: matches.map(m => ({
          advisory: {
            id: m.advisory.id,
            severity: m.advisory.severity,
            type: m.advisory.type,
            title: m.advisory.title,
            description: m.advisory.description,
            action: m.advisory.action,
            published: m.advisory.published,
            exploitability_score: normalizeExploitabilityScore(m.advisory.exploitability_score),
            exploitability_rationale: m.advisory.exploitability_rationale || null,
          },
          skill: m.skill,
          matchedAffected: m.matchedAffected,
        })),
        cacheAge: `${cacheAgeMinutes} minutes`,
        cacheTimestamp: cacheData.fetchedAt,
      };

      return {
        content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
      };
    } catch (error) {
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: false,
            error: `Failed to check advisories: ${error instanceof Error ? error.message : String(error)}`
          }, null, 2)
        }],
        isError: true,
      };
    }
  }
);

server.tool(
  'clawsec_check_skill_safety',
  'Check if a specific skill is safe to install based on ClawSec advisory feed. Returns safety recommendation (install/block/review) with reasons. Use this as a pre-install gate before installing any skill.',
  {
    skillName: z.string().describe('Name of skill to check'),
    skillVersion: z.string().optional().describe('Version of skill (optional, for version-specific checks)'),
  },
  async (args) => {
    try {
      const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));

      // Find matching advisories for this skill
      const matchingAdvisories = cacheData.feed.advisories.filter((advisory: any) =>
        advisory.affected.some((affected: string) => {
          return matchesAffectedSpecifier(affected, args.skillName, args.skillVersion || null);
        })
      );

      if (matchingAdvisories.length === 0) {
        return {
          content: [{
            type: 'text' as const,
            text: JSON.stringify({
              safe: true,
              advisories: [],
              recommendation: 'install',
              reason: 'No known advisories for this skill',
            }, null, 2),
          }],
        };
      }

      const risk = evaluateAdvisoryRisk(matchingAdvisories);

      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            safe: risk.safe,
            advisories: matchingAdvisories.map((a: any) => ({
              id: a.id,
              severity: a.severity,
              type: a.type,
              title: a.title,
              description: a.description,
              action: a.action,
              published: a.published,
              affected: a.affected,
              exploitability_score: normalizeExploitabilityScore(a.exploitability_score),
              exploitability_rationale: a.exploitability_rationale || null,
            })),
            recommendation: risk.recommendation,
            reason: risk.reason,
            skillName: args.skillName,
            skillVersion: args.skillVersion || null,
            advisoryCount: matchingAdvisories.length,
          }, null, 2),
        }],
      };
    } catch (error) {
      // Conservative: block on error
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            safe: false,
            advisories: [],
            recommendation: 'review',
            reason: `Failed to verify safety: ${error instanceof Error ? error.message : String(error)}`,
            error: true,
          }, null, 2),
        }],
      };
    }
  }
);

server.tool(
  'clawsec_list_advisories',
  'List ClawSec advisories with optional filtering. Use this to browse security advisories, filter by severity/type/exploitability, or search for specific affected skills.',
  {
    severity: z.enum(['critical', 'high', 'medium', 'low']).optional().describe('Filter by severity level'),
    type: z.string().optional().describe('Filter by advisory type (for example: vulnerable_skill, malicious_skill, prompt_injection)'),
    exploitabilityScore: z.enum(['high', 'medium', 'low', 'unknown']).optional()
      .describe('Filter by exploitability score'),
    affectedSkill: z.string().optional().describe('Filter by affected skill name (partial match supported)'),
    limit: z.number().optional().describe('Maximum number of results (default: unlimited)'),
  },
  async (args) => {
    try {
      const cacheData = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf8'));
      let advisories = [...cacheData.feed.advisories];

      // Apply filters
      if (args.severity) {
        advisories = advisories.filter((a: any) => a.severity === args.severity);
      }
      if (args.type) {
        const typeFilter = String(args.type).toLowerCase().trim();
        advisories = advisories.filter((a: any) => String(a.type || '').toLowerCase().trim() === typeFilter);
      }
      if (args.exploitabilityScore) {
        advisories = advisories.filter(
          (a: any) => normalizeExploitabilityScore(a.exploitability_score) === args.exploitabilityScore
        );
      }
      if (args.affectedSkill) {
        advisories = advisories.filter((a: any) =>
          a.affected.some((spec: string) => spec.includes(args.affectedSkill!))
        );
      }

      // Sort by exploitability first, then severity, then publish date (newest first).
      advisories.sort((a: any, b: any) => {
        const exploitabilityDiff =
          (exploitabilityOrder[normalizeExploitabilityScore(a.exploitability_score)] ?? 999) -
          (exploitabilityOrder[normalizeExploitabilityScore(b.exploitability_score)] ?? 999);
        if (exploitabilityDiff !== 0) return exploitabilityDiff;

        const severityDiff = (severityOrder[a.severity] || 999) - (severityOrder[b.severity] || 999);
        if (severityDiff !== 0) return severityDiff;
        return (b.published || '').localeCompare(a.published || '');
      });

      // Apply limit
      const originalCount = advisories.length;
      if (args.limit && args.limit > 0) {
        advisories = advisories.slice(0, args.limit);
      }

      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: true,
            feedUpdated: cacheData.feed.updated || null,
            advisories: advisories.map((a: any) => ({
              id: a.id,
              severity: a.severity,
              type: a.type,
              title: a.title,
              description: a.description,
              action: a.action,
              published: a.published,
              affected: a.affected,
              exploitability_score: normalizeExploitabilityScore(a.exploitability_score),
              exploitability_rationale: a.exploitability_rationale || null,
            })),
            total: cacheData.feed.advisories.length,
            filtered: originalCount,
            returned: advisories.length,
            filters: {
              severity: args.severity || null,
              type: args.type || null,
              exploitabilityScore: args.exploitabilityScore || null,
              affectedSkill: args.affectedSkill || null,
              limit: args.limit || null,
            },
          }, null, 2),
        }],
      };
    } catch (error) {
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: false,
            error: `Failed to list advisories: ${error instanceof Error ? error.message : String(error)}`,
          }, null, 2),
        }],
        isError: true,
      };
    }
  }
);

server.tool(
  'clawsec_refresh_cache',
  'Request immediate refresh of the advisory cache from ClawSec feed. This fetches the latest advisories and verifies signatures. Use when you need up-to-date advisory information.',
  {},
  async () => {
    writeIpcFile(TASKS_DIR, {
      type: 'refresh_advisory_cache',
      groupFolder,
      timestamp: new Date().toISOString(),
    });

    return {
      content: [{
        type: 'text' as const,
        text: 'Advisory cache refresh requested. This may take a few seconds. Check status with clawsec_check_advisories.',
      }],
    };
  }
);


path: mcp-tools/integrity-tools.ts
content: /**
 * ClawSec File Integrity Monitoring MCP Tools for NanoClaw
 *
 * Add these tools to /workspace/project/container/agent-runner/src/ipc-mcp-stdio.ts
 *
 * These tools run in the container context and communicate with the host-side
 * integrity monitor via IPC.
 */

import fs from 'fs';
import path from 'path';
import { z } from 'zod';

// These variables are provided by the host environment (ipc-mcp-stdio.ts)
// when this code is integrated into the NanoClaw container agent.
/* eslint-disable @typescript-eslint/no-explicit-any */
declare const server: { tool: (...args: any[]) => void };
declare function writeIpcFile(dir: string, data: any): void;
declare const TASKS_DIR: string;
declare const groupFolder: string;
/* eslint-enable @typescript-eslint/no-explicit-any */

// Result waiting helper
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async function waitForResult(requestId: string, timeoutMs: number = 60000): Promise<any> {
  const resultDir = '/workspace/ipc/clawsec_results';
  const resultPath = path.join(resultDir, `${requestId}.json`);

  const startTime = Date.now();
  while (Date.now() - startTime < timeoutMs) {
    if (fs.existsSync(resultPath)) {
      const result = JSON.parse(fs.readFileSync(resultPath, 'utf-8'));
      fs.unlinkSync(resultPath); // Cleanup
      return result;
    }
    await new Promise(resolve => setTimeout(resolve, 1000)); // Poll every 1s
  }

  throw new Error(`Timeout waiting for result: ${requestId}`);
}

// ============================================================================
// MCP Tool 1: clawsec_check_integrity
// ============================================================================

server.tool(
  'clawsec_check_integrity',
  'Check protected files for unauthorized changes (drift). Automatically restores critical files to approved baselines. Use this for scheduled integrity monitoring or manual security checks.',
  {
    mode: z.enum(['check', 'status']).optional().describe('check=detect drift and restore, status=view baselines only (default: check)'),
    autoRestore: z.boolean().optional().describe('Auto-restore files in restore mode (default: true)'),
  },
  async (args) => {
    const requestId = `integrity-check-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

    // Write IPC request
    writeIpcFile(TASKS_DIR, {
      type: 'integrity_check',
      requestId,
      mode: args.mode || 'check',
      autoRestore: args.autoRestore !== false,
      groupFolder,
      timestamp: new Date().toISOString()
    });

    try {
      // Wait for result
      const result = await waitForResult(requestId, 60000);

      return {
        content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
        isError: !result.success
      };
    } catch (error) {
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: false,
            error: `Integrity check failed: ${error instanceof Error ? error.message : String(error)}`
          }, null, 2)
        }],
        isError: true
      };
    }
  }
);

// ============================================================================
// MCP Tool 2: clawsec_approve_change
// ============================================================================

server.tool(
  'clawsec_approve_change',
  'Approve an intentional file modification as the new approved baseline. Use this after making legitimate changes to protected files (e.g., updating CLAUDE.md or registered_groups.json).',
  {
    path: z.string().describe('Absolute path to file to approve (e.g., /workspace/group/CLAUDE.md)'),
    note: z.string().optional().describe('Optional note explaining why this change is being approved'),
  },
  async (args) => {
    const requestId = `integrity-approve-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

    // Write IPC request
    writeIpcFile(TASKS_DIR, {
      type: 'integrity_approve',
      requestId,
      path: args.path,
      note: args.note || '',
      approvedBy: 'agent', // In production, should be user JID
      groupFolder,
      timestamp: new Date().toISOString()
    });

    try {
      const result = await waitForResult(requestId, 30000);

      return {
        content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
        isError: !result.success
      };
    } catch (error) {
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: false,
            error: `Approve failed: ${error instanceof Error ? error.message : String(error)}`
          }, null, 2)
        }],
        isError: true
      };
    }
  }
);

// ============================================================================
// MCP Tool 3: clawsec_integrity_status
// ============================================================================

server.tool(
  'clawsec_integrity_status',
  'View current baseline status for protected files without checking for drift. Use this to see what files are monitored, when baselines were created, and their current hashes.',
  {
    path: z.string().optional().describe('Optional: specific file path to check. If omitted, shows all protected files.'),
  },
  async (args) => {
    const requestId = `integrity-status-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

    writeIpcFile(TASKS_DIR, {
      type: 'integrity_status',
      requestId,
      path: args.path,
      groupFolder,
      timestamp: new Date().toISOString()
    });

    try {
      const result = await waitForResult(requestId, 30000);

      return {
        content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
        isError: !result.success
      };
    } catch (error) {
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: false,
            error: `Status check failed: ${error instanceof Error ? error.message : String(error)}`
          }, null, 2)
        }],
        isError: true
      };
    }
  }
);

// ============================================================================
// MCP Tool 4: clawsec_verify_audit
// ============================================================================

server.tool(
  'clawsec_verify_audit',
  'Verify the integrity of the audit log hash chain. Use this to detect if the audit log has been tampered with. A valid chain proves all logged events are authentic.',
  {},
  async () => {
    const requestId = `integrity-verify-audit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;

    writeIpcFile(TASKS_DIR, {
      type: 'integrity_verify_audit',
      requestId,
      groupFolder,
      timestamp: new Date().toISOString()
    });

    try {
      const result = await waitForResult(requestId, 30000);

      return {
        content: [{ type: 'text' as const, text: JSON.stringify(result, null, 2) }],
        isError: !result.success
      };
    } catch (error) {
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: false,
            error: `Audit verification failed: ${error instanceof Error ? error.message : String(error)}`
          }, null, 2)
        }],
        isError: true
      };
    }
  }
);

// ============================================================================
// Usage Examples (for documentation)
// ============================================================================

// Usage Examples (for documentation):
//
// Example 1: Scheduled Integrity Check
//
// schedule_task({
//   prompt: 'Check file integrity with clawsec_check_integrity...',
//   schedule_type: 'cron',
//   schedule_value: '0,30 * * * *',  // Every 30 minutes
//   context_mode: 'isolated'
// });
//
// Example 2: Pre-Deployment Check
//
// const check = await tools.clawsec_check_integrity({ mode: 'check', autoRestore: false });
// if (check.drift_detected) { ... }
//
// Example 3: Approve Legitimate Changes
//
// await tools.clawsec_approve_change({
//   path: '/workspace/group/CLAUDE.md',
//   note: 'Updated agent instructions to include new skill'
// });
//
// Example 4: Audit Verification
//
// const audit = await tools.clawsec_verify_audit();
// if (!audit.valid) { ... }


path: mcp-tools/signature-verification.ts
content: /**
 * ClawSec Skill Signature Verification MCP Tool for NanoClaw
 *
 * Add this tool to /workspace/project/container/agent-runner/src/ipc-mcp-stdio.ts
 *
 * This tool verifies Ed25519 signatures on skill packages to prevent supply chain attacks.
 */

/* eslint-disable @typescript-eslint/no-explicit-any */
import fs from 'fs';
import path from 'path';
import { z } from 'zod';

// These variables are provided by the host environment (ipc-mcp-stdio.ts)
// when this code is integrated into the NanoClaw container agent.
declare const server: { tool: (...args: any[]) => void };
declare function writeIpcFile(dir: string, data: any): void;
declare const TASKS_DIR: string;
declare const groupFolder: string;

const ALLOWED_VERIFICATION_ROOTS = [
  '/tmp',
  '/var/tmp',
  '/workspace/ipc',
  '/workspace/project/data',
  '/workspace/project/tmp',
  '/workspace/project/downloads',
] as const;

const ALLOWED_PACKAGE_EXTENSIONS = ['.zip', '.tar', '.tgz', '.tar.gz'] as const;

function isWithinAllowedRoots(filePath: string): boolean {
  return ALLOWED_VERIFICATION_ROOTS.some((root) => filePath === root || filePath.startsWith(`${root}/`));
}

function validatePackagePath(rawPath: string): string {
  if (!path.isAbsolute(rawPath)) {
    throw new Error('packagePath must be absolute');
  }

  const resolved = path.resolve(rawPath);
  if (!isWithinAllowedRoots(resolved)) {
    throw new Error(`packagePath must be under: ${ALLOWED_VERIFICATION_ROOTS.join(', ')}`);
  }

  if (!ALLOWED_PACKAGE_EXTENSIONS.some((ext) => resolved.endsWith(ext))) {
    throw new Error(`packagePath must end with one of: ${ALLOWED_PACKAGE_EXTENSIONS.join(', ')}`);
  }

  return resolved;
}

function validateSignaturePath(rawPath: string): string {
  if (!path.isAbsolute(rawPath)) {
    throw new Error('signaturePath must be absolute');
  }

  const resolved = path.resolve(rawPath);
  if (!isWithinAllowedRoots(resolved)) {
    throw new Error(`signaturePath must be under: ${ALLOWED_VERIFICATION_ROOTS.join(', ')}`);
  }

  if (!resolved.endsWith('.sig')) {
    throw new Error('signaturePath must end with .sig');
  }

  return resolved;
}

// Result waiting helper
async function waitForResult(requestId: string, timeoutMs: number = 5000): Promise<any> {
  const resultDir = '/workspace/ipc/clawsec_results';
  const resultPath = path.join(resultDir, `${requestId}.json`);

  const startTime = Date.now();
  while (Date.now() - startTime < timeoutMs) {
    if (fs.existsSync(resultPath)) {
      const result = JSON.parse(fs.readFileSync(resultPath, 'utf-8'));
      fs.unlinkSync(resultPath); // Cleanup
      return result;
    }
    await new Promise(resolve => setTimeout(resolve, 100)); // Poll every 100ms
  }

  throw new Error(`Timeout waiting for result: ${requestId}`);
}

// ============================================================================
// MCP Tool: clawsec_verify_skill_package
// ============================================================================

server.tool(
  'clawsec_verify_skill_package',
  'Verify Ed25519 signature of a skill package before installation. Prevents installation of tampered or malicious skill packages by checking ClawSec signatures.',
  {
    packagePath: z.string().describe('Absolute path to skill package (.tar.gz or .zip)'),
    signaturePath: z.string().optional().describe('Path to signature file. If omitted, auto-detects <packagePath>.sig'),
  },
  async (args: { packagePath: string; signaturePath?: string }) => {
    const requestId = `verify-signature-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
    let packagePath: string;
    let sigPath: string;

    try {
      packagePath = validatePackagePath(args.packagePath);
      sigPath = validateSignaturePath(args.signaturePath || `${packagePath}.sig`);
    } catch (error) {
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: false,
            valid: false,
            recommendation: 'block',
            error: error instanceof Error ? error.message : String(error),
          }, null, 2)
        }],
        isError: true
      };
    }

    // Validate package file exists
    if (!fs.existsSync(packagePath)) {
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: false,
            valid: false,
            recommendation: 'block',
            error: `Package file not found: ${packagePath}`
          }, null, 2)
        }],
        isError: true
      };
    }

    // Write IPC request to host
    writeIpcFile(TASKS_DIR, {
      type: 'verify_skill_signature',
      requestId,
      groupFolder,
      timestamp: new Date().toISOString(),
      packagePath,
      signaturePath: sigPath,
    });

    try {
      // Wait for host to verify (5 second timeout)
      const result = await waitForResult(requestId, 5000);

      if (!result.success) {
        // Service error or file not found
        return {
          content: [{
            type: 'text' as const,
            text: JSON.stringify({
              success: false,
              valid: false,
              recommendation: 'block',
              packagePath,
              signaturePath: sigPath,
              error: result.message || 'Verification failed',
              reason: result.error?.code || 'UNKNOWN_ERROR'
            }, null, 2)
          }],
          isError: true
        };
      }

      // Check if signature is valid
      if (!result.data?.valid) {
        return {
          content: [{
            type: 'text' as const,
            text: JSON.stringify({
              success: true,
              valid: false,
              recommendation: 'block',
              packagePath,
              signaturePath: sigPath,
              reason: result.data?.error || 'Signature verification failed',
              packageInfo: {
                sha256: result.data?.packageHash || 'unknown'
              }
            }, null, 2)
          }],
        };
      }

      // Signature valid!
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: true,
            valid: true,
            recommendation: 'install',
            packagePath,
            signaturePath: sigPath,
            signer: result.data.signer,
            algorithm: result.data.algorithm,
            verifiedAt: result.data.verifiedAt,
            packageInfo: {
              size: fs.statSync(packagePath).size,
              sha256: result.data.packageHash
            }
          }, null, 2)
        }]
      };
    } catch (error) {
      return {
        content: [{
          type: 'text' as const,
          text: JSON.stringify({
            success: false,
            valid: false,
            recommendation: 'block',
            error: `Verification timeout or error: ${error instanceof Error ? error.message : String(error)}`
          }, null, 2)
        }],
        isError: true
      };
    }
  }
);


path: skill.json
content: {
  "name": "clawsec-nanoclaw",
  "version": "0.0.6",
  "description": "ClawSec security suite for NanoClaw - Advisory feed monitoring, MCP tools for vulnerability checking, and Ed25519 signature verification for containerized WhatsApp bot agents",
  "author": "prompt-security",
  "license": "AGPL-3.0-or-later",
  "homepage": "https://clawsec.prompt.security/",
  "keywords": [
    "security",
    "nanoclaw",
    "whatsapp-bot",
    "mcp-tools",
    "advisory",
    "feed",
    "threat-intel",
    "containers",
    "signature-verification",
    "vulnerability-scanning",
    "agents",
    "ai"
  ],
  "platform": "nanoclaw",
  "sbom": {
    "files": [
      {
        "path": "SKILL.md",
        "required": true,
        "description": "NanoClaw skill documentation"
      },
      {
        "path": "CHANGELOG.md",
        "required": true,
        "description": "Version history and release notes"
      },
      {
        "path": "INSTALL.md",
        "required": true,
        "description": "Installation guide for NanoClaw deployments"
      },
      {
        "path": "mcp-tools/advisory-tools.ts",
        "required": true,
        "description": "MCP tools for advisory checking in container context"
      },
      {
        "path": "host-services/advisory-cache.ts",
        "required": true,
        "description": "Host-side advisory cache manager with periodic feed fetching"
      },
      {
        "path": "host-services/ipc-handlers.ts",
        "required": true,
        "description": "IPC handlers for MCP tool requests"
      },
      {
        "path": "lib/signatures.ts",
        "required": true,
        "description": "Ed25519 signature verification utilities"
      },
      {
        "path": "lib/local_file_io.ts",
        "required": true,
        "description": "Local file access helpers used by signature verification routines"
      },
      {
        "path": "lib/advisories.ts",
        "required": true,
        "description": "Advisory matching and vulnerability detection"
      },
      {
        "path": "lib/types.ts",
        "required": true,
        "description": "TypeScript type definitions"
      },
      {
        "path": "lib/risk.ts",
        "required": true,
        "description": "Shared advisory risk evaluation logic for host and MCP tools"
      },
      {
        "path": "advisories/feed-signing-public.pem",
        "required": true,
        "description": "Pinned Ed25519 public key for feed signature verification"
      },
      {
        "path": "mcp-tools/signature-verification.ts",
        "required": true,
        "description": "Phase 1: MCP tool for skill package signature verification"
      },
      {
        "path": "host-services/skill-signature-handler.ts",
        "required": true,
        "description": "Phase 1: Host-side signature verification service"
      },
      {
        "path": "docs/SKILL_SIGNING.md",
        "required": true,
        "description": "Phase 1: Documentation for skill signing and verification"
      },
      {
        "path": "mcp-tools/integrity-tools.ts",
        "required": true,
        "description": "Phase 2: MCP tools for file integrity monitoring"
      },
      {
        "path": "host-services/integrity-handler.ts",
        "required": true,
        "description": "Phase 2: Host-side integrity monitoring service"
      },
      {
        "path": "guardian/integrity-monitor.ts",
        "required": true,
        "description": "Phase 2: Core file integrity monitoring engine"
      },
      {
        "path": "guardian/policy.json",
        "required": true,
        "description": "Phase 2: NanoClaw-specific file protection policy"
      },
      {
        "path": "docs/INTEGRITY.md",
        "required": true,
        "description": "Phase 2: Documentation for file integrity monitoring"
      }
    ]
  },
  "capabilities": [
    "Advisory feed monitoring from clawsec.prompt.security",
    "MCP tools for agent-initiated vulnerability scans",
    "Exploitability-aware advisory prioritization for agent environments",
    "Pre-installation skill safety checks",
    "Ed25519 signature verification for advisory feeds",
    "Platform metadata preserved in advisory records for downstream filtering",
    "Containerized agent support with IPC communication"
  ],
  "nanoclaw": {
    "mcp_tools": [
      "clawsec_check_advisories",
      "clawsec_check_skill_safety",
      "clawsec_list_advisories",
      "clawsec_refresh_cache",
      "clawsec_verify_skill_package",
      "clawsec_check_integrity",
      "clawsec_approve_change",
      "clawsec_integrity_status",
      "clawsec_verify_audit"
    ],
    "requires": {
      "node": ">=18.0.0",
      "nanoclaw": ">=0.1.0"
    },
    "integration": {
      "mcp_tools_file": "container/agent-runner/src/ipc-mcp-stdio.ts",
      "ipc_handlers_file": "src/ipc.ts",
      "cache_location": "/workspace/project/data/clawsec-advisory-cache.json"
    }
  }
}


path: test/security-hardening.test.mjs
content: import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';

const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const SKILL_ROOT = path.resolve(__dirname, '..');

function readSkillFile(relativePath) {
  return fs.readFileSync(path.join(SKILL_ROOT, relativePath), 'utf8');
}

test('signature verifier enforces pinned key and path policy', () => {
  const source = readSkillFile('host-services/skill-signature-handler.ts');

  assert.ok(!source.includes('publicKeyPem?: string'), 'publicKeyPem override must be removed');
  assert.ok(!source.includes('allowUnsigned?: boolean'), 'allowUnsigned override must be removed');

  assert.ok(source.includes('const ALLOWED_PACKAGE_ROOTS'), 'must define allowed package roots');
  assert.ok(source.includes('validatePackagePath('), 'must validate package path before hashing');
  assert.ok(source.includes('validateSignaturePath('), 'must validate signature path before verification');
});

test('IPC advisory handler does not forward key or unsigned overrides', () => {
  const source = readSkillFile('host-services/ipc-handlers.ts');

  assert.ok(!source.includes('publicKeyPem'), 'IPC handler must not accept publicKeyPem override');
  assert.ok(!source.includes('allowUnsigned'), 'IPC handler must not accept allowUnsigned override');
});

test('MCP signature tool validates filesystem boundaries', () => {
  const source = readSkillFile('mcp-tools/signature-verification.ts');

  assert.ok(source.includes('const ALLOWED_VERIFICATION_ROOTS'), 'must define allowed verification roots');
  assert.ok(source.includes('validatePackagePath('), 'must validate package path in MCP layer');
  assert.ok(source.includes('validateSignaturePath('), 'must validate signature path in MCP layer');
});

test('integrity approvals are restricted to policy targets', () => {
  const source = readSkillFile('guardian/integrity-monitor.ts');

  assert.ok(source.includes('const normalizedFilePath = path.resolve(filePath);'), 'must normalize approved path');
  assert.ok(
    source.includes("if (!target || target.mode === 'ignore')"),
    'must require approved file to exist in non-ignored policy target list'
  );
});

test('integrity targets and baselines use normalized absolute paths', () => {
  const source = readSkillFile('guardian/integrity-monitor.ts');

  assert.ok(source.includes('path: path.resolve(target.path)'), 'resolveTargets must normalize direct target paths');
  assert.ok(source.includes('const normalizedFilePath = path.resolve(filePath);'), 'status/approval lookups must normalize file paths');
  assert.ok(source.includes('normalizedFiles[path.resolve(filePath)] = baseline;'), 'loaded baselines must be normalized to absolute keys');
});


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal:  | SkillSpector: suspicious (CRITICAL)