Connections

What the agent can reach, what it cannot, and the card it shows when it needs something connected — with the reason, and a way to answer.

6 min read
19 sections
Edit this page

A connector without a credential used to fail mid-run and return a string saying so. A missing connection is not an error for the agent to work around — it is a decision for you.

python
from shipit_agent import ConnectionRegistry

registry = ConnectionRegistry(credential_store=store, tools=agent.tools)
print(registry.render())
text
✓ Slack — connected
      tools: slack
  · BigQuery — not connected
      tools: warehouse
      → Connect BigQuery — it needs you to sign in.
  ⧗ Gmail — expired
      → Reconnect Gmail — its token has run out.

What it looks like

Revenue analysis Live
Total last quarter's bookings by region from the warehouse.
Checked connections2 connected · 1 needs action1 tool · 0.4ms
BigQuery — analytics.usage Read the bookings tables to total revenue by region. warehouse · sign in Not nowConnect
I'll pick this up as soon as the warehouse is connected — everything else is done.
4,102 tokens · claude-opus-5

The reason is the card. Somebody deciding whether to hand over an account is answering why, not what.


Five states

StateMeansWhat to tell the user
CONNECTEDUsable now
DISCONNECTEDNothing configured"Connect it"
NEEDS_AUTHConfigured, credential missing"Sign in"
EXPIREDWas connected, token ran out"Reconnect it"
ERRORConfigured and failing otherwiseThe error

EXPIRED is deliberately distinct from DISCONNECTED. Telling someone to set up something they already configured is how trust in a status display dies.


The agent asking

python
agent.run("Post the release note to #eng.")

The connections tool takes action="request" with a reason, and a request without one is refused — the reason is what a user reads in order to decide.

The run then carries an event:

json
{"type": "connection_requested",
 "payload": {"connection_id": "warehouse",
             "title": "BigQuery — analytics.usage",
             "reason": "Read the usage tables to total revenue by region.",
             "auth": "oauth",
             "tool": "connections"}}

Which every surface draws:

  • Live panel — a card with the title, the reason, and Not now · Connect
  • Tree — a Connection needed branch
  • Timeline — a connection_required step

The card says sign in, paste an API key or paste a token according to the connection's auth kind, because those are different actions for the person answering.

The agent is told to keep working on anything that does not need it, and not to retry the connector until it is connected.


Answering

python
registry.resolve("warehouse", accepted=True, credential="wh-key-123", by="you")

On accept with a credential, it is written to the credential store — so the very next state check reads connected rather than asking again for something you just gave it.

python
registry.resolve("warehouse", accepted=False)     # denied, and closed

A denial closes the request too. The agent must not keep asking.

Credentials are accepted in whichever shape you have:

python
registry.resolve("slack", accepted=True, credential="xoxb-…")
registry.resolve("slack", accepted=True, credential={"token": "…", "team": "acme"})
registry.resolve("slack", accepted=True, credential=CredentialRecord(...))

Wiring a UI

python
@app.post("/connections/{connection_id}")
def answer(connection_id: str, body: AnswerBody):
    registry.resolve(
        connection_id,
        accepted=body.accepted,
        credential=body.credential,
        by=current_user.email,
    )

The pending queue is readable at any time:

python
for request in registry.pending_requests():
    print(request.title, "—", request.reason)

What the registry knows

It is built from what the agent actually has:

  • Connectors — any tool carrying a credential_key (17 built-in SaaS connectors do). Tools sharing one key — the Google family — collapse into one connection listing all of them.
  • MCP servers — attached servers and their state.
python
registry.all()            # every connection and its state
registry.connected()      # the usable ones
registry.needing_action() # the ones a human must fix
registry.is_connected("slack")
registry.summary()        # counts, for a header

The MCP catalog — servers you could install — is off by default: "what's connected?" should not answer with a shopping list. Pass include_catalog=True if you want it.


In the agent

The registry is published into tool state automatically, so the connections tool works with no wiring:

python
agent.run("Which of my connections need attention?")
text
⚯ Checked connections                             2 need action

Using it, end to end

1 · Give the agent the connections tool

It ships as a builtin, and the registry is published into tool state for you — so nothing needs wiring.

python
from shipit_agent import Agent
from shipit_agent.builtins import get_builtin_tool_map
from shipit_agent.integrations import InMemoryCredentialStore

