teamcopilot 0.1.16 → 0.2.0

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 (54) hide show
  1. package/README.md +88 -9
  2. package/dist/chat/index.js +23 -1
  3. package/dist/frontend/assets/{cssMode-CH26ItO2.js → cssMode-CM1GmZ3H.js} +1 -1
  4. package/dist/frontend/assets/{freemarker2-CiRHXG8W.js → freemarker2-C8TeljYR.js} +1 -1
  5. package/dist/frontend/assets/{handlebars-DXV-JQiR.js → handlebars-B2e-Wzyt.js} +1 -1
  6. package/dist/frontend/assets/{html-DKdYDRJv.js → html-DtBAvTj2.js} +1 -1
  7. package/dist/frontend/assets/{htmlMode-D466XPJJ.js → htmlMode-Dta08RE6.js} +1 -1
  8. package/dist/frontend/assets/index-BirlyHV4.css +1 -0
  9. package/dist/frontend/assets/{index-CvsPLefz.js → index-Dp0jlIX9.js} +201 -201
  10. package/dist/frontend/assets/{javascript-D5lHN8tF.js → javascript-BYeHq-2v.js} +1 -1
  11. package/dist/frontend/assets/{jsonMode-C9Wdxaho.js → jsonMode-DkJo6l8K.js} +1 -1
  12. package/dist/frontend/assets/{liquid-NIH--tpJ.js → liquid-nmEuajdb.js} +1 -1
  13. package/dist/frontend/assets/{mdx-xwEbqXME.js → mdx-BJybRyf3.js} +1 -1
  14. package/dist/frontend/assets/{python-BzErW_b3.js → python-DRAABm9s.js} +1 -1
  15. package/dist/frontend/assets/{razor-B0v-Bw5B.js → razor-7lH4jzk8.js} +1 -1
  16. package/dist/frontend/assets/{tsMode-B9YN5EEb.js → tsMode-ClcmdG3S.js} +1 -1
  17. package/dist/frontend/assets/{typescript-DIMXtHre.js → typescript-D9oav8M6.js} +1 -1
  18. package/dist/frontend/assets/{xml-DQ5HnppJ.js → xml-B0ks0e6Y.js} +1 -1
  19. package/dist/frontend/assets/{yaml-BQCOKj13.js → yaml-CCDt1oK4.js} +1 -1
  20. package/dist/frontend/index.html +2 -2
  21. package/dist/index.js +99 -90
  22. package/dist/secrets/index.js +74 -0
  23. package/dist/skills/index.js +43 -1
  24. package/dist/users/index.js +98 -0
  25. package/dist/utils/redact.js +52 -5
  26. package/dist/utils/resource-file-routes.js +2 -4
  27. package/dist/utils/resource-files.js +10 -2
  28. package/dist/utils/secret-contract-validation.js +184 -0
  29. package/dist/utils/secrets.js +127 -0
  30. package/dist/utils/skill-files.js +7 -0
  31. package/dist/utils/skill.js +50 -1
  32. package/dist/utils/workflow-runner.js +19 -4
  33. package/dist/utils/workflow.js +13 -1
  34. package/dist/workflows/index.js +10 -1
  35. package/dist/workspace_files/.opencode/plugins/createSkill.ts +1 -26
  36. package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +3 -3
  37. package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +93 -5
  38. package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +31 -49
  39. package/dist/workspace_files/.opencode/plugins/listAvailableSecretKeys.ts +107 -0
  40. package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +2 -2
  41. package/dist/workspace_files/.opencode/plugins/secret-proxy.ts +818 -0
  42. package/dist/workspace_files/AGENTS.md +91 -21
  43. package/package.json +5 -3
  44. package/prisma/generated/client/edge.js +24 -3
  45. package/prisma/generated/client/index-browser.js +21 -0
  46. package/prisma/generated/client/index.d.ts +3139 -128
  47. package/prisma/generated/client/index.js +24 -3
  48. package/prisma/generated/client/package.json +1 -1
  49. package/prisma/generated/client/schema.prisma +27 -0
  50. package/prisma/generated/client/wasm.js +24 -3
  51. package/prisma/migrations/20260402060129_add_secret_management/migration.sql +38 -0
  52. package/prisma/migrations/20260404052800_remove_global_secret_user_fkeys/migration.sql +20 -0
  53. package/prisma/schema.prisma +27 -0
  54. package/dist/frontend/assets/index-B8Ip8I8F.css +0 -1
