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
22 changes: 21 additions & 1 deletion docs/integrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,10 +237,30 @@ agentbox config set --project integrations.linear.enabled true

Linear stores plaintext credentials at `~/.config/linear/credentials.toml` (keyring is opt-in, not used). Unlike `ntn` (which needs `NOTION_KEYRING=0` to read file-based auth on Linux boxes), `linear` already reads the toml on every host by default — so the connector declares **no** `env` block. `mergeConnectorEnv` would only allow `LINEAR_*` keys anyway. The `agentbox.yaml` `carry:` block ships the file into nested boxes that run their own relay.

### Verification / live e2e results

LT2 ran the integration against the live `waldosai` workspace from inside a real AgentBox box. Captured evidence:

- **`linear whoami` (read)** — round-trips through the in-box shim → host relay → host `linear` v2.0.0 → Linear API; returns `Workspace: waldosai … User: Marco D'Alia … Role: admin` with no approval prompt.
- **`linear issue mine/list --team WAL` and `linear team list` (reads)** — same path; exit 0, no prompt. `team list` returns `WAL Waldosai` (the team key used for the writes).
- **`linear api '{ viewer { id name email } }'` (read)** — the GraphQL passthrough returns the viewer JSON. `refuseGraphqlNonQuery` correctly classifies the `{ … }` shorthand as a query (`anonymous`) and lets it through.
- **`linear api 'mutation { … }'` (refused)** — exits 65 with `linear api: only GraphQL queries are proxied (use issue.create / issue.update / issue.comment for writes); detected operation 'mutation'`. Refused before any host process is spawned; reproduces through both the shim path and the direct `agentbox-ctl integration linear api` path (the gate lives in the connector, not the shim).
- **`linear auth token` (refused at the shim)** — exits 2 with `'auth token' leaks the raw API key — refused. Use 'linear whoami' for identity.`. The relay's op allowlist would also refuse it (no op maps to `auth token`); the shim is the first of three defenses.
- **Gated writes — three approve→succeed→ground-truth-read cycles.**
- `linear issue create --team WAL --title "agentbox LT2 e2e …" -d "…"` created **WAL-5** (URL `linear.app/waldosai/issue/WAL-5/…`). `linear issue view WAL-5` confirms title + description + Backlog state.
- `linear issue comment add WAL-5 -b "agentbox LT2 e2e comment via host relay (gated write)"` added the comment. `linear api '{ issue(id:"WAL-5") { … comments { nodes { body } } } }'` confirms the comment body matches verbatim.
- `linear issue update WAL-5 -s "Canceled"` moved the state. Post-update `linear issue view WAL-5` shows `**State:** Canceled`.
- **No agent-side credential** — `printenv | grep -E '^LINEAR'` inside the box returns nothing. The only token-shaped env var is `AGENTBOX_RELAY_TOKEN`. The carried `~/.config/linear/credentials.toml` sits on disk for the nested-box case (see below) but is not consumed during the primary e2e — the host's own `linear` reads its own `~/.config/linear/` host-side.
- **No source changes needed.** LT1's connector + shim + gate worked unchanged against the live host CLI — no LT4-style argv drift this round.

### Nested-box e2e — deferred, not blocking

The nested-box scenario (a box-inside-a-box running a `linear` op through this box's relay) was time-boxed in LT2 and deferred for the same architectural reason as Notion. The in-box `agentbox-ctl` daemon forwards `/rpc` to the HOST relay (`host.docker.internal:8787`), not to a relay running in this box — so a nested box's `linear issue create` would still terminate at the host relay's spawn, not in this box's daemon. That means nested-box e2e exercises the carry mechanics (already verified — `~/.config/linear/credentials.toml` present in this box) more than the connector's spawn path. Installing the real `linear` in this box would also break the primary e2e by shadowing: npm's global prefix here is `/usr`, so `npm i -g @schpet/linear-cli` lands the real binary at `/usr/bin/linear`, but the shim at `/usr/local/bin/linear` precedes it on `$PATH` and keeps winning resolution — the in-box agent would still hit the shim, and the daemon would need a separately-shaped PATH (or an absolute `hostBin` path) to reach the real binary, which is out of scope here. A future follow-up that lifts the relay into the box's daemon would change this; tracked under "Open follow-ups" below alongside the Notion entry.

## Open follow-ups

- **Trello / ClickUp** — see [`integrations_backlog.md`](./integrations_backlog.md). Each is a new descriptor + a small shim; no relay change. ClickUp will be the one custom REST connector (no good CLI on PyPI / npm).
- **`comment.add`** — deferred; needs a Notion-API-aware payload translator that maps CLI flags to the structured `POST /v1/comments` body.
- **Least-privilege tokens** — Notion capability toggles for the host token; Trello supports `scope=read` (when we add it); Linear personal keys inherit full user perms (OAuth-only for read-scope tokens). Document on each service's user-facing page.
- **Host-initiated tokens** — the relay already accepts `params.hostInitiated` and validates it against `HostInitiatedTokens` (scope + params-hash bound). The host-CLI mint path that issues those tokens isn't wired yet for integrations; once it is, a host-typed `agentbox-ctl integration notion page.create …` can skip the prompt by minting a token first (same shape as the existing `gh.pr.*` and `cp.*` host-initiated paths).
- **Nested-box e2e** — T4 in [`notion_backlog.md`](./notion_backlog.md). Verify the carry-based file-auth path against a real Notion workspace.
- **Nested-box e2e** — deferred for both Notion (T4) and Linear (LT2) for the same architectural reason (in-box `agentbox-ctl` forwards `/rpc` to the original host relay, so a nested-box's `/rpc` terminates at the host's spawn regardless). Lifting the relay into the box's daemon would change this — tracked here, not blocking either path. The carry-based file-auth mechanics are already verified (the carried files land at the expected per-service paths).
31 changes: 29 additions & 2 deletions docs/integrations_backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,33 @@ Manual e2e (docker first, then one cloud provider — follow CLAUDE.md "Testing"
agent's relay calls terminate at the host relay either way, so this
exercises the carry block more than the spawn-side; tracked, not
blocking).
- **Linear / Trello / ClickUp paths: NOT STARTED.** Each is a new descriptor
- **Linear path: COMPLETE (LT1–LT2 done, 2026-06-07).** Validated the
Notion-built abstraction: LT1 was descriptor-only (no relay/ctl core
change), and LT2 added zero source edits — the connector + shim + gate
worked unchanged against the live `waldosai` workspace. Shipped surface:
the `linear` connector with 9 ops (`whoami`, `issue.list/mine/view/query`,
`team.list`, `api` with `refuseGraphqlNonQuery` rejecting GraphQL
mutation/subscription + `--variable key=@<path>` host-file loads,
`issue.create/update/comment` as gated writes); `linear-shim` at
`/usr/local/bin/linear` with hard-rejects for `auth token` (raw API
key leak), `auth login/logout/migrate/default`, `issue/team delete`,
`team create`; `integrations.linear.enabled` typed config flag (default
false); `agentbox doctor` row (auto from `ALL_CONNECTORS`); cross-
provider staging (docker COPY, hetzner install-box.sh, vercel
provision.sh, e2b build-template.sh; daytona shim-less by design).
LT2 e2e captured live evidence: reads (`whoami`, `issue mine/list`,
`team list`, `api { viewer … }`) pass with no prompt; the GraphQL
mutation gate exits 65 with a clear refusal; the shim refuses `auth
token` with exit 2; **WAL-5** was created → commented → moved to
`Canceled` via three approve→succeed→ground-truth-read cycles; and
`printenv | grep -E '^LINEAR'` returns nothing inside the box.
**Deferred / follow-ups**: nested-box e2e (same architectural reason
as Notion — the in-box `agentbox-ctl` forwards `/rpc` to the original
host relay, not to a relay in this box; documented in
`docs/integrations.md`); host-initiated tokens for integrations
(same status as Notion). See [`linear_backlog.md`](./linear_backlog.md)
for full LT2 evidence.
- **Trello / ClickUp paths: NOT STARTED.** Each is a new descriptor
+ small shim; no relay/ctl core changes (the abstraction was validated by
Notion). ClickUp will be the one custom-REST connector (no good CLI).
Notion and re-confirmed by Linear). ClickUp will be the one custom-REST
connector (no good CLI).
70 changes: 69 additions & 1 deletion docs/linear_backlog.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ e2e. v2.0.0 surface (richer than the plan assumed):
- `pnpm typecheck && pnpm test && pnpm build` green → `/simplify` → `/review high`
→ PR into `add-ticketing-integrations` → fix bugbot → merge.

### LT2 — Live e2e against Waldosai + nested-box best-effort + closeout — **status: not started**
### LT2 — Live e2e against Waldosai + nested-box best-effort + closeout — **status: done (2026-06-07)**
- Orchestrator prep (host): rebuild + restart relay with LT1 merged; set
`integrations.linear.enabled=true` in host project config.
- Primary e2e from inside a box: `linear whoami` (read, no prompt) → `linear
Expand Down Expand Up @@ -136,3 +136,71 @@ e2e. v2.0.0 surface (richer than the plan assumed):
pointer, `docs/host-relay.md` bullet extension, `docs/features.md`
"what works today" bullet. Live e2e against the Waldosai workspace
is LT2 — deliberately not run in LT1.
- 2026-06-07: **LT2 shipped.** Live e2e against the `waldosai` workspace,
no code changes — the LT1 surface worked unchanged. Evidence captured
from inside an AgentBox box (in-box agent → host relay → host `linear`
v2.0.0 → Linear API):
- **Reads pass with no prompt.** `linear whoami` returns
`Workspace: waldosai … User: Marco D'Alia … Role: admin`. `linear
issue mine --team WAL --sort priority` and `linear issue list --team
WAL --sort priority` both exit 0 (empty result on `unstarted`, a
valid filtered read). `linear team list` returns `WAL Waldosai`
(UUID `09ca67e1-ccd7-499b-b2fa-63220d56ce08`). `linear api '{ viewer
{ id name email } }'` returns `{"data":{"viewer":{"id":"85d5fa14-…",
"name":"Marco D'Alia","email":"accounts@waldos.ai"}}}` — the
`refuseGraphqlNonQuery` predicate correctly classifies the `{ … }`
shorthand as a query and passes it.
- **GraphQL mutation refused locally.** `linear api 'mutation {
issueDelete(id: "x") { success } }'` exits 65 with `linear api: only
GraphQL queries are proxied (use issue.create / issue.update /
issue.comment for writes); detected operation 'mutation'` — refused
before any host process is spawned (verified via both the shim path
and the direct `agentbox-ctl integration linear api` path; the gate
lives in the connector, not the shim).
- **`linear auth token` refused at the shim.** Exits 2 with
`'auth token' leaks the raw API key — refused. Use 'linear whoami'
for identity.`. The relay's op allowlist would also refuse it (no op
maps to `auth token`); the shim is the first of three defenses.
- **Gated writes work end-to-end.** `linear issue create --team WAL
--title "agentbox LT2 e2e 20260607T000618Z" -d "…"` round-tripped
through the relay's `askPrompt` → orchestrator approve → host
`linear` → Linear API; created **WAL-5**
(https://linear.app/waldosai/issue/WAL-5/agentbox-lt2-e2e-20260607t000618z).
Ground-truth read via `linear issue view WAL-5` confirms title +
description + Backlog state. `linear issue comment add WAL-5 -b
"agentbox LT2 e2e comment via host relay (gated write)"` added the
comment (URL with `#comment-3e8fe4e2` fragment). Ground-truth `linear
api '{ issue(id:"WAL-5") { … comments { nodes { body } } } }'`
confirms the comment body matches. `linear issue update WAL-5 -s
"Canceled"` moved the state; the post-update `linear issue view
WAL-5` shows `**State:** Canceled` and the comment thread. Three
gated writes, three approve→succeed→ground-truth-read cycles.
- **No-token assertion.** `printenv | grep -E '^LINEAR'` returns
nothing (`(no LINEAR_* keys present)`). The only token-shaped env
var in the box is `AGENTBOX_RELAY_TOKEN`. The carried
`~/.config/linear/credentials.toml` is on disk (it's for the
nested-box scenario where THIS box would host a nested-box's relay)
but no agent process reads it during the primary e2e — the host's
own `linear` does, host-side, via its own `~/.config/linear/`.
- **Nested-box e2e — deferred, same architectural reason as Notion.**
The in-box `agentbox-ctl` daemon forwards `/rpc` to the original
host relay (`host.docker.internal:8787`), not to a relay running in
this box. So a nested box's `linear issue create` would still
terminate at the **original** host's relay spawn, not in this box's
daemon — exercising the carry mechanics, not a different connector
spawn path. Also: installing the real `linear` in this box would
shadow the shim — `npm i -g @schpet/linear-cli` lands the binary at
`/usr/bin/linear` (npm prefix here is `/usr`), but the shim at
`/usr/local/bin/linear` precedes `/usr/bin` on `$PATH` and keeps
winning resolution, so the in-box agent would still hit the shim
and the daemon would need a separately-shaped PATH (or an absolute
`hostBin` path) to reach the real binary — out of scope here. Documented in `docs/integrations.md` under "Linear →
Nested-box e2e — deferred, not blocking" mirroring the Notion
sub-section. The carry block + `mergeConnectorEnv` namespace guard
are validated by the LT1 unit tests; a real nested-box round-trip
would require lifting the relay into the box's daemon (cross-cutting
follow-up tracked under both connectors' "Nested-box e2e" notes).
- No source changes needed — LT1's connector + shim + gate worked
as-shipped against the live host CLI. The pre-merge unit tests
matched live behaviour exactly (no LT4-style `pages` vs `page`
drift).
Loading