Notion integration + shared foundation
June 7, 2026 · View on GitHub
Live tracker for building the Notion integration and the shared
integrations foundation described in integrations_backlog.md.
Each task is one box → one PR into the add-ticketing-integrations feature
branch. Boxes work sequentially: each branches off the latest feature-branch
HEAD after the previous task merges (PRs stack cleanly, no conflicts).
Model recap (why this shape)
Mirror the gh relay model exactly: in-box notion shim → agentbox-ctl integration notion <op> → host relay → host's authenticated ntn CLI. Writes
gated by askPrompt; reads pass through; the box never holds a Notion token.
The host relay runs the host's ntn (keychain auth on macOS — plain ntn login).
For the internal-dev nested-box path, ntn creds are carried into the box as
file-based auth (NOTION_KEYRING=0 ntn login → ~/.config/notion/auth.json).
The connector does not force NOTION_KEYRING=0 (removed — see the
2026-06-07 status-log entry); the in-box nested path sets it manually. See
docs/development.md.
Reference implementations to copy: packages/relay/src/gh.ts,
packages/ctl/src/commands/gh.ts, packages/sandbox-docker/scripts/gh-shim,
packages/relay/src/prompts.ts, packages/relay/src/host-initiated.ts.
Per-box workflow (every task)
- Plan first — enter plan mode, produce a concrete plan, get it approved.
- Implement on a branch off the current feature-branch HEAD.
- Verify —
pnpm typecheck,pnpm test,pnpm build, plus the task's own verification (unit tests + a realagentbox-ctl integration notion …round-trip where applicable). Verify ground truth, not exit codes. /review highthen/simplify— apply findings.- File a PR into
add-ticketing-integrations(notmain). - Fix bugbot comments on the PR until clean.
- Merge, then the orchestrator moves to the next task in a fresh box.
Tasks
T1 — Shared foundation + Notion core plumbing ✅ done
The working vertical slice: agentbox-ctl integration notion <op> round-trips
through the relay to host ntn, with read/write classification + write gating.
packages/integrations/package:types.ts(IntegrationOp, IntegrationConnector),registry.ts(getConnector, ALL_CONNECTORS),connectors/notion.ts.- Notion ops (start minimal, allowlist-only): read
api(refuseUnsafeApiCall: GET to any endpoint, e.g.ntn api v1/users/me; plus read-by-POSTv1/search,v1/databases/{id}/query,v1/data_sources/{id}/querywith a-d <JSON>body — see the 2026-06-08 entry); writepage.create,page.update(archive/props),comment.add— all gated.
- Notion ops (start minimal, allowlist-only): read
packages/relay/src/integrations.ts:runHostIntegration,assertIntegrationReady, genericintegration.<svc>.<op>dispatch (reuseaskPrompt+HostInitiatedTokens). Connector forcesNOTION_KEYRING=0env.- Wire dispatch into both
packages/relay/src/server.ts(POST /rpc) andpackages/relay/src/host-actions.ts(cloud path — "fix across all providers"). packages/ctl/src/commands/integration.ts(built from descriptors) + register in the ctl entrypoint next toghCommand.- Unit tests: op read/write classification; allowlist denies unknown ops; dispatch gates writes (askPrompt called) and not reads; denied → exit 10.
T2 — In-box notion shim + image provisioning + config flags ✅ done
Make a box agent able to type notion … or ntn ….
packages/sandbox-docker/scripts/ntn-shim(gh-shim pattern: strict subcommand allowlist →agentbox-ctl integration notion <op> -- "$@"). Installed on PATH as/usr/local/bin/ntn;/usr/local/bin/notionis a symlink to it. Same shim for both invocations.- Staged:
contextFiles+execBitFilesinapps/cli/scripts/stage-runtime.mjsplus thehetznerFiles/vercelFiles/e2bFileslists; COPY'd inDockerfile.boxnext to thegh-shim/git-shimCOPY; mirrored intopackages/sandbox-hetzner/scripts/install-box.sh,packages/sandbox-vercel/scripts/provision.sh, andpackages/sandbox-e2b/scripts/build-template.sh(plus each provider'ssrc/runtime-assets.tsso the staged file gets uploaded). Daytona stays shim-less (matches its T1 gh/git decision). - Config: added
integrations.notion.enabled(default false) topackages/config/src/types.ts—UserConfig,EffectiveConfig,BUILT_IN_DEFAULTS, andKEY_REGISTRY. Parser/merger/writer were taught to walk 3-level nested keys (branch.subbranch.leaf) so the YAML stays natural. Set withagentbox config set --project integrations.notion.enabled true. - Gate placement: the relay (
refuseIfIntegrationDisabledinpackages/relay/src/integrations.ts, wired into BOTHhandleIntegrationRpcinserver.ts(docker) andrunIntegrationRpcinhost-actions.ts(cloud — daytona/hetzner/vercel/e2b) per the "fix across all providers" rule). One check covers every caller (shim /notionalias / directagentbox-ctl integration/ future host-initiated tokens) and re-reads the layered config per call so a flag flip takes effect without bouncing the relay (same approach asloadAutopauseConfig). Disabled → exit 65 with aagentbox config set …hint; no host process is touched. - Connector cleanup (minimal): the T1
comment.addop is dropped.ntnexposes no top-levelcommentsubcommand — the only host path would bentn api v1/comments -X POST -f …, which the T1apiop refuses (GET-only). The op also had no callers (T1 just merged, no shim yet), so a forward-only drop is cleaner than carrying dead surface through. The shim refusesnotion comment add …with a clear "deferred from T2" message; comments are tracked as a focused follow-up (will need a Notion-API-aware payload assembly that maps flag args to the structured POST body). Added awhoamiread op sontn whoamidoesn't have to widen theapiallowlist.
T3 — agentbox doctor detection + docs ✅ done
agentbox doctornow reports each integration in a dedicatedintegrations:group, driven offALL_CONNECTORS(no hardcoded'notion') so Linear/Trello light up here automatically when they land. Each row probes<hostBin> <versionArgs>(install check) and<hostBin> <authArgs>(login check) and surfaces install/login hints from new optionalIntegrationConnector.detect.installHint/loginHintfields (filled for the Notion connector). The doctor deliberately does NOT forceNOTION_KEYRING=0— on the host the keychain entry IS the credential, and the file-auth env override would make a keychain-authed user falsely show as "not logged in". A newinfoCheckStatusrolls up likeokso a disabled-but-configured integration never pushes the overall doctor status to "warn". Unit test (apps/cli/test/doctor-integrations.test.ts) stubs a fakentnon PATH and asserts the four transitions: disabled / missing / unauthed / authed.- Docs:
docs/integrations.md— new internal design/reference doc (descriptor model, relay dispatch flow, the read/write Notion op surface, the enable flag, doctor wiring, the carry-based file-auth path for nested boxes, open follow-ups).apps/web/content/docs/integrations-notion.mdx— new user-facing Fumadocs page (prerequisites, enabling, what works in the box, security model). Wired intometa.jsonunder a new---Services---section.apps/web/content/docs/configuration.mdx— new## integrationssection documentingintegrations.notion.enabled.apps/web/content/docs/cli.mdx—agentbox doctorsentence updated to mention the new group.docs/host-relay.md— new RPC method-family bullet forintegration.<service>.<op>(parser, allowlist, enable gate,refuseCall, readiness probe, host-initiated token short-circuit,askPromptfor writes, the<SERVICE>_*env namespace guard).docs/features.md— Notion integration bullet; the "Additional/rpcmethods" line updated to listgh.pr.*/integration.<svc>.<op>already in place.
T4 — Live e2e verification + bug-fixes-from-e2e + closeout ✅ done
- Primary e2e (real box → host relay → live Notion API):
notion whoamiandnotion api v1/users/mereturn the host bot identity with no prompt;notion api v1/comments -X POSTandnotion api ... --method PATCHare refused withnotion api: only GET is proxied (use page.create / page.update for writes); detected method 'POST'(exit 65).printenv | grep -i notionshows nothing in the box's process env — the agent never holds the credential. (The carried~/.config/notion/auth.jsonis a separate concern for nested-box use; the in-box AGENT itself sees no token in env.) - Two bugs surfaced and fixed in T4:
agentbox config getcouldn't read nested 3-level keys. The helpers inapps/cli/src/commands/config.tssplit on the FIRST dot only, sointegrations.notion.enabledresolved toeffective.integrations["notion.enabled"](undefined /<unset>) even whenconfig set+loadEffectiveConfigworked correctly. Fix: replaceleafValue/rawLeafFromValueswith a singlewalkKeyhelper that splits on ALL dots (mirrorsreadLeafinpackages/config/src/load.ts). New regression testapps/cli/test/config-get-nested.test.tscovers the plain,--json,--all, and unset/default cases; without the fix all four fail.- Connector
buildArgvused singularpagebut realntnispages(plural). Live evidence: anotion pages createcall through the host relay hit approval → spawnedntn page create→ failed witherror: unrecognized subcommand 'page'. tip: some similar subcommands exist: 'update', 'pages'(exit 2).ntn --helpconfirms the surface isapi datasources files pages login logout whoami workers. Fix:connectors/notion.tsnow builds['pages', 'create', …]/['pages', 'update', …]. Existing tests inpackages/integrations/test/registry.test.tsandpackages/relay/test/integrations.test.tsupdated to assert the correct argv.
- Live write round-trip (host relay → live
ntn pages create) was not re-run from this box, because the host relay was rebuilt with T3 code (pre-fixpageargv) before T4 started. Once T4 merges and the host relay rebuilds with the newpagesargv, the prompted write path will work end-to-end. The fix is validated by (a) the live failure mode matching the bug exactly, (b) thepages create --helphost probe showing the correct surface, (c) updated unit tests that pin the new argv. - Nested-box e2e (boxes-launch-boxes path): deferred. Docker-in-docker
is available but the base image isn't baked in this box, and the chain
for an in-box agent's
notionwrite would still terminate at the same HOST relay (not this box's daemon — the box daemon forwards to the host relay athost.docker.internal:8787), so it wouldn't actually exercise MY fixed code on the spawn side. The carry block inagentbox.yamlalready ships the file-auth into nested boxes (verified present at~/.config/notion/auth.jsonin this box). Real nested-relay isolation testing is tracked as a follow-up — see "Open follow-ups" inintegrations.md. integrations_backlog.mdupdated: Notion path marked complete through T4.
Status log
- 2026-06-06: Backlog created; host-side carry for
ntnfile-auth added toagentbox.yaml. Top-level box testing uses the host's keychain-authedntn. - 2026-06-06: T1 shipped —
@agentbox/integrationspackage with Notion descriptor,packages/relay/src/integrations.ts(host exec + readiness probe), genericintegration.<svc>.<op>dispatch wired into bothserver.ts(docker) andhost-actions.ts(cloud), andagentbox-ctl integrationcommand tree. PR pending. - 2026-06-06: T2 shipped —
ntn-shim+notionsymlink on PATH across docker/hetzner/vercel/e2b;integrations.notion.enabled(default false) added to the typed config (with nested-key support in parser/merger/ writer); host-side enable gate inhandleIntegrationRpcreturning exit 65 with a config-hint when disabled; connector cleanup (droppedcomment.add, addedwhoamiread op). Comments deferred to a focused follow-up — they need a Notion-API-aware payload translator that maps CLI flags to the structuredPOST /v1/commentsbody. - 2026-06-06: T3 shipped —
agentbox doctornow reports the newintegrations:group (registry-driven), withinfofor disabled and install/login hints sourced from the connector descriptor.IntegrationConnector.detectgained optionalinstallHint/loginHintfields (filled for Notion: install URL +ntn login). Unit test stubs a fakentnon PATH and verifies the four status transitions. Doctor's host probe does NOT setNOTION_KEYRING=0(a comment in the code records why). Public docs site + internal reference doc landed in the same PR: newdocs/integrations.md, newapps/web/content/docs/integrations-notion.mdx(Services section inmeta.json), config-key + doctor sentence in the publishedconfiguration.mdx/cli.mdx, new RPC method-family bullet indocs/host-relay.md, Notion entry indocs/features.md. T4 (nested- box e2e + carry-based file-auth verification) is the remaining task. - 2026-06-06: T4 shipped — live e2e from inside a box against the real
Notion API. Reads pass through with no prompt (
notion whoami,notion api v1/users/me),notion apicorrectly refuses non-GET methods (-X POST/--method PATCH→ exit 65 withrefuseApiNonGetmessage),printenv | grep -i notionshows nothing in the agent's env. Two bugs fixed: (1)config getcouldn't read 3-level nested keys becauseapps/cli/src/commands/config.tssplit on the first dot only — replaced with awalkKeyhelper that splits on all segments (mirrorsreadLeafinpackages/config/src/load.ts); regression testapps/cli/test/config-get-nested.test.tsadded; (2) connector built singular['page', 'create', …]argv but realntnispages(plural) — confirmed live by the host relay's spawn failing withunrecognized subcommand 'page', fixed inconnectors/notion.ts, existing tests inpackages/integrations/test/registry.test.tsandpackages/relay/test/integrations.test.tsupdated. Live write round-trip with the fix needs a host relay rebuild post-merge. Nested-box e2e deferred — the box-daemon → host-relay chain means an in-box agent's write still terminates at the host relay (not this box's daemon), so it wouldn't exercise the spawn-side fix from a nested box anyway; the carry block is verified present. - 2026-06-06: Live-write loop closed (orchestrator, post-#76-merge). The host
relay was rebuilt + restarted with the
pagesargv fix, then a real write was issued from inside a box:notion pages create --parent page:<id> --content '# agentbox write-verify <ts>'→ host approval gate fired → approved → a real child page was created in "Marco D'alia's Space" (ground-truth confirmed viantn api v1/pages/<new-id>→object: page, created by the integration bot), then archived (in_trash:true) to clean up. Notes for users: the realntn pages createflags are--parent page:<id>+--content <markdown>(no--title), and the shim already injects the--arg separator so callers must NOT add their own (a doubled--makesntnreject the flags). Notion path verified DONE end-to-end: reads pass through, writes are gated + create real pages on approval, the box holds no token. - 2026-06-07: Removed the forced
env: { NOTION_KEYRING: '0' }from the Notion connector. It was only ever needed for the internal-dev nested-box path, butmergeConnectorEnvapplies a connector'senvon the host relay spawn too — which forced the hostntninto file-auth mode and disagreed with both the docs (ntn login→ keychain) andagentbox doctor(probes keychain). Result: a keychain-authed user got a green doctor but a relay that couldn't find the token. With the env gone the relay usesntn's default (keychain on macOS) — relay, doctor, and docs now agree. The genericenvfield +mergeConnectorEnv<SERVICE>_*namespace guard stay (no connector uses them now). The nested-devNOTION_KEYRING=0 ntn loginrequirement moved todocs/development.md. Earlier status-log/task lines that say "the connector forcesNOTION_KEYRING=0" are superseded by this entry. - 2026-06-08:
ntn apiis no longer GET-only —refuseApiNonGetbecamerefuseUnsafeApiCall, which allows GET to any endpoint plus the read-by-POST endpointsv1/search,v1/databases/{id}/query,v1/data_sources/{id}/query(anchored regex; JSON body inline via-d <JSON>). Querying a database/data source and searching now work from a box. Two things drove the rewrite: (1) the old gate was written forgh-style-f/-Ffield flags thatntndoesn't have —ntninfers POST from-d/inlinepath=value/path:=jsonbodies, so the old gate silently classifiedntn api v1/pages -d '{…}'(a write) as "GET" and let it through — a write-bypass hole the new gate closes; (2)--input/--file(stdin / host-file body) stay refused. Thentn-shimapihandler now forwards argv verbatim (mirrors realntn: options may precede the path). Gate table inpackages/integrations/test/notion.test.ts.