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
5 changes: 5 additions & 0 deletions apps/cli/scripts/stage-runtime.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ const execBitFiles = new Set([
'packages/sandbox-docker/scripts/gh-shim',
'packages/sandbox-docker/scripts/git-shim',
'packages/sandbox-docker/scripts/ntn-shim',
'packages/sandbox-docker/scripts/linear-shim',
'packages/sandbox-docker/scripts/chromium-resolver',
]);
const contextFiles = [
Expand All @@ -56,6 +57,7 @@ const contextFiles = [
'packages/sandbox-docker/scripts/gh-shim',
'packages/sandbox-docker/scripts/git-shim',
'packages/sandbox-docker/scripts/ntn-shim',
'packages/sandbox-docker/scripts/linear-shim',
'packages/sandbox-docker/scripts/chromium-resolver',
'packages/sandbox-docker/scripts/custom-system-CLAUDE.md',
'packages/sandbox-docker/scripts/claude-managed-settings.json',
Expand Down Expand Up @@ -101,6 +103,7 @@ const hetznerFiles = [
['packages/sandbox-docker/scripts/gh-shim', 'gh-shim', true],
['packages/sandbox-docker/scripts/git-shim', 'git-shim', true],
['packages/sandbox-docker/scripts/ntn-shim', 'ntn-shim', true],
['packages/sandbox-docker/scripts/linear-shim', 'linear-shim', true],
['packages/sandbox-hetzner/scripts/custom-system-CLAUDE.md', 'custom-system-CLAUDE.md', false],
['packages/sandbox-docker/scripts/claude-managed-settings.json', 'claude-managed-settings.json', false],
['packages/sandbox-docker/scripts/agentbox-codex-hooks.json', 'agentbox-codex-hooks.json', false],
Expand Down Expand Up @@ -138,6 +141,7 @@ const vercelFiles = [
['packages/sandbox-docker/scripts/gh-shim', 'gh-shim', true],
['packages/sandbox-docker/scripts/git-shim', 'git-shim', true],
['packages/sandbox-docker/scripts/ntn-shim', 'ntn-shim', true],
['packages/sandbox-docker/scripts/linear-shim', 'linear-shim', true],
['packages/sandbox-vercel/scripts/custom-system-CLAUDE.md', 'custom-system-CLAUDE.md', false],
['packages/sandbox-docker/scripts/claude-managed-settings.json', 'claude-managed-settings.json', false],
['packages/sandbox-docker/scripts/agentbox-codex-hooks.json', 'agentbox-codex-hooks.json', false],
Expand All @@ -164,6 +168,7 @@ const e2bFiles = [
['packages/sandbox-docker/scripts/gh-shim', 'gh-shim', true],
['packages/sandbox-docker/scripts/git-shim', 'git-shim', true],
['packages/sandbox-docker/scripts/ntn-shim', 'ntn-shim', true],
['packages/sandbox-docker/scripts/linear-shim', 'linear-shim', true],
['packages/sandbox-e2b/scripts/custom-system-CLAUDE.md', 'custom-system-CLAUDE.md', false],
['packages/sandbox-docker/scripts/claude-managed-settings.json', 'claude-managed-settings.json', false],
['packages/sandbox-docker/scripts/agentbox-codex-hooks.json', 'agentbox-codex-hooks.json', false],
Expand Down
29 changes: 19 additions & 10 deletions apps/cli/test/doctor-integrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,18 +80,25 @@ describe('doctor — integrations group', () => {
it('renders info / "disabled" when the flag is off (default)', async () => {
emptyPath();
const results = await integrationsChecks(disabled);
expect(results).toHaveLength(1);
const row = results[0]!;
expect(row.label).toBe('notion');
expect(row.status).toBe('info');
expect(row.detail).toBe('disabled');
expect(row.hint).toContain('integrations.notion.enabled true');
// One row per registered connector (notion, linear, …). All should
// surface as `info`/disabled when no flag has been flipped — disabling
// an integration is a setting, not a problem.
expect(results.length).toBeGreaterThanOrEqual(2);
for (const row of results) {
expect(row.status).toBe('info');
expect(row.detail).toBe('disabled');
expect(row.hint).toContain(`integrations.${row.label}.enabled true`);
}
const notion = results.find((r) => r.label === 'notion');
expect(notion).toBeDefined();
const linear = results.find((r) => r.label === 'linear');
expect(linear).toBeDefined();
});

it('renders warn / "not installed" when enabled but ntn is missing', async () => {
emptyPath();
const results = await integrationsChecks(enabled);
const row = results[0]!;
const row = results.find((r) => r.label === 'notion')!;
expect(row.status).toBe('warn');
expect(row.detail).toMatch(/not installed/);
expect(row.hint).toMatch(/install ntn/);
Expand All @@ -101,7 +108,7 @@ describe('doctor — integrations group', () => {
await stageStub();
delete process.env.NTN_TEST_AUTH;
const results = await integrationsChecks(enabled);
const row = results[0]!;
const row = results.find((r) => r.label === 'notion')!;
expect(row.status).toBe('warn');
expect(row.detail).toBe('not logged in');
expect(row.hint).toBe('ntn login');
Expand All @@ -111,7 +118,7 @@ describe('doctor — integrations group', () => {
await stageStub();
process.env.NTN_TEST_AUTH = 'ok';
const results = await integrationsChecks(enabled);
const row = results[0]!;
const row = results.find((r) => r.label === 'notion')!;
expect(row.status).toBe('ok');
expect(row.detail).toContain('ntn version 0.42.0');
expect(row.detail).toContain('authed');
Expand All @@ -122,6 +129,8 @@ describe('doctor — integrations group', () => {
const broken: IntegrationsConfigLoader = () =>
Promise.reject(new Error('malformed yaml'));
const results = await integrationsChecks(broken);
expect(results[0]?.status).toBe('info');
// Every row falls back to disabled (info), regardless of which connectors
// are registered — a broken config is treated as "not enabled".
for (const row of results) expect(row.status).toBe('info');
});
});
2 changes: 1 addition & 1 deletion apps/web/content/docs/cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ agentbox prepare -p hetzner
agentbox prepare -p docker --build
```

`install` is the first-run setup wizard (system check, pick a provider, log in, prepare its base image, install the host skill). `install cmux` pins a live `agentbox list` panel (all your boxes) to the [cmux](https://cmux.com) sidebar dock — see [cmux integration](/docs/integrations-cmux#the-agentbox-dock-right-sidebar). `doctor` diagnoses system and provider readiness — and reports each [service integration](/docs/integrations-notion) (host CLI installed? authed? enabled per project?). `prepare` builds base images or snapshots — omit `--provider` for status only.
`install` is the first-run setup wizard (system check, pick a provider, log in, prepare its base image, install the host skill). `install cmux` pins a live `agentbox list` panel (all your boxes) to the [cmux](https://cmux.com) sidebar dock — see [cmux integration](/docs/integrations-cmux#the-agentbox-dock-right-sidebar). `doctor` diagnoses system and provider readiness — and reports each service integration ([Notion](/docs/integrations-notion), [Linear](/docs/integrations-linear)): host CLI installed? authed? enabled per project? `prepare` builds base images or snapshots — omit `--provider` for status only.

<Callout title="TIP">`agentbox config get <key> --all` shows which layer each value comes from. See the full key reference in [Configuration](/docs/configuration).</Callout>

Expand Down
4 changes: 3 additions & 1 deletion apps/web/content/docs/configuration.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -232,14 +232,16 @@ See [browser and screen](/docs/browser-and-screen).

## integrations

Per-service toggles for relay-gated service integrations. Each integration is **disabled by default** — even when the host CLI is installed and authed, the box can't call out until you flip it on. The box never holds the service's token; reads pass through, writes prompt on the host. See [Notion](/docs/integrations-notion).
Per-service toggles for relay-gated service integrations. Each integration is **disabled by default** — even when the host CLI is installed and authed, the box can't call out until you flip it on. The box never holds the service's token; reads pass through, writes prompt on the host. See [Notion](/docs/integrations-notion) and [Linear](/docs/integrations-linear).

| Key | Type | Default | Meaning |
| --- | --- | --- | --- |
| `integrations.notion.enabled` | bool | `false` | proxy `ntn` calls from the box through the host relay; reads pass, writes prompt |
| `integrations.linear.enabled` | bool | `false` | proxy `linear` calls (`@schpet/linear-cli`) from the box through the host relay; reads pass, writes prompt; `auth token` is hard-rejected |

```bash
agentbox config set --project integrations.notion.enabled true
agentbox config set --project integrations.linear.enabled true
```

`agentbox doctor` reports a row per integration in a dedicated `integrations:` group: disabled (default), `ntn not installed`, `not logged in`, or `authed` — with a one-line hint for each non-`ok` state.
Expand Down
105 changes: 105 additions & 0 deletions apps/web/content/docs/integrations-linear.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: Linear
description: Let your box read and write Linear issues through the host's authenticated linear CLI — your API token never enters the box
---

AgentBox can proxy Linear calls from inside a box to the host's authenticated `linear` CLI (`@schpet/linear-cli`). The box agent can list and view issues, run filtered queries, and (with your approval for each write) create or update issues and post comments — without your Linear API token ever entering the box. Same model as `agentbox-ctl git push`, `agentbox-ctl git pr create`, and the [Notion integration](/docs/integrations-notion).

<Callout title="HOW IT WORKS">
The box runs a tiny `linear` shim. Calls go through `agentbox-ctl integration linear <op>` to the **host relay**, which runs the host's real `linear` and ships the result back. Reads pass straight through. Writes raise a one-line confirm in your terminal first.
</Callout>

## Prerequisites

The integration wraps [`@schpet/linear-cli`](https://github.com/schpet/linear-cli) (the `linear` binary, v2). Install it on the **host** (not in the box):

```bash
npm install -g @schpet/linear-cli
linear auth login # opens the browser, stores auth in ~/.config/linear/credentials.toml
```

Then verify with `agentbox doctor`:

```text
integrations:
[info] linear disabled (enable with `agentbox config set --project integrations.linear.enabled true`)
```

The integration is **off by default**, so even with `linear` installed the box can't call it until you opt in. Doctor's `info` line confirms `linear` is detected; flip the flag to graduate it to a usable state.

## Enable it for this project

```bash
agentbox config set --project integrations.linear.enabled true
```

`--project` scopes it to the current project (config file under `~/.agentbox/projects/<hash>/`). Drop `--project` for global. Run `agentbox doctor` again — the row should now read:

```text
integrations:
[ ok ] linear linear/2.0.0 (…) · authed
```

If you see `[warn] not logged in`, run `linear auth login` on the host. If you see `[warn] linear not installed`, the host install didn't put `linear` on `PATH`.

## What works inside the box

The in-box shim exposes a strict allowlist. Anything outside the list — including `linear auth token`, `linear issue delete`, and any of `project` / `cycle` / `milestone` / `label` / `document` / `schema` — is rejected with a clear message.

| In-box command | Class | What happens |
| --- | --- | --- |
| `linear whoami` (or `linear auth whoami`) | read | Passes through; prints the authed host user. |
| `linear issue list` | read | Lists issues for the authed user/team; pass through filters with `--`. |
| `linear issue mine` | read | v2-native "issues assigned to me" (replaces the older `issue list --me`). |
| `linear issue view <id>` | read | Detail view for one issue. |
| `linear issue query …` | read | Structured filter query. |
| `linear team list` | read | Lists teams. |
| `linear api '<query>'` | read | GraphQL query passthrough — `mutation` / `subscription` are **refused** (exit 65). `--variable key=@<path>` is also refused (exfiltration vector). |
| `linear issue create …` | write | **Prompts** the host for approval; on `y` the host runs `linear issue create` and ships back the result. |
| `linear issue update …` | write | **Prompts** the host for approval; covers status/title/etc. |
| `linear issue comment add …` | write | **Prompts** the host for approval; posts a comment. (Upstream uses `add`, not `create`.) |

Reads are unprompted; every write raises a one-line confirm in your attached terminal (or in `agentbox agent approvals` for orchestrators driving boxes headlessly — see [Background & parallel](/docs/background-and-parallel)).

```bash
# Inside the box — these all flow through the host relay:
linear whoami
linear issue mine
linear issue view ABC-42
linear api '{ teams { id name } }' # GraphQL query, passes
linear api --paginate '{ teams { id } }' # pre-positional flags work
linear issue create --title "Draft from the box" # prompts on the host
linear issue comment add ABC-42 --body "from box" # prompts on the host (v2 uses `add`)
```

<Callout type="warn" title="auth token is hard-rejected">
`linear auth token` PRINTS the raw API token to stdout. The shim explicitly refuses it with `'auth token' leaks the raw API key — refused. Use 'linear whoami' for identity.`, and the connector exposes no op that maps to it. The whole point of the integration is to keep the token on the host — proxying `auth token` would defeat that. Same for `auth login` / `auth logout` / `auth migrate` / `auth default` (host owns auth state).
</Callout>

## Security model

| Concern | What AgentBox does |
| --- | --- |
| Where the Linear token lives | **Host only** — in `~/.config/linear/credentials.toml`. The box has no access to it. (The carry block ships the file into nested-box relay hosts only — never to the agent's process env.) |
| What the box can do unprompted | **Reads only** (`whoami`, `issue list/view/query`, `team list`, `api` GraphQL **queries**). |
| What needs your approval | **Every write** (`issue.create`, `issue.update`, `issue.comment`), and **any `mutation` / `subscription` GraphQL operation** through `api` is refused outright with exit 65. |
| Where the approval lives | The host relay raises a confirm prompt; you answer in the attached terminal (`y` / `n`) or via `agentbox agent approve <id>` from an orchestrator. |
| Inside the box, does the agent ever see the token? | **No.** `printenv \| grep -i linear` inside a box returns nothing — only `AGENTBOX_RELAY_TOKEN`, which only authenticates to the box-local relay endpoint. |
| Destructive ops | `issue delete` / `team delete` / `team create` are **off the allowlist**. Start conservative; widen deliberately if a real flow needs them. |
| `auth token` | **Hard-rejected by the shim**, with no connector op exposing it. Three defenses (shim, connector allowlist, relay dispatch), all in series. |
| Auditability | Every approved write is logged as a relay event (visible via `/admin/events`, `agentbox agent`, the dashboard). |

The integration is **off by default** for every new project. You flip it on per project once you've installed and authed `linear` on the host.

<Callout title="WHY this shape">
The box is the untrusted side. Tokens in the box would survive `agentbox download`, leak into commits if the agent mishandles them, and undermine the entire sandbox premise. Keeping the token on the host and putting the gate at the host boundary is the same model AgentBox uses for `git push`, `gh pr create`, and the [Notion integration](/docs/integrations-notion) — one model, audited in one place.
</Callout>

## Limitations and roadmap

- **GraphQL query-only `api` passthrough.** `mutation` / `subscription` are refused; use the dedicated `issue.*` write ops instead. This guards against an agent slipping a write past the read classification.
- **No destructive ops.** `issue delete` / `team delete` / `team create` are off-list by default.
- **Allowlist starts conservative.** As real agent flows surface needs, the op set will widen — file an issue with the failing call if something's missing.
- **Trello / ClickUp** are still on the integrations roadmap; their connectors will appear in `agentbox doctor` the same way once they ship.

See also [CLI commands](/docs/cli) for `agentbox doctor`, [Configuration](/docs/configuration) for the `integrations.linear.enabled` flag, and [Background & parallel](/docs/background-and-parallel) for the host-action approval surface.
1 change: 1 addition & 0 deletions apps/web/content/docs/meta.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"integrations-cmux",
"---Services---",
"integrations-notion",
"integrations-linear",
"---Providers---",
"local-docker",
"hetzner",
Expand Down
Loading
Loading