@sentry/junior 0.203.0 → 0.204.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/dist/agent-CMyTD4DD.js +17509 -0
- package/dist/agent-hooks-Bbcp9e33.js +4040 -0
- package/dist/agent-runner-DNb1A49M.js +678 -0
- package/dist/api/schema.d.ts +2728 -2639
- package/dist/api/schema.js +4 -169
- package/dist/api.d.ts +23 -17
- package/dist/api.js +3881 -5326
- package/dist/app-B1TOqBI8.d.ts +246 -0
- package/dist/app.d.ts +4 -520
- package/dist/app.js +9522 -11875
- package/dist/auth-pause-state-DosEalIG.js +168 -0
- package/dist/catalog-runtime-BReos-D4.js +7 -0
- package/dist/checkpoint-CNrPyRJN.js +2450 -0
- package/dist/cli/chat.d.ts +20 -0
- package/dist/cli/chat.js +229 -336
- package/dist/cli/check.d.ts +8 -0
- package/dist/cli/check.js +513 -806
- package/dist/cli/env.d.ts +13 -0
- package/dist/cli/env.js +63 -12
- package/dist/cli/init.d.ts +3 -0
- package/dist/cli/init.js +90 -159
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.js +43 -64
- package/dist/cli/plugins.d.ts +13 -0
- package/dist/cli/plugins.js +148 -227
- package/dist/cli/run.d.ts +16 -0
- package/dist/cli/run.js +52 -8
- package/dist/cli/snapshot-create.d.ts +4 -0
- package/dist/cli/snapshot-create.js +71 -125
- package/dist/cli/upgrade.d.ts +15 -0
- package/dist/cli/upgrade.js +232 -335
- package/dist/config-nGCW1OTG.js +941 -0
- package/dist/context-B45_WqVX.js +298 -0
- package/dist/conversation-privacy-B4voFRSm.js +182 -0
- package/dist/conversation-work-C6y3T5Hx.js +51 -0
- package/dist/credential-sync-kXr1hvO1.js +2660 -0
- package/dist/db-BeyXKJOI.d.ts +5 -0
- package/dist/destinations-BeBiJeEM.js +29 -0
- package/dist/execution-stats-DWfSihcH.js +373 -0
- package/dist/executor-DcMSlI5c.js +721 -0
- package/dist/experimental-CIXf7Uye.js +46 -0
- package/dist/history-ClE4toxe.d.ts +32 -0
- package/dist/history-CqTuSAdI.js +441 -0
- package/dist/instrumentation.d.ts +3 -3
- package/dist/instrumentation.js +130 -176
- package/dist/job-queue-Drn_LXlM.js +63 -0
- package/dist/logging-BzAk1S_s.js +1210 -0
- package/dist/model-profile-CZmVoGdX.js +49 -0
- package/dist/nitro.d.ts +31 -36
- package/dist/nitro.js +275 -456
- package/dist/oauth-callback-server-JiftokvA.js +130 -0
- package/dist/oauth-relay-CdCnu80W.js +62 -0
- package/dist/package-discovery-7a9YqLhE.js +455 -0
- package/dist/pending-auth-CWmSdZVg.js +185 -0
- package/dist/plugin-module-BKGOXca2.js +81 -0
- package/dist/plugins-BI9A1vKA.d.ts +95 -0
- package/dist/plugins-egKWjgue.js +99 -0
- package/dist/private-trace-result-BLvBjy_e.js +16 -0
- package/dist/profile-CrT5nXbw.js +861 -0
- package/dist/registry-DH1xUo2N.js +1419 -0
- package/dist/reporting-schema-DosTN__Q.js +105 -0
- package/dist/resolve-CZHnfxaS.js +546 -0
- package/dist/rolldown-runtime-B4FMCO8f.js +28 -0
- package/dist/runner-CZV2sC46.js +392 -0
- package/dist/sandbox-egress-signals-bHZfmhbd.js +46 -0
- package/dist/schema-Rn8-XEaN.js +1140 -0
- package/dist/sentry-CD3SdJl-.js +26 -0
- package/dist/skills-CIcDQSgM.js +316 -0
- package/dist/spawn-DdkUFRmq.js +29 -0
- package/dist/stats-DTUbAb88.js +2634 -0
- package/dist/store-C5qxF2ac.d.ts +208 -0
- package/dist/task-queue-CJIR4RF7.js +136 -0
- package/dist/task-runner-D-O3A-Cp.js +639 -0
- package/dist/thread-state-CRc9jTJS.js +431 -0
- package/dist/turn-execution-BkinIdJF.js +47 -0
- package/dist/turn-lifecycle-DQV19VTW.js +54 -0
- package/dist/usage-D3EbSO7w.js +296 -0
- package/dist/usage-schema-CKaDkH_j.js +20 -0
- package/dist/validation-C2CR-OX3.js +40 -0
- package/dist/vercel-queue-DIkVcfK4.js +167 -0
- package/dist/vercel.d.ts +6 -6
- package/dist/vercel.js +14 -6
- package/dist/version-CnXZ614E.d.ts +5 -0
- package/dist/version-DxcWdoKa.js +8 -0
- package/dist/version.d.ts +2 -4
- package/dist/version.js +2 -6
- package/dist/web-authorization-RMBxAR_t.js +76 -0
- package/dist/web-input-3EPfpf55.js +494 -0
- package/dist/work-fGofI7Op.js +509 -0
- package/dist/worker-CsOLq-mR.js +625 -0
- package/package.json +8 -8
- package/dist/agent-hooks-GB2VPTNA.js +0 -51
- package/dist/catalog-runtime-UBLIWD3O.js +0 -14
- package/dist/chunk-2LYHWMR4.js +0 -46
- package/dist/chunk-352OWAZH.js +0 -320
- package/dist/chunk-3Y47VMHV.js +0 -6
- package/dist/chunk-3YJSKOWF.js +0 -164
- package/dist/chunk-4KSSKTQ5.js +0 -229
- package/dist/chunk-4UO6FK4G.js +0 -64
- package/dist/chunk-5AHX53NQ.js +0 -717
- package/dist/chunk-5J3E5FWA.js +0 -700
- package/dist/chunk-5PDAG6O3.js +0 -206
- package/dist/chunk-6Y5B5PEX.js +0 -80
- package/dist/chunk-7GWQ5S5R.js +0 -2086
- package/dist/chunk-7H2VZTWN.js +0 -103
- package/dist/chunk-7NL7T6VU.js +0 -406
- package/dist/chunk-A5L27LE4.js +0 -387
- package/dist/chunk-ADIAWL34.js +0 -81
- package/dist/chunk-ASTEMBKJ.js +0 -519
- package/dist/chunk-CEMMLV5L.js +0 -98
- package/dist/chunk-CM5HNV2W.js +0 -131
- package/dist/chunk-CRTRBBRI.js +0 -105
- package/dist/chunk-DA2KGZZI.js +0 -13
- package/dist/chunk-DIB4GX3G.js +0 -8
- package/dist/chunk-DKIJ2RP5.js +0 -68
- package/dist/chunk-DWPN6SCH.js +0 -172
- package/dist/chunk-EDKUFGPL.js +0 -64
- package/dist/chunk-ENY6SKIU.js +0 -1228
- package/dist/chunk-F4MGCZOX.js +0 -2943
- package/dist/chunk-FA6POGLH.js +0 -70
- package/dist/chunk-FSUR7GTG.js +0 -20475
- package/dist/chunk-GKEOHJL5.js +0 -1856
- package/dist/chunk-GMCB6W5Y.js +0 -333
- package/dist/chunk-IE2VPG7O.js +0 -1135
- package/dist/chunk-IZ5B5IHQ.js +0 -3656
- package/dist/chunk-JAL3CH4G.js +0 -27
- package/dist/chunk-KIIEUXD3.js +0 -1764
- package/dist/chunk-KXU4NIL7.js +0 -692
- package/dist/chunk-MTM5IFCY.js +0 -1190
- package/dist/chunk-N7TMSOZH.js +0 -235
- package/dist/chunk-NCEBK6PH.js +0 -36
- package/dist/chunk-NHJU2ZAL.js +0 -56
- package/dist/chunk-P6KTRNVC.js +0 -321
- package/dist/chunk-PDO5BLNM.js +0 -22
- package/dist/chunk-PDRNLLFT.js +0 -3295
- package/dist/chunk-PEQXYSSE.js +0 -45
- package/dist/chunk-PKEX2AQX.js +0 -198
- package/dist/chunk-PSJCGIGO.js +0 -1163
- package/dist/chunk-PZUSUL55.js +0 -506
- package/dist/chunk-R45M4TUS.js +0 -37
- package/dist/chunk-RC745GGZ.js +0 -122
- package/dist/chunk-SP3QKZXJ.js +0 -820
- package/dist/chunk-TXKUKMVO.js +0 -417
- package/dist/chunk-UE4GPWSD.js +0 -931
- package/dist/chunk-UIE3R5XU.js +0 -17
- package/dist/chunk-UXPG6ZIN.js +0 -84
- package/dist/chunk-VFUK3X5B.js +0 -50
- package/dist/chunk-VNMJWREK.js +0 -787
- package/dist/chunk-VZ7CSOX2.js +0 -706
- package/dist/chunk-WCNQJKFT.js +0 -10
- package/dist/chunk-WDZR3QQM.js +0 -135
- package/dist/chunk-WLYVJCHV.js +0 -1433
- package/dist/chunk-WS7NAFTM.js +0 -1520
- package/dist/chunk-X6GDDWQH.js +0 -60
- package/dist/chunk-XPDEYNQI.js +0 -102
- package/dist/chunk-XRW44VGM.js +0 -58
- package/dist/chunk-Y2CM7HXH.js +0 -111
- package/dist/chunk-Y7X25LFY.js +0 -15
- package/dist/conversation-work-VIOQ5ZF3.js +0 -54
- package/dist/db-NGDJV7YH.js +0 -26
- package/dist/discovery-DWVDM7UK.js +0 -44
- package/dist/experimental-P5FZM4CU.js +0 -12
- package/dist/oauth-callback-server-YFY7YEGK.js +0 -243
- package/dist/oauth-relay-5XPVRPYB.js +0 -8
- package/dist/plugins-YTYJMMJF.js +0 -14
- package/dist/runner-ORTNSB2Q.js +0 -522
- package/dist/sandbox-egress-signals-FIL7EGPR.js +0 -73
- package/dist/sentry-MB3NL3IR.js +0 -42
- package/dist/skills-H376WQOH.js +0 -27
- package/dist/spawn-T43WHLOS.js +0 -42
- package/dist/task-runner-LBZWF32V.js +0 -36
- package/dist/validation-REJLRKI6.js +0 -17
- package/dist/work-3KJ3RE76.js +0 -51
- package/dist/worker-QFEBU72E.js +0 -24
|
@@ -0,0 +1,1419 @@
|
|
|
1
|
+
import { _ as setSpanAttributes, d as logInfo, f as logWarn } from "./logging-BzAk1S_s.js";
|
|
2
|
+
import { i as credentialUserSubjectId } from "./context-B45_WqVX.js";
|
|
3
|
+
import { n as normalizePluginPackageNames, o as pluginRoots, t as discoverInstalledPluginPackageContent } from "./package-discovery-7a9YqLhE.js";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { randomUUID } from "node:crypto";
|
|
7
|
+
import { parse } from "yaml";
|
|
8
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
9
|
+
//#region src/chat/plugins/inline-manifest-source.ts
|
|
10
|
+
function setDefined$1(target, key, value) {
|
|
11
|
+
if (value !== void 0) target[key] = value;
|
|
12
|
+
}
|
|
13
|
+
function isRecord(value) {
|
|
14
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
15
|
+
}
|
|
16
|
+
function unqualifyManifestToken(name, value) {
|
|
17
|
+
if (typeof name === "string" && typeof value === "string" && value.startsWith(`${name}.`)) return value.slice(name.length + 1);
|
|
18
|
+
return value;
|
|
19
|
+
}
|
|
20
|
+
function inlineTokenListSource(name, values) {
|
|
21
|
+
if (values === void 0 || !Array.isArray(values)) return values;
|
|
22
|
+
return values.map((value) => unqualifyManifestToken(name, value));
|
|
23
|
+
}
|
|
24
|
+
function inlineCredentialsSource(credentials) {
|
|
25
|
+
if (credentials === void 0 || !isRecord(credentials)) return credentials;
|
|
26
|
+
const result = {};
|
|
27
|
+
setDefined$1(result, "type", credentials.type);
|
|
28
|
+
setDefined$1(result, "domains", credentials.domains);
|
|
29
|
+
setDefined$1(result, "api-headers", credentials.apiHeaders);
|
|
30
|
+
setDefined$1(result, "auth-token-env", credentials.authTokenEnv);
|
|
31
|
+
setDefined$1(result, "auth-token-placeholder", credentials.authTokenPlaceholder);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
function inlineMcpSource(mcp) {
|
|
35
|
+
if (mcp === void 0 || !isRecord(mcp)) return mcp;
|
|
36
|
+
const result = {};
|
|
37
|
+
setDefined$1(result, "transport", mcp.transport);
|
|
38
|
+
setDefined$1(result, "url", mcp.url);
|
|
39
|
+
setDefined$1(result, "headers", mcp.headers);
|
|
40
|
+
setDefined$1(result, "allowed-tools", mcp.allowedTools);
|
|
41
|
+
setDefined$1(result, "wrapped-tools", mcp.wrappedTools);
|
|
42
|
+
return result;
|
|
43
|
+
}
|
|
44
|
+
function inlineOauthSource(oauth) {
|
|
45
|
+
if (oauth === void 0 || !isRecord(oauth)) return oauth;
|
|
46
|
+
const result = {};
|
|
47
|
+
setDefined$1(result, "client-id-env", oauth.clientIdEnv);
|
|
48
|
+
setDefined$1(result, "client-secret-env", oauth.clientSecretEnv);
|
|
49
|
+
setDefined$1(result, "authorize-endpoint", oauth.authorizeEndpoint);
|
|
50
|
+
setDefined$1(result, "token-endpoint", oauth.tokenEndpoint);
|
|
51
|
+
setDefined$1(result, "scope", oauth.scope);
|
|
52
|
+
setDefined$1(result, "authorize-params", oauth.authorizeParams);
|
|
53
|
+
setDefined$1(result, "token-auth-method", oauth.tokenAuthMethod);
|
|
54
|
+
setDefined$1(result, "token-extra-headers", oauth.tokenExtraHeaders);
|
|
55
|
+
setDefined$1(result, "treat-empty-scope-as-unreported", oauth.treatEmptyScopeAsUnreported);
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function inlineTargetSource(name, target) {
|
|
59
|
+
if (target === void 0 || !isRecord(target)) return target;
|
|
60
|
+
const result = {};
|
|
61
|
+
setDefined$1(result, "type", target.type);
|
|
62
|
+
setDefined$1(result, "config-key", unqualifyManifestToken(name, target.configKey));
|
|
63
|
+
setDefined$1(result, "command-flags", target.commandFlags);
|
|
64
|
+
return result;
|
|
65
|
+
}
|
|
66
|
+
/** Convert inline JavaScript plugin manifests to the canonical source shape. */
|
|
67
|
+
function inlineManifestSource(manifest) {
|
|
68
|
+
const result = {};
|
|
69
|
+
setDefined$1(result, "name", manifest.name);
|
|
70
|
+
setDefined$1(result, "display-name", manifest.displayName);
|
|
71
|
+
setDefined$1(result, "description", manifest.description);
|
|
72
|
+
setDefined$1(result, "config-keys", inlineTokenListSource(manifest.name, manifest.configKeys));
|
|
73
|
+
setDefined$1(result, "domains", manifest.domains);
|
|
74
|
+
setDefined$1(result, "api-headers", manifest.apiHeaders);
|
|
75
|
+
setDefined$1(result, "command-env", manifest.commandEnv);
|
|
76
|
+
setDefined$1(result, "env-vars", manifest.envVars);
|
|
77
|
+
setDefined$1(result, "credentials", inlineCredentialsSource(manifest.credentials));
|
|
78
|
+
setDefined$1(result, "runtime-dependencies", manifest.runtimeDependencies);
|
|
79
|
+
setDefined$1(result, "runtime-postinstall", manifest.runtimePostinstall);
|
|
80
|
+
setDefined$1(result, "mcp", inlineMcpSource(manifest.mcp));
|
|
81
|
+
setDefined$1(result, "oauth", inlineOauthSource(manifest.oauth));
|
|
82
|
+
setDefined$1(result, "target", inlineTargetSource(manifest.name, manifest.target));
|
|
83
|
+
return result;
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
//#region src/chat/plugins/manifest.ts
|
|
87
|
+
const PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
88
|
+
const SHORT_CONFIG_KEY_RE = /^[a-z0-9]+(\.[a-z0-9-]+)*$/;
|
|
89
|
+
const TARGET_FLAG_RE = /^-{1,2}[A-Za-z0-9][A-Za-z0-9-]*$/;
|
|
90
|
+
const AUTH_TOKEN_ENV_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
91
|
+
const ENV_VAR_NAME_RE = /^[A-Z_][A-Z0-9_]*$/;
|
|
92
|
+
const ENV_PLACEHOLDER_RE$2 = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
93
|
+
const 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])?$/;
|
|
94
|
+
const RUNTIME_POSTINSTALL_CMD_RE = /^[A-Za-z0-9._/-]+$/;
|
|
95
|
+
const RESERVED_AUTHORIZE_PARAM_KEYS = /* @__PURE__ */ new Set([
|
|
96
|
+
"client_id",
|
|
97
|
+
"scope",
|
|
98
|
+
"state",
|
|
99
|
+
"redirect_uri",
|
|
100
|
+
"response_type"
|
|
101
|
+
]);
|
|
102
|
+
const FORBIDDEN_API_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
|
|
103
|
+
const FORBIDDEN_TOKEN_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
|
|
104
|
+
const nonEmptyTrimmedString = z.string().transform((value) => value.trim()).pipe(z.string().min(1, { error: "must be a non-empty string" }));
|
|
105
|
+
const nonEmptyStringArraySchema = (fieldName, options = {}) => z.array(z.string(), { error: "must be an array of strings when provided" }).min(1, { error: options.nonEmptyMessage ?? "must be a non-empty array of strings when provided" }).transform((values, ctx) => {
|
|
106
|
+
const result = [];
|
|
107
|
+
const seen = /* @__PURE__ */ new Set();
|
|
108
|
+
for (const rawValue of values) {
|
|
109
|
+
const value = rawValue.trim();
|
|
110
|
+
if (!value) {
|
|
111
|
+
ctx.addIssue({
|
|
112
|
+
code: z.ZodIssueCode.custom,
|
|
113
|
+
message: `${fieldName} entries must be non-empty strings`
|
|
114
|
+
});
|
|
115
|
+
return z.NEVER;
|
|
116
|
+
}
|
|
117
|
+
if (seen.has(value)) continue;
|
|
118
|
+
seen.add(value);
|
|
119
|
+
result.push(value);
|
|
120
|
+
}
|
|
121
|
+
return result;
|
|
122
|
+
});
|
|
123
|
+
const envVarString = nonEmptyTrimmedString.refine((value) => AUTH_TOKEN_ENV_RE.test(value), { error: "must be an uppercase env var name" });
|
|
124
|
+
const httpsUrlString = nonEmptyTrimmedString.superRefine((value, ctx) => {
|
|
125
|
+
let parsed;
|
|
126
|
+
try {
|
|
127
|
+
parsed = new URL(value);
|
|
128
|
+
} catch {
|
|
129
|
+
ctx.addIssue({
|
|
130
|
+
code: z.ZodIssueCode.custom,
|
|
131
|
+
message: "must be a valid URL"
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (parsed.protocol !== "https:") ctx.addIssue({
|
|
136
|
+
code: z.ZodIssueCode.custom,
|
|
137
|
+
message: "must use https"
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
const stringMapSchema = z.record(z.string(), z.unknown()).transform((record, ctx) => {
|
|
141
|
+
const entries = Object.entries(record);
|
|
142
|
+
const result = {};
|
|
143
|
+
const seen = /* @__PURE__ */ new Set();
|
|
144
|
+
for (const [rawKey, rawValue] of entries) {
|
|
145
|
+
const key = rawKey.trim();
|
|
146
|
+
if (!key) {
|
|
147
|
+
ctx.addIssue({
|
|
148
|
+
code: z.ZodIssueCode.custom,
|
|
149
|
+
message: "keys must be non-empty strings"
|
|
150
|
+
});
|
|
151
|
+
return z.NEVER;
|
|
152
|
+
}
|
|
153
|
+
if (typeof rawValue !== "string" || !rawValue.trim()) {
|
|
154
|
+
ctx.addIssue({
|
|
155
|
+
code: z.ZodIssueCode.custom,
|
|
156
|
+
message: `${key} must be a non-empty string`
|
|
157
|
+
});
|
|
158
|
+
return z.NEVER;
|
|
159
|
+
}
|
|
160
|
+
const normalizedKey = key.toLowerCase();
|
|
161
|
+
if (seen.has(normalizedKey)) {
|
|
162
|
+
ctx.addIssue({
|
|
163
|
+
code: z.ZodIssueCode.custom,
|
|
164
|
+
message: `${key} is duplicated`
|
|
165
|
+
});
|
|
166
|
+
return z.NEVER;
|
|
167
|
+
}
|
|
168
|
+
seen.add(normalizedKey);
|
|
169
|
+
result[key] = rawValue.trim();
|
|
170
|
+
}
|
|
171
|
+
return result;
|
|
172
|
+
});
|
|
173
|
+
const domainsSchema = z.array(z.unknown()).min(1, { error: "must be a non-empty array of strings" }).transform((domains, ctx) => {
|
|
174
|
+
return domains.map((rawDomain) => {
|
|
175
|
+
const domain = typeof rawDomain === "string" ? rawDomain.trim().toLowerCase() : "";
|
|
176
|
+
if (!domain) {
|
|
177
|
+
ctx.addIssue({
|
|
178
|
+
code: z.ZodIssueCode.custom,
|
|
179
|
+
message: "entries must be non-empty strings"
|
|
180
|
+
});
|
|
181
|
+
return z.NEVER;
|
|
182
|
+
}
|
|
183
|
+
if (!API_DOMAIN_RE.test(domain)) {
|
|
184
|
+
ctx.addIssue({
|
|
185
|
+
code: z.ZodIssueCode.custom,
|
|
186
|
+
message: "entries must be valid domain names"
|
|
187
|
+
});
|
|
188
|
+
return z.NEVER;
|
|
189
|
+
}
|
|
190
|
+
return domain;
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
const oauthBearerCredentialsSchema = z.object({ domains: domainsSchema.optional() }).passthrough().extend({
|
|
194
|
+
"api-headers": stringMapSchema.optional(),
|
|
195
|
+
"auth-token-env": envVarString,
|
|
196
|
+
"auth-token-placeholder": nonEmptyTrimmedString.optional(),
|
|
197
|
+
type: z.literal("oauth-bearer")
|
|
198
|
+
});
|
|
199
|
+
const runtimeDependencyEntrySchema = z.object({
|
|
200
|
+
type: z.enum(["npm", "system"]),
|
|
201
|
+
package: z.string().optional(),
|
|
202
|
+
version: z.string().optional(),
|
|
203
|
+
url: z.string().optional(),
|
|
204
|
+
sha256: z.string().optional()
|
|
205
|
+
}).passthrough();
|
|
206
|
+
const runtimePostinstallCommandSourceSchema = z.object({
|
|
207
|
+
cmd: nonEmptyTrimmedString,
|
|
208
|
+
args: z.array(z.string(), { error: "args must be an array of strings when provided" }).optional(),
|
|
209
|
+
sudo: z.boolean({ error: "sudo must be a boolean when provided" }).optional()
|
|
210
|
+
}).passthrough();
|
|
211
|
+
const oauthSourceSchema = z.object({
|
|
212
|
+
"client-id-env": envVarString,
|
|
213
|
+
"client-secret-env": envVarString,
|
|
214
|
+
"authorize-endpoint": httpsUrlString,
|
|
215
|
+
"token-endpoint": httpsUrlString,
|
|
216
|
+
scope: nonEmptyTrimmedString.optional(),
|
|
217
|
+
"authorize-params": stringMapSchema.optional(),
|
|
218
|
+
"token-extra-headers": stringMapSchema.optional(),
|
|
219
|
+
"token-auth-method": nonEmptyTrimmedString.refine((value) => value === "body" || value === "basic", { error: "must be \"body\" or \"basic\"" }).optional(),
|
|
220
|
+
"treat-empty-scope-as-unreported": z.boolean().optional()
|
|
221
|
+
}).passthrough();
|
|
222
|
+
const mcpSourceSchema = z.object({
|
|
223
|
+
transport: nonEmptyTrimmedString.refine((value) => value === "http", { error: "must be \"http\"" }).optional(),
|
|
224
|
+
url: httpsUrlString,
|
|
225
|
+
headers: stringMapSchema.optional(),
|
|
226
|
+
"allowed-tools": nonEmptyStringArraySchema("allowed-tools").optional(),
|
|
227
|
+
"wrapped-tools": nonEmptyStringArraySchema("wrapped-tools").optional()
|
|
228
|
+
}).passthrough();
|
|
229
|
+
const targetSourceSchema = z.object({
|
|
230
|
+
type: nonEmptyTrimmedString.refine((value) => PLUGIN_NAME_RE.test(value), { error: "type must be a lowercase target identifier" }),
|
|
231
|
+
"config-key": nonEmptyTrimmedString,
|
|
232
|
+
"command-flags": nonEmptyStringArraySchema("command-flags").optional()
|
|
233
|
+
}).passthrough();
|
|
234
|
+
const manifestSourceSchema = z.object({
|
|
235
|
+
name: z.string().refine((value) => PLUGIN_NAME_RE.test(value), { error: "invalid" }),
|
|
236
|
+
"display-name": nonEmptyTrimmedString,
|
|
237
|
+
description: nonEmptyTrimmedString,
|
|
238
|
+
"config-keys": z.array(z.string(), { error: "must be an array when provided" }).optional(),
|
|
239
|
+
domains: domainsSchema.optional(),
|
|
240
|
+
"api-headers": stringMapSchema.optional(),
|
|
241
|
+
"command-env": stringMapSchema.optional(),
|
|
242
|
+
credentials: z.record(z.string(), z.unknown(), { error: "must be an object when provided" }).optional(),
|
|
243
|
+
"runtime-dependencies": z.array(z.unknown(), { error: "must be an array" }).optional(),
|
|
244
|
+
"runtime-postinstall": z.array(z.unknown(), { error: "must be an array" }).optional(),
|
|
245
|
+
"env-vars": z.record(z.string(), z.unknown(), { error: "must be an object" }).optional(),
|
|
246
|
+
mcp: z.record(z.string(), z.unknown(), { error: "must be an object" }).optional(),
|
|
247
|
+
oauth: z.record(z.string(), z.unknown(), { error: "must be an object" }).optional(),
|
|
248
|
+
target: z.record(z.string(), z.unknown(), { error: "must be an object" }).optional()
|
|
249
|
+
}).passthrough();
|
|
250
|
+
function setDefined(target, key, value) {
|
|
251
|
+
if (value !== void 0) target[key] = value;
|
|
252
|
+
}
|
|
253
|
+
function manifestConfigPatch(config) {
|
|
254
|
+
const result = {};
|
|
255
|
+
setDefined(result, "display-name", config.displayName);
|
|
256
|
+
setDefined(result, "description", config.description);
|
|
257
|
+
setDefined(result, "config-keys", config.configKeys);
|
|
258
|
+
setDefined(result, "domains", config.domains);
|
|
259
|
+
setDefined(result, "api-headers", config.apiHeaders);
|
|
260
|
+
setDefined(result, "command-env", config.commandEnv);
|
|
261
|
+
setDefined(result, "env-vars", config.envVars);
|
|
262
|
+
if (config.credentials !== void 0) {
|
|
263
|
+
if (!config.credentials) result.credentials = null;
|
|
264
|
+
else {
|
|
265
|
+
const credentials = {};
|
|
266
|
+
setDefined(credentials, "type", config.credentials.type);
|
|
267
|
+
setDefined(credentials, "domains", config.credentials.domains);
|
|
268
|
+
setDefined(credentials, "api-headers", config.credentials.apiHeaders);
|
|
269
|
+
setDefined(credentials, "auth-token-env", config.credentials.authTokenEnv);
|
|
270
|
+
setDefined(credentials, "auth-token-placeholder", config.credentials.authTokenPlaceholder);
|
|
271
|
+
result.credentials = credentials;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
setDefined(result, "runtime-dependencies", config.runtimeDependencies);
|
|
275
|
+
setDefined(result, "runtime-postinstall", config.runtimePostinstall);
|
|
276
|
+
if (config.mcp !== void 0) {
|
|
277
|
+
if (!config.mcp) result.mcp = null;
|
|
278
|
+
else {
|
|
279
|
+
const mcp = {};
|
|
280
|
+
setDefined(mcp, "transport", config.mcp.transport);
|
|
281
|
+
setDefined(mcp, "url", config.mcp.url);
|
|
282
|
+
setDefined(mcp, "headers", config.mcp.headers);
|
|
283
|
+
setDefined(mcp, "allowed-tools", config.mcp.allowedTools);
|
|
284
|
+
setDefined(mcp, "wrapped-tools", config.mcp.wrappedTools);
|
|
285
|
+
result.mcp = mcp;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
if (config.oauth !== void 0) {
|
|
289
|
+
if (!config.oauth) result.oauth = null;
|
|
290
|
+
else {
|
|
291
|
+
const oauth = {};
|
|
292
|
+
setDefined(oauth, "client-id-env", config.oauth.clientIdEnv);
|
|
293
|
+
setDefined(oauth, "client-secret-env", config.oauth.clientSecretEnv);
|
|
294
|
+
setDefined(oauth, "authorize-endpoint", config.oauth.authorizeEndpoint);
|
|
295
|
+
setDefined(oauth, "token-endpoint", config.oauth.tokenEndpoint);
|
|
296
|
+
setDefined(oauth, "scope", config.oauth.scope);
|
|
297
|
+
setDefined(oauth, "authorize-params", config.oauth.authorizeParams);
|
|
298
|
+
setDefined(oauth, "token-auth-method", config.oauth.tokenAuthMethod);
|
|
299
|
+
setDefined(oauth, "token-extra-headers", config.oauth.tokenExtraHeaders);
|
|
300
|
+
setDefined(oauth, "treat-empty-scope-as-unreported", config.oauth.treatEmptyScopeAsUnreported);
|
|
301
|
+
result.oauth = oauth;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
if (config.target !== void 0) {
|
|
305
|
+
if (!config.target) result.target = null;
|
|
306
|
+
else {
|
|
307
|
+
const target = {};
|
|
308
|
+
setDefined(target, "type", config.target.type);
|
|
309
|
+
setDefined(target, "config-key", config.target.configKey);
|
|
310
|
+
setDefined(target, "command-flags", config.target.commandFlags);
|
|
311
|
+
result.target = target;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return result;
|
|
315
|
+
}
|
|
316
|
+
function isPlainRecord(value) {
|
|
317
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
318
|
+
}
|
|
319
|
+
function mergeManifestConfig(base, patch, options = {}) {
|
|
320
|
+
const merged = { ...base };
|
|
321
|
+
for (const [key, value] of Object.entries(patch)) {
|
|
322
|
+
if (options.root && key === "name") throw new Error("plugins.manifests cannot change plugin names");
|
|
323
|
+
if (value === null) {
|
|
324
|
+
delete merged[key];
|
|
325
|
+
continue;
|
|
326
|
+
}
|
|
327
|
+
const current = merged[key];
|
|
328
|
+
if (isPlainRecord(current) && isPlainRecord(value)) {
|
|
329
|
+
merged[key] = mergeManifestConfig(current, value);
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
merged[key] = value;
|
|
333
|
+
}
|
|
334
|
+
return merged;
|
|
335
|
+
}
|
|
336
|
+
function applyManifestConfig(source, config) {
|
|
337
|
+
const name = source.name;
|
|
338
|
+
if (typeof name !== "string") return source;
|
|
339
|
+
const manifestConfig = config?.manifests?.[name];
|
|
340
|
+
if (manifestConfig && "name" in manifestConfig) throw new Error("plugins.manifests cannot change plugin names");
|
|
341
|
+
return manifestConfig ? mergeManifestConfig(source, manifestConfigPatch(manifestConfig), { root: true }) : source;
|
|
342
|
+
}
|
|
343
|
+
function formatPath(path) {
|
|
344
|
+
return path.map((segment) => String(segment)).join(".");
|
|
345
|
+
}
|
|
346
|
+
function issueMessage(error, prefix) {
|
|
347
|
+
const issue = error.issues[0];
|
|
348
|
+
if (!issue) return prefix;
|
|
349
|
+
const suffix = formatPath(issue.path);
|
|
350
|
+
return suffix ? `${prefix}.${suffix} ${issue.message}` : `${prefix} ${issue.message}`;
|
|
351
|
+
}
|
|
352
|
+
function normalizeStringMap(value, prefix, options = {}) {
|
|
353
|
+
if (!value) return;
|
|
354
|
+
const keys = Object.keys(value);
|
|
355
|
+
if (keys.length === 0) return;
|
|
356
|
+
for (const key of keys) {
|
|
357
|
+
const normalizedKey = key.toLowerCase();
|
|
358
|
+
if (options.reservedKeys?.has(normalizedKey)) throw new Error(`${prefix}.${key} is reserved by the runtime`);
|
|
359
|
+
if (options.forbiddenKeys?.has(normalizedKey)) throw new Error(`${prefix}.${key} is not allowed`);
|
|
360
|
+
}
|
|
361
|
+
return value;
|
|
362
|
+
}
|
|
363
|
+
function envReferences(value) {
|
|
364
|
+
return Array.from(value.matchAll(ENV_PLACEHOLDER_RE$2), (match) => {
|
|
365
|
+
return match[1];
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
function assertDeclaredEnvReferences(value, envVars, context) {
|
|
369
|
+
for (const name of envReferences(value)) {
|
|
370
|
+
if (!Object.prototype.hasOwnProperty.call(envVars, name)) throw new Error(`${context} references env var ${name} which is not declared in env-vars`);
|
|
371
|
+
if (envVars[name]?.default !== void 0) throw new Error(`${context} references env var ${name}, but API header env vars must not declare defaults`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
function normalizeRequiredApiHeaders(value, prefix, envVars) {
|
|
375
|
+
const apiHeaders = normalizeStringMap(value, prefix);
|
|
376
|
+
if (!apiHeaders) throw new Error(`${prefix} must contain at least one header`);
|
|
377
|
+
for (const [key, headerValue] of Object.entries(apiHeaders)) assertDeclaredEnvReferences(headerValue, envVars, `${prefix}.${key}`);
|
|
378
|
+
return apiHeaders;
|
|
379
|
+
}
|
|
380
|
+
function assertCommandEnvReferencesDeclared(value, envVars, context) {
|
|
381
|
+
for (const name of envReferences(value)) if (!Object.prototype.hasOwnProperty.call(envVars, name)) throw new Error(`${context} references env var ${name} which is not declared in env-vars`);
|
|
382
|
+
}
|
|
383
|
+
function expandCommandEnvPlaceholders(template, envVars, context) {
|
|
384
|
+
return template.replace(ENV_PLACEHOLDER_RE$2, (match, name) => {
|
|
385
|
+
if (envVars[name]?.default === void 0) return match;
|
|
386
|
+
return expandEnvPlaceholders(match, envVars, context);
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
function normalizeCommandEnv(value, prefix, envVars) {
|
|
390
|
+
const env = normalizeStringMap(value, prefix);
|
|
391
|
+
if (!env) throw new Error(`${prefix} must contain at least one env var`);
|
|
392
|
+
for (const [key, envValue] of Object.entries(env)) {
|
|
393
|
+
if (!ENV_VAR_NAME_RE.test(key)) throw new Error(`${prefix}.${key} must be an uppercase env var name`);
|
|
394
|
+
assertCommandEnvReferencesDeclared(envValue, envVars, `${prefix}.${key}`);
|
|
395
|
+
}
|
|
396
|
+
return Object.fromEntries(Object.entries(env).map(([key, envValue]) => [key, expandCommandEnvPlaceholders(envValue, envVars, `${prefix}.${key}`)]));
|
|
397
|
+
}
|
|
398
|
+
function assertCommandEnvDoesNotExposeHostSecretRefs(commandEnv, apiHeaders, credentials, oauth, pluginName) {
|
|
399
|
+
if (!commandEnv) return;
|
|
400
|
+
const hostOnlyRefs = /* @__PURE__ */ new Set();
|
|
401
|
+
for (const value of Object.values(apiHeaders ?? {})) for (const name of envReferences(value)) hostOnlyRefs.add(name);
|
|
402
|
+
if (credentials) {
|
|
403
|
+
if (credentials.authTokenEnv) hostOnlyRefs.add(credentials.authTokenEnv);
|
|
404
|
+
}
|
|
405
|
+
if (oauth) {
|
|
406
|
+
hostOnlyRefs.add(oauth.clientIdEnv);
|
|
407
|
+
hostOnlyRefs.add(oauth.clientSecretEnv);
|
|
408
|
+
}
|
|
409
|
+
for (const [key, value] of Object.entries(commandEnv)) for (const name of envReferences(value)) if (hostOnlyRefs.has(name)) throw new Error(`Plugin ${pluginName} command-env.${key} references env var ${name}, but credential/API header env vars must stay host-only`);
|
|
410
|
+
}
|
|
411
|
+
function assertCommandEnvHostRefsAreExplicitlyExposed(commandEnv, envVars, pluginName) {
|
|
412
|
+
if (!commandEnv) return;
|
|
413
|
+
for (const [key, value] of Object.entries(commandEnv)) for (const name of envReferences(value)) {
|
|
414
|
+
const declaration = envVars[name];
|
|
415
|
+
if (declaration && declaration.default === void 0 && declaration.exposeToCommandEnv !== true) throw new Error(`Plugin ${pluginName} command-env.${key} references env var ${name}, but env-vars.${name} must set expose-to-command-env: true before host env can be exposed to sandbox`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
function normalizeCredentials(data, name) {
|
|
419
|
+
if (data.type !== "oauth-bearer") throw new Error(`Plugin ${name} has unsupported credentials.type: "${String(data.type)}"`);
|
|
420
|
+
const result = oauthBearerCredentialsSchema.safeParse(data);
|
|
421
|
+
if (!result.success) throw new Error(issueMessage(result.error, `Plugin ${name} credentials`));
|
|
422
|
+
if (!result.data.domains) throw new Error(`Plugin ${name} credentials requires domains`);
|
|
423
|
+
const domains = result.data.domains;
|
|
424
|
+
const apiHeaders = result.data["api-headers"] ? normalizeStringMap(result.data["api-headers"], `Plugin ${name} credentials.api-headers`, { forbiddenKeys: FORBIDDEN_API_HEADER_NAMES }) : void 0;
|
|
425
|
+
return {
|
|
426
|
+
type: "oauth-bearer",
|
|
427
|
+
domains,
|
|
428
|
+
...apiHeaders ? { apiHeaders } : void 0,
|
|
429
|
+
authTokenEnv: result.data["auth-token-env"],
|
|
430
|
+
...result.data["auth-token-placeholder"] ? { authTokenPlaceholder: result.data["auth-token-placeholder"] } : void 0
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
function normalizeRuntimeDependencies(entries, name) {
|
|
434
|
+
const parsed = [];
|
|
435
|
+
const seen = /* @__PURE__ */ new Set();
|
|
436
|
+
for (const entry of entries) {
|
|
437
|
+
const result = runtimeDependencyEntrySchema.safeParse(entry);
|
|
438
|
+
if (!result.success) throw new Error(issueMessage(result.error, `Plugin ${name} runtime-dependencies entries`));
|
|
439
|
+
const record = result.data;
|
|
440
|
+
const packageName = typeof record.package === "string" ? record.package.trim() : "";
|
|
441
|
+
const packageUrl = typeof record.url === "string" ? record.url.trim() : "";
|
|
442
|
+
const version = record.version;
|
|
443
|
+
const sha256 = record.sha256;
|
|
444
|
+
if (record.type === "npm") {
|
|
445
|
+
if (!packageName) throw new Error(`Plugin ${name} runtime dependency package must be a non-empty string`);
|
|
446
|
+
if (record.url !== void 0 || sha256 !== void 0) throw new Error(`Plugin ${name} npm runtime dependencies must only include package/version fields`);
|
|
447
|
+
const normalizedVersion = typeof version === "string" ? version.trim() : "latest";
|
|
448
|
+
if (!normalizedVersion) throw new Error(`Plugin ${name} runtime dependency version must be a non-empty string when provided`);
|
|
449
|
+
const dedupeKey = `${record.type}:${packageName}:${normalizedVersion}`;
|
|
450
|
+
if (seen.has(dedupeKey)) continue;
|
|
451
|
+
seen.add(dedupeKey);
|
|
452
|
+
parsed.push({
|
|
453
|
+
type: "npm",
|
|
454
|
+
package: packageName,
|
|
455
|
+
version: normalizedVersion
|
|
456
|
+
});
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
if (version !== void 0) throw new Error(`Plugin ${name} system runtime dependencies must not include a version`);
|
|
460
|
+
if (packageName && packageUrl) throw new Error(`Plugin ${name} system runtime dependencies must specify either package or url, not both`);
|
|
461
|
+
if (!packageName && !packageUrl) throw new Error(`Plugin ${name} system runtime dependencies must specify package or url`);
|
|
462
|
+
if (packageName) {
|
|
463
|
+
if (sha256 !== void 0) throw new Error(`Plugin ${name} system runtime dependency package entries must not include sha256`);
|
|
464
|
+
const dedupeKey = `${record.type}:package:${packageName}`;
|
|
465
|
+
if (seen.has(dedupeKey)) continue;
|
|
466
|
+
seen.add(dedupeKey);
|
|
467
|
+
parsed.push({
|
|
468
|
+
type: "system",
|
|
469
|
+
package: packageName
|
|
470
|
+
});
|
|
471
|
+
continue;
|
|
472
|
+
}
|
|
473
|
+
if (!/^https:\/\//i.test(packageUrl)) throw new Error(`Plugin ${name} system runtime dependency url must be an https URL`);
|
|
474
|
+
const normalizedSha256 = typeof sha256 === "string" ? sha256.trim().toLowerCase() : "";
|
|
475
|
+
if (!/^[a-f0-9]{64}$/.test(normalizedSha256)) throw new Error(`Plugin ${name} system runtime dependency url entries must include a valid sha256`);
|
|
476
|
+
const dedupeKey = `${record.type}:url:${packageUrl}:${normalizedSha256}`;
|
|
477
|
+
if (seen.has(dedupeKey)) continue;
|
|
478
|
+
seen.add(dedupeKey);
|
|
479
|
+
parsed.push({
|
|
480
|
+
type: "system",
|
|
481
|
+
url: packageUrl,
|
|
482
|
+
sha256: normalizedSha256
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
return parsed.length > 0 ? parsed : void 0;
|
|
486
|
+
}
|
|
487
|
+
function normalizeRuntimePostinstall(commands, name) {
|
|
488
|
+
const parsed = [];
|
|
489
|
+
for (const command of commands) {
|
|
490
|
+
const result = runtimePostinstallCommandSourceSchema.safeParse(command);
|
|
491
|
+
if (!result.success) throw new Error(issueMessage(result.error, `Plugin ${name} runtime-postinstall`));
|
|
492
|
+
if (!RUNTIME_POSTINSTALL_CMD_RE.test(result.data.cmd)) throw new Error(`Plugin ${name} runtime-postinstall cmd must be a single executable token (letters, digits, ., _, /, -)`);
|
|
493
|
+
const normalizedArgs = result.data.args?.map((arg) => arg.trim()).filter((arg) => arg.length > 0);
|
|
494
|
+
parsed.push({
|
|
495
|
+
cmd: result.data.cmd,
|
|
496
|
+
...normalizedArgs && normalizedArgs.length > 0 ? { args: normalizedArgs } : void 0,
|
|
497
|
+
...typeof result.data.sudo === "boolean" ? { sudo: result.data.sudo } : void 0
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
return parsed.length > 0 ? parsed : void 0;
|
|
501
|
+
}
|
|
502
|
+
const envVarDeclarationSchema = z.preprocess((value) => value === null || value === void 0 ? {} : value, z.object({
|
|
503
|
+
default: z.string().optional(),
|
|
504
|
+
"expose-to-command-env": z.boolean().optional(),
|
|
505
|
+
exposeToCommandEnv: z.boolean().optional()
|
|
506
|
+
}).strict());
|
|
507
|
+
function normalizeEnvVars(data, pluginName) {
|
|
508
|
+
const normalized = {};
|
|
509
|
+
for (const [rawName, rawDecl] of Object.entries(data)) {
|
|
510
|
+
const name = rawName.trim();
|
|
511
|
+
if (!ENV_VAR_NAME_RE.test(name)) throw new Error(`Plugin ${pluginName} env-vars key "${rawName}" must match [A-Z_][A-Z0-9_]*`);
|
|
512
|
+
const parsed = envVarDeclarationSchema.safeParse(rawDecl);
|
|
513
|
+
if (!parsed.success) throw new Error(issueMessage(parsed.error, `Plugin ${pluginName} env-vars.${name}`));
|
|
514
|
+
const decl = {};
|
|
515
|
+
if (parsed.data.default !== void 0) decl.default = parsed.data.default;
|
|
516
|
+
if (parsed.data["expose-to-command-env"] === true || parsed.data.exposeToCommandEnv === true) decl.exposeToCommandEnv = true;
|
|
517
|
+
normalized[name] = decl;
|
|
518
|
+
}
|
|
519
|
+
return normalized;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Expand `${NAME}` placeholders in a manifest string field using the plugin's
|
|
523
|
+
* declared env-vars block. `NAME` must match `[A-Z_][A-Z0-9_]*` and must be
|
|
524
|
+
* declared in the plugin's `env-vars` block — otherwise load fails. This
|
|
525
|
+
* keeps the manifest's env-var surface explicit and auditable, and prevents
|
|
526
|
+
* a plugin manifest from opportunistically exfiltrating ambient process env
|
|
527
|
+
* vars via `mcp.url`. If `NAME` is declared without a default and
|
|
528
|
+
* `process.env[NAME]` is unset, load also fails.
|
|
529
|
+
*/
|
|
530
|
+
function expandEnvPlaceholders(template, envVars, context) {
|
|
531
|
+
return template.replace(ENV_PLACEHOLDER_RE$2, (_match, name) => {
|
|
532
|
+
const varName = name;
|
|
533
|
+
if (!Object.prototype.hasOwnProperty.call(envVars, varName)) throw new Error(`${context} references env var ${varName} which is not declared in env-vars`);
|
|
534
|
+
const decl = envVars[varName];
|
|
535
|
+
const fromProcess = process.env[varName];
|
|
536
|
+
if (fromProcess !== void 0 && fromProcess !== "") return fromProcess;
|
|
537
|
+
if (decl.default !== void 0) return decl.default;
|
|
538
|
+
throw new Error(`${context} env var ${varName} is unset and has no default in env-vars`);
|
|
539
|
+
});
|
|
540
|
+
}
|
|
541
|
+
function normalizeMcp(data, envVars, name) {
|
|
542
|
+
const prepared = { ...data };
|
|
543
|
+
if (typeof prepared.url === "string") prepared.url = expandEnvPlaceholders(prepared.url, envVars, `Plugin ${name} mcp.url`);
|
|
544
|
+
const result = mcpSourceSchema.safeParse(prepared);
|
|
545
|
+
if (!result.success) throw new Error(issueMessage(result.error, `Plugin ${name} mcp`));
|
|
546
|
+
const headers = result.data.headers ? normalizeStringMap(result.data.headers, `Plugin ${name} mcp.headers`, { forbiddenKeys: FORBIDDEN_API_HEADER_NAMES }) : void 0;
|
|
547
|
+
return {
|
|
548
|
+
transport: "http",
|
|
549
|
+
url: result.data.url,
|
|
550
|
+
...headers ? { headers } : void 0,
|
|
551
|
+
...result.data["allowed-tools"] ? { allowedTools: result.data["allowed-tools"] } : void 0,
|
|
552
|
+
...result.data["wrapped-tools"] ? { wrappedTools: result.data["wrapped-tools"] } : void 0
|
|
553
|
+
};
|
|
554
|
+
}
|
|
555
|
+
function parseManifestSource(parsedSource, dir, config, options) {
|
|
556
|
+
const source = applyManifestConfig(parsedSource, config);
|
|
557
|
+
const sourceResult = manifestSourceSchema.safeParse(source);
|
|
558
|
+
if (!sourceResult.success) {
|
|
559
|
+
const issue = sourceResult.error.issues[0];
|
|
560
|
+
const path = formatPath(issue?.path ?? []);
|
|
561
|
+
if (path === "name") throw new Error(`Invalid plugin name in ${dir}: "${parsedSource.name}"`);
|
|
562
|
+
if (path === "description") throw new Error(`Invalid plugin description in ${dir}`);
|
|
563
|
+
if (path === "config-keys") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} config-keys must be an array when provided`);
|
|
564
|
+
if (path === "domains") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} ${path} must be a non-empty array of domains`);
|
|
565
|
+
if (path === "api-headers") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} api-headers must be an object when provided`);
|
|
566
|
+
if (path === "command-env") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} command-env must be an object when provided`);
|
|
567
|
+
if (path === "credentials") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} credentials must be an object when provided`);
|
|
568
|
+
if (path === "runtime-dependencies") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} runtime-dependencies must be an array`);
|
|
569
|
+
if (path === "runtime-postinstall") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} runtime-postinstall must be an array`);
|
|
570
|
+
if (path === "env-vars") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} env-vars must be an object`);
|
|
571
|
+
if (path === "mcp") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} mcp must be an object`);
|
|
572
|
+
if (path === "oauth") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} oauth must be an object`);
|
|
573
|
+
if (path === "target") throw new Error(`Plugin ${String(parsedSource.name ?? "unknown")} target must be an object`);
|
|
574
|
+
throw new Error(issue?.message ?? `Invalid plugin manifest in ${dir}`);
|
|
575
|
+
}
|
|
576
|
+
const data = sourceResult.data;
|
|
577
|
+
const configKeys = (data["config-keys"] ?? []).map((key) => {
|
|
578
|
+
if (!SHORT_CONFIG_KEY_RE.test(key)) throw new Error(`Invalid config key "${key}" in plugin ${data.name}`);
|
|
579
|
+
return `${data.name}.${key}`;
|
|
580
|
+
});
|
|
581
|
+
const envVars = data["env-vars"] ? normalizeEnvVars(data["env-vars"], data.name) : {};
|
|
582
|
+
const apiHeaders = data["api-headers"] ? normalizeRequiredApiHeaders(data["api-headers"], `Plugin ${data.name} api-headers`, envVars) : void 0;
|
|
583
|
+
const domains = data.domains;
|
|
584
|
+
if (apiHeaders && !domains) throw new Error(`Plugin ${data.name} api-headers requires domains`);
|
|
585
|
+
if (domains && !apiHeaders && !data.credentials && options?.allowHookManagedEgress !== true) throw new Error(`Plugin ${data.name} domains requires credentials or api-headers`);
|
|
586
|
+
const commandEnv = data["command-env"] ? normalizeCommandEnv(data["command-env"], `Plugin ${data.name} command-env`, envVars) : void 0;
|
|
587
|
+
const credentials = data.credentials ? normalizeCredentials(data.credentials, data.name) : void 0;
|
|
588
|
+
const runtimeDependencies = data["runtime-dependencies"] ? normalizeRuntimeDependencies(data["runtime-dependencies"], data.name) : void 0;
|
|
589
|
+
const runtimePostinstall = data["runtime-postinstall"] ? normalizeRuntimePostinstall(data["runtime-postinstall"], data.name) : void 0;
|
|
590
|
+
const mcp = data.mcp ? normalizeMcp(data.mcp, envVars, data.name) : void 0;
|
|
591
|
+
const manifest = {
|
|
592
|
+
name: data.name,
|
|
593
|
+
displayName: data["display-name"],
|
|
594
|
+
description: data.description,
|
|
595
|
+
configKeys,
|
|
596
|
+
...domains ? { domains } : void 0,
|
|
597
|
+
...apiHeaders ? { apiHeaders } : void 0,
|
|
598
|
+
...commandEnv ? { commandEnv } : void 0,
|
|
599
|
+
...Object.keys(envVars).length > 0 ? { envVars } : void 0,
|
|
600
|
+
...credentials ? { credentials } : void 0,
|
|
601
|
+
...runtimeDependencies ? { runtimeDependencies } : void 0,
|
|
602
|
+
...runtimePostinstall ? { runtimePostinstall } : void 0,
|
|
603
|
+
...mcp ? { mcp } : void 0
|
|
604
|
+
};
|
|
605
|
+
if (data.oauth) {
|
|
606
|
+
if (!credentials && options?.allowHookManagedEgress !== true) throw new Error(`Plugin ${data.name} oauth requires credentials`);
|
|
607
|
+
const result = oauthSourceSchema.safeParse(data.oauth);
|
|
608
|
+
if (!result.success) throw new Error(issueMessage(result.error, `Plugin ${data.name} oauth`));
|
|
609
|
+
const authorizeParams = result.data["authorize-params"] ? normalizeStringMap(result.data["authorize-params"], `Plugin ${data.name} oauth.authorize-params`, { reservedKeys: RESERVED_AUTHORIZE_PARAM_KEYS }) : void 0;
|
|
610
|
+
const tokenExtraHeaders = result.data["token-extra-headers"] ? normalizeStringMap(result.data["token-extra-headers"], `Plugin ${data.name} oauth.token-extra-headers`, { forbiddenKeys: FORBIDDEN_TOKEN_HEADER_NAMES }) : void 0;
|
|
611
|
+
manifest.oauth = {
|
|
612
|
+
clientIdEnv: result.data["client-id-env"],
|
|
613
|
+
clientSecretEnv: result.data["client-secret-env"],
|
|
614
|
+
authorizeEndpoint: result.data["authorize-endpoint"],
|
|
615
|
+
tokenEndpoint: result.data["token-endpoint"],
|
|
616
|
+
...result.data.scope ? { scope: result.data.scope } : void 0,
|
|
617
|
+
...authorizeParams ? { authorizeParams } : void 0,
|
|
618
|
+
...result.data["token-auth-method"] ? { tokenAuthMethod: result.data["token-auth-method"] } : void 0,
|
|
619
|
+
...tokenExtraHeaders ? { tokenExtraHeaders } : void 0,
|
|
620
|
+
...result.data["treat-empty-scope-as-unreported"] ? { treatEmptyScopeAsUnreported: true } : void 0
|
|
621
|
+
};
|
|
622
|
+
}
|
|
623
|
+
assertCommandEnvDoesNotExposeHostSecretRefs(data["command-env"], apiHeaders, credentials, manifest.oauth, data.name);
|
|
624
|
+
assertCommandEnvHostRefsAreExplicitlyExposed(data["command-env"], envVars, data.name);
|
|
625
|
+
if (data.target) {
|
|
626
|
+
const result = targetSourceSchema.safeParse(data.target);
|
|
627
|
+
if (!result.success) throw new Error(issueMessage(result.error, `Plugin ${data.name} target`));
|
|
628
|
+
if (!SHORT_CONFIG_KEY_RE.test(result.data["config-key"])) throw new Error(`Plugin ${data.name} target.config-key "${result.data["config-key"]}" is invalid`);
|
|
629
|
+
const qualifiedKey = `${data.name}.${result.data["config-key"]}`;
|
|
630
|
+
if (!configKeys.includes(qualifiedKey)) throw new Error(`Plugin ${data.name} target.config-key "${result.data["config-key"]}" must be listed in config-keys`);
|
|
631
|
+
const commandFlags = result.data["command-flags"];
|
|
632
|
+
if (commandFlags && commandFlags.some((flag) => !TARGET_FLAG_RE.test(flag))) throw new Error(`Plugin ${data.name} target.command-flags must contain CLI flags like --repo or -R`);
|
|
633
|
+
manifest.target = {
|
|
634
|
+
type: result.data.type,
|
|
635
|
+
configKey: qualifiedKey,
|
|
636
|
+
...commandFlags ? { commandFlags } : void 0
|
|
637
|
+
};
|
|
638
|
+
}
|
|
639
|
+
return manifest;
|
|
640
|
+
}
|
|
641
|
+
/** Parse one plugin.yaml manifest after applying install-level plugin config. */
|
|
642
|
+
function parsePluginManifest(raw, dir, config) {
|
|
643
|
+
let parsedYaml;
|
|
644
|
+
try {
|
|
645
|
+
parsedYaml = parse(raw);
|
|
646
|
+
} catch (error) {
|
|
647
|
+
throw new Error(`Invalid plugin manifest in ${dir}: ${error instanceof Error ? error.message : String(error)}`);
|
|
648
|
+
}
|
|
649
|
+
if (!parsedYaml || typeof parsedYaml !== "object" || Array.isArray(parsedYaml)) throw new Error(`Invalid plugin manifest in ${dir}: expected an object`);
|
|
650
|
+
return parseManifestSource(parsedYaml, dir, config);
|
|
651
|
+
}
|
|
652
|
+
/** Parse one inline JavaScript manifest through the same effective manifest pipeline as plugin.yaml. */
|
|
653
|
+
function parseInlinePluginManifest(manifest, dir, config) {
|
|
654
|
+
return parseManifestSource(inlineManifestSource(manifest), dir, config, { allowHookManagedEgress: true });
|
|
655
|
+
}
|
|
656
|
+
//#endregion
|
|
657
|
+
//#region src/chat/credentials/broker.ts
|
|
658
|
+
var CredentialUnavailableError = class extends Error {
|
|
659
|
+
provider;
|
|
660
|
+
constructor(provider, message) {
|
|
661
|
+
super(message);
|
|
662
|
+
this.name = "CredentialUnavailableError";
|
|
663
|
+
this.provider = provider;
|
|
664
|
+
}
|
|
665
|
+
};
|
|
666
|
+
//#endregion
|
|
667
|
+
//#region src/chat/credentials/header-transforms.ts
|
|
668
|
+
/** Merge transforms by domain so later transforms override earlier headers. */
|
|
669
|
+
function mergeHeaderTransforms(transforms) {
|
|
670
|
+
const byDomain = /* @__PURE__ */ new Map();
|
|
671
|
+
for (const transform of transforms) byDomain.set(transform.domain, {
|
|
672
|
+
...byDomain.get(transform.domain) ?? {},
|
|
673
|
+
...transform.headers
|
|
674
|
+
});
|
|
675
|
+
return [...byDomain.entries()].map(([domain, headers]) => ({
|
|
676
|
+
domain,
|
|
677
|
+
headers
|
|
678
|
+
}));
|
|
679
|
+
}
|
|
680
|
+
//#endregion
|
|
681
|
+
//#region src/chat/credentials/oauth-scope.ts
|
|
682
|
+
function parseScope(scope) {
|
|
683
|
+
if (!scope) return [];
|
|
684
|
+
return [...new Set(scope.split(/\s+/).filter(Boolean))].sort();
|
|
685
|
+
}
|
|
686
|
+
/** Normalize OAuth scope strings so persisted grants can be compared reliably. */
|
|
687
|
+
function normalizeOAuthScope(scope) {
|
|
688
|
+
const parsed = parseScope(scope);
|
|
689
|
+
return parsed.length > 0 ? parsed.join(" ") : void 0;
|
|
690
|
+
}
|
|
691
|
+
/** Return whether the stored grant still satisfies the provider's current scope contract. */
|
|
692
|
+
function hasRequiredOAuthScope(storedScope, requiredScope) {
|
|
693
|
+
const required = parseScope(requiredScope);
|
|
694
|
+
if (required.length === 0) return true;
|
|
695
|
+
const stored = new Set(parseScope(storedScope));
|
|
696
|
+
if (stored.size === 0) return false;
|
|
697
|
+
return required.every((scope) => stored.has(scope));
|
|
698
|
+
}
|
|
699
|
+
//#endregion
|
|
700
|
+
//#region src/chat/oauth-response.ts
|
|
701
|
+
const MAX_OAUTH_ERROR_BODY_BYTES = 16384;
|
|
702
|
+
const MAX_OAUTH_ERROR_BODY_READS = MAX_OAUTH_ERROR_BODY_BYTES;
|
|
703
|
+
const MAX_OAUTH_ERROR_BODY_READ_MS = 5e3;
|
|
704
|
+
const OAUTH_ERROR_BODY_READ_TIMEOUT = Symbol("oauth-error-body-read-timeout");
|
|
705
|
+
async function readOAuthErrorBodyChunk(reader) {
|
|
706
|
+
let timeout;
|
|
707
|
+
const timeoutResult = new Promise((resolve) => {
|
|
708
|
+
timeout = setTimeout(resolve, MAX_OAUTH_ERROR_BODY_READ_MS, OAUTH_ERROR_BODY_READ_TIMEOUT);
|
|
709
|
+
});
|
|
710
|
+
try {
|
|
711
|
+
const result = await Promise.race([reader.read(), timeoutResult]);
|
|
712
|
+
if (result === OAUTH_ERROR_BODY_READ_TIMEOUT) {
|
|
713
|
+
await reader.cancel();
|
|
714
|
+
throw new Error("OAuth error body read timed out");
|
|
715
|
+
}
|
|
716
|
+
return result;
|
|
717
|
+
} finally {
|
|
718
|
+
if (timeout) clearTimeout(timeout);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
/** Safe failure at a generic OAuth provider boundary. */
|
|
722
|
+
var OAuthProviderError = class extends Error {
|
|
723
|
+
phase;
|
|
724
|
+
provider;
|
|
725
|
+
resourceHost;
|
|
726
|
+
status;
|
|
727
|
+
constructor(details) {
|
|
728
|
+
super(`OAuth provider ${details.phase.replace("_", " ")} failed`);
|
|
729
|
+
this.name = "OAuthProviderError";
|
|
730
|
+
this.phase = details.phase;
|
|
731
|
+
this.provider = details.provider;
|
|
732
|
+
this.resourceHost = details.resourceHost;
|
|
733
|
+
this.status = details.status;
|
|
734
|
+
}
|
|
735
|
+
};
|
|
736
|
+
/** Return safe generic OAuth provider fields for logs and spans. */
|
|
737
|
+
function getOAuthProviderErrorAttributes(error) {
|
|
738
|
+
if (!(error instanceof OAuthProviderError)) return {};
|
|
739
|
+
return {
|
|
740
|
+
"app.credential.provider": error.provider,
|
|
741
|
+
"app.oauth.error.phase": error.phase,
|
|
742
|
+
"server.address": error.resourceHost,
|
|
743
|
+
...error.status !== void 0 ? { "http.response.status_code": error.status } : void 0
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
/** Read only the bounded prefix needed to classify an OAuth error response. */
|
|
747
|
+
async function readBoundedOAuthErrorBody(response) {
|
|
748
|
+
if (!response.body) return "";
|
|
749
|
+
const reader = response.body.getReader();
|
|
750
|
+
const decoder = new TextDecoder();
|
|
751
|
+
let bytesRead = 0;
|
|
752
|
+
let reads = 0;
|
|
753
|
+
let text = "";
|
|
754
|
+
try {
|
|
755
|
+
while (bytesRead < MAX_OAUTH_ERROR_BODY_BYTES && reads < MAX_OAUTH_ERROR_BODY_READS) {
|
|
756
|
+
reads += 1;
|
|
757
|
+
const { done, value } = await readOAuthErrorBodyChunk(reader);
|
|
758
|
+
if (done) return text + decoder.decode();
|
|
759
|
+
const remaining = MAX_OAUTH_ERROR_BODY_BYTES - bytesRead;
|
|
760
|
+
const chunk = value.subarray(0, remaining);
|
|
761
|
+
bytesRead += chunk.byteLength;
|
|
762
|
+
text += decoder.decode(chunk, { stream: true });
|
|
763
|
+
if (chunk.byteLength < value.byteLength) {
|
|
764
|
+
await reader.cancel();
|
|
765
|
+
return text + decoder.decode();
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
await reader.cancel();
|
|
769
|
+
return text + decoder.decode();
|
|
770
|
+
} finally {
|
|
771
|
+
reader.releaseLock();
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
/** Preserve small OAuth error responses while bounding bodies before SDK parsing. */
|
|
775
|
+
function fetchWithBoundedOAuthErrorBodies(fetchFn = globalThis.fetch, onErrorResponseStatus) {
|
|
776
|
+
return (async (input, init) => {
|
|
777
|
+
const response = await fetchFn(input, init);
|
|
778
|
+
if (response.ok) return response;
|
|
779
|
+
onErrorResponseStatus?.(response.status);
|
|
780
|
+
if (!response.body) return response;
|
|
781
|
+
let body = "";
|
|
782
|
+
try {
|
|
783
|
+
body = await readBoundedOAuthErrorBody(response);
|
|
784
|
+
} catch {
|
|
785
|
+
await response.body.cancel().catch(() => void 0);
|
|
786
|
+
}
|
|
787
|
+
return new Response(body, {
|
|
788
|
+
headers: response.headers,
|
|
789
|
+
status: response.status,
|
|
790
|
+
statusText: response.statusText
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
//#endregion
|
|
795
|
+
//#region src/chat/plugins/command-env.ts
|
|
796
|
+
const ENV_PLACEHOLDER_RE$1 = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
797
|
+
function resolveValue(manifest, value) {
|
|
798
|
+
let missing = false;
|
|
799
|
+
const resolved = value.replace(ENV_PLACEHOLDER_RE$1, (match, name) => {
|
|
800
|
+
const envName = name;
|
|
801
|
+
const declaration = manifest.envVars?.[envName];
|
|
802
|
+
if (!declaration || declaration.default !== void 0) return match;
|
|
803
|
+
const hostValue = process.env[envName];
|
|
804
|
+
if (hostValue === void 0 || hostValue === "") {
|
|
805
|
+
missing = true;
|
|
806
|
+
return "";
|
|
807
|
+
}
|
|
808
|
+
return hostValue;
|
|
809
|
+
});
|
|
810
|
+
return missing ? void 0 : resolved;
|
|
811
|
+
}
|
|
812
|
+
/** Resolve sandbox command env declared by a plugin manifest. */
|
|
813
|
+
function resolvePluginCommandEnv(manifest) {
|
|
814
|
+
const env = {};
|
|
815
|
+
for (const [key, value] of Object.entries(manifest.commandEnv ?? {})) {
|
|
816
|
+
const resolved = resolveValue(manifest, value);
|
|
817
|
+
if (resolved === void 0) continue;
|
|
818
|
+
env[key] = resolved;
|
|
819
|
+
}
|
|
820
|
+
return env;
|
|
821
|
+
}
|
|
822
|
+
//#endregion
|
|
823
|
+
//#region src/chat/plugins/auth/auth-token-placeholder.ts
|
|
824
|
+
const DEFAULT_PLACEHOLDERS = { "oauth-bearer": "host_managed_credential" };
|
|
825
|
+
/** Resolve the non-secret sandbox token placeholder for token-backed credentials. */
|
|
826
|
+
function resolveAuthTokenPlaceholder(credentials) {
|
|
827
|
+
return credentials.authTokenPlaceholder?.trim() || DEFAULT_PLACEHOLDERS[credentials.type];
|
|
828
|
+
}
|
|
829
|
+
//#endregion
|
|
830
|
+
//#region src/chat/plugins/auth/api-headers-broker.ts
|
|
831
|
+
const MAX_LEASE_MS$1 = 36e5;
|
|
832
|
+
const ENV_PLACEHOLDER_RE = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
|
|
833
|
+
function resolveHeaders(provider, headers) {
|
|
834
|
+
return Object.fromEntries(Object.entries(headers).map(([key, value]) => {
|
|
835
|
+
return [key, value.replace(ENV_PLACEHOLDER_RE, (_match, name) => {
|
|
836
|
+
const envName = name;
|
|
837
|
+
const envValue = process.env[envName]?.trim();
|
|
838
|
+
if (!envValue) throw new Error(`Missing ${envName} for API header provider "${provider}"`);
|
|
839
|
+
return envValue;
|
|
840
|
+
})];
|
|
841
|
+
}));
|
|
842
|
+
}
|
|
843
|
+
/** Resolve plugin-level API headers into sandbox header transforms. */
|
|
844
|
+
function resolveApiHeaderTransforms(manifest) {
|
|
845
|
+
const { domains, apiHeaders } = manifest;
|
|
846
|
+
if (!domains || !apiHeaders) return [];
|
|
847
|
+
const resolvedHeaders = resolveHeaders(manifest.name, apiHeaders);
|
|
848
|
+
return domains.map((domain) => ({
|
|
849
|
+
domain,
|
|
850
|
+
headers: resolvedHeaders
|
|
851
|
+
}));
|
|
852
|
+
}
|
|
853
|
+
/** Issue host-managed API header transforms backed by deployment env vars. */
|
|
854
|
+
function createApiHeadersBroker(manifest) {
|
|
855
|
+
const provider = manifest.name;
|
|
856
|
+
return { async issue(input) {
|
|
857
|
+
const headerTransforms = resolveApiHeaderTransforms(manifest);
|
|
858
|
+
if (headerTransforms.length === 0) throw new Error(`No API headers configured for plugin "${provider}"`);
|
|
859
|
+
return {
|
|
860
|
+
id: randomUUID(),
|
|
861
|
+
provider,
|
|
862
|
+
env: resolvePluginCommandEnv(manifest),
|
|
863
|
+
headerTransforms,
|
|
864
|
+
expiresAt: new Date(Date.now() + MAX_LEASE_MS$1).toISOString(),
|
|
865
|
+
metadata: { reason: input.reason }
|
|
866
|
+
};
|
|
867
|
+
} };
|
|
868
|
+
}
|
|
869
|
+
//#endregion
|
|
870
|
+
//#region src/chat/plugins/auth/oauth-request.ts
|
|
871
|
+
const DEFAULT_TOKEN_CONTENT_TYPE = "application/x-www-form-urlencoded";
|
|
872
|
+
function requireNonEmptyTokenField(data, field) {
|
|
873
|
+
const value = data[field];
|
|
874
|
+
if (typeof value !== "string" || !value.trim()) throw new Error(`OAuth token response missing ${field}`);
|
|
875
|
+
return value;
|
|
876
|
+
}
|
|
877
|
+
function requireTokenResponseObject(data) {
|
|
878
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("OAuth token response must be an object");
|
|
879
|
+
return data;
|
|
880
|
+
}
|
|
881
|
+
const parsedOAuthTokenResponseSchema = z.object({
|
|
882
|
+
accessToken: z.string().min(1),
|
|
883
|
+
refreshToken: z.string().min(1),
|
|
884
|
+
expiresAt: z.number().positive().optional(),
|
|
885
|
+
refreshTokenExpiresAt: z.number().positive().optional(),
|
|
886
|
+
scope: z.string().min(1).optional()
|
|
887
|
+
}).strict();
|
|
888
|
+
function contentTypeToBody(contentType, payload) {
|
|
889
|
+
const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
|
|
890
|
+
if (!mediaType || mediaType === DEFAULT_TOKEN_CONTENT_TYPE) return new URLSearchParams(payload);
|
|
891
|
+
if (mediaType === "application/json" || mediaType.endsWith("+json")) return JSON.stringify(payload);
|
|
892
|
+
throw new Error(`Unsupported OAuth token Content-Type: ${contentType}`);
|
|
893
|
+
}
|
|
894
|
+
function buildOAuthTokenRequest(input) {
|
|
895
|
+
const headers = new Headers({ Accept: "application/json" });
|
|
896
|
+
for (const [name, value] of Object.entries(input.tokenExtraHeaders ?? {})) headers.set(name, value);
|
|
897
|
+
if (!headers.has("Content-Type")) headers.set("Content-Type", DEFAULT_TOKEN_CONTENT_TYPE);
|
|
898
|
+
const payload = { ...input.payload };
|
|
899
|
+
if (input.tokenAuthMethod === "basic") headers.set("Authorization", `Basic ${Buffer.from(`${input.clientId}:${input.clientSecret}`).toString("base64")}`);
|
|
900
|
+
else {
|
|
901
|
+
payload.client_id = input.clientId;
|
|
902
|
+
payload.client_secret = input.clientSecret;
|
|
903
|
+
}
|
|
904
|
+
const contentType = headers.get("Content-Type") ?? DEFAULT_TOKEN_CONTENT_TYPE;
|
|
905
|
+
const serializedHeaders = {};
|
|
906
|
+
headers.forEach((value, key) => {
|
|
907
|
+
serializedHeaders[key] = value;
|
|
908
|
+
});
|
|
909
|
+
return {
|
|
910
|
+
headers: serializedHeaders,
|
|
911
|
+
body: contentTypeToBody(contentType, payload)
|
|
912
|
+
};
|
|
913
|
+
}
|
|
914
|
+
/** Parse provider tokens, retaining the request token when a refresh response omits rotation. */
|
|
915
|
+
function parseOAuthTokenResponse(data, requestedScope, options) {
|
|
916
|
+
const response = requireTokenResponseObject(data);
|
|
917
|
+
const accessToken = requireNonEmptyTokenField(response, "access_token");
|
|
918
|
+
const resolvedRefreshToken = response.refresh_token === void 0 ? options?.refreshToken : requireNonEmptyTokenField(response, "refresh_token");
|
|
919
|
+
if (!resolvedRefreshToken) throw new Error("OAuth token response missing refresh_token");
|
|
920
|
+
const expiresIn = response.expires_in;
|
|
921
|
+
const refreshTokenExpiresIn = response.refresh_token_expires_in;
|
|
922
|
+
const responseScope = response.scope;
|
|
923
|
+
let scope;
|
|
924
|
+
if (responseScope !== void 0) {
|
|
925
|
+
if (typeof responseScope !== "string") throw new Error("OAuth token response returned invalid scope");
|
|
926
|
+
const normalized = normalizeOAuthScope(responseScope);
|
|
927
|
+
if (normalized !== void 0) scope = normalized;
|
|
928
|
+
else if (options?.treatEmptyScopeAsUnreported) scope = normalizeOAuthScope(requestedScope);
|
|
929
|
+
else throw new Error("OAuth token response returned empty scope");
|
|
930
|
+
} else scope = normalizeOAuthScope(requestedScope);
|
|
931
|
+
const result = {
|
|
932
|
+
accessToken,
|
|
933
|
+
refreshToken: resolvedRefreshToken,
|
|
934
|
+
...scope ? { scope } : void 0
|
|
935
|
+
};
|
|
936
|
+
if (expiresIn !== void 0) {
|
|
937
|
+
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) throw new Error("OAuth token response returned invalid expires_in");
|
|
938
|
+
result.expiresAt = Date.now() + expiresIn * 1e3;
|
|
939
|
+
}
|
|
940
|
+
if (refreshTokenExpiresIn !== void 0) {
|
|
941
|
+
if (typeof refreshTokenExpiresIn !== "number" || !Number.isFinite(refreshTokenExpiresIn) || refreshTokenExpiresIn <= 0) throw new Error("OAuth token response returned invalid refresh_token_expires_in");
|
|
942
|
+
result.refreshTokenExpiresAt = Date.now() + refreshTokenExpiresIn * 1e3;
|
|
943
|
+
}
|
|
944
|
+
return parsedOAuthTokenResponseSchema.parse(result);
|
|
945
|
+
}
|
|
946
|
+
//#endregion
|
|
947
|
+
//#region src/chat/plugins/auth/oauth-bearer-broker.ts
|
|
948
|
+
const MAX_LEASE_MS = 36e5;
|
|
949
|
+
const REFRESH_BUFFER_MS = 3e5;
|
|
950
|
+
const TOKEN_REFRESH_TIMEOUT_MS = 2e4;
|
|
951
|
+
var OAuthRefreshRejectedError = class extends Error {
|
|
952
|
+
constructor(message) {
|
|
953
|
+
super(message);
|
|
954
|
+
this.name = "OAuthRefreshRejectedError";
|
|
955
|
+
}
|
|
956
|
+
};
|
|
957
|
+
function parseRefreshError(text) {
|
|
958
|
+
if (!text.trim()) return;
|
|
959
|
+
try {
|
|
960
|
+
const parsed = JSON.parse(text);
|
|
961
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) && typeof parsed.error === "string" ? parsed.error : void 0;
|
|
962
|
+
} catch {
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
}
|
|
966
|
+
async function refreshAccessToken(provider, refreshToken, oauth, requestedScope) {
|
|
967
|
+
const clientId = process.env[oauth.clientIdEnv]?.trim();
|
|
968
|
+
const clientSecret = process.env[oauth.clientSecretEnv]?.trim();
|
|
969
|
+
if (!clientId || !clientSecret) throw new Error(`Missing ${oauth.clientIdEnv} or ${oauth.clientSecretEnv} for token refresh`);
|
|
970
|
+
const request = buildOAuthTokenRequest({
|
|
971
|
+
clientId,
|
|
972
|
+
clientSecret,
|
|
973
|
+
payload: {
|
|
974
|
+
grant_type: "refresh_token",
|
|
975
|
+
refresh_token: refreshToken
|
|
976
|
+
},
|
|
977
|
+
tokenAuthMethod: oauth.tokenAuthMethod,
|
|
978
|
+
tokenExtraHeaders: oauth.tokenExtraHeaders
|
|
979
|
+
});
|
|
980
|
+
const errorDetails = {
|
|
981
|
+
phase: "token_refresh",
|
|
982
|
+
provider,
|
|
983
|
+
resourceHost: new URL(oauth.tokenEndpoint).hostname
|
|
984
|
+
};
|
|
985
|
+
let response;
|
|
986
|
+
try {
|
|
987
|
+
response = await fetch(oauth.tokenEndpoint, {
|
|
988
|
+
method: "POST",
|
|
989
|
+
headers: request.headers,
|
|
990
|
+
body: request.body,
|
|
991
|
+
signal: AbortSignal.timeout(TOKEN_REFRESH_TIMEOUT_MS)
|
|
992
|
+
});
|
|
993
|
+
} catch {
|
|
994
|
+
throw new OAuthProviderError(errorDetails);
|
|
995
|
+
}
|
|
996
|
+
if (!response.ok) {
|
|
997
|
+
let errorCode;
|
|
998
|
+
try {
|
|
999
|
+
errorCode = parseRefreshError(await readBoundedOAuthErrorBody(response));
|
|
1000
|
+
} catch {
|
|
1001
|
+
throw new OAuthProviderError({
|
|
1002
|
+
...errorDetails,
|
|
1003
|
+
status: response.status
|
|
1004
|
+
});
|
|
1005
|
+
}
|
|
1006
|
+
if (errorCode === "invalid_grant" || errorCode === "bad_refresh_token") throw new OAuthRefreshRejectedError("Token refresh was rejected");
|
|
1007
|
+
throw new OAuthProviderError({
|
|
1008
|
+
...errorDetails,
|
|
1009
|
+
status: response.status
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
try {
|
|
1013
|
+
return parseOAuthTokenResponse(await response.json(), requestedScope, {
|
|
1014
|
+
treatEmptyScopeAsUnreported: oauth.treatEmptyScopeAsUnreported,
|
|
1015
|
+
refreshToken
|
|
1016
|
+
});
|
|
1017
|
+
} catch {
|
|
1018
|
+
throw new OAuthProviderError(errorDetails);
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
function getLeaseExpiry(expiresAt) {
|
|
1022
|
+
return expiresAt ? Math.min(expiresAt, Date.now() + MAX_LEASE_MS) : Date.now() + MAX_LEASE_MS;
|
|
1023
|
+
}
|
|
1024
|
+
function canUseStoredToken(stored) {
|
|
1025
|
+
return stored !== void 0 && (stored.expiresAt === void 0 || stored.expiresAt > Date.now());
|
|
1026
|
+
}
|
|
1027
|
+
function shouldRefreshStoredToken(stored) {
|
|
1028
|
+
return stored !== void 0 && stored.expiresAt !== void 0 && stored.expiresAt - Date.now() < REFRESH_BUFFER_MS;
|
|
1029
|
+
}
|
|
1030
|
+
function createOAuthBearerBroker(manifest, credentials, deps) {
|
|
1031
|
+
const provider = manifest.name;
|
|
1032
|
+
const { domains, apiHeaders, authTokenEnv } = credentials;
|
|
1033
|
+
const authTokenPlaceholder = resolveAuthTokenPlaceholder(credentials);
|
|
1034
|
+
const pluginHeaderTransforms = () => resolveApiHeaderTransforms(manifest);
|
|
1035
|
+
function buildLease(token, expiresAtMs, reason) {
|
|
1036
|
+
return {
|
|
1037
|
+
id: randomUUID(),
|
|
1038
|
+
provider,
|
|
1039
|
+
env: {
|
|
1040
|
+
...resolvePluginCommandEnv(manifest),
|
|
1041
|
+
[authTokenEnv]: authTokenPlaceholder
|
|
1042
|
+
},
|
|
1043
|
+
headerTransforms: mergeHeaderTransforms([...pluginHeaderTransforms(), ...domains.map((domain) => ({
|
|
1044
|
+
domain,
|
|
1045
|
+
headers: {
|
|
1046
|
+
...apiHeaders ?? {},
|
|
1047
|
+
Authorization: `Bearer ${token}`
|
|
1048
|
+
}
|
|
1049
|
+
}))]),
|
|
1050
|
+
expiresAt: new Date(expiresAtMs).toISOString(),
|
|
1051
|
+
metadata: { reason }
|
|
1052
|
+
};
|
|
1053
|
+
}
|
|
1054
|
+
return { async issue(input) {
|
|
1055
|
+
const envToken = process.env[authTokenEnv]?.trim();
|
|
1056
|
+
const oauth = manifest.oauth;
|
|
1057
|
+
const userSubjectId = credentialUserSubjectId(input.context);
|
|
1058
|
+
if (!oauth) {
|
|
1059
|
+
if (envToken) return buildLease(envToken, Date.now() + MAX_LEASE_MS, input.reason);
|
|
1060
|
+
throw new CredentialUnavailableError(provider, `No ${provider} credentials available.`);
|
|
1061
|
+
}
|
|
1062
|
+
if (userSubjectId) {
|
|
1063
|
+
const stored = await deps.userTokenStore.get(userSubjectId, provider);
|
|
1064
|
+
if (stored) {
|
|
1065
|
+
if (!hasRequiredOAuthScope(stored.scope, oauth.scope)) throw new CredentialUnavailableError(provider, `Your ${provider} connection needs to be reauthorized.`);
|
|
1066
|
+
if (shouldRefreshStoredToken(stored)) try {
|
|
1067
|
+
return await deps.userTokenStore.withRefresh(userSubjectId, provider, async () => {
|
|
1068
|
+
const latest = await deps.userTokenStore.get(userSubjectId, provider);
|
|
1069
|
+
if (latest && !hasRequiredOAuthScope(latest.scope, oauth.scope)) throw new CredentialUnavailableError(provider, `Your ${provider} connection needs to be reauthorized.`);
|
|
1070
|
+
if (!shouldRefreshStoredToken(latest) && canUseStoredToken(latest)) return buildLease(latest.accessToken, getLeaseExpiry(latest.expiresAt), input.reason);
|
|
1071
|
+
if (!latest) throw new CredentialUnavailableError(provider, `No ${provider} credentials available.`);
|
|
1072
|
+
const refreshed = await refreshAccessToken(provider, latest.refreshToken, oauth, latest.scope ?? oauth.scope);
|
|
1073
|
+
if (!hasRequiredOAuthScope(refreshed.scope, oauth.scope)) throw new CredentialUnavailableError(provider, `Your ${provider} connection needs to be reauthorized.`);
|
|
1074
|
+
const refreshedTokens = {
|
|
1075
|
+
...latest.refreshTokenExpiresAt ? { refreshTokenExpiresAt: latest.refreshTokenExpiresAt } : void 0,
|
|
1076
|
+
...refreshed,
|
|
1077
|
+
...latest.account ? { account: latest.account } : void 0
|
|
1078
|
+
};
|
|
1079
|
+
await deps.userTokenStore.set(userSubjectId, provider, refreshedTokens);
|
|
1080
|
+
return buildLease(refreshed.accessToken, getLeaseExpiry(refreshed.expiresAt), input.reason);
|
|
1081
|
+
});
|
|
1082
|
+
} catch (error) {
|
|
1083
|
+
if (error instanceof CredentialUnavailableError) throw error;
|
|
1084
|
+
if (error instanceof OAuthRefreshRejectedError) throw new CredentialUnavailableError(provider, `Your ${provider} connection has expired.`);
|
|
1085
|
+
throw error;
|
|
1086
|
+
}
|
|
1087
|
+
if (canUseStoredToken(stored)) return buildLease(stored.accessToken, getLeaseExpiry(stored.expiresAt), input.reason);
|
|
1088
|
+
throw new CredentialUnavailableError(provider, `Your ${provider} connection has expired.`);
|
|
1089
|
+
}
|
|
1090
|
+
throw new CredentialUnavailableError(provider, `No ${provider} credentials available.`);
|
|
1091
|
+
}
|
|
1092
|
+
if (envToken) return buildLease(envToken, getLeaseExpiry(), input.reason);
|
|
1093
|
+
throw new CredentialUnavailableError(provider, `No ${provider} credentials available.`);
|
|
1094
|
+
} };
|
|
1095
|
+
}
|
|
1096
|
+
//#endregion
|
|
1097
|
+
//#region src/chat/plugins/registry.ts
|
|
1098
|
+
function getLoggedPluginNames() {
|
|
1099
|
+
const globalState = globalThis;
|
|
1100
|
+
globalState.__juniorLoggedPluginNames ??= /* @__PURE__ */ new Set();
|
|
1101
|
+
return globalState.__juniorLoggedPluginNames;
|
|
1102
|
+
}
|
|
1103
|
+
function createLoadedPluginState(signature) {
|
|
1104
|
+
return {
|
|
1105
|
+
signature,
|
|
1106
|
+
pluginDefinitions: [],
|
|
1107
|
+
pluginMigrationRoots: /* @__PURE__ */ new Map(),
|
|
1108
|
+
domainToPlugin: /* @__PURE__ */ new Map(),
|
|
1109
|
+
pluginConfigKeys: /* @__PURE__ */ new Set(),
|
|
1110
|
+
pluginsByName: /* @__PURE__ */ new Map(),
|
|
1111
|
+
packageSkillRoots: /* @__PURE__ */ new Set()
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
function providerDomains(manifest) {
|
|
1115
|
+
return [.../* @__PURE__ */ new Set([...manifest.credentials?.domains ?? [], ...manifest.domains ?? []])].sort((left, right) => left.localeCompare(right));
|
|
1116
|
+
}
|
|
1117
|
+
function registerPluginManifest(state, manifest, pluginDir, skillsDir, migrationsDir) {
|
|
1118
|
+
if (state.pluginsByName.has(manifest.name)) throw new Error(`Duplicate plugin name "${manifest.name}"`);
|
|
1119
|
+
for (const domain of providerDomains(manifest)) {
|
|
1120
|
+
const owner = state.domainToPlugin.get(domain);
|
|
1121
|
+
if (owner) throw new Error(`Duplicate provider domain "${domain}" in plugin "${manifest.name}" already declared by plugin "${owner}". Use plugins.manifests in PluginCatalogConfig to change one plugin's domains or credentials.`);
|
|
1122
|
+
}
|
|
1123
|
+
const definition = {
|
|
1124
|
+
manifest,
|
|
1125
|
+
dir: pluginDir,
|
|
1126
|
+
...migrationsDir ? { migrationsDir } : void 0,
|
|
1127
|
+
...skillsDir ? { skillsDir } : void 0
|
|
1128
|
+
};
|
|
1129
|
+
state.pluginDefinitions.push(definition);
|
|
1130
|
+
state.pluginsByName.set(manifest.name, definition);
|
|
1131
|
+
if (definition.migrationsDir) state.pluginMigrationRoots.set(manifest.name, definition.migrationsDir);
|
|
1132
|
+
for (const key of manifest.configKeys) state.pluginConfigKeys.add(key);
|
|
1133
|
+
for (const domain of providerDomains(manifest)) state.domainToPlugin.set(domain, manifest.name);
|
|
1134
|
+
}
|
|
1135
|
+
function registerYamlPluginManifest(state, raw, pluginDir, config) {
|
|
1136
|
+
const manifest = parsePluginManifest(raw, pluginDir, config);
|
|
1137
|
+
const candidateSkillsDir = path.join(pluginDir, "skills");
|
|
1138
|
+
registerPluginManifest(state, manifest, pluginDir, (() => {
|
|
1139
|
+
try {
|
|
1140
|
+
return statSync(candidateSkillsDir).isDirectory();
|
|
1141
|
+
} catch {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
})() ? candidateSkillsDir : void 0);
|
|
1145
|
+
}
|
|
1146
|
+
function normalizePluginRoots(roots) {
|
|
1147
|
+
const resolved = [];
|
|
1148
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1149
|
+
for (const root of roots) {
|
|
1150
|
+
const normalized = path.resolve(root);
|
|
1151
|
+
if (seen.has(normalized)) continue;
|
|
1152
|
+
seen.add(normalized);
|
|
1153
|
+
resolved.push(normalized);
|
|
1154
|
+
}
|
|
1155
|
+
return resolved;
|
|
1156
|
+
}
|
|
1157
|
+
function getPluginCatalogSource(runtime) {
|
|
1158
|
+
const packagedContent = discoverConfiguredPluginPackageContent(runtime.pluginConfig);
|
|
1159
|
+
const manifestRoots = normalizePluginRoots([...normalizePluginRoots(pluginRoots()), ...packagedContent.manifestRoots]);
|
|
1160
|
+
const packagedSkillRoots = normalizePluginRoots(packagedContent.skillRoots);
|
|
1161
|
+
const inlineManifests = runtime.pluginConfig?.inlineManifests ?? [];
|
|
1162
|
+
return {
|
|
1163
|
+
inlineManifests,
|
|
1164
|
+
manifestRoots,
|
|
1165
|
+
packagedSkillRoots,
|
|
1166
|
+
packagedContent,
|
|
1167
|
+
signature: JSON.stringify({
|
|
1168
|
+
inlineManifests,
|
|
1169
|
+
manifestRoots,
|
|
1170
|
+
packages: packagedContent.packages.map((pkg) => ({
|
|
1171
|
+
dir: path.resolve(pkg.dir),
|
|
1172
|
+
hasMigrationsDir: pkg.hasMigrationsDir,
|
|
1173
|
+
hasSkillsDir: pkg.hasSkillsDir,
|
|
1174
|
+
packageName: pkg.packageName
|
|
1175
|
+
})).sort((left, right) => left.packageName.localeCompare(right.packageName)),
|
|
1176
|
+
packagedSkillRoots,
|
|
1177
|
+
packageNames: [...packagedContent.packageNames].sort(),
|
|
1178
|
+
pluginConfig: runtime.pluginConfig ?? {}
|
|
1179
|
+
})
|
|
1180
|
+
};
|
|
1181
|
+
}
|
|
1182
|
+
function normalizePluginCatalogConfig(config) {
|
|
1183
|
+
if (!config) return;
|
|
1184
|
+
return {
|
|
1185
|
+
inlineManifests: config.inlineManifests ? structuredClone(config.inlineManifests) : void 0,
|
|
1186
|
+
packages: normalizePluginPackageNames(config.packages),
|
|
1187
|
+
...config.manifests ? { manifests: structuredClone(config.manifests) } : void 0
|
|
1188
|
+
};
|
|
1189
|
+
}
|
|
1190
|
+
function clonePluginCatalogConfig(config) {
|
|
1191
|
+
if (!config) return;
|
|
1192
|
+
return {
|
|
1193
|
+
...config.inlineManifests ? { inlineManifests: structuredClone(config.inlineManifests) } : void 0,
|
|
1194
|
+
packages: [...config.packages ?? []],
|
|
1195
|
+
...config.manifests ? { manifests: structuredClone(config.manifests) } : void 0
|
|
1196
|
+
};
|
|
1197
|
+
}
|
|
1198
|
+
function packageContentByName(packagedContent, packageName) {
|
|
1199
|
+
return packagedContent.packages.find((pkg) => pkg.packageName === packageName);
|
|
1200
|
+
}
|
|
1201
|
+
function registerInlineManifests(state, source, config) {
|
|
1202
|
+
const migrationOwners = /* @__PURE__ */ new Map();
|
|
1203
|
+
for (const definition of source.inlineManifests) {
|
|
1204
|
+
const pkg = definition.packageName ? packageContentByName(source.packagedContent, definition.packageName) : void 0;
|
|
1205
|
+
const dir = pkg?.dir ?? process.cwd();
|
|
1206
|
+
const skillsDir = pkg?.hasSkillsDir ? path.join(pkg.dir, "skills") : void 0;
|
|
1207
|
+
const migrationsDir = pkg?.hasMigrationsDir && statSync(path.join(pkg.dir, "migrations"), { throwIfNoEntry: false })?.isDirectory() ? path.join(pkg.dir, "migrations") : void 0;
|
|
1208
|
+
const manifest = parseInlinePluginManifest(definition.manifest, dir, config);
|
|
1209
|
+
if (migrationsDir) {
|
|
1210
|
+
const owner = migrationOwners.get(migrationsDir);
|
|
1211
|
+
if (owner) throw new Error(`Plugin "${manifest.name}" cannot share migrations directory with plugin "${owner}"`);
|
|
1212
|
+
migrationOwners.set(migrationsDir, manifest.name);
|
|
1213
|
+
}
|
|
1214
|
+
registerPluginManifest(state, manifest, dir, skillsDir, migrationsDir);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
function discoverConfiguredPluginPackageContent(config) {
|
|
1218
|
+
return discoverInstalledPluginPackageContent(process.cwd(), { packageNames: config?.packages });
|
|
1219
|
+
}
|
|
1220
|
+
function buildLoadedPluginState(source, config) {
|
|
1221
|
+
const state = createLoadedPluginState(source.signature);
|
|
1222
|
+
for (const skillRoot of source.packagedSkillRoots) state.packageSkillRoots.add(skillRoot);
|
|
1223
|
+
registerInlineManifests(state, source, config);
|
|
1224
|
+
const roots = source.manifestRoots;
|
|
1225
|
+
for (const pluginsRoot of roots) {
|
|
1226
|
+
let entries;
|
|
1227
|
+
let rootStat;
|
|
1228
|
+
try {
|
|
1229
|
+
rootStat = statSync(pluginsRoot);
|
|
1230
|
+
} catch (error) {
|
|
1231
|
+
logWarn("plugin.root.read.failed", {
|
|
1232
|
+
"file.directory": pluginsRoot,
|
|
1233
|
+
"exception.message": error instanceof Error ? error.message : String(error)
|
|
1234
|
+
});
|
|
1235
|
+
continue;
|
|
1236
|
+
}
|
|
1237
|
+
if (rootStat.isDirectory()) {
|
|
1238
|
+
const manifestPath = path.join(pluginsRoot, "plugin.yaml");
|
|
1239
|
+
let hasRootManifest = false;
|
|
1240
|
+
try {
|
|
1241
|
+
hasRootManifest = statSync(manifestPath).isFile();
|
|
1242
|
+
} catch {
|
|
1243
|
+
hasRootManifest = false;
|
|
1244
|
+
}
|
|
1245
|
+
if (hasRootManifest) {
|
|
1246
|
+
registerYamlPluginManifest(state, readFileSync(manifestPath, "utf8"), pluginsRoot, config);
|
|
1247
|
+
continue;
|
|
1248
|
+
}
|
|
1249
|
+
}
|
|
1250
|
+
try {
|
|
1251
|
+
entries = readdirSync(pluginsRoot);
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
logWarn("plugin.root.read.failed", {
|
|
1254
|
+
"file.directory": pluginsRoot,
|
|
1255
|
+
"exception.message": error instanceof Error ? error.message : String(error)
|
|
1256
|
+
});
|
|
1257
|
+
continue;
|
|
1258
|
+
}
|
|
1259
|
+
for (const entry of entries.sort()) {
|
|
1260
|
+
const pluginDir = path.join(pluginsRoot, entry);
|
|
1261
|
+
try {
|
|
1262
|
+
if (!statSync(pluginDir).isDirectory()) continue;
|
|
1263
|
+
} catch {
|
|
1264
|
+
continue;
|
|
1265
|
+
}
|
|
1266
|
+
const manifestPath = path.join(pluginDir, "plugin.yaml");
|
|
1267
|
+
let raw;
|
|
1268
|
+
try {
|
|
1269
|
+
raw = readFileSync(manifestPath, "utf8");
|
|
1270
|
+
} catch {
|
|
1271
|
+
continue;
|
|
1272
|
+
}
|
|
1273
|
+
registerYamlPluginManifest(state, raw, pluginDir, config);
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
for (const name of Object.keys(config?.manifests ?? {})) if (!state.pluginsByName.has(name)) throw new Error(`plugins.manifests.${name} does not match a loaded plugin`);
|
|
1277
|
+
return state;
|
|
1278
|
+
}
|
|
1279
|
+
function logLoadedPlugins(state) {
|
|
1280
|
+
const loggedPluginNames = getLoggedPluginNames();
|
|
1281
|
+
for (const plugin of [...state.pluginDefinitions].sort((left, right) => left.manifest.name.localeCompare(right.manifest.name))) {
|
|
1282
|
+
if (loggedPluginNames.has(plugin.manifest.name)) continue;
|
|
1283
|
+
loggedPluginNames.add(plugin.manifest.name);
|
|
1284
|
+
logInfo("plugin.loaded", {
|
|
1285
|
+
"app.plugin.name": plugin.manifest.name,
|
|
1286
|
+
"app.plugin.config_key_count": plugin.manifest.configKeys.length,
|
|
1287
|
+
"app.plugin.has_mcp": Boolean(plugin.manifest.mcp),
|
|
1288
|
+
"file.directory": plugin.dir,
|
|
1289
|
+
...plugin.skillsDir ? { "app.file.skill_directory": plugin.skillsDir } : void 0
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function ensurePluginsLoaded(runtime) {
|
|
1294
|
+
const source = getPluginCatalogSource(runtime);
|
|
1295
|
+
if (runtime.loadedPluginState?.signature === source.signature) return runtime.loadedPluginState;
|
|
1296
|
+
const state = buildLoadedPluginState(source, runtime.pluginConfig);
|
|
1297
|
+
runtime.loadedPluginState = state;
|
|
1298
|
+
logLoadedPlugins(state);
|
|
1299
|
+
return state;
|
|
1300
|
+
}
|
|
1301
|
+
/** Create an isolated plugin catalog runtime for explicit app wiring. */
|
|
1302
|
+
function createPluginCatalogRuntime() {
|
|
1303
|
+
const runtime = {};
|
|
1304
|
+
return {
|
|
1305
|
+
setConfig(config) {
|
|
1306
|
+
const previousConfig = clonePluginCatalogConfig(runtime.pluginConfig);
|
|
1307
|
+
runtime.pluginConfig = normalizePluginCatalogConfig(config);
|
|
1308
|
+
return previousConfig;
|
|
1309
|
+
},
|
|
1310
|
+
parseConfiguredInlineManifest(manifest, dir) {
|
|
1311
|
+
return parseInlinePluginManifest(manifest, dir, runtime.pluginConfig);
|
|
1312
|
+
},
|
|
1313
|
+
getPackageContent() {
|
|
1314
|
+
return discoverConfiguredPluginPackageContent(runtime.pluginConfig);
|
|
1315
|
+
},
|
|
1316
|
+
getSignature() {
|
|
1317
|
+
return ensurePluginsLoaded(runtime).signature;
|
|
1318
|
+
},
|
|
1319
|
+
getProviders() {
|
|
1320
|
+
return [...ensurePluginsLoaded(runtime).pluginDefinitions];
|
|
1321
|
+
},
|
|
1322
|
+
getMigrationRoots() {
|
|
1323
|
+
return [...ensurePluginsLoaded(runtime).pluginMigrationRoots.entries()].map(([pluginName, dir]) => ({
|
|
1324
|
+
pluginName,
|
|
1325
|
+
dir
|
|
1326
|
+
})).sort((left, right) => left.pluginName.localeCompare(right.pluginName));
|
|
1327
|
+
},
|
|
1328
|
+
getMcpProviders() {
|
|
1329
|
+
return ensurePluginsLoaded(runtime).pluginDefinitions.filter((plugin) => Boolean(plugin.manifest.mcp));
|
|
1330
|
+
},
|
|
1331
|
+
getRuntimeDependencies() {
|
|
1332
|
+
const state = ensurePluginsLoaded(runtime);
|
|
1333
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1334
|
+
const deps = [];
|
|
1335
|
+
for (const plugin of state.pluginDefinitions) for (const dep of plugin.manifest.runtimeDependencies ?? []) {
|
|
1336
|
+
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}`;
|
|
1337
|
+
if (seen.has(key)) continue;
|
|
1338
|
+
seen.add(key);
|
|
1339
|
+
deps.push(dep);
|
|
1340
|
+
}
|
|
1341
|
+
return deps.sort((left, right) => {
|
|
1342
|
+
if (left.type !== right.type) return left.type.localeCompare(right.type);
|
|
1343
|
+
const leftIdentity = "package" in left ? `package:${left.package}` : `url:${left.url}:${left.sha256}`;
|
|
1344
|
+
const rightIdentity = "package" in right ? `package:${right.package}` : `url:${right.url}:${right.sha256}`;
|
|
1345
|
+
if (leftIdentity !== rightIdentity) return leftIdentity.localeCompare(rightIdentity);
|
|
1346
|
+
if (left.type === "npm" && right.type === "npm") return left.version.localeCompare(right.version);
|
|
1347
|
+
return 0;
|
|
1348
|
+
});
|
|
1349
|
+
},
|
|
1350
|
+
getRuntimePostinstall() {
|
|
1351
|
+
const state = ensurePluginsLoaded(runtime);
|
|
1352
|
+
const commands = [];
|
|
1353
|
+
for (const plugin of state.pluginDefinitions) for (const command of plugin.manifest.runtimePostinstall ?? []) commands.push({
|
|
1354
|
+
cmd: command.cmd,
|
|
1355
|
+
...command.args ? { args: [...command.args] } : void 0,
|
|
1356
|
+
...command.sudo !== void 0 ? { sudo: command.sudo } : void 0
|
|
1357
|
+
});
|
|
1358
|
+
return commands;
|
|
1359
|
+
},
|
|
1360
|
+
getOAuthConfig(provider) {
|
|
1361
|
+
const plugin = ensurePluginsLoaded(runtime).pluginsByName.get(provider);
|
|
1362
|
+
if (!plugin?.manifest.oauth) return void 0;
|
|
1363
|
+
const oauth = plugin.manifest.oauth;
|
|
1364
|
+
return {
|
|
1365
|
+
clientIdEnv: oauth.clientIdEnv,
|
|
1366
|
+
clientSecretEnv: oauth.clientSecretEnv,
|
|
1367
|
+
authorizeEndpoint: oauth.authorizeEndpoint,
|
|
1368
|
+
tokenEndpoint: oauth.tokenEndpoint,
|
|
1369
|
+
...oauth.scope ? { scope: oauth.scope } : void 0,
|
|
1370
|
+
...oauth.authorizeParams ? { authorizeParams: { ...oauth.authorizeParams } } : void 0,
|
|
1371
|
+
...oauth.tokenAuthMethod ? { tokenAuthMethod: oauth.tokenAuthMethod } : void 0,
|
|
1372
|
+
...oauth.tokenExtraHeaders ? { tokenExtraHeaders: { ...oauth.tokenExtraHeaders } } : void 0,
|
|
1373
|
+
...oauth.treatEmptyScopeAsUnreported ? { treatEmptyScopeAsUnreported: true } : void 0,
|
|
1374
|
+
callbackPath: `/api/oauth/callback/${plugin.manifest.name}`
|
|
1375
|
+
};
|
|
1376
|
+
},
|
|
1377
|
+
getSkillRoots() {
|
|
1378
|
+
const state = ensurePluginsLoaded(runtime);
|
|
1379
|
+
return [.../* @__PURE__ */ new Set([...state.pluginDefinitions.flatMap((plugin) => plugin.skillsDir ? [plugin.skillsDir] : []), ...state.packageSkillRoots])];
|
|
1380
|
+
},
|
|
1381
|
+
getForSkillPath(skillPath) {
|
|
1382
|
+
const state = ensurePluginsLoaded(runtime);
|
|
1383
|
+
const resolvedSkillPath = path.resolve(skillPath);
|
|
1384
|
+
return state.pluginDefinitions.find((plugin) => {
|
|
1385
|
+
if (!plugin.skillsDir) return false;
|
|
1386
|
+
const resolvedSkillsDir = path.resolve(plugin.skillsDir);
|
|
1387
|
+
return resolvedSkillPath === resolvedSkillsDir || resolvedSkillPath.startsWith(`${resolvedSkillsDir}${path.sep}`);
|
|
1388
|
+
});
|
|
1389
|
+
},
|
|
1390
|
+
getDefinition(provider) {
|
|
1391
|
+
return ensurePluginsLoaded(runtime).pluginsByName.get(provider);
|
|
1392
|
+
},
|
|
1393
|
+
getDisplayName(provider) {
|
|
1394
|
+
return ensurePluginsLoaded(runtime).pluginsByName.get(provider)?.manifest.displayName;
|
|
1395
|
+
},
|
|
1396
|
+
isProvider(provider) {
|
|
1397
|
+
return ensurePluginsLoaded(runtime).pluginsByName.has(provider);
|
|
1398
|
+
},
|
|
1399
|
+
isConfigKey(key) {
|
|
1400
|
+
return ensurePluginsLoaded(runtime).pluginConfigKeys.has(key);
|
|
1401
|
+
},
|
|
1402
|
+
createBroker(provider, deps) {
|
|
1403
|
+
const plugin = ensurePluginsLoaded(runtime).pluginsByName.get(provider);
|
|
1404
|
+
if (!plugin) throw new Error(`Unknown plugin provider: "${provider}"`);
|
|
1405
|
+
const { credentials, name } = plugin.manifest;
|
|
1406
|
+
if (!credentials && !plugin.manifest.apiHeaders) throw new Error(`Provider "${name}" has no credentials or API headers configured`);
|
|
1407
|
+
let broker;
|
|
1408
|
+
if (!credentials) broker = createApiHeadersBroker(plugin.manifest);
|
|
1409
|
+
else broker = createOAuthBearerBroker(plugin.manifest, credentials, deps);
|
|
1410
|
+
setSpanAttributes({
|
|
1411
|
+
"app.plugin.name": name,
|
|
1412
|
+
"app.plugin.has_oauth": Boolean(plugin.manifest.oauth)
|
|
1413
|
+
});
|
|
1414
|
+
return broker;
|
|
1415
|
+
}
|
|
1416
|
+
};
|
|
1417
|
+
}
|
|
1418
|
+
//#endregion
|
|
1419
|
+
export { resolvePluginCommandEnv as a, getOAuthProviderErrorAttributes as c, parseInlinePluginManifest as d, parsePluginManifest as f, resolveAuthTokenPlaceholder as i, hasRequiredOAuthScope as l, buildOAuthTokenRequest as n, OAuthProviderError as o, parseOAuthTokenResponse as r, fetchWithBoundedOAuthErrorBodies as s, createPluginCatalogRuntime as t, CredentialUnavailableError as u };
|