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,93 @@
|
|
|
1
|
+
import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
function getApiBaseUrl(): string {
|
|
4
|
+
const port = process.env.TEAMCOPILOT_PORT?.trim()
|
|
5
|
+
if (!port) {
|
|
6
|
+
throw new Error("TEAMCOPILOT_PORT must be set.")
|
|
7
|
+
}
|
|
8
|
+
return `http://localhost:${port}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
interface WorkflowSummary {
|
|
14
|
+
slug: string
|
|
15
|
+
name: string
|
|
16
|
+
intent_summary: string
|
|
17
|
+
is_approved: boolean
|
|
18
|
+
can_edit: boolean
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readErrorMessageFromResponse(
|
|
22
|
+
response: Response,
|
|
23
|
+
fallbackMessage: string
|
|
24
|
+
): Promise<string> {
|
|
25
|
+
try {
|
|
26
|
+
const text = await response.text()
|
|
27
|
+
if (!text) return fallbackMessage
|
|
28
|
+
try {
|
|
29
|
+
const parsed: unknown = JSON.parse(text)
|
|
30
|
+
if (parsed && typeof parsed === "object" && "message" in parsed) {
|
|
31
|
+
const msg = (parsed as { message?: unknown }).message
|
|
32
|
+
if (typeof msg === "string" && msg.trim().length > 0) return msg
|
|
33
|
+
}
|
|
34
|
+
} catch {
|
|
35
|
+
// fall back to plain text
|
|
36
|
+
}
|
|
37
|
+
return text.trim().length > 0 ? text : fallbackMessage
|
|
38
|
+
} catch {
|
|
39
|
+
return fallbackMessage
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const ListAvailableWorkflowsPlugin: Plugin = async (_ctx) => {
|
|
44
|
+
return {
|
|
45
|
+
tool: {
|
|
46
|
+
listAvailableWorkflows: tool({
|
|
47
|
+
description:
|
|
48
|
+
"List workflows that are available for this user to use. Returns all workflows the current user can access, including those pending approval.",
|
|
49
|
+
args: {},
|
|
50
|
+
async execute(_args, context) {
|
|
51
|
+
const { sessionID } = context
|
|
52
|
+
|
|
53
|
+
const response = await fetch(`${getApiBaseUrl()}/api/workflows`, {
|
|
54
|
+
headers: {
|
|
55
|
+
Authorization: `Bearer ${sessionID}`,
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
if (!response.ok) {
|
|
60
|
+
const errorMessage = await readErrorMessageFromResponse(
|
|
61
|
+
response,
|
|
62
|
+
`Failed to list workflows (HTTP ${response.status})`
|
|
63
|
+
)
|
|
64
|
+
throw new Error(errorMessage)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const payload = (await response.json()) as {
|
|
68
|
+
workflows?: WorkflowSummary[]
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const availableWorkflows = (payload.workflows ?? [])
|
|
72
|
+
.map((workflow) => ({
|
|
73
|
+
slug: workflow.slug,
|
|
74
|
+
name: workflow.name,
|
|
75
|
+
intent_summary: workflow.intent_summary,
|
|
76
|
+
is_approved: workflow.is_approved,
|
|
77
|
+
}))
|
|
78
|
+
|
|
79
|
+
return JSON.stringify(
|
|
80
|
+
{
|
|
81
|
+
workflows: availableWorkflows,
|
|
82
|
+
total: availableWorkflows.length,
|
|
83
|
+
},
|
|
84
|
+
null,
|
|
85
|
+
2
|
|
86
|
+
)
|
|
87
|
+
},
|
|
88
|
+
}),
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export default ListAvailableWorkflowsPlugin
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
import type { Dirent } from "node:fs"
|
|
3
|
+
import fs from "node:fs/promises"
|
|
4
|
+
import path from "node:path"
|
|
5
|
+
|
|
6
|
+
const pythonInterpreterPattern =
|
|
7
|
+
/(^|[\s;|&()])(?:\/[\w./-]*python(?:\d+(?:\.\d+)*)?|python(?:\d+(?:\.\d+)*)?|py|pypy)(?=$|[\s;|&()])/i
|
|
8
|
+
|
|
9
|
+
const tokenizeCommand = (command: string): string[] => {
|
|
10
|
+
const tokens = command.match(/"[^"]*"|'[^']*'|&&|\|\||[;|]|[^\s]+/g) ?? []
|
|
11
|
+
return tokens.map((token) => {
|
|
12
|
+
if (
|
|
13
|
+
(token.startsWith("\"") && token.endsWith("\"")) ||
|
|
14
|
+
(token.startsWith("'") && token.endsWith("'"))
|
|
15
|
+
) {
|
|
16
|
+
return token.slice(1, -1)
|
|
17
|
+
}
|
|
18
|
+
return token
|
|
19
|
+
})
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const isPythonInterpreterToken = (token: string): boolean => {
|
|
23
|
+
const base = path.basename(token)
|
|
24
|
+
return /^(?:python(?:\d+(?:\.\d+)*)?|py|pypy)$/i.test(base)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const CONTROL_TOKENS = new Set(["&&", "||", ";", "|"])
|
|
28
|
+
|
|
29
|
+
const resolveCommandCwd = (rawCwd: unknown, fallbackDirectory: string): string => {
|
|
30
|
+
if (typeof rawCwd !== "string" || rawCwd.trim() === "") {
|
|
31
|
+
return fallbackDirectory
|
|
32
|
+
}
|
|
33
|
+
return path.isAbsolute(rawCwd) ? rawCwd : path.resolve(fallbackDirectory, rawCwd)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const resolveRunPyTarget = (command: string, executionCwd: string): string | null => {
|
|
37
|
+
const tokens = tokenizeCommand(command)
|
|
38
|
+
let currentCwd = executionCwd
|
|
39
|
+
|
|
40
|
+
for (let i = 0; i < tokens.length; i += 1) {
|
|
41
|
+
const token = tokens[i]
|
|
42
|
+
|
|
43
|
+
if (token === "cd") {
|
|
44
|
+
const destination = tokens[i + 1]
|
|
45
|
+
if (destination && !CONTROL_TOKENS.has(destination)) {
|
|
46
|
+
currentCwd = path.resolve(currentCwd, destination)
|
|
47
|
+
i += 1
|
|
48
|
+
}
|
|
49
|
+
continue
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (!isPythonInterpreterToken(token)) {
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
for (let j = i + 1; j < tokens.length; j += 1) {
|
|
57
|
+
const nextToken = tokens[j]
|
|
58
|
+
if (CONTROL_TOKENS.has(nextToken)) {
|
|
59
|
+
break
|
|
60
|
+
}
|
|
61
|
+
if (nextToken === "-m" || nextToken === "-c" || nextToken === "-") {
|
|
62
|
+
break
|
|
63
|
+
}
|
|
64
|
+
if (nextToken.startsWith("-")) {
|
|
65
|
+
continue
|
|
66
|
+
}
|
|
67
|
+
if (path.basename(nextToken) === "run.py") {
|
|
68
|
+
return path.resolve(currentCwd, nextToken)
|
|
69
|
+
}
|
|
70
|
+
break
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const getWorkflowRunPyFilesFromDir = async (workflowsDir: string): Promise<string[]> => {
|
|
78
|
+
let entries: Dirent[]
|
|
79
|
+
try {
|
|
80
|
+
entries = await fs.readdir(workflowsDir, { withFileTypes: true })
|
|
81
|
+
} catch {
|
|
82
|
+
return []
|
|
83
|
+
}
|
|
84
|
+
return entries.filter((entry) => entry.isDirectory()).map((entry) => path.join(workflowsDir, entry.name, "run.py"))
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const getWorkflowRunPyFiles = async (roots: string[]): Promise<string[]> => {
|
|
88
|
+
const all = new Set<string>()
|
|
89
|
+
for (const root of roots) {
|
|
90
|
+
const candidates = [
|
|
91
|
+
path.join(root, "workflows"),
|
|
92
|
+
path.join(root, "src", "workspace_files", "workflows"),
|
|
93
|
+
]
|
|
94
|
+
for (const workflowsDir of candidates) {
|
|
95
|
+
const files = await getWorkflowRunPyFilesFromDir(workflowsDir)
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
all.add(file)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return Array.from(all)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const shouldBlockByContentMatch = async (targetRunPy: string, roots: string[]): Promise<boolean> => {
|
|
105
|
+
let targetContent: string
|
|
106
|
+
try {
|
|
107
|
+
targetContent = await fs.readFile(targetRunPy, "utf8")
|
|
108
|
+
} catch {
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
const workflowRunPyFiles = await getWorkflowRunPyFiles(roots)
|
|
112
|
+
|
|
113
|
+
for (const workflowRunPy of workflowRunPyFiles) {
|
|
114
|
+
try {
|
|
115
|
+
const workflowContent = await fs.readFile(workflowRunPy, "utf8")
|
|
116
|
+
if (workflowContent === targetContent) {
|
|
117
|
+
return true
|
|
118
|
+
}
|
|
119
|
+
} catch {
|
|
120
|
+
// Ignore missing/unreadable workflow run.py files.
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return false
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export const PythonProtection: Plugin = async ({ directory, worktree }) => {
|
|
128
|
+
const checkCommand = async (command: string, rawCwd?: unknown): Promise<void> => {
|
|
129
|
+
if (!pythonInterpreterPattern.test(command)) {
|
|
130
|
+
return
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const commandCwd = resolveCommandCwd(rawCwd, directory)
|
|
134
|
+
let targetRunPy = resolveRunPyTarget(command, commandCwd)
|
|
135
|
+
if (!targetRunPy && /(^|[\/\s])run\.py($|[\s;|&()])/.test(command)) {
|
|
136
|
+
targetRunPy = path.resolve(commandCwd, "run.py")
|
|
137
|
+
}
|
|
138
|
+
if (!targetRunPy) {
|
|
139
|
+
return
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const roots = Array.from(new Set([
|
|
143
|
+
directory,
|
|
144
|
+
worktree,
|
|
145
|
+
path.resolve(directory, ".."),
|
|
146
|
+
path.resolve(directory, "../.."),
|
|
147
|
+
path.resolve(commandCwd, ".."),
|
|
148
|
+
path.resolve(commandCwd, "../.."),
|
|
149
|
+
].filter(Boolean)))
|
|
150
|
+
|
|
151
|
+
if (await shouldBlockByContentMatch(targetRunPy, roots)) {
|
|
152
|
+
throw new Error("Direct workflow execution via Python is not allowed. Use the runWorkflow tool instead.")
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
"command.execute.before": async (input) => {
|
|
158
|
+
const command = [input.command, input.arguments].filter(Boolean).join(" ").trim()
|
|
159
|
+
if (!command) {
|
|
160
|
+
return
|
|
161
|
+
}
|
|
162
|
+
await checkCommand(command, directory)
|
|
163
|
+
},
|
|
164
|
+
"tool.execute.before": async (input, output) => {
|
|
165
|
+
const commandCandidates: unknown[] = [
|
|
166
|
+
output.args?.command,
|
|
167
|
+
output.args?.cmd,
|
|
168
|
+
output.args?.arguments,
|
|
169
|
+
output.args?.script,
|
|
170
|
+
(input as { args?: { command?: string; cmd?: string; arguments?: string; script?: string } }).args?.command,
|
|
171
|
+
(input as { args?: { command?: string; cmd?: string; arguments?: string; script?: string } }).args?.cmd,
|
|
172
|
+
(input as { args?: { command?: string; cmd?: string; arguments?: string; script?: string } }).args?.arguments,
|
|
173
|
+
(input as { args?: { command?: string; cmd?: string; arguments?: string; script?: string } }).args?.script,
|
|
174
|
+
]
|
|
175
|
+
const commandValue = commandCandidates.find((value) => typeof value === "string" && value.trim() !== "")
|
|
176
|
+
if (typeof commandValue !== "string") {
|
|
177
|
+
return
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const commandCwd = output.args?.workdir ?? output.args?.cwd ?? input.args?.workdir ?? input.args?.cwd
|
|
181
|
+
await checkCommand(commandValue, commandCwd)
|
|
182
|
+
},
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { type Plugin, tool } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
function getApiBaseUrl(): string {
|
|
4
|
+
const port = process.env.TEAMCOPILOT_PORT?.trim()
|
|
5
|
+
if (!port) {
|
|
6
|
+
throw new Error("TEAMCOPILOT_PORT must be set.")
|
|
7
|
+
}
|
|
8
|
+
return `http://localhost:${port}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
function extractMessageId(context: unknown): string | null {
|
|
13
|
+
if (!context || typeof context !== "object") {
|
|
14
|
+
return null
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const candidate = (context as { messageID?: unknown; messageId?: unknown; message_id?: unknown })
|
|
18
|
+
const raw = candidate.messageID ?? candidate.messageId ?? candidate.message_id
|
|
19
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
20
|
+
return raw
|
|
21
|
+
}
|
|
22
|
+
if (typeof raw === "number" && Number.isFinite(raw)) {
|
|
23
|
+
return String(raw)
|
|
24
|
+
}
|
|
25
|
+
return null
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractCallId(context: unknown): string | null {
|
|
29
|
+
if (!context || typeof context !== "object") {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const candidate = (context as { callID?: unknown; callId?: unknown; call_id?: unknown })
|
|
34
|
+
const raw = candidate.callID ?? candidate.callId ?? candidate.call_id
|
|
35
|
+
if (typeof raw === "string" && raw.trim().length > 0) {
|
|
36
|
+
return raw
|
|
37
|
+
}
|
|
38
|
+
return null
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function readErrorMessageFromResponse(
|
|
42
|
+
response: Response,
|
|
43
|
+
fallbackMessage: string
|
|
44
|
+
): Promise<string> {
|
|
45
|
+
try {
|
|
46
|
+
const text = await response.text()
|
|
47
|
+
if (!text) return fallbackMessage
|
|
48
|
+
try {
|
|
49
|
+
const parsed: unknown = JSON.parse(text)
|
|
50
|
+
if (parsed && typeof parsed === "object" && "message" in parsed) {
|
|
51
|
+
const msg = (parsed as { message?: unknown }).message
|
|
52
|
+
if (typeof msg === "string" && msg.trim().length > 0) return msg
|
|
53
|
+
}
|
|
54
|
+
} catch {
|
|
55
|
+
// fall back to text
|
|
56
|
+
}
|
|
57
|
+
return text.trim().length > 0 ? text : fallbackMessage
|
|
58
|
+
} catch {
|
|
59
|
+
return fallbackMessage
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function sleep(ms: number): Promise<void> {
|
|
64
|
+
return new Promise((resolve) => setTimeout(resolve, ms))
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export const RunWorkflowPlugin: Plugin = async (_ctx) => {
|
|
68
|
+
return {
|
|
69
|
+
tool: {
|
|
70
|
+
runWorkflow: tool({
|
|
71
|
+
description:
|
|
72
|
+
"Execute a workflow with the provided inputs. Validates inputs against the workflow's schema defined in workflow.json, runs the workflow's venv Python with run.py and appropriate arguments, streams output in real-time, and enforces the timeout defined in workflow.json.",
|
|
73
|
+
args: {
|
|
74
|
+
slug: tool.schema
|
|
75
|
+
.string()
|
|
76
|
+
.describe(
|
|
77
|
+
"The workflow slug (folder name under workflows/)"
|
|
78
|
+
),
|
|
79
|
+
inputs: tool.schema
|
|
80
|
+
.record(tool.schema.string(), tool.schema.unknown())
|
|
81
|
+
.optional()
|
|
82
|
+
.default({})
|
|
83
|
+
.describe(
|
|
84
|
+
"Key-value pairs matching the workflow's input schema from workflow.json"
|
|
85
|
+
),
|
|
86
|
+
},
|
|
87
|
+
async execute(args, context) {
|
|
88
|
+
const { sessionID } = context
|
|
89
|
+
const { slug, inputs = {} } = args
|
|
90
|
+
const messageId = extractMessageId(context)
|
|
91
|
+
const callId = extractCallId(context)
|
|
92
|
+
|
|
93
|
+
if (!messageId) {
|
|
94
|
+
throw new Error("Could not determine message id from tool context.")
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (!callId) {
|
|
98
|
+
throw new Error("Could not determine call id from tool context.")
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const startResponse = await fetch(`${getApiBaseUrl()}/api/workflows/execute`, {
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: {
|
|
104
|
+
"Content-Type": "application/json",
|
|
105
|
+
Authorization: `Bearer ${sessionID}`,
|
|
106
|
+
},
|
|
107
|
+
body: JSON.stringify({
|
|
108
|
+
slug,
|
|
109
|
+
inputs,
|
|
110
|
+
message_id: messageId,
|
|
111
|
+
call_id: callId,
|
|
112
|
+
}),
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
if (!startResponse.ok) {
|
|
116
|
+
const errorMessage = await readErrorMessageFromResponse(
|
|
117
|
+
startResponse,
|
|
118
|
+
`Failed to execute workflow (HTTP ${startResponse.status})`
|
|
119
|
+
)
|
|
120
|
+
throw new Error(errorMessage)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const startedPayload = (await startResponse.json()) as {
|
|
124
|
+
execution_id?: unknown
|
|
125
|
+
}
|
|
126
|
+
const executionId = startedPayload.execution_id
|
|
127
|
+
if (typeof executionId !== "string" || executionId.trim().length === 0) {
|
|
128
|
+
throw new Error("Workflow execute start response did not include execution_id.")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
while (true) {
|
|
132
|
+
const resultResponse = await fetch(
|
|
133
|
+
`${getApiBaseUrl()}/api/workflows/execute/${encodeURIComponent(executionId)}`,
|
|
134
|
+
{
|
|
135
|
+
method: "GET",
|
|
136
|
+
headers: {
|
|
137
|
+
Authorization: `Bearer ${sessionID}`,
|
|
138
|
+
},
|
|
139
|
+
}
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
if (!resultResponse.ok) {
|
|
143
|
+
const errorMessage = await readErrorMessageFromResponse(
|
|
144
|
+
resultResponse,
|
|
145
|
+
`Failed to fetch workflow execution result (HTTP ${resultResponse.status})`
|
|
146
|
+
)
|
|
147
|
+
throw new Error(errorMessage)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const resultPayload = (await resultResponse.json()) as {
|
|
151
|
+
status?: unknown
|
|
152
|
+
}
|
|
153
|
+
if (resultPayload.status === "running") {
|
|
154
|
+
await sleep(500)
|
|
155
|
+
continue
|
|
156
|
+
}
|
|
157
|
+
if (resultPayload.status !== "success") {
|
|
158
|
+
throw new Error(JSON.stringify(resultPayload))
|
|
159
|
+
}
|
|
160
|
+
return JSON.stringify(resultPayload)
|
|
161
|
+
}
|
|
162
|
+
},
|
|
163
|
+
}),
|
|
164
|
+
},
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export default RunWorkflowPlugin
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"strict": false,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"forceConsistentCasingInFileNames": true,
|
|
10
|
+
"declaration": true,
|
|
11
|
+
"outDir": "./dist",
|
|
12
|
+
"rootDir": "./plugins"
|
|
13
|
+
},
|
|
14
|
+
"include": ["plugins/**/*.ts"],
|
|
15
|
+
"exclude": ["node_modules", "dist"]
|
|
16
|
+
}
|