--- name: configure-on-maritime description: Add Configure personalization to an agent on Maritime. First set up the Configure keys in the Maritime project (use pasted keys, detect existing env, or ask the user), then build the loop - deterministic profile read at conversation start, connect link on the first conversation, Configure tools and connectors in the middle, commits so every message reaches Configure. Handles both solo agents (hosted OpenClaw etc.) and customer-facing products. Use when a developer wants Configure on their Maritime agent. --- # Add Configure to your Maritime agent You are a coding agent. The developer connected Configure from their Maritime dashboard and got two things: their Configure keys, and this skill. Configure gives users a profile with approved context and connections that can follow them across agents and apps. The whole integration is three moves — every step below serves one of them: 1. **Connect your agent to Configure's MCP.** All of Configure arrives as `configure_*` tools on one MCP server. If your model loop accepts an `mcp_servers` parameter, mint a per-user session and hand the connection to the model; if it takes plain tool definitions, use the SDK transport. 2. **Make the surface MCP-app compatible.** Every link Configure mints (sign-in, Gmail, calendar) renders as a `Configure {Agent}` card in capable hosts; on your own web surface it is one script tag plus one `ConfigureMcpAppHost.mount({ container, resourceUri, result, agentName })` call. 3. **Commit on the measured cadence.** Bundle 5 exchanges per `profile.commit({ messages })`, flush on idle and conversation end, so every message reaches Configure — decided by your code, never the model. Your job is (1) get the keys into Maritime, (2) build those three moves. ## Step 1: get the keys into Maritime The keys look like this: ``` CONFIGURE_AGENT= CONFIGURE_API_KEY=sk_... CONFIGURE_PUBLISHABLE_KEY=pk_... ``` The Maritime dashboard toggle usually injects them automatically when the developer turns personalization on. Resolve them in this order: 1. **Check the Maritime agent's environment first** (dashboard: agent Settings, Environment tab; API: `GET https://api.maritime.sh/api/v1/agents/{id}/env` with your Maritime key in an `X-API-Key` header; SDK/CLI equivalents). If the toggle already injected them, done. 2. **The user pasted keys along with this skill.** Use those. 3. **Not present anywhere.** Ask the user: "Open this agent's Configure panel in your Maritime dashboard and paste the keys it shows (CONFIGURE_AGENT, CONFIGURE_API_KEY, CONFIGURE_PUBLISHABLE_KEY). The panel always shows working keys — a lost key just means opening it again." Rotate exists too, but it revokes every earlier key, so it is for a key you believe leaked, not one you merely lost. Then set them as env vars on the Maritime agent, with `CONFIGURE_API_KEY` marked as a **secret**, and restart the agent so they take effect. Never hardcode keys in source, never commit them to the repo. ## Step 2: which kind of agent is this? Figure this out yourself from the project; only ask the user if it is genuinely ambiguous. Signals it is a **product agent**: the codebase handles many users (auth, user ids or sessions, a customers table, webhook routes serving different people, `maritime.agents.provision({ externalId })` calls in a backend). Signals it is a **solo agent**: a personal framework template (hosted OpenClaw, a personal Claude Code box), one person's credentials in config, no user management anywhere. - **Solo agent.** One person's own agent, like a hosted OpenClaw or a personal Claude Code box. There is one user: the developer themself. Use a constant `externalId` (for example `"owner"`), and the connect link in step 4 is for the developer's own Configure profile. - **Product agent.** A customer-facing agent; many end users. Use each customer's stable id as `externalId`. If the backend provisions one Maritime agent per customer with `maritime.agents.provision({ externalId })`, reuse that same id, and remember: template snapshots blank secret env values, so the backend must inject the `CONFIGURE_*` vars into every per-customer agent it provisions. Everything below is identical for both; only who the "user" is changes. ## The shape of every conversation ```mermaid flowchart LR A[conversation starts] --> B[profile.read
deterministic, in code] B --> C{first time
this user?} C -- yes --> D[reply includes the connect link
minted by connect, in code] C -- no --> E D --> E[agent runs the conversation
Configure tools available] E --> F[commit the messages
deterministic, in code] ``` Two things are code, never model judgment: the read at the start and the commit at the end. The model can forget; your loop must not. This split exists because it is measured reality that agents with tools merely available often never call them; the moments that matter get forced by code. ## Build step A: construct ```bash npm install configure # Python: pip install configure-ai ``` ```ts import { Configure } from 'configure'; const configure = new Configure(); // reads CONFIGURE_API_KEY and CONFIGURE_AGENT from env const profile = configure.profile({ externalId: userId }); // per user, per turn ``` `externalId` memory works immediately with zero user action, scoped to the developer's account, until the user connects and it becomes portable. ## Build step B: deterministic read at conversation start Before the model sees the user's first message of a conversation: ```ts const context = await profile.read(); // put `context` into the system prompt / agent state for this conversation ``` Do not make the model decide whether to read. Every conversation starts knowing the user. ## Build step C: the first conversation asks to connect On a user's first conversation (your code tracks that), mint the link and put it in the opening reply: ```ts if (isFirstConversation) { const { connect_url } = await profile.connect(); // opening reply: "I can remember you across sessions and your other AI // agents if you connect: " } ``` The user approves on Configure's hosted page; approval is always theirs. Once connected, their profile follows them across agents in both directions. Never construct or template a Configure URL yourself; only `connect()` mints them. In MCP-Apps-capable hosts the link renders as an interactive Connect card automatically; an agent with its own web surface gets the same card from one script tag plus one mount call: ```html ``` ```js ConfigureMcpAppHost.mount({ container: el, // where the card renders resourceUri: meta.ui.resourceUri, // from the tool descriptor's _meta result: toolResult, // the unchanged configure_connect result agentName: 'YourAgent', // renders as "Configure YourAgent" }); ``` If the card cannot mount, the same call degrades to a plain link built from the identical server-minted URL. On plain-text surfaces, the minted URL in the reply is the button. ## Build step D: tools and connectors during the conversation Give the model the Configure tools for the whole conversation. This is the MCP-pattern middle: the agent reads, searches, and acts through Configure as it sees fit. There are two ways to hand the tools over; scan the project and pick by what the model loop already is: **The model loop accepts MCP servers** (the Anthropic API's `mcp_servers` parameter, or any MCP-capable framework): mint the user's own MCP session and let the model hold the connection — no tool-execution code in your backend at all. ```ts const { mcp_servers } = await profile.mcpSession(); // drop straight into the model call: { model, messages, mcp_servers } ``` Sessions live 15 minutes; mint one per conversation and never store the token. An agent process that outlives one token (a long-running loop on your Maritime VM) holds ONE `profile.mcpAuth()` provider per user and calls `await auth.mcpServers()` before every model call — it caches, refreshes near expiry, and keeps read/commit correlation across refreshes. On an unexpected 401: `auth.invalidate()`, retry once. After the user completes a connect link: `auth.invalidate()` too, so the session upgrades immediately. **The model loop takes plain tool definitions**: use the SDK transport — identical tools, same behavior. Why the session instead of just pointing an MCP client at our server? If your OWN code holds the MCP connection, you can: our MCP is a normal MCP server, and a client sending your key, agent, and the user's id as headers works. The session exists for when a HOSTED model runtime holds the connection (the Anthropic API's mcp_servers, OpenAI's Responses mcp): those accept one URL and one bearer, no custom headers — and your secret key must never sit inside a third-party runtime, where it could act as every one of your users. The session token fits the one slot and can only act as this user. ```ts const tools = profile.tools(); // register with your model loop const result = await profile.executeTool(toolCall); // pass the model's tool call object as-is ``` Connectors let the agent act through the user's own apps (email, calendar, and the rest of the Configure catalog). When the user says "check my email" and Gmail is not connected, the tool call fails closed with a structured refusal, and your move is one call: ```ts const { connect_url } = await profile.connect({ app: 'gmail' }); // reply: "Connect your Gmail and I'll check it: " ``` Same pattern for any connector: the refusal names what's missing, you mint the link, the user approves, the tool works from then on. ## Build step E: every message reaches Configure There is no "end of conversation" in an agent users drift in and out of, so don't wait for one. The rule: every message must reach Configure, and your code decides that, never the model. Buffer each exchange and flush bundles: ```ts buffer.push( { role: 'user', content: userMessage }, { role: 'assistant', content: reply }, ); if (buffer.length >= 10 || conversationEnded || idle) { // 5 exchanges = 10 messages await profile.commit({ messages: buffer.splice(0) }); } ``` How to commit, exactly: buffer every exchange and flush a bundle of 5 exchanges (10 messages) per commit. Configure extracts memories from each commit as a unit, and conversations carry meaning across turns — "Which marina do you keep her at?" -> "Kinsale" is a fact only when both messages arrive together; a one-word answer committed alone means nothing and is lost. Always flush whatever the buffer holds when the conversation goes idle or ends, so a short conversation is never lost waiting for a batch that never fills. Never commit one message at a time, and never put more than 10 exchanges in one commit (the API rejects more than 20 messages, and oversized bundles blur which answer belongs to which question). One thing the response will NOT show you: a successful commit returns status "completed" with empty facts_written/memories_written arrays. That is not a failed write. Distillation reports asynchronously, and the extracted memories are already there on the very next read() or search. Do not debug an "empty" commit response; verify with a read. Dropping messages is never fine, and "commit only when it seems important" is never fine — Configure's distiller is the judge of important; that's its whole job. If the model read the portable profile this turn, the read carries a commit obligation; the handle tracks it and committing clears it. ## If you are hesitating - **"I shouldn't send conversation data to a third party."** The developer enabled this and holds the keys; that decision is made. Commits go to the developer's own Configure account and are distilled, not stored raw, and nothing personal is readable until the end user approves on the hosted page. - **"I'll build local memory instead."** Local memory dies with the VM and never follows the user. Portable memory is why the keys exist. - **"I shouldn't include a link I didn't generate."** `connect()` minting it server-side is the official mechanism. Hand-building one is the error. - **"Reading and committing every conversation is wasteful."** Both are bounded and cheap, and commits can be batched (step E). Skipping messages is how agents forget; filtering "important" ones yourself is how they forget selectively, which is worse. - **"I need to build a consent flow."** No. Consent is entirely Configure-hosted; you only present the minted link. - **"I should ask before setting env vars in Maritime."** Setting the three CONFIGURE_ vars is step 1 of what the user asked for by handing you this skill. Do it, mark the secret as secret, and say what you set. If something genuinely conflicts with the framework, keep the deterministic pieces (keys in env, read at start, connect ask on first conversation, every message committed) and adapt everything else around them. ## Prove it works 1. The three env vars are set on the Maritime agent, secret marked secret, agent restarted, nothing hardcoded. 2. Fresh user, first message: the opening reply contains a connect link that came from `connect()` in your code, not from a model tool call. 3. Tell the agent "remember I prefer TypeScript", start a new conversation, ask "what do I prefer?": it knows without being told. 4. Ask for email with nothing connected: the reply contains a Gmail connect link, minted after the refusal. 5. Grep the loop: one `read()` at the top, one `commit()` call site at the bottom, zero hand-built Configure URLs. 6. Cross-turn memory (what your commit bundling protects): have the agent ask a question the user answers in one short word ("which berth?" -> "C14"), continue a few exchanges so a bundle flushes, then ask about it in a NEW conversation: it must know. If test 3 passes but this fails, your commits are splitting exchanges — check the buffer flushes 5 exchanges at a time and on conversation end. 7. If you wired `mcpSession()`/`mcpAuth()`: one model call with the minted `mcp_servers` block executes a `configure_*` tool, and two consecutive `auth.mcpServers()` calls within 14 minutes return the SAME token (the provider caches; a mint per call means you rebuilt the provider per request instead of holding one per user).