Skip to content

Page: register Installation runtimes with the proxy, and name the Installation's app id (#1700) - #1729

Closed
michielbdejong wants to merge 3 commits into
claude/atomic-proxy-schemefrom
claude/page-runtimes
Closed

michielbdejong wants to merge 3 commits into
claude/atomic-proxy-schemefrom
claude/page-runtimes

Conversation

@michielbdejong

Copy link
Copy Markdown
Contributor

Closes two data-browser gaps from #1700 (answers 1 and 2), stacked on #1725.

Merges #1697. appAgentOf and the v2-signed ProxyConnections (delegations, capabilities) live in #1697 (claude/frame-proxy-capability), which is not in #1725's stack (#1696 → #1702 → #1710 → #1725). This branch merges it cleanly (it touches only data-browser, browser/plugin and view-client.js), so this diff also shows #1697 until that lands. The commit of its own is c98a30233.

Gap 1: the page calls POST /runtimes

New helpers/installationRuntimes.ts, plus runtimes(), registerRuntime() and unregisterRuntime() on ProxyConnections. Every call is signed v2 with the user key through the existing call().

  • What it sends: POST {proxy}/runtimes {app, agent, label}.
    • app is the Installation's integrationAppAgent and agent the node's agent from an InstallationRuntime child, both canonical atomic:agent:.
    • label is the child's name, the node's device name, cut to the proxy's 200 characters.
    • The body field is agent, not runtime: the proxy's RuntimeBody is deny_unknown_fields.
  • Which children count: only a child whose genesis signer (getCreatedBy) is the agent it names, so another drive writer can't get an agent of their choice registered. A runtime equal to the app or the owner is skipped, because the proxy refuses those.
  • When:
    • an Installation page opens (for someone who can write it, and not once it's revoked), and whenever a runtime child appears there (useChildren);
    • an app frame opens, which covers the return from a connect handoff;
    • after "Use existing connection" delegates.
    • For anything without integrationAppAgent it does nothing.
  • Idempotent:
    • Registrations are remembered per (proxy, owner, app, agent, label), so the same page doesn't ask again.
    • Otherwise one GET /connections, and only runtimes the proxy doesn't list with the same app and label get posted.
    • The proxy upserts on (owner, agent) anyway. A failure is forgotten, so the next trigger retries.
  • Revoke and uninstall: DELETE /runtimes/{agent} for each runtime child. On uninstall they're read before the destroy.
  • Failures: never awaited by the UI. They show as a toast with the ProxyError message ("The integration proxy refused …: …").

Gap 2: appAgentOf prefers integrationAppAgent

It reads the resource first and returns the Installation's app id. GET /app-agent is only the fallback, for createApp apps and Installations from before app ids. Its callers are AppFrame's connect and "Use existing connection", and the cached agent behind host() (capabilities and delegated). So delegations, the pending connect's appAgent and frame capabilities all name the app id, not the node's agent.

Tests

The fake proxy mirrors atomic-plugins#122 and verifies each v2 signature over method, URL, timestamp and body hash. It refuses unknown body fields and agent = app/owner, upserts, and supports DELETE.

  • helpers/installationRuntimes.test.ts:
    • body and headers, signature verification, and a tampered body failing it;
    • idempotency within a page and across a reload;
    • a re-post on a label change;
    • app id spelling;
    • app/owner skipped;
    • error, then retry;
    • revoke → DELETE (signed), then registering again;
    • forged children ignored;
    • a no-op without an app id.
  • chunks/AppPage/appAgent.test.ts: the preference, the fallback's signed GET /app-agent, and the error when neither side has an identity.
  • data-browser typecheck is clean; vitest for src/helpers, src/chunks/AppPage and src/views/Installation passes (68 files, 545 tests).

No new UI text and no .po changes. The only visible change is a toast when the proxy refuses, so there are no screenshots.

Not done / follow-ups

  • Nothing in the page delegates a connection to an Installation yet: AppFrame renders createApp apps. The page also doesn't write integrationConnections (answer 3) when it delegates.
  • The Installation page hits the configured proxy (default https://localthought.io) whenever runtime children exist, even when no connection is delegated. That's one GET /connections per page load. If the proxy is down, the user sees a toast.

Refs #1700, ontola/atomic-plugins#122.

🤖 Generated with Claude Code

Michiel de Jong and others added 3 commits September 24, 2026 13:43
…lay (atomic-plugins#54)

Replaces #1657's `proxy` relay op, which kept LocalThought's rotating
connection code in the page and made every call for the frame. The #54
proxy has no tenant login and no connection codes (decision 6, flag day),
so the relay is removed rather than kept alongside.

Frame (server/src/plugins/assets/view-client.js):
- generates an Ed25519 key in memory, non-extractable, on first use; fails
  with a clear message where WebCrypto has no Ed25519 (#1688);
- asks the page for a capability (`proxyCapability` {platform,
  connectionId, publicKey}), caches it until 60 s before `exp`;
- fetches `{aud}/proxy/{connection_id}/{platform}{path}` itself with
  `Authorization: Capability <payload>.<sig>` and v2 request-signature
  headers signed by the frame key; re-mints once on 401
  `capability_expired`. `store.proxy.request(...)` keeps its signature and
  `{status, headers, body}` result.

Page (helpers/proxyConnections.ts, AppFrame, ProxyConnectReturn):
- every call to the proxy is signed v2 with the user key, as
  `atomic:agent:<pubkey>`: POST /connect/redeem, GET /connections,
  POST /connections/{id}/agents, DELETE /connections/{id}[/agents/{agent}];
- /connect without `user_id`; the PKCE verifier is the only thing kept in
  localStorage, for the handoff window;
- after redeem, the connection is delegated to the app's agent (from
  GET /app-agent); the connect bar also offers an existing connection for
  the platform, which only adds the delegation;
- mints capabilities only for a connection the user owns, of that platform,
  delegated to this app: claims {v:2, connection_id, platform, aud, app,
  cnf: atomic:agent:<frame key>, exp: now + 600 s}, payload base64url(JSON),
  signature by the user key over "integration-proxy-capability-v2\n" + JSON.
- Apps without their own agent (installed catalog plugins) are told they
  cannot be given a connection yet (#54 decision 2, later).

A fixed capability vector (user key of authentication_v2_vectors.json) is
pinned in proxyConnections.test.ts for the proxy's tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s app id (#1700)

Gap 1: the page now calls the integration proxy's runtimes routes
(ontola/atomic-plugins#122), signed v2 with the user key:
- POST /runtimes {app, agent, label} for each InstallationRuntime child
  whose genesis the named agent signed, with the Installation's
  integrationAppAgent as app and the node's name as label. Runs when an
  Installation page or app frame opens, when a runtime child appears, and
  after a delegation. Skips runtimes already registered in this page or
  listed by GET /connections for the same app and label; the proxy upserts
  anyway. In the background; failures are toasted.
- DELETE /runtimes/{agent} on revoke and on uninstall.

Gap 2: appAgentOf prefers the Installation's integrationAppAgent and only
falls back to GET /app-agent (createApp apps, older Installations), so
delegations and frame capabilities name the app id rather than one node's
agent.

Merges #1697 (claude/frame-proxy-capability), which holds appAgentOf and the
v2 ProxyConnections this builds on and is not in #1725's stack.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@michielbdejong

Copy link
Copy Markdown
Contributor Author

Merge-risk triage (2026-09-28). This is a read-only review of this PR's own diff. The rule it applies: nothing that works today may break for existing users or clients, and new behaviour is added alongside the old. The stack is being rebased onto develop as one chain before merging, so line references are against the current stacked base.

Verdict: SAFE-WITH-FIX (merge together with #1733)

After the rebase this PR's delta should be only c98a30233; d3fa5fed2 (frame capabilities / view-client) is already on develop.

Fix before merging: AppFrame calls registerRuntimesInBackground(store, app) with no check that the Installation uses the proxy. Every new install (which has an app id after #1710) then makes default outbound GET /connections + POST /runtimes calls to the default proxy, and shows an error toast when those fail. #1733's installationUsesProxy gate fixes this, so merge the two together.

🤖 Generated with Claude Code

@michielbdejong

Copy link
Copy Markdown
Contributor Author

Closing as part of the plugin-host stack cleanup (Q-101): not needed by atomic-plugins now. See the inventory at #1726 (comment). The branch is kept, and pin candidates are unaffected. Closed by Claude (VPS coordinator) on behalf of Michiel.

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

Labels

enhancement New feature or request plugin Should probably be an Atomic Plugin

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant