teamcopilot 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -0
- package/LICENSE.md +21 -0
- package/README.md +131 -0
- package/bin/teamcopilot.js +281 -0
- package/dist/auth/index.js +189 -0
- package/dist/change-user-role.js +77 -0
- package/dist/chat/index.js +849 -0
- package/dist/constants.js +2 -0
- package/dist/create-user.js +98 -0
- package/dist/cronjob/index.js +16 -0
- package/dist/cronjob/resource-reconciliation.js +33 -0
- package/dist/delete-user.js +66 -0
- package/dist/frontend/assets/abap-CRCWOmpq.js +1 -0
- package/dist/frontend/assets/apex-DnsZk_dE.js +1 -0
- package/dist/frontend/assets/azcli-1IWB1ccx.js +1 -0
- package/dist/frontend/assets/bat-DPkNLes8.js +1 -0
- package/dist/frontend/assets/bicep-Corcdgou.js +2 -0
- package/dist/frontend/assets/cameligo-CGrWLZr3.js +1 -0
- package/dist/frontend/assets/clojure-D9WOWImG.js +1 -0
- package/dist/frontend/assets/codicon-DCmgc-ay.ttf +0 -0
- package/dist/frontend/assets/coffee-B7EJu28W.js +1 -0
- package/dist/frontend/assets/cpp-SEyurbux.js +1 -0
- package/dist/frontend/assets/csharp-BoL64M5l.js +1 -0
- package/dist/frontend/assets/csp-C46ZqvIl.js +1 -0
- package/dist/frontend/assets/css-DQU6DXDx.js +3 -0
- package/dist/frontend/assets/cssMode-BDT3WbVs.js +4 -0
- package/dist/frontend/assets/cypher-D84EuPTj.js +1 -0
- package/dist/frontend/assets/dart-D8lhlL1r.js +1 -0
- package/dist/frontend/assets/dockerfile-DLk6rpji.js +1 -0
- package/dist/frontend/assets/ecl-BO6FnfXk.js +1 -0
- package/dist/frontend/assets/editor.worker-B4pQIWZD.js +12 -0
- package/dist/frontend/assets/elixir-BRjLKONM.js +1 -0
- package/dist/frontend/assets/flow9-Cac8vKd7.js +1 -0
- package/dist/frontend/assets/freemarker2-C7-hEgID.js +3 -0
- package/dist/frontend/assets/fsharp-fd1GTHhf.js +1 -0
- package/dist/frontend/assets/go-O9LJTZXk.js +1 -0
- package/dist/frontend/assets/graphql-LQdxqEYJ.js +1 -0
- package/dist/frontend/assets/handlebars-4cwTkPir.js +1 -0
- package/dist/frontend/assets/hcl-DxDQ3s82.js +1 -0
- package/dist/frontend/assets/html-YNfE1Q0A.js +1 -0
- package/dist/frontend/assets/htmlMode-opTQ1HoB.js +4 -0
- package/dist/frontend/assets/index-DWyaVa1h.js +782 -0
- package/dist/frontend/assets/index-lXrsgeTF.css +1 -0
- package/dist/frontend/assets/ini-BvajGCUy.js +1 -0
- package/dist/frontend/assets/java-SYsfObOQ.js +1 -0
- package/dist/frontend/assets/javascript-BEwGzk7T.js +1 -0
- package/dist/frontend/assets/jsonMode-CGhIS5Al.js +10 -0
- package/dist/frontend/assets/julia-DQXNmw_w.js +1 -0
- package/dist/frontend/assets/kotlin-qQ0MG-9I.js +1 -0
- package/dist/frontend/assets/less-GGFNNJHn.js +2 -0
- package/dist/frontend/assets/lexon-Canl7DCW.js +1 -0
- package/dist/frontend/assets/liquid-QekTGCGJ.js +1 -0
- package/dist/frontend/assets/lua-D28Ae8-K.js +1 -0
- package/dist/frontend/assets/m3-DPitgjJI.js +1 -0
- package/dist/frontend/assets/markdown-B811l8j2.js +1 -0
- package/dist/frontend/assets/mdx-BAVDaB7v.js +1 -0
- package/dist/frontend/assets/mips-CdjsipkG.js +1 -0
- package/dist/frontend/assets/msdax-CYqgjx_P.js +1 -0
- package/dist/frontend/assets/mysql-BHd6q0vd.js +1 -0
- package/dist/frontend/assets/objective-c-B1aVtJYH.js +1 -0
- package/dist/frontend/assets/pascal-BhNW15KB.js +1 -0
- package/dist/frontend/assets/pascaligo-5jv8CcQD.js +1 -0
- package/dist/frontend/assets/perl-DlYyT36c.js +1 -0
- package/dist/frontend/assets/pgsql-Dy0bjov7.js +1 -0
- package/dist/frontend/assets/php-120yhfDK.js +1 -0
- package/dist/frontend/assets/pla-CjnFlu4u.js +1 -0
- package/dist/frontend/assets/postiats-CQpG440k.js +1 -0
- package/dist/frontend/assets/powerquery-DdJtto1Z.js +1 -0
- package/dist/frontend/assets/powershell-Bu_VLpJB.js +1 -0
- package/dist/frontend/assets/protobuf-IBS6jZEB.js +2 -0
- package/dist/frontend/assets/pug-kFxLfcjb.js +1 -0
- package/dist/frontend/assets/python-BQlHw7XO.js +1 -0
- package/dist/frontend/assets/qsharp-q7JyzKFN.js +1 -0
- package/dist/frontend/assets/r-BIFz-_sK.js +1 -0
- package/dist/frontend/assets/razor-Be3Wwc2E.js +1 -0
- package/dist/frontend/assets/redis-CHOsPHWR.js +1 -0
- package/dist/frontend/assets/redshift-CBifECDb.js +1 -0
- package/dist/frontend/assets/restructuredtext-CghPJEOS.js +1 -0
- package/dist/frontend/assets/ruby-CYWGW-b1.js +1 -0
- package/dist/frontend/assets/rust-DMDD0SHb.js +1 -0
- package/dist/frontend/assets/sb-BYAiYHFx.js +1 -0
- package/dist/frontend/assets/scala-Bqvq8jcR.js +1 -0
- package/dist/frontend/assets/scheme-Dhb-2j9p.js +1 -0
- package/dist/frontend/assets/scss-CTwUZ5N7.js +3 -0
- package/dist/frontend/assets/shell-CsDZo4DB.js +1 -0
- package/dist/frontend/assets/solidity-CME5AdoB.js +1 -0
- package/dist/frontend/assets/sophia-RYC1BQQz.js +1 -0
- package/dist/frontend/assets/sparql-KEyrF7De.js +1 -0
- package/dist/frontend/assets/sql-BdTr02Mf.js +1 -0
- package/dist/frontend/assets/st-C7iG7M4S.js +1 -0
- package/dist/frontend/assets/swift-D7IUmUK8.js +1 -0
- package/dist/frontend/assets/systemverilog-DgMryOEJ.js +1 -0
- package/dist/frontend/assets/tcl-PloMZuKG.js +1 -0
- package/dist/frontend/assets/tsMode-CIBFoN3z.js +11 -0
- package/dist/frontend/assets/twig-BfRIq3la.js +1 -0
- package/dist/frontend/assets/typescript-BuV9wEIE.js +1 -0
- package/dist/frontend/assets/typespec-CzxlYoT_.js +1 -0
- package/dist/frontend/assets/vb-BwAE3J76.js +1 -0
- package/dist/frontend/assets/wgsl-B_1kOXbF.js +298 -0
- package/dist/frontend/assets/xml-DcDKYaM4.js +1 -0
- package/dist/frontend/assets/yaml-CuBNmOuI.js +1 -0
- package/dist/frontend/index.html +14 -0
- package/dist/frontend/logo.svg +50 -0
- package/dist/index.js +169 -0
- package/dist/logging.js +30 -0
- package/dist/opencode-auth/index.js +122 -0
- package/dist/opencode-server.js +91 -0
- package/dist/prisma/client.js +38 -0
- package/dist/reset-password.js +73 -0
- package/dist/rotate-jwt-secret.js +20 -0
- package/dist/scripts/prisma-workspace.js +34 -0
- package/dist/skills/index.js +311 -0
- package/dist/types/permissions.js +2 -0
- package/dist/types/shared/permissions.js +17 -0
- package/dist/types/shared/skill.js +17 -0
- package/dist/types/shared/workflow-files.js +17 -0
- package/dist/types/shared/workflow.js +17 -0
- package/dist/types/skill.js +2 -0
- package/dist/types/workflow-files.js +2 -0
- package/dist/types/workflow.js +2 -0
- package/dist/users/index.js +22 -0
- package/dist/utils/approval-snapshot-common.js +596 -0
- package/dist/utils/assert.js +20 -0
- package/dist/utils/chat-session.js +44 -0
- package/dist/utils/cli-bootstrap.js +26 -0
- package/dist/utils/index.js +95 -0
- package/dist/utils/jwt-secret.js +63 -0
- package/dist/utils/opencode-auth.js +126 -0
- package/dist/utils/opencode-client.js +109 -0
- package/dist/utils/password-policy.js +12 -0
- package/dist/utils/permission-common.js +280 -0
- package/dist/utils/redact.js +108 -0
- package/dist/utils/resource-access.js +37 -0
- package/dist/utils/resource-file-routes.js +115 -0
- package/dist/utils/resource-files.js +572 -0
- package/dist/utils/runtime-paths.js +61 -0
- package/dist/utils/session-abort.js +52 -0
- package/dist/utils/skill-approval-snapshot.js +39 -0
- package/dist/utils/skill-files.js +17 -0
- package/dist/utils/skill-permissions.js +15 -0
- package/dist/utils/skill.js +217 -0
- package/dist/utils/user-role.js +14 -0
- package/dist/utils/workflow-approval-snapshot.js +38 -0
- package/dist/utils/workflow-files.js +17 -0
- package/dist/utils/workflow-interruption.js +50 -0
- package/dist/utils/workflow-permissions.js +27 -0
- package/dist/utils/workflow-runner.js +414 -0
- package/dist/utils/workflow.js +158 -0
- package/dist/utils/workspace-sync.js +204 -0
- package/dist/workflows/index.js +751 -0
- package/dist/workspace_files/.opencode/opencode.json +17 -0
- package/dist/workspace_files/.opencode/package.json +14 -0
- package/dist/workspace_files/.opencode/plugins/createSkill.ts +339 -0
- package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +345 -0
- package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +173 -0
- package/dist/workspace_files/.opencode/plugins/findSkill.ts +211 -0
- package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +135 -0
- package/dist/workspace_files/.opencode/plugins/honeytoken-protection.ts +64 -0
- package/dist/workspace_files/.opencode/plugins/listAvailableSkills.ts +93 -0
- package/dist/workspace_files/.opencode/plugins/listAvailableWorkflows.ts +93 -0
- package/dist/workspace_files/.opencode/plugins/python-protection.ts +184 -0
- package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +168 -0
- package/dist/workspace_files/.opencode/tsconfig.json +16 -0
- package/dist/workspace_files/AGENTS.md +483 -0
- package/dist/workspace_files/package-lock.json +167 -0
- package/dist/workspace_files/package.json +5 -0
- package/package.json +86 -0
- package/prisma/migrations/20260203040755_init/migration.sql +20 -0
- package/prisma/migrations/20260204034845_replace_google_auth_with_email_password/migration.sql +25 -0
- package/prisma/migrations/20260207022226_add_user_role/migration.sql +25 -0
- package/prisma/migrations/20260210161254_add_workflow_runs/migration.sql +16 -0
- package/prisma/migrations/20260211050606_adds_workflow_table/migration.sql +40 -0
- package/prisma/migrations/20260211050750_adds_fkey_constraint/migration.sql +21 -0
- package/prisma/migrations/20260211051912_removes_workflow_table/migration.sql +34 -0
- package/prisma/migrations/20260211052238_changes_workflow_id_to_slug/migration.sql +27 -0
- package/prisma/migrations/20260212051912_add_output_to_workflow_runs/migration.sql +2 -0
- package/prisma/migrations/20260213073006_add_chat_sessions/migration.sql +13 -0
- package/prisma/migrations/20260216053202_add_chat_sessions_opencode_session_id_idx/migration.sql +2 -0
- package/prisma/migrations/20260216053237_drop_redundant_chat_sessions_opencode_idx/migration.sql +2 -0
- package/prisma/migrations/20260219060705_makes/migration.sql +24 -0
- package/prisma/migrations/20260222040542_add_workflow_execution_permissions/migration.sql +18 -0
- package/prisma/migrations/20260222040815_remove_workflow_execution_permissions/migration.sql +10 -0
- package/prisma/migrations/20260222041348_add_workflow_execution_permissions_final/migration.sql +17 -0
- package/prisma/migrations/20260222041741_rename_to_tool_execution_permissions/migration.sql +30 -0
- package/prisma/migrations/20260222041826_simplify_tool_execution_permissions/migration.sql +29 -0
- package/prisma/migrations/20260222041950_add_fields_for_standalone_permissions/migration.sql +32 -0
- package/prisma/migrations/20260222042954_simplify_tool_permissions_table/migration.sql +27 -0
- package/prisma/migrations/20260223073902_add_workflow_run_permissions_tables/migration.sql +23 -0
- package/prisma/migrations/20260225025151_add_workflow_metadata/migration.sql +16 -0
- package/prisma/migrations/20260225031035_merge_workflow_permissions_into_metadata/migration.sql +44 -0
- package/prisma/migrations/20260225031752_removes_default_for_run_permission_mode/migration.sql +20 -0
- package/prisma/migrations/20260225033603_remove_workflow_metadata_user_fkeys/migration.sql +18 -0
- package/prisma/migrations/20260225043032_restore_workflow_metadata_user_fkeys/migration.sql +20 -0
- package/prisma/migrations/20260225091423_add_workflow_approved_snapshots/migration.sql +28 -0
- package/prisma/migrations/20260226032121_add_is_approved_to_workflow_metadata/migration.sql +21 -0
- package/prisma/migrations/20260226032444_undoes_last_db_change/migration.sql +26 -0
- package/prisma/migrations/20260227120000_remove_snapshot_hash_from_approved_snapshots/migration.sql +16 -0
- package/prisma/migrations/20260228071125_adds_workspace_path_to_snapshot_table/migration.sql +22 -0
- package/prisma/migrations/20260228071217_modifies_index_and_removes_default_value/migration.sql +22 -0
- package/prisma/migrations/20260228071710_undoes_previous/migration.sql +27 -0
- package/prisma/migrations/20260228105022_add_must_change_password_first_login/migration.sql +20 -0
- package/prisma/migrations/20260301115439_add_workflow_run_log_refs/migration.sql +8 -0
- package/prisma/migrations/20260301122557_add_workflow_aborted_sessions/migration.sql +5 -0
- package/prisma/migrations/20260302045545_move_workflow_run_log_refs_into_workflow_runs/migration.sql +17 -0
- package/prisma/migrations/20260303040318_add_skill_tables/migration.sql +61 -0
- package/prisma/migrations/20260303051533_unify_resource_permissions/migration.sql +97 -0
- package/prisma/migrations/20260303064255_unify_resource_metadata_and_snapshots/migration.sql +179 -0
- package/prisma/migrations/migration_lock.toml +3 -0
- package/prisma/schema.prisma +147 -0
|
@@ -0,0 +1,751 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const express_1 = __importDefault(require("express"));
|
|
7
|
+
const fs_1 = __importDefault(require("fs"));
|
|
8
|
+
const promises_1 = __importDefault(require("fs/promises"));
|
|
9
|
+
const os_1 = __importDefault(require("os"));
|
|
10
|
+
const path_1 = __importDefault(require("path"));
|
|
11
|
+
const crypto_1 = require("crypto");
|
|
12
|
+
const multer_1 = __importDefault(require("multer"));
|
|
13
|
+
const client_1 = __importDefault(require("../prisma/client"));
|
|
14
|
+
const index_1 = require("../utils/index");
|
|
15
|
+
const workflow_1 = require("../utils/workflow");
|
|
16
|
+
const workflow_files_1 = require("../utils/workflow-files");
|
|
17
|
+
const workflow_permissions_1 = require("../utils/workflow-permissions");
|
|
18
|
+
const permission_common_1 = require("../utils/permission-common");
|
|
19
|
+
const workflow_approval_snapshot_1 = require("../utils/workflow-approval-snapshot");
|
|
20
|
+
const workspace_sync_1 = require("../utils/workspace-sync");
|
|
21
|
+
const workflow_runner_1 = require("../utils/workflow-runner");
|
|
22
|
+
const workflow_interruption_1 = require("../utils/workflow-interruption");
|
|
23
|
+
const session_abort_1 = require("../utils/session-abort");
|
|
24
|
+
const resource_file_routes_1 = require("../utils/resource-file-routes");
|
|
25
|
+
const resource_access_1 = require("../utils/resource-access");
|
|
26
|
+
const router = express_1.default.Router({ mergeParams: true });
|
|
27
|
+
const uploadTmpDir = path_1.default.join(os_1.default.tmpdir(), "teamcopilot-workflow-uploads");
|
|
28
|
+
fs_1.default.mkdirSync(uploadTmpDir, { recursive: true });
|
|
29
|
+
const maxUploadBytes = (() => {
|
|
30
|
+
const parsed = Number(process.env.WORKFLOW_FILE_UPLOAD_MAX_MB ?? "1024");
|
|
31
|
+
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
32
|
+
return 1024 * 1024 * 1024;
|
|
33
|
+
}
|
|
34
|
+
return Math.floor(parsed * 1024 * 1024);
|
|
35
|
+
})();
|
|
36
|
+
const workflowFileUpload = (0, multer_1.default)({
|
|
37
|
+
dest: uploadTmpDir,
|
|
38
|
+
limits: {
|
|
39
|
+
files: 1,
|
|
40
|
+
fileSize: maxUploadBytes,
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
function isPathInside(childPath, parentPath) {
|
|
44
|
+
const parent = path_1.default.resolve(parentPath) + path_1.default.sep;
|
|
45
|
+
const child = path_1.default.resolve(childPath) + path_1.default.sep;
|
|
46
|
+
return child.startsWith(parent);
|
|
47
|
+
}
|
|
48
|
+
function sanitizeFilenamePart(value) {
|
|
49
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
50
|
+
}
|
|
51
|
+
async function assertCurrentUserCanRunWorkflow(slug, userId) {
|
|
52
|
+
await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
53
|
+
const approvalState = await (0, workflow_approval_snapshot_1.getWorkflowSnapshotApprovalState)(slug);
|
|
54
|
+
if (!approvalState.is_current_code_approved) {
|
|
55
|
+
throw {
|
|
56
|
+
status: 403,
|
|
57
|
+
message: 'Workflow is not approved for the current code version'
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const permissionSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, userId);
|
|
61
|
+
if (!permissionSummary.can_edit) {
|
|
62
|
+
throw {
|
|
63
|
+
status: 403,
|
|
64
|
+
message: permissionSummary.is_locked_due_to_missing_users
|
|
65
|
+
? 'Workflow cannot be run because no allowed users remain'
|
|
66
|
+
: 'You do not have permission to run this workflow. Please contact the workflow owner to request permission.'
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function getWorkflowEditorAccess(slug, userId) {
|
|
71
|
+
const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, userId);
|
|
72
|
+
const workflowStatus = accessSummary.is_approved ? "approved" : "pending";
|
|
73
|
+
return {
|
|
74
|
+
can_view: accessSummary.can_view,
|
|
75
|
+
can_edit: accessSummary.can_edit,
|
|
76
|
+
editor_status: workflowStatus,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
async function assertCanEditWorkflowFiles(slug, userId) {
|
|
80
|
+
const access = await getWorkflowEditorAccess(slug, userId);
|
|
81
|
+
if (!access.can_edit) {
|
|
82
|
+
throw {
|
|
83
|
+
status: 403,
|
|
84
|
+
message: "You do not have permission to edit this workflow"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
async function assertCanViewWorkflowFiles(slug, userId) {
|
|
89
|
+
const access = await getWorkflowEditorAccess(slug, userId);
|
|
90
|
+
if (!access.can_view) {
|
|
91
|
+
throw {
|
|
92
|
+
status: 403,
|
|
93
|
+
message: "You do not have permission to view this workflow"
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const workflowExecutions = new Map();
|
|
98
|
+
// GET /api/workflows - List available workflows from filesystem
|
|
99
|
+
router.get('/', (0, index_1.apiHandler)(async (req, res) => {
|
|
100
|
+
const slugs = (0, workflow_1.listWorkflowSlugs)();
|
|
101
|
+
const workflows = [];
|
|
102
|
+
const creatorIds = new Set();
|
|
103
|
+
const manifests = new Map();
|
|
104
|
+
const metadataBySlug = new Map();
|
|
105
|
+
for (const slug of slugs) {
|
|
106
|
+
const { manifest, metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
107
|
+
manifests.set(slug, manifest);
|
|
108
|
+
metadataBySlug.set(slug, metadata);
|
|
109
|
+
if (metadata.created_by_user_id) {
|
|
110
|
+
creatorIds.add(metadata.created_by_user_id);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const creators = creatorIds.size > 0
|
|
114
|
+
? await client_1.default.users.findMany({
|
|
115
|
+
where: { id: { in: Array.from(creatorIds) } },
|
|
116
|
+
select: { id: true, name: true, email: true }
|
|
117
|
+
})
|
|
118
|
+
: [];
|
|
119
|
+
const creatorNameById = new Map(creators.map((creator) => [creator.id, creator.name]));
|
|
120
|
+
const creatorEmailById = new Map(creators.map((creator) => [creator.id, creator.email]));
|
|
121
|
+
for (const slug of slugs) {
|
|
122
|
+
const manifest = manifests.get(slug);
|
|
123
|
+
const metadata = metadataBySlug.get(slug);
|
|
124
|
+
if (!manifest)
|
|
125
|
+
continue;
|
|
126
|
+
if (!metadata)
|
|
127
|
+
continue;
|
|
128
|
+
const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, req.userId);
|
|
129
|
+
if (!accessSummary.can_view) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
const createdByUserId = metadata.created_by_user_id;
|
|
133
|
+
workflows.push({
|
|
134
|
+
slug,
|
|
135
|
+
name: slug,
|
|
136
|
+
intent_summary: manifest.intent_summary,
|
|
137
|
+
created_by_user_id: createdByUserId,
|
|
138
|
+
created_by_user_name: createdByUserId ? (creatorNameById.get(createdByUserId) ?? null) : null,
|
|
139
|
+
created_by_user_email: createdByUserId ? (creatorEmailById.get(createdByUserId) ?? null) : null,
|
|
140
|
+
approved_by_user_id: metadata.approved_by_user_id ?? null,
|
|
141
|
+
is_approved: accessSummary.is_approved,
|
|
142
|
+
can_view: accessSummary.can_view,
|
|
143
|
+
can_edit: accessSummary.can_edit,
|
|
144
|
+
permission_mode: accessSummary.permission_mode,
|
|
145
|
+
is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
res.json({ workflows });
|
|
149
|
+
}, true));
|
|
150
|
+
// GET /api/workflows/runs - List workflow run history (last 50, all users)
|
|
151
|
+
router.get('/runs', (0, index_1.apiHandler)(async (_req, res) => {
|
|
152
|
+
const runs = await client_1.default.workflow_runs.findMany({
|
|
153
|
+
orderBy: { started_at: 'desc' },
|
|
154
|
+
take: 50,
|
|
155
|
+
include: {
|
|
156
|
+
user: {
|
|
157
|
+
select: { name: true, email: true }
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
res.json({ runs });
|
|
162
|
+
}, true));
|
|
163
|
+
// GET /api/workflows/runs/logs - Get log file by run_id OR session_id+message_id
|
|
164
|
+
router.get('/runs/logs', (0, index_1.apiHandler)(async (req, res) => {
|
|
165
|
+
const runIdRaw = req.query.run_id;
|
|
166
|
+
const sessionIdRaw = req.query.session_id;
|
|
167
|
+
const messageIdRaw = req.query.message_id;
|
|
168
|
+
const runId = typeof runIdRaw === 'string' && runIdRaw.trim().length > 0 ? runIdRaw : null;
|
|
169
|
+
const sessionId = typeof sessionIdRaw === 'string' && sessionIdRaw.trim().length > 0 ? sessionIdRaw : null;
|
|
170
|
+
const messageId = typeof messageIdRaw === 'string' && messageIdRaw.trim().length > 0 ? messageIdRaw : null;
|
|
171
|
+
if (!runId && !(sessionId && messageId)) {
|
|
172
|
+
throw {
|
|
173
|
+
status: 400,
|
|
174
|
+
message: 'Provide either run_id, or both session_id and message_id'
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (runId && (sessionId || messageId)) {
|
|
178
|
+
throw {
|
|
179
|
+
status: 400,
|
|
180
|
+
message: 'Provide either run_id, or session_id + message_id, not both'
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
const run = runId
|
|
184
|
+
? await client_1.default.workflow_runs.findUnique({
|
|
185
|
+
where: { id: runId },
|
|
186
|
+
select: { session_id: true, message_id: true }
|
|
187
|
+
})
|
|
188
|
+
: await client_1.default.workflow_runs.findFirst({
|
|
189
|
+
where: {
|
|
190
|
+
session_id: sessionId,
|
|
191
|
+
message_id: messageId,
|
|
192
|
+
},
|
|
193
|
+
select: { session_id: true, message_id: true },
|
|
194
|
+
orderBy: { started_at: 'desc' }
|
|
195
|
+
});
|
|
196
|
+
if (!run?.session_id || !run.message_id) {
|
|
197
|
+
res.json({
|
|
198
|
+
found: false,
|
|
199
|
+
logs: null
|
|
200
|
+
});
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
|
|
204
|
+
const workflowRunsDir = path_1.default.join(workspaceDir, 'workflow-runs');
|
|
205
|
+
const logPath = path_1.default.join(workflowRunsDir, `${sanitizeFilenamePart(run.session_id)}-${sanitizeFilenamePart(run.message_id)}.txt`);
|
|
206
|
+
if (!isPathInside(logPath, workflowRunsDir)) {
|
|
207
|
+
throw {
|
|
208
|
+
status: 400,
|
|
209
|
+
message: 'Invalid log path'
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
try {
|
|
213
|
+
const logs = await promises_1.default.readFile(logPath, 'utf-8');
|
|
214
|
+
res.json({
|
|
215
|
+
found: true,
|
|
216
|
+
logs
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
res.json({
|
|
221
|
+
found: false,
|
|
222
|
+
logs: null
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
}, true));
|
|
226
|
+
// GET /api/workflows/runs/:id - Get details for a specific workflow run
|
|
227
|
+
router.get('/runs/:id', (0, index_1.apiHandler)(async (req, res) => {
|
|
228
|
+
const id = req.params.id;
|
|
229
|
+
const run = await client_1.default.workflow_runs.findUnique({
|
|
230
|
+
where: { id },
|
|
231
|
+
include: {
|
|
232
|
+
user: {
|
|
233
|
+
select: { name: true, email: true }
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
if (!run) {
|
|
238
|
+
throw {
|
|
239
|
+
status: 404,
|
|
240
|
+
message: 'Workflow run not found'
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
res.json({ run });
|
|
244
|
+
}, true));
|
|
245
|
+
// GET /api/workflows/interruption-status/:sessionId - Check if a workflow session should stop
|
|
246
|
+
router.get('/interruption-status/:sessionId', (0, index_1.apiHandler)(async (req, res) => {
|
|
247
|
+
const sessionId = req.params.sessionId;
|
|
248
|
+
const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
|
|
249
|
+
const interrupted = await (0, workflow_interruption_1.isWorkflowSessionInterrupted)(sessionId, workspaceDir);
|
|
250
|
+
res.json({ interrupted });
|
|
251
|
+
}, true));
|
|
252
|
+
// POST /api/workflows/runs/:id/stop - Stop an in-progress workflow run
|
|
253
|
+
router.post('/runs/:id/stop', (0, index_1.apiHandler)(async (req, res) => {
|
|
254
|
+
const id = req.params.id;
|
|
255
|
+
const run = await client_1.default.workflow_runs.findUnique({
|
|
256
|
+
where: { id }
|
|
257
|
+
});
|
|
258
|
+
if (!run) {
|
|
259
|
+
throw {
|
|
260
|
+
status: 404,
|
|
261
|
+
message: 'Workflow run not found'
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (run.status !== 'running') {
|
|
265
|
+
res.json({ success: true });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const permissionSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", run.workflow_slug, req.userId);
|
|
269
|
+
if (!permissionSummary.can_edit) {
|
|
270
|
+
throw {
|
|
271
|
+
status: 403,
|
|
272
|
+
message: 'You do not have permission to stop this workflow run'
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
if (!run.session_id) {
|
|
276
|
+
throw {
|
|
277
|
+
status: 404,
|
|
278
|
+
message: 'Workflow run session not found'
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
const isManualSession = run.session_id.startsWith("manual-");
|
|
282
|
+
if (isManualSession) {
|
|
283
|
+
await (0, workflow_interruption_1.markWorkflowSessionAborted)(run.session_id);
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
await (0, session_abort_1.abortOpencodeSession)(run.session_id);
|
|
287
|
+
}
|
|
288
|
+
res.json({ success: true });
|
|
289
|
+
}, true));
|
|
290
|
+
// POST /api/workflows/:slug/manual-run - Start a workflow run from manual mode UI
|
|
291
|
+
router.post('/:slug/manual-run', (0, index_1.apiHandler)(async (req, res) => {
|
|
292
|
+
const slug = req.params.slug;
|
|
293
|
+
const inputsRaw = req.body.inputs ?? {};
|
|
294
|
+
if (!inputsRaw || typeof inputsRaw !== 'object' || Array.isArray(inputsRaw)) {
|
|
295
|
+
throw {
|
|
296
|
+
status: 400,
|
|
297
|
+
message: 'inputs must be an object'
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
const inputs = inputsRaw;
|
|
301
|
+
const manualSessionId = `manual-${req.userId}-${(0, crypto_1.randomUUID)()}`;
|
|
302
|
+
const manualMessageId = `manual-message-${(0, crypto_1.randomUUID)()}`;
|
|
303
|
+
const manualCallId = `manual-call-${(0, crypto_1.randomUUID)()}`;
|
|
304
|
+
const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
|
|
305
|
+
await assertCurrentUserCanRunWorkflow(slug, req.userId);
|
|
306
|
+
const startedRun = await (0, workflow_runner_1.startWorkflowRunViaBackend)({
|
|
307
|
+
workspaceDir,
|
|
308
|
+
slug,
|
|
309
|
+
inputs,
|
|
310
|
+
authUserId: req.userId,
|
|
311
|
+
sessionId: manualSessionId,
|
|
312
|
+
messageId: manualMessageId,
|
|
313
|
+
callId: manualCallId,
|
|
314
|
+
requirePermissionPrompt: false,
|
|
315
|
+
});
|
|
316
|
+
void startedRun.completion.catch(() => undefined);
|
|
317
|
+
res.json({
|
|
318
|
+
run_id: startedRun.runId
|
|
319
|
+
});
|
|
320
|
+
}, true));
|
|
321
|
+
// POST /api/workflows/execute - Start workflow execution without blocking on completion.
|
|
322
|
+
router.post('/execute', (0, index_1.apiHandler)(async (req, res) => {
|
|
323
|
+
if (!req.opencode_session_id) {
|
|
324
|
+
throw {
|
|
325
|
+
status: 400,
|
|
326
|
+
message: 'This endpoint requires an opencode session token'
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
const body = req.body;
|
|
330
|
+
if (typeof body.slug !== "string") {
|
|
331
|
+
throw { status: 400, message: "slug is required" };
|
|
332
|
+
}
|
|
333
|
+
if (typeof body.message_id !== "string") {
|
|
334
|
+
throw { status: 400, message: "message_id is required" };
|
|
335
|
+
}
|
|
336
|
+
if (typeof body.call_id !== "string") {
|
|
337
|
+
throw { status: 400, message: "call_id is required" };
|
|
338
|
+
}
|
|
339
|
+
const slug = body.slug;
|
|
340
|
+
const inputs = body.inputs ?? {};
|
|
341
|
+
const messageId = body.message_id;
|
|
342
|
+
const callId = body.call_id;
|
|
343
|
+
const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
|
|
344
|
+
await assertCurrentUserCanRunWorkflow(slug, req.userId);
|
|
345
|
+
const startedRun = await (0, workflow_runner_1.startWorkflowRunViaBackend)({
|
|
346
|
+
workspaceDir,
|
|
347
|
+
slug,
|
|
348
|
+
inputs,
|
|
349
|
+
authUserId: req.userId,
|
|
350
|
+
sessionId: req.opencode_session_id,
|
|
351
|
+
messageId,
|
|
352
|
+
callId,
|
|
353
|
+
requirePermissionPrompt: true,
|
|
354
|
+
});
|
|
355
|
+
const executionId = (0, crypto_1.randomUUID)();
|
|
356
|
+
const executionRecord = {
|
|
357
|
+
slug,
|
|
358
|
+
runId: startedRun.runId,
|
|
359
|
+
timeoutSeconds: startedRun.timeoutSeconds,
|
|
360
|
+
status: "running",
|
|
361
|
+
output: null,
|
|
362
|
+
errorMessage: null,
|
|
363
|
+
};
|
|
364
|
+
workflowExecutions.set(executionId, executionRecord);
|
|
365
|
+
void startedRun.completion
|
|
366
|
+
.then((result) => {
|
|
367
|
+
executionRecord.output = result.output;
|
|
368
|
+
executionRecord.status = result.status;
|
|
369
|
+
executionRecord.errorMessage = result.status === "success" ? null : `Workflow execution ${result.status}`;
|
|
370
|
+
})
|
|
371
|
+
.catch((err) => {
|
|
372
|
+
const errorOutput = err instanceof Error ? err.message : JSON.stringify(err);
|
|
373
|
+
executionRecord.status = "error";
|
|
374
|
+
executionRecord.errorMessage = errorOutput;
|
|
375
|
+
executionRecord.output = errorOutput;
|
|
376
|
+
});
|
|
377
|
+
res.json({
|
|
378
|
+
execution_id: executionId,
|
|
379
|
+
status: "running",
|
|
380
|
+
});
|
|
381
|
+
}, true));
|
|
382
|
+
// GET /api/workflows/execute/:executionId - Get execution status/result.
|
|
383
|
+
router.get('/execute/:executionId', (0, index_1.apiHandler)(async (req, res) => {
|
|
384
|
+
const executionId = req.params.executionId;
|
|
385
|
+
const execution = workflowExecutions.get(executionId);
|
|
386
|
+
if (!execution) {
|
|
387
|
+
throw {
|
|
388
|
+
status: 404,
|
|
389
|
+
message: 'Execution not found'
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
if (execution.status === "running") {
|
|
393
|
+
res.json({
|
|
394
|
+
execution_id: executionId,
|
|
395
|
+
status: "running",
|
|
396
|
+
});
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const responsePayload = {
|
|
400
|
+
status: execution.status,
|
|
401
|
+
output: execution.output,
|
|
402
|
+
workflow: execution.slug,
|
|
403
|
+
timeout_seconds: execution.timeoutSeconds,
|
|
404
|
+
run_id: execution.runId,
|
|
405
|
+
};
|
|
406
|
+
workflowExecutions.delete(executionId);
|
|
407
|
+
if (execution.status !== "success") {
|
|
408
|
+
throw {
|
|
409
|
+
status: 500,
|
|
410
|
+
message: "Workflow execution failed: " + JSON.stringify(responsePayload),
|
|
411
|
+
doLogging: false,
|
|
412
|
+
maskErrorMessage: false,
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
res.json(responsePayload);
|
|
416
|
+
}, true));
|
|
417
|
+
// PATCH /api/workflows/runs/:id - Update run status
|
|
418
|
+
router.patch('/runs/:id', (0, index_1.apiHandler)(async (req, res) => {
|
|
419
|
+
const id = req.params.id;
|
|
420
|
+
const { status, error_message, output } = req.body;
|
|
421
|
+
if (!status || !['running', 'success', 'failed'].includes(status)) {
|
|
422
|
+
throw {
|
|
423
|
+
status: 400,
|
|
424
|
+
message: 'status must be "running", "success", or "failed"'
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
const existingRun = await client_1.default.workflow_runs.findUnique({ where: { id } });
|
|
428
|
+
if (!existingRun) {
|
|
429
|
+
throw {
|
|
430
|
+
status: 404,
|
|
431
|
+
message: 'Workflow run not found'
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
if (existingRun.status !== 'running') {
|
|
435
|
+
throw {
|
|
436
|
+
status: 400,
|
|
437
|
+
message: 'Can only update runs that are in running status'
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
const updateData = { status };
|
|
441
|
+
if (status === 'success' || status === 'failed') {
|
|
442
|
+
updateData.completed_at = BigInt(Date.now());
|
|
443
|
+
}
|
|
444
|
+
if (status === 'failed' && error_message) {
|
|
445
|
+
updateData.error_message = error_message;
|
|
446
|
+
}
|
|
447
|
+
if (output) {
|
|
448
|
+
updateData.output = output;
|
|
449
|
+
}
|
|
450
|
+
const run = await client_1.default.workflow_runs.update({
|
|
451
|
+
where: { id },
|
|
452
|
+
data: updateData
|
|
453
|
+
});
|
|
454
|
+
res.json({ run });
|
|
455
|
+
}, true));
|
|
456
|
+
// POST /api/workflows/:slug/approve - Approve a workflow
|
|
457
|
+
router.post('/:slug/approve', (0, index_1.apiHandler)(async (req, res) => {
|
|
458
|
+
const slug = req.params.slug;
|
|
459
|
+
const approvalResult = await (0, workflow_approval_snapshot_1.approveWorkflowWithSnapshot)(slug, req.userId);
|
|
460
|
+
const { metadata: approvedMetadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
461
|
+
await (0, workflow_permissions_1.addApproverToWorkflowRunPermissionsIfRestricted)(slug, req.userId, approvedMetadata.created_by_user_id);
|
|
462
|
+
res.json({
|
|
463
|
+
workflow: {
|
|
464
|
+
slug,
|
|
465
|
+
approved_by_user_id: approvalResult.approved_by_user_id,
|
|
466
|
+
is_approved: true,
|
|
467
|
+
snapshot_hash: approvalResult.snapshot_hash,
|
|
468
|
+
snapshot_file_count: approvalResult.snapshot_file_count
|
|
469
|
+
}
|
|
470
|
+
});
|
|
471
|
+
}, true));
|
|
472
|
+
// POST /api/workflows/:slug/reject-restore - Restore workflow files to last approved snapshot
|
|
473
|
+
router.post('/:slug/reject-restore', (0, index_1.apiHandler)(async (req, res) => {
|
|
474
|
+
const slug = req.params.slug;
|
|
475
|
+
const result = await (0, workflow_approval_snapshot_1.restoreWorkflowToApprovedSnapshot)(slug, req.userId);
|
|
476
|
+
res.json({
|
|
477
|
+
workflow: {
|
|
478
|
+
slug,
|
|
479
|
+
restored_file_count: result.restored_file_count,
|
|
480
|
+
snapshot_hash: result.snapshot_hash,
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
}, true));
|
|
484
|
+
// POST /api/workflows/:slug/creator - Set the creator user id for a workflow
|
|
485
|
+
router.post('/:slug/creator', (0, index_1.apiHandler)(async (req, res) => {
|
|
486
|
+
const slug = req.params.slug;
|
|
487
|
+
const { metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
488
|
+
const existingCreator = metadata.created_by_user_id ?? null;
|
|
489
|
+
if (existingCreator && existingCreator !== req.userId) {
|
|
490
|
+
throw {
|
|
491
|
+
status: 409,
|
|
492
|
+
message: 'Workflow creator is already set and cannot be changed'
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
const updatedMetadata = existingCreator
|
|
496
|
+
? metadata
|
|
497
|
+
: await (0, workflow_1.setWorkflowCreator)(slug, req.userId);
|
|
498
|
+
await (0, workflow_permissions_1.initializeWorkflowRunPermissionsForCreator)(slug, req.userId);
|
|
499
|
+
res.json({
|
|
500
|
+
workflow: {
|
|
501
|
+
slug,
|
|
502
|
+
created_by_user_id: updatedMetadata.created_by_user_id
|
|
503
|
+
}
|
|
504
|
+
});
|
|
505
|
+
}, true));
|
|
506
|
+
// DELETE /api/workflows/:slug - Delete workflow directory and all past runs
|
|
507
|
+
router.delete('/:slug', (0, index_1.apiHandler)(async (req, res) => {
|
|
508
|
+
const slug = req.params.slug;
|
|
509
|
+
const { metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
510
|
+
const creatorUserId = metadata.created_by_user_id ?? null;
|
|
511
|
+
const creator = creatorUserId
|
|
512
|
+
? await client_1.default.users.findUnique({
|
|
513
|
+
where: { id: creatorUserId },
|
|
514
|
+
select: { id: true }
|
|
515
|
+
})
|
|
516
|
+
: null;
|
|
517
|
+
const isOwner = creatorUserId === req.userId;
|
|
518
|
+
const hasNoCreatorUser = creator === null;
|
|
519
|
+
const isEngineer = req.role === 'Engineer';
|
|
520
|
+
if (!isOwner && !(hasNoCreatorUser && isEngineer)) {
|
|
521
|
+
throw {
|
|
522
|
+
status: 403,
|
|
523
|
+
message: 'Only the workflow owner can delete this workflow. Engineers can only delete workflows whose owner no longer exists.'
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
await (0, workflow_1.deleteWorkflow)(slug);
|
|
527
|
+
res.json({ success: true });
|
|
528
|
+
}, true));
|
|
529
|
+
// GET /api/workflows/:slug/approval-diff - Preview current code diff vs approved snapshot
|
|
530
|
+
router.get('/:slug/approval-diff', (0, index_1.apiHandler)(async (req, res) => {
|
|
531
|
+
const slug = req.params.slug;
|
|
532
|
+
if (req.role !== 'Engineer') {
|
|
533
|
+
throw {
|
|
534
|
+
status: 403,
|
|
535
|
+
message: 'Only Engineers can review approval diffs'
|
|
536
|
+
};
|
|
537
|
+
}
|
|
538
|
+
await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
539
|
+
const previousSnapshot = await (0, workflow_approval_snapshot_1.loadApprovedSnapshotFromDb)(slug);
|
|
540
|
+
const currentSnapshot = (0, workflow_approval_snapshot_1.collectCurrentWorkflowSnapshot)(slug);
|
|
541
|
+
const diff = (0, workflow_approval_snapshot_1.buildApprovalDiffResponse)(previousSnapshot, currentSnapshot);
|
|
542
|
+
res.locals.skipResponseSanitization = true;
|
|
543
|
+
res.json(diff);
|
|
544
|
+
}, true));
|
|
545
|
+
(0, resource_file_routes_1.registerResourceFileRoutes)({
|
|
546
|
+
router,
|
|
547
|
+
uploadMiddleware: workflowFileUpload.single("file"),
|
|
548
|
+
assertCanView: assertCanViewWorkflowFiles,
|
|
549
|
+
ensureResourceExists: async (slug) => {
|
|
550
|
+
await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
551
|
+
},
|
|
552
|
+
getEditorAccess: getWorkflowEditorAccess,
|
|
553
|
+
assertCanEdit: assertCanEditWorkflowFiles,
|
|
554
|
+
listDirectory: workflow_files_1.listWorkflowDirectory,
|
|
555
|
+
readFileContent: workflow_files_1.readWorkflowFileContent,
|
|
556
|
+
saveFileContent: workflow_files_1.saveWorkflowFileContent,
|
|
557
|
+
createFileOrFolder: workflow_files_1.createWorkflowFileOrFolder,
|
|
558
|
+
uploadFileFromTempPath: workflow_files_1.uploadWorkflowFileFromTempPath,
|
|
559
|
+
renamePath: workflow_files_1.renameWorkflowPath,
|
|
560
|
+
deletePath: workflow_files_1.deleteWorkflowPath,
|
|
561
|
+
});
|
|
562
|
+
// GET /api/workflows/:slug - Get workflow details from filesystem
|
|
563
|
+
router.get('/:slug', (0, index_1.apiHandler)(async (req, res) => {
|
|
564
|
+
const slug = req.params.slug;
|
|
565
|
+
await assertCanViewWorkflowFiles(slug, req.userId);
|
|
566
|
+
const { manifest, metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
567
|
+
const permission = await (0, workflow_permissions_1.getWorkflowRunPermissionWithUsers)(slug);
|
|
568
|
+
const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, req.userId);
|
|
569
|
+
const createdByUserId = metadata.created_by_user_id ?? null;
|
|
570
|
+
const approvedByUserId = metadata.approved_by_user_id ?? null;
|
|
571
|
+
const userIds = [createdByUserId, approvedByUserId].filter((id) => typeof id === "string");
|
|
572
|
+
const users = userIds.length > 0
|
|
573
|
+
? await client_1.default.users.findMany({
|
|
574
|
+
where: { id: { in: userIds } },
|
|
575
|
+
select: { id: true, name: true, email: true }
|
|
576
|
+
})
|
|
577
|
+
: [];
|
|
578
|
+
const usersById = new Map(users.map((user) => [user.id, user]));
|
|
579
|
+
const creator = createdByUserId ? (usersById.get(createdByUserId) ?? null) : null;
|
|
580
|
+
const approver = approvedByUserId ? (usersById.get(approvedByUserId) ?? null) : null;
|
|
581
|
+
const permissionMode = (0, permission_common_1.assertCommonPermissionMode)(permission.permission_mode, "workflow run");
|
|
582
|
+
const permissions = permissionMode === "everyone"
|
|
583
|
+
? { mode: "everyone" }
|
|
584
|
+
: { mode: "restricted", allowed_user_ids: permission.allowedUsers.map((row) => row.user_id) };
|
|
585
|
+
res.json({
|
|
586
|
+
workflow: {
|
|
587
|
+
slug,
|
|
588
|
+
name: slug,
|
|
589
|
+
intent_summary: manifest.intent_summary,
|
|
590
|
+
created_by_user_id: createdByUserId,
|
|
591
|
+
created_by_user_name: creator?.name ?? null,
|
|
592
|
+
created_by_user_email: creator?.email ?? null,
|
|
593
|
+
approved_by_user_id: approvedByUserId,
|
|
594
|
+
is_approved: accessSummary.is_approved,
|
|
595
|
+
approved_by_user_name: approver?.name ?? null,
|
|
596
|
+
approved_by_user_email: approver?.email ?? null,
|
|
597
|
+
can_view: accessSummary.can_view,
|
|
598
|
+
can_edit: accessSummary.can_edit,
|
|
599
|
+
permission_mode: accessSummary.permission_mode,
|
|
600
|
+
is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users,
|
|
601
|
+
permissions,
|
|
602
|
+
allowed_users_resolved: permission.allowedUsers.map((row) => ({
|
|
603
|
+
user_id: row.user.id,
|
|
604
|
+
name: row.user.name,
|
|
605
|
+
email: row.user.email,
|
|
606
|
+
is_owner: row.user.id === metadata.created_by_user_id,
|
|
607
|
+
is_approver: row.user.id === metadata.approved_by_user_id
|
|
608
|
+
})),
|
|
609
|
+
manifest
|
|
610
|
+
}
|
|
611
|
+
});
|
|
612
|
+
}, true));
|
|
613
|
+
const updateWorkflowPermissionsHandler = (0, index_1.apiHandler)(async (req, res) => {
|
|
614
|
+
const slug = req.params.slug;
|
|
615
|
+
const { metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
616
|
+
const approvalState = await (0, workflow_approval_snapshot_1.getWorkflowSnapshotApprovalState)(slug);
|
|
617
|
+
if (!approvalState.is_current_code_approved) {
|
|
618
|
+
throw {
|
|
619
|
+
status: 403,
|
|
620
|
+
message: 'Workflow must be approved before updating run permissions'
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
const currentSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, req.userId);
|
|
624
|
+
if (!currentSummary.can_edit) {
|
|
625
|
+
throw {
|
|
626
|
+
status: 403,
|
|
627
|
+
message: currentSummary.is_locked_due_to_missing_users
|
|
628
|
+
? 'Workflow permissions cannot be modified because no allowed users remain'
|
|
629
|
+
: 'You do not have permission to modify workflow run permissions'
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
const { mode, allowed_user_ids } = req.body;
|
|
633
|
+
if (mode !== 'restricted' && mode !== 'everyone') {
|
|
634
|
+
throw {
|
|
635
|
+
status: 400,
|
|
636
|
+
message: 'mode must be "restricted" or "everyone"'
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
const updatedPermission = mode === 'everyone'
|
|
640
|
+
? await (0, workflow_permissions_1.setWorkflowRunPermissions)(slug, { mode: 'everyone' }, metadata.created_by_user_id)
|
|
641
|
+
: await (0, workflow_permissions_1.setWorkflowRunPermissions)(slug, {
|
|
642
|
+
mode: 'restricted',
|
|
643
|
+
allowed_user_ids: Array.isArray(allowed_user_ids) ? allowed_user_ids.map((id) => String(id)) : []
|
|
644
|
+
}, metadata.created_by_user_id);
|
|
645
|
+
const updatedSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, req.userId);
|
|
646
|
+
const updatedPermissionMode = (0, permission_common_1.assertCommonPermissionMode)(updatedPermission.permission_mode, "workflow run");
|
|
647
|
+
const permissions = updatedPermissionMode === "everyone"
|
|
648
|
+
? { mode: "everyone" }
|
|
649
|
+
: { mode: "restricted", allowed_user_ids: updatedPermission.allowedUsers.map((row) => row.user_id) };
|
|
650
|
+
res.json({
|
|
651
|
+
workflow: {
|
|
652
|
+
slug,
|
|
653
|
+
...updatedSummary,
|
|
654
|
+
permissions,
|
|
655
|
+
allowed_users_resolved: updatedPermission.allowedUsers.map((row) => ({
|
|
656
|
+
user_id: row.user.id,
|
|
657
|
+
name: row.user.name,
|
|
658
|
+
email: row.user.email,
|
|
659
|
+
is_owner: row.user.id === metadata.created_by_user_id,
|
|
660
|
+
is_approver: row.user.id === metadata.approved_by_user_id
|
|
661
|
+
}))
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
}, true);
|
|
665
|
+
// PATCH /api/workflows/:slug/permissions - Update permissions (canonical)
|
|
666
|
+
router.patch('/:slug/permissions', updateWorkflowPermissionsHandler);
|
|
667
|
+
// POST /api/workflows/request-permission - Create a tool execution permission request
|
|
668
|
+
router.post('/request-permission', (0, index_1.apiHandler)(async (req, res) => {
|
|
669
|
+
const { message_id, call_id } = req.body;
|
|
670
|
+
if (!message_id || !call_id) {
|
|
671
|
+
throw {
|
|
672
|
+
status: 400,
|
|
673
|
+
message: 'opencode_session_id, message_id, and call_id are required'
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
// Create or update permission request in database
|
|
677
|
+
const permission = await client_1.default.tool_execution_permissions.upsert({
|
|
678
|
+
where: {
|
|
679
|
+
opencode_session_id_message_id_call_id: {
|
|
680
|
+
opencode_session_id: req.opencode_session_id,
|
|
681
|
+
message_id,
|
|
682
|
+
call_id
|
|
683
|
+
}
|
|
684
|
+
},
|
|
685
|
+
create: {
|
|
686
|
+
opencode_session_id: req.opencode_session_id,
|
|
687
|
+
message_id,
|
|
688
|
+
call_id,
|
|
689
|
+
status: 'pending',
|
|
690
|
+
created_at: BigInt(Date.now())
|
|
691
|
+
},
|
|
692
|
+
update: {
|
|
693
|
+
status: 'pending',
|
|
694
|
+
responded_at: null
|
|
695
|
+
}
|
|
696
|
+
});
|
|
697
|
+
res.json({ permission_id: permission.id });
|
|
698
|
+
}, true)); // Plugin authenticates with opencode session token
|
|
699
|
+
// GET /api/workflows/permission-status/:id - Check permission status
|
|
700
|
+
router.get('/permission-status/:id', (0, index_1.apiHandler)(async (req, res) => {
|
|
701
|
+
const id = req.params.id;
|
|
702
|
+
const permission = await client_1.default.tool_execution_permissions.findUnique({
|
|
703
|
+
where: { id }
|
|
704
|
+
});
|
|
705
|
+
if (!permission) {
|
|
706
|
+
throw {
|
|
707
|
+
status: 404,
|
|
708
|
+
message: 'Permission request not found'
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
if (req.opencode_session_id !== permission.opencode_session_id) {
|
|
712
|
+
throw {
|
|
713
|
+
status: 403,
|
|
714
|
+
message: 'Authorization token does not match permission session'
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
res.json({
|
|
718
|
+
status: permission.status,
|
|
719
|
+
approved: permission.status === 'approved'
|
|
720
|
+
});
|
|
721
|
+
}, true)); // Plugin authenticates with opencode session token
|
|
722
|
+
// POST /api/workflows/permission-reject/:id - Reject a pending permission request (plugin cleanup)
|
|
723
|
+
router.post('/permission-reject/:id', (0, index_1.apiHandler)(async (req, res) => {
|
|
724
|
+
const id = req.params.id;
|
|
725
|
+
const permission = await client_1.default.tool_execution_permissions.findUnique({
|
|
726
|
+
where: { id }
|
|
727
|
+
});
|
|
728
|
+
if (!permission) {
|
|
729
|
+
throw {
|
|
730
|
+
status: 404,
|
|
731
|
+
message: 'Permission request not found'
|
|
732
|
+
};
|
|
733
|
+
}
|
|
734
|
+
if (req.opencode_session_id !== permission.opencode_session_id) {
|
|
735
|
+
throw {
|
|
736
|
+
status: 403,
|
|
737
|
+
message: 'Authorization token does not match permission session'
|
|
738
|
+
};
|
|
739
|
+
}
|
|
740
|
+
if (permission.status === 'pending') {
|
|
741
|
+
await client_1.default.tool_execution_permissions.update({
|
|
742
|
+
where: { id },
|
|
743
|
+
data: {
|
|
744
|
+
status: 'rejected',
|
|
745
|
+
responded_at: BigInt(Date.now())
|
|
746
|
+
}
|
|
747
|
+
});
|
|
748
|
+
}
|
|
749
|
+
res.json({ success: true });
|
|
750
|
+
}, true)); // Plugin authenticates with opencode session token
|
|
751
|
+
exports.default = router;
|