From 15eac43b43f78cfe890823378a00824828e272d9 Mon Sep 17 00:00:00 2001
From: Dotta <34892728+cryppadotta@users.noreply.github.com>
Date: Sun, 3 May 2026 11:30:48 -0500
Subject: [PATCH 01/27] [codex] Retry max-turn exhausted heartbeats (#5096)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Thinking Path
> - Paperclip orchestrates AI agents for autonomous companies, and
heartbeat execution is the control-plane loop that keeps assigned work
moving.
> - Max-turn exhaustion is a recoverable local-adapter stop condition
for Claude and Gemini agents when a run needs another heartbeat to
continue safely.
> - The previous behavior could leave max-turn continuation details hard
to inspect, and duplicate/stale continuation wakes could keep running
after issue state changed.
> - The adapter layer also needed to avoid trusting arbitrary
stdout/stderr text as scheduler control metadata.
> - This pull request adds bounded max-turn continuation scheduling,
visible retry state, structured stop metadata handling, and
stale/duplicate continuation guards.
> - The benefit is safer automatic continuation after max-turn stops,
clearer operator visibility, and fewer duplicate or stale agent runs.
## What Changed
- Replaces closed PR #4952, whose head repository was deleted.
- Rebases the recovered max-turn continuation branch onto current
`paperclipai/paperclip:master`.
- Adds max-turn continuation scheduling and retry-state plumbing for
heartbeat runs.
- Adds stale/duplicate continuation suppression when issue status,
ownership, or execution locks change.
- Normalizes Claude/Gemini max-turn detection around structured stop
metadata instead of unstructured stdout/stderr text.
- Surfaces max-turn continuation settings and retry visibility in the
board UI.
- Adds focused server, adapter, and UI tests for max-turn stop metadata,
retry scheduling, stale queued-run invalidation, adapter
parsing/execution, run ledger display, and agent config patching.
## Verification
- `pnpm install --no-frozen-lockfile` to refresh local dependencies
after rebasing onto current `master`.
- `pnpm run preflight:workspace-links && pnpm exec vitest run
server/src/__tests__/claude-local-adapter.test.ts
server/src/__tests__/claude-local-execute.test.ts
server/src/__tests__/gemini-local-adapter.test.ts
server/src/__tests__/gemini-local-execute.test.ts
server/src/__tests__/heartbeat-retry-scheduling.test.ts
server/src/__tests__/heartbeat-stale-queue-invalidation.test.ts
server/src/services/heartbeat-stop-metadata.test.ts
ui/src/components/IssueRunLedger.test.tsx
ui/src/lib/agent-config-patch.test.ts ui/src/lib/runRetryState.test.ts
--testTimeout=20000`
- `pnpm --filter @paperclipai/adapter-claude-local typecheck && pnpm
--filter @paperclipai/adapter-gemini-local typecheck && pnpm --filter
@paperclipai/server typecheck && pnpm --filter @paperclipai/ui
typecheck`
- UI screenshot note: the UI changes are limited to config/ledger state
rendering rather than layout changes; component/unit coverage above
verifies the rendered behavior.
## Risks
- Medium behavior risk: heartbeat retry gating now suppresses max-turn
continuations when issue state or execution locks drift, so any callers
that relied on stale continuations running will now see cancellation
instead.
- Low adapter risk: Claude/Gemini unstructured text no longer triggers
max-turn scheduler metadata, so only structured stop signals and Gemini
exit code 53 are trusted.
- No database migrations.
> 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 coding agent, GPT-5-class model, tool-enabled local
repository editing and command execution.
## 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 (not applicable: state/default rendering only; covered by
component/unit tests)
- [x] I have updated relevant documentation to reflect my changes (not
applicable: no user-facing command or docs contract changed)
- [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
+ Live OpenCode model discovery only runs for Local environments. Using the curated list and manual entry for {currentDefaultEnvironment.name}. +
+ )} {supportsModelProfiles && (` is provided
+
+For the real `--publish` step, the maintainer machine must already be authenticated to npm.
+If `npm whoami` returns `401`, first run `npm logout --registry=https://registry.npmjs.org/` to clear any stale local auth, then run `npm login` or `npm adduser` locally as an npm org member, and finally rerun the helper.
+That local human auth is fine for the one-time bootstrap publish; we just do not want the same auth model inside CI.
+The helper now requires `--otp ` up front for `--publish`, so it fails before the real publish attempt if the one-time password is missing.
+
+After that first publish succeeds:
+
+1. open `https://www.npmjs.com/package/@paperclipai/adapter-acpx-local`
+2. go to `Settings` → `Trusted publishing`
+3. add repository `paperclipai/paperclip`
+4. set workflow filename to `release.yml`
+5. optionally go to `Settings` → `Publishing access` and enable `Require two-factor authentication and disallow tokens`
+6. keep `publishFromCi: true` in [`scripts/release-package-manifest.json`](../scripts/release-package-manifest.json)
+
+Once those steps are done, future canary and stable publishes for that package are automated through GitHub OIDC. The manual step is only the first package creation on npm.
+
## Rollback model
Rollback does not unpublish anything.
diff --git a/doc/RELEASE-AUTOMATION-SETUP.md b/doc/RELEASE-AUTOMATION-SETUP.md
index 25982892d35..d6e08b9f1a5 100644
--- a/doc/RELEASE-AUTOMATION-SETUP.md
+++ b/doc/RELEASE-AUTOMATION-SETUP.md
@@ -67,6 +67,27 @@ Why:
- the single `release.yml` workflow handles both canary and stable publishing
- GitHub environments `npm-canary` and `npm-stable` still enforce different approval rules on the GitHub side
+### 2.2.1. Newly added public packages need a bootstrap phase
+
+Trusted publishing is configured on the npm package itself, not at the repo scope.
+That means a brand-new public package must not be auto-enrolled into CI publishing until its npm package exists and its trusted publisher has been configured.
+
+Repo policy:
+
+1. add every non-private package to [`scripts/release-package-manifest.json`](../scripts/release-package-manifest.json)
+2. set `"publishFromCi": true` only when CI is expected to publish that package
+3. if the package is not ready for CI publishing yet, keep `"publishFromCi": false`
+4. complete the package bootstrap before merging any PR that changes a release-enabled new package
+
+Bootstrap sequence for a new package:
+
+1. publish the package once from a trusted maintainer machine using normal npm auth
+2. open that package on npm and add the `paperclipai/paperclip` trusted publisher for `.github/workflows/release.yml`
+3. rerun or dry-run the release flow as needed to confirm CI publishing now works
+4. only then enable `"publishFromCi": true`
+
+PR CI enforces this by checking changed release-enabled package manifests against npm. That keeps `master` canary publishing healthy while preserving the no-long-lived-token model for normal CI releases.
+
### 2.3. Verify trusted publishing before removing old auth
After the workflows are live:
diff --git a/package.json b/package.json
index 7a9ec0c27f4..9d6b5bec65e 100644
--- a/package.json
+++ b/package.json
@@ -30,13 +30,14 @@
"release:stable": "./scripts/release.sh stable",
"release:github": "./scripts/create-github-release.sh",
"release:rollback": "./scripts/rollback-latest.sh",
+ "release:bootstrap-package": "node scripts/bootstrap-npm-package.mjs",
"check:tokens": "node scripts/check-forbidden-tokens.mjs",
"docs:dev": "cd docs && npx mintlify dev",
"smoke:openclaw-join": "./scripts/smoke/openclaw-join.sh",
"smoke:openclaw-docker-ui": "./scripts/smoke/openclaw-docker-ui.sh",
"smoke:openclaw-sse-standalone": "./scripts/smoke/openclaw-sse-standalone.sh",
"smoke:terminal-bench-loop-skill": "node scripts/smoke/terminal-bench-loop-skill-smoke.mjs",
- "test:release-registry": "node --test scripts/verify-release-registry-state.test.mjs",
+ "test:release-registry": "node --test scripts/verify-release-registry-state.test.mjs scripts/release-package-map.test.mjs scripts/check-release-package-bootstrap.test.mjs",
"test:e2e": "npx playwright test --config tests/e2e/playwright.config.ts",
"test:e2e:headed": "npx playwright test --config tests/e2e/playwright.config.ts --headed",
"test:e2e:multiuser-authenticated": "npx playwright test --config tests/e2e/playwright-multiuser-authenticated.config.ts",
diff --git a/scripts/bootstrap-npm-package.mjs b/scripts/bootstrap-npm-package.mjs
new file mode 100644
index 00000000000..9b1e01beb04
--- /dev/null
+++ b/scripts/bootstrap-npm-package.mjs
@@ -0,0 +1,294 @@
+#!/usr/bin/env node
+
+import { spawnSync } from "node:child_process";
+import { fileURLToPath } from "node:url";
+import { dirname, join, resolve } from "node:path";
+
+import { buildReleasePackagePlan } from "./release-package-map.mjs";
+
+const __dirname = dirname(fileURLToPath(import.meta.url));
+const repoRoot = resolve(__dirname, "..");
+
+function normalizePath(filePath) {
+ return filePath.replace(/\\/g, "/").replace(/^\.\//, "");
+}
+
+function usage() {
+ process.stderr.write(
+ [
+ "Usage:",
+ " node scripts/bootstrap-npm-package.mjs [--publish --otp ] [--skip-build]",
+ "",
+ "Examples:",
+ " node scripts/bootstrap-npm-package.mjs @paperclipai/adapter-acpx-local",
+ " node scripts/bootstrap-npm-package.mjs packages/adapters/acpx-local --publish",
+ "",
+ ].join("\n"),
+ );
+}
+
+function parseArgs(argv) {
+ const flags = new Set();
+ let selector = null;
+ let otp = null;
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const arg = argv[index];
+ if (arg === "--") {
+ continue;
+ }
+
+ if (arg === "--publish" || arg === "--skip-build") {
+ flags.add(arg);
+ continue;
+ }
+
+ if (arg === "--otp") {
+ const value = argv[index + 1];
+ if (!value || value.startsWith("--")) {
+ throw new Error("expected a one-time password after --otp");
+ }
+ otp = value;
+ index += 1;
+ continue;
+ }
+
+ if (arg === "--help" || arg === "-h") {
+ return { help: true, selector: null, publish: false, skipBuild: false, otp: null };
+ }
+
+ if (arg.startsWith("--")) {
+ throw new Error(`unknown option: ${arg}`);
+ }
+
+ if (selector) {
+ throw new Error("expected exactly one package selector");
+ }
+
+ selector = arg;
+ }
+
+ return {
+ help: false,
+ selector,
+ publish: flags.has("--publish"),
+ skipBuild: flags.has("--skip-build"),
+ otp,
+ };
+}
+
+function runCommand(command, args, options = {}) {
+ const result = spawnSync(command, args, {
+ cwd: repoRoot,
+ encoding: "utf8",
+ stdio: ["inherit", "pipe", "pipe"],
+ ...options,
+ });
+
+ if (result.error) {
+ throw result.error;
+ }
+
+ return result;
+}
+
+function runChecked(command, args, options = {}) {
+ const result = runCommand(command, args, options);
+ const stdout = result.stdout ?? "";
+ const stderr = result.stderr ?? "";
+
+ if (stdout) process.stdout.write(stdout);
+ if (stderr) process.stderr.write(stderr);
+
+ if (result.status !== 0) {
+ throw new Error(`${command} ${args.join(" ")} failed with status ${result.status ?? "unknown"}`);
+ }
+}
+
+function formatCommand(command, args) {
+ return `${command} ${args.join(" ")}`;
+}
+
+function ensureNpmAuth() {
+ const result = runCommand("npm", ["whoami"]);
+ const stdout = result.stdout ?? "";
+ const stderr = result.stderr ?? "";
+
+ if (stdout) process.stdout.write(stdout);
+ if (stderr) process.stderr.write(stderr);
+
+ if (result.status === 0) {
+ return;
+ }
+
+ const output = `${stdout}\n${stderr}`.trim();
+ if (/\bE401\b|401 Unauthorized/i.test(output)) {
+ throw new Error(
+ [
+ "npm auth check failed.",
+ "This usually means the machine is either not logged into npm yet or has a stale token in ~/.npmrc.",
+ "Run `npm logout --registry=https://registry.npmjs.org/` and then `npm login` or `npm adduser` on this maintainer machine with an npm account that can publish to the @paperclipai scope, then rerun with --publish.",
+ "Do not use this auth flow in CI; it is only for the one-time human bootstrap publish.",
+ ].join(" "),
+ );
+ }
+
+ throw new Error("npm whoami failed");
+}
+
+function inspectNpmPackage(packageName) {
+ const result = runCommand("npm", ["view", packageName, "version", "--json"]);
+
+ if (result.status === 0) {
+ const version = JSON.parse((result.stdout ?? "").trim());
+ return { exists: true, version };
+ }
+
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
+ if (/\bE404\b|404 Not Found|could not be found/i.test(output)) {
+ return { exists: false };
+ }
+
+ process.stderr.write(output ? `${output}\n` : "");
+ throw new Error(`failed to query npm for ${packageName}`);
+}
+
+function resolveTargetPackage(selector, packages = buildReleasePackagePlan()) {
+ const normalizedSelector = normalizePath(selector);
+ const matches = packages.filter(
+ (pkg) => pkg.name === selector || normalizePath(pkg.dir) === normalizedSelector,
+ );
+
+ if (matches.length === 1) {
+ return matches[0];
+ }
+
+ if (matches.length > 1) {
+ throw new Error(`package selector is ambiguous: ${selector}`);
+ }
+
+ throw new Error(
+ `unknown package selector: ${selector}\nKnown packages:\n- ${packages.map((pkg) => `${pkg.name} (${pkg.dir})`).join("\n- ")}`,
+ );
+}
+
+function printNextSteps(pkg) {
+ process.stdout.write(
+ [
+ "",
+ "Publish succeeded. Next:",
+ `1. Open https://www.npmjs.com/package/${pkg.name}`,
+ "2. Go to Settings -> Trusted publishing",
+ "3. Add repository paperclipai/paperclip",
+ "4. Set workflow filename to release.yml",
+ "5. Optionally enable Settings -> Publishing access -> Require two-factor authentication and disallow tokens",
+ "",
+ ].join("\n"),
+ );
+}
+
+function publishPackage(pkg, otp) {
+ const publishArgs = ["publish", "--access", "public"];
+ if (otp) {
+ publishArgs.push("--otp", otp);
+ }
+
+ const result = runCommand("npm", publishArgs, { cwd: join(repoRoot, pkg.dir) });
+ const stdout = result.stdout ?? "";
+ const stderr = result.stderr ?? "";
+ const output = `${stdout}\n${stderr}`.trim();
+
+ if (stdout) process.stdout.write(stdout);
+ if (stderr) process.stderr.write(stderr);
+
+ if (result.status === 0) {
+ return;
+ }
+
+ if (/\bEOTP\b|one-time password/i.test(output)) {
+ throw new Error(
+ [
+ "npm publish reached the publish-time 2FA check.",
+ "Complete the browser auth URL printed by npm and rerun the helper, or rerun with `--otp ` if your npm account uses authenticator-app codes.",
+ ].join(" "),
+ );
+ }
+
+ throw new Error(`${formatCommand("npm", publishArgs)} failed with status ${result.status ?? "unknown"}`);
+}
+
+function main(argv) {
+ const { help, selector, publish, skipBuild, otp } = parseArgs(argv);
+
+ if (help) {
+ usage();
+ return;
+ }
+
+ if (!selector) {
+ usage();
+ throw new Error("missing package selector");
+ }
+
+ const pkg = resolveTargetPackage(selector);
+ process.stdout.write(`Selected ${pkg.name} (${pkg.dir})\n`);
+
+ if (publish && !otp) {
+ throw new Error("`--publish` requires `--otp `. Generate a fresh npm one-time password and rerun.");
+ }
+
+ const npmState = inspectNpmPackage(pkg.name);
+ if (npmState.exists) {
+ throw new Error(`${pkg.name} already exists on npm at version ${npmState.version}; bootstrap is only for first publish`);
+ }
+
+ process.stdout.write(`${pkg.name} is not on npm yet; continuing with bootstrap flow.\n`);
+
+ if (publish) {
+ process.stdout.write("Checking npm auth with npm whoami...\n");
+ ensureNpmAuth();
+ }
+
+ if (!skipBuild && typeof pkg.pkg?.scripts?.build === "string") {
+ process.stdout.write(`Building ${pkg.name}...\n`);
+ runChecked("pnpm", ["--filter", pkg.name, "build"]);
+ }
+
+ process.stdout.write(`Previewing publish payload for ${pkg.name}...\n`);
+ runChecked("npm", ["pack", "--dry-run"], { cwd: join(repoRoot, pkg.dir) });
+
+ if (!publish) {
+ process.stdout.write(
+ [
+ "",
+ "Dry run complete. To perform the first publish from an authenticated maintainer machine, run:",
+ `node scripts/bootstrap-npm-package.mjs ${pkg.name} --publish --otp `,
+ "",
+ ].join("\n"),
+ );
+ return;
+ }
+
+ process.stdout.write(`Publishing ${pkg.name}...\n`);
+ publishPackage(pkg, otp);
+ printNextSteps(pkg);
+}
+
+const isDirectRun = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
+
+if (isDirectRun) {
+ try {
+ main(process.argv.slice(2));
+ } catch (error) {
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
+ process.exit(1);
+ }
+}
+
+export {
+ ensureNpmAuth,
+ inspectNpmPackage,
+ parseArgs,
+ publishPackage,
+ resolveTargetPackage,
+};
diff --git a/scripts/bootstrap-npm-package.test.mjs b/scripts/bootstrap-npm-package.test.mjs
new file mode 100644
index 00000000000..48deb739583
--- /dev/null
+++ b/scripts/bootstrap-npm-package.test.mjs
@@ -0,0 +1,54 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import { parseArgs, resolveTargetPackage } from "./bootstrap-npm-package.mjs";
+
+test("parseArgs recognizes publish and skip-build flags", () => {
+ assert.deepEqual(parseArgs(["@paperclipai/adapter-acpx-local", "--publish", "--skip-build"]), {
+ help: false,
+ selector: "@paperclipai/adapter-acpx-local",
+ publish: true,
+ skipBuild: true,
+ otp: null,
+ });
+});
+
+test("parseArgs accepts an explicit otp value", () => {
+ assert.deepEqual(parseArgs(["packages/adapters/acpx-local", "--publish", "--otp", "123456"]), {
+ help: false,
+ selector: "packages/adapters/acpx-local",
+ publish: true,
+ skipBuild: false,
+ otp: "123456",
+ });
+});
+
+test("parseArgs leaves otp null when omitted", () => {
+ assert.deepEqual(parseArgs(["packages/adapters/acpx-local", "--publish"]), {
+ help: false,
+ selector: "packages/adapters/acpx-local",
+ publish: true,
+ skipBuild: false,
+ otp: null,
+ });
+});
+
+test("parseArgs returns help mode", () => {
+ assert.deepEqual(parseArgs(["--help"]), {
+ help: true,
+ selector: null,
+ publish: false,
+ skipBuild: false,
+ otp: null,
+ });
+});
+
+test("resolveTargetPackage matches by package name or dir", () => {
+ const packages = [
+ { dir: "packages/a", name: "@paperclipai/a", pkg: {} },
+ { dir: "packages/b", name: "@paperclipai/b", pkg: {} },
+ ];
+
+ assert.equal(resolveTargetPackage("@paperclipai/a", packages).dir, "packages/a");
+ assert.equal(resolveTargetPackage("./packages/b", packages).name, "@paperclipai/b");
+});
diff --git a/scripts/check-release-package-bootstrap.mjs b/scripts/check-release-package-bootstrap.mjs
new file mode 100644
index 00000000000..a9d792df807
--- /dev/null
+++ b/scripts/check-release-package-bootstrap.mjs
@@ -0,0 +1,206 @@
+#!/usr/bin/env node
+
+import { spawnSync } from "node:child_process";
+
+import { buildReleasePackagePlan } from "./release-package-map.mjs";
+
+function normalizePath(filePath) {
+ return filePath.replace(/\\/g, "/");
+}
+
+function classifyNpmViewFailure(output) {
+ return /\bE404\b|404 Not Found|could not be found/i.test(output) ? "missing" : "registry_error";
+}
+
+function inspectNpmPackage(packageName) {
+ const result = spawnSync("npm", ["view", packageName, "name", "--json"], {
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+
+ if (result.error) {
+ throw result.error;
+ }
+
+ if (result.status === 0) {
+ return { status: "exists" };
+ }
+
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
+ const failureType = classifyNpmViewFailure(output);
+
+ if (failureType === "missing") {
+ return { status: "missing" };
+ }
+
+ return {
+ status: "registry_error",
+ detail: output || `npm view exited with status ${result.status ?? "unknown"}`,
+ };
+}
+
+function readGitFileAtRevision(revision, filePath) {
+ const result = spawnSync("git", ["show", `${revision}:${normalizePath(filePath)}`], {
+ encoding: "utf8",
+ stdio: ["ignore", "pipe", "pipe"],
+ });
+
+ if (result.error) {
+ throw result.error;
+ }
+
+ if (result.status === 0) {
+ return result.stdout;
+ }
+
+ const output = `${result.stdout ?? ""}\n${result.stderr ?? ""}`.trim();
+
+ if (
+ /exists on disk, but not in/i.test(output) ||
+ /does not exist in/i.test(output)
+ ) {
+ return null;
+ }
+
+ throw new Error(`failed to read ${filePath} at ${revision}:\n${output || "git show failed"}`);
+}
+
+function getBaseReleaseState(
+ revision,
+ releasePackages = buildReleasePackagePlan(),
+ readFileAtRevision = readGitFileAtRevision,
+) {
+ if (!revision) return null;
+
+ const manifestText = readFileAtRevision(revision, "scripts/release-package-manifest.json");
+
+ if (manifestText) {
+ const manifestEntries = JSON.parse(manifestText);
+
+ if (!Array.isArray(manifestEntries)) {
+ throw new Error(`expected scripts/release-package-manifest.json at ${revision} to contain an array`);
+ }
+
+ return {
+ source: "manifest",
+ byDir: new Map(
+ manifestEntries
+ .filter((entry) => entry?.publishFromCi === true && typeof entry.dir === "string" && typeof entry.name === "string")
+ .map((entry) => [entry.dir, { name: entry.name, publishFromCi: true }]),
+ ),
+ };
+ }
+
+ const byDir = new Map();
+
+ for (const pkg of releasePackages) {
+ const packageJsonText = readFileAtRevision(revision, `${pkg.dir}/package.json`);
+ if (!packageJsonText) continue;
+
+ const basePackage = JSON.parse(packageJsonText);
+ if (basePackage.private) continue;
+
+ byDir.set(pkg.dir, {
+ name: basePackage.name,
+ publishFromCi: true,
+ });
+ }
+
+ return {
+ source: "public-packages",
+ byDir,
+ };
+}
+
+function collectReleasePackagesForChangedPaths(
+ changedPaths,
+ releasePackages = buildReleasePackagePlan(),
+ baseReleaseState = null,
+) {
+ const normalizedChangedPaths = changedPaths.map(normalizePath);
+ const manifestFileChanged = normalizedChangedPaths.includes("scripts/release-package-manifest.json");
+ const changedReleasePackages = [];
+ const seen = new Set();
+
+ for (const pkg of releasePackages) {
+ if (!pkg.publishFromCi) continue;
+ const packageJsonPath = `${pkg.dir}/package.json`;
+ const packageJsonChanged = normalizedChangedPaths.includes(packageJsonPath);
+ const basePackage = baseReleaseState?.byDir.get(pkg.dir);
+ const newlyReleaseEnabled =
+ manifestFileChanged &&
+ (!baseReleaseState || !basePackage || basePackage.publishFromCi !== true || basePackage.name !== pkg.name);
+ const isRelevant = packageJsonChanged || newlyReleaseEnabled;
+
+ if (!isRelevant) continue;
+ if (seen.has(pkg.name)) continue;
+
+ changedReleasePackages.push(pkg);
+ seen.add(pkg.name);
+ }
+
+ return changedReleasePackages;
+}
+
+function main(changedPaths) {
+ const releasePackages = buildReleasePackagePlan();
+ const baseReleaseState = getBaseReleaseState(process.env.PAPERCLIP_RELEASE_BOOTSTRAP_BASE_SHA, releasePackages);
+ const changedReleasePackages = collectReleasePackagesForChangedPaths(changedPaths, releasePackages, baseReleaseState);
+
+ if (changedReleasePackages.length === 0) {
+ process.stdout.write("No release-enabled package manifests changed in this PR.\n");
+ return;
+ }
+
+ const missingPackages = [];
+ const registryFailures = [];
+
+ for (const pkg of changedReleasePackages) {
+ const npmStatus = inspectNpmPackage(pkg.name);
+
+ if (npmStatus.status === "missing") {
+ missingPackages.push(pkg);
+ continue;
+ }
+
+ if (npmStatus.status === "registry_error") {
+ registryFailures.push({ pkg, detail: npmStatus.detail });
+ }
+ }
+
+ if (missingPackages.length > 0) {
+ const details = missingPackages
+ .map(
+ (pkg) =>
+ `${pkg.name} (${pkg.dir}) is release-enabled but does not exist on npm yet; bootstrap the first publish before merge or keep it out of CI release enrollment`,
+ )
+ .join("\n- ");
+
+ throw new Error(`release package bootstrap check failed:\n- ${details}`);
+ }
+
+ if (registryFailures.length > 0) {
+ const details = registryFailures
+ .map(
+ ({ pkg, detail }) =>
+ `${pkg.name} (${pkg.dir}) could not be checked against npm due to a registry error:\n${detail}`,
+ )
+ .join("\n- ");
+
+ throw new Error(`release package bootstrap check could not verify npm state:\n- ${details}`);
+ }
+
+ process.stdout.write(
+ `Release bootstrap OK for changed manifests: ${changedReleasePackages.map((pkg) => pkg.name).join(", ")}\n`,
+ );
+}
+
+if (process.argv[1] && normalizePath(process.argv[1]).endsWith("scripts/check-release-package-bootstrap.mjs")) {
+ main(process.argv.slice(2));
+}
+
+export {
+ classifyNpmViewFailure,
+ collectReleasePackagesForChangedPaths,
+ getBaseReleaseState,
+};
diff --git a/scripts/check-release-package-bootstrap.test.mjs b/scripts/check-release-package-bootstrap.test.mjs
new file mode 100644
index 00000000000..90c923089da
--- /dev/null
+++ b/scripts/check-release-package-bootstrap.test.mjs
@@ -0,0 +1,104 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ classifyNpmViewFailure,
+ collectReleasePackagesForChangedPaths,
+ getBaseReleaseState,
+} from "./check-release-package-bootstrap.mjs";
+
+test("manifest changes without base state validate all release-enabled packages", () => {
+ const releasePackages = [
+ { dir: "packages/a", name: "@paperclipai/a", publishFromCi: true },
+ { dir: "packages/b", name: "@paperclipai/b", publishFromCi: true },
+ { dir: "packages/c", name: "@paperclipai/c", publishFromCi: false },
+ ];
+
+ const changedPackages = collectReleasePackagesForChangedPaths(
+ ["scripts/release-package-manifest.json"],
+ releasePackages,
+ );
+
+ assert.deepEqual(
+ changedPackages.map((pkg) => pkg.name),
+ ["@paperclipai/a", "@paperclipai/b"],
+ );
+});
+
+test("manifest changes only validate newly release-enabled packages relative to base state", () => {
+ const releasePackages = [
+ { dir: "packages/a", name: "@paperclipai/a", publishFromCi: true },
+ { dir: "packages/b", name: "@paperclipai/b", publishFromCi: true },
+ { dir: "packages/c", name: "@paperclipai/c", publishFromCi: false },
+ ];
+ const baseReleaseState = {
+ source: "manifest",
+ byDir: new Map([["packages/a", { name: "@paperclipai/a", publishFromCi: true }]]),
+ };
+
+ const changedPackages = collectReleasePackagesForChangedPaths(
+ ["scripts/release-package-manifest.json"],
+ releasePackages,
+ baseReleaseState,
+ );
+
+ assert.deepEqual(
+ changedPackages.map((pkg) => pkg.name),
+ ["@paperclipai/b"],
+ );
+});
+
+test("package-specific changes only validate affected release-enabled packages", () => {
+ const releasePackages = [
+ { dir: "packages/a", name: "@paperclipai/a", publishFromCi: true },
+ { dir: "packages/b", name: "@paperclipai/b", publishFromCi: true },
+ ];
+
+ const changedPackages = collectReleasePackagesForChangedPaths(
+ ["packages/b/package.json", "README.md"],
+ releasePackages,
+ );
+
+ assert.deepEqual(
+ changedPackages.map((pkg) => pkg.name),
+ ["@paperclipai/b"],
+ );
+});
+
+test("npm E404 failures are treated as missing packages", () => {
+ assert.equal(classifyNpmViewFailure("npm error code E404"), "missing");
+ assert.equal(classifyNpmViewFailure("404 Not Found"), "missing");
+});
+
+test("non-404 npm failures are treated as registry errors", () => {
+ assert.equal(classifyNpmViewFailure("npm error code EAI_AGAIN"), "registry_error");
+ assert.equal(classifyNpmViewFailure("npm error code E429"), "registry_error");
+});
+
+test("base release state falls back to public packages when manifest is absent", () => {
+ const releasePackages = [
+ { dir: "packages/a", name: "@paperclipai/a", publishFromCi: true },
+ { dir: "packages/b", name: "@paperclipai/b", publishFromCi: true },
+ ];
+
+ const baseReleaseState = getBaseReleaseState("base-sha", releasePackages, (_revision, filePath) => {
+ if (filePath === "scripts/release-package-manifest.json") {
+ return null;
+ }
+
+ if (filePath === "packages/a/package.json") {
+ return JSON.stringify({ name: "@paperclipai/a", private: false });
+ }
+
+ if (filePath === "packages/b/package.json") {
+ return JSON.stringify({ name: "@paperclipai/b", private: true });
+ }
+
+ return null;
+ });
+
+ assert.equal(baseReleaseState?.source, "public-packages");
+ assert.deepEqual([...baseReleaseState.byDir.entries()], [
+ ["packages/a", { name: "@paperclipai/a", publishFromCi: true }],
+ ]);
+});
diff --git a/scripts/release-package-manifest.json b/scripts/release-package-manifest.json
new file mode 100644
index 00000000000..a81cf28f93b
--- /dev/null
+++ b/scripts/release-package-manifest.json
@@ -0,0 +1,92 @@
+[
+ {
+ "dir": "packages/adapter-utils",
+ "name": "@paperclipai/adapter-utils",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/adapters/acpx-local",
+ "name": "@paperclipai/adapter-acpx-local",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/adapters/claude-local",
+ "name": "@paperclipai/adapter-claude-local",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/adapters/codex-local",
+ "name": "@paperclipai/adapter-codex-local",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/adapters/cursor-local",
+ "name": "@paperclipai/adapter-cursor-local",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/adapters/gemini-local",
+ "name": "@paperclipai/adapter-gemini-local",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/adapters/opencode-local",
+ "name": "@paperclipai/adapter-opencode-local",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/adapters/pi-local",
+ "name": "@paperclipai/adapter-pi-local",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/adapters/openclaw-gateway",
+ "name": "@paperclipai/adapter-openclaw-gateway",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/shared",
+ "name": "@paperclipai/shared",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/db",
+ "name": "@paperclipai/db",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/plugins/sdk",
+ "name": "@paperclipai/plugin-sdk",
+ "publishFromCi": true
+ },
+ {
+ "dir": "server",
+ "name": "@paperclipai/server",
+ "publishFromCi": true
+ },
+ {
+ "dir": "cli",
+ "name": "paperclipai",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/mcp-server",
+ "name": "@paperclipai/mcp-server",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/plugins/create-paperclip-plugin",
+ "name": "@paperclipai/create-paperclip-plugin",
+ "publishFromCi": true
+ },
+ {
+ "dir": "packages/plugins/sandbox-providers/e2b",
+ "name": "@paperclipai/plugin-e2b",
+ "publishFromCi": true
+ },
+ {
+ "dir": "ui",
+ "name": "@paperclipai/ui",
+ "publishFromCi": true
+ }
+]
diff --git a/scripts/release-package-map.mjs b/scripts/release-package-map.mjs
index 60deba31044..a990b88ef78 100644
--- a/scripts/release-package-map.mjs
+++ b/scripts/release-package-map.mjs
@@ -6,6 +6,7 @@ import { dirname, join, resolve } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(__dirname, "..");
+const manifestPath = join(repoRoot, "scripts", "release-package-manifest.json");
const roots = ["packages", "server", "ui", "cli"];
function readJson(filePath) {
@@ -48,6 +49,84 @@ function discoverPublicPackages() {
return packages;
}
+function loadReleaseManifest() {
+ const manifest = readJson(manifestPath);
+
+ if (!Array.isArray(manifest)) {
+ throw new Error(`expected ${manifestPath} to contain an array.`);
+ }
+
+ return manifest.map((entry, index) => {
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
+ throw new Error(`manifest entry ${index + 1} in ${manifestPath} must be an object.`);
+ }
+
+ if (typeof entry.dir !== "string" || entry.dir.length === 0) {
+ throw new Error(`manifest entry ${index + 1} in ${manifestPath} is missing a non-empty "dir".`);
+ }
+
+ if (typeof entry.name !== "string" || entry.name.length === 0) {
+ throw new Error(`manifest entry ${index + 1} in ${manifestPath} is missing a non-empty "name".`);
+ }
+
+ if (typeof entry.publishFromCi !== "boolean") {
+ throw new Error(
+ `manifest entry ${index + 1} (${entry.dir}) in ${manifestPath} must set boolean "publishFromCi".`,
+ );
+ }
+
+ return entry;
+ });
+}
+
+function buildReleasePackagePlan() {
+ const discoveredPackages = discoverPublicPackages();
+ const manifestEntries = loadReleaseManifest();
+ const packageByDir = new Map(discoveredPackages.map((pkg) => [pkg.dir, pkg]));
+ const manifestByDir = new Map();
+ const problems = [];
+
+ for (const entry of manifestEntries) {
+ if (manifestByDir.has(entry.dir)) {
+ problems.push(`duplicate manifest entry for ${entry.dir}`);
+ continue;
+ }
+
+ manifestByDir.set(entry.dir, entry);
+ const pkg = packageByDir.get(entry.dir);
+
+ if (!pkg) {
+ problems.push(`${entry.dir} is listed in ${manifestPath} but is not a public package in this repo`);
+ continue;
+ }
+
+ if (pkg.name !== entry.name) {
+ problems.push(
+ `${entry.dir} is listed as ${entry.name} in ${manifestPath}, but package.json declares ${pkg.name}`,
+ );
+ }
+ }
+
+ for (const pkg of discoveredPackages) {
+ if (!manifestByDir.has(pkg.dir)) {
+ problems.push(
+ `${pkg.dir} (${pkg.name}) is public but missing from ${manifestPath}; add it with publishFromCi true or false`,
+ );
+ }
+ }
+
+ if (problems.length > 0) {
+ throw new Error(`release package manifest validation failed:\n- ${problems.join("\n- ")}`);
+ }
+
+ const packages = discoveredPackages.map((pkg) => ({
+ ...pkg,
+ publishFromCi: manifestByDir.get(pkg.dir).publishFromCi,
+ }));
+
+ return packages;
+}
+
function sortTopologically(packages) {
const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
const visited = new Set();
@@ -57,7 +136,7 @@ function sortTopologically(packages) {
function visit(pkg) {
if (visited.has(pkg.name)) return;
if (visiting.has(pkg.name)) {
- throw new Error(`cycle detected in public package graph at ${pkg.name}`);
+ throw new Error(`cycle detected in release package graph at ${pkg.name}`);
}
visiting.add(pkg.name);
@@ -87,6 +166,10 @@ function sortTopologically(packages) {
return ordered;
}
+function getReleasePackages() {
+ return sortTopologically(buildReleasePackagePlan().filter((pkg) => pkg.publishFromCi));
+}
+
function replaceWorkspaceDeps(deps, version) {
if (!deps) return deps;
const next = { ...deps };
@@ -101,7 +184,7 @@ function replaceWorkspaceDeps(deps, version) {
}
function setVersion(version) {
- const packages = sortTopologically(discoverPublicPackages());
+ const packages = getReleasePackages();
for (const pkg of packages) {
const nextPkg = {
@@ -134,17 +217,32 @@ function setVersion(version) {
}
function listPackages() {
- const packages = sortTopologically(discoverPublicPackages());
+ const packages = getReleasePackages();
for (const pkg of packages) {
process.stdout.write(`${pkg.dir}\t${pkg.name}\t${pkg.version}\n`);
}
}
+function checkConfiguration() {
+ const packages = buildReleasePackagePlan();
+ const enabledCount = packages.filter((pkg) => pkg.publishFromCi).length;
+ const disabledCount = packages.length - enabledCount;
+
+ if (enabledCount === 0) {
+ throw new Error(`no packages are enabled for CI publishing in ${manifestPath}`);
+ }
+
+ process.stdout.write(
+ `Release package manifest OK: ${enabledCount} enabled for CI publish, ${disabledCount} disabled pending bootstrap.\n`,
+ );
+}
+
function usage() {
process.stderr.write(
[
"Usage:",
" node scripts/release-package-map.mjs list",
+ " node scripts/release-package-map.mjs check",
" node scripts/release-package-map.mjs set-version ",
"",
].join("\n"),
@@ -152,20 +250,36 @@ function usage() {
}
const [command, arg] = process.argv.slice(2);
+const isDirectRun = process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url);
-if (command === "list") {
- listPackages();
- process.exit(0);
-}
-
-if (command === "set-version") {
- if (!arg) {
- usage();
- process.exit(1);
+if (isDirectRun) {
+ if (command === "list") {
+ listPackages();
+ process.exit(0);
}
- setVersion(arg);
- process.exit(0);
+
+ if (command === "check") {
+ checkConfiguration();
+ process.exit(0);
+ }
+
+ if (command === "set-version") {
+ if (!arg) {
+ usage();
+ process.exit(1);
+ }
+ setVersion(arg);
+ process.exit(0);
+ }
+
+ usage();
+ process.exit(1);
}
-usage();
-process.exit(1);
+export {
+ buildReleasePackagePlan,
+ checkConfiguration,
+ discoverPublicPackages,
+ getReleasePackages,
+ loadReleaseManifest,
+};
diff --git a/scripts/release-package-map.test.mjs b/scripts/release-package-map.test.mjs
new file mode 100644
index 00000000000..704dd3ddbec
--- /dev/null
+++ b/scripts/release-package-map.test.mjs
@@ -0,0 +1,24 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+
+import {
+ buildReleasePackagePlan,
+ checkConfiguration,
+ getReleasePackages,
+} from "./release-package-map.mjs";
+
+test("release package manifest covers all public packages with explicit CI enrollment", () => {
+ const packages = buildReleasePackagePlan();
+ assert.ok(packages.length > 0);
+ assert.ok(packages.every((pkg) => typeof pkg.publishFromCi === "boolean"));
+});
+
+test("release package list only contains CI-enrolled packages", () => {
+ const enabledPackages = getReleasePackages();
+ assert.ok(enabledPackages.length > 0);
+ assert.ok(enabledPackages.every((pkg) => pkg.publishFromCi === true));
+});
+
+test("release package configuration validates successfully", () => {
+ assert.doesNotThrow(() => checkConfiguration());
+});
diff --git a/server/src/__tests__/heartbeat-plugin-environment.test.ts b/server/src/__tests__/heartbeat-plugin-environment.test.ts
index 46de3f47f42..9e92f260439 100644
--- a/server/src/__tests__/heartbeat-plugin-environment.test.ts
+++ b/server/src/__tests__/heartbeat-plugin-environment.test.ts
@@ -38,6 +38,7 @@ vi.mock("../adapters/index.js", () => ({
execute: adapterExecute,
supportsLocalAgentJwt: false,
}),
+ listAdapterModelProfiles: async () => [],
runningProcesses: new Map(),
}));
@@ -70,6 +71,7 @@ describeEmbeddedPostgres("heartbeat plugin environments", () => {
});
afterAll(async () => {
+ await db.$client.end();
await stopDb?.();
});
From ae23e02526968a6eb7ea5a609f88f50889bfb399 Mon Sep 17 00:00:00 2001
From: Dotta
Date: Sun, 3 May 2026 21:50:23 -0500
Subject: [PATCH 14/27] Support Cloud tenant identity bootstrap
Co-Authored-By: Paperclip
---
.../src/__tests__/auth-session-route.test.ts | 77 ++++++++-
server/src/middleware/auth.ts | 157 +++++++++++++++++-
server/src/types/express.d.ts | 2 +-
3 files changed, 232 insertions(+), 4 deletions(-)
diff --git a/server/src/__tests__/auth-session-route.test.ts b/server/src/__tests__/auth-session-route.test.ts
index 91eeb8a07f2..65f6cbd33a1 100644
--- a/server/src/__tests__/auth-session-route.test.ts
+++ b/server/src/__tests__/auth-session-route.test.ts
@@ -1,6 +1,6 @@
import express from "express";
import request from "supertest";
-import { describe, expect, it, vi } from "vitest";
+import { afterEach, describe, expect, it, vi } from "vitest";
import { actorMiddleware } from "../middleware/auth.js";
function createSelectChain(rows: unknown[]) {
@@ -25,6 +25,13 @@ function createDb() {
}
describe("actorMiddleware authenticated session profile", () => {
+ const originalCloudTenantToken = process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN;
+
+ afterEach(() => {
+ if (originalCloudTenantToken === undefined) delete process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN;
+ else process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN = originalCloudTenantToken;
+ });
+
it("preserves the signed-in user name and email on the board actor", async () => {
const app = express();
app.use(
@@ -58,4 +65,72 @@ describe("actorMiddleware authenticated session profile", () => {
isInstanceAdmin: false,
});
});
+
+ it("trusts Cloud tenant identity headers and seeds board access", async () => {
+ process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN = "tenant-token";
+ const inserts: Array<{ values: Record }> = [];
+ const db = {
+ insert: vi.fn(() => {
+ const chain = {
+ values(values: Record) {
+ inserts.push({ values });
+ return chain;
+ },
+ onConflictDoUpdate() {
+ return chain;
+ },
+ onConflictDoNothing() {
+ return chain;
+ },
+ returning() {
+ return Promise.resolve([{
+ companyId: inserts.at(-1)?.values.companyId,
+ membershipRole: inserts.at(-1)?.values.membershipRole,
+ status: inserts.at(-1)?.values.status,
+ }]);
+ },
+ };
+ return chain;
+ }),
+ select: vi.fn(),
+ } as any;
+ const app = express();
+ app.use(
+ actorMiddleware(db, {
+ deploymentMode: "authenticated",
+ resolveSession: async () => null,
+ }),
+ );
+ app.get("/actor", (req, res) => {
+ res.json(req.actor);
+ });
+
+ const res = await request(app)
+ .get("/actor")
+ .set("x-paperclip-cloud-tenant-token", "tenant-token")
+ .set("x-paperclip-cloud-user-id", "global-user-1")
+ .set("x-paperclip-cloud-user-email", "owner@example.com")
+ .set("x-paperclip-cloud-user-name", "Stack Owner")
+ .set("x-paperclip-cloud-stack-id", "stack-alpha")
+ .set("x-paperclip-cloud-paperclip-company-id", "paperclip-stack-alpha")
+ .set("x-paperclip-cloud-stack-role", "owner");
+
+ expect(res.status).toBe(200);
+ expect(res.body).toMatchObject({
+ type: "board",
+ userId: "global-user-1",
+ userName: "Stack Owner",
+ userEmail: "owner@example.com",
+ source: "cloud_tenant",
+ isInstanceAdmin: true,
+ memberships: [expect.objectContaining({ membershipRole: "owner", status: "active" })],
+ });
+ expect(res.body.companyIds[0]).toMatch(/^[0-9a-f-]{36}$/);
+ expect(inserts).toHaveLength(4);
+ expect(inserts[0]?.values).toMatchObject({
+ id: "global-user-1",
+ email: "owner@example.com",
+ emailVerified: true,
+ });
+ });
});
diff --git a/server/src/middleware/auth.ts b/server/src/middleware/auth.ts
index 2438aee3069..ad0b4289bb3 100644
--- a/server/src/middleware/auth.ts
+++ b/server/src/middleware/auth.ts
@@ -1,8 +1,8 @@
-import { createHash } from "node:crypto";
+import { createHash, timingSafeEqual } from "node:crypto";
import type { Request, RequestHandler } from "express";
import { and, eq, isNull } from "drizzle-orm";
import type { Db } from "@paperclipai/db";
-import { agentApiKeys, agents, companyMemberships, instanceUserRoles } from "@paperclipai/db";
+import { agentApiKeys, agents, authUsers, companies, companyMemberships, instanceUserRoles } from "@paperclipai/db";
import { verifyLocalAgentJwt } from "../agent-auth-jwt.js";
import type { DeploymentMode } from "@paperclipai/shared";
import type { BetterAuthSessionResult } from "../auth/better-auth.js";
@@ -38,6 +38,16 @@ export function actorMiddleware(db: Db, opts: ActorMiddlewareOptions): RequestHa
const authHeader = req.header("authorization");
if (!authHeader?.toLowerCase().startsWith("bearer ")) {
if (opts.deploymentMode === "authenticated" && opts.resolveSession) {
+ const cloudTenantActor = await resolveCloudTenantActor(db, req);
+ if (cloudTenantActor) {
+ req.actor = {
+ ...cloudTenantActor,
+ runId: runIdHeader ?? undefined,
+ };
+ next();
+ return;
+ }
+
let session: BetterAuthSessionResult | null = null;
try {
session = await opts.resolveSession(req);
@@ -189,6 +199,149 @@ export function actorMiddleware(db: Db, opts: ActorMiddlewareOptions): RequestHa
};
}
+async function resolveCloudTenantActor(db: Db, req: Request): Promise {
+ const expectedToken = process.env.PAPERCLIP_CLOUD_TENANT_SERVER_TOKEN?.trim();
+ if (!expectedToken) return null;
+
+ const token = req.header("x-paperclip-cloud-tenant-token")?.trim();
+ if (!token || !constantTimeStringEqual(token, expectedToken)) return null;
+
+ const userId = requiredCloudHeader(req, "x-paperclip-cloud-user-id");
+ const userEmail = requiredCloudHeader(req, "x-paperclip-cloud-user-email").toLowerCase();
+ const stackId = requiredCloudHeader(req, "x-paperclip-cloud-stack-id");
+ const stackRole = stackMembershipRole(req.header("x-paperclip-cloud-stack-role"));
+ const userName = req.header("x-paperclip-cloud-user-name")?.trim() || userEmail;
+ const paperclipCompanyId = req.header("x-paperclip-cloud-paperclip-company-id")?.trim();
+ const companyId = cloudTenantCompanyId(stackId);
+ const companyName = paperclipCompanyId || `${stackId} Paperclip`;
+ const now = new Date();
+
+ await db
+ .insert(authUsers)
+ .values({
+ id: userId,
+ name: userName,
+ email: userEmail,
+ emailVerified: true,
+ image: null,
+ createdAt: now,
+ updatedAt: now,
+ })
+ .onConflictDoUpdate({
+ target: authUsers.id,
+ set: {
+ name: userName,
+ email: userEmail,
+ emailVerified: true,
+ updatedAt: now,
+ },
+ });
+
+ await db
+ .insert(instanceUserRoles)
+ .values({
+ userId,
+ role: "instance_admin",
+ updatedAt: now,
+ })
+ .onConflictDoNothing({
+ target: [instanceUserRoles.userId, instanceUserRoles.role],
+ });
+
+ await db
+ .insert(companies)
+ .values({
+ id: companyId,
+ name: companyName,
+ description: `Provisioned by Paperclip Cloud for stack ${stackId}.`,
+ status: "active",
+ issuePrefix: issuePrefixForCloudStack(stackId),
+ updatedAt: now,
+ })
+ .onConflictDoNothing({
+ target: companies.id,
+ });
+
+ const membershipRole = stackRole === "owner" || stackRole === "admin" ? "owner" : stackRole;
+ const membership = await db
+ .insert(companyMemberships)
+ .values({
+ companyId,
+ principalType: "user",
+ principalId: userId,
+ status: "active",
+ membershipRole,
+ updatedAt: now,
+ })
+ .onConflictDoUpdate({
+ target: [
+ companyMemberships.companyId,
+ companyMemberships.principalType,
+ companyMemberships.principalId,
+ ],
+ set: {
+ status: "active",
+ membershipRole,
+ updatedAt: now,
+ },
+ })
+ .returning()
+ .then((rows) => rows[0] ?? {
+ companyId,
+ membershipRole,
+ status: "active",
+ });
+
+ return {
+ type: "board",
+ userId,
+ userName,
+ userEmail,
+ companyIds: [companyId],
+ memberships: [{
+ companyId,
+ membershipRole: membership.membershipRole,
+ status: membership.status,
+ }],
+ isInstanceAdmin: true,
+ source: "cloud_tenant",
+ };
+}
+
+function requiredCloudHeader(req: Request, name: string): string {
+ const value = req.header(name)?.trim();
+ if (!value) {
+ throw new Error(`Missing trusted Cloud tenant header ${name}`);
+ }
+ return value;
+}
+
+function stackMembershipRole(value: string | undefined): "owner" | "admin" | "member" | "support" {
+ if (value === "owner" || value === "admin" || value === "member" || value === "support") {
+ return value;
+ }
+ throw new Error("Invalid trusted Cloud tenant stack role");
+}
+
+function constantTimeStringEqual(left: string, right: string): boolean {
+ const leftBuffer = Buffer.from(left);
+ const rightBuffer = Buffer.from(right);
+ return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer);
+}
+
+function cloudTenantCompanyId(stackId: string): string {
+ const bytes = createHash("sha256").update(`paperclip-cloud-tenant-company:${stackId}`).digest();
+ bytes[6] = (bytes[6] & 0x0f) | 0x50;
+ bytes[8] = (bytes[8] & 0x3f) | 0x80;
+ const hex = bytes.subarray(0, 16).toString("hex");
+ return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20, 32)}`;
+}
+
+function issuePrefixForCloudStack(stackId: string): string {
+ const hash = createHash("sha256").update(stackId).digest("hex").slice(0, 4).toUpperCase();
+ return `PC${hash}`;
+}
+
export function requireBoard(req: Express.Request) {
return req.actor.type === "board";
}
diff --git a/server/src/types/express.d.ts b/server/src/types/express.d.ts
index 1915e2ffdb1..17f94e752e9 100644
--- a/server/src/types/express.d.ts
+++ b/server/src/types/express.d.ts
@@ -19,7 +19,7 @@ declare global {
isInstanceAdmin?: boolean;
keyId?: string;
runId?: string;
- source?: "local_implicit" | "session" | "board_key" | "agent_key" | "agent_jwt" | "none";
+ source?: "local_implicit" | "session" | "board_key" | "agent_key" | "agent_jwt" | "cloud_tenant" | "none";
};
}
}
From 47920f9c477c6a3b976f3f3d190f39db991ca9ce Mon Sep 17 00:00:00 2001
From: Devin Foley
Date: Sun, 3 May 2026 20:20:14 -0700
Subject: [PATCH 15/27] Speed up PR CI critical path (#5147)
## Thinking Path
> - Paperclip orchestrates AI agents for autonomous companies, so
developer throughput on the control plane repo directly affects how fast
the product can evolve.
> - The PR workflow is part of that throughput surface because every
change waits on it before review and merge.
> - This branch started from measured evidence that the PR critical path
was dominated by work that was either serialized unnecessarily or placed
on the wrong part of the graph.
> - The biggest concrete problems were: the canary dry run living inside
`verify`, the server isolated suites running one-by-one in a single
lane, and duplicate CI work that the PR path was paying for without
increasing coverage proportionally.
> - This pull request restructures the PR workflow so those costs are
reduced without removing the important coverage that was already
protecting release and test quality.
> - Follow-up fixes on the branch hardened the new entrypoints so they
work on clean GitHub runners and so the reduced PR typecheck path stays
self-maintaining as workspace packages evolve.
> - The benefit is materially faster PR wall-clock time while keeping
canary packaging checks, serialized-suite isolation, plugin SDK
consumers, and explicit TypeScript coverage where builds do not already
provide it.
## What Changed
- Moved the PR canary dry run into its own `Canary Dry Run` job so it
still runs on PRs but no longer extends the `verify` critical path.
- Split the custom Vitest runner into `general`, `serialized`, and `all`
modes, and added shard support for the isolated server suites.
- Added `test:run:general` and `test:run:serialized` scripts, then
rewired PR CI to fan the serialized server suites out across a 4-way
matrix.
- Added the required `@paperclipai/plugin-sdk` build preflight before
the new reduced-scope typecheck and test entrypoints so they succeed on
clean CI runners.
- Replaced the hardcoded PR build-gap list with
`scripts/run-typecheck-build-gaps.mjs`, which discovers workspace
packages whose `build` scripts skip TypeScript and runs only their
explicit `typecheck` scripts.
- Removed the redundant `pnpm build` from the PR `e2e` job because the
Playwright onboarding path boots Paperclip from source.
## Verification
- `ruby -e "require 'yaml'; YAML.load_file('.github/workflows/pr.yml');
puts 'workflow ok'"`
- `node scripts/run-vitest-stable.mjs --mode general --dry-run`
- `node scripts/run-vitest-stable.mjs --mode serialized --shard-index 0
--shard-count 4 --dry-run`
- `pnpm run typecheck:build-gaps`
- `pnpm test:run:general`
- `pnpm test:run:serialized -- --shard-index 0 --shard-count 4`
- `pnpm build`
- `pnpm paperclipai onboard --yes --run`
- `curl http://127.0.0.1:3299/api/health`
## Risks
- Branch protection or required-check configuration may need to be
updated for the new standalone `Canary Dry Run` job and the
serialized-suite matrix job names.
- `scripts/run-typecheck-build-gaps.mjs` assumes packages that need
explicit PR-time typechecking are the ones whose `build` scripts omit
`tsc`; if build conventions change, that heuristic needs to stay
aligned.
- Serialized test sharding preserves per-suite isolation, but the first
few CI runs should still be watched for shard-balance or naming
assumptions in downstream tooling.
## Model Used
- OpenAI GPT-5.4 via the Codex local adapter, using high reasoning
effort with shell, git, and file-edit tool use in a local worktree.
## 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
---
.github/workflows/pr.yml | 82 +++++++++--
package.json | 3 +
scripts/run-typecheck-build-gaps.mjs | 93 ++++++++++++
scripts/run-vitest-stable.mjs | 206 ++++++++++++++++++++++++---
4 files changed, 356 insertions(+), 28 deletions(-)
create mode 100644 scripts/run-typecheck-build-gaps.mjs
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 8ac8f710ad5..fabaab1b710 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -85,11 +85,11 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- - name: Typecheck
- run: pnpm -r typecheck
+ - name: Typecheck workspaces whose build scripts skip TypeScript
+ run: pnpm run typecheck:build-gaps
- - name: Run tests
- run: pnpm test:run
+ - name: Run general test suites
+ run: pnpm test:run:general
- name: Verify release registry test coverage
run: pnpm run test:release-registry
@@ -97,7 +97,76 @@ jobs:
- name: Build
run: pnpm build
- - name: Release canary dry run
+ verify_serialized_server:
+ name: Verify serialized server suites (${{ matrix.shard_label }})
+ needs: [policy]
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+ strategy:
+ fail-fast: false
+ matrix:
+ include:
+ - shard_index: 0
+ shard_count: 4
+ shard_label: 1/4
+ - shard_index: 1
+ shard_count: 4
+ shard_label: 2/4
+ - shard_index: 2
+ shard_count: 4
+ shard_label: 3/4
+ - shard_index: 3
+ shard_count: 4
+ shard_label: 4/4
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9.15.4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ - name: Run serialized server test shard
+ run: pnpm test:run:serialized -- --shard-index ${{ matrix.shard_index }} --shard-count ${{ matrix.shard_count }}
+
+ canary_dry_run:
+ name: Canary Dry Run
+ needs: [policy]
+ runs-on: ubuntu-latest
+ timeout-minutes: 20
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ - name: Setup pnpm
+ uses: pnpm/action-setup@v4
+ with:
+ version: 9.15.4
+
+ - name: Setup Node.js
+ uses: actions/setup-node@v4
+ with:
+ node-version: 24
+ cache: pnpm
+
+ - name: Install dependencies
+ run: pnpm install --frozen-lockfile
+
+ # `release.sh` always executes its Step 2/7 workspace build, even when
+ # `--skip-verify` bypasses the initial verification gate.
+ - name: Release canary dry run via release.sh internal build
run: |
git checkout -B master HEAD
git checkout -- pnpm-lock.yaml
@@ -126,9 +195,6 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile
- - name: Build
- run: pnpm build
-
- name: Install Playwright
run: npx playwright install --with-deps chromium
diff --git a/package.json b/package.json
index 9d6b5bec65e..152fe4711e3 100644
--- a/package.json
+++ b/package.json
@@ -15,9 +15,12 @@
"build-storybook": "pnpm --filter @paperclipai/ui build-storybook",
"build": "pnpm run preflight:workspace-links && pnpm -r build",
"typecheck": "pnpm run preflight:workspace-links && pnpm -r typecheck",
+ "typecheck:build-gaps": "pnpm run preflight:workspace-links && node scripts/run-typecheck-build-gaps.mjs",
"test": "pnpm run test:run",
"test:watch": "pnpm run preflight:workspace-links && vitest",
"test:run": "pnpm run preflight:workspace-links && node scripts/run-vitest-stable.mjs",
+ "test:run:general": "pnpm run preflight:workspace-links && pnpm --filter @paperclipai/plugin-sdk build && node scripts/run-vitest-stable.mjs --mode general",
+ "test:run:serialized": "pnpm run preflight:workspace-links && pnpm --filter @paperclipai/plugin-sdk build && node scripts/run-vitest-stable.mjs --mode serialized",
"db:generate": "pnpm --filter @paperclipai/db generate",
"db:migrate": "pnpm --filter @paperclipai/db migrate",
"issue-references:backfill": "pnpm run preflight:workspace-links && tsx scripts/backfill-issue-reference-mentions.ts",
diff --git a/scripts/run-typecheck-build-gaps.mjs b/scripts/run-typecheck-build-gaps.mjs
new file mode 100644
index 00000000000..6210ab2a17e
--- /dev/null
+++ b/scripts/run-typecheck-build-gaps.mjs
@@ -0,0 +1,93 @@
+#!/usr/bin/env node
+import { readFileSync } from "node:fs";
+import path from "node:path";
+import { spawnSync } from "node:child_process";
+
+const repoRoot = process.cwd();
+
+function fail(message) {
+ console.error(`[typecheck:build-gaps] ${message}`);
+ process.exit(1);
+}
+
+function run(command, args) {
+ const result = spawnSync(command, args, {
+ cwd: repoRoot,
+ stdio: "inherit",
+ });
+
+ if (result.error) {
+ console.error(`[typecheck:build-gaps] Failed to spawn ${command}: ${result.error.message}`);
+ process.exit(1);
+ }
+
+ if (result.status !== 0) {
+ process.exit(result.status ?? 1);
+ }
+}
+
+function readJson(filePath) {
+ return JSON.parse(readFileSync(filePath, "utf8"));
+}
+
+function listWorkspacePackages() {
+ const result = spawnSync("pnpm", ["ls", "-r", "--depth", "-1", "--json"], {
+ cwd: repoRoot,
+ encoding: "utf8",
+ });
+
+ if (result.error) {
+ fail(`Unable to spawn pnpm to list workspace packages: ${result.error.message}`);
+ }
+
+ if (result.status !== 0) {
+ fail("Unable to list pnpm workspace packages.");
+ }
+
+ return JSON.parse(result.stdout);
+}
+
+function buildSkipsTypeScript(pkg) {
+ const buildScript = pkg.scripts?.build;
+ if (typeof buildScript !== "string") {
+ return false;
+ }
+
+ return !/\btsc\b/.test(buildScript);
+}
+
+const workspacePackages = listWorkspacePackages();
+const buildGapCandidates = workspacePackages
+ .filter((workspacePkg) => workspacePkg.path !== repoRoot)
+ .map((workspacePkg) => ({
+ name: workspacePkg.name,
+ path: workspacePkg.path,
+ pkg: readJson(path.join(workspacePkg.path, "package.json")),
+ }))
+ .filter(({ pkg }) => buildSkipsTypeScript(pkg));
+const packagesMissingTypecheck = buildGapCandidates.filter(
+ ({ pkg }) => typeof pkg.scripts?.typecheck !== "string",
+);
+if (packagesMissingTypecheck.length > 0) {
+ const missingNames = packagesMissingTypecheck.map((workspacePkg) => workspacePkg.name).join(", ");
+ fail(
+ `Workspace packages with build scripts that skip tsc must define a typecheck script. Missing: ${missingNames}`,
+ );
+}
+const buildGapPackages = buildGapCandidates.filter(
+ ({ pkg }) => typeof pkg.scripts?.typecheck === "string",
+);
+
+console.log(
+ `[typecheck:build-gaps] typechecking ${buildGapPackages.length} workspace(s): ${buildGapPackages.map(({ name }) => name).join(", ") || "(none)"}`,
+);
+
+if (buildGapPackages.length === 0) {
+ process.exit(0);
+}
+
+run("pnpm", ["--filter", "@paperclipai/plugin-sdk", "build"]);
+
+for (const workspacePkg of buildGapPackages) {
+ run("pnpm", ["--filter", workspacePkg.name, "typecheck"]);
+}
diff --git a/scripts/run-vitest-stable.mjs b/scripts/run-vitest-stable.mjs
index e016719f0d6..4ded9794581 100644
--- a/scripts/run-vitest-stable.mjs
+++ b/scripts/run-vitest-stable.mjs
@@ -45,6 +45,9 @@ const additionalSerializedServerTests = new Set([
"server/src/__tests__/routines-e2e.test.ts",
]);
let invocationIndex = 0;
+const serializedModeName = "serialized";
+const generalModeName = "general";
+const allModeName = "all";
function walk(dir) {
const entries = readdirSync(dir);
@@ -77,6 +80,130 @@ function isRouteOrAuthzTest(file) {
return additionalSerializedServerTests.has(file);
}
+function fail(message) {
+ console.error(`[test:run] ${message}`);
+ process.exit(1);
+}
+
+function readOptionValue(argv, index, argName) {
+ const value = argv[index + 1];
+ if (value === undefined) {
+ fail(`Missing value for ${argName}`);
+ }
+
+ return value;
+}
+
+function parseNonNegativeInteger(value, argName) {
+ const parsed = Number(value);
+ if (value.trim() === "" || !Number.isInteger(parsed) || parsed < 0) {
+ fail(`${argName} must be a non-negative integer. Received "${value}".`);
+ }
+
+ return parsed;
+}
+
+function parsePositiveInteger(value, argName) {
+ const parsed = Number(value);
+ if (value.trim() === "" || !Number.isInteger(parsed) || parsed < 1) {
+ fail(`${argName} must be a positive integer. Received "${value}".`);
+ }
+
+ return parsed;
+}
+
+function parseCliOptions(argv) {
+ let mode = allModeName;
+ let shardIndex = null;
+ let shardCount = null;
+ let dryRun = false;
+
+ for (let index = 0; index < argv.length; index += 1) {
+ const arg = argv[index];
+ if (arg === "--") {
+ continue;
+ }
+
+ if (arg === "--mode") {
+ mode = readOptionValue(argv, index, arg);
+ index += 1;
+ continue;
+ }
+
+ if (arg.startsWith("--mode=")) {
+ mode = arg.slice("--mode=".length);
+ continue;
+ }
+
+ if (arg === "--shard-index") {
+ shardIndex = parseNonNegativeInteger(readOptionValue(argv, index, arg), arg);
+ index += 1;
+ continue;
+ }
+
+ if (arg.startsWith("--shard-index=")) {
+ shardIndex = parseNonNegativeInteger(arg.slice("--shard-index=".length), "--shard-index");
+ continue;
+ }
+
+ if (arg === "--shard-count") {
+ shardCount = parsePositiveInteger(readOptionValue(argv, index, arg), arg);
+ index += 1;
+ continue;
+ }
+
+ if (arg.startsWith("--shard-count=")) {
+ shardCount = parsePositiveInteger(arg.slice("--shard-count=".length), "--shard-count");
+ continue;
+ }
+
+ if (arg === "--dry-run") {
+ dryRun = true;
+ continue;
+ }
+
+ fail(`Unknown argument "${arg}".`);
+ }
+
+ if (!new Set([allModeName, generalModeName, serializedModeName]).has(mode)) {
+ fail(`Unknown mode "${mode}". Expected one of: ${allModeName}, ${generalModeName}, ${serializedModeName}.`);
+ }
+
+ if ((shardIndex === null) !== (shardCount === null)) {
+ fail("--shard-index and --shard-count must be provided together.");
+ }
+
+ if (mode !== serializedModeName && shardIndex !== null) {
+ fail("--shard-index/--shard-count are only valid with --mode serialized.");
+ }
+
+ if (mode === serializedModeName) {
+ const resolvedShardCount = shardCount ?? 1;
+ const resolvedShardIndex = shardIndex ?? 0;
+ if (resolvedShardIndex >= resolvedShardCount) {
+ fail(`--shard-index must be less than --shard-count. Received ${resolvedShardIndex} of ${resolvedShardCount}.`);
+ }
+
+ return {
+ mode,
+ shardIndex: resolvedShardIndex,
+ shardCount: resolvedShardCount,
+ dryRun,
+ };
+ }
+
+ return {
+ mode,
+ shardIndex: null,
+ shardCount: null,
+ dryRun,
+ };
+}
+
+function selectSerializedSuites(routeTests, shardIndex, shardCount) {
+ return routeTests.filter((_, index) => index % shardCount === shardIndex);
+}
+
function runVitest(args, label) {
console.log(`\n[test:run] ${label}`);
invocationIndex += 1;
@@ -103,6 +230,38 @@ function runVitest(args, label) {
}
}
+function runGeneralSuites(routeTests) {
+ const excludeRouteArgs = routeTests.flatMap((file) => ["--exclude", file.serverPath]);
+ for (const project of nonServerProjects) {
+ runVitest(["--project", project], `non-server project ${project}`);
+ }
+
+ runVitest(
+ ["--project", "@paperclipai/server", ...excludeRouteArgs],
+ `server suites excluding ${routeTests.length} serialized suites`,
+ );
+}
+
+function runSerializedSuites(routeTests, shardIndex, shardCount) {
+ const shardTests = selectSerializedSuites(routeTests, shardIndex, shardCount);
+ console.log(
+ `\n[test:run] serialized shard ${shardIndex + 1}/${shardCount} running ${shardTests.length} of ${routeTests.length} suites`,
+ );
+
+ for (const routeTest of shardTests) {
+ runVitest(
+ [
+ "--project",
+ "@paperclipai/server",
+ routeTest.repoPath,
+ "--pool=forks",
+ "--poolOptions.forks.isolate=true",
+ ],
+ routeTest.repoPath,
+ );
+ }
+}
+
const routeTests = walk(serverTestsDir)
.filter((file) => isRouteOrAuthzTest(toRepoPath(file)))
.map((file) => ({
@@ -111,25 +270,32 @@ const routeTests = walk(serverTestsDir)
}))
.sort((a, b) => a.repoPath.localeCompare(b.repoPath));
-const excludeRouteArgs = routeTests.flatMap((file) => ["--exclude", file.serverPath]);
-for (const project of nonServerProjects) {
- runVitest(["--project", project], `non-server project ${project}`);
-}
-
-runVitest(
- ["--project", "@paperclipai/server", ...excludeRouteArgs],
- `server suites excluding ${routeTests.length} serialized suites`,
-);
-
-for (const routeTest of routeTests) {
- runVitest(
- [
- "--project",
- "@paperclipai/server",
- routeTest.repoPath,
- "--pool=forks",
- "--poolOptions.forks.isolate=true",
- ],
- routeTest.repoPath,
+const options = parseCliOptions(process.argv.slice(2));
+if (options.dryRun) {
+ const serializedSuites =
+ options.mode === serializedModeName
+ ? selectSerializedSuites(routeTests, options.shardIndex, options.shardCount)
+ : routeTests;
+ console.log(
+ JSON.stringify(
+ {
+ mode: options.mode,
+ shardIndex: options.shardIndex,
+ shardCount: options.shardCount,
+ serializedSuiteCount: routeTests.length,
+ selectedSerializedSuites: serializedSuites.map((routeTest) => routeTest.repoPath),
+ },
+ null,
+ 2,
+ ),
);
+ process.exit(0);
+}
+
+if (options.mode === generalModeName || options.mode === allModeName) {
+ runGeneralSuites(routeTests);
+}
+
+if (options.mode === serializedModeName || options.mode === allModeName) {
+ runSerializedSuites(routeTests, options.shardIndex ?? 0, options.shardCount ?? 1);
}
From fd10404374a64a458511ed395c4f82c5774647d2 Mon Sep 17 00:00:00 2001
From: Dotta
Date: Sun, 3 May 2026 22:52:33 -0500
Subject: [PATCH 16/27] Raise Docker image build timeout
Co-Authored-By: Paperclip
---
.github/workflows/docker.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml
index 490290c2620..08ff2462f0d 100644
--- a/.github/workflows/docker.yml
+++ b/.github/workflows/docker.yml
@@ -14,7 +14,7 @@ permissions:
jobs:
build-and-push:
runs-on: ubuntu-latest
- timeout-minutes: 30
+ timeout-minutes: 60
concurrency:
group: docker-${{ github.ref }}
cancel-in-progress: true
From d6bee62f02de058a4f12ac9a10dedf8c58c4b1d5 Mon Sep 17 00:00:00 2001
From: Dotta <34892728+cryppadotta@users.noreply.github.com>
Date: Mon, 4 May 2026 13:20:58 -0500
Subject: [PATCH 17/27] Fix Cloud tenant issue identifier routes (#5196)
## Summary
- Allow Cloud tenant issue identifiers with alphanumeric prefixes, such
as `PC1897-1`, to normalize as issue references.
- Resolve those identifiers through issue detail/update routes, active
run/live run polling, activity, costs, and `issueService.getById`.
- Keep UI issue-link parsing aligned so tenant links normalize back to
`/issues/`.
## Root Cause
Cloud tenant issue prefixes include digits from the stack-id hash. The
app-side route normalization still accepted only all-letter prefixes, so
`/api/issues/PC1897-1` skipped identifier lookup and fell through as a
non-UUID id.
## Verification
- `pnpm exec vitest run packages/shared/src/issue-references.test.ts
ui/src/lib/issue-reference.test.ts
server/src/__tests__/issue-identifier-routes.test.ts
server/src/__tests__/activity-routes.test.ts
server/src/__tests__/costs-service.test.ts
server/src/__tests__/agent-live-run-routes.test.ts
server/src/__tests__/issues-service.test.ts`
- `pnpm --filter @paperclipai/shared typecheck && pnpm --filter
@paperclipai/server typecheck`
- `git diff --check`
Co-authored-by: Paperclip
---
packages/shared/src/issue-references.test.ts | 13 ++-
packages/shared/src/issue-references.ts | 4 +-
server/src/__tests__/activity-routes.test.ts | 6 +-
.../__tests__/agent-live-run-routes.test.ts | 6 +-
server/src/__tests__/costs-service.test.ts | 8 +-
.../__tests__/issue-identifier-routes.test.ts | 105 ++++++++++++++++++
server/src/__tests__/issues-service.test.ts | 10 +-
server/src/routes/activity.ts | 6 +-
server/src/routes/agents.ts | 9 +-
server/src/routes/costs.ts | 6 +-
server/src/routes/issues.ts | 12 +-
server/src/services/issues.ts | 13 ++-
ui/src/lib/issue-reference.test.ts | 5 +
ui/src/lib/issue-reference.ts | 4 +-
14 files changed, 166 insertions(+), 41 deletions(-)
create mode 100644 server/src/__tests__/issue-identifier-routes.test.ts
diff --git a/packages/shared/src/issue-references.test.ts b/packages/shared/src/issue-references.test.ts
index fa091c412a0..d4af275ae8c 100644
--- a/packages/shared/src/issue-references.test.ts
+++ b/packages/shared/src/issue-references.test.ts
@@ -10,6 +10,7 @@ import {
describe("issue references", () => {
it("normalizes identifiers to uppercase", () => {
expect(normalizeIssueIdentifier("pap-123")).toBe("PAP-123");
+ expect(normalizeIssueIdentifier("pc1a2-7")).toBe("PC1A2-7");
expect(normalizeIssueIdentifier("not-an-issue")).toBeNull();
});
@@ -27,14 +28,14 @@ describe("issue references", () => {
});
it("finds identifiers and issue paths in plain text", () => {
- expect(findIssueReferenceMatches("See PAP-1, /issues/PAP-2, and https://x.test/PAP/issues/pap-3.")).toEqual([
+ expect(findIssueReferenceMatches("See PAP-1, /issues/PC1A2-2, and https://x.test/PAP/issues/pc1a2-3.")).toEqual([
{ index: 4, length: 5, identifier: "PAP-1", matchedText: "PAP-1" },
- { index: 11, length: 13, identifier: "PAP-2", matchedText: "/issues/PAP-2" },
+ { index: 11, length: 15, identifier: "PC1A2-2", matchedText: "/issues/PC1A2-2" },
{
- index: 30,
- length: 31,
- identifier: "PAP-3",
- matchedText: "https://x.test/PAP/issues/pap-3",
+ index: 32,
+ length: 33,
+ identifier: "PC1A2-3",
+ matchedText: "https://x.test/PAP/issues/pc1a2-3",
},
]);
});
diff --git a/packages/shared/src/issue-references.ts b/packages/shared/src/issue-references.ts
index eb45ff9a815..bfdd2126fb1 100644
--- a/packages/shared/src/issue-references.ts
+++ b/packages/shared/src/issue-references.ts
@@ -1,4 +1,4 @@
-export const ISSUE_REFERENCE_IDENTIFIER_RE = /^[A-Z]+-\d+$/;
+export const ISSUE_REFERENCE_IDENTIFIER_RE = /^[A-Z][A-Z0-9]*-\d+$/;
export interface IssueReferenceMatch {
index: number;
@@ -7,7 +7,7 @@ export interface IssueReferenceMatch {
matchedText: string;
}
-const ISSUE_REFERENCE_TOKEN_RE = /https?:\/\/[^\s<>()]+|\/[^\s<>()]+|[A-Z]+-\d+/gi;
+const ISSUE_REFERENCE_TOKEN_RE = /https?:\/\/[^\s<>()]+|\/[^\s<>()]+|[A-Z][A-Z0-9]*-\d+/gi;
function preserveNewlinesAsWhitespace(value: string) {
return value.replace(/[^\n]/g, " ");
diff --git a/server/src/__tests__/activity-routes.test.ts b/server/src/__tests__/activity-routes.test.ts
index f505980db4d..270673917df 100644
--- a/server/src/__tests__/activity-routes.test.ts
+++ b/server/src/__tests__/activity-routes.test.ts
@@ -128,7 +128,7 @@ describe.sequential("activity routes", () => {
});
});
- it("resolves issue identifiers before loading runs", async () => {
+ it("resolves alphanumeric issue identifiers before loading runs", async () => {
mockIssueService.getByIdentifier.mockResolvedValue({
id: "issue-uuid-1",
companyId: "company-1",
@@ -141,10 +141,10 @@ describe.sequential("activity routes", () => {
]);
const app = await createApp();
- const res = await requestApp(app, (baseUrl) => request(baseUrl).get("/api/issues/PAP-475/runs"));
+ const res = await requestApp(app, (baseUrl) => request(baseUrl).get("/api/issues/pc1a2-475/runs"));
expect(res.status).toBe(200);
- expect(mockIssueService.getByIdentifier).toHaveBeenCalledWith("PAP-475");
+ expect(mockIssueService.getByIdentifier).toHaveBeenCalledWith("PC1A2-475");
expect(mockIssueService.getById).not.toHaveBeenCalled();
expect(mockActivityService.runsForIssue).toHaveBeenCalledWith("company-1", "issue-uuid-1");
expect(res.body).toEqual([{ runId: "run-1", adapterType: "codex_local" }]);
diff --git a/server/src/__tests__/agent-live-run-routes.test.ts b/server/src/__tests__/agent-live-run-routes.test.ts
index 35434b86ff3..470ef85d5eb 100644
--- a/server/src/__tests__/agent-live-run-routes.test.ts
+++ b/server/src/__tests__/agent-live-run-routes.test.ts
@@ -215,11 +215,11 @@ describe("agent live run routes", () => {
it("returns a compact active run payload for issue polling", async () => {
const res = await requestApp(
await createApp(),
- (baseUrl) => request(baseUrl).get("/api/issues/PAP-1295/active-run"),
+ (baseUrl) => request(baseUrl).get("/api/issues/pc1a2-1295/active-run"),
);
expect(res.status, JSON.stringify(res.body)).toBe(200);
- expect(mockIssueService.getByIdentifier).toHaveBeenCalledWith("PAP-1295");
+ expect(mockIssueService.getByIdentifier).toHaveBeenCalledWith("PC1A2-1295");
expect(mockHeartbeatService.getRunIssueSummary).toHaveBeenCalledWith("run-1");
expect(res.body).toMatchObject({
id: "run-1",
@@ -268,7 +268,7 @@ describe("agent live run routes", () => {
const res = await requestApp(
await createApp(),
- (baseUrl) => request(baseUrl).get("/api/issues/PAP-1295/active-run"),
+ (baseUrl) => request(baseUrl).get("/api/issues/PC1A2-1295/active-run"),
);
expect(res.status, JSON.stringify(res.body)).toBe(200);
diff --git a/server/src/__tests__/costs-service.test.ts b/server/src/__tests__/costs-service.test.ts
index f9e32f0f1e7..38e1f671942 100644
--- a/server/src/__tests__/costs-service.test.ts
+++ b/server/src/__tests__/costs-service.test.ts
@@ -178,12 +178,12 @@ beforeEach(() => {
mockIssueService.getById.mockResolvedValue({
id: "issue-1",
companyId: "company-1",
- identifier: "PAP-1",
+ identifier: "PC1A2-1",
});
mockIssueService.getByIdentifier.mockResolvedValue({
id: "issue-1",
companyId: "company-1",
- identifier: "PAP-1",
+ identifier: "PC1A2-1",
});
mockBudgetService.upsertPolicy.mockResolvedValue(undefined);
});
@@ -227,10 +227,10 @@ describe("cost routes", () => {
it("returns issue subtree cost summaries for issue refs", async () => {
const app = await createApp();
- const res = await request(app).get("/api/issues/PAP-1/cost-summary");
+ const res = await request(app).get("/api/issues/pc1a2-1/cost-summary");
expect(res.status).toBe(200);
- expect(mockIssueService.getByIdentifier).toHaveBeenCalledWith("PAP-1");
+ expect(mockIssueService.getByIdentifier).toHaveBeenCalledWith("PC1A2-1");
expect(mockCostService.issueTreeSummary).toHaveBeenCalledWith("company-1", "issue-1");
expect(res.body).toEqual({
issueId: "issue-1",
diff --git a/server/src/__tests__/issue-identifier-routes.test.ts b/server/src/__tests__/issue-identifier-routes.test.ts
new file mode 100644
index 00000000000..1a204db5220
--- /dev/null
+++ b/server/src/__tests__/issue-identifier-routes.test.ts
@@ -0,0 +1,105 @@
+import { randomUUID } from "node:crypto";
+import express from "express";
+import request from "supertest";
+import { afterAll, beforeAll, describe, expect, it } from "vitest";
+import { eq } from "drizzle-orm";
+import { companies, createDb, issues } from "@paperclipai/db";
+import {
+ getEmbeddedPostgresTestSupport,
+ startEmbeddedPostgresTestDatabase,
+} from "./helpers/embedded-postgres.js";
+import { errorHandler } from "../middleware/index.js";
+import { issueRoutes } from "../routes/issues.js";
+
+const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport();
+const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip;
+
+if (!embeddedPostgresSupport.supported) {
+ console.warn(
+ `Skipping embedded Postgres issue identifier route tests on this host: ${embeddedPostgresSupport.reason ?? "unsupported environment"}`,
+ );
+}
+
+describeEmbeddedPostgres("issue identifier routes", () => {
+ let db!: ReturnType;
+ let tempDb: Awaited> | null = null;
+
+ beforeAll(async () => {
+ tempDb = await startEmbeddedPostgresTestDatabase("paperclip-issue-identifier-routes-");
+ db = createDb(tempDb.connectionString);
+ }, 20_000);
+
+ afterAll(async () => {
+ await tempDb?.cleanup();
+ });
+
+ function createApp(companyId: string) {
+ const app = express();
+ app.use(express.json());
+ app.use((req, _res, next) => {
+ (req as any).actor = {
+ type: "board",
+ userId: "cloud-user-1",
+ companyIds: [companyId],
+ memberships: [{ companyId, membershipRole: "owner", status: "active" }],
+ source: "cloud_tenant",
+ isInstanceAdmin: true,
+ };
+ next();
+ });
+ app.use("/api", issueRoutes(db, {} as any));
+ app.use(errorHandler);
+ return app;
+ }
+
+ it("resolves alphanumeric Cloud tenant issue identifiers for detail reads and updates", async () => {
+ const companyId = randomUUID();
+ const issueId = randomUUID();
+
+ await db.insert(companies).values({
+ id: companyId,
+ name: "Cloud tenant",
+ issuePrefix: "PC1A2",
+ requireBoardApprovalForNewAgents: false,
+ });
+ await db.insert(issues).values({
+ id: issueId,
+ companyId,
+ issueNumber: 7,
+ identifier: "PC1A2-7",
+ title: "Tenant identifier route",
+ status: "todo",
+ priority: "medium",
+ createdByUserId: "cloud-user-1",
+ });
+
+ const app = createApp(companyId);
+ const read = await request(app).get("/api/issues/pc1a2-7");
+
+ expect(read.status, JSON.stringify(read.body)).toBe(200);
+ expect(read.body).toMatchObject({
+ id: issueId,
+ companyId,
+ identifier: "PC1A2-7",
+ });
+
+ const updated = await request(app)
+ .patch("/api/issues/PC1A2-7")
+ .send({ priority: "high" });
+
+ expect(updated.status, JSON.stringify(updated.body)).toBe(200);
+ expect(updated.body).toMatchObject({
+ id: issueId,
+ companyId,
+ identifier: "PC1A2-7",
+ priority: "high",
+ });
+
+ const stored = await db
+ .select({ priority: issues.priority })
+ .from(issues)
+ .where(eq(issues.id, issueId))
+ .then((rows) => rows[0] ?? null);
+ expect(stored?.priority).toBe("high");
+ });
+});
diff --git a/server/src/__tests__/issues-service.test.ts b/server/src/__tests__/issues-service.test.ts
index 3e2ab7940fa..eeb82316cc2 100644
--- a/server/src/__tests__/issues-service.test.ts
+++ b/server/src/__tests__/issues-service.test.ts
@@ -460,14 +460,14 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
expect(result.map((issue) => issue.id)).toEqual([grandchildId]);
});
- it("accepts issue identifiers through getById", async () => {
+ it("accepts issue identifiers with alphanumeric prefixes through getById", async () => {
const companyId = randomUUID();
const issueId = randomUUID();
await db.insert(companies).values({
id: companyId,
name: "Paperclip",
- issuePrefix: "PAP",
+ issuePrefix: "PC1A2",
requireBoardApprovalForNewAgents: false,
});
@@ -475,19 +475,19 @@ describeEmbeddedPostgres("issueService.list participantAgentId", () => {
id: issueId,
companyId,
issueNumber: 1064,
- identifier: "PAP-1064",
+ identifier: "PC1A2-1064",
title: "Feedback votes error",
status: "todo",
priority: "medium",
createdByUserId: "user-1",
});
- const issue = await svc.getById("PAP-1064");
+ const issue = await svc.getById("pc1a2-1064");
expect(issue).toEqual(
expect.objectContaining({
id: issueId,
- identifier: "PAP-1064",
+ identifier: "PC1A2-1064",
}),
);
});
diff --git a/server/src/routes/activity.ts b/server/src/routes/activity.ts
index 831bc0e7321..590ed797921 100644
--- a/server/src/routes/activity.ts
+++ b/server/src/routes/activity.ts
@@ -1,6 +1,7 @@
import { Router } from "express";
import { z } from "zod";
import type { Db } from "@paperclipai/db";
+import { normalizeIssueIdentifier } from "@paperclipai/shared";
import { validate } from "../middleware/validate.js";
import { activityService, normalizeActivityLimit } from "../services/activity.js";
import { assertAuthenticated, assertBoard, assertCompanyAccess } from "./authz.js";
@@ -24,8 +25,9 @@ export function activityRoutes(db: Db) {
const issueSvc = issueService(db);
async function resolveIssueByRef(rawId: string) {
- if (/^[A-Z]+-\d+$/i.test(rawId)) {
- return issueSvc.getByIdentifier(rawId);
+ const identifier = normalizeIssueIdentifier(rawId);
+ if (identifier) {
+ return issueSvc.getByIdentifier(identifier);
}
return issueSvc.getById(rawId);
}
diff --git a/server/src/routes/agents.ts b/server/src/routes/agents.ts
index 3e92cacf887..81e165ddd00 100644
--- a/server/src/routes/agents.ts
+++ b/server/src/routes/agents.ts
@@ -13,6 +13,7 @@ import {
createAgentSchema,
deriveAgentUrlKey,
isUuidLike,
+ normalizeIssueIdentifier,
resetAgentSessionSchema,
testAdapterEnvironmentSchema,
type AgentSkillSnapshot,
@@ -3088,8 +3089,8 @@ export function agentRoutes(
router.get("/issues/:issueId/live-runs", async (req, res) => {
const rawId = req.params.issueId as string;
const issueSvc = issueService(db);
- const isIdentifier = /^[A-Z]+-\d+$/i.test(rawId);
- const issue = isIdentifier ? await issueSvc.getByIdentifier(rawId) : await issueSvc.getById(rawId);
+ const identifier = normalizeIssueIdentifier(rawId);
+ const issue = identifier ? await issueSvc.getByIdentifier(identifier) : await issueSvc.getById(rawId);
if (!issue) {
res.status(404).json({ error: "Issue not found" });
return;
@@ -3142,8 +3143,8 @@ export function agentRoutes(
router.get("/issues/:issueId/active-run", async (req, res) => {
const rawId = req.params.issueId as string;
const issueSvc = issueService(db);
- const isIdentifier = /^[A-Z]+-\d+$/i.test(rawId);
- const issue = isIdentifier ? await issueSvc.getByIdentifier(rawId) : await issueSvc.getById(rawId);
+ const identifier = normalizeIssueIdentifier(rawId);
+ const issue = identifier ? await issueSvc.getByIdentifier(identifier) : await issueSvc.getById(rawId);
if (!issue) {
res.status(404).json({ error: "Issue not found" });
return;
diff --git a/server/src/routes/costs.ts b/server/src/routes/costs.ts
index 3f3514e69fb..6e65e33f146 100644
--- a/server/src/routes/costs.ts
+++ b/server/src/routes/costs.ts
@@ -3,6 +3,7 @@ import type { Db } from "@paperclipai/db";
import {
createCostEventSchema,
createFinanceEventSchema,
+ normalizeIssueIdentifier,
resolveBudgetIncidentSchema,
updateBudgetSchema,
upsertBudgetPolicySchema,
@@ -62,8 +63,9 @@ export function costRoutes(
const issues = issueService(db);
async function resolveIssueByRef(rawId: string) {
- if (/^[A-Z]+-\d+$/i.test(rawId)) {
- return issues.getByIdentifier(rawId);
+ const identifier = normalizeIssueIdentifier(rawId);
+ if (identifier) {
+ return issues.getByIdentifier(identifier);
}
return issues.getById(rawId);
}
diff --git a/server/src/routes/issues.ts b/server/src/routes/issues.ts
index 4a6492b531c..f37fbaa83b5 100644
--- a/server/src/routes/issues.ts
+++ b/server/src/routes/issues.ts
@@ -30,6 +30,7 @@ import {
updateIssueSchema,
getClosedIsolatedExecutionWorkspaceMessage,
isClosedIsolatedExecutionWorkspace,
+ normalizeIssueIdentifier as normalizeIssueReferenceIdentifier,
type ExecutionWorkspace,
} from "@paperclipai/shared";
import { trackAgentTaskCompleted } from "@paperclipai/shared/telemetry";
@@ -880,9 +881,10 @@ export function issueRoutes(
});
}
- async function normalizeIssueIdentifier(rawId: string): Promise {
- if (/^[A-Z]+-\d+$/i.test(rawId)) {
- const issue = await svc.getByIdentifier(rawId);
+ async function resolveIssueRouteId(rawId: string): Promise {
+ const identifier = normalizeIssueReferenceIdentifier(rawId);
+ if (identifier) {
+ const issue = await svc.getByIdentifier(identifier);
if (issue) {
return issue.id;
}
@@ -920,7 +922,7 @@ export function issueRoutes(
// Resolve issue identifiers (e.g. "PAP-39") to UUIDs for all /issues/:id routes
router.param("id", async (req, res, next, rawId) => {
try {
- req.params.id = await normalizeIssueIdentifier(rawId);
+ req.params.id = await resolveIssueRouteId(rawId);
next();
} catch (err) {
next(err);
@@ -930,7 +932,7 @@ export function issueRoutes(
// Resolve issue identifiers (e.g. "PAP-39") to UUIDs for company-scoped attachment routes.
router.param("issueId", async (req, res, next, rawId) => {
try {
- req.params.issueId = await normalizeIssueIdentifier(rawId);
+ req.params.issueId = await resolveIssueRouteId(rawId);
next();
} catch (err) {
next(err);
diff --git a/server/src/services/issues.ts b/server/src/services/issues.ts
index 48ddc5616b3..66c83389acc 100644
--- a/server/src/services/issues.ts
+++ b/server/src/services/issues.ts
@@ -33,7 +33,13 @@ import type {
IssueProductivityReviewTrigger,
IssueRelationIssueSummary,
} from "@paperclipai/shared";
-import { clampIssueRequestDepth, extractAgentMentionIds, extractProjectMentionIds, isUuidLike } from "@paperclipai/shared";
+import {
+ clampIssueRequestDepth,
+ extractAgentMentionIds,
+ extractProjectMentionIds,
+ isUuidLike,
+ normalizeIssueIdentifier as normalizeIssueReferenceIdentifier,
+} from "@paperclipai/shared";
import { conflict, notFound, unprocessable } from "../errors.js";
import { parseObject } from "../adapters/utils.js";
import {
@@ -2418,8 +2424,9 @@ export function issueService(db: Db) {
getById: async (raw: string) => {
const id = raw.trim();
- if (/^[A-Z]+-\d+$/i.test(id)) {
- return getIssueByIdentifier(id);
+ const identifier = normalizeIssueReferenceIdentifier(id);
+ if (identifier) {
+ return getIssueByIdentifier(identifier);
}
if (!isUuidLike(id)) {
return null;
diff --git a/ui/src/lib/issue-reference.test.ts b/ui/src/lib/issue-reference.test.ts
index 54224bfb55c..a7d712633c4 100644
--- a/ui/src/lib/issue-reference.test.ts
+++ b/ui/src/lib/issue-reference.test.ts
@@ -5,6 +5,7 @@ describe("issue-reference", () => {
it("extracts issue ids from company-scoped issue paths", () => {
expect(parseIssuePathIdFromPath("/PAP/issues/PAP-1271")).toBe("PAP-1271");
expect(parseIssuePathIdFromPath("/PAP/issues/pap-1272")).toBe("PAP-1272");
+ expect(parseIssuePathIdFromPath("/PC1A2/issues/pc1a2-7")).toBe("PC1A2-7");
expect(parseIssuePathIdFromPath("/issues/PAP-1179")).toBe("PAP-1179");
expect(parseIssuePathIdFromPath("/issues/:id")).toBeNull();
});
@@ -30,6 +31,10 @@ describe("issue-reference", () => {
issuePathId: "PAP-1271",
href: "/issues/PAP-1271",
});
+ expect(parseIssueReferenceFromHref("pc1a2-7")).toEqual({
+ issuePathId: "PC1A2-7",
+ href: "/issues/PC1A2-7",
+ });
expect(parseIssueReferenceFromHref("/PAP/issues/pap-1180")).toEqual({
issuePathId: "PAP-1180",
href: "/issues/PAP-1180",
diff --git a/ui/src/lib/issue-reference.ts b/ui/src/lib/issue-reference.ts
index 616bb876237..89c1a61da8a 100644
--- a/ui/src/lib/issue-reference.ts
+++ b/ui/src/lib/issue-reference.ts
@@ -5,9 +5,9 @@ type MarkdownNode = {
children?: MarkdownNode[];
};
-const BARE_ISSUE_IDENTIFIER_RE = /^[A-Z][A-Z0-9]+-\d+$/i;
+const BARE_ISSUE_IDENTIFIER_RE = /^[A-Z][A-Z0-9]*-\d+$/i;
const ISSUE_SCHEME_RE = /^issue:\/\/:?([^?#\s]+)(?:[?#].*)?$/i;
-const ISSUE_REFERENCE_TOKEN_RE = /issue:\/\/:?[^\s<>()]+|https?:\/\/[^\s<>()]+|\/(?:[^\s<>()/]+\/)*issues\/[A-Z][A-Z0-9]+-\d+(?=$|[\s<>)\],.;!?:])|\b[A-Z][A-Z0-9]+-\d+\b/gi;
+const ISSUE_REFERENCE_TOKEN_RE = /issue:\/\/:?[^\s<>()]+|https?:\/\/[^\s<>()]+|\/(?:[^\s<>()/]+\/)*issues\/[A-Z][A-Z0-9]*-\d+(?=$|[\s<>)\],.;!?:])|\b[A-Z][A-Z0-9]*-\d+\b/gi;
export function parseIssuePathIdFromPath(pathOrUrl: string | null | undefined): string | null {
if (!pathOrUrl) return null;
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 18/27] 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 e3c243783a6..01edfcffbd7 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 ed8dea6b2c0..31b9c9d0d75 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 00000000000..3620f16917d
--- /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 00000000000..d002c177609
--- /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 f0d21b99889..0c82238a6ee 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 322a326dfc0..7e4a0eb1ba7 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 00000000000..ec254d14961
--- /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 2429412267a..967fe56a91c 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 0e12d903dcf..e7088fbc8b6 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 a168983406f..cf038cf0173 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 826dd83258d..ea3cb491e31 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 plugin page
+ Open plugin page
{projectRef ? (
Open project tab
@@ -1542,7 +1580,7 @@ function KitchenSinkConsole({ context }: { context: { companyId: string | null;
) : null}
{selectedIssueId ? (
Open selected issue
@@ -2199,6 +2237,7 @@ export function KitchenSinkSettingsPage({ context }: PluginSettingsPageProps) {
}
export function KitchenSinkDashboardWidget({ context }: PluginWidgetProps) {
+ const hostNavigation = useHostNavigation();
const overview = usePluginOverview(context.companyId);
const writeMetric = usePluginAction("write-metric");
@@ -2217,7 +2256,7 @@ export function KitchenSinkDashboardWidget({ context }: PluginWidgetProps) {
Issues: {overview.data?.counts.issues ?? 0}
);
}
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 faadc501572..c1f73d98ba2 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 b17989e600f..e761489c098 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 8e1af813d6f..13f0a5925de 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 92b60bbfb00..3d0bd07e19e 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 19566570c15..6961c9294bc 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 16efee05d02..5ed1c1c4fa9 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 599a06ecac9..f4a946f9ecc 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 b93c1db44e7..c148fc8014e 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 56a0938b802..686d02cf4b3 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 68b8a43ba42..8f90af70a3a 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 b1eddea5a22..b8216836157 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 80e548b5f66..7683605d3c4 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 3724d42d8ab..001ccee26f2 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 87a298c77aa..810ac1e6bd3 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 35eb94c6378..1b24f771bf5 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 955b719fb4b..d5231aadff5 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 2aa50361557..302d504879b 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 aea256bea46..565c94f009e 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 00000000000..7de0c31687c
--- /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 3ade091245f..336620a0acb 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 eeb82316cc2..2c7054951d5 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 9aaca2f15e1..6392b78e791 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 00000000000..89e00eff966
--- /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 00000000000..ed512a8d65a
--- /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 00000000000..6f89cac17ae
--- /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 b8ee02e962f..402419c68b8 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 df41f7ff529..faf3487bf68 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 1dab1edfad7..4e2ce297bc0 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 d68cb60a534..957262d8fec 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 6b27357301d..d94475afeab 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 f37fbaa83b5..5887df79e0e 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 43a6e7dd25e..35b60812609 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 66c83389acc..adb49b33ad6 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 e69332ca56d..ef54d61d2a6 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 e1822e45cd2..e6811702beb 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