From 23cb623da6e4b5494353811e9b3f2734ebc6ff60 Mon Sep 17 00:00:00 2001 From: Marco D'Alia Date: Sun, 7 Jun 2026 00:14:36 +0000 Subject: [PATCH] docs(integrations): linear live e2e + closeout (LT2) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit LT2 — verified the LT1 Linear surface against the live `waldosai` workspace end-to-end from inside a real AgentBox box (in-box agent → host relay → host `linear` v2.0.0 → Linear API). No source changes: the LT1 connector + shim + GraphQL mutation gate worked as-shipped. Captured live evidence: reads (`whoami`, `issue mine/list --team WAL`, `team list`, `api { viewer … }`) pass with no approval prompt; `linear api 'mutation { … }'` exits 65 via `refuseGraphqlNonQuery` (verified at both the shim path and the direct `agentbox-ctl integration linear api` path — the gate lives in the connector); `linear auth token` exits 2 at the shim with the raw-API-key-leak refusal. Three gated writes round-tripped through `askPrompt` → orchestrator-approve → host `linear` → Linear API: created **WAL-5**, added a comment, moved the issue to `Canceled`; each verified by a ground-truth read after. `printenv | grep -E '^LINEAR'` inside the box returns nothing — the only token-shaped env is `AGENTBOX_RELAY_TOKEN`. Nested-box e2e deferred for the same architectural reason as Notion (the in-box `agentbox-ctl` daemon forwards `/rpc` to the original host relay, so a nested box's writes terminate at the original spawn regardless), and because installing the real `linear` in this box would land at `/usr/bin/linear` (npm prefix `/usr`) — the shim at `/usr/local/bin/linear` precedes it on `$PATH` and keeps winning resolution, so the test setup would break the primary e2e. Marks the Linear path COMPLETE in `docs/integrations_backlog.md` (LT1+LT2 done) and flips LT2 to done in `docs/linear_backlog.md` with the full evidence log. Adds a "Nested-box e2e — deferred, not blocking" section to `docs/integrations.md` mirroring the Notion sub-section. --- docs/integrations.md | 22 +++++++++++- docs/integrations_backlog.md | 31 ++++++++++++++-- docs/linear_backlog.md | 70 +++++++++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 4 deletions(-) diff --git a/docs/integrations.md b/docs/integrations.md index f3a7c49..b1e736b 100644 --- a/docs/integrations.md +++ b/docs/integrations.md @@ -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). diff --git a/docs/integrations_backlog.md b/docs/integrations_backlog.md index 285653b..d19fe83 100644 --- a/docs/integrations_backlog.md +++ b/docs/integrations_backlog.md @@ -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=@` 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). diff --git a/docs/linear_backlog.md b/docs/linear_backlog.md index ec2b4d0..0164513 100644 --- a/docs/linear_backlog.md +++ b/docs/linear_backlog.md @@ -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 @@ -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).