Skill: masterworrall/solid-agent-storage (version 0.3.9)

--- name: solid-agent-storage description: Give your AI agent persistent identity (WebID) and personal data storage (Pod) using the Solid Protocol version: 0.3.9 author: Interition license: Apache-2.0 metadata: openclaw: requires: env: - INTERITION_PASSPHRASE - SOLID_SERVER_URL bins: - node - curl - jq primaryEnv: INTERITION_PASSPHRASE install: - kind: brew formula: jq bins: [jq] categories: - storage - identity - data homepage: https://github.com/masterworrall/agent-interition --- # Solid Agent Storage This Skill gives you a **Solid Pod** — a personal data store with a **WebID** (your identity on the web). You can store data, read it back, and share specific resources with other agents. ## When to Use This Skill - You need to **remember something** across conversations (notes, preferences, learned facts) - You need to **store structured data** (RDF/Turtle format for linked data, or any content type) - You need to **share data** with another agent who also has a Pod - You need a **persistent identity** that other agents or services can verify ## Setup Before using any commands, set the `INTERITION_PASSPHRASE` environment variable. This passphrase encrypts the Solid server credentials stored on your device (under `~/.interition/agents/`), protecting them at rest in your OpenClaw environment. Use a strong passphrase and keep it secret. ### Running your own Solid server (recommended) For full control, run your own [Community Solid Server](https://github.com/CommunitySolidServer/CommunitySolidServer) (CSS). The CSS is open-source software maintained by the [Open Data Institute](https://www.theodi.org/). Set `SOLID_SERVER_URL` to your server's URL: ```bash export SOLID_SERVER_URL="http://localhost:3000" ``` See the [source repository](https://github.com/masterworrall/agent-interition) for a hardened Docker configuration you can use as a starting point. ### Using a shared Solid server (for multi-agent collaboration) If your agents need to collaborate with other agents using the Solid Protocol, they need to share the same Solid server. Two public CSS instances are available: - **https://solidcommunity.net** — operated by the [Open Data Institute](https://www.theodi.org/) - **https://crawlout.io** — operated by [Interition](https://interition.net), the provider of this Skill To use a shared server, set `SOLID_SERVER_URL`: ```bash export SOLID_SERVER_URL="https://crawlout.io" ``` If `SOLID_SERVER_URL` is not set, the Skill defaults to `https://crawlout.io`. ## How Credentials Work The Solid server issues credentials to prevent anyone other than your agent from accessing its services and data. This is the same trust model as any remote service that requires users to identify themselves and prove they are authorised. When you provision an agent: 1. The CSS **creates** an account, WebID, and Pod for your agent 2. The CSS **issues** client credentials (ID and secret) that your agent uses to authenticate 3. Your agent uses those credentials to obtain **time-limited Bearer tokens** for reading and writing data 4. The CSS **enforces access control** (WAC) on every request — only authorised agents can access protected resources `INTERITION_PASSPHRASE` is **not** a server credential. It seeds the encryption (AES-256-GCM) of the server-issued credentials stored locally on your device (under `~/.interition/agents/` with `0600` permissions), making them less vulnerable in your OpenClaw environment. The server never sees your passphrase. ## How It Works This Skill provides **three management scripts** for CSS-specific operations (provisioning, deprovisioning, status) plus a **token helper** for authentication. All standard Solid operations (read, write, delete, share) are done with **curl and a Bearer token** — your Pod is a standard W3C Solid server. For broader guidance on using Solid servers and the wider Solid ecosystem, refer to [solidproject.org](https://solidproject.org). The full protocol specification is at [Solid Protocol (W3C)](https://solidproject.org/TR/protocol). ### Two-Step Workflow **Step 1:** Get a token: ```bash scripts/get-token.sh --agent <name> ``` Output: ```json {"token": "eyJhbG...", "expiresIn": 600, "serverUrl": "https://crawlout.io", "podUrl": "https://crawlout.io/example-agent/", "webId": "https://crawlout.io/example-agent/profile/card#me"} ``` **Step 2:** Use curl with `Authorization: Bearer $TOKEN` for any Solid operation. ### Token Expiry Tokens last **600 seconds** (10 minutes). If more than **8 minutes** have elapsed since your last `get-token.sh` call, fetch a new token before making requests. ## Quick Reference Extract token and URLs: ```bash TOKEN_JSON=$(scripts/get-token.sh --agent example-agent) [REDACTED_SECRET] "$TOKEN_JSON" | jq -r '.token') POD_URL=$(echo "$TOKEN_JSON" | jq -r '.podUrl') ``` **Read a resource:** ```bash curl -s -H "Authorization: Bearer $TOKEN" "${POD_URL}memory/notes.ttl" ``` **Write a resource:** ```bash curl -s -X PUT \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: text/turtle" \ --data-raw '@prefix schema: <http://schema.org/>. <#note-1> a schema:Note; schema:text "Important finding"; schema:dateCreated "2024-01-15".' \ "${POD_URL}memory/notes.ttl" ``` **Delete a resource:** ```bash curl -s -X DELETE -H "Authorization: Bearer $TOKEN" "${POD_URL}memory/old.ttl" ``` For the full set of operations (containers, PATCH, access control, public access), see `references/solid-http-reference.md`. ## Management Commands ### Provision Identity and Storage Creates a WebID and Pod for an agent. Run this once per unique agent name. ```bash scripts/provision.sh --name <agent-name> [--displayName <display-name>] ``` **Example:** ```bash scripts/provision.sh --name example-agent --displayName "Example Agent" ``` **Output:** ```json {"status": "ok", "agent": "example-agent", "webId": "https://crawlout.io/example-agent/profile/card#me", "podUrl": "https://crawlout.io/example-agent/"} ``` ### Deprovision Identity and Storage Fully removes an agent's WebID and Pod: deletes its pods, client credentials, WebID links, and password logins from the CSS server, then deletes local credential files. ```bash scripts/deprovision.sh --name <agent-name> ``` **Example:** ```bash scripts/deprovision.sh --name example-agent ``` **Output (success):** ```json {"status": "ok", "agent": "example-agent", "accountDeleted": true, "credentialsDeleted": true} ``` **Output (partial — e.g. server unreachable):** ```json {"status": "partial", "agent": "example-agent", "accountDeleted": false, "credentialsDeleted": true, "warnings": ["Could not delete CSS account: ..."]} ``` - `status: "ok"` — CSS account fully dismantled and local files deleted - `status: "partial"` — local files deleted but CSS cleanup failed (see warnings) If the agent was provisioned before email/password storage was added, CSS cleanup is skipped and a warning explains why. ### Check Status Lists all provisioned agents and their details. ```bash scripts/status.sh ``` ## Pod Structure Each agent's Pod has these containers: | Path | Purpose | |------|---------| | `/{name}/memory/` | Private agent memory (notes, learned facts, preferences) | | `/{name}/shared/` | Resources intended for sharing with other agents | | `/{name}/conversations/` | Conversation logs and context | ## Turtle Templates When storing structured data, use Turtle (RDF) format. Here are templates for common patterns: ### A note or memory ```turtle @prefix schema: <http://schema.org/>. <#note-1> a schema:Note; schema:text "The content of the note"; schema:dateCreated "2024-01-15". ``` ### An agent preference ```turtle @prefix foaf: <http://xmlns.com/foaf/0.1/>. @prefix schema: <http://schema.org/>. <#pref-1> a schema:PropertyValue; schema:name "response-style"; schema:value "concise". ``` ### A shared dataset ```turtle @prefix schema: <http://schema.org/>. <#dataset-1> a schema:Dataset; schema:name "Research Results"; schema:description "Findings from the analysis task"; schema:dateModified "2024-01-15". ``` ## Error Handling All management commands output JSON. On error, stderr will contain: ```json {"error": "description of what went wrong"} ``` Common errors: - `"No passphrase provided"` — Set `INTERITION_PASSPHRASE` env var - `"No credentials found"` — Run `provision.sh` first - `"Invalid passphrase"` — Wrong `INTERITION_PASSPHRASE` value - `"Token request failed: 401"` — Credentials expired; re-provision the agent's WebID and Pod - `"HTTP 404"` — Resource doesn't exist at that URL

