Repository navigation
Description
Support Muse as a caller: route custom-connector outbound operations through auth-proxy for audit and approval
Summary
Muse is a personal agent that can be extended with custom connectors: small local helper programs the agent invokes to call third-party APIs on the user's behalf (for example a Discord bot connector that reads channels and posts messages). The connector credential is brokered by the Muse runtime — the helper only ever handles a surrogate, and the real credential is swapped in when a request targets a registered host.
That brokering solves credential custody, but it has two gaps for outbound operations:
- No authorization gate. The runtime swaps the credential for any request to a registered host. Whether a send should happen at all is decided by the agent's own behavior ("ask the user first, then send"), which is a convention, not an enforcement. A prompt-injected or malfunctioning agent can call the send operation directly.
- No independent audit. The only record of what was sent is whatever the agent itself logged.
This issue proposes Muse support in auth-proxy: give Muse a caller identity, point its custom-connector helpers at auth-proxy instead of the real API hosts, and let auth-proxy's per-identity policy classify each outbound operation — reads pass, writes are audited, and high-risk writes are parked for human approval. The approval mechanism itself is specified in #5; this issue is about the Muse integration scenario that consumes it.
Related: #5 (require_approval policy action). Same direction as openabdev/openab-sb#20 (identity-bound authorization at a relay), applied to a personal agent's outbound actions.
Current state (behavioral gate only)
flowchart LR
subgraph musevm["Muse VM"]
agent["Muse agent"]
helper["Custom connector helper<br/>holds surrogate only"]
agent --> helper
end
runtime["Muse runtime<br/>credential broker"]
api["Third-party API<br/>e.g. Discord"]
helper -->|"request to registered host"| runtime
runtime -->|"real credential swapped in"| api
user["User"]
agent -.->|"asks in chat: may I send?"| user
user -.->|"approves in chat"| agent
The dotted lines are the problem: approval lives in the conversation, and the solid path works whether or not approval happened.
Target state (proxy-enforced gate)
flowchart LR
subgraph musevm["Muse VM - agent environment"]
agent["Muse agent"]
helper["Custom connector helper<br/>caller key + surrogate<br/>no real credential"]
agent --> helper
end
subgraph home["User home pod - user controlled"]
proxy["auth-proxy<br/>caller check, canonicalize<br/>per-identity policy<br/>credential swap, audit line"]
store[("Credential store<br/>proxy-only")]
approval["Approval plane<br/>approver key on user device only"]
proxy --- store
proxy --- approval
end
api["Third-party API<br/>e.g. Discord"]
user["User"]
helper -->|"over Tailscale"| proxy
proxy -->|"approved or policy-allowed<br/>real credential applied"| api
proxy -.->|"parked request notification"| user
user -->|"approve / deny"| approval
Trust boundaries: the agent can reach the proxy's caller plane only. It has no path to the credential store, the approval plane, or the upstream with a usable credential.
Concrete scenario: Discord mention workflow
Muse polls Discord channels for mentions of its bot via a custom connector, drafts replies, and — after the user approves — posts them. With auth-proxy in the path, the Muse identity's policy for the discord.com upstream is:
| Operation | Policy | Rationale |
|---|---|---|
GET channels / messages (polling) |
allow |
Read-only, high frequency |
POST /channels/{id}/messages (incl. replies) |
require_approval (see #5) |
Speaks in the user's name, public side effect |
PATCH / DELETE own messages |
require_approval |
Same as above |
| Anything else | deny (default, fail closed) |
Not needed by the workflow |
Gated send, step by step
sequenceDiagram
participant A as Muse agent
participant P as auth-proxy
participant U as User (approver)
participant D as Discord API
A->>P: POST /channels/{id}/messages<br/>caller key + surrogate, draft text
P->>P: Caller check + canonicalize
P->>P: Policy lookup: require_approval
P->>P: Park request, write audit line (parked)
P-->>A: 202 + request id (not sent)
P-->>U: Notification: pending request, full text
U->>P: Approve request id (approval plane)
P->>P: Verify approval binds to request hash
P->>D: Credential swap, forward parked request once
D-->>P: 200, message id
P->>P: Audit line (executed, message id)
P-->>A: Result available for request id
If the user denies (or the parked request expires), the request is discarded, the caller receives a terminal error, and the audit line records denied / expired. Nothing reaches Discord.
What "Muse support" needs
- Caller identity for Muse. A registered identity (e.g.
muse) with caller key + per-session surrogates, equivalent to existing identities but issued outside the Kubernetes pod shape. - A registrar the user controls. Phase 1 registers surrogates via the in-pod runtime shim (Ed25519
hello). For Muse, the registrar role must be fillable by a user-controlled component (e.g. on the home pod), so sessions can be issued, renewed, and revoked for a remote agent. - Remote caller transport. Accept caller-plane requests over a private network path (Tailscale) instead of loopback only, without weakening the caller check: canonicalization, ambiguous host/path refusal, and fail-closed policy all apply unchanged.
- Thin-connector convention. Document how a Muse custom-connector helper is pointed at auth-proxy: base URL = proxy, credential = caller key + surrogate, real upstream credential lives only in the proxy's store. The helper needs no changes beyond configuration.
- Audit completeness. Every outbound operation from the Muse identity produces an audit line (allowed, parked, approved, denied, executed) that the user can inspect without trusting the agent's own logs.
- Approval integration. Consume FR: require_approval policy action — human-in-the-loop approval for gated requests #5: parked requests, approval plane, deferred execution, single-execution and hash-binding invariants.
- Revocation story. Revoking the Muse session (or identity) immediately blocks new requests and drops its parked ones — the kill switch if the agent is suspected compromised.
Deployment sketch
flowchart TB
subgraph cloud["Muse cloud VM"]
agent2["Muse agent + connector helpers"]
end
subgraph tailnet["Tailscale tailnet"]
link[" "]
end
subgraph homepod["Home pod / Mac mini"]
proxy2["auth-proxy<br/>caller plane + approval plane"]
creds[("Upstream credentials<br/>Discord bot token, ...")]
proxy2 --- creds
end
phone["User phone / laptop<br/>approver"]
agent2 -->|"caller key + surrogate"| link
link --> proxy2
phone -->|"approve / deny"| proxy2
proxy2 -->|"real credential"| internet["Third-party APIs"]
Open questions
- Session lifetime for a long-lived personal agent: the 12 h absolute TTL fits pod workloads; does Muse get the same, with renewal via the user-controlled registrar?
- One Muse identity for all upstreams, or one identity per connector (finer policy, more keys to manage)?
- Should read operations (
allow) also be sampled into a user-visible feed, or is the audit log enough? - Notification channel for parked requests: generic webhook (per FR: require_approval policy action — human-in-the-loop approval for gated requests #5) is enough for the pilot; the Muse chat itself is the natural place for the user to see drafts — but the approval action must still land on the approval plane, not in chat, or the gate is behavioral again.
Non-goals
- Replacing Muse's built-in connectors; this covers custom connectors whose helpers the user controls.
- Changing how Muse brokers credentials for traffic that does not go through auth-proxy.
- A general "agent firewall" product surface — this is one concrete caller (Muse) and one concrete first upstream (Discord).
Activity
ttmuse commentedon Nov 12, 2026
Approval client: a native app is not a prerequisite — but it is the preferred end state
Follow-up on the approval-UX open question, recording a design decision from the owner discussion on 2026-11-12.
The phone-side client only has three jobs: receive a push, display the full parked request, and return a signed approve/deny decision. Nothing in the security model requires a native app:
Design consequence for the approval plane (#5): keep the approval API client-agnostic. The PWA and a future native app must be interchangeable thin shells over the same endpoints (
list,approve,deny) and the same signature scheme, so swapping the client never touches the proxy's trust boundary.ttmuse commentedon Nov 12, 2026
Notification layer identified: nearpush
Follow-up from the owner discussion on 2026-11-12. The parked-request notification hook proposed in #5 has a concrete first consumer: openabdev/nearpush, the owner's desk-display daemon (a small service on the laptop streams agent-state cards over Tailscale to a home-screen web page on a desk iPhone). The nearpush side of this integration is tracked in openabdev/nearpush#2.
How the layers fit:
allow.card: summary in the card body, and the card'surlpoints at the approval plane's page for that request id.allow.card on the desk display, taps it, lands on the approval plane, reviews the full canonicalized request, and approves with a WebAuthn signature — exactly the flow this issue requires.all_setwith the upstream result, or a warning card for denied / expired) and dismisses the stale approval card if the request can no longer be approved.Why this does not weaken the gate:
allowed_logins) can only view and dismiss. They cannot push events, and there is deliberately no approve action anywhere in nearpush — its/pusheven refuses any request that arrives throughtailscale serve. Visibility can be shared with subscribers; authority cannot, because approval still requires the approver key on the approval plane.Net effect: auth-proxy does not need to build its own notification surface for the pilot. It emits one local
nearpush sendper state transition, and nearpush + the approval plane cover the human side between them.