Skip to content

Add ticketing integrations#79

Merged
madarco merged 13 commits into
nightlyfrom
add-ticketing-integrations
Jun 7, 2026
Merged

Add ticketing integrations#79
madarco merged 13 commits into
nightlyfrom
add-ticketing-integrations

Conversation

@madarco

@madarco madarco commented Jun 7, 2026

Copy link
Copy Markdown
Owner

Note

Medium Risk
Touches relay RPC dispatch and optional credential carry into boxes; mitigated by default-off flags, write prompts, and strict allowlists (e.g. blocking linear auth token).

Overview
Adds relay-gated Notion and Linear integrations so in-box agents can call host-authenticated ntn / linear CLIs through the same read-passthrough / write-prompt model as git and gh, without service tokens in the box.

New @agentbox/integrations holds connector descriptors (ops, allowlists, pre-flight guards like GET-only Notion api and query-only Linear GraphQL). The relay dispatches integration.<service>.<op> on docker and cloud; agentbox-ctl integration exposes ops from those descriptors. In-box ntn/notion and linear shims forward allowlisted commands; shims are staged into box images and provider runtime bundles.

Config and doctor: integrations.notion.enabled and integrations.linear.enabled (default false), with full dotted-path support in parse/merge/write and a fix to agentbox config get for keys like integrations.notion.enabled. agentbox doctor gains an integrations: group (registry-driven, new info status for disabled integrations) and shows that group when doctor -p <provider> is used.

Nested-box relay hosts: optional carry: entries ship Notion file-auth and Linear credentials.toml for e2e on boxes that spawn nested boxes. User-facing and internal docs cover prerequisites, security, and CLI behavior.

Reviewed by Cursor Bugbot for commit 3bd3c8f. Configure here.

madarco added 11 commits June 6, 2026 12:35
Generalize the gh relay pattern (in-box ctl -> host relay -> host-authed
CLI with read/write classification + write gating) into a reusable
connector model so future ticketing integrations (Notion, Linear, Trello,
ClickUp) all share one spine. Box never holds a service token; host runs
the real CLI.

- New package @agentbox/integrations: IntegrationConnector / IntegrationOp
  descriptor model, registry (getConnector / ALL_CONNECTORS), and a
  Notion descriptor wrapping ntn. Conservative starter allowlist: api
  (GET passthrough, refuseCall mirrors gh.api's -X/-f detection so the
  read classification stays honest) plus page.create / page.update /
  comment.add (write-gated). NOTION_KEYRING=0 forced via the connector
  env so ntn reads file-based auth on Linux boxes; harmless on macOS.

- packages/relay/src/integrations.ts: runHostIntegration +
  assertIntegrationReady + parseIntegrationMethod, plus
  refuseIntegrationCall and a mergeConnectorEnv namespace guard that
  prevents a descriptor from setting env vars outside <SERVICE>_*
  (returns a typed exit-78 envelope when violated).

- Generic integration.<svc>.<op> dispatch wired into BOTH
  packages/relay/src/server.ts (POST /rpc) AND
  packages/relay/src/host-actions.ts (cloud path) so docker, daytona,
  hetzner, vercel, and e2b all get it for free.

- packages/ctl/src/commands/integration.ts: builds one commander subtree
  per connector from its descriptor, each op calling postRpcAndExit
  through the existing relay-rpc transport. Registered next to ghCommand.

Tests (pure vitest, no docker/network): allowlist denies unknown ops;
api refuseCall blocks POST/PATCH/DELETE/-f field flags/--input on
docker AND cloud paths; reads bypass the prompt; writes enqueue an
askPrompt and a denied prompt yields exit 10; descriptor env outside
its SERVICE_ namespace surfaces as exit 78; cloud and docker paths
emit identical envelopes for unknown shape / unknown service / op
not on allowlist.

