From 3c73ed26b514144b831f02361de809008742194d Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Tue, 5 May 2026 07:42:57 -0500 Subject: [PATCH] Expand plugin host surface (#5205) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip orchestrates AI agents for zero-human companies > - The plugin system is the extension boundary for optional product capabilities > - Rich plugins need more than a worker entrypoint: they need scoped database storage, local project folders, managed agents/routines, host navigation, and reusable UI components > - The LLM Wiki work exposed those missing host surfaces while keeping plugin code outside the core control plane > - This pull request expands the core plugin host, SDK, server APIs, and UI bridge so plugins can declare and use those surfaces > - The benefit is that future plugins can integrate with Paperclip through documented, validated contracts instead of bespoke server or UI imports ## What Changed - Added plugin-managed database namespaces and migration tracking, including Drizzle schema/migration files and SQL validation for namespace isolation. - Added server support for plugin local folders, managed agents, managed routines, scoped plugin APIs, and plugin operation visibility. - Expanded shared plugin manifest/types/validators and SDK host/testing/UI exports for richer plugin surfaces. - Added reusable UI pieces for file trees, managed routines, resizable sidebars, route sidebars, and plugin bridge initialization. - Updated plugin docs and example plugins to use the expanded host and SDK surface. ## Verification - `pnpm install --frozen-lockfile` - `pnpm run preflight:workspace-links && pnpm exec vitest run packages/shared/src/validators/plugin.test.ts server/src/__tests__/plugin-database.test.ts server/src/__tests__/plugin-local-folders.test.ts server/src/__tests__/plugin-managed-agents.test.ts server/src/__tests__/plugin-managed-routines.test.ts server/src/__tests__/plugin-orchestration-apis.test.ts ui/src/api/plugins.test.ts ui/src/components/FileTree.test.tsx ui/src/components/ResizableSidebarPane.test.tsx ui/src/pages/PluginPage.test.tsx ui/src/plugins/bridge.test.ts` passed: 11 files, 67 tests. - Confirmed this PR changes 89 files and does not include `pnpm-lock.yaml` or `.github/workflows/*`. ## Risks - Medium: this expands plugin host contracts across db/shared/server/ui and includes a new core migration (`0076_useful_elektra.sql`). - The plugin database namespace validator is intentionally restrictive; plugin authors may need follow-up affordances for SQL patterns that remain blocked. - Merge this before the LLM Wiki plugin PR so the plugin can resolve the new SDK and host APIs. > 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 Codex, GPT-5 coding agent, tool-enabled shell/git/GitHub workflow. Context window size was not exposed by the 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 --------- Co-authored-by: Paperclip --- doc/plugins/PLUGIN_AUTHORING_GUIDE.md | 185 +- doc/plugins/PLUGIN_SPEC.md | 19 +- .../db/src/migrations/0076_useful_elektra.sql | 29 + .../db/src/migrations/meta/0076_snapshot.json | 16134 ++++++++++++++++ packages/db/src/migrations/meta/_journal.json | 9 +- packages/db/src/schema/index.ts | 1 + .../db/src/schema/plugin_managed_resources.ts | 34 + .../plugins/create-paperclip-plugin/README.md | 2 +- .../src/ui/index.tsx | 234 +- .../plugin-file-browser-example/src/worker.ts | 77 +- .../src/ui/index.tsx | 84 +- packages/plugins/sdk/README.md | 188 +- packages/plugins/sdk/src/bundlers.ts | 3 +- .../plugins/sdk/src/host-client-factory.ts | 98 + packages/plugins/sdk/src/index.ts | 17 + packages/plugins/sdk/src/protocol.ts | 112 + packages/plugins/sdk/src/testing.ts | 559 +- packages/plugins/sdk/src/types.ts | 156 + packages/plugins/sdk/src/ui/components.ts | 279 + packages/plugins/sdk/src/ui/hooks.ts | 53 + packages/plugins/sdk/src/ui/index.ts | 70 + packages/plugins/sdk/src/ui/types.ts | 94 + packages/plugins/sdk/src/worker-rpc-host.ts | 106 + packages/shared/src/constants.ts | 15 + packages/shared/src/index.ts | 16 + packages/shared/src/types/index.ts | 13 +- packages/shared/src/types/plugin.ts | 194 +- packages/shared/src/types/project.ts | 13 + packages/shared/src/types/routine.ts | 13 + packages/shared/src/validators/plugin.test.ts | 72 + packages/shared/src/validators/plugin.ts | 211 +- server/src/__tests__/issues-service.test.ts | 137 + server/src/__tests__/plugin-database.test.ts | 181 +- .../__tests__/plugin-local-folders.test.ts | 263 + .../__tests__/plugin-managed-agents.test.ts | 365 + .../__tests__/plugin-managed-routines.test.ts | 249 + .../plugin-orchestration-apis.test.ts | 305 + .../src/__tests__/plugin-routes-authz.test.ts | 57 + .../plugin-scoped-api-routes.test.ts | 50 + .../plugin-sdk-orchestration-contract.test.ts | 17 + server/src/app.ts | 3 + server/src/routes/issues.ts | 3 + server/src/routes/plugins.ts | 153 + server/src/services/issues.ts | 22 +- .../services/plugin-capability-validator.ts | 19 + server/src/services/plugin-database.ts | 80 +- server/src/services/plugin-host-services.ts | 234 +- server/src/services/plugin-loader.ts | 116 +- server/src/services/plugin-local-folders.ts | 564 + server/src/services/plugin-managed-agents.ts | 508 + .../src/services/plugin-managed-routines.ts | 523 + server/src/services/plugin-registry.ts | 60 + server/src/services/projects.ts | 314 +- server/src/services/routines.ts | 112 +- ui/src/App.tsx | 2 +- ui/src/api/issues.ts | 2 + ui/src/api/plugins.test.ts | 64 + ui/src/api/plugins.ts | 91 + ui/src/components/CompanySettingsSidebar.tsx | 8 +- ui/src/components/FileTree.test.tsx | 190 + ui/src/components/FileTree.tsx | 500 + ui/src/components/InstanceSidebar.tsx | 14 +- ui/src/components/Layout.test.tsx | 284 +- ui/src/components/Layout.tsx | 72 +- ui/src/components/ManagedRoutinesList.tsx | 180 + ui/src/components/MarkdownBody.test.tsx | 68 +- ui/src/components/MarkdownBody.tsx | 183 +- ui/src/components/PackageFileTree.tsx | 344 +- .../components/ResizableSidebarPane.test.tsx | 121 + ui/src/components/ResizableSidebarPane.tsx | 176 + ui/src/components/RoutineList.tsx | 196 + ui/src/components/Sidebar.tsx | 2 +- ui/src/lib/queryKeys.ts | 4 + ui/src/lib/status-colors.ts | 3 + ui/src/pages/AgentDetail.tsx | 4 +- ui/src/pages/CompanyExport.tsx | 7 +- ui/src/pages/CompanyImport.tsx | 7 +- ui/src/pages/PluginPage.test.tsx | 207 + ui/src/pages/PluginPage.tsx | 99 +- ui/src/pages/PluginSettings.test.tsx | 286 +- ui/src/pages/PluginSettings.tsx | 363 +- ui/src/pages/ProjectDetail.test.tsx | 187 + ui/src/pages/ProjectDetail.tsx | 88 +- ui/src/pages/RoutineDetail.tsx | 66 +- ui/src/pages/Routines.tsx | 137 +- ui/src/plugins/bridge-init.ts | 506 + ui/src/plugins/bridge.test.ts | 306 + ui/src/plugins/bridge.ts | 185 +- ui/src/plugins/slots.tsx | 53 +- 89 files changed, 27516 insertions(+), 914 deletions(-) create mode 100644 packages/db/src/migrations/0076_useful_elektra.sql create mode 100644 packages/db/src/migrations/meta/0076_snapshot.json create mode 100644 packages/db/src/schema/plugin_managed_resources.ts create mode 100644 packages/shared/src/validators/plugin.test.ts create mode 100644 server/src/__tests__/plugin-local-folders.test.ts create mode 100644 server/src/__tests__/plugin-managed-agents.test.ts create mode 100644 server/src/__tests__/plugin-managed-routines.test.ts create mode 100644 server/src/services/plugin-local-folders.ts create mode 100644 server/src/services/plugin-managed-agents.ts create mode 100644 server/src/services/plugin-managed-routines.ts create mode 100644 ui/src/api/plugins.test.ts create mode 100644 ui/src/components/FileTree.test.tsx create mode 100644 ui/src/components/FileTree.tsx create mode 100644 ui/src/components/ManagedRoutinesList.tsx create mode 100644 ui/src/components/ResizableSidebarPane.test.tsx create mode 100644 ui/src/components/ResizableSidebarPane.tsx create mode 100644 ui/src/components/RoutineList.tsx create mode 100644 ui/src/pages/PluginPage.test.tsx create mode 100644 ui/src/pages/ProjectDetail.test.tsx create mode 100644 ui/src/plugins/bridge.test.ts diff --git a/doc/plugins/PLUGIN_AUTHORING_GUIDE.md b/doc/plugins/PLUGIN_AUTHORING_GUIDE.md index e3c24378..01edfcff 100644 --- a/doc/plugins/PLUGIN_AUTHORING_GUIDE.md +++ b/doc/plugins/PLUGIN_AUTHORING_GUIDE.md @@ -13,7 +13,9 @@ It is intentionally narrower than [PLUGIN_SPEC.md](./PLUGIN_SPEC.md). The spec i - Plugin database migrations are restricted to a host-derived plugin namespace. - Plugin-owned JSON API routes must be declared in the manifest and are mounted only under `/api/plugins/:pluginId/api/*`. -- There is no host-provided shared React component kit for plugins yet. +- The host provides a small shared React component kit through + `@paperclipai/plugin-sdk/ui`; use it for common Paperclip controls before + building custom versions. - `ctx.assets` is not supported in the current runtime. ## Scaffold a plugin @@ -168,6 +170,187 @@ Mount surfaces currently wired in the host include: - `commentAnnotation` - `commentContextMenuItem` +## Shared host components + +Use shared components from `@paperclipai/plugin-sdk/ui` when the plugin needs a +Paperclip-native control. The host owns the implementation, so plugins inherit +the board's current styling, ordering, recent selections, and dark-mode behavior +without importing `ui/src` internals. + +Currently exposed components include: + +- `MarkdownBlock` and `MarkdownEditor` for rendered and editable markdown. +- `FileTree` for serializable file and directory trees. +- `IssuesList` for a native company-scoped issue table. +- `AssigneePicker` for the same agent/user selector used in the new issue pane. + Use the controlled `value` format `agent:`, `user:`, or `""`. +- `ProjectPicker` for the same project selector used in the new issue pane. + Use the controlled project id value, or `""` for no project. +- `ManagedRoutinesList` for plugin-owned routine settings pages. + +```tsx +import { AssigneePicker, ProjectPicker } from "@paperclipai/plugin-sdk/ui"; + +export function PluginAssignmentControls({ companyId }: { companyId: string }) { + const [assignee, setAssignee] = useState(""); + const [projectId, setProjectId] = useState(""); + + return ( + <> + setAssignee(value)} + /> + + + ); +} +``` + +## File and path UI + +Plugin UI often needs to render a file tree, accept a folder path, or browse a +project workspace. There are three different surfaces for that, and they map to +different trust and data-flow boundaries. Pick the surface that matches the +data the plugin actually has. + +### When to use the shared `FileTree` + +Use `FileTree` from `@paperclipai/plugin-sdk/ui` whenever the plugin only needs +to render a serializable file/directory list and react to selection or +expand/collapse. The host owns the implementation, so plugin UI inherits the +board's icons, indent, focus ring, and dark-mode styling without importing host +internals. + +```tsx +import { + FileTree, + type FileTreeNode, +} from "@paperclipai/plugin-sdk/ui"; + +const nodes: FileTreeNode[] = [ + { name: "AGENTS.md", path: "AGENTS.md", kind: "file", children: [] }, + { + name: "wiki", + path: "wiki", + kind: "dir", + children: [ + { name: "index.md", path: "wiki/index.md", kind: "file", children: [] }, + ], + }, +]; + +export function WikiTree() { + const [expanded, setExpanded] = useState>(() => new Set(["wiki"])); + const [selected, setSelected] = useState(null); + + return ( + setSelected(path)} + onToggleDir={(path) => + setExpanded((current) => { + const next = new Set(current); + next.has(path) ? next.delete(path) : next.add(path); + return next; + }) + } + /> + ); +} +``` + +Good fits: + +- LLM Wiki page navigation in `packages/plugins/plugin-llm-wiki` builds a + `FileTreeNode[]` from worker query results and renders it through `FileTree`. +- The example `plugin-file-browser-example` lazily fetches a directory's + children through a `loadFileList` action when `onToggleDir` fires, then + merges the children into the local tree state — letting the shared component + handle rendering and selection. + +Boundary rules: + +- Keep the prop surface serializable (`nodes`, `expandedPaths`, `checkedPaths`, + `fileBadges`, `fileTones`). Do not pass arbitrary render functions across the + plugin/host boundary in v1; the supported escape hatches are + `fileBadges` (status pill keyed by path) and `fileTones` (row tone keyed by + path). +- Do not import the host's `FileTree.tsx` or any `ui/src/*` module. The SDK + declaration is the only supported import path for plugin UI. +- The shared `FileTree` is for rendering and selection. Plugin-specific editors, + ingest flows, query forms, and lint runs stay inside the plugin and do not + belong as `FileTree` props. + +### When to declare `localFolders` + +When the plugin needs operator-configured filesystem roots — typically for +trusted local plugins like wiki tooling — declare `localFolders[]` on the +manifest and add the `local.folders` capability. The host renders a settings +surface for the operator to set the absolute path, validates the path +server-side (containment, symlinks, required files/directories), and exposes +`ctx.localFolders.readText()` and `ctx.localFolders.writeTextAtomic()` in the +worker. + +```ts +export const manifest = { + capabilities: ["local.folders"], + localFolders: [ + { + folderKey: "content-root", + displayName: "Content root", + access: "readWrite", + requiredDirectories: ["sources", "pages"], + requiredFiles: ["schema.md"], + }, + ], +}; +``` + +Use this when: + +- The data lives outside any project workspace. +- Reads and writes need company-scoped configuration. +- The operator picks the path once in plugin settings and the worker resolves + files relative to that root. + +Do not use `localFolders` to grant the UI direct browser-side access to the +filesystem — there is no such capability. The browser still goes through the +worker via `getData` / `performAction`, and the worker only exposes paths it +chose to expose. + +### When to keep worker-mediated project workspace browsing + +When the data lives inside an existing project workspace, keep the browsing +flow worker-mediated: + +- The worker uses `ctx.projects.listWorkspaces()` to resolve the workspace + path, then reads its filesystem with normal Node APIs. +- The plugin UI calls a `getData` handler for the root listing and an action + for lazy children, then renders them through `FileTree`. +- The worker is the only side that touches the disk. The browser receives a + serializable tree and never sees raw absolute paths it can replay. + +The example `plugin-file-browser-example` is the reference for this pattern: +the worker registers `fileList` (data) and `loadFileList` (action) over the +same handler, and the UI uses the action for on-toggle directory loading so the +shared `FileTree` stays the rendering surface. + +### Mixing surfaces + +A single plugin can use more than one of these. The LLM Wiki uses +`localFolders` for its content root, then renders the resulting page list +through `FileTree`. The file browser example uses `ctx.projects.listWorkspaces` +to pick a workspace and renders its on-disk tree through `FileTree` with lazy +loading. Pick the boundary per data source, not per plugin. + ## Company routes Plugins may declare a `page` slot with `routePath` to own a company route like: diff --git a/doc/plugins/PLUGIN_SPEC.md b/doc/plugins/PLUGIN_SPEC.md index ed8dea6b..31b9c9d0 100644 --- a/doc/plugins/PLUGIN_SPEC.md +++ b/doc/plugins/PLUGIN_SPEC.md @@ -27,7 +27,7 @@ Current limitations to keep in mind: - Published npm packages are the intended install artifact for deployed plugins. - The repo example plugins under `packages/plugins/examples/` are development conveniences. They work from a source checkout and should not be assumed to exist in a generic published build unless they are explicitly shipped with that build. - Dynamic plugin install is not yet cloud-ready for horizontally scaled or ephemeral deployments. There is no shared artifact store, install coordination, or cross-node distribution layer yet. -- The current runtime does not yet ship a real host-provided plugin UI component kit, and it does not support plugin asset uploads/reads. Treat those as future-scope ideas in this spec, not current implementation promises. +- The current runtime ships a small host-provided plugin UI component kit through `@paperclipai/plugin-sdk/ui`, but does not support plugin asset uploads/reads yet. Treat plugin asset APIs as future-scope ideas, not current implementation promises. - Scoped plugin API routes are JSON-only and must be declared in `apiRoutes`. They mount under `/api/plugins/:pluginId/api/*`; plugins cannot shadow core API routes. @@ -976,13 +976,23 @@ export function DashboardWidget({ context }: PluginWidgetProps) { The SDK includes a `ui` subpath export that plugin frontends import. This subpath provides: -- **Bridge hooks**: `usePluginData(key, params)`, `usePluginAction(key)`, `useHostContext()` +- **Bridge hooks**: `usePluginData(key, params)`, `usePluginAction(key)`, `useHostContext()`, `useHostNavigation()` - **Design tokens**: colors, spacing, typography, shadows matching the host theme - **Shared components**: `MetricCard`, `StatusBadge`, `DataTable`, `LogView`, `ActionBar`, `Spinner`, etc. - **Type definitions**: `PluginPageProps`, `PluginWidgetProps`, `PluginDetailTabProps` Plugins are encouraged but not required to use the shared components. A plugin may render entirely custom UI as long as it communicates through the bridge. +`useHostNavigation()` is the supported way for plugin UI to navigate to +Paperclip-internal pages. It exposes `resolveHref(to)`, `navigate(to, +options?)`, and `linkProps(to, options?)`. Plugin links should prefer +`linkProps()` so anchors keep real `href` values for copy-link, modifier-click, +middle-click, and open-in-new-tab behavior while plain left-clicks route through +the host SPA router. The host resolves company-scoped paths against the active +company prefix without double-prefixing already-prefixed paths. Plugin UI should +not use raw same-origin `href`s or `window.location.assign()` for internal +Paperclip navigation because those can force a full document reload. + ### 19.0.2 Bundle Isolation Plugin UI bundles are loaded as standard ES modules, not iframed. This gives plugins full rendering performance and access to the host's design tokens. @@ -1062,6 +1072,11 @@ The host SDK ships shared components that plugins can import to quickly build UI | `LogView` | Scrollable log output with timestamps | Webhook deliveries, job output, process logs | | `JsonTree` | Collapsible JSON tree for debugging | Raw API responses, plugin state inspection | | `Spinner` | Loading indicator | Data fetch states | +| `FileTree` | Host-styled file/directory tree | Wiki pages, workspace files, import previews | +| `IssuesList` | Host issue list | Plugin pages that need a native issue view | +| `AssigneePicker` | Host assignee picker for agents and board users | Creating issues, assigning routines, filtering work | +| `ProjectPicker` | Host project picker | Creating issues, scoping dashboards, filtering work | +| `ManagedRoutinesList` | Host routine list | Plugin settings pages that manage routines | Plugins may also use entirely custom components. The shared components exist to reduce boilerplate and keep visual consistency, not to limit what plugins can render. diff --git a/packages/db/src/migrations/0076_useful_elektra.sql b/packages/db/src/migrations/0076_useful_elektra.sql new file mode 100644 index 00000000..3620f169 --- /dev/null +++ b/packages/db/src/migrations/0076_useful_elektra.sql @@ -0,0 +1,29 @@ +CREATE TABLE IF NOT EXISTS "plugin_managed_resources" ( + "id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL, + "company_id" uuid NOT NULL, + "plugin_id" uuid NOT NULL, + "plugin_key" text NOT NULL, + "resource_kind" text NOT NULL, + "resource_key" text NOT NULL, + "resource_id" uuid NOT NULL, + "defaults_json" jsonb DEFAULT '{}'::jsonb 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 + ALTER TABLE "plugin_managed_resources" ADD CONSTRAINT "plugin_managed_resources_company_id_companies_id_fk" FOREIGN KEY ("company_id") REFERENCES "public"."companies"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "plugin_managed_resources" ADD CONSTRAINT "plugin_managed_resources_plugin_id_plugins_id_fk" FOREIGN KEY ("plugin_id") REFERENCES "public"."plugins"("id") ON DELETE cascade ON UPDATE no action; +EXCEPTION + WHEN duplicate_object THEN NULL; +END $$; +--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "plugin_managed_resources_company_idx" ON "plugin_managed_resources" USING btree ("company_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "plugin_managed_resources_plugin_idx" ON "plugin_managed_resources" USING btree ("plugin_id");--> statement-breakpoint +CREATE INDEX IF NOT EXISTS "plugin_managed_resources_resource_idx" ON "plugin_managed_resources" USING btree ("resource_kind","resource_id");--> statement-breakpoint +CREATE UNIQUE INDEX IF NOT EXISTS "plugin_managed_resources_company_plugin_resource_uq" ON "plugin_managed_resources" USING btree ("company_id","plugin_id","resource_kind","resource_key"); diff --git a/packages/db/src/migrations/meta/0076_snapshot.json b/packages/db/src/migrations/meta/0076_snapshot.json new file mode 100644 index 00000000..d002c177 --- /dev/null +++ b/packages/db/src/migrations/meta/0076_snapshot.json @@ -0,0 +1,16134 @@ +{ + "id": "063c8887-ed46-4125-a08f-51c16b636245", + "prevId": "fdc9cd8b-5423-4d64-b255-9bc1497fdd6a", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.activity_log": { + "name": "activity_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "actor_type": { + "name": "actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "actor_id": { + "name": "actor_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "action": { + "name": "action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "activity_log_company_created_idx": { + "name": "activity_log_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_run_id_idx": { + "name": "activity_log_run_id_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "activity_log_entity_type_id_idx": { + "name": "activity_log_entity_type_id_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "activity_log_company_id_companies_id_fk": { + "name": "activity_log_company_id_companies_id_fk", + "tableFrom": "activity_log", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_agent_id_agents_id_fk": { + "name": "activity_log_agent_id_agents_id_fk", + "tableFrom": "activity_log", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "activity_log_run_id_heartbeat_runs_id_fk": { + "name": "activity_log_run_id_heartbeat_runs_id_fk", + "tableFrom": "activity_log", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_api_keys": { + "name": "agent_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_api_keys_key_hash_idx": { + "name": "agent_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_api_keys_company_agent_idx": { + "name": "agent_api_keys_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_api_keys_agent_id_agents_id_fk": { + "name": "agent_api_keys_agent_id_agents_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_api_keys_company_id_companies_id_fk": { + "name": "agent_api_keys_company_id_companies_id_fk", + "tableFrom": "agent_api_keys", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_config_revisions": { + "name": "agent_config_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'patch'" + }, + "rolled_back_from_revision_id": { + "name": "rolled_back_from_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "changed_keys": { + "name": "changed_keys", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "before_config": { + "name": "before_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "after_config": { + "name": "after_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_config_revisions_company_agent_created_idx": { + "name": "agent_config_revisions_company_agent_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_config_revisions_agent_created_idx": { + "name": "agent_config_revisions_agent_created_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_config_revisions_company_id_companies_id_fk": { + "name": "agent_config_revisions_company_id_companies_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_config_revisions_agent_id_agents_id_fk": { + "name": "agent_config_revisions_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "agent_config_revisions_created_by_agent_id_agents_id_fk": { + "name": "agent_config_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "agent_config_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_runtime_state": { + "name": "agent_runtime_state", + "schema": "", + "columns": { + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_id": { + "name": "session_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state_json": { + "name": "state_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_run_status": { + "name": "last_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "total_input_tokens": { + "name": "total_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_output_tokens": { + "name": "total_output_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cached_input_tokens": { + "name": "total_cached_input_tokens", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost_cents": { + "name": "total_cost_cents", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_runtime_state_company_agent_idx": { + "name": "agent_runtime_state_company_agent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_runtime_state_company_updated_idx": { + "name": "agent_runtime_state_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_runtime_state_agent_id_agents_id_fk": { + "name": "agent_runtime_state_agent_id_agents_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_runtime_state_company_id_companies_id_fk": { + "name": "agent_runtime_state_company_id_companies_id_fk", + "tableFrom": "agent_runtime_state", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_task_sessions": { + "name": "agent_task_sessions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "task_key": { + "name": "task_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "session_params_json": { + "name": "session_params_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_display_id": { + "name": "session_display_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_run_id": { + "name": "last_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_task_sessions_company_agent_adapter_task_uniq": { + "name": "agent_task_sessions_company_agent_adapter_task_uniq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "adapter_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_agent_updated_idx": { + "name": "agent_task_sessions_company_agent_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_task_sessions_company_task_updated_idx": { + "name": "agent_task_sessions_company_task_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "task_key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_task_sessions_company_id_companies_id_fk": { + "name": "agent_task_sessions_company_id_companies_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_agent_id_agents_id_fk": { + "name": "agent_task_sessions_agent_id_agents_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_task_sessions_last_run_id_heartbeat_runs_id_fk": { + "name": "agent_task_sessions_last_run_id_heartbeat_runs_id_fk", + "tableFrom": "agent_task_sessions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "last_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agent_wakeup_requests": { + "name": "agent_wakeup_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "coalesced_count": { + "name": "coalesced_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "requested_by_actor_type": { + "name": "requested_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_by_actor_id": { + "name": "requested_by_actor_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_at": { + "name": "requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agent_wakeup_requests_company_agent_status_idx": { + "name": "agent_wakeup_requests_company_agent_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_company_requested_idx": { + "name": "agent_wakeup_requests_company_requested_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agent_wakeup_requests_agent_requested_idx": { + "name": "agent_wakeup_requests_agent_requested_idx", + "columns": [ + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requested_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agent_wakeup_requests_company_id_companies_id_fk": { + "name": "agent_wakeup_requests_company_id_companies_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agent_wakeup_requests_agent_id_agents_id_fk": { + "name": "agent_wakeup_requests_agent_id_agents_id_fk", + "tableFrom": "agent_wakeup_requests", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.agents": { + "name": "agents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'general'" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "icon": { + "name": "icon", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'idle'" + }, + "reports_to": { + "name": "reports_to", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'process'" + }, + "adapter_config": { + "name": "adapter_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "runtime_config": { + "name": "runtime_config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "default_environment_id": { + "name": "default_environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "permissions": { + "name": "permissions", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_heartbeat_at": { + "name": "last_heartbeat_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "agents_company_status_idx": { + "name": "agents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_reports_to_idx": { + "name": "agents_company_reports_to_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reports_to", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "agents_company_default_environment_idx": { + "name": "agents_company_default_environment_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "default_environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "agents_company_id_companies_id_fk": { + "name": "agents_company_id_companies_id_fk", + "tableFrom": "agents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_reports_to_agents_id_fk": { + "name": "agents_reports_to_agents_id_fk", + "tableFrom": "agents", + "tableTo": "agents", + "columnsFrom": [ + "reports_to" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "agents_default_environment_id_environments_id_fk": { + "name": "agents_default_environment_id_environments_id_fk", + "tableFrom": "agents", + "tableTo": "environments", + "columnsFrom": [ + "default_environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approval_comments": { + "name": "approval_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approval_comments_company_idx": { + "name": "approval_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_idx": { + "name": "approval_comments_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "approval_comments_approval_created_idx": { + "name": "approval_comments_approval_created_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approval_comments_company_id_companies_id_fk": { + "name": "approval_comments_company_id_companies_id_fk", + "tableFrom": "approval_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_approval_id_approvals_id_fk": { + "name": "approval_comments_approval_id_approvals_id_fk", + "tableFrom": "approval_comments", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approval_comments_author_agent_id_agents_id_fk": { + "name": "approval_comments_author_agent_id_agents_id_fk", + "tableFrom": "approval_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.approvals": { + "name": "approvals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requested_by_agent_id": { + "name": "requested_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "requested_by_user_id": { + "name": "requested_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "decision_note": { + "name": "decision_note", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_by_user_id": { + "name": "decided_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "decided_at": { + "name": "decided_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "approvals_company_status_type_idx": { + "name": "approvals_company_status_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "approvals_company_id_companies_id_fk": { + "name": "approvals_company_id_companies_id_fk", + "tableFrom": "approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "approvals_requested_by_agent_id_agents_id_fk": { + "name": "approvals_requested_by_agent_id_agents_id_fk", + "tableFrom": "approvals", + "tableTo": "agents", + "columnsFrom": [ + "requested_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.assets": { + "name": "assets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "object_key": { + "name": "object_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "byte_size": { + "name": "byte_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_filename": { + "name": "original_filename", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "assets_company_created_idx": { + "name": "assets_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_provider_idx": { + "name": "assets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "assets_company_object_key_uq": { + "name": "assets_company_object_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "object_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "assets_company_id_companies_id_fk": { + "name": "assets_company_id_companies_id_fk", + "tableFrom": "assets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "assets_created_by_agent_id_agents_id_fk": { + "name": "assets_created_by_agent_id_agents_id_fk", + "tableFrom": "assets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.board_api_keys": { + "name": "board_api_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key_hash": { + "name": "key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "board_api_keys_key_hash_idx": { + "name": "board_api_keys_key_hash_idx", + "columns": [ + { + "expression": "key_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "board_api_keys_user_idx": { + "name": "board_api_keys_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "board_api_keys_user_id_user_id_fk": { + "name": "board_api_keys_user_id_user_id_fk", + "tableFrom": "board_api_keys", + "tableTo": "user", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_incidents": { + "name": "budget_incidents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "policy_id": { + "name": "policy_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "window_start": { + "name": "window_start", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "window_end": { + "name": "window_end", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "threshold_type": { + "name": "threshold_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount_limit": { + "name": "amount_limit", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "amount_observed": { + "name": "amount_observed", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'open'" + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_incidents_company_status_idx": { + "name": "budget_incidents_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_company_scope_idx": { + "name": "budget_incidents_company_scope_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_incidents_policy_window_threshold_idx": { + "name": "budget_incidents_policy_window_threshold_idx", + "columns": [ + { + "expression": "policy_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_start", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "threshold_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"budget_incidents\".\"status\" <> 'dismissed'", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_incidents_company_id_companies_id_fk": { + "name": "budget_incidents_company_id_companies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_policy_id_budget_policies_id_fk": { + "name": "budget_incidents_policy_id_budget_policies_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "budget_policies", + "columnsFrom": [ + "policy_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "budget_incidents_approval_id_approvals_id_fk": { + "name": "budget_incidents_approval_id_approvals_id_fk", + "tableFrom": "budget_incidents", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.budget_policies": { + "name": "budget_policies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "metric": { + "name": "metric", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'billed_cents'" + }, + "window_kind": { + "name": "window_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "amount": { + "name": "amount", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "warn_percent": { + "name": "warn_percent", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 80 + }, + "hard_stop_enabled": { + "name": "hard_stop_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "notify_enabled": { + "name": "notify_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "budget_policies_company_scope_active_idx": { + "name": "budget_policies_company_scope_active_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_window_idx": { + "name": "budget_policies_company_window_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "budget_policies_company_scope_metric_unique_idx": { + "name": "budget_policies_company_scope_metric_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "metric", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "window_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "budget_policies_company_id_companies_id_fk": { + "name": "budget_policies_company_id_companies_id_fk", + "tableFrom": "budget_policies", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cli_auth_challenges": { + "name": "cli_auth_challenges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_hash": { + "name": "secret_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "client_name": { + "name": "client_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "requested_access": { + "name": "requested_access", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'board'" + }, + "requested_company_id": { + "name": "requested_company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "pending_key_hash": { + "name": "pending_key_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "pending_key_name": { + "name": "pending_key_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "board_api_key_id": { + "name": "board_api_key_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cli_auth_challenges_secret_hash_idx": { + "name": "cli_auth_challenges_secret_hash_idx", + "columns": [ + { + "expression": "secret_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_approved_by_idx": { + "name": "cli_auth_challenges_approved_by_idx", + "columns": [ + { + "expression": "approved_by_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cli_auth_challenges_requested_company_idx": { + "name": "cli_auth_challenges_requested_company_idx", + "columns": [ + { + "expression": "requested_company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cli_auth_challenges_requested_company_id_companies_id_fk": { + "name": "cli_auth_challenges_requested_company_id_companies_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "companies", + "columnsFrom": [ + "requested_company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_approved_by_user_id_user_id_fk": { + "name": "cli_auth_challenges_approved_by_user_id_user_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "user", + "columnsFrom": [ + "approved_by_user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk": { + "name": "cli_auth_challenges_board_api_key_id_board_api_keys_id_fk", + "tableFrom": "cli_auth_challenges", + "tableTo": "board_api_keys", + "columnsFrom": [ + "board_api_key_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.companies": { + "name": "companies", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "issue_prefix": { + "name": "issue_prefix", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'PAP'" + }, + "issue_counter": { + "name": "issue_counter", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "budget_monthly_cents": { + "name": "budget_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "spent_monthly_cents": { + "name": "spent_monthly_cents", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "attachment_max_bytes": { + "name": "attachment_max_bytes", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 10485760 + }, + "require_board_approval_for_new_agents": { + "name": "require_board_approval_for_new_agents", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feedback_data_sharing_enabled": { + "name": "feedback_data_sharing_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "feedback_data_sharing_consent_at": { + "name": "feedback_data_sharing_consent_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_consent_by_user_id": { + "name": "feedback_data_sharing_consent_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "feedback_data_sharing_terms_version": { + "name": "feedback_data_sharing_terms_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "brand_color": { + "name": "brand_color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "companies_issue_prefix_idx": { + "name": "companies_issue_prefix_idx", + "columns": [ + { + "expression": "issue_prefix", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_logos": { + "name": "company_logos", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_logos_company_uq": { + "name": "company_logos_company_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_logos_asset_uq": { + "name": "company_logos_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_logos_company_id_companies_id_fk": { + "name": "company_logos_company_id_companies_id_fk", + "tableFrom": "company_logos", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_logos_asset_id_assets_id_fk": { + "name": "company_logos_asset_id_assets_id_fk", + "tableFrom": "company_logos", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_memberships": { + "name": "company_memberships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "membership_role": { + "name": "membership_role", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_memberships_company_principal_unique_idx": { + "name": "company_memberships_company_principal_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_principal_status_idx": { + "name": "company_memberships_principal_status_idx", + "columns": [ + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_memberships_company_status_idx": { + "name": "company_memberships_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_memberships_company_id_companies_id_fk": { + "name": "company_memberships_company_id_companies_id_fk", + "tableFrom": "company_memberships", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secret_versions": { + "name": "company_secret_versions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "material": { + "name": "material", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "value_sha256": { + "name": "value_sha256", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "company_secret_versions_secret_idx": { + "name": "company_secret_versions_secret_idx", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_value_sha256_idx": { + "name": "company_secret_versions_value_sha256_idx", + "columns": [ + { + "expression": "value_sha256", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secret_versions_secret_version_uq": { + "name": "company_secret_versions_secret_version_uq", + "columns": [ + { + "expression": "secret_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secret_versions_secret_id_company_secrets_id_fk": { + "name": "company_secret_versions_secret_id_company_secrets_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "company_secret_versions_created_by_agent_id_agents_id_fk": { + "name": "company_secret_versions_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secret_versions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_secrets": { + "name": "company_secrets", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_encrypted'" + }, + "external_ref": { + "name": "external_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "latest_version": { + "name": "latest_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_secrets_company_idx": { + "name": "company_secrets_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_provider_idx": { + "name": "company_secrets_company_provider_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_secrets_company_name_uq": { + "name": "company_secrets_company_name_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_secrets_company_id_companies_id_fk": { + "name": "company_secrets_company_id_companies_id_fk", + "tableFrom": "company_secrets", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "company_secrets_created_by_agent_id_agents_id_fk": { + "name": "company_secrets_created_by_agent_id_agents_id_fk", + "tableFrom": "company_secrets", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_skills": { + "name": "company_skills", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "markdown": { + "name": "markdown", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "source_locator": { + "name": "source_locator", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_ref": { + "name": "source_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trust_level": { + "name": "trust_level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown_only'" + }, + "compatibility": { + "name": "compatibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'compatible'" + }, + "file_inventory": { + "name": "file_inventory", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_skills_company_key_idx": { + "name": "company_skills_company_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_skills_company_name_idx": { + "name": "company_skills_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_skills_company_id_companies_id_fk": { + "name": "company_skills_company_id_companies_id_fk", + "tableFrom": "company_skills", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.company_user_sidebar_preferences": { + "name": "company_user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "project_order": { + "name": "project_order", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "company_user_sidebar_preferences_company_idx": { + "name": "company_user_sidebar_preferences_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_user_idx": { + "name": "company_user_sidebar_preferences_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "company_user_sidebar_preferences_company_user_uq": { + "name": "company_user_sidebar_preferences_company_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "company_user_sidebar_preferences_company_id_companies_id_fk": { + "name": "company_user_sidebar_preferences_company_id_companies_id_fk", + "tableFrom": "company_user_sidebar_preferences", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.cost_events": { + "name": "cost_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "billing_type": { + "name": "billing_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "input_tokens": { + "name": "input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cached_input_tokens": { + "name": "cached_input_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "output_tokens": { + "name": "output_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "cost_cents": { + "name": "cost_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "cost_events_company_occurred_idx": { + "name": "cost_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_agent_occurred_idx": { + "name": "cost_events_company_agent_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_provider_occurred_idx": { + "name": "cost_events_company_provider_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_biller_occurred_idx": { + "name": "cost_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "cost_events_company_heartbeat_run_idx": { + "name": "cost_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "cost_events_company_id_companies_id_fk": { + "name": "cost_events_company_id_companies_id_fk", + "tableFrom": "cost_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_agent_id_agents_id_fk": { + "name": "cost_events_agent_id_agents_id_fk", + "tableFrom": "cost_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_issue_id_issues_id_fk": { + "name": "cost_events_issue_id_issues_id_fk", + "tableFrom": "cost_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_project_id_projects_id_fk": { + "name": "cost_events_project_id_projects_id_fk", + "tableFrom": "cost_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_goal_id_goals_id_fk": { + "name": "cost_events_goal_id_goals_id_fk", + "tableFrom": "cost_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "cost_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "cost_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "cost_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.document_revisions": { + "name": "document_revisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "revision_number": { + "name": "revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "change_summary": { + "name": "change_summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "document_revisions_document_revision_uq": { + "name": "document_revisions_document_revision_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revision_number", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "document_revisions_company_document_created_idx": { + "name": "document_revisions_company_document_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_revisions_company_id_companies_id_fk": { + "name": "document_revisions_company_id_companies_id_fk", + "tableFrom": "document_revisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "document_revisions_document_id_documents_id_fk": { + "name": "document_revisions_document_id_documents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "document_revisions_created_by_agent_id_agents_id_fk": { + "name": "document_revisions_created_by_agent_id_agents_id_fk", + "tableFrom": "document_revisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "document_revisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "document_revisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "document_revisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.documents": { + "name": "documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "format": { + "name": "format", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'markdown'" + }, + "latest_body": { + "name": "latest_body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "latest_revision_id": { + "name": "latest_revision_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "latest_revision_number": { + "name": "latest_revision_number", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "documents_company_updated_idx": { + "name": "documents_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "documents_company_created_idx": { + "name": "documents_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "documents_company_id_companies_id_fk": { + "name": "documents_company_id_companies_id_fk", + "tableFrom": "documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "documents_created_by_agent_id_agents_id_fk": { + "name": "documents_created_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "documents_updated_by_agent_id_agents_id_fk": { + "name": "documents_updated_by_agent_id_agents_id_fk", + "tableFrom": "documents", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environment_leases": { + "name": "environment_leases", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "environment_id": { + "name": "environment_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "lease_policy": { + "name": "lease_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'ephemeral'" + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_lease_id": { + "name": "provider_lease_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "acquired_at": { + "name": "acquired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_status": { + "name": "cleanup_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environment_leases_company_environment_status_idx": { + "name": "environment_leases_company_environment_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "environment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_execution_workspace_idx": { + "name": "environment_leases_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_issue_idx": { + "name": "environment_leases_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_heartbeat_run_idx": { + "name": "environment_leases_heartbeat_run_idx", + "columns": [ + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_company_last_used_idx": { + "name": "environment_leases_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environment_leases_provider_lease_idx": { + "name": "environment_leases_provider_lease_idx", + "columns": [ + { + "expression": "provider_lease_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environment_leases_company_id_companies_id_fk": { + "name": "environment_leases_company_id_companies_id_fk", + "tableFrom": "environment_leases", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_leases_environment_id_environments_id_fk": { + "name": "environment_leases_environment_id_environments_id_fk", + "tableFrom": "environment_leases", + "tableTo": "environments", + "columnsFrom": [ + "environment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "environment_leases_execution_workspace_id_execution_workspaces_id_fk": { + "name": "environment_leases_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "environment_leases", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_issue_id_issues_id_fk": { + "name": "environment_leases_issue_id_issues_id_fk", + "tableFrom": "environment_leases", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "environment_leases_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "environment_leases", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.environments": { + "name": "environments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "driver": { + "name": "driver", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "environments_company_status_idx": { + "name": "environments_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_company_driver_idx": { + "name": "environments_company_driver_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "driver", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"environments\".\"driver\" = 'local'", + "concurrently": false, + "method": "btree", + "with": {} + }, + "environments_company_name_idx": { + "name": "environments_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "environments_company_id_companies_id_fk": { + "name": "environments_company_id_companies_id_fk", + "tableFrom": "environments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.execution_workspaces": { + "name": "execution_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "strategy_type": { + "name": "strategy_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "base_ref": { + "name": "base_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "branch_name": { + "name": "branch_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_type": { + "name": "provider_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_fs'" + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "derived_from_execution_workspace_id": { + "name": "derived_from_execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "opened_at": { + "name": "opened_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "closed_at": { + "name": "closed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_eligible_at": { + "name": "cleanup_eligible_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cleanup_reason": { + "name": "cleanup_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "execution_workspaces_company_project_status_idx": { + "name": "execution_workspaces_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_project_workspace_status_idx": { + "name": "execution_workspaces_company_project_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_source_issue_idx": { + "name": "execution_workspaces_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_last_used_idx": { + "name": "execution_workspaces_company_last_used_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_used_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "execution_workspaces_company_branch_idx": { + "name": "execution_workspaces_company_branch_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "branch_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "execution_workspaces_company_id_companies_id_fk": { + "name": "execution_workspaces_company_id_companies_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "execution_workspaces_project_id_projects_id_fk": { + "name": "execution_workspaces_project_id_projects_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "execution_workspaces_project_workspace_id_project_workspaces_id_fk": { + "name": "execution_workspaces_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_source_issue_id_issues_id_fk": { + "name": "execution_workspaces_source_issue_id_issues_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk": { + "name": "execution_workspaces_derived_from_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "execution_workspaces", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "derived_from_execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_exports": { + "name": "feedback_exports", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "feedback_vote_id": { + "name": "feedback_vote_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_only'" + }, + "destination": { + "name": "destination", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "export_id": { + "name": "export_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "schema_version": { + "name": "schema_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-envelope-v2'" + }, + "bundle_version": { + "name": "bundle_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-bundle-v2'" + }, + "payload_version": { + "name": "payload_version", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paperclip-feedback-v1'" + }, + "payload_digest": { + "name": "payload_digest", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload_snapshot": { + "name": "payload_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "target_summary": { + "name": "target_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "attempt_count": { + "name": "attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempted_at": { + "name": "last_attempted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "exported_at": { + "name": "exported_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_exports_feedback_vote_idx": { + "name": "feedback_exports_feedback_vote_idx", + "columns": [ + { + "expression": "feedback_vote_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_created_idx": { + "name": "feedback_exports_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_status_idx": { + "name": "feedback_exports_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_issue_idx": { + "name": "feedback_exports_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_project_idx": { + "name": "feedback_exports_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_exports_company_author_idx": { + "name": "feedback_exports_company_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_exports_company_id_companies_id_fk": { + "name": "feedback_exports_company_id_companies_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_exports_feedback_vote_id_feedback_votes_id_fk": { + "name": "feedback_exports_feedback_vote_id_feedback_votes_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "feedback_votes", + "columnsFrom": [ + "feedback_vote_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_issue_id_issues_id_fk": { + "name": "feedback_exports_issue_id_issues_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "feedback_exports_project_id_projects_id_fk": { + "name": "feedback_exports_project_id_projects_id_fk", + "tableFrom": "feedback_exports", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.feedback_votes": { + "name": "feedback_votes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_type": { + "name": "target_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_id": { + "name": "target_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "vote": { + "name": "vote", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_with_labs": { + "name": "shared_with_labs", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "shared_at": { + "name": "shared_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "consent_version": { + "name": "consent_version", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "redaction_summary": { + "name": "redaction_summary", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "feedback_votes_company_issue_idx": { + "name": "feedback_votes_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_issue_target_idx": { + "name": "feedback_votes_issue_target_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_author_idx": { + "name": "feedback_votes_author_idx", + "columns": [ + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "feedback_votes_company_target_author_idx": { + "name": "feedback_votes_company_target_author_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "feedback_votes_company_id_companies_id_fk": { + "name": "feedback_votes_company_id_companies_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "feedback_votes_issue_id_issues_id_fk": { + "name": "feedback_votes_issue_id_issues_id_fk", + "tableFrom": "feedback_votes", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.finance_events": { + "name": "finance_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "cost_event_id": { + "name": "cost_event_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "event_kind": { + "name": "event_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "direction": { + "name": "direction", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'debit'" + }, + "biller": { + "name": "biller", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_adapter_type": { + "name": "execution_adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "pricing_tier": { + "name": "pricing_tier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "region": { + "name": "region", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "quantity": { + "name": "quantity", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "unit": { + "name": "unit", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "amount_cents": { + "name": "amount_cents", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "currency": { + "name": "currency", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'USD'" + }, + "estimated": { + "name": "estimated", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "external_invoice_id": { + "name": "external_invoice_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata_json": { + "name": "metadata_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "occurred_at": { + "name": "occurred_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "finance_events_company_occurred_idx": { + "name": "finance_events_company_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_biller_occurred_idx": { + "name": "finance_events_company_biller_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "biller", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_kind_occurred_idx": { + "name": "finance_events_company_kind_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "event_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_direction_occurred_idx": { + "name": "finance_events_company_direction_occurred_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "direction", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "occurred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_heartbeat_run_idx": { + "name": "finance_events_company_heartbeat_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "finance_events_company_cost_event_idx": { + "name": "finance_events_company_cost_event_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "cost_event_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "finance_events_company_id_companies_id_fk": { + "name": "finance_events_company_id_companies_id_fk", + "tableFrom": "finance_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_agent_id_agents_id_fk": { + "name": "finance_events_agent_id_agents_id_fk", + "tableFrom": "finance_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_issue_id_issues_id_fk": { + "name": "finance_events_issue_id_issues_id_fk", + "tableFrom": "finance_events", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_project_id_projects_id_fk": { + "name": "finance_events_project_id_projects_id_fk", + "tableFrom": "finance_events", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_goal_id_goals_id_fk": { + "name": "finance_events_goal_id_goals_id_fk", + "tableFrom": "finance_events", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "finance_events_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "finance_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "finance_events_cost_event_id_cost_events_id_fk": { + "name": "finance_events_cost_event_id_cost_events_id_fk", + "tableFrom": "finance_events", + "tableTo": "cost_events", + "columnsFrom": [ + "cost_event_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.goals": { + "name": "goals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'task'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'planned'" + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "goals_company_idx": { + "name": "goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "goals_company_id_companies_id_fk": { + "name": "goals_company_id_companies_id_fk", + "tableFrom": "goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_parent_id_goals_id_fk": { + "name": "goals_parent_id_goals_id_fk", + "tableFrom": "goals", + "tableTo": "goals", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "goals_owner_agent_id_agents_id_fk": { + "name": "goals_owner_agent_id_agents_id_fk", + "tableFrom": "goals", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_events": { + "name": "heartbeat_run_events", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "bigserial", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "seq": { + "name": "seq", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "event_type": { + "name": "event_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stream": { + "name": "stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_events_run_seq_idx": { + "name": "heartbeat_run_events_run_seq_idx", + "columns": [ + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "seq", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_run_idx": { + "name": "heartbeat_run_events_company_run_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_events_company_created_idx": { + "name": "heartbeat_run_events_company_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_events_company_id_companies_id_fk": { + "name": "heartbeat_run_events_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_events_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_events_agent_id_agents_id_fk": { + "name": "heartbeat_run_events_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_events", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_run_watchdog_decisions": { + "name": "heartbeat_run_watchdog_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "run_id": { + "name": "run_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "evaluation_issue_id": { + "name": "evaluation_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "decision": { + "name": "decision", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "snoozed_until": { + "name": "snoozed_until", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_run_watchdog_decisions_company_run_created_idx": { + "name": "heartbeat_run_watchdog_decisions_company_run_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_run_watchdog_decisions_company_run_snooze_idx": { + "name": "heartbeat_run_watchdog_decisions_company_run_snooze_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "snoozed_until", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_run_watchdog_decisions_company_id_companies_id_fk": { + "name": "heartbeat_run_watchdog_decisions_company_id_companies_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_watchdog_decisions_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_evaluation_issue_id_issues_id_fk": { + "name": "heartbeat_run_watchdog_decisions_evaluation_issue_id_issues_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "issues", + "columnsFrom": [ + "evaluation_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_created_by_agent_id_agents_id_fk": { + "name": "heartbeat_run_watchdog_decisions_created_by_agent_id_agents_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "heartbeat_run_watchdog_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_run_watchdog_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_run_watchdog_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.heartbeat_runs": { + "name": "heartbeat_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "agent_id": { + "name": "agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "invocation_source": { + "name": "invocation_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'on_demand'" + }, + "trigger_detail": { + "name": "trigger_detail", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'queued'" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "wakeup_request_id": { + "name": "wakeup_request_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "signal": { + "name": "signal", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "usage_json": { + "name": "usage_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "result_json": { + "name": "result_json", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "session_id_before": { + "name": "session_id_before", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "session_id_after": { + "name": "session_id_after", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_code": { + "name": "error_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_run_id": { + "name": "external_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "process_pid": { + "name": "process_pid", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_group_id": { + "name": "process_group_id", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "process_started_at": { + "name": "process_started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_output_at": { + "name": "last_output_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_output_seq": { + "name": "last_output_seq", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_output_stream": { + "name": "last_output_stream", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_output_bytes": { + "name": "last_output_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "retry_of_run_id": { + "name": "retry_of_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "process_loss_retry_count": { + "name": "process_loss_retry_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_retry_at": { + "name": "scheduled_retry_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "scheduled_retry_attempt": { + "name": "scheduled_retry_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "scheduled_retry_reason": { + "name": "scheduled_retry_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_comment_status": { + "name": "issue_comment_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'not_applicable'" + }, + "issue_comment_satisfied_by_comment_id": { + "name": "issue_comment_satisfied_by_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_comment_retry_queued_at": { + "name": "issue_comment_retry_queued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "liveness_state": { + "name": "liveness_state", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "liveness_reason": { + "name": "liveness_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "continuation_attempt": { + "name": "continuation_attempt", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_useful_action_at": { + "name": "last_useful_action_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_action": { + "name": "next_action", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context_snapshot": { + "name": "context_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "heartbeat_runs_company_agent_started_idx": { + "name": "heartbeat_runs_company_agent_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_liveness_idx": { + "name": "heartbeat_runs_company_liveness_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "liveness_state", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_status_last_output_idx": { + "name": "heartbeat_runs_company_status_last_output_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "last_output_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "heartbeat_runs_company_status_process_started_idx": { + "name": "heartbeat_runs_company_status_process_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "process_started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "heartbeat_runs_company_id_companies_id_fk": { + "name": "heartbeat_runs_company_id_companies_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_agent_id_agents_id_fk": { + "name": "heartbeat_runs_agent_id_agents_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agents", + "columnsFrom": [ + "agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk": { + "name": "heartbeat_runs_wakeup_request_id_agent_wakeup_requests_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "agent_wakeup_requests", + "columnsFrom": [ + "wakeup_request_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk": { + "name": "heartbeat_runs_retry_of_run_id_heartbeat_runs_id_fk", + "tableFrom": "heartbeat_runs", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "retry_of_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.inbox_dismissals": { + "name": "inbox_dismissals", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "item_key": { + "name": "item_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "dismissed_at": { + "name": "dismissed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "inbox_dismissals_company_user_idx": { + "name": "inbox_dismissals_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_item_idx": { + "name": "inbox_dismissals_company_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "inbox_dismissals_company_user_item_idx": { + "name": "inbox_dismissals_company_user_item_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "item_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "inbox_dismissals_company_id_companies_id_fk": { + "name": "inbox_dismissals_company_id_companies_id_fk", + "tableFrom": "inbox_dismissals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_settings": { + "name": "instance_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "singleton_key": { + "name": "singleton_key", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "general": { + "name": "general", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "experimental": { + "name": "experimental", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_settings_singleton_key_idx": { + "name": "instance_settings_singleton_key_idx", + "columns": [ + { + "expression": "singleton_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.instance_user_roles": { + "name": "instance_user_roles", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'instance_admin'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "instance_user_roles_user_role_unique_idx": { + "name": "instance_user_roles_user_role_unique_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "instance_user_roles_role_idx": { + "name": "instance_user_roles_role_idx", + "columns": [ + { + "expression": "role", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invites": { + "name": "invites", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "invite_type": { + "name": "invite_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'company_join'" + }, + "token_hash": { + "name": "token_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allowed_join_types": { + "name": "allowed_join_types", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'both'" + }, + "defaults_payload": { + "name": "defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + }, + "invited_by_user_id": { + "name": "invited_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "revoked_at": { + "name": "revoked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invites_token_hash_unique_idx": { + "name": "invites_token_hash_unique_idx", + "columns": [ + { + "expression": "token_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invites_company_invite_state_idx": { + "name": "invites_company_invite_state_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "invite_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "revoked_at", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invites_company_id_companies_id_fk": { + "name": "invites_company_id_companies_id_fk", + "tableFrom": "invites", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_approvals": { + "name": "issue_approvals", + "schema": "", + "columns": { + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "approval_id": { + "name": "approval_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "linked_by_agent_id": { + "name": "linked_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "linked_by_user_id": { + "name": "linked_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_approvals_issue_idx": { + "name": "issue_approvals_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_approval_idx": { + "name": "issue_approvals_approval_idx", + "columns": [ + { + "expression": "approval_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_approvals_company_idx": { + "name": "issue_approvals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_approvals_company_id_companies_id_fk": { + "name": "issue_approvals_company_id_companies_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_approvals_issue_id_issues_id_fk": { + "name": "issue_approvals_issue_id_issues_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_approval_id_approvals_id_fk": { + "name": "issue_approvals_approval_id_approvals_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "approvals", + "columnsFrom": [ + "approval_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_approvals_linked_by_agent_id_agents_id_fk": { + "name": "issue_approvals_linked_by_agent_id_agents_id_fk", + "tableFrom": "issue_approvals", + "tableTo": "agents", + "columnsFrom": [ + "linked_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_approvals_pk": { + "name": "issue_approvals_pk", + "columns": [ + "issue_id", + "approval_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_attachments": { + "name": "issue_attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "asset_id": { + "name": "asset_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_comment_id": { + "name": "issue_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_attachments_company_issue_idx": { + "name": "issue_attachments_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_issue_comment_idx": { + "name": "issue_attachments_issue_comment_idx", + "columns": [ + { + "expression": "issue_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_attachments_asset_uq": { + "name": "issue_attachments_asset_uq", + "columns": [ + { + "expression": "asset_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_attachments_company_id_companies_id_fk": { + "name": "issue_attachments_company_id_companies_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_attachments_issue_id_issues_id_fk": { + "name": "issue_attachments_issue_id_issues_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_asset_id_assets_id_fk": { + "name": "issue_attachments_asset_id_assets_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "assets", + "columnsFrom": [ + "asset_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_attachments_issue_comment_id_issue_comments_id_fk": { + "name": "issue_attachments_issue_comment_id_issue_comments_id_fk", + "tableFrom": "issue_attachments", + "tableTo": "issue_comments", + "columnsFrom": [ + "issue_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_comments": { + "name": "issue_comments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "author_agent_id": { + "name": "author_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "author_user_id": { + "name": "author_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_comments_issue_idx": { + "name": "issue_comments_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_idx": { + "name": "issue_comments_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_issue_created_at_idx": { + "name": "issue_comments_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_company_author_issue_created_at_idx": { + "name": "issue_comments_company_author_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "author_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_comments_body_search_idx": { + "name": "issue_comments_body_search_idx", + "columns": [ + { + "expression": "body", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "issue_comments_company_id_companies_id_fk": { + "name": "issue_comments_company_id_companies_id_fk", + "tableFrom": "issue_comments", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_issue_id_issues_id_fk": { + "name": "issue_comments_issue_id_issues_id_fk", + "tableFrom": "issue_comments", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_author_agent_id_agents_id_fk": { + "name": "issue_comments_author_agent_id_agents_id_fk", + "tableFrom": "issue_comments", + "tableTo": "agents", + "columnsFrom": [ + "author_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_comments_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_comments_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_comments", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_documents": { + "name": "issue_documents", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_documents_company_issue_key_uq": { + "name": "issue_documents_company_issue_key_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_document_uq": { + "name": "issue_documents_document_uq", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_documents_company_issue_updated_idx": { + "name": "issue_documents_company_issue_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_documents_company_id_companies_id_fk": { + "name": "issue_documents_company_id_companies_id_fk", + "tableFrom": "issue_documents", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_documents_issue_id_issues_id_fk": { + "name": "issue_documents_issue_id_issues_id_fk", + "tableFrom": "issue_documents", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_documents_document_id_documents_id_fk": { + "name": "issue_documents_document_id_documents_id_fk", + "tableFrom": "issue_documents", + "tableTo": "documents", + "columnsFrom": [ + "document_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_execution_decisions": { + "name": "issue_execution_decisions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_id": { + "name": "stage_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "stage_type": { + "name": "stage_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "actor_agent_id": { + "name": "actor_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "actor_user_id": { + "name": "actor_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "outcome": { + "name": "outcome", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "body": { + "name": "body", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_execution_decisions_company_issue_idx": { + "name": "issue_execution_decisions_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_execution_decisions_stage_idx": { + "name": "issue_execution_decisions_stage_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stage_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_execution_decisions_company_id_companies_id_fk": { + "name": "issue_execution_decisions_company_id_companies_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_issue_id_issues_id_fk": { + "name": "issue_execution_decisions_issue_id_issues_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_execution_decisions_actor_agent_id_agents_id_fk": { + "name": "issue_execution_decisions_actor_agent_id_agents_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "agents", + "columnsFrom": [ + "actor_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_execution_decisions_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_execution_decisions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_inbox_archives": { + "name": "issue_inbox_archives", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_inbox_archives_company_issue_idx": { + "name": "issue_inbox_archives_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_user_idx": { + "name": "issue_inbox_archives_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_inbox_archives_company_issue_user_idx": { + "name": "issue_inbox_archives_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_inbox_archives_company_id_companies_id_fk": { + "name": "issue_inbox_archives_company_id_companies_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_inbox_archives_issue_id_issues_id_fk": { + "name": "issue_inbox_archives_issue_id_issues_id_fk", + "tableFrom": "issue_inbox_archives", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_labels": { + "name": "issue_labels", + "schema": "", + "columns": { + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "label_id": { + "name": "label_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_labels_issue_idx": { + "name": "issue_labels_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_label_idx": { + "name": "issue_labels_label_idx", + "columns": [ + { + "expression": "label_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_labels_company_idx": { + "name": "issue_labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_labels_issue_id_issues_id_fk": { + "name": "issue_labels_issue_id_issues_id_fk", + "tableFrom": "issue_labels", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_label_id_labels_id_fk": { + "name": "issue_labels_label_id_labels_id_fk", + "tableFrom": "issue_labels", + "tableTo": "labels", + "columnsFrom": [ + "label_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_labels_company_id_companies_id_fk": { + "name": "issue_labels_company_id_companies_id_fk", + "tableFrom": "issue_labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "issue_labels_pk": { + "name": "issue_labels_pk", + "columns": [ + "issue_id", + "label_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_read_states": { + "name": "issue_read_states", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "last_read_at": { + "name": "last_read_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_read_states_company_issue_idx": { + "name": "issue_read_states_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_user_idx": { + "name": "issue_read_states_company_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_read_states_company_issue_user_idx": { + "name": "issue_read_states_company_issue_user_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_read_states_company_id_companies_id_fk": { + "name": "issue_read_states_company_id_companies_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_read_states_issue_id_issues_id_fk": { + "name": "issue_read_states_issue_id_issues_id_fk", + "tableFrom": "issue_read_states", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_reference_mentions": { + "name": "issue_reference_mentions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_issue_id": { + "name": "source_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "target_issue_id": { + "name": "target_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "source_kind": { + "name": "source_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_record_id": { + "name": "source_record_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "document_key": { + "name": "document_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "matched_text": { + "name": "matched_text", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_reference_mentions_company_source_issue_idx": { + "name": "issue_reference_mentions_company_source_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_target_issue_idx": { + "name": "issue_reference_mentions_company_target_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_issue_pair_idx": { + "name": "issue_reference_mentions_company_issue_pair_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_source_mention_record_uq": { + "name": "issue_reference_mentions_company_source_mention_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_record_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_reference_mentions\".\"source_record_id\" is not null", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_reference_mentions_company_source_mention_null_record_uq": { + "name": "issue_reference_mentions_company_source_mention_null_record_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_reference_mentions\".\"source_record_id\" is null", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_reference_mentions_company_id_companies_id_fk": { + "name": "issue_reference_mentions_company_id_companies_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_reference_mentions_source_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_source_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "issues", + "columnsFrom": [ + "source_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_reference_mentions_target_issue_id_issues_id_fk": { + "name": "issue_reference_mentions_target_issue_id_issues_id_fk", + "tableFrom": "issue_reference_mentions", + "tableTo": "issues", + "columnsFrom": [ + "target_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_relations": { + "name": "issue_relations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "related_issue_id": { + "name": "related_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_relations_company_issue_idx": { + "name": "issue_relations_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_related_issue_idx": { + "name": "issue_relations_company_related_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_type_idx": { + "name": "issue_relations_company_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_relations_company_edge_uq": { + "name": "issue_relations_company_edge_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "related_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_relations_company_id_companies_id_fk": { + "name": "issue_relations_company_id_companies_id_fk", + "tableFrom": "issue_relations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_relations_issue_id_issues_id_fk": { + "name": "issue_relations_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_related_issue_id_issues_id_fk": { + "name": "issue_relations_related_issue_id_issues_id_fk", + "tableFrom": "issue_relations", + "tableTo": "issues", + "columnsFrom": [ + "related_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_relations_created_by_agent_id_agents_id_fk": { + "name": "issue_relations_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_relations", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_thread_interactions": { + "name": "issue_thread_interactions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "continuation_policy": { + "name": "continuation_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'wake_assignee'" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "source_comment_id": { + "name": "source_comment_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source_run_id": { + "name": "source_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "resolved_by_agent_id": { + "name": "resolved_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "resolved_by_user_id": { + "name": "resolved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "result": { + "name": "result", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "resolved_at": { + "name": "resolved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_thread_interactions_issue_idx": { + "name": "issue_thread_interactions_issue_idx", + "columns": [ + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_created_at_idx": { + "name": "issue_thread_interactions_company_issue_created_at_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_status_idx": { + "name": "issue_thread_interactions_company_issue_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_company_issue_idempotency_uq": { + "name": "issue_thread_interactions_company_issue_idempotency_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issue_thread_interactions\".\"idempotency_key\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_thread_interactions_source_comment_idx": { + "name": "issue_thread_interactions_source_comment_idx", + "columns": [ + { + "expression": "source_comment_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_thread_interactions_company_id_companies_id_fk": { + "name": "issue_thread_interactions_company_id_companies_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_issue_id_issues_id_fk": { + "name": "issue_thread_interactions_issue_id_issues_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_source_comment_id_issue_comments_id_fk": { + "name": "issue_thread_interactions_source_comment_id_issue_comments_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "issue_comments", + "columnsFrom": [ + "source_comment_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_source_run_id_heartbeat_runs_id_fk": { + "name": "issue_thread_interactions_source_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "source_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_thread_interactions_created_by_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_thread_interactions_resolved_by_agent_id_agents_id_fk": { + "name": "issue_thread_interactions_resolved_by_agent_id_agents_id_fk", + "tableFrom": "issue_thread_interactions", + "tableTo": "agents", + "columnsFrom": [ + "resolved_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_tree_hold_members": { + "name": "issue_tree_hold_members", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "hold_id": { + "name": "hold_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "depth": { + "name": "depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "issue_identifier": { + "name": "issue_identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_title": { + "name": "issue_title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issue_status": { + "name": "issue_status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "active_run_id": { + "name": "active_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "active_run_status": { + "name": "active_run_status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "skipped": { + "name": "skipped", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "skip_reason": { + "name": "skip_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_tree_hold_members_hold_issue_uq": { + "name": "issue_tree_hold_members_hold_issue_uq", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_hold_members_company_issue_idx": { + "name": "issue_tree_hold_members_company_issue_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_hold_members_hold_depth_idx": { + "name": "issue_tree_hold_members_hold_depth_idx", + "columns": [ + { + "expression": "hold_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "depth", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_tree_hold_members_company_id_companies_id_fk": { + "name": "issue_tree_hold_members_company_id_companies_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_tree_hold_members_hold_id_issue_tree_holds_id_fk": { + "name": "issue_tree_hold_members_hold_id_issue_tree_holds_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issue_tree_holds", + "columnsFrom": [ + "hold_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_hold_members_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_hold_members_parent_issue_id_issues_id_fk": { + "name": "issue_tree_hold_members_parent_issue_id_issues_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_hold_members_assignee_agent_id_agents_id_fk": { + "name": "issue_tree_hold_members_assignee_agent_id_agents_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_hold_members_active_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_hold_members_active_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_hold_members", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "active_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_tree_holds": { + "name": "issue_tree_holds", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "root_issue_id": { + "name": "root_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "mode": { + "name": "mode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "reason": { + "name": "reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_policy": { + "name": "release_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_actor_type": { + "name": "created_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_at": { + "name": "released_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "released_by_actor_type": { + "name": "released_by_actor_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_by_agent_id": { + "name": "released_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "released_by_user_id": { + "name": "released_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "released_by_run_id": { + "name": "released_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "release_reason": { + "name": "release_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "release_metadata": { + "name": "release_metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_tree_holds_company_root_status_idx": { + "name": "issue_tree_holds_company_root_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "root_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_tree_holds_company_status_mode_idx": { + "name": "issue_tree_holds_company_status_mode_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "mode", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_tree_holds_company_id_companies_id_fk": { + "name": "issue_tree_holds_company_id_companies_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_tree_holds_root_issue_id_issues_id_fk": { + "name": "issue_tree_holds_root_issue_id_issues_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "issues", + "columnsFrom": [ + "root_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_tree_holds_created_by_agent_id_agents_id_fk": { + "name": "issue_tree_holds_created_by_agent_id_agents_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_holds_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_released_by_agent_id_agents_id_fk": { + "name": "issue_tree_holds_released_by_agent_id_agents_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "agents", + "columnsFrom": [ + "released_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_tree_holds_released_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_tree_holds_released_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_tree_holds", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "released_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issue_work_products": { + "name": "issue_work_products", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "runtime_service_id": { + "name": "runtime_service_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "review_state": { + "name": "review_state", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'none'" + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "summary": { + "name": "summary", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_by_run_id": { + "name": "created_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issue_work_products_company_issue_type_idx": { + "name": "issue_work_products_company_issue_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_execution_workspace_type_idx": { + "name": "issue_work_products_company_execution_workspace_type_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_provider_external_id_idx": { + "name": "issue_work_products_company_provider_external_id_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issue_work_products_company_updated_idx": { + "name": "issue_work_products_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issue_work_products_company_id_companies_id_fk": { + "name": "issue_work_products_company_id_companies_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issue_work_products_project_id_projects_id_fk": { + "name": "issue_work_products_project_id_projects_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_issue_id_issues_id_fk": { + "name": "issue_work_products_issue_id_issues_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "issue_work_products_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issue_work_products_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk": { + "name": "issue_work_products_runtime_service_id_workspace_runtime_services_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "workspace_runtime_services", + "columnsFrom": [ + "runtime_service_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issue_work_products_created_by_run_id_heartbeat_runs_id_fk": { + "name": "issue_work_products_created_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "issue_work_products", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "created_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.issues": { + "name": "issues", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_id": { + "name": "parent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "assignee_user_id": { + "name": "assignee_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "checkout_run_id": { + "name": "checkout_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_run_id": { + "name": "execution_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_agent_name_key": { + "name": "execution_agent_name_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_locked_at": { + "name": "execution_locked_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "issue_number": { + "name": "issue_number", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_kind": { + "name": "origin_kind", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'manual'" + }, + "origin_id": { + "name": "origin_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_run_id": { + "name": "origin_run_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "origin_fingerprint": { + "name": "origin_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "request_depth": { + "name": "request_depth", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "billing_code": { + "name": "billing_code", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_adapter_overrides": { + "name": "assignee_adapter_overrides", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_policy": { + "name": "execution_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "execution_state": { + "name": "execution_state", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "monitor_next_check_at": { + "name": "monitor_next_check_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_wake_requested_at": { + "name": "monitor_wake_requested_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_last_triggered_at": { + "name": "monitor_last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "monitor_attempt_count": { + "name": "monitor_attempt_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "monitor_notes": { + "name": "monitor_notes", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "monitor_scheduled_by": { + "name": "monitor_scheduled_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_preference": { + "name": "execution_workspace_preference", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_settings": { + "name": "execution_workspace_settings", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "cancelled_at": { + "name": "cancelled_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "hidden_at": { + "name": "hidden_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "issues_company_status_idx": { + "name": "issues_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_status_idx": { + "name": "issues_company_assignee_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_assignee_user_status_idx": { + "name": "issues_company_assignee_user_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_parent_idx": { + "name": "issues_company_parent_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_idx": { + "name": "issues_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_origin_idx": { + "name": "issues_company_origin_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_project_workspace_idx": { + "name": "issues_company_project_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_execution_workspace_idx": { + "name": "issues_company_execution_workspace_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_company_monitor_due_idx": { + "name": "issues_company_monitor_due_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "monitor_next_check_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_identifier_idx": { + "name": "issues_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_title_search_idx": { + "name": "issues_title_search_idx", + "columns": [ + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_identifier_search_idx": { + "name": "issues_identifier_search_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_description_search_idx": { + "name": "issues_description_search_idx", + "columns": [ + { + "expression": "description", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "gin_trgm_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "issues_open_routine_execution_uq": { + "name": "issues_open_routine_execution_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'routine_execution'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"execution_run_id\" is not null\n and \"issues\".\"status\" in ('backlog', 'todo', 'in_progress', 'in_review', 'blocked')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_liveness_recovery_incident_uq": { + "name": "issues_active_liveness_recovery_incident_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'harness_liveness_escalation'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_liveness_recovery_leaf_uq": { + "name": "issues_active_liveness_recovery_leaf_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'harness_liveness_escalation'\n and \"issues\".\"origin_fingerprint\" <> 'default'\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_stale_run_evaluation_uq": { + "name": "issues_active_stale_run_evaluation_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'stale_active_run_evaluation'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_productivity_review_uq": { + "name": "issues_active_productivity_review_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'issue_productivity_review'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + }, + "issues_active_stranded_issue_recovery_uq": { + "name": "issues_active_stranded_issue_recovery_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "origin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"issues\".\"origin_kind\" = 'stranded_issue_recovery'\n and \"issues\".\"origin_id\" is not null\n and \"issues\".\"hidden_at\" is null\n and \"issues\".\"status\" not in ('done', 'cancelled')", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "issues_company_id_companies_id_fk": { + "name": "issues_company_id_companies_id_fk", + "tableFrom": "issues", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_id_projects_id_fk": { + "name": "issues_project_id_projects_id_fk", + "tableFrom": "issues", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_project_workspace_id_project_workspaces_id_fk": { + "name": "issues_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_goal_id_goals_id_fk": { + "name": "issues_goal_id_goals_id_fk", + "tableFrom": "issues", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_parent_id_issues_id_fk": { + "name": "issues_parent_id_issues_id_fk", + "tableFrom": "issues", + "tableTo": "issues", + "columnsFrom": [ + "parent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_assignee_agent_id_agents_id_fk": { + "name": "issues_assignee_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_checkout_run_id_heartbeat_runs_id_fk": { + "name": "issues_checkout_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "checkout_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_execution_run_id_heartbeat_runs_id_fk": { + "name": "issues_execution_run_id_heartbeat_runs_id_fk", + "tableFrom": "issues", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "execution_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "issues_created_by_agent_id_agents_id_fk": { + "name": "issues_created_by_agent_id_agents_id_fk", + "tableFrom": "issues", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "issues_execution_workspace_id_execution_workspaces_id_fk": { + "name": "issues_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "issues", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.join_requests": { + "name": "join_requests", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "invite_id": { + "name": "invite_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "request_type": { + "name": "request_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending_approval'" + }, + "request_ip": { + "name": "request_ip", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "requesting_user_id": { + "name": "requesting_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "request_email_snapshot": { + "name": "request_email_snapshot", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_name": { + "name": "agent_name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "adapter_type": { + "name": "adapter_type", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "capabilities": { + "name": "capabilities", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "agent_defaults_payload": { + "name": "agent_defaults_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "claim_secret_hash": { + "name": "claim_secret_hash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "claim_secret_expires_at": { + "name": "claim_secret_expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "claim_secret_consumed_at": { + "name": "claim_secret_consumed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_agent_id": { + "name": "created_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "approved_by_user_id": { + "name": "approved_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "approved_at": { + "name": "approved_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "rejected_by_user_id": { + "name": "rejected_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "rejected_at": { + "name": "rejected_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "join_requests_invite_unique_idx": { + "name": "join_requests_invite_unique_idx", + "columns": [ + { + "expression": "invite_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_company_status_type_created_idx": { + "name": "join_requests_company_status_type_created_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "request_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_pending_human_user_uq": { + "name": "join_requests_pending_human_user_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "requesting_user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"requesting_user_id\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + }, + "join_requests_pending_human_email_uq": { + "name": "join_requests_pending_human_email_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "lower(\"request_email_snapshot\")", + "asc": true, + "isExpression": true, + "nulls": "last" + } + ], + "isUnique": true, + "where": "\"join_requests\".\"request_type\" = 'human' AND \"join_requests\".\"status\" = 'pending_approval' AND \"join_requests\".\"request_email_snapshot\" IS NOT NULL", + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "join_requests_invite_id_invites_id_fk": { + "name": "join_requests_invite_id_invites_id_fk", + "tableFrom": "join_requests", + "tableTo": "invites", + "columnsFrom": [ + "invite_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_company_id_companies_id_fk": { + "name": "join_requests_company_id_companies_id_fk", + "tableFrom": "join_requests", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "join_requests_created_agent_id_agents_id_fk": { + "name": "join_requests_created_agent_id_agents_id_fk", + "tableFrom": "join_requests", + "tableTo": "agents", + "columnsFrom": [ + "created_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.labels": { + "name": "labels", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "labels_company_idx": { + "name": "labels_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "labels_company_name_idx": { + "name": "labels_company_name_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "labels_company_id_companies_id_fk": { + "name": "labels_company_id_companies_id_fk", + "tableFrom": "labels", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_company_settings": { + "name": "plugin_company_settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "settings_json": { + "name": "settings_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_company_settings_company_idx": { + "name": "plugin_company_settings_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_plugin_idx": { + "name": "plugin_company_settings_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_company_settings_company_plugin_uq": { + "name": "plugin_company_settings_company_plugin_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_company_settings_company_id_companies_id_fk": { + "name": "plugin_company_settings_company_id_companies_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_company_settings_plugin_id_plugins_id_fk": { + "name": "plugin_company_settings_plugin_id_plugins_id_fk", + "tableFrom": "plugin_company_settings", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_config": { + "name": "plugin_config", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "config_json": { + "name": "config_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_config_plugin_id_idx": { + "name": "plugin_config_plugin_id_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_config_plugin_id_plugins_id_fk": { + "name": "plugin_config_plugin_id_plugins_id_fk", + "tableFrom": "plugin_config", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_database_namespaces": { + "name": "plugin_database_namespaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_name": { + "name": "namespace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_mode": { + "name": "namespace_mode", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'schema'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_database_namespaces_plugin_idx": { + "name": "plugin_database_namespaces_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_database_namespaces_namespace_idx": { + "name": "plugin_database_namespaces_namespace_idx", + "columns": [ + { + "expression": "namespace_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_database_namespaces_status_idx": { + "name": "plugin_database_namespaces_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_database_namespaces_plugin_id_plugins_id_fk": { + "name": "plugin_database_namespaces_plugin_id_plugins_id_fk", + "tableFrom": "plugin_database_namespaces", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_entities": { + "name": "plugin_entities", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_entities_plugin_idx": { + "name": "plugin_entities_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_type_idx": { + "name": "plugin_entities_type_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_scope_idx": { + "name": "plugin_entities_scope_idx", + "columns": [ + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_entities_external_idx": { + "name": "plugin_entities_external_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "external_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_entities_plugin_id_plugins_id_fk": { + "name": "plugin_entities_plugin_id_plugins_id_fk", + "tableFrom": "plugin_entities", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_job_runs": { + "name": "plugin_job_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "job_id": { + "name": "job_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "logs": { + "name": "logs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_job_runs_job_idx": { + "name": "plugin_job_runs_job_idx", + "columns": [ + { + "expression": "job_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_plugin_idx": { + "name": "plugin_job_runs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_job_runs_status_idx": { + "name": "plugin_job_runs_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_job_runs_job_id_plugin_jobs_id_fk": { + "name": "plugin_job_runs_job_id_plugin_jobs_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugin_jobs", + "columnsFrom": [ + "job_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_job_runs_plugin_id_plugins_id_fk": { + "name": "plugin_job_runs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_job_runs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_jobs": { + "name": "plugin_jobs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "job_key": { + "name": "job_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schedule": { + "name": "schedule", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_jobs_plugin_idx": { + "name": "plugin_jobs_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_next_run_idx": { + "name": "plugin_jobs_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_jobs_unique_idx": { + "name": "plugin_jobs_unique_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "job_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_jobs_plugin_id_plugins_id_fk": { + "name": "plugin_jobs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_jobs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_logs": { + "name": "plugin_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'info'" + }, + "message": { + "name": "message", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "meta": { + "name": "meta", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_logs_plugin_time_idx": { + "name": "plugin_logs_plugin_time_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_logs_level_idx": { + "name": "plugin_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_logs_plugin_id_plugins_id_fk": { + "name": "plugin_logs_plugin_id_plugins_id_fk", + "tableFrom": "plugin_logs", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_migrations": { + "name": "plugin_migrations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace_name": { + "name": "namespace_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "migration_key": { + "name": "migration_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "checksum": { + "name": "checksum", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "plugin_version": { + "name": "plugin_version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "applied_at": { + "name": "applied_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "plugin_migrations_plugin_key_idx": { + "name": "plugin_migrations_plugin_key_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "migration_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_migrations_plugin_idx": { + "name": "plugin_migrations_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_migrations_status_idx": { + "name": "plugin_migrations_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_migrations_plugin_id_plugins_id_fk": { + "name": "plugin_migrations_plugin_id_plugins_id_fk", + "tableFrom": "plugin_migrations", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_state": { + "name": "plugin_state", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "scope_kind": { + "name": "scope_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "state_key": { + "name": "state_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value_json": { + "name": "value_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_state_plugin_scope_idx": { + "name": "plugin_state_plugin_scope_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "scope_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_state_plugin_id_plugins_id_fk": { + "name": "plugin_state_plugin_id_plugins_id_fk", + "tableFrom": "plugin_state", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "plugin_state_unique_entry_idx": { + "name": "plugin_state_unique_entry_idx", + "nullsNotDistinct": true, + "columns": [ + "plugin_id", + "scope_kind", + "scope_id", + "namespace", + "state_key" + ] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_webhook_deliveries": { + "name": "plugin_webhook_deliveries", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "webhook_key": { + "name": "webhook_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "external_id": { + "name": "external_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "duration_ms": { + "name": "duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "error": { + "name": "error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "payload": { + "name": "payload", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "headers": { + "name": "headers", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_webhook_deliveries_plugin_idx": { + "name": "plugin_webhook_deliveries_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_status_idx": { + "name": "plugin_webhook_deliveries_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_webhook_deliveries_key_idx": { + "name": "plugin_webhook_deliveries_key_idx", + "columns": [ + { + "expression": "webhook_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_webhook_deliveries_plugin_id_plugins_id_fk": { + "name": "plugin_webhook_deliveries_plugin_id_plugins_id_fk", + "tableFrom": "plugin_webhook_deliveries", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugins": { + "name": "plugins", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "package_name": { + "name": "package_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "api_version": { + "name": "api_version", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1 + }, + "categories": { + "name": "categories", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "manifest_json": { + "name": "manifest_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'installed'" + }, + "install_order": { + "name": "install_order", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "package_path": { + "name": "package_path", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "installed_at": { + "name": "installed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugins_plugin_key_idx": { + "name": "plugins_plugin_key_idx", + "columns": [ + { + "expression": "plugin_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugins_status_idx": { + "name": "plugins_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.principal_permission_grants": { + "name": "principal_permission_grants", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "principal_type": { + "name": "principal_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "principal_id": { + "name": "principal_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_key": { + "name": "permission_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "granted_by_user_id": { + "name": "granted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "principal_permission_grants_unique_idx": { + "name": "principal_permission_grants_unique_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "principal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "principal_permission_grants_company_permission_idx": { + "name": "principal_permission_grants_company_permission_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "principal_permission_grants_company_id_companies_id_fk": { + "name": "principal_permission_grants_company_id_companies_id_fk", + "tableFrom": "principal_permission_grants", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_goals": { + "name": "project_goals", + "schema": "", + "columns": { + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_goals_project_idx": { + "name": "project_goals_project_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_goal_idx": { + "name": "project_goals_goal_idx", + "columns": [ + { + "expression": "goal_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_goals_company_idx": { + "name": "project_goals_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_goals_project_id_projects_id_fk": { + "name": "project_goals_project_id_projects_id_fk", + "tableFrom": "project_goals", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_goal_id_goals_id_fk": { + "name": "project_goals_goal_id_goals_id_fk", + "tableFrom": "project_goals", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "project_goals_company_id_companies_id_fk": { + "name": "project_goals_company_id_companies_id_fk", + "tableFrom": "project_goals", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": { + "project_goals_project_id_goal_id_pk": { + "name": "project_goals_project_id_goal_id_pk", + "columns": [ + "project_id", + "goal_id" + ] + } + }, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.project_workspaces": { + "name": "project_workspaces", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_type": { + "name": "source_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'local_path'" + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_url": { + "name": "repo_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "repo_ref": { + "name": "repo_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "default_ref": { + "name": "default_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "setup_command": { + "name": "setup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cleanup_command": { + "name": "cleanup_command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_provider": { + "name": "remote_provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "remote_workspace_ref": { + "name": "remote_workspace_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "shared_workspace_key": { + "name": "shared_workspace_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "is_primary": { + "name": "is_primary", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "project_workspaces_company_project_idx": { + "name": "project_workspaces_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_primary_idx": { + "name": "project_workspaces_project_primary_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_primary", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_source_type_idx": { + "name": "project_workspaces_project_source_type_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_company_shared_key_idx": { + "name": "project_workspaces_company_shared_key_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "shared_workspace_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "project_workspaces_project_remote_ref_idx": { + "name": "project_workspaces_project_remote_ref_idx", + "columns": [ + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_provider", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "remote_workspace_ref", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "project_workspaces_company_id_companies_id_fk": { + "name": "project_workspaces_company_id_companies_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "project_workspaces_project_id_projects_id_fk": { + "name": "project_workspaces_project_id_projects_id_fk", + "tableFrom": "project_workspaces", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.projects": { + "name": "projects", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'backlog'" + }, + "lead_agent_id": { + "name": "lead_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "target_date": { + "name": "target_date", + "type": "date", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "env": { + "name": "env", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "pause_reason": { + "name": "pause_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_policy": { + "name": "execution_workspace_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "archived_at": { + "name": "archived_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "projects_company_idx": { + "name": "projects_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "projects_company_id_companies_id_fk": { + "name": "projects_company_id_companies_id_fk", + "tableFrom": "projects", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_goal_id_goals_id_fk": { + "name": "projects_goal_id_goals_id_fk", + "tableFrom": "projects", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "projects_lead_agent_id_agents_id_fk": { + "name": "projects_lead_agent_id_agents_id_fk", + "tableFrom": "projects", + "tableTo": "agents", + "columnsFrom": [ + "lead_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_runs": { + "name": "routine_runs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "trigger_id": { + "name": "trigger_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'received'" + }, + "triggered_at": { + "name": "triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "idempotency_key": { + "name": "idempotency_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "trigger_payload": { + "name": "trigger_payload", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "dispatch_fingerprint": { + "name": "dispatch_fingerprint", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "linked_issue_id": { + "name": "linked_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "coalesced_into_run_id": { + "name": "coalesced_into_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_runs_company_routine_idx": { + "name": "routine_runs_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idx": { + "name": "routine_runs_trigger_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_dispatch_fingerprint_idx": { + "name": "routine_runs_dispatch_fingerprint_idx", + "columns": [ + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "dispatch_fingerprint", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_linked_issue_idx": { + "name": "routine_runs_linked_issue_idx", + "columns": [ + { + "expression": "linked_issue_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_runs_trigger_idempotency_idx": { + "name": "routine_runs_trigger_idempotency_idx", + "columns": [ + { + "expression": "trigger_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "idempotency_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_runs_company_id_companies_id_fk": { + "name": "routine_runs_company_id_companies_id_fk", + "tableFrom": "routine_runs", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_routine_id_routines_id_fk": { + "name": "routine_runs_routine_id_routines_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_runs_trigger_id_routine_triggers_id_fk": { + "name": "routine_runs_trigger_id_routine_triggers_id_fk", + "tableFrom": "routine_runs", + "tableTo": "routine_triggers", + "columnsFrom": [ + "trigger_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_runs_linked_issue_id_issues_id_fk": { + "name": "routine_runs_linked_issue_id_issues_id_fk", + "tableFrom": "routine_runs", + "tableTo": "issues", + "columnsFrom": [ + "linked_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routine_triggers": { + "name": "routine_triggers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "routine_id": { + "name": "routine_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "kind": { + "name": "kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "label": { + "name": "label", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_fired_at": { + "name": "last_fired_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "public_id": { + "name": "public_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "secret_id": { + "name": "secret_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "signing_mode": { + "name": "signing_mode", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "replay_window_sec": { + "name": "replay_window_sec", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "last_rotated_at": { + "name": "last_rotated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_result": { + "name": "last_result", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routine_triggers_company_routine_idx": { + "name": "routine_triggers_company_routine_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "routine_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_company_kind_idx": { + "name": "routine_triggers_company_kind_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "kind", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_next_run_idx": { + "name": "routine_triggers_next_run_idx", + "columns": [ + { + "expression": "next_run_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_idx": { + "name": "routine_triggers_public_id_idx", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routine_triggers_public_id_uq": { + "name": "routine_triggers_public_id_uq", + "columns": [ + { + "expression": "public_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routine_triggers_company_id_companies_id_fk": { + "name": "routine_triggers_company_id_companies_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_routine_id_routines_id_fk": { + "name": "routine_triggers_routine_id_routines_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "routines", + "columnsFrom": [ + "routine_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routine_triggers_secret_id_company_secrets_id_fk": { + "name": "routine_triggers_secret_id_company_secrets_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "company_secrets", + "columnsFrom": [ + "secret_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_created_by_agent_id_agents_id_fk": { + "name": "routine_triggers_created_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routine_triggers_updated_by_agent_id_agents_id_fk": { + "name": "routine_triggers_updated_by_agent_id_agents_id_fk", + "tableFrom": "routine_triggers", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.routines": { + "name": "routines", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "goal_id": { + "name": "goal_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "parent_issue_id": { + "name": "parent_issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assignee_agent_id": { + "name": "assignee_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "priority": { + "name": "priority", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'medium'" + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "concurrency_policy": { + "name": "concurrency_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'coalesce_if_active'" + }, + "catch_up_policy": { + "name": "catch_up_policy", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'skip_missed'" + }, + "variables": { + "name": "variables", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_by_agent_id": { + "name": "created_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "created_by_user_id": { + "name": "created_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "updated_by_agent_id": { + "name": "updated_by_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "updated_by_user_id": { + "name": "updated_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "last_triggered_at": { + "name": "last_triggered_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "last_enqueued_at": { + "name": "last_enqueued_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "routines_company_status_idx": { + "name": "routines_company_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_assignee_idx": { + "name": "routines_company_assignee_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "assignee_agent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "routines_company_project_idx": { + "name": "routines_company_project_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "routines_company_id_companies_id_fk": { + "name": "routines_company_id_companies_id_fk", + "tableFrom": "routines", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_project_id_projects_id_fk": { + "name": "routines_project_id_projects_id_fk", + "tableFrom": "routines", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "routines_goal_id_goals_id_fk": { + "name": "routines_goal_id_goals_id_fk", + "tableFrom": "routines", + "tableTo": "goals", + "columnsFrom": [ + "goal_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_parent_issue_id_issues_id_fk": { + "name": "routines_parent_issue_id_issues_id_fk", + "tableFrom": "routines", + "tableTo": "issues", + "columnsFrom": [ + "parent_issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_assignee_agent_id_agents_id_fk": { + "name": "routines_assignee_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "assignee_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "routines_created_by_agent_id_agents_id_fk": { + "name": "routines_created_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "created_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "routines_updated_by_agent_id_agents_id_fk": { + "name": "routines_updated_by_agent_id_agents_id_fk", + "tableFrom": "routines", + "tableTo": "agents", + "columnsFrom": [ + "updated_by_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_sidebar_preferences": { + "name": "user_sidebar_preferences", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "company_order": { + "name": "company_order", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "user_sidebar_preferences_user_uq": { + "name": "user_sidebar_preferences_user_uq", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_operations": { + "name": "workspace_operations", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "heartbeat_run_id": { + "name": "heartbeat_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "phase": { + "name": "phase", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "exit_code": { + "name": "exit_code", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "log_store": { + "name": "log_store", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_ref": { + "name": "log_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_bytes": { + "name": "log_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": false + }, + "log_sha256": { + "name": "log_sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "log_compressed": { + "name": "log_compressed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "stdout_excerpt": { + "name": "stdout_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stderr_excerpt": { + "name": "stderr_excerpt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "finished_at": { + "name": "finished_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_operations_company_run_started_idx": { + "name": "workspace_operations_company_run_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "heartbeat_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_operations_company_workspace_started_idx": { + "name": "workspace_operations_company_workspace_started_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_operations_company_id_companies_id_fk": { + "name": "workspace_operations_company_id_companies_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_operations_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_operations_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk": { + "name": "workspace_operations_heartbeat_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_operations", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "heartbeat_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_runtime_services": { + "name": "workspace_runtime_services", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "project_id": { + "name": "project_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "project_workspace_id": { + "name": "project_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "execution_workspace_id": { + "name": "execution_workspace_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issue_id": { + "name": "issue_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "scope_type": { + "name": "scope_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope_id": { + "name": "scope_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "service_name": { + "name": "service_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "lifecycle": { + "name": "lifecycle", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reuse_key": { + "name": "reuse_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "command": { + "name": "command", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cwd": { + "name": "cwd", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "port": { + "name": "port", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_ref": { + "name": "provider_ref", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "owner_agent_id": { + "name": "owner_agent_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "started_by_run_id": { + "name": "started_by_run_id", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "last_used_at": { + "name": "last_used_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "started_at": { + "name": "started_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "stopped_at": { + "name": "stopped_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": false + }, + "stop_policy": { + "name": "stop_policy", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "health_status": { + "name": "health_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'unknown'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_runtime_services_company_workspace_status_idx": { + "name": "workspace_runtime_services_company_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_execution_workspace_status_idx": { + "name": "workspace_runtime_services_company_execution_workspace_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "execution_workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_project_status_idx": { + "name": "workspace_runtime_services_company_project_status_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "project_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_run_idx": { + "name": "workspace_runtime_services_run_idx", + "columns": [ + { + "expression": "started_by_run_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_runtime_services_company_updated_idx": { + "name": "workspace_runtime_services_company_updated_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_runtime_services_company_id_companies_id_fk": { + "name": "workspace_runtime_services_company_id_companies_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_id_projects_id_fk": { + "name": "workspace_runtime_services_project_id_projects_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "projects", + "columnsFrom": [ + "project_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk": { + "name": "workspace_runtime_services_project_workspace_id_project_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "project_workspaces", + "columnsFrom": [ + "project_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk": { + "name": "workspace_runtime_services_execution_workspace_id_execution_workspaces_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "execution_workspaces", + "columnsFrom": [ + "execution_workspace_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_issue_id_issues_id_fk": { + "name": "workspace_runtime_services_issue_id_issues_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "issues", + "columnsFrom": [ + "issue_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_owner_agent_id_agents_id_fk": { + "name": "workspace_runtime_services_owner_agent_id_agents_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "agents", + "columnsFrom": [ + "owner_agent_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + }, + "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk": { + "name": "workspace_runtime_services_started_by_run_id_heartbeat_runs_id_fk", + "tableFrom": "workspace_runtime_services", + "tableTo": "heartbeat_runs", + "columnsFrom": [ + "started_by_run_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.plugin_managed_resources": { + "name": "plugin_managed_resources", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "company_id": { + "name": "company_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_id": { + "name": "plugin_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "plugin_key": { + "name": "plugin_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_kind": { + "name": "resource_kind", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_key": { + "name": "resource_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resource_id": { + "name": "resource_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "defaults_json": { + "name": "defaults_json", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "created_at": { + "name": "created_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "plugin_managed_resources_company_idx": { + "name": "plugin_managed_resources_company_idx", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_plugin_idx": { + "name": "plugin_managed_resources_plugin_idx", + "columns": [ + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_resource_idx": { + "name": "plugin_managed_resources_resource_idx", + "columns": [ + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "plugin_managed_resources_company_plugin_resource_uq": { + "name": "plugin_managed_resources_company_plugin_resource_uq", + "columns": [ + { + "expression": "company_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "plugin_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_kind", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "resource_key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "plugin_managed_resources_company_id_companies_id_fk": { + "name": "plugin_managed_resources_company_id_companies_id_fk", + "tableFrom": "plugin_managed_resources", + "tableTo": "companies", + "columnsFrom": [ + "company_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "plugin_managed_resources_plugin_id_plugins_id_fk": { + "name": "plugin_managed_resources_plugin_id_plugins_id_fk", + "tableFrom": "plugin_managed_resources", + "tableTo": "plugins", + "columnsFrom": [ + "plugin_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": {}, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/src/migrations/meta/_journal.json b/packages/db/src/migrations/meta/_journal.json index f0d21b99..0c82238a 100644 --- a/packages/db/src/migrations/meta/_journal.json +++ b/packages/db/src/migrations/meta/_journal.json @@ -533,6 +533,13 @@ "when": 1777572332006, "tag": "0075_cultured_sebastian_shaw", "breakpoints": true + }, + { + "idx": 76, + "version": "7", + "when": 1777675301279, + "tag": "0076_useful_elektra", + "breakpoints": true } ] -} \ No newline at end of file +} diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts index 322a326d..7e4a0eb1 100644 --- a/packages/db/src/schema/index.ts +++ b/packages/db/src/schema/index.ts @@ -65,6 +65,7 @@ export { companySkills } from "./company_skills.js"; export { plugins } from "./plugins.js"; export { pluginConfig } from "./plugin_config.js"; export { pluginCompanySettings } from "./plugin_company_settings.js"; +export { pluginManagedResources } from "./plugin_managed_resources.js"; export { pluginState } from "./plugin_state.js"; export { pluginEntities } from "./plugin_entities.js"; export { pluginDatabaseNamespaces, pluginMigrations } from "./plugin_database.js"; diff --git a/packages/db/src/schema/plugin_managed_resources.ts b/packages/db/src/schema/plugin_managed_resources.ts new file mode 100644 index 00000000..ec254d14 --- /dev/null +++ b/packages/db/src/schema/plugin_managed_resources.ts @@ -0,0 +1,34 @@ +import { pgTable, uuid, text, timestamp, jsonb, index, uniqueIndex } from "drizzle-orm/pg-core"; +import { companies } from "./companies.js"; +import { plugins } from "./plugins.js"; + +export const pluginManagedResources = pgTable( + "plugin_managed_resources", + { + id: uuid("id").primaryKey().defaultRandom(), + companyId: uuid("company_id") + .notNull() + .references(() => companies.id, { onDelete: "cascade" }), + pluginId: uuid("plugin_id") + .notNull() + .references(() => plugins.id, { onDelete: "cascade" }), + pluginKey: text("plugin_key").notNull(), + resourceKind: text("resource_kind").notNull(), + resourceKey: text("resource_key").notNull(), + resourceId: uuid("resource_id").notNull(), + defaultsJson: jsonb("defaults_json").$type>().notNull().default({}), + createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(), + updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow(), + }, + (table) => ({ + companyIdx: index("plugin_managed_resources_company_idx").on(table.companyId), + pluginIdx: index("plugin_managed_resources_plugin_idx").on(table.pluginId), + resourceIdx: index("plugin_managed_resources_resource_idx").on(table.resourceKind, table.resourceId), + companyPluginResourceUq: uniqueIndex("plugin_managed_resources_company_plugin_resource_uq").on( + table.companyId, + table.pluginId, + table.resourceKind, + table.resourceKey, + ), + }), +); diff --git a/packages/plugins/create-paperclip-plugin/README.md b/packages/plugins/create-paperclip-plugin/README.md index 24294122..967fe56a 100644 --- a/packages/plugins/create-paperclip-plugin/README.md +++ b/packages/plugins/create-paperclip-plugin/README.md @@ -27,7 +27,7 @@ Generates: - `esbuild` and `rollup` config files using SDK bundler presets - dev server script for hot-reload (`paperclip-plugin-dev-server`) -The scaffold intentionally uses plain React elements rather than host-provided UI kit components, because the current plugin runtime does not ship a stable shared component library yet. +The scaffold starts with plain React elements so the generated plugin stays minimal. For Paperclip-native controls, import shared host components such as `MarkdownEditor`, `FileTree`, `AssigneePicker`, and `ProjectPicker` from `@paperclipai/plugin-sdk/ui`. Inside this repo, the generated package uses `@paperclipai/plugin-sdk` via `workspace:*`. diff --git a/packages/plugins/examples/plugin-file-browser-example/src/ui/index.tsx b/packages/plugins/examples/plugin-file-browser-example/src/ui/index.tsx index 0e12d903..e7088fbc 100644 --- a/packages/plugins/examples/plugin-file-browser-example/src/ui/index.tsx +++ b/packages/plugins/examples/plugin-file-browser-example/src/ui/index.tsx @@ -1,11 +1,12 @@ import type { + FileTreeNode, PluginProjectSidebarItemProps, PluginDetailTabProps, PluginCommentAnnotationProps, PluginCommentContextMenuItemProps, } from "@paperclipai/plugin-sdk/ui"; -import { usePluginAction, usePluginData } from "@paperclipai/plugin-sdk/ui"; -import { useMemo, useState, useEffect, useRef, type MouseEvent, type RefObject } from "react"; +import { FileTree, usePluginAction, usePluginData } from "@paperclipai/plugin-sdk/ui"; +import { useCallback, useMemo, useState, useEffect, useRef, type MouseEvent, type RefObject } from "react"; import { EditorView } from "@codemirror/view"; import { basicSetup } from "codemirror"; import { javascript } from "@codemirror/lang-javascript"; @@ -129,15 +130,31 @@ const editorLightHighlightStyle = HighlightStyle.define([ type Workspace = { id: string; projectId: string; name: string; path: string; isPrimary: boolean }; type FileEntry = { name: string; path: string; isDirectory: boolean }; -type FileTreeNodeProps = { - entry: FileEntry; - companyId: string | null; - projectId: string; - workspaceId: string; - selectedPath: string | null; - onSelect: (path: string) => void; - depth?: number; -}; + +function entryToFileTreeNode(entry: FileEntry): FileTreeNode { + return { + name: entry.name, + path: entry.path, + kind: entry.isDirectory ? "dir" : "file", + children: [], + }; +} + +function entriesToFileTreeNodes(entries: FileEntry[]): FileTreeNode[] { + return entries.map(entryToFileTreeNode); +} + +function setChildrenAtPath(nodes: FileTreeNode[], path: string, children: FileTreeNode[]): FileTreeNode[] { + return nodes.map((node) => { + if (node.path === path) { + return { ...node, children }; + } + if (node.kind === "dir" && node.children.length > 0 && (path === node.path || path.startsWith(`${node.path}/`))) { + return { ...node, children: setChildrenAtPath(node.children, path, children) }; + } + return node; + }); +} const PathLikePattern = /[\\/]/; const WindowsDrivePathPattern = /^[A-Za-z]:[\\/]/; @@ -235,109 +252,6 @@ function useAvailableHeight( return height; } -function FileTreeNode({ - entry, - companyId, - projectId, - workspaceId, - selectedPath, - onSelect, - depth = 0, -}: FileTreeNodeProps) { - const [isExpanded, setIsExpanded] = useState(false); - const isSelected = selectedPath === entry.path; - - if (entry.isDirectory) { - return ( -
  • - - {isExpanded ? ( - - ) : null} -
  • - ); - } - - return ( -
  • - -
  • - ); -} - -function ExpandedDirectoryChildren({ - directoryPath, - companyId, - projectId, - workspaceId, - selectedPath, - onSelect, - depth, -}: { - directoryPath: string; - companyId: string | null; - projectId: string; - workspaceId: string; - selectedPath: string | null; - onSelect: (path: string) => void; - depth: number; -}) { - const { data: childData } = usePluginData<{ entries: FileEntry[] }>("fileList", { - companyId, - projectId, - workspaceId, - directoryPath, - }); - const children = childData?.entries ?? []; - - if (children.length === 0) { - return null; - } - - return ( -
      - {children.map((child) => ( - - ))} -
    - ); -} - /** * Project sidebar item: link "Files" that opens the project detail with the Files plugin tab. */ @@ -430,11 +344,60 @@ export function FilesTab({ context }: PluginDetailTabProps) { () => (selectedWorkspace ? { projectId, companyId, workspaceId: selectedWorkspace.id } : {}), [companyId, projectId, selectedWorkspace], ); - const { data: fileListData, loading: fileListLoading } = usePluginData<{ entries: FileEntry[] }>( + const { data: fileListData, loading: fileListLoading, error: fileListError } = usePluginData<{ entries: FileEntry[] }>( "fileList", fileListParams, ); - const entries = fileListData?.entries ?? []; + + // Lazy-load directory children through an imperative action so the shared + // FileTree can reuse `expandedPaths` for state without spawning a hook per + // expanded directory. + const loadFileList = usePluginAction("loadFileList"); + const [nodes, setNodes] = useState([]); + const [expandedPaths, setExpandedPaths] = useState>(() => new Set()); + const [loadedDirs, setLoadedDirs] = useState>(() => new Set()); + const [loadingDirs, setLoadingDirs] = useState>(() => new Set()); + + useEffect(() => { + setNodes(fileListData?.entries ? entriesToFileTreeNodes(fileListData.entries) : []); + setExpandedPaths(new Set()); + setLoadedDirs(new Set()); + setLoadingDirs(new Set()); + }, [fileListData, selectedWorkspace?.id]); + + const handleToggleDir = useCallback( + (dirPath: string) => { + setExpandedPaths((current) => { + const next = new Set(current); + if (next.has(dirPath)) next.delete(dirPath); + else next.add(dirPath); + return next; + }); + if (!selectedWorkspace) return; + if (loadedDirs.has(dirPath) || loadingDirs.has(dirPath)) return; + setLoadingDirs((current) => new Set(current).add(dirPath)); + void loadFileList({ + projectId, + companyId, + workspaceId: selectedWorkspace.id, + directoryPath: dirPath, + }) + .then((response) => { + const entries = (response as { entries?: FileEntry[] })?.entries ?? []; + const children = entriesToFileTreeNodes(entries); + setNodes((current) => setChildrenAtPath(current, dirPath, children)); + setLoadedDirs((current) => new Set(current).add(dirPath)); + }) + .finally(() => { + setLoadingDirs((current) => { + const next = new Set(current); + next.delete(dirPath); + return next; + }); + }); + }, + [companyId, loadFileList, loadedDirs, loadingDirs, projectId, selectedWorkspace], + ); // Track the `?file=` query parameter across navigations (popstate). const [urlFilePath, setUrlFilePath] = useState(() => { @@ -610,28 +573,23 @@ export function FilesTab({ context }: PluginDetailTabProps) {
    {selectedWorkspace ? ( - fileListLoading ? ( -

    Loading files...

    - ) : entries.length > 0 ? ( -
      - {entries.map((entry) => ( - { - setSelectedPath(path); - setMobileView("editor"); - }} - /> - ))} -
    - ) : ( -

    No files found in this workspace.

    - ) + { + setSelectedPath(path); + setMobileView("editor"); + }} + loading={fileListLoading} + error={fileListError ? { message: fileListError.message } : null} + empty={{ + title: "No files", + description: "No files found in this workspace.", + }} + ariaLabel="Workspace files" + /> ) : (

    Select a workspace to browse files.

    )} diff --git a/packages/plugins/examples/plugin-file-browser-example/src/worker.ts b/packages/plugins/examples/plugin-file-browser-example/src/worker.ts index a1689834..cf038cf0 100644 --- a/packages/plugins/examples/plugin-file-browser-example/src/worker.ts +++ b/packages/plugins/examples/plugin-file-browser-example/src/worker.ts @@ -106,43 +106,46 @@ const plugin = definePlugin({ })); }); - ctx.data.register( - "fileList", - async (params: Record) => { - const projectId = params.projectId as string; - const companyId = typeof params.companyId === "string" ? params.companyId : ""; - const workspaceId = params.workspaceId as string; - const directoryPath = typeof params.directoryPath === "string" ? params.directoryPath : ""; - if (!projectId || !companyId || !workspaceId) return { entries: [] }; - const workspaces = await ctx.projects.listWorkspaces(projectId, companyId); - const workspace = workspaces.find((w) => w.id === workspaceId); - if (!workspace) return { entries: [] }; - const workspacePath = sanitizeWorkspacePath(workspace.path); - if (!workspacePath) return { entries: [] }; - const dirPath = resolveWorkspace(workspacePath, directoryPath); - if (!dirPath) { - return { entries: [] }; - } - if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) { - return { entries: [] }; - } - const names = fs.readdirSync(dirPath).sort((a, b) => a.localeCompare(b)); - const entries = names.map((name) => { - const full = path.join(dirPath, name); - const stat = fs.lstatSync(full); - const relativePath = path.relative(workspacePath, full); - return { - name, - path: relativePath, - isDirectory: stat.isDirectory(), - }; - }).sort((a, b) => { - if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1; - return a.name.localeCompare(b.name); - }); - return { entries }; - }, - ); + async function readFileList(params: Record) { + const projectId = params.projectId as string; + const companyId = typeof params.companyId === "string" ? params.companyId : ""; + const workspaceId = params.workspaceId as string; + const directoryPath = typeof params.directoryPath === "string" ? params.directoryPath : ""; + if (!projectId || !companyId || !workspaceId) return { entries: [] }; + const workspaces = await ctx.projects.listWorkspaces(projectId, companyId); + const workspace = workspaces.find((w) => w.id === workspaceId); + if (!workspace) return { entries: [] }; + const workspacePath = sanitizeWorkspacePath(workspace.path); + if (!workspacePath) return { entries: [] }; + const dirPath = resolveWorkspace(workspacePath, directoryPath); + if (!dirPath) { + return { entries: [] }; + } + if (!fs.existsSync(dirPath) || !fs.statSync(dirPath).isDirectory()) { + return { entries: [] }; + } + const names = fs.readdirSync(dirPath).sort((a, b) => a.localeCompare(b)); + const entries = names.map((name) => { + const full = path.join(dirPath, name); + const stat = fs.lstatSync(full); + const relativePath = path.relative(workspacePath, full); + return { + name, + path: relativePath, + isDirectory: stat.isDirectory(), + }; + }).sort((a, b) => { + if (a.isDirectory !== b.isDirectory) return a.isDirectory ? -1 : 1; + return a.name.localeCompare(b.name); + }); + return { entries }; + } + + ctx.data.register("fileList", readFileList); + + // Mirror `fileList` as an action so the UI can lazily fetch directory + // children on tree expand without spawning a usePluginData hook per dir. + ctx.actions.register("loadFileList", readFileList); ctx.data.register( "fileContent", diff --git a/packages/plugins/examples/plugin-kitchen-sink-example/src/ui/index.tsx b/packages/plugins/examples/plugin-kitchen-sink-example/src/ui/index.tsx index 826dd832..ea3cb491 100644 --- a/packages/plugins/examples/plugin-kitchen-sink-example/src/ui/index.tsx +++ b/packages/plugins/examples/plugin-kitchen-sink-example/src/ui/index.tsx @@ -1,6 +1,9 @@ import { useEffect, useMemo, useState, type CSSProperties, type FormEvent, type ReactNode } from "react"; import { + AssigneePicker, + ProjectPicker, useHostContext, + useHostNavigation, usePluginAction, usePluginData, usePluginStream, @@ -248,14 +251,6 @@ const mutedTextStyle: CSSProperties = { lineHeight: 1.45, }; -function hostPath(companyPrefix: string | null | undefined, suffix: string): string { - return companyPrefix ? `/${companyPrefix}${suffix}` : suffix; -} - -function pluginPagePath(companyPrefix: string | null | undefined): string { - return hostPath(companyPrefix, `/${PAGE_ROUTE}`); -} - function getErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } @@ -521,6 +516,7 @@ function CompactSurfaceSummary({ label, entityType }: { label: string; entityTyp function KitchenSinkPageWidgets({ context }: { context: PluginPageProps["context"] }) { const overview = usePluginOverview(context.companyId); const toast = usePluginToast(); + const hostNavigation = useHostNavigation(); const emitDemoEvent = usePluginAction("emit-demo-event"); const startProgressStream = usePluginAction("start-progress-stream"); const writeMetric = usePluginAction("write-metric"); @@ -591,7 +587,7 @@ function KitchenSinkPageWidgets({ context }: { context: PluginPageProps["context tone: "info", action: { label: "Go", - href: hostPath(context.companyPrefix, "/dashboard"), + href: hostNavigation.resolveHref("/dashboard"), }, })} > @@ -1079,6 +1075,7 @@ function KitchenSinkCompanyCrudDemo({ context }: { context: PluginPageProps["con } function KitchenSinkTopRow({ context }: { context: PluginPageProps["context"] }) { + const hostNavigation = useHostNavigation(); return (
    The company sidebar entry opens this route directly, so the plugin feels like a first-class company page instead of a settings subpage.
    - - {pluginPagePath(context.companyPrefix)} + + {hostNavigation.resolveHref(`/${PAGE_ROUTE}`)}
    @@ -1193,6 +1190,7 @@ function KitchenSinkStorageDemo({ context }: { context: PluginPageProps["context } function KitchenSinkHostIntegrationDemo({ context }: { context: PluginPageProps["context"] }) { + const hostNavigation = useHostNavigation(); const [liveRuns, setLiveRuns] = useState([]); const [recentRuns, setRecentRuns] = useState([]); const [loading, setLoading] = useState(false); @@ -1228,7 +1226,7 @@ function KitchenSinkHostIntegrationDemo({ context }: { context: PluginPageProps[
    Company Route - +
    This page is mounted as a real company route instead of living only under `/plugins/:pluginId`. @@ -1260,7 +1258,7 @@ function KitchenSinkHostIntegrationDemo({ context }: { context: PluginPageProps[
    {run.id}
    {run.agentId ? ( - + Open run ) : null} @@ -1294,6 +1292,44 @@ function KitchenSinkHostIntegrationDemo({ context }: { context: PluginPageProps[ ); } +function KitchenSinkSharedPickerDemo({ context }: { context: PluginPageProps["context"] }) { + const [assigneeValue, setAssigneeValue] = useState(""); + const [projectId, setProjectId] = useState(context.projectId ?? ""); + + useEffect(() => { + setProjectId(context.projectId ?? ""); + }, [context.projectId]); + + return ( +
    +
    + These controls are imported from `@paperclipai/plugin-sdk/ui` and reuse the host's assignee and project pickers from the new issue pane. +
    + {!context.companyId ? ( +
    Select a company to load picker options.
    + ) : ( +
    +
    + setAssigneeValue(value)} + /> + +
    +
    + Selected assignee: {assigneeValue || "none"}, selected project: {projectId || "none"} +
    +
    + )} +
    + ); +} + function KitchenSinkEmbeddedApp({ context }: { context: PluginPageProps["context"] }) { return (
    @@ -1301,12 +1337,14 @@ function KitchenSinkEmbeddedApp({ context }: { context: PluginPageProps["context +
    ); } function KitchenSinkConsole({ context }: { context: { companyId: string | null; companyPrefix?: string | null; projectId?: string | null; entityId?: string | null; entityType?: string | null } }) { + const hostNavigation = useHostNavigation(); const companyId = context.companyId; const overview = usePluginOverview(companyId); const [companiesLimit, setCompaniesLimit] = useState(20); @@ -1531,10 +1569,10 @@ function KitchenSinkConsole({ context }: { context: { companyId: string | null;
    - Open page + Open page
    ); } export function KitchenSinkProjectSidebarItem({ context }: PluginProjectSidebarItemProps) { + const hostNavigation = useHostNavigation(); const config = usePluginConfigData(); if (config.data && config.data.showProjectSidebarItem === false) return null; return ( Kitchen Sink diff --git a/packages/plugins/sdk/README.md b/packages/plugins/sdk/README.md index faadc501..c1f73d98 100644 --- a/packages/plugins/sdk/README.md +++ b/packages/plugins/sdk/README.md @@ -15,7 +15,7 @@ Reference: `doc/plugins/PLUGIN_SPEC.md` | Import | Purpose | |--------|--------| | `@paperclipai/plugin-sdk` | Worker entry: `definePlugin`, `runWorker`, context types, protocol helpers | -| `@paperclipai/plugin-sdk/ui` | UI entry: `usePluginData`, `usePluginAction`, `usePluginStream`, `useHostContext`, slot prop types | +| `@paperclipai/plugin-sdk/ui` | UI entry: `usePluginData`, `usePluginAction`, `usePluginStream`, `useHostContext`, `useHostNavigation`, slot prop types | | `@paperclipai/plugin-sdk/ui/hooks` | Hooks only | | `@paperclipai/plugin-sdk/ui/types` | UI types and slot prop interfaces | | `@paperclipai/plugin-sdk/testing` | `createTestHarness` for unit/integration tests | @@ -47,7 +47,7 @@ The SDK is stable enough for local development and first-party examples, but the - For deployed plugins, publish an npm package and install that package into the Paperclip instance at runtime. - The current host runtime expects a writable filesystem, `npm` available at runtime, and network access to the package registry used for plugin installation. - Dynamic plugin install is currently best suited to single-node persistent deployments. Multi-instance cloud deployments still need a shared artifact/distribution model before runtime installs are reliable across nodes. -- The host does not currently ship a real shared React component kit for plugins. Build your plugin UI with ordinary React components and CSS. +- The host ships a small shared React component kit through `@paperclipai/plugin-sdk/ui`. Use it for native Paperclip controls; custom React and CSS are still supported. - `ctx.assets` is not part of the supported runtime in this build. Do not depend on asset upload/read APIs yet. If you are authoring a plugin for others to deploy, treat npm-packaged installation as the supported path and treat repo-local example installs as a development convenience. @@ -100,12 +100,14 @@ runWorker(plugin, import.meta.url); | `onValidateConfig?(config)` | Optional. Return `{ ok, warnings?, errors? }` for settings UI / Test Connection. | | `onWebhook?(input)` | Optional. Handle `POST /api/plugins/:pluginId/webhooks/:endpointKey`; required if webhooks declared. | -**Context (`ctx`) in setup:** `config`, `events`, `jobs`, `launchers`, `http`, `secrets`, `activity`, `state`, `entities`, `projects`, `companies`, `issues`, `agents`, `goals`, `data`, `actions`, `streams`, `tools`, `metrics`, `logger`, `manifest`. Worker-side host APIs are capability-gated; declare capabilities in the manifest. +**Context (`ctx`) in setup:** `config`, `localFolders`, `events`, `jobs`, `launchers`, `http`, `secrets`, `activity`, `state`, `entities`, `projects`, `companies`, `issues`, `agents`, `goals`, `data`, `actions`, `streams`, `tools`, `metrics`, `logger`, `manifest`. Worker-side host APIs are capability-gated; declare capabilities in the manifest. **Agents:** `ctx.agents.invoke(agentId, companyId, opts)` for one-shot invocation. `ctx.agents.sessions` for two-way chat: `create`, `list`, `sendMessage` (with streaming `onEvent` callback), `close`. See the [Plugin Authoring Guide](../../doc/plugins/PLUGIN_AUTHORING_GUIDE.md#agent-sessions-two-way-chat) for details. **Jobs:** Declare in `manifest.jobs` with `jobKey`, `displayName`, `schedule` (cron). Register handler with `ctx.jobs.register(jobKey, fn)`. **Webhooks:** Declare in `manifest.webhooks` with `endpointKey`; handle in `onWebhook(input)`. **State:** `ctx.state.get/set/delete(scopeKey)`; scope kinds: `instance`, `company`, `project`, `project_workspace`, `agent`, `issue`, `goal`, `run`. +**Trusted local folders:** Declare `manifest.localFolders[]` and the `local.folders` capability when a plugin needs an operator-configured company-scoped folder. Use `ctx.localFolders.configure()`, `status()`, `readText()`, and `writeTextAtomic()` instead of resolving arbitrary filesystem paths yourself. The host validates absolute roots, read/write access, required relative folders/files, traversal attempts, symlink escapes, and writes through temp-file-plus-rename atomic replacement. + ## Events Subscribe in `setup` with `ctx.events.on(name, handler)` or `ctx.events.on(name, filter, handler)`. Emit plugin-scoped events with `ctx.events.emit(name, companyId, payload)` (requires `events.emit`). @@ -201,12 +203,13 @@ Slots are mount points for plugin React components. Launchers are host-rendered ### Slot types / launcher placement zones -The same set of values is used as **slot types** (where a component mounts) and **launcher placement zones** (where a launcher can appear). Hierarchy: +Slot types describe where a component mounts. Most values also exist as launcher placement zones. | Slot type / placement zone | Scope | Entity types (when context-sensitive) | |----------------------------|-------|---------------------------------------| | `page` | Global | — | | `sidebar` | Global | — | +| `routeSidebar` | Global | — | | `sidebarPanel` | Global | — | | `settingsPage` | Global | — | | `dashboardWidget` | Global | — | @@ -233,6 +236,10 @@ A full-page extension mounted at `/plugins/:pluginId` (global) or `/:company/plu Adds a navigation-style entry to the main company sidebar navigation area, rendered alongside the core nav items (Dashboard, Issues, Goals, etc.). Use this for lightweight, always-visible links or status indicators that feel native to the sidebar. Receives `PluginSidebarProps` with `context.companyId` set to the active company. Requires the `ui.sidebar.register` capability. +#### `routeSidebar` + +Replaces the normal company sidebar while the current route is a plugin page route with the same `routePath`. Use this for full-page plugin workspaces that need their own local navigation while keeping the company rail and account footer. Receives `PluginRouteSidebarProps` with `context.companyId` and `context.companyPrefix` set to the active company. Requires the `ui.sidebar.register` capability. + #### `sidebarPanel` Renders richer inline content in a dedicated panel area below the company sidebar navigation sections. Use this for mini-widgets, summary cards, quick-action panels, or at-a-glance status views that need more vertical space than a nav link. Receives `context.companyId` set to the active company via `useHostContext()`. Requires the `ui.sidebar.register` capability. @@ -338,6 +345,7 @@ Declare in `manifest.capabilities`. Grouped by scope: | | `http.outbound` | | | `secrets.read-ref` | | | `environment.drivers.register` | +| | `local.folders` | | **Agent** | `agent.tools.register` | | | `agents.invoke` | | | `agent.sessions.create` | @@ -372,6 +380,38 @@ only inside the plugin namespace. Runtime `ctx.db.query()` allows `SELECT` from `ctx.db.execute()` allows `INSERT`, `UPDATE`, and `DELETE` only against the plugin namespace. +### Trusted Local Folders + +Trusted local plugins can request operator-configured folders per company: + +```ts +export const manifest = { + // ... + capabilities: ["local.folders"], + localFolders: [ + { + folderKey: "content-root", + displayName: "Content root", + access: "readWrite", + requiredDirectories: ["sources", "pages"], + requiredFiles: ["schema.md"], + }, + ], +}; +``` + +The host stores the selected path in company-scoped plugin settings and exposes +readiness through: + +- `GET /api/plugins/:pluginId/companies/:companyId/local-folders` +- `GET /api/plugins/:pluginId/companies/:companyId/local-folders/:folderKey/status` +- `POST /api/plugins/:pluginId/companies/:companyId/local-folders/:folderKey/validate` +- `PUT /api/plugins/:pluginId/companies/:companyId/local-folders/:folderKey` + +Worker code should access files through `ctx.localFolders.readText()` and +`ctx.localFolders.writeTextAtomic()`. Relative paths must stay inside the +configured root; symlinks that escape the root are rejected. + ### Scoped API Routes Manifest-declared `apiRoutes` expose JSON routes under @@ -599,6 +639,23 @@ export function IssueLinearLink({ context }: PluginDetailTabProps) { } ``` +#### `useHostNavigation()` + +Routes Paperclip-internal plugin links through the host router without a full document reload. Use `linkProps()` for anchors so the browser still gets a real `href` for copy-link, modifier-click, middle-click, and open-in-new-tab behavior. + +```tsx +import { useHostNavigation } from "@paperclipai/plugin-sdk/ui"; + +export function WikiSidebarLink() { + const hostNavigation = useHostNavigation(); + return Wiki; +} +``` + +`linkProps("/wiki")` resolves against the active company prefix, so in company `PAP` it renders `href="/PAP/wiki"`. Already-prefixed paths such as `/PAP/wiki` are not prefixed again. For button-style commands, call `hostNavigation.navigate("/issues/PAP-123")`. + +Avoid raw same-origin `href`s or `window.location.assign()` for Paperclip-internal navigation from plugin UI. Those bypass the host router and can reload the whole app. External links should keep normal anchors with `target="_blank"` and `rel="noopener noreferrer"` as appropriate. + #### `usePluginStream(channel, options?)` Subscribes to a real-time event stream pushed from the plugin worker via SSE. The worker pushes events using `ctx.streams.emit(channel, event)` and the hook receives them as they arrive. Returns `{ events, lastEvent, connecting, connected, error, close }`. @@ -629,7 +686,118 @@ The SSE connection targets `GET /api/plugins/:pluginId/bridge/stream/:channel?co ### UI authoring note -The current host does **not** provide a real shared component library to plugins yet. Use normal React components, your own CSS, or your own small design primitives inside the plugin package. +The host provides selected shared UI components through `@paperclipai/plugin-sdk/ui`. +Plugins can also use normal React components, their own CSS, or small design +primitives inside the plugin package. + +Use the shared components when the plugin needs to look and behave like a native +Paperclip surface: + +| Component | Use when | +|---|---| +| `MarkdownBlock` | Rendering markdown from plugin or host data | +| `MarkdownEditor` | Editing markdown with the host editor treatment | +| `FileTree` | Showing serializable workspace/wiki/import paths | +| `IssuesList` | Embedding a company-scoped native issue list | +| `AssigneePicker` | Selecting an agent or board user with the same picker as the new issue pane | +| `ProjectPicker` | Selecting a project with the same picker as the new issue pane | +| `ManagedRoutinesList` | Showing plugin-managed routines in settings UI | + +#### Shared Markdown Components + +Plugin UI can render markdown and edit markdown using the same host components +used by Paperclip issue comments and documents: + +```tsx +import { MarkdownBlock, MarkdownEditor } from "@paperclipai/plugin-sdk/ui"; + +export function WikiPageEditor() { + const [body, setBody] = useState("# Wiki page"); + + return ( + <> + + + + ); +} +``` + +`MarkdownBlock` can opt into Obsidian-style wikilinks when a plugin owns the +target URL shape: + +```tsx + +``` + +#### Shared FileTree + +Plugin UI can render the host file tree without importing host internals: + +```tsx +import { FileTree, type FileTreeNode } from "@paperclipai/plugin-sdk/ui"; + +const nodes: FileTreeNode[] = [ + { name: "AGENTS.md", path: "AGENTS.md", kind: "file", children: [] }, + { + name: "wiki", + path: "wiki", + kind: "dir", + children: [ + { name: "index.md", path: "wiki/index.md", kind: "file", children: [] }, + ], + }, +]; + +export function WikiFiles() { + return ( + console.log("toggle", path)} + onSelectFile={(path) => console.log("select", path)} + /> + ); +} +``` + +#### Shared Assignee and Project Pickers + +Use `AssigneePicker` and `ProjectPicker` when a plugin needs to create, filter, +or configure work against Paperclip entities. Both are controlled components and +load their options from the host for the provided company. + +```tsx +import { AssigneePicker, ProjectPicker } from "@paperclipai/plugin-sdk/ui"; + +export function AssignmentControls({ companyId }: { companyId: string }) { + const [assignee, setAssignee] = useState(""); + const [projectId, setProjectId] = useState(""); + + return ( + <> + { + setAssignee(value); + console.log(selection.assigneeAgentId, selection.assigneeUserId); + }} + /> + + + ); +} +``` ### Slot component props @@ -639,6 +807,7 @@ Each slot type receives a typed props object with `context: PluginHostContext`. |-----------|----------------|------------------| | `page` | `PluginPageProps` | — | | `sidebar` | `PluginSidebarProps` | — | +| `routeSidebar` | `PluginRouteSidebarProps` | — | | `settingsPage` | `PluginSettingsPageProps` | — | | `dashboardWidget` | `PluginWidgetProps` | — | | `globalToolbarButton` | `PluginGlobalToolbarButtonProps` | — | @@ -741,14 +910,17 @@ Plugins can add a link under each project in the sidebar via the `projectSidebar Minimal React component that links to the project’s plugin tab (see project detail tabs in the spec): ```tsx -import type { PluginProjectSidebarItemProps } from "@paperclipai/plugin-sdk/ui"; +import { + useHostNavigation, + type PluginProjectSidebarItemProps, +} from "@paperclipai/plugin-sdk/ui"; export function FilesLink({ context }: PluginProjectSidebarItemProps) { + const hostNavigation = useHostNavigation(); const projectId = context.entityId; - const prefix = context.companyPrefix ? `/${context.companyPrefix}` : ""; const projectRef = projectId; // or resolve from host; entityId is project id return ( - + Files ); diff --git a/packages/plugins/sdk/src/bundlers.ts b/packages/plugins/sdk/src/bundlers.ts index b17989e6..e761489c 100644 --- a/packages/plugins/sdk/src/bundlers.ts +++ b/packages/plugins/sdk/src/bundlers.ts @@ -89,11 +89,12 @@ export function createPluginBundlerPresets(input: PluginBundlerPresetInput = {}) const esbuildManifest: EsbuildLikeOptions = { entryPoints: [manifestEntry], outdir, - bundle: false, + bundle: true, format: "esm", platform: "node", target: "node20", sourcemap, + external: ["@paperclipai/plugin-sdk"], }; const esbuildUi = uiEntry diff --git a/packages/plugins/sdk/src/host-client-factory.ts b/packages/plugins/sdk/src/host-client-factory.ts index 8e1af813..13f0a592 100644 --- a/packages/plugins/sdk/src/host-client-factory.ts +++ b/packages/plugins/sdk/src/host-client-factory.ts @@ -90,6 +90,16 @@ export interface HostServices { get(): Promise>; }; + /** Provides trusted company-scoped local folder helpers. */ + localFolders: { + declarations(params: WorkerToHostMethods["localFolders.declarations"][0]): Promise; + configure(params: WorkerToHostMethods["localFolders.configure"][0]): Promise; + status(params: WorkerToHostMethods["localFolders.status"][0]): Promise; + list(params: WorkerToHostMethods["localFolders.list"][0]): Promise; + readText(params: WorkerToHostMethods["localFolders.readText"][0]): Promise; + writeTextAtomic(params: WorkerToHostMethods["localFolders.writeTextAtomic"][0]): Promise; + }; + /** Provides `state.get`, `state.set`, `state.delete`. */ state: { get(params: WorkerToHostMethods["state.get"][0]): Promise; @@ -165,6 +175,18 @@ export interface HostServices { listWorkspaces(params: WorkerToHostMethods["projects.listWorkspaces"][0]): Promise; getPrimaryWorkspace(params: WorkerToHostMethods["projects.getPrimaryWorkspace"][0]): Promise; getWorkspaceForIssue(params: WorkerToHostMethods["projects.getWorkspaceForIssue"][0]): Promise; + getManaged(params: WorkerToHostMethods["projects.managed.get"][0]): Promise; + reconcileManaged(params: WorkerToHostMethods["projects.managed.reconcile"][0]): Promise; + resetManaged(params: WorkerToHostMethods["projects.managed.reset"][0]): Promise; + }; + + /** Provides `routines.managed.*`. */ + routines: { + managedGet(params: WorkerToHostMethods["routines.managed.get"][0]): Promise; + managedReconcile(params: WorkerToHostMethods["routines.managed.reconcile"][0]): Promise; + managedReset(params: WorkerToHostMethods["routines.managed.reset"][0]): Promise; + managedUpdate(params: WorkerToHostMethods["routines.managed.update"][0]): Promise; + managedRun(params: WorkerToHostMethods["routines.managed.run"][0]): Promise; }; /** Provides issue read/write, relation, checkout, wakeup, summary, comment methods. */ @@ -202,6 +224,9 @@ export interface HostServices { pause(params: WorkerToHostMethods["agents.pause"][0]): Promise; resume(params: WorkerToHostMethods["agents.resume"][0]): Promise; invoke(params: WorkerToHostMethods["agents.invoke"][0]): Promise; + managedGet(params: WorkerToHostMethods["agents.managed.get"][0]): Promise; + managedReconcile(params: WorkerToHostMethods["agents.managed.reconcile"][0]): Promise; + managedReset(params: WorkerToHostMethods["agents.managed.reset"][0]): Promise; }; /** Provides `agents.sessions.create`, `agents.sessions.list`, `agents.sessions.sendMessage`, `agents.sessions.close`. */ @@ -281,6 +306,14 @@ const METHOD_CAPABILITY_MAP: Record { + return services.localFolders.declarations(params); + }), + "localFolders.configure": gated("localFolders.configure", async (params) => { + return services.localFolders.configure(params); + }), + "localFolders.status": gated("localFolders.status", async (params) => { + return services.localFolders.status(params); + }), + "localFolders.list": gated("localFolders.list", async (params) => { + return services.localFolders.list(params); + }), + "localFolders.readText": gated("localFolders.readText", async (params) => { + return services.localFolders.readText(params); + }), + "localFolders.writeTextAtomic": gated("localFolders.writeTextAtomic", async (params) => { + return services.localFolders.writeTextAtomic(params); + }), + // State "state.get": gated("state.get", async (params) => { return services.state.get(params); @@ -530,6 +593,32 @@ export function createHostClientHandlers( "projects.getWorkspaceForIssue": gated("projects.getWorkspaceForIssue", async (params) => { return services.projects.getWorkspaceForIssue(params); }), + "projects.managed.get": gated("projects.managed.get", async (params) => { + return services.projects.getManaged(params); + }), + "projects.managed.reconcile": gated("projects.managed.reconcile", async (params) => { + return services.projects.reconcileManaged(params); + }), + "projects.managed.reset": gated("projects.managed.reset", async (params) => { + return services.projects.resetManaged(params); + }), + + // Routines + "routines.managed.get": gated("routines.managed.get", async (params) => { + return services.routines.managedGet(params); + }), + "routines.managed.reconcile": gated("routines.managed.reconcile", async (params) => { + return services.routines.managedReconcile(params); + }), + "routines.managed.reset": gated("routines.managed.reset", async (params) => { + return services.routines.managedReset(params); + }), + "routines.managed.update": gated("routines.managed.update", async (params) => { + return services.routines.managedUpdate(params); + }), + "routines.managed.run": gated("routines.managed.run", async (params) => { + return services.routines.managedRun(params); + }), // Issues "issues.list": gated("issues.list", async (params) => { @@ -611,6 +700,15 @@ export function createHostClientHandlers( "agents.invoke": gated("agents.invoke", async (params) => { return services.agents.invoke(params); }), + "agents.managed.get": gated("agents.managed.get", async (params) => { + return services.agents.managedGet(params); + }), + "agents.managed.reconcile": gated("agents.managed.reconcile", async (params) => { + return services.agents.managedReconcile(params); + }), + "agents.managed.reset": gated("agents.managed.reset", async (params) => { + return services.agents.managedReset(params); + }), // Agent Sessions "agents.sessions.create": gated("agents.sessions.create", async (params) => { diff --git a/packages/plugins/sdk/src/index.ts b/packages/plugins/sdk/src/index.ts index 92b60bbf..3d0bd07e 100644 --- a/packages/plugins/sdk/src/index.ts +++ b/packages/plugins/sdk/src/index.ts @@ -180,6 +180,13 @@ export type { export type { PluginContext, PluginConfigClient, + PluginLocalFolderProblem, + PluginLocalFolderStatus, + PluginLocalFolderConfigureInput, + PluginLocalFolderListOptions, + PluginLocalFolderEntry, + PluginLocalFolderListing, + PluginLocalFoldersClient, PluginEventsClient, PluginJobsClient, PluginLaunchersClient, @@ -255,6 +262,14 @@ export type { PluginWebhookDeclaration, PluginToolDeclaration, PluginEnvironmentDriverDeclaration, + PluginManagedAgentDeclaration, + PluginManagedAgentResolution, + PluginManagedProjectDeclaration, + PluginManagedProjectResolution, + PluginManagedRoutineDeclaration, + PluginManagedRoutineResolution, + PluginManagedResourceKind, + PluginManagedResourceRef, PluginUiSlotDeclaration, PluginUiDeclaration, PluginLauncherActionDeclaration, @@ -264,6 +279,8 @@ export type { PluginDatabaseDeclaration, PluginApiRouteCompanyResolution, PluginApiRouteDeclaration, + PluginLocalFolderDeclaration, + PluginCompanySettings, PluginRecord, PluginDatabaseNamespaceRecord, PluginMigrationRecord, diff --git a/packages/plugins/sdk/src/protocol.ts b/packages/plugins/sdk/src/protocol.ts index 19566570..6961c929 100644 --- a/packages/plugins/sdk/src/protocol.ts +++ b/packages/plugins/sdk/src/protocol.ts @@ -29,8 +29,14 @@ import type { IssueDocumentSummary, IssueThreadInteraction, CreateIssueThreadInteraction, + PluginManagedAgentResolution, + PluginManagedProjectResolution, + PluginManagedRoutineResolution, + Routine, + RoutineRun, Agent, Goal, + PluginLocalFolderDeclaration, } from "@paperclipai/shared"; export type { PluginLauncherRenderContextSnapshot } from "@paperclipai/shared"; @@ -46,6 +52,8 @@ import type { PluginWorkspace, ToolRunContext, ToolResult, + PluginLocalFolderListing, + PluginLocalFolderStatus, } from "./types.js"; import type { PluginHealthDiagnostics, @@ -566,6 +574,44 @@ export interface WorkerToHostMethods { // Config "config.get": [params: Record, result: Record]; + // Trusted local folders + "localFolders.declarations": [ + params: Record, + result: PluginLocalFolderDeclaration[], + ]; + "localFolders.configure": [ + params: { + companyId: string; + folderKey: string; + path: string; + access?: "read" | "readWrite"; + requiredDirectories?: string[]; + requiredFiles?: string[]; + }, + result: PluginLocalFolderStatus, + ]; + "localFolders.status": [ + params: { companyId: string; folderKey: string }, + result: PluginLocalFolderStatus, + ]; + "localFolders.list": [ + params: { companyId: string; folderKey: string; relativePath?: string | null; recursive?: boolean; maxEntries?: number }, + result: PluginLocalFolderListing, + ]; + "localFolders.readText": [ + params: { companyId: string; folderKey: string; relativePath: string }, + result: string, + ]; + "localFolders.writeTextAtomic": [ + params: { + companyId: string; + folderKey: string; + relativePath: string; + contents: string; + }, + result: PluginLocalFolderStatus, + ]; + // State "state.get": [ params: { scopeKind: string; scopeId?: string; namespace?: string; stateKey: string }, @@ -724,6 +770,57 @@ export interface WorkerToHostMethods { params: { issueId: string; companyId: string }, result: PluginWorkspace | null, ]; + "projects.managed.get": [ + params: { projectKey: string; companyId: string }, + result: PluginManagedProjectResolution, + ]; + "projects.managed.reconcile": [ + params: { projectKey: string; companyId: string }, + result: PluginManagedProjectResolution, + ]; + "projects.managed.reset": [ + params: { projectKey: string; companyId: string }, + result: PluginManagedProjectResolution, + ]; + "routines.managed.get": [ + params: { routineKey: string; companyId: string }, + result: PluginManagedRoutineResolution, + ]; + "routines.managed.reconcile": [ + params: { + routineKey: string; + companyId: string; + assigneeAgentId?: string | null; + projectId?: string | null; + }, + result: PluginManagedRoutineResolution, + ]; + "routines.managed.reset": [ + params: { + routineKey: string; + companyId: string; + assigneeAgentId?: string | null; + projectId?: string | null; + }, + result: PluginManagedRoutineResolution, + ]; + "routines.managed.update": [ + params: { + routineKey: string; + companyId: string; + status?: string; + }, + result: Routine, + ]; + "routines.managed.run": [ + params: { + routineKey: string; + companyId: string; + assigneeAgentId?: string | null; + projectId?: string | null; + }, + result: RoutineRun, + ]; // Issues "issues.list": [ @@ -732,8 +829,10 @@ export interface WorkerToHostMethods { projectId?: string; assigneeAgentId?: string; originKind?: string; + originKindPrefix?: string; originId?: string; status?: string; + includePluginOperations?: boolean; limit?: number; offset?: number; }, @@ -758,6 +857,7 @@ export interface WorkerToHostMethods { assigneeUserId?: string | null; requestDepth?: number; billingCode?: string | null; + surfaceVisibility?: string | null; originKind?: string | null; originId?: string | null; originRunId?: string | null; @@ -940,6 +1040,18 @@ export interface WorkerToHostMethods { params: { agentId: string; companyId: string; prompt: string; reason?: string }, result: { runId: string }, ]; + "agents.managed.get": [ + params: { agentKey: string; companyId: string }, + result: PluginManagedAgentResolution, + ]; + "agents.managed.reconcile": [ + params: { agentKey: string; companyId: string }, + result: PluginManagedAgentResolution, + ]; + "agents.managed.reset": [ + params: { agentKey: string; companyId: string }, + result: PluginManagedAgentResolution, + ]; // Agent Sessions "agents.sessions.create": [ diff --git a/packages/plugins/sdk/src/testing.ts b/packages/plugins/sdk/src/testing.ts index 16efee05..5ed1c1c4 100644 --- a/packages/plugins/sdk/src/testing.ts +++ b/packages/plugins/sdk/src/testing.ts @@ -1,11 +1,16 @@ import { randomUUID } from "node:crypto"; +import { pluginOperationIssueOriginKind } from "@paperclipai/shared"; import type { PaperclipPluginManifestV1, PluginCapability, PluginEventType, PluginIssueOriginKind, + PluginManagedAgentResolution, + PluginManagedRoutineResolution, Company, Project, + Routine, + RoutineRun, Issue, IssueComment, IssueThreadInteraction, @@ -419,6 +424,8 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { const entityExternalIndex = new Map(); const companies = new Map(); const projects = new Map(); + const routines = new Map(); + const routineRuns = new Map(); const issues = new Map(); const blockedByIssueIds = new Map(); const issueComments = new Map(); @@ -465,6 +472,53 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { } const defaultPluginOriginKind: PluginIssueOriginKind = `plugin:${manifest.id}`; + + function managedAgentDeclaration(agentKey: string) { + const declaration = manifest.agents?.find((agent) => agent.agentKey === agentKey); + if (!declaration) throw new Error(`Managed agent declaration not found: ${agentKey}`); + return declaration; + } + + function isManagedAgent(agent: Agent, agentKey: string) { + const marker = agent.metadata?.paperclipManagedResource; + return Boolean( + marker + && typeof marker === "object" + && !Array.isArray(marker) + && (marker as Record).pluginKey === manifest.id + && (marker as Record).resourceKind === "agent" + && (marker as Record).resourceKey === agentKey, + ); + } + + function managedAgentMetadata(agentKey: string, existing?: Record | null) { + return { + ...(existing ?? {}), + paperclipManagedResource: { + pluginKey: manifest.id, + resourceKind: "agent", + resourceKey: agentKey, + }, + }; + } + + function managedResolution( + agentKey: string, + companyId: string, + agent: Agent | null, + status: PluginManagedAgentResolution["status"], + ): PluginManagedAgentResolution { + return { + pluginKey: manifest.id, + resourceKind: "agent", + resourceKey: agentKey, + companyId, + agentId: agent?.id ?? null, + agent, + status, + approvalId: null, + }; + } function normalizePluginOriginKind(originKind: unknown = defaultPluginOriginKind): PluginIssueOriginKind { if (originKind == null || originKind === "") return defaultPluginOriginKind; if (typeof originKind !== "string") throw new Error("Plugin issue originKind must be a string"); @@ -481,6 +535,81 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { return { ...currentConfig }; }, }, + localFolders: { + declarations() { + return manifest.localFolders ?? []; + }, + async configure(input) { + requireCapability(manifest, capabilitySet, "local.folders"); + return { + folderKey: input.folderKey, + configured: true, + path: input.path, + realPath: input.path, + access: input.access ?? "readWrite", + readable: true, + writable: input.access === "read" ? false : true, + requiredDirectories: input.requiredDirectories ?? [], + requiredFiles: input.requiredFiles ?? [], + missingDirectories: [], + missingFiles: [], + healthy: true, + problems: [], + checkedAt: new Date().toISOString(), + }; + }, + async status(_companyId, folderKey) { + requireCapability(manifest, capabilitySet, "local.folders"); + return { + folderKey, + configured: false, + path: null, + realPath: null, + access: "readWrite", + readable: false, + writable: false, + requiredDirectories: [], + requiredFiles: [], + missingDirectories: [], + missingFiles: [], + healthy: false, + problems: [{ code: "not_configured", message: "No local folder path is configured." }], + checkedAt: new Date().toISOString(), + }; + }, + async list(_companyId, folderKey, options) { + requireCapability(manifest, capabilitySet, "local.folders"); + return { + folderKey, + relativePath: options?.relativePath ?? null, + entries: [], + truncated: false, + }; + }, + async readText() { + requireCapability(manifest, capabilitySet, "local.folders"); + throw new Error("Test harness local folder readText is not implemented"); + }, + async writeTextAtomic(_companyId, folderKey) { + requireCapability(manifest, capabilitySet, "local.folders"); + return { + folderKey, + configured: false, + path: null, + realPath: null, + access: "readWrite", + readable: false, + writable: false, + requiredDirectories: [], + requiredFiles: [], + missingDirectories: [], + missingFiles: [], + healthy: false, + problems: [{ code: "not_configured", message: "No local folder path is configured." }], + checkedAt: new Date().toISOString(), + }; + }, + }, events: { on(name: PluginEventType | `plugin.${string}`, filterOrFn: EventFilter | ((event: PluginEvent) => Promise), maybeFn?: (event: PluginEvent) => Promise): () => void { requireCapability(manifest, capabilitySet, "events.subscribe"); @@ -647,6 +776,314 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { const workspaces = projectWorkspaces.get(projectId) ?? []; return workspaces.find((workspace) => workspace.isPrimary) ?? null; }, + managed: { + async get(projectKey, companyId) { + requireCapability(manifest, capabilitySet, "projects.managed"); + const declaration = manifest.projects?.find((project) => project.projectKey === projectKey); + if (!declaration) { + return { + pluginKey: manifest.id, + resourceKind: "project", + resourceKey: projectKey, + companyId, + projectId: null, + project: null, + status: "missing", + }; + } + const externalId = `${manifest.id}:project:${projectKey}`; + const existingEntity = [...entities.values()].find((entity) => + entity.entityType === "managed_resource" + && entity.scopeKind === "company" + && entity.scopeId === companyId + && entity.externalId === externalId + ); + const existingProject = existingEntity ? projects.get(String(existingEntity.data?.projectId ?? "")) : null; + if (existingProject && isInCompany(existingProject, companyId)) { + return { + pluginKey: manifest.id, + resourceKind: "project", + resourceKey: projectKey, + companyId, + projectId: existingProject.id, + project: existingProject, + status: "resolved", + }; + } + const now = new Date(); + const project = { + id: `project-${projects.size + 1}`, + companyId, + urlKey: declaration.projectKey, + goalId: null, + goalIds: [], + goals: [], + name: declaration.displayName, + description: declaration.description ?? null, + status: declaration.status ?? "in_progress", + leadAgentId: null, + targetDate: null, + color: declaration.color ?? null, + env: null, + pauseReason: null, + pausedAt: null, + executionWorkspacePolicy: null, + codebase: { + workspaceId: null, + repoUrl: null, + repoRef: null, + defaultRef: null, + repoName: null, + localFolder: null, + managedFolder: `/tmp/${declaration.projectKey}`, + effectiveLocalFolder: `/tmp/${declaration.projectKey}`, + origin: "managed_checkout", + }, + workspaces: [], + primaryWorkspace: null, + managedByPlugin: { + id: `managed-${projects.size + 1}`, + pluginId: manifest.id, + pluginKey: manifest.id, + pluginDisplayName: manifest.displayName, + resourceKind: "project", + resourceKey: projectKey, + defaultsJson: { displayName: declaration.displayName, settings: declaration.settings ?? {} }, + createdAt: now, + updatedAt: now, + }, + archivedAt: null, + createdAt: now, + updatedAt: now, + } as Project; + projects.set(project.id, project); + const externalKey = `managed_resource|company|${companyId}|${externalId}`; + const nowIso = now.toISOString(); + const record: PluginEntityRecord = { + id: randomUUID(), + entityType: "managed_resource", + scopeKind: "company", + scopeId: companyId, + externalId, + title: declaration.displayName, + status: null, + data: { resourceKind: "project", resourceKey: projectKey, projectId: project.id }, + createdAt: nowIso, + updatedAt: nowIso, + }; + entities.set(record.id, record); + entityExternalIndex.set(externalKey, record.id); + return { + pluginKey: manifest.id, + resourceKind: "project", + resourceKey: projectKey, + companyId, + projectId: project.id, + project, + status: "created", + }; + }, + async reconcile(projectKey, companyId) { + return this.get(projectKey, companyId); + }, + async reset(projectKey, companyId) { + const resolved = await this.get(projectKey, companyId); + return { ...resolved, status: resolved.project ? "reset" : resolved.status }; + }, + }, + }, + routines: { + managed: { + async get(routineKey, companyId) { + requireCapability(manifest, capabilitySet, "routines.managed"); + const declaration = manifest.routines?.find((routine) => routine.routineKey === routineKey); + if (!declaration) { + return { + pluginKey: manifest.id, + resourceKind: "routine", + resourceKey: routineKey, + companyId, + routineId: null, + routine: null, + status: "missing", + missingRefs: [], + } satisfies PluginManagedRoutineResolution; + } + const externalId = `${manifest.id}:routine:${routineKey}`; + const existingEntity = [...entities.values()].find((entity) => + entity.entityType === "managed_resource" + && entity.scopeKind === "company" + && entity.scopeId === companyId + && entity.externalId === externalId + ); + const existingRoutine = existingEntity ? routines.get(String(existingEntity.data?.routineId ?? "")) : null; + if (existingRoutine && isInCompany(existingRoutine, companyId)) { + return { + pluginKey: manifest.id, + resourceKind: "routine", + resourceKey: routineKey, + companyId, + routineId: existingRoutine.id, + routine: existingRoutine, + status: "resolved", + missingRefs: [], + } satisfies PluginManagedRoutineResolution; + } + return { + pluginKey: manifest.id, + resourceKind: "routine", + resourceKey: routineKey, + companyId, + routineId: null, + routine: null, + status: "missing", + missingRefs: [], + } satisfies PluginManagedRoutineResolution; + }, + async reconcile(routineKey, companyId, overrides) { + const existing = await this.get(routineKey, companyId); + if (existing.routine) return existing; + const declaration = manifest.routines?.find((routine) => routine.routineKey === routineKey); + if (!declaration) return existing; + const now = new Date(); + const agentRef = declaration.assigneeRef; + const projectRef = declaration.projectRef; + const assigneeAgentId = overrides?.assigneeAgentId + ?? (agentRef?.resourceKind === "agent" + ? [...agents.values()].find((agent) => isInCompany(agent, companyId) && isManagedAgent(agent, agentRef.resourceKey))?.id + : null) + ?? null; + const projectId = overrides?.projectId + ?? (projectRef?.resourceKind === "project" + ? [...projects.values()].find((project) => ( + isInCompany(project, companyId) + && project.managedByPlugin?.pluginKey === manifest.id + && project.managedByPlugin?.resourceKey === projectRef.resourceKey + ))?.id + : null) + ?? null; + const missingRefs: NonNullable = []; + if (agentRef && !assigneeAgentId) missingRefs.push({ ...agentRef, pluginKey: manifest.id }); + if (projectRef && !projectId) missingRefs.push({ ...projectRef, pluginKey: manifest.id }); + if (missingRefs.length > 0) { + return { + pluginKey: manifest.id, + resourceKind: "routine", + resourceKey: routineKey, + companyId, + routineId: null, + routine: null, + status: "missing_refs", + missingRefs, + } satisfies PluginManagedRoutineResolution; + } + const routine = { + id: `routine-${routines.size + 1}`, + companyId, + projectId, + goalId: declaration.goalId ?? null, + parentIssueId: null, + title: declaration.title, + description: declaration.description ?? null, + assigneeAgentId, + priority: declaration.priority ?? "medium", + status: declaration.status ?? (assigneeAgentId ? "active" : "paused"), + concurrencyPolicy: declaration.concurrencyPolicy ?? "coalesce_if_active", + catchUpPolicy: declaration.catchUpPolicy ?? "skip_missed", + variables: declaration.variables ?? [], + createdByAgentId: null, + createdByUserId: null, + updatedByAgentId: null, + updatedByUserId: null, + lastTriggeredAt: null, + lastEnqueuedAt: null, + createdAt: now, + updatedAt: now, + managedByPlugin: { + id: `managed-routine-${routines.size + 1}`, + pluginId: manifest.id, + pluginKey: manifest.id, + pluginDisplayName: manifest.displayName, + resourceKind: "routine", + resourceKey: routineKey, + defaultsJson: { title: declaration.title, issueTemplate: declaration.issueTemplate ?? null }, + createdAt: now, + updatedAt: now, + }, + } as Routine; + routines.set(routine.id, routine); + const nowIso = now.toISOString(); + const record: PluginEntityRecord = { + id: randomUUID(), + entityType: "managed_resource", + scopeKind: "company", + scopeId: companyId, + externalId: `${manifest.id}:routine:${routineKey}`, + title: declaration.title, + status: null, + data: { resourceKind: "routine", resourceKey: routineKey, routineId: routine.id }, + createdAt: nowIso, + updatedAt: nowIso, + }; + entities.set(record.id, record); + return { + pluginKey: manifest.id, + resourceKind: "routine", + resourceKey: routineKey, + companyId, + routineId: routine.id, + routine, + status: "created", + missingRefs: [], + } satisfies PluginManagedRoutineResolution; + }, + async reset(routineKey, companyId, overrides) { + const resolved = await this.reconcile(routineKey, companyId, overrides); + return { ...resolved, status: resolved.routine ? "reset" : resolved.status } satisfies PluginManagedRoutineResolution; + }, + async update(routineKey, companyId, patch) { + const resolved = await this.get(routineKey, companyId); + if (!resolved.routine) throw new Error(`Managed routine not found: ${routineKey}`); + const next = { + ...resolved.routine, + ...(patch.status !== undefined ? { status: patch.status } : {}), + updatedAt: new Date(), + }; + routines.set(next.id, next); + return next; + }, + async run(routineKey, companyId) { + const resolved = await this.get(routineKey, companyId); + if (!resolved.routine) throw new Error(`Managed routine not found: ${routineKey}`); + const now = new Date(); + const run = { + id: `routine-run-${routineRuns.size + 1}`, + companyId, + routineId: resolved.routine.id, + triggerId: null, + source: "manual", + status: "queued", + triggeredAt: now, + idempotencyKey: null, + triggerPayload: null, + dispatchFingerprint: null, + linkedIssueId: null, + coalescedIntoRunId: null, + failureReason: null, + completedAt: null, + createdAt: now, + updatedAt: now, + } satisfies RoutineRun; + routineRuns.set(run.id, run); + routines.set(resolved.routine.id, { + ...resolved.routine, + lastTriggeredAt: now, + lastEnqueuedAt: now, + updatedAt: now, + }); + return run; + }, + }, }, companies: { async list(input) { @@ -673,6 +1110,12 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { if (input.originKind.startsWith("plugin:")) normalizePluginOriginKind(input.originKind); out = out.filter((issue) => issue.originKind === input.originKind); } + if (input?.originKindPrefix) { + const prefix = input.originKindPrefix; + out = out.filter((issue) => + typeof issue.originKind === "string" && issue.originKind.startsWith(prefix), + ); + } if (input?.originId) out = out.filter((issue) => issue.originId === input.originId); if (input?.status) out = out.filter((issue) => issue.status === input.status); if (input?.offset) out = out.slice(input.offset); @@ -687,6 +1130,11 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { async create(input) { requireCapability(manifest, capabilitySet, "issues.create"); const now = new Date(); + const originKind = normalizePluginOriginKind( + input.surfaceVisibility === "plugin_operation" && !input.originKind + ? pluginOperationIssueOriginKind(manifest.id) + : input.originKind, + ); const record: Issue = { id: randomUUID(), companyId: input.companyId, @@ -708,7 +1156,7 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { createdByUserId: null, issueNumber: null, identifier: null, - originKind: normalizePluginOriginKind(input.originKind), + originKind, originId: input.originId ?? null, originRunId: input.originRunId ?? null, requestDepth: input.requestDepth ?? 0, @@ -1064,6 +1512,115 @@ export function createTestHarness(options: TestHarnessOptions): TestHarness { } return { runId: randomUUID() }; }, + managed: { + async get(agentKey, companyId) { + requireCapability(manifest, capabilitySet, "agents.managed"); + const cid = requireCompanyId(companyId); + managedAgentDeclaration(agentKey); + const agent = [...agents.values()].find((candidate) => + candidate.companyId === cid && + candidate.status !== "terminated" && + isManagedAgent(candidate, agentKey), + ) ?? null; + return managedResolution(agentKey, cid, agent, agent ? "resolved" : "missing"); + }, + async reconcile(agentKey, companyId) { + requireCapability(manifest, capabilitySet, "agents.managed"); + const cid = requireCompanyId(companyId); + const declaration = managedAgentDeclaration(agentKey); + const existingAgent = [...agents.values()].find((candidate) => + candidate.companyId === cid && + candidate.status !== "terminated" && + isManagedAgent(candidate, agentKey), + ) ?? null; + const existing = managedResolution(agentKey, cid, existingAgent, existingAgent ? "resolved" : "missing"); + if (existing.agent) return existing; + const now = new Date(); + const created: Agent = { + id: randomUUID(), + companyId: cid, + name: declaration.displayName, + urlKey: declaration.displayName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""), + role: (declaration.role ?? "general") as Agent["role"], + title: declaration.title ?? null, + icon: declaration.icon ?? null, + status: declaration.status ?? "idle", + reportsTo: null, + capabilities: declaration.capabilities ?? null, + adapterType: (declaration.adapterType ?? "process") as Agent["adapterType"], + adapterConfig: declaration.adapterConfig ?? {}, + runtimeConfig: declaration.runtimeConfig ?? {}, + budgetMonthlyCents: declaration.budgetMonthlyCents ?? 0, + spentMonthlyCents: 0, + pauseReason: null, + pausedAt: null, + permissions: { canCreateAgents: Boolean(declaration.permissions?.canCreateAgents) }, + lastHeartbeatAt: null, + metadata: managedAgentMetadata(agentKey), + createdAt: now, + updatedAt: now, + }; + agents.set(created.id, created); + return managedResolution(agentKey, cid, created, "created"); + }, + async reset(agentKey, companyId) { + requireCapability(manifest, capabilitySet, "agents.managed"); + const cid = requireCompanyId(companyId); + const declaration = managedAgentDeclaration(agentKey); + let agent = [...agents.values()].find((candidate) => + candidate.companyId === cid && + candidate.status !== "terminated" && + isManagedAgent(candidate, agentKey), + ) ?? null; + if (!agent) { + const now = new Date(); + agent = { + id: randomUUID(), + companyId: cid, + name: declaration.displayName, + urlKey: declaration.displayName.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, ""), + role: (declaration.role ?? "general") as Agent["role"], + title: declaration.title ?? null, + icon: declaration.icon ?? null, + status: declaration.status ?? "idle", + reportsTo: null, + capabilities: declaration.capabilities ?? null, + adapterType: (declaration.adapterType ?? "process") as Agent["adapterType"], + adapterConfig: declaration.adapterConfig ?? {}, + runtimeConfig: declaration.runtimeConfig ?? {}, + budgetMonthlyCents: declaration.budgetMonthlyCents ?? 0, + spentMonthlyCents: 0, + pauseReason: null, + pausedAt: null, + permissions: { canCreateAgents: Boolean(declaration.permissions?.canCreateAgents) }, + lastHeartbeatAt: null, + metadata: managedAgentMetadata(agentKey), + createdAt: now, + updatedAt: now, + }; + agents.set(agent.id, agent); + } + const resolved = managedResolution(agentKey, cid, agent, "resolved"); + if (!resolved.agent) return resolved; + const updated: Agent = { + ...resolved.agent, + name: declaration.displayName, + role: (declaration.role ?? "general") as Agent["role"], + title: declaration.title ?? null, + icon: declaration.icon ?? null, + capabilities: declaration.capabilities ?? null, + adapterType: (declaration.adapterType ?? "process") as Agent["adapterType"], + adapterConfig: declaration.adapterConfig ?? {}, + runtimeConfig: declaration.runtimeConfig ?? {}, + budgetMonthlyCents: declaration.budgetMonthlyCents ?? 0, + permissions: { canCreateAgents: Boolean(declaration.permissions?.canCreateAgents) }, + metadata: managedAgentMetadata(agentKey, resolved.agent.metadata), + updatedAt: new Date(), + }; + agents.set(updated.id, updated); + return managedResolution(agentKey, cid, updated, "reset"); + }, + }, sessions: { async create(agentId, companyId, opts) { requireCapability(manifest, capabilitySet, "agent.sessions.create"); diff --git a/packages/plugins/sdk/src/types.ts b/packages/plugins/sdk/src/types.ts index 599a06ec..f4a946f9 100644 --- a/packages/plugins/sdk/src/types.ts +++ b/packages/plugins/sdk/src/types.ts @@ -28,6 +28,12 @@ import type { RequestConfirmationInteraction, CreateIssueThreadInteraction, PluginIssueOriginKind, + IssueSurfaceVisibility, + PluginManagedAgentResolution, + PluginManagedProjectResolution, + PluginManagedRoutineResolution, + Routine, + RoutineRun, Agent, Goal, } from "@paperclipai/shared"; @@ -42,6 +48,18 @@ export type { PluginWebhookDeclaration, PluginToolDeclaration, PluginEnvironmentDriverDeclaration, + PluginManagedAgentDeclaration, + PluginManagedAgentResolution, + PluginManagedProjectDeclaration, + PluginManagedProjectResolution, + PluginManagedRoutineDeclaration, + PluginManagedRoutineResolution, + Routine, + RoutineRun, + PluginLocalFolderDeclaration, + PluginCompanySettings, + PluginManagedResourceKind, + PluginManagedResourceRef, PluginUiSlotDeclaration, PluginUiDeclaration, PluginLauncherActionDeclaration, @@ -92,6 +110,7 @@ export type { RequestConfirmationInteraction, CreateIssueThreadInteraction, PluginIssueOriginKind, + IssueSurfaceVisibility, Agent, Goal, } from "@paperclipai/shared"; @@ -349,6 +368,90 @@ export interface PluginConfigClient { get(): Promise>; } +export interface PluginLocalFolderProblem { + code: + | "not_configured" + | "not_absolute" + | "missing" + | "not_directory" + | "not_readable" + | "not_writable" + | "missing_directory" + | "missing_file" + | "path_traversal" + | "symlink_escape" + | "atomic_write_failed"; + message: string; + path?: string; +} + +export interface PluginLocalFolderStatus { + folderKey: string; + configured: boolean; + path: string | null; + realPath: string | null; + access: "read" | "readWrite"; + readable: boolean; + writable: boolean; + requiredDirectories: string[]; + requiredFiles: string[]; + missingDirectories: string[]; + missingFiles: string[]; + healthy: boolean; + problems: PluginLocalFolderProblem[]; + checkedAt: string; +} + +export interface PluginLocalFolderConfigureInput { + companyId: string; + folderKey: string; + path: string; + access?: "read" | "readWrite"; + requiredDirectories?: string[]; + requiredFiles?: string[]; +} + +export interface PluginLocalFolderListOptions { + relativePath?: string | null; + recursive?: boolean; + maxEntries?: number; +} + +export interface PluginLocalFolderEntry { + path: string; + name: string; + kind: "file" | "directory"; + size: number | null; + modifiedAt: string | null; +} + +export interface PluginLocalFolderListing { + folderKey: string; + relativePath: string | null; + entries: PluginLocalFolderEntry[]; + truncated: boolean; +} + +export interface PluginLocalFoldersClient { + /** Manifest-declared local folders for this plugin. */ + declarations(): import("@paperclipai/shared").PluginLocalFolderDeclaration[]; + /** Persist a company-scoped local folder path after validating it. */ + configure(input: PluginLocalFolderConfigureInput): Promise; + /** Check the stored folder readiness for a company and folder key. */ + status(companyId: string, folderKey: string): Promise; + /** List entries below a configured folder after containment checks. */ + list(companyId: string, folderKey: string, options?: PluginLocalFolderListOptions): Promise; + /** Read a UTF-8 text file below a configured folder after containment checks. */ + readText(companyId: string, folderKey: string, relativePath: string): Promise; + /** Write a UTF-8 text file below a configured folder using atomic rename. */ + writeTextAtomic( + companyId: string, + folderKey: string, + relativePath: string, + contents: string, + ): Promise; +} + /** * `ctx.events` — subscribe to and emit Paperclip domain events. * @@ -697,6 +800,44 @@ export interface PluginProjectsClient { * @see PLUGIN_SPEC.md §20 — Local Tooling */ getWorkspaceForIssue(issueId: string, companyId: string): Promise; + + /** Resolve and reconcile manifest-declared plugin-managed projects by stable key. Requires `projects.managed`. */ + managed: { + get(projectKey: string, companyId: string): Promise; + reconcile(projectKey: string, companyId: string): Promise; + reset(projectKey: string, companyId: string): Promise; + }; +} + +/** + * `ctx.routines` — resolve and reconcile plugin-managed Paperclip routines. + * + * Requires `routines.managed` capability. + */ +export interface PluginRoutinesClient { + managed: { + get(routineKey: string, companyId: string): Promise; + reconcile( + routineKey: string, + companyId: string, + overrides?: { assigneeAgentId?: string | null; projectId?: string | null }, + ): Promise; + reset( + routineKey: string, + companyId: string, + overrides?: { assigneeAgentId?: string | null; projectId?: string | null }, + ): Promise; + update( + routineKey: string, + companyId: string, + patch: { status?: string }, + ): Promise; + run( + routineKey: string, + companyId: string, + overrides?: { assigneeAgentId?: string | null; projectId?: string | null }, + ): Promise; + }; } /** @@ -1099,8 +1240,10 @@ export interface PluginIssuesClient { projectId?: string; assigneeAgentId?: string; originKind?: PluginIssueOriginKind; + originKindPrefix?: string; originId?: string; status?: Issue["status"]; + includePluginOperations?: boolean; limit?: number; offset?: number; }): Promise; @@ -1119,6 +1262,7 @@ export interface PluginIssuesClient { assigneeUserId?: string | null; requestDepth?: number; billingCode?: string | null; + surfaceVisibility?: IssueSurfaceVisibility; originKind?: PluginIssueOriginKind; originId?: string | null; originRunId?: string | null; @@ -1241,6 +1385,12 @@ export interface PluginAgentsClient { resume(agentId: string, companyId: string): Promise; /** Invoke (wake up) an agent with a prompt payload. Throws if paused, terminated, pending_approval, or not found. Requires `agents.invoke`. */ invoke(agentId: string, companyId: string, opts: { prompt: string; reason?: string }): Promise<{ runId: string }>; + /** Resolve and reconcile manifest-declared plugin-managed agents by stable key. Requires `agents.managed`. */ + managed: { + get(agentKey: string, companyId: string): Promise; + reconcile(agentKey: string, companyId: string): Promise; + reset(agentKey: string, companyId: string): Promise; + }; /** Create, message, and close agent chat sessions. Requires `agent.sessions.*` capabilities. */ sessions: PluginAgentSessionsClient; } @@ -1436,6 +1586,9 @@ export interface PluginContext { /** Read resolved operator configuration. */ config: PluginConfigClient; + /** Configure and safely access trusted company-scoped local folders. */ + localFolders: PluginLocalFoldersClient; + /** Subscribe to and emit domain events. Requires `events.subscribe` / `events.emit`. */ events: PluginEventsClient; @@ -1466,6 +1619,9 @@ export interface PluginContext { /** Read project and workspace metadata. Requires `projects.read` / `project.workspaces.read`. */ projects: PluginProjectsClient; + /** Resolve and reconcile plugin-managed routines. Requires `routines.managed`. */ + routines: PluginRoutinesClient; + /** Read company metadata. Requires `companies.read`. */ companies: PluginCompaniesClient; diff --git a/packages/plugins/sdk/src/ui/components.ts b/packages/plugins/sdk/src/ui/components.ts index b93c1db4..c148fc80 100644 --- a/packages/plugins/sdk/src/ui/components.ts +++ b/packages/plugins/sdk/src/ui/components.ts @@ -125,6 +125,36 @@ export interface TimeseriesChartProps { export interface MarkdownBlockProps { /** Markdown content to render. */ content: string; + /** Optional CSS class name forwarded to the host renderer. */ + className?: string; + /** Opt into Obsidian-style [[target]] / [[target|label]] wikilinks. */ + enableWikiLinks?: boolean; + /** Base href used for wikilinks when no resolver is supplied. */ + wikiLinkRoot?: string; + /** Optional href resolver for wikilinks. Return null to leave a token as plain text. */ + resolveWikiLinkHref?: (target: string, label: string) => string | null | undefined; +} + +/** Props for `MarkdownEditor`. */ +export interface MarkdownEditorProps { + /** Markdown source controlled by the plugin. */ + value: string; + /** Called whenever the markdown source changes. */ + onChange: (value: string) => void; + /** Placeholder text shown when the document is empty. */ + placeholder?: string; + /** Optional wrapper CSS class name. */ + className?: string; + /** Optional editable content CSS class name. */ + contentClassName?: string; + /** Called when the editor loses focus. */ + onBlur?: () => void; + /** Render the editor with a host border treatment. */ + bordered?: boolean; + /** Render the rich editor without allowing edits. */ + readOnly?: boolean; + /** Called on Cmd/Ctrl+Enter. */ + onSubmit?: () => void; } /** A single key-value pair for `KeyValueList`. */ @@ -217,6 +247,211 @@ export interface ErrorBoundaryProps { onError?: (error: Error, info: React.ErrorInfo) => void; } +/** File or directory node rendered by `FileTree`. */ +export interface FileTreeNode { + /** Display name for this path segment. */ + name: string; + /** Slash-separated path relative to the tree root. */ + path: string; + /** Whether this node is a directory or file. */ + kind: "dir" | "file"; + /** Child nodes. Files should use an empty array. */ + children: FileTreeNode[]; + /** Optional stable action metadata for host/plugin workflows. */ + action?: string | null; +} + +/** Badge status variants supported by `FileTree`. */ +export type FileTreeBadgeVariant = "ok" | "warning" | "error" | "info" | "pending"; + +/** Serializable badge metadata keyed by file path. */ +export interface FileTreeBadge { + label: string; + status: FileTreeBadgeVariant; + tooltip?: string; +} + +/** Row tone variants supported by `FileTree`. */ +export type FileTreeTone = "default" | "warning" | "error" | "muted"; + +/** Empty-state content shown when a tree has no nodes. */ +export interface FileTreeEmptyState { + title?: string; + description?: string; +} + +/** Error-state content shown when a tree cannot be loaded. */ +export interface FileTreeErrorState { + message: string; + retry?: () => void; +} + +/** Accepted path collection shape for expanded and checked file tree state. */ +export type FileTreePathCollection = ReadonlySet | readonly string[]; + +/** Props for `FileTree`. */ +export interface FileTreeProps { + /** Tree nodes to render. */ + nodes: FileTreeNode[]; + /** Currently selected file path. */ + selectedFile?: string | null; + /** Expanded directory paths. */ + expandedPaths?: FileTreePathCollection; + /** Checked file paths. */ + checkedPaths?: FileTreePathCollection; + /** Called when a directory row is toggled. */ + onToggleDir?: (path: string) => void; + /** Called when a file row is selected. */ + onSelectFile?: (path: string) => void; + /** Called when a checkbox is toggled. */ + onToggleCheck?: (path: string, kind: "file" | "dir") => void; + /** Badge metadata keyed by path. */ + fileBadges?: Record; + /** Row tone metadata keyed by path. */ + fileTones?: Record; + /** Whether to render checkboxes. Defaults to false for plugin UIs. */ + showCheckboxes?: boolean; + /** Allow long file and directory names to wrap. */ + wrapLabels?: boolean; + /** Render a loading skeleton instead of nodes. */ + loading?: boolean; + /** Render a structured error state instead of nodes. */ + error?: FileTreeErrorState | null; + /** Empty state content. */ + empty?: FileTreeEmptyState; + /** Accessible label for the tree. */ + ariaLabel?: string; +} + +export interface IssuesListFilters { + status?: string; + projectId?: string; + parentId?: string; + assigneeAgentId?: string; + participantAgentId?: string; + assigneeUserId?: string; + labelId?: string; + workspaceId?: string; + executionWorkspaceId?: string; + originKind?: string; + originKindPrefix?: string; + originId?: string; + descendantOf?: string; + includeRoutineExecutions?: boolean; +} + +export interface IssuesListProps { + companyId: string | null; + projectId?: string | null; + filters?: IssuesListFilters; + viewStateKey?: string; + initialSearch?: string; + createIssueLabel?: string; + searchWithinLoadedIssues?: boolean; +} + +export interface AssigneePickerSelection { + assigneeAgentId: string | null; + assigneeUserId: string | null; +} + +export interface AssigneePickerProps { + /** Company whose agents and users should be listed. Defaults to host context. */ + companyId?: string | null; + /** Controlled value. Use `agent:`, `user:`, or an empty string. */ + value: string; + /** Called with the encoded value plus parsed assignee IDs. */ + onChange: (value: string, selection: AssigneePickerSelection) => void; + /** Button placeholder when no assignee is selected. */ + placeholder?: string; + /** Label for the empty option. */ + noneLabel?: string; + /** Search input placeholder. */ + searchPlaceholder?: string; + /** Empty search result message. */ + emptyMessage?: string; + /** Include active board users alongside agents. Defaults to true. */ + includeUsers?: boolean; + /** Include terminated agents. Defaults to false. */ + includeTerminatedAgents?: boolean; + /** CSS class forwarded to the trigger button. */ + className?: string; + /** Called after the user confirms a selection with Enter, Tab, or click. */ + onConfirm?: () => void; +} + +export interface ProjectPickerProps { + /** Company whose projects should be listed. Defaults to host context. */ + companyId?: string | null; + /** Controlled project id, or an empty string for no project. */ + value: string; + /** Called with the selected project id. Empty string means no project. */ + onChange: (projectId: string) => void; + /** Button placeholder when no project is selected. */ + placeholder?: string; + /** Label for the empty option. */ + noneLabel?: string; + /** Search input placeholder. */ + searchPlaceholder?: string; + /** Empty search result message. */ + emptyMessage?: string; + /** Include archived projects. Defaults to false. */ + includeArchived?: boolean; + /** CSS class forwarded to the trigger button. */ + className?: string; + /** Called after the user confirms a selection with Enter, Tab, or click. */ + onConfirm?: () => void; +} + +export interface ManagedRoutinesListAgent { + id: string; + name: string; + icon?: string | null; +} + +export interface ManagedRoutinesListProject { + id: string; + name: string; + color?: string | null; +} + +export interface ManagedRoutineMissingRef { + resourceKind: string; + resourceKey: string; +} + +export interface ManagedRoutinesListItem { + key: string; + title: string; + status: string; + routineId?: string | null; + href?: string | null; + resourceKey?: string | null; + projectId?: string | null; + assigneeAgentId?: string | null; + cronExpression?: string | null; + lastRunAt?: Date | string | null; + lastRunStatus?: string | null; + managedByPluginDisplayName?: string | null; + missingRefs?: ManagedRoutineMissingRef[]; +} + +export interface ManagedRoutinesListProps { + routines: ManagedRoutinesListItem[]; + agents?: ManagedRoutinesListAgent[]; + projects?: ManagedRoutinesListProject[]; + pluginDisplayName?: string | null; + emptyMessage?: string; + runningRoutineKey?: string | null; + statusMutationRoutineKey?: string | null; + reconcilingRoutineKey?: string | null; + resettingRoutineKey?: string | null; + onRunNow?: (routine: ManagedRoutinesListItem) => void; + onToggleEnabled?: (routine: ManagedRoutinesListItem, enabled: boolean) => void; + onReconcile?: (routine: ManagedRoutinesListItem) => void; + onReset?: (routine: ManagedRoutinesListItem) => void; +} + // --------------------------------------------------------------------------- // Component declarations (provided by host at runtime) // --------------------------------------------------------------------------- @@ -266,6 +501,13 @@ export const TimeseriesChart = createSdkUiComponent("Times */ export const MarkdownBlock = createSdkUiComponent("MarkdownBlock"); +/** + * Renders Paperclip's shared Markdown editor. + * + * @see PLUGIN_SPEC.md §19.6 — Shared Components + */ +export const MarkdownEditor = createSdkUiComponent("MarkdownEditor"); + /** * Renders a definition-list of label/value pairs. * @@ -308,3 +550,40 @@ export const Spinner = createSdkUiComponent("Spinner"); * @see PLUGIN_SPEC.md §19.7 — Error Propagation Through The Bridge */ export const ErrorBoundary = createSdkUiComponent("ErrorBoundary"); + +/** + * Renders the host file tree component with a stable plugin-safe prop surface. + * + * @example + * ```tsx + * import { FileTree, type FileTreeNode } from "@paperclipai/plugin-sdk/ui"; + * + * const nodes: FileTreeNode[] = [ + * { name: "README.md", path: "README.md", kind: "file", children: [] }, + * ]; + * + * console.log(path)} />; + * ``` + */ +export const FileTree = createSdkUiComponent("FileTree"); + +/** + * Renders Paperclip's native issue list component for company-scoped plugin + * pages that need a standard board issue view. + */ +export const IssuesList = createSdkUiComponent("IssuesList"); + +/** + * Renders the same host assignee picker used by the new issue pane. + */ +export const AssigneePicker = createSdkUiComponent("AssigneePicker"); + +/** + * Renders the same host project picker used by the new issue pane. + */ +export const ProjectPicker = createSdkUiComponent("ProjectPicker"); + +/** + * Renders Paperclip's native managed routines list for plugin settings pages. + */ +export const ManagedRoutinesList = createSdkUiComponent("ManagedRoutinesList"); diff --git a/packages/plugins/sdk/src/ui/hooks.ts b/packages/plugins/sdk/src/ui/hooks.ts index 56a0938b..686d02cf 100644 --- a/packages/plugins/sdk/src/ui/hooks.ts +++ b/packages/plugins/sdk/src/ui/hooks.ts @@ -1,6 +1,8 @@ import type { PluginDataResult, PluginActionFn, + HostLocation, + HostNavigation, PluginHostContext, PluginStreamResult, PluginToastFn, @@ -115,6 +117,57 @@ export function useHostContext(): PluginHostContext { return impl(); } +// --------------------------------------------------------------------------- +// useHostNavigation +// --------------------------------------------------------------------------- + +/** + * Navigate within the Paperclip host without forcing a full document reload. + * + * Use `linkProps()` for links so browser-native behavior still works: + * modifier-click, middle-click, copy-link, and open-in-new-tab all use the + * returned real `href`. + * + * @example + * ```tsx + * function WikiSidebarLink() { + * const hostNavigation = useHostNavigation(); + * return Wiki; + * } + * ``` + */ +export function useHostNavigation(): HostNavigation { + const impl = getSdkUiRuntimeValue<() => HostNavigation>("useHostNavigation"); + return impl(); +} + +// --------------------------------------------------------------------------- +// useHostLocation +// --------------------------------------------------------------------------- + +/** + * Observe the current host router location. + * + * Returns a snapshot of the active `pathname`, `search`, and `hash`. The + * component re-renders when any of these change (e.g. after the host router + * pushes a new entry, or after the browser back/forward gestures). Use this + * for URL-driven plugin UI such as a takeover sidebar with section-aware + * active state. + * + * @example + * ```tsx + * function WikiSection() { + * const { pathname } = useHostLocation(); + * const section = pathname.split("/").filter(Boolean).at(-1) ?? "wiki"; + * return
    Active section: {section}
    ; + * } + * ``` + */ +export function useHostLocation(): HostLocation { + const impl = getSdkUiRuntimeValue<() => HostLocation>("useHostLocation"); + return impl(); +} + // --------------------------------------------------------------------------- // usePluginStream // --------------------------------------------------------------------------- diff --git a/packages/plugins/sdk/src/ui/index.ts b/packages/plugins/sdk/src/ui/index.ts index 68b8a43b..8f90af70 100644 --- a/packages/plugins/sdk/src/ui/index.ts +++ b/packages/plugins/sdk/src/ui/index.ts @@ -43,20 +43,89 @@ * - `usePluginData(key, params)` — fetch data from the worker's `getData` handler * - `usePluginAction(key)` — get a callable that invokes the worker's `performAction` handler * - `useHostContext()` — read the current active company, project, entity, and user IDs + * - `useHostNavigation()` — navigate Paperclip-internal links through the host router + * - `useHostLocation()` — observe the current host pathname/search/hash for URL-driven UI * - `usePluginStream(channel)` — subscribe to real-time SSE events from the worker */ export { usePluginData, usePluginAction, useHostContext, + useHostNavigation, + useHostLocation, usePluginStream, usePluginToast, } from "./hooks.js"; +export { + MetricCard, + StatusBadge, + DataTable, + TimeseriesChart, + MarkdownBlock, + MarkdownEditor, + KeyValueList, + ActionBar, + LogView, + JsonTree, + Spinner, + ErrorBoundary, + FileTree, + IssuesList, + AssigneePicker, + ProjectPicker, + ManagedRoutinesList, +} from "./components.js"; + +export type { + MetricTrend, + MetricCardProps, + StatusBadgeVariant, + StatusBadgeProps, + DataTableColumn, + DataTableProps, + TimeseriesDataPoint, + TimeseriesChartProps, + MarkdownBlockProps, + MarkdownEditorProps, + KeyValuePair, + KeyValueListProps, + ActionBarItem, + ActionBarProps, + LogViewEntry, + LogViewProps, + JsonTreeProps, + SpinnerProps, + ErrorBoundaryProps, + FileTreeNode, + FileTreeBadgeVariant, + FileTreeBadge, + FileTreeTone, + FileTreeEmptyState, + FileTreeErrorState, + FileTreePathCollection, + FileTreeProps, + IssuesListFilters, + IssuesListProps, + AssigneePickerSelection, + AssigneePickerProps, + ProjectPickerProps, + ManagedRoutineMissingRef, + ManagedRoutinesListAgent, + ManagedRoutinesListItem, + ManagedRoutinesListProject, + ManagedRoutinesListProps, +} from "./components.js"; + // Bridge error and host context types export type { PluginBridgeError, PluginBridgeErrorCode, + HostNavigation, + HostNavigationOptions, + HostNavigationLinkOptions, + HostNavigationLinkProps, + HostLocation, PluginHostContext, PluginModalBoundsRequest, PluginRenderCloseEvent, @@ -80,6 +149,7 @@ export type { PluginWidgetProps, PluginDetailTabProps, PluginSidebarProps, + PluginRouteSidebarProps, PluginProjectSidebarItemProps, PluginCommentAnnotationProps, PluginCommentContextMenuItemProps, diff --git a/packages/plugins/sdk/src/ui/types.ts b/packages/plugins/sdk/src/ui/types.ts index b1eddea5..b8216836 100644 --- a/packages/plugins/sdk/src/ui/types.ts +++ b/packages/plugins/sdk/src/ui/types.ts @@ -14,6 +14,10 @@ * @see PLUGIN_SPEC.md §29.2 — SDK Versioning */ +import type { + AnchorHTMLAttributes, + MouseEvent as ReactMouseEvent, +} from "react"; import type { PluginBridgeErrorCode, PluginLauncherBounds, @@ -131,6 +135,83 @@ export interface PluginRenderEnvironmentContext closeLifecycle?: PluginRenderCloseLifecycle | null; } +// --------------------------------------------------------------------------- +// Host navigation +// --------------------------------------------------------------------------- + +/** + * Options for host-managed Paperclip navigation from plugin UI. + */ +export interface HostNavigationOptions { + /** Replace the current history entry instead of pushing a new one. */ + replace?: boolean; + /** Optional state forwarded to the host router. */ + state?: unknown; +} + +/** + * Options for `useHostNavigation().linkProps()`. + */ +export interface HostNavigationLinkOptions extends HostNavigationOptions { + /** Standard anchor target. Non-`_self` targets are not intercepted. */ + target?: AnchorHTMLAttributes["target"]; + /** Standard anchor rel attribute. */ + rel?: AnchorHTMLAttributes["rel"]; +} + +/** + * Anchor props returned by `useHostNavigation().linkProps()`. + * + * The `href` is always real so browser affordances such as copy-link, + * modifier-click, middle-click, and open-in-new-tab continue to work. + */ +export interface HostNavigationLinkProps + extends Pick, "href" | "target" | "rel"> { + onClick: (event: ReactMouseEvent) => void; +} + +/** + * Snapshot of the host router location, exposed to plugin UI through + * `useHostLocation()`. Mirrors the relevant subset of `Location` from + * `react-router-dom` so plugins can react to URL changes without importing + * router internals. + * + * @see PLUGIN_SPEC.md §19 — UI Extension Model + */ +export interface HostLocation { + /** Current pathname, e.g. `/PAP/wiki`. */ + pathname: string; + /** Current search string, e.g. `?tab=config` (includes the leading `?`). */ + search: string; + /** Current hash, e.g. `#document-plan` (includes the leading `#`). */ + hash: string; + /** Optional state forwarded by the host router for same-tab SPA navigation. */ + state?: unknown; +} + +/** + * Host-managed navigation helpers for plugin UI. + */ +export interface HostNavigation { + /** + * Resolve a Paperclip-internal path using the active company prefix. + * + * For example, in company `PAP`, `resolveHref("/wiki")` returns + * `"/PAP/wiki"`, while `resolveHref("/PAP/wiki")` stays unchanged. + */ + resolveHref(to: string): string; + /** Navigate through the host router without reloading the document. */ + navigate(to: string, options?: HostNavigationOptions): void; + /** + * Build anchor props for host-managed links. + * + * Plain left-clicks are routed through the host SPA router. Browser-native + * link gestures are left alone because the returned props include a real + * `href`. + */ + linkProps(to: string, options?: HostNavigationLinkOptions): HostNavigationLinkProps; +} + // --------------------------------------------------------------------------- // Slot component prop interfaces // --------------------------------------------------------------------------- @@ -188,6 +269,19 @@ export interface PluginSidebarProps { context: PluginHostContext; } +/** + * Props passed to a plugin route sidebar component. + * + * A route sidebar replaces the normal company sidebar while the user is on a + * matching plugin page route declared with the same `routePath`. + * + * @see PLUGIN_SPEC.md §19.5 — Sidebar Entries + */ +export interface PluginRouteSidebarProps { + /** The current host context. */ + context: PluginHostContext; +} + /** * Props passed to a plugin project sidebar item component. * diff --git a/packages/plugins/sdk/src/worker-rpc-host.ts b/packages/plugins/sdk/src/worker-rpc-host.ts index 80e548b5..7683605d 100644 --- a/packages/plugins/sdk/src/worker-rpc-host.ts +++ b/packages/plugins/sdk/src/worker-rpc-host.ts @@ -387,6 +387,51 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost }, }, + localFolders: { + declarations() { + if (!manifest) throw new Error("Plugin context accessed before initialization"); + return manifest.localFolders ?? []; + }, + + async configure(input) { + return callHost("localFolders.configure", { + companyId: input.companyId, + folderKey: input.folderKey, + path: input.path, + access: input.access, + requiredDirectories: input.requiredDirectories, + requiredFiles: input.requiredFiles, + }); + }, + + async status(companyId: string, folderKey: string) { + return callHost("localFolders.status", { companyId, folderKey }); + }, + + async list(companyId: string, folderKey: string, options = {}) { + return callHost("localFolders.list", { + companyId, + folderKey, + relativePath: options.relativePath, + recursive: options.recursive, + maxEntries: options.maxEntries, + }); + }, + + async readText(companyId: string, folderKey: string, relativePath: string) { + return callHost("localFolders.readText", { companyId, folderKey, relativePath }); + }, + + async writeTextAtomic(companyId: string, folderKey: string, relativePath: string, contents: string) { + return callHost("localFolders.writeTextAtomic", { + companyId, + folderKey, + relativePath, + contents, + }); + }, + }, + events: { on( name: string, @@ -580,6 +625,50 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost async getWorkspaceForIssue(issueId: string, companyId: string) { return callHost("projects.getWorkspaceForIssue", { issueId, companyId }); }, + + managed: { + async get(projectKey: string, companyId: string) { + return callHost("projects.managed.get", { projectKey, companyId }); + }, + async reconcile(projectKey: string, companyId: string) { + return callHost("projects.managed.reconcile", { projectKey, companyId }); + }, + async reset(projectKey: string, companyId: string) { + return callHost("projects.managed.reset", { projectKey, companyId }); + }, + }, + }, + + routines: { + managed: { + async get(routineKey: string, companyId: string) { + return callHost("routines.managed.get", { routineKey, companyId }); + }, + async reconcile( + routineKey: string, + companyId: string, + overrides?: { assigneeAgentId?: string | null; projectId?: string | null }, + ) { + return callHost("routines.managed.reconcile", { routineKey, companyId, ...overrides }); + }, + async reset( + routineKey: string, + companyId: string, + overrides?: { assigneeAgentId?: string | null; projectId?: string | null }, + ) { + return callHost("routines.managed.reset", { routineKey, companyId, ...overrides }); + }, + async update(routineKey: string, companyId: string, patch: { status?: string }) { + return callHost("routines.managed.update", { routineKey, companyId, ...patch }); + }, + async run( + routineKey: string, + companyId: string, + overrides?: { assigneeAgentId?: string | null; projectId?: string | null }, + ) { + return callHost("routines.managed.run", { routineKey, companyId, ...overrides }); + }, + }, }, companies: { @@ -602,8 +691,10 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost projectId: input.projectId, assigneeAgentId: input.assigneeAgentId, originKind: input.originKind, + originKindPrefix: input.originKindPrefix, originId: input.originId, status: input.status, + includePluginOperations: input.includePluginOperations, limit: input.limit, offset: input.offset, }); @@ -628,6 +719,7 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost assigneeUserId: input.assigneeUserId, requestDepth: input.requestDepth, billingCode: input.billingCode, + surfaceVisibility: input.surfaceVisibility, originKind: input.originKind, originId: input.originId, originRunId: input.originRunId, @@ -863,6 +955,20 @@ export function startWorkerRpcHost(options: WorkerRpcHostOptions): WorkerRpcHost return callHost("agents.invoke", { agentId, companyId, prompt: opts.prompt, reason: opts.reason }); }, + managed: { + async get(agentKey: string, companyId: string) { + return callHost("agents.managed.get", { agentKey, companyId }); + }, + + async reconcile(agentKey: string, companyId: string) { + return callHost("agents.managed.reconcile", { agentKey, companyId }); + }, + + async reset(agentKey: string, companyId: string) { + return callHost("agents.managed.reset", { agentKey, companyId }); + }, + }, + sessions: { async create(agentId: string, companyId: string, opts?: { taskKey?: string; reason?: string }) { return callHost("agents.sessions.create", { diff --git a/packages/shared/src/constants.ts b/packages/shared/src/constants.ts index 3724d42d..001ccee2 100644 --- a/packages/shared/src/constants.ts +++ b/packages/shared/src/constants.ts @@ -190,6 +190,16 @@ export const ISSUE_ORIGIN_KINDS = [ export type BuiltInIssueOriginKind = (typeof ISSUE_ORIGIN_KINDS)[number]; export type PluginIssueOriginKind = `plugin:${string}`; export type IssueOriginKind = BuiltInIssueOriginKind | PluginIssueOriginKind; +export const ISSUE_SURFACE_VISIBILITIES = ["default", "plugin_operation"] as const; +export type IssueSurfaceVisibility = (typeof ISSUE_SURFACE_VISIBILITIES)[number]; + +export function pluginOperationIssueOriginKind(pluginKey: string): PluginIssueOriginKind { + return `plugin:${pluginKey}:operation`; +} + +export function isPluginOperationIssueOriginKind(originKind: string | null | undefined): boolean { + return typeof originKind === "string" && /^plugin:[^:]+:operation(?::|$)/.test(originKind); +} export const ISSUE_RELATION_TYPES = ["blocks"] as const; export type IssueRelationType = (typeof ISSUE_RELATION_TYPES)[number]; @@ -634,9 +644,12 @@ export const PLUGIN_CAPABILITIES = [ "issue.comments.create", "issue.interactions.create", "issue.documents.write", + "projects.managed", + "routines.managed", "agents.pause", "agents.resume", "agents.invoke", + "agents.managed", "agent.sessions.create", "agent.sessions.list", "agent.sessions.send", @@ -658,6 +671,7 @@ export const PLUGIN_CAPABILITIES = [ "http.outbound", "secrets.read-ref", "environment.drivers.register", + "local.folders", // Agent Tools "agent.tools.register", // UI @@ -728,6 +742,7 @@ export const PLUGIN_UI_SLOT_TYPES = [ "taskDetailView", "dashboardWidget", "sidebar", + "routeSidebar", "sidebarPanel", "projectSidebarItem", "globalToolbarButton", diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 87a298c7..810ac1e6 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -25,6 +25,9 @@ export { ISSUE_THREAD_INTERACTION_STATUSES, ISSUE_THREAD_INTERACTION_CONTINUATION_POLICIES, ISSUE_ORIGIN_KINDS, + ISSUE_SURFACE_VISIBILITIES, + pluginOperationIssueOriginKind, + isPluginOperationIssueOriginKind, ISSUE_RELATION_TYPES, ISSUE_TREE_CONTROL_MODES, ISSUE_TREE_HOLD_RELEASE_POLICY_STRATEGIES, @@ -133,6 +136,7 @@ export { type BuiltInIssueOriginKind, type PluginIssueOriginKind, type IssueOriginKind, + type IssueSurfaceVisibility, type IssueRelationType, type IssueTreeControlMode, type IssueTreeHoldReleasePolicyStrategy, @@ -303,6 +307,7 @@ export type { ProjectCodebase, ProjectCodebaseOrigin, ProjectGoalRef, + ProjectManagedByPlugin, ProjectWorkspace, ExecutionWorkspace, ExecutionWorkspaceSummary, @@ -493,6 +498,7 @@ export type { CompanySecret, SecretProviderDescriptor, Routine, + RoutineManagedByPlugin, RoutineVariable, RoutineVariableDefaultValue, RoutineTrigger, @@ -507,6 +513,15 @@ export type { PluginWebhookDeclaration, PluginToolDeclaration, PluginEnvironmentDriverDeclaration, + PluginManagedAgentDeclaration, + PluginManagedProjectDeclaration, + PluginManagedRoutineDeclaration, + PluginLocalFolderDeclaration, + PluginManagedAgentResolution, + PluginManagedProjectResolution, + PluginManagedRoutineResolution, + PluginManagedResourceKind, + PluginManagedResourceRef, PluginUiSlotDeclaration, PluginLauncherActionDeclaration, PluginLauncherRenderDeclaration, @@ -523,6 +538,7 @@ export type { PluginMigrationRecord, PluginStateRecord, PluginConfig, + PluginCompanySettings, PluginEntityRecord, PluginEntityQuery, PluginJobRecord, diff --git a/packages/shared/src/types/index.ts b/packages/shared/src/types/index.ts index 35eb94c6..1b24f771 100644 --- a/packages/shared/src/types/index.ts +++ b/packages/shared/src/types/index.ts @@ -89,7 +89,7 @@ export type { AdapterEnvironmentTestResult, } from "./agent.js"; export type { AssetImage } from "./asset.js"; -export type { Project, ProjectCodebase, ProjectCodebaseOrigin, ProjectGoalRef, ProjectWorkspace } from "./project.js"; +export type { Project, ProjectCodebase, ProjectCodebaseOrigin, ProjectGoalRef, ProjectManagedByPlugin, ProjectWorkspace } from "./project.js"; export type { ExecutionWorkspace, ExecutionWorkspaceSummary, @@ -221,6 +221,7 @@ export type { } from "./secrets.js"; export type { Routine, + RoutineManagedByPlugin, RoutineVariable, RoutineVariableDefaultValue, RoutineTrigger, @@ -315,6 +316,15 @@ export type { PluginWebhookDeclaration, PluginToolDeclaration, PluginEnvironmentDriverDeclaration, + PluginManagedAgentDeclaration, + PluginManagedProjectDeclaration, + PluginManagedRoutineDeclaration, + PluginLocalFolderDeclaration, + PluginManagedAgentResolution, + PluginManagedProjectResolution, + PluginManagedRoutineResolution, + PluginManagedResourceKind, + PluginManagedResourceRef, PluginUiSlotDeclaration, PluginLauncherActionDeclaration, PluginLauncherRenderDeclaration, @@ -331,6 +341,7 @@ export type { PluginMigrationRecord, PluginStateRecord, PluginConfig, + PluginCompanySettings, PluginEntityRecord, PluginEntityQuery, PluginJobRecord, diff --git a/packages/shared/src/types/plugin.ts b/packages/shared/src/types/plugin.ts index 955b719f..d5231aad 100644 --- a/packages/shared/src/types/plugin.ts +++ b/packages/shared/src/types/plugin.ts @@ -16,7 +16,19 @@ import type { PluginDatabaseMigrationStatus, PluginDatabaseNamespaceMode, PluginDatabaseNamespaceStatus, + AgentAdapterType, + AgentRole, + AgentStatus, + IssuePriority, + ProjectStatus, + RoutineCatchUpPolicy, + RoutineConcurrencyPolicy, + RoutineStatus, + IssueSurfaceVisibility, } from "../constants.js"; +import type { Agent } from "./agent.js"; +import type { Project } from "./project.js"; +import type { Routine, RoutineTrigger, RoutineVariable } from "./routine.js"; // --------------------------------------------------------------------------- // JSON Schema placeholder – plugins declare config schemas as JSON Schema @@ -113,6 +125,162 @@ export interface PluginEnvironmentDriverDeclaration { configSchema: JsonSchema; } +/** + * Declares a normal Paperclip agent that a plugin can provision and later + * resolve by stable key within each company. + */ +export interface PluginManagedAgentDeclaration { + /** Stable identifier for this managed agent, unique within the plugin. */ + agentKey: string; + /** Suggested visible agent name. */ + displayName: string; + /** Optional suggested role. Defaults to `general`. */ + role?: AgentRole | string; + /** Optional suggested title shown in agent surfaces. */ + title?: string | null; + /** Optional icon for agent list/detail surfaces. */ + icon?: string | null; + /** Suggested capability summary for the agent. */ + capabilities?: string | null; + /** Suggested adapter type. Defaults to `process`. */ + adapterType?: AgentAdapterType | string; + /** + * Optional ordered list of compatible adapter types. When present, the host + * prefers the most-used compatible adapter already configured in the company, + * falling back to `adapterType`. + */ + adapterPreference?: Array; + /** Suggested adapter configuration. */ + adapterConfig?: Record; + /** Suggested Paperclip runtime configuration. */ + runtimeConfig?: Record; + /** Suggested permissions object. Normalized by the host on create/reset. */ + permissions?: Record; + /** Suggested starting status when no board approval is required. */ + status?: Extract; + /** Suggested monthly budget in cents. */ + budgetMonthlyCents?: number; + /** Optional managed instructions content or pointer metadata for plugin UI. */ + instructions?: { + entryFile?: string; + content?: string; + assetPath?: string; + }; +} + +/** + * Declares a company-scoped local folder a trusted plugin wants the operator + * to configure. The host treats this as a generic filesystem root: plugin + * code may request required relative folders/files, then use SDK helpers for + * path-safe reads and atomic writes under that root. + */ +export interface PluginLocalFolderDeclaration { + /** Stable identifier for this folder, unique within the plugin. */ + folderKey: string; + /** Human-readable name shown in plugin settings. */ + displayName: string; + /** Optional operator-facing description. */ + description?: string; + /** Access level requested by the plugin. Defaults to `readWrite`. */ + access?: "read" | "readWrite"; + /** Relative directories expected to exist under the configured root. */ + requiredDirectories?: string[]; + /** Relative files expected to exist under the configured root. */ + requiredFiles?: string[]; +} + +/** + * Declares a normal Paperclip project that a plugin can provision and later + * resolve by stable key within each company. + */ +export interface PluginManagedProjectDeclaration { + /** Stable identifier for this managed project, unique within the plugin. */ + projectKey: string; + /** Suggested visible project name. */ + displayName: string; + /** Suggested project description. */ + description?: string | null; + /** Suggested starting status. Defaults to `in_progress`. */ + status?: ProjectStatus; + /** Suggested project color. Defaults to the normal project palette. */ + color?: string | null; + /** Optional plugin-specific defaults retained for reset/reconcile UI. */ + settings?: Record; +} + +export type PluginManagedResourceKind = "agent" | "project" | "routine"; + +export interface PluginManagedResourceRef { + pluginKey?: string; + resourceKind: PluginManagedResourceKind; + resourceKey: string; +} + +export interface PluginManagedRoutineDeclaration { + /** Stable identifier for this managed routine, unique within the plugin. */ + routineKey: string; + /** Suggested routine title template. */ + title: string; + /** Suggested routine description template. */ + description?: string | null; + /** Stable managed agent reference for the default assignee. */ + assigneeRef?: PluginManagedResourceRef | null; + /** Stable managed project reference for routine-created issues. */ + projectRef?: PluginManagedResourceRef | null; + /** Optional goal id to set on the routine in this company. */ + goalId?: string | null; + /** Suggested starting status. Defaults to `paused` when no assignee is resolved, otherwise `active`. */ + status?: RoutineStatus; + /** Suggested issue priority. Defaults to `medium`. */ + priority?: IssuePriority; + /** Suggested concurrency behavior. Defaults to core routine default. */ + concurrencyPolicy?: RoutineConcurrencyPolicy; + /** Suggested missed-trigger behavior. Defaults to core routine default. */ + catchUpPolicy?: RoutineCatchUpPolicy; + /** Suggested routine variables. */ + variables?: RoutineVariable[]; + /** Suggested triggers created when the routine is first reconciled. */ + triggers?: Array>; + /** Defaults for issues created by this routine. */ + issueTemplate?: { + surfaceVisibility?: IssueSurfaceVisibility; + originId?: string | null; + billingCode?: string | null; + }; +} + +export interface PluginManagedAgentResolution { + pluginKey: string; + resourceKind: "agent"; + resourceKey: string; + companyId: string; + agentId: string | null; + agent: Agent | null; + status: "missing" | "resolved" | "created" | "relinked" | "reset"; + approvalId?: string | null; +} + +export interface PluginManagedProjectResolution { + pluginKey: string; + resourceKind: "project"; + resourceKey: string; + companyId: string; + projectId: string | null; + project: Project | null; + status: "missing" | "resolved" | "created" | "relinked" | "reset"; +} + +export interface PluginManagedRoutineResolution { + pluginKey: string; + resourceKind: "routine"; + resourceKey: string; + companyId: string; + routineId: string | null; + routine: Routine | null; + status: "missing" | "missing_refs" | "resolved" | "created" | "relinked" | "reset"; + missingRefs?: PluginManagedResourceRef[]; +} + /** * Declares a UI extension slot the plugin fills with a React component. * @@ -133,7 +301,7 @@ export interface PluginUiSlotDeclaration { */ entityTypes?: PluginUiSlotEntityType[]; /** - * Optional company-scoped route segment for page slots. + * Optional company-scoped route segment for page and routeSidebar slots. * Example: `kitchensink` becomes `/:companyPrefix/kitchensink`. */ routePath?: string; @@ -322,6 +490,14 @@ export interface PaperclipPluginManifestV1 { apiRoutes?: PluginApiRouteDeclaration[]; /** Environment drivers this plugin contributes. Requires `environment.drivers.register` capability. */ environmentDrivers?: PluginEnvironmentDriverDeclaration[]; + /** Suggested company-scoped agents this plugin can provision and resolve by stable key. */ + agents?: PluginManagedAgentDeclaration[]; + /** Suggested company-scoped projects this plugin can provision and resolve by stable key. */ + projects?: PluginManagedProjectDeclaration[]; + /** Suggested company-scoped routines this plugin can provision and resolve by stable key. */ + routines?: PluginManagedRoutineDeclaration[]; + /** Trusted local folders this plugin can configure and access by stable key. */ + localFolders?: PluginLocalFolderDeclaration[]; /** * Legacy top-level launcher declarations. * Prefer `ui.launchers` for new manifests. @@ -455,6 +631,22 @@ export interface PluginConfig { updatedAt: Date; } +/** + * Company-scoped plugin settings row. This is intentionally generic; plugin + * features such as local folders live inside `settingsJson` under namespaced + * keys instead of requiring feature-specific database columns. + */ +export interface PluginCompanySettings { + id: string; + companyId: string; + pluginId: string; + enabled: boolean; + settingsJson: Record; + lastError: string | null; + createdAt: Date; + updatedAt: Date; +} + /** * Query filter for `ctx.entities.list`. */ diff --git a/packages/shared/src/types/project.ts b/packages/shared/src/types/project.ts index 2aa50361..302d5048 100644 --- a/packages/shared/src/types/project.ts +++ b/packages/shared/src/types/project.ts @@ -52,6 +52,18 @@ export interface ProjectCodebase { origin: ProjectCodebaseOrigin; } +export interface ProjectManagedByPlugin { + id: string; + pluginId: string; + pluginKey: string; + pluginDisplayName: string; + resourceKind: "project"; + resourceKey: string; + defaultsJson: Record; + createdAt: Date; + updatedAt: Date; +} + export interface Project { id: string; companyId: string; @@ -73,6 +85,7 @@ export interface Project { codebase: ProjectCodebase; workspaces: ProjectWorkspace[]; primaryWorkspace: ProjectWorkspace | null; + managedByPlugin?: ProjectManagedByPlugin | null; archivedAt: Date | null; createdAt: Date; updatedAt: Date; diff --git a/packages/shared/src/types/routine.ts b/packages/shared/src/types/routine.ts index aea256be..565c94f0 100644 --- a/packages/shared/src/types/routine.ts +++ b/packages/shared/src/types/routine.ts @@ -58,6 +58,19 @@ export interface Routine { lastEnqueuedAt: Date | null; createdAt: Date; updatedAt: Date; + managedByPlugin?: RoutineManagedByPlugin | null; +} + +export interface RoutineManagedByPlugin { + id: string; + pluginId: string; + pluginKey: string; + pluginDisplayName: string; + resourceKind: "routine"; + resourceKey: string; + defaultsJson: Record; + createdAt: Date; + updatedAt: Date; } export interface RoutineTrigger { diff --git a/packages/shared/src/validators/plugin.test.ts b/packages/shared/src/validators/plugin.test.ts new file mode 100644 index 00000000..7de0c316 --- /dev/null +++ b/packages/shared/src/validators/plugin.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it } from "vitest"; +import { PLUGIN_CAPABILITIES } from "../constants.js"; +import { pluginManagedRoutineDeclarationSchema, pluginUiSlotDeclarationSchema } from "./plugin.js"; + +describe("plugin capability constants", () => { + it("exposes each capability once", () => { + expect(new Set(PLUGIN_CAPABILITIES).size).toBe(PLUGIN_CAPABILITIES.length); + }); +}); + +describe("plugin managed routine validators", () => { + it("accepts core issue surface visibility values in routine templates", () => { + const parsed = pluginManagedRoutineDeclarationSchema.parse({ + routineKey: "wiki.refresh", + title: "Refresh Wiki", + issueTemplate: { surfaceVisibility: "default" }, + }); + + expect(parsed.issueTemplate?.surfaceVisibility).toBe("default"); + }); + + it("rejects non-core issue surface visibility values in routine templates", () => { + const parsed = pluginManagedRoutineDeclarationSchema.safeParse({ + routineKey: "wiki.refresh", + title: "Refresh Wiki", + issueTemplate: { surfaceVisibility: "normal" }, + }); + + expect(parsed.success).toBe(false); + }); +}); + +describe("plugin UI slot validators", () => { + it("accepts route-scoped sidebar slots with a routePath", () => { + const parsed = pluginUiSlotDeclarationSchema.parse({ + type: "routeSidebar", + id: "wiki-route-sidebar", + displayName: "Wiki Sidebar", + exportName: "WikiSidebar", + routePath: "wiki", + }); + + expect(parsed.routePath).toBe("wiki"); + }); + + it("requires route-scoped sidebar slots to declare a routePath", () => { + const parsed = pluginUiSlotDeclarationSchema.safeParse({ + type: "routeSidebar", + id: "wiki-route-sidebar", + displayName: "Wiki Sidebar", + exportName: "WikiSidebar", + }); + + expect(parsed.success).toBe(false); + if (parsed.success) return; + expect(parsed.error.issues[0]?.message).toBe("routeSidebar slots require routePath"); + }); + + it("keeps reserved company route protection for route-scoped sidebars", () => { + const parsed = pluginUiSlotDeclarationSchema.safeParse({ + type: "routeSidebar", + id: "settings-route-sidebar", + displayName: "Settings Sidebar", + exportName: "SettingsSidebar", + routePath: "settings", + }); + + expect(parsed.success).toBe(false); + if (parsed.success) return; + expect(parsed.error.issues.some((issue) => issue.message.includes("reserved by the host"))).toBe(true); + }); +}); diff --git a/packages/shared/src/validators/plugin.ts b/packages/shared/src/validators/plugin.ts index 3ade0912..336620a0 100644 --- a/packages/shared/src/validators/plugin.ts +++ b/packages/shared/src/validators/plugin.ts @@ -15,7 +15,15 @@ import { PLUGIN_API_ROUTE_AUTH_MODES, PLUGIN_API_ROUTE_CHECKOUT_POLICIES, PLUGIN_API_ROUTE_METHODS, + ISSUE_PRIORITIES, + ROUTINE_CATCH_UP_POLICIES, + ROUTINE_CONCURRENCY_POLICIES, + ROUTINE_STATUSES, + ROUTINE_TRIGGER_KINDS, + ROUTINE_TRIGGER_SIGNING_MODES, + ISSUE_SURFACE_VISIBILITIES, } from "../constants.js"; +import { routineVariableSchema } from "./routine.js"; // --------------------------------------------------------------------------- // JSON Schema placeholder – a permissive validator for JSON Schema objects @@ -124,6 +132,106 @@ export type PluginEnvironmentDriverDeclarationInput = z.infer< export type PluginToolDeclarationInput = z.infer; +export const pluginManagedAgentDeclarationSchema = z.object({ + agentKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, { + message: "agentKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens", + }), + displayName: z.string().min(1).max(100), + role: z.string().min(1).max(100).optional(), + title: z.string().max(200).nullable().optional(), + icon: z.string().max(100).nullable().optional(), + capabilities: z.string().max(2000).nullable().optional(), + adapterType: z.string().min(1).max(100).optional(), + adapterPreference: z.array(z.string().min(1).max(100)).max(10).optional(), + adapterConfig: z.record(z.unknown()).optional(), + runtimeConfig: z.record(z.unknown()).optional(), + permissions: z.record(z.unknown()).optional(), + status: z.enum(["idle", "paused"]).optional(), + budgetMonthlyCents: z.number().int().min(0).optional(), + instructions: z.object({ + entryFile: z.string().min(1).max(200).optional(), + content: z.string().max(200_000).optional(), + assetPath: z.string().min(1).max(500).optional(), + }).optional(), +}); + +export type PluginManagedAgentDeclarationInput = z.infer; + +export const pluginManagedProjectDeclarationSchema = z.object({ + projectKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, { + message: "projectKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens", + }), + displayName: z.string().min(1).max(120), + description: z.string().max(2000).nullable().optional(), + status: z.enum(["backlog", "planned", "in_progress", "completed", "cancelled"]).optional(), + color: z.string().max(32).nullable().optional(), + settings: z.record(z.unknown()).optional(), +}); + +export type PluginManagedProjectDeclarationInput = z.infer; + +const pluginManagedResourceRefSchema = z.object({ + pluginKey: z.string().min(1).max(100).optional(), + resourceKind: z.enum(["agent", "project", "routine"]), + resourceKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, { + message: "resourceKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens", + }), +}); + +export const pluginManagedRoutineDeclarationSchema = z.object({ + routineKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, { + message: "routineKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens", + }), + title: z.string().trim().min(1).max(200), + description: z.string().max(10_000).nullable().optional(), + assigneeRef: pluginManagedResourceRefSchema.extend({ resourceKind: z.literal("agent") }).nullable().optional(), + projectRef: pluginManagedResourceRefSchema.extend({ resourceKind: z.literal("project") }).nullable().optional(), + goalId: z.string().uuid().nullable().optional(), + status: z.enum(ROUTINE_STATUSES).optional(), + priority: z.enum(ISSUE_PRIORITIES).optional(), + concurrencyPolicy: z.enum(ROUTINE_CONCURRENCY_POLICIES).optional(), + catchUpPolicy: z.enum(ROUTINE_CATCH_UP_POLICIES).optional(), + variables: z.array(routineVariableSchema).optional(), + triggers: z.array(z.object({ + kind: z.enum(ROUTINE_TRIGGER_KINDS), + label: z.string().trim().max(120).nullable().optional(), + enabled: z.boolean().optional(), + cronExpression: z.string().trim().min(1).optional().nullable(), + timezone: z.string().trim().min(1).optional().nullable(), + signingMode: z.enum(ROUTINE_TRIGGER_SIGNING_MODES).optional().nullable(), + replayWindowSec: z.number().int().min(30).max(86_400).optional().nullable(), + })).max(20).optional(), + issueTemplate: z.object({ + surfaceVisibility: z.enum(ISSUE_SURFACE_VISIBILITIES).optional(), + originId: z.string().trim().max(255).nullable().optional(), + billingCode: z.string().trim().max(200).nullable().optional(), + }).optional(), +}); + +export type PluginManagedRoutineDeclarationInput = z.infer; + +const pluginLocalFolderRelativePathSchema = z.string().min(1).max(500).refine( + (value) => + !value.startsWith("/") && + !value.includes("..") && + !value.includes("\\") && + !value.split("/").some((segment) => segment === "" || segment === "."), + { message: "local folder paths must be relative paths without traversal, empty segments, or backslashes" }, +); + +export const pluginLocalFolderDeclarationSchema = z.object({ + folderKey: z.string().min(1).max(100).regex(/^[a-z0-9][a-z0-9._:-]*$/, { + message: "folderKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens", + }), + displayName: z.string().min(1).max(100), + description: z.string().max(500).optional(), + access: z.enum(["read", "readWrite"]).optional(), + requiredDirectories: z.array(pluginLocalFolderRelativePathSchema).optional(), + requiredFiles: z.array(pluginLocalFolderRelativePathSchema).optional(), +}); + +export type PluginLocalFolderDeclarationInput = z.infer; + /** * Validates a {@link PluginUiSlotDeclaration} — a UI extension slot the plugin * fills with a React component. Includes `superRefine` checks for slot-specific @@ -178,10 +286,17 @@ export const pluginUiSlotDeclarationSchema = z.object({ path: ["entityTypes"], }); } - if (value.routePath && value.type !== "page") { + if (value.routePath && value.type !== "page" && value.type !== "routeSidebar") { ctx.addIssue({ code: z.ZodIssueCode.custom, - message: "routePath is only supported for page slots", + message: "routePath is only supported for page and routeSidebar slots", + path: ["routePath"], + }); + } + if (value.type === "routeSidebar" && !value.routePath) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "routeSidebar slots require routePath", path: ["routePath"], }); } @@ -471,6 +586,10 @@ export const pluginManifestV1Schema = z.object({ database: pluginDatabaseDeclarationSchema.optional(), apiRoutes: z.array(pluginApiRouteDeclarationSchema).optional(), environmentDrivers: z.array(pluginEnvironmentDriverDeclarationSchema).optional(), + agents: z.array(pluginManagedAgentDeclarationSchema).optional(), + projects: z.array(pluginManagedProjectDeclarationSchema).optional(), + routines: z.array(pluginManagedRoutineDeclarationSchema).optional(), + localFolders: z.array(pluginLocalFolderDeclarationSchema).optional(), launchers: z.array(pluginLauncherDeclarationSchema).optional(), ui: z.object({ slots: z.array(pluginUiSlotDeclarationSchema).min(1).optional(), @@ -529,6 +648,46 @@ export const pluginManifestV1Schema = z.object({ } } + if (manifest.agents && manifest.agents.length > 0) { + if (!manifest.capabilities.includes("agents.managed")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Capability 'agents.managed' is required when managed agents are declared", + path: ["capabilities"], + }); + } + } + + if (manifest.projects && manifest.projects.length > 0) { + if (!manifest.capabilities.includes("projects.managed")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Capability 'projects.managed' is required when managed projects are declared", + path: ["capabilities"], + }); + } + } + + if (manifest.routines && manifest.routines.length > 0) { + if (!manifest.capabilities.includes("routines.managed")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Capability 'routines.managed' is required when managed routines are declared", + path: ["capabilities"], + }); + } + } + + if (manifest.localFolders && manifest.localFolders.length > 0) { + if (!manifest.capabilities.includes("local.folders")) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: "Capability 'local.folders' is required when local folders are declared", + path: ["capabilities"], + }); + } + } + // jobs require jobs.schedule (PLUGIN_SPEC.md §17) if (manifest.jobs && manifest.jobs.length > 0) { if (!manifest.capabilities.includes("jobs.schedule")) { @@ -664,6 +823,54 @@ export const pluginManifestV1Schema = z.object({ } } + if (manifest.localFolders) { + const folderKeys = manifest.localFolders.map((folder) => folder.folderKey); + const duplicates = folderKeys.filter((key, i) => folderKeys.indexOf(key) !== i); + if (duplicates.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Duplicate local folder keys: ${[...new Set(duplicates)].join(", ")}`, + path: ["localFolders"], + }); + } + } + + if (manifest.agents) { + const agentKeys = manifest.agents.map((agent) => agent.agentKey); + const duplicates = agentKeys.filter((key, i) => agentKeys.indexOf(key) !== i); + if (duplicates.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Duplicate managed agent keys: ${[...new Set(duplicates)].join(", ")}`, + path: ["agents"], + }); + } + } + + if (manifest.projects) { + const projectKeys = manifest.projects.map((project) => project.projectKey); + const duplicates = projectKeys.filter((key, i) => projectKeys.indexOf(key) !== i); + if (duplicates.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Duplicate managed project keys: ${[...new Set(duplicates)].join(", ")}`, + path: ["projects"], + }); + } + } + + if (manifest.routines) { + const routineKeys = manifest.routines.map((routine) => routine.routineKey); + const duplicates = routineKeys.filter((key, i) => routineKeys.indexOf(key) !== i); + if (duplicates.length > 0) { + ctx.addIssue({ + code: z.ZodIssueCode.custom, + message: `Duplicate managed routine keys: ${[...new Set(duplicates)].join(", ")}`, + path: ["routines"], + }); + } + } + // UI slot ids must be unique within the plugin (namespaced at runtime) if (manifest.ui) { if (manifest.ui.slots) { diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts index eeb82316..2c705495 100644 --- a/server/src/__tests__/issues-service.test.ts +++ b/server/src/__tests__/issues-service.test.ts @@ -657,6 +657,143 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => { expect(projectResult.map((issue) => issue.id).sort()).toEqual([executionLinkedIssueId, projectLinkedIssueId].sort()); }); + it("hides plugin operation issues from default lists and inbox-style filters while preserving explicit retrieval", async () => { + const companyId = randomUUID(); + const agentId = randomUUID(); + const projectId = randomUUID(); + const normalIssueId = randomUUID(); + const pluginVisibleIssueId = randomUUID(); + const operationIssueId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + await db.insert(agents).values({ + id: agentId, + companyId, + name: "Plugin Runner", + role: "engineer", + status: "active", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }); + await db.insert(projects).values({ + id: projectId, + companyId, + name: "Plugin operations", + status: "in_progress", + }); + await db.insert(issues).values([ + { + id: normalIssueId, + companyId, + title: "Normal issue", + status: "todo", + priority: "medium", + assigneeAgentId: agentId, + }, + { + id: pluginVisibleIssueId, + companyId, + title: "Plugin-visible issue", + status: "todo", + priority: "medium", + assigneeAgentId: agentId, + originKind: "plugin:paperclip.missions:feature", + }, + { + id: operationIssueId, + companyId, + projectId, + title: "Plugin operation issue", + status: "todo", + priority: "medium", + assigneeAgentId: agentId, + originKind: "plugin:paperclip.missions:operation", + originId: "mission-alpha:operation-1", + }, + ]); + + const defaultIssueIds = (await svc.list(companyId)).map((issue) => issue.id); + expect(defaultIssueIds).toContain(normalIssueId); + expect(defaultIssueIds).toContain(pluginVisibleIssueId); + expect(defaultIssueIds).not.toContain(operationIssueId); + + const inboxIssueIds = (await svc.list(companyId, { + assigneeAgentId: agentId, + status: "todo,in_progress,blocked", + includeRoutineExecutions: true, + })).map((issue) => issue.id); + expect(inboxIssueIds).toContain(normalIssueId); + expect(inboxIssueIds).not.toContain(operationIssueId); + + await expect(svc.list(companyId, { originKind: "plugin:paperclip.missions:operation" })) + .resolves.toEqual([expect.objectContaining({ id: operationIssueId })]); + await expect(svc.list(companyId, { originId: "mission-alpha:operation-1" })) + .resolves.toEqual([expect.objectContaining({ id: operationIssueId })]); + + const projectIssueIds = (await svc.list(companyId, { projectId })).map((issue) => issue.id); + expect(projectIssueIds).toContain(operationIssueId); + + const advancedIssueIds = (await svc.list(companyId, { includePluginOperations: true })).map((issue) => issue.id); + expect(advancedIssueIds).toContain(operationIssueId); + }); + + it("excludes plugin operation issues from unread inbox counts", async () => { + const companyId = randomUUID(); + const userId = "board-user"; + const otherUserId = "other-user"; + const normalIssueId = randomUUID(); + const operationIssueId = randomUUID(); + + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: `T${companyId.replace(/-/g, "").slice(0, 6).toUpperCase()}`, + requireBoardApprovalForNewAgents: false, + }); + await db.insert(issues).values([ + { + id: normalIssueId, + companyId, + title: "Normal touched issue", + status: "todo", + priority: "medium", + createdByUserId: userId, + }, + { + id: operationIssueId, + companyId, + title: "Plugin operation touched issue", + status: "todo", + priority: "medium", + createdByUserId: userId, + originKind: "plugin:paperclip.missions:operation", + }, + ]); + await db.insert(issueComments).values([ + { + companyId, + issueId: normalIssueId, + authorUserId: otherUserId, + body: "Unread normal update.", + }, + { + companyId, + issueId: operationIssueId, + authorUserId: otherUserId, + body: "Unread operation update.", + }, + ]); + + await expect(svc.countUnreadTouchedByUser(companyId, userId, "todo")).resolves.toBe(1); + }); + it("hides archived inbox issues until new external activity arrives", async () => { const companyId = randomUUID(); const userId = "user-1"; diff --git a/server/src/__tests__/plugin-database.test.ts b/server/src/__tests__/plugin-database.test.ts index 9aaca2f1..6392b78e 100644 --- a/server/src/__tests__/plugin-database.test.ts +++ b/server/src/__tests__/plugin-database.test.ts @@ -3,7 +3,7 @@ import { mkdtemp, rm, mkdir, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { and, eq, sql } from "drizzle-orm"; -import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; import { companies, createDb, @@ -25,9 +25,11 @@ import { validatePluginRuntimeExecute, validatePluginRuntimeQuery, } from "../services/plugin-database.js"; +import { pluginLoader } from "../services/plugin-loader.js"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; +const multiMigrationPluginKey = "paperclip.dbfixture"; if (!embeddedPostgresSupport.supported) { console.warn( @@ -93,7 +95,7 @@ describeEmbeddedPostgres("plugin database namespaces", () => { }, 20_000); afterEach(async () => { - for (const pluginKey of ["paperclip.dbtest", "paperclip.escape"]) { + for (const pluginKey of ["paperclip.dbtest", "paperclip.escape", "paperclip.refresh", multiMigrationPluginKey]) { const namespace = derivePluginDatabaseNamespace(pluginKey); await db.execute(sql.raw(`DROP SCHEMA IF EXISTS "${namespace}" CASCADE`)); } @@ -120,6 +122,31 @@ describeEmbeddedPostgres("plugin database namespaces", () => { return packageRoot; } + async function createInstallablePluginPackage( + pluginManifest: PaperclipPluginManifestV1, + migrationSql: string, + ) { + const packageRoot = await createPluginPackage(pluginManifest, migrationSql); + await writeFile( + path.join(packageRoot, "package.json"), + JSON.stringify({ + name: pluginManifest.id, + version: pluginManifest.version, + type: "module", + paperclipPlugin: { manifest: "./manifest.js" }, + }), + "utf8", + ); + await writeFile( + path.join(packageRoot, "manifest.js"), + `export default ${JSON.stringify(pluginManifest, null, 2)};\n`, + "utf8", + ); + await mkdir(path.join(packageRoot, "dist"), { recursive: true }); + await writeFile(path.join(packageRoot, "dist", "worker.js"), "export {};\n", "utf8"); + return packageRoot; + } + async function installPluginRecord(manifest: PaperclipPluginManifestV1) { const pluginId = randomUUID(); await db.insert(plugins).values({ @@ -158,6 +185,31 @@ describeEmbeddedPostgres("plugin database namespaces", () => { }; } + it("applies multi-file plugin migrations through the production validator", async () => { + const pluginManifest = manifest(multiMigrationPluginKey); + const namespace = derivePluginDatabaseNamespace(pluginManifest.id); + const packageRoot = await createPluginPackage( + pluginManifest, + `CREATE TABLE ${namespace}.source_rows (id uuid PRIMARY KEY, label text NOT NULL);`, + ); + await writeFile( + path.join(packageRoot, pluginManifest.database!.migrationsDir, "002_derived.sql"), + `CREATE TABLE ${namespace}.derived_rows ( + id uuid PRIMARY KEY, + source_id uuid NOT NULL REFERENCES ${namespace}.source_rows(id) + );`, + "utf8", + ); + const pluginId = await installPluginRecord(pluginManifest); + await pluginDatabaseService(db).applyMigrations(pluginId, pluginManifest, packageRoot); + + const migrations = await db + .select() + .from(pluginMigrations) + .where(and(eq(pluginMigrations.pluginId, pluginId), eq(pluginMigrations.status, "applied"))); + expect(migrations).toHaveLength(2); + }); + it("applies migrations once and allows whitelisted core joins at runtime", async () => { const pluginManifest = manifest(); const namespace = derivePluginDatabaseNamespace(pluginManifest.id); @@ -246,6 +298,131 @@ describeEmbeddedPostgres("plugin database namespaces", () => { expect(migration?.status).toBe("failed"); }); + it("rolls back plugin install when migration validation fails", async () => { + const pluginManifest = manifest("paperclip.escape"); + const namespace = derivePluginDatabaseNamespace(pluginManifest.id); + const packageRoot = await createInstallablePluginPackage( + pluginManifest, + "CREATE TABLE public.plugin_escape (id uuid PRIMARY KEY);", + ); + const loader = pluginLoader(db, { + enableLocalFilesystem: false, + enableNpmDiscovery: false, + }); + + await expect(loader.installPlugin({ localPath: packageRoot })) + .rejects.toThrow(/public\.plugin_escape|public/i); + + const installedPlugins = await db + .select() + .from(plugins) + .where(eq(plugins.pluginKey, pluginManifest.id)); + const namespaces = await db + .select() + .from(pluginDatabaseNamespaces) + .where(eq(pluginDatabaseNamespaces.pluginKey, pluginManifest.id)); + const migrations = await db + .select() + .from(pluginMigrations) + .where(eq(pluginMigrations.pluginKey, pluginManifest.id)); + const schemaRows = Array.from( + await db.execute( + sql<{ schema_name: string }>`SELECT schema_name FROM information_schema.schemata WHERE schema_name = ${namespace}`, + ) as Iterable<{ schema_name: string }>, + ); + + expect(installedPlugins).toHaveLength(0); + expect(namespaces).toHaveLength(0); + expect(migrations).toHaveLength(0); + expect(schemaRows).toHaveLength(0); + }); + + it("refreshes persisted manifests from disk before activation", async () => { + const staleManifest = manifest("paperclip.refresh"); + const refreshedManifest: PaperclipPluginManifestV1 = { + ...staleManifest, + database: { + ...staleManifest.database!, + coreReadTables: ["companies"], + }, + }; + const namespace = derivePluginDatabaseNamespace(refreshedManifest.id); + const packageRoot = await createInstallablePluginPackage( + refreshedManifest, + ` + CREATE TABLE ${namespace}.company_refs ( + id uuid PRIMARY KEY, + company_id uuid NOT NULL REFERENCES public.companies(id) + ); + `, + ); + const pluginId = await installPluginRecord(staleManifest); + await db + .update(plugins) + .set({ + packagePath: packageRoot, + status: "ready", + }) + .where(eq(plugins.id, pluginId)); + + const workerManager = { + startWorker: vi.fn().mockResolvedValue(undefined), + stopAll: vi.fn().mockResolvedValue(undefined), + }; + const loader = pluginLoader(db, { + enableLocalFilesystem: false, + enableNpmDiscovery: false, + }, { + workerManager, + eventBus: { + forPlugin: vi.fn(() => ({})), + subscriptionCount: vi.fn(() => 0), + }, + jobScheduler: { + registerPlugin: vi.fn().mockResolvedValue(undefined), + stop: vi.fn(), + }, + jobStore: { + syncJobDeclarations: vi.fn().mockResolvedValue(undefined), + }, + toolDispatcher: { + registerPluginTools: vi.fn(), + }, + lifecycleManager: { + markError: vi.fn().mockResolvedValue(undefined), + }, + buildHostHandlers: vi.fn(() => ({})), + instanceInfo: { + instanceId: "test-instance", + hostVersion: "1.0.0", + deploymentMode: "authenticated", + deploymentExposure: "public", + }, + } as never); + + const result = await loader.loadSingle(pluginId); + + expect(result.success).toBe(true); + expect(workerManager.startWorker).toHaveBeenCalledWith( + pluginId, + expect.objectContaining({ + databaseNamespace: namespace, + env: { + PAPERCLIP_DEPLOYMENT_MODE: "authenticated", + PAPERCLIP_DEPLOYMENT_EXPOSURE: "public", + }, + manifest: expect.objectContaining({ + database: expect.objectContaining({ coreReadTables: ["companies"] }), + }), + }), + ); + const [plugin] = await db + .select() + .from(plugins) + .where(eq(plugins.id, pluginId)); + expect(plugin?.manifestJson.database?.coreReadTables).toEqual(["companies"]); + }); + it("rejects checksum changes for already applied migrations", async () => { const pluginManifest = manifest(); const namespace = derivePluginDatabaseNamespace(pluginManifest.id); diff --git a/server/src/__tests__/plugin-local-folders.test.ts b/server/src/__tests__/plugin-local-folders.test.ts new file mode 100644 index 00000000..89e00eff --- /dev/null +++ b/server/src/__tests__/plugin-local-folders.test.ts @@ -0,0 +1,263 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import os from "node:os"; +import path from "node:path"; +import { promises as fs } from "node:fs"; +import { + assertConfiguredLocalFolder, + assertWritableConfiguredLocalFolder, + inspectPluginLocalFolder, + listPluginLocalFolderEntries, + preparePluginLocalFolder, + readPluginLocalFolderText, + resolvePluginLocalFolderPath, + writePluginLocalFolderTextAtomic, +} from "../services/plugin-local-folders.js"; + +describe("plugin local folders", () => { + const tempRoots: string[] = []; + + afterEach(async () => { + await Promise.all(tempRoots.map((root) => fs.rm(root, { recursive: true, force: true }))); + tempRoots.length = 0; + }); + + async function makeRoot() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-plugin-folder-")); + tempRoots.push(root); + return root; + } + + it("reports a healthy generic folder when required paths exist", async () => { + const root = await makeRoot(); + await fs.mkdir(path.join(root, "sources")); + await fs.writeFile(path.join(root, "schema.md"), "schema", "utf8"); + + const status = await inspectPluginLocalFolder({ + folderKey: "content-root", + storedConfig: { + path: root, + access: "readWrite", + requiredDirectories: ["sources"], + requiredFiles: ["schema.md"], + }, + }); + + expect(status.healthy).toBe(true); + expect(status.problems).toEqual([]); + expect(status.requiredDirectories).toEqual(["sources"]); + expect(status.requiredFiles).toEqual(["schema.md"]); + }); + + it("reports missing required folders and files without using product-specific branches", async () => { + const root = await makeRoot(); + + const status = await inspectPluginLocalFolder({ + folderKey: "content-root", + storedConfig: { + path: root, + requiredDirectories: ["sources"], + requiredFiles: ["schema.md"], + }, + }); + + expect(status.healthy).toBe(false); + expect(status.missingDirectories).toEqual(["sources"]); + expect(status.missingFiles).toEqual(["schema.md"]); + expect(status.problems.map((item) => item.code)).toEqual( + expect.arrayContaining(["missing_directory", "missing_file"]), + ); + }); + + it("reports all required paths as missing when the configured root does not exist", async () => { + const root = await makeRoot(); + const missingRoot = path.join(root, "missing-root"); + + const status = await inspectPluginLocalFolder({ + folderKey: "content-root", + storedConfig: { + path: missingRoot, + requiredDirectories: ["sources"], + requiredFiles: ["schema.md"], + }, + }); + + expect(status.healthy).toBe(false); + expect(status.configured).toBe(true); + expect(status.readable).toBe(false); + expect(status.missingDirectories).toEqual(["sources"]); + expect(status.missingFiles).toEqual(["schema.md"]); + expect(status.problems.map((item) => item.code)).toContain("missing"); + }); + + it("uses manifest declaration access and required paths over stored or caller overrides", async () => { + const root = await makeRoot(); + await fs.mkdir(path.join(root, "manifest-dir")); + await fs.writeFile(path.join(root, "manifest.md"), "schema", "utf8"); + + const status = await inspectPluginLocalFolder({ + folderKey: "content-root", + declaration: { + folderKey: "content-root", + displayName: "Content root", + access: "read", + requiredDirectories: ["manifest-dir"], + requiredFiles: ["manifest.md"], + }, + storedConfig: { + path: root, + access: "readWrite", + requiredDirectories: ["stored-dir"], + requiredFiles: ["stored.md"], + }, + overrideConfig: { + access: "readWrite", + requiredDirectories: ["override-dir"], + requiredFiles: ["override.md"], + }, + }); + + expect(status.access).toBe("read"); + expect(status.writable).toBe(false); + expect(status.requiredDirectories).toEqual(["manifest-dir"]); + expect(status.requiredFiles).toEqual(["manifest.md"]); + expect(status.healthy).toBe(true); + }); + + it("prepares required directories for a read-write folder without creating required files", async () => { + const root = await makeRoot(); + + await preparePluginLocalFolder({ + folderKey: "content-root", + storedConfig: { + path: root, + access: "readWrite", + requiredDirectories: ["sources", "wiki/concepts"], + requiredFiles: ["schema.md"], + }, + }); + + await expect(fs.stat(path.join(root, "sources"))).resolves.toMatchObject({}); + await expect(fs.stat(path.join(root, "wiki/concepts"))).resolves.toMatchObject({}); + await expect(fs.stat(path.join(root, "schema.md"))).rejects.toMatchObject({ code: "ENOENT" }); + + const status = await inspectPluginLocalFolder({ + folderKey: "content-root", + storedConfig: { + path: root, + access: "readWrite", + requiredDirectories: ["sources", "wiki/concepts"], + requiredFiles: ["schema.md"], + }, + }); + expect(status.missingDirectories).toEqual([]); + expect(status.missingFiles).toEqual(["schema.md"]); + }); + + it("allows write access to repair folders that are only missing required paths", async () => { + const root = await makeRoot(); + const status = await inspectPluginLocalFolder({ + folderKey: "content-root", + storedConfig: { + path: root, + access: "readWrite", + requiredFiles: ["schema.md"], + }, + }); + + expect(status.healthy).toBe(false); + expect(() => assertConfiguredLocalFolder(status)).toThrow("Local folder is not healthy"); + expect(() => assertWritableConfiguredLocalFolder(status)).not.toThrow(); + + await writePluginLocalFolderTextAtomic(root, "schema.md", "schema"); + const repaired = await inspectPluginLocalFolder({ + folderKey: "content-root", + storedConfig: { + path: root, + access: "readWrite", + requiredFiles: ["schema.md"], + }, + }); + expect(repaired.healthy).toBe(true); + }); + + it("rejects traversal outside the configured folder", async () => { + const root = await makeRoot(); + + await expect(resolvePluginLocalFolderPath(root, "../outside.txt")).rejects.toMatchObject({ + status: 403, + }); + }); + + it("detects required symlinks that escape the configured folder", async () => { + const root = await makeRoot(); + const outside = await makeRoot(); + await fs.writeFile(path.join(outside, "secret.txt"), "nope", "utf8"); + await fs.symlink(path.join(outside, "secret.txt"), path.join(root, "linked.txt")); + + const status = await inspectPluginLocalFolder({ + folderKey: "content-root", + storedConfig: { + path: root, + requiredFiles: ["linked.txt"], + }, + }); + + expect(status.healthy).toBe(false); + expect(status.problems.some((item) => item.code === "symlink_escape")).toBe(true); + }); + + it("writes files atomically under the root and can read them back", async () => { + const root = await makeRoot(); + await fs.mkdir(path.join(root, "nested")); + + await writePluginLocalFolderTextAtomic(root, "nested/page.md", "hello"); + await writePluginLocalFolderTextAtomic(root, "nested/page.md", "updated"); + + await expect(readPluginLocalFolderText(root, "nested/page.md")).resolves.toBe("updated"); + const leftovers = await fs.readdir(path.join(root, "nested")); + expect(leftovers.filter((name) => name.includes(".paperclip-"))).toEqual([]); + }); + + it("lists nested local folder entries without following symlink escapes", async () => { + const root = await makeRoot(); + const outside = await makeRoot(); + await fs.mkdir(path.join(root, "wiki/concepts"), { recursive: true }); + await fs.writeFile(path.join(root, "wiki/concepts/live.md"), "# Live\n", "utf8"); + await fs.writeFile(path.join(outside, "secret.md"), "# Secret\n", "utf8"); + await fs.symlink(outside, path.join(root, "wiki/outside")); + + const listing = await listPluginLocalFolderEntries(root, { + relativePath: "wiki", + recursive: true, + maxEntries: 20, + }); + + expect(listing.entries.map((entry) => entry.path)).toContain("wiki/concepts/live.md"); + expect(listing.entries.map((entry) => entry.path)).not.toContain("wiki/outside/secret.md"); + expect(listing.truncated).toBe(false); + }); + + it("revalidates temp-file containment before writing atomic contents", async () => { + const root = await makeRoot(); + const outside = await makeRoot(); + const nested = path.join(root, "nested"); + await fs.mkdir(nested); + const originalOpen = fs.open.bind(fs); + const openSpy = vi.spyOn(fs, "open"); + openSpy.mockImplementationOnce(async (file, flags, mode) => { + await fs.rm(nested, { recursive: true, force: true }); + await fs.symlink(outside, nested); + return originalOpen(file, flags, mode); + }); + + try { + await expect(writePluginLocalFolderTextAtomic(root, "nested/page.md", "secret")).rejects.toMatchObject({ + status: 403, + }); + await expect(fs.readFile(path.join(outside, "page.md"), "utf8")).rejects.toMatchObject({ code: "ENOENT" }); + expect(await fs.readdir(outside)).toEqual([]); + } finally { + openSpy.mockRestore(); + } + }); +}); diff --git a/server/src/__tests__/plugin-managed-agents.test.ts b/server/src/__tests__/plugin-managed-agents.test.ts new file mode 100644 index 00000000..ed512a8d --- /dev/null +++ b/server/src/__tests__/plugin-managed-agents.test.ts @@ -0,0 +1,365 @@ +import { randomUUID } from "node:crypto"; +import { promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { eq } from "drizzle-orm"; +import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; +import { + activityLog, + agentConfigRevisions, + agents, + approvals, + companies, + createDb, + pluginEntities, + pluginCompanySettings, + pluginManagedResources, + plugins, +} from "@paperclipai/db"; +import type { PaperclipPluginManifestV1 } from "@paperclipai/shared"; +import { + getEmbeddedPostgresTestSupport, + startEmbeddedPostgresTestDatabase, +} from "./helpers/embedded-postgres.js"; +import { buildHostServices } from "../services/plugin-host-services.js"; + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +function createEventBusStub() { + return { + forPlugin() { + return { + emit: async () => {}, + subscribe: () => {}, + }; + }, + } as any; +} + +function issuePrefix(id: string) { + return `T${id.replace(/-/g, "").slice(0, 6).toUpperCase()}`; +} + +function manifest(): PaperclipPluginManifestV1 { + return { + id: "paperclip.managed-agents-test", + apiVersion: 1, + version: "0.1.0", + displayName: "Managed Agents Test", + description: "Test plugin", + author: "Paperclip", + categories: ["automation"], + capabilities: ["agents.managed"], + entrypoints: { worker: "./dist/worker.js" }, + agents: [ + { + agentKey: "wiki-maintainer", + displayName: "Wiki Maintainer", + role: "engineer", + title: "Maintains plugin-owned knowledge", + capabilities: "Maintains a plugin-owned wiki.", + adapterType: "process", + adapterConfig: { command: "pnpm wiki:maintain" }, + runtimeConfig: { modelProfiles: { cheap: { enabled: true, adapterConfig: { model: "small" } } } }, + permissions: { canCreateAgents: false }, + budgetMonthlyCents: 1234, + }, + ], + }; +} + +if (!embeddedPostgresSupport.supported) { + console.warn( + `Skipping embedded Postgres plugin-managed agent tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, + ); +} + +describeEmbeddedPostgres("plugin-managed agents", () => { + let db!: ReturnType; + let tempDb: Awaited> | null = null; + + beforeAll(async () => { + tempDb = await startEmbeddedPostgresTestDatabase("paperclip-plugin-managed-agents-"); + db = createDb(tempDb.connectionString); + }, 20_000); + + afterEach(async () => { + await db.delete(agentConfigRevisions); + await db.delete(activityLog); + await db.delete(pluginEntities); + await db.delete(pluginManagedResources); + await db.delete(pluginCompanySettings); + await db.delete(approvals); + await db.delete(agents); + await db.delete(plugins); + await db.delete(companies); + }); + + afterAll(async () => { + await tempDb?.cleanup(); + }); + + async function seedCompanyAndPlugin(options: { requireApproval?: boolean; manifest?: PaperclipPluginManifestV1 } = {}) { + const companyId = randomUUID(); + const pluginId = randomUUID(); + const pluginManifest = options.manifest ?? manifest(); + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: issuePrefix(companyId), + requireBoardApprovalForNewAgents: options.requireApproval ?? false, + }); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: pluginManifest.id, + packageName: "@paperclipai/plugin-managed-agents-test", + version: pluginManifest.version, + apiVersion: pluginManifest.apiVersion, + categories: pluginManifest.categories, + manifestJson: pluginManifest, + status: "ready", + installOrder: 1, + }); + const services = buildHostServices(db, pluginId, pluginManifest.id, createEventBusStub(), undefined, { + manifest: pluginManifest, + }); + return { companyId, pluginId, pluginManifest, services }; + } + + it("creates and resolves managed agents by stable resource key", async () => { + const { companyId, services } = await seedCompanyAndPlugin(); + + const created = await services.agents.managedReconcile({ + companyId, + agentKey: "wiki-maintainer", + }); + + expect(created.status).toBe("created"); + expect(created.agentId).toBeTruthy(); + expect(created.agent).toMatchObject({ + name: "Wiki Maintainer", + role: "engineer", + adapterConfig: { command: "pnpm wiki:maintain" }, + }); + + const resolved = await services.agents.managedGet({ + companyId, + agentKey: "wiki-maintainer", + }); + expect(resolved.status).toBe("resolved"); + expect(resolved.agentId).toBe(created.agentId); + + const [binding] = await db.select().from(pluginEntities); + expect(binding?.entityType).toBe("managed_agent"); + expect(binding?.scopeKind).toBe("company"); + expect(binding?.scopeId).toBe(companyId); + expect(binding?.data).toMatchObject({ + resourceKind: "agent", + resourceKey: "wiki-maintainer", + agentId: created.agentId, + }); + }); + + it("preserves user edits during reconcile and resets only on explicit reset", async () => { + const { companyId, services } = await seedCompanyAndPlugin(); + const created = await services.agents.managedReconcile({ companyId, agentKey: "wiki-maintainer" }); + expect(created.agentId).toBeTruthy(); + + await db + .update(agents) + .set({ + name: "Knowledge Lead", + adapterConfig: { command: "custom" }, + updatedAt: new Date(), + }) + .where(eq(agents.id, created.agentId!)); + + const reconciled = await services.agents.managedReconcile({ companyId, agentKey: "wiki-maintainer" }); + expect(reconciled.status).toBe("resolved"); + expect(reconciled.agent).toMatchObject({ + name: "Knowledge Lead", + adapterConfig: { command: "custom" }, + }); + + const reset = await services.agents.managedReset({ companyId, agentKey: "wiki-maintainer" }); + expect(reset.status).toBe("reset"); + expect(reset.agent).toMatchObject({ + name: "Wiki Maintainer", + adapterConfig: { command: "pnpm wiki:maintain" }, + }); + }); + + it("creates managed agents with the most-used compatible company adapter", async () => { + const pluginManifest = manifest(); + pluginManifest.agents![0] = { + ...pluginManifest.agents![0]!, + adapterType: "claude_local", + adapterPreference: ["claude_local", "codex_local"], + adapterConfig: {}, + }; + const { companyId, services } = await seedCompanyAndPlugin({ manifest: pluginManifest }); + await db.insert(agents).values([ + { + id: randomUUID(), + companyId, + name: "Codex One", + role: "engineer", + status: "idle", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }, + { + id: randomUUID(), + companyId, + name: "Codex Two", + role: "engineer", + status: "idle", + adapterType: "codex_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }, + { + id: randomUUID(), + companyId, + name: "Claude One", + role: "engineer", + status: "idle", + adapterType: "claude_local", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }, + ]); + + const created = await services.agents.managedReconcile({ companyId, agentKey: "wiki-maintainer" }); + + expect(created.status).toBe("created"); + expect(created.agent?.adapterType).toBe("codex_local"); + }); + + it("materializes declared managed agent instructions with local folder paths", async () => { + const previousHome = process.env.PAPERCLIP_HOME; + const previousInstance = process.env.PAPERCLIP_INSTANCE_ID; + const tempHome = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-managed-agent-home-")); + const wikiRoot = await fs.realpath(await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-managed-agent-wiki-"))); + process.env.PAPERCLIP_HOME = tempHome; + process.env.PAPERCLIP_INSTANCE_ID = "test"; + try { + const pluginManifest = manifest(); + pluginManifest.localFolders = [ + { + folderKey: "wiki-root", + displayName: "Wiki root", + access: "readWrite", + requiredDirectories: [], + requiredFiles: ["AGENTS.md"], + }, + ]; + pluginManifest.agents![0] = { + ...pluginManifest.agents![0]!, + adapterType: "claude_local", + adapterConfig: {}, + instructions: { + entryFile: "AGENTS.md", + content: [ + "# LLM Wiki Maintainer", + "", + "You are the LLM Wiki Maintainer.", + "Wiki root: `{{localFolders.wiki-root.path}}`", + "Wiki schema: `{{localFolders.wiki-root.agentsPath}}`", + "", + ].join("\n"), + }, + }; + const { companyId, pluginId, services } = await seedCompanyAndPlugin({ manifest: pluginManifest }); + await fs.writeFile(path.join(wikiRoot, "AGENTS.md"), "# Wiki schema\n", "utf8"); + await db.insert(pluginCompanySettings).values({ + companyId, + pluginId, + enabled: true, + settingsJson: { + localFolders: { + "wiki-root": { + path: wikiRoot, + access: "readWrite", + requiredDirectories: [], + requiredFiles: ["AGENTS.md"], + }, + }, + }, + }); + + const created = await services.agents.managedReconcile({ companyId, agentKey: "wiki-maintainer" }); + + const instructionsFilePath = created.agent?.adapterConfig.instructionsFilePath; + expect(typeof instructionsFilePath).toBe("string"); + const content = await fs.readFile(instructionsFilePath as string, "utf8"); + expect(content).toContain("You are the LLM Wiki Maintainer."); + expect(content).toContain(`Wiki root: \`${wikiRoot}\``); + expect(content).toContain(`Wiki schema: \`${path.join(wikiRoot, "AGENTS.md")}\``); + } finally { + if (previousHome === undefined) delete process.env.PAPERCLIP_HOME; + else process.env.PAPERCLIP_HOME = previousHome; + if (previousInstance === undefined) delete process.env.PAPERCLIP_INSTANCE_ID; + else process.env.PAPERCLIP_INSTANCE_ID = previousInstance; + await fs.rm(tempHome, { recursive: true, force: true }); + await fs.rm(wikiRoot, { recursive: true, force: true }); + } + }); + + it("repairs a missing binding by relinking a same-company managed agent marker", async () => { + const { companyId, pluginId, pluginManifest, services } = await seedCompanyAndPlugin(); + const agentId = randomUUID(); + await db.insert(agents).values({ + id: agentId, + companyId, + name: "Renamed Wiki Agent", + role: "engineer", + status: "idle", + adapterType: "process", + adapterConfig: { command: "custom" }, + runtimeConfig: {}, + permissions: {}, + metadata: { + paperclipManagedResource: { + pluginId, + pluginKey: pluginManifest.id, + resourceKind: "agent", + resourceKey: "wiki-maintainer", + }, + }, + }); + + const relinked = await services.agents.managedReconcile({ companyId, agentKey: "wiki-maintainer" }); + expect(relinked.status).toBe("relinked"); + expect(relinked.agentId).toBe(agentId); + + const [binding] = await db.select().from(pluginEntities); + expect(binding?.data).toMatchObject({ agentId }); + }); + + it("respects board approval policy for new managed agents", async () => { + const { companyId, services } = await seedCompanyAndPlugin({ requireApproval: true }); + + const created = await services.agents.managedReconcile({ companyId, agentKey: "wiki-maintainer" }); + + expect(created.status).toBe("created"); + expect(created.agent?.status).toBe("pending_approval"); + expect(created.approvalId).toBeTruthy(); + + const [approval] = await db.select().from(approvals).where(eq(approvals.id, created.approvalId!)); + expect(approval).toMatchObject({ + type: "hire_agent", + status: "pending", + }); + expect(approval?.payload).toMatchObject({ + agentId: created.agentId, + sourcePluginKey: "paperclip.managed-agents-test", + managedResourceKey: "wiki-maintainer", + }); + }); +}); diff --git a/server/src/__tests__/plugin-managed-routines.test.ts b/server/src/__tests__/plugin-managed-routines.test.ts new file mode 100644 index 00000000..6f89cac1 --- /dev/null +++ b/server/src/__tests__/plugin-managed-routines.test.ts @@ -0,0 +1,249 @@ +import { randomUUID } from "node:crypto"; +import { eq } from "drizzle-orm"; +import { afterAll, afterEach, beforeAll, describe, expect, it, vi } from "vitest"; +import { + activityLog, + agentConfigRevisions, + agents, + companies, + createDb, + issues, + pluginManagedResources, + plugins, + projects, + routineRuns, + routineTriggers, + routines, +} from "@paperclipai/db"; +import type { PaperclipPluginManifestV1 } from "@paperclipai/shared"; +import { + getEmbeddedPostgresTestSupport, + startEmbeddedPostgresTestDatabase, +} from "./helpers/embedded-postgres.js"; +import { buildHostServices } from "../services/plugin-host-services.js"; +import { routineService } from "../services/routines.js"; + +const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); +const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; + +function createEventBusStub() { + return { + forPlugin() { + return { + emit: async () => {}, + subscribe: () => {}, + }; + }, + } as any; +} + +function issuePrefix(id: string) { + return `T${id.replace(/-/g, "").slice(0, 6).toUpperCase()}`; +} + +function manifest(): PaperclipPluginManifestV1 { + return { + id: "paperclip.managed-routines-test", + apiVersion: 1, + version: "0.1.0", + displayName: "Managed Routines Test", + description: "Test plugin", + author: "Paperclip", + categories: ["automation"], + capabilities: ["agents.managed", "projects.managed", "routines.managed"], + entrypoints: { worker: "./dist/worker.js" }, + agents: [{ + agentKey: "wiki-maintainer", + displayName: "Wiki Maintainer", + role: "engineer", + adapterType: "process", + adapterConfig: { command: "pnpm wiki:maintain" }, + }], + projects: [{ + projectKey: "operations", + displayName: "Plugin Operations", + description: "Plugin operation inspection", + status: "in_progress", + }], + routines: [{ + routineKey: "nightly-lint", + title: "Nightly lint", + description: "Lint plugin state", + assigneeRef: { resourceKind: "agent", resourceKey: "wiki-maintainer" }, + projectRef: { resourceKind: "project", resourceKey: "operations" }, + status: "active", + priority: "medium", + concurrencyPolicy: "coalesce_if_active", + catchUpPolicy: "skip_missed", + triggers: [{ + kind: "schedule", + label: "Nightly", + cronExpression: "0 3 * * *", + timezone: "UTC", + }], + issueTemplate: { + surfaceVisibility: "plugin_operation", + originId: "operation:nightly-lint", + billingCode: "plugin-test:nightly-lint", + }, + }], + }; +} + +if (!embeddedPostgresSupport.supported) { + console.warn( + `Skipping embedded Postgres plugin-managed routine tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`, + ); +} + +describeEmbeddedPostgres("plugin-managed routines", () => { + let db!: ReturnType; + let tempDb: Awaited> | null = null; + + beforeAll(async () => { + tempDb = await startEmbeddedPostgresTestDatabase("paperclip-plugin-managed-routines-"); + db = createDb(tempDb.connectionString); + }, 20_000); + + afterEach(async () => { + await db.delete(routineRuns); + await db.delete(routineTriggers); + await db.delete(routines); + await db.delete(issues); + await db.delete(agentConfigRevisions); + await db.delete(activityLog); + await db.delete(pluginManagedResources); + await db.delete(agents); + await db.delete(projects); + await db.delete(plugins); + await db.delete(companies); + }); + + afterAll(async () => { + await tempDb?.cleanup(); + }); + + async function seedCompanyAndPlugin(pluginManifest = manifest()) { + const companyId = randomUUID(); + const pluginId = randomUUID(); + await db.insert(companies).values({ + id: companyId, + name: "Paperclip", + issuePrefix: issuePrefix(companyId), + }); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: pluginManifest.id, + packageName: "@paperclipai/plugin-managed-routines-test", + version: pluginManifest.version, + apiVersion: pluginManifest.apiVersion, + categories: pluginManifest.categories, + manifestJson: pluginManifest, + status: "ready", + installOrder: 1, + }); + const services = buildHostServices(db, pluginId, pluginManifest.id, createEventBusStub(), undefined, { + manifest: pluginManifest, + }); + return { companyId, pluginId, pluginManifest, services }; + } + + it("resolves routine agent and project refs by stable managed keys", async () => { + const { companyId, services } = await seedCompanyAndPlugin(); + const agent = await services.agents.managedReconcile({ companyId, agentKey: "wiki-maintainer" }); + const project = await services.projects.reconcileManaged({ companyId, projectKey: "operations" }); + + const created = await services.routines.managedReconcile({ companyId, routineKey: "nightly-lint" }); + + expect(created.status).toBe("created"); + expect(created.routine).toMatchObject({ + title: "Nightly lint", + assigneeAgentId: agent.agentId, + projectId: project.projectId, + managedByPlugin: expect.objectContaining({ + pluginKey: "paperclip.managed-routines-test", + resourceKind: "routine", + resourceKey: "nightly-lint", + }), + }); + + const [trigger] = await db.select().from(routineTriggers).where(eq(routineTriggers.routineId, created.routineId!)); + expect(trigger).toMatchObject({ + kind: "schedule", + cronExpression: "0 3 * * *", + timezone: "UTC", + }); + }); + + it("returns missing refs until the operator repairs them and preserves routine edits on reconcile", async () => { + const { companyId, services } = await seedCompanyAndPlugin(); + + const missing = await services.routines.managedReconcile({ companyId, routineKey: "nightly-lint" }); + expect(missing.status).toBe("missing_refs"); + expect(missing.missingRefs).toEqual([ + expect.objectContaining({ resourceKind: "agent", resourceKey: "wiki-maintainer" }), + expect.objectContaining({ resourceKind: "project", resourceKey: "operations" }), + ]); + + const [agent] = await db.insert(agents).values({ + companyId, + name: "Operator-selected maintainer", + role: "engineer", + status: "idle", + adapterType: "process", + adapterConfig: {}, + runtimeConfig: {}, + permissions: {}, + }).returning(); + const [project] = await db.insert(projects).values({ + companyId, + name: "Operator-selected project", + status: "in_progress", + }).returning(); + + const repaired = await services.routines.managedReconcile({ + companyId, + routineKey: "nightly-lint", + assigneeAgentId: agent.id, + projectId: project.id, + }); + expect(repaired.status).toBe("created"); + expect(repaired.routine).toMatchObject({ + assigneeAgentId: agent.id, + projectId: project.id, + }); + + await db + .update(routines) + .set({ title: "Operator renamed lint", updatedAt: new Date() }) + .where(eq(routines.id, repaired.routineId!)); + + const reconciled = await services.routines.managedReconcile({ companyId, routineKey: "nightly-lint" }); + expect(reconciled.status).toBe("resolved"); + expect(reconciled.routine?.title).toBe("Operator renamed lint"); + }); + + it("creates routine operation issues with plugin visibility and managed project scoping", async () => { + const { companyId, services } = await seedCompanyAndPlugin(); + const agent = await services.agents.managedReconcile({ companyId, agentKey: "wiki-maintainer" }); + const project = await services.projects.reconcileManaged({ companyId, projectKey: "operations" }); + const routine = await services.routines.managedReconcile({ companyId, routineKey: "nightly-lint" }); + const wakeup = vi.fn(async () => ({ id: randomUUID() })); + const routinesSvc = routineService(db, { heartbeat: { wakeup } }); + + const run = await routinesSvc.runRoutine(routine.routineId!, { source: "manual" }, { userId: "board-user" }); + + expect(run.status).toBe("issue_created"); + const [issue] = await db.select().from(issues).where(eq(issues.id, run.linkedIssueId!)); + expect(issue).toMatchObject({ + originKind: "plugin:paperclip.managed-routines-test:operation", + originId: "operation:nightly-lint", + billingCode: "plugin-test:nightly-lint", + projectId: project.projectId, + assigneeAgentId: agent.agentId, + }); + expect(wakeup).toHaveBeenCalledWith(agent.agentId, expect.objectContaining({ + reason: "issue_assigned", + })); + }); +}); diff --git a/server/src/__tests__/plugin-orchestration-apis.test.ts b/server/src/__tests__/plugin-orchestration-apis.test.ts index b8ee02e9..402419c6 100644 --- a/server/src/__tests__/plugin-orchestration-apis.test.ts +++ b/server/src/__tests__/plugin-orchestration-apis.test.ts @@ -1,4 +1,7 @@ import { randomUUID } from "node:crypto"; +import { promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; import { and, eq } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { @@ -11,6 +14,9 @@ import { heartbeatRuns, issueRelations, issues, + pluginManagedResources, + plugins, + projects, } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, @@ -45,6 +51,7 @@ if (!embeddedPostgresSupport.supported) { describeEmbeddedPostgres("plugin orchestration APIs", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; + const tempRoots: string[] = []; beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("paperclip-plugin-orchestration-"); @@ -52,12 +59,17 @@ describeEmbeddedPostgres("plugin orchestration APIs", () => { }, 20_000); afterEach(async () => { + await Promise.all(tempRoots.map((root) => fs.rm(root, { recursive: true, force: true }))); + tempRoots.length = 0; await db.delete(activityLog); await db.delete(costEvents); await db.delete(heartbeatRuns); await db.delete(agentWakeupRequests); await db.delete(issueRelations); await db.delete(issues); + await db.delete(pluginManagedResources); + await db.delete(projects); + await db.delete(plugins); await db.delete(agents); await db.delete(companies); }); @@ -89,6 +101,12 @@ describeEmbeddedPostgres("plugin orchestration APIs", () => { return { companyId, agentId }; } + async function makeLocalRoot() { + const root = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-plugin-host-folder-")); + tempRoots.push(root); + return root; + } + it("creates plugin-origin issues with full orchestration fields and audit activity", async () => { const { companyId, agentId } = await seedCompanyAndAgent(); const blockerIssueId = randomUUID(); @@ -189,6 +207,293 @@ describeEmbeddedPostgres("plugin orchestration APIs", () => { ).rejects.toThrow("Plugin may only use originKind values under plugin:paperclip.missions"); }); + it("creates plugin operation issues with the generic operation origin", async () => { + const { companyId } = await seedCompanyAndAgent(); + const services = buildHostServices(db, "plugin-record-id", "paperclip.missions", createEventBusStub()); + + const issue = await services.issues.create({ + companyId, + title: "Background operation", + surfaceVisibility: "plugin_operation", + originId: "mission-alpha:operation-1", + }); + + expect(issue.originKind).toBe("plugin:paperclip.missions:operation"); + expect(issue.originId).toBe("mission-alpha:operation-1"); + }); + + it("lets bootstrap-style actions initialize required local folders from an empty root", async () => { + const { companyId } = await seedCompanyAndAgent(); + const pluginId = randomUUID(); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "paperclipai.plugin-llm-wiki", + packageName: "@paperclipai/plugin-llm-wiki", + version: "0.1.0", + manifestJson: { + id: "paperclipai.plugin-llm-wiki", + apiVersion: 1, + version: "0.1.0", + displayName: "LLM Wiki", + description: "Local-file LLM Wiki plugin", + author: "Paperclip", + categories: ["automation"], + capabilities: ["local.folders"], + entrypoints: { worker: "./dist/worker.js" }, + localFolders: [ + { + folderKey: "wiki-root", + displayName: "Wiki root", + access: "readWrite", + requiredDirectories: ["raw", "wiki", "wiki/concepts", ".paperclip"], + requiredFiles: ["WIKI.md", "AGENTS.md"], + }, + ], + }, + status: "ready", + }); + const root = await makeLocalRoot(); + const services = buildHostServices( + db, + pluginId, + "paperclipai.plugin-llm-wiki", + createEventBusStub(), + undefined, + { + manifest: { + id: "paperclipai.plugin-llm-wiki", + apiVersion: 1, + version: "0.1.0", + displayName: "LLM Wiki", + description: "Local-file LLM Wiki plugin", + author: "Paperclip", + categories: ["automation"], + capabilities: ["local.folders"], + entrypoints: { worker: "./dist/worker.js" }, + localFolders: [ + { + folderKey: "wiki-root", + displayName: "Wiki root", + access: "readWrite", + requiredDirectories: ["raw", "wiki", "wiki/concepts", ".paperclip"], + requiredFiles: ["WIKI.md", "AGENTS.md"], + }, + ], + }, + }, + ); + + const configured = await services.localFolders.configure({ + companyId, + folderKey: "wiki-root", + path: root, + access: "readWrite", + requiredDirectories: ["raw", "wiki", "wiki/concepts", ".paperclip"], + requiredFiles: ["WIKI.md", "AGENTS.md"], + }); + expect(configured.healthy).toBe(false); + expect(configured.missingDirectories).toEqual([]); + expect(configured.missingFiles).toEqual(["WIKI.md", "AGENTS.md"]); + + await fs.rm(path.join(root, "raw"), { recursive: true, force: true }); + await fs.rm(path.join(root, "wiki"), { recursive: true, force: true }); + await expect(services.localFolders.readText({ companyId, folderKey: "wiki-root", relativePath: "WIKI.md" })) + .rejects.toThrow("Local folder is not healthy"); + await services.localFolders.writeTextAtomic({ + companyId, + folderKey: "wiki-root", + relativePath: "WIKI.md", + contents: "# Wiki\n", + }); + await services.localFolders.writeTextAtomic({ + companyId, + folderKey: "wiki-root", + relativePath: "AGENTS.md", + contents: "# Agents\n", + }); + + const finalStatus = await services.localFolders.status({ companyId, folderKey: "wiki-root" }); + expect(finalStatus.healthy).toBe(true); + await expect(fs.stat(path.join(root, "raw"))).resolves.toMatchObject({}); + await expect(fs.stat(path.join(root, "wiki/concepts"))).resolves.toMatchObject({}); + await expect(fs.readFile(path.join(root, "WIKI.md"), "utf8")).resolves.toBe("# Wiki\n"); + }); + + it("rejects worker local-folder access for undeclared manifest keys", async () => { + const { companyId } = await seedCompanyAndAgent(); + const pluginId = randomUUID(); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "paperclip.local-folders", + packageName: "@paperclip/plugin-local-folders", + version: "0.1.0", + manifestJson: { + id: "paperclip.local-folders", + apiVersion: 1, + version: "0.1.0", + displayName: "Local Folders", + description: "Local folder fixture", + author: "Paperclip", + categories: ["automation"], + capabilities: ["local.folders"], + entrypoints: { worker: "./dist/worker.js" }, + localFolders: [ + { + folderKey: "content-root", + displayName: "Content root", + access: "readWrite", + }, + ], + }, + status: "ready", + }); + const services = buildHostServices( + db, + pluginId, + "paperclip.local-folders", + createEventBusStub(), + undefined, + { + manifest: { + id: "paperclip.local-folders", + apiVersion: 1, + version: "0.1.0", + displayName: "Local Folders", + description: "Local folder fixture", + author: "Paperclip", + categories: ["automation"], + capabilities: ["local.folders"], + entrypoints: { worker: "./dist/worker.js" }, + localFolders: [ + { + folderKey: "content-root", + displayName: "Content root", + access: "readWrite", + }, + ], + }, + }, + ); + await expect(services.localFolders.configure({ + companyId, + folderKey: "ssh", + path: "/tmp", + access: "read", + })).rejects.toThrow("Local folder key is not declared"); + await expect(services.localFolders.status({ companyId, folderKey: "ssh" })) + .rejects.toThrow("Local folder key is not declared"); + await expect(services.localFolders.readText({ companyId, folderKey: "ssh", relativePath: "id_rsa" })) + .rejects.toThrow("Local folder key is not declared"); + await expect(services.localFolders.writeTextAtomic({ + companyId, + folderKey: "ssh", + relativePath: "id_rsa", + contents: "secret", + })).rejects.toThrow("Local folder key is not declared"); + }); + + it("resolves plugin-managed projects by stable key without overwriting user edits", async () => { + const { companyId } = await seedCompanyAndAgent(); + const pluginId = randomUUID(); + await db.insert(plugins).values({ + id: pluginId, + pluginKey: "paperclip.missions", + packageName: "@paperclip/plugin-missions", + version: "0.1.0", + apiVersion: 1, + categories: ["automation"], + status: "ready", + manifestJson: { + id: "paperclip.missions", + apiVersion: 1, + version: "0.1.0", + displayName: "Missions", + description: "Mission orchestration", + author: "Paperclip", + categories: ["automation"], + capabilities: ["projects.managed"], + entrypoints: { worker: "./dist/worker.js" }, + projects: [{ + projectKey: "operations", + displayName: "Mission Operations", + description: "Plugin operation inspection area", + status: "in_progress", + color: "#14b8a6", + settings: { surface: "operations" }, + }], + }, + }); + + const services = buildHostServices(db, pluginId, "paperclip.missions", createEventBusStub()); + const missing = await services.projects.getManaged({ companyId, projectKey: "operations" }); + expect(missing.status).toBe("missing"); + expect(missing.projectId).toBeNull(); + await expect( + db + .select() + .from(pluginManagedResources) + .where(and( + eq(pluginManagedResources.companyId, companyId), + eq(pluginManagedResources.pluginId, pluginId), + eq(pluginManagedResources.resourceKind, "project"), + eq(pluginManagedResources.resourceKey, "operations"), + )), + ).resolves.toHaveLength(0); + + const created = await services.projects.reconcileManaged({ companyId, projectKey: "operations" }); + + expect(created.status).toBe("created"); + expect(created.projectId).toEqual(expect.any(String)); + expect(created.project?.managedByPlugin).toMatchObject({ + pluginId, + pluginKey: "paperclip.missions", + pluginDisplayName: "Missions", + resourceKind: "project", + resourceKey: "operations", + }); + + await db + .update(projects) + .set({ name: "Renamed by operator", description: "User-owned text", updatedAt: new Date() }) + .where(eq(projects.id, created.projectId!)); + await db + .update(plugins) + .set({ + manifestJson: { + id: "paperclip.missions", + apiVersion: 1, + version: "0.2.0", + displayName: "Missions", + description: "Mission orchestration", + author: "Paperclip", + categories: ["automation"], + capabilities: ["projects.managed"], + entrypoints: { worker: "./dist/worker.js" }, + projects: [{ + projectKey: "operations", + displayName: "Upgraded Default Name", + description: "Upgraded default description", + status: "planned", + color: "#f97316", + settings: { surface: "operations", upgraded: true }, + }], + }, + updatedAt: new Date(), + }) + .where(eq(plugins.id, pluginId)); + + const resolved = await services.projects.reconcileManaged({ companyId, projectKey: "operations" }); + + expect(resolved.status).toBe("resolved"); + expect(resolved.projectId).toBe(created.projectId); + expect(resolved.project?.name).toBe("Renamed by operator"); + expect(resolved.project?.description).toBe("User-owned text"); + expect(resolved.project?.managedByPlugin?.defaultsJson).toMatchObject({ + displayName: "Upgraded Default Name", + settings: { upgraded: true }, + }); + }); + it("asserts checkout ownership for run-scoped plugin actions", async () => { const { companyId, agentId } = await seedCompanyAndAgent(); const issueId = randomUUID(); diff --git a/server/src/__tests__/plugin-routes-authz.test.ts b/server/src/__tests__/plugin-routes-authz.test.ts index df41f7ff..faf3487b 100644 --- a/server/src/__tests__/plugin-routes-authz.test.ts +++ b/server/src/__tests__/plugin-routes-authz.test.ts @@ -6,6 +6,8 @@ const mockRegistry = vi.hoisted(() => ({ getById: vi.fn(), getByKey: vi.fn(), upsertConfig: vi.fn(), + getCompanySettings: vi.fn(), + upsertCompanySettings: vi.fn(), })); const mockLifecycle = vi.hoisted(() => ({ @@ -317,6 +319,61 @@ describe.sequential("scoped plugin API routes", () => { }, 20_000); }); +describe.sequential("plugin local folder routes", () => { + beforeEach(() => { + vi.clearAllMocks(); + mockRegistry.getCompanySettings.mockResolvedValue(null); + }); + + function readyLocalFolderPlugin() { + mockRegistry.getById.mockResolvedValue({ + id: pluginId, + pluginKey: "paperclip.example", + version: "1.0.0", + status: "ready", + manifestJson: { + id: "paperclip.example", + capabilities: ["local.folders"], + localFolders: [ + { + folderKey: "content-root", + displayName: "Content root", + access: "readWrite", + requiredDirectories: ["docs"], + requiredFiles: ["README.md"], + }, + ], + }, + }); + } + + it("rejects validation for undeclared local folder keys", async () => { + readyLocalFolderPlugin(); + const { app } = await createApp(boardActor()); + + const res = await request(app) + .post(`/api/plugins/${pluginId}/companies/${companyA}/local-folders/ssh/validate`) + .send({ path: "/tmp" }); + + expect(res.status).toBe(400); + expect(res.body.error).toContain("Local folder key is not declared"); + expect(mockRegistry.upsertCompanySettings).not.toHaveBeenCalled(); + }); + + it("rejects saving undeclared local folder keys", async () => { + readyLocalFolderPlugin(); + const { app } = await createApp(boardActor()); + + const res = await request(app) + .put(`/api/plugins/${pluginId}/companies/${companyA}/local-folders/ssh`) + .send({ path: "/tmp" }); + + expect(res.status).toBe(400); + expect(res.body.error).toContain("Local folder key is not declared"); + expect(mockRegistry.upsertCompanySettings).not.toHaveBeenCalled(); + }); +}); + describe.sequential("plugin tool and bridge authz", () => { beforeEach(() => { vi.clearAllMocks(); diff --git a/server/src/__tests__/plugin-scoped-api-routes.test.ts b/server/src/__tests__/plugin-scoped-api-routes.test.ts index 1dab1edf..4e2ce297 100644 --- a/server/src/__tests__/plugin-scoped-api-routes.test.ts +++ b/server/src/__tests__/plugin-scoped-api-routes.test.ts @@ -98,6 +98,7 @@ describe.sequential("plugin scoped API routes", () => { const pluginId = "11111111-1111-4111-8111-111111111111"; const companyId = "22222222-2222-4222-8222-222222222222"; const agentId = "33333333-3333-4333-8333-333333333333"; + const peerAgentId = "33333333-3333-4333-8333-333333333334"; const runId = "44444444-4444-4444-8444-444444444444"; const issueId = "55555555-5555-4555-8555-555555555555"; @@ -252,6 +253,55 @@ describe.sequential("plugin scoped API routes", () => { })); }); + it("allows non-assignee agents on in-progress required checkout routes without claiming checkout ownership", async () => { + const apiRoutes = manifest([ + { + routeKey: "issue.advance", + method: "POST", + path: "/issues/:issueId/advance", + auth: "agent", + capability: "api.routes.register", + checkoutPolicy: "required-for-agent-in-progress", + companyResolution: { from: "issue", param: "issueId" }, + }, + ]); + mockIssueService.getById.mockResolvedValue({ + id: issueId, + companyId, + status: "in_progress", + assigneeAgentId: agentId, + }); + const { app, workerManager } = await createApp({ + actor: { + type: "agent", + agentId: peerAgentId, + companyId, + runId, + source: "agent_key", + }, + plugin: { + id: pluginId, + pluginKey: apiRoutes.id, + status: "ready", + manifestJson: apiRoutes, + }, + }); + + const res = await request(app) + .post(`/api/plugins/${pluginId}/api/issues/${issueId}/advance`) + .send({ step: "next" }); + + expect(res.status).toBe(200); + expect(mockIssueService.assertCheckoutOwner).not.toHaveBeenCalled(); + expect(workerManager.call).toHaveBeenCalledWith(pluginId, "handleApiRequest", expect.objectContaining({ + routeKey: "issue.advance", + params: { issueId }, + body: { step: "next" }, + actor: expect.objectContaining({ actorType: "agent", agentId: peerAgentId, runId }), + companyId, + })); + }); + it("rejects checkout-protected agent routes without a run id before worker dispatch", async () => { const apiRoutes = manifest([ { diff --git a/server/src/__tests__/plugin-sdk-orchestration-contract.test.ts b/server/src/__tests__/plugin-sdk-orchestration-contract.test.ts index d68cb60a..957262d8 100644 --- a/server/src/__tests__/plugin-sdk-orchestration-contract.test.ts +++ b/server/src/__tests__/plugin-sdk-orchestration-contract.test.ts @@ -150,6 +150,23 @@ describe("plugin SDK orchestration contract", () => { ).rejects.toThrow("Plugin may only use originKind values under plugin:paperclip.test-orchestration"); }); + it("supports generic plugin operation issue visibility in the test harness", async () => { + const companyId = randomUUID(); + const harness = createTestHarness({ + manifest: manifest(["issues.create"]), + }); + + const created = await harness.ctx.issues.create({ + companyId, + title: "Background operation", + surfaceVisibility: "plugin_operation", + originId: "operation-1", + }); + + expect(created.originKind).toBe("plugin:paperclip.test-orchestration:operation"); + expect(created.originId).toBe("operation-1"); + }); + it("enforces checkout and wakeup capabilities in the test harness", async () => { const companyId = randomUUID(); const agentId = randomUUID(); diff --git a/server/src/app.ts b/server/src/app.ts index 6b273573..d94475af 100644 --- a/server/src/app.ts +++ b/server/src/app.ts @@ -253,6 +253,8 @@ export async function createApp( instanceInfo: { instanceId: opts.instanceId ?? "default", hostVersion: opts.hostVersion ?? "0.0.0", + deploymentMode: opts.deploymentMode, + deploymentExposure: opts.deploymentExposure, }, buildHostHandlers: (pluginId, manifest) => { const notifyWorker = (method: string, params: unknown) => { @@ -261,6 +263,7 @@ export async function createApp( }; const services = buildHostServices(db, pluginId, manifest.id, eventBus, notifyWorker, { pluginWorkerManager: workerManager, + manifest, }); hostServicesDisposers.set(pluginId, () => services.dispose()); return createHostClientHandlers({ diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts index f37fbaa8..5887df79 100644 --- a/server/src/routes/issues.ts +++ b/server/src/routes/issues.ts @@ -1020,11 +1020,14 @@ export function issueRoutes( descendantOf: req.query.descendantOf as string | undefined, labelId: req.query.labelId as string | undefined, originKind: req.query.originKind as string | undefined, + originKindPrefix: req.query.originKindPrefix as string | undefined, originId: req.query.originId as string | undefined, includeRoutineExecutions: req.query.includeRoutineExecutions === "true" || req.query.includeRoutineExecutions === "1", excludeRoutineExecutions: req.query.excludeRoutineExecutions === "true" || req.query.excludeRoutineExecutions === "1", + includePluginOperations: + req.query.includePluginOperations === "true" || req.query.includePluginOperations === "1", includeBlockedBy: req.query.includeBlockedBy === "true" || req.query.includeBlockedBy === "1", q: req.query.q as string | undefined, limit, diff --git a/server/src/routes/plugins.ts b/server/src/routes/plugins.ts index 43a6e7dd..35b60812 100644 --- a/server/src/routes/plugins.ts +++ b/server/src/routes/plugins.ts @@ -66,6 +66,13 @@ import { getActorInfo, } from "./authz.js"; import { validateInstanceConfig } from "../services/plugin-config-validator.js"; +import { + findLocalFolderDeclaration, + getStoredLocalFolders, + inspectPluginLocalFolder, + requireLocalFolderDeclaration, + setStoredLocalFolder, +} from "../services/plugin-local-folders.js"; import { badRequest, forbidden, notFound, unauthorized, unprocessable } from "../errors.js"; /** UI slot declaration extracted from plugin manifest */ @@ -2379,6 +2386,152 @@ export function pluginRoutes( } }); + // =========================================================================== + // Company-scoped trusted local folders + // =========================================================================== + + router.get("/plugins/:pluginId/companies/:companyId/local-folders", async (req, res) => { + assertBoardOrgAccess(req); + const { pluginId, companyId } = req.params; + assertCompanyAccess(req, companyId); + + const plugin = await resolvePlugin(registry, pluginId); + if (!plugin) { + res.status(404).json({ error: "Plugin not found" }); + return; + } + + const settings = await registry.getCompanySettings(plugin.id, companyId); + const storedFolders = getStoredLocalFolders(settings?.settingsJson); + const declarations = plugin.manifestJson.localFolders ?? []; + const folderKeys = declarations.map((declaration) => declaration.folderKey); + + const statuses = await Promise.all(folderKeys.map((folderKey) => + inspectPluginLocalFolder({ + folderKey, + declaration: findLocalFolderDeclaration(declarations, folderKey), + storedConfig: storedFolders[folderKey] ?? null, + }))); + + res.json({ + pluginId: plugin.id, + companyId, + declarations, + folders: statuses, + }); + }); + + router.get("/plugins/:pluginId/companies/:companyId/local-folders/:folderKey/status", async (req, res) => { + assertBoardOrgAccess(req); + const { pluginId, companyId, folderKey } = req.params; + assertCompanyAccess(req, companyId); + + const plugin = await resolvePlugin(registry, pluginId); + if (!plugin) { + res.status(404).json({ error: "Plugin not found" }); + return; + } + + const settings = await registry.getCompanySettings(plugin.id, companyId); + const storedFolders = getStoredLocalFolders(settings?.settingsJson); + const declarations = plugin.manifestJson.localFolders ?? []; + const declaration = requireLocalFolderDeclaration(declarations, folderKey); + const status = await inspectPluginLocalFolder({ + folderKey, + declaration, + storedConfig: storedFolders[folderKey] ?? null, + }); + res.json(status); + }); + + router.post("/plugins/:pluginId/companies/:companyId/local-folders/:folderKey/validate", async (req, res) => { + assertBoardOrgAccess(req); + const { pluginId, companyId, folderKey } = req.params; + assertCompanyAccess(req, companyId); + + const plugin = await resolvePlugin(registry, pluginId); + if (!plugin) { + res.status(404).json({ error: "Plugin not found" }); + return; + } + + const body = req.body as { + path?: unknown; + access?: "read" | "readWrite"; + requiredDirectories?: string[]; + requiredFiles?: string[]; + } | undefined; + if (typeof body?.path !== "string" || body.path.trim().length === 0) { + res.status(400).json({ error: '"path" is required and must be a non-empty string' }); + return; + } + + const declaration = requireLocalFolderDeclaration(plugin.manifestJson.localFolders ?? [], folderKey); + const status = await inspectPluginLocalFolder({ + folderKey, + declaration, + overrideConfig: { + path: body.path, + }, + }); + res.json(status); + }); + + router.put("/plugins/:pluginId/companies/:companyId/local-folders/:folderKey", async (req, res) => { + assertBoardOrgAccess(req); + const { pluginId, companyId, folderKey } = req.params; + assertCompanyAccess(req, companyId); + + const plugin = await resolvePlugin(registry, pluginId); + if (!plugin) { + res.status(404).json({ error: "Plugin not found" }); + return; + } + + const body = req.body as { + path?: unknown; + access?: "read" | "readWrite"; + requiredDirectories?: string[]; + requiredFiles?: string[]; + } | undefined; + if (typeof body?.path !== "string" || body.path.trim().length === 0) { + res.status(400).json({ error: '"path" is required and must be a non-empty string' }); + return; + } + + const existing = await registry.getCompanySettings(plugin.id, companyId); + const declaration = requireLocalFolderDeclaration(plugin.manifestJson.localFolders ?? [], folderKey); + const status = await inspectPluginLocalFolder({ + folderKey, + declaration, + storedConfig: getStoredLocalFolders(existing?.settingsJson)[folderKey] ?? null, + overrideConfig: { + path: body.path, + }, + }); + + const nextSettings = setStoredLocalFolder(existing?.settingsJson, folderKey, { + path: body.path, + access: status.access, + requiredDirectories: status.requiredDirectories, + requiredFiles: status.requiredFiles, + }); + await registry.upsertCompanySettings(plugin.id, companyId, { + enabled: existing?.enabled ?? true, + settingsJson: nextSettings, + lastError: status.healthy ? null : status.problems.map((item: { message: string }) => item.message).join("; "), + }); + await logPluginMutationActivity(req, "plugin.local_folder.configured", plugin.id, { + pluginId: plugin.id, + pluginKey: plugin.pluginKey, + companyId, + folderKey, + healthy: status.healthy, + }); + + res.json(status); + }); + // =========================================================================== // Plugin health dashboard — aggregated diagnostics for the settings page // =========================================================================== diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts index 66c83389..adb49b33 100644 --- a/server/src/services/issues.ts +++ b/server/src/services/issues.ts @@ -1,5 +1,5 @@ import { Buffer } from "node:buffer"; -import { and, asc, desc, eq, gt, inArray, isNull, lt, ne, notInArray, or, sql } from "drizzle-orm"; +import { and, asc, desc, eq, gt, inArray, isNull, like, lt, ne, notInArray, or, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { activityLog, @@ -127,9 +127,11 @@ export interface IssueFilters { descendantOf?: string; labelId?: string; originKind?: string; + originKindPrefix?: string; originId?: string; includeRoutineExecutions?: boolean; excludeRoutineExecutions?: boolean; + includePluginOperations?: boolean; includeBlockedBy?: boolean; q?: string; limit?: number; @@ -563,6 +565,19 @@ function inboxVisibleForUserCondition(companyId: string, userId: string) { `; } +function nonPluginOperationIssueCondition() { + return sql`NOT (${issues.originKind} LIKE 'plugin:%:operation' OR ${issues.originKind} LIKE 'plugin:%:operation:%')`; +} + +function shouldIncludePluginOperationIssues(filters: IssueFilters | undefined) { + return Boolean( + filters?.includePluginOperations || + filters?.originKind || + filters?.originId || + filters?.projectId, + ); +} + /** Named entities commonly emitted in saved issue bodies; unknown `&name;` sequences are left unchanged. */ const WELL_KNOWN_NAMED_HTML_ENTITIES: Readonly> = { amp: "&", @@ -2201,7 +2216,11 @@ export function issueService(db: Db) { } if (filters?.parentId) conditions.push(eq(issues.parentId, filters.parentId)); if (filters?.originKind) conditions.push(eq(issues.originKind, filters.originKind)); + if (filters?.originKindPrefix) conditions.push(like(issues.originKind, `${filters.originKindPrefix}%`)); if (filters?.originId) conditions.push(eq(issues.originId, filters.originId)); + if (!shouldIncludePluginOperationIssues(filters)) { + conditions.push(nonPluginOperationIssueCondition()); + } if (filters?.labelId) { const labeledIssueIds = await db .select({ issueId: issueLabels.issueId }) @@ -2333,6 +2352,7 @@ export function issueService(db: Db) { const conditions = [ eq(issues.companyId, companyId), isNull(issues.hiddenAt), + nonPluginOperationIssueCondition(), unreadForUserCondition(companyId, userId), ]; if (status) { diff --git a/server/src/services/plugin-capability-validator.ts b/server/src/services/plugin-capability-validator.ts index e69332ca..ef54d61d 100644 --- a/server/src/services/plugin-capability-validator.ts +++ b/server/src/services/plugin-capability-validator.ts @@ -47,6 +47,12 @@ const OPERATION_CAPABILITIES: Record = { "companies.get": ["companies.read"], "projects.list": ["projects.read"], "projects.get": ["projects.read"], + "projects.managed.get": ["projects.managed"], + "projects.managed.reconcile": ["projects.managed"], + "projects.managed.reset": ["projects.managed"], + "routines.managed.get": ["routines.managed"], + "routines.managed.reconcile": ["routines.managed"], + "routines.managed.reset": ["routines.managed"], "project.workspaces.list": ["project.workspaces.read"], "project.workspaces.get": ["project.workspaces.read"], "issues.list": ["issues.read"], @@ -56,6 +62,9 @@ const OPERATION_CAPABILITIES: Record = { "issue.comments.get": ["issue.comments.read"], "agents.list": ["agents.read"], "agents.get": ["agents.read"], + "agents.managed.get": ["agents.managed"], + "agents.managed.reconcile": ["agents.managed"], + "agents.managed.reset": ["agents.managed"], "goals.list": ["goals.read"], "goals.get": ["goals.read"], "activity.list": ["activity.read"], @@ -65,6 +74,12 @@ const OPERATION_CAPABILITIES: Record = { "issues.summaries.getOrchestration": ["issues.orchestration.read"], "db.namespace": ["database.namespace.read"], "db.query": ["database.namespace.read"], + "localFolders.declarations": [], + "localFolders.configure": ["local.folders"], + "localFolders.status": ["local.folders"], + "localFolders.list": ["local.folders"], + "localFolders.readText": ["local.folders"], + "localFolders.writeTextAtomic": ["local.folders"], // Data write operations "issues.create": ["issues.create"], @@ -133,6 +148,7 @@ const UI_SLOT_CAPABILITIES: Record = { commentAnnotation: "ui.commentAnnotation.register", commentContextMenuItem: "ui.action.register", settingsPage: "instance.settings.register", + routeSidebar: "ui.sidebar.register", }; /** @@ -167,6 +183,9 @@ const FEATURE_CAPABILITIES: Record = { webhooks: "webhooks.receive", database: "database.namespace.migrate", environmentDrivers: "environment.drivers.register", + agents: "agents.managed", + projects: "projects.managed", + routines: "routines.managed", }; // --------------------------------------------------------------------------- diff --git a/server/src/services/plugin-database.ts b/server/src/services/plugin-database.ts index e1822e45..e6811702 100644 --- a/server/src/services/plugin-database.ts +++ b/server/src/services/plugin-database.ts @@ -303,7 +303,19 @@ function resolveMigrationsDir(packageRoot: string, migrationsDir: string): strin return resolvedDir; } -export function pluginDatabaseService(db: Db) { +type PluginDatabaseClient = Pick; +type PluginDatabaseRootClient = PluginDatabaseClient & Partial>; + +export interface ApplyPluginMigrationsOptions { + /** + * Persist failed migration ledger rows. Fresh install uses false because the + * caller owns a larger transaction and must roll back the plugin row and + * namespace together. + */ + persistFailure?: boolean; +} + +export function pluginDatabaseService(db: PluginDatabaseRootClient) { async function getPluginRecord(pluginId: string) { const rows = await db.select().from(plugins).where(eq(plugins.id, pluginId)).limit(1); const plugin = rows[0]; @@ -311,14 +323,18 @@ export function pluginDatabaseService(db: Db) { return plugin; } - async function ensureNamespace(pluginId: string, manifest: PaperclipPluginManifestV1) { + async function ensureNamespaceWithClient( + client: PluginDatabaseClient, + pluginId: string, + manifest: PaperclipPluginManifestV1, + ) { if (!manifest.database) return null; const namespaceName = derivePluginDatabaseNamespace( manifest.id, manifest.database.namespaceSlug, ); - await db.execute(sql.raw(`CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier(namespaceName)}`)); - const rows = await db + await client.execute(sql.raw(`CREATE SCHEMA IF NOT EXISTS ${quoteIdentifier(namespaceName)}`)); + const rows = await client .insert(pluginDatabaseNamespaces) .values({ pluginId, @@ -341,6 +357,10 @@ export function pluginDatabaseService(db: Db) { return rows[0] ?? null; } + async function ensureNamespace(pluginId: string, manifest: PaperclipPluginManifestV1) { + return ensureNamespaceWithClient(db, pluginId, manifest); + } + async function getNamespace(pluginId: string) { const rows = await db .select() @@ -358,7 +378,7 @@ export function pluginDatabaseService(db: Db) { return namespace.namespaceName; } - async function recordMigrationFailure(input: { + async function recordMigrationFailure(client: PluginDatabaseClient, input: { pluginId: string; pluginKey: string; namespaceName: string; @@ -368,7 +388,7 @@ export function pluginDatabaseService(db: Db) { error: unknown; }): Promise { const message = input.error instanceof Error ? input.error.message : String(input.error); - await db + await client .insert(pluginMigrations) .values({ pluginId: input.pluginId, @@ -391,7 +411,7 @@ export function pluginDatabaseService(db: Db) { appliedAt: null, }, }); - await db + await client .update(pluginDatabaseNamespaces) .set({ status: "migration_failed", updatedAt: new Date() }) .where(eq(pluginDatabaseNamespaces.pluginId, input.pluginId)); @@ -400,7 +420,12 @@ export function pluginDatabaseService(db: Db) { return { ensureNamespace, - async applyMigrations(pluginId: string, manifest: PaperclipPluginManifestV1, packageRoot: string) { + async applyMigrations( + pluginId: string, + manifest: PaperclipPluginManifestV1, + packageRoot: string, + options: ApplyPluginMigrationsOptions = {}, + ) { if (!manifest.database) return null; const namespace = await ensureNamespace(pluginId, manifest); if (!namespace) return null; @@ -409,13 +434,14 @@ export function pluginDatabaseService(db: Db) { const migrationFiles = await listSqlMigrationFiles(migrationDir); const coreReadTables = manifest.database.coreReadTables ?? []; const lockKey = Number.parseInt(createHash("sha256").update(pluginId).digest("hex").slice(0, 12), 16); + const persistFailure = options.persistFailure ?? true; - await db.transaction(async (tx) => { - await tx.execute(sql`SELECT pg_advisory_xact_lock(${lockKey})`); + const applyWithClient = async (client: PluginDatabaseClient) => { + await client.execute(sql`SELECT pg_advisory_xact_lock(${lockKey})`); for (const migrationKey of migrationFiles) { const content = await readFile(path.join(migrationDir, migrationKey), "utf8"); const checksum = createHash("sha256").update(content).digest("hex"); - const existingRows = await tx + const existingRows = await client .select() .from(pluginMigrations) .where(and(eq(pluginMigrations.pluginId, pluginId), eq(pluginMigrations.migrationKey, migrationKey))) @@ -435,9 +461,9 @@ export function pluginDatabaseService(db: Db) { } for (const statement of statements) { validatePluginMigrationStatement(statement, namespace.namespaceName, coreReadTables); - await tx.execute(sql.raw(statement)); + await client.execute(sql.raw(statement)); } - await tx + await client .insert(pluginMigrations) .values({ pluginId, @@ -461,19 +487,27 @@ export function pluginDatabaseService(db: Db) { }, }); } catch (error) { - await recordMigrationFailure({ - pluginId, - pluginKey: manifest.id, - namespaceName: namespace.namespaceName, - migrationKey, - checksum, - pluginVersion: manifest.version, - error, - }); + if (persistFailure) { + await recordMigrationFailure(db, { + pluginId, + pluginKey: manifest.id, + namespaceName: namespace.namespaceName, + migrationKey, + checksum, + pluginVersion: manifest.version, + error, + }); + } throw error; } } - }); + }; + + if (typeof db.transaction === "function") { + await db.transaction(async (tx) => applyWithClient(tx as PluginDatabaseClient)); + } else { + await applyWithClient(db); + } return namespace; }, diff --git a/server/src/services/plugin-host-services.ts b/server/src/services/plugin-host-services.ts index fbb2dc05..e97ab8d0 100644 --- a/server/src/services/plugin-host-services.ts +++ b/server/src/services/plugin-host-services.ts @@ -22,6 +22,7 @@ import type { PluginIssueOrchestrationSummary, } from "@paperclipai/plugin-sdk"; import type { CreateIssueThreadInteraction, IssueDocumentSummary } from "@paperclipai/shared"; +import { pluginOperationIssueOriginKind } from "@paperclipai/shared"; import { companyService } from "./companies.js"; import { agentService } from "./agents.js"; import { projectService } from "./projects.js"; @@ -34,12 +35,27 @@ import { budgetService } from "./budgets.js"; import { issueApprovalService } from "./issue-approvals.js"; import { subscribeCompanyLiveEvents } from "./live-events.js"; import { randomUUID } from "node:crypto"; +import path from "node:path"; import { activityService } from "./activity.js"; import { costService } from "./costs.js"; import { assetService } from "./assets.js"; import { pluginRegistryService } from "./plugin-registry.js"; import { pluginStateStore } from "./plugin-state-store.js"; import { pluginDatabaseService } from "./plugin-database.js"; +import { pluginManagedAgentService } from "./plugin-managed-agents.js"; +import { pluginManagedRoutineService } from "./plugin-managed-routines.js"; +import { + assertConfiguredLocalFolder, + assertWritableConfiguredLocalFolder, + getStoredLocalFolders, + inspectPluginLocalFolder, + listPluginLocalFolderEntries, + preparePluginLocalFolder, + readPluginLocalFolderText, + requireLocalFolderDeclaration, + setStoredLocalFolder, + writePluginLocalFolderTextAtomic, +} from "./plugin-local-folders.js"; import { createPluginSecretsHandler } from "./plugin-secrets-handler.js"; import { logActivity } from "./activity-log.js"; import type { PluginEventBus } from "./plugin-event-bus.js"; @@ -460,7 +476,7 @@ export function buildHostServices( pluginKey: string, eventBus: PluginEventBus, notifyWorker?: (method: string, params: unknown) => void, - options: { pluginWorkerManager?: PluginWorkerManager } = {}, + options: { pluginWorkerManager?: PluginWorkerManager; manifest?: import("@paperclipai/shared").PaperclipPluginManifestV1 } = {}, ): HostServices & { dispose(): void } { const registry = pluginRegistryService(db); const stateStore = pluginStateStore(db); @@ -468,6 +484,31 @@ export function buildHostServices( const secretsHandler = createPluginSecretsHandler({ db, pluginId }); const companies = companyService(db); const agents = agentService(db); + const managedAgents = pluginManagedAgentService(db, { + pluginId, + pluginKey, + manifest: options.manifest, + instructionTemplateVariables: async (companyId) => { + const variables: Record = {}; + for (const declaration of options.manifest?.localFolders ?? []) { + const status = await inspectPluginLocalFolder({ + folderKey: declaration.folderKey, + declaration, + storedConfig: await getStoredLocalFolderConfig(companyId, declaration.folderKey), + }); + const prefix = `localFolders.${declaration.folderKey}`; + variables[`${prefix}.path`] = status.realPath ?? status.path ?? null; + variables[`${prefix}.agentsPath`] = status.realPath ? path.join(status.realPath, "AGENTS.md") : null; + } + return variables; + }, + }); + const managedRoutines = pluginManagedRoutineService(db, { + pluginId, + pluginKey, + manifest: options.manifest, + pluginWorkerManager: options.pluginWorkerManager, + }); const heartbeat = heartbeatService(db, { pluginWorkerManager: options.pluginWorkerManager, }); @@ -518,6 +559,23 @@ export function buildHostServices( */ const ensurePluginAvailableForCompany = async (_companyId: string) => {}; + const getLocalFolderDeclaration = (folderKey: string) => + requireLocalFolderDeclaration(options.manifest?.localFolders, folderKey); + + const getStoredLocalFolderConfig = async (companyId: string, folderKey: string) => { + ensureCompanyId(companyId); + await ensurePluginAvailableForCompany(companyId); + const settings = await registry.getCompanySettings(pluginId, companyId); + return getStoredLocalFolders(settings?.settingsJson)[folderKey] ?? null; + }; + + const inspectStoredLocalFolder = async (companyId: string, folderKey: string) => + inspectPluginLocalFolder({ + folderKey, + declaration: getLocalFolderDeclaration(folderKey), + storedConfig: await getStoredLocalFolderConfig(companyId, folderKey), + }); + const inCompany = ( record: T | null | undefined, companyId: string, @@ -752,6 +810,86 @@ export function buildHostServices( }, }, + localFolders: { + async declarations() { + return options.manifest?.localFolders ?? []; + }, + + async configure(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + const declaration = getLocalFolderDeclaration(params.folderKey); + const existing = await registry.getCompanySettings(pluginId, companyId); + const existingConfig = getStoredLocalFolders(existing?.settingsJson)[params.folderKey] ?? null; + await preparePluginLocalFolder({ + folderKey: params.folderKey, + declaration, + storedConfig: existingConfig, + overrideConfig: { + path: params.path, + }, + }); + const status = await inspectPluginLocalFolder({ + folderKey: params.folderKey, + declaration, + storedConfig: existingConfig, + overrideConfig: { + path: params.path, + }, + }); + + const nextSettings = setStoredLocalFolder(existing?.settingsJson, params.folderKey, { + path: params.path, + access: status.access, + requiredDirectories: status.requiredDirectories, + requiredFiles: status.requiredFiles, + }); + await registry.upsertCompanySettings(pluginId, companyId, { + enabled: existing?.enabled ?? true, + settingsJson: nextSettings, + lastError: status.healthy ? null : status.problems.map((item: { message: string }) => item.message).join("; "), + }); + return status; + }, + + async status(params) { + return inspectStoredLocalFolder(params.companyId, params.folderKey); + }, + + async list(params) { + const status = await inspectStoredLocalFolder(params.companyId, params.folderKey); + assertConfiguredLocalFolder(status); + const listing = await listPluginLocalFolderEntries(status.realPath!, { + relativePath: params.relativePath, + recursive: params.recursive, + maxEntries: params.maxEntries, + }); + return { ...listing, folderKey: params.folderKey }; + }, + + async readText(params) { + const status = await inspectStoredLocalFolder(params.companyId, params.folderKey); + assertConfiguredLocalFolder(status); + return readPluginLocalFolderText(status.realPath!, params.relativePath); + }, + + async writeTextAtomic(params) { + const companyId = ensureCompanyId(params.companyId); + await preparePluginLocalFolder({ + folderKey: params.folderKey, + declaration: getLocalFolderDeclaration(params.folderKey), + storedConfig: await getStoredLocalFolderConfig(companyId, params.folderKey), + }); + const status = await inspectStoredLocalFolder(companyId, params.folderKey); + assertWritableConfiguredLocalFolder(status); + if (status.access !== "readWrite" || !status.writable) { + throw new Error("Local folder is not configured for writes"); + } + await writePluginLocalFolderTextAtomic(status.realPath!, params.relativePath, params.contents); + return inspectStoredLocalFolder(companyId, params.folderKey); + }, + }, + state: { async get(params) { return stateStore.get(pluginId, params.scopeKind as any, params.stateKey, { @@ -1013,6 +1151,77 @@ export function buildHostServices( updatedAt: (row?.updatedAt ?? project.updatedAt).toISOString(), }; }, + async getManaged(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return projects.resolveManagedProject({ + companyId, + pluginId, + pluginKey, + projectKey: params.projectKey, + createIfMissing: false, + }); + }, + async reconcileManaged(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return projects.resolveManagedProject({ + companyId, + pluginId, + pluginKey, + projectKey: params.projectKey, + }); + }, + async resetManaged(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return projects.resolveManagedProject({ + companyId, + pluginId, + pluginKey, + projectKey: params.projectKey, + reset: true, + }); + }, + }, + + routines: { + async managedGet(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return managedRoutines.get(params.routineKey, companyId); + }, + async managedReconcile(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return managedRoutines.reconcile(params.routineKey, companyId, { + assigneeAgentId: params.assigneeAgentId, + projectId: params.projectId, + }); + }, + async managedReset(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return managedRoutines.reset(params.routineKey, companyId, { + assigneeAgentId: params.assigneeAgentId, + projectId: params.projectId, + }); + }, + async managedUpdate(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return managedRoutines.update(params.routineKey, companyId, { + status: params.status, + }); + }, + async managedRun(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return managedRoutines.run(params.routineKey, companyId, { + assigneeAgentId: params.assigneeAgentId, + projectId: params.projectId, + }); + }, }, issues: { @@ -1031,8 +1240,12 @@ export function buildHostServices( async create(params) { const companyId = ensureCompanyId(params.companyId); await ensurePluginAvailableForCompany(companyId); - const { actorAgentId, actorUserId, actorRunId, originKind, ...issueInput } = params; - const normalizedOriginKind = normalizePluginOriginKind(originKind); + const { actorAgentId, actorUserId, actorRunId, originKind, surfaceVisibility, ...issueInput } = params; + const normalizedOriginKind = normalizePluginOriginKind( + surfaceVisibility === "plugin_operation" && !originKind + ? pluginOperationIssueOriginKind(pluginKey) + : originKind, + ); const issue = (await issues.create(companyId, { ...(issueInput as any), originKind: normalizedOriginKind, @@ -1641,6 +1854,21 @@ export function buildHostServices( if (!run) throw new Error("Agent wakeup was skipped by heartbeat policy"); return { runId: run.id }; }, + async managedGet(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return managedAgents.get(params.agentKey, companyId); + }, + async managedReconcile(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return managedAgents.reconcile(params.agentKey, companyId); + }, + async managedReset(params) { + const companyId = ensureCompanyId(params.companyId); + await ensurePluginAvailableForCompany(companyId); + return managedAgents.reset(params.agentKey, companyId); + }, }, goals: { diff --git a/server/src/services/plugin-loader.ts b/server/src/services/plugin-loader.ts index 95801180..073620b4 100644 --- a/server/src/services/plugin-loader.ts +++ b/server/src/services/plugin-loader.ts @@ -29,7 +29,7 @@ import { readdir, readFile, rm, stat } from "node:fs/promises"; import { execFile } from "node:child_process"; import os from "node:os"; import path from "node:path"; -import { fileURLToPath } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; import { promisify } from "node:util"; import type { Db } from "@paperclipai/db"; import type { @@ -248,6 +248,8 @@ export interface PluginRuntimeServices { instanceInfo: { instanceId: string; hostVersion: string; + deploymentMode?: "local_trusted" | "authenticated"; + deploymentExposure?: "private" | "public"; }; } @@ -932,7 +934,10 @@ export function pluginLoader( try { // Dynamic import works for both .js (ESM) and .cjs (CJS) manifests - const mod = await import(manifestPath) as Record; + const manifestUrl = pathToFileURL(manifestPath); + const manifestStat = await stat(manifestPath); + manifestUrl.searchParams.set("mtime", String(Math.trunc(manifestStat.mtimeMs))); + const mod = await import(manifestUrl.href) as Record; // The manifest may be the default export or the module itself raw = mod["default"] ?? mod; } catch (err) { @@ -944,6 +949,51 @@ export function pluginLoader( return manifestValidator.parseOrThrow(raw); } + async function loadManifestFromPackageRoot( + packageRoot: string, + ): Promise { + const pkgJson = await readPackageJson(packageRoot); + if (!pkgJson) return null; + + const manifestPath = resolveManifestPath(packageRoot, pkgJson); + if (!manifestPath || !existsSync(manifestPath)) return null; + + return loadManifestFromPath(manifestPath); + } + + async function refreshPluginManifestFromPackage( + plugin: PluginRecord, + packageRoot: string, + ): Promise { + const manifest = await loadManifestFromPackageRoot(packageRoot); + if (!manifest) { + throw new Error(`Plugin package ${plugin.packageName} no longer exposes a Paperclip manifest`); + } + if (manifest.id !== plugin.pluginKey) { + throw new Error( + `Plugin manifest ID '${manifest.id}' does not match installed plugin '${plugin.pluginKey}'`, + ); + } + + if (JSON.stringify(manifest) === JSON.stringify(plugin.manifestJson)) { + return plugin; + } + + await registry.update(plugin.id, { + packageName: plugin.packageName, + version: manifest.version, + manifest, + }); + + return { + ...plugin, + version: manifest.version, + apiVersion: manifest.apiVersion, + categories: manifest.categories, + manifestJson: manifest, + }; + } + /** * Build a DiscoveredPlugin from a resolved package directory, or null * if the package is not a Paperclip plugin. @@ -1256,22 +1306,43 @@ export function pluginLoader( async installPlugin(installOptions: PluginInstallOptions): Promise { const discovered = await fetchAndValidate(installOptions); + const manifest = discovered.manifest!; - // Step 6: Persist install record in Postgres (include packagePath for local installs so the worker can be resolved) - await registry.install( - { - packageName: discovered.packageName, - packagePath: discovered.source === "local-filesystem" ? discovered.packagePath : undefined, - }, - discovered.manifest!, - ); + // Step 6: Persist install record and apply plugin-owned schema migrations + // in one database transaction. If migration validation fails, the plugin + // row, namespace record, migration ledger, and created schema all roll back. + const installDb = manifest.database ? migrationDb : db; + await installDb.transaction(async (tx) => { + const txDb = tx as unknown as Db; + const txRegistry = pluginRegistryService(txDb); + const installed = await txRegistry.install( + { + packageName: discovered.packageName, + packagePath: discovered.source === "local-filesystem" ? discovered.packagePath : undefined, + }, + manifest, + ); + + if (!installed) { + throw new Error(`Plugin install did not return a registry row: ${manifest.id}`); + } + + if (manifest.database) { + await pluginDatabaseService(txDb).applyMigrations( + installed.id, + manifest, + discovered.packagePath, + { persistFailure: false }, + ); + } + }); log.info( { - pluginId: discovered.manifest!.id, + pluginId: manifest.id, packageName: discovered.packageName, version: discovered.version, - capabilities: discovered.manifest!.capabilities, + capabilities: manifest.capabilities, }, "plugin-loader: plugin installed successfully", ); @@ -1663,9 +1734,10 @@ export function pluginLoader( * `error` in the database when activation fails. */ async function activatePlugin(plugin: PluginRecord): Promise { - const manifest = plugin.manifestJson; const pluginId = plugin.id; const pluginKey = plugin.pluginKey; + let activePlugin = plugin; + let manifest = activePlugin.manifestJson; const registered: PluginLoadResult["registered"] = { worker: false, @@ -1705,8 +1777,10 @@ export function pluginLoader( // ------------------------------------------------------------------ // 1. Resolve worker entrypoint // ------------------------------------------------------------------ - const workerEntrypoint = resolveWorkerEntrypoint(plugin, localPluginDir); - const packageRoot = resolvePluginPackageRoot(plugin, localPluginDir); + const packageRoot = resolvePluginPackageRoot(activePlugin, localPluginDir); + activePlugin = await refreshPluginManifestFromPackage(activePlugin, packageRoot); + manifest = activePlugin.manifestJson; + const workerEntrypoint = resolveWorkerEntrypoint(activePlugin, localPluginDir); // ------------------------------------------------------------------ // 2. Apply restricted database migrations before worker startup @@ -1746,12 +1820,16 @@ export function pluginLoader( databaseNamespace, hostHandlers, autoRestart: true, + env: { + PAPERCLIP_DEPLOYMENT_MODE: instanceInfo.deploymentMode ?? "", + PAPERCLIP_DEPLOYMENT_EXPOSURE: instanceInfo.deploymentExposure ?? "", + }, }; // Repo-local plugin installs can resolve workspace TS sources at runtime // (for example @paperclipai/shared exports). Run those workers through // the tsx loader so first-party example plugins work in development. - if (plugin.packagePath && existsSync(DEV_TSX_LOADER_PATH)) { + if (activePlugin.packagePath && existsSync(DEV_TSX_LOADER_PATH)) { workerOptions.execArgv = ["--import", DEV_TSX_LOADER_PATH]; } @@ -1842,13 +1920,13 @@ export function pluginLoader( { pluginId, pluginKey, - version: plugin.version, + version: activePlugin.version, registered, }, "plugin-loader: plugin activated successfully", ); - return { plugin, success: true, registered }; + return { plugin: activePlugin, success: true, registered }; } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); @@ -1872,7 +1950,7 @@ export function pluginLoader( } return { - plugin, + plugin: activePlugin, success: false, error: errorMessage, registered, diff --git a/server/src/services/plugin-local-folders.ts b/server/src/services/plugin-local-folders.ts new file mode 100644 index 00000000..8aa590d7 --- /dev/null +++ b/server/src/services/plugin-local-folders.ts @@ -0,0 +1,564 @@ +import { constants as fsConstants, promises as fs } from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { randomUUID } from "node:crypto"; +import type { + PluginLocalFolderDeclaration, + PluginLocalFolderEntry, + PluginLocalFolderListing, + PluginLocalFolderProblem, + PluginLocalFolderStatus, +} from "@paperclipai/plugin-sdk"; +import { badRequest, forbidden, notFound } from "../errors.js"; + +export interface StoredPluginLocalFolderConfig { + path: string; + access?: "read" | "readWrite"; + requiredDirectories?: string[]; + requiredFiles?: string[]; + updatedAt?: string; +} + +export interface PluginLocalFolderSettingsJson { + localFolders?: Record; + [key: string]: unknown; +} + +const LOCAL_FOLDER_KEY_PATTERN = /^[a-z0-9][a-z0-9._:-]*$/; + +function problem( + code: PluginLocalFolderProblem["code"], + message: string, + problemPath?: string, +): PluginLocalFolderProblem { + return { code, message, path: problemPath }; +} + +export function assertPluginLocalFolderKey(folderKey: string) { + if (!LOCAL_FOLDER_KEY_PATTERN.test(folderKey)) { + throw badRequest("folderKey must start with a lowercase alphanumeric and contain only lowercase letters, digits, dots, colons, underscores, or hyphens"); + } +} + +export function findLocalFolderDeclaration( + declarations: PluginLocalFolderDeclaration[] | undefined, + folderKey: string, +) { + return declarations?.find((declaration) => declaration.folderKey === folderKey) ?? null; +} + +export function requireLocalFolderDeclaration( + declarations: PluginLocalFolderDeclaration[] | undefined, + folderKey: string, +) { + assertPluginLocalFolderKey(folderKey); + const declaration = findLocalFolderDeclaration(declarations, folderKey); + if (!declaration) { + throw badRequest("Local folder key is not declared by this plugin manifest"); + } + return declaration; +} + +function normalizeRelativePath(relativePath: string): string { + if ( + !relativePath || + path.isAbsolute(relativePath) || + relativePath.includes("\\") || + relativePath.split("/").some((segment) => segment === "" || segment === "." || segment === "..") + ) { + throw forbidden("Local folder relative paths must stay inside the configured root"); + } + return relativePath; +} + +function validateRequiredPath(pathValue: string, label: string): string { + try { + return normalizeRelativePath(pathValue); + } catch { + throw badRequest(`${label} must contain only relative paths without traversal, empty segments, or backslashes`); + } +} + +function normalizeListRelativePath(relativePath: string | null | undefined): string | null { + const trimmed = relativePath?.trim(); + if (!trimmed) return null; + return normalizeRelativePath(trimmed); +} + +function normalizeMaxEntries(value: number | undefined): number { + if (typeof value !== "number" || !Number.isFinite(value)) return 1000; + return Math.max(1, Math.min(5000, Math.floor(value))); +} + +function mergeFolderConfig( + declaration: PluginLocalFolderDeclaration | null, + stored: StoredPluginLocalFolderConfig | null, + override?: Partial, +): StoredPluginLocalFolderConfig | null { + const pathValue = override?.path ?? stored?.path; + if (!pathValue) return null; + return { + path: pathValue, + access: declaration?.access ?? override?.access ?? stored?.access ?? "readWrite", + requiredDirectories: + declaration?.requiredDirectories ?? override?.requiredDirectories ?? stored?.requiredDirectories ?? [], + requiredFiles: + declaration?.requiredFiles ?? override?.requiredFiles ?? stored?.requiredFiles ?? [], + updatedAt: stored?.updatedAt, + }; +} + +export function getStoredLocalFolders(settingsJson: Record | null | undefined) { + const folders = (settingsJson as PluginLocalFolderSettingsJson | undefined)?.localFolders; + if (!folders || typeof folders !== "object") return {}; + return folders; +} + +export function setStoredLocalFolder( + settingsJson: Record | null | undefined, + folderKey: string, + config: StoredPluginLocalFolderConfig, +): PluginLocalFolderSettingsJson { + return { + ...(settingsJson ?? {}), + localFolders: { + ...getStoredLocalFolders(settingsJson), + [folderKey]: { + ...config, + updatedAt: new Date().toISOString(), + }, + }, + }; +} + +export async function inspectPluginLocalFolder(input: { + folderKey: string; + declaration?: PluginLocalFolderDeclaration | null; + storedConfig?: StoredPluginLocalFolderConfig | null; + overrideConfig?: Partial; +}): Promise { + assertPluginLocalFolderKey(input.folderKey); + const config = mergeFolderConfig( + input.declaration ?? null, + input.storedConfig ?? null, + input.overrideConfig, + ); + const access = config?.access ?? input.declaration?.access ?? "readWrite"; + const requiredDirectories = (config?.requiredDirectories ?? []).map((item) => + validateRequiredPath(item, "requiredDirectories"), + ); + const requiredFiles = (config?.requiredFiles ?? []).map((item) => + validateRequiredPath(item, "requiredFiles"), + ); + const checkedAt = new Date().toISOString(); + + if (!config?.path) { + return { + folderKey: input.folderKey, + configured: false, + path: null, + realPath: null, + access, + readable: false, + writable: false, + requiredDirectories, + requiredFiles, + missingDirectories: requiredDirectories, + missingFiles: requiredFiles, + healthy: false, + problems: [problem("not_configured", "No local folder path is configured.")], + checkedAt, + }; + } + + const configuredPath = path.resolve(config.path); + const problems: PluginLocalFolderProblem[] = []; + const missingDirectories: string[] = []; + const missingFiles: string[] = []; + const markRequiredPathsMissing = () => { + missingDirectories.push(...requiredDirectories); + missingFiles.push(...requiredFiles); + }; + let realPath: string | null = null; + let readable = false; + let writable = false; + + if (!path.isAbsolute(config.path)) { + problems.push(problem("not_absolute", "Local folder path must be absolute.", config.path)); + } + + try { + const stat = await fs.stat(configuredPath); + if (!stat.isDirectory()) { + problems.push(problem("not_directory", "Configured local folder path is not a directory.", configuredPath)); + markRequiredPathsMissing(); + } else { + realPath = await fs.realpath(configuredPath); + try { + await fs.access(realPath, fsConstants.R_OK); + readable = true; + } catch { + problems.push(problem("not_readable", "Configured local folder is not readable.", configuredPath)); + } + + if (access === "readWrite") { + try { + await fs.access(realPath, fsConstants.W_OK); + const probePath = path.join(realPath, `.paperclip-local-folder-probe-${process.pid}-${Date.now()}`); + await fs.writeFile(probePath, ""); + await fs.rm(probePath, { force: true }); + writable = true; + } catch { + problems.push(problem("not_writable", "Configured local folder is not writable.", configuredPath)); + } + } + + for (const requiredDir of requiredDirectories) { + const requiredStatus = await inspectChildPath(realPath, requiredDir, "directory"); + if (!requiredStatus.exists) { + missingDirectories.push(requiredDir); + problems.push(problem("missing_directory", "Required directory is missing.", requiredDir)); + } else if (!requiredStatus.contained) { + problems.push(problem("symlink_escape", "Required directory escapes the configured root.", requiredDir)); + } else if (!requiredStatus.matchesKind) { + missingDirectories.push(requiredDir); + problems.push(problem("missing_directory", "Required path is not a directory.", requiredDir)); + } + } + + for (const requiredFile of requiredFiles) { + const requiredStatus = await inspectChildPath(realPath, requiredFile, "file"); + if (!requiredStatus.exists) { + missingFiles.push(requiredFile); + problems.push(problem("missing_file", "Required file is missing.", requiredFile)); + } else if (!requiredStatus.contained) { + problems.push(problem("symlink_escape", "Required file escapes the configured root.", requiredFile)); + } else if (!requiredStatus.matchesKind) { + missingFiles.push(requiredFile); + problems.push(problem("missing_file", "Required path is not a file.", requiredFile)); + } + } + } + } catch (error) { + const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : ""; + problems.push(problem(code === "ENOENT" ? "missing" : "not_readable", "Configured local folder cannot be inspected.", configuredPath)); + if (code === "ENOENT") { + markRequiredPathsMissing(); + } + } + + return { + folderKey: input.folderKey, + configured: true, + path: configuredPath, + realPath, + access, + readable, + writable: access === "read" ? false : writable, + requiredDirectories, + requiredFiles, + missingDirectories, + missingFiles, + healthy: + problems.length === 0 && + readable && + (access === "read" || writable), + problems, + checkedAt, + }; +} + +function isInsideRoot(rootRealPath: string, candidateRealPath: string) { + const relative = path.relative(rootRealPath, candidateRealPath); + return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative)); +} + +async function assertPathInsideRoot(rootRealPath: string, candidatePath: string) { + const candidateRealPath = await fs.realpath(candidatePath); + if (!isInsideRoot(rootRealPath, candidateRealPath)) { + throw forbidden("Local folder symlink escape is not allowed"); + } + return candidateRealPath; +} + +async function ensureDirectoryInsideRoot(rootRealPath: string, relativePath: string) { + const normalized = normalizeRelativePath(relativePath); + const segments = normalized.split("/"); + let currentRealPath = rootRealPath; + + for (const segment of segments) { + const nextPath = path.join(currentRealPath, segment); + try { + const stat = await fs.stat(nextPath); + if (!stat.isDirectory()) { + throw badRequest("Required directory path exists but is not a directory"); + } + } catch (error) { + const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : ""; + if (code !== "ENOENT") throw error; + await fs.mkdir(nextPath); + } + + const nextRealPath = await fs.realpath(nextPath); + if (!isInsideRoot(rootRealPath, nextRealPath)) { + throw forbidden("Local folder symlink escape is not allowed"); + } + currentRealPath = nextRealPath; + } +} + +export async function preparePluginLocalFolder(input: { + folderKey: string; + declaration?: PluginLocalFolderDeclaration | null; + storedConfig?: StoredPluginLocalFolderConfig | null; + overrideConfig?: Partial; +}) { + assertPluginLocalFolderKey(input.folderKey); + const config = mergeFolderConfig( + input.declaration ?? null, + input.storedConfig ?? null, + input.overrideConfig, + ); + const access = config?.access ?? input.declaration?.access ?? "readWrite"; + if (!config?.path || access !== "readWrite" || !path.isAbsolute(config.path)) return; + + const configuredPath = path.resolve(config.path); + try { + const stat = await fs.stat(configuredPath); + if (!stat.isDirectory()) return; + } catch (error) { + const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : ""; + if (code !== "ENOENT") return; + try { + await fs.mkdir(configuredPath, { recursive: true }); + } catch { + return; + } + } + const rootRealPath = await fs.realpath(configuredPath); + + for (const requiredDir of config.requiredDirectories ?? []) { + await ensureDirectoryInsideRoot(rootRealPath, validateRequiredPath(requiredDir, "requiredDirectories")); + } +} + +async function inspectChildPath( + rootRealPath: string, + relativePath: string, + kind: "directory" | "file", +) { + let resolvedPath: Awaited>; + try { + resolvedPath = await resolvePluginLocalFolderPath(rootRealPath, relativePath, { + mustExist: true, + allowMissingLeaf: true, + }); + } catch { + return { exists: true, contained: false, matchesKind: false }; + } + if (!resolvedPath.exists) { + return { exists: false, contained: true, matchesKind: false }; + } + const stat = await fs.stat(resolvedPath.realPath); + return { + exists: true, + contained: true, + matchesKind: kind === "directory" ? stat.isDirectory() : stat.isFile(), + }; +} + +export async function resolvePluginLocalFolderPath( + rootPath: string, + relativePath: string, + options?: { mustExist?: boolean; allowMissingLeaf?: boolean }, +) { + const normalized = normalizeRelativePath(relativePath); + const rootRealPath = await fs.realpath(rootPath); + const absolutePath = path.resolve(rootRealPath, normalized); + const relativeFromRoot = path.relative(rootRealPath, absolutePath); + if (relativeFromRoot.startsWith("..") || path.isAbsolute(relativeFromRoot)) { + throw forbidden("Local folder path traversal is not allowed"); + } + + try { + const realPath = await fs.realpath(absolutePath); + const realRelative = path.relative(rootRealPath, realPath); + if (realRelative.startsWith("..") || path.isAbsolute(realRelative)) { + throw forbidden("Local folder symlink escape is not allowed"); + } + return { absolutePath, realPath, exists: true }; + } catch (error) { + const code = typeof error === "object" && error && "code" in error ? String((error as { code?: unknown }).code) : ""; + if (code !== "ENOENT" || options?.mustExist) { + if (options?.allowMissingLeaf && code === "ENOENT") { + return { absolutePath, realPath: absolutePath, exists: false }; + } + throw error; + } + + const parentRealPath = await fs.realpath(path.dirname(absolutePath)); + const parentRelative = path.relative(rootRealPath, parentRealPath); + if (parentRelative.startsWith("..") || path.isAbsolute(parentRelative)) { + throw forbidden("Local folder symlink escape is not allowed"); + } + return { absolutePath, realPath: absolutePath, exists: false }; + } +} + +export async function readPluginLocalFolderText(rootPath: string, relativePath: string) { + const resolved = await resolvePluginLocalFolderPath(rootPath, relativePath, { mustExist: true }); + const stat = await fs.stat(resolved.realPath); + if (!stat.isFile()) { + throw badRequest("Local folder read target must be a file"); + } + return fs.readFile(resolved.realPath, "utf8"); +} + +export async function listPluginLocalFolderEntries( + rootPath: string, + options: { relativePath?: string | null; recursive?: boolean; maxEntries?: number } = {}, +): Promise { + const rootRealPath = await fs.realpath(rootPath); + const relativePath = normalizeListRelativePath(options.relativePath); + const target = relativePath + ? await resolvePluginLocalFolderPath(rootRealPath, relativePath, { mustExist: true }) + : { absolutePath: rootRealPath, realPath: rootRealPath, exists: true }; + const targetStat = await fs.stat(target.realPath); + if (!targetStat.isDirectory()) { + throw badRequest("Local folder list target must be a directory"); + } + + const maxEntries = normalizeMaxEntries(options.maxEntries); + const entries: PluginLocalFolderEntry[] = []; + let truncated = false; + + const visit = async (directoryRealPath: string, directoryRelativePath: string | null) => { + if (truncated) return; + const dirents = await fs.readdir(directoryRealPath, { withFileTypes: true }); + dirents.sort((a, b) => a.name.localeCompare(b.name)); + + for (const dirent of dirents) { + if (entries.length >= maxEntries) { + truncated = true; + return; + } + + const childRelativePath = directoryRelativePath ? `${directoryRelativePath}/${dirent.name}` : dirent.name; + let resolvedChild: Awaited>; + try { + resolvedChild = await resolvePluginLocalFolderPath(rootRealPath, childRelativePath, { mustExist: true }); + } catch { + continue; + } + + const stat = await fs.stat(resolvedChild.realPath).catch(() => null); + if (!stat) continue; + const kind = stat.isDirectory() ? "directory" : stat.isFile() ? "file" : null; + if (!kind) continue; + + entries.push({ + path: childRelativePath, + name: dirent.name, + kind, + size: kind === "file" ? stat.size : null, + modifiedAt: stat.mtime.toISOString(), + }); + + if (options.recursive && kind === "directory") { + await visit(resolvedChild.realPath, childRelativePath); + if (truncated) return; + } + } + }; + + await visit(target.realPath, relativePath); + return { + folderKey: "list-result", + relativePath, + entries, + truncated, + }; +} + +export async function writePluginLocalFolderTextAtomic( + rootPath: string, + relativePath: string, + contents: string, +) { + const rootRealPath = await fs.realpath(rootPath); + const resolved = await resolvePluginLocalFolderPath(rootPath, relativePath); + await fs.mkdir(path.dirname(resolved.absolutePath), { recursive: true }); + await assertPathInsideRoot(rootRealPath, path.dirname(resolved.absolutePath)); + const tempPath = path.join( + path.dirname(resolved.absolutePath), + `.paperclip-${path.basename(resolved.absolutePath)}-${process.pid}-${randomUUID()}.tmp`, + ); + let tempCreated = false; + try { + const handle = await fs.open(tempPath, "wx"); + tempCreated = true; + try { + await assertPathInsideRoot(rootRealPath, tempPath); + await handle.writeFile(contents, "utf8"); + await handle.sync(); + } finally { + await handle.close(); + } + } catch (error) { + if (tempCreated) { + await fs.rm(tempPath, { force: true }); + } + throw error; + } + + try { + await resolvePluginLocalFolderPath(rootRealPath, relativePath); + await fs.rename(tempPath, resolved.absolutePath); + await resolvePluginLocalFolderPath(rootRealPath, relativePath, { mustExist: true }); + } catch (error) { + await fs.rm(tempPath, { force: true }); + throw error; + } + + if (process.platform !== "win32") { + const dirHandle = await fs.open(path.dirname(resolved.absolutePath), "r"); + try { + await dirHandle.sync(); + } finally { + await dirHandle.close(); + } + } + + return inspectPluginLocalFolder({ + folderKey: "write-result", + storedConfig: { + path: rootPath, + access: "readWrite", + }, + }); +} + +export function defaultLocalFolderBasePath(pluginKey: string, companyId: string) { + return path.join(os.homedir(), ".paperclip", "plugin-data", companyId, pluginKey); +} + +export function assertConfiguredLocalFolder(status: PluginLocalFolderStatus) { + if (!status.configured || !status.realPath || !status.readable) { + throw notFound("Local folder is not configured or readable"); + } + if (!status.healthy) { + throw badRequest("Local folder is not healthy"); + } +} + +export function assertWritableConfiguredLocalFolder(status: PluginLocalFolderStatus) { + if (!status.configured || !status.realPath || !status.readable) { + throw notFound("Local folder is not configured or readable"); + } + const onlyMissingRequiredPaths = status.problems.every((item) => + item.code === "missing_directory" || item.code === "missing_file" + ); + if (!status.healthy && !onlyMissingRequiredPaths) { + throw badRequest("Local folder is not healthy"); + } +} diff --git a/server/src/services/plugin-managed-agents.ts b/server/src/services/plugin-managed-agents.ts new file mode 100644 index 00000000..84a55c3a --- /dev/null +++ b/server/src/services/plugin-managed-agents.ts @@ -0,0 +1,508 @@ +import { and, eq, ne } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { + agents, + companies, + pluginEntities, + pluginManagedResources, +} from "@paperclipai/db"; +import type { + Agent, + PaperclipPluginManifestV1, + PluginManagedAgentDeclaration, + PluginManagedAgentResolution, +} from "@paperclipai/shared"; +import { notFound } from "../errors.js"; +import { agentService } from "./agents.js"; +import { approvalService } from "./approvals.js"; +import { logActivity } from "./activity-log.js"; +import { agentInstructionsService } from "./agent-instructions.js"; + +const MANAGED_AGENT_ENTITY_TYPE = "managed_agent"; +const DEFAULT_MANAGED_AGENT_ADAPTER_TYPE = "process"; + +interface PluginManagedAgentServiceOptions { + pluginId: string; + pluginKey: string; + manifest?: PaperclipPluginManifestV1 | null; + instructionTemplateVariables?: (companyId: string) => Promise>; +} + +function bindingExternalId(companyId: string, agentKey: string) { + return `managed:agent:${companyId}:${agentKey}`; +} + +function managedMetadata( + pluginId: string, + pluginKey: string, + declaration: PluginManagedAgentDeclaration, + existing?: Record | null, +) { + return { + ...(existing ?? {}), + paperclipManagedResource: { + pluginId, + pluginKey, + resourceKind: "agent", + resourceKey: declaration.agentKey, + }, + pluginManagedAgent: { + pluginId, + pluginKey, + agentKey: declaration.agentKey, + displayName: declaration.displayName, + instructions: declaration.instructions ?? null, + }, + }; +} + +function normalizeAdapterType(value: unknown) { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + +function fallbackAdapterType(declaration: PluginManagedAgentDeclaration) { + return normalizeAdapterType(declaration.adapterType) ?? DEFAULT_MANAGED_AGENT_ADAPTER_TYPE; +} + +function adapterPreference(declaration: PluginManagedAgentDeclaration) { + const seen = new Set(); + const preference: string[] = []; + for (const value of declaration.adapterPreference ?? []) { + const adapterType = normalizeAdapterType(value); + if (!adapterType || seen.has(adapterType)) continue; + seen.add(adapterType); + preference.push(adapterType); + } + return preference; +} + +function selectPreferredAdapterType( + declaration: PluginManagedAgentDeclaration, + usage: Array<{ adapterType: string; count: number }>, +) { + const fallback = fallbackAdapterType(declaration); + const preference = adapterPreference(declaration); + if (preference.length === 0) return fallback; + + const rank = new Map(preference.map((adapterType, index) => [adapterType, index])); + let selected: { adapterType: string; count: number; rank: number } | null = null; + for (const entry of usage) { + const adapterRank = rank.get(entry.adapterType); + if (adapterRank === undefined) continue; + if ( + !selected || + entry.count > selected.count || + (entry.count === selected.count && adapterRank < selected.rank) + ) { + selected = { ...entry, rank: adapterRank }; + } + } + return selected?.adapterType ?? fallback; +} + +function declarationPatch(declaration: PluginManagedAgentDeclaration, input: { adapterType?: string } = {}) { + return { + name: declaration.displayName, + role: declaration.role ?? "general", + title: declaration.title ?? null, + icon: declaration.icon ?? null, + capabilities: declaration.capabilities ?? null, + adapterType: input.adapterType ?? fallbackAdapterType(declaration), + adapterConfig: declaration.adapterConfig ?? {}, + runtimeConfig: declaration.runtimeConfig ?? {}, + permissions: declaration.permissions ?? {}, + budgetMonthlyCents: declaration.budgetMonthlyCents ?? 0, + }; +} + +function applyInstructionTemplateVariables( + content: string, + variables: Record, +) { + let next = content; + for (const [key, value] of Object.entries(variables)) { + next = next.replaceAll(`{{${key}}}`, value?.trim() || "(not configured)"); + } + return next; +} + +function rowIsManagedAgent( + row: typeof agents.$inferSelect, + pluginKey: string, + agentKey: string, +) { + const metadata = row.metadata; + if (!metadata || typeof metadata !== "object" || Array.isArray(metadata)) return false; + const marker = (metadata as Record).paperclipManagedResource; + if (!marker || typeof marker !== "object" || Array.isArray(marker)) return false; + const record = marker as Record; + return ( + record.pluginKey === pluginKey + && record.resourceKind === "agent" + && record.resourceKey === agentKey + ); +} + +export function pluginManagedAgentService( + db: Db, + options: PluginManagedAgentServiceOptions, +) { + const agentSvc = agentService(db); + const approvalSvc = approvalService(db); + const instructions = agentInstructionsService(); + + function declarationFor(agentKey: string) { + const declaration = options.manifest?.agents?.find((agent) => agent.agentKey === agentKey); + if (!declaration) { + throw notFound(`Managed agent declaration not found: ${agentKey}`); + } + return declaration; + } + + async function getBinding(companyId: string, agentKey: string) { + return db + .select() + .from(pluginEntities) + .where( + and( + eq(pluginEntities.pluginId, options.pluginId), + eq(pluginEntities.entityType, MANAGED_AGENT_ENTITY_TYPE), + eq(pluginEntities.externalId, bindingExternalId(companyId, agentKey)), + ), + ) + .then((rows) => rows[0] ?? null); + } + + async function upsertBinding( + companyId: string, + declaration: PluginManagedAgentDeclaration, + agentId: string, + extraData: Record = {}, + effectiveAdapterType?: string, + ) { + const adapterType = effectiveAdapterType ?? (await resolveManagedAdapterType(companyId, declaration)); + const defaultsJson = { + agentKey: declaration.agentKey, + displayName: declaration.displayName, + role: declaration.role ?? "general", + title: declaration.title ?? null, + icon: declaration.icon ?? null, + capabilities: declaration.capabilities ?? null, + adapterType, + adapterPreference: declaration.adapterPreference ?? null, + adapterConfig: declaration.adapterConfig ?? {}, + runtimeConfig: declaration.runtimeConfig ?? {}, + permissions: declaration.permissions ?? {}, + budgetMonthlyCents: declaration.budgetMonthlyCents ?? 0, + instructions: declaration.instructions ?? null, + }; + const managedResource = await db + .select({ id: pluginManagedResources.id }) + .from(pluginManagedResources) + .where(and( + eq(pluginManagedResources.companyId, companyId), + eq(pluginManagedResources.pluginId, options.pluginId), + eq(pluginManagedResources.resourceKind, "agent"), + eq(pluginManagedResources.resourceKey, declaration.agentKey), + )) + .then((rows) => rows[0] ?? null); + if (managedResource) { + await db + .update(pluginManagedResources) + .set({ resourceId: agentId, defaultsJson, updatedAt: new Date() }) + .where(eq(pluginManagedResources.id, managedResource.id)); + } else { + await db.insert(pluginManagedResources).values({ + companyId, + pluginId: options.pluginId, + pluginKey: options.pluginKey, + resourceKind: "agent", + resourceKey: declaration.agentKey, + resourceId: agentId, + defaultsJson, + }); + } + + const externalId = bindingExternalId(companyId, declaration.agentKey); + const data = { + pluginKey: options.pluginKey, + resourceKind: "agent", + resourceKey: declaration.agentKey, + agentId, + adapterType, + declarationSnapshot: declaration, + lastReconciledAt: new Date().toISOString(), + ...extraData, + }; + const existing = await getBinding(companyId, declaration.agentKey); + if (existing) { + return db + .update(pluginEntities) + .set({ + scopeKind: "company", + scopeId: companyId, + title: declaration.displayName, + status: "resolved", + data, + updatedAt: new Date(), + }) + .where(eq(pluginEntities.id, existing.id)) + .returning() + .then((rows) => rows[0]); + } + return db + .insert(pluginEntities) + .values({ + pluginId: options.pluginId, + entityType: MANAGED_AGENT_ENTITY_TYPE, + scopeKind: "company", + scopeId: companyId, + externalId, + title: declaration.displayName, + status: "resolved", + data, + }) + .returning() + .then((rows) => rows[0]); + } + + async function findRelinkCandidate(companyId: string, declaration: PluginManagedAgentDeclaration) { + const rows = await db + .select() + .from(agents) + .where(and(eq(agents.companyId, companyId), ne(agents.status, "terminated"))); + return rows.find((row) => rowIsManagedAgent(row, options.pluginKey, declaration.agentKey)) ?? null; + } + + async function companyAdapterUsage(companyId: string) { + const rows = await db + .select({ adapterType: agents.adapterType }) + .from(agents) + .where(and(eq(agents.companyId, companyId), ne(agents.status, "terminated"))); + const counts = new Map(); + for (const row of rows) { + const adapterType = normalizeAdapterType(row.adapterType); + if (!adapterType) continue; + counts.set(adapterType, (counts.get(adapterType) ?? 0) + 1); + } + return [...counts.entries()] + .map(([adapterType, count]) => ({ adapterType, count })) + .sort((a, b) => b.count - a.count || a.adapterType.localeCompare(b.adapterType)) + .slice(0, 10); + } + + async function resolveManagedAdapterType(companyId: string, declaration: PluginManagedAgentDeclaration) { + return selectPreferredAdapterType(declaration, await companyAdapterUsage(companyId)); + } + + async function materializeDeclaredInstructions( + companyId: string, + agent: Agent, + declaration: PluginManagedAgentDeclaration, + options: { replaceExisting: boolean }, + ): Promise { + const instructionDeclaration = declaration.instructions; + if (!instructionDeclaration?.content) return agent; + + const entryFile = instructionDeclaration.entryFile ?? "AGENTS.md"; + const variables = await optionsForInstructionVariables(companyId); + const materialized = await instructions.materializeManagedBundle( + agent, + { [entryFile]: applyInstructionTemplateVariables(instructionDeclaration.content, variables) }, + { + entryFile, + replaceExisting: options.replaceExisting, + clearLegacyPromptTemplate: true, + }, + ); + const updated = await agentSvc.update(agent.id, { + adapterConfig: materialized.adapterConfig, + }, { + recordRevision: { + source: `plugin:${optionsForRevisionSource()}:managed-agent-instructions`, + }, + }); + return (updated as Agent | null) ?? { ...agent, adapterConfig: materialized.adapterConfig }; + } + + async function optionsForInstructionVariables(companyId: string) { + return options.instructionTemplateVariables ? options.instructionTemplateVariables(companyId) : {}; + } + + function optionsForRevisionSource() { + return options.pluginKey; + } + + function resolution( + companyId: string, + declaration: PluginManagedAgentDeclaration, + agent: Agent | null, + status: PluginManagedAgentResolution["status"], + approvalId?: string | null, + ): PluginManagedAgentResolution { + return { + pluginKey: options.pluginKey, + resourceKind: "agent", + resourceKey: declaration.agentKey, + companyId, + agentId: agent?.id ?? null, + agent, + status, + approvalId: approvalId ?? null, + }; + } + + async function createManagedAgent(companyId: string, declaration: PluginManagedAgentDeclaration) { + const company = await db + .select() + .from(companies) + .where(eq(companies.id, companyId)) + .then((rows) => rows[0] ?? null); + if (!company) throw notFound("Company not found"); + + const requiresApproval = company.requireBoardApprovalForNewAgents; + const adapterType = await resolveManagedAdapterType(companyId, declaration); + let created = await agentSvc.create(companyId, { + ...declarationPatch(declaration, { adapterType }), + status: requiresApproval ? "pending_approval" : declaration.status ?? "idle", + metadata: managedMetadata(options.pluginId, options.pluginKey, declaration), + spentMonthlyCents: 0, + lastHeartbeatAt: null, + }) as Agent; + created = await materializeDeclaredInstructions(companyId, created, declaration, { replaceExisting: true }); + + let approvalId: string | null = null; + if (requiresApproval) { + const approval = await approvalSvc.create(companyId, { + type: "hire_agent", + requestedByAgentId: null, + requestedByUserId: null, + status: "pending", + payload: { + name: created.name, + role: created.role, + title: created.title, + icon: created.icon, + reportsTo: created.reportsTo, + capabilities: created.capabilities, + adapterType: created.adapterType, + adapterConfig: created.adapterConfig, + runtimeConfig: created.runtimeConfig, + budgetMonthlyCents: created.budgetMonthlyCents, + metadata: created.metadata, + agentId: created.id, + sourcePluginId: options.pluginId, + sourcePluginKey: options.pluginKey, + managedResourceKey: declaration.agentKey, + }, + decisionNote: null, + decidedByUserId: null, + decidedAt: null, + updatedAt: new Date(), + }); + approvalId = approval.id; + await logActivity(db, { + companyId, + actorType: "plugin", + actorId: options.pluginId, + action: "approval.created", + entityType: "approval", + entityId: approval.id, + details: { + type: "hire_agent", + linkedAgentId: created.id, + sourcePluginKey: options.pluginKey, + managedResourceKey: declaration.agentKey, + }, + }); + } + + await upsertBinding(companyId, declaration, created.id, { approvalId }, adapterType); + await logActivity(db, { + companyId, + actorType: "plugin", + actorId: options.pluginId, + action: "plugin.managed_agent.created", + entityType: "agent", + entityId: created.id, + details: { + sourcePluginKey: options.pluginKey, + managedResourceKey: declaration.agentKey, + adapterType, + requiresApproval, + approvalId, + }, + }); + return resolution(companyId, declaration, created as Agent, "created", approvalId); + } + + async function get(agentKey: string, companyId: string) { + const declaration = declarationFor(agentKey); + const binding = await getBinding(companyId, agentKey); + const boundAgentId = typeof binding?.data?.agentId === "string" ? binding.data.agentId : null; + if (!boundAgentId) return resolution(companyId, declaration, null, "missing"); + const agent = await agentSvc.getById(boundAgentId); + if (!agent || agent.companyId !== companyId || agent.status === "terminated") { + return resolution(companyId, declaration, null, "missing"); + } + return resolution(companyId, declaration, agent as Agent, "resolved"); + } + + async function reconcile(agentKey: string, companyId: string) { + const declaration = declarationFor(agentKey); + const current = await get(agentKey, companyId); + if (current.agent) { + await upsertBinding(companyId, declaration, current.agent.id); + return current; + } + + const relinkCandidate = await findRelinkCandidate(companyId, declaration); + if (relinkCandidate) { + await upsertBinding(companyId, declaration, relinkCandidate.id); + const agent = await agentSvc.getById(relinkCandidate.id); + return resolution(companyId, declaration, agent as Agent, "relinked"); + } + + return createManagedAgent(companyId, declaration); + } + + async function reset(agentKey: string, companyId: string) { + const declaration = declarationFor(agentKey); + const reconciled = await reconcile(agentKey, companyId); + if (!reconciled.agent) return reconciled; + const currentMetadata = reconciled.agent.metadata && typeof reconciled.agent.metadata === "object" + ? reconciled.agent.metadata + : {}; + const adapterType = await resolveManagedAdapterType(companyId, declaration); + const updated = await agentSvc.update(reconciled.agent.id, { + ...declarationPatch(declaration, { adapterType }), + metadata: managedMetadata(options.pluginId, options.pluginKey, declaration, currentMetadata), + }, { + recordRevision: { + source: `plugin:${options.pluginKey}:managed-agent-reset`, + }, + }); + if (!updated) throw notFound("Managed agent not found"); + const updatedAgent = await materializeDeclaredInstructions(companyId, updated as Agent, declaration, { replaceExisting: true }); + await upsertBinding(companyId, declaration, updatedAgent.id, {}, adapterType); + await logActivity(db, { + companyId, + actorType: "plugin", + actorId: options.pluginId, + action: "plugin.managed_agent.reset", + entityType: "agent", + entityId: updatedAgent.id, + details: { + sourcePluginKey: options.pluginKey, + managedResourceKey: declaration.agentKey, + }, + }); + return resolution(companyId, declaration, updatedAgent, "reset"); + } + + return { + get, + reconcile, + reset, + }; +} diff --git a/server/src/services/plugin-managed-routines.ts b/server/src/services/plugin-managed-routines.ts new file mode 100644 index 00000000..94027dd3 --- /dev/null +++ b/server/src/services/plugin-managed-routines.ts @@ -0,0 +1,523 @@ +import { and, eq } from "drizzle-orm"; +import type { Db } from "@paperclipai/db"; +import { + agents, + pluginManagedResources, + plugins, + projects, + routines, + routineTriggers, +} from "@paperclipai/db"; +import type { + CreateRoutineTrigger, + PluginManagedResourceRef, + PluginManagedRoutineDeclaration, + PluginManagedRoutineResolution, + Routine, + RoutineManagedByPlugin, + RoutineStatus, +} from "@paperclipai/shared"; +import { ROUTINE_STATUSES } from "@paperclipai/shared"; +import { notFound, unprocessable } from "../errors.js"; +import { logActivity } from "./activity-log.js"; +import { routineService } from "./routines.js"; +import type { PluginWorkerManager } from "./plugin-worker-manager.js"; + +const MANAGED_ROUTINE_RESOURCE_KIND = "routine"; + +interface PluginManagedRoutineServiceOptions { + pluginId: string; + pluginKey: string; + manifest?: import("@paperclipai/shared").PaperclipPluginManifestV1 | null; + pluginWorkerManager?: PluginWorkerManager; +} + +interface RoutineOverrides { + assigneeAgentId?: string | null; + projectId?: string | null; +} + +function buildRoutineDefaults(declaration: PluginManagedRoutineDeclaration) { + return { + routineKey: declaration.routineKey, + title: declaration.title, + description: declaration.description ?? null, + assigneeRef: declaration.assigneeRef ?? null, + projectRef: declaration.projectRef ?? null, + goalId: declaration.goalId ?? null, + status: declaration.status ?? null, + priority: declaration.priority ?? "medium", + concurrencyPolicy: declaration.concurrencyPolicy ?? "coalesce_if_active", + catchUpPolicy: declaration.catchUpPolicy ?? "skip_missed", + variables: declaration.variables ?? [], + triggers: declaration.triggers ?? [], + issueTemplate: declaration.issueTemplate ?? null, + }; +} + +function normalizeRef( + pluginKey: string, + ref: PluginManagedResourceRef | null | undefined, + resourceKind: "agent" | "project", +) { + if (!ref) return null; + if (ref.resourceKind !== resourceKind) { + throw unprocessable(`Managed routine ${resourceKind} ref must target ${resourceKind}`); + } + if (ref.pluginKey && ref.pluginKey !== pluginKey) { + throw unprocessable("Managed routine refs must target the declaring plugin"); + } + return { ...ref, pluginKey }; +} + +function managedByPlugin(row: { + id: string; + pluginId: string; + pluginKey: string; + manifestJson: { displayName?: string } | null; + resourceKey: string; + defaultsJson: Record; + createdAt: Date; + updatedAt: Date; +}): RoutineManagedByPlugin { + return { + id: row.id, + pluginId: row.pluginId, + pluginKey: row.pluginKey, + pluginDisplayName: row.manifestJson?.displayName ?? row.pluginKey, + resourceKind: "routine", + resourceKey: row.resourceKey, + defaultsJson: row.defaultsJson, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }; +} + +function triggerInput(trigger: NonNullable[number]): CreateRoutineTrigger { + if (trigger.kind === "schedule") { + if (!trigger.cronExpression) { + throw unprocessable("Managed schedule routine triggers require cronExpression"); + } + return { + kind: "schedule", + label: trigger.label ?? null, + enabled: trigger.enabled ?? true, + cronExpression: trigger.cronExpression, + timezone: trigger.timezone ?? "UTC", + }; + } + if (trigger.kind === "webhook") { + return { + kind: "webhook", + label: trigger.label ?? null, + enabled: trigger.enabled ?? true, + signingMode: (trigger.signingMode ?? "bearer") as Extract["signingMode"], + replayWindowSec: trigger.replayWindowSec ?? 300, + }; + } + return { + kind: "api", + label: trigger.label ?? null, + enabled: trigger.enabled ?? true, + }; +} + +export function pluginManagedRoutineService( + db: Db, + options: PluginManagedRoutineServiceOptions, +) { + const routinesSvc = routineService(db, { + pluginWorkerManager: options.pluginWorkerManager, + }); + + function declarationFor(routineKey: string) { + const declaration = options.manifest?.routines?.find((routine) => routine.routineKey === routineKey); + if (!declaration) { + throw notFound(`Managed routine declaration not found: ${routineKey}`); + } + return declaration; + } + + async function getBinding(companyId: string, routineKey: string) { + return db + .select({ + id: pluginManagedResources.id, + companyId: pluginManagedResources.companyId, + pluginId: pluginManagedResources.pluginId, + pluginKey: pluginManagedResources.pluginKey, + resourceKind: pluginManagedResources.resourceKind, + resourceKey: pluginManagedResources.resourceKey, + resourceId: pluginManagedResources.resourceId, + defaultsJson: pluginManagedResources.defaultsJson, + manifestJson: plugins.manifestJson, + createdAt: pluginManagedResources.createdAt, + updatedAt: pluginManagedResources.updatedAt, + }) + .from(pluginManagedResources) + .innerJoin(plugins, eq(pluginManagedResources.pluginId, plugins.id)) + .where( + and( + eq(pluginManagedResources.companyId, companyId), + eq(pluginManagedResources.pluginId, options.pluginId), + eq(pluginManagedResources.resourceKind, MANAGED_ROUTINE_RESOURCE_KIND), + eq(pluginManagedResources.resourceKey, routineKey), + ), + ) + .then((rows) => rows[0] ?? null); + } + + async function upsertBinding( + companyId: string, + declaration: PluginManagedRoutineDeclaration, + routineId: string, + ) { + const defaultsJson = buildRoutineDefaults(declaration); + const existing = await getBinding(companyId, declaration.routineKey); + if (existing) { + return db + .update(pluginManagedResources) + .set({ + resourceId: routineId, + defaultsJson, + updatedAt: new Date(), + }) + .where(eq(pluginManagedResources.id, existing.id)) + .returning() + .then((rows) => rows[0]); + } + return db + .insert(pluginManagedResources) + .values({ + companyId, + pluginId: options.pluginId, + pluginKey: options.pluginKey, + resourceKind: MANAGED_ROUTINE_RESOURCE_KIND, + resourceKey: declaration.routineKey, + resourceId: routineId, + defaultsJson, + }) + .returning() + .then((rows) => rows[0]); + } + + async function getRoutineWithManagedBy(companyId: string, declaration: PluginManagedRoutineDeclaration) { + const binding = await getBinding(companyId, declaration.routineKey); + if (!binding) return null; + const routine = await db + .select() + .from(routines) + .where(and(eq(routines.companyId, companyId), eq(routines.id, binding.resourceId))) + .then((rows) => rows[0] ?? null); + if (!routine) return null; + return { + ...routine, + managedByPlugin: managedByPlugin(binding), + } as Routine; + } + + async function resolveAgentId( + companyId: string, + declaration: PluginManagedRoutineDeclaration, + overrides?: RoutineOverrides, + ) { + if (overrides?.assigneeAgentId !== undefined) { + if (!overrides.assigneeAgentId) return { agentId: null, missingRef: null }; + const row = await db + .select({ id: agents.id }) + .from(agents) + .where(and(eq(agents.companyId, companyId), eq(agents.id, overrides.assigneeAgentId))) + .then((rows) => rows[0] ?? null); + if (!row) throw notFound("Assignee agent not found"); + return { agentId: row.id, missingRef: null }; + } + + const ref = normalizeRef(options.pluginKey, declaration.assigneeRef, "agent"); + if (!ref) return { agentId: null, missingRef: null }; + const binding = await db + .select({ resourceId: pluginManagedResources.resourceId }) + .from(pluginManagedResources) + .where( + and( + eq(pluginManagedResources.companyId, companyId), + eq(pluginManagedResources.pluginId, options.pluginId), + eq(pluginManagedResources.resourceKind, "agent"), + eq(pluginManagedResources.resourceKey, ref.resourceKey), + ), + ) + .then((rows) => rows[0] ?? null); + if (!binding) return { agentId: null, missingRef: ref }; + const row = await db + .select({ id: agents.id }) + .from(agents) + .where(and(eq(agents.companyId, companyId), eq(agents.id, binding.resourceId))) + .then((rows) => rows[0] ?? null); + return row ? { agentId: row.id, missingRef: null } : { agentId: null, missingRef: ref }; + } + + async function resolveProjectId( + companyId: string, + declaration: PluginManagedRoutineDeclaration, + overrides?: RoutineOverrides, + ) { + if (overrides?.projectId !== undefined) { + if (!overrides.projectId) return { projectId: null, missingRef: null }; + const row = await db + .select({ id: projects.id }) + .from(projects) + .where(and(eq(projects.companyId, companyId), eq(projects.id, overrides.projectId))) + .then((rows) => rows[0] ?? null); + if (!row) throw notFound("Project not found"); + return { projectId: row.id, missingRef: null }; + } + + const ref = normalizeRef(options.pluginKey, declaration.projectRef, "project"); + if (!ref) return { projectId: null, missingRef: null }; + const binding = await db + .select({ resourceId: pluginManagedResources.resourceId }) + .from(pluginManagedResources) + .where( + and( + eq(pluginManagedResources.companyId, companyId), + eq(pluginManagedResources.pluginId, options.pluginId), + eq(pluginManagedResources.resourceKind, "project"), + eq(pluginManagedResources.resourceKey, ref.resourceKey), + ), + ) + .then((rows) => rows[0] ?? null); + if (!binding) return { projectId: null, missingRef: ref }; + const row = await db + .select({ id: projects.id }) + .from(projects) + .where(and(eq(projects.companyId, companyId), eq(projects.id, binding.resourceId))) + .then((rows) => rows[0] ?? null); + return row ? { projectId: row.id, missingRef: null } : { projectId: null, missingRef: ref }; + } + + async function resolveRefs( + companyId: string, + declaration: PluginManagedRoutineDeclaration, + overrides?: RoutineOverrides, + ) { + const [agent, project] = await Promise.all([ + resolveAgentId(companyId, declaration, overrides), + resolveProjectId(companyId, declaration, overrides), + ]); + const missingRefs: PluginManagedResourceRef[] = []; + if (agent.missingRef) missingRefs.push(agent.missingRef); + if (project.missingRef) missingRefs.push(project.missingRef); + return { + assigneeAgentId: agent.agentId, + projectId: project.projectId, + missingRefs, + }; + } + + function resolution( + companyId: string, + declaration: PluginManagedRoutineDeclaration, + routine: Routine | null, + status: PluginManagedRoutineResolution["status"], + missingRefs: PluginManagedResourceRef[] = [], + ): PluginManagedRoutineResolution { + return { + pluginKey: options.pluginKey, + resourceKind: "routine", + resourceKey: declaration.routineKey, + companyId, + routineId: routine?.id ?? null, + routine, + status, + missingRefs, + }; + } + + async function ensureDefaultTriggers( + routineId: string, + declaration: PluginManagedRoutineDeclaration, + ) { + const triggers = declaration.triggers ?? []; + if (triggers.length === 0) return; + const existingCount = await db + .select({ id: routineTriggers.id }) + .from(routineTriggers) + .where(eq(routineTriggers.routineId, routineId)) + .limit(1) + .then((rows) => rows.length); + if (existingCount > 0) return; + + for (const trigger of triggers) { + await routinesSvc.createTrigger(routineId, triggerInput(trigger), { agentId: null, userId: null }); + } + } + + async function createManagedRoutine( + companyId: string, + declaration: PluginManagedRoutineDeclaration, + overrides?: RoutineOverrides, + ) { + const refs = await resolveRefs(companyId, declaration, overrides); + if (refs.missingRefs.length > 0) { + return resolution(companyId, declaration, null, "missing_refs", refs.missingRefs); + } + + const created = await routinesSvc.create(companyId, { + projectId: refs.projectId, + goalId: declaration.goalId ?? null, + title: declaration.title, + description: declaration.description ?? null, + assigneeAgentId: refs.assigneeAgentId, + priority: declaration.priority ?? "medium", + status: declaration.status ?? (refs.assigneeAgentId ? "active" : "paused"), + concurrencyPolicy: declaration.concurrencyPolicy ?? "coalesce_if_active", + catchUpPolicy: declaration.catchUpPolicy ?? "skip_missed", + variables: declaration.variables ?? [], + }, { agentId: null, userId: null }); + await upsertBinding(companyId, declaration, created.id); + await ensureDefaultTriggers(created.id, declaration); + const routine = await getRoutineWithManagedBy(companyId, declaration); + await logActivity(db, { + companyId, + actorType: "plugin", + actorId: options.pluginId, + action: "plugin.managed_routine.created", + entityType: "routine", + entityId: created.id, + details: { + sourcePluginKey: options.pluginKey, + managedResourceKey: declaration.routineKey, + assigneeAgentId: refs.assigneeAgentId, + projectId: refs.projectId, + }, + }); + return resolution(companyId, declaration, routine, "created"); + } + + async function get(routineKey: string, companyId: string) { + const declaration = declarationFor(routineKey); + const routine = await getRoutineWithManagedBy(companyId, declaration); + return resolution(companyId, declaration, routine, routine ? "resolved" : "missing"); + } + + async function reconcile(routineKey: string, companyId: string, overrides?: RoutineOverrides) { + const declaration = declarationFor(routineKey); + const current = await get(routineKey, companyId); + if (current.routine) { + await upsertBinding(companyId, declaration, current.routine.id); + await ensureDefaultTriggers(current.routine.id, declaration); + return current; + } + return createManagedRoutine(companyId, declaration, overrides); + } + + async function reset(routineKey: string, companyId: string, overrides?: RoutineOverrides) { + const declaration = declarationFor(routineKey); + const current = await get(routineKey, companyId); + if (!current.routine) { + return createManagedRoutine(companyId, declaration, overrides); + } + + const refs = await resolveRefs(companyId, declaration, overrides); + if (refs.missingRefs.length > 0) { + return resolution(companyId, declaration, current.routine, "missing_refs", refs.missingRefs); + } + const updated = await routinesSvc.update(current.routine.id, { + projectId: refs.projectId, + goalId: declaration.goalId ?? null, + title: declaration.title, + description: declaration.description ?? null, + assigneeAgentId: refs.assigneeAgentId, + priority: declaration.priority ?? "medium", + status: declaration.status ?? (refs.assigneeAgentId ? "active" : "paused"), + concurrencyPolicy: declaration.concurrencyPolicy ?? "coalesce_if_active", + catchUpPolicy: declaration.catchUpPolicy ?? "skip_missed", + variables: declaration.variables ?? [], + }, { agentId: null, userId: null }); + if (!updated) throw notFound("Managed routine not found"); + await upsertBinding(companyId, declaration, updated.id); + await ensureDefaultTriggers(updated.id, declaration); + const routine = await getRoutineWithManagedBy(companyId, declaration); + await logActivity(db, { + companyId, + actorType: "plugin", + actorId: options.pluginId, + action: "plugin.managed_routine.reset", + entityType: "routine", + entityId: updated.id, + details: { + sourcePluginKey: options.pluginKey, + managedResourceKey: declaration.routineKey, + assigneeAgentId: refs.assigneeAgentId, + projectId: refs.projectId, + }, + }); + return resolution(companyId, declaration, routine, "reset"); + } + + async function update( + routineKey: string, + companyId: string, + patch: { status?: string }, + ) { + const declaration = declarationFor(routineKey); + const current = await get(routineKey, companyId); + if (!current.routine) throw notFound("Managed routine not found"); + const updatePatch: { status?: RoutineStatus } = {}; + if (patch.status !== undefined) { + if (!ROUTINE_STATUSES.includes(patch.status as RoutineStatus)) { + throw unprocessable("Invalid routine status"); + } + updatePatch.status = patch.status as RoutineStatus; + } + const updated = await routinesSvc.update(current.routine.id, updatePatch, { agentId: null, userId: null }); + if (!updated) throw notFound("Managed routine not found"); + await logActivity(db, { + companyId, + actorType: "plugin", + actorId: options.pluginId, + action: "plugin.managed_routine.updated", + entityType: "routine", + entityId: updated.id, + details: { + sourcePluginKey: options.pluginKey, + managedResourceKey: declaration.routineKey, + status: updated.status, + }, + }); + const routine = await getRoutineWithManagedBy(companyId, declaration); + return routine ?? updated; + } + + async function run(routineKey: string, companyId: string, overrides?: RoutineOverrides) { + const declaration = declarationFor(routineKey); + const current = await get(routineKey, companyId); + if (!current.routine) throw notFound("Managed routine not found"); + const run = await routinesSvc.runRoutine(current.routine.id, { + source: "manual", + assigneeAgentId: overrides?.assigneeAgentId, + projectId: overrides?.projectId, + }, { agentId: null, userId: null }); + await logActivity(db, { + companyId, + actorType: "plugin", + actorId: options.pluginId, + action: "plugin.managed_routine.run_triggered", + entityType: "routine_run", + entityId: run.id, + details: { + sourcePluginKey: options.pluginKey, + managedResourceKey: declaration.routineKey, + routineId: current.routine.id, + status: run.status, + }, + }); + return run; + } + + return { + get, + reconcile, + reset, + update, + run, + }; +} diff --git a/server/src/services/plugin-registry.ts b/server/src/services/plugin-registry.ts index 79859a4e..ce544a9c 100644 --- a/server/src/services/plugin-registry.ts +++ b/server/src/services/plugin-registry.ts @@ -3,6 +3,7 @@ import type { Db } from "@paperclipai/db"; import { plugins, pluginConfig, + pluginCompanySettings, pluginEntities, pluginJobs, pluginJobRuns, @@ -15,6 +16,7 @@ import type { UpdatePluginStatus, UpsertPluginConfig, PatchPluginConfig, + PluginCompanySettings, PluginEntityRecord, PluginEntityQuery, PluginJobRecord, @@ -387,6 +389,64 @@ export function pluginRegistryService(db: Db) { return rows[0] ?? null; }, + // ----- Company settings ---------------------------------------------- + + /** Retrieve company-scoped plugin settings. */ + getCompanySettings: (pluginId: string, companyId: string): Promise => + db + .select() + .from(pluginCompanySettings) + .where(and( + eq(pluginCompanySettings.pluginId, pluginId), + eq(pluginCompanySettings.companyId, companyId), + )) + .then((rows) => rows[0] ?? null) as Promise, + + /** Create or replace company-scoped plugin settings. */ + upsertCompanySettings: async ( + pluginId: string, + companyId: string, + input: { enabled?: boolean; settingsJson: Record; lastError?: string | null }, + ): Promise => { + const plugin = await getById(pluginId); + if (!plugin) throw notFound("Plugin not found"); + + const existing = await db + .select() + .from(pluginCompanySettings) + .where(and( + eq(pluginCompanySettings.pluginId, pluginId), + eq(pluginCompanySettings.companyId, companyId), + )) + .then((rows) => rows[0] ?? null); + + if (existing) { + return db + .update(pluginCompanySettings) + .set({ + enabled: input.enabled ?? existing.enabled, + settingsJson: input.settingsJson, + lastError: input.lastError ?? null, + updatedAt: new Date(), + }) + .where(eq(pluginCompanySettings.id, existing.id)) + .returning() + .then((rows) => rows[0]) as Promise; + } + + return db + .insert(pluginCompanySettings) + .values({ + pluginId, + companyId, + enabled: input.enabled ?? true, + settingsJson: input.settingsJson, + lastError: input.lastError ?? null, + }) + .returning() + .then((rows) => rows[0]) as Promise; + }, + // ----- Entities ------------------------------------------------------- /** diff --git a/server/src/services/projects.ts b/server/src/services/projects.ts index 2bcf7aff..4d568c68 100644 --- a/server/src/services/projects.ts +++ b/server/src/services/projects.ts @@ -1,6 +1,14 @@ import { and, asc, desc, eq, inArray } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; -import { projects, projectGoals, goals, projectWorkspaces, workspaceRuntimeServices } from "@paperclipai/db"; +import { + projects, + projectGoals, + goals, + pluginManagedResources, + plugins, + projectWorkspaces, + workspaceRuntimeServices, +} from "@paperclipai/db"; import { PROJECT_COLORS, deriveProjectUrlKey, @@ -10,9 +18,12 @@ import { type ProjectCodebase, type ProjectExecutionWorkspacePolicy, type ProjectGoalRef, + type ProjectManagedByPlugin, type ProjectWorkspaceRuntimeConfig, type ProjectWorkspace, type WorkspaceRuntimeService, + type PluginManagedProjectDeclaration, + type PluginManagedProjectResolution, } from "@paperclipai/shared"; import { listCurrentRuntimeServicesForProjectWorkspaces } from "./workspace-runtime-read-model.js"; import { parseProjectExecutionWorkspacePolicy } from "./execution-workspace-policy.js"; @@ -50,6 +61,7 @@ interface ProjectWithGoals extends Omit codebase: ProjectCodebase; workspaces: ProjectWorkspace[]; primaryWorkspace: ProjectWorkspace | null; + managedByPlugin: ProjectManagedByPlugin | null; } interface ProjectShortnameRow { @@ -245,6 +257,40 @@ async function attachWorkspaces(db: Db, rows: ProjectWithGoals[]): Promise(); + for (const row of managedRows) { + managedByProjectId.set(row.resourceId, { + id: row.id, + pluginId: row.pluginId, + pluginKey: row.pluginKey, + pluginDisplayName: row.manifestJson.displayName ?? row.pluginKey, + resourceKind: "project", + resourceKey: row.resourceKey, + defaultsJson: row.defaultsJson, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + }); + } + return rows.map((row) => { const projectWorkspaceRows = map.get(row.id) ?? []; const workspaces = projectWorkspaceRows.map((workspace) => @@ -264,6 +310,7 @@ async function attachWorkspaces(db: Db, rows: ProjectWithGoals[]): Promise & { goalIds?: string[] }, + ): Promise => { + const { goalIds: inputGoalIds, ...projectData } = data; + const ids = resolveGoalIds({ goalIds: inputGoalIds, goalId: projectData.goalId }); + + // Auto-assign a color from the palette if none provided + if (!projectData.color) { + const existing = await db.select({ color: projects.color }).from(projects).where(eq(projects.companyId, companyId)); + const usedColors = new Set(existing.map((r) => r.color).filter(Boolean)); + const nextColor = PROJECT_COLORS.find((c) => !usedColors.has(c)) ?? PROJECT_COLORS[existing.length % PROJECT_COLORS.length]; + projectData.color = nextColor; + } + + const existingProjects = await db + .select({ id: projects.id, name: projects.name }) + .from(projects) + .where(eq(projects.companyId, companyId)); + projectData.name = resolveProjectNameForUniqueShortname(projectData.name, existingProjects); + + // Also write goalId to the legacy column (first goal or null) + const legacyGoalId = ids && ids.length > 0 ? ids[0] : projectData.goalId ?? null; + + const row = await db + .insert(projects) + .values({ ...projectData, goalId: legacyGoalId, companyId }) + .returning() + .then((rows) => rows[0]); + + if (ids && ids.length > 0) { + await syncGoalLinks(db, row.id, companyId, ids); + } + + const [withGoals] = await attachGoals(db, [row]); + const [enriched] = withGoals ? await attachWorkspaces(db, [withGoals]) : []; + return enriched!; + }; + + const getProjectById = async (id: string): Promise => { + const row = await db + .select() + .from(projects) + .where(eq(projects.id, id)) + .then((rows) => rows[0] ?? null); + if (!row) return null; + const [withGoals] = await attachGoals(db, [row]); + if (!withGoals) return null; + const [enriched] = await attachWorkspaces(db, [withGoals]); + return enriched ?? null; + }; + return { list: async (companyId: string): Promise => { const rows = await db.select().from(projects).where(eq(projects.companyId, companyId)); @@ -418,58 +528,170 @@ export function projectService(db: Db) { return dedupedIds.map((id) => byId.get(id)).filter((project): project is ProjectWithGoals => Boolean(project)); }, - getById: async (id: string): Promise => { - const row = await db - .select() - .from(projects) - .where(eq(projects.id, id)) + getById: getProjectById, + + resolveManagedProject: async (input: { + companyId: string; + pluginId: string; + pluginKey: string; + projectKey: string; + reset?: boolean; + createIfMissing?: boolean; + }): Promise => { + const plugin = await db + .select({ id: plugins.id, pluginKey: plugins.pluginKey, manifestJson: plugins.manifestJson }) + .from(plugins) + .where(eq(plugins.id, input.pluginId)) .then((rows) => rows[0] ?? null); - if (!row) return null; - const [withGoals] = await attachGoals(db, [row]); - if (!withGoals) return null; - const [enriched] = await attachWorkspaces(db, [withGoals]); - return enriched ?? null; - }, - - create: async ( - companyId: string, - data: Omit & { goalIds?: string[] }, - ): Promise => { - const { goalIds: inputGoalIds, ...projectData } = data; - const ids = resolveGoalIds({ goalIds: inputGoalIds, goalId: projectData.goalId }); - - // Auto-assign a color from the palette if none provided - if (!projectData.color) { - const existing = await db.select({ color: projects.color }).from(projects).where(eq(projects.companyId, companyId)); - const usedColors = new Set(existing.map((r) => r.color).filter(Boolean)); - const nextColor = PROJECT_COLORS.find((c) => !usedColors.has(c)) ?? PROJECT_COLORS[existing.length % PROJECT_COLORS.length]; - projectData.color = nextColor; + if (!plugin || plugin.pluginKey !== input.pluginKey) { + return { + pluginKey: input.pluginKey, + resourceKind: "project", + resourceKey: input.projectKey, + companyId: input.companyId, + projectId: null, + project: null, + status: "missing", + }; } - const existingProjects = await db - .select({ id: projects.id, name: projects.name }) - .from(projects) - .where(eq(projects.companyId, companyId)); - projectData.name = resolveProjectNameForUniqueShortname(projectData.name, existingProjects); - - // Also write goalId to the legacy column (first goal or null) - const legacyGoalId = ids && ids.length > 0 ? ids[0] : projectData.goalId ?? null; - - const row = await db - .insert(projects) - .values({ ...projectData, goalId: legacyGoalId, companyId }) - .returning() - .then((rows) => rows[0]); - - if (ids && ids.length > 0) { - await syncGoalLinks(db, row.id, companyId, ids); + const declaration = plugin.manifestJson.projects?.find((project) => project.projectKey === input.projectKey); + if (!declaration) { + return { + pluginKey: input.pluginKey, + resourceKind: "project", + resourceKey: input.projectKey, + companyId: input.companyId, + projectId: null, + project: null, + status: "missing", + }; } - const [withGoals] = await attachGoals(db, [row]); - const [enriched] = withGoals ? await attachWorkspaces(db, [withGoals]) : []; - return enriched!; + const defaults = buildManagedProjectDefaults(declaration); + const existingBinding = await db + .select() + .from(pluginManagedResources) + .where(and( + eq(pluginManagedResources.companyId, input.companyId), + eq(pluginManagedResources.pluginId, input.pluginId), + eq(pluginManagedResources.resourceKind, "project"), + eq(pluginManagedResources.resourceKey, input.projectKey), + )) + .then((rows) => rows[0] ?? null); + + if (existingBinding) { + const existingProject = await db + .select({ id: projects.id }) + .from(projects) + .where(and(eq(projects.companyId, input.companyId), eq(projects.id, existingBinding.resourceId))) + .then((rows) => rows[0] ?? null); + if (existingProject) { + if (input.reset) { + await db + .update(projects) + .set({ + name: declaration.displayName, + description: declaration.description ?? null, + status: declaration.status ?? "in_progress", + color: declaration.color ?? null, + updatedAt: new Date(), + }) + .where(and(eq(projects.companyId, input.companyId), eq(projects.id, existingBinding.resourceId))); + } + if (input.createIfMissing !== false) { + await db + .update(pluginManagedResources) + .set({ defaultsJson: defaults, updatedAt: new Date() }) + .where(eq(pluginManagedResources.id, existingBinding.id)); + } + const project = await getProjectById(existingBinding.resourceId); + return { + pluginKey: input.pluginKey, + resourceKind: "project", + resourceKey: input.projectKey, + companyId: input.companyId, + projectId: project?.id ?? existingBinding.resourceId, + project: project as import("@paperclipai/shared").Project | null, + status: input.reset ? "reset" : "resolved", + }; + } + + if (input.createIfMissing === false) { + return { + pluginKey: input.pluginKey, + resourceKind: "project", + resourceKey: input.projectKey, + companyId: input.companyId, + projectId: null, + project: null, + status: "missing", + }; + } + + const project = await createProject(input.companyId, { + name: declaration.displayName, + description: declaration.description ?? null, + status: declaration.status ?? "in_progress", + color: declaration.color ?? undefined, + }); + await db + .update(pluginManagedResources) + .set({ resourceId: project.id, defaultsJson: defaults, updatedAt: new Date() }) + .where(eq(pluginManagedResources.id, existingBinding.id)); + const hydrated = await getProjectById(project.id); + return { + pluginKey: input.pluginKey, + resourceKind: "project", + resourceKey: input.projectKey, + companyId: input.companyId, + projectId: hydrated?.id ?? project.id, + project: hydrated as import("@paperclipai/shared").Project | null, + status: "relinked", + }; + } + + if (input.createIfMissing === false) { + return { + pluginKey: input.pluginKey, + resourceKind: "project", + resourceKey: input.projectKey, + companyId: input.companyId, + projectId: null, + project: null, + status: "missing", + }; + } + + const project = await createProject(input.companyId, { + name: declaration.displayName, + description: declaration.description ?? null, + status: declaration.status ?? "in_progress", + color: declaration.color ?? undefined, + }); + await db.insert(pluginManagedResources).values({ + companyId: input.companyId, + pluginId: input.pluginId, + pluginKey: input.pluginKey, + resourceKind: "project", + resourceKey: input.projectKey, + resourceId: project.id, + defaultsJson: defaults, + }); + const hydrated = await getProjectById(project.id); + return { + pluginKey: input.pluginKey, + resourceKind: "project", + resourceKey: input.projectKey, + companyId: input.companyId, + projectId: hydrated?.id ?? project.id, + project: hydrated as import("@paperclipai/shared").Project | null, + status: "created", + }; }, + create: createProject, + update: async ( id: string, data: Partial & { goalIds?: string[] }, diff --git a/server/src/services/routines.ts b/server/src/services/routines.ts index a632f776..b1242624 100644 --- a/server/src/services/routines.ts +++ b/server/src/services/routines.ts @@ -10,6 +10,8 @@ import { issueInboxArchives, issueReadStates, issues, + pluginManagedResources, + plugins, projects, routineRuns, routines, @@ -21,6 +23,7 @@ import type { Routine, RoutineDetail, RoutineListItem, + RoutineManagedByPlugin, RoutineRunSummary, RoutineTrigger, RoutineTriggerSecretMaterial, @@ -34,6 +37,7 @@ import { getBuiltinRoutineVariableValues, extractRoutineVariableNames, interpolateRoutineTemplate, + pluginOperationIssueOriginKind, stringifyRoutineVariableValue, syncRoutineVariablesWithTemplate, } from "@paperclipai/shared"; @@ -354,6 +358,16 @@ function createRoutineDispatchFingerprint(input: { return crypto.createHash("sha256").update(canonical).digest("hex"); } +function readManagedRoutineIssueTemplate(defaultsJson: Record | null | undefined) { + const value = defaultsJson?.issueTemplate; + if (!isPlainRecord(value)) return null; + return { + surfaceVisibility: typeof value.surfaceVisibility === "string" ? value.surfaceVisibility : null, + originId: typeof value.originId === "string" && value.originId.trim() ? value.originId.trim() : null, + billingCode: typeof value.billingCode === "string" && value.billingCode.trim() ? value.billingCode.trim() : null, + }; +} + function routineUsesWorkspaceBranch(routine: typeof routines.$inferSelect) { return (routine.variables ?? []).some((variable) => variable.name === WORKSPACE_BRANCH_ROUTINE_VARIABLE) || extractRoutineVariableNames([routine.title, routine.description]).includes(WORKSPACE_BRANCH_ROUTINE_VARIABLE); @@ -380,6 +394,63 @@ export function routineService( .then((rows) => rows[0] ?? null); } + async function getManagedRoutineBinding(routine: typeof routines.$inferSelect) { + return db + .select({ + pluginKey: pluginManagedResources.pluginKey, + defaultsJson: pluginManagedResources.defaultsJson, + manifestJson: plugins.manifestJson, + }) + .from(pluginManagedResources) + .innerJoin(plugins, eq(pluginManagedResources.pluginId, plugins.id)) + .where( + and( + eq(pluginManagedResources.companyId, routine.companyId), + eq(pluginManagedResources.resourceKind, "routine"), + eq(pluginManagedResources.resourceId, routine.id), + ), + ) + .then((rows) => rows[0] ?? null); + } + + async function listManagedRoutineMetadata(routineIds: string[]) { + if (routineIds.length === 0) return new Map(); + const rows = await db + .select({ + id: pluginManagedResources.id, + pluginId: pluginManagedResources.pluginId, + pluginKey: pluginManagedResources.pluginKey, + manifestJson: plugins.manifestJson, + resourceKey: pluginManagedResources.resourceKey, + resourceId: pluginManagedResources.resourceId, + defaultsJson: pluginManagedResources.defaultsJson, + createdAt: pluginManagedResources.createdAt, + updatedAt: pluginManagedResources.updatedAt, + }) + .from(pluginManagedResources) + .innerJoin(plugins, eq(pluginManagedResources.pluginId, plugins.id)) + .where( + and( + eq(pluginManagedResources.resourceKind, "routine"), + inArray(pluginManagedResources.resourceId, routineIds), + ), + ); + return new Map(rows.map((row) => [ + row.resourceId, + { + id: row.id, + pluginId: row.pluginId, + pluginKey: row.pluginKey, + pluginDisplayName: row.manifestJson.displayName ?? row.pluginKey, + resourceKind: "routine", + resourceKey: row.resourceKey, + defaultsJson: row.defaultsJson, + createdAt: row.createdAt, + updatedAt: row.updatedAt, + } satisfies RoutineManagedByPlugin, + ])); + } + async function getTriggerById(id: string) { return db .select() @@ -664,8 +735,11 @@ export function routineService( routine: typeof routines.$inferSelect, executor: Db = db, dispatchFingerprint?: string | null, + origin?: { kind: string; id: string | null }, ) { const fingerprintCondition = routineExecutionFingerprintCondition(dispatchFingerprint); + const originKind = origin?.kind ?? "routine_execution"; + const originId = origin?.id ?? routine.id; const executionBoundIssue = await executor .select() .from(issues) @@ -679,8 +753,8 @@ export function routineService( .where( and( eq(issues.companyId, routine.companyId), - eq(issues.originKind, "routine_execution"), - eq(issues.originId, routine.id), + eq(issues.originKind, originKind), + eq(issues.originId, originId), inArray(issues.status, OPEN_ISSUE_STATUSES), isNull(issues.hiddenAt), ...(fingerprintCondition ? [fingerprintCondition] : []), @@ -705,8 +779,8 @@ export function routineService( .where( and( eq(issues.companyId, routine.companyId), - eq(issues.originKind, "routine_execution"), - eq(issues.originId, routine.id), + eq(issues.originKind, originKind), + eq(issues.originId, originId), inArray(issues.status, OPEN_ISSUE_STATUSES), isNull(issues.hiddenAt), ...(fingerprintCondition ? [fingerprintCondition] : []), @@ -844,6 +918,13 @@ export function routineService( const title = interpolateRoutineTemplate(input.routine.title, allVariables) ?? input.routine.title; const description = interpolateRoutineTemplate(input.routine.description, allVariables); const triggerPayload = mergeRoutineRunPayload(input.payload, { ...automaticVariables, ...resolvedVariables }); + const managedRoutineBinding = await getManagedRoutineBinding(input.routine); + const managedIssueTemplate = readManagedRoutineIssueTemplate(managedRoutineBinding?.defaultsJson); + const issueOriginKind = managedIssueTemplate?.surfaceVisibility === "plugin_operation" && managedRoutineBinding + ? pluginOperationIssueOriginKind(managedRoutineBinding.pluginKey) + : "routine_execution"; + const issueOriginId = managedIssueTemplate?.originId ?? input.routine.id; + const issueBillingCode = managedIssueTemplate?.billingCode ?? null; const dispatchFingerprint = createRoutineDispatchFingerprint({ payload: triggerPayload, projectId, @@ -902,7 +983,10 @@ export function routineService( let createdIssue: Awaited> | null = null; try { - const activeIssue = await findLiveExecutionIssue(input.routine, txDb, dispatchFingerprint); + const activeIssue = await findLiveExecutionIssue(input.routine, txDb, dispatchFingerprint, { + kind: issueOriginKind, + id: issueOriginId, + }); if (activeIssue && input.routine.concurrencyPolicy !== "always_enqueue") { const status = input.routine.concurrencyPolicy === "skip_if_active" ? "skipped" : "coalesced"; if (manualRunnerUserId) { @@ -942,10 +1026,11 @@ export function routineService( assigneeAgentId, createdByAgentId: input.source === "manual" ? input.actor?.agentId ?? null : null, createdByUserId: manualRunnerUserId, - originKind: "routine_execution", - originId: input.routine.id, + originKind: issueOriginKind, + originId: issueOriginId, originRunId: createdRun.id, originFingerprint: dispatchFingerprint, + billingCode: issueBillingCode, executionWorkspaceId: input.executionWorkspaceId ?? null, executionWorkspacePreference: input.executionWorkspacePreference ?? null, executionWorkspaceSettings: input.executionWorkspaceSettings ?? null, @@ -962,7 +1047,10 @@ export function routineService( throw error; } - const existingIssue = await findLiveExecutionIssue(input.routine, txDb, dispatchFingerprint); + const existingIssue = await findLiveExecutionIssue(input.routine, txDb, dispatchFingerprint, { + kind: issueOriginKind, + id: issueOriginId, + }); if (!existingIssue) throw error; const status = input.routine.concurrencyPolicy === "skip_if_active" ? "skipped" : "coalesced"; if (manualRunnerUserId) { @@ -1084,13 +1172,15 @@ export function routineService( .where(and(...conditions)) .orderBy(desc(routines.updatedAt), asc(routines.title)); const routineIds = rows.map((row) => row.id); - const [triggersByRoutine, latestRunByRoutine, activeIssueByRoutine] = await Promise.all([ + const [triggersByRoutine, latestRunByRoutine, activeIssueByRoutine, managedByRoutine] = await Promise.all([ listTriggersForRoutineIds(companyId, routineIds), listLatestRunByRoutineIds(companyId, routineIds), listLiveIssueByRoutineIds(companyId, routineIds), + listManagedRoutineMetadata(routineIds), ]); return rows.map((row) => ({ ...row, + managedByPlugin: managedByRoutine.get(row.id) ?? null, triggers: (triggersByRoutine.get(row.id) ?? []).map((trigger) => ({ id: trigger.id, kind: trigger.kind as RoutineListItem["triggers"][number]["kind"], @@ -1110,7 +1200,7 @@ export function routineService( getDetail: async (id: string): Promise => { const row = await getRoutineById(id); if (!row) return null; - const [project, assignee, parentIssue, triggers, recentRuns, activeIssue] = await Promise.all([ + const [project, assignee, parentIssue, triggers, recentRuns, activeIssue, managedByRoutine] = await Promise.all([ row.projectId ? db.select().from(projects).where(eq(projects.id, row.projectId)).then((rows) => rows[0] ?? null) : null, @@ -1189,10 +1279,12 @@ export function routineService( })), ), findLiveExecutionIssue(row), + listManagedRoutineMetadata([row.id]), ]); return { ...row, + managedByPlugin: managedByRoutine.get(row.id) ?? null, project, assignee, parentIssue, diff --git a/ui/src/App.tsx b/ui/src/App.tsx index e29e6efe..c9637a9c 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -129,7 +129,7 @@ function boardRoutes() { } /> } /> } /> - } /> + } /> } /> ); diff --git a/ui/src/api/issues.ts b/ui/src/api/issues.ts index acae3a71..4bf96222 100644 --- a/ui/src/api/issues.ts +++ b/ui/src/api/issues.ts @@ -43,6 +43,7 @@ export const issuesApi = { workspaceId?: string; executionWorkspaceId?: string; originKind?: string; + originKindPrefix?: string; originId?: string; descendantOf?: string; includeRoutineExecutions?: boolean; @@ -66,6 +67,7 @@ export const issuesApi = { if (filters?.workspaceId) params.set("workspaceId", filters.workspaceId); if (filters?.executionWorkspaceId) params.set("executionWorkspaceId", filters.executionWorkspaceId); if (filters?.originKind) params.set("originKind", filters.originKind); + if (filters?.originKindPrefix) params.set("originKindPrefix", filters.originKindPrefix); if (filters?.originId) params.set("originId", filters.originId); if (filters?.descendantOf) params.set("descendantOf", filters.descendantOf); if (filters?.includeRoutineExecutions) params.set("includeRoutineExecutions", "true"); diff --git a/ui/src/api/plugins.test.ts b/ui/src/api/plugins.test.ts new file mode 100644 index 00000000..20d1153b --- /dev/null +++ b/ui/src/api/plugins.test.ts @@ -0,0 +1,64 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockApi = vi.hoisted(() => ({ + get: vi.fn(), + post: vi.fn(), + put: vi.fn(), +})); + +vi.mock("./client", () => ({ + api: mockApi, +})); + +import { pluginsApi } from "./plugins"; + +describe("pluginsApi local folders", () => { + beforeEach(() => { + mockApi.get.mockReset(); + mockApi.post.mockReset(); + mockApi.put.mockReset(); + mockApi.get.mockResolvedValue({}); + mockApi.post.mockResolvedValue({}); + mockApi.put.mockResolvedValue({}); + }); + + it("lists company-scoped local folders for a plugin", async () => { + await pluginsApi.listLocalFolders("plugin-1", "company-1"); + + expect(mockApi.get).toHaveBeenCalledWith( + "/plugins/plugin-1/companies/company-1/local-folders", + ); + }); + + it("validates a candidate folder path without saving", async () => { + await pluginsApi.validateLocalFolder("plugin-1", "company-1", "wiki-root", { + path: "/tmp/wiki", + access: "readWrite", + requiredFiles: ["WIKI.md"], + }); + + expect(mockApi.post).toHaveBeenCalledWith( + "/plugins/plugin-1/companies/company-1/local-folders/wiki-root/validate", + { + path: "/tmp/wiki", + access: "readWrite", + requiredFiles: ["WIKI.md"], + }, + ); + }); + + it("saves through the local-folder PUT endpoint", async () => { + await pluginsApi.configureLocalFolder("plugin-1", "company-1", "wiki-root", { + path: "/tmp/wiki", + requiredDirectories: ["wiki"], + }); + + expect(mockApi.put).toHaveBeenCalledWith( + "/plugins/plugin-1/companies/company-1/local-folders/wiki-root", + { + path: "/tmp/wiki", + requiredDirectories: ["wiki"], + }, + ); + }); +}); diff --git a/ui/src/api/plugins.ts b/ui/src/api/plugins.ts index 0edc580f..2e4c981a 100644 --- a/ui/src/api/plugins.ts +++ b/ui/src/api/plugins.ts @@ -14,6 +14,7 @@ import type { PluginLauncherDeclaration, PluginLauncherRenderContextSnapshot, PluginUiSlotDeclaration, + PluginLocalFolderDeclaration, PluginRecord, PluginConfig, PluginStatus, @@ -140,6 +141,54 @@ export interface AvailablePluginExample { tag: "example"; } +export interface PluginLocalFolderProblem { + code: + | "not_configured" + | "not_absolute" + | "missing" + | "not_directory" + | "not_readable" + | "not_writable" + | "missing_directory" + | "missing_file" + | "path_traversal" + | "symlink_escape" + | "atomic_write_failed"; + message: string; + path?: string; +} + +export interface PluginLocalFolderStatus { + folderKey: string; + configured: boolean; + path: string | null; + realPath: string | null; + access: "read" | "readWrite"; + readable: boolean; + writable: boolean; + requiredDirectories: string[]; + requiredFiles: string[]; + missingDirectories: string[]; + missingFiles: string[]; + healthy: boolean; + problems: PluginLocalFolderProblem[]; + checkedAt: string; +} + +export interface PluginLocalFoldersResponse { + pluginId: string; + companyId: string; + declarations: PluginLocalFolderDeclaration[]; + folders: PluginLocalFolderStatus[]; +} + +export interface PluginLocalFolderSaveInput { + path: string; + access?: "read" | "readWrite"; + requiredDirectories?: string[]; + requiredFiles?: string[]; +} + /** * Plugin management API client. * @@ -337,6 +386,48 @@ export const pluginsApi = { testConfig: (pluginId: string, configJson: Record) => api.post<{ valid: boolean; message?: string }>(`/plugins/${pluginId}/config/test`, { configJson }), + /** + * List manifest-declared and stored company-scoped local folders for a plugin. + */ + listLocalFolders: (pluginId: string, companyId: string) => + api.get(`/plugins/${pluginId}/companies/${companyId}/local-folders`), + + /** + * Inspect a configured local folder without changing persisted settings. + */ + localFolderStatus: (pluginId: string, companyId: string, folderKey: string) => + api.get( + `/plugins/${pluginId}/companies/${companyId}/local-folders/${encodeURIComponent(folderKey)}/status`, + ), + + /** + * Validate a candidate local folder path without saving it. + */ + validateLocalFolder: ( + pluginId: string, + companyId: string, + folderKey: string, + input: PluginLocalFolderSaveInput, + ) => + api.post( + `/plugins/${pluginId}/companies/${companyId}/local-folders/${encodeURIComponent(folderKey)}/validate`, + input, + ), + + /** + * Persist a company-scoped local folder path and return its inspected status. + */ + configureLocalFolder: ( + pluginId: string, + companyId: string, + folderKey: string, + input: PluginLocalFolderSaveInput, + ) => + api.put( + `/plugins/${pluginId}/companies/${companyId}/local-folders/${encodeURIComponent(folderKey)}`, + input, + ), + // =========================================================================== // Bridge proxy endpoints — used by the plugin UI bridge runtime // =========================================================================== diff --git a/ui/src/components/CompanySettingsSidebar.tsx b/ui/src/components/CompanySettingsSidebar.tsx index 4acebfec..95158201 100644 --- a/ui/src/components/CompanySettingsSidebar.tsx +++ b/ui/src/components/CompanySettingsSidebar.tsx @@ -7,7 +7,6 @@ import { queryKeys } from "@/lib/queryKeys"; import { useCompany } from "@/context/CompanyContext"; import { useSidebar } from "@/context/SidebarContext"; import { SidebarNavItem } from "./SidebarNavItem"; -import { SidebarCompanyMenu } from "./SidebarCompanyMenu"; export function CompanySettingsSidebar() { const { selectedCompany, selectedCompanyId } = useCompany(); @@ -32,11 +31,8 @@ export function CompanySettingsSidebar() { }); return ( -
    @@ -558,6 +569,350 @@ export function PluginSettings() { ); } +// --------------------------------------------------------------------------- +// PluginLocalFoldersSettings — host-managed company-scoped folders +// --------------------------------------------------------------------------- + +interface PluginLocalFoldersSettingsProps { + pluginId: string; + companyId: string | null; + declarations: PluginLocalFolderDeclaration[]; +} + +function PluginLocalFoldersSettings({ pluginId, companyId, declarations }: PluginLocalFoldersSettingsProps) { + const { data, isLoading, error } = useQuery({ + queryKey: companyId + ? queryKeys.plugins.localFolders(pluginId, companyId) + : ["plugins", pluginId, "companies", "none", "local-folders"], + queryFn: () => pluginsApi.listLocalFolders(pluginId, companyId!), + enabled: !!companyId, + }); + + const statusByKey = new Map((data?.folders ?? []).map((folder) => [folder.folderKey, folder])); + + if (!companyId) { + return ( +
    + Select a company to configure this plugin's local folders. +
    + ); + } + + return ( +
    +
    + +

    Local folders

    +
    + {error ? ( +
    + {(error as Error).message || "Failed to load local folder settings."} +
    + ) : null} + {isLoading ? ( +
    + + Loading local folders... +
    + ) : ( +
    + {declarations.map((declaration) => ( + + ))} +
    + )} +
    + ); +} + +interface PluginLocalFolderRowProps { + pluginId: string; + companyId: string; + declaration: PluginLocalFolderDeclaration; + status?: PluginLocalFolderStatus; +} + +function PluginLocalFolderRow({ pluginId, companyId, declaration, status }: PluginLocalFolderRowProps) { + const queryClient = useQueryClient(); + const serverPath = status?.path ?? ""; + const [pathValue, setPathValue] = useState(serverPath); + const [message, setMessage] = useState<{ type: "success" | "error"; text: string } | null>(null); + + useEffect(() => { + setPathValue(serverPath); + setMessage(null); + }, [serverPath, declaration.folderKey]); + + const saveMutation = useMutation({ + mutationFn: (path: string) => + pluginsApi.configureLocalFolder(pluginId, companyId, declaration.folderKey, { + path, + access: declaration.access, + requiredDirectories: declaration.requiredDirectories, + requiredFiles: declaration.requiredFiles, + }), + onSuccess: (nextStatus) => { + setMessage({ + type: nextStatus.healthy ? "success" : "error", + text: nextStatus.healthy + ? "Local folder saved." + : "Local folder saved, but validation still needs attention.", + }); + queryClient.invalidateQueries({ queryKey: queryKeys.plugins.localFolders(pluginId, companyId) }); + }, + onError: (err: Error) => { + setMessage({ type: "error", text: err.message || "Failed to save local folder." }); + }, + }); + + const trimmedPath = pathValue.trim(); + const isDirty = trimmedPath !== serverPath; + const access = status?.access ?? declaration.access ?? "readWrite"; + + const handleSave = useCallback(() => { + if (!trimmedPath) { + setMessage({ type: "error", text: "Local folder path is required." }); + return; + } + if (!isLikelyAbsolutePath(trimmedPath)) { + setMessage({ type: "error", text: "Local folder must be a full absolute path." }); + return; + } + setMessage(null); + saveMutation.mutate(trimmedPath); + }, [saveMutation, trimmedPath]); + + return ( +
    +
    +
    +
    +

    {declaration.displayName}

    + + {declaration.folderKey} + + + {status?.healthy ? "Healthy" : "Needs attention"} + +
    + {declaration.description ? ( +

    + {declaration.description} +

    + ) : null} +
    + + {access === "readWrite" ? "Read/write" : "Read only"} + +
    + +
    + + + +
    + + {status?.path ? ( +
    +
    Configured path
    +
    + {status.path} +
    +
    + ) : null} + +
    + +
    + { + setPathValue(event.target.value); + setMessage(null); + }} + placeholder="/absolute/path/to/folder" + /> + + +
    +
    + + + + {status?.problems?.length ? ( +
    +
    Validation problems
    +
      + {status.problems.map((problem, index) => ( +
    • + {problem.message} + {problem.path ? {problem.path} : null} +
    • + ))} +
    +
    + ) : null} + + {message ? ( +
    + {message.text} +
    + ) : null} +
    + ); +} + +function FolderStatusMetric({ label, value, ok }: { label: string; value: string; ok: boolean }) { + return ( +
    + {label} + {value} +
    + ); +} + +function FolderRequirements({ + status, + declaration, +}: { + status?: PluginLocalFolderStatus; + declaration: PluginLocalFolderDeclaration; +}) { + const requiredDirectories = status?.requiredDirectories ?? declaration.requiredDirectories ?? []; + const requiredFiles = status?.requiredFiles ?? declaration.requiredFiles ?? []; + const missingDirectories = status?.missingDirectories ?? requiredDirectories; + const missingFiles = status?.missingFiles ?? requiredFiles; + const rootNotInspected = isRootNotInspected(status); + + if (requiredDirectories.length === 0 && requiredFiles.length === 0) return null; + + return ( +
    + + +
    + ); +} + +function isRootNotInspected(status?: PluginLocalFolderStatus) { + if (!status?.configured || status.readable) return false; + return status.problems.some((problem) => + problem.code === "missing" || problem.code === "not_readable" || problem.code === "not_directory" + ); +} + +function RequirementList({ + title, + items, + missingItems, + missingLabel, + inspectionUnavailable, +}: { + title: string; + items: string[]; + missingItems: string[]; + missingLabel: string; + inspectionUnavailable?: boolean; +}) { + return ( +
    +
    + {title} + {inspectionUnavailable ? ( + + Not inspected + + ) : missingItems.length > 0 ? ( + + {missingItems.length} missing + + ) : ( + Present + )} +
    + {items.length > 0 ? ( +
    + {items.map((item) => { + const missing = missingItems.includes(item); + return ( + + {item} + + ); + })} +
    + ) : ( +

    None declared.

    + )} + {inspectionUnavailable ? ( +

    Configured root was not inspected.

    + ) : missingItems.length > 0 ? ( +

    {missingLabel}: {missingItems.join(", ")}

    + ) : null} +
    + ); +} + +function isLikelyAbsolutePath(pathValue: string) { + return ( + pathValue.startsWith("/") || + /^[A-Za-z]:[\\/]/.test(pathValue) || + pathValue.startsWith("\\\\") + ); +} + // --------------------------------------------------------------------------- // PluginConfigForm — auto-generated form for instanceConfigSchema // --------------------------------------------------------------------------- diff --git a/ui/src/pages/ProjectDetail.test.tsx b/ui/src/pages/ProjectDetail.test.tsx new file mode 100644 index 00000000..fce062c2 --- /dev/null +++ b/ui/src/pages/ProjectDetail.test.tsx @@ -0,0 +1,187 @@ +// @vitest-environment jsdom + +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import type { Project } from "@paperclipai/shared"; +import { act, type ReactNode } from "react"; +import { createRoot, type Root } from "react-dom/client"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { ProjectDetail } from "./ProjectDetail"; + +(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true; + +const mockProjectsApi = vi.hoisted(() => ({ + get: vi.fn(), + list: vi.fn(), + update: vi.fn(), +})); +const mockIssuesApi = vi.hoisted(() => ({ + list: vi.fn(), + update: vi.fn(), +})); +const mockAgentsApi = vi.hoisted(() => ({ list: vi.fn() })); +const mockHeartbeatsApi = vi.hoisted(() => ({ liveRunsForCompany: vi.fn() })); +const mockBudgetsApi = vi.hoisted(() => ({ overview: vi.fn(), upsertPolicy: vi.fn() })); +const mockExecutionWorkspacesApi = vi.hoisted(() => ({ list: vi.fn() })); +const mockInstanceSettingsApi = vi.hoisted(() => ({ getExperimental: vi.fn() })); +const mockAssetsApi = vi.hoisted(() => ({ uploadImage: vi.fn() })); +const mockNavigate = vi.hoisted(() => vi.fn()); +const mockSetBreadcrumbs = vi.hoisted(() => vi.fn()); +const mockIssuesList = vi.hoisted(() => vi.fn()); + +vi.mock("../api/projects", () => ({ projectsApi: mockProjectsApi })); +vi.mock("../api/issues", () => ({ issuesApi: mockIssuesApi })); +vi.mock("../api/agents", () => ({ agentsApi: mockAgentsApi })); +vi.mock("../api/heartbeats", () => ({ heartbeatsApi: mockHeartbeatsApi })); +vi.mock("../api/budgets", () => ({ budgetsApi: mockBudgetsApi })); +vi.mock("../api/execution-workspaces", () => ({ executionWorkspacesApi: mockExecutionWorkspacesApi })); +vi.mock("../api/instanceSettings", () => ({ instanceSettingsApi: mockInstanceSettingsApi })); +vi.mock("../api/assets", () => ({ assetsApi: mockAssetsApi })); + +vi.mock("@/lib/router", () => ({ + Link: ({ children, to }: { children?: ReactNode; to: string }) => {children}, + Navigate: ({ to }: { to: string }) =>
    {to}
    , + useLocation: () => ({ pathname: "/projects/project-1/plugin-operations", search: "", hash: "", state: null }), + useNavigate: () => mockNavigate, + useParams: () => ({ projectId: "project-1" }), +})); + +vi.mock("../context/CompanyContext", () => ({ + useCompany: () => ({ + companies: [{ id: "company-1", issuePrefix: "PAP" }], + selectedCompanyId: "company-1", + setSelectedCompanyId: vi.fn(), + }), +})); +vi.mock("../context/PanelContext", () => ({ usePanel: () => ({ closePanel: vi.fn() }) })); +vi.mock("../context/ToastContext", () => ({ useToastActions: () => ({ pushToast: vi.fn() }) })); +vi.mock("../context/BreadcrumbContext", () => ({ useBreadcrumbs: () => ({ setBreadcrumbs: mockSetBreadcrumbs }) })); +vi.mock("@/plugins/slots", () => ({ + PluginSlotMount: () => null, + PluginSlotOutlet: () => null, + usePluginSlots: () => ({ slots: [], isLoading: false }), +})); +vi.mock("@/plugins/launchers", () => ({ PluginLauncherOutlet: () => null })); +vi.mock("../components/ProjectProperties", () => ({ + ProjectProperties: () =>
    , +})); +vi.mock("../components/BudgetPolicyCard", () => ({ + BudgetPolicyCard: () =>
    , +})); +vi.mock("../components/InlineEditor", () => ({ + InlineEditor: ({ value, placeholder }: { value?: string; placeholder?: string }) => ( + {value || placeholder || null} + ), +})); +vi.mock("../components/ProjectWorkspacesContent", () => ({ + ProjectWorkspacesContent: () =>
    , +})); +vi.mock("../components/PageTabBar", () => ({ + PageTabBar: ({ items }: { items: Array<{ value: string; label: string }> }) => ( +
    {items.map((item) => )}
    + ), +})); +vi.mock("../components/IssuesList", () => ({ + IssuesList: (props: unknown) => { + mockIssuesList(props); + return
    ; + }, +})); + +function project(overrides: Partial = {}): Project { + const now = new Date("2026-05-01T00:00:00Z"); + return { + id: "project-1", + companyId: "company-1", + urlKey: "project-1", + goalId: null, + goalIds: [], + goals: [], + name: "Managed Project", + description: null, + status: "in_progress", + leadAgentId: null, + targetDate: null, + color: "#14b8a6", + env: null, + pauseReason: null, + pausedAt: null, + executionWorkspacePolicy: null, + codebase: { + workspaceId: null, + repoUrl: null, + repoRef: null, + defaultRef: null, + repoName: null, + localFolder: null, + managedFolder: "/tmp/project-1", + effectiveLocalFolder: "/tmp/project-1", + origin: "managed_checkout", + }, + workspaces: [], + primaryWorkspace: null, + managedByPlugin: { + id: "managed-1", + pluginId: "plugin-1", + pluginKey: "paperclip.missions", + pluginDisplayName: "Missions", + resourceKind: "project", + resourceKey: "operations", + defaultsJson: {}, + createdAt: now, + updatedAt: now, + }, + archivedAt: null, + createdAt: now, + updatedAt: now, + ...overrides, + }; +} + +describe("ProjectDetail", () => { + let root: Root | null = null; + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement("div"); + document.body.appendChild(container); + mockProjectsApi.get.mockResolvedValue(project()); + mockProjectsApi.list.mockResolvedValue([project()]); + mockIssuesApi.list.mockResolvedValue([]); + mockAgentsApi.list.mockResolvedValue([]); + mockHeartbeatsApi.liveRunsForCompany.mockResolvedValue([]); + mockBudgetsApi.overview.mockResolvedValue({ policies: [] }); + mockInstanceSettingsApi.getExperimental.mockResolvedValue({ enableIsolatedWorkspaces: false }); + mockExecutionWorkspacesApi.list.mockResolvedValue([]); + }); + + afterEach(() => { + act(() => root?.unmount()); + root = null; + container.remove(); + vi.clearAllMocks(); + }); + + it("shows managed plugin affordances and filters the operations tab by plugin origin", async () => { + const queryClient = new QueryClient({ defaultOptions: { queries: { retry: false } } }); + + await act(async () => { + root = createRoot(container); + root.render( + + + , + ); + }); + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 0)); + await new Promise((resolve) => setTimeout(resolve, 0)); + }); + + expect(container.textContent).toContain("Managed by Missions"); + expect(container.textContent).toContain("Plugin operations"); + expect(mockIssuesApi.list).toHaveBeenCalledWith("company-1", { + projectId: "project-1", + originKindPrefix: "plugin:paperclip.missions", + }); + }); +}); diff --git a/ui/src/pages/ProjectDetail.tsx b/ui/src/pages/ProjectDetail.tsx index e49f0668..fb0f053b 100644 --- a/ui/src/pages/ProjectDetail.tsx +++ b/ui/src/pages/ProjectDetail.tsx @@ -33,7 +33,7 @@ import { PluginSlotMount, PluginSlotOutlet, usePluginSlots } from "@/plugins/slo /* ── Top-level tab types ── */ -type ProjectBaseTab = "overview" | "list" | "workspaces" | "configuration" | "budget"; +type ProjectBaseTab = "overview" | "list" | "plugin-operations" | "workspaces" | "configuration" | "budget"; type ProjectPluginTab = `plugin:${string}`; type ProjectTab = ProjectBaseTab | ProjectPluginTab; @@ -50,6 +50,7 @@ function resolveProjectTab(pathname: string, projectId: string): ProjectTab | nu if (tab === "configuration") return "configuration"; if (tab === "budget") return "budget"; if (tab === "issues") return "list"; + if (tab === "plugin-operations") return "plugin-operations"; if (tab === "workspaces") return "workspaces"; return null; } @@ -208,6 +209,67 @@ function ProjectIssuesList({ projectId, companyId }: { projectId: string; compan ); } +function ProjectPluginOperationsList({ + projectId, + companyId, + pluginKey, +}: { + projectId: string; + companyId: string; + pluginKey: string; +}) { + const queryClient = useQueryClient(); + const originKindPrefix = `plugin:${pluginKey}`; + + const { data: agents } = useQuery({ + queryKey: queryKeys.agents.list(companyId), + queryFn: () => agentsApi.list(companyId), + enabled: !!companyId, + }); + const { data: projects } = useQuery({ + queryKey: queryKeys.projects.list(companyId), + queryFn: () => projectsApi.list(companyId), + enabled: !!companyId, + }); + const { data: liveRuns } = useQuery({ + queryKey: queryKeys.liveRuns(companyId), + queryFn: () => heartbeatsApi.liveRunsForCompany(companyId), + enabled: !!companyId, + refetchInterval: 5000, + }); + const liveIssueIds = useMemo(() => collectLiveIssueIds(liveRuns), [liveRuns]); + + const { data: issues, isLoading, error } = useQuery({ + queryKey: queryKeys.issues.listPluginOperationsByProject(companyId, projectId, originKindPrefix), + queryFn: () => issuesApi.list(companyId, { projectId, originKindPrefix }), + enabled: !!companyId && !!projectId, + }); + + const updateIssue = useMutation({ + mutationFn: ({ id, data }: { id: string; data: Record }) => + issuesApi.update(id, data), + onSuccess: () => { + queryClient.invalidateQueries({ queryKey: queryKeys.issues.listPluginOperationsByProject(companyId, projectId, originKindPrefix) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.listByProject(companyId, projectId) }); + queryClient.invalidateQueries({ queryKey: queryKeys.issues.list(companyId) }); + }, + }); + + return ( + updateIssue.mutate({ id, data })} + /> + ); +} + /* ── Main project page ── */ export function ProjectDetail() { @@ -390,6 +452,10 @@ export function ProjectDetail() { navigate(`/projects/${canonicalProjectRef}/budget`, { replace: true }); return; } + if (activeTab === "plugin-operations") { + navigate(`/projects/${canonicalProjectRef}/plugin-operations`, { replace: true }); + return; + } if (activeTab === "workspaces") { navigate(`/projects/${canonicalProjectRef}/workspaces`, { replace: true }); return; @@ -523,6 +589,9 @@ export function ProjectDetail() { if (cachedTab === "budget") { return ; } + if (cachedTab === "plugin-operations" && project?.managedByPlugin) { + return ; + } if (cachedTab === "workspaces" && workspaceTabDecisionLoaded && showWorkspacesTab) { return ; } @@ -554,6 +623,8 @@ export function ProjectDetail() { navigate(`/projects/${canonicalProjectRef}/workspaces`); } else if (tab === "budget") { navigate(`/projects/${canonicalProjectRef}/budget`); + } else if (tab === "plugin-operations") { + navigate(`/projects/${canonicalProjectRef}/plugin-operations`); } else if (tab === "configuration") { navigate(`/projects/${canonicalProjectRef}/configuration`); } else { @@ -583,6 +654,12 @@ export function ProjectDetail() { Paused by budget hard stop
    ) : null} + {project.managedByPlugin ? ( +
    + + Managed by {project.managedByPlugin.pluginDisplayName} +
    + ) : null}
    @@ -622,6 +699,7 @@ export function ProjectDetail() { items={[ { value: "list", label: "Issues" }, { value: "overview", label: "Overview" }, + ...(project.managedByPlugin ? [{ value: "plugin-operations", label: "Plugin operations" }] : []), ...(showWorkspacesTab ? [{ value: "workspaces", label: "Workspaces" }] : []), { value: "configuration", label: "Configuration" }, { value: "budget", label: "Budget" }, @@ -651,6 +729,14 @@ export function ProjectDetail() { )} + {activeTab === "plugin-operations" && project?.id && resolvedCompanyId && project.managedByPlugin && ( + + )} + {activeTab === "workspaces" ? ( workspaceTabDecisionLoaded ? ( workspaceTabError ? ( diff --git a/ui/src/pages/RoutineDetail.tsx b/ui/src/pages/RoutineDetail.tsx index bb36f1d4..a2af906f 100644 --- a/ui/src/pages/RoutineDetail.tsx +++ b/ui/src/pages/RoutineDetail.tsx @@ -702,36 +702,44 @@ export function RoutineDetail() {
    {/* Header: editable title + actions */}
    -