Skip to content

feat(integrations): linear connector + shim + GraphQL gate + docs (LT1)#77

Merged
madarco merged 4 commits into
add-ticketing-integrationsfrom
agentbox/linear-t1
Jun 6, 2026
Merged

feat(integrations): linear connector + shim + GraphQL gate + docs (LT1)#77
madarco merged 4 commits into
add-ticketing-integrationsfrom
agentbox/linear-t1

Conversation

@madarco
Copy link
Copy Markdown
Owner

@madarco madarco commented Jun 6, 2026

Summary

Linear path of the @agentbox/integrations foundation, descriptor-only (no relay/ctl core changes — this is what validates the abstraction the Notion T1–T4 work built). Tracked in docs/linear_backlog.md LT1.

Connector + ops

  • packages/integrations/src/connectors/linear.ts — new descriptor:
    • Reads: whoami (→ linear auth whoami), issue.list, issue.mine (v2-native; replaces the dropped issue list --me), issue.view, issue.query, team.list, api (GraphQL passthrough).
    • Writes (gated): issue.create, issue.update, issue.comment (→ linear issue comment add@schpet/linear-cli v2 uses add, not create; the orchestrator brief was wrong and I caught it during review).
  • IntegrationService union widened to 'notion' | 'linear'.

GraphQL gate (refuseGraphqlNonQuery)