path: SECURITY.md
content: # Security Manifest — solid-agent-storage

## Network Access

- **Default target**: `https://crawlout.io` (Interition's hosted Solid server)
- **Configurable**: The Skill contacts whatever server is set in `SOLID_SERVER_URL`. If you point it at a remote server, credentials and tokens will be exchanged with that server. **Only use a server you control and trust.**
- **No other network calls**: The Skill does not contact analytics endpoints, third-party APIs, or any server other than the configured Solid server
- **No telemetry**: Nothing is reported back to Interition or any other party

## File System Scope

- **Credentials**: `~/.interition/agents/{name}/credentials.enc` — encrypted agent credentials
- **No other files**: The Skill does not read or write files outside `~/.interition/`

## Credentials Handling

- **Encrypted at rest**: AES-256-GCM encryption with PBKDF2 key derivation (100,000 iterations, SHA-256, random 32-byte salt per file)
- **Passphrase-protected**: Requires `INTERITION_PASSPHRASE` environment variable
- **File permissions**: Credentials files are set to `0600` (owner read/write only) as defence in depth
- **Never logged**: Credentials, tokens, and passphrases are never written to stdout or log files
- **Session-only key**: The decryption key exists only in process memory for the duration of a single command

## Encryption Details

| Parameter | Value |
|-----------|-------|
| Algorithm | AES-256-GCM |
| Key derivation | PBKDF2 |
| PBKDF2 iterations | 100,000 |
| PBKDF2 hash | SHA-256 |
| Salt length | 32 bytes (random per file) |
| IV length | 16 bytes (random per encryption) |
| Auth tag | 16 bytes (GCM authentication tag) |

## Process Execution

- **Only spawns**: `node` (to run compiled TypeScript CLI commands)
- **No shell expansion**: Shell scripts use `set -euo pipefail` and pass arguments safely via `"$@"`
- **No eval or dynamic code**: No `eval()`, `Function()`, or dynamic `import()` in any code path

## Dependencies

- **Zero runtime dependencies**: Uses only Node.js built-in modules (`crypto`, `fs`, `path`, `os`)
- **Build-time only**: TypeScript compiler and Vitest (test framework) are devDependencies


path: dist/auth/client-credentials.d.ts
content: /**
 * Gets an authenticated fetch function using CSS client credentials.
 *
 * CSS v7 flow:
 * 1. POST /.oidc/token with Basic auth (id:secret) and grant_type=client_credentials
 * 2. Receive a Bearer access_token (JWT, 600s expiry)
 * 3. Use Authorization: Bearer <token> on subsequent requests
 */
export declare function getAuthenticatedFetch(serverUrl: string, id: string, [REDACTED_SECRET]): Promise<typeof fetch>;


path: dist/auth/client-credentials.js
content: /**
 * Gets an authenticated fetch function using CSS client credentials.
 *
 * CSS v7 flow:
 * 1. POST /.oidc/token with Basic auth (id:secret) and grant_type=client_credentials
 * 2. Receive a Bearer access_token (JWT, 600s expiry)
 * 3. Use Authorization: Bearer <token> on subsequent requests
 */
export async function getAuthenticatedFetch(serverUrl, id, secret) {
    const tokenUrl = `${serverUrl}/.oidc/token`;
    const authString = Buffer.from(`${id}:${secret}`).toString('base64');
    let token = null;
    let tokenExpiry = 0;
    async function refreshToken() {
        const res = await fetch(tokenUrl, {
            method: 'POST',
            headers: {
                authorization: `Basic ${authString}`,
                'content-type': 'application/x-www-form-urlencoded',
            },
            body: new URLSearchParams({
                grant_type: 'client_credentials',
                scope: 'webid',
            }),
        });
        if (!res.ok) {
            throw new Error(`Failed to get access [REDACTED_SECRET] ${await res.text()}`);
        }
        const json = await res.json();
        [REDACTED_SECRET];
        // Refresh 30s before expiry
        tokenExpiry = Date.now() + (json.expires_in - 30) * 1000;
        return token;
    }
    async function getToken() {
        if (!token || Date.now() >= tokenExpiry) {
            return refreshToken();
        }
        return token;
    }
    return async (input, init) => {
        const accessToken = await getToken();
        const headers = new Headers(init?.headers);
        headers.set('authorization', `Bearer ${accessToken}`);
        return fetch(input, {
            ...init,
            headers,
        });
    };
}
//# sourceMappingURL=client-credentials.js.map

path: dist/auth/index.d.ts
content: export { getAuthenticatedFetch } from './client-credentials.js';


path: dist/auth/index.js
content: export { getAuthenticatedFetch } from './client-credentials.js';
//# sourceMappingURL=index.js.map

path: dist/bootstrap/agent-provisioner.d.ts
content: import { AgentConfig, ProvisionedAgent } from './types.js';
/**
 * Full agent provisioning flow:
 * 1. Create CSS account
 * 2. Add password login
 * 3. Create pod at /agents/{name}/
 * 4. Create client credentials
 * 5. Patch WebID profile with agent metadata
 * 6. Create standard container structure
 */
export declare function provisionAgent(config: AgentConfig): Promise<ProvisionedAgent>;


path: dist/bootstrap/agent-provisioner.js
content: import { createAccount, addPasswordLogin, createPod, createClientCredentials } from './css-client.js';
import { buildProfilePatch } from './webid-profile.js';
import { createPodContainers } from './pod-structure.js';
import { getAuthenticatedFetch } from '../auth/client-credentials.js';
/**
 * Full agent provisioning flow:
 * 1. Create CSS account
 * 2. Add password login
 * 3. Create pod at /agents/{name}/
 * 4. Create client credentials
 * 5. Patch WebID profile with agent metadata
 * 6. Create standard container structure
 */
export async function provisionAgent(config) {
    const { serverUrl, name, displayName } = config;
    console.log(`[bootstrap] Creating account for agent "${name}"...`);
    const account = await createAccount(serverUrl);
    const email = `${name}@agents.interition.local`;
    const password = `agent-${name}-${Date.now()}`;
    console.log(`[bootstrap] Adding password login...`);
    await addPasswordLogin(serverUrl, account.cookie, email, password);
    console.log(`[bootstrap] Creating pod at /agents/${name}/...`);
    const { pod: podUrl, webId } = await createPod(serverUrl, account.cookie, name);
    console.log(`[bootstrap] Creating client credentials...`);
    const creds = await createClientCredentials(serverUrl, account.cookie, name, webId);
    console.log(`[bootstrap] Patching WebID profile with agent metadata...`);
    const authFetch = await getAuthenticatedFetch(serverUrl, creds.id, creds.secret);
    const profileUrl = webId.replace(/#me$/, '');
    const patchBody = buildProfilePatch(config, webId);
    const patchRes = await authFetch(profileUrl, {
        method: 'PATCH',
        headers: { 'content-type': 'application/sparql-update' },
        body: patchBody,
    });
    if (!patchRes.ok) {
        console.warn(`[bootstrap] Warning: WebID profile patch returned ${patchRes.status}: ${await patchRes.text()}`);
    }
    console.log(`[bootstrap] Creating pod container structure...`);
    await createPodContainers(podUrl, authFetch);
    console.log(`[bootstrap] Agent "${name}" provisioned successfully!`);
    console.log(`  WebID: ${webId}`);
    console.log(`  Pod:   ${podUrl}`);
    return {
        webId,
        podUrl,
        email,
        password,
        clientCredentials: { id: creds.id, [REDACTED_SECRET] },
        config,
    };
}
//# sourceMappingURL=agent-provisioner.js.map

path: dist/bootstrap/css-client.d.ts
content: import { CssAccount } from './types.js';
/**
 * Low-level HTTP client for the CSS v7 Account API.
 *
 * CSS v7 flow:
 * 1. POST /.account/account/ → creates account, returns cookie + controls
 * 2. POST controls.password.create → adds email/password login
 * 3. POST controls.account.pod → creates pod, returns pod URL + WebID
 * 4. POST controls.account.clientCredentials → creates client credentials
 */
export declare function createAccount(serverUrl: string): Promise<CssAccount>;
export declare function addPasswordLogin(serverUrl: string, cookie: string, email: string, [REDACTED_SECRET]): Promise<void>;
export declare function createPod(serverUrl: string, cookie: string, name: string): Promise<{
    pod: string;
    webId: string;
}>;
export declare function createClientCredentials(serverUrl: string, cookie: string, name: string, webId: string): Promise<{
    id: string;
    [REDACTED_SECRET];
}>;
export declare function loginWithPassword(serverUrl: string, email: string, [REDACTED_SECRET]): Promise<string>;
/**
 * Fully dismantles a CSS account by deleting all its components.
 *
 * CSS v7 has no single "delete account" endpoint. Instead, you delete
 * each component individually via resource URLs returned by GET on the
 * list endpoints:
 *   1. DELETE each client credential
 *   2. DELETE each pod
 *   3. DELETE each WebID link
 *   4. DELETE each password login
 *   5. POST logout to invalidate the session
 */
export declare function deleteAccount(serverUrl: string, cookie: string): Promise<void>;


path: dist/bootstrap/css-client.js
content: /**
 * Low-level HTTP client for the CSS v7 Account API.
 *
 * CSS v7 flow:
 * 1. POST /.account/account/ → creates account, returns cookie + controls
 * 2. POST controls.password.create → adds email/password login
 * 3. POST controls.account.pod → creates pod, returns pod URL + WebID
 * 4. POST controls.account.clientCredentials → creates client credentials
 */
export async function createAccount(serverUrl) {
    const res = await fetch(`${serverUrl}/.account/account/`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: '{}',
    });
    if (!res.ok) {
        throw new Error(`Failed to create account: ${res.status} ${await res.text()}`);
    }
    const cookie = res.headers.get('set-cookie');
    if (!cookie) {
        throw new Error('No cookie returned from account creation');
    }
    const json = await res.json();
    return { cookie, accountUrl: `${serverUrl}/.account/` };
}
export async function addPasswordLogin(serverUrl, cookie, email, password) {
    const controls = await getControls(serverUrl, cookie);
    const passwordUrl = controls.password?.create;
    if (!passwordUrl) {
        throw new Error('Password creation endpoint not found in controls');
    }
    const res = await fetch(passwordUrl, {
        method: 'POST',
        headers: {
            'content-type': 'application/json',
            cookie,
        },
        body: JSON.stringify({ email, password }),
    });
    if (!res.ok) {
        throw new Error(`Failed to add password login: ${res.status} ${await res.text()}`);
    }
}
export async function createPod(serverUrl, cookie, name) {
    const controls = await getControls(serverUrl, cookie);
    const podUrl = controls.account?.pod;
    if (!podUrl) {
        throw new Error('Pod creation endpoint not found in controls');
    }
    const res = await fetch(podUrl, {
        method: 'POST',
        headers: {
            'content-type': 'application/json',
            cookie,
        },
        body: JSON.stringify({ name }),
    });
    if (!res.ok) {
        throw new Error(`Failed to create pod: ${res.status} ${await res.text()}`);
    }
    const json = await res.json();
    return { pod: json.pod, webId: json.webId };
}
export async function createClientCredentials(serverUrl, cookie, name, webId) {
    const controls = await getControls(serverUrl, cookie);
    const credUrl = controls.account?.clientCredentials;
    if (!credUrl) {
        throw new Error('Client credentials endpoint not found in controls');
    }
    const res = await fetch(credUrl, {
        method: 'POST',
        headers: {
            'content-type': 'application/json',
            cookie,
        },
        body: JSON.stringify({ name, webId }),
    });
    if (!res.ok) {
        throw new Error(`Failed to create client credentials: ${res.status} ${await res.text()}`);
    }
    const json = await res.json();
    return { id: json.id, [REDACTED_SECRET] };
}
export async function loginWithPassword(serverUrl, email, password) {
    const res = await fetch(`${serverUrl}/.account/login/password/`, {
        method: 'POST',
        headers: { 'content-type': 'application/json' },
        body: JSON.stringify({ email, password }),
    });
    if (!res.ok) {
        throw new Error(`Failed to login: ${res.status} ${await res.text()}`);
    }
    const cookie = res.headers.get('set-cookie');
    if (!cookie) {
        throw new Error('No cookie returned from login');
    }
    return cookie;
}
/**
 * Fully dismantles a CSS account by deleting all its components.
 *
 * CSS v7 has no single "delete account" endpoint. Instead, you delete
 * each component individually via resource URLs returned by GET on the
 * list endpoints:
 *   1. DELETE each client credential
 *   2. DELETE each pod
 *   3. DELETE each WebID link
 *   4. DELETE each password login
 *   5. POST logout to invalidate the session
 */
export async function deleteAccount(serverUrl, cookie) {
    const controls = await getControls(serverUrl, cookie);
    // 1. Delete all client credentials
    if (controls.account?.clientCredentials) {
        await deleteAllResources(controls.account.clientCredentials, cookie);
    }
    // 2. Delete all pods
    if (controls.account?.pod) {
        await deleteAllResources(controls.account.pod, cookie);
    }
    // 3. Unlink all WebIDs
    if (controls.account?.webId) {
        await deleteAllResources(controls.account.webId, cookie);
    }
    // 4. Delete all password logins
    if (controls.password?.create) {
        await deleteAllResources(controls.password.create, cookie);
    }
    // 5. Delete the account itself
    if (controls.account?.account) {
        const res = await fetch(controls.account.account, {
            method: 'DELETE',
            headers: { cookie },
        });
        if (!res.ok) {
            throw new Error(`Failed to delete account: ${res.status} ${await res.text()}`);
        }
    }
}
/**
 * GET a list endpoint to discover individual resource URLs, then DELETE each one.
 * CSS list endpoints return objects like { "id1": "http://.../resource/id1", "id2": "http://.../resource/id2" }.
 */
async function deleteAllResources(listUrl, cookie) {
    const res = await fetch(listUrl, { headers: { cookie } });
    if (!res.ok)
        return;
    const json = await res.json();
    for (const deleteUrl of Object.values(json)) {
        if (typeof deleteUrl === 'string' && deleteUrl.startsWith('http')) {
            const delRes = await fetch(deleteUrl, { method: 'DELETE', headers: { cookie } });
            if (!delRes.ok) {
                const body = await delRes.text();
                throw new Error(`Failed to delete ${deleteUrl}: ${delRes.status} ${body}`);
            }
        }
    }
}
async function getControls(serverUrl, cookie) {
    const res = await fetch(`${serverUrl}/.account/`, {
        headers: { cookie },
    });
    if (!res.ok) {
        throw new Error(`Failed to get account controls: ${res.status}`);
    }
    const json = await res.json();
    return json.controls ?? json;
}
//# sourceMappingURL=css-client.js.map

path: dist/bootstrap/index.d.ts
content: export { provisionAgent } from './agent-provisioner.js';
export type { AgentConfig, ProvisionedAgent } from './types.js';


path: dist/bootstrap/index.js
content: export { provisionAgent } from './agent-provisioner.js';
//# sourceMappingURL=index.js.map

path: dist/bootstrap/pod-structure.d.ts
content: /**
 * Creates the standard container structure inside an agent's pod.
 * Containers in Solid are created by PUT-ing with the right content type.
 */
export declare function createPodContainers(podUrl: string, authFetch: typeof fetch): Promise<void>;


path: dist/bootstrap/pod-structure.js
content: /**
 * Creates the standard container structure inside an agent's pod.
 * Containers in Solid are created by PUT-ing with the right content type.
 */
export async function createPodContainers(podUrl, authFetch) {
    const containers = ['memory/', 'shared/', 'conversations/'];
    for (const container of containers) {
        const url = new URL(container, podUrl).href;
        const res = await authFetch(url, {
            method: 'PUT',
            headers: {
                'content-type': 'text/turtle',
                'if-none-match': '*',
                link: '<http://www.w3.org/ns/ldp#BasicContainer>; rel="type"',
            },
            body: '',
        });
        // 201 Created or 409 Conflict (already exists) are both fine
        if (!res.ok && res.status !== 409) {
            throw new Error(`Failed to create container ${container}: ${res.status} ${await res.text()}`);
        }
    }
}
//# sourceMappingURL=pod-structure.js.map

path: dist/bootstrap/types.d.ts
content: export interface AgentConfig {
    name: string;
    displayName: string;
    serverUrl: string;
    capabilities?: string[];
}
export interface ProvisionedAgent {
    webId: string;
    podUrl: string;
    email: string;
    [REDACTED_SECRET];
    clientCredentials: {
        id: string;
        [REDACTED_SECRET];
    };
    config: AgentConfig;
}
export interface CssAccount {
    cookie: string;
    accountUrl: string;
}


path: dist/bootstrap/types.js
content: export {};
//# sourceMappingURL=types.js.map

path: dist/bootstrap/webid-profile.d.ts
content: import { AgentConfig } from './types.js';
/**
 * Generate a SPARQL UPDATE to patch agent metadata into a WebID profile.
 * CSS auto-generates a basic profile; we add agent-specific triples.
 */
export declare function buildProfilePatch(config: AgentConfig, webId: string): string;


path: dist/bootstrap/webid-profile.js
content: /**
 * Generate a SPARQL UPDATE to patch agent metadata into a WebID profile.
 * CSS auto-generates a basic profile; we add agent-specific triples.
 */
export function buildProfilePatch(config, webId) {
    const prefixes = [
        'PREFIX foaf: <http://xmlns.com/foaf/0.1/>',
        'PREFIX solid: <http://www.w3.org/ns/solid/terms#>',
        'PREFIX schema: <http://schema.org/>',
        'PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>',
        'PREFIX interition: <https://vocab.interition.org/agents#>',
    ].join('\n');
    const me = `<${webId}>`;
    const inserts = [
        `${me} a interition:Agent .`,
        `${me} a foaf:Agent .`,
        `${me} foaf:name "${escapeString(config.displayName)}" .`,
        `${me} rdfs:label "${escapeString(config.displayName)}" .`,
        `${me} schema:name "${escapeString(config.displayName)}" .`,
        `${me} interition:agentName "${escapeString(config.name)}" .`,
    ];
    if (config.capabilities?.length) {
        for (const cap of config.capabilities) {
            inserts.push(`${me} interition:capability "${escapeString(cap)}" .`);
        }
    }
    return `${prefixes}

INSERT DATA {
  ${inserts.join('\n  ')}
}`;
}
function escapeString(s) {
    return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}
//# sourceMappingURL=webid-profile.js.map

path: dist/cli/args.d.ts
content: export declare function getArg(name: string): string | undefined;
export declare function requireArg(name: string, usage: string): string;
export declare function getServerUrl(): string;
export declare function getPassphrase(): string;


path: dist/cli/args.js
content: const argv = process.argv.slice(2);
export function getArg(name) {
    const idx = argv.indexOf(`--${name}`);
    return idx >= 0 ? argv[idx + 1] : undefined;
}
export function requireArg(name, usage) {
    const value = getArg(name);
    if (!value) {
        console.error(usage);
        process.exit(1);
    }
    return value;
}
export function getServerUrl() {
    return process.env.SOLID_SERVER_URL ?? getArg('serverUrl') ?? 'https://crawlout.io';
}
export function getPassphrase() {
    const passphrase = process.env.INTERITION_PASSPHRASE;
    if (!passphrase) {
        console.error(JSON.stringify({
            error: 'No passphrase provided. Set INTERITION_PASSPHRASE environment variable.',
        }));
        process.exit(1);
    }
    return passphrase;
}
//# sourceMappingURL=args.js.map

path: dist/cli/credentials-store.d.ts
content: export interface StoredCredentials {
    webId: string;
    podUrl: string;
    id: string;
    [REDACTED_SECRET];
    email?: string;
    password?: string;
}
export declare function initStore(passphrase: string): void;
export declare function isStoreInitialised(): boolean;
export declare function saveCredentials(name: string, credentials: StoredCredentials): void;
export declare function loadCredentials(name: string): StoredCredentials;
export declare function deleteAgentCredentials(name: string): void;
export declare function listAgents(): string[];


path: dist/cli/credentials-store.js
content: import { randomBytes, createCipheriv, createDecipheriv, pbkdf2Sync } from 'node:crypto';
import { mkdirSync, writeFileSync, readFileSync, readdirSync, chmodSync, existsSync, rmSync } from 'node:fs';
import { join } from 'node:path';
import { homedir } from 'node:os';
const ALGORITHM = 'aes-256-gcm';
const KEY_LENGTH = 32;
const PBKDF2_ITERATIONS = 100_000;
const SALT_LENGTH = 32;
const IV_LENGTH = 16;
let encryptionKey = null;
function getStoreDir() {
    return join(homedir(), '.interition', 'agents');
}
function deriveKey(passphrase, salt) {
    return pbkdf2Sync(passphrase, salt, PBKDF2_ITERATIONS, KEY_LENGTH, 'sha256');
}
export function initStore(passphrase) {
    // Derive a key from passphrase with a fixed salt for session use.
    // Each file has its own random salt for actual encryption.
    // We store the passphrase-derived material to re-derive per-file keys.
    // Actually, we just hold the passphrase and derive per-file.
    // Simpler: hold passphrase in closure. But we need it in module scope.
    // Store passphrase hash as the "session key" indicator.
    encryptionKey = Buffer.from(passphrase, 'utf-8');
}
export function isStoreInitialised() {
    return encryptionKey !== null;
}
function getPassphrase() {
    if (!encryptionKey) {
        throw new Error('Credentials store not initialised. Call initStore(passphrase) first.');
    }
    return encryptionKey;
}
function encrypt(data, passphrase) {
    const salt = randomBytes(SALT_LENGTH);
    const key = deriveKey(passphrase.toString('utf-8'), salt);
    const iv = randomBytes(IV_LENGTH);
    const cipher = createCipheriv(ALGORITHM, key, iv);
    const encrypted = Buffer.concat([cipher.update(data, 'utf-8'), cipher.final()]);
    const tag = cipher.getAuthTag();
    return {
        salt: salt.toString('base64'),
        iv: iv.toString('base64'),
        tag: tag.toString('base64'),
        data: encrypted.toString('base64'),
    };
}
function decrypt(payload, passphrase) {
    const salt = Buffer.from(payload.salt, 'base64');
    const key = deriveKey(passphrase.toString('utf-8'), salt);
    const iv = Buffer.from(payload.iv, 'base64');
    const tag = Buffer.from(payload.tag, 'base64');
    const data = Buffer.from(payload.data, 'base64');
    const decipher = createDecipheriv(ALGORITHM, key, iv);
    decipher.setAuthTag(tag);
    try {
        const decrypted = Buffer.concat([decipher.update(data), decipher.final()]);
        return decrypted.toString('utf-8');
    }
    catch {
        throw new Error('Invalid passphrase — cannot decrypt credentials');
    }
}
export function saveCredentials(name, credentials) {
    const passphrase = getPassphrase();
    const agentDir = join(getStoreDir(), name);
    mkdirSync(agentDir, { recursive: true });
    const payload = encrypt(JSON.stringify(credentials), passphrase);
    const filePath = join(agentDir, 'credentials.enc');
    writeFileSync(filePath, JSON.stringify(payload, null, 2), 'utf-8');
    // Defence in depth: restrict file permissions (owner only)
    try {
        chmodSync(filePath, 0o600);
    }
    catch {
        // chmod may fail on some platforms (e.g., Windows) — non-fatal
    }
}
export function loadCredentials(name) {
    const passphrase = getPassphrase();
    const filePath = join(getStoreDir(), name, 'credentials.enc');
    if (!existsSync(filePath)) {
        throw new Error(`No credentials found for agent "${name}". Run provision first.`);
    }
    const raw = readFileSync(filePath, 'utf-8');
    const payload = JSON.parse(raw);
    const decrypted = decrypt(payload, passphrase);
    return JSON.parse(decrypted);
}
export function deleteAgentCredentials(name) {
    const agentDir = join(getStoreDir(), name);
    if (existsSync(agentDir)) {
        rmSync(agentDir, { recursive: true, force: true });
    }
}
export function listAgents() {
    const dir = getStoreDir();
    if (!existsSync(dir))
        return [];
    return readdirSync(dir, { withFileTypes: true })
        .filter((d) => d.isDirectory())
        .map((d) => d.name);
}
//# sourceMappingURL=credentials-store.js.map

path: dist/cli/deprovision.d.ts
content: export {};


path: dist/cli/deprovision.js
content: import { loginWithPassword, deleteAccount } from '../bootstrap/css-client.js';
import { initStore, loadCredentials, deleteAgentCredentials } from './credentials-store.js';
import { requireArg, getServerUrl, getPassphrase } from './args.js';
const name = requireArg('name', 'Usage: deprovision --name <agent-name>');
const serverUrl = getServerUrl();
initStore(getPassphrase());
async function deprovision() {
    const warnings = [];
    let accountDeleted = false;
    // 1. Load credentials
    const creds = loadCredentials(name);
    // 2. Attempt full CSS account deletion if email+password are available
    if (creds.email && creds.password) {
        try {
            const cookie = await loginWithPassword(serverUrl, creds.email, creds.password);
            await deleteAccount(serverUrl, cookie);
            accountDeleted = true;
        }
        catch (err) {
            warnings.push(`Could not delete CSS account: ${err.message}`);
        }
    }
    else {
        warnings.push('No email/password stored (legacy provision). '
            + 'CSS account was not touched — remove it manually via the server admin.');
    }
    // 3. Delete local credential files
    deleteAgentCredentials(name);
    const result = {
        status: accountDeleted ? 'ok' : 'partial',
        agent: name,
        accountDeleted,
        credentialsDeleted: true,
    };
    if (warnings.length > 0) {
        result.warnings = warnings;
    }
    return result;
}
deprovision()
    .then((result) => {
    console.log(JSON.stringify(result));
})
    .catch((err) => {
    console.error(JSON.stringify({ error: String(err.message ?? err) }));
    process.exit(1);
});
//# sourceMappingURL=deprovision.js.map

path: dist/cli/get-token.d.ts
content: export {};


path: dist/cli/get-token.js
content: import { initStore, loadCredentials } from './credentials-store.js';
import { requireArg, getServerUrl, getPassphrase } from './args.js';
const agent = requireArg('agent', 'Usage: get-token --agent <name>');
const serverUrl = getServerUrl();
initStore(getPassphrase());
(async () => {
    const creds = loadCredentials(agent);
    const tokenUrl = `${serverUrl}/.oidc/token`;
    const authString = Buffer.from(`${creds.id}:${creds.secret}`).toString('base64');
    const res = await fetch(tokenUrl, {
        method: 'POST',
        headers: {
            authorization: `Basic ${authString}`,
            'content-type': 'application/x-www-form-urlencoded',
        },
        body: new URLSearchParams({
            grant_type: 'client_credentials',
            scope: 'webid',
        }),
    });
    if (!res.ok) {
        console.error(JSON.stringify({ error: `Token request failed: ${res.status} ${await res.text()}` }));
        process.exit(1);
    }
    const json = await res.json();
    console.log(JSON.stringify({
        [REDACTED_SECRET],
        expiresIn: json.expires_in,
        serverUrl,
        podUrl: creds.podUrl,
        webId: creds.webId,
    }));
})().catch((err) => {
    console.error(JSON.stringify({ error: String(err.message ?? err) }));
    process.exit(1);
});
//# sourceMappingURL=get-token.js.map

path: dist/cli/provision.d.ts
content: export {};


path: dist/cli/provision.js
content: import { provisionAgent } from '../bootstrap/agent-provisioner.js';
import { initStore, saveCredentials } from './credentials-store.js';
import { requireArg, getArg, getServerUrl, getPassphrase } from './args.js';
const name = requireArg('name', 'Usage: provision --name <name> [--displayName <name>]');
const displayName = getArg('displayName') ?? name;
const serverUrl = getServerUrl();
initStore(getPassphrase());
provisionAgent({ name, displayName, serverUrl })
    .then((agent) => {
    saveCredentials(name, {
        webId: agent.webId,
        podUrl: agent.podUrl,
        id: agent.clientCredentials.id,
        [REDACTED_SECRET],
        email: agent.email,
        [REDACTED_SECRET],
    });
    console.log(JSON.stringify({
        status: 'ok',
        agent: name,
        webId: agent.webId,
        podUrl: agent.podUrl,
    }));
})
    .catch((err) => {
    console.error(JSON.stringify({ error: String(err.message ?? err) }));
    process.exit(1);
});
//# sourceMappingURL=provision.js.map

path: dist/cli/status.d.ts
content: export {};


path: dist/cli/status.js
content: import { initStore, listAgents, loadCredentials } from './credentials-store.js';
import { getPassphrase } from './args.js';
initStore(getPassphrase());
const agents = listAgents();
const result = agents.map((name) => {
    try {
        const creds = loadCredentials(name);
        return { name, webId: creds.webId, podUrl: creds.podUrl };
    }
    catch {
        return { name, error: 'Could not decrypt credentials' };
    }
});
console.log(JSON.stringify({ status: 'ok', agents: result }, null, 2));
//# sourceMappingURL=status.js.map

path: dist/index.d.ts
content: export { provisionAgent, type AgentConfig, type ProvisionedAgent } from './bootstrap/index.js';
export { getAuthenticatedFetch } from './auth/index.js';
export { grantAccess, revokeAccess, type AccessMode } from './sharing/index.js';


path: dist/index.js
content: export { provisionAgent } from './bootstrap/index.js';
export { getAuthenticatedFetch } from './auth/index.js';
export { grantAccess, revokeAccess } from './sharing/index.js';
//# sourceMappingURL=index.js.map

path: references/solid-http-reference.md
content: # Solid HTTP Reference

Concrete curl examples for all standard Solid Protocol operations. All examples use these shell variables — set them from the output of `scripts/get-token.sh`:

```bash
[REDACTED_SECRET]"
POD_URL="https://crawlout.io/example-agent/"
WEBID="https://crawlout.io/example-agent/profile/card#me"
SERVER_URL="https://crawlout.io"
```

## Authentication

Get a Bearer token:

```bash
TOKEN_JSON=$(scripts/get-token.sh --agent example-agent)
[REDACTED_SECRET] "$TOKEN_JSON" | jq -r '.token')
POD_URL=$(echo "$TOKEN_JSON" | jq -r '.podUrl')
WEBID=$(echo "$TOKEN_JSON" | jq -r '.webId')
```

**Token expiry:** Tokens last 600 seconds (10 minutes). Re-fetch if more than 8 minutes have elapsed since the last `get-token.sh` call.

## Read a Resource

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "${POD_URL}memory/notes.ttl"
```

The server returns the resource body with the appropriate `Content-Type` header.

## Write / Replace a Resource

Use PUT to create or overwrite a resource. Always set the correct `Content-Type`.

**Turtle (RDF):**
```bash
curl -s -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/turtle" \
  --data-raw '@prefix schema: <http://schema.org/>.
<#note-1> a schema:Note;
  schema:text "User prefers bullet-point summaries";
  schema:dateCreated "2024-01-15".' \
  "${POD_URL}memory/notes.ttl"
```

**Plain text:**
```bash
curl -s -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/plain" \
  -d "Key finding: the API rate limit is 100 req/min" \
  "${POD_URL}memory/summary.txt"
```

**JSON:**
```bash
curl -s -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"key": "value"}' \
  "${POD_URL}memory/data.json"
