Repository navigation
let people work together in organizations, with shared and own connections and agents - #138
Merged
Merged
Conversation
… and one owner Everything stays the organization's: its connections, vault, agents and activity. A person can make more organizations or be invited into others', and the human side works in the one open in the switcher at the top of the sidebar (Session.activeOrgId, read on every request; the first one joined when none is open). Every organization has one owner, whoever made it, kept on the organization (ownerUserId): the owner renames and deletes it, invites people by email and removes members; members leave. Nobody deletes the last organization they own, so everyone owns one. An invitation (POST /api/orgs/:id/invitations) lasts a week, follows ALLOWED_SIGNUP_EMAILS for addresses without an account, is emailed with Resend or logged as [Invitation] in development or without RESEND_KEY, and keeps only its token's SHA-256. Its link, /invite/:token, signs the person in or up, shows the organization and who invited them (GET /api/invitations/:token), and is taken up only by the address it was sent to (POST /api/invitations/:token/accept), which opens the organization in their switcher. Members (/members) lists who is in the organization and who is invited; Settings renames and deletes the organization open, and deleting an account now deletes every organization the person owns, undoing their connections at their providers first, and takes them out of the others. /api/me carries the organization open and the person's role; the experiments on are the open organization's. One migration, v024, adds the owner, one membership per person per organization, the session's open organization and invitations.
A connection is either one member's own, which nobody else in the organization sees, its owner
included, or shared with every member of it (Connection.ownerUserId null), and a shared one stays
shared; connectedById keeps who connected it, which is who a shared sign-in acts as. Every read on
the human side goes through a Viewer, {orgId, userId}, which sees the organization's shared
connections and its own: the connection routes, the records routes, the home page's widgets and
an agent's grants. A shared connection is renamed, disconnected, signed in to again, given a new
key and has its defaults changed by whoever connected it and the organization's owner (managed.ts);
everyone else uses it as it is, its permissions shown read-only. The vault is each member's own in
each organization, made as they join it, so an organization has one per member.
Connecting asks who a new connection is for once anyone else is in the organization or invited:
`?share=everyone` on a sign-in's start, kept through the sign-in in its cookie with the organization
it began in, and `shared` in the body of the email, database, custom MCP server, API key and Plaid
routes. A sign-in to the same account renews the existing connection only where the person may
change it; another member's own connection of the account, or a shared one they only use, gets a
new connection beside it. `POST /api/connections/:id/share` shares one's own connection for good.
Leaving or being removed disconnects the member's own connections there; deleting an organization
disconnects every connection in it.
Connections shows Just me and Everyone once there is anyone to share with, the shared list saying
who each acts as, and a connection's page has Sharing: who can use it, who it acts as and who
connected it, with Share with everyone on one's own. Until agents are a member's own or shared
(the next slice), an agent reaches every connection in its organization, as before. Migration v025
adds the columns and makes every existing connection its organization's owner's own.
An agent login is one member's own, which nobody else in the organization sees, or shared with every member of it (Agent.ownerUserId null), and a shared one stays shared; createdById keeps who made it. An agent reaches what a viewer of its organization sees: a member's own agent the connections shared there and that member's own, a shared agent the shared ones alone, so `listConnections` and `loadConnection` serve the agent side as they serve the human side, and the interim where an agent reached every connection in its organization goes. A shared agent is changed, revoked, deleted, handed a new password and given its settings by whoever made it and the organization's owner; everyone else sees it as it is, its access read-only, and its MCP clients are theirs to see. `POST /api/agents/:id/share` shares one's own agent for good, dropping its settings on the member's own connections, which it no longer reaches. Postgres tells nulls apart in the unique index, so `createAgent` refuses a second shared login of a provider itself. The activity log and tasks say whose they are the same way (ownerUserId: a member's own agent's, or, for what Personal Agent Proxy did itself, whose own the connection was; null when shared), and the log, the lanes and a task's page show shared ones and the person's own. Leaving or being removed deletes the member's own agents there with their logins and MCP clients; a login made when an MCP client signs in is the person's own. Agents shows Just me and Everyone once there is anyone to share with, New agent asks who the agent is for, and an agent's page has Sharing, with what it reaches and who made it, and Share with everyone on one's own; the Sharing section is one component for connections and agents. Migration v026 adds the columns, makes every existing agent, entry and task its organization's owner's own, and replaces the unique index with one per owner.
Main added sub-tasks and request durations beside where the organizations work says whose a task or a log entry is: both are kept. Main also took migration v024, so the organizations migrations are now v025 to v027; a local database that applied them under the earlier numbers has to be reset.
The organization a page shows is named first in its address, by a slug fixed when it is made (`/acme/connections`), and the api's routes for everything in one are under `/api/orgs/:org/…`, read by `requireViewer`, which checks the person is a member. The session no longer keeps an open organization, so a link to a page is a link to it in that organization, two tabs can be in two organizations, and `/` sends the person to their first. `/connect-agent` asks which organization the agent is in when the person is in more than one; the seed, the screen map's crawl and the docs follow.
…e them A login is named by the person or after its provider, numbered as "Claude 2" when the person sees one of that name already. New agent and the consent page offer a new login of every provider; the setup flow waits for the login made while it is open. Agents can be renamed on their page.
Adding an agent is now: choose its kind, name it, which makes its login, then set up that agent. The setup screen always has one agent, and an MCP agent is signed in once a client has signed in as it. The setup leaves a note in the browser, so the sign-in page the client opens has the agent chosen. Names are no longer numbered. Only the choice of agent in the welcome flow can be skipped.
Claude Duet fits the new setup: its own tile, a Claude login made for it, and its own steps.
An organization's address is /<slug>, so a name such as MCP, API or Login now gets mcp-2, api-2 or login-2 rather than an address the api or another page already has.
Signing in for every member to an account that was the person's own renewed that connection and left it private. It is shared now, as asked, rather than the account connected twice.
…ganization With Prisma's relations there are no foreign keys to take an organization's connections, credentials included, agents and activity along, so v025 deletes each itself.
Every organization now has a vault of its own, shared with everyone in it, beside each member's own: made with the organization, named by its name and renamed with it, with both actions on by default as every vault has them (migration v029 adds one to every organization there is). A member's own agent reaches both; a shared agent, which reaches the shared connections alone, has the organization's, where before it had no vault at all. The MCP instructions tell an agent which is which and what belongs where, and list_connections says which connections are shared. The vault page lists both, with a toggle between them once anyone else is in the organization, and the agent page tells the two apart.
…n, and close the gaps around deleting A member's MCP clients are signed in by them, kept on the code and the grant (migration v030), so taking a member out of an organization signs their clients out of its shared agents, and deleting their account signs them out everywhere; a member who only uses a shared agent sees and signs out the clients they signed in themselves, and whoever manages the agent sees every one with who signed it in. What Personal Agent Proxy does itself from the home page is logged with the member who asked, named in the activity log, so a write on a shared connection says who made it. Sharing an agent takes its tasks and activity along, but for its requests on the member's own connections, which stay theirs. Deleting an organization or an account undoes its connections as a batch, so a Google account connected twice among them is revoked once rather than kept for the other; the last-owned guard is checked again as the organization is deleted, serializable, so two deletions at once can't each take the other for the one that stays; and a person in no organization at all is told so at the root rather than sent between it and /login. Who may change a connection or an agent is one predicate for both. A saved link to a page from before organizations were in the address opens that page in the person's first organization.
Main's v025 logs requests the provider fails; the organizations' migrations are v026 to v031 now, so a local database from an earlier commit of this branch has to be reset again.
hkonsti
force-pushed
the
hkonsti/pap-organizations
branch
from
October 22, 2026 22:22
3ef7815 to
417ff73
Compare
… the migrations and clear the leftovers Sharing an agent moves only what it did on shared connections to everyone; its requests on its owner's own connections, or on ones since disconnected, stay theirs with the tasks they were made for. The activity lanes count only the requests the viewer sees. v030 adds the defaults to the vaults it makes alone, so a vault whose organization is gone doesn't fail it, and v031 backfills who signed in every MCP client and who asked for every home-page write, so those columns are required and a former member's older clients are signed out too; a dev database from an earlier commit has to be reset. The seed wipes only the demo user's own organizations. The web app decides who may change a connection or an agent in one place, as the api does, and shows Make default and the tools refresh only to them; pages reset on switching organizations, the consent page ignores agent lists for an organization no longer picked, the MCP setup waits for a client the person signed in themselves since it opened, and old links to pages under a page redirect to the first organization. Gone: the unused getConnection, the consent fallback to the first organization, stale requireUserOrgId mocks, canShare, unused orgId fields, ownedOrgs, the dead legacy-redirect branch and the "from before" null paths. The shared-or-mine filter, the error message helper, apply and the ?for= toggle each live once. Members who only use a shared connection or agent are refused on every route that changes one, and a shared agent's viewer is tested to see shared connections alone.
hkonsti
force-pushed
the
hkonsti/pap-organizations
branch
from
October 22, 2026 22:58
5e71b17 to
ea06851
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
People can now belong to several organizations: everyone still gets one at sign-up, and can make more or be invited into others' by email, each with one owner, whoever made it, who invites and removes members, renames and deletes it. Every page is one organization's, named first in its address by a slug fixed when it is made (
/acme/connections,/api/orgs/acme/…on the api), so a link to a page is a link to it in that organization and two tabs can be in two; the switcher at the top of the sidebar moves between them, and/opens the person's first.Connections and agents follow one model: each is a member's own, which nobody else in the organization sees, or shared with everyone in it, for good. A member's own agent reaches the shared connections and that member's own; a shared agent reaches the shared ones alone, so one
Viewerpredicate serves the human side and the agent side. A shared connection or agent is changed by whoever connected or made it and the organization's owner; everyone else uses it as it is. The vault follows it too: every organization has one of its own, shared with everyone in it, beside each member's own, so a shared agent has a memory as well, and an agent with both is told what belongs where. The activity log and tasks say whose they are, so a member's own agent's doings stay theirs.Six commits, one per step, each green on its own: organizations with members, invitations and the switcher (migration v026); own or shared connections (v028); own or shared agents (v028); the organization in the address instead of on the session; the organization's vault (v030); and the seams between them (v031): a member's MCP clients are signed in by them, so leaving an organization signs them out of its shared agents and a member who only uses a shared agent sees and signs out their own; what the home page does on a shared connection is logged with who asked; sharing an agent takes its history along, but for what it did on the member's own connections; deleting an organization or an account revokes a Google account connected twice among its connections; and the last-owned guard holds against two deletions at once. Existing data migrates as the organization owner's own, nothing becomes shared by itself, except that every organization gets its shared vault, empty. Anyone with a local dev database from the earlier
hkonsti/pap-orgs-and-scopesbranch has to reset it, since its migrations collide with main's v019 to v024; one from an earlier commit of this branch has to be reset too, since v025 changed and, after merging main's v025, every migration of this branch moved up a number.Open workspace in Conductor