Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
b7c45d3
Update dart_terminal viewport fix (#65)
bharathm03 Sep 1, 2026
5d7c60c
Handler: a status frame answers for exactly one append (#64)
bharathm03 Sep 1, 2026
80de12a
feat: start an isolated session's agent alongside worktree.setup (#66)
bharathm03 Sep 1, 2026
af9a4bc
A dropped instruction owes the app the frame its record spends (#68)
bharathm03 Sep 1, 2026
101d180
Handler: confirm the undo that leaves this machine, band escalations,…
bharathm03 Sep 1, 2026
dafb22b
feat(app): markdown document viewer with heading outline, link routin…
bharathm03 Sep 1, 2026
032d85a
fix(app): bump webview_all to 1.4.1 for the Windows exit crash (#70)
bharathm03 Sep 1, 2026
1d0c80c
Handler: submit the line it types, and ask the agent before answering…
bharathm03 Sep 1, 2026
c21d091
A terminal resize the PTY never got is one the app must keep offering…
bharathm03 Sep 1, 2026
f871dce
Handler: retire notify-only, make the wrap-up durable, and unhook the…
bharathm03 Sep 1, 2026
db3fcad
The drawer's trailing glyphs share one column, and hover actions stop…
bharathm03 Sep 2, 2026
df96f21
A WS tunnel that loses its prefix is refused, not spliced (#78)
bharathm03 Sep 2, 2026
dbbc54a
Crash reporting for the bridge, and the two silent failures it uncove…
bharathm03 Sep 2, 2026
30c1ae2
Follow-up to #79: the DSN it shipped with cannot work, and the native…
bharathm03 Sep 2, 2026
85d7a29
Feat/git sync status (#77)
abinesh-balasubramaniyan Sep 2, 2026
bd5d810
Site: sell what the product does, capture founding-price interest, an…
bharathm03 Sep 2, 2026
7a97598
Handler belongs to the session, and a refused arm says why (#81)
bharathm03 Sep 2, 2026
fc0e415
release (#74) (#84)
bharathm03 Sep 2, 2026
b65b9f7
Record main as an ancestor (no content change, #84 was squashed)
bharathm03 Sep 2, 2026
4d9d3af
Merge pull request #85 from antgrid-ai/fix/record-main-ancestry
bharathm03 Sep 2, 2026
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
5 changes: 4 additions & 1 deletion .github/workflows/build-desktop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -277,11 +277,13 @@ jobs:
--target bun-darwin-arm64 \
--outfile bridge/dist/antgrid-bridge-arm64 \
--define 'process.env.LICENSE_API_URL="${{ env.LICENSE_API_URL }}"' \
--define 'process.env.SENTRY_DSN="${{ secrets.SENTRY_DSN_BRIDGE }}"' \
--define 'process.env.ANTGRID_BRIDGE_COMPILED="1"'
bun build bridge/src/index.ts --compile \
--target bun-darwin-x64 \
--outfile bridge/dist/antgrid-bridge-x64 \
--define 'process.env.LICENSE_API_URL="${{ env.LICENSE_API_URL }}"' \
--define 'process.env.SENTRY_DSN="${{ secrets.SENTRY_DSN_BRIDGE }}"' \
--define 'process.env.ANTGRID_BRIDGE_COMPILED="1"'
lipo -create -output bridge/dist/antgrid-bridge \
bridge/dist/antgrid-bridge-arm64 \
Expand Down Expand Up @@ -640,7 +642,7 @@ jobs:
run: |
$ErrorActionPreference = 'Stop'
New-Item -ItemType Directory -Force bridge/dist | Out-Null
bun build bridge/src/index.ts --compile --target bun-windows-x64 --outfile bridge/dist/antgrid-bridge.exe --define 'process.env.LICENSE_API_URL="${{ env.LICENSE_API_URL }}"' --define 'process.env.ANTGRID_BRIDGE_COMPILED="1"'
bun build bridge/src/index.ts --compile --target bun-windows-x64 --outfile bridge/dist/antgrid-bridge.exe --define 'process.env.LICENSE_API_URL="${{ env.LICENSE_API_URL }}"' --define 'process.env.SENTRY_DSN="${{ secrets.SENTRY_DSN_BRIDGE }}"' --define 'process.env.ANTGRID_BRIDGE_COMPILED="1"'

- name: Smoke test compiled bridge hook
shell: pwsh
Expand Down Expand Up @@ -844,6 +846,7 @@ jobs:
--target bun-linux-x64 \
--outfile bridge/dist/antgrid-bridge \
--define 'process.env.LICENSE_API_URL="${{ env.LICENSE_API_URL }}"' \
--define 'process.env.SENTRY_DSN="${{ secrets.SENTRY_DSN_BRIDGE }}"' \
--define 'process.env.ANTGRID_BRIDGE_COMPILED="1"'

- name: Smoke test compiled bridge hook
Expand Down
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
90 changes: 88 additions & 2 deletions app/lib/analytics/crash_reporting.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
import 'dart:io';

import 'package:flutter/foundation.dart' show kReleaseMode, visibleForTesting;
import 'package:path/path.dart' as p;
import 'package:path_provider/path_provider.dart';
import 'package:sentry_flutter/sentry_flutter.dart';

import '../util/ab_log.dart';

final _pathLike = RegExp(r'([a-zA-Z]:)?[\\/][^\s"]+');

String _redact(String input) => input.replaceAll(_pathLike, '<redacted-path>');
Expand Down Expand Up @@ -35,15 +42,22 @@ Object? _redactDeep(Object? value) {
// than mutated in place: 9.x makes absPath/contextLine settable, but
// preContext/postContext/vars stay getter-only, so a frame is the one object
// here that assignment alone can't neutralize.
//
// module/package are redacted alongside absPath because the frames
// sentry-native contributes carry an absolute module path there; on a Dart
// frame both are null, so it costs nothing. fileName deliberately is NOT, and
// that is where this diverges from the bridge's twin on purpose: a Dart frame's
// fileName is a `package:`/`dart:` URI naming our own source, and _pathLike
// would eat everything after its first slash — identity lost, nothing gained.
SentryStackFrame _scrubFrame(SentryStackFrame f) => SentryStackFrame(
absPath: _redactNullable(f.absPath),
fileName: f.fileName,
function: f.function,
module: f.module,
module: _redactNullable(f.module),
lineNo: f.lineNo,
colNo: f.colNo,
inApp: f.inApp,
package: f.package,
package: _redactNullable(f.package),
native: f.native,
platform: f.platform,
imageAddr: f.imageAddr,
Expand Down Expand Up @@ -136,6 +150,51 @@ SentryEvent? scrubCrashEvent(SentryEvent event) {
return event;
}

/// Whether this platform's native Sentry SDK is the sentry-native C library.
/// Only that one reads [SentryFlutterOptions.nativeDatabasePath]; the Apple and
/// Android SDKs pick their own location and ignore it.
@visibleForTesting
bool get usesNativeCrashDatabase => Platform.isWindows || Platform.isLinux;

/// Where sentry-native keeps its crash database, given the app's per-user
/// support directory.
@visibleForTesting
String nativeCrashDatabasePath(String supportDir) =>
p.join(supportDir, '.sentry-native');

/// Resolves a WRITABLE crash-database directory, or null to leave the SDK on
/// its own default.
///
/// `sentry_flutter` never assigns `nativeDatabasePath` itself, and sentry-native
/// then falls back to `.sentry-native` relative to the CURRENT WORKING
/// DIRECTORY. That is unwritable in exactly the configuration we ship on
/// Windows: a Store-launched MSIX gets `C:\Windows\System32` as its cwd, and
/// its own install dir under `WindowsApps` is read-only. `sentry_init` then
/// fails and native crash capture is absent with NO symptom to notice it by —
/// no handler process, no database, and (because auto-session-tracking is a
/// native option) no release-health sessions either, which is what would
/// otherwise have shown the pipeline was dead. Anchoring the path to the
/// support directory is what makes native capture work in a packaged build.
Future<String?> _resolveNativeDatabasePath() async {
if (!usesNativeCrashDatabase) return null;
try {
final dir = await getApplicationSupportDirectory();
return nativeCrashDatabasePath(dir.path);
} catch (e) {
// Crash reporting must never be the reason the app fails to start; the SDK
// falls back to the cwd-relative default, which is today's behaviour. Logged
// rather than swallowed because that fallback IS the bug this function
// exists to fix, and it has no other symptom — no handler, no database, no
// release-health session, nothing that looks like a failure.
AbLog.warn(
'crashReporting',
'support dir unresolvable; leaving nativeDatabasePath at the SDK default',
fields: {'error': '$e'},
);
return null;
}
}

Future<void> initCrashReporting({
required bool enabled,
required String dsn,
Expand All @@ -145,12 +204,39 @@ Future<void> initCrashReporting({
await runApp();
return;
}
final nativeDatabasePath = await _resolveNativeDatabasePath();
await SentryFlutter.init((options) {
options.dsn = dsn;
options.sendDefaultPii = false;
options.attachScreenshot = false;
// The SDK reports its OWN failures at debug level and nowhere else, so a
// release build that cannot initialise its native layer looks exactly like
// one that simply never crashed. Costly to leave on in production (every
// envelope is logged), so it is on everywhere else instead.
options.debug = !kReleaseMode;
if (nativeDatabasePath != null) {
options.nativeDatabasePath = nativeDatabasePath;
}
// attachViewHierarchy is @experimental and defaults to false; no explicit
// set needed.
options.beforeSend = (event, hint) => scrubCrashEvent(event);
// beforeSend is NOT the whole story on the platforms that have a native
// layer. A native crash is written and posted by that layer itself, and the
// C binding only ever sets dsn/release/database_path and friends — it never
// calls `sentry_options_set_before_send` — so nothing sent from there passes
// through the callback above. Breadcrumbs are the part of that envelope we
// still control: NativeScopeObserver mirrors the Dart scope down, and
// beforeBreadcrumb runs before the observers are notified, so scrubbing here
// is what keeps a path out of the copy the native layer holds. The frames
// and contexts of a native crash remain outside our reach by construction.
options.beforeBreadcrumb = (breadcrumb, hint) {
if (breadcrumb == null) return null;
breadcrumb.message = _redactNullable(breadcrumb.message);
final data = breadcrumb.data;
if (data != null) {
breadcrumb.data = Map<String, dynamic>.from(_redactDeep(data) as Map);
}
return breadcrumb;
};
}, appRunner: runApp);
}
48 changes: 48 additions & 0 deletions app/lib/demo/demo_transport.dart
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,26 @@ class DemoTransport extends BufferedAgentTransport {
case 'git:list-branches':
return <Map<String, Object?>>[kDemoGitBranches];

// No fixture curates a commit log, and fabricating SHAs/dates that
// nothing else in the demo can act on risks looking broken rather than
// read-only — an empty page (renders as "No commits yet") is the
// honest answer, consistent with every other git verb here refusing to
// mutate anything real. Answered explicitly rather than falling into
// the fire-and-forget default: unlike a mutation, the History tab is
// WAITING on this reply and would otherwise sit on its spinner for the
// full gitActionTimeout.
case 'git:log':
return <Map<String, Object?>>[
<String, Object?>{
'type': 'git:log-result',
'projectId': kDemoProjectId,
'checkoutId': 'main',
'commits': const <Object?>[],
'skip': message['skip'] as int? ?? 0,
'hasMore': false,
},
];

case 'git:checkout':
return <Map<String, Object?>>[
<String, Object?>{
Expand Down Expand Up @@ -381,6 +401,34 @@ class DemoTransport extends BufferedAgentTransport {
_gitFailure('git:unstage-result', files: message['files']),
];

case 'git:sync':
return <Map<String, Object?>>[
<String, Object?>{
..._gitFailure('git:sync-result'),
'op': message['op'] as String? ?? 'push',
'branch': kDemoBranch,
'failureKind': 'unknown',
},
];

// Non-zero counts on purpose: the demo should show the sync control in
// the state worth looking at, not greyed out with nothing to do.
case 'git:sync-status':
return <Map<String, Object?>>[
<String, Object?>{
'type': 'git:sync-state',
'projectId': kDemoProjectId,
'checkoutId': 'main',
'branch': kDemoBranch,
'remote': 'origin',
'remoteBranch': kDemoBranch,
'ahead': 2,
'behind': 1,
'hasUpstream': true,
'hasRemote': true,
},
];

case 'file:search':
return _search(
query: message['query'] as String? ?? '',
Expand Down
2 changes: 1 addition & 1 deletion app/lib/demo/fixtures/demo_workspace_fixtures.dart
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ library;

import '../demo_identity.dart';

const String kDemoBranch = 'feature/checkout-validation';
const String kDemoBranch = 'checkout';

/// Shared by the wire frame below and the New Session picker's branch catalog,
/// which cannot go to a bridge for the sample project's branches.
Expand Down
2 changes: 2 additions & 0 deletions app/lib/design/ab_icons.dart
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,8 @@ abstract final class AbIcons {
static const bell = Codicon.bell;
static const gitCommit = Codicon.git_commit;
static const gitBranch = Codicon.source_control;
static const gitPush = Codicon.repo_push;
static const gitPull = Codicon.repo_pull;
static const code = Codicon.code;
// Unchecked-state indicator for toggle rows (outline only, no fill).
static const circle = Codicon.circle_large_outline;
Expand Down
21 changes: 21 additions & 0 deletions app/lib/design/widgets/ab_branch_pill.dart
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ class AbBranchPill extends StatelessWidget {
super.key,
required this.branch,
this.ahead = 0,
this.behind = 0,
this.onTap,
});

final String branch;
final int ahead;
final int behind;
final VoidCallback? onTap;

@override
Expand Down Expand Up @@ -42,12 +44,31 @@ class AbBranchPill extends StatelessWidget {
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
// Default TextWidthBasis.parent reports the FULL Flexible share
// as this Text's width regardless of how short `branch` is, so
// a one-word branch still claims the whole cap and leaves a
// sibling with nothing to shrink into. longestLine reports the
// actual ink width instead — unchanged once ellipsis is
// actually clipping (that already fills the share).
textWidthBasis: TextWidthBasis.longestLine,
style: AbTokens.monoStyle(
fontSize: AbTokens.fontXs,
color: palette.textMuted,
),
),
),
// Behind before ahead, the order every SCM status line uses.
if (behind > 0) ...[
const SizedBox(width: AbTokens.space4),
Text(
'↓$behind',
style: AbTokens.monoStyle(
fontSize: AbTokens.fontXs,
color: palette.textMuted,
fontWeight: FontWeight.w500,
),
),
],
if (ahead > 0) ...[
const SizedBox(width: AbTokens.space4),
Text(
Expand Down
Loading
Loading