// Security & data handling
function Security() {
  const shell = useShell();
  const tocItems = [
    { id: 'overview', label: 'Overview' },
    { id: 'isolation', label: 'Tenant isolation' },
    { id: 'auth', label: 'Authentication & keys' },
    { id: 'sandbox', label: 'Sandbox execution' },
    { id: 'data', label: 'Data at rest' },
    { id: 'data-transit', label: 'Data in transit' },
    { id: 'secrets', label: 'Secret handling' },
    { id: 'audit', label: 'Audit trail' },
    { id: 'retention', label: 'Retention & deletion' },
    { id: 'disclosure', label: 'Responsible disclosure' },
  ];

  return (
    <div className="app">
      <Topbar section="docs" theme={shell.theme} setTheme={shell.setTheme}
              onSearch={() => shell.setSearchOpen(true)}
              onMenuToggle={() => shell.setMobileMenuOpen(true)} />
      <div className="main">
        <Sidebar activeId="security"
                 mobileOpen={shell.mobileMenuOpen}
                 onMobileClose={() => shell.setMobileMenuOpen(false)} />
        <article className="content">
          <div className="crumbs">
            <a href="index.html">Docs</a>
            <span className="sep">/</span>
            <span>Security</span>
          </div>

          <div className="eyebrow">Trust · data handling</div>
          <h1 className="h1">Security <em>& data handling.</em></h1>
          <p className="lede">
            How ClawAgen isolates tenants, handles credentials, runs untrusted LLM output,
            and what you control when you self-host. The short version: one folder per tenant,
            one Docker sandbox per shell call, no cross-tenant code paths.
          </p>

          <h2 id="overview" className="h2">Overview</h2>
          <p>
            Security posture depends on who hosts: <strong>self-hosted</strong> (you own the VM and
            disk) or <strong>our managed cloud</strong> (Azure VM operated by us with the same
            codebase). In both, the isolation model is identical — differences are operational
            (who holds the keys, who monitors the VM, who applies patches).
          </p>

          <h2 id="isolation" className="h2">Tenant isolation</h2>
          <p>
            Every tenant is a folder at <code>/data/tenants/&#123;accountId&#125;/</code> with its
            own SQLite file, its own Markdown context, and its own channel credentials. There is
            no cross-tenant query path because the DB handle itself is different — a bug that
            forgot to filter by <code>accountId</code> would reach the wrong database file, not a
            shared table with a missed predicate.
          </p>
          <ul>
            <li><strong>Per-tenant DB handle</strong> — <code>getTenantDb(accountId)</code> opens <code>/data/tenants/&#123;id&#125;/data.db</code>; no shared-tables pool.</li>
            <li><strong>Path checks</strong> — every filesystem op routes through <code>resolveTenantPath()</code>; escape attempts (<code>../..</code>) throw before disk I/O.</li>
            <li><strong>Key scope</strong> — each API key is bound to a single <code>accountId</code> in <code>platform.db</code>; an admin UI rotates or revokes per-tenant.</li>
          </ul>

          <h2 id="auth" className="h2">Authentication & keys</h2>
          <p>
            Every request carries <code>X-API-Key: ck_live_...</code>. The auth middleware hashes
            it and looks it up in <code>platform.db</code>, resolves <code>accountId</code>, and
            injects it into the request context. The key is never logged, never returned in
            responses, and is hashed at rest with SHA-256.
          </p>
          <ul>
            <li><strong>Rotate</strong> by minting a new key in the admin UI and deleting the old one. In-flight requests under the old key complete; new requests fail.</li>
            <li><strong>Revoke</strong> sets <code>revoked_at</code> on the row; auth returns 401 on the next use.</li>
            <li><strong>Scope</strong> today is tenant-wide. Per-agent and per-tool scopes are on the roadmap.</li>
          </ul>

          <h2 id="sandbox" className="h2">Sandbox execution</h2>
          <p>
            The <code>bash</code> and <code>exec</code> tools run shell commands inside an
            ephemeral Docker container, not on the host. Each invocation spawns a fresh
            <code>ai-agents-sandbox:latest</code> container with:
          </p>
          <ul>
            <li><strong>Tenant workspace only.</strong> Only <code>/data/tenants/&#123;id&#125;/</code> is mounted, read/write, at <code>/workspace</code>.</li>
            <li><strong>No network by default.</strong> <code>--network=none</code> unless the tool explicitly opts in.</li>
            <li><strong>Read-only root.</strong> Container filesystem outside <code>/workspace</code> is immutable.</li>
            <li><strong>Timebox.</strong> A hard timeout kills the container; output above a cap is truncated.</li>
            <li><strong>Role-gated.</strong> Only the admin agent sees <code>bash</code> and <code>exec</code>; CS never gets shell access.</li>
          </ul>
          <Callout type="warn" title="Prompt injection is always possible">
            A malicious prompt can still ask the LLM to <em>try</em> destructive commands — the
            sandbox contains the blast radius to that one tenant's workspace. Treat the sandbox as
            a safety net, not a silver bullet; review skills before installing ones you didn't
            write.
          </Callout>

          <h2 id="data" className="h2">Data at rest</h2>
          <ul>
            <li><strong>Primary storage</strong> — the tenant's folder on disk. Markdown files + SQLite + sqlite-vec.</li>
            <li><strong>Disk encryption</strong> — depends on host. Self-host: your choice (LUKS, Azure/AWS/GCP disk encryption). Our cloud: Azure managed-disk encryption at rest.</li>
            <li><strong>No plaintext keys in DB</strong> — tenant API keys are SHA-256 hashed.</li>
            <li><strong>Backups</strong> — see <a className="inline" href="selfhost.html#backup">self-host backups</a>. Default is tar + off-box copy; encrypt the archive if the destination isn't trusted.</li>
          </ul>

          <h2 id="data-transit" className="h2">Data in transit</h2>
          <ul>
            <li><strong>HTTPS everywhere</strong> — the bundled Caddy container fetches Let's Encrypt certs; no plaintext inbound.</li>
            <li><strong>LLM calls</strong> — go to your configured provider over HTTPS. Azure OpenAI, Anthropic, OpenAI, etc. each have their own data-handling policy; pick one whose posture matches your requirements.</li>
            <li><strong>Channel webhooks</strong> — signature-verified before routing (Meta: <code>X-Hub-Signature-256</code>; Telegram: <code>X-Telegram-Bot-Api-Secret-Token</code>).</li>
          </ul>

          <h2 id="secrets" className="h2">Secret handling</h2>
          <ul>
            <li><strong>Env vars</strong> — Azure/OpenAI/embedding keys live in <code>agents/.env</code>, gitignored. Loaded once at process start.</li>
            <li><strong>Per-tenant channel creds</strong> — stored under <code>/data/tenants/&#123;id&#125;/config/channels/*.yaml</code>; not in the main database.</li>
            <li><strong>Logs</strong> — redact known secret shapes (<code>Bearer ...</code>, <code>sk-...</code>, <code>ck_live_...</code>) before pino-pretty writes them.</li>
          </ul>

          <h2 id="audit" className="h2">Audit trail</h2>
          <ul>
            <li><strong>Session history</strong> — every message, tool call, and tool result is persisted to <code>sessions/&#123;id&#125;.json</code> per tenant. Filesystem-readable forever.</li>
            <li><strong>Tool metrics</strong> — <code>GET /api/metrics/tools</code> shows count, duration, and error rate per (tenant, tool).</li>
            <li><strong>Platform auth log</strong> — successful/failed API-key lookups are recorded in <code>platform.db</code>.</li>
          </ul>

          <h2 id="retention" className="h2">Retention & deletion</h2>
          <p>
            There is no automatic retention policy — tenant data lives forever until you delete it.
            Delete by removing the folder or dropping the tenant row in <code>platform.db</code>.
          </p>
          <CodeBlock tabs={[{ label: 'delete-tenant.sh', raw: `# Delete a tenant's data + revoke their keys\nTENANT_ID=5\ndocker compose exec agents bun run scripts/delete-tenant.ts $TENANT_ID\n# Or, raw:\nrm -rf /var/lib/docker/volumes/ai-agents_agents_data/_data/tenants/$TENANT_ID/`,
            code: `<span class="tok-com"># Delete a tenant's data + revoke their keys</span>
<span class="tok-com">$</span> <span class="tok-var">TENANT_ID</span>=5
<span class="tok-com">$</span> docker compose exec agents bun run scripts/delete-tenant.ts $TENANT_ID
<span class="tok-com"># Or, raw:</span>
<span class="tok-com">$</span> rm -rf /var/lib/docker/volumes/ai-agents_agents_data/_data/tenants/$TENANT_ID/` }]} />

          <h2 id="disclosure" className="h2">Responsible disclosure</h2>
          <p>
            Security issues: email <code>security@clawagen.com</code> (or open a private security
            advisory on the <a className="inline" href="https://github.com/carrickcheah/ai-agents/security/advisories" target="_blank" rel="noreferrer">GitHub repo</a>).
            Please do not file public issues for vulnerabilities. We aim to acknowledge within 72
            hours.
          </p>

          <Feedback />
          <PageFoot
            prev={{ label: 'Self-host', href: 'selfhost.html' }}
            next={{ label: 'Errors & rate limits', href: 'errors.html' }}
          />
        </article>
        <TOC items={tocItems} />
      </div>
      <SearchOverlay open={shell.searchOpen} onClose={() => shell.setSearchOpen(false)} />
      <TweaksPanel visible={shell.tweaksVisible} theme={shell.theme} setTheme={shell.setTheme} />
    </div>
  );
}
ReactDOM.createRoot(document.getElementById('root')).render(<Security />);
