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,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