Skip to content

Support Muse as a caller: route custom-connector outbound operations through auth-proxy for audit and approval #6

Open

Description

@ttmuse

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:

  1. 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.
  2. 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
Loading

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
Loading

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
Loading

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"]
Loading

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

  1. ttmuse commented on Nov 12, 2026

    @ttmuse
    Author

    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:

    • Viable v1 without any app: the approval page is a PWA served by the approval plane (add-to-home-screen; Web Push works for installed PWAs on iOS 16.4+). The approve action is a WebAuthn / passkey assertion over the parked request's hash — Face ID authorizes the signature, the private key never leaves the device's Secure Enclave. Push delivery can also be borrowed from an existing channel (e.g. a self-hosted ntfy) with a deep link into the approval page, as long as the approval itself lands on the approval plane.
    • Owner preference: the target experience is a native iOS app — most reliable APNs push, keys in the Secure Enclave/Keychain by default, and room for richer surfaces (widgets, Live Activities, batch review). It is an experience upgrade, not a security prerequisite.

    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.

  2. ttmuse commented on Nov 12, 2026

    @ttmuse
    Author

    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:

    • When auth-proxy parks a request, it calls nearpush locally (loopback + push token, the only path nearpush accepts) and emits an allow. card: summary in the card body, and the card's url points at the approval plane's page for that request id.
    • The user sees the red 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.
    • On resolution, auth-proxy emits a result card on the same session (all_set with 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:

    • nearpush viewers (Tailscale allowed_logins) can only view and dismiss. They cannot push events, and there is deliberately no approve action anywhere in nearpush — its /push even refuses any request that arrives through tailscale serve. Visibility can be shared with subscribers; authority cannot, because approval still requires the approver key on the approval plane.
    • Cards carry a summary only, within nearpush's field limits; the full request text stays behind the approval plane's authentication.
    • Known limitation, stated in nearpush's own README: it is a display, not a notifier — iOS suspends the page in the background, so nothing arrives while the phone is locked; events replay from the buffer on return. nearpush therefore covers the at-desk case, and the PWA / native-app push from the approval-client discussion above remains the away-from-desk complement.

    Net effect: auth-proxy does not need to build its own notification surface for the pilot. It emits one local nearpush send per state transition, and nearpush + the approval plane cover the human side between them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions