teamcopilot 0.1.15 → 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.
- package/README.md +88 -9
- package/dist/chat/index.js +58 -2
- package/dist/frontend/assets/{cssMode-CCfsnGw-.js → cssMode-CM1GmZ3H.js} +1 -1
- package/dist/frontend/assets/{freemarker2-D5YlwJVL.js → freemarker2-C8TeljYR.js} +1 -1
- package/dist/frontend/assets/{handlebars-9s0i4GaW.js → handlebars-B2e-Wzyt.js} +1 -1
- package/dist/frontend/assets/{html-Cdh0XmJw.js → html-DtBAvTj2.js} +1 -1
- package/dist/frontend/assets/{htmlMode-BSHrN6Bq.js → htmlMode-Dta08RE6.js} +1 -1
- package/dist/frontend/assets/index-BirlyHV4.css +1 -0
- package/dist/frontend/assets/{index-D-2uUwdy.js → index-Dp0jlIX9.js} +201 -201
- package/dist/frontend/assets/{javascript-XHADDYl8.js → javascript-BYeHq-2v.js} +1 -1
- package/dist/frontend/assets/{jsonMode-BH_ZyyEk.js → jsonMode-DkJo6l8K.js} +1 -1
- package/dist/frontend/assets/{liquid-OK5u8pZp.js → liquid-nmEuajdb.js} +1 -1
- package/dist/frontend/assets/{mdx-CHepXUVQ.js → mdx-BJybRyf3.js} +1 -1
- package/dist/frontend/assets/{python-Bci25sLK.js → python-DRAABm9s.js} +1 -1
- package/dist/frontend/assets/{razor-CwhIEHZy.js → razor-7lH4jzk8.js} +1 -1
- package/dist/frontend/assets/{tsMode-D9cC2xst.js → tsMode-ClcmdG3S.js} +1 -1
- package/dist/frontend/assets/{typescript-BIL9nk5m.js → typescript-D9oav8M6.js} +1 -1
- package/dist/frontend/assets/{xml-BkJ5rFpZ.js → xml-B0ks0e6Y.js} +1 -1
- package/dist/frontend/assets/{yaml-DPBtyE47.js → yaml-CCDt1oK4.js} +1 -1
- package/dist/frontend/index.html +2 -2
- package/dist/index.js +99 -90
- package/dist/secrets/index.js +74 -0
- package/dist/skills/index.js +43 -1
- package/dist/users/index.js +98 -0
- package/dist/utils/redact.js +52 -5
- package/dist/utils/resource-file-routes.js +2 -4
- package/dist/utils/resource-files.js +10 -2
- package/dist/utils/secret-contract-validation.js +184 -0
- package/dist/utils/secrets.js +127 -0
- package/dist/utils/skill-files.js +7 -0
- package/dist/utils/skill.js +50 -1
- package/dist/utils/workflow-runner.js +19 -4
- package/dist/utils/workflow.js +13 -1
- package/dist/workflows/index.js +10 -1
- package/dist/workspace_files/.opencode/plugins/createSkill.ts +1 -26
- package/dist/workspace_files/.opencode/plugins/createWorkflow.ts +3 -3
- package/dist/workspace_files/.opencode/plugins/findSimilarWorkflow.ts +93 -5
- package/dist/workspace_files/.opencode/plugins/getSkillContent.ts +31 -49
- package/dist/workspace_files/.opencode/plugins/listAvailableSecretKeys.ts +107 -0
- package/dist/workspace_files/.opencode/plugins/runWorkflow.ts +2 -2
- package/dist/workspace_files/.opencode/plugins/secret-proxy.ts +818 -0
- package/dist/workspace_files/AGENTS.md +91 -21
- package/package.json +5 -3
- package/prisma/generated/client/edge.js +24 -3
- package/prisma/generated/client/index-browser.js +21 -0
- package/prisma/generated/client/index.d.ts +3139 -128
- package/prisma/generated/client/index.js +24 -3
- package/prisma/generated/client/package.json +1 -1
- package/prisma/generated/client/schema.prisma +27 -0
- package/prisma/generated/client/wasm.js +24 -3
- package/prisma/migrations/20260402060129_add_secret_management/migration.sql +38 -0
- package/prisma/migrations/20260404052800_remove_global_secret_user_fkeys/migration.sql +20 -0
- package/prisma/schema.prisma +27 -0
- 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;
|
package/dist/utils/skill.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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 },
|
package/dist/utils/workflow.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/workflows/index.js
CHANGED
|
@@ -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
|
|
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
|
|
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(
|