Out of scope (T2-T4): in-box notion shim, Dockerfile/stage-runtime
staging, hetzner/cloud install-script mirror, config flags, doctor
detection, docs site, nested-box e2e.
…flag (T2) (#74)

Lets agents in a sandbox type `notion …` / `ntn …` and have the call
routed through the host relay to the host's authenticated `ntn` CLI;
gated by a new typed config flag so the integration is opt-in.

Surface
- packages/sandbox-docker/scripts/ntn-shim — bash shim modeled on
  gh-shim. Strict allowlist: `whoami`, `api <endpoint>`, `pages create`,
  `pages update`. Anything else dies with a clear message. Installed at
  /usr/local/bin/ntn; /usr/local/bin/notion is a symlink to the same
  shim (per docs/integrations_backlog.md's per-service surface naming).

Image provisioning (all providers except daytona, which stays
shim-less to match its T1 gh/git decision)
- Dockerfile.box: COPY ntn-shim, chmod, symlink notion.
- apps/cli/scripts/stage-runtime.mjs: stage ntn-shim into the docker
  contextFiles + execBitFiles plus the hetzner / vercel / e2b file lists.
- Hetzner install-box.sh, Vercel provision.sh, E2B build-template.sh:
  install + symlink the shim.
- Each provider's runtime-assets.ts: map the shim name + remote /tmp
  path so the staged file gets uploaded into the prepare VM.

Config flag (opt-in by default)
- packages/config/src/types.ts: new `integrations.notion.enabled` key
  (UserConfig, EffectiveConfig, BUILT_IN_DEFAULTS, KEY_REGISTRY entry).
- parse.ts / load.ts / write.ts: parser, merger, and writer now walk
  N-level dotted leaves so the 3-level path is natural YAML
  (`integrations: { notion: { enabled: true } }`).

Relay gate (cross-provider)
- packages/relay/src/integrations.ts: `refuseIfIntegrationDisabled`
  re-reads the layered config per call (matches loadAutopauseConfig).
  Disabled → exit 65 with a config-set hint; no host process touched.
- Wired into both `handleIntegrationRpc` (server.ts, docker) and
  `runIntegrationRpc` (host-actions.ts, daytona/hetzner/vercel/e2b) per
  the "fix across all providers" rule. Order: refuseIntegrationCall
  first (op-level), then enablement gate, then readiness probe, then
  prompt. Same order on both providers — a malformed-args-to-disabled-
  integration call returns the same envelope shape.

Connector cleanup (T1 minimal change)
- packages/integrations/src/connectors/notion.ts: drop `comment.add`.
  `ntn` exposes no top-level `comment` subcommand; the only host path
  is `ntn api v1/comments -X POST -f …` which the T1 `api` op refuses
  (GET-only). No callers exist (T1 just merged), so a forward-only
  drop is cleaner than carrying dead surface. The shim refuses
  `notion comment add …` with a "deferred from T2" message. Comments
  tracked as a focused follow-up (need a Notion-API-aware payload
  translator that maps CLI flags to the structured POST body).
- Added `whoami` read op so `ntn whoami` doesn't widen the `api`
  allowlist. The connector already declared `api v1/users/me` as the
  T3 doctor probe — `whoami` reuses the same auth check via its
  dedicated host CLI subcommand.

Tests (vitest, no docker, no network)
- packages/ctl/test/gh-and-shims.test.ts: extended with NTN_SHIM cases
  mirroring the gh / git shim patterns.
- packages/config/test/merge-precedence.test.ts +
  set-unset-roundtrip.test.ts: cover the new 3-level cascade and the
  YAML pruning roundtrip.
- packages/relay/test/integrations.test.ts: workspace agentbox.yaml in
  beforeEach now flips the integration on; new tests for the disabled
  envelope and the injectable-loader unit shape.
- packages/relay/test/host-actions.test.ts: parity check for the cloud
  gate.

Docs
- docs/notion_backlog.md: T2 flipped to done with the comment-handling +
  gate-placement decisions and the comments-deferred note.

T3 (doctor + docs site) and T4 (nested-box e2e) remain.
…ocs (T3) (#75)

* feat(integrations): agentbox doctor reports each connector + Notion docs (T3)

Doctor now renders a registry-driven `integrations:` group alongside the
provider groups (no hardcoded 'notion' — Linear/Trello will light up
automatically when their descriptors ship). Per connector: probe the host
binary (`<hostBin> --version`) and auth (`<hostBin> <authArgs>`), surface
install/login hints from two new optional descriptor fields. A new `info`
CheckStatus rolls up like ok so a disabled-but-correctly-configured
integration never flips the overall doctor status to "warn".

The `-p <provider>` scoped path now includes the integrations group too —
otherwise users who scope doctor to one provider couldn't see whether
their Notion is enabled/installed/authed. The host probe deliberately does
NOT set NOTION_KEYRING=0 (a comment in doctor-checks.ts records why).

Docs (the bulk of T3, per the same-change rule):
- new docs/integrations.md — internal design/reference (descriptor model,
  relay dispatch flow, op surface, enable flag, doctor wiring, carry-based
  file-auth for nested boxes, open follow-ups).
- new apps/web/content/docs/integrations-notion.mdx — user-facing Fumadocs
  page; meta.json wired under a new ---Services--- section.
- apps/web/content/docs/configuration.mdx — new `## integrations` section
  documenting `integrations.notion.enabled`.
- apps/web/content/docs/cli.mdx — `agentbox doctor` sentence updated.
- docs/host-relay.md — new RPC method-family bullet for
  `integration.<service>.<op>`.
- docs/features.md — Notion integration bullet; `/rpc` line updated.
- docs/notion_backlog.md — T3 marked done with status log.

Unit test stubs a fake `ntn` on PATH and asserts the four status
transitions (disabled / missing / unauthed / authed). The live host probe
runs post-merge on the host.

* docs(integrations): fix user-facing 'ntn page' -> 'ntn pages' (match shim)

The in-box ntn-shim accepts 'ntn pages {create,update}' (plural) — that's
the real ntn CLI's subcommand name and what the shim's allowlist matches.
The docs incorrectly showed singular 'ntn page create' / 'ntn page update'
in 5 places; a user following them would be rejected by the shim.

Internal op names (page.create, page.update) and the wire method
(integration.notion.page.create) stay unchanged — they're the descriptor
keys, not the user-facing command form.
…argv fix (T4) (#76)

Two bugs surfaced by the live Notion e2e:

1. `agentbox config get integrations.notion.enabled` returned `<unset>`
   even though `config set` + `loadEffectiveConfig` worked. The CLI's
   `leafValue` / `rawLeafFromValues` helpers split on the FIRST dot only,
   so 3-level keys never reached the leaf. Replaced with a single
   `walkKey` helper that walks every segment (mirrors `readLeaf` in
   `packages/config/src/load.ts`). New regression test
   `apps/cli/test/config-get-nested.test.ts` covers the plain, `--json`,
   `--all`, and unset/default branches; all four fail without the fix.

2. The Notion connector's `buildArgv` produced singular `['page',
   'create', ...]` but real `ntn` uses plural `pages`
   (`api datasources files pages login logout whoami workers`). A live
   `notion pages create` through the host relay failed with
   `error: unrecognized subcommand 'page'`. Fixed in
   `connectors/notion.ts`; existing tests in `integrations/test/` and
   `relay/test/` updated to assert the new argv.

Live read e2e: `notion whoami` and `notion api v1/users/me` round-trip
through the in-box shim → host relay → Notion API with no prompt;
`notion api -X POST` and `--method PATCH` are refused via
`refuseApiNonGet` (exit 65); `printenv | grep -i notion` shows nothing in
the agent's env.

Closes T4. Nested-box e2e deferred — the in-box ctl daemon forwards to
the host relay, so a nested box's write still terminates at the host
relay's spawn, not exercising the connector code path differently;
tracked in docs/integrations.md.
…1) (#77)

* feat(integrations): linear connector + shim + config + doctor + tests (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): add Linear public + design + status docs (LT1)

- 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.

* fix(linear): correct subcommand names + harden refuseGraphqlNonQuery (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.

* fix(linear): harden GraphQL gate against Unicode/flag-shape bypasses (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.
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.
@vercel

vercel Bot commented Jun 7, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
agentbox-web Ready Ready Preview, Comment Jun 7, 2026 4:55pm

Request Review

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 3bd3c8f. Configure here.

if (method === 'GET') return null;
return refuse(
`only GET is proxied (use page.create / page.update for writes); detected method '${method}'`,
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Notion search via api blocked

Medium Severity

The Notion integration docs show ntn api v1/search with -f as a working in-box read, but refuseApiNonGet treats any -f / field flag as an implicit POST and exits 65 before the host runs ntn. Agents following the published search example get a refusal instead of results.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3bd3c8f. Configure here.

…ctor

The connector forced env { NOTION_KEYRING: '0' } on every relay->ntn spawn.
But mergeConnectorEnv applies a connector's env wherever the relay runs --
which for a normal docker box is the macOS host -- so it forced the host ntn
into file-auth mode. That disagreed with both the docs (`ntn login` -> keychain)
and `agentbox doctor` (probes keychain): a keychain-authed user got a green
doctor but a relay that couldn't find the token.

The env was only ever needed for the internal-dev nested-box path. Remove it so
the relay uses ntn's default (keychain on macOS) -- relay, doctor, and docs now
agree. Keep the generic env field + mergeConnectorEnv <SERVICE>_* namespace
guard (no connector uses it now). The nested-dev `NOTION_KEYRING=0 ntn login`
requirement is documented in docs/development.md.

Also fixes a pre-existing macOS test-portability bug: the env-guard test stubbed
/bin/true, which doesn't exist on macOS (-> ENOENT/127); use /usr/bin/true.

Docs updated: development.md (dev note), integrations.md, host-relay.md,
notion_backlog.md, linear_backlog.md, agentbox.yaml carry comment.
… via the GET-only api op

Bugbot (PR #79): the docs showed `ntn api v1/search -f` as a working in-box
read, but Notion search is POST /v1/search and refuseApiNonGet correctly
refuses any -f/field flag as a non-GET. Replace the search examples with real
GET reads (v1/users/me, v1/pages/<id>, v1/databases/<id>) and note that POST
endpoints (search, db-query) aren't proxied through the read-only api op. No
code change — the GET-only gate is behaving as designed.
@madarco madarco changed the base branch from main to nightly June 7, 2026 16:59
@madarco madarco merged commit 2419596 into nightly Jun 7, 2026
3 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