The api op is a query-only passthrough. The gate:

  • Refuses any positional whose first significant keyword (after whitespace + # … line-comment strip) is mutation or subscription (exit 65).
  • Consumes value-bearing flags (--variable, --variables-json) so a benign JSON payload starting with mutation isn't misread as the operation. Skips consumption when the next token itself looks like a flag, so --input defenses still fire.
  • Refuses --variable key=@<path> — linear-cli's @<path> syntax reads from a host file and sends contents as a GraphQL variable, an exfiltration channel. Refuses on =@ anywhere in the value (last-=-split parser safety).
  • Refuses sources prefixed with Unicode whitespace / BOM (U+FEFF, NBSP U+00A0, LSEP U+2028) — pre-fix the ASCII-only whitespace skip let those bypass.
  • Refuses positionals whose first significant char isn't { or an ASCII letter ('unparseable' shapes — defense-in-depth).
  • Narrows flag-skip from arg.startsWith('-') to arg.startsWith('--') so single-dash positionals like -mutation are inspected.

Auth-token exclusion (key security invariant)

linear auth token PRINTS the raw API key to stdout. Three defenses in series:

  1. Shim hard-rejects linear auth token with 'auth token' leaks the raw API key — refused. Use 'linear whoami' for identity. (exit 2). Same hard-reject for auth login/logout/migrate/default.
  2. Connector allowlist exposes no op that maps to auth token.
  3. Relay dispatch denies anything not on the connector allowlist.

Destructive ops (issue delete, team delete, team create) are off-list (start conservative, widen deliberately).

Shim + image staging

  • packages/sandbox-docker/scripts/linear-shim — strict allowlist mirroring ntn-shim. Installed at /usr/local/bin/linear (no symlink alias).
  • Staged across all five providers: stage-runtime.mjs (5 lists), Dockerfile.box COPY, packages/sandbox-{hetzner,vercel,e2b}/src/runtime-assets.ts + each provider's install/provision/build-template script. Daytona is shim-less by design.

Config + doctor

  • Typed flag integrations.linear.enabled (default false) added to UserConfig / EffectiveConfig / BUILT_IN_DEFAULTS / KEY_REGISTRY in packages/config/src/types.ts.
  • agentbox doctor: zero-line change — ALL_CONNECTORS drives integrationsChecks, so the Linear row appears automatically with the install/login hints from the connector descriptor.

Tests (pure, no docker / no network)

  • packages/integrations/test/registry.test.ts — registry resolves linear, op classification, argv shapes (including the comment add shape), refuseGraphqlNonQuery (mutation refused, query/anonymous allowed, leading whitespace + comments tolerated, value-bearing flag consumption, --variable key=@<path> refused, --variable foo=name=@<path> refused, Unicode-whitespace / BOM / NBSP / LSEP prefix refused, single-dash positional refused, unparseable shape refused, --input survives a --variable consume).
  • packages/ctl/test/gh-and-shims.test.tslinear-shim allowlist including the explicit auth token rejection, destructive-op refusals, issue mine + issue comment add forwarding, pre-positional api --paginate invocation.
  • apps/cli/test/doctor-integrations.test.ts — updated for multi-connector iteration.
  • packages/relay/test/{host-actions,integrations}.test.ts — updated the two existing "unknown service" tests (used linear; now use trello).

Docs (same change)

  • docs/integrations.md — full Linear section: connector descriptor, op surface, GraphQL gate, auth-token three-defense invariant, env/credentials notes.
  • apps/web/content/docs/integrations-linear.mdx (new) — public page mirroring the Notion mdx.
  • apps/web/content/docs/{meta.json,configuration.mdx,cli.mdx} — sidebar entry, config row, doctor pointer.
  • docs/host-relay.md — integrations bullet extended to cover the new linear ops + auth-token rejection.
  • docs/features.md — symmetrical "Linear integration" "what works today" bullet.
  • docs/linear_backlog.md — LT1 marked done with shipped-evidence status log. docs/integrations_backlog.md Session 2 plan updated for the corrected subcommand names. LT1 path is NOT yet marked "done" in integrations_backlog.md Status section — that closeout belongs to LT2 after the live Waldosai e2e.

Test plan

  • pnpm typecheck green
  • pnpm test green (495 + new linear-shim tests)
  • pnpm lint green
  • pnpm build green (no [stage-runtime] WARN missing source)
  • Manual shim smoke-test: whoami / auth whoami / issue mine / issue comment add forward correctly; auth token / issue delete / team create / team delete rejected; api --paginate <query> forwards
  • Live Waldosai e2e — LT2 (not run in LT1; deliberate)

Notes

The orchestrator brief specified issue comment create and the orchestrator's reading of the surface had a few other inaccuracies. Verified against the actual @schpet/linear-cli v2 source via the GitHub Contents API: auth-whoami.ts exists, issue-comment-add.ts is the v2 subcommand (create does not exist), issue-mine.ts is the v2-canonical "issues assigned to me" read. The connector, shim, tests, and docs all reflect the verified surface.


Note

Medium Risk
Touches security-sensitive integration boundaries (token leakage, GraphQL write bypass, host-file exfiltration) but reuses the existing Notion relay pattern with layered guards and broad unit test coverage; live e2e is deferred to LT2.

Overview
Adds a Linear service integration on the same relay-gated host-CLI model as Notion: a new connector in @agentbox/integrations, an in-box linear shim, opt-in integrations.linear.enabled, and packaging across Docker/Hetzner/Vercel/E2B.

Connector & relay surface (descriptor-only): Registers linearConnector with read ops (whoami, issue list/mine/view/query, team.list, query-only api) and gated writes (issue.create, issue.update, issue.comment → upstream comment add). The api op uses refuseGraphqlNonQuery to block mutations/subscriptions, --input, and --variable host-file loads (@path exfiltration). auth token and destructive/issue-admin commands stay off the allowlist; the shim hard-rejects token-printing and host-auth mutations.

Runtime & config: Stages linear-shim via stage-runtime.mjs, Dockerfile.box, and each cloud provider’s install/provision scripts and runtime-assets. Typed config defaults integrations.linear.enabled to false. agentbox doctor picks up Linear automatically via ALL_CONNECTORS; integration doctor tests now assert multi-connector rows.

Docs & tests: New integrations-linear doc page plus configuration/CLI/features/relay design updates; unit tests cover the connector registry, GraphQL gate, shim allowlist, and relay “unknown service” examples (now trello).

Reviewed by Cursor Bugbot for commit 7a914c1. Configure here.

madarco added 4 commits June 6, 2026 23:11
… (LT1, WIP)

Linear path of @agentbox/integrations, mirroring the Notion T1-T3 shape.
Descriptor-only: no relay/ctl core changes.

- packages/integrations: linear connector (whoami, issue {list,view,query,
  create,update,comment}, team list, api with refuseGraphqlNonQuery GraphQL
  mutation/subscription gate); widen IntegrationService union; register in
  ALL_CONNECTORS; unit tests for ops + GraphQL gate.

- packages/sandbox-docker/scripts/linear-shim: strict allowlist mirroring
  ntn-shim. Explicit hard-reject for 'auth token' (which would print the
  raw API key), auth login/logout/migrate/default, issue/team delete,
  team create. Installed at /usr/local/bin/linear (no symlink).

- Staging: stage-runtime.mjs, Dockerfile.box, install-box.sh,
  provision.sh, build-template.sh and runtime-assets.ts for
  hetzner/vercel/e2b all wire the shim.

- Config: integrations.linear.enabled typed flag (default false).

- Doctor: ALL_CONNECTORS-driven, lights up linear automatically.

- ctl tests: linear-shim allowlist + auth-token rejection; updated
  doctor-integrations test for multi-connector case; updated relay
  unknown-service tests to use trello (still unknown).

Docs follow in the next commit.
- docs/integrations.md: full Linear section covering the connector descriptor,
  op surface, refuseGraphqlNonQuery GraphQL gate, auth-token exclusion as a
  three-defense (shim + connector + relay) invariant, env / credentials notes.
- apps/web/content/docs/integrations-linear.mdx (new): public page mirroring
  the Notion mdx structure (prerequisites, enable flag, op table, security
  model, limitations); meta.json listing.
- apps/web/content/docs/configuration.mdx: integrations.linear.enabled row.
- apps/web/content/docs/cli.mdx: doctor pointer extended to Linear.
- docs/host-relay.md: integrations bullet covers the new linear ops + the
  auth-token rejection.
- docs/features.md: a Linear "what works today" bullet symmetrical to Notion.
- docs/linear_backlog.md: LT1 marked done with shipped-evidence status log.

Live e2e against Waldosai is LT2 — deliberately not run here.
…(LT1 review)

Findings from /simplify pass against the live @schpet/linear-cli v2 source:

- issue.comment buildArgv was 'issue comment create' — v2 uses 'add'
  (verified against src/commands/issue/issue-comment-add.ts). Fixed
  connector + shim + tests + docs. Wire op name 'issue.comment' is
  unchanged for stability.

- Added issue.mine read op (v2-native 'issues assigned to me'; the
  older 'issue list --me' path was dropped upstream and agents typing
  the README-recommended 'linear issue mine' would have been hard-
  rejected by the shim).

- Shim's 'api' positional check refused any leading flag, blocking
  valid invocations like 'linear api --paginate \"<query>\"'. Relaxed
  to require at least one arg; the relay's refuseGraphqlNonQuery
  still enforces query-only.

- refuseGraphqlNonQuery now consumes the value after --variable /
  --variables-json so a JSON payload starting with 'mutation' isn't
  misread as the GraphQL operation (false-positive refusal).

- NEW security guard: refuseGraphqlNonQuery refuses
  --variable key=@<path>. linear-cli's @<path> syntax loads the
  value from a host file and sends it as a GraphQL variable, which
  the box could echo back through the query response — an
  exfiltration channel. Refused in every split / glued / equals
  shape.

- Cleaned up stray </content></invoke> tool-call tags at EOF of
  docs/linear_backlog.md (leaked from a previous LLM-authored
  commit).

Docs (integrations.md design doc, integrations-linear.mdx public
page, host-relay.md, features.md) updated in lockstep — the op
table, in-box example commands, and bullet summaries all reflect
the corrected names and the new guards.

pnpm typecheck && pnpm test && pnpm lint && pnpm build all green.
…(LT1 review high)

Findings from /review high pass on the LT1 diff:

- firstGraphqlOperationKeyword now uses /\\s/ + an explicit BOM (U+FEFF)
  skip so a source like '\\uFEFFmutation { x }' or '\\u00A0mutation { x }'
  (NBSP) no longer bypasses the gate. Pre-fix the ASCII-only skip set
  returned null on these prefixes and the outer guard treated null as
  not-a-mutation.

- 'unparseable' sentinel: a positional whose first significant char isn't
  `{` or an ASCII letter is now refused at exit 65 with a clear message,
  instead of fail-OPEN. Real GraphQL sources always start with `query`,
  `mutation`, `subscription`, or `{` after whitespace + line-comment
  strip.

- refuseGraphqlNonQuery's flag-skip is narrowed from `arg.startsWith('-')`
  to `arg.startsWith('--')`. A bare `-` or `-mutation { x }` is now
  inspected as a positional (rejected as 'unparseable') instead of being
  silently treated as a flag.

- --variable / --variables-json consume-next branches refuse to swallow
  a following flag (`--input`, `--variable`, etc.). Pre-fix
  ['--variable', '--input=/etc/passwd'] silently bypassed the --input
  defense — explicitly written in for the day a future linear-cli adds
  the flag.

- variableValueIsFileLoad refuses on `=@` anywhere in the value, not just
  the suffix after the first `=`. Covers both first-`=`-split and
  last-`=`-split parser interpretations: `foo=name=@/etc/passwd` is now
  refused regardless of how linear-cli's own parser tokenizes the value.

- Doc drift: docs/linear_backlog.md's proposed-surface table still said
  'issue comment create' and 'issue.list a.k.a. mine' from the original
  brief — fixed to match the shipped connector (issue.mine is now a
  separate row, issue.comment maps to `comment add`). LT1 status-log op
  enumeration extended to include issue.mine and the new GraphQL guards.
  docs/integrations_backlog.md Session 2 plan likewise updated to say
  `comment add`.

New tests cover every fix path: the =@-anywhere split-safety, the
--variable-consuming-next-flag survival, three Unicode-whitespace prefix
shapes (BOM, NBSP, LSEP), single-dash positional refusal, and unparseable
shape refusal. pnpm typecheck && pnpm test && pnpm lint all green.
@vercel
Copy link
Copy Markdown

vercel Bot commented Jun 6, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
agentbox-web Ready Ready Preview, Comment Jun 6, 2026 11:43pm

Request Review

@madarco madarco merged commit 67ba180 into add-ticketing-integrations Jun 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant