Add ticketing integrations#79
Merged
Merged
Conversation
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.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ 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}'`, | ||
| ); |
There was a problem hiding this comment.
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)
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.
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.


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/linearCLIs through the same read-passthrough / write-prompt model asgitandgh, without service tokens in the box.New
@agentbox/integrationsholds connector descriptors (ops, allowlists, pre-flight guards like GET-only Notionapiand query-only Linear GraphQL). The relay dispatchesintegration.<service>.<op>on docker and cloud;agentbox-ctl integrationexposes ops from those descriptors. In-boxntn/notionandlinearshims forward allowlisted commands; shims are staged into box images and provider runtime bundles.Config and doctor:
integrations.notion.enabledandintegrations.linear.enabled(default false), with full dotted-path support in parse/merge/write and a fix toagentbox config getfor keys likeintegrations.notion.enabled.agentbox doctorgains anintegrations:group (registry-driven, newinfostatus for disabled integrations) and shows that group whendoctor -p <provider>is used.Nested-box relay hosts: optional
carry:entries ship Notion file-auth and Linearcredentials.tomlfor 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.