tools = get_builtin_tool_map(llm=llm, project_root=".")

agent = Agent(
    llm=llm,
    tools=[tools["connections"], tools["slack"], tools["google_sheets"]],
    credential_store=InMemoryCredentialStore(),   # or your own store
)

2 · Ask what is reachable

python
agent.run("What can you reach right now, and what needs setting up?")
text
⚯ Checked connections                              1 connected · 2 need action

Or without a model in the loop:

python
from shipit_agent import ConnectionRegistry

registry = ConnectionRegistry(credential_store=store, tools=agent.tools)
print(registry.render())

3 · Let it ask for what it is missing

python
for event in agent.stream("Post the Q2 numbers to #finance."):
    if event.type == "connection_requested":
        card = event.payload
        print(f"CONNECT {card['title']}: {card['reason']} ({card['auth']})")
text
CONNECT Slack: Post the Q2 revenue summary to the #finance channel. (oauth)

The run keeps going — the agent is told to continue with anything that does not need it, so a missing credential costs you the one step rather than the whole task.

4 · Draw the card

python
from shipit_agent.narrate import render_chat_html
from IPython.display import HTML

HTML(render_chat_html(events, title="Q2 numbers"))

Or build your own from the payload — connection_id, title, reason, auth. Those four fields are the card.

5 · Answer it

python
@app.post("/connections/{connection_id}/accept")
def accept(connection_id: str, credential: str, user=Depends(current_user)):
    registry.resolve(
        connection_id, accepted=True, credential=credential, by=user.email
    )
    return {"state": registry.get(connection_id).state.value}   # "connected"
python
@app.post("/connections/{connection_id}/deny")
def deny(connection_id: str, user=Depends(current_user)):
    registry.resolve(connection_id, accepted=False, by=user.email)

6 · Carry on

python
agent.run("Post the Q2 numbers to #finance.")     # now it just works

An OAuth round trip

resolve() is where your OAuth callback lands:

python
@app.get("/oauth/slack/callback")
def slack_callback(code: str):
    token = exchange_code_for_token(code)          # your own exchange
    registry.resolve(
        "slack",
        accepted=True,
        credential={"token": token["access_token"], "team": token["team"]["id"]},
        by="oauth",
    )
    return RedirectResponse("/chat")

Nothing about the agent needs restarting: the next state check reads the credential store and returns CONNECTED.


Connectable tools

Every tool carrying a credential_key becomes a connection. The built-ins:

ConnectionToolsAuth
SlackslackOAuth
Gmailgmail_searchOAuth
Google Calendargoogle_calendarOAuth
Google Drivegoogle_driveOAuth
Google Sheetsgoogle_sheetsOAuth
GitHubgithubToken
GitLabgitlabToken
JirajiraToken
LinearlinearAPI key
NotionnotionToken
ConfluenceconfluenceToken
FigmafigmaToken
SalesforcesalesforceOAuth
HubSpothubspotAPI key
StripestripeAPI key
ZendeskzendeskAPI key
LinkedInlinkedin_searchOAuth
Custom APIcustom_apiWhatever you configure

Tools that share a credential — the Google family — collapse into one connection listing all of them, because that is one thing to connect.

MCP servers appear alongside them, with their own state.

Your own connector

Any tool with a credential_key joins the registry:

python
class Warehouse:
    name = "warehouse"
    description = "Query the analytics warehouse."
    prompt_instructions = ""
    credential_key = "warehouse"          # ← this is the whole requirement

    def schema(self): ...
    def run(self, context, **kwargs): ...
text
· Warehouse — not connected
      tools: warehouse
      → Connect Warehouse.

How each tool shows up

Connections are one of several things a run surfaces as a card rather than as text. The full set, and where each appears:

The run emitsPanelTreeTimeline
tool_called / tool_completedA work row, 2 tools · 13.0msTool grouptool_call_started / _completed
action_queuedApproval cardApproval requiredapproval_required
connection_requestedConnect cardConnection neededconnection_required
artifact_createdFile cardArtifactartifact_created
agent_decision / agent_observationDecision stepDecision / Observedsame names
sub_agent_eventAttributed nested rowDelegatedsub_agent_tool_call
lockdown_engagedNoticeNotenotice

Every one of those arrives on agent.stream(). See The UI timeline for the JSON, and The live UI panel for the rendering.

See also