@sentry/junior 0.7.0 → 0.9.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.
@@ -0,0 +1,1436 @@
1
+ import {
2
+ discoverInstalledPluginPackageContent,
3
+ pluginRoots
4
+ } from "./chunk-KCLEEKYX.js";
5
+ import {
6
+ logInfo,
7
+ logWarn,
8
+ setSpanAttributes
9
+ } from "./chunk-ZW4OVKF5.js";
10
+
11
+ // src/chat/plugins/manifest.ts
12
+ import { z } from "zod";
13
+ import { parse as parseYaml } from "yaml";
14
+ var PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
15
+ var SHORT_CAPABILITY_RE = /^[a-z0-9-]+(\.[a-z0-9-]+)*$/;
16
+ var SHORT_CONFIG_KEY_RE = /^[a-z0-9]+(\.[a-z0-9-]+)*$/;
17
+ var AUTH_TOKEN_ENV_RE = /^[A-Z][A-Z0-9_]*$/;
18
+ var API_DOMAIN_RE = /^(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
19
+ var RUNTIME_POSTINSTALL_CMD_RE = /^[A-Za-z0-9._/-]+$/;
20
+ var RESERVED_AUTHORIZE_PARAM_KEYS = /* @__PURE__ */ new Set([
21
+ "client_id",
22
+ "scope",
23
+ "state",
24
+ "redirect_uri",
25
+ "response_type"
26
+ ]);
27
+ var FORBIDDEN_API_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
28
+ var FORBIDDEN_TOKEN_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
29
+ var trimmedString = z.string().transform((value) => value.trim());
30
+ var nonEmptyTrimmedString = trimmedString.pipe(
31
+ z.string().min(1, { error: "must be a non-empty string" })
32
+ );
33
+ var nonEmptyStringArraySchema = (fieldName, options = {}) => z.array(z.string(), {
34
+ error: "must be an array of strings when provided"
35
+ }).min(1, {
36
+ error: options.nonEmptyMessage ?? "must be a non-empty array of strings when provided"
37
+ }).transform((values, ctx) => {
38
+ const result = [];
39
+ const seen = /* @__PURE__ */ new Set();
40
+ for (const rawValue of values) {
41
+ const value = rawValue.trim();
42
+ if (!value) {
43
+ ctx.addIssue({
44
+ code: z.ZodIssueCode.custom,
45
+ message: `${fieldName} entries must be non-empty strings`
46
+ });
47
+ return z.NEVER;
48
+ }
49
+ if (seen.has(value)) {
50
+ continue;
51
+ }
52
+ seen.add(value);
53
+ result.push(value);
54
+ }
55
+ return result;
56
+ });
57
+ var envVarString = nonEmptyTrimmedString.refine(
58
+ (value) => AUTH_TOKEN_ENV_RE.test(value),
59
+ {
60
+ error: "must be an uppercase env var name"
61
+ }
62
+ );
63
+ var httpsUrlString = nonEmptyTrimmedString.superRefine((value, ctx) => {
64
+ let parsed;
65
+ try {
66
+ parsed = new URL(value);
67
+ } catch {
68
+ ctx.addIssue({
69
+ code: z.ZodIssueCode.custom,
70
+ message: "must be a valid URL"
71
+ });
72
+ return;
73
+ }
74
+ if (parsed.protocol !== "https:") {
75
+ ctx.addIssue({
76
+ code: z.ZodIssueCode.custom,
77
+ message: "must use https"
78
+ });
79
+ }
80
+ });
81
+ var stringMapSchema = z.record(z.string(), z.unknown()).transform((record, ctx) => {
82
+ const entries = Object.entries(record);
83
+ const result = {};
84
+ const seen = /* @__PURE__ */ new Set();
85
+ for (const [rawKey, rawValue] of entries) {
86
+ const key = rawKey.trim();
87
+ if (!key) {
88
+ ctx.addIssue({
89
+ code: z.ZodIssueCode.custom,
90
+ message: "keys must be non-empty strings"
91
+ });
92
+ return z.NEVER;
93
+ }
94
+ if (typeof rawValue !== "string" || !rawValue.trim()) {
95
+ ctx.addIssue({
96
+ code: z.ZodIssueCode.custom,
97
+ message: `${key} must be a non-empty string`
98
+ });
99
+ return z.NEVER;
100
+ }
101
+ const normalizedKey = key.toLowerCase();
102
+ if (seen.has(normalizedKey)) {
103
+ ctx.addIssue({
104
+ code: z.ZodIssueCode.custom,
105
+ message: `${key} is duplicated`
106
+ });
107
+ return z.NEVER;
108
+ }
109
+ seen.add(normalizedKey);
110
+ result[key] = rawValue.trim();
111
+ }
112
+ return Object.keys(result).length > 0 ? result : void 0;
113
+ });
114
+ var apiDomainsSchema = z.array(z.unknown()).min(1, {
115
+ error: "must be a non-empty array of strings"
116
+ }).transform((domains, ctx) => {
117
+ return domains.map((rawDomain) => {
118
+ const domain = typeof rawDomain === "string" ? rawDomain.trim().toLowerCase() : "";
119
+ if (!domain) {
120
+ ctx.addIssue({
121
+ code: z.ZodIssueCode.custom,
122
+ message: "entries must be non-empty strings"
123
+ });
124
+ return z.NEVER;
125
+ }
126
+ if (!API_DOMAIN_RE.test(domain)) {
127
+ ctx.addIssue({
128
+ code: z.ZodIssueCode.custom,
129
+ message: "entries must be valid domain names"
130
+ });
131
+ return z.NEVER;
132
+ }
133
+ return domain;
134
+ });
135
+ });
136
+ var baseCredentialsSchema = z.object({
137
+ "api-domains": apiDomainsSchema,
138
+ "api-headers": stringMapSchema.optional(),
139
+ "auth-token-env": envVarString,
140
+ "auth-token-placeholder": nonEmptyTrimmedString.optional()
141
+ }).passthrough();
142
+ var oauthBearerCredentialsSchema = baseCredentialsSchema.extend({
143
+ type: z.literal("oauth-bearer")
144
+ });
145
+ var githubAppCredentialsSchema = baseCredentialsSchema.extend({
146
+ type: z.literal("github-app"),
147
+ "app-id-env": envVarString,
148
+ "private-key-env": envVarString,
149
+ "installation-id-env": envVarString
150
+ });
151
+ var runtimeDependencyEntrySchema = z.object({
152
+ type: z.enum(["npm", "system"]),
153
+ package: z.string().optional(),
154
+ version: z.string().optional(),
155
+ url: z.string().optional(),
156
+ sha256: z.string().optional()
157
+ }).passthrough();
158
+ var runtimePostinstallCommandSourceSchema = z.object({
159
+ cmd: nonEmptyTrimmedString,
160
+ args: z.array(z.string(), {
161
+ error: "args must be an array of strings when provided"
162
+ }).optional(),
163
+ sudo: z.boolean({
164
+ error: "sudo must be a boolean when provided"
165
+ }).optional()
166
+ }).passthrough();
167
+ var oauthSourceSchema = z.object({
168
+ "client-id-env": envVarString,
169
+ "client-secret-env": envVarString,
170
+ "authorize-endpoint": httpsUrlString,
171
+ "token-endpoint": httpsUrlString,
172
+ scope: nonEmptyTrimmedString.optional(),
173
+ "authorize-params": stringMapSchema.optional(),
174
+ "token-extra-headers": stringMapSchema.optional(),
175
+ "token-auth-method": nonEmptyTrimmedString.refine((value) => value === "body" || value === "basic", {
176
+ error: 'must be "body" or "basic"'
177
+ }).optional()
178
+ }).passthrough();
179
+ var mcpSourceSchema = z.object({
180
+ transport: nonEmptyTrimmedString.refine((value) => value === "http", {
181
+ error: 'must be "http"'
182
+ }),
183
+ url: httpsUrlString,
184
+ headers: stringMapSchema.optional(),
185
+ "allowed-tools": nonEmptyStringArraySchema("allowed-tools").optional()
186
+ }).passthrough();
187
+ var targetSourceSchema = z.object({
188
+ type: z.literal("repo", {
189
+ error: 'type must be "repo"'
190
+ }),
191
+ "config-key": nonEmptyTrimmedString
192
+ }).passthrough();
193
+ var manifestSourceSchema = z.object({
194
+ name: z.string().refine((value) => PLUGIN_NAME_RE.test(value), {
195
+ error: "invalid"
196
+ }),
197
+ description: nonEmptyTrimmedString,
198
+ capabilities: z.array(z.string(), {
199
+ error: "must be an array when provided"
200
+ }).optional(),
201
+ "config-keys": z.array(z.string(), {
202
+ error: "must be an array when provided"
203
+ }).optional(),
204
+ credentials: z.record(z.string(), z.unknown(), {
205
+ error: "must be an object when provided"
206
+ }).optional(),
207
+ "runtime-dependencies": z.array(z.unknown(), {
208
+ error: "must be an array"
209
+ }).optional(),
210
+ "runtime-postinstall": z.array(z.unknown(), {
211
+ error: "must be an array"
212
+ }).optional(),
213
+ mcp: z.record(z.string(), z.unknown(), {
214
+ error: "must be an object"
215
+ }).optional(),
216
+ oauth: z.record(z.string(), z.unknown(), {
217
+ error: "must be an object"
218
+ }).optional(),
219
+ target: z.record(z.string(), z.unknown(), {
220
+ error: "must be an object"
221
+ }).optional()
222
+ }).passthrough();
223
+ function formatPath(path2) {
224
+ return path2.map((segment) => String(segment)).join(".");
225
+ }
226
+ function issueMessage(error, prefix) {
227
+ const issue = error.issues[0];
228
+ if (!issue) {
229
+ return prefix;
230
+ }
231
+ const suffix = formatPath(issue.path);
232
+ return suffix ? `${prefix}.${suffix} ${issue.message}` : `${prefix} ${issue.message}`;
233
+ }
234
+ function normalizeStringMap(value, prefix, options = {}) {
235
+ if (!value) {
236
+ return void 0;
237
+ }
238
+ for (const key of Object.keys(value)) {
239
+ const normalizedKey = key.toLowerCase();
240
+ if (options.reservedKeys?.has(normalizedKey)) {
241
+ throw new Error(`${prefix}.${key} is reserved by the runtime`);
242
+ }
243
+ if (options.forbiddenKeys?.has(normalizedKey)) {
244
+ throw new Error(`${prefix}.${key} is not allowed`);
245
+ }
246
+ }
247
+ return value;
248
+ }
249
+ function normalizeCredentials(data, name) {
250
+ const schema = data.type === "oauth-bearer" ? oauthBearerCredentialsSchema : data.type === "github-app" ? githubAppCredentialsSchema : void 0;
251
+ if (!schema) {
252
+ throw new Error(
253
+ `Plugin ${name} has unsupported credentials.type: "${String(data.type)}"`
254
+ );
255
+ }
256
+ const result = schema.safeParse(data);
257
+ if (!result.success) {
258
+ throw new Error(issueMessage(result.error, `Plugin ${name} credentials`));
259
+ }
260
+ if (result.data.type === "oauth-bearer") {
261
+ return {
262
+ type: "oauth-bearer",
263
+ apiDomains: result.data["api-domains"],
264
+ ...result.data["api-headers"] ? {
265
+ apiHeaders: normalizeStringMap(
266
+ result.data["api-headers"],
267
+ `Plugin ${name} credentials.api-headers`,
268
+ { forbiddenKeys: FORBIDDEN_API_HEADER_NAMES }
269
+ )
270
+ } : {},
271
+ authTokenEnv: result.data["auth-token-env"],
272
+ ...result.data["auth-token-placeholder"] ? { authTokenPlaceholder: result.data["auth-token-placeholder"] } : {}
273
+ };
274
+ }
275
+ return {
276
+ type: "github-app",
277
+ apiDomains: result.data["api-domains"],
278
+ ...result.data["api-headers"] ? {
279
+ apiHeaders: normalizeStringMap(
280
+ result.data["api-headers"],
281
+ `Plugin ${name} credentials.api-headers`,
282
+ { forbiddenKeys: FORBIDDEN_API_HEADER_NAMES }
283
+ )
284
+ } : {},
285
+ authTokenEnv: result.data["auth-token-env"],
286
+ ...result.data["auth-token-placeholder"] ? { authTokenPlaceholder: result.data["auth-token-placeholder"] } : {},
287
+ appIdEnv: result.data["app-id-env"],
288
+ privateKeyEnv: result.data["private-key-env"],
289
+ installationIdEnv: result.data["installation-id-env"]
290
+ };
291
+ }
292
+ function normalizeRuntimeDependencies(entries, name) {
293
+ const parsed = [];
294
+ const seen = /* @__PURE__ */ new Set();
295
+ for (const entry of entries) {
296
+ const result = runtimeDependencyEntrySchema.safeParse(entry);
297
+ if (!result.success) {
298
+ throw new Error(
299
+ issueMessage(
300
+ result.error,
301
+ `Plugin ${name} runtime-dependencies entries`
302
+ )
303
+ );
304
+ }
305
+ const record = result.data;
306
+ const packageName = typeof record.package === "string" ? record.package.trim() : "";
307
+ const packageUrl = typeof record.url === "string" ? record.url.trim() : "";
308
+ const version = record.version;
309
+ const sha256 = record.sha256;
310
+ if (record.type === "npm") {
311
+ if (!packageName) {
312
+ throw new Error(
313
+ `Plugin ${name} runtime dependency package must be a non-empty string`
314
+ );
315
+ }
316
+ if (record.url !== void 0 || sha256 !== void 0) {
317
+ throw new Error(
318
+ `Plugin ${name} npm runtime dependencies must only include package/version fields`
319
+ );
320
+ }
321
+ const normalizedVersion = typeof version === "string" ? version.trim() : "latest";
322
+ if (!normalizedVersion) {
323
+ throw new Error(
324
+ `Plugin ${name} runtime dependency version must be a non-empty string when provided`
325
+ );
326
+ }
327
+ const dedupeKey2 = `${record.type}:${packageName}:${normalizedVersion}`;
328
+ if (seen.has(dedupeKey2)) {
329
+ continue;
330
+ }
331
+ seen.add(dedupeKey2);
332
+ parsed.push({
333
+ type: "npm",
334
+ package: packageName,
335
+ version: normalizedVersion
336
+ });
337
+ continue;
338
+ }
339
+ if (version !== void 0) {
340
+ throw new Error(
341
+ `Plugin ${name} system runtime dependencies must not include a version`
342
+ );
343
+ }
344
+ if (packageName && packageUrl) {
345
+ throw new Error(
346
+ `Plugin ${name} system runtime dependencies must specify either package or url, not both`
347
+ );
348
+ }
349
+ if (!packageName && !packageUrl) {
350
+ throw new Error(
351
+ `Plugin ${name} system runtime dependencies must specify package or url`
352
+ );
353
+ }
354
+ if (packageName) {
355
+ if (sha256 !== void 0) {
356
+ throw new Error(
357
+ `Plugin ${name} system runtime dependency package entries must not include sha256`
358
+ );
359
+ }
360
+ const dedupeKey2 = `${record.type}:package:${packageName}`;
361
+ if (seen.has(dedupeKey2)) {
362
+ continue;
363
+ }
364
+ seen.add(dedupeKey2);
365
+ parsed.push({
366
+ type: "system",
367
+ package: packageName
368
+ });
369
+ continue;
370
+ }
371
+ if (!/^https:\/\//i.test(packageUrl)) {
372
+ throw new Error(
373
+ `Plugin ${name} system runtime dependency url must be an https URL`
374
+ );
375
+ }
376
+ const normalizedSha256 = typeof sha256 === "string" ? sha256.trim().toLowerCase() : "";
377
+ if (!/^[a-f0-9]{64}$/.test(normalizedSha256)) {
378
+ throw new Error(
379
+ `Plugin ${name} system runtime dependency url entries must include a valid sha256`
380
+ );
381
+ }
382
+ const dedupeKey = `${record.type}:url:${packageUrl}:${normalizedSha256}`;
383
+ if (seen.has(dedupeKey)) {
384
+ continue;
385
+ }
386
+ seen.add(dedupeKey);
387
+ parsed.push({
388
+ type: "system",
389
+ url: packageUrl,
390
+ sha256: normalizedSha256
391
+ });
392
+ }
393
+ return parsed.length > 0 ? parsed : void 0;
394
+ }
395
+ function normalizeRuntimePostinstall(commands, name) {
396
+ const parsed = [];
397
+ for (const command of commands) {
398
+ const result = runtimePostinstallCommandSourceSchema.safeParse(command);
399
+ if (!result.success) {
400
+ throw new Error(
401
+ issueMessage(result.error, `Plugin ${name} runtime-postinstall`)
402
+ );
403
+ }
404
+ if (!RUNTIME_POSTINSTALL_CMD_RE.test(result.data.cmd)) {
405
+ throw new Error(
406
+ `Plugin ${name} runtime-postinstall cmd must be a single executable token (letters, digits, ., _, /, -)`
407
+ );
408
+ }
409
+ const normalizedArgs = result.data.args?.map((arg) => arg.trim()).filter((arg) => arg.length > 0);
410
+ parsed.push({
411
+ cmd: result.data.cmd,
412
+ ...normalizedArgs && normalizedArgs.length > 0 ? { args: normalizedArgs } : {},
413
+ ...typeof result.data.sudo === "boolean" ? { sudo: result.data.sudo } : {}
414
+ });
415
+ }
416
+ return parsed.length > 0 ? parsed : void 0;
417
+ }
418
+ function normalizeMcp(data, name) {
419
+ const result = mcpSourceSchema.safeParse(data);
420
+ if (!result.success) {
421
+ throw new Error(issueMessage(result.error, `Plugin ${name} mcp`));
422
+ }
423
+ return {
424
+ transport: "http",
425
+ url: result.data.url,
426
+ ...result.data.headers ? {
427
+ headers: normalizeStringMap(
428
+ result.data.headers,
429
+ `Plugin ${name} mcp.headers`,
430
+ { forbiddenKeys: FORBIDDEN_API_HEADER_NAMES }
431
+ )
432
+ } : {},
433
+ ...result.data["allowed-tools"] ? { allowedTools: result.data["allowed-tools"] } : {}
434
+ };
435
+ }
436
+ function parsePluginManifest(raw, dir) {
437
+ let parsedYaml;
438
+ try {
439
+ parsedYaml = parseYaml(raw);
440
+ } catch (error) {
441
+ throw new Error(
442
+ `Invalid plugin manifest in ${dir}: ${error instanceof Error ? error.message : String(error)}`
443
+ );
444
+ }
445
+ if (!parsedYaml || typeof parsedYaml !== "object" || Array.isArray(parsedYaml)) {
446
+ throw new Error(`Invalid plugin manifest in ${dir}: expected an object`);
447
+ }
448
+ const sourceResult = manifestSourceSchema.safeParse(parsedYaml);
449
+ if (!sourceResult.success) {
450
+ const issue = sourceResult.error.issues[0];
451
+ const path2 = formatPath(issue?.path ?? []);
452
+ if (path2 === "name") {
453
+ throw new Error(
454
+ `Invalid plugin name in ${dir}: "${parsedYaml.name}"`
455
+ );
456
+ }
457
+ if (path2 === "description") {
458
+ throw new Error(`Invalid plugin description in ${dir}`);
459
+ }
460
+ if (path2 === "capabilities") {
461
+ throw new Error(
462
+ `Plugin ${parsedYaml.name ?? "unknown"} capabilities must be an array when provided`
463
+ );
464
+ }
465
+ if (path2 === "config-keys") {
466
+ throw new Error(
467
+ `Plugin ${parsedYaml.name ?? "unknown"} config-keys must be an array when provided`
468
+ );
469
+ }
470
+ if (path2 === "credentials") {
471
+ throw new Error(
472
+ `Plugin ${parsedYaml.name ?? "unknown"} credentials must be an object when provided`
473
+ );
474
+ }
475
+ if (path2 === "runtime-dependencies") {
476
+ throw new Error(
477
+ `Plugin ${parsedYaml.name ?? "unknown"} runtime-dependencies must be an array`
478
+ );
479
+ }
480
+ if (path2 === "runtime-postinstall") {
481
+ throw new Error(
482
+ `Plugin ${parsedYaml.name ?? "unknown"} runtime-postinstall must be an array`
483
+ );
484
+ }
485
+ if (path2 === "mcp") {
486
+ throw new Error(
487
+ `Plugin ${parsedYaml.name ?? "unknown"} mcp must be an object`
488
+ );
489
+ }
490
+ if (path2 === "oauth") {
491
+ throw new Error(
492
+ `Plugin ${parsedYaml.name ?? "unknown"} oauth must be an object`
493
+ );
494
+ }
495
+ if (path2 === "target") {
496
+ throw new Error(
497
+ `Plugin ${parsedYaml.name ?? "unknown"} target must be an object`
498
+ );
499
+ }
500
+ throw new Error(issue?.message ?? `Invalid plugin manifest in ${dir}`);
501
+ }
502
+ const data = sourceResult.data;
503
+ const capabilities = (data.capabilities ?? []).map((cap) => {
504
+ if (!SHORT_CAPABILITY_RE.test(cap)) {
505
+ throw new Error(
506
+ `Invalid capability token "${cap}" in plugin ${data.name}`
507
+ );
508
+ }
509
+ return `${data.name}.${cap}`;
510
+ });
511
+ const configKeys = (data["config-keys"] ?? []).map((key) => {
512
+ if (!SHORT_CONFIG_KEY_RE.test(key)) {
513
+ throw new Error(`Invalid config key "${key}" in plugin ${data.name}`);
514
+ }
515
+ return `${data.name}.${key}`;
516
+ });
517
+ const credentials = data.credentials ? normalizeCredentials(data.credentials, data.name) : void 0;
518
+ const runtimeDependencies = data["runtime-dependencies"] ? normalizeRuntimeDependencies(data["runtime-dependencies"], data.name) : void 0;
519
+ const runtimePostinstall = data["runtime-postinstall"] ? normalizeRuntimePostinstall(data["runtime-postinstall"], data.name) : void 0;
520
+ const mcp = data.mcp ? normalizeMcp(data.mcp, data.name) : void 0;
521
+ const manifest = {
522
+ name: data.name,
523
+ description: data.description,
524
+ capabilities,
525
+ configKeys,
526
+ ...credentials ? { credentials } : {},
527
+ ...runtimeDependencies ? { runtimeDependencies } : {},
528
+ ...runtimePostinstall ? { runtimePostinstall } : {},
529
+ ...mcp ? { mcp } : {}
530
+ };
531
+ if (data.oauth) {
532
+ if (!credentials) {
533
+ throw new Error(`Plugin ${data.name} oauth requires credentials`);
534
+ }
535
+ if (credentials.type !== "oauth-bearer") {
536
+ throw new Error(
537
+ `Plugin ${data.name} oauth requires credentials.type "oauth-bearer"`
538
+ );
539
+ }
540
+ const result = oauthSourceSchema.safeParse(data.oauth);
541
+ if (!result.success) {
542
+ throw new Error(issueMessage(result.error, `Plugin ${data.name} oauth`));
543
+ }
544
+ const authorizeParams = result.data["authorize-params"] ? normalizeStringMap(
545
+ result.data["authorize-params"],
546
+ `Plugin ${data.name} oauth.authorize-params`,
547
+ {
548
+ reservedKeys: RESERVED_AUTHORIZE_PARAM_KEYS
549
+ }
550
+ ) : void 0;
551
+ const tokenExtraHeaders = result.data["token-extra-headers"] ? normalizeStringMap(
552
+ result.data["token-extra-headers"],
553
+ `Plugin ${data.name} oauth.token-extra-headers`,
554
+ {
555
+ forbiddenKeys: FORBIDDEN_TOKEN_HEADER_NAMES
556
+ }
557
+ ) : void 0;
558
+ manifest.oauth = {
559
+ clientIdEnv: result.data["client-id-env"],
560
+ clientSecretEnv: result.data["client-secret-env"],
561
+ authorizeEndpoint: result.data["authorize-endpoint"],
562
+ tokenEndpoint: result.data["token-endpoint"],
563
+ ...result.data.scope ? { scope: result.data.scope } : {},
564
+ ...authorizeParams ? { authorizeParams } : {},
565
+ ...result.data["token-auth-method"] ? { tokenAuthMethod: result.data["token-auth-method"] } : {},
566
+ ...tokenExtraHeaders ? { tokenExtraHeaders } : {}
567
+ };
568
+ }
569
+ if (data.target) {
570
+ const result = targetSourceSchema.safeParse(data.target);
571
+ if (!result.success) {
572
+ throw new Error(issueMessage(result.error, `Plugin ${data.name} target`));
573
+ }
574
+ if (!SHORT_CONFIG_KEY_RE.test(result.data["config-key"])) {
575
+ throw new Error(
576
+ `Plugin ${data.name} target.config-key "${result.data["config-key"]}" is invalid`
577
+ );
578
+ }
579
+ const qualifiedKey = `${data.name}.${result.data["config-key"]}`;
580
+ if (!configKeys.includes(qualifiedKey)) {
581
+ throw new Error(
582
+ `Plugin ${data.name} target.config-key "${result.data["config-key"]}" must be listed in config-keys`
583
+ );
584
+ }
585
+ manifest.target = { type: "repo", configKey: qualifiedKey };
586
+ }
587
+ return manifest;
588
+ }
589
+
590
+ // src/chat/plugins/registry.ts
591
+ import { readFileSync, readdirSync, statSync } from "fs";
592
+ import path from "path";
593
+
594
+ // src/chat/plugins/auth/github-app-broker.ts
595
+ import { createPrivateKey, createSign, randomUUID } from "crypto";
596
+
597
+ // src/chat/plugins/auth/auth-token-placeholder.ts
598
+ var DEFAULT_PLACEHOLDERS = {
599
+ "oauth-bearer": "host_managed_credential",
600
+ "github-app": "ghp_host_managed_credential"
601
+ };
602
+ function resolveAuthTokenPlaceholder(credentials) {
603
+ return credentials.authTokenPlaceholder?.trim() || DEFAULT_PLACEHOLDERS[credentials.type];
604
+ }
605
+
606
+ // src/chat/plugins/auth/github-app-broker.ts
607
+ var MAX_LEASE_MS = 60 * 60 * 1e3;
608
+ function normalizeTargetScope(target) {
609
+ const owner = target?.owner?.trim().toLowerCase();
610
+ const repo = target?.repo?.trim().toLowerCase();
611
+ if (!owner || !repo) {
612
+ return "all";
613
+ }
614
+ return `${owner}/${repo}`;
615
+ }
616
+ function base64Url(input) {
617
+ return Buffer.from(input).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
618
+ }
619
+ function normalizePrivateKey(raw) {
620
+ let normalized = raw.trim();
621
+ if (normalized.startsWith('"') && normalized.endsWith('"') || normalized.startsWith("'") && normalized.endsWith("'")) {
622
+ normalized = normalized.slice(1, -1);
623
+ }
624
+ normalized = normalized.replace(/\r\n/g, "\n");
625
+ if (normalized.includes("\\n")) {
626
+ normalized = normalized.replace(/\\n/g, "\n");
627
+ }
628
+ if (!normalized.includes("-----BEGIN")) {
629
+ try {
630
+ const decoded = Buffer.from(normalized, "base64").toString("utf8").trim();
631
+ if (decoded.includes("-----BEGIN")) {
632
+ normalized = decoded;
633
+ }
634
+ } catch {
635
+ }
636
+ }
637
+ return normalized;
638
+ }
639
+ function getPrivateKey(envName) {
640
+ const raw = process.env[envName];
641
+ if (!raw) {
642
+ throw new Error(`Missing ${envName}`);
643
+ }
644
+ const normalized = normalizePrivateKey(raw);
645
+ let key;
646
+ try {
647
+ key = createPrivateKey({ key: normalized, format: "pem" });
648
+ } catch {
649
+ throw new Error(
650
+ `Invalid ${envName}: expected a PEM-encoded RSA private key (raw PEM, escaped newlines, or base64-encoded PEM)`
651
+ );
652
+ }
653
+ if (key.asymmetricKeyType !== "rsa") {
654
+ throw new Error(
655
+ `Invalid ${envName}: GitHub App signing requires an RSA private key`
656
+ );
657
+ }
658
+ return key;
659
+ }
660
+ function createAppJwt(appId, privateKeyEnv) {
661
+ const now = Math.floor(Date.now() / 1e3);
662
+ const header = { alg: "RS256", typ: "JWT" };
663
+ const payload = { iat: now - 60, exp: now + 9 * 60, iss: appId };
664
+ const encodedHeader = base64Url(JSON.stringify(header));
665
+ const encodedPayload = base64Url(JSON.stringify(payload));
666
+ const signingInput = `${encodedHeader}.${encodedPayload}`;
667
+ const signer = createSign("RSA-SHA256");
668
+ signer.update(signingInput);
669
+ signer.end();
670
+ const signature = signer.sign(getPrivateKey(privateKeyEnv)).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
671
+ return `${signingInput}.${signature}`;
672
+ }
673
+ async function githubRequest(apiBase, path2, params) {
674
+ const response = await fetch(`${apiBase}${path2}`, {
675
+ method: params.method ?? "GET",
676
+ headers: {
677
+ Accept: "application/vnd.github+json",
678
+ Authorization: `Bearer ${params.token}`,
679
+ "X-GitHub-Api-Version": "2022-11-28",
680
+ ...params.body ? { "Content-Type": "application/json" } : {}
681
+ },
682
+ ...params.body ? { body: JSON.stringify(params.body) } : {}
683
+ });
684
+ const text = await response.text();
685
+ let parsed = void 0;
686
+ if (text) {
687
+ try {
688
+ parsed = JSON.parse(text);
689
+ } catch {
690
+ parsed = void 0;
691
+ }
692
+ }
693
+ if (!response.ok) {
694
+ const message = parsed && typeof parsed === "object" && "message" in parsed && typeof parsed.message === "string" ? parsed.message : `GitHub API error ${response.status}`;
695
+ throw new Error(message);
696
+ }
697
+ return parsed;
698
+ }
699
+ var CAPABILITY_ALIASES = {
700
+ "issues.comment": { permission: "issues", level: "write" },
701
+ "labels.write": { permission: "issues", level: "write" }
702
+ };
703
+ var KNOWN_SCOPES = /* @__PURE__ */ new Set([
704
+ "actions",
705
+ "administration",
706
+ "checks",
707
+ "codespaces",
708
+ "contents",
709
+ "deployments",
710
+ "environments",
711
+ "issues",
712
+ "metadata",
713
+ "packages",
714
+ "pages",
715
+ "pull_requests",
716
+ "repository_hooks",
717
+ "repository_projects",
718
+ "secret_scanning_alerts",
719
+ "secrets",
720
+ "security_events",
721
+ "statuses",
722
+ "vulnerability_alerts",
723
+ "workflows"
724
+ ]);
725
+ function capabilityToPermissions(capability, pluginName) {
726
+ const prefix = `${pluginName}.`;
727
+ if (!capability.startsWith(prefix)) {
728
+ throw new Error(`Unsupported GitHub capability: ${capability}`);
729
+ }
730
+ const suffix = capability.slice(prefix.length);
731
+ const alias = CAPABILITY_ALIASES[suffix];
732
+ if (alias) {
733
+ return { [alias.permission]: alias.level };
734
+ }
735
+ const lastDot = suffix.lastIndexOf(".");
736
+ if (lastDot === -1) {
737
+ throw new Error(`Unsupported GitHub capability: ${capability}`);
738
+ }
739
+ const scopeRaw = suffix.slice(0, lastDot);
740
+ const level = suffix.slice(lastDot + 1);
741
+ if (level !== "read" && level !== "write") {
742
+ throw new Error(`Unsupported GitHub capability: ${capability}`);
743
+ }
744
+ const scope = scopeRaw.replace(/-/g, "_");
745
+ if (!KNOWN_SCOPES.has(scope)) {
746
+ throw new Error(`Unsupported GitHub capability: ${capability}`);
747
+ }
748
+ return { [scope]: level };
749
+ }
750
+ function createGitHubAppBroker(manifest, credentials) {
751
+ const tokenCache = /* @__PURE__ */ new Map();
752
+ const provider = manifest.name;
753
+ const {
754
+ apiDomains,
755
+ apiHeaders,
756
+ authTokenEnv,
757
+ appIdEnv,
758
+ privateKeyEnv,
759
+ installationIdEnv
760
+ } = credentials;
761
+ const apiBase = `https://${apiDomains[0]}`;
762
+ const placeholder = resolveAuthTokenPlaceholder(credentials);
763
+ const GIT_DOMAIN = "github.com";
764
+ const GIT_CAPABILITIES = /* @__PURE__ */ new Set([
765
+ `${provider}.contents.read`,
766
+ `${provider}.contents.write`
767
+ ]);
768
+ function leaseDomainsFor(capability) {
769
+ return GIT_CAPABILITIES.has(capability) ? [...apiDomains, GIT_DOMAIN] : apiDomains;
770
+ }
771
+ const supportedCapabilities = new Set(manifest.capabilities);
772
+ return {
773
+ async issue(input) {
774
+ if (!supportedCapabilities.has(input.capability)) {
775
+ throw new Error(
776
+ `Unsupported ${provider} capability: ${input.capability}`
777
+ );
778
+ }
779
+ const permissions = capabilityToPermissions(input.capability, provider);
780
+ const appId = process.env[appIdEnv];
781
+ if (!appId) {
782
+ throw new Error(`Missing ${appIdEnv}`);
783
+ }
784
+ const installationIdRaw = process.env[installationIdEnv]?.trim();
785
+ if (!installationIdRaw) {
786
+ throw new Error(`Missing ${installationIdEnv}`);
787
+ }
788
+ const installationId = Number(installationIdRaw);
789
+ if (!Number.isFinite(installationId)) {
790
+ throw new Error(`Invalid ${installationIdEnv}`);
791
+ }
792
+ const targetScope = normalizeTargetScope(input.target);
793
+ const cacheKey = `${installationId}:${input.capability}:${targetScope}`;
794
+ const cached = tokenCache.get(cacheKey);
795
+ const now = Date.now();
796
+ if (cached && cached.expiresAt - now > 2 * 60 * 1e3) {
797
+ const domains2 = leaseDomainsFor(input.capability);
798
+ return {
799
+ id: randomUUID(),
800
+ provider,
801
+ capability: input.capability,
802
+ env: { [authTokenEnv]: placeholder },
803
+ headerTransforms: domains2.map((domain) => ({
804
+ domain,
805
+ headers: {
806
+ ...apiHeaders ?? {},
807
+ Authorization: `Bearer ${cached.token}`
808
+ }
809
+ })),
810
+ expiresAt: new Date(cached.expiresAt).toISOString(),
811
+ metadata: {
812
+ installationId: String(cached.installationId),
813
+ targetScope,
814
+ reason: input.reason
815
+ }
816
+ };
817
+ }
818
+ const appJwt = createAppJwt(appId, privateKeyEnv);
819
+ const repositoryName = input.target?.repo?.trim().toLowerCase();
820
+ const tokenRequestBody = {
821
+ permissions
822
+ };
823
+ if (repositoryName) {
824
+ tokenRequestBody.repositories = [repositoryName];
825
+ }
826
+ const accessTokenResponse = await githubRequest(apiBase, `/app/installations/${installationId}/access_tokens`, {
827
+ method: "POST",
828
+ token: appJwt,
829
+ body: tokenRequestBody
830
+ });
831
+ const providerExpiresAtMs = Date.parse(accessTokenResponse.expires_at);
832
+ const expiresAtMs = Math.min(
833
+ providerExpiresAtMs,
834
+ Date.now() + MAX_LEASE_MS
835
+ );
836
+ tokenCache.set(cacheKey, {
837
+ installationId,
838
+ token: accessTokenResponse.token,
839
+ expiresAt: expiresAtMs
840
+ });
841
+ const domains = leaseDomainsFor(input.capability);
842
+ return {
843
+ id: randomUUID(),
844
+ provider,
845
+ capability: input.capability,
846
+ env: { [authTokenEnv]: placeholder },
847
+ headerTransforms: domains.map((domain) => ({
848
+ domain,
849
+ headers: {
850
+ ...apiHeaders ?? {},
851
+ Authorization: `Bearer ${accessTokenResponse.token}`
852
+ }
853
+ })),
854
+ expiresAt: new Date(expiresAtMs).toISOString(),
855
+ metadata: {
856
+ installationId: String(installationId),
857
+ targetScope,
858
+ reason: input.reason
859
+ }
860
+ };
861
+ }
862
+ };
863
+ }
864
+
865
+ // src/chat/plugins/auth/oauth-bearer-broker.ts
866
+ import { randomUUID as randomUUID2 } from "crypto";
867
+
868
+ // src/chat/credentials/broker.ts
869
+ var CredentialUnavailableError = class extends Error {
870
+ provider;
871
+ constructor(provider, message) {
872
+ super(message);
873
+ this.name = "CredentialUnavailableError";
874
+ this.provider = provider;
875
+ }
876
+ };
877
+
878
+ // src/chat/plugins/auth/oauth-request.ts
879
+ var DEFAULT_TOKEN_CONTENT_TYPE = "application/x-www-form-urlencoded";
880
+ function requireNonEmptyTokenField(data, field) {
881
+ const value = data[field];
882
+ if (typeof value !== "string" || !value.trim()) {
883
+ throw new Error(`OAuth token response missing ${field}`);
884
+ }
885
+ return value;
886
+ }
887
+ function contentTypeToBody(contentType, payload) {
888
+ const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
889
+ if (!mediaType || mediaType === DEFAULT_TOKEN_CONTENT_TYPE) {
890
+ return new URLSearchParams(payload);
891
+ }
892
+ if (mediaType === "application/json" || mediaType.endsWith("+json")) {
893
+ return JSON.stringify(payload);
894
+ }
895
+ throw new Error(`Unsupported OAuth token Content-Type: ${contentType}`);
896
+ }
897
+ function buildOAuthTokenRequest(input) {
898
+ const headers = new Headers({ Accept: "application/json" });
899
+ for (const [name, value] of Object.entries(input.tokenExtraHeaders ?? {})) {
900
+ headers.set(name, value);
901
+ }
902
+ if (!headers.has("Content-Type")) {
903
+ headers.set("Content-Type", DEFAULT_TOKEN_CONTENT_TYPE);
904
+ }
905
+ const payload = { ...input.payload };
906
+ if (input.tokenAuthMethod === "basic") {
907
+ headers.set(
908
+ "Authorization",
909
+ `Basic ${Buffer.from(`${input.clientId}:${input.clientSecret}`).toString("base64")}`
910
+ );
911
+ } else {
912
+ payload.client_id = input.clientId;
913
+ payload.client_secret = input.clientSecret;
914
+ }
915
+ const contentType = headers.get("Content-Type") ?? DEFAULT_TOKEN_CONTENT_TYPE;
916
+ const serializedHeaders = {};
917
+ headers.forEach((value, key) => {
918
+ serializedHeaders[key] = value;
919
+ });
920
+ return {
921
+ headers: serializedHeaders,
922
+ body: contentTypeToBody(contentType, payload)
923
+ };
924
+ }
925
+ function parseOAuthTokenResponse(data) {
926
+ const accessToken = requireNonEmptyTokenField(data, "access_token");
927
+ const refreshToken = requireNonEmptyTokenField(data, "refresh_token");
928
+ const expiresIn = data.expires_in;
929
+ if (expiresIn === void 0) {
930
+ return { accessToken, refreshToken };
931
+ }
932
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
933
+ throw new Error("OAuth token response returned invalid expires_in");
934
+ }
935
+ return {
936
+ accessToken,
937
+ refreshToken,
938
+ expiresAt: Date.now() + expiresIn * 1e3
939
+ };
940
+ }
941
+
942
+ // src/chat/plugins/auth/oauth-bearer-broker.ts
943
+ var MAX_LEASE_MS2 = 60 * 60 * 1e3;
944
+ var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
945
+ async function refreshAccessToken(refreshToken, oauth) {
946
+ const clientId = process.env[oauth.clientIdEnv]?.trim();
947
+ const clientSecret = process.env[oauth.clientSecretEnv]?.trim();
948
+ if (!clientId || !clientSecret) {
949
+ throw new Error(
950
+ `Missing ${oauth.clientIdEnv} or ${oauth.clientSecretEnv} for token refresh`
951
+ );
952
+ }
953
+ const request = buildOAuthTokenRequest({
954
+ clientId,
955
+ clientSecret,
956
+ payload: {
957
+ grant_type: "refresh_token",
958
+ refresh_token: refreshToken
959
+ },
960
+ tokenAuthMethod: oauth.tokenAuthMethod,
961
+ tokenExtraHeaders: oauth.tokenExtraHeaders
962
+ });
963
+ const response = await fetch(oauth.tokenEndpoint, {
964
+ method: "POST",
965
+ headers: request.headers,
966
+ body: request.body
967
+ });
968
+ if (!response.ok) {
969
+ throw new Error(`Token refresh failed: ${response.status}`);
970
+ }
971
+ const data = await response.json();
972
+ return parseOAuthTokenResponse(data);
973
+ }
974
+ function getLeaseExpiry(expiresAt) {
975
+ return expiresAt ? Math.min(expiresAt, Date.now() + MAX_LEASE_MS2) : Date.now() + MAX_LEASE_MS2;
976
+ }
977
+ function createOAuthBearerBroker(manifest, credentials, deps) {
978
+ const provider = manifest.name;
979
+ const supportedCapabilities = new Set(manifest.capabilities);
980
+ const { apiDomains, apiHeaders, authTokenEnv } = credentials;
981
+ const authTokenPlaceholder = resolveAuthTokenPlaceholder(credentials);
982
+ function buildLease(token, capability, expiresAtMs, reason) {
983
+ return {
984
+ id: randomUUID2(),
985
+ provider,
986
+ capability,
987
+ env: { [authTokenEnv]: authTokenPlaceholder },
988
+ headerTransforms: apiDomains.map((domain) => ({
989
+ domain,
990
+ headers: { ...apiHeaders ?? {}, Authorization: `Bearer ${token}` }
991
+ })),
992
+ expiresAt: new Date(expiresAtMs).toISOString(),
993
+ metadata: { reason }
994
+ };
995
+ }
996
+ return {
997
+ async issue(input) {
998
+ if (!supportedCapabilities.has(input.capability)) {
999
+ throw new Error(
1000
+ `Unsupported ${provider} capability: ${input.capability}`
1001
+ );
1002
+ }
1003
+ const envToken = process.env[authTokenEnv]?.trim();
1004
+ const oauth = manifest.oauth;
1005
+ if (!oauth) {
1006
+ if (envToken) {
1007
+ return buildLease(
1008
+ envToken,
1009
+ input.capability,
1010
+ Date.now() + MAX_LEASE_MS2,
1011
+ input.reason
1012
+ );
1013
+ }
1014
+ throw new CredentialUnavailableError(
1015
+ provider,
1016
+ `No ${provider} credentials available.`
1017
+ );
1018
+ }
1019
+ if (input.requesterId) {
1020
+ const stored = await deps.userTokenStore.get(
1021
+ input.requesterId,
1022
+ provider
1023
+ );
1024
+ if (stored) {
1025
+ const now = Date.now();
1026
+ if (stored.expiresAt !== void 0 && stored.expiresAt - now < REFRESH_BUFFER_MS) {
1027
+ try {
1028
+ const refreshed = await refreshAccessToken(
1029
+ stored.refreshToken,
1030
+ oauth
1031
+ );
1032
+ await deps.userTokenStore.set(
1033
+ input.requesterId,
1034
+ provider,
1035
+ refreshed
1036
+ );
1037
+ return buildLease(
1038
+ refreshed.accessToken,
1039
+ input.capability,
1040
+ getLeaseExpiry(refreshed.expiresAt),
1041
+ input.reason
1042
+ );
1043
+ } catch {
1044
+ if (stored.expiresAt > Date.now()) {
1045
+ return buildLease(
1046
+ stored.accessToken,
1047
+ input.capability,
1048
+ getLeaseExpiry(stored.expiresAt),
1049
+ input.reason
1050
+ );
1051
+ }
1052
+ throw new CredentialUnavailableError(
1053
+ provider,
1054
+ `Your ${provider} connection has expired.`
1055
+ );
1056
+ }
1057
+ }
1058
+ if (stored.expiresAt === void 0 || stored.expiresAt > Date.now()) {
1059
+ return buildLease(
1060
+ stored.accessToken,
1061
+ input.capability,
1062
+ getLeaseExpiry(stored.expiresAt),
1063
+ input.reason
1064
+ );
1065
+ }
1066
+ throw new CredentialUnavailableError(
1067
+ provider,
1068
+ `Your ${provider} connection has expired.`
1069
+ );
1070
+ }
1071
+ throw new CredentialUnavailableError(
1072
+ provider,
1073
+ `No ${provider} credentials available.`
1074
+ );
1075
+ }
1076
+ if (envToken) {
1077
+ return buildLease(
1078
+ envToken,
1079
+ input.capability,
1080
+ getLeaseExpiry(),
1081
+ input.reason
1082
+ );
1083
+ }
1084
+ throw new CredentialUnavailableError(
1085
+ provider,
1086
+ `No ${provider} credentials available.`
1087
+ );
1088
+ }
1089
+ };
1090
+ }
1091
+
1092
+ // src/chat/plugins/registry.ts
1093
+ var loadedPluginState;
1094
+ function getLoggedPluginNames() {
1095
+ const globalState = globalThis;
1096
+ globalState.__juniorLoggedPluginNames ??= /* @__PURE__ */ new Set();
1097
+ return globalState.__juniorLoggedPluginNames;
1098
+ }
1099
+ function createLoadedPluginState(signature) {
1100
+ return {
1101
+ signature,
1102
+ pluginDefinitions: [],
1103
+ capabilityToPlugin: /* @__PURE__ */ new Map(),
1104
+ pluginConfigKeys: /* @__PURE__ */ new Set(),
1105
+ pluginsByName: /* @__PURE__ */ new Map(),
1106
+ packageSkillRoots: /* @__PURE__ */ new Set()
1107
+ };
1108
+ }
1109
+ function registerPluginManifest(state, raw, pluginDir) {
1110
+ const manifest = parsePluginManifest(raw, pluginDir);
1111
+ if (state.pluginsByName.has(manifest.name)) {
1112
+ return;
1113
+ }
1114
+ for (const cap of manifest.capabilities) {
1115
+ if (state.capabilityToPlugin.has(cap)) {
1116
+ throw new Error(
1117
+ `Duplicate capability "${cap}" in plugin "${manifest.name}"`
1118
+ );
1119
+ }
1120
+ }
1121
+ const definition = {
1122
+ manifest,
1123
+ dir: pluginDir,
1124
+ skillsDir: path.join(pluginDir, "skills")
1125
+ };
1126
+ state.pluginDefinitions.push(definition);
1127
+ state.pluginsByName.set(manifest.name, definition);
1128
+ for (const cap of manifest.capabilities) {
1129
+ state.capabilityToPlugin.set(cap, definition);
1130
+ }
1131
+ for (const key of manifest.configKeys) {
1132
+ state.pluginConfigKeys.add(key);
1133
+ }
1134
+ }
1135
+ function normalizePluginRoots(roots) {
1136
+ const resolved = [];
1137
+ const seen = /* @__PURE__ */ new Set();
1138
+ for (const root of roots) {
1139
+ const normalized = path.resolve(root);
1140
+ if (seen.has(normalized)) {
1141
+ continue;
1142
+ }
1143
+ seen.add(normalized);
1144
+ resolved.push(normalized);
1145
+ }
1146
+ return resolved;
1147
+ }
1148
+ function getExtraPluginRoots() {
1149
+ const raw = process.env.JUNIOR_EXTRA_PLUGIN_ROOTS?.trim();
1150
+ if (!raw) {
1151
+ return [];
1152
+ }
1153
+ if (raw.startsWith("[")) {
1154
+ try {
1155
+ const parsed = JSON.parse(raw);
1156
+ if (Array.isArray(parsed)) {
1157
+ return normalizePluginRoots(
1158
+ parsed.filter((value) => typeof value === "string")
1159
+ );
1160
+ }
1161
+ } catch {
1162
+ return [];
1163
+ }
1164
+ }
1165
+ return normalizePluginRoots(
1166
+ raw.split(path.delimiter).map((entry) => entry.trim()).filter((entry) => entry.length > 0)
1167
+ );
1168
+ }
1169
+ function getPluginCatalogSource() {
1170
+ const packagedContent = discoverInstalledPluginPackageContent();
1171
+ const localRoots = normalizePluginRoots([
1172
+ ...pluginRoots(),
1173
+ ...getExtraPluginRoots()
1174
+ ]);
1175
+ const manifestRoots = normalizePluginRoots([
1176
+ ...localRoots,
1177
+ ...packagedContent.manifestRoots
1178
+ ]);
1179
+ const packagedSkillRoots = normalizePluginRoots(packagedContent.skillRoots);
1180
+ return {
1181
+ manifestRoots,
1182
+ packagedSkillRoots,
1183
+ signature: JSON.stringify({
1184
+ manifestRoots,
1185
+ packagedSkillRoots,
1186
+ packageNames: [...packagedContent.packageNames].sort()
1187
+ })
1188
+ };
1189
+ }
1190
+ function buildLoadedPluginState(source) {
1191
+ const state = createLoadedPluginState(source.signature);
1192
+ for (const skillRoot of source.packagedSkillRoots) {
1193
+ state.packageSkillRoots.add(skillRoot);
1194
+ }
1195
+ const roots = source.manifestRoots;
1196
+ for (const pluginsRoot of roots) {
1197
+ let entries;
1198
+ let rootStat;
1199
+ try {
1200
+ rootStat = statSync(pluginsRoot);
1201
+ } catch (error) {
1202
+ logWarn(
1203
+ "plugin_root_read_failed",
1204
+ {},
1205
+ {
1206
+ "file.directory": pluginsRoot,
1207
+ "error.message": error instanceof Error ? error.message : String(error)
1208
+ },
1209
+ "Failed to read plugin root"
1210
+ );
1211
+ continue;
1212
+ }
1213
+ if (rootStat.isDirectory()) {
1214
+ const manifestPath = path.join(pluginsRoot, "plugin.yaml");
1215
+ let hasRootManifest = false;
1216
+ try {
1217
+ hasRootManifest = statSync(manifestPath).isFile();
1218
+ } catch {
1219
+ hasRootManifest = false;
1220
+ }
1221
+ if (hasRootManifest) {
1222
+ const rawRootManifest = readFileSync(manifestPath, "utf8");
1223
+ registerPluginManifest(state, rawRootManifest, pluginsRoot);
1224
+ continue;
1225
+ }
1226
+ }
1227
+ try {
1228
+ entries = readdirSync(pluginsRoot);
1229
+ } catch (error) {
1230
+ logWarn(
1231
+ "plugin_root_read_failed",
1232
+ {},
1233
+ {
1234
+ "file.directory": pluginsRoot,
1235
+ "error.message": error instanceof Error ? error.message : String(error)
1236
+ },
1237
+ "Failed to read plugin root"
1238
+ );
1239
+ continue;
1240
+ }
1241
+ for (const entry of entries.sort()) {
1242
+ const pluginDir = path.join(pluginsRoot, entry);
1243
+ try {
1244
+ const stat = statSync(pluginDir);
1245
+ if (!stat.isDirectory()) continue;
1246
+ } catch {
1247
+ continue;
1248
+ }
1249
+ const manifestPath = path.join(pluginDir, "plugin.yaml");
1250
+ let raw;
1251
+ try {
1252
+ raw = readFileSync(manifestPath, "utf8");
1253
+ } catch {
1254
+ continue;
1255
+ }
1256
+ registerPluginManifest(state, raw, pluginDir);
1257
+ }
1258
+ }
1259
+ return state;
1260
+ }
1261
+ function logLoadedPlugins(state) {
1262
+ const loggedPluginNames = getLoggedPluginNames();
1263
+ for (const plugin of [...state.pluginDefinitions].sort(
1264
+ (left, right) => left.manifest.name.localeCompare(right.manifest.name)
1265
+ )) {
1266
+ if (loggedPluginNames.has(plugin.manifest.name)) {
1267
+ continue;
1268
+ }
1269
+ loggedPluginNames.add(plugin.manifest.name);
1270
+ logInfo(
1271
+ "plugin_loaded",
1272
+ {},
1273
+ {
1274
+ "app.plugin.name": plugin.manifest.name,
1275
+ "app.plugin.capability_count": plugin.manifest.capabilities.length,
1276
+ "app.plugin.config_key_count": plugin.manifest.configKeys.length,
1277
+ "app.plugin.has_mcp": Boolean(plugin.manifest.mcp),
1278
+ "file.directory": plugin.dir,
1279
+ "file.skill_directory": plugin.skillsDir
1280
+ },
1281
+ "Loaded plugin"
1282
+ );
1283
+ }
1284
+ }
1285
+ function ensurePluginsLoaded() {
1286
+ const source = getPluginCatalogSource();
1287
+ if (loadedPluginState?.signature === source.signature) {
1288
+ return loadedPluginState;
1289
+ }
1290
+ const state = buildLoadedPluginState(source);
1291
+ loadedPluginState = state;
1292
+ logLoadedPlugins(state);
1293
+ return state;
1294
+ }
1295
+ function getPluginCapabilityProviders() {
1296
+ const state = ensurePluginsLoaded();
1297
+ return state.pluginDefinitions.map((plugin) => ({
1298
+ provider: plugin.manifest.name,
1299
+ capabilities: [...plugin.manifest.capabilities],
1300
+ configKeys: [...plugin.manifest.configKeys],
1301
+ ...plugin.manifest.target ? { target: { ...plugin.manifest.target } } : {}
1302
+ }));
1303
+ }
1304
+ function getPluginProviders() {
1305
+ return [...ensurePluginsLoaded().pluginDefinitions];
1306
+ }
1307
+ function getPluginMcpProviders() {
1308
+ return ensurePluginsLoaded().pluginDefinitions.filter(
1309
+ (plugin) => Boolean(plugin.manifest.mcp)
1310
+ );
1311
+ }
1312
+ function getPluginRuntimeDependencies() {
1313
+ const state = ensurePluginsLoaded();
1314
+ const seen = /* @__PURE__ */ new Set();
1315
+ const deps = [];
1316
+ for (const plugin of state.pluginDefinitions) {
1317
+ for (const dep of plugin.manifest.runtimeDependencies ?? []) {
1318
+ const key = dep.type === "npm" ? `${dep.type}:${dep.package}:${dep.version}` : "package" in dep ? `${dep.type}:package:${dep.package}` : `${dep.type}:url:${dep.url}:${dep.sha256}`;
1319
+ if (seen.has(key)) {
1320
+ continue;
1321
+ }
1322
+ seen.add(key);
1323
+ deps.push(dep);
1324
+ }
1325
+ }
1326
+ return deps.sort((left, right) => {
1327
+ if (left.type !== right.type) {
1328
+ return left.type.localeCompare(right.type);
1329
+ }
1330
+ const leftIdentity = "package" in left ? `package:${left.package}` : `url:${left.url}:${left.sha256}`;
1331
+ const rightIdentity = "package" in right ? `package:${right.package}` : `url:${right.url}:${right.sha256}`;
1332
+ if (leftIdentity !== rightIdentity) {
1333
+ return leftIdentity.localeCompare(rightIdentity);
1334
+ }
1335
+ if (left.type === "npm" && right.type === "npm") {
1336
+ return left.version.localeCompare(right.version);
1337
+ }
1338
+ return 0;
1339
+ });
1340
+ }
1341
+ function getPluginRuntimePostinstall() {
1342
+ const state = ensurePluginsLoaded();
1343
+ const commands = [];
1344
+ for (const plugin of state.pluginDefinitions) {
1345
+ for (const command of plugin.manifest.runtimePostinstall ?? []) {
1346
+ commands.push({
1347
+ cmd: command.cmd,
1348
+ ...command.args ? { args: [...command.args] } : {},
1349
+ ...command.sudo !== void 0 ? { sudo: command.sudo } : {}
1350
+ });
1351
+ }
1352
+ }
1353
+ return commands;
1354
+ }
1355
+ function getPluginOAuthConfig(provider) {
1356
+ const plugin = ensurePluginsLoaded().pluginsByName.get(provider);
1357
+ if (!plugin?.manifest.oauth) return void 0;
1358
+ const oauth = plugin.manifest.oauth;
1359
+ return {
1360
+ clientIdEnv: oauth.clientIdEnv,
1361
+ clientSecretEnv: oauth.clientSecretEnv,
1362
+ authorizeEndpoint: oauth.authorizeEndpoint,
1363
+ tokenEndpoint: oauth.tokenEndpoint,
1364
+ ...oauth.scope ? { scope: oauth.scope } : {},
1365
+ ...oauth.authorizeParams ? { authorizeParams: { ...oauth.authorizeParams } } : {},
1366
+ ...oauth.tokenAuthMethod ? { tokenAuthMethod: oauth.tokenAuthMethod } : {},
1367
+ ...oauth.tokenExtraHeaders ? { tokenExtraHeaders: { ...oauth.tokenExtraHeaders } } : {},
1368
+ callbackPath: `/api/oauth/callback/${plugin.manifest.name}`
1369
+ };
1370
+ }
1371
+ function getPluginSkillRoots() {
1372
+ const state = ensurePluginsLoaded();
1373
+ return [
1374
+ .../* @__PURE__ */ new Set([
1375
+ ...state.pluginDefinitions.map((plugin) => plugin.skillsDir),
1376
+ ...state.packageSkillRoots
1377
+ ])
1378
+ ];
1379
+ }
1380
+ function getPluginForSkillPath(skillPath) {
1381
+ const state = ensurePluginsLoaded();
1382
+ const resolvedSkillPath = path.resolve(skillPath);
1383
+ return state.pluginDefinitions.find((plugin) => {
1384
+ const resolvedSkillsDir = path.resolve(plugin.skillsDir);
1385
+ return resolvedSkillPath === resolvedSkillsDir || resolvedSkillPath.startsWith(`${resolvedSkillsDir}${path.sep}`);
1386
+ });
1387
+ }
1388
+ function getPluginDefinition(provider) {
1389
+ return ensurePluginsLoaded().pluginsByName.get(provider);
1390
+ }
1391
+ function isPluginProvider(provider) {
1392
+ return ensurePluginsLoaded().pluginsByName.has(provider);
1393
+ }
1394
+ function createPluginBroker(provider, deps) {
1395
+ const plugin = ensurePluginsLoaded().pluginsByName.get(provider);
1396
+ if (!plugin) {
1397
+ throw new Error(`Unknown plugin provider: "${provider}"`);
1398
+ }
1399
+ const { credentials, name } = plugin.manifest;
1400
+ if (!credentials) {
1401
+ throw new Error(`Provider "${name}" has no credentials configured`);
1402
+ }
1403
+ let broker;
1404
+ if (credentials.type === "oauth-bearer") {
1405
+ broker = createOAuthBearerBroker(plugin.manifest, credentials, deps);
1406
+ } else if (credentials.type === "github-app") {
1407
+ broker = createGitHubAppBroker(plugin.manifest, credentials);
1408
+ } else {
1409
+ throw new Error(`Unsupported credentials type for plugin "${name}"`);
1410
+ }
1411
+ setSpanAttributes({
1412
+ "app.plugin.name": name,
1413
+ "app.plugin.capabilities": plugin.manifest.capabilities,
1414
+ "app.plugin.has_oauth": Boolean(plugin.manifest.oauth)
1415
+ });
1416
+ return broker;
1417
+ }
1418
+
1419
+ export {
1420
+ resolveAuthTokenPlaceholder,
1421
+ parsePluginManifest,
1422
+ CredentialUnavailableError,
1423
+ buildOAuthTokenRequest,
1424
+ parseOAuthTokenResponse,
1425
+ getPluginCapabilityProviders,
1426
+ getPluginProviders,
1427
+ getPluginMcpProviders,
1428
+ getPluginRuntimeDependencies,
1429
+ getPluginRuntimePostinstall,
1430
+ getPluginOAuthConfig,
1431
+ getPluginSkillRoots,
1432
+ getPluginForSkillPath,
1433
+ getPluginDefinition,
1434
+ isPluginProvider,
1435
+ createPluginBroker
1436
+ };