Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![CI](https://github.com/SSHDotCodes/auto/actions/workflows/ci.yml/badge.svg)](https://github.com/SSHDotCodes/auto/actions/workflows/ci.yml)
[![License: Apache 2.0](https://img.shields.io/badge/License-Apache_2.0-22543d.svg)](LICENSE)

![Auto — local permission intelligence](site/assets/mountain.webp)
![Auto — local permission intelligence](site/assets/og.webp)

**Local permission intelligence for coding agents.** Works with Claude Code, Codex, Gemini CLI, Cursor, GitHub Copilot CLI, Pi, OpenCode and Hermes, plus the Claude Agent SDK, LangChain and any custom harness.

Expand Down
8 changes: 3 additions & 5 deletions docs/assets.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
# Asset provenance

`site/assets/mountain.png` was generated with OpenAI ImageGen for this project on 2026-09-09. Generation mode: text-to-image. Actual output: 1672×941 PNG. The requested 4K size was not the generated size; no 4K-resolution claim is made. WebP encodings and the 900-pixel responsive version are produced by `scripts/build-site.mjs`.
The page draws its starfield and horizon glow in CSS; it uses no photographs. The link preview `site/assets/og.png` is rendered from HTML by `scripts/render-social.mjs` (`node scripts/render-social.mjs og site/assets/og.png`) and encoded to `og.webp` by `scripts/build-site.mjs`; only the WebP is served. The same script renders the launch post image (`x`).

Prompt:
Typography: Geist and Geist Mono (variable), and Instrument Serif italic, sourced through their Fontsource npm packages. All three are distributed under the SIL Open Font License; copies are included beside the built font files in `site/assets/`. These retain their original licenses. The Auto mark is a project-created SVG. Project code is Apache-2.0.

> Use case: photorealistic-natural. Asset type: full-bleed background for the landing page of Auto, an elegant open-source local AI tool-permission classifier. Create a stunning, extraordinarily detailed, realistic alpine mountain photograph at first light. A monumental sharp snow-covered mountain massif with delicate geological detail, blue shadowed glaciers and rocky ridgelines, glowing warm pale sunlight on the summit, multiple layers of distant mountains and a sea of low mist. Panoramic 16:9, preferably 3840x2160. The mountain is dominant in the center-right with calm dark blue sky and mist extending to the left for white website typography. Deep natural midnight/slate blues, silver snow, subtle warm sunlight. Cinematic atmosphere with photographic restraint, crisp rich detail, grand scale and beautiful natural light. Edge-to-edge image, no border, no text, no lettering, no logo, no people, no buildings, no UI. Designed to remain beautiful when cropped on mobile. This is the actual hero photograph asset, not a website mockup.

Typography: Manrope variable and Instrument Serif italic, sourced through their Fontsource npm packages. Both are distributed under the SIL Open Font License; copies are included beside the built font files in `site/assets/`. These retain their original licenses. The Auto mark is a project-created SVG. Project code is Apache-2.0; the generated mountain asset is included for reuse with this project, subject to applicable law concerning generated works.
Earlier versions used a generated mountain photograph (OpenAI ImageGen, 2026-09-09); it was removed in favor of the ssh.codes theme.
4 changes: 2 additions & 2 deletions docs/website.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# Website

`site/` is the complete static website. Run `npm ci && npm run build:site` to generate optimized WebP backgrounds, copy local fonts and their licenses, and copy the canonical installer scripts. `python site/serve.py --port 8542` serves only the intended public files on loopback. A reverse proxy provides HTTPS. No model inference or management interface is exposed through the website.
`site/` is the complete static website. Run `npm ci && npm run build:site` to encode the social preview image, copy local fonts and their licenses, and copy the canonical installer scripts. `python site/serve.py --port 8542` serves only the intended public files on loopback. A reverse proxy provides HTTPS. No model inference or management interface is exposed through the website.

The production origin is a Raspberry Pi, managed as the `auto` app. Deploy the whole built site tree, set the command to `python3 serve.py --port 8542`, enable autostart and restart-always, and route `auto.ssh.codes` to the loopback origin. `/healthz` returns a minimal JSON readiness response. Before modifying an existing deployment, pull its complete current source and reapply intended changes to a fresh pull immediately before upload.

Charts report the v3 benchmark (2,948 held-out tool calls in near-twin pairs) for the v3 models, three hosted guards and auto-0.4b-2. Metric selectors change both bars and accessible labels; all axes start at zero. Probe examples are static and clearly identified. Install controls generate agent/OS-specific commands and support copying or keyboard fallback. The site has no analytics or third-party font requests.

The mountain background was generated for this project using OpenAI ImageGen on September 9, 2026. The original asset is 1672×941; WebP derivatives preserve its composition. It depicts an imagined alpine scene, not an identified real location. See [asset provenance](assets.md) for the prompt and font licenses.
The site follows the ssh.codes theme: a black, CSS-only starfield and horizon glow, Geist and Geist Mono with Instrument Serif italic accents. It loads no images in the page itself. `assets/og.png`, the link preview, is rendered by `scripts/render-social.mjs` and served as WebP. See [asset provenance](assets.md) for fonts and licenses.
19 changes: 15 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
"format:check": "prettier --check 'site/*.{html,css,js}' 'src/auto_gate/adapters/*.mjs' 'scripts/*.mjs' 'tests/**/*.mjs' 'tests/site/*.js' playwright.config.js"
},
"devDependencies": {
"@fontsource-variable/manrope": "5.3.0",
"@fontsource-variable/geist": "5.3.0",
"@fontsource-variable/geist-mono": "5.3.0",
"@fontsource/instrument-serif": "5.3.0",
"@playwright/test": "1.63.0",
"prettier": "3.9.6",
Expand Down
24 changes: 13 additions & 11 deletions scripts/build-site.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,22 +13,24 @@ const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const site = path.join(root, "site");
await mkdir(path.join(site, "assets"), { recursive: true });
await Promise.all([
sharp(path.join(site, "assets/mountain.png"))
.webp({ quality: 90 })
.toFile(path.join(site, "assets/mountain.webp")),
sharp(path.join(site, "assets/mountain.png"))
.resize({ width: 900 })
.webp({ quality: 86 })
.toFile(path.join(site, "assets/mountain-mobile.webp")),
// Social preview, rendered by scripts/social_images.py; the PNG source is not served.
sharp(path.join(site, "assets/og.png"))
.webp({ quality: 92 })
.toFile(path.join(site, "assets/og.webp")),
...["install.sh", "install.ps1", "install.py"].map((name) =>
copyFile(path.join(root, "scripts", name), path.join(site, name)),
),
]);
for (const [pkg, match, target] of [
[
"@fontsource-variable/manrope",
"manrope-latin-wght-normal.woff2",
"manrope.woff2",
"@fontsource-variable/geist",
"geist-latin-wght-normal.woff2",
"geist.woff2",
],
[
"@fontsource-variable/geist-mono",
"geist-mono-latin-wght-normal.woff2",
"geist-mono.woff2",
],
[
"@fontsource/instrument-serif",
Expand All @@ -52,4 +54,4 @@ await writeFile(
path.join(site, "probes.json"),
JSON.stringify(probes, null, 2) + "\n",
);
console.log("Built local image, fonts, installers, and probe fixtures.");
console.log("Built social image, fonts, installers, and probe fixtures.");
156 changes: 156 additions & 0 deletions scripts/render-social.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
// Social images in the site's theme, rendered with Playwright so they use the real fonts.
// node scripts/render-social.mjs og site/assets/og.png (link preview, 1200×630 at 2×)
// node scripts/render-social.mjs x OUT.png (launch post image, 1600×900 at 2×)
// Numbers: v3 benchmark (2,948 held-out tool calls) and median time per decision from the v0.4.0 release.
import { chromium } from "playwright";
import { fileURLToPath, pathToFileURL } from "node:url";
import path from "node:path";
import { mkdtemp, writeFile } from "node:fs/promises";
import os from "node:os";

const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
const font = (pkg, file) =>
pathToFileURL(path.join(root, "node_modules", pkg, "files", file)).href;
const [kind, out] = process.argv.slice(2);
if (!["og", "x"].includes(kind) || !out)
throw new Error("usage: render-social.mjs og|x OUT.png");

const css = `
@font-face { font-family: Geist; src: url(${font("@fontsource-variable/geist", "geist-latin-wght-normal.woff2")}); font-weight: 100 900; }
@font-face { font-family: "Geist Mono"; src: url(${font("@fontsource-variable/geist-mono", "geist-mono-latin-wght-normal.woff2")}); font-weight: 100 900; }
@font-face { font-family: Instrument; src: url(${font("@fontsource/instrument-serif", "instrument-serif-latin-400-italic.woff2")}); font-style: italic; }
* { box-sizing: border-box; margin: 0; }
body { width: var(--w); height: var(--h); background: #000; color: #f4f6fb; font-family: Geist; overflow: hidden; position: relative; }
body::before { content: ""; position: absolute; inset: 0; opacity: .6; background-image:
radial-gradient(1px 1px at 23px 41px, #ffffffb0, transparent), radial-gradient(1px 1px at 151px 97px, #ffffff70, transparent),
radial-gradient(1px 1px at 87px 171px, #dfeaff80, transparent), radial-gradient(1.5px 1.5px at 211px 23px, #ffffff90, transparent),
radial-gradient(1px 1px at 301px 233px, #ffffff60, transparent), radial-gradient(1.5px 1.5px at 397px 151px, #ffffff80, transparent);
background-size: 233px 233px, 277px 277px, 313px 313px, 389px 389px, 431px 431px, 523px 523px; }
.horizon { position: absolute; left: 50%; width: 3200px; aspect-ratio: 1; border-radius: 50%; transform: translateX(-30%);
background: radial-gradient(circle at 50% 0%, #16335f 0%, #0a1a33 9%, #03070f 22%, #000 34%);
box-shadow: 0 -1px 0 rgba(159,208,255,.55), 0 -18px 60px rgba(110,170,255,.28), 0 -80px 160px rgba(80,130,255,.12); }
.brand { display: flex; align-items: center; gap: 12px; font-weight: 600; letter-spacing: -.02em; }
.orb { width: 24px; height: 24px; border-radius: 50%; background: radial-gradient(circle at 34% 30%, #e2f1ff 0%, #6ea6f4 30%, #1d4c9c 60%, #04101f 85%);
box-shadow: 0 0 0 1px rgba(159,208,255,.4), 0 0 16px rgba(110,170,255,.55); }
.mono { font-family: "Geist Mono"; text-transform: uppercase; letter-spacing: .12em; font-weight: 500; }
h1 { font-weight: 600; letter-spacing: -.05em; line-height: 1; }
em { font-family: Instrument; font-style: italic; font-weight: 400; letter-spacing: -.01em; padding-right: .06em;
background: linear-gradient(100deg, #7597ff 0%, #95b2ff 50%, #b4ceff 100%); -webkit-background-clip: text; background-clip: text; color: transparent; }
.sky { color: #9fd0ff; }
.muted { color: rgba(226,233,247,.68); }
`;

const rows = [
["auto-3b-3", "open weights · 3.08B", 96.0, "21 ms", true],
["DeepSeek V4.1 Flash", "hosted · max reasoning", 95.6, "1.3 s", false],
["Codex auto-review", "hosted · medium", 95.5, "–", false],
["GLM-5.3 Flash", "hosted · high reasoning", 95.4, "1.2 s", false],
["auto-0.4b-3", "open weights · 396M", 93.1, "7 ms", true],
["auto-150m-3", "open weights · 150M", 88.8, "5 ms", true],
];
const lo = 85,
hi = 100;
const pos = (v) => `${((v - lo) / (hi - lo)) * 100}%`;

const x = `
<style>${css}
:root { --w: 1600px; --h: 900px; }
.horizon { top: 868px; }
.top { position: absolute; left: 88px; right: 88px; top: 64px; display: flex; justify-content: space-between; align-items: center; }
.top .brand { font-size: 28px; }
.top .mono { font-size: 15px; }
h1 { position: absolute; left: 88px; top: 142px; font-size: 76px; }
.card { position: absolute; left: 88px; right: 88px; top: 352px; padding: 26px 40px 20px; border-radius: 28px;
border: 1px solid rgba(255,255,255,.1); background: rgba(8,11,18,.78); backdrop-filter: blur(8px); }
.head, .row { display: grid; grid-template-columns: 340px 1fr 170px; align-items: center; column-gap: 40px; }
.head { font-size: 13px; padding-bottom: 12px; border-bottom: 1px solid rgba(255,255,255,.085); }
.head span:last-child { text-align: right; }
.row { height: 64px; border-bottom: 1px solid rgba(255,255,255,.05); }
.row:last-child { border-bottom: 0; }
.name { font-size: 25px; font-weight: 500; letter-spacing: -.02em; line-height: 1.1; }
.name small { display: block; font: 500 12.5px/1.5 "Geist Mono"; letter-spacing: .06em; text-transform: uppercase; color: rgba(226,233,247,.6); }
.track { position: relative; height: 2px; background: rgba(255,255,255,.1); border-radius: 2px; }
.fill { position: absolute; left: 0; top: 0; height: 2px; border-radius: 2px; background: rgba(255,255,255,.22); }
.auto .fill { background: linear-gradient(90deg, rgba(121,174,252,.15), #79aefc); }
.dot { position: absolute; top: 50%; width: 16px; height: 16px; margin: -8px 0 0 -8px; border-radius: 50%; background: #6b6f7a; box-shadow: 0 0 0 4px #080b12; }
.auto .dot { background: #b6dcff; box-shadow: 0 0 0 4px #080b12, 0 0 18px rgba(121,174,252,.9); }
.val { position: absolute; top: -34px; transform: translateX(-50%); font-size: 21px; font-weight: 500; letter-spacing: -.02em; color: rgba(226,233,247,.7); white-space: nowrap; }
.auto .val { color: #fff; font-weight: 600; }
.speed { text-align: right; font: 500 27px/1 "Geist Mono"; letter-spacing: -.02em; color: rgba(226,233,247,.62); }
.auto .speed { color: #9fd0ff; }
.axis { display: grid; grid-template-columns: 340px 1fr 170px; column-gap: 40px; margin-top: 6px; }
.ticks { position: relative; height: 18px; font: 500 12px/1 "Geist Mono"; color: rgba(226,233,247,.45); }
.ticks span { position: absolute; transform: translateX(-50%); }
.foot { position: absolute; left: 88px; right: 88px; bottom: 34px; display: flex; justify-content: space-between; gap: 48px; font-size: 12.5px; }
</style>
<div class="horizon"></div>
<div class="top"><div class="brand"><span class="orb"></span>auto</div><div class="mono sky">v3 · open-weight tool-call guards</div></div>
<h1>As accurate as hosted guards.<br>At <em>60×</em> the speed.</h1>
<div class="card">
<div class="head mono muted"><span>Model</span><span>Accuracy · v3 benchmark</span><span>Per decision</span></div>
${rows
.map(
([
n,
s,
a,
t,
auto,
]) => `<div class="row ${auto ? "auto" : ""}"><div class="name">${n}<small>${s}</small></div>
<div class="track"><div class="fill" style="width:${pos(a)}"></div><div class="dot" style="left:${pos(a)}"></div><div class="val" style="left:${pos(a)}">${a.toFixed(1)}%</div></div>
<div class="speed">${t}</div></div>`,
)
.join("")}
<div class="axis"><span></span><div class="ticks">${[85, 90, 95, 100].map((v) => `<span style="left:${pos(v)}">${v}%</span>`).join("")}</div><span></span></div>
</div>
<div class="foot mono muted"><span>2,948 held-out tool calls · Auto on one RTX PRO 6000 · hosted via API · Codex via CLI, not timed</span><span class="sky">huggingface.co/ProCreations</span></div>
`;

const og = `
<style>${css}
:root { --w: 1200px; --h: 630px; }
.horizon { top: 520px; transform: translateX(-36%); }
.brand { position: absolute; left: 72px; top: 58px; font-size: 26px; }
.eyebrow { position: absolute; left: 72px; top: 150px; font-size: 14px; }
h1 { position: absolute; left: 72px; top: 190px; font-size: 92px; }
.stats { position: absolute; left: 72px; right: 72px; top: 438px; display: flex; gap: 64px; }
.stats b { display: block; font-size: 44px; font-weight: 500; letter-spacing: -.04em; line-height: 1; }
.stats span { display: block; margin-top: 10px; font-size: 12.5px; }
.url { position: absolute; right: 72px; top: 64px; font-size: 15px; }
</style>
<div class="horizon"></div>
<div class="brand"><span class="orb"></span>auto</div>
<div class="url mono sky">auto.ssh.codes</div>
<div class="eyebrow mono sky">Local permission intelligence for coding agents</div>
<h1>A little intelligence.<br>A <em>clearer</em> path.</h1>
<div class="stats">
<div><b>96.0%</b><span class="mono muted">v3 accuracy · auto-3b-3</span></div>
<div><b>21 ms</b><span class="mono muted">per decision</span></div>
<div><b>3</b><span class="mono muted">open-weight models</span></div>
</div>
`;

const [w, h] = kind === "x" ? [1600, 900] : [1200, 630];
const browser = await chromium.launch();
const page = await browser.newPage({
viewport: { width: w, height: h },
deviceScaleFactor: 2,
});
// Loaded from a file so the file:// fonts are same-origin.
const html = path.join(
await mkdtemp(path.join(os.tmpdir(), "auto-social-")),
"card.html",
);
await writeFile(
html,
`<!doctype html><html><head><meta charset="utf-8"></head><body>${kind === "x" ? x : og}</body></html>`,
);
await page.goto(pathToFileURL(html).href);
await page.evaluate(() => document.fonts.ready);
const families = await page.evaluate(() =>
[...document.fonts].filter((f) => f.status === "loaded").map((f) => f.family),
);
if (families.length < 3) throw new Error(`fonts did not load: ${families}`);
await page.screenshot({ path: out });
await browser.close();
console.log(`wrote ${out}`);
Binary file added site/assets/geist-mono.woff2
Binary file not shown.
Loading
Loading