Merge pull request 'feature/0.12.0' (#120) from feature/0.12.0 into main

Reviewed-on: #120
This commit is contained in:
clawdie 2026-06-23 13:39:50 +02:00
commit bcab969ef7
37 changed files with 7592 additions and 40 deletions

View file

@ -1083,6 +1083,41 @@ install_zot_agent() {
# Stage an on-image NVIDIA pkg repo (all branches) so clawdie_live_gpu can
# `pkg install` the detected branch at boot (NVIDIA_UNIVERSAL lane).
#
# FreeBSD-build-host step (authored on Linux; runs + must be validated on
# FreeBSD). Verify on the build host: (1) the `pkg fetch -o` layout matches what
# `pkg repo` expects, (2) the dependency closure is complete for offline boot
# Build the Astro docs site and stage it as static HTML on the ISO.
# When ISO_VERSION is set, the docs carry a version badge matching the image.
# Skips silently if node/npm are unavailable.
build_and_stage_docs() {
local _docs_src="${SCRIPT_DIR}/docs/website"
local _docs_out="${MOUNT_POINT}/usr/local/share/clawdie-iso/docs"
if [ ! -d "${_docs_src}" ]; then
echo " Docs source not found at ${_docs_src} — skipping"
return 0
fi
if ! command -v node >/dev/null 2>&1 || ! command -v npm >/dev/null 2>&1; then
echo " Node/npm not available — skipping docs build"
return 0
fi
echo " Building docs (ISO ${ISO_VERSION})..."
(
cd "${_docs_src}"
npm ci --silent 2>&1 || { echo " WARN: npm ci failed, skipping docs"; exit 0; }
ASTRO_ISO_VERSION="${ISO_VERSION}" \
ASTRO_SITE_URL="file:///usr/local/share/clawdie-iso/docs" \
npm run build --silent 2>&1 || { echo " WARN: astro build failed, skipping docs"; exit 0; }
)
if [ -d "${_docs_src}/dist" ]; then
mkdir -p "${_docs_out}"
cp -a "${_docs_src}/dist/." "${_docs_out}/"
echo " Docs staged at /usr/local/share/clawdie-iso/docs/ (version ${ISO_VERSION})"
fi
}
# FreeBSD-build-host step (authored on Linux; runs + must be validated on
# FreeBSD). Verify on the build host: (1) the `pkg fetch -o` layout matches what
# `pkg repo` expects, (2) the dependency closure is complete for offline boot
@ -1288,6 +1323,9 @@ branch, commit, modified state, ISO version, and build channel at image build ti
clawdie-ai (TypeScript) is no longer included — it is being phased out in favor
of the colibri (Rust) control plane. The `clawdie` name is retained as the brand
and bare-metal installer identity.
hermes-bsd (Python) is the agent CLI and gateway — powers the Telegram bot,
agent-to-agent relay, and the operator's primary chat interface.
EOF
seed_live_ai_source_repo "${SCRIPT_DIR}" "clawdie-iso"
seed_live_ai_source_repo "${_resolved_colibri_repo}" "colibri"
@ -1295,6 +1333,9 @@ EOF
# just Colibri (Rust). Skipped automatically if the zot checkout is absent.
resolve_zot_paths
seed_live_ai_source_repo "${_resolved_zot_repo}" "zot"
# Hermes Agent source — the agent CLI + gateway for Telegram bot and
# agent-to-agent relay. Skipped silently if the checkout is absent.
seed_live_ai_source_repo "${HERMES_REPO:-/home/clawdie/ai/hermes-bsd}" "hermes-bsd"
chroot "${MOUNT_POINT}" chown -R clawdie:clawdie /home/clawdie/ai
}
@ -2477,6 +2518,7 @@ configure_live_operator_session
install_colibri_service
install_zot_agent
install_nvidia_universal_repo
build_and_stage_docs
# Copy payload
# Rebuild payload paths from scratch inside the reusable work image. A failed

View file

@ -0,0 +1,28 @@
import { defineConfig, passthroughImageService } from 'astro/config';
import sitemap from '@astrojs/sitemap';
const site = process.env.ASTRO_SITE_URL || 'https://clawdie.si';
const outDir = process.env.ASTRO_OUT_DIR || './dist';
export default defineConfig({
site,
outDir,
output: 'static',
trailingSlash: 'always',
image: {
service: passthroughImageService(),
},
i18n: {
defaultLocale: 'en',
locales: ['en', 'sl'],
routing: { prefixDefaultLocale: true, redirectToDefaultLocale: false },
},
integrations: [
sitemap({
i18n: {
defaultLocale: 'en',
locales: { en: 'en', sl: 'sl' },
},
}),
],
});

6581
docs/website/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

23
docs/website/package.json Normal file
View file

@ -0,0 +1,23 @@
{
"name": "clawdie-si",
"private": true,
"version": "0.0.1",
"type": "module",
"scripts": {
"dev": "astro dev --host 0.0.0.0",
"start": "astro dev --host 0.0.0.0",
"build": "astro build",
"predeploy": "npm run build",
"deploy": "node scripts/deploy.mjs",
"preview": "astro preview --host 0.0.0.0",
"astro": "astro"
},
"dependencies": {
"@astrojs/sitemap": "^3.2.1",
"astro": "^5.16.11"
},
"devDependencies": {
"@astrojs/check": "^0.9.6",
"typescript": "^5.9.3"
}
}

View file

@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Clawdie mark">
<rect width="64" height="64" rx="14" fill="#0d1117"/>
<path d="M32 11 53 50H11L32 11Z" fill="none" stroke="#00b4d8" stroke-width="3" stroke-linejoin="round"/>
<path d="M21.5 30.5 L42.5 30.5 L32 50 Z" fill="none" stroke="#00b4d8" stroke-width="3" stroke-linejoin="round"/>
</svg>

After

Width:  |  Height:  |  Size: 383 B

View file

@ -0,0 +1,22 @@
import fs from 'node:fs';
import path from 'node:path';
import { spawnSync } from 'node:child_process';
const localRoot = process.cwd();
const distDir = path.join(localRoot, 'dist');
const webroot = process.env.CMS_WEBROOT || '/usr/local/www/clawdie-si';
if (!fs.existsSync(distDir)) {
console.error(`Build output not found: ${distDir}`);
process.exit(1);
}
const result = spawnSync(
'rsync',
['-av', '--delete', `${distDir}/`, `${webroot}/`],
{ stdio: 'inherit', env: process.env },
);
if (result.status !== 0) {
process.exit(result.status ?? 1);
}

View file

@ -0,0 +1,83 @@
---
import { getCollection, render } from 'astro:content';
import type { Locale } from '../i18n';
import { t } from '../i18n';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const strings = t(locale);
const all = await getCollection('landing', ({ id }) => id.startsWith(`${locale}/`));
const bySection = Object.fromEntries(all.map((e) => [e.data.section, e]));
const heroEntry = bySection['hero'];
const aboutEntry = bySection['about'];
const archEntry = bySection['architecture'];
const ctaEntry = bySection['install-cta'];
const Hero = heroEntry ? (await render(heroEntry)).Content : null;
const About = aboutEntry ? (await render(aboutEntry)).Content : null;
const Arch = archEntry ? (await render(archEntry)).Content : null;
const Cta = ctaEntry ? (await render(ctaEntry)).Content : null;
// Only set during ISO builds — shows the exact image version.
const isoVersion = import.meta.env.ASTRO_ISO_VERSION || null;
const isoTagUrl = isoVersion
? `https://code.smilepowered.org/clawdie/clawdie-iso/releases/tag/v${isoVersion}`
: null;
---
<header class="hero">
<div class="header-inner">
<div class="brand-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="64" height="64" role="img" aria-label="Clawdie"><path d="M32 11 53 50H11L32 11Z" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round"/><path d="M21.5 30.5 L42.5 30.5 L32 50 Z" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round"/></svg></div>
<div class="header-text">
<h1>Clawdie<br /><span>{locale === 'sl' ? 'AI v vaših rokah' : 'AI you own'}</span></h1>
<p class="tagline">{strings.meta.description}</p>
</div>
</div>
</header>
<div class="hero-statement">
{Hero && <Hero />}
</div>
{
About && (
<section class="landing-section" id="about">
<h2>{aboutEntry?.data.title}</h2>
<About />
</section>
)
}
{
Arch && (
<section class="landing-section" id="architecture">
<h2>{archEntry?.data.title}</h2>
<Arch />
</section>
)
}
{
Cta && (
<section class="landing-section" id="install">
<h2>{ctaEntry?.data.title}</h2>
<Cta />
</section>
)
}
{
isoVersion && (
<footer class="iso-version-badge">
<p>
Clawdie ISO {isoVersion} —{' '}
<a href={isoTagUrl}>release notes</a>
</p>
</footer>
)
}

View file

@ -0,0 +1,34 @@
---
import type { Locale } from '../i18n';
import { locales, t } from '../i18n';
interface Props {
current: Locale;
}
const { current } = Astro.props;
const pathname = Astro.url.pathname;
const prefix = `/${current}`;
const rest = pathname.startsWith(prefix)
? pathname.slice(prefix.length) || '/'
: pathname;
const labelFor: Record<Locale, string> = {
en: 'EN',
sl: 'SI',
};
---
<nav class="lang-switch" aria-label={t(current).switcher.label}>
{
locales.map((loc) => {
const href = `/${loc}${rest.startsWith('/') ? rest : `/${rest}`}`;
return (
<a href={href} class={loc === current ? 'active' : ''} lang={loc} hreflang={loc}>
{labelFor[loc]}
</a>
);
})
}
</nav>

View file

@ -0,0 +1,31 @@
---
import type { Locale } from '../i18n';
import { t } from '../i18n';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const s = t(locale).banner;
---
<div id="operator-banner" class="operator-banner" hidden>
<div class="operator-banner-text">
<strong>{s.heading}</strong> {s.body}
</div>
<div class="operator-banner-actions">
<a href="/controlplane/">{s.openControlplane}</a>
<a href="https://docs.clawdie.si/">{s.readDocs}</a>
<a href="https://docs.clawdie.si/operate/public-domain/">{s.claimDomain}</a>
</div>
</div>
<script is:inline>
(function () {
if (typeof window === 'undefined') return;
if (window.location.hostname === 'clawdie.si') return;
var el = document.getElementById('operator-banner');
if (el) el.hidden = false;
})();
</script>

View file

@ -0,0 +1,13 @@
import { defineCollection, z } from 'astro:content';
import { glob } from 'astro/loaders';
const landing = defineCollection({
loader: glob({ base: './src/content/landing', pattern: '**/*.md' }),
schema: z.object({
title: z.string(),
order: z.number(),
section: z.enum(['hero', 'about', 'architecture', 'install-cta']),
}),
});
export const collections = { landing };

View file

@ -0,0 +1,16 @@
---
title: What is Clawdie
order: 2
section: about
---
Clawdie is a **self-hosted AI control plane** for operators who refuse to rent
their compute, their data, or their users' privacy.
You install it on your own hardware — a workstation, a colocated box, a basement
server. It provisions FreeBSD jails, runs your models, and gives every tenant on
your machine a clean slice of the platform: their own database, their own site,
their own backups.
No telemetry leaks upstream. No model weights walk out the door. No vendor
decides what your assistant is allowed to say tomorrow.

View file

@ -0,0 +1,16 @@
---
title: Architecture
order: 3
section: architecture
---
A Clawdie install has three layers:
- **Host** — FreeBSD with ZFS, nginx, Postgres, jail manager.
- **Control plane** — the operator's command surface; provisions tenants, runs
the assistant, ships the publish report.
- **Tenants** — isolated jails, each with its own site, database, and backup
policy. Tenants don't see each other; the operator sees everything.
Everything is reproducible. The same `setup` command that builds your machine
today will build the same machine on a fresh box tomorrow.

View file

@ -0,0 +1,9 @@
---
title: Hero
order: 1
section: hero
---
**Sovereign AI is an engineering problem, not a regulatory one.**
The hardware exists. The models exist. The know-how exists.
**The only thing missing is the decision to install it yourself.**

View file

@ -0,0 +1,12 @@
---
title: Install
order: 4
section: install-cta
---
Boot the ISO, answer four questions, walk away.
By the time you come back, your machine is provisioned, your assistant is
online, and your first tenant site is reachable.
Read the [installation guide](https://docs.clawdie.si/install/) or jump
straight to the [ISO download](https://docs.clawdie.si/install/iso/).

View file

@ -0,0 +1,10 @@
---
title: Kaj je Clawdie
order: 2
section: about
---
Clawdie je **samogosotvana AI nadzorna ravan** za operaterje, ki nočejo najemati
svoje računske moči, svojih podatkov ali zasebnosti svojih uporabnikov.
<!-- TODO: full SL translation pending Crowdin wiring -->

View file

@ -0,0 +1,9 @@
---
title: Arhitektura
order: 3
section: architecture
---
Namestitev Clawdie ima tri plasti: gostitelj, nadzorna ravan, najemniki.
<!-- TODO: full SL translation pending Crowdin wiring -->

View file

@ -0,0 +1,11 @@
---
title: Hero
order: 1
section: hero
---
**Suverena AI je inženirski problem, ne regulativni.**
Strojna oprema obstaja. Modeli obstajajo. Znanje obstaja.
**Edino, kar manjka, je odločitev, da to namestite sami.**
<!-- TODO: full SL translation pending Crowdin wiring -->

View file

@ -0,0 +1,11 @@
---
title: Namestitev
order: 4
section: install-cta
---
Zaženite ISO, odgovorite na štiri vprašanja, odidite.
Preberite [vodič za namestitev](https://docs.clawdie.si/install/).
<!-- TODO: full SL translation pending Crowdin wiring -->

View file

@ -0,0 +1,32 @@
import type { Strings } from './types';
export const en: Strings = {
meta: {
title: 'Clawdie — Sovereign AI infrastructure',
description:
'Self-hosted AI control plane: your data, your jails, your rules. Operator-owned infrastructure that ships ready-to-use.',
},
nav: {
docs: 'Docs',
controlplane: 'Control plane',
source: 'Source',
},
switcher: {
label: 'Language',
en: 'EN',
sl: 'SL',
},
banner: {
heading: 'You are not on clawdie.si.',
body: 'This page is being served from an operator install. Use the controls below.',
openControlplane: 'Open control plane',
readDocs: 'Read docs',
claimDomain: 'Claim a public domain',
},
footer: {
tagline: 'Operator-owned AI · No surveillance, no rent extraction',
docsLink: 'docs.clawdie.si',
sourceLink: 'codeberg.org/Clawdie/Clawdie-AI',
rights: 'Released under AGPL-3.0',
},
};

View file

@ -0,0 +1,14 @@
import { en } from './en';
import { sl } from './sl';
import type { Locale, Strings } from './types';
export const locales: Locale[] = ['en', 'sl'];
export const defaultLocale: Locale = 'en';
const registry: Record<Locale, Strings> = { en, sl };
export function t(locale: Locale): Strings {
return registry[locale];
}
export type { Locale, Strings };

View file

@ -0,0 +1,32 @@
import type { Strings } from './types';
export const sl: Strings = {
meta: {
title: 'Clawdie — Suverena AI infrastruktura',
description:
'Samogosotvana AI nadzorna ravan: vaši podatki, vaši jaili, vaša pravila. Operaterjeva infrastruktura, ki je takoj uporabna.',
},
nav: {
docs: 'Dokumentacija',
controlplane: 'Nadzorna ravan',
source: 'Izvorna koda',
},
switcher: {
label: 'Jezik',
en: 'EN',
sl: 'SL',
},
banner: {
heading: 'Niste na clawdie.si.',
body: 'Ta stran se streže iz operaterjeve namestitve. Uporabite spodnje gumbe.',
openControlplane: 'Odpri nadzorno ravan',
readDocs: 'Preberi dokumentacijo',
claimDomain: 'Prevzemi javno domeno',
},
footer: {
tagline: 'AI v lasti operaterja · Brez nadzora, brez najemnine',
docsLink: 'docs.clawdie.si',
sourceLink: 'codeberg.org/Clawdie/Clawdie-AI',
rights: 'Izdano pod AGPL-3.0',
},
};

View file

@ -0,0 +1,31 @@
export type Locale = 'en' | 'sl';
export interface Strings {
meta: {
title: string;
description: string;
};
nav: {
docs: string;
controlplane: string;
source: string;
};
switcher: {
label: string;
en: string;
sl: string;
};
banner: {
heading: string;
body: string;
openControlplane: string;
readDocs: string;
claimDomain: string;
};
footer: {
tagline: string;
docsLink: string;
sourceLink: string;
rights: string;
};
}

View file

@ -0,0 +1,68 @@
---
import '../styles/global.css';
import LangSwitcher from '../components/LangSwitcher.astro';
import type { Locale } from '../i18n';
import { locales, t } from '../i18n';
interface Props {
locale: Locale;
}
const { locale } = Astro.props;
const strings = t(locale);
const site = Astro.site?.toString().replace(/\/$/, '') ?? 'https://clawdie.si';
const path = Astro.url.pathname;
const stripPrefix = (p: string, l: Locale) => {
const pref = `/${l}`;
return p.startsWith(pref) ? p.slice(pref.length) || '/' : p;
};
const sharedPath = stripPrefix(path, locale);
---
<!doctype html>
<html lang={locale}>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{strings.meta.title}</title>
<meta name="description" content={strings.meta.description} />
{
locales.map((l) => (
<link
rel="alternate"
hreflang={l}
href={`${site}/${l}${sharedPath === '/' ? '/' : sharedPath}`}
/>
))
}
<link
rel="alternate"
hreflang="x-default"
href={`${site}/en${sharedPath === '/' ? '/' : sharedPath}`}
/>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link
href="https://fonts.googleapis.com/css2?family=Cormorant+Garamond:ital,wght@0,300;0,400;0,600;1,300;1,400&family=DM+Mono:wght@300;400&display=swap"
rel="stylesheet"
/>
</head>
<body>
<div class="triangle-bg"></div>
<div class="container">
<LangSwitcher current={locale} />
<slot />
<footer class="site-footer">
<div>
{strings.footer.tagline}<br />
<a href="https://docs.clawdie.si/">{strings.footer.docsLink}</a>
·
<a href="https://codeberg.org/Clawdie/Clawdie-AI">{strings.footer.sourceLink}</a>
<br />
{strings.footer.rights}
</div>
<div class="footer-mark"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" width="32" height="32"><path d="M32 11 53 50H11L32 11Z" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round"/><path d="M21.5 30.5 L42.5 30.5 L32 50 Z" fill="none" stroke="currentColor" stroke-width="3" stroke-linejoin="round"/></svg></div>
</footer>
</div>
</body>
</html>

View file

@ -0,0 +1,8 @@
---
import Landing from '../../layouts/Landing.astro';
import LandingBody from '../../components/LandingBody.astro';
---
<Landing locale="en">
<LandingBody locale="en" />
</Landing>

View file

@ -0,0 +1,8 @@
---
import Landing from '../../layouts/Landing.astro';
import LandingBody from '../../components/LandingBody.astro';
---
<Landing locale="sl">
<LandingBody locale="sl" />
</Landing>

View file

@ -0,0 +1,355 @@
:root {
--bg: #0d1117;
--accent: #00b4d8;
--accent-dark: #0096b7;
--fg: #e2e8f0;
--fg-dim: #c9d1d9;
--grey: #8b949e;
--rule: #21262d;
--paper: #161b22;
--paper-2: #1c2333;
}
*,
*::before,
*::after {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
scroll-behavior: smooth;
}
body {
background-color: var(--bg);
color: var(--fg-dim);
font-family: 'Cormorant Garamond', Georgia, serif;
font-size: 22px;
line-height: 1.7;
overflow-x: hidden;
}
.triangle-bg {
position: fixed;
inset: 0;
opacity: 0.06;
pointer-events: none;
z-index: 0;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='100' height='100'%3E%3Cg stroke='%2300b4d8' stroke-width='0.8' fill='none'%3E%3Cline x1='0' y1='25' x2='40' y2='25'/%3E%3Cline x1='60' y1='25' x2='100' y2='25'/%3E%3Cline x1='0' y1='75' x2='40' y2='75'/%3E%3Cline x1='60' y1='75' x2='100' y2='75'/%3E%3Cline x1='25' y1='0' x2='25' y2='40'/%3E%3Cline x1='25' y1='60' x2='25' y2='100'/%3E%3Cline x1='75' y1='0' x2='75' y2='40'/%3E%3Cline x1='75' y1='60' x2='75' y2='100'/%3E%3Cpath d='M40 25 L50 25 L50 40'/%3E%3Cpath d='M60 25 L50 25 L50 40'/%3E%3Cpath d='M40 75 L50 75 L50 60'/%3E%3Cpath d='M60 75 L50 75 L50 60'/%3E%3C/g%3E%3Cg fill='%2300b4d8'%3E%3Ccircle cx='25' cy='25' r='2.5'/%3E%3Ccircle cx='75' cy='25' r='2.5'/%3E%3Ccircle cx='25' cy='75' r='2.5'/%3E%3Ccircle cx='75' cy='75' r='2.5'/%3E%3Ccircle cx='50' cy='50' r='2.5'/%3E%3C/g%3E%3C/svg%3E");
background-size: 100px 100px;
}
.container {
max-width: 900px;
margin: 0 auto;
padding: 0 2rem;
position: relative;
z-index: 1;
}
.top-nav {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1.5rem;
padding: 1.2rem 0;
border-bottom: 1px solid var(--rule);
font-family: 'DM Mono', monospace;
font-size: 0.85rem;
letter-spacing: 0.12em;
text-transform: uppercase;
}
.top-nav a {
color: var(--grey);
text-decoration: none;
transition: color 0.2s;
}
.top-nav a:hover,
.top-nav a.active {
color: var(--accent);
}
header.hero {
padding: 5rem 0 3rem;
position: relative;
animation: fadeUp 1.2s ease forwards;
}
.header-inner {
display: flex;
align-items: flex-start;
gap: 3rem;
}
.brand-mark {
font-size: 5rem;
line-height: 1;
animation: float 6s ease-in-out infinite;
flex-shrink: 0;
color: var(--accent);
font-family: 'DM Mono', monospace;
}
.header-text h1 {
font-size: clamp(3rem, 8vw, 5.5rem);
font-weight: 300;
letter-spacing: -0.02em;
line-height: 0.95;
color: var(--fg);
}
.header-text h1 span {
color: var(--accent);
font-style: italic;
}
.tagline {
font-family: 'DM Mono', monospace;
font-size: 0.85rem;
font-weight: 300;
letter-spacing: 0.15em;
text-transform: uppercase;
color: var(--grey);
margin-top: 1.2rem;
border-left: 2px solid var(--accent);
padding-left: 1rem;
}
.hero-statement {
background: var(--paper-2);
color: var(--fg);
padding: 4rem 3rem;
margin: 3rem 0;
position: relative;
overflow: hidden;
}
.hero-statement::before {
content: '△';
position: absolute;
right: -1rem;
top: -2rem;
font-size: 12rem;
color: var(--accent);
opacity: 0.08;
font-family: 'DM Mono', monospace;
line-height: 1;
}
.hero-statement p {
font-size: clamp(1.4rem, 3vw, 2rem);
font-weight: 300;
line-height: 1.5;
color: var(--fg);
}
.hero-statement strong {
color: var(--accent);
font-weight: 600;
}
section.landing-section {
margin: 4rem 0;
}
section.landing-section h2 {
font-size: 1.8rem;
font-weight: 300;
font-style: italic;
color: var(--fg);
margin-bottom: 1.2rem;
position: relative;
padding-bottom: 0.5rem;
}
section.landing-section h2::after {
content: '';
position: absolute;
bottom: 0;
left: 0;
width: 40px;
height: 1px;
background: var(--accent);
}
section.landing-section p {
color: var(--fg-dim);
font-size: 1.1em;
margin-bottom: 1rem;
font-weight: 300;
}
section.landing-section a {
color: var(--accent);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
section.landing-section a:hover {
border-bottom-color: var(--accent);
}
footer.site-footer {
margin-top: 6rem;
padding: 3rem 0;
border-top: 1px solid var(--rule);
display: flex;
justify-content: space-between;
align-items: center;
flex-wrap: wrap;
gap: 1rem;
font-family: 'DM Mono', monospace;
font-size: 0.85rem;
letter-spacing: 0.1em;
color: var(--grey);
text-transform: uppercase;
line-height: 1.8;
}
footer.site-footer a {
color: var(--accent);
text-decoration: none;
border-bottom: 1px solid transparent;
transition: border-color 0.2s;
}
footer.site-footer a:hover {
border-bottom-color: var(--accent);
}
.footer-mark {
font-size: 2rem;
opacity: 0.3;
color: var(--accent);
animation: float 8s ease-in-out infinite reverse;
}
/* --- LANG SWITCHER (OSA-style) --- */
.lang-switch {
position: absolute;
top: 1.5rem;
right: 2rem;
display: flex;
gap: 0.3rem;
font-family: 'DM Mono', monospace;
font-size: 0.8rem;
letter-spacing: 0.12em;
text-transform: uppercase;
z-index: 10;
}
.lang-switch a {
color: var(--grey);
text-decoration: none;
padding: 0.25rem 0.55rem;
border: 1px solid var(--rule);
transition:
color 0.2s,
border-color 0.2s;
}
.lang-switch a.active,
.lang-switch a:hover {
color: var(--accent);
border-color: var(--accent);
}
/* --- OPERATOR BANNER --- */
.operator-banner {
background: var(--paper);
border-bottom: 1px solid var(--accent);
padding: 1rem 2rem;
display: flex;
align-items: center;
justify-content: space-between;
gap: 1.5rem;
flex-wrap: wrap;
position: relative;
z-index: 2;
}
.operator-banner-text {
font-family: 'DM Mono', monospace;
font-size: 0.78rem;
letter-spacing: 0.08em;
color: var(--fg-dim);
}
.operator-banner-text strong {
color: var(--accent);
}
.operator-banner-actions {
display: flex;
gap: 0.6rem;
flex-wrap: wrap;
}
.operator-banner-actions a {
font-family: 'DM Mono', monospace;
font-size: 0.72rem;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--fg);
text-decoration: none;
border: 1px solid var(--accent);
padding: 0.45rem 0.9rem;
transition:
background 0.2s,
color 0.2s;
}
.operator-banner-actions a:hover {
background: var(--accent);
color: var(--bg);
}
@keyframes float {
0%,
100% {
transform: translateY(0px) rotate(-5deg);
}
50% {
transform: translateY(-12px) rotate(3deg);
}
}
@keyframes fadeUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@media (max-width: 640px) {
.header-inner {
flex-direction: column;
gap: 1.5rem;
}
.brand-mark {
font-size: 3.5rem;
}
.header-text h1 {
font-size: 3rem;
}
.hero-statement {
padding: 2.5rem 1.5rem;
}
footer.site-footer {
flex-direction: column;
text-align: center;
}
.lang-switch {
right: 1rem;
top: 1rem;
}
}

View file

@ -0,0 +1,5 @@
{
"extends": "astro/tsconfigs/strict",
"include": [".astro/types.d.ts", "**/*"],
"exclude": ["dist"]
}

View file

@ -60,6 +60,11 @@ agent's keys were loaded before the daemon started, so Colibri auto-spawns
the agent on boot. Check with:
colibri status
If a mother-mcp key was seeded, git pull from Forgejo works out-of-the-box:
cd ~/ai/clawdie-iso && git pull
cd ~/ai/clawdie-ai && git pull
The key is wired via ~/.ssh/config for both mother MCP and code.smilepowered.org.
Readable operator guide:
/usr/local/share/clawdie-iso/seed/README.txt

View file

@ -94,12 +94,17 @@ Inside it, any of these are honored:
/<agent>/ssh/<name>.pub OUTBOUND: installed to ~/.ssh/<name>.pub (0644).
/<agent>/ssh/config OUTBOUND: installed to ~/.ssh/config (0600).
Typical use — a host alias for the mother server:
Typical use — host entries for the dual-purpose
mother-mcp key (see next section):
Host mother
HostName osa.smilepowered.org
User clawdie
IdentityFile ~/.ssh/osa-mother-2026
User colibri
IdentityFile ~/.ssh/mother-mcp
Host code.smilepowered.org
IdentityFile ~/.ssh/mother-mcp
IdentitiesOnly yes
/<agent>/ssh/known_hosts OUTBOUND: merged into ~/.ssh/known_hosts (0644),
de-duplicated. Pin the mother server's host key
@ -107,6 +112,30 @@ Inside it, any of these are honored:
not stop on an unknown-host prompt. Get the line
with: ssh-keyscan osa.smilepowered.org
/<agent>/ssh/mother-mcp DUAL-PURPOSE OUTBOUND KEY. This private key
serves two roles with a single identity:
1. MCP calls to mother via colibri-mcp.
The mother server's authorized_keys entry
forces command="colibri-mcp",restrict —
this key can ONLY invoke the MCP tool,
never a shell session.
2. Git pull from Forgejo (code.smilepowered.org).
Add this same key as a read-only deploy key
in the Forgejo repository settings (repo →
Settings → Deploy Keys → Add Deploy Key,
with "Enable write access" OFF). Read-only
is sufficient for `git pull` and limits
blast radius if the key is ever compromised.
Placing a key named mother-mcp here gives the
agent git pull from Forgejo out-of-the-box with
no additional configuration — the accompanying
ssh/config Host entries route it for both
destinations. No other key is needed for either
purpose.
Agent directory names may contain only A-Z a-z 0-9 . _ - (no spaces or
slashes). The name `ssh` is reserved for Layer 1.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.6 KiB

View file

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
<!-- Clawdie Start-button icon: upward triangle, brand navy->blue gradient
fill with a coral edge. Matches the clawdie.si triangle wordmark and the
operator wallpaper palette. Source of truth; PNG renders are derived. -->
<defs>
<linearGradient id="tri" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#21457a"/>
<stop offset="100%" stop-color="#0f3460"/>
</linearGradient>
</defs>
<!-- Clawdie Start-button icon: unified geodesic 2V logo.
Outer triangle + inverted inner triangle whose vertices
touch the midpoints of the outer sides.
Cyan on transparent — inherits panel background. -->
<path d="M32 7 L57 55 L7 55 Z"
fill="url(#tri)"
stroke="#e94560"
stroke-width="3.5"
fill="none"
stroke="#00b4d8"
stroke-width="3"
stroke-linejoin="round"/>
<path d="M19.5 31 L44.5 31 L32 55 Z"
fill="none"
stroke="#00b4d8"
stroke-width="3"
stroke-linejoin="round"/>
</svg>

Before

Width:  |  Height:  |  Size: 707 B

After

Width:  |  Height:  |  Size: 642 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 KiB

After

Width:  |  Height:  |  Size: 176 KiB

View file

@ -1,28 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="2560" height="1440" viewBox="0 0 2560 1440">
<!-- Clawdie operator USB wallpaper: brand navy->blue diagonal gradient with a
subtle centered triangle motif and wordmark. Same palette as clawdie.si
(#1a1a2e / #16213e / #0f3460 / #e94560). Source of truth; PNG is derived. -->
<!-- Clawdie operator USB wallpaper: dark diagonal gradient with a
centered geodesic 2V triangle motif and wordmark.
Unified logo — outer triangle + inverted inner triangle at midpoints. -->
<defs>
<linearGradient id="bg" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#1a1a2e"/>
<stop offset="50%" stop-color="#16213e"/>
<stop offset="100%" stop-color="#0f3460"/>
</linearGradient>
<linearGradient id="tri" x1="0" y1="0" x2="0" y2="1">
<stop offset="0%" stop-color="#21457a"/>
<stop offset="100%" stop-color="#0f3460"/>
</linearGradient>
</defs>
<rect width="2560" height="1440" fill="url(#bg)"/>
<path d="M1280 400 L1620 1010 L940 1010 Z"
fill="url(#tri)"
stroke="#e94560"
stroke-width="7"
<!-- Geodesic 2V logo — outer triangle -->
<path d="M1280 340 L1680 1060 L880 1060 Z"
fill="none"
stroke="#00b4d8"
stroke-width="6"
stroke-linejoin="round"
opacity="0.22"/>
opacity="0.35"/>
<!-- Geodesic 2V logo — inverted inner triangle at midpoints -->
<path d="M1080 700 L1480 700 L1280 1060 Z"
fill="none"
stroke="#00b4d8"
stroke-width="6"
stroke-linejoin="round"
opacity="0.35"/>
<text x="1280" y="1180" text-anchor="middle" font-family="sans-serif"
font-size="120" font-weight="bold" fill="#e94560" opacity="0.18">Clawdie</text>
font-size="120" font-weight="bold" fill="#00b4d8" opacity="0.18">Clawdie</text>
<text x="1280" y="1260" text-anchor="middle" font-family="sans-serif"
font-size="42" fill="#ffffff" opacity="0.10">AI you own</text>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -113,10 +113,17 @@ cat > "${ETC_DIR}/provider.env" <<'EOF'
VAULT_SERVER="https://vault.smilepowered.org"
BW_SERVER="https://vault.smilepowered.org"
# Auto-spawn one Pi agent on daemon startup once a DeepSeek key is present
# Auto-spawn one zot agent on daemon startup once a DeepSeek key is present
# (the Operator Image OOTB flow). The daemon sources this file, so the spawned
# Pi inherits the provider keys set here.
COLIBRI_AUTOSPAWN_PI="YES"
# agent inherits the provider keys set here.
COLIBRI_AUTOSPAWN="YES"
# zot is the default harness — pi is available but zot has richer provider
# support (DeepSeek native, OpenRouter, ~25 providers) and a built-in
# Telegram bot mode. Set COLIBRI_AUTOSPAWN_BINARY=pi to switch back.
COLIBRI_AUTOSPAWN_BINARY="zot"
# Telegram bot token — set this to enable the bot channel (@your_bot).
# Leave blank to use CLI/TUI/Dashboard channels only.
# TELEGRAM_BOT_TOKEN=""
EOF
chmod 0600 "${ETC_DIR}/provider.env" 2>/dev/null || true
@ -155,16 +162,14 @@ BW_SERVER="https://vault.smilepowered.org"
# DEEPSEEK_ENDPOINT="https://api.deepseek.com/chat/completions"
# DEEPSEEK_MODEL="deepseek-v4-pro"
#
# Telegram bot (optional):
# TELEGRAM_BOT_TOKEN="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
#
# Behavior toggles (non-secret):
# COLIBRI_AUTOSPAWN_PI="YES" # auto-spawn one Pi on daemon startup
# COLIBRI_PI_BINARY="pi" # Pi executable name/path; zot is staged on
# # the image but needs the zot-rpc driver
# # before it can autospawn (spawner uses
# # stdin(Stdio::null()) — zot needs stdin).
# # See: ADR-agent-harness-consolidation.md
# # + colibri#143 (zot-rpc-driver).
# COLIBRI_AUTOSPAWN_PI_ARGS="--mode json" # Pi argv
# COLIBRI_MCP_EXTERNAL_CALL="1" # allow Pi (via colibri-mcp) to call
# COLIBRI_AUTOSPAWN="YES" # auto-spawn one agent on daemon startup
# COLIBRI_AUTOSPAWN_BINARY="zot" # agent harness: zot (default) | pi
# COLIBRI_AUTOSPAWN_ARGS="--mode json" # agent argv
# COLIBRI_MCP_EXTERNAL_CALL="1" # allow agent (via colibri-mcp) to call
# # external MCP servers; set by
# # clawdie-enable-mother
EOF