feat(integrations): linear connector + shim + GraphQL gate + docs (LT1)#77
Merged
Merged
Conversation
… (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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Linear path of the
@agentbox/integrationsfoundation, descriptor-only (no relay/ctl core changes — this is what validates the abstraction the Notion T1–T4 work built). Tracked indocs/linear_backlog.mdLT1.Connector + ops
packages/integrations/src/connectors/linear.ts— new descriptor:whoami(→linear auth whoami),issue.list,issue.mine(v2-native; replaces the droppedissue list --me),issue.view,issue.query,team.list,api(GraphQL passthrough).issue.create,issue.update,issue.comment(→linear issue comment add—@schpet/linear-cliv2 usesadd, notcreate; the orchestrator brief was wrong and I caught it during review).IntegrationServiceunion widened to'notion' | 'linear'.GraphQL gate (
refuseGraphqlNonQuery)The
apiop is a query-only passthrough. The gate:# …line-comment strip) ismutationorsubscription(exit 65).--variable,--variables-json) so a benign JSON payload starting withmutationisn't misread as the operation. Skips consumption when the next token itself looks like a flag, so--inputdefenses still fire.--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).{or an ASCII letter ('unparseable'shapes — defense-in-depth).arg.startsWith('-')toarg.startsWith('--')so single-dash positionals like-mutationare inspected.Auth-token exclusion (key security invariant)
linear auth tokenPRINTS the raw API key to stdout. Three defenses in series:linear auth tokenwith'auth token' leaks the raw API key — refused. Use 'linear whoami' for identity.(exit 2). Same hard-reject forauth login/logout/migrate/default.auth token.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 mirroringntn-shim. Installed at/usr/local/bin/linear(no symlink alias).stage-runtime.mjs(5 lists),Dockerfile.boxCOPY,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
integrations.linear.enabled(defaultfalse) added toUserConfig/EffectiveConfig/BUILT_IN_DEFAULTS/KEY_REGISTRYinpackages/config/src/types.ts.agentbox doctor: zero-line change —ALL_CONNECTORSdrivesintegrationsChecks, 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 resolveslinear, op classification, argv shapes (including thecomment addshape),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,--inputsurvives a--variableconsume).packages/ctl/test/gh-and-shims.test.ts—linear-shimallowlist including the explicitauth tokenrejection, destructive-op refusals,issue mine+issue comment addforwarding, pre-positionalapi --paginateinvocation.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 (usedlinear; now usetrello).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.mdSession 2 plan updated for the corrected subcommand names. LT1 path is NOT yet marked "done" inintegrations_backlog.mdStatus section — that closeout belongs to LT2 after the live Waldosai e2e.Test plan
pnpm typecheckgreenpnpm testgreen (495 + new linear-shim tests)pnpm lintgreenpnpm buildgreen (no[stage-runtime] WARN missing source)whoami/auth whoami/issue mine/issue comment addforward correctly;auth token/issue delete/team create/team deleterejected;api --paginate <query>forwardsNotes
The orchestrator brief specified
issue comment createand the orchestrator's reading of the surface had a few other inaccuracies. Verified against the actual@schpet/linear-cliv2 source via the GitHub Contents API:auth-whoami.tsexists,issue-comment-add.tsis the v2 subcommand (createdoes not exist),issue-mine.tsis 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-boxlinearshim, opt-inintegrations.linear.enabled, and packaging across Docker/Hetzner/Vercel/E2B.Connector & relay surface (descriptor-only): Registers
linearConnectorwith read ops (whoami, issue list/mine/view/query,team.list, query-onlyapi) and gated writes (issue.create,issue.update,issue.comment→ upstreamcomment add). Theapiop usesrefuseGraphqlNonQueryto block mutations/subscriptions,--input, and--variablehost-file loads (@pathexfiltration).auth tokenand destructive/issue-admin commands stay off the allowlist; the shim hard-rejects token-printing and host-auth mutations.Runtime & config: Stages
linear-shimviastage-runtime.mjs,Dockerfile.box, and each cloud provider’s install/provision scripts andruntime-assets. Typed config defaultsintegrations.linear.enabledtofalse.agentbox doctorpicks up Linear automatically viaALL_CONNECTORS; integration doctor tests now assert multi-connector rows.Docs & tests: New
integrations-lineardoc page plus configuration/CLI/features/relay design updates; unit tests cover the connector registry, GraphQL gate, shim allowlist, and relay “unknown service” examples (nowtrello).Reviewed by Cursor Bugbot for commit 7a914c1. Configure here.