teamcopilot 0.3.4 → 0.3.6
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 +7 -0
- package/dist/frontend/assets/{cssMode-CB9iAlw8.js → cssMode-BRVRAYCz.js} +1 -1
- package/dist/frontend/assets/{freemarker2-DI2xjASY.js → freemarker2-B5FvHwsO.js} +1 -1
- package/dist/frontend/assets/{handlebars-CmcQby9J.js → handlebars-DWX2asql.js} +1 -1
- package/dist/frontend/assets/{html-ClP1XQBQ.js → html-BEBxxD9G.js} +1 -1
- package/dist/frontend/assets/{htmlMode-DIlZsDL4.js → htmlMode-B2LbPTwC.js} +1 -1
- package/dist/frontend/assets/index-D1Hcz_bo.css +1 -0
- package/dist/frontend/assets/{index-NFAjAMOV.js → index-D3TE04C5.js} +224 -219
- package/dist/frontend/assets/{javascript-CbZA1Ase.js → javascript-Bh4JwoPV.js} +1 -1
- package/dist/frontend/assets/{jsonMode-VYZo-ljl.js → jsonMode-7j-aplXT.js} +1 -1
- package/dist/frontend/assets/{liquid-I2CXh5hp.js → liquid-BP4OxkO7.js} +1 -1
- package/dist/frontend/assets/{mdx-BrO6zg6G.js → mdx-C1OIcGbY.js} +1 -1
- package/dist/frontend/assets/{python-S8m0lqgl.js → python-BO8Wy5jz.js} +1 -1
- package/dist/frontend/assets/{razor-CV1dlAuV.js → razor-BDtqXvAH.js} +1 -1
- package/dist/frontend/assets/{tsMode-mSUzzhBd.js → tsMode-D22HcCuX.js} +1 -1
- package/dist/frontend/assets/{typescript-BOhssVGT.js → typescript-CagwEzRw.js} +1 -1
- package/dist/frontend/assets/{xml-DF5nbc8Y.js → xml-fE5sGZ5z.js} +1 -1
- package/dist/frontend/assets/{yaml-CjFdxQq7.js → yaml-CZMoG4WG.js} +1 -1
- package/dist/frontend/index.html +2 -2
- package/dist/index.js +2 -0
- package/dist/utils/external-host.js +22 -0
- package/dist/utils/opencode-auth.js +48 -1
- package/dist/utils/secrets.js +15 -0
- package/dist/utils/workflow-api-keys.js +88 -0
- package/dist/utils/workflow-interruption.js +2 -2
- package/dist/utils/workflow-runner.js +11 -2
- package/dist/utils/workflow.js +3 -0
- package/dist/utils/workspace-sync.js +6 -1
- package/dist/workflow-api.js +185 -0
- package/dist/workflows/index.js +35 -0
- package/dist/workspace_files/AGENTS.md +3 -1
- package/package.json +1 -1
- package/prisma/generated/client/edge.js +15 -4
- package/prisma/generated/client/index-browser.js +12 -1
- package/prisma/generated/client/index.d.ts +2002 -270
- package/prisma/generated/client/index.js +15 -4
- package/prisma/generated/client/package.json +1 -1
- package/prisma/generated/client/schema.prisma +28 -12
- package/prisma/generated/client/wasm.js +15 -4
- package/prisma/migrations/20260430033133_add_workflow_api_keys/migration.sql +46 -0
- package/prisma/migrations/20260430161041_remove_run_source_default/migration.sql +28 -0
- package/prisma/schema.prisma +18 -2
- package/dist/frontend/assets/index-bnKwfDfp.css +0 -1
|
@@ -11,9 +11,14 @@ exports.getRuntimeProviderAuth = getRuntimeProviderAuth;
|
|
|
11
11
|
exports.setRuntimeProviderAuth = setRuntimeProviderAuth;
|
|
12
12
|
exports.syncManagedProviderConfiguration = syncManagedProviderConfiguration;
|
|
13
13
|
const promises_1 = __importDefault(require("fs/promises"));
|
|
14
|
+
const fs_1 = require("fs");
|
|
14
15
|
const path_1 = __importDefault(require("path"));
|
|
15
16
|
const assert_1 = require("./assert");
|
|
16
17
|
const workspace_sync_1 = require("./workspace-sync");
|
|
18
|
+
function isGoogleVertexManagedProvider(providerId) {
|
|
19
|
+
const id = normalizeProviderId(providerId).toLowerCase();
|
|
20
|
+
return id === "google-vertex" || id.startsWith("google-vertex-");
|
|
21
|
+
}
|
|
17
22
|
const WORKSPACE_OPENCODE_DIR = ".opencode";
|
|
18
23
|
const WORKSPACE_AUTH_FILE = "auth.json";
|
|
19
24
|
const WORKSPACE_CONFIG_FILE = "opencode.json";
|
|
@@ -159,8 +164,9 @@ function getAuthForProvider(record, providerId) {
|
|
|
159
164
|
function isAzureCustomProvider(providerId) {
|
|
160
165
|
return normalizeProviderId(providerId).toLowerCase() === "azure-openai";
|
|
161
166
|
}
|
|
167
|
+
/** Service-level credentials (TeamCopilot env, not per-user auth UI) — keep in sync with isManagedServiceLevelProvider in OpencodeAuthSetup.tsx */
|
|
162
168
|
function isServiceManagedProvider(providerId) {
|
|
163
|
-
return isAzureCustomProvider(providerId);
|
|
169
|
+
return isAzureCustomProvider(providerId) || isGoogleVertexManagedProvider(providerId);
|
|
164
170
|
}
|
|
165
171
|
function normalizeAzureEndpoint(endpoint) {
|
|
166
172
|
return endpoint.trim().replace(/\/+$/, "");
|
|
@@ -169,6 +175,42 @@ function hasRequiredAzureEnvironment() {
|
|
|
169
175
|
return isNonEmptyString(process.env.AZURE_API_KEY)
|
|
170
176
|
&& isNonEmptyString(process.env.AZURE_OPENAI_ENDPOINT);
|
|
171
177
|
}
|
|
178
|
+
function getGoogleCloudProjectFromEnv() {
|
|
179
|
+
const trimmed = (process.env.GOOGLE_CLOUD_PROJECT ?? "").trim();
|
|
180
|
+
return isNonEmptyString(trimmed) ? trimmed : undefined;
|
|
181
|
+
}
|
|
182
|
+
async function hasRequiredVertexManagedProject() {
|
|
183
|
+
return getGoogleCloudProjectFromEnv() !== undefined && hasVertexLocationConfigured() && await googleApplicationCredentialsConfigured();
|
|
184
|
+
}
|
|
185
|
+
function hasVertexLocationConfigured() {
|
|
186
|
+
return isNonEmptyString(process.env.VERTEX_LOCATION?.trim());
|
|
187
|
+
}
|
|
188
|
+
async function googleApplicationCredentialsConfigured() {
|
|
189
|
+
const credPath = process.env.GOOGLE_APPLICATION_CREDENTIALS?.trim();
|
|
190
|
+
if (!credPath) {
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
try {
|
|
194
|
+
await promises_1.default.access(credPath, fs_1.constants.R_OK);
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function hasVertexManagedRuntimeReady(providerId) {
|
|
202
|
+
if (!isGoogleVertexManagedProvider(providerId)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
if (!(await hasRequiredVertexManagedProject())) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
const modelTail = getConfiguredModelId().trim();
|
|
209
|
+
if (!modelTail) {
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
172
214
|
async function hasAzureProviderConfiguration(providerId) {
|
|
173
215
|
const deployment = getConfiguredModelId().trim();
|
|
174
216
|
const config = await readOpencodeConfig(getWorkspaceOpencodeConfigPath());
|
|
@@ -187,6 +229,9 @@ async function hasRuntimeProviderCredentials(providerId) {
|
|
|
187
229
|
if (isAzureCustomProvider(providerId)) {
|
|
188
230
|
return hasRequiredAzureEnvironment() && await hasAzureProviderConfiguration(providerId);
|
|
189
231
|
}
|
|
232
|
+
if (isGoogleVertexManagedProvider(providerId)) {
|
|
233
|
+
return await hasVertexManagedRuntimeReady(providerId);
|
|
234
|
+
}
|
|
190
235
|
return Boolean(await getRuntimeProviderAuth(providerId));
|
|
191
236
|
}
|
|
192
237
|
async function getRuntimeProviderAuth(providerId) {
|
|
@@ -202,6 +247,8 @@ async function setRuntimeProviderAuth(providerId, info) {
|
|
|
202
247
|
await writeAuthRecord(getRuntimeAuthPath(), runtimeRecord);
|
|
203
248
|
}
|
|
204
249
|
async function syncManagedProviderConfiguration() {
|
|
250
|
+
// Azure OpenAI: workspace opencode.json must list the deployment, base URL, and @ai-sdk/azure options.
|
|
251
|
+
// Google Vertex providers are built into OpenCode; project/region/credentials come from process env only — no stanza needed here.
|
|
205
252
|
const providerId = getConfiguredModelProviderId();
|
|
206
253
|
if (!isAzureCustomProvider(providerId) || !hasRequiredAzureEnvironment()) {
|
|
207
254
|
return;
|
package/dist/utils/secrets.js
CHANGED
|
@@ -8,6 +8,7 @@ exports.normalizeSecretKeyList = normalizeSecretKeyList;
|
|
|
8
8
|
exports.assertValidSecretKeyList = assertValidSecretKeyList;
|
|
9
9
|
exports.toSecretListItem = toSecretListItem;
|
|
10
10
|
exports.resolveSecretsForUser = resolveSecretsForUser;
|
|
11
|
+
exports.resolveGlobalSecrets = resolveGlobalSecrets;
|
|
11
12
|
exports.resolveSecretsFromResolvedMap = resolveSecretsFromResolvedMap;
|
|
12
13
|
exports.listResolvedSecretsForUser = listResolvedSecretsForUser;
|
|
13
14
|
const client_1 = __importDefault(require("../prisma/client"));
|
|
@@ -83,6 +84,20 @@ async function resolveSecretsForUser(userId, requiredKeys) {
|
|
|
83
84
|
const resolvedSecrets = await listResolvedSecretsForUser(userId);
|
|
84
85
|
return resolveSecretsFromResolvedMap(resolvedSecrets, keys);
|
|
85
86
|
}
|
|
87
|
+
async function resolveGlobalSecrets(requiredKeys) {
|
|
88
|
+
const keys = normalizeSecretKeyList(requiredKeys);
|
|
89
|
+
if (keys.length === 0) {
|
|
90
|
+
return {
|
|
91
|
+
secretMap: {},
|
|
92
|
+
missingKeys: [],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const globalSecrets = await client_1.default.global_secrets.findMany({
|
|
96
|
+
orderBy: { key: "asc" }
|
|
97
|
+
});
|
|
98
|
+
const globalSecretMap = Object.fromEntries(globalSecrets.map((row) => [row.key, row.value]));
|
|
99
|
+
return resolveSecretsFromResolvedMap(globalSecretMap, keys);
|
|
100
|
+
}
|
|
86
101
|
function resolveSecretsFromResolvedMap(resolvedSecrets, requiredKeys) {
|
|
87
102
|
const keys = normalizeSecretKeyList(requiredKeys);
|
|
88
103
|
if (keys.length === 0) {
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
exports.assertCanManageWorkflowApiKeys = assertCanManageWorkflowApiKeys;
|
|
7
|
+
exports.listWorkflowApiKeys = listWorkflowApiKeys;
|
|
8
|
+
exports.createWorkflowApiKey = createWorkflowApiKey;
|
|
9
|
+
exports.deleteWorkflowApiKey = deleteWorkflowApiKey;
|
|
10
|
+
const crypto_1 = require("crypto");
|
|
11
|
+
const client_1 = __importDefault(require("../prisma/client"));
|
|
12
|
+
const resource_access_1 = require("./resource-access");
|
|
13
|
+
const workflow_approval_snapshot_1 = require("./workflow-approval-snapshot");
|
|
14
|
+
const workflow_1 = require("./workflow");
|
|
15
|
+
async function assertCanManageWorkflowApiKeys(slug, userId) {
|
|
16
|
+
await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
17
|
+
const approvalState = await (0, workflow_approval_snapshot_1.getWorkflowSnapshotApprovalState)(slug);
|
|
18
|
+
if (!approvalState.is_current_code_approved) {
|
|
19
|
+
throw {
|
|
20
|
+
status: 403,
|
|
21
|
+
message: "Workflow must be approved before managing API keys"
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
const access = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, userId);
|
|
25
|
+
if (!access.can_edit) {
|
|
26
|
+
throw {
|
|
27
|
+
status: 403,
|
|
28
|
+
message: "You do not have permission to manage API keys for this workflow"
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async function ensureWorkflowApiKey(slug, createdByUserId) {
|
|
33
|
+
const existing = await client_1.default.workflow_api_keys.findFirst({
|
|
34
|
+
where: { workflow_slug: slug },
|
|
35
|
+
orderBy: { created_at: "asc" }
|
|
36
|
+
});
|
|
37
|
+
if (existing) {
|
|
38
|
+
return existing;
|
|
39
|
+
}
|
|
40
|
+
return await client_1.default.workflow_api_keys.create({
|
|
41
|
+
data: {
|
|
42
|
+
workflow_slug: slug,
|
|
43
|
+
api_key: (0, crypto_1.randomUUID)(),
|
|
44
|
+
created_by_user_id: createdByUserId,
|
|
45
|
+
created_at: BigInt(Date.now()),
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
async function listWorkflowApiKeys(slug, createdByUserId) {
|
|
50
|
+
await ensureWorkflowApiKey(slug, createdByUserId);
|
|
51
|
+
return await client_1.default.workflow_api_keys.findMany({
|
|
52
|
+
where: { workflow_slug: slug },
|
|
53
|
+
orderBy: { created_at: "asc" }
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
async function createWorkflowApiKey(slug, createdByUserId) {
|
|
57
|
+
return await client_1.default.workflow_api_keys.create({
|
|
58
|
+
data: {
|
|
59
|
+
workflow_slug: slug,
|
|
60
|
+
api_key: (0, crypto_1.randomUUID)(),
|
|
61
|
+
created_by_user_id: createdByUserId,
|
|
62
|
+
created_at: BigInt(Date.now()),
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
async function deleteWorkflowApiKey(slug, keyId) {
|
|
67
|
+
const key = await client_1.default.workflow_api_keys.findUnique({
|
|
68
|
+
where: { id: keyId }
|
|
69
|
+
});
|
|
70
|
+
if (!key || key.workflow_slug !== slug) {
|
|
71
|
+
throw {
|
|
72
|
+
status: 404,
|
|
73
|
+
message: "Workflow API key not found"
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
const keyCount = await client_1.default.workflow_api_keys.count({
|
|
77
|
+
where: { workflow_slug: slug }
|
|
78
|
+
});
|
|
79
|
+
if (keyCount <= 1) {
|
|
80
|
+
throw {
|
|
81
|
+
status: 400,
|
|
82
|
+
message: "Each workflow must always have at least one API key"
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
await client_1.default.workflow_api_keys.delete({
|
|
86
|
+
where: { id: keyId }
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -8,8 +8,8 @@ exports.markWorkflowSessionAborted = markWorkflowSessionAborted;
|
|
|
8
8
|
const client_1 = __importDefault(require("../prisma/client"));
|
|
9
9
|
const opencode_client_1 = require("./opencode-client");
|
|
10
10
|
async function isWorkflowSessionInterrupted(sessionId, workspaceDir) {
|
|
11
|
-
const
|
|
12
|
-
if (
|
|
11
|
+
const usesDatabaseAbortMarker = sessionId.startsWith("manual-") || sessionId.startsWith("api-");
|
|
12
|
+
if (usesDatabaseAbortMarker) {
|
|
13
13
|
const aborted = await client_1.default.workflow_aborted_sessions.findUnique({
|
|
14
14
|
where: { session_id: sessionId }
|
|
15
15
|
});
|
|
@@ -382,9 +382,16 @@ async function startWorkflowRunViaBackend(options) {
|
|
|
382
382
|
if (!validation.valid) {
|
|
383
383
|
throw new Error(`Input validation failed: ${JSON.stringify(validation.errors)}`);
|
|
384
384
|
}
|
|
385
|
-
const
|
|
385
|
+
const secretResolutionMode = options.secretResolutionMode;
|
|
386
|
+
if (secretResolutionMode === "user" && !options.authUserId) {
|
|
387
|
+
throw new Error("authUserId is required for user secret resolution.");
|
|
388
|
+
}
|
|
389
|
+
const secretResolution = secretResolutionMode === "global"
|
|
390
|
+
? await (0, secrets_1.resolveGlobalSecrets)(requiredSecrets)
|
|
391
|
+
: await (0, secrets_1.resolveSecretsForUser)(options.authUserId, requiredSecrets);
|
|
386
392
|
if (secretResolution.missingKeys.length > 0) {
|
|
387
|
-
|
|
393
|
+
const secretLocation = secretResolutionMode === "global" ? "global secrets" : "your profile secrets";
|
|
394
|
+
throw new Error(`Missing required secrets: ${secretResolution.missingKeys.join(", ")}. Add these keys in ${secretLocation} before running this workflow.`);
|
|
388
395
|
}
|
|
389
396
|
const timeoutSeconds = parseTimeoutSeconds(workflowJson.runtime?.timeout_seconds);
|
|
390
397
|
if (!timeoutSeconds) {
|
|
@@ -400,6 +407,8 @@ async function startWorkflowRunViaBackend(options) {
|
|
|
400
407
|
args: JSON.stringify(options.inputs),
|
|
401
408
|
session_id: options.sessionId,
|
|
402
409
|
message_id: options.messageId,
|
|
410
|
+
run_source: options.runSource,
|
|
411
|
+
workflow_api_key_id: options.workflowApiKeyId ?? null,
|
|
403
412
|
}
|
|
404
413
|
});
|
|
405
414
|
const workflowRunsDir = path.join(options.workspaceDir, "workflow-runs");
|
package/dist/utils/workflow.js
CHANGED
|
@@ -45,6 +45,9 @@ async function deleteWorkflow(slug) {
|
|
|
45
45
|
await client_1.default.workflow_runs.deleteMany({
|
|
46
46
|
where: { workflow_slug: slug }
|
|
47
47
|
});
|
|
48
|
+
await client_1.default.workflow_api_keys.deleteMany({
|
|
49
|
+
where: { workflow_slug: slug }
|
|
50
|
+
});
|
|
48
51
|
await client_1.default.resource_metadata.deleteMany({
|
|
49
52
|
where: {
|
|
50
53
|
resource_kind: "workflow",
|
|
@@ -23,6 +23,7 @@ const WORKSPACE_DB_FILENAME = "data.db";
|
|
|
23
23
|
const HONEYTOKEN_UUID = "1f9f0b72-5f9f-4c9b-aef1-2fb2e0f6d8c4";
|
|
24
24
|
const HONEYTOKEN_FILE_NAME = `honeytoken-${HONEYTOKEN_UUID}.txt`;
|
|
25
25
|
const WORKSPACE_AZURE_PROVIDER_VERSION = "3.0.48";
|
|
26
|
+
const WORKSPACE_GOOGLE_VERTEX_PROVIDER_VERSION = "4.0.114";
|
|
26
27
|
const WORKSPACE_INSTALL_STATE_RELATIVE_PATH = path_1.default.join(".opencode", "install-state.json");
|
|
27
28
|
const WORKSPACE_OPENCODE_DIRECTORY = ".opencode";
|
|
28
29
|
const WORKSPACE_OPENCODE_PACKAGE_JSON = path_1.default.join(WORKSPACE_OPENCODE_DIRECTORY, "package.json");
|
|
@@ -208,9 +209,13 @@ async function initializeWorkspaceNodeDependencies(workspaceDir) {
|
|
|
208
209
|
...(existingPackageJson.dependencies ?? {}),
|
|
209
210
|
"opencode-ai": "1.3.7",
|
|
210
211
|
};
|
|
211
|
-
|
|
212
|
+
const opencodeModelProvider = ((0, assert_1.assertEnv)("OPENCODE_MODEL").split("/")[0] ?? "").toLowerCase();
|
|
213
|
+
if (opencodeModelProvider === "azure-openai") {
|
|
212
214
|
dependencies["@ai-sdk/azure"] = WORKSPACE_AZURE_PROVIDER_VERSION;
|
|
213
215
|
}
|
|
216
|
+
if (opencodeModelProvider === "google-vertex" || opencodeModelProvider.startsWith("google-vertex-")) {
|
|
217
|
+
dependencies["@ai-sdk/google-vertex"] = WORKSPACE_GOOGLE_VERTEX_PROVIDER_VERSION;
|
|
218
|
+
}
|
|
214
219
|
const workspacePackageJson = {
|
|
215
220
|
...existingPackageJson,
|
|
216
221
|
dependencies,
|
|
@@ -0,0 +1,185 @@
|
|
|
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 promises_1 = __importDefault(require("fs/promises"));
|
|
8
|
+
const path_1 = __importDefault(require("path"));
|
|
9
|
+
const crypto_1 = require("crypto");
|
|
10
|
+
const client_1 = __importDefault(require("./prisma/client"));
|
|
11
|
+
const workflow_1 = require("./utils/workflow");
|
|
12
|
+
const workflow_approval_snapshot_1 = require("./utils/workflow-approval-snapshot");
|
|
13
|
+
const workspace_sync_1 = require("./utils/workspace-sync");
|
|
14
|
+
const workflow_runner_1 = require("./utils/workflow-runner");
|
|
15
|
+
const workflow_interruption_1 = require("./utils/workflow-interruption");
|
|
16
|
+
function sanitizeFilenamePart(value) {
|
|
17
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
18
|
+
}
|
|
19
|
+
function isPathInside(childPath, parentPath) {
|
|
20
|
+
const parent = path_1.default.resolve(parentPath) + path_1.default.sep;
|
|
21
|
+
const child = path_1.default.resolve(childPath) + path_1.default.sep;
|
|
22
|
+
return child.startsWith(parent);
|
|
23
|
+
}
|
|
24
|
+
function workflowApiHandler(handler) {
|
|
25
|
+
return async (req, res, next) => {
|
|
26
|
+
try {
|
|
27
|
+
const authHeader = req.headers.authorization;
|
|
28
|
+
if (!authHeader) {
|
|
29
|
+
throw {
|
|
30
|
+
status: 401,
|
|
31
|
+
message: "Missing authorization header. Please pass the workflow API key as an authorization bearer token."
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
const rawToken = authHeader.split(" ")[1];
|
|
35
|
+
if (!rawToken) {
|
|
36
|
+
throw {
|
|
37
|
+
status: 401,
|
|
38
|
+
message: "Missing authorization bearer token"
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const key = await client_1.default.workflow_api_keys.findUnique({
|
|
42
|
+
where: { api_key: rawToken },
|
|
43
|
+
select: { id: true, workflow_slug: true, api_key: true }
|
|
44
|
+
});
|
|
45
|
+
if (!key) {
|
|
46
|
+
throw {
|
|
47
|
+
status: 401,
|
|
48
|
+
message: "Invalid workflow API key"
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const apiReq = req;
|
|
52
|
+
apiReq.workflowApiKey = key;
|
|
53
|
+
await handler(apiReq, res);
|
|
54
|
+
}
|
|
55
|
+
catch (err) {
|
|
56
|
+
next(err);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function assertWorkflowCanRunViaApi(slug) {
|
|
61
|
+
await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
|
|
62
|
+
const approvalState = await (0, workflow_approval_snapshot_1.getWorkflowSnapshotApprovalState)(slug);
|
|
63
|
+
if (!approvalState.is_current_code_approved) {
|
|
64
|
+
throw {
|
|
65
|
+
status: 403,
|
|
66
|
+
message: "Workflow is not approved for the current code version"
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
async function assertApiKeyCanAccessRun(req, runHandle) {
|
|
71
|
+
const run = await client_1.default.workflow_runs.findUnique({
|
|
72
|
+
where: { id: runHandle }
|
|
73
|
+
});
|
|
74
|
+
if (!run) {
|
|
75
|
+
throw {
|
|
76
|
+
status: 404,
|
|
77
|
+
message: "Workflow run not found"
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
if (req.workflowApiKey.workflow_slug !== run.workflow_slug) {
|
|
81
|
+
throw {
|
|
82
|
+
status: 403,
|
|
83
|
+
message: "Workflow API key does not have access to this run"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return run;
|
|
87
|
+
}
|
|
88
|
+
async function readWorkflowRunLogs(run) {
|
|
89
|
+
if (!run.session_id || !run.message_id) {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
const workspaceDir = (0, workspace_sync_1.getWorkspaceDirFromEnv)();
|
|
93
|
+
const workflowRunsDir = path_1.default.join(workspaceDir, "workflow-runs");
|
|
94
|
+
const logPath = path_1.default.join(workflowRunsDir, `${sanitizeFilenamePart(run.session_id)}-${sanitizeFilenamePart(run.message_id)}.txt`);
|
|
95
|
+
if (!isPathInside(logPath, workflowRunsDir)) {
|
|
96
|
+
throw {
|
|
97
|
+
status: 400,
|
|
98
|
+
message: "Invalid log path"
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
try {
|
|
102
|
+
return await promises_1.default.readFile(logPath, "utf-8");
|
|
103
|
+
}
|
|
104
|
+
catch {
|
|
105
|
+
return null;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function parseRunInputs(args) {
|
|
109
|
+
if (args === null || args === "") {
|
|
110
|
+
return {};
|
|
111
|
+
}
|
|
112
|
+
return JSON.parse(args);
|
|
113
|
+
}
|
|
114
|
+
const workflowApiRouter = express_1.default.Router();
|
|
115
|
+
workflowApiRouter.post("/runs", workflowApiHandler(async (req, res) => {
|
|
116
|
+
const body = req.body;
|
|
117
|
+
if (typeof body.workflow_slug !== "string" || body.workflow_slug.trim().length === 0) {
|
|
118
|
+
throw {
|
|
119
|
+
status: 400,
|
|
120
|
+
message: "workflow_slug is required"
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
if (body.inputs !== undefined && (!body.inputs || typeof body.inputs !== "object" || Array.isArray(body.inputs))) {
|
|
124
|
+
throw {
|
|
125
|
+
status: 400,
|
|
126
|
+
message: "inputs must be an object"
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const slug = body.workflow_slug;
|
|
130
|
+
if (req.workflowApiKey.workflow_slug !== slug) {
|
|
131
|
+
throw {
|
|
132
|
+
status: 403,
|
|
133
|
+
message: "Workflow API key does not belong to this workflow"
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
await assertWorkflowCanRunViaApi(slug);
|
|
137
|
+
const startedRun = await (0, workflow_runner_1.startWorkflowRunViaBackend)({
|
|
138
|
+
workspaceDir: (0, workspace_sync_1.getWorkspaceDirFromEnv)(),
|
|
139
|
+
slug,
|
|
140
|
+
inputs: (body.inputs ?? {}),
|
|
141
|
+
authUserId: null,
|
|
142
|
+
sessionId: `api-${req.workflowApiKey.id}-${(0, crypto_1.randomUUID)()}`,
|
|
143
|
+
messageId: `api-message-${(0, crypto_1.randomUUID)()}`,
|
|
144
|
+
callId: `api-call-${(0, crypto_1.randomUUID)()}`,
|
|
145
|
+
requirePermissionPrompt: false,
|
|
146
|
+
secretResolutionMode: "global",
|
|
147
|
+
runSource: "api",
|
|
148
|
+
workflowApiKeyId: req.workflowApiKey.id,
|
|
149
|
+
});
|
|
150
|
+
void startedRun.completion.catch(() => undefined);
|
|
151
|
+
res.json({
|
|
152
|
+
run_handle: startedRun.runId
|
|
153
|
+
});
|
|
154
|
+
}));
|
|
155
|
+
workflowApiRouter.get("/runs/:runHandle", workflowApiHandler(async (req, res) => {
|
|
156
|
+
const runHandle = req.params.runHandle;
|
|
157
|
+
const run = await assertApiKeyCanAccessRun(req, runHandle);
|
|
158
|
+
const logs = await readWorkflowRunLogs(run);
|
|
159
|
+
const inputs = parseRunInputs(run.args);
|
|
160
|
+
res.json({
|
|
161
|
+
run_handle: run.id,
|
|
162
|
+
workflow_slug: run.workflow_slug,
|
|
163
|
+
status: run.status,
|
|
164
|
+
logs,
|
|
165
|
+
error_message: run.error_message,
|
|
166
|
+
started_at: run.started_at,
|
|
167
|
+
completed_at: run.completed_at,
|
|
168
|
+
inputs,
|
|
169
|
+
});
|
|
170
|
+
}));
|
|
171
|
+
workflowApiRouter.post("/runs/:runHandle/stop", workflowApiHandler(async (req, res) => {
|
|
172
|
+
const runHandle = req.params.runHandle;
|
|
173
|
+
const run = await assertApiKeyCanAccessRun(req, runHandle);
|
|
174
|
+
if (run.status === "running") {
|
|
175
|
+
if (!run.session_id) {
|
|
176
|
+
throw {
|
|
177
|
+
status: 404,
|
|
178
|
+
message: "Workflow run session not found"
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
await (0, workflow_interruption_1.markWorkflowSessionAborted)(run.session_id);
|
|
182
|
+
}
|
|
183
|
+
res.json({ success: true });
|
|
184
|
+
}));
|
|
185
|
+
exports.default = workflowApiRouter;
|
package/dist/workflows/index.js
CHANGED
|
@@ -25,6 +25,8 @@ const resource_file_routes_1 = require("../utils/resource-file-routes");
|
|
|
25
25
|
const resource_access_1 = require("../utils/resource-access");
|
|
26
26
|
const secrets_1 = require("../utils/secrets");
|
|
27
27
|
const secret_contract_validation_1 = require("../utils/secret-contract-validation");
|
|
28
|
+
const workflow_api_keys_1 = require("../utils/workflow-api-keys");
|
|
29
|
+
const external_host_1 = require("../utils/external-host");
|
|
28
30
|
const router = express_1.default.Router({ mergeParams: true });
|
|
29
31
|
const uploadTmpDir = path_1.default.join(os_1.default.tmpdir(), "teamcopilot-workflow-uploads");
|
|
30
32
|
fs_1.default.mkdirSync(uploadTmpDir, { recursive: true });
|
|
@@ -317,6 +319,8 @@ router.post('/:slug/manual-run', (0, index_1.apiHandler)(async (req, res) => {
|
|
|
317
319
|
messageId: manualMessageId,
|
|
318
320
|
callId: manualCallId,
|
|
319
321
|
requirePermissionPrompt: false,
|
|
322
|
+
runSource: "user",
|
|
323
|
+
secretResolutionMode: "user",
|
|
320
324
|
});
|
|
321
325
|
void startedRun.completion.catch(() => undefined);
|
|
322
326
|
res.json({
|
|
@@ -356,6 +360,8 @@ router.post('/execute', (0, index_1.apiHandler)(async (req, res) => {
|
|
|
356
360
|
messageId,
|
|
357
361
|
callId,
|
|
358
362
|
requirePermissionPrompt: true,
|
|
363
|
+
runSource: "user",
|
|
364
|
+
secretResolutionMode: "user",
|
|
359
365
|
});
|
|
360
366
|
const executionId = (0, crypto_1.randomUUID)();
|
|
361
367
|
const executionRecord = {
|
|
@@ -532,6 +538,35 @@ router.delete('/:slug', (0, index_1.apiHandler)(async (req, res) => {
|
|
|
532
538
|
await (0, workflow_1.deleteWorkflow)(slug);
|
|
533
539
|
res.json({ success: true });
|
|
534
540
|
}, true));
|
|
541
|
+
// GET /api/workflows/:slug/api-keys - List API keys for an approved workflow
|
|
542
|
+
router.get('/:slug/api-keys', (0, index_1.apiHandler)(async (req, res) => {
|
|
543
|
+
const slug = req.params.slug;
|
|
544
|
+
await (0, workflow_api_keys_1.assertCanManageWorkflowApiKeys)(slug, req.userId);
|
|
545
|
+
const apiKeys = await (0, workflow_api_keys_1.listWorkflowApiKeys)(slug, req.userId);
|
|
546
|
+
res.locals.skipResponseSanitization = true;
|
|
547
|
+
res.json({
|
|
548
|
+
api_base_url: (0, external_host_1.getWorkflowApiBaseUrl)(),
|
|
549
|
+
api_keys: apiKeys,
|
|
550
|
+
});
|
|
551
|
+
}, true));
|
|
552
|
+
// POST /api/workflows/:slug/api-keys - Add an API key for an approved workflow
|
|
553
|
+
router.post('/:slug/api-keys', (0, index_1.apiHandler)(async (req, res) => {
|
|
554
|
+
const slug = req.params.slug;
|
|
555
|
+
await (0, workflow_api_keys_1.assertCanManageWorkflowApiKeys)(slug, req.userId);
|
|
556
|
+
const apiKey = await (0, workflow_api_keys_1.createWorkflowApiKey)(slug, req.userId);
|
|
557
|
+
res.locals.skipResponseSanitization = true;
|
|
558
|
+
res.json({
|
|
559
|
+
api_key: apiKey,
|
|
560
|
+
});
|
|
561
|
+
}, true));
|
|
562
|
+
// DELETE /api/workflows/:slug/api-keys/:keyId - Remove an API key
|
|
563
|
+
router.delete('/:slug/api-keys/:keyId', (0, index_1.apiHandler)(async (req, res) => {
|
|
564
|
+
const slug = req.params.slug;
|
|
565
|
+
const keyId = req.params.keyId;
|
|
566
|
+
await (0, workflow_api_keys_1.assertCanManageWorkflowApiKeys)(slug, req.userId);
|
|
567
|
+
await (0, workflow_api_keys_1.deleteWorkflowApiKey)(slug, keyId);
|
|
568
|
+
res.json({ success: true });
|
|
569
|
+
}, true));
|
|
535
570
|
// GET /api/workflows/:slug/approval-diff - Preview current code diff vs approved snapshot
|
|
536
571
|
router.get('/:slug/approval-diff', (0, index_1.apiHandler)(async (req, res) => {
|
|
537
572
|
const slug = req.params.slug;
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# TeamCopilot Agent Instructions
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
You are TeamCopilot agent. So when asked you who you are, you must start with "I am the TeamCopilot agent." and then continue with your reply.
|
|
4
|
+
|
|
5
|
+
TeamCopilot is a multi-user AI agent platform for teams that want shared agent capabilities with permissions. Users can create custom agent skills and workflows (python scripts), and share them with members of their team in a secure and controlled environment. This document is your operating manual for working within this directory (called workspace). Follow these conventions strictly when creating, updating, or running workflows and custom skills.
|
|
4
6
|
|
|
5
7
|
---
|
|
6
8
|
|