mirror of
https://github.com/alkimake/paperclip.git
synced 2026-06-14 01:50:39 +09:00
[codex] Add resource membership controls (#6677)
## Thinking Path > - Paperclip orchestrates AI-agent companies through company-scoped issues, projects, agents, and board-visible workflows. > - The board sidebar and project list are the daily navigation surface for that control plane. > - Users need to keep all projects and agents accessible while hiding resources they have intentionally left from their own sidebar. > - That requires user-scoped resource membership state backed by company-scoped API and database contracts. > - The branch also needed to preserve HTTP worktree login sessions and keep the project list easier to scan after membership grouping. > - This pull request adds resource membership controls, sidebar leave actions, grouped/sortable project listings, and focused tests. > - The benefit is a cleaner personal workspace view without weakening company-scoped access to the underlying project or agent detail pages. ## What Changed - Added `project_memberships` and `agent_memberships` tables with API/shared/server contracts for current-user join/leave state. - Renumbered the membership migration to `0090_resource_memberships` after rebasing onto current `master`, and made it idempotent for anyone who had applied the old branch-local `0087` migration. - Added project and agent sidebar leave actions, plus list filtering that waits for membership state before hiding resources. - Added grouped project listing, project sorting controls, and reserved row subtitle height for cleaner scanning. - Fixed HTTP auth cookie security handling so HTTP worktree sessions can persist. - Updated focused server and UI tests for the new membership, sidebar, project list, and auth behavior. ## Verification - `pnpm exec vitest run server/src/__tests__/better-auth.test.ts server/src/__tests__/resource-memberships-routes.test.ts ui/src/pages/Projects.test.tsx ui/src/components/SidebarProjects.test.tsx ui/src/components/SidebarAgents.test.tsx ui/src/components/MembershipAction.test.tsx ui/src/components/EntityRow.test.tsx` - Confirmed the branch is rebased on current `origin/master`. - Confirmed the PR diff does not include `pnpm-lock.yaml` or `.github/workflows` changes. ## Risks - Migration safety: low to medium. The migration now uses `IF NOT EXISTS` / guarded constraints and is numbered after current master migrations, but it should still get CI coverage against fresh databases. - UI behavior: low. Left resources are hidden from sidebar only after membership state loads; direct detail access remains available. - Auth behavior: low. Cookie security is relaxed only for HTTP/private local-style origins where secure cookies would prevent login persistence. > For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and discuss it in `#dev` before opening the PR. Feature PRs that overlap with planned core work may need to be redirected — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used - OpenAI GPT-5 Codex coding agent, tool-enabled shell/git workflow, context window not exposed by runtime. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] If this change affects the UI, I have included before/after screenshots - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [x] I will address all Greptile and reviewer comments before requesting merge Screenshot note: no browser screenshots were captured in this heartbeat; the UI changes are covered by focused component tests above. --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
parent
60efa38f86
commit
9aea3e3d35
42 changed files with 20241 additions and 201 deletions
55
packages/db/src/migrations/0090_resource_memberships.sql
Normal file
55
packages/db/src/migrations/0090_resource_memberships.sql
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
CREATE TABLE IF NOT EXISTS "agent_memberships" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"agent_id" uuid NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"state" text DEFAULT 'joined' NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
CREATE TABLE IF NOT EXISTS "project_memberships" (
|
||||
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||
"company_id" uuid NOT NULL,
|
||||
"project_id" uuid NOT NULL,
|
||||
"user_id" text NOT NULL,
|
||||
"state" text DEFAULT 'joined' NOT NULL,
|
||||
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||
"updated_at" timestamp with time zone DEFAULT now() NOT NULL
|
||||
);
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agent_memberships_company_id_companies_id_fk') THEN
|
||||
ALTER TABLE "agent_memberships" ADD CONSTRAINT "agent_memberships_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'agent_memberships_agent_id_agents_id_fk') THEN
|
||||
ALTER TABLE "agent_memberships" ADD CONSTRAINT "agent_memberships_agent_id_agents_id_fk" FOREIGN KEY ("agent_id") REFERENCES "public"."agents"("id") ON DELETE cascade ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'project_memberships_company_id_companies_id_fk') THEN
|
||||
ALTER TABLE "project_memberships" ADD CONSTRAINT "project_memberships_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
DO $$ BEGIN
|
||||
IF NOT EXISTS (SELECT 1 FROM pg_constraint WHERE conname = 'project_memberships_project_id_projects_id_fk') THEN
|
||||
ALTER TABLE "project_memberships" ADD CONSTRAINT "project_memberships_project_id_projects_id_fk" FOREIGN KEY ("project_id") REFERENCES "public"."projects"("id") ON DELETE cascade ON UPDATE no action;
|
||||
END IF;
|
||||
END $$;
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "agent_memberships_company_user_idx" ON "agent_memberships" USING btree ("company_id","user_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "agent_memberships_agent_idx" ON "agent_memberships" USING btree ("agent_id");
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "agent_memberships_company_user_agent_uq" ON "agent_memberships" USING btree ("company_id","user_id","agent_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "project_memberships_company_user_idx" ON "project_memberships" USING btree ("company_id","user_id");
|
||||
--> statement-breakpoint
|
||||
CREATE INDEX IF NOT EXISTS "project_memberships_project_idx" ON "project_memberships" USING btree ("project_id");
|
||||
--> statement-breakpoint
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS "project_memberships_company_user_project_uq" ON "project_memberships" USING btree ("company_id","user_id","project_id");
|
||||
17974
packages/db/src/migrations/meta/0090_snapshot.json
Normal file
17974
packages/db/src/migrations/meta/0090_snapshot.json
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -631,6 +631,13 @@
|
|||
"when": 1779129600000,
|
||||
"tag": "0089_cloud_upstreams",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 90,
|
||||
"version": "7",
|
||||
"when": 1779573019125,
|
||||
"tag": "0090_resource_memberships",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
25
packages/db/src/schema/agent_memberships.ts
Normal file
25
packages/db/src/schema/agent_memberships.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { pgTable, uuid, text, timestamp, uniqueIndex, index } from "drizzle-orm/pg-core";
|
||||
import { agents } from "./agents.js";
|
||||
import { companies } from "./companies.js";
|
||||
|
||||
export const agentMemberships = pgTable(
|
||||
"agent_memberships",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
agentId: uuid("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id").notNull(),
|
||||
state: text("state").notNull().default("joined"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyUserIdx: index("agent_memberships_company_user_idx").on(table.companyId, table.userId),
|
||||
agentIdx: index("agent_memberships_agent_idx").on(table.agentId),
|
||||
companyUserAgentUq: uniqueIndex("agent_memberships_company_user_agent_uq").on(
|
||||
table.companyId,
|
||||
table.userId,
|
||||
table.agentId,
|
||||
),
|
||||
}),
|
||||
);
|
||||
|
|
@ -6,6 +6,7 @@ export { cloudUpstreamConnections, cloudUpstreamRuns } from "./cloud_upstreams.j
|
|||
export { instanceUserRoles } from "./instance_user_roles.js";
|
||||
export { userSidebarPreferences } from "./user_sidebar_preferences.js";
|
||||
export { agents } from "./agents.js";
|
||||
export { agentMemberships } from "./agent_memberships.js";
|
||||
export { boardApiKeys } from "./board_api_keys.js";
|
||||
export { cliAuthChallenges } from "./cli_auth_challenges.js";
|
||||
export { companyMemberships } from "./company_memberships.js";
|
||||
|
|
@ -21,6 +22,7 @@ export { agentRuntimeState } from "./agent_runtime_state.js";
|
|||
export { agentTaskSessions } from "./agent_task_sessions.js";
|
||||
export { agentWakeupRequests } from "./agent_wakeup_requests.js";
|
||||
export { projects } from "./projects.js";
|
||||
export { projectMemberships } from "./project_memberships.js";
|
||||
export { projectWorkspaces } from "./project_workspaces.js";
|
||||
export { executionWorkspaces } from "./execution_workspaces.js";
|
||||
export { environments } from "./environments.js";
|
||||
|
|
|
|||
25
packages/db/src/schema/project_memberships.ts
Normal file
25
packages/db/src/schema/project_memberships.ts
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
import { pgTable, uuid, text, timestamp, uniqueIndex, index } from "drizzle-orm/pg-core";
|
||||
import { companies } from "./companies.js";
|
||||
import { projects } from "./projects.js";
|
||||
|
||||
export const projectMemberships = pgTable(
|
||||
"project_memberships",
|
||||
{
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id").notNull().references(() => companies.id, { onDelete: "cascade" }),
|
||||
projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
|
||||
userId: text("user_id").notNull(),
|
||||
state: text("state").notNull().default("joined"),
|
||||
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => ({
|
||||
companyUserIdx: index("project_memberships_company_user_idx").on(table.companyId, table.userId),
|
||||
projectIdx: index("project_memberships_project_idx").on(table.projectId),
|
||||
companyUserProjectUq: uniqueIndex("project_memberships_company_user_project_uq").on(
|
||||
table.companyId,
|
||||
table.userId,
|
||||
table.projectId,
|
||||
),
|
||||
}),
|
||||
);
|
||||
Loading…
Add table
Add a link
Reference in a new issue