@@ -0,0 +1,184 @@
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.extractReferencedWorkflowSecrets = extractReferencedWorkflowSecrets;
7
+ exports.extractReferencedSkillSecrets = extractReferencedSkillSecrets;
8
+ exports.parseWorkflowRequiredSecrets = parseWorkflowRequiredSecrets;
9
+ exports.parseSkillRequiredSecrets = parseSkillRequiredSecrets;
10
+ exports.validateWorkflowSecretContract = validateWorkflowSecretContract;
11
+ exports.validateSkillSecretContract = validateSkillSecretContract;
12
+ exports.validateWorkflowFilesAtPath = validateWorkflowFilesAtPath;
13
+ const fs_1 = __importDefault(require("fs"));
14
+ const path_1 = __importDefault(require("path"));
15
+ const secrets_1 = require("./secrets");
16
+ const PYTHON_SECRET_PATTERNS = [
17
+ /os\.environ\[\s*["']([A-Za-z_][A-Za-z0-9_]*)["']\s*\]/g,
18
+ /os\.getenv\(\s*["']([A-Za-z_][A-Za-z0-9_]*)["']\s*[\),]/g,
19
+ /environ\.get\(\s*["']([A-Za-z_][A-Za-z0-9_]*)["']\s*[\),]/g,
20
+ ];
21
+ const SKILL_SECRET_PLACEHOLDER_PATTERN = /\{\{SECRET:([A-Z][A-Z0-9_]*)\}\}/g;
22
+ function findDuplicateKeys(rawKeys) {
23
+ const seen = new Set();
24
+ const duplicates = new Set();
25
+ for (const rawKey of rawKeys) {
26
+ if (typeof rawKey !== "string") {
27
+ continue;
28
+ }
29
+ const normalized = rawKey.trim().toUpperCase();
30
+ if (!normalized) {
31
+ continue;
32
+ }
33
+ if (seen.has(normalized)) {
34
+ duplicates.add(normalized);
35
+ continue;
36
+ }
37
+ seen.add(normalized);
38
+ }
39
+ return Array.from(duplicates).sort();
40
+ }
41
+ function assertNoDuplicateSecretKeys(rawKeys, context) {
42
+ const duplicates = findDuplicateKeys(rawKeys);
43
+ if (duplicates.length > 0) {
44
+ throw {
45
+ status: 400,
46
+ message: `${context} contains duplicate required secret keys: ${duplicates.join(", ")}`
47
+ };
48
+ }
49
+ }
50
+ function extractReferencedWorkflowSecrets(runPyContent) {
51
+ const found = new Set();
52
+ for (const pattern of PYTHON_SECRET_PATTERNS) {
53
+ pattern.lastIndex = 0;
54
+ let match;
55
+ while ((match = pattern.exec(runPyContent)) !== null) {
56
+ found.add(match[1].trim().toUpperCase());
57
+ }
58
+ }
59
+ return Array.from(found).sort();
60
+ }
61
+ function extractReferencedSkillSecrets(skillMarkdownContent) {
62
+ const found = new Set();
63
+ let match;
64
+ SKILL_SECRET_PLACEHOLDER_PATTERN.lastIndex = 0;
65
+ while ((match = SKILL_SECRET_PLACEHOLDER_PATTERN.exec(skillMarkdownContent)) !== null) {
66
+ found.add(match[1]);
67
+ }
68
+ return Array.from(found).sort();
69
+ }
70
+ function parseWorkflowRequiredSecrets(workflowJsonContent) {
71
+ let parsed = null;
72
+ try {
73
+ parsed = JSON.parse(workflowJsonContent);
74
+ }
75
+ catch {
76
+ throw {
77
+ status: 400,
78
+ message: "workflow.json must contain valid JSON"
79
+ };
80
+ }
81
+ const rawKeys = Array.isArray(parsed?.required_secrets) ? parsed.required_secrets : [];
82
+ assertNoDuplicateSecretKeys(rawKeys, "workflow.json required_secrets");
83
+ return (0, secrets_1.assertValidSecretKeyList)(rawKeys.filter((item) => typeof item === "string"), "workflow.json required_secrets");
84
+ }
85
+ function extractSkillFrontmatterBlock(skillMarkdownContent) {
86
+ const frontmatterMatch = skillMarkdownContent.match(/^---\s*\n([\s\S]*?)\n---\s*/);
87
+ return frontmatterMatch?.[1] ?? "";
88
+ }
89
+ function extractFrontmatterRequiredSecrets(frontmatter) {
90
+ const lines = frontmatter.split("\n");
91
+ for (let index = 0; index < lines.length; index += 1) {
92
+ const line = lines[index] ?? "";
93
+ const match = line.match(/^required_secrets\s*:\s*(.*)$/);
94
+ if (!match) {
95
+ continue;
96
+ }
97
+ const rawValue = match[1]?.trim() ?? "";
98
+ if (rawValue.startsWith("[")) {
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(rawValue.replace(/'/g, "\""));
102
+ }
103
+ catch {
104
+ throw {
105
+ status: 400,
106
+ message: "SKILL.md frontmatter required_secrets must be a valid string array"
107
+ };
108
+ }
109
+ if (!Array.isArray(parsed) || !parsed.every((item) => typeof item === "string")) {
110
+ throw {
111
+ status: 400,
112
+ message: "SKILL.md frontmatter required_secrets must contain only strings"
113
+ };
114
+ }
115
+ assertNoDuplicateSecretKeys(parsed, "SKILL.md required_secrets");
116
+ return {
117
+ raw: parsed,
118
+ normalized: (0, secrets_1.assertValidSecretKeyList)(parsed, "SKILL.md required_secrets"),
119
+ };
120
+ }
121
+ if (rawValue.length > 0) {
122
+ assertNoDuplicateSecretKeys([rawValue], "SKILL.md required_secrets");
123
+ return {
124
+ raw: [rawValue],
125
+ normalized: (0, secrets_1.assertValidSecretKeyList)([rawValue], "SKILL.md required_secrets"),
126
+ };
127
+ }
128
+ const items = [];
129
+ for (let itemIndex = index + 1; itemIndex < lines.length; itemIndex += 1) {
130
+ const itemLine = lines[itemIndex] ?? "";
131
+ if (itemLine.trim().length === 0) {
132
+ continue;
133
+ }
134
+ const itemMatch = itemLine.match(/^\s*-\s*(.+)\s*$/);
135
+ if (!itemMatch) {
136
+ break;
137
+ }
138
+ items.push(itemMatch[1].replace(/^["']|["']$/g, "").trim());
139
+ }
140
+ assertNoDuplicateSecretKeys(items, "SKILL.md required_secrets");
141
+ return {
142
+ raw: items,
143
+ normalized: (0, secrets_1.assertValidSecretKeyList)(items, "SKILL.md required_secrets"),
144
+ };
145
+ }
146
+ return {
147
+ raw: [],
148
+ normalized: [],
149
+ };
150
+ }
151
+ function parseSkillRequiredSecrets(skillMarkdownContent) {
152
+ const { normalized } = extractFrontmatterRequiredSecrets(extractSkillFrontmatterBlock(skillMarkdownContent));
153
+ return normalized;
154
+ }
155
+ function validateWorkflowSecretContract(contents) {
156
+ const declared = new Set(parseWorkflowRequiredSecrets(contents.workflowJsonContent));
157
+ const referenced = extractReferencedWorkflowSecrets(contents.runPyContent);
158
+ const missing = referenced.filter((key) => !declared.has(key));
159
+ if (missing.length > 0) {
160
+ throw {
161
+ status: 400,
162
+ message: `run.py references secret keys not declared in workflow.json required_secrets: ${missing.join(", ")}`
163
+ };
164
+ }
165
+ }
166
+ function validateSkillSecretContract(skillMarkdownContent) {
167
+ const declared = new Set(parseSkillRequiredSecrets(skillMarkdownContent));
168
+ const referenced = extractReferencedSkillSecrets(skillMarkdownContent);
169
+ const missing = referenced.filter((key) => !declared.has(key));
170
+ if (missing.length > 0) {
171
+ throw {
172
+ status: 400,
173
+ message: `SKILL.md uses secret placeholders not declared in required_secrets: ${missing.join(", ")}`
174
+ };
175
+ }
176
+ }
177
+ function validateWorkflowFilesAtPath(workflowPath) {
178
+ const workflowJsonPath = path_1.default.join(workflowPath, "workflow.json");
179
+ const runPyPath = path_1.default.join(workflowPath, "run.py");
180
+ validateWorkflowSecretContract({
181
+ workflowJsonContent: fs_1.default.readFileSync(workflowJsonPath, "utf-8"),
182
+ runPyContent: fs_1.default.readFileSync(runPyPath, "utf-8"),
183
+ });
184
+ }
@@ -0,0 +1,127 @@
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.assertSecretKey = assertSecretKey;
7
+ exports.normalizeSecretKeyList = normalizeSecretKeyList;
8
+ exports.assertValidSecretKeyList = assertValidSecretKeyList;
9
+ exports.toSecretListItem = toSecretListItem;
10
+ exports.resolveSecretsForUser = resolveSecretsForUser;
11
+ exports.resolveSecretsFromResolvedMap = resolveSecretsFromResolvedMap;
12
+ exports.listResolvedSecretsForUser = listResolvedSecretsForUser;
13
+ const client_1 = __importDefault(require("../prisma/client"));
14
+ const SECRET_KEY_REGEX = /^[A-Z][A-Z0-9_]*$/;
15
+ function assertSecretKey(key) {
16
+ const normalized = key.trim().toUpperCase();
17
+ if (!SECRET_KEY_REGEX.test(normalized)) {
18
+ throw {
19
+ status: 400,
20
+ message: "Secret key must contain only uppercase letters, numbers, and underscores, and must start with a letter"
21
+ };
22
+ }
23
+ return normalized;
24
+ }
25
+ function normalizeSecretKeyList(keys) {
26
+ if (!Array.isArray(keys)) {
27
+ return [];
28
+ }
29
+ const normalized = [];
30
+ const seen = new Set();
31
+ for (const rawKey of keys) {
32
+ if (typeof rawKey !== "string") {
33
+ continue;
34
+ }
35
+ const key = rawKey.trim().toUpperCase();
36
+ if (!SECRET_KEY_REGEX.test(key) || seen.has(key)) {
37
+ continue;
38
+ }
39
+ seen.add(key);
40
+ normalized.push(key);
41
+ }
42
+ return normalized;
43
+ }
44
+ function assertValidSecretKeyList(rawKeys, context) {
45
+ if (!Array.isArray(rawKeys)) {
46
+ return [];
47
+ }
48
+ const invalidKeys = rawKeys
49
+ .filter((rawKey) => typeof rawKey === "string")
50
+ .map((rawKey) => rawKey.trim())
51
+ .filter((key) => key.length > 0 && !SECRET_KEY_REGEX.test(key.toUpperCase()));
52
+ if (invalidKeys.length > 0) {
53
+ throw {
54
+ status: 400,
55
+ message: `${context} contains invalid required secret keys: ${invalidKeys.join(", ")}. Secret keys must start with a letter and contain only uppercase letters, numbers, and underscores. Example: GITHUB_TOKEN`
56
+ };
57
+ }
58
+ return normalizeSecretKeyList(rawKeys);
59
+ }
60
+ function maskSecretValue(value) {
61
+ if (value.length === 0) {
62
+ return "***";
63
+ }
64
+ const suffixLength = value.length <= 4 ? 1 : 4;
65
+ return `***${value.slice(-suffixLength)}`;
66
+ }
67
+ function toSecretListItem(row, maskValueForClient) {
68
+ return {
69
+ key: row.key,
70
+ value: maskValueForClient ? maskSecretValue(row.value) : row.value,
71
+ updated_at: row.updated_at,
72
+ created_at: row.created_at,
73
+ };
74
+ }
75
+ async function resolveSecretsForUser(userId, requiredKeys) {
76
+ const keys = normalizeSecretKeyList(requiredKeys);
77
+ if (keys.length === 0) {
78
+ return {
79
+ secretMap: {},
80
+ missingKeys: [],
81
+ };
82
+ }
83
+ const resolvedSecrets = await listResolvedSecretsForUser(userId);
84
+ return resolveSecretsFromResolvedMap(resolvedSecrets, keys);
85
+ }
86
+ function resolveSecretsFromResolvedMap(resolvedSecrets, requiredKeys) {
87
+ const keys = normalizeSecretKeyList(requiredKeys);
88
+ if (keys.length === 0) {
89
+ return {
90
+ secretMap: {},
91
+ missingKeys: [],
92
+ };
93
+ }
94
+ const secretMap = {};
95
+ const missingKeys = [];
96
+ for (const key of keys) {
97
+ const value = resolvedSecrets[key];
98
+ if (value !== undefined) {
99
+ secretMap[key] = value;
100
+ continue;
101
+ }
102
+ missingKeys.push(key);
103
+ }
104
+ return {
105
+ secretMap,
106
+ missingKeys,
107
+ };
108
+ }
109
+ async function listResolvedSecretsForUser(userId) {
110
+ const [userSecrets, globalSecrets] = await Promise.all([
111
+ client_1.default.user_secrets.findMany({
112
+ where: { user_id: userId },
113
+ orderBy: { key: "asc" }
114
+ }),
115
+ client_1.default.global_secrets.findMany({
116
+ orderBy: { key: "asc" }
117
+ }),
118
+ ]);
119
+ const resolvedSecretMap = {};
120
+ for (const row of globalSecrets) {
121
+ resolvedSecretMap[row.key] = row.value;
122
+ }
123
+ for (const row of userSecrets) {
124
+ resolvedSecretMap[row.key] = row.value;
125
+ }
126
+ return Object.fromEntries(Object.entries(resolvedSecretMap).sort(([left], [right]) => left.localeCompare(right)));
127
+ }
@@ -3,10 +3,17 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.deleteSkillPath = exports.renameSkillPath = exports.uploadSkillFileFromTempPath = exports.createSkillFileOrFolder = exports.saveSkillFileContent = exports.readSkillFileContent = exports.listSkillDirectory = void 0;
4
4
  const resource_files_1 = require("./resource-files");
5
5
  const skill_1 = require("./skill");
6
+ const secret_contract_validation_1 = require("./secret-contract-validation");
6
7
  const skillFileManager = (0, resource_files_1.createResourceFileManager)({
7
8
  getResourcePath: skill_1.getSkillPath,
8
9
  resourceLabel: "skill",
9
10
  editorLabel: "Skill",
11
+ validateBeforeSave: ({ relativePath, nextContent }) => {
12
+ if (relativePath !== "SKILL.md") {
13
+ return;
14
+ }
15
+ (0, secret_contract_validation_1.validateSkillSecretContract)(nextContent);
16
+ }
10
17
  });
11
18
  exports.listSkillDirectory = skillFileManager.listDirectory;
12
19
  exports.readSkillFileContent = skillFileManager.readFileContent;
@@ -14,6 +14,7 @@ const path_1 = __importDefault(require("path"));
14
14
  const client_1 = __importDefault(require("../prisma/client"));
15
15
  const workspace_sync_1 = require("./workspace-sync");
16
16
  const permission_common_1 = require("./permission-common");
17
+ const secret_contract_validation_1 = require("./secret-contract-validation");
17
18
  function getSkillsRootPath() {
18
19
  return path_1.default.join((0, workspace_sync_1.getWorkspaceDirFromEnv)(), ".agents", "skills");
19
20
  }
@@ -113,6 +114,49 @@ function extractFrontmatterValue(frontmatter, key) {
113
114
  }
114
115
  return null;
115
116
  }
117
+ function stripYamlStringQuotes(value) {
118
+ return value.replace(/^["']|["']$/g, "").trim();
119
+ }
120
+ function extractFrontmatterStringArray(frontmatter, key) {
121
+ const lines = frontmatter.split("\n");
122
+ for (let index = 0; index < lines.length; index += 1) {
123
+ const line = lines[index] ?? "";
124
+ const match = line.match(new RegExp(`^${key}\\s*:\\s*(.*)$`));
125
+ if (!match) {
126
+ continue;
127
+ }
128
+ const rawValue = match[1]?.trim() ?? "";
129
+ if (rawValue.startsWith("[")) {
130
+ try {
131
+ const parsed = JSON.parse(rawValue.replace(/'/g, "\""));
132
+ if (Array.isArray(parsed) && parsed.every((item) => typeof item === "string")) {
133
+ return parsed.map((item) => item.trim()).filter(Boolean);
134
+ }
135
+ }
136
+ catch {
137
+ return [];
138
+ }
139
+ return [];
140
+ }
141
+ if (rawValue.length > 0) {
142
+ return [stripYamlStringQuotes(rawValue)].filter(Boolean);
143
+ }
144
+ const items = [];
145
+ for (let itemIndex = index + 1; itemIndex < lines.length; itemIndex += 1) {
146
+ const itemLine = lines[itemIndex] ?? "";
147
+ if (itemLine.trim().length === 0) {
148
+ continue;
149
+ }
150
+ const itemMatch = itemLine.match(/^\s*-\s*(.+)\s*$/);
151
+ if (!itemMatch) {
152
+ break;
153
+ }
154
+ items.push(stripYamlStringQuotes(itemMatch[1]?.trim() ?? ""));
155
+ }
156
+ return items.filter(Boolean);
157
+ }
158
+ return [];
159
+ }
116
160
  function readSkillManifest(slug) {
117
161
  const skillManifestPath = getSkillManifestPath(slug);
118
162
  if (!fs_1.default.existsSync(skillManifestPath)) {
@@ -126,9 +170,13 @@ function readSkillManifest(slug) {
126
170
  const frontmatter = frontmatterMatch?.[1] ?? "";
127
171
  const name = extractFrontmatterValue(frontmatter, "name") ?? slug;
128
172
  const description = extractFrontmatterValue(frontmatter, "description") ?? "";
173
+ // Keep manifest reads lenient so malformed skills still show up in browse/editor flows and can be repaired.
174
+ // Strict secret-contract validation is enforced on save and at runtime via getSkillContent.
175
+ const requiredSecrets = extractFrontmatterStringArray(frontmatter, "required_secrets");
129
176
  return {
130
177
  name,
131
178
  description,
179
+ required_secrets: requiredSecrets,
132
180
  };
133
181
  }
134
182
  function toSkillFrontmatterValue(value) {
@@ -174,7 +222,8 @@ async function createSkill(input) {
174
222
  }
175
223
  fs_1.default.mkdirSync(skillPath, { recursive: false });
176
224
  const skillMdPath = getSkillManifestPath(slug);
177
- const body = `---\nname: ${toSkillFrontmatterValue(name)}\ndescription: ${toSkillFrontmatterValue("")}\n---\n\n# ${name}\n\nDescribe what this skill does.\n\n## Instructions\n\nAdd detailed, actionable instructions for the agent here.\n`;
225
+ const body = `---\nname: ${toSkillFrontmatterValue(name)}\ndescription: ${toSkillFrontmatterValue("")}\nrequired_secrets: []\n---\n\n# ${name}\n\nDescribe what this skill does.\n\n## Instructions\n\nAdd detailed, actionable instructions for the agent here.\n`;
226
+ (0, secret_contract_validation_1.validateSkillSecretContract)(body);
178
227
  fs_1.default.writeFileSync(skillMdPath, body, "utf-8");
179
228
  const now = BigInt(Date.now());
180
229
  await client_1.default.$transaction(async (tx) => {
@@ -44,6 +44,8 @@ const fsp = __importStar(require("fs/promises"));
44
44
  const path = __importStar(require("path"));
45
45
  const client_1 = __importDefault(require("../prisma/client"));
46
46
  const workflow_interruption_1 = require("./workflow-interruption");
47
+ const secrets_1 = require("./secrets");
48
+ const secret_contract_validation_1 = require("./secret-contract-validation");
47
49
  const MAX_OUTPUT_CHARS = 300000;
48
50
  const SLUG_REGEX = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
49
51
  function sanitizeFilenamePart(value) {
@@ -226,7 +228,7 @@ async function requestWorkflowPermission(opencodeSessionId, messageId, callId) {
226
228
  });
227
229
  throw new Error("Permission request timed out");
228
230
  }
229
- function runWithTimeout(workflowPath, args, timeoutSeconds, outputFilePath, shouldAbort) {
231
+ function runWithTimeout(workflowPath, args, timeoutSeconds, outputFilePath, shouldAbort, resolvedSecretMap) {
230
232
  return new Promise((resolve) => {
231
233
  const venvPython = getVenvPythonPath(workflowPath);
232
234
  const runScript = path.join(workflowPath, "run.py");
@@ -256,6 +258,7 @@ function runWithTimeout(workflowPath, args, timeoutSeconds, outputFilePath, shou
256
258
  cwd: workflowPath,
257
259
  env: {
258
260
  ...process.env,
261
+ ...resolvedSecretMap,
259
262
  PYTHONUNBUFFERED: "1",
260
263
  VIRTUAL_ENV: path.join(workflowPath, ".venv"),
261
264
  PATH: [venvBinDir, process.env.PATH ?? ""].filter(Boolean).join(path.delimiter),
@@ -359,18 +362,30 @@ async function startWorkflowRunViaBackend(options) {
359
362
  throw new Error("Invalid workflow path (must be inside workflows/).");
360
363
  }
361
364
  await assertDirectory(workflowPath);
362
- await assertPathExists(path.join(workflowPath, "run.py"));
365
+ const runPyPath = path.join(workflowPath, "run.py");
366
+ await assertPathExists(runPyPath);
363
367
  await assertVenvExists(workflowPath);
364
368
  await assertPathExists(getVenvPythonPath(workflowPath));
365
369
  if (options.requirePermissionPrompt) {
366
370
  await requestWorkflowPermission(options.sessionId, options.messageId, options.callId);
367
371
  }
368
- const workflowJson = JSON.parse(await fsp.readFile(path.join(workflowPath, "workflow.json"), "utf-8"));
372
+ const workflowJsonContent = await fsp.readFile(path.join(workflowPath, "workflow.json"), "utf-8");
373
+ const runPyContent = await fsp.readFile(runPyPath, "utf-8");
374
+ (0, secret_contract_validation_1.validateWorkflowSecretContract)({
375
+ workflowJsonContent,
376
+ runPyContent,
377
+ });
378
+ const workflowJson = JSON.parse(workflowJsonContent);
369
379
  const inputSchema = workflowJson.inputs || {};
380
+ const requiredSecrets = (0, secrets_1.normalizeSecretKeyList)(workflowJson.required_secrets);
370
381
  const validation = validateInputs(options.inputs, inputSchema);
371
382
  if (!validation.valid) {
372
383
  throw new Error(`Input validation failed: ${JSON.stringify(validation.errors)}`);
373
384
  }
385
+ const secretResolution = await (0, secrets_1.resolveSecretsForUser)(options.authUserId, requiredSecrets);
386
+ 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.`);
388
+ }
374
389
  const timeoutSeconds = parseTimeoutSeconds(workflowJson.runtime?.timeout_seconds);
375
390
  if (!timeoutSeconds) {
376
391
  throw new Error(`Could not read runtime.timeout_seconds from workflow.json for '${options.slug}'`);
@@ -394,7 +409,7 @@ async function startWorkflowRunViaBackend(options) {
394
409
  const completion = (async () => {
395
410
  const runResult = await runWithTimeout(workflowPath, cmdArgs, timeoutSeconds, outputFilePath, async () => {
396
411
  return await (0, workflow_interruption_1.isWorkflowSessionInterrupted)(options.sessionId, options.workspaceDir);
397
- });
412
+ }, secretResolution.secretMap);
398
413
  const finalStatus = runResult.status === "success" ? "success" : "failed";
399
414
  await client_1.default.workflow_runs.update({
400
415
  where: { id: createdRun.id },
@@ -15,6 +15,7 @@ const fs_1 = __importDefault(require("fs"));
15
15
  const path_1 = __importDefault(require("path"));
16
16
  const client_1 = __importDefault(require("../prisma/client"));
17
17
  const assert_1 = require("./assert");
18
+ const secret_contract_validation_1 = require("./secret-contract-validation");
18
19
  const workflow_permissions_1 = require("./workflow-permissions");
19
20
  const workspace_sync_1 = require("./workspace-sync");
20
21
  /** Get the absolute path to the workspace directory */
@@ -70,7 +71,18 @@ function readWorkflowManifest(slug) {
70
71
  };
71
72
  }
72
73
  const content = fs_1.default.readFileSync(manifestPath, "utf-8");
73
- return JSON.parse(content);
74
+ let manifest;
75
+ try {
76
+ manifest = JSON.parse(content);
77
+ }
78
+ catch {
79
+ throw {
80
+ status: 400,
81
+ message: `Workflow "${slug}" has an invalid workflow.json file. workflow.json must contain valid JSON.`
82
+ };
83
+ }
84
+ manifest.required_secrets = (0, secret_contract_validation_1.parseWorkflowRequiredSecrets)(content);
85
+ return manifest;
74
86
  }
75
87
  async function readWorkflowManifestAndEnsurePermissions(slug) {
76
88
  const manifest = readWorkflowManifest(slug);
@@ -23,6 +23,8 @@ const workflow_interruption_1 = require("../utils/workflow-interruption");
23
23
  const session_abort_1 = require("../utils/session-abort");
24
24
  const resource_file_routes_1 = require("../utils/resource-file-routes");
25
25
  const resource_access_1 = require("../utils/resource-access");
26
+ const secrets_1 = require("../utils/secrets");
27
+ const secret_contract_validation_1 = require("../utils/secret-contract-validation");
26
28
  const router = express_1.default.Router({ mergeParams: true });
27
29
  const uploadTmpDir = path_1.default.join(os_1.default.tmpdir(), "teamcopilot-workflow-uploads");
28
30
  fs_1.default.mkdirSync(uploadTmpDir, { recursive: true });
@@ -102,6 +104,7 @@ router.get('/', (0, index_1.apiHandler)(async (req, res) => {
102
104
  const creatorIds = new Set();
103
105
  const manifests = new Map();
104
106
  const metadataBySlug = new Map();
107
+ const resolvedSecrets = await (0, secrets_1.listResolvedSecretsForUser)(req.userId);
105
108
  for (const slug of slugs) {
106
109
  const { manifest, metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
107
110
  manifests.set(slug, manifest);
@@ -142,7 +145,9 @@ router.get('/', (0, index_1.apiHandler)(async (req, res) => {
142
145
  can_view: accessSummary.can_view,
143
146
  can_edit: accessSummary.can_edit,
144
147
  permission_mode: accessSummary.permission_mode,
145
- is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users
148
+ is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users,
149
+ required_secrets: manifest.required_secrets ?? [],
150
+ missing_required_secrets: (0, secrets_1.resolveSecretsFromResolvedMap)(resolvedSecrets, manifest.required_secrets ?? []).missingKeys,
146
151
  });
147
152
  }
148
153
  res.json({ workflows });
@@ -495,6 +500,7 @@ router.post('/:slug/creator', (0, index_1.apiHandler)(async (req, res) => {
495
500
  const updatedMetadata = existingCreator
496
501
  ? metadata
497
502
  : await (0, workflow_1.setWorkflowCreator)(slug, req.userId);
503
+ (0, secret_contract_validation_1.validateWorkflowFilesAtPath)(path_1.default.join((0, workspace_sync_1.getWorkspaceDirFromEnv)(), "workflows", slug));
498
504
  await (0, workflow_permissions_1.initializeWorkflowRunPermissionsForCreator)(slug, req.userId);
499
505
  res.json({
500
506
  workflow: {
@@ -566,6 +572,7 @@ router.get('/:slug', (0, index_1.apiHandler)(async (req, res) => {
566
572
  const { manifest, metadata } = await (0, workflow_1.readWorkflowManifestAndEnsurePermissions)(slug);
567
573
  const permission = await (0, workflow_permissions_1.getWorkflowRunPermissionWithUsers)(slug);
568
574
  const accessSummary = await (0, resource_access_1.getResourceAccessSummary)("workflow", slug, req.userId);
575
+ const secretResolution = await (0, secrets_1.resolveSecretsForUser)(req.userId, manifest.required_secrets ?? []);
569
576
  const createdByUserId = metadata.created_by_user_id ?? null;
570
577
  const approvedByUserId = metadata.approved_by_user_id ?? null;
571
578
  const userIds = [createdByUserId, approvedByUserId].filter((id) => typeof id === "string");
@@ -598,6 +605,8 @@ router.get('/:slug', (0, index_1.apiHandler)(async (req, res) => {
598
605
  can_edit: accessSummary.can_edit,
599
606
  permission_mode: accessSummary.permission_mode,
600
607
  is_locked_due_to_missing_users: accessSummary.is_locked_due_to_missing_users,
608
+ required_secrets: manifest.required_secrets ?? [],
609
+ missing_required_secrets: secretResolution.missingKeys,
601
610
  permissions,
602
611
  allowed_users_resolved: permission.allowedUsers.map((row) => ({
603
612
  user_id: row.user.id,
@@ -176,29 +176,6 @@ async function requestCreationPermission(
176
176
  throw new Error("Permission request timed out")
177
177
  }
178
178
 
179
- function stripLeadingFrontmatter(markdown: string): string {
180
- const trimmedStart = markdown.trimStart()
181
- if (!trimmedStart.startsWith("---\n")) {
182
- return markdown.trim()
183
- }
184
-
185
- const frontmatterEnd = trimmedStart.indexOf("\n---\n", 4)
186
- if (frontmatterEnd < 0) {
187
- return markdown.trim()
188
- }
189
-
190
- return trimmedStart.slice(frontmatterEnd + "\n---\n".length).trim()
191
- }
192
-
193
- function buildSkillMarkdown(
194
- slug: string,
195
- description: string,
196
- markdownContent: string
197
- ): string {
198
- const body = stripLeadingFrontmatter(markdownContent)
199
- return `---\nname: ${JSON.stringify(slug)}\ndescription: ${JSON.stringify(description)}\n---\n\n${body}\n`
200
- }
201
-
202
179
  export const CreateSkillPlugin: Plugin = async ({ client }) => {
203
180
  async function resolveRootSessionID(sessionID: string): Promise<string> {
204
181
  let currentSessionID = sessionID
@@ -327,8 +304,6 @@ export const CreateSkillPlugin: Plugin = async ({ client }) => {
327
304
  throw new Error(`SKILL.md for ${slug} is not a text file.`)
328
305
  }
329
306
 
330
- const nextContent = buildSkillMarkdown(slug, description, content)
331
-
332
307
  const saveResponse = await fetch(`${getApiBaseUrl()}/api/skills/${encodeURIComponent(slug)}/files/content`, {
333
308
  method: "PUT",
334
309
  headers: {
@@ -337,7 +312,7 @@ export const CreateSkillPlugin: Plugin = async ({ client }) => {
337
312
  },
338
313
  body: JSON.stringify({
339
314
  path: "SKILL.md",
340
- content: nextContent,
315
+ content,
341
316
  base_etag: filePayload.etag,
342
317
  }),
343
318
  })
@@ -25,6 +25,7 @@ interface WorkflowInput {
25
25
  interface WorkflowManifest {
26
26
  intent_summary: string
27
27
  inputs?: Record<string, WorkflowInput>
28
+ required_secrets?: string[]
28
29
  triggers?: {
29
30
  manual?: boolean
30
31
  }
@@ -230,7 +231,7 @@ export const CreateWorkflowPlugin: Plugin = async ({ client }) => {
230
231
  tool: {
231
232
  createWorkflow: tool({
232
233
  description:
233
- "Create a new workflow with the specified slug and configuration. Creates the workflow folder with all required files (workflow.json, run.py, requirements.txt, etc.). The workflow will need admin approval before it can be executed.",
234
+ "Create a new workflow with the specified slug and configuration. Creates the workflow folder with the required files (workflow.json, run.py, requirements.txt, requirements.lock.txt, README.md). The workflow will need admin approval before it can be executed.",
234
235
  args: {
235
236
  slug: tool.schema
236
237
  .string()
@@ -324,6 +325,7 @@ export const CreateWorkflowPlugin: Plugin = async ({ client }) => {
324
325
  const workflowJson: WorkflowManifest = {
325
326
  intent_summary,
326
327
  inputs: inputs as Record<string, WorkflowInput>,
328
+ required_secrets: [],
327
329
  triggers: { manual: true },
328
330
  runtime: { timeout_seconds },
329
331
  }
@@ -337,8 +339,6 @@ export const CreateWorkflowPlugin: Plugin = async ({ client }) => {
337
339
  await fs.writeFile(path.join(workflowDir, "run.py"), "", "utf-8")
338
340
  await fs.writeFile(path.join(workflowDir, "requirements.txt"), "", "utf-8")
339
341
  await fs.writeFile(path.join(workflowDir, "requirements.lock.txt"), "", "utf-8")
340
- await fs.writeFile(path.join(workflowDir, ".env"), "", "utf-8")
341
- await fs.writeFile(path.join(workflowDir, ".env.example"), "", "utf-8")
342
342
  await fs.writeFile(path.join(workflowDir, "README.md"), "", "utf-8")
343
343
 
344
344
  const creatorResponse = await fetch(