```

## Delete a Resource

```bash
curl -s -X DELETE \
  -H "Authorization: Bearer $TOKEN" \
  "${POD_URL}memory/old-notes.ttl"
```

Returns 200 or 204 on success. 404 if the resource doesn't exist.

## Create a Container

Containers are like directories. Their URLs **must** end with `/`.

```bash
curl -s -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/turtle" \
  -H 'Link: <http://www.w3.org/ns/ldp#BasicContainer>; rel="type"' \
  -d '' \
  "${POD_URL}memory/projects/"
```

## List Container Contents

GET a container URL to see its contents (returned as Turtle with `ldp:contains` triples):

```bash
curl -s -H "Authorization: Bearer $TOKEN" \
  "${POD_URL}memory/"
```

The response includes `ldp:contains` entries for each resource in the container.

## Append Data (SPARQL Update — INSERT)

Use PATCH with `application/sparql-update` to add triples without replacing the whole resource:

```bash
curl -s -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/sparql-update" \
  -d 'PREFIX schema: <http://schema.org/>
INSERT DATA {
  <#note-2> a schema:Note;
    schema:text "New finding from today";
    schema:dateCreated "2024-06-15".
}' \
  "${POD_URL}memory/notes.ttl"
```

## Modify Data (SPARQL Update — DELETE/INSERT)

Replace specific triples while leaving the rest intact:

```bash
curl -s -X PATCH \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/sparql-update" \
  -d 'PREFIX schema: <http://schema.org/>
