Skip to content

let people work together in organizations, with shared and own connections and agents - #138

Merged
hkonsti merged 19 commits into
mainfrom
hkonsti/pap-organizations
Oct 22, 2026
Merged

hkonsti merged 19 commits into
mainfrom
hkonsti/pap-organizations

Conversation

@hkonsti

@hkonsti hkonsti commented Oct 21, 2026 •

Copy link
Copy Markdown
Collaborator

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 Viewer predicate 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-scopes branch 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

… 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
hkonsti force-pushed the hkonsti/pap-organizations branch from 3ef7815 to 417ff73 Compare October 22, 2026 22:22
… 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
hkonsti force-pushed the hkonsti/pap-organizations branch from 5e71b17 to ea06851 Compare October 22, 2026 22:58
@hkonsti
hkonsti merged commit 58cc881 into main Oct 22, 2026
2 checks passed
@hkonsti
hkonsti deleted the hkonsti/pap-organizations branch October 22, 2026 23:00
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.

1 participant