Skip to content

Fail closed on installation identities too; merges #1644 (#1700, piece 8) - #1833

Closed
michielbdejong wants to merge 3 commits into
claude/proxy-hardening-v2from
claude/proxy-hardening
Closed

michielbdejong wants to merge 3 commits into
claude/proxy-hardening-v2from
claude/proxy-hardening

Conversation

@michielbdejong

Copy link
Copy Markdown
Contributor

Piece 8 of #1700 (answer 8): #1644's fail-closed check now sees installation identities, so installed plugins aren't refused.

Third of three stacked PRs:

  1. Refuse wasip2 class extenders at the integration proxy, and say why (#1700, piece 5) #1831: piece 5;
  2. Require v2 signatures on state-changing endpoints, each proof once (#1700, piece 7) #1832: piece 7;
  3. this one: stacked on Require v2 signatures on state-changing endpoints, each proof once (#1700, piece 7) #1832.

The code doesn't depend on #1832; this PR is stacked only to keep the chain linear.

It merges #1644

This PR merges #1644 (claude/app-agent-fail-closed, "refuse to run an app whose key is not on this node"), because piece 8 extends its check. The merge commit (7a77c28) merges fb53a66, the commit that carries the fix, not the branch tip. The tip (6e26b5f) only adds a merge of develop on top. Merging the tip would pull unrelated develop changes into the plugin line and conflicts with it (kanban, integration settings, .po catalogs, e2e). fb53a66's parent is already in this line, so the merge brings exactly #1644's three files.

What changes

Answer 8 said to record installation identities where the check looks. Copying them into the app-identities folder and the write lists would give a keyless id write rights for nothing, and would need a second writer. So instead, the check reads them where #1710 already records them in synced data:

  • installation_identity::installation_of_identity(db, agent): the Installation that an agent is an identity of. That's either its keyless app id (integrationAppAgent) or a node's agent published on an InstallationRuntime child. It accepts both the atomic:agent: and did:ad:agent: spellings.
  • installation_identity::carries_identity(db, installation): whether the Installation has an app id or a published runtime agent.

What installation::resolve does on a resource this node holds no key for (Legacy):

  • An Installation that carries an installation identity: refused ("…has no agent on this node; activate it on this node before running it here"). Before, it silently ran as the server's agent.
  • A write-list agent that's an identity of Installation I: fine when this node holds an agent for I; the walk reaches I and signs as this node's agent. Refused when this node doesn't. This is what keeps installed plugins running when their resources name the app id, or another node's agent.
  • Any other write-list agent: goes through fix(plugins): refuse to run an app whose key is not on this node #1644's createApp check unchanged, so an unknown key is still refused.

Installations from before #1710, with no app id and no runtime child, behave as before.

Tests

Written before the change. an_installation_not_activated_on_this_node_does_not_run_as_the_server failed with Ok(None) before the fix, meaning it would have run as the server.

  • an_installed_plugin_runs_as_this_nodes_agent_whatever_identities_it_names: the entry point names the app id, another node's published agent, and this node's agent. It resolves to this node's key.
  • an_installation_not_activated_on_this_node_does_not_run_as_the_server: the same Installation as synced data, with no agent here. It's refused, and so is its entry point.
  • an_app_key_that_is_not_on_this_node_is_still_refused: a createApp agent with no key here, under an Installation this node activated, is still refused.
  • fix(plugins): refuse to run an app whose key is not on this node #1644's own scheduler tests and all plugins:: tests pass.

cargo test -p atomic-server --lib (300 passing), clippy -D warnings and the pre-commit workspace clippy all pass.

🤖 Generated with Claude Code

Michiel de Jong and others added 3 commits September 23, 2026 10:50
An app's agent resource syncs with its drive, but the key that signs as
that agent does not. The key is posted once to POST /app-agent and stays
on the node it was posted to. Any other node reads the missing key as
Legacy. Scheduled, trigger and plugin runs there then fell back to the
server's own agent, so the app's writes were attributed to the server and
the app's own rights no longer bounded them.

installation::resolve now refuses when a Legacy resource has an app
identity. An app identity means an agent DID in the resource's write list
whose agent resource is in the drive's app-identities folder, which is
where createApp puts it. Plugins without an app identity keep the legacy
signer.

Refs ontola/atomic-plugins#41, ontola/atomic-plugins#54

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…piece 8 of #1700

Merges fb53a66, the commit of claude/app-agent-fail-closed that carries
the fix. That branch's tip (6e26b5f) only adds a merge of develop on
top, which would pull unrelated develop changes into the plugin line and
conflicts with it; fb53a66's parent is already in this line, so this
merge brings #1644's three files and nothing else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Answer 8 on #1700: the check that refuses to run an app whose key is not
on this node should see installation identities too. Rather than copying
them into the app-identities folder and write lists (which would give a
keyless id write rights for nothing), the check reads them where #1710
records them, in synced data:

- `installation_identity::installation_of_identity`: the Installation an
  agent is an identity of, via `integrationAppAgent` or an
  `InstallationRuntime` child's `integrationRuntimeAgent`, in either agent
  spelling.
- `installation_identity::carries_identity`: an Installation with an app
  id or a published runtime agent.

`installation::resolve`, on a resource this node holds no key for:
- an Installation that carries an identity was not activated here; it is
  refused instead of running as the server;
- a write-list agent that is an identity of Installation I is fine when
  this node holds an agent for I (the walk then signs as it), and refused
  when it does not;
- other write-list agents go through #1644's createApp check unchanged,
  so an unknown key is still refused.

Tests: an installed plugin whose entry point names its app id and another
node's agent runs as this node's agent; the same Installation on a node
that never activated it is refused, as is its entry point; a createApp
agent with no key here is still refused under an activated Installation.

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

The #1644 fail-closed check is already on develop; this extends it to installation identities.

Fix before merging: An Installation is now refused whenever it "carries an identity", and that includes the InstallationRuntime children #1710/#1836 publish onto existing Installations. An existing Installation activated on node A and synced to node B (still Legacy on B) used to run on B as the server agent and would now be refused. Base the refusal on integrationAppAgent only (which only new installs carry), or mint an agent when a synced Installation arrives. Also, identity_without_key does up to 6 queries per agent writer per ancestor on every resolve() (every run and fetch). Gating #1710's publish on a declared proxy removes most of this.

🤖 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

plugin Should probably be an Atomic Plugin security server atomic-server

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant