__tests__feat: a refused agent says so, and offers the way in The agent page rendered "This agent could not be loaded" with the raw error underneath — which is what a refusal, a deleted agent and a network drop all look like. A refusal is not a failure and must not render as one; documents learned that in Phase 6 and agents had not. It now shows the request-access screen, saying "agent" rather than "document". Only on NOT_FOUND, which is what agent.get answers both for an agent you may not open and for one that is not there — deliberately the same answer, since telling them apart tells you an agent exists. Everything else keeps the error rendering. The hard part is that the caller has an agent id from a URL and cannot turn it into a document id: the resolver that would is the one refusing them. files.agentRefusalContext does it server-side, finding the subagents row by metadata->>'agent_id' org-wide and keeping the ANSWER narrow — a document id only for a row in the caller's own org, the same empty context for "no such agent" as for "an agent in another organisation", and never the agent's name. It prefers the org-level row when a rep also holds their own copy, because a grant on somebody's personal copy is not an audience any approver administers. The route is on files rather than agent: every procedure on the agent router goes through resolveAgentForCaller, whose documented invariant is that it never returns another user's agent — which is every agent this answers about. One screen, not two. An agent is a document, and a second screen saying the same sentence is a second screen to keep in step. Co-Authored-By: Claude Opus 5 (1M context) <<email>> Claude-Session: https://claude.ai/code/session_014LBvyeyqQ8U7J2aRYpD9mLSep 12, 2026, 10:13 AMcomponentsfeat: a refused agent says so, and offers the way in The agent page rendered "This agent could not be loaded" with the raw error underneath — which is what a refusal, a deleted agent and a network drop all look like. A refusal is not a failure and must not render as one; documents learned that in Phase 6 and agents had not. It now shows the request-access screen, saying "agent" rather than "document". Only on NOT_FOUND, which is what agent.get answers both for an agent you may not open and for one that is not there — deliberately the same answer, since telling them apart tells you an agent exists. Everything else keeps the error rendering. The hard part is that the caller has an agent id from a URL and cannot turn it into a document id: the resolver that would is the one refusing them. files.agentRefusalContext does it server-side, finding the subagents row by metadata->>'agent_id' org-wide and keeping the ANSWER narrow — a document id only for a row in the caller's own org, the same empty context for "no such agent" as for "an agent in another organisation", and never the agent's name. It prefers the org-level row when a rep also holds their own copy, because a grant on somebody's personal copy is not an audience any approver administers. The route is on files rather than agent: every procedure on the agent router goes through resolveAgentForCaller, whose documented invariant is that it never returns another user's agent — which is every agent this answers about. One screen, not two. An agent is a document, and a second screen saying the same sentence is a second screen to keep in step. Co-Authored-By: Claude Opus 5 (1M context) <<email>> Claude-Session: https://claude.ai/code/session_014LBvyeyqQ8U7J2aRYpD9mLSep 12, 2026, 10:13 AMhooksfix(sharing): close the gates a review found open, and make one predicate mean one thing Six holes, and they fall into two shapes. SURFACES NEVER BROUGHT ONTO THE GATE. `documents.createShareLink` and its four siblings were written before the access model and check tenancy alone, so any member of an org could mint a permanent anonymous public link to any document in it — a teammate's file, a deal they are not on, a folder explicitly made private — and `/share/:token` honours those tokens. `agentSharing.list`/`resend` asked only whether the caller could RESOLVE the agent, which for an org-published one is true of everybody, so an agent's whole recipient list (names and addresses) was readable by the tenant and its invite notifications were a primitive anyone could aim at anyone. And `getFolderVisibility` took an id and no viewer at all, which made `visibilityPreview` an existence oracle over every organisation. TWO RENDERINGS THAT HAD DRIFTED. The floor was a chain guarded on `floor === null` where `accessibleFilter` renders independent OR arms — so the first arm to match suppressed the rest, and a read-only seat on a deal capped you on your own file inside it. `agentIdFromPath` scanned every segment while the SQL tests two, so a folder named `agent-{id}` four deep was reachable by id and invisible in every listing. Both are the same failure: a listing that admits a row the mutation then refuses, which is the split this subsystem exists to prevent. Also: `grantAccess` took a client-supplied user id with no membership check, and `namesFor` looks users up by id alone — so the share control doubled as a cross-tenant directory. `viewerFor` trusted its `orgId`, which the filter renders and the resolver does not. Private→public silently dropped the role you picked, because it read the org's id off an entry a barrier had cut. Plus a capped password on the unauthenticated unlock route (it reaches a blocking scryptSync), beacons bound to their token, two disjoint cache invalidations, a hardcoded "Owner" on every viewer of an agent panel, and a deterministic grant tie-break. `refusals.test.ts` pins all six; the two floor cases were checked against the old chain to be sure they fail for the right reason. Rides along: the `autoExpandDepth` work already in the tree. Two of its five files carry review fixes as well and cannot be split, so the test and doc that prove it come with them rather than landing a behaviour change without either. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 10, 2026, 8:14 PMtypes.tsfix(sharing): close the gates a review found open, and make one predicate mean one thing Six holes, and they fall into two shapes. SURFACES NEVER BROUGHT ONTO THE GATE. `documents.createShareLink` and its four siblings were written before the access model and check tenancy alone, so any member of an org could mint a permanent anonymous public link to any document in it — a teammate's file, a deal they are not on, a folder explicitly made private — and `/share/:token` honours those tokens. `agentSharing.list`/`resend` asked only whether the caller could RESOLVE the agent, which for an org-published one is true of everybody, so an agent's whole recipient list (names and addresses) was readable by the tenant and its invite notifications were a primitive anyone could aim at anyone. And `getFolderVisibility` took an id and no viewer at all, which made `visibilityPreview` an existence oracle over every organisation. TWO RENDERINGS THAT HAD DRIFTED. The floor was a chain guarded on `floor === null` where `accessibleFilter` renders independent OR arms — so the first arm to match suppressed the rest, and a read-only seat on a deal capped you on your own file inside it. `agentIdFromPath` scanned every segment while the SQL tests two, so a folder named `agent-{id}` four deep was reachable by id and invisible in every listing. Both are the same failure: a listing that admits a row the mutation then refuses, which is the split this subsystem exists to prevent. Also: `grantAccess` took a client-supplied user id with no membership check, and `namesFor` looks users up by id alone — so the share control doubled as a cross-tenant directory. `viewerFor` trusted its `orgId`, which the filter renders and the resolver does not. Private→public silently dropped the role you picked, because it read the org's id off an entry a barrier had cut. Plus a capped password on the unauthenticated unlock route (it reaches a blocking scryptSync), beacons bound to their token, two disjoint cache invalidations, a hardcoded "Owner" on every viewer of an agent panel, and a deterministic grant tie-break. `refusals.test.ts` pins all six; the two floor cases were checked against the old chain to be sure they fail for the right reason. Rides along: the `autoExpandDepth` work already in the tree. Two of its five files carry review fixes as well and cannot be split, so the test and doc that prove it come with them rather than landing a behaviour change without either. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 10, 2026, 8:14 PMview-as.tsxfix(tests): complete the fixtures the workspace types grew (types:test, 4/n) `AgentSummary` gained `namespace`/`defaultFile`/`chatEnabled`, `ResolvedAgent` gained `frontmatter`/`content`, `ResolvedFileGrant` gained `namespace`, and `SubagentSummary` gained `instructions` — none of which the fixtures said. The access harness re-declared the grant principal union by hand, which is how `agent` could be added to `PrincipalType` without the harness ever knowing; it derives from the column's own type now, so the next principal cannot silently skip it. 73 -> 64 errors. Co-Authored-By: Claude Opus 5 (1M context) <<email>>Sep 9, 2026, 10:48 AM