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.
Files changed (43) hide show
  1. package/.env.example +7 -0
  2. package/dist/frontend/assets/{cssMode-CB9iAlw8.js → cssMode-BRVRAYCz.js} +1 -1
  3. package/dist/frontend/assets/{freemarker2-DI2xjASY.js → freemarker2-B5FvHwsO.js} +1 -1
  4. package/dist/frontend/assets/{handlebars-CmcQby9J.js → handlebars-DWX2asql.js} +1 -1
  5. package/dist/frontend/assets/{html-ClP1XQBQ.js → html-BEBxxD9G.js} +1 -1
  6. package/dist/frontend/assets/{htmlMode-DIlZsDL4.js → htmlMode-B2LbPTwC.js} +1 -1
  7. package/dist/frontend/assets/index-D1Hcz_bo.css +1 -0
  8. package/dist/frontend/assets/{index-NFAjAMOV.js → index-D3TE04C5.js} +224 -219
  9. package/dist/frontend/assets/{javascript-CbZA1Ase.js → javascript-Bh4JwoPV.js} +1 -1
  10. package/dist/frontend/assets/{jsonMode-VYZo-ljl.js → jsonMode-7j-aplXT.js} +1 -1
  11. package/dist/frontend/assets/{liquid-I2CXh5hp.js → liquid-BP4OxkO7.js} +1 -1
  12. package/dist/frontend/assets/{mdx-BrO6zg6G.js → mdx-C1OIcGbY.js} +1 -1
  13. package/dist/frontend/assets/{python-S8m0lqgl.js → python-BO8Wy5jz.js} +1 -1
  14. package/dist/frontend/assets/{razor-CV1dlAuV.js → razor-BDtqXvAH.js} +1 -1
  15. package/dist/frontend/assets/{tsMode-mSUzzhBd.js → tsMode-D22HcCuX.js} +1 -1
  16. package/dist/frontend/assets/{typescript-BOhssVGT.js → typescript-CagwEzRw.js} +1 -1
  17. package/dist/frontend/assets/{xml-DF5nbc8Y.js → xml-fE5sGZ5z.js} +1 -1
  18. package/dist/frontend/assets/{yaml-CjFdxQq7.js → yaml-CZMoG4WG.js} +1 -1
  19. package/dist/frontend/index.html +2 -2
  20. package/dist/index.js +2 -0
  21. package/dist/utils/external-host.js +22 -0
  22. package/dist/utils/opencode-auth.js +48 -1
  23. package/dist/utils/secrets.js +15 -0
  24. package/dist/utils/workflow-api-keys.js +88 -0
  25. package/dist/utils/workflow-interruption.js +2 -2
  26. package/dist/utils/workflow-runner.js +11 -2
  27. package/dist/utils/workflow.js +3 -0
  28. package/dist/utils/workspace-sync.js +6 -1
  29. package/dist/workflow-api.js +185 -0
  30. package/dist/workflows/index.js +35 -0
  31. package/dist/workspace_files/AGENTS.md +3 -1
  32. package/package.json +1 -1
  33. package/prisma/generated/client/edge.js +15 -4
  34. package/prisma/generated/client/index-browser.js +12 -1
  35. package/prisma/generated/client/index.d.ts +2002 -270
  36. package/prisma/generated/client/index.js +15 -4
  37. package/prisma/generated/client/package.json +1 -1
  38. package/prisma/generated/client/schema.prisma +28 -12
  39. package/prisma/generated/client/wasm.js +15 -4
  40. package/prisma/migrations/20260430033133_add_workflow_api_keys/migration.sql +46 -0
  41. package/prisma/migrations/20260430161041_remove_run_source_default/migration.sql +28 -0
  42. package/prisma/schema.prisma +18 -2
  43. 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;
@@ -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 isManualSession = sessionId.startsWith("manual-");
12
- if (isManualSession) {
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 secretResolution = await (0, secrets_1.resolveSecretsForUser)(options.authUserId, requiredSecrets);
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
- throw new Error(`Missing required secrets: ${secretResolution.missingKeys.join(", ")}. Add these keys in your profile secrets before running this workflow.`);
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");
@@ -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
- if ((0, assert_1.assertEnv)("OPENCODE_MODEL").startsWith("azure-openai/")) {
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;
@@ -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
- 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.
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
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "teamcopilot",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "A shared AI Agent for Teams",
5
5
  "homepage": "https://teamcopilot.ai",
6
6
  "repository": {