Opt-in TAP mode — reach any service with no keys in the agent process - #34
Open
nanaknihal wants to merge 12 commits into
Open
Opt-in TAP mode — reach any service with no keys in the agent process#34nanaknihal wants to merge 12 commits into
nanaknihal wants to merge 12 commits into
Conversation
…scover + tap_call) With TAP_AGENT_KEY set, the agent reaches Linear, Notion, PostHog, and any other service connected to the team's TAP account (tap.human.tech) through the TAP credential proxy instead of holding API keys in this process: - tap_discover lists the credentials the agent may use, each with its approval policy and usage examples; tap_call makes one universal authenticated call (credential name + target URL + method + body). A new service needs no code here — an admin connects it in TAP and it is usable on the next message. A missing credential returns a prefilled setup link the bot relays in-channel. - No keys in the process: TAP injects each credential server-side and pins it to its own API host. The direct MCP connections are skipped in TAP mode, so none of their tokens need to exist here. - The stock write gate is preserved: a mutating tap_call emits the same confirm_write interrupt as the MCP interceptor before the request is sent (Linear GraphQL queries and Notion search/query POSTs count as reads; ambiguous calls count as writes). TAP's per-credential policy can additionally hold a call for a human approval, which the tool awaits. Off by default: without TAP_AGENT_KEY, behavior is unchanged. No new dependencies (stdlib urllib). tests/conftest.py clears ambient TAP_* env so the suite stays deterministic on machines where TAP is configured. Co-Authored-By: 0xZKnw <0xzknw@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…n surface Co-Authored-By: 0xZKnw <0xzknw@gmail.com> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Direct keys vs TAP mode is now a visible fork everywhere a deployer actually decides how services get connected, instead of a trailing doc section: - setup.md: 'Optional sources' opens with the two options side by side - Railway template: TAP_AGENT_KEY is a declared (preserved) variable, so the deploy screen shows the no-keys option next to LINEAR_API_KEY - first boot: when direct keys load, one line notes the TAP alternative; when nothing is configured, the previously silent return now says how to connect sources either way TAP stays opt-in and off by default; stock behavior is unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ions Setting TAP_AGENT_KEY no longer disconnects every direct MCP integration. The tool set is the union: a service whose key is present keeps its direct MCP connection (the deployer's explicit choice), and tap_discover/tap_call cover everything else with no key in the process. The prompt nudges the model to prefer a service's direct tools when both exist, purely so the same action always takes the same path (a write should not be TAP-held on some turns and not others). Boot log states which services are direct and that the rest go through tap_call. This is the incremental adoption path: move one sensitive service behind TAP by deleting its key, keep low-stakes keys direct, no all-at-once migration. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Security: - Origin-check every human-facing TAP link before the model may relay it (credential setup links and approval links): only tap.human.tech, its subdomains, or the deployment's own proxy host survive; anything else is stripped with a warning. Closes the injected-model / hostile-upstream link-swap phishing seam. - Treat a body-less GraphQL POST as a write (the query could ride in the URL); ambiguous still means confirm. - Reject malformed header names (RFC 7230 token check) so no normalization quirk can smuggle a reserved X-TAP-* header. - Require https for TAP_PROXY_URL except toward loopback — that URL receives the agent key. - Docs now say plainly the in-channel gate is UX, TAP policy is the enforced boundary. Usability: - Surface the approval link + txn_id from TAP's 202 instead of discarding them, and add a tap_check_approval tool so a late approval is retrievable (timeout message says exactly what to do). - Transport failures (connection refused, DNS, timeout) return a corrective proxy-unreachable message instead of crashing the turn. - Default Content-Type: application/json when a body is present — urllib's form-urlencoded default 400ed Linear GraphQL reads. - Boot-time TAP connectivity probe: startup log now shows available credential names, or a loud bad-key/unreachable diagnosis. - Poll 404-after-202 reads as expired/resolved, not failure; an approved call with a failed upstream is reported as such. - Prompt guidance for 401/403 (deployer problem, don't retry) and for who the setup link is for. Docs/funnel: - UTM parameters on all tap.human.tech links (readme/setup/env/docs). - .env.example and setup.md Option B lead with the benefit (no pasted keys, injected agent can't leak what it never held) + free tier. - Cut the stale pricing figure; note PostHog read-only parity difference; document TAP_APPROVAL_TIMEOUT=0; railway.ts comment now matches the compose model and preserves TAP_PROXY_URL/TIMEOUT. 85 agent tests pass (18 new). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A held call blocked the agent's turn for the full TAP_APPROVAL_TIMEOUT (300s). That both leaves the chat user staring at a typing indicator and overruns the runtime's HTTP body timeout — undici defaults to 300s, and the stock runtime crashed with UND_ERR_BODY_TIMEOUT against the 300s wait in live testing. 60s keeps fast approvals synchronous; past the deadline the tool returns the approval link + txn_id and the outcome is retrieved with tap_check_approval. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Generic tap_discover/tap_call turns take more graph steps than curated MCP tools (discover, then compose the raw call, then corrective retries), and the stock limit of 25 tripped mid-answer in live testing. 50 in TAP mode; stock stays at 25. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…esume
confirm_write interrupts mid-step. With parallel tool calls, the
interrupt freezes the step before the sibling call's output is
recorded, and the resumed conversation fails the Responses API's
bookkeeping ('No tool output found for function call ...') — observed
live on the first gated tap_call write. Single-call steps sidestep the
class; this also protects the stock MCP write interceptor, which
interrupts the same way.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The AG-UI layer constructs each run's RunnableConfig itself, so the graph-level with_config recursion_limit never reaches LangGraph — runs were failing at the stamped default of 25 despite the 50-step TAP-mode budget. LangGraphAGUIAgent accepts a config merged into every run; setting the limit there makes it actually apply. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
A TAP-held write previously collected two approvals from the same human: the in-channel confirm card, then TAP's server-side approval. tap_call now evaluates the credential's declared policy rules (the same require-over-auto, url-over-method semantics TAP documents, mirrored in _pattern_matches/_tap_will_hold) and skips the card exactly when TAP will hold the call — the enforced gate is the single gate. Auto-approved routes keep the in-channel card, so every write still gets one human approval. Any doubt (proxy unreachable, unknown credential, malformed rules) fails closed to showing the card. An active time-boxed grant may auto-approve a call predicted as held; that is TAP's own semantics — the grant is a human-authored pre-approval. 19 new tests: matcher table, rule-precedence cases, fail-closed variants, and the three tap_call behaviors (skip card when held, card when auto-approved, card when rules unavailable). 104 total pass. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Upstream added a GitHub MCP direct integration and renamed the deploy section; TAP-mode copy updated accordingly (GitHub joins the composable direct sources; example list of no-integration services adjusted). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…erate it as a list TAP renders /agent/services URL-override rules with "methods": "ANY" (a string), while method rules carry a list. Iterating the string yielded characters, silently dropping every URL-override rule from _tap_will_hold. For a credential with an auto-approve URL override plus default method gating, that mispredicted "held" — skipping the in-channel card for a call TAP then auto-approved: a write with no human gate anywhere. _rule_covers_method now handles string-"ANY", list, and list-containing-"ANY"; the precedence tests mirror the real rendered shape instead of a fabricated list form. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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.
Opt-in TAP mode — reach any service with no keys in the agent process
What this adds
An optional, off-by-default way to connect OpenTag's sources without
holding service API keys in the agent process. Set one env var
(
TAP_AGENT_KEY) and the agent reaches GitHub, Linear, Notion, PostHog —and any other service the team connects — through the
TAP
credential proxy: the secret is injected server-side, pinned to that
service's own API host, and every call is audited.
Without
TAP_AGENT_KEY, nothing changes. No new dependencies (stdliburllib only). It composes per service with the existing integrations: any
key still set in
.envkeeps its direct MCP connection; each key you leaveout moves that service behind TAP. No all-at-once migration.
# .env — instead of LINEAR_API_KEY / NOTION_MCP_AUTH_TOKEN / POSTHOG_PERSONAL_API_KEY / … TAP_AGENT_KEY=tap_...Why
OpenTag's
confirm_writecard is good conversation UX, but the servicekeys still live in the agent process — one prompt injection away from
env | grep -i key. TAP mode removes the thing that can leak, and makesthe write gate enforced rather than advisory where it matters:
tap_discover+tap_call) cover every service without a direct MCP connection here.Connect a new service in the TAP dashboard and the bot can use it on the
next Slack message.
agent has no key to leak and nowhere else to send one — the credential is
injected server-side and destination-pinned to the service's own host.
tap_callreads thecredential's declared TAP policy: when TAP will hold the call for an
approver, that enforced server-side approval is the single gate (the
in-channel card is skipped); when TAP auto-approves the route, the usual
confirm_writecard appears. Any doubt falls back to showing the card.and the bot replies with an origin-verified setup link — the secret goes
into the TAP dashboard, never into chat.
What it looks like
Boot, with no service keys anywhere in the process:
Security notes (reviewed adversarially before submission)
Destination pinning and SSRF guards are server-side.
origin-checked before the model may relay them; anything that isn't
TAP's origin is stripped. This closes the "injected model swaps the setup
link for a phishing page" seam.
x-tap-*headers are dropped (spoof-tested); header namesare validated against the RFC 7230 token charset.
body-less GraphQL POSTs are treated as writes.
TAP_PROXY_URLrequires https except toward loopback (it receives theagent key).
corrupts the Responses API's bookkeeping on resume (found in live
testing; this also protects the stock MCP write interceptor, which
interrupts the same way).
Testing
cd agent && uv run pytest); TAP mode addscoverage for the policy-rule matcher and precedence, spoofed-header
rejection, link origin validation, approval poll states (approved /
denied / expired / pending), fail-closed fallbacks, and the read/write
split table. All hermetic — no network.
approved from the TAP dashboard, the lazy credential-setup flow, denials
failing closed, and a time-boxed approval grant.
Files
agent/tap_tools.py—tap_discover,tap_call,tap_check_approval(stdlib-only)
agent/prompts/tap.py— system-prompt addendum (only added in TAP mode)agent/internal_sources.py— per-service compose + boot diagnosticsagent/agent.py,agent/main.py— single-call steps; recursion budgetcarried into the AG-UI per-run config (with_config was being dropped)
docs/tap.md,setup.md,README.md,.env.example,.railway/railway.ts— setup docs, presented as a per-service choice atthe credential step
Demo video to follow in a comment.
🤖 Generated with Claude Code