DELETE {
  <#note-1> schema:text "User prefers bullet-point summaries".
}
INSERT {
  <#note-1> schema:text "User prefers numbered lists".
}
WHERE {
  <#note-1> schema:text "User prefers bullet-point summaries".
}' \
  "${POD_URL}memory/notes.ttl"
```

## Access Control (WAC)

Solid uses **Web Access Control (WAC)**. Each resource can have an associated `.acl` resource that defines who can do what.

### Discover the ACL URL

```bash
curl -s -I -H "Authorization: Bearer $TOKEN" \
  "${POD_URL}shared/report.ttl" | grep -i 'link.*rel="acl"'
```

The `Link` header contains the ACL URL, e.g.:
```
Link: <https://crawlout.io/example-agent/shared/report.ttl.acl>; rel="acl"
```

If no ACL link is returned, the convention is to append `.acl` to the resource URL.

### Read Existing ACL

```bash
ACL_URL="${POD_URL}shared/report.ttl.acl"
curl -s -H "Authorization: Bearer $TOKEN" "$ACL_URL"
```

Returns 404 if no ACL has been set (resource inherits from parent container).

### Grant Access to Another Agent

Write a complete ACL document. **Always include an owner rule** so you don't lock yourself out.

```bash
ACL_URL="${POD_URL}shared/report.ttl.acl"
GRANTEE_WEBID="https://crawlout.io/example-collaborator/profile/card#me"
RESOURCE_URL="${POD_URL}shared/report.ttl"

