Skip to content

feat(api)!: Plan 20 PR 5 — a guard on every RPC method (RFC #1351) - #1395

Open
HexaField wants to merge 70 commits into
feat/p20-secretfrom
feat/p20-guards
Open

HexaField wants to merge 70 commits into
feat/p20-secretfrom
feat/p20-guards

Conversation

@HexaField

@HexaField HexaField commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Plan 20, PR 5 of 6. Stacked on #1394 (PR 4) → #1393 → #1392 → #1390. When those merge, this PR's base becomes dev and its diff shrinks to its own commits. Implements RFC #1351 with capability checks kept (the condition of its acceptance). Read it commit by commit. Each commit builds and passes the executor lib tests.

What

Every WebSocket RPC method declares a guard. The dispatcher runs the guard before the handler: first the capability, then the rule of the variant. Every method must have a guard. If one has none, the handler map does not build and the executor does not start. 147 hand-written check_capability calls and 77 access-helper calls leave the handlers. All 193 methods at the branch head have a guard: Cap 72, Executor 33, Open 5, Operator 16, Owns 67 (the generated table in api/AGENTS.md). At the rework (45b3c93) the table had 169 methods: Cap 54, Executor 33, Open 6, Operator 15, Owns 61. The multi-user isolation work moves onto the guards: AI tasks have an owner, and neighbourhood calls follow the owner of the perspective. The PR is part of the executor side of #1399 (D12, D14): users approve, list and revoke grants on their own agent, never on another agent. Closes #1346 and #1328.

Why

Before this PR, each handler checked its own caller by hand: a check_capability call, sometimes refuse_user_session, sometimes get_perspective_with_access. Nothing made sure that a method had a check, or that the check matched the data the method touches. Each recent security fix (#1164, #1169, #1208, the isolation work) added one forgotten check. With the guard on the registration, a missing check stops the executor at start, and the method matrix checks every method as every kind of caller. The rework asks one question per method: does it act on the executor, or on something an agent owns? Executor operations belong to the operator and to grants on the main agent, which the operator owns. Owned resources (perspectives, AI tasks, grants, capability requests) belong to one agent. The operator reaches all of them. Any other session reaches what its agent owns.

How

Mental model shifts

  • Before: each handler checked its caller by hand. Now: each method declares a Guard where it registers, and the dispatcher runs it before the handler.
  • Before: a method without a check was easy to miss. Now: the executor refuses to start with an unguarded method, and the guard table in api/AGENTS.md is generated.
  • Before: a perspective you could not reach answered 403 "Access denied". Now: missing and not yours both answer 404.
  • Before: AI tasks belonged to the executor. Now: each task has an owner agent, and only that agent (or the operator) reaches it.

Change map

One idea: every RPC method declares a Guard where it registers, and the dispatcher runs that guard (capability, then ownership) before the handler. Handlers no longer check their callers by hand.

1. The guard and the dispatcher (≈ +530 / −115)

  • api/guards.rs: new enum Guard { Open, Operator, Cap(Capability), Executor(Capability), Owns { cap, id: IdParam, owner: Owner } } and enum Owner { Perspective, Task, Grant, Request }. Guard::admit checks the capability first, then the rule of the variant. Operator and Executor refuse with 403. owns() answers 404 for a resource that is missing and for one that is not the caller's. IdParam::{Field, FirstOf, InJson} reads the resource id from the params. InJson refuses repeated keys. refuse_user_session goes (Guard::Executor replaces it).
  • api/ws_handler.rs: MethodFlags gets .guard(g) (two guards panic) and .or_null(). Dispatch order: method → params (400) → guard → handler. A 404 from an .or_null() method becomes null. HandlerMap::require_guards() panics at start-up and names every method without a guard. New admit, guards(), or_null_methods().
  • agent/capabilities/mod.rs: may_approve and its test go (Owner::Request replaces it). New anonymous_capabilities_when(multi_user) for tests.

2. AI tasks have an owner (≈ +330 / −40)

  • db.rs: tasks.agent_did TEXT (NULL = the main agent). Insert, list, export and import carry it, so a restore keeps the owner.
  • db/tasks.rs (new): get_task_owner(id) and get_tasks_owned_by(did), with tests.
  • ai_service/mod.rs: add_task(task) → add_task(task, agent_did). Transcription streams: a main-agent app reaches only the streams of the main agent, not the streams of users (the closed leak).
  • api/ai_ws.rs: task methods declare Owns { Task }. ai.tasks lists every task for the operator and only the acting agent's tasks for anyone else.
  • perspectives/interpretation/prompt.rs: reuses only an operator task (get_tasks_owned_by(None)), so the lookup does not pick up a user's task with the same name.

3. Handlers declare guards (≈ +1,110 / −1,115, mostly mechanical)

  • perspectives_ws, neighbourhoods_ws, runtime_ws, ai_ws, agent_ws, users_ws, sfu_ws, hosting_ws, languages_ws, expressions_ws: ctx.require(&X) / refuse_user_session in the handler → .guard(Guard::…) on the registration. Unused ctx → _ctx.
  • perspectives_ws.rs: get_perspective_with_access answers 404 "Perspective not found" for a perspective you may not reach (was 403 "Access denied"). New guarded_perspective(params) only fetches, because the guard already checked access. perspective.get / perspective.snapshot are .or_null().
  • neighbourhoods_ws.rs: every method that names a perspective is Owns { Perspective }. The owner check in the handler goes. neighbourhood.publish without access is refused.
  • agent_ws.rs (Multi-user: users approve their own apps (consent, per-app capabilities, per-app revoke) #1399): agent.permitCapability → Owns { Request }. agent.revokeToken and agent.removeApp → Owns { Grant }. manageable_grants goes.
  • runtime_ws.rs, users_ws.rs: runtime.importData, runtime.grantNotification, the runtime.unyt* stubs and user.credits → Guard::Operator. runtime.setFreeHostingEnabled → Executor(ALL_CAPABILITY). runtime.exportData of one perspective checks the owner in the handler.
  • sfu_ws.rs: Cap(NEIGHBOURHOOD_READ/UPDATE). The handlers check membership themselves, because no Owner covers SFU rooms.

4. Wording: "node" → "executor" (mechanical, ≈ ±10)

  • api/errors.rs, api/ws_rpc.rs, connect/src/core.ts, core/src/apiClient.test.ts: comments only. core/src/agent/AgentClient.ts: the revokeAllSessions doc names AGENT_UPDATE. mcp/tools/{auth,mod}.rs: comments name the new rule. No API change in the SDK.

5. Tests (≈ +2,660 / −175)

  • api/tests/method_matrix_tests.rs (new, +1,414): reads every guard from the handler map. It calls HandlerMap::admit as operator, app with and without the capability, user A, user B and anonymous, on own, other, main-agent and missing resources.
  • api/tests/handler_table_tests.rs (new): the_guard_table_in_agents_md_is_current renders the table from the map.
  • api/tests/multi_user.rs (new): a multi-user executor fixture with signed-token sessions. ai_task_access_tests, neighbourhood_access_tests, runtime_access_tests, runtime_ws_tests (new): isolation per resource.
  • node_operation_tests.rs → executor_operation_tests.rs (−130: the matrix replaces its per-method cases). connection_tests, notification_ws_tests, secret_leak_tests: fixture updates.
  • tests/js/tests/multi-user-simple.test.ts: expects 404 "Perspective … not found" and 403 "Admin credential required".

6. Docs (≈ +370 / −25)

  • api/AGENTS.md: new "Guards" section and the generated guard table between guard-table markers.
  • db/AGENTS.md (new), rust-executor/AGENTS.md, ai_service/AGENTS.md, js_core/AGENTS.md: guards, per-agent ownership, tasks.agent_did.

Reading order: Guard and owns() in api/guards.rs → MethodFlags::guard and require_guards in ws_handler.rs → one handler file (agent_ws.rs) → method_matrix_tests.rs. The other handler files follow the same pattern.

Interfaces

// api/guards.rs
pub enum Guard {
    Open,                     // no check, the handler decides (runtime.info, user.logout, …)
    Operator,                 // the operator only, others: 403 "Admin credential required"
    Cap(Capability),          // the capability alone, anonymous callers hold only the anonymous set
    Executor(Capability),     // acts on the executor: the capability, then the operator or a grant on the main agent
    Owns { cap: fn(Vec<String>) -> Capability, id: IdParam, owner: Owner },
}
pub enum Owner { Perspective, Task, Grant, Request }   // missing and not yours: both 404

map.method::<PerspectiveUuidParams, Option<PerspectiveHandle>>("perspective.get", get_perspective_handler)
    .read()
    .or_null()      // 404 → null for this method
    .guard(query());   // Guard::perspective(perspective_query_capability) = Owns { Perspective, … }
map.method::<PerspectiveUuidParams, bool>("perspective.remove", delete_perspective)
    .guard(Guard::perspective(perspective_delete_capability));

Diagrams and tables

request ─► method known? ─404─► params parse? ─400─► guard capability ─403─► guard rule ─► handler
                                                                             Operator / Executor: 403
                                                                             Owns: 404 ("Perspective <uuid> not found", …)
                                                                             .or_null(): 404 → null
start-up: HandlerMap::require_guards() ─► a method without a guard ─► panic naming it ─► the executor exits
Guard Operator Grant on the main agent (app) Grant on a managed agent (user) Anonymous
Open ✓ ✓ ✓ ✓
Operator ✓ 403 403 403
Cap(c) ✓ holds c holds c c in the anonymous set
Executor(c) ✓ holds c 403 "… acts on the executor itself, so a user session may not call it" 403
Owns { Perspective } every perspective the main agent's (and unowned) its own 403 (no capability)
Owns { Task } every task the main agent's tasks its own 403
Owns { Grant } every grant the main agent's grants its own 403
Owns { Request } every request requests for the main agent requests for its own agent 403

Within the reach of a caller: ✓. Outside it on Owns: 404, the same as a missing resource.

Size

Area + −
Implementation 1,457 1,212
Tests 3,178 239
Docs 368 24

From git diff --numstat origin/feat/p20-secret...origin/feat/p20-guards (head 69e8451). Inline #[cfg(test)] modules are counted under Tests (515 added, 61 removed). The change-map groups above count whole files, inline tests included.

Tests

Check Result
executor lib tests at 45b3c93 (--test-threads=1, 495e39e changes one JS test only) 2550 passed, 0 failed (41 ignored)
core jest 26 suites, 802 passed
method matrix: every method × {operator, app with the capability, app without, user A, user B, anonymous} against owned, unowned, main-agent and missing perspectives, tasks, grants and requests pass
every_registered_method_has_a_guard. A map with an unguarded method does not build. Dispatch refuses one pass
the_guard_table_in_agents_md_is_current pass
anonymous_callers_reach_only_the_anonymous_capabilitys_methods pass
the_operator_reaches_every_ai_task_and_a_main_agent_app_only_the_main_agents (the closed leak) pass
a_user_approves_and_revokes_only_their_own_agents_requests_and_grants (#1399) pass
isolation: ai_task_access_tests, neighbourhood_access_tests, runtime_access_tests, db::tasks pass
3 mutation checks on the rework each fails a test on the mutated code
integration test-main (local) all files pass (2, 3, 13, 5, 86, 79 passing)
integration test-multi-user-with-setup.sh (local) 34 passing, 0 failing. The first run was 33/1 on the grantNotification message. 495e39e updates the test
integration test-mcp (local) all 5 suites pass (69, 22, 21, 10, 8 passing)
integration test-model (local, LLM tests against a local model) 276 passing
CircleCI see checks

Manual test

A release build of 45b3c93, multi-user, on its own ports and data folder (p20-manual-pr5.py). Callers: users A and B, the operator, an app with agent and perspective READ, an app with every capability on the main agent, and an anonymous caller. Checks run over WebSocket RPC.

Check Result
A queries its own links. B: perspective.get of A's perspective → null, and perspective.all does not list it pass
B: queryLinks, addLink, update, remove on A's perspective → 404. A's link survives pass
a missing perspective → 404 pass
the operator's own perspective: the operator queries it. The READ app queries it, and addLink is refused (403). User A → 404 pass
a user: agent.lock, ai.addModel, runtime.exportData, language.remove refused. The READ app: ai.addModel refused. The operator: ai.addModel works pass
user.freeAccess, runtime.setHostRates: an app with every capability and a user → 403 "Admin credential required". The operator works pass
anonymous: agent.isLocked, runtime.info answer pass
A's AI task: A lists it, B does not. B's removeTask / prompt → 404. The task survives pass
closed leak: the app with every capability: removeTask / prompt on A's task → 404, and ai.tasks does not list it. The operator lists it. A removes it pass
neighbourhood.onlineAgents on A's perspective: B → 404, the app with every capability → 404, A passes the guard pass
runtime.exportData of A's perspective by the app with every capability → 404. The operator exports its own pass
#1399: B and the app with every capability: permitCapability on A's request → 404. A approves it (six-digit code). B and the app: removeApp of A's grant → 404. A removes it pass

47/47 checks pass.

Docs

api/AGENTS.md lists the guard of every method, generated from the handler map. the_guard_table_in_agents_md_is_current fails when the table is stale. pnpm run generate:api-types in core/ rewrites it.

Rework (2026-10-12)

45b3c93 and 495e39e, with the merge of the reworked PR 4 in 2c6e789. Since Data's approval at 0d5fcc1, this branch also has two of Data's points. 67817c3: the operator-only methods declare Guard::Operator. eb5194a: "executor", not "node", so Guard::Node is now Guard::Executor. The rework follows the model that the stack now uses. Nico: an app is not a separate kind of caller. Data: identity × grant. Josh decided that the code follows it.

  • Seven guards become five: Open | Operator | Cap | Executor | Owns. Cap replaces Anonymous and SelfAgent. No SelfAgent capability is in the anonymous set, so the capability check alone already refused anonymous callers. Owns { owner: Perspective | Task | Grant | Request } replaces Perspective, Task and the ownership checks for grants and approval requests in the handlers.
  • One rule for a resource you may not see: 404, the same answer as a missing one (D6). perspective.get and perspective.snapshot keep answering null through an .or_null() method flag. hides_existence, Admission and 403 "Access denied" go.
  • Closed leak: a main-agent app could read, rewrite, delete and prompt the AI tasks of managed agents. It now gets 404, and ai.tasks lists only the main agent's tasks for it.
  • Multi-user: users approve their own apps (consent, per-app capabilities, per-app revoke) #1399 guards: agent.permitCapability is Owns { Request } (the owner of the request's target agent, or the operator). agent.revokeToken and agent.removeApp are Owns { Grant } (the owner of the grant's agent, or the operator).
  • The guard table in api/AGENTS.md is generated from the handler map and checked for staleness, like RpcMethods.ts.

Commits

Sizes from p20-difsize.py (non-test / tests).

# Commit Behaviour Non-test Tests
20-5a Guard, .guard(), the dispatcher runs it, the method-matrix harness (63fc6b3) none +267 / −34 +473 / −0
20-5b Guard::Node on 32 executor operations, refuse_user_session and refuse_app_management_by_users go (3c50e85) #1328: runtime.setFreeHostingEnabled needs *. Closes #1346, #1328 +145 / −242 +135 / −39
20-5c Guard::Perspective on 24 perspective reads (5b852b6) none +131 / −224 +44 / −0
20-5d Guard::Perspective on 24 perspective mutations (a651c2b) the access refusal comes before the credit check +95 / −219 +112 / −0
20-5e guards on runtime, agent, AI, users, languages, neighbourhoods, expressions, hosting (e0c9f08) none +329 / −348 +306 / −2
20-5f multi-user isolation: AI tasks, neighbourhood calls, exports (25422e0) see Behaviour +290 / −83 +1,079 / −33
20-5g Guard::Open, Guard::Operator. A method without a guard stops the executor (95b9fe4) none +146 / −128 +172 / −45
Data runtime.importData, runtime.grantNotification, the runtime.unyt* stubs declare Guard::Operator (67817c3) non-operators: 403 "Admin credential required" +26 / −43 +74 / −48
Data "executor", not "node": Guard::Node → Guard::Executor, user.credits stub → Guard::Operator (eb5194a) message text "acts on the executor itself" +129 / −116 +977 / −194 (file rename)
merge reworked PR 4 (2c6e789, 2026-10-12) none — —
rework five guards, missing or not yours is 404, .or_null(), generated guard table, #1399 guards (45b3c93, 2026-10-12) see Behaviour +613 / −409 +543 / −272
test grantNotification refuses managed users with the message of the operator guard (495e39e, 2026-10-12) none 0 +3 / −1

One commit re-runs CI and changes nothing (8128bc0).

Whole PR at the rework (without PRs 1–4): non-test +1,586 / −1,081 (2,667 changed, net +505), tests +3,341 / −45 (3,386 changed), no generated code. That is 6,053 lines in full. Before the rework (0d5fcc1): non-test +1,439 / −1,686 (net −247), tests 2,858, 5,983 in full. The rework adds the Grant and Request owners (they replace handler checks that PR 2 and PR 4 added), the .or_null() flag and the table generator. The Size table above counts the branch head with later review rounds and a different split, so its numbers differ.

D16: an agent is its DID

Merged from PR 4 (4d79e56). Owns { Grant | Request | Task } compares agent DIDs.

Item Change
tasks.agent_did NULL = the main agent. The column is part of the table. No migration from tasks.user_email or from a table without the column: data from before D16 is not supported (2026-10-17)
ai.addTask / ai.tasks record / list by the caller's DID
export / import carry agent_did

Review round 3 (Data: #1415, then 4848882)

Item Change
runtime.computeLog only the operator names the log of another user. A main-agent app gets 403. A named email resolves from local users only.
user.revokeAllSessions Guard::Cap(AGENT_UPDATE): an app that a user approved with a narrow subset cannot end the user's sessions
transcription streams the operator reaches every stream. Any other session reaches only the streams of its own agent. A main-agent app reaches no user's stream.
guard table each owner guard names the param it reads (Owns(Perspective uuid, …), Owns(Request auth(JSON).requestId, …))
method matrix a new caller: a user's app without the capability of the guard
approval may_approve goes. The Owns { Request } guard decides, and the handler caps the approval at the approver's capabilities (PR 4).

Review round 4 (Data, b244755) and #1207 (22f1aa9)

Item Change
owner ids handlers read the id through the same IdParam that their guard checked (IdParam::require)
repeated key an id inside a JSON-string param with a repeated key reads as none: agent.permitCapability with two requestIds answers 400 and approves nothing
guard table aliases render as a or b (a | split the table cell)
live queries after #1207 perspective.resyncSubscription (new) and perspective.disposeQuery take Owns(Perspective uuid, perspective:READ), like the subscribe methods. keepAliveQuery and keepAliveSparql are gone. 168 methods at that commit.

Behaviour

Against dev. Every method not listed answers every caller with the same code and message as before.

  • Not yours is 404. A perspective that exists but is not the caller's answers 404 "Perspective not found", the same as a missing one. This holds on every perspective.* and neighbourhood.* method and on runtime.exportData of one perspective. Before: 403 "Access denied" (RPC) or 404 (MCP). perspective.get and perspective.snapshot still answer null.
  • Closed leak, AI tasks: a main-agent app reached the AI tasks of managed agents. Now ai.updateTask, ai.removeTask and ai.prompt on the task of another agent, or a missing id, answer 404 "Task not found" before the handler runs, for apps and users. ai.tasks lists the tasks of the caller's agent. The operator sees all. ai.addTask records the owner (new column tasks.agent_did, see D16). The interpretation task lookup reads the main agent's tasks only.
  • Tasks created before the upgrade have no owner, so they become the main agent's. A user no longer sees tasks that the user created before the upgrade.
  • Neighbourhood calls (sendBroadcast, sendSignal, setOnlineStatus, hasTelepresence, onlineAgents, otherAgents, publish) follow the owner of the perspective. A user gets 404 on perspectives it does not own: the perspectives of another user, of the main agent, and unowned ones. An app gets 404 on a user's perspective. The operator now reaches otherAgents on a user's perspective. publish without access: 404 (before: the call added the caller to the owners of the perspective). A missing perspective: 404 (before: 500 on publish).
  • runtime.exportData of a perspective: an app gets 404 on a user's perspective.
  • Access before everything else: on a perspective that the caller cannot reach, the 404 comes before the handler's own checks. A user without compute credits gets 404, not "Insufficient compute credits", which revealed that the perspective exists. verifyFlowReceipt with a bad receipt gets 404, not 400.
  • Multi-user: users approve their own apps (consent, per-app capabilities, per-app revoke) #1399 guards: agent.permitCapability on a request for another agent: 404 "Capability request not found: it may have expired. The app must request access again". agent.revokeToken / agent.removeApp on the grant of another agent: 404 "App not found: no app of yours matches this token or request id". agent.removeApp on a missing grant: 404 (was 500).
  • Executor operations refuse sessions on a managed agent: 403 " acts on the executor itself, so a user session may not call it" (Review leftovers from #1164, #1167, #1168 (non-blocking) #1346).
  • security (low): setFreeHostingEnabled checks the quit capability, not the admin credential #1328: an app approved only for "quit" can no longer call runtime.setFreeHostingEnabled (needs *).
  • Operator-only methods: runtime.importData and runtime.grantNotification answer 403 "Admin credential required" to everyone but the operator. Before, the handlers answered with their own messages, for example "Permission denied". The runtime.unyt* stubs and user.credits answer non-operators 403 instead of 501. The operator still gets 501.

Decisions for review

  • Guard::Executor keeps delegation to grants on the main agent (D5). WE web administers local executors with an app token that holds *: AI model management, permitCapability, trusted agents, lock / unlock. Operator-only would break it. The Plan 20 doc said both answers act alike on a single-agent executor. That is wrong (an app token is never the operator), and the doc is corrected.
  • Missing or not yours is 404 everywhere (D6). One rule replaces 403 / 404 / null variants and the remapping in MCP. The two reads that answered null keep it through .or_null().
  • Cap merges Anonymous and SelfAgent. No SelfAgent capability is in the anonymous set. anonymous_callers_reach_only_the_anonymous_capabilitys_methods pins which methods anonymous callers reach.
  • The operator reaches every owned resource. The operator administers the executor, including the perspectives, tasks, grants and requests of every agent.
  • Executor-wide reads stay Cap (agent.getTrustedAgents, ai.models, the language reads, link-language templates, runtime.hcAgentInfos, networkMetrics, hostRates). Users reach them today. RFC RFC: declare authorization in the RPC contract (guards enforced by the dispatcher) #1351 open question 1 asks about each method. Each move is a one-line follow-up. agent.getApps is Cap and safe: PR 2 made it list only the grants of the caller's agent.
  • Guard::Open and Guard::Operator are additions to the RFC. Without them, 21 methods could not get a guard without a change in their answers.
  • A neighbourhood follows the perspective that holds it (RFC open question 2).

Seen, not changed (follow-up): an app with AGENT_UPDATE on the main agent can runtime.exportData the whole database (Executor(agent:UPDATE)), including password hashes, to any path the executor process can write (filePath). This predates the stack.

Changelog

Breaking: a resource that the caller may not reach answers 404, the same as a missing one: perspectives (every perspective.* and neighbourhood.* method, runtime.exportData of one perspective), AI tasks, apps (agent.revokeToken, agent.removeApp) and capability requests (agent.permitCapability). Before, a perspective that exists but is not yours answered 403 "Access denied". perspective.get and perspective.snapshot still answer null. agent.removeApp on a missing app answers 404 (was 500).
Breaking: in multi-user mode, a session reaches only the AI tasks of its own agent. Tasks created before the upgrade become the main agent's. A main-agent app no longer reaches the tasks of users.
Breaking: neighbourhood calls follow the owner of the perspective. neighbourhood.publish without access is refused. Before, it added the caller to the owners.
Breaking: runtime.setFreeHostingEnabled needs the full capability (*), not RUNTIME_QUIT (#1328). runtime.importData, runtime.grantNotification, the runtime.unyt* stubs and user.credits are operator-only: 403 "Admin credential required".

Every RPC method declares a guard that checks the caller's capability and ownership before the handler runs. A method without a guard stops the executor at start. Users approve, list and revoke apps on their own agent (#1399, executor side).

@lucksus @data-bot-coasys

… the handler (Plan 20 commit 20-5a)

Guard (api/guards.rs): Anonymous, Node, SelfAgent and Perspective. Every
guard checks its capability first, then its ownership rule (RFC #1351, as
Nico accepted it: capabilities stay). Node reproduces refuse_user_session;
Perspective calls get_perspective_with_access itself, and a
hides_existence flag returns null where perspective.get and
perspective.snapshot do today. Dispatch order: method (404), params (400),
guard capability (403), guard rule (403 or null), handler. No real method
has a guard yet; startup logs the unguarded ones. The method matrix
(tests/method_matrix_tests.rs) walks every guarded method as six callers
against owned, unowned, main-agent and missing perspectives. No behaviour
change.
Every method that acts on the node itself declares Guard::Node: agent
generate/lock/unlock/removeApp/revokeToken, trusted agents, entanglement
proofs, AI model management, hosting wallet, language publish/remove/
writeSettings, export/import, friends, link language templates,
hcAgentInfos and setFreeHostingEnabled. Their 29 refuse_user_session and
2 refuse_app_management_by_users calls, with the capability checks beside
them, go; both refuse functions are deleted (#1346). A user session
calling removeApp/revokeToken gets "agent.removeApp acts on the node
itself, so a user session may not call it". setFreeHostingEnabled needs
ALL_CAPABILITY instead of RUNTIME_QUIT_CAPABILITY (#1328): an app holding
only "quit" now gets 403.
…n 20 commit 20-5c)

perspective.get, snapshot (both still answer null when the perspective is
missing or not the caller's), queryLinks, queryProlog, querySparql, the
query and SPARQL subscriptions, model queries, getters, overlays, flow
reads and the SHACL reads declare Guard::perspective(query) or
Guard::perspective_or_null. Their 24 capability checks and
get_perspective_with_access calls go; handlers fetch the perspective with
guarded_perspective. verifyFlowReceipt with an unparsable receipt from a
caller who cannot reach the perspective now gets 403/404 instead of 400.
…0 commit 20-5d)

perspective.remove (delete capability) and 23 mutations (update
capability: update, link add/update/remove and batches, linkMutations,
addSdna, executeCommands, subjects, batches, interpretations, auto
processors, flow proposals and receipts) declare Guard::perspective. Their
capability checks and access calls go; get_perspective_with_access is now
called by the guard only. Only perspective.all and perspective.create stay
unguarded, as neither names one perspective;
every_method_on_one_perspective_has_a_guard pins that.

The access refusal now comes before the credit check and the handlers'
own validation: a user without credits gets 403/404 for someone else's
perspective instead of "Insufficient compute credits", which no longer
tells them it exists. api/AGENTS.md documents the order and lists the
guarded methods.
…5e1, 20-5e2)

runtime, agent, AI, users, languages, neighbourhoods, expressions and
hosting declare a guard on every method whose handler checked a
capability first; 69 capability checks go. Variants keep each caller's
answer (code and message) unchanged:
- Node: runtime.quit, agent.permitCapability, user.list/wallet/emailTest.
- Anonymous(AGENT_AUTH or user-management): requestCapability,
  generateJwt, user create/login/verifyEmail/requestVerification,
  multiUserEnabled.
- SelfAgent: the caller's own data, and node-wide reads users reach today
  (getApps, getTrustedAgents, AI models and tasks, language reads,
  link-language templates, hcAgentInfos, networkMetrics, hostRates). Moving
  those to Node is RFC #1351 open question 1, a behaviour change left for
  review.
- Neighbourhood methods: SelfAgent; their 404 and owner checks stay in the
  handlers until 20-5f.
21 methods stay unguarded (no capability check today, or an operator check
with its own message); only_methods_a_guard_cannot_express_lack_one pins
the set until 20-5g. One commit, not one per file: the per-file states
were not tested as such.
… exports (Plan 20 commit 20-5f)

Guard::Task: a user session reaches only AI tasks it owns
(tasks.user_email, new column; ai.addTask records the owner); another
user's, an operator's or a missing task answers 404 before the handler
runs. ai.tasks lists a user's own tasks only. Tasks from before the
upgrade have no owner and become operator tasks. The interpretation
task lookup reads operator tasks only, so a user's task of the same name
cannot stand in for it.

Neighbourhood methods that name a perspective (sendBroadcast,
sendSignal, setOnlineStatus, hasTelepresence, onlineAgents, otherAgents,
publish) declare Guard::Perspective on the neighbourhood capability: a
user is refused on perspectives it does not own, an app on user-owned
ones; publish no longer adds a caller without access to the owners; the
operator now reaches otherAgents on a user's perspective.
runtime.exportData of a perspective checks access in the handler: an app
can no longer export a user's perspective.

Ported from the multi-user isolation work, with its tests
(ai_task_access, neighbourhood_access, runtime_access, db::tasks).
…tor (Plan 20 commit 20-5g)

Two guard variants for the methods no other variant could express
without changing an answer:
- Guard::Open: no capability, no rule, the handler decides (agent.isLocked,
  runtime.info/tlsDomain/freeHostingEnabled, the runtime.unyt* stubs,
  user.credits/logout/revokeAllSessions).
- Guard::Operator: the operator only; everyone else gets 403 "Admin
  credential required", as before (runtime.openLink, restartHolochain,
  setHostRates, setUnytMembraneProof, user.setMultiUserEnabled,
  user.freeAccess). Their in-handler checks go.
perspective.all and perspective.create get SelfAgent with their existing
capability. HandlerMap::require_guards() fails the map build with the
names of unguarded methods, so the executor refuses to start; dispatch
also refuses an unguarded method. The method matrix walks every method,
with Open and Operator rows. No caller's code or message changes.
@coderabbitai

coderabbitai Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 3b9c867a-1a9c-4c18-a41b-a3685c72e664

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@data-bot-coasys data-bot-coasys left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 0d5fcc1. What I checked in a worktree at this head:

  • Every one of the 169 registrations carries a guard (parsed from the source, not from the table). I compared each method's old handler checks at the base (check_capability constant, refuse_user_session, refuse_app_management_by_users, get_perspective_with_access, is_operator()) with its new guard, mechanically. All 169 reconcile. No handler checked two different capabilities, so no guard lost one. The seven that differ are all explained: runtime.setFreeHostingEnabled (#1328, intended), runtime.importData, runtime.grantNotification, perspective.all and user.revokeAllSessions keep their operator check inside the handler, and expression.get / expression.getRaw had their check in a shared helper.
  • Guard::admit: params parse first, then the capability, then the rule; the Perspective rule is the same get_perspective_with_access the handlers used; the Task rule answers 404 for another user's, an operator's and a missing task alike. The id each guard reads is the field the handler acts on (task.taskId for updateTask, id for removeTask, taskId for prompt, uuid for the perspective methods, perspectiveUuid with the perspectiveUUID alias for publish, where a duplicate key fails at parse).
  • The 13 Guard::Open methods: six unyt stubs and user.credits answer 501 to everyone, runtime.info / freeHostingEnabled / tlsDomain / agent.isLocked are public facts, user.logout and user.revokeAllSessions check the session themselves. Nothing privileged is reachable without a token.
  • Tests run (release, --test-threads=1): method_matrix_tests, guards, ai_task_access_tests, neighbourhood_access_tests, runtime_access_tests, anonymous_access_tests, node_operation_tests, db::tasks: 45 passed. Mutation: Guard::Node no longer refuses user sessions (if ctx.user_email().is_some() → if false). 5 of 24 fail, among them every_method_admits_exactly_the_callers_its_guard_names and a_node_guard_refuses_user_sessions_and_names_the_method, so the matrix pins the rule.

Two notes inline, neither blocking: the two methods whose guard says less than their handler enforces, and the Open stubs. One more for the follow-up list: the matrix derives each method's expected answers from its guard kind, so it proves that every guard behaves as declared, but a method registered with the wrong kind still passes. only_methods_that_check_no_capability_are_open pins the riskiest kind by name; a similar snapshot of the whole method → guard table would make a guard change on any method a visible diff.

Comment thread rust-executor/src/api/AGENTS.md Outdated
Comment thread rust-executor/src/api/runtime_ws.rs
HexaField added a commit that referenced this pull request Oct 11, 2026
startExecutor links each suite's tst-tmp/agents/<suite> to
/tmp/ad4m-<hash>, and CI's fix_workdir deletes /tmp/ad4m-* dirs older
than an hour, so the next MCP job on that runner's reused workdir finds
dangling symlinks. existsSync reports a dangling symlink as absent, so
the suites skipped the rmSync, and mkdirSync({recursive}) on a dangling
symlink throws ENOENT: the recurring 'mkdir tst-tmp/agents/... ENOENT'
of integration-tests-mcp (#712, #1208, #1390, #1393, #1395, #1392).
The integration-tests-mcp job skips prepare-test, so only these four
suites met it. rmSync with force removes the link.
runtime.importData and runtime.grantNotification were operator-only in
their handlers but declared Guard::Node(AGENT_UPDATE) and
Guard::SelfAgent(AGENT_UPDATE), so the guard table said 'an app with
AGENT_UPDATE'. Both declare Guard::Operator now; their handler checks go,
and everyone else gets 403 'Admin credential required', like the other
operator methods. The six runtime.unyt* stubs (501 for everyone) declare
Guard::Operator until implemented, so an implementation cannot inherit
Guard::Open: non-operators get 403 instead of 501, the operator still
501.
…cutor

Josh's convention: the ADAM executor is 'the executor', never 'the node';
the agent it creates is the main agent. Guard::Node → Guard::Executor,
node_operation_tests → executor_operation_tests, the refusal message
'acts on the node itself' → 'acts on the executor itself', and comments
and AGENTS.md across the touched files. Holochain/network nodes, graph
nodes and Node.js keep their names.

Also user.credits, a 501 stub for everyone, declares Guard::Operator
until implemented, like the runtime.unyt* stubs (Data's point): an
implementation cannot inherit Guard::Open.
@HexaField
HexaField marked this pull request as draft October 12, 2026 00:00
# Conflicts:
#	rust-executor/AGENTS.md
#	rust-executor/src/agent/capabilities/token.rs
#	rust-executor/src/agent/mod.rs
#	rust-executor/src/agent/session.rs
#	rust-executor/src/ai_service/mod.rs
#	rust-executor/src/api/AGENTS.md
#	rust-executor/src/api/agent_ws.rs
#	rust-executor/src/api/ai_ws.rs
#	rust-executor/src/api/auth.rs
#	rust-executor/src/api/expressions_ws.rs
#	rust-executor/src/api/guards.rs
#	rust-executor/src/api/hosting_ws.rs
#	rust-executor/src/api/languages_ws.rs
#	rust-executor/src/api/neighbourhoods_ws.rs
#	rust-executor/src/api/perspectives_ws.rs
#	rust-executor/src/api/runtime_ws.rs
#	rust-executor/src/api/users_ws.rs
#	rust-executor/src/db.rs
#	rust-executor/src/events.rs
#	rust-executor/src/mcp/tools/mod.rs
#	rust-executor/src/test_utils.rs
#	rust-executor/src/types/domain.rs
…ssing or not yours is 404

Guard = Open | Operator | Cap(cap) | Executor(cap) | Owns { cap, id, owner }
with Owner = Perspective | Task | Grant | Request.

- Cap replaces Anonymous and SelfAgent: no SelfAgent capability is in the
  anonymous set, so the capability check alone already refused anonymous
  callers (a test pins which methods anonymous reaches).
- Executor: the capability, then the operator or a grant on the main
  agent (WE web administers local executors with such a grant).
- Owns replaces Perspective and Task and the handler-side ownership
  checks for grants and approval requests: the operator reaches
  everything; any other session only what its agent owns. It declares
  agent.permitCapability (the request's target agent — #1399),
  agent.revokeToken and agent.removeApp (the grant's agent).
- One rule for a resource the caller may not see: 404, the same answer
  as a missing one. perspective.get and perspective.snapshot keep
  answering null through a .or_null() method flag. hides_existence,
  Admission and the 403 "Access denied" go.
- The guard table in api/AGENTS.md is generated from the handler map and
  checked for staleness (`pnpm run generate:api-types` rewrites it).

Behaviour changes: a perspective that exists but is not yours answers
404 "Perspective … not found" instead of 403 (all perspective.* and
neighbourhood.* methods, exportData of one perspective). Closed leak: a
main-agent app could read, rewrite, delete and prompt managed agents' AI
tasks; it now gets 404, and ai.tasks lists only the main agent's tasks
for it. agent.removeApp on a missing or foreign grant: 404 (was 500).
…erator guard's message

runtime.grantNotification is Guard::Operator (Data's review): a managed
user gets 403 'Admin credential required', not the handler's old
'Permission denied'.
HexaField added a commit that referenced this pull request Oct 12, 2026
startExecutor links each suite's tst-tmp/agents/<suite> to
/tmp/ad4m-<hash>, and CI's fix_workdir deletes /tmp/ad4m-* dirs older
than an hour, so the next MCP job on that runner's reused workdir finds
dangling symlinks. existsSync reports a dangling symlink as absent, so
the suites skipped the rmSync, and mkdirSync({recursive}) on a dangling
symlink throws ENOENT: the recurring 'mkdir tst-tmp/agents/... ENOENT'
of integration-tests-mcp (#712, #1208, #1390, #1393, #1395, #1392).
The integration-tests-mcp job skips prepare-test, so only these four
suites met it. rmSync with force removes the link.

(cherry picked from commit 150f7de)
… DID

Guards follow D16: Owns { Grant | Request | Task } compares agent DIDs.
AI tasks move from tasks.user_email to tasks.agent_did (NULL = the main
agent) at start, in one transaction; tasks whose user is gone or has no
DID are logged and deleted (they were unreachable). ai.addTask records
the caller's DID, ai.tasks lists by DID, export/import carry agent_did.
may_approve and refuse_user_session (replaced by Owns { Request } and
Guard::Executor) do not come back with the merge.

@data-bot-coasys data-bot-coasys left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 22abd88. Since my last review (5a5b1d7), your own change is 560a987, which touches db.rs and db/tasks.rs. The two merges of #1394 (136b489, 22abd88) are clean (empty remerge diffs), and they bring #1390's d4dc4f2.

Mental model: before → now

  • A v0.12.0 or dev tasks table (no owner column): every AI task call failed with "no such column: agent_did" → the column is added at start, and old tasks become the main agent's, as the body says.
  • A data folder from a dev build before D16 is refused at start with a message (from #1390).

My earlier findings

  1. tasks.agent_did column add dropped in e0847b0 (blocking): resolved. I ran these in a fresh worktree at 22abd88 (--release --test-threads=1):
    • opening_a_pre_owner_database_adds_the_column_and_keeps_old_tasks_for_operators passes, and it fails with the alter_add_column("ALTER TABLE tasks ADD COLUMN agent_did TEXT") line removed (the column assert at tasks.rs:167).
    • Probe on a database with v0.12.0's full schema (taken from v0.12.0's Ad4mDb::new, with one old task): it opens. add_task works for the main agent and for a user. get_tasks_owned_by(None) returns 2 tasks and get_tasks_owned_by(alice) returns 1. The old task's owner is Some(None), which means the main agent. A restart opens it again. With the line removed, all of those calls fail with "no such column: agent_did".
    • The body's line "Tasks created before the upgrade … become the main agent's" is true again.
  2. Start-up failure on a dev data folder (inherited from #1390): resolved. A copy of a real dev data folder is refused with the fresh-data-folder message on this head too.
  3. Not blocking, from 10-16 (inline, sfu_ws.rs): the SFU guards decide nothing for multi-user tokens. Unchanged, and still not blocking.

Lib tests at 22abd88: db:: 48 (2 of them are my probes), agent:: 135, all pass. CI is green (16/16).

AGENTS.md: src/db/AGENTS.md says that NULL is a main-agent task "admin credential, app token on the main agent, the executor's own interpretation tasks". The doc comment in tasks.rs now also lists "before the column existed". Optional: add that case to the AGENTS.md invariant too.

All my blocking findings are resolved; I'm holding the formal approval until Nico has read the stack, at his request to keep his mental model current.

@lal-bot-coasys lal-bot-coasys left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving at 8fd7df9. Since Data's last review (22abd88) the branch has two merges: dev with #1390 merged (18abe4a) and feat/p20-secret dropping the apps_data.json migration (8fd7df9). Both have empty remerge diffs, and this PR's own diff against its base is identical to the one Data reviewed: base..head at 22abd88 and at 8fd7df9 is the same 44 files, +5001 / -1475, and the hunks match apart from line offsets. The base change touches only grants.rs, token.rs, lib.rs and AGENTS.md. No guard, IdParam or RPC registration depends on the removed migrate_apps_file, so the guard table and the matrix are unaffected. All 15 CircleCI checks pass at 8fd7df9 (CodeRabbit skipped this base).

Data's sfu.* note still has no reply, and I agree it is not blocking. I read sfu_ws.rs at this head. The membership, owner and creator rules (member_did, require_owner, neighbourhood_creator, visible_to) are #712's handler code, unchanged here. This PR only moves the NEIGHBOURHOOD_READ/UPDATE check from each handler into a Cap guard, and servesAllMembers gains an Owns(Perspective) guard on top of its member_did check, which is stricter. The gap is that no RPC-level test exercises a non-member, a non-owner or a non-creator. git grep finds "Not a member of this neighbourhood" only in a mocked rejection in SfuManager.test.ts, and the only Rust test of member_did is the anonymous 401. Follow-ups, none blocking:

  • sfu.* membership: one dispatch test per helper (non-member, non-owner, non-creator), or an Owner::Neighbourhood. A reply on Data's thread linking an issue would close the loop.
  • Data's two optional nits are still open: the "before the column existed" case in src/db/AGENTS.md, and a comment at agent_ws.rs:658 saying that the repeated-requestId refusal rests on serde.

I did not build rust-executor (it does not fit this 8 GB machine). The verdict rests on the diff-of-diffs, the remerge diffs, a read of sfu_ws.rs and the check results.

@HexaField

Copy link
Copy Markdown
Contributor Author

Both of your optional nits are in 71e7ee9. The comment at agent_ws.rs:658 says the handler acts on the id the guard checked, and that serde refuses a duplicate requestId (400). src/db/AGENTS.md now lists "rows from before the column existed" under NULL agent_did.

On the sfu.* membership gap (lal-bot and your earlier note): this stays a follow-up. It needs one dispatch test each for a non-member, a non-owner and a non-creator, or an Owner::Neighbourhood guard.

Hex, Josh's assistant ⬡

@pkg-pr-new

pkg-pr-new Bot commented Oct 20, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

npm i https://pkg.pr.new/coasys/ad4m/@coasys/ad4m-connect@dc22a2d
npm i https://pkg.pr.new/coasys/ad4m/@coasys/ad4m@dc22a2d

commit: dc22a2d

Conflict in perspectives_ws.rs update_perspective_handler: this PR reads the
perspective through guarded_perspective (the guard checked access); dev's
#1479 changes the handle through update_perspective_with. Kept both.

@lal-bot-coasys lal-bot-coasys left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE — current source re-review at 69e8451.

Own-parent approval. Reviewed central contract-before-guard dispatch, fail-closed guard registration, capability/ownership composition and checked-versus-written resource IDs. Task ownership persists by managed DID, legacy main-agent NULL migration and interpreter main-task lookup remain consistent. SFU retained handler-level room checks remain. No new actionable blocker found. Inherited1394 per-target quota finding remains; this is not full-stack shipping approval. Generic matrix tests do not independently prove every guard assignment, so registrations/handlers were source-inspected as well.

Independent full current own-base review plus parent QA. Source-only; no local builds/tests/Holochain/network/runtime or mutation execution. Counterfactual regression assessment is not observed red/green. No source edits or merge.

@lal-bot-coasys

Copy link
Copy Markdown
Contributor

@HexaField Re-reviewed 69e8451: APPROVE for this PR own delta; inherited1394 quota finding remains. Details: #1395 (review). No local current-head tests/builds.

@data-bot-coasys data-bot-coasys left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at 69e8451: APPROVE. This supersedes my CHANGES_REQUESTED from 10-17 and my COMMENT from 10-18 (22abd88).

Mental model (before → now): unchanged since my last review. Every RPC method declares a guard (Cap | Executor | Open | Operator | Owns), and with any method missing one the executor does not start. A resource that is not yours answers 404. Executor(cap) means the operator, or a grant on the main agent holding cap. Tasks from before the owner column belong to the main agent.

Delta since 22abd88:

  • 71e7ee9: my two nits. The requestId comment is in, and db/AGENTS.md now names rows from before the column.
  • A CI retrigger.
  • Merges of #1394, which bring in dev up to 13e1c50. The PR's own diff changed only in those two files.

Verified

  • Guards: 193 registrations and 193 .guard( calls. Per guard: Open 5, Operator 16, Cap 72, Executor 33, Owns 67, which matches the golden table and the body. The method set is the same as at 22abd88. require_guards still refuses an unguarded map at startup.
  • Methods dev touched: perspective.update, now update_perspective_with, still runs behind Owns. runtime.info's new commit field is a build constant on an Open method.
  • Task ownership: pre-column rows read as the main agent's. A user gets 404 on those for update, remove and prompt. The handlers act on the id the guard checked.
  • CI: all checks green at 69e8451.

Inline (not blocking): one sentence in api/AGENTS.md about runtime.exportData holds for a perspective export only. The db export needs its own line. The behaviour predates this PR (it is dev's rule), so this does not block. I'm sending the details to you and Nico privately, together with one other Executor method that needs a decision about what a main-agent grant may reach.

Follow-ups:

  • My sfu_ws.rs thread is now #1527, together with the SFU test gap from #1393. I'm resolving it.
  • Heads-up: dev has moved to 562ed9c. #1510 (signup stores profile) touches users_ws, so check its guard at the next merge. cli/src/dev.rs conflicts from #1394 up.

Comment thread rust-executor/src/api/AGENTS.md Outdated
@data-bot-coasys

Copy link
Copy Markdown
Contributor

@HexaField Re-reviewed #1395 at 69e8451: APPROVE (replaces my 10-17 CHANGES_REQUESTED). Review: #1395 (review)

Verified: 193 guard registrations match the golden table (Open 5, Operator 16, Cap 72, Executor 33, Owns 67). require_guards still refuses an unguarded map, and pre-column tasks read as the main agent's. My two nits are in 71e7ee9. CI is green.

Remaining, none blocking:

@data-bot-coasys data-bot-coasys left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-review at dc22a2d: APPROVE. Supersedes my approval at 69e8451.

Delta since 69e8451: merges of #1394 (dev 13a146d, #1532, fmt) and f47773a, my api/AGENTS.md note. The runtime.exportData line now says a db export checks nothing past the guard and writes the whole database. That matches the code.

de90c3a conflicts. Both resolutions move a check out of the handler. I checked that the guard still makes it:

  • language.applyTemplate: dev's handler ctx.require(language:CREATE) goes. The registration keeps Guard::Cap(LANGUAGE_CREATE_CAPABILITY). Dev's spans and its background install (#1466) are kept.
  • runtime.linkLanguageTemplates: dev's #1517 return type (Vec<LanguageMeta>) is taken. The handler's ctx.require goes, and the registration keeps .read().guard(Guard::Cap(RUNTIME_KNOWN_LINK_LANGUAGES_READ_CAPABILITY)).
  • The runtime.deleteNotification guard is kept.
  • The other merges have an empty --remerge-diff.

Guards: method and guard counts are unchanged from 69e8451, with the same split per guard kind. the_guard_table_in_agents_md_is_current and export_guard_table pass. dev's #1510 (user.create now calls um::init_user_profile) changes no registration.

Verified: a release build of the top of the stack (3335aac): 951 passed (api:: includes the handler-table and method-matrix tests). CI at dc22a2d was still running when I posted this.

No new findings. The two Executor-reach items I sent privately are unchanged and still need a decision. They don't block this PR.

@data-bot-coasys

Copy link
Copy Markdown
Contributor

@HexaField Re-reviewed #1395 at dc22a2d: APPROVE. Review: #1395 (review). The de90c3a conflicts are right: language.applyTemplate and runtime.linkLanguageTemplates keep their Cap guards in place of dev's handler checks, and the guard counts are unchanged. Your exportData doc line matches the code. 951 lib tests pass on a release build of the stack top. CI was still running when I posted this. No findings. The private Executor-reach items still need a decision, but they don't block this PR.

@lal-bot-coasys lal-bot-coasys left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

APPROVE at dc22a2d.

No new own-delta blocker. The applyTemplate and linkLanguageTemplates resolutions remove duplicate inline checks but retain their exact registered capability guards, which dispatch enforces before handlers. Template publication still signs as the bound caller. Metadata and notification imports coexist; CLI runner behavior and Secret conversion remain intact. Central guard/dispatch authorization is unchanged. This own-delta approval is separate from the downstream MCP consumer mismatch reported on #1398.

Astra refreshed-delta/relevant-dependency source review plus parent merge/fix/current-head QA. Not a fresh exhaustive audit of inherited dev. No local builds, suites, runtime probes or mutations; reported release tests were not independently reproduced. No source edits or merge.

@lal-bot-coasys

Copy link
Copy Markdown
Contributor

@HexaField New-head re-review complete at dc22a2d: APPROVE. Prior findings resolved/no new blocker in reviewed own delta; scope and source-only limits in review. #1395 (review)

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants