Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
6 changes: 6 additions & 0 deletions DEVELOPMENT.md
Original file line number Diff line number Diff line change
Expand Up @@ -506,6 +506,12 @@ The `.env.example` files are documentation and drift.
`GITHUB_CLIENT_SECRET`, `GOOGLE_CLIENT_ID`, `GOOGLE_CLIENT_SECRET`,
`CORS_ORIGINS`.

`CORS_ORIGINS` must list the **marketing site's** origin in staging and
production, not just the app's: `site/` is a static build on another host and its
founding-price capture POSTs to `/api/waitlist` here (`WEB_URL` in
`site/src/config.ts`). Omit it and every submit fails in the browser as a network
error, with nothing in the web service's logs to say why.

**`web/.env` — defaulted, safe to omit:** `NODE_ENV` (`development`),
`EMAIL_FROM`, `PORT` (8787). `BETTER_AUTH_URL` auto-derives to
`http://localhost:${PORT}` in development and test; it is required only in
Expand Down
60 changes: 42 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,36 +1,50 @@
# Antgrid

**Your agent says it's done. Make it prove it.**
**Your machines. Your agents. One control plane.**

Evidence-gated supervision for the CLI coding agents you already runon your own
hardware, end-to-end encrypted.
Every CLI coding agent you run, on every machine you own, in one place — end-to-end
encrypted, on hardware you control.

[![CI](https://github.com/antgrid-ai/antgrid/actions/workflows/ci.yml/badge.svg)](https://github.com/antgrid-ai/antgrid/actions/workflows/ci.yml)
[![License: Elastic License 2.0](https://img.shields.io/badge/license-Elastic%20License%202.0-4b5563?style=flat)](LICENSE.md)
[![Latest release](https://img.shields.io/github/v/release/antgrid-ai/antgrid?style=flat&logo=github&label=release)](https://github.com/antgrid-ai/antgrid/releases/latest)
[![Stars](https://img.shields.io/github/stars/antgrid-ai/antgrid?style=flat&logo=github)](https://github.com/antgrid-ai/antgrid/stargazers)

Antgrid runs the coding agents you already use — Claude Code, Codex, Cursor and others —
in real terminals on your own hardware. Arm its supervisor on a session and it watches the
agent's attention signals, answers what it can, escalates what it can't, and calls a task
done only on concrete evidence — test output, exit codes, a diff — rather than the agent's
own report.

Around each agent it puts the context you need to check that work yourself: multi-session
terminals, a file tree, git review with diffs, and a live browser preview. The same
workspace opens on a phone, over a relay that is end-to-end encrypted and cannot read a
byte of what passes through it.
in real terminals on your own hardware, and puts one screen over all of them: every
session on every machine you have signed in, grouped by the machine it is on. Around each
agent it puts the context you need to check the work yourself — multi-session terminals, a
file tree, git review with diffs, and a live browser preview. The same workspace opens on
a phone, over a relay that is end-to-end encrypted and cannot read a byte of what passes
through it.

Arm its supervisor on a session and it goes further: it watches the agent's attention
signals, answers what it can, escalates what it can't, and calls a task done only on
concrete evidence — test output, exit codes, a diff — rather than the agent's own report.
That part is opt-in and it is the paid tier — `CAPABILITIES` in
[`bridge/src/entitlement.ts`](bridge/src/entitlement.ts) is the whole capability gate. The
only other paid line is how many machines one account may run agents on
(`FREE_WORKER_LIMIT` in [`web/src/billing/plans.ts`](web/src/billing/plans.ts)); everything
else is free.

Antgrid does not replace your agent and ships no model of its own.

> Status: pre-release, working towards v1.
> [!NOTE]
> **Pre-release, working towards v1.**
>
> **Licence** — source-available under [Elastic License 2.0](#licence): free to read,
> fork, modify and self-host, including commercially. Not OSI open source.
>
> **Contributing** — bug reports are welcome; pull requests are not open yet
> ([CONTRIBUTING.md](CONTRIBUTING.md)).

## Features

- **Supervisor.** Arm it on a session and it watches the agent's attention signals,
answers what it can, escalates what it can't, and calls a task done only on concrete
evidence — test output, exit codes — rather than the agent's own report. You can also
give it follow-up steps to carry out once the task is done; it works through them in
order and stays armed until each one is satisfied.
- **Supervisor** *(paid tier)*. Arm it on a session and it watches the agent's attention
signals, answers what it can, escalates what it can't, and calls a task done only on
concrete evidence — test output, exit codes — rather than the agent's own report. You
can also give it follow-up steps to carry out once the task is done; it works through
them in order and stays armed until each one is satisfied.
- **Bring your own agent.** Claude Code, Codex, opencode, Cursor, GitHub Copilot,
Antigravity, Kilo, Kimi and Mistral Vibe are wired for notifications and session naming
— the current set is `AGENTS` in [`bridge/src/agents/registry.ts`](bridge/src/agents/registry.ts).
Expand Down Expand Up @@ -83,6 +97,16 @@ feature flag.
Encryption protects the transport. It does not sandbox the agent, and it cannot make an
untrusted agent safe to run on your machine.

And two things the list above is not. It is not an audit: there has been no external
penetration test and no certification. And it does not empty the trust boundary — it moves
the relay out of it, not our account service. Your phone learns a machine's Ed25519
identity from your account's device inventory, which `app.antgrid.ai` serves, so that
service is trusted to hand you the right key even though the relay never is.

None of this needs taking on trust. The handshake specification, both implementations and
the relay itself are linked above and in this repo; [SECURITY.md](SECURITY.md) is the
reporting policy if you find something wrong with them.

## Architecture

| Component | Path | Stack | Role |
Expand Down
39 changes: 28 additions & 11 deletions app/lib/screens/device_cap_dialog.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,21 @@ import '../design/ab_icons.dart';
import '../design/ab_tokens.dart';
import '../design/widgets/ab_button.dart';
import '../design/widgets/ab_confirm_dialog.dart';
import '../design/widgets/ab_icon.dart';
import '../design/widgets/ab_icon_button.dart';
import '../design/widgets/ab_list_row.dart';
import '../design/widgets/ab_tooltip.dart';
import '../providers/auth.dart';
import '../providers/device_provisioning.dart';
import '../providers/post_signin_provisioning.dart';
import '../services/devices_api.dart';
import '../util/detached.dart';
import '../util/external_url.dart';

/// Where the worker-cap variant sends someone who wants more machines. There is
/// no checkout to send them to during the beta, so the ask is captured on the
/// site instead; no price is named here or on the way out, because none is
/// committed to yet.
const _foundingPricingUrl = 'https://antgrid.ai/pricing';

/// Shows the device-cap remediation dialog and resolves when it is dismissed.
/// Always clears [deviceCapProvider] on close so a later provisioning attempt
Expand All @@ -34,8 +42,7 @@ Future<void> showDeviceCapDialog(
/// same way: revoke one of the listed devices, then retry provisioning this
/// machine. Only the copy differs — `appDeviceLimit` is an abuse ceiling that
/// pricing never mentions, so its variant never offers upgrading, while the
/// worker cap is the paid axis and shows an upgrade affordance (disabled until
/// checkout ships).
/// worker cap is the paid axis and points at the founding-price waitlist.
class DeviceCapDialog extends ConsumerStatefulWidget {
const DeviceCapDialog({super.key, required this.info});

Expand Down Expand Up @@ -206,20 +213,30 @@ class _DeviceCapDialogState extends ConsumerState<DeviceCapDialog> {
runSpacing: AbTokens.space8,
children: [
if (_isWorker) ...[
// Checkout is not wired yet, so the paid path is shown and
// legibly shut rather than absent. The tooltip alone would
// leave the button unexplained on mobile (no hover), hence
// the inline label beside it.
// The machine slot cannot be bought during the beta, so the
// paid path leads somewhere that works instead of standing
// there disabled. The line says why the button is a
// waitlist and not a purchase; a tooltip could not, having
// no hover on mobile.
Text(
'Coming soon',
'More machines aren\'t on sale yet.',
style: AbTokens.sansStyle(
fontSize: AbTokens.fontXs,
color: p.textMuted,
),
),
const AbTooltip(
message: 'Coming soon',
child: AbButton(label: 'Upgrade'),
AbButton(
label: 'Join the waitlist',
leading: AbIcon(
AbIcons.openExternal,
size: AbTokens.iconButtonGlyph,
color: p.textSecondary,
),
onTap: () => detached(
'DeviceCapDialog',
'open founding-pricing waitlist',
() => openExternalUrl(context, _foundingPricingUrl),
),
),
],
AbButton(
Expand Down
11 changes: 7 additions & 4 deletions app/lib/screens/upgrade_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ const _proYearlyFeatures = [

/// TEMP-PROMO: why the plan can't be bought, said in the CTA itself. "Coming
/// soon" reads as half-built to someone who arrived from a site that told them
/// the beta is free. Same wording as web's `UNAVAILABLE_CTA_LABEL` and the
/// marketing site's `PlanCard.astro`. Carries no beta flag of its own: the
/// whole static block this belongs to is deleted when checkout opens — see the
/// TEMP-PROMO marker at the foot of this file.
/// the beta is free. This is now the only DEAD paid CTA left: web's pricing page
/// and the marketing site's plan cards capture an address in place, and
/// `device_cap_dialog.dart` — the app's own answer to the same problem — sends
/// the reader out to the site's capture rather than standing there disabled.
/// Carries no beta flag of its own: the whole static block this belongs to is
/// deleted when checkout opens — see the TEMP-PROMO marker at the foot of this
/// file.
const _unavailableCtaLabel = 'Available after beta';

/// The machine count sits mid-sentence, so it has to agree with its noun —
Expand Down
5 changes: 4 additions & 1 deletion relay/relay-requirements.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,10 @@
> `pair-connected` pairing ceremony below. Both are gone: the relay now
> authenticates a single signed `hello` and routing is account-derived
> (`mayRoute`), with no pairing step at all. See `relay/CLAUDE.md` for the
> current protocol.
> current protocol. The offline message queue in section 4 never shipped either:
> a frame for a disconnected peer is answered `PEER_OFFLINE` and dropped, and
> nothing is buffered or written to disk — which is what `/privacy` on the site
> states, so do not implement section 4 without changing that page first.

---

Expand Down
1 change: 1 addition & 0 deletions site/.env.example
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
PUBLIC_SITE_URL=https://antgrid.ai
PUBLIC_APP_URL=https://app.antgrid.ai
PUBLIC_WEB_URL=https://app.antgrid.ai
21 changes: 20 additions & 1 deletion site/astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,24 @@ export default defineConfig({
// the docs document, so only the nesting should need to change.
experimental: { fonts },
vite: { plugins: [tailwindcss()] },
integrations: [icon(), sitemap({ filter: (page) => !page.includes("/og-card") })],
integrations: [
// simple-icons is named explicitly because astro-icon otherwise assigns an
// installed collection `["*"]` and inlines the whole pack into the build's
// virtual module — 3,700 icons and ~4.7MB of source, to draw seven brand
// marks in Compat.astro. Collections left unnamed (tabler) keep `*`.
icon({
include: {
"simple-icons": [
"claudecode",
"openai",
"opencode",
"cursor",
"githubcopilot",
"kimi",
"mistralai",
],
},
}),
sitemap({ filter: (page) => !page.includes("/og-card") }),
],
});
3 changes: 3 additions & 0 deletions site/bun.lock

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

1 change: 1 addition & 0 deletions site/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"@fontsource-variable/archivo": "^5.3.0",
"@fontsource-variable/inter": "^5.3.0",
"@fontsource-variable/jetbrains-mono": "^5.3.0",
"@iconify-json/simple-icons": "^1.2.94",
"@iconify-json/tabler": "^1.2.0",
"astro": "^5.0.0",
"astro-icon": "^1.1.5"
Expand Down
6 changes: 6 additions & 0 deletions site/public/.well-known/security.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Contact: mailto:contact@radhaai.com
Contact: https://github.com/antgrid-ai/antgrid/security/advisories/new
Preferred-Languages: en
Canonical: https://antgrid.ai/.well-known/security.txt
Expires: 2027-06-30T23:59:59.000Z
Policy: https://github.com/antgrid-ai/antgrid/blob/HEAD/SECURITY.md
Binary file added site/public/og/control-plane.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions site/public/staticwebapp.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
{ "route": "/og/*", "headers": { "Cache-Control": "public, max-age=86400" } }
],
"mimeTypes": {
".txt": "text/plain",
".webmanifest": "application/manifest+json",
".svg": "image/svg+xml"
}
Expand Down
4 changes: 2 additions & 2 deletions site/scripts/shoot-og.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ await page.evaluate(() => document.fonts.ready);
// shipping: the card should show the run resolved, not caught mid-populate.
await page
.locator("#og")
.screenshot({ path: "public/og/one-screen.png", animations: "disabled" });
.screenshot({ path: "public/og/control-plane.png", animations: "disabled" });
await browser.close();
console.log("wrote public/og/one-screen.png");
console.log("wrote public/og/control-plane.png");
3 changes: 2 additions & 1 deletion site/src/components/Footer.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import Wordmark from "./ui/Wordmark.astro";
<Wordmark class="h-6 w-auto" />
</div>
<p class="mb-3.5 max-w-[230px] text-meta leading-relaxed text-muted">
Run your coding agents on your machine, steer them from your phone — and nothing counts as done without evidence.
Every agent you run, on every machine you own, in one control plane — and a gate on every phase when you arm Handler.
</p>
<span class="inline-flex items-center gap-1.5 rounded border border-edge px-2.5 py-1 font-mono text-marker text-muted2">
<Icon name="tabler:lock" class="text-signal" width="12" height="12" />end-to-end encrypted · zero-knowledge relay
Expand All @@ -25,6 +25,7 @@ import Wordmark from "./ui/Wordmark.astro";
<a href={links.download} class="hover:text-ink">Download</a>
<a href={links.getStarted} class="hover:text-ink">Get started</a>
<a href={links.support} class="hover:text-ink">Support</a>
<a href={links.security} class="hover:text-ink">Security</a>
</div>
</div>
<div>
Expand Down
16 changes: 13 additions & 3 deletions site/src/components/Seo.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,28 @@ interface Props {
ogImage?: string;
ogImageAlt?: string;
path?: string;
// Only ever set to keep a page OUT of the index (/404). Left undefined the tag
// is omitted entirely rather than emitted as "index, follow" — that is already
// the default, and a page that states it invites the question of which pages
// set it deliberately.
robots?: string;
}
// The two defaults describe the same file — a page overriding one must override
// the other, or the card ships someone else's alt text. The filename tracks the
// card's claim on purpose: scrapers cache og:image by URL and re-shooting in
// place leaves the superseded card in previews for as long as they hold it, so a
// recut that changes what the card SAYS gets a new name. Keep it in step with
// scripts/shoot-og.mjs, which writes it.
// scripts/shoot-og.mjs, which writes it. The superseded PNG stays in public/og
// even though nothing references it — that is the point of the scheme, not
// leftovers: a scraper still holding the old URL re-fetches it, and deleting the
// file turns every one of those cached previews into a broken image.
const {
title,
description,
ogImage = "/og/one-screen.png",
ogImageAlt = "Every agent. Every machine. One screen. Below, antgrid checking an agent's work against test output while you slept.",
ogImage = "/og/control-plane.png",
ogImageAlt = "Your machines. Your agents. One control plane. Below, antgrid checking an agent's work against test output while you slept.",
path = "/",
robots,
} = Astro.props;
const canonical = new URL(path, SITE_URL).href;
const ogUrl = new URL(ogImage, SITE_URL).href;
Expand All @@ -27,6 +36,7 @@ const ogUrl = new URL(ogImage, SITE_URL).href;
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title}</title>
<meta name="description" content={description} />
{robots && <meta name="robots" content={robots} />}
<link rel="canonical" href={canonical} />
{/* Keep in lockstep with --color-page in styles/global.css. */}
<meta name="theme-color" content="#101015" />
Expand Down
Loading