curl -s -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/turtle" \
  --data-raw "@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.

<#owner>
    a acl:Authorization;
    acl:agent <${WEBID}>;
    acl:accessTo <${RESOURCE_URL}>;
    acl:mode acl:Read, acl:Write, acl:Control.

<#grantee>
    a acl:Authorization;
    acl:agent <${GRANTEE_WEBID}>;
    acl:accessTo <${RESOURCE_URL}>;
    acl:mode acl:Read." \
  "$ACL_URL"
```

**Valid modes:** `acl:Read`, `acl:Write`, `acl:Append`, `acl:Control`

### Revoke Access

To revoke a specific agent's access, PUT a new ACL document that omits their rule (keep the owner rule):

```bash
curl -s -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/turtle" \
  --data-raw "@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.

<#owner>
    a acl:Authorization;
    acl:agent <${WEBID}>;
    acl:accessTo <${RESOURCE_URL}>;
    acl:mode acl:Read, acl:Write, acl:Control." \
  "$ACL_URL"
```

### Grant Public Read Access

Allow anyone (unauthenticated) to read a resource:

```bash
curl -s -X PUT \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: text/turtle" \
  --data-raw "@prefix acl: <http://www.w3.org/ns/auth/acl#>.
@prefix foaf: <http://xmlns.com/foaf/0.1/>.

<#owner>
    a acl:Authorization;
    acl:agent <${WEBID}>;
    acl:accessTo <${RESOURCE_URL}>;
    acl:mode acl:Read, acl:Write, acl:Control.

<#public>
    a acl:Authorization;
    acl:agentClass foaf:Agent;
    acl:accessTo <${RESOURCE_URL}>;
    acl:mode acl:Read." \
  "$ACL_URL"
```

## Tips

- **Use `--data-raw` instead of `-d` for Turtle content.** Curl's `-d` flag treats a leading `@` as a file path reference. Since Turtle content starts with `@prefix`, using `-d` sends empty content. `--data-raw` sends the string literally.
- **Trailing slashes matter:** Container URLs end with `/`, resource URLs don't.
- **Content-Type is required** on PUT and PATCH — the server rejects requests without it.
- **Check HTTP status codes:** 200/201/204 = success, 401 = expired token, 403 = no access, 404 = not found, 409 = conflict.
- **Turtle syntax:** Prefixes with `@prefix`, URIs in `<angle-brackets>`, statements end with `.`


path: references/solid-primer.md
content: # Solid Protocol Primer

This document explains the core Solid concepts you need to understand when using this Skill.

## What is Solid?

Solid (Social Linked Data) is a W3C specification that lets people and agents store their data in personal online datastores called **Pods**. The key idea: you control your data, not the application.

## Key Concepts

### Pod

A Pod is a personal data store — think of it as a private web server for your data. Each agent gets its own Pod with a URL like `http://localhost:3000/myagent/`.

