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.
- package/README.md +88 -9
- package/dist/chat/index.js +23 -1
- package/dist/frontend/assets/{cssMode-CH26ItO2.js → cssMode-CM1GmZ3H.js} +1 -1
- package/dist/frontend/assets/{freemarker2-CiRHXG8W.js → freemarker2-C8TeljYR.js} +1 -1
- package/dist/frontend/assets/{handlebars-DXV-JQiR.js → handlebars-B2e-Wzyt.js} +1 -1
- package/dist/frontend/assets/{html-DKdYDRJv.js → html-DtBAvTj2.js} +1 -1
- package/dist/frontend/assets/{htmlMode-D466XPJJ.js → htmlMode-Dta08RE6.js} +1 -1
- package/dist/frontend/assets/index-BirlyHV4.css +1 -0
- package/dist/frontend/assets/{index-CvsPLefz.js → index-Dp0jlIX9.js} +201 -201
- package/dist/frontend/assets/{javascript-D5lHN8tF.js → javascript-BYeHq-2v.js} +1 -1
- package/dist/frontend/assets/{jsonMode-C9Wdxaho.js → jsonMode-DkJo6l8K.js} +1 -1
- package/dist/frontend/assets/{liquid-NIH--tpJ.js → liquid-nmEuajdb.js} +1 -1
- package/dist/frontend/assets/{mdx-xwEbqXME.js → mdx-BJybRyf3.js} +1 -1
- package/dist/frontend/assets/{python-BzErW_b3.js → python-DRAABm9s.js} +1 -1
- package/dist/frontend/assets/{razor-B0v-Bw5B.js → razor-7lH4jzk8.js} +1 -1
- package/dist/frontend/assets/{tsMode-B9YN5EEb.js → tsMode-ClcmdG3S.js} +1 -1
- package/dist/frontend/assets/{typescript-DIMXtHre.js → typescript-D9oav8M6.js} +1 -1
- package/dist/frontend/assets/{xml-DQ5HnppJ.js → xml-B0ks0e6Y.js} +1 -1
- package/dist/frontend/assets/{yaml-BQCOKj13.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,818 @@
|
|
|
1
|
+
import type { Plugin } from "@opencode-ai/plugin"
|
|
2
|
+
|
|
3
|
+
function getApiBaseUrl(): string {
|
|
4
|
+
const port = process.env.TEAMCOPILOT_PORT?.trim()
|
|
5
|
+
if (!port) {
|
|
6
|
+
throw new Error("TEAMCOPILOT_PORT must be set.")
|
|
7
|
+
}
|
|
8
|
+
return `http://localhost:${port}`
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface SessionLookupResponse {
|
|
12
|
+
error?: unknown
|
|
13
|
+
data?: {
|
|
14
|
+
id?: string
|
|
15
|
+
parentID?: string
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
type SecretMapResolutionResponse = {
|
|
20
|
+
secret_map?: Record<string, string>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const SECRET_PLACEHOLDER_PATTERN = /\{\{SECRET:([A-Za-z_][A-Za-z0-9_]*)\}\}/g
|
|
24
|
+
const SECRET_ENV_REFERENCE_PATTERN = /\$\{__TEAMCOPILOT_RUNTIME_SECRET_([A-Z][A-Z0-9_]*)\}/g
|
|
25
|
+
const AGENT_VISIBLE_SECRET_ENV_REFERENCE_PATTERN = /__TEAMCOPILOT_RUNTIME_SECRET_[A-Z][A-Z0-9_]*/
|
|
26
|
+
const SECRET_ENV_PREFIX = "__TEAMCOPILOT_RUNTIME_SECRET_"
|
|
27
|
+
const SHELL_CONTROL_TOKENS = new Set(["&&", "||", ";", "|"])
|
|
28
|
+
const CURL_SAFE_VALUE_OPTIONS = new Set([
|
|
29
|
+
"-H",
|
|
30
|
+
"--header",
|
|
31
|
+
"-d",
|
|
32
|
+
"--data",
|
|
33
|
+
"--data-raw",
|
|
34
|
+
"--data-binary",
|
|
35
|
+
"--data-ascii",
|
|
36
|
+
"--data-urlencode",
|
|
37
|
+
"-u",
|
|
38
|
+
"--user",
|
|
39
|
+
"-A",
|
|
40
|
+
"--user-agent",
|
|
41
|
+
"-e",
|
|
42
|
+
"--referer",
|
|
43
|
+
"-b",
|
|
44
|
+
"--cookie",
|
|
45
|
+
"-F",
|
|
46
|
+
"--form",
|
|
47
|
+
"--form-string",
|
|
48
|
+
"--url",
|
|
49
|
+
"-x",
|
|
50
|
+
"--proxy",
|
|
51
|
+
"-U",
|
|
52
|
+
"--proxy-user",
|
|
53
|
+
"--oauth2-bearer",
|
|
54
|
+
"--request-target",
|
|
55
|
+
"--resolve",
|
|
56
|
+
"--connect-to",
|
|
57
|
+
])
|
|
58
|
+
const CURL_UNSAFE_VALUE_OPTIONS = new Set([
|
|
59
|
+
"-o",
|
|
60
|
+
"--output",
|
|
61
|
+
"-O",
|
|
62
|
+
"--remote-name",
|
|
63
|
+
"--output-dir",
|
|
64
|
+
"-D",
|
|
65
|
+
"--dump-header",
|
|
66
|
+
"-K",
|
|
67
|
+
"--config",
|
|
68
|
+
"-w",
|
|
69
|
+
"--write-out",
|
|
70
|
+
"-E",
|
|
71
|
+
"--cert",
|
|
72
|
+
"--key",
|
|
73
|
+
"--proxy-key",
|
|
74
|
+
"--stderr",
|
|
75
|
+
"--trace",
|
|
76
|
+
"--trace-ascii",
|
|
77
|
+
"--trace-config",
|
|
78
|
+
])
|
|
79
|
+
const CURL_ALLOWED_HEADER_NAMES = new Set([
|
|
80
|
+
"authorization",
|
|
81
|
+
"proxy-authorization",
|
|
82
|
+
"x-api-key",
|
|
83
|
+
"api-key",
|
|
84
|
+
"x-auth-token",
|
|
85
|
+
"x-access-token",
|
|
86
|
+
"x-authorization",
|
|
87
|
+
"x-token",
|
|
88
|
+
"x-session-token",
|
|
89
|
+
"x-authentication-token",
|
|
90
|
+
"authentication",
|
|
91
|
+
"x-csrf-token",
|
|
92
|
+
"x-xsrf-token",
|
|
93
|
+
"csrf-token",
|
|
94
|
+
"xsrf-token",
|
|
95
|
+
"cookie",
|
|
96
|
+
"set-cookie",
|
|
97
|
+
"www-authenticate",
|
|
98
|
+
"x-amz-security-token",
|
|
99
|
+
"x-amz-content-sha256",
|
|
100
|
+
"x-goog-api-key",
|
|
101
|
+
"x-goog-authuser",
|
|
102
|
+
"x-ms-client-principal",
|
|
103
|
+
"x-ms-token-aad-access-token",
|
|
104
|
+
"cf-access-jwt-assertion",
|
|
105
|
+
"x-parse-rest-api-key",
|
|
106
|
+
"x-parse-master-key",
|
|
107
|
+
"x-hasura-admin-secret",
|
|
108
|
+
"x-hasura-access-key",
|
|
109
|
+
"x-supabase-api-key",
|
|
110
|
+
"x-supabase-auth",
|
|
111
|
+
"x-notion-secret",
|
|
112
|
+
"x-appwrite-project",
|
|
113
|
+
"x-appwrite-key",
|
|
114
|
+
"x-elastic-api-key",
|
|
115
|
+
"private-token",
|
|
116
|
+
"job-token",
|
|
117
|
+
"circle-token",
|
|
118
|
+
"x-circleci-token",
|
|
119
|
+
"x-airtable-api-key",
|
|
120
|
+
])
|
|
121
|
+
|
|
122
|
+
function readSessionLookupErrorMessage(error: unknown, fallbackMessage: string): string {
|
|
123
|
+
if (typeof error === "string" && error.trim().length > 0) {
|
|
124
|
+
return error
|
|
125
|
+
}
|
|
126
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
127
|
+
const message = (error as { message?: unknown }).message
|
|
128
|
+
if (typeof message === "string" && message.trim().length > 0) {
|
|
129
|
+
return message
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return fallbackMessage
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async function readErrorMessageFromResponse(
|
|
136
|
+
response: Response,
|
|
137
|
+
fallbackMessage: string
|
|
138
|
+
): Promise<string> {
|
|
139
|
+
try {
|
|
140
|
+
const text = await response.text()
|
|
141
|
+
if (!text) return fallbackMessage
|
|
142
|
+
try {
|
|
143
|
+
const parsed: unknown = JSON.parse(text)
|
|
144
|
+
if (parsed && typeof parsed === "object" && "message" in parsed) {
|
|
145
|
+
const msg = (parsed as { message?: unknown }).message
|
|
146
|
+
if (typeof msg === "string" && msg.trim().length > 0) return msg
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// fall back to plain text
|
|
150
|
+
}
|
|
151
|
+
return text.trim().length > 0 ? text : fallbackMessage
|
|
152
|
+
} catch {
|
|
153
|
+
return fallbackMessage
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type CommandToken = {
|
|
158
|
+
raw: string
|
|
159
|
+
start: number
|
|
160
|
+
end: number
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function tokenizeCommand(command: string): CommandToken[] {
|
|
164
|
+
const tokens: CommandToken[] = []
|
|
165
|
+
const length = command.length
|
|
166
|
+
let index = 0
|
|
167
|
+
|
|
168
|
+
while (index < length) {
|
|
169
|
+
const char = command[index]
|
|
170
|
+
if (char === undefined) {
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (/\s/.test(char)) {
|
|
175
|
+
index += 1
|
|
176
|
+
continue
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const start = index
|
|
180
|
+
if (char === "&" && command[index + 1] === "&") {
|
|
181
|
+
index += 2
|
|
182
|
+
tokens.push({ raw: command.slice(start, index), start, end: index })
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (char === "|" && command[index + 1] === "|") {
|
|
187
|
+
index += 2
|
|
188
|
+
tokens.push({ raw: command.slice(start, index), start, end: index })
|
|
189
|
+
continue
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (char === ";" || char === "|") {
|
|
193
|
+
index += 1
|
|
194
|
+
tokens.push({ raw: command.slice(start, index), start, end: index })
|
|
195
|
+
continue
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
while (index < length) {
|
|
199
|
+
const current = command[index]
|
|
200
|
+
if (current === undefined) {
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (current === "'") {
|
|
205
|
+
index += 1
|
|
206
|
+
while (index < length && command[index] !== "'") {
|
|
207
|
+
index += 1
|
|
208
|
+
}
|
|
209
|
+
if (index < length) {
|
|
210
|
+
index += 1
|
|
211
|
+
}
|
|
212
|
+
continue
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (current === "\"") {
|
|
216
|
+
index += 1
|
|
217
|
+
while (index < length) {
|
|
218
|
+
const quoted = command[index]
|
|
219
|
+
if (quoted === undefined) {
|
|
220
|
+
break
|
|
221
|
+
}
|
|
222
|
+
if (quoted === "\\") {
|
|
223
|
+
index += Math.min(2, length - index)
|
|
224
|
+
continue
|
|
225
|
+
}
|
|
226
|
+
index += 1
|
|
227
|
+
if (quoted === "\"") {
|
|
228
|
+
break
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
continue
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (/\s/.test(current)) {
|
|
235
|
+
break
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (current === "&" && command[index + 1] === "&") {
|
|
239
|
+
break
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (current === "|" || current === ";") {
|
|
243
|
+
break
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
index += 1
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
tokens.push({ raw: command.slice(start, index), start, end: index })
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return tokens
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function unwrapToken(rawToken: string): { quote: '"' | "'" | null; inner: string } {
|
|
256
|
+
if (rawToken.length >= 2) {
|
|
257
|
+
const first = rawToken[0]
|
|
258
|
+
const last = rawToken[rawToken.length - 1]
|
|
259
|
+
if ((first === "\"" || first === "'") && first === last) {
|
|
260
|
+
return {
|
|
261
|
+
quote: first,
|
|
262
|
+
inner: rawToken.slice(1, -1),
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
return {
|
|
268
|
+
quote: null,
|
|
269
|
+
inner: rawToken,
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function wrapToken(quote: '"' | "'" | null, inner: string): string {
|
|
274
|
+
return quote ? `${quote}${inner}${quote}` : inner
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function escapeForDoubleQuotedShell(inner: string): string {
|
|
278
|
+
return inner.replace(/[\\`"]/g, "\\$&")
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
function wrapTokenForShellExpansion(
|
|
282
|
+
originalQuote: '"' | "'" | null,
|
|
283
|
+
inner: string,
|
|
284
|
+
substituted: boolean
|
|
285
|
+
): string {
|
|
286
|
+
if (!substituted) {
|
|
287
|
+
return wrapToken(originalQuote, inner)
|
|
288
|
+
}
|
|
289
|
+
if (originalQuote === "'") {
|
|
290
|
+
return `"${escapeForDoubleQuotedShell(inner)}"`
|
|
291
|
+
}
|
|
292
|
+
return wrapToken(originalQuote, inner)
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
function normalizeSecretKey(rawKey: string): string {
|
|
296
|
+
return rawKey.trim().toUpperCase()
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function toSecretEnvReference(key: string): string {
|
|
300
|
+
return `\${${SECRET_ENV_PREFIX}${key}}`
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function replacePlaceholdersWithEnvRefs(value: string): { rewritten: string; referencedKeys: string[] } {
|
|
304
|
+
const referencedKeys: string[] = []
|
|
305
|
+
const seen = new Set<string>()
|
|
306
|
+
const rewritten = value.replace(SECRET_PLACEHOLDER_PATTERN, (_match, rawKey: string) => {
|
|
307
|
+
const key = normalizeSecretKey(rawKey)
|
|
308
|
+
if (!seen.has(key)) {
|
|
309
|
+
seen.add(key)
|
|
310
|
+
referencedKeys.push(key)
|
|
311
|
+
}
|
|
312
|
+
return toSecretEnvReference(key)
|
|
313
|
+
})
|
|
314
|
+
|
|
315
|
+
return {
|
|
316
|
+
rewritten,
|
|
317
|
+
referencedKeys,
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function isCurlExecutableToken(rawToken: string): boolean {
|
|
322
|
+
const { inner } = unwrapToken(rawToken)
|
|
323
|
+
const base = inner.split("/").pop() ?? inner
|
|
324
|
+
return base === "curl"
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function getLongOptionName(inner: string): string | null {
|
|
328
|
+
const eqIndex = inner.indexOf("=")
|
|
329
|
+
const optionName = eqIndex === -1 ? inner : inner.slice(0, eqIndex)
|
|
330
|
+
return optionName.startsWith("--") ? optionName : null
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function isSafeCurlOption(inner: string): boolean {
|
|
334
|
+
const optionName = getLongOptionName(inner) ?? inner
|
|
335
|
+
return CURL_SAFE_VALUE_OPTIONS.has(optionName)
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function isUnsafeCurlOption(inner: string): boolean {
|
|
339
|
+
const optionName = getLongOptionName(inner) ?? inner
|
|
340
|
+
return CURL_UNSAFE_VALUE_OPTIONS.has(optionName)
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function normalizeHeaderName(headerName: string): string {
|
|
344
|
+
return headerName.trim().toLowerCase()
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function isAllowedCurlHeaderValue(value: string): boolean {
|
|
348
|
+
const colonIndex = value.indexOf(":")
|
|
349
|
+
if (colonIndex === -1) {
|
|
350
|
+
return false
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const headerName = normalizeHeaderName(value.slice(0, colonIndex))
|
|
354
|
+
return CURL_ALLOWED_HEADER_NAMES.has(headerName)
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
358
|
+
return !!value && typeof value === "object" && !Array.isArray(value)
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function collectReferencedEnvKeys(value: unknown, found: Set<string>): void {
|
|
362
|
+
if (typeof value === "string") {
|
|
363
|
+
let match: RegExpExecArray | null
|
|
364
|
+
SECRET_ENV_REFERENCE_PATTERN.lastIndex = 0
|
|
365
|
+
while ((match = SECRET_ENV_REFERENCE_PATTERN.exec(value)) !== null) {
|
|
366
|
+
found.add(match[1]!)
|
|
367
|
+
}
|
|
368
|
+
return
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
if (Array.isArray(value)) {
|
|
372
|
+
for (const item of value) {
|
|
373
|
+
collectReferencedEnvKeys(item, found)
|
|
374
|
+
}
|
|
375
|
+
return
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
if (!isPlainObject(value)) {
|
|
379
|
+
return
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
for (const nestedValue of Object.values(value)) {
|
|
383
|
+
collectReferencedEnvKeys(nestedValue, found)
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
function assertNoAgentAuthoredSecretEnvReference(value: unknown): void {
|
|
388
|
+
if (typeof value === "string") {
|
|
389
|
+
if (AGENT_VISIBLE_SECRET_ENV_REFERENCE_PATTERN.test(value)) {
|
|
390
|
+
throw new Error(
|
|
391
|
+
"Agent-authored __TEAMCOPILOT_RUNTIME_SECRET_* references are not allowed. Use {{SECRET:KEY}} placeholders instead."
|
|
392
|
+
)
|
|
393
|
+
}
|
|
394
|
+
return
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
if (Array.isArray(value)) {
|
|
398
|
+
for (const item of value) {
|
|
399
|
+
assertNoAgentAuthoredSecretEnvReference(item)
|
|
400
|
+
}
|
|
401
|
+
return
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (!isPlainObject(value)) {
|
|
405
|
+
return
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
for (const nestedValue of Object.values(value)) {
|
|
409
|
+
assertNoAgentAuthoredSecretEnvReference(nestedValue)
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
export const SecretProxyPlugin: Plugin = async ({ client }) => {
|
|
414
|
+
async function resolveRootSessionID(sessionID: string): Promise<string> {
|
|
415
|
+
let currentSessionID = sessionID
|
|
416
|
+
|
|
417
|
+
while (true) {
|
|
418
|
+
const response = (await client.session.get({
|
|
419
|
+
path: {
|
|
420
|
+
id: currentSessionID,
|
|
421
|
+
},
|
|
422
|
+
})) as SessionLookupResponse
|
|
423
|
+
if (response.error) {
|
|
424
|
+
throw new Error(
|
|
425
|
+
readSessionLookupErrorMessage(
|
|
426
|
+
response.error,
|
|
427
|
+
`Failed to resolve root session for ${currentSessionID}`
|
|
428
|
+
)
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const parentID = response.data?.parentID
|
|
433
|
+
if (!parentID) {
|
|
434
|
+
return currentSessionID
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
currentSessionID = parentID
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
async function resolveSecretMapForKeys(
|
|
442
|
+
sessionID: string,
|
|
443
|
+
keys: string[]
|
|
444
|
+
): Promise<Record<string, string>> {
|
|
445
|
+
const rootSessionID = await resolveRootSessionID(sessionID)
|
|
446
|
+
const response = await fetch(`${getApiBaseUrl()}/api/users/me/resolve-secrets`, {
|
|
447
|
+
method: "POST",
|
|
448
|
+
headers: {
|
|
449
|
+
"Content-Type": "application/json",
|
|
450
|
+
Authorization: `Bearer ${rootSessionID}`,
|
|
451
|
+
},
|
|
452
|
+
body: JSON.stringify({
|
|
453
|
+
keys,
|
|
454
|
+
}),
|
|
455
|
+
})
|
|
456
|
+
|
|
457
|
+
if (!response.ok) {
|
|
458
|
+
const errorMessage = await readErrorMessageFromResponse(
|
|
459
|
+
response,
|
|
460
|
+
`Failed to resolve secret values for bash command (HTTP ${response.status})`
|
|
461
|
+
)
|
|
462
|
+
throw new Error(errorMessage)
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const payload = (await response.json()) as SecretMapResolutionResponse
|
|
466
|
+
return payload.secret_map ?? {}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
async function rewriteStringFieldsInPlace(
|
|
470
|
+
value: unknown,
|
|
471
|
+
cache: Map<string, { rewritten: string; referencedKeys: string[] }>,
|
|
472
|
+
): Promise<void> {
|
|
473
|
+
if (typeof value === "string") {
|
|
474
|
+
return
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (Array.isArray(value)) {
|
|
478
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
479
|
+
const item = value[index]
|
|
480
|
+
if (typeof item === "string") {
|
|
481
|
+
const rewritten = await maybeRewriteSupportedString(item, cache)
|
|
482
|
+
value[index] = rewritten.rewritten
|
|
483
|
+
continue
|
|
484
|
+
}
|
|
485
|
+
await rewriteStringFieldsInPlace(item, cache)
|
|
486
|
+
}
|
|
487
|
+
return
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
if (!isPlainObject(value)) {
|
|
491
|
+
return
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
for (const [key, nestedValue] of Object.entries(value)) {
|
|
495
|
+
if (typeof nestedValue === "string") {
|
|
496
|
+
const rewritten = await maybeRewriteSupportedString(nestedValue, cache)
|
|
497
|
+
value[key] = rewritten.rewritten
|
|
498
|
+
continue
|
|
499
|
+
}
|
|
500
|
+
await rewriteStringFieldsInPlace(nestedValue, cache)
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
async function maybeRewriteSupportedString(
|
|
505
|
+
text: string,
|
|
506
|
+
cache: Map<string, { rewritten: string; referencedKeys: string[] }>,
|
|
507
|
+
): Promise<{ rewritten: string; referencedKeys: string[] }> {
|
|
508
|
+
if (!text.includes("{{SECRET:")) {
|
|
509
|
+
return {
|
|
510
|
+
rewritten: text,
|
|
511
|
+
referencedKeys: [],
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const cached = cache.get(text)
|
|
516
|
+
if (cached !== undefined) {
|
|
517
|
+
return cached
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
const rewritten = substitutePlaceholdersInCurlShellString(text)
|
|
521
|
+
cache.set(text, rewritten)
|
|
522
|
+
return rewritten
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function rewriteTokenInner(inner: string): { rewrittenInner: string; referencedKeys: string[] } {
|
|
526
|
+
return replacePlaceholdersWithEnvRefs(inner)
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
function rewriteRawToken(rawToken: string): { rewritten: string; referencedKeys: string[] } {
|
|
530
|
+
const { quote, inner } = unwrapToken(rawToken)
|
|
531
|
+
const { rewritten, referencedKeys } = rewriteTokenInner(inner)
|
|
532
|
+
return {
|
|
533
|
+
rewritten: wrapTokenForShellExpansion(quote, rewritten, referencedKeys.length > 0),
|
|
534
|
+
referencedKeys,
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function rewriteRawTokenValuePortion(rawToken: string): { rewritten: string; referencedKeys: string[] } {
|
|
539
|
+
const { quote, inner } = unwrapToken(rawToken)
|
|
540
|
+
const eqIndex = inner.indexOf("=")
|
|
541
|
+
if (eqIndex === -1) {
|
|
542
|
+
return {
|
|
543
|
+
rewritten: rawToken,
|
|
544
|
+
referencedKeys: [],
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
const prefix = inner.slice(0, eqIndex + 1)
|
|
549
|
+
const value = inner.slice(eqIndex + 1)
|
|
550
|
+
const { rewritten, referencedKeys } = rewriteTokenInner(value)
|
|
551
|
+
return {
|
|
552
|
+
rewritten: wrapTokenForShellExpansion(quote, `${prefix}${rewritten}`, referencedKeys.length > 0),
|
|
553
|
+
referencedKeys,
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function rewriteHeaderTokenIfAllowed(rawToken: string): { rewritten: string; referencedKeys: string[] } {
|
|
558
|
+
const { quote, inner } = unwrapToken(rawToken)
|
|
559
|
+
if (!isAllowedCurlHeaderValue(inner)) {
|
|
560
|
+
return {
|
|
561
|
+
rewritten: rawToken,
|
|
562
|
+
referencedKeys: [],
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const { rewritten, referencedKeys } = rewriteTokenInner(inner)
|
|
567
|
+
return {
|
|
568
|
+
rewritten: wrapTokenForShellExpansion(quote, rewritten, referencedKeys.length > 0),
|
|
569
|
+
referencedKeys,
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function rewriteInlineHeaderValueIfAllowed(rawToken: string): { rewritten: string; referencedKeys: string[] } {
|
|
574
|
+
const { quote, inner } = unwrapToken(rawToken)
|
|
575
|
+
const eqIndex = inner.indexOf("=")
|
|
576
|
+
if (eqIndex === -1) {
|
|
577
|
+
return {
|
|
578
|
+
rewritten: rawToken,
|
|
579
|
+
referencedKeys: [],
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
const prefix = inner.slice(0, eqIndex + 1)
|
|
584
|
+
const value = inner.slice(eqIndex + 1)
|
|
585
|
+
if (!isAllowedCurlHeaderValue(value)) {
|
|
586
|
+
return {
|
|
587
|
+
rewritten: rawToken,
|
|
588
|
+
referencedKeys: [],
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const { rewritten, referencedKeys } = rewriteTokenInner(value)
|
|
593
|
+
return {
|
|
594
|
+
rewritten: wrapTokenForShellExpansion(quote, `${prefix}${rewritten}`, referencedKeys.length > 0),
|
|
595
|
+
referencedKeys,
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
function substitutePlaceholdersInCurlShellString(
|
|
600
|
+
text: string,
|
|
601
|
+
): { rewritten: string; referencedKeys: string[] } {
|
|
602
|
+
const rawTokens = tokenizeCommand(text)
|
|
603
|
+
if (rawTokens.length === 0) {
|
|
604
|
+
return {
|
|
605
|
+
rewritten: text,
|
|
606
|
+
referencedKeys: [],
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
const replacements: Array<{ start: number; end: number; text: string }> = []
|
|
611
|
+
const referencedKeys = new Set<string>()
|
|
612
|
+
let mutated = false
|
|
613
|
+
let atCommandStart = true
|
|
614
|
+
|
|
615
|
+
for (let index = 0; index < rawTokens.length; index += 1) {
|
|
616
|
+
const rawToken = rawTokens[index]
|
|
617
|
+
|
|
618
|
+
if (SHELL_CONTROL_TOKENS.has(rawToken.raw)) {
|
|
619
|
+
atCommandStart = true
|
|
620
|
+
continue
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
if (!atCommandStart) {
|
|
624
|
+
continue
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
if (!isCurlExecutableToken(rawToken.raw)) {
|
|
628
|
+
atCommandStart = false
|
|
629
|
+
continue
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
atCommandStart = false
|
|
633
|
+
let expectedValueKind: "safe" | "unsafe" | "header" | null = null
|
|
634
|
+
|
|
635
|
+
for (let j = index + 1; j < rawTokens.length; j += 1) {
|
|
636
|
+
const segmentToken = rawTokens[j]
|
|
637
|
+
if (SHELL_CONTROL_TOKENS.has(segmentToken.raw)) {
|
|
638
|
+
atCommandStart = true
|
|
639
|
+
index = j - 1
|
|
640
|
+
break
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
const { inner } = unwrapToken(segmentToken.raw)
|
|
644
|
+
|
|
645
|
+
if (expectedValueKind !== null) {
|
|
646
|
+
const rewritten = expectedValueKind === "safe"
|
|
647
|
+
? rewriteRawToken(segmentToken.raw)
|
|
648
|
+
: expectedValueKind === "header"
|
|
649
|
+
? rewriteHeaderTokenIfAllowed(segmentToken.raw)
|
|
650
|
+
: { rewritten: segmentToken.raw, referencedKeys: [] }
|
|
651
|
+
if (rewritten.rewritten !== segmentToken.raw) {
|
|
652
|
+
replacements.push({
|
|
653
|
+
start: segmentToken.start,
|
|
654
|
+
end: segmentToken.end,
|
|
655
|
+
text: rewritten.rewritten,
|
|
656
|
+
})
|
|
657
|
+
mutated = true
|
|
658
|
+
}
|
|
659
|
+
for (const key of rewritten.referencedKeys) {
|
|
660
|
+
referencedKeys.add(key)
|
|
661
|
+
}
|
|
662
|
+
expectedValueKind = null
|
|
663
|
+
continue
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
if (inner === "--") {
|
|
667
|
+
continue
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
if (inner.startsWith("-")) {
|
|
671
|
+
if (isUnsafeCurlOption(inner)) {
|
|
672
|
+
expectedValueKind = !inner.includes("=") ? "unsafe" : null
|
|
673
|
+
continue
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (isSafeCurlOption(inner)) {
|
|
677
|
+
const optionName = getLongOptionName(inner) ?? inner
|
|
678
|
+
if (inner.includes("=")) {
|
|
679
|
+
const rewritten = optionName === "--header"
|
|
680
|
+
? rewriteInlineHeaderValueIfAllowed(segmentToken.raw)
|
|
681
|
+
: rewriteRawTokenValuePortion(segmentToken.raw)
|
|
682
|
+
if (rewritten.rewritten !== segmentToken.raw) {
|
|
683
|
+
replacements.push({
|
|
684
|
+
start: segmentToken.start,
|
|
685
|
+
end: segmentToken.end,
|
|
686
|
+
text: rewritten.rewritten,
|
|
687
|
+
})
|
|
688
|
+
mutated = true
|
|
689
|
+
}
|
|
690
|
+
for (const key of rewritten.referencedKeys) {
|
|
691
|
+
referencedKeys.add(key)
|
|
692
|
+
}
|
|
693
|
+
} else {
|
|
694
|
+
expectedValueKind = optionName === "-H" || optionName === "--header" ? "header" : "safe"
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
continue
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
const rewritten = rewriteRawToken(segmentToken.raw)
|
|
701
|
+
if (rewritten.rewritten !== segmentToken.raw) {
|
|
702
|
+
replacements.push({
|
|
703
|
+
start: segmentToken.start,
|
|
704
|
+
end: segmentToken.end,
|
|
705
|
+
text: rewritten.rewritten,
|
|
706
|
+
})
|
|
707
|
+
mutated = true
|
|
708
|
+
}
|
|
709
|
+
for (const key of rewritten.referencedKeys) {
|
|
710
|
+
referencedKeys.add(key)
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
let rewrittenText = text
|
|
716
|
+
if (mutated) {
|
|
717
|
+
const output: string[] = []
|
|
718
|
+
let cursor = 0
|
|
719
|
+
for (const replacement of replacements.sort((left, right) => left.start - right.start)) {
|
|
720
|
+
output.push(text.slice(cursor, replacement.start))
|
|
721
|
+
output.push(replacement.text)
|
|
722
|
+
cursor = replacement.end
|
|
723
|
+
}
|
|
724
|
+
output.push(text.slice(cursor))
|
|
725
|
+
rewrittenText = output.join("")
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
return {
|
|
729
|
+
rewritten: rewrittenText,
|
|
730
|
+
referencedKeys: Array.from(referencedKeys).sort(),
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
return {
|
|
735
|
+
"command.execute.before": async (input) => {
|
|
736
|
+
const sessionID = typeof input.sessionID === "string" ? input.sessionID.trim() : ""
|
|
737
|
+
if (!sessionID) {
|
|
738
|
+
return
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
assertNoAgentAuthoredSecretEnvReference(input.command)
|
|
742
|
+
assertNoAgentAuthoredSecretEnvReference(input.arguments)
|
|
743
|
+
|
|
744
|
+
const commandCache = new Map<string, { rewritten: string; referencedKeys: string[] }>()
|
|
745
|
+
if (typeof input.command === "string" && input.command.includes("{{SECRET:")) {
|
|
746
|
+
if (isCurlExecutableToken(input.command) && typeof input.arguments === "string") {
|
|
747
|
+
const fullCurlCommand = input.arguments.trim().length > 0
|
|
748
|
+
? `${input.command} ${input.arguments}`
|
|
749
|
+
: input.command
|
|
750
|
+
const rewritten = substitutePlaceholdersInCurlShellString(fullCurlCommand)
|
|
751
|
+
const prefix = `${input.command} `
|
|
752
|
+
if (rewritten.rewritten.startsWith(prefix)) {
|
|
753
|
+
input.arguments = rewritten.rewritten.slice(prefix.length)
|
|
754
|
+
} else {
|
|
755
|
+
input.command = rewritten.rewritten
|
|
756
|
+
input.arguments = ""
|
|
757
|
+
}
|
|
758
|
+
} else {
|
|
759
|
+
const rewritten = await maybeRewriteSupportedString(input.command, commandCache)
|
|
760
|
+
input.command = rewritten.rewritten
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
if (typeof input.arguments === "string" && input.arguments.includes("{{SECRET:")) {
|
|
764
|
+
if (isCurlExecutableToken(input.command)) {
|
|
765
|
+
const fullCurlCommand = input.arguments.trim().length > 0
|
|
766
|
+
? `${input.command} ${input.arguments}`
|
|
767
|
+
: input.command
|
|
768
|
+
const rewritten = substitutePlaceholdersInCurlShellString(fullCurlCommand)
|
|
769
|
+
const prefix = `${input.command} `
|
|
770
|
+
if (rewritten.rewritten.startsWith(prefix)) {
|
|
771
|
+
input.arguments = rewritten.rewritten.slice(prefix.length)
|
|
772
|
+
}
|
|
773
|
+
} else {
|
|
774
|
+
const rewritten = await maybeRewriteSupportedString(input.arguments, commandCache)
|
|
775
|
+
input.arguments = rewritten.rewritten
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
},
|
|
779
|
+
"tool.execute.before": async (input, output) => {
|
|
780
|
+
if (input.tool !== "bash") {
|
|
781
|
+
return
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
const sessionID = typeof input.sessionID === "string" ? input.sessionID.trim() : ""
|
|
785
|
+
if (!sessionID) {
|
|
786
|
+
return
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
assertNoAgentAuthoredSecretEnvReference(input.args)
|
|
790
|
+
assertNoAgentAuthoredSecretEnvReference(output.args)
|
|
791
|
+
|
|
792
|
+
const cache = new Map<string, { rewritten: string; referencedKeys: string[] }>()
|
|
793
|
+
await rewriteStringFieldsInPlace(output.args, cache)
|
|
794
|
+
await rewriteStringFieldsInPlace(input.args, cache)
|
|
795
|
+
},
|
|
796
|
+
"shell.env": async (input, output) => {
|
|
797
|
+
const sessionID = typeof input.sessionID === "string" ? input.sessionID.trim() : ""
|
|
798
|
+
if (!sessionID) {
|
|
799
|
+
return
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const referencedKeys = new Set<string>()
|
|
803
|
+
collectReferencedEnvKeys(input, referencedKeys)
|
|
804
|
+
collectReferencedEnvKeys(output, referencedKeys)
|
|
805
|
+
|
|
806
|
+
if (referencedKeys.size === 0) {
|
|
807
|
+
return
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
const resolvedSecretMap = await resolveSecretMapForKeys(sessionID, Array.from(referencedKeys).sort())
|
|
811
|
+
for (const [key, value] of Object.entries(resolvedSecretMap)) {
|
|
812
|
+
output.env[`${SECRET_ENV_PREFIX}${key}`] = value
|
|
813
|
+
}
|
|
814
|
+
},
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
export default SecretProxyPlugin
|