Inside a Pod, data is organized into **containers** (like folders) and **resources** (like files):

```
/myagent/
├── profile/card          ← Your WebID profile document
├── memory/               ← Container for private data
│   ├── notes.ttl
│   └── preferences.ttl
├── shared/               ← Container for shared data
│   └── report.ttl
└── conversations/        ← Container for conversation logs
```

### WebID

A WebID is a URL that uniquely identifies an agent. It looks like:

```
http://localhost:3000/myagent/profile/card#me
```

The `#me` fragment points to the specific entity described in the profile document. When another agent or server needs to verify who you are, they dereference your WebID to read your profile.

### Turtle (RDF)

Turtle is the primary data format for Solid. It's a way to express linked data as subject-predicate-object triples:

```turtle
@prefix foaf: <http://xmlns.com/foaf/0.1/>.

<#me> a foaf:Agent;
  foaf:name "My Agent".
```

This says: "The entity `#me` is a `foaf:Agent` and its name is 'My Agent'."

You can also store non-RDF data (plain text, JSON, etc.) — Solid doesn't require Turtle for everything.

### Web Access Control (WAC)

WAC is how Solid handles permissions. Each resource can have an Access Control List (ACL) that specifies who can do what:

- **Read** — View the resource
- **Write** — Modify or replace the resource
- **Append** — Add to the resource without modifying existing content
- **Control** — Manage the ACL itself

ACL rules are expressed in Turtle:

```turtle
@prefix acl: <http://www.w3.org/ns/auth/acl#>.

<#rule1>
    a acl:Authorization;
    acl:agent <http://localhost:3000/other/profile/card#me>;
    acl:accessTo <http://localhost:3000/myagent/shared/report.ttl>;
    acl:mode acl:Read.
```

This grants the agent `other` read access to `report.ttl`.

### Linked Data Platform (LDP)

Solid builds on LDP, a W3C standard for read-write linked data on the web. Key operations:

- **GET** a resource → read its contents
- **PUT** a resource → create or replace it
- **DELETE** a resource → remove it
- **POST** to a container → create a new resource inside it

All of these are standard HTTP operations, which is why Solid works with any HTTP client.

## Common Prefixes

When writing Turtle data, these prefixes are commonly used:

| Prefix | URI | Used for |
|--------|-----|----------|
| `foaf:` | `http://xmlns.com/foaf/0.1/` | People, agents, names |
| `schema:` | `http://schema.org/` | General-purpose metadata |
| `acl:` | `http://www.w3.org/ns/auth/acl#` | Access control |
| `solid:` | `http://www.w3.org/ns/solid/terms#` | Solid-specific terms |
| `ldp:` | `http://www.w3.org/ns/ldp#` | Linked Data Platform |


path: references/troubleshooting.md
content: # Troubleshooting

## Common Errors

### "No passphrase provided"

**Cause:** The `INTERITION_PASSPHRASE` environment variable is not set.

**Fix:**
```bash
export INTERITION_PASSPHRASE="your-passphrase-here"
```

### "No credentials found for agent X"

**Cause:** No WebID and Pod have been provisioned for this agent, or credentials are stored under a different name.

**Fix:**
1. Check existing agents: `scripts/status.sh`
2. Provision a WebID and Pod: `scripts/provision.sh --name <agent-name>`

### "Invalid passphrase — cannot decrypt credentials"

**Cause:** The `INTERITION_PASSPHRASE` value doesn't match the one used when the agent was provisioned.

**Fix:** Use the same passphrase that was set when `provision.sh` was first run. If you've lost the passphrase, you'll need to re-provision the agent's WebID and Pod (which creates new identity and storage).

### "Token request failed: 401"

**Cause:** The client credentials stored for this agent are invalid or the CSS server has been reset.

**Fix:** Re-provision the agent's WebID and Pod with `scripts/provision.sh --name <agent-name>`.

### Expired token (HTTP 401 on curl requests)

**Cause:** The Bearer token has expired. Tokens last 600 seconds (10 minutes).

**Fix:** Run `scripts/get-token.sh --agent <name>` to get a fresh token. As a rule of thumb, re-fetch if more than 8 minutes have elapsed since your last token request.

### "HTTP 404 Not Found"

**Cause:** The resource doesn't exist at that URL.

**Fix:**
- Check the URL is correct (including trailing slashes for containers)
- Write data to the URL first before reading it
- Container URLs must end with `/`

### "HTTP 409 Conflict"

**Cause:** Trying to create a resource that already exists, or a naming conflict.

**Fix:** Use PUT to overwrite, or choose a different URL.

### "ECONNREFUSED"

**Cause:** The Solid server is not running.

**Fix:**
1. Start the server: `docker-compose up -d`
2. Verify it's running: `curl http://localhost:3000/`
3. Check `SOLID_SERVER_URL` matches the server address

## Data Format Issues

### Turtle syntax errors

If writing Turtle data and getting 400 errors, check:
- Prefixes are declared with `@prefix`
- Statements end with `.`
- URIs are wrapped in `<angle-brackets>`
- Strings are wrapped in `"double-quotes"`

### Content-Type missing or wrong on curl requests

The server rejects PUT and PATCH requests without a `Content-Type` header. Always include one:
- Turtle: `-H "Content-Type: text/turtle"`
- Plain text: `-H "Content-Type: text/plain"`
- JSON: `-H "Content-Type: application/json"`
- JSON-LD: `-H "Content-Type: application/ld+json"`
- SPARQL Update: `-H "Content-Type: application/sparql-update"`

### Missing trailing slash on container URLs

Container URLs **must** end with `/`. If you GET or PUT to a container path without a trailing slash, you'll get unexpected results (404 or creating a resource instead of a container).

```bash
# Wrong — this is a resource URL
curl -s -H "Authorization: Bearer $TOKEN" "${POD_URL}memory"

# Correct — this is a container URL
curl -s -H "Authorization: Bearer $TOKEN" "${POD_URL}memory/"
```


path: scripts/deprovision.sh
content: #!/bin/sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
node "$SCRIPT_DIR/../dist/cli/deprovision.js" "$@"


path: scripts/get-token.sh
content: #!/bin/sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
node "$SCRIPT_DIR/../dist/cli/get-token.js" "$@"


path: scripts/provision.sh
content: #!/bin/sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
node "$SCRIPT_DIR/../dist/cli/provision.js" "$@"


path: scripts/status.sh
content: #!/bin/sh
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
node "$SCRIPT_DIR/../dist/cli/status.js" "$@"


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