@sentry/junior 0.2.0 → 0.4.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.
@@ -4,6 +4,10 @@ import {
4
4
  setSpanAttributes,
5
5
  withSpan
6
6
  } from "./chunk-PY4AI2GZ.js";
7
+ import {
8
+ discoverInstalledPluginPackageContent,
9
+ discoverProjectRoots
10
+ } from "./chunk-Z5E25LRN.js";
7
11
 
8
12
  // src/chat/config.ts
9
13
  var MIN_AGENT_TURN_TIMEOUT_MS = 10 * 1e3;
@@ -385,15 +389,15 @@ import { createHash } from "crypto";
385
389
  import { Sandbox } from "@vercel/sandbox";
386
390
 
387
391
  // src/chat/plugins/registry.ts
388
- import { readFileSync as readFileSync2, readdirSync, statSync as statSync2 } from "fs";
389
- import path3 from "path";
392
+ import { readFileSync, readdirSync, statSync } from "fs";
393
+ import path2 from "path";
390
394
  import { parse as parseYaml } from "yaml";
391
395
 
392
396
  // src/chat/home.ts
393
397
  import fs from "fs";
394
398
  import path from "path";
395
399
  function homeDir() {
396
- return detectAppDir();
400
+ return resolveHomeDir();
397
401
  }
398
402
  function unique(values) {
399
403
  return [...new Set(values)];
@@ -409,46 +413,60 @@ function pathExists(targetPath) {
409
413
  function hasAnyDataMarkers(appDir) {
410
414
  return pathExists(path.join(appDir, "SOUL.md"));
411
415
  }
412
- function detectAppDir() {
413
- const cwd = process.cwd();
414
- const directApp = path.resolve(cwd, "app");
415
- if (pathExists(directApp)) {
416
- return directApp;
416
+ function scoreAppCandidate(appDir) {
417
+ let score = 0;
418
+ if (pathExists(path.join(appDir, "SOUL.md"))) {
419
+ score += 4;
417
420
  }
418
- const candidates = [];
419
- const packageRoots = [];
420
- const localPackagesDir = path.join(cwd, "packages");
421
- if (pathExists(localPackagesDir)) {
422
- for (const entry of fs.readdirSync(localPackagesDir)) {
423
- packageRoots.push(path.join(localPackagesDir, entry));
424
- }
421
+ if (pathExists(path.join(appDir, "ABOUT.md"))) {
422
+ score += 2;
425
423
  }
426
- for (const entry of fs.readdirSync(cwd)) {
427
- const child = path.join(cwd, entry);
428
- const nestedPackages = path.join(child, "packages");
429
- if (!pathExists(nestedPackages)) {
430
- continue;
431
- }
432
- for (const nestedEntry of fs.readdirSync(nestedPackages)) {
433
- packageRoots.push(path.join(nestedPackages, nestedEntry));
434
- }
424
+ if (pathExists(path.join(appDir, "skills"))) {
425
+ score += 1;
435
426
  }
436
- for (const root of packageRoots) {
437
- const appDir = path.join(root, "app");
427
+ if (pathExists(path.join(appDir, "plugins"))) {
428
+ score += 1;
429
+ }
430
+ return score;
431
+ }
432
+ function resolveCandidateAppDirs(cwd, projectRoots) {
433
+ const roots = projectRoots ?? discoverProjectRoots(cwd);
434
+ const resolved = [];
435
+ const seen = /* @__PURE__ */ new Set();
436
+ for (const root of roots) {
437
+ const appDir = path.resolve(root, "app");
438
438
  if (!pathExists(appDir)) {
439
439
  continue;
440
440
  }
441
- candidates.push(appDir);
441
+ if (seen.has(appDir)) {
442
+ continue;
443
+ }
444
+ seen.add(appDir);
445
+ resolved.push(appDir);
442
446
  }
447
+ return resolved;
448
+ }
449
+ function resolveHomeDir(cwd = process.cwd(), options) {
450
+ const resolvedCwd = path.resolve(cwd);
451
+ const directApp = path.resolve(resolvedCwd, "app");
452
+ if (pathExists(directApp) && hasAnyDataMarkers(directApp)) {
453
+ return directApp;
454
+ }
455
+ const candidates = resolveCandidateAppDirs(resolvedCwd, options?.projectRoots);
443
456
  if (candidates.length === 0) {
444
457
  return directApp;
445
458
  }
446
459
  candidates.sort((left, right) => {
447
- const leftScore = Number(hasAnyDataMarkers(left));
448
- const rightScore = Number(hasAnyDataMarkers(right));
460
+ const leftScore = scoreAppCandidate(left);
461
+ const rightScore = scoreAppCandidate(right);
449
462
  if (leftScore !== rightScore) {
450
463
  return rightScore - leftScore;
451
464
  }
465
+ const leftDistance = path.relative(resolvedCwd, left).split(path.sep).length;
466
+ const rightDistance = path.relative(resolvedCwd, right).split(path.sep).length;
467
+ if (leftDistance !== rightDistance) {
468
+ return leftDistance - rightDistance;
469
+ }
452
470
  return left.localeCompare(right);
453
471
  });
454
472
  return candidates[0];
@@ -536,7 +554,9 @@ function getPrivateKey(envName) {
536
554
  );
537
555
  }
538
556
  if (key.asymmetricKeyType !== "rsa") {
539
- throw new Error(`Invalid ${envName}: GitHub App signing requires an RSA private key`);
557
+ throw new Error(
558
+ `Invalid ${envName}: GitHub App signing requires an RSA private key`
559
+ );
540
560
  }
541
561
  return key;
542
562
  }
@@ -553,8 +573,8 @@ function createAppJwt(appId, privateKeyEnv) {
553
573
  const signature = signer.sign(getPrivateKey(privateKeyEnv)).toString("base64").replace(/=/g, "").replace(/\+/g, "-").replace(/\//g, "_");
554
574
  return `${signingInput}.${signature}`;
555
575
  }
556
- async function githubRequest(apiBase, path4, params) {
557
- const response = await fetch(`${apiBase}${path4}`, {
576
+ async function githubRequest(apiBase, path3, params) {
577
+ const response = await fetch(`${apiBase}${path3}`, {
558
578
  method: params.method ?? "GET",
559
579
  headers: {
560
580
  Accept: "application/vnd.github+json",
@@ -591,7 +611,14 @@ function capabilityToPermissions(capability, pluginName) {
591
611
  function createGitHubAppBroker(manifest, credentials) {
592
612
  const tokenCache = /* @__PURE__ */ new Map();
593
613
  const provider = manifest.name;
594
- const { apiDomains, authTokenEnv, appIdEnv, privateKeyEnv, installationIdEnv } = credentials;
614
+ const {
615
+ apiDomains,
616
+ apiHeaders,
617
+ authTokenEnv,
618
+ appIdEnv,
619
+ privateKeyEnv,
620
+ installationIdEnv
621
+ } = credentials;
595
622
  const apiBase = `https://${apiDomains[0]}`;
596
623
  const placeholder = resolveAuthTokenPlaceholder(credentials);
597
624
  return {
@@ -622,6 +649,7 @@ function createGitHubAppBroker(manifest, credentials) {
622
649
  headerTransforms: apiDomains.map((domain) => ({
623
650
  domain,
624
651
  headers: {
652
+ ...apiHeaders ?? {},
625
653
  Authorization: `Bearer ${cached.token}`
626
654
  }
627
655
  })),
@@ -641,17 +669,16 @@ function createGitHubAppBroker(manifest, credentials) {
641
669
  if (repositoryName) {
642
670
  tokenRequestBody.repositories = [repositoryName];
643
671
  }
644
- const accessTokenResponse = await githubRequest(
645
- apiBase,
646
- `/app/installations/${installationId}/access_tokens`,
647
- {
648
- method: "POST",
649
- token: appJwt,
650
- body: tokenRequestBody
651
- }
652
- );
672
+ const accessTokenResponse = await githubRequest(apiBase, `/app/installations/${installationId}/access_tokens`, {
673
+ method: "POST",
674
+ token: appJwt,
675
+ body: tokenRequestBody
676
+ });
653
677
  const providerExpiresAtMs = Date.parse(accessTokenResponse.expires_at);
654
- const expiresAtMs = Math.min(providerExpiresAtMs, Date.now() + MAX_LEASE_MS);
678
+ const expiresAtMs = Math.min(
679
+ providerExpiresAtMs,
680
+ Date.now() + MAX_LEASE_MS
681
+ );
655
682
  tokenCache.set(cacheKey, {
656
683
  installationId,
657
684
  token: accessTokenResponse.token,
@@ -665,6 +692,7 @@ function createGitHubAppBroker(manifest, credentials) {
665
692
  headerTransforms: apiDomains.map((domain) => ({
666
693
  domain,
667
694
  headers: {
695
+ ...apiHeaders ?? {},
668
696
  Authorization: `Bearer ${accessTokenResponse.token}`
669
697
  }
670
698
  })),
@@ -692,6 +720,70 @@ var CredentialUnavailableError = class extends Error {
692
720
  }
693
721
  };
694
722
 
723
+ // src/chat/plugins/oauth-request.ts
724
+ var DEFAULT_TOKEN_CONTENT_TYPE = "application/x-www-form-urlencoded";
725
+ function requireNonEmptyTokenField(data, field) {
726
+ const value = data[field];
727
+ if (typeof value !== "string" || !value.trim()) {
728
+ throw new Error(`OAuth token response missing ${field}`);
729
+ }
730
+ return value;
731
+ }
732
+ function contentTypeToBody(contentType, payload) {
733
+ const mediaType = contentType.split(";", 1)[0]?.trim().toLowerCase();
734
+ if (!mediaType || mediaType === DEFAULT_TOKEN_CONTENT_TYPE) {
735
+ return new URLSearchParams(payload);
736
+ }
737
+ if (mediaType === "application/json" || mediaType.endsWith("+json")) {
738
+ return JSON.stringify(payload);
739
+ }
740
+ throw new Error(`Unsupported OAuth token Content-Type: ${contentType}`);
741
+ }
742
+ function buildOAuthTokenRequest(input) {
743
+ const headers = new Headers({ Accept: "application/json" });
744
+ for (const [name, value] of Object.entries(input.tokenExtraHeaders ?? {})) {
745
+ headers.set(name, value);
746
+ }
747
+ if (!headers.has("Content-Type")) {
748
+ headers.set("Content-Type", DEFAULT_TOKEN_CONTENT_TYPE);
749
+ }
750
+ const payload = { ...input.payload };
751
+ if (input.tokenAuthMethod === "basic") {
752
+ headers.set(
753
+ "Authorization",
754
+ `Basic ${Buffer.from(`${input.clientId}:${input.clientSecret}`).toString("base64")}`
755
+ );
756
+ } else {
757
+ payload.client_id = input.clientId;
758
+ payload.client_secret = input.clientSecret;
759
+ }
760
+ const contentType = headers.get("Content-Type") ?? DEFAULT_TOKEN_CONTENT_TYPE;
761
+ const serializedHeaders = {};
762
+ headers.forEach((value, key) => {
763
+ serializedHeaders[key] = value;
764
+ });
765
+ return {
766
+ headers: serializedHeaders,
767
+ body: contentTypeToBody(contentType, payload)
768
+ };
769
+ }
770
+ function parseOAuthTokenResponse(data) {
771
+ const accessToken = requireNonEmptyTokenField(data, "access_token");
772
+ const refreshToken = requireNonEmptyTokenField(data, "refresh_token");
773
+ const expiresIn = data.expires_in;
774
+ if (expiresIn === void 0) {
775
+ return { accessToken, refreshToken };
776
+ }
777
+ if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
778
+ throw new Error("OAuth token response returned invalid expires_in");
779
+ }
780
+ return {
781
+ accessToken,
782
+ refreshToken,
783
+ expiresAt: Date.now() + expiresIn * 1e3
784
+ };
785
+ }
786
+
695
787
  // src/chat/plugins/oauth-bearer-broker.ts
696
788
  var MAX_LEASE_MS2 = 60 * 60 * 1e3;
697
789
  var REFRESH_BUFFER_MS = 5 * 60 * 1e3;
@@ -699,35 +791,38 @@ async function refreshAccessToken(refreshToken, oauth) {
699
791
  const clientId = process.env[oauth.clientIdEnv]?.trim();
700
792
  const clientSecret = process.env[oauth.clientSecretEnv]?.trim();
701
793
  if (!clientId || !clientSecret) {
702
- throw new Error(`Missing ${oauth.clientIdEnv} or ${oauth.clientSecretEnv} for token refresh`);
794
+ throw new Error(
795
+ `Missing ${oauth.clientIdEnv} or ${oauth.clientSecretEnv} for token refresh`
796
+ );
703
797
  }
798
+ const request = buildOAuthTokenRequest({
799
+ clientId,
800
+ clientSecret,
801
+ payload: {
802
+ grant_type: "refresh_token",
803
+ refresh_token: refreshToken
804
+ },
805
+ tokenAuthMethod: oauth.tokenAuthMethod,
806
+ tokenExtraHeaders: oauth.tokenExtraHeaders
807
+ });
704
808
  const response = await fetch(oauth.tokenEndpoint, {
705
809
  method: "POST",
706
- headers: { "Content-Type": "application/x-www-form-urlencoded" },
707
- body: new URLSearchParams({
708
- grant_type: "refresh_token",
709
- refresh_token: refreshToken,
710
- client_id: clientId,
711
- client_secret: clientSecret
712
- })
810
+ headers: request.headers,
811
+ body: request.body
713
812
  });
714
813
  if (!response.ok) {
715
814
  throw new Error(`Token refresh failed: ${response.status}`);
716
815
  }
717
816
  const data = await response.json();
718
- if (!data.access_token || !data.refresh_token || typeof data.expires_in !== "number") {
719
- throw new Error("Token refresh returned malformed response");
720
- }
721
- return {
722
- accessToken: data.access_token,
723
- refreshToken: data.refresh_token,
724
- expiresIn: data.expires_in
725
- };
817
+ return parseOAuthTokenResponse(data);
818
+ }
819
+ function getLeaseExpiry(expiresAt) {
820
+ return expiresAt ? Math.min(expiresAt, Date.now() + MAX_LEASE_MS2) : Date.now() + MAX_LEASE_MS2;
726
821
  }
727
822
  function createOAuthBearerBroker(manifest, credentials, deps) {
728
823
  const provider = manifest.name;
729
824
  const supportedCapabilities = new Set(manifest.capabilities);
730
- const { apiDomains, authTokenEnv } = credentials;
825
+ const { apiDomains, apiHeaders, authTokenEnv } = credentials;
731
826
  const authTokenPlaceholder = resolveAuthTokenPlaceholder(credentials);
732
827
  function buildLease(token, capability, expiresAtMs, reason) {
733
828
  return {
@@ -737,7 +832,7 @@ function createOAuthBearerBroker(manifest, credentials, deps) {
737
832
  env: { [authTokenEnv]: authTokenPlaceholder },
738
833
  headerTransforms: apiDomains.map((domain) => ({
739
834
  domain,
740
- headers: { Authorization: `Bearer ${token}` }
835
+ headers: { ...apiHeaders ?? {}, Authorization: `Bearer ${token}` }
741
836
  })),
742
837
  expiresAt: new Date(expiresAtMs).toISOString(),
743
838
  metadata: { reason }
@@ -746,27 +841,58 @@ function createOAuthBearerBroker(manifest, credentials, deps) {
746
841
  return {
747
842
  async issue(input) {
748
843
  if (!supportedCapabilities.has(input.capability)) {
749
- throw new Error(`Unsupported ${provider} capability: ${input.capability}`);
844
+ throw new Error(
845
+ `Unsupported ${provider} capability: ${input.capability}`
846
+ );
847
+ }
848
+ const envToken = process.env[authTokenEnv]?.trim();
849
+ const oauth = manifest.oauth;
850
+ if (!oauth) {
851
+ if (envToken) {
852
+ return buildLease(
853
+ envToken,
854
+ input.capability,
855
+ Date.now() + MAX_LEASE_MS2,
856
+ input.reason
857
+ );
858
+ }
859
+ throw new CredentialUnavailableError(
860
+ provider,
861
+ `No ${provider} credentials available.`
862
+ );
750
863
  }
751
- if (input.requesterId && deps.userTokenStore) {
752
- const stored = await deps.userTokenStore.get(input.requesterId, provider);
864
+ if (input.requesterId) {
865
+ const stored = await deps.userTokenStore.get(
866
+ input.requesterId,
867
+ provider
868
+ );
753
869
  if (stored) {
754
870
  const now = Date.now();
755
- if (stored.expiresAt - now < REFRESH_BUFFER_MS && stored.refreshToken && manifest.oauth) {
871
+ if (stored.expiresAt !== void 0 && stored.expiresAt - now < REFRESH_BUFFER_MS) {
756
872
  try {
757
- const refreshed = await refreshAccessToken(stored.refreshToken, manifest.oauth);
758
- const expiresAt = Date.now() + refreshed.expiresIn * 1e3;
759
- await deps.userTokenStore.set(input.requesterId, provider, {
760
- accessToken: refreshed.accessToken,
761
- refreshToken: refreshed.refreshToken,
762
- expiresAt
763
- });
764
- const leaseExpiry = Math.min(expiresAt, Date.now() + MAX_LEASE_MS2);
765
- return buildLease(refreshed.accessToken, input.capability, leaseExpiry, input.reason);
873
+ const refreshed = await refreshAccessToken(
874
+ stored.refreshToken,
875
+ oauth
876
+ );
877
+ await deps.userTokenStore.set(
878
+ input.requesterId,
879
+ provider,
880
+ refreshed
881
+ );
882
+ return buildLease(
883
+ refreshed.accessToken,
884
+ input.capability,
885
+ getLeaseExpiry(refreshed.expiresAt),
886
+ input.reason
887
+ );
766
888
  } catch {
767
889
  if (stored.expiresAt > Date.now()) {
768
- const leaseExpiry = Math.min(stored.expiresAt, Date.now() + MAX_LEASE_MS2);
769
- return buildLease(stored.accessToken, input.capability, leaseExpiry, input.reason);
890
+ return buildLease(
891
+ stored.accessToken,
892
+ input.capability,
893
+ getLeaseExpiry(stored.expiresAt),
894
+ input.reason
895
+ );
770
896
  }
771
897
  throw new CredentialUnavailableError(
772
898
  provider,
@@ -774,9 +900,13 @@ function createOAuthBearerBroker(manifest, credentials, deps) {
774
900
  );
775
901
  }
776
902
  }
777
- if (stored.expiresAt > Date.now()) {
778
- const leaseExpiry = Math.min(stored.expiresAt, Date.now() + MAX_LEASE_MS2);
779
- return buildLease(stored.accessToken, input.capability, leaseExpiry, input.reason);
903
+ if (stored.expiresAt === void 0 || stored.expiresAt > Date.now()) {
904
+ return buildLease(
905
+ stored.accessToken,
906
+ input.capability,
907
+ getLeaseExpiry(stored.expiresAt),
908
+ input.reason
909
+ );
780
910
  }
781
911
  throw new CredentialUnavailableError(
782
912
  provider,
@@ -788,10 +918,13 @@ function createOAuthBearerBroker(manifest, credentials, deps) {
788
918
  `No ${provider} credentials available.`
789
919
  );
790
920
  }
791
- const envToken = process.env[authTokenEnv]?.trim();
792
921
  if (envToken) {
793
- const expiresAtMs = Date.now() + MAX_LEASE_MS2;
794
- return buildLease(envToken, input.capability, expiresAtMs, input.reason);
922
+ return buildLease(
923
+ envToken,
924
+ input.capability,
925
+ getLeaseExpiry(),
926
+ input.reason
927
+ );
795
928
  }
796
929
  throw new CredentialUnavailableError(
797
930
  provider,
@@ -801,98 +934,6 @@ function createOAuthBearerBroker(manifest, credentials, deps) {
801
934
  };
802
935
  }
803
936
 
804
- // src/chat/plugins/package-discovery.ts
805
- import { readFileSync, statSync } from "fs";
806
- import path2 from "path";
807
- function isDirectory(targetPath) {
808
- try {
809
- return statSync(targetPath).isDirectory();
810
- } catch {
811
- return false;
812
- }
813
- }
814
- function isFile(targetPath) {
815
- try {
816
- return statSync(targetPath).isFile();
817
- } catch {
818
- return false;
819
- }
820
- }
821
- function readRootPackageJson(cwd) {
822
- const rootPackageJsonPath = path2.join(cwd, "package.json");
823
- try {
824
- const raw = readFileSync(rootPackageJsonPath, "utf8");
825
- return JSON.parse(raw);
826
- } catch {
827
- return null;
828
- }
829
- }
830
- function uniqueSorted(values) {
831
- return [...new Set(values)].sort((left, right) => left.localeCompare(right));
832
- }
833
- function readInstalledDependencyNames(cwd) {
834
- const rootPackageJson = readRootPackageJson(cwd);
835
- if (!rootPackageJson) {
836
- return [];
837
- }
838
- return uniqueSorted([
839
- ...Object.keys(rootPackageJson.dependencies ?? {}),
840
- ...Object.keys(rootPackageJson.optionalDependencies ?? {})
841
- ]);
842
- }
843
- function packageInstallDir(cwd, packageName) {
844
- return path2.join(cwd, "node_modules", ...packageName.split("/"));
845
- }
846
- function discoverInstalledJuniorContentPackages(cwd = process.cwd()) {
847
- const dependencies = readInstalledDependencyNames(cwd);
848
- const discovered = [];
849
- for (const dependency of dependencies) {
850
- const dir = packageInstallDir(cwd, dependency);
851
- if (!isDirectory(dir)) {
852
- continue;
853
- }
854
- const hasRootPluginManifest = isFile(path2.join(dir, "plugin.yaml"));
855
- const hasPluginsDir = isDirectory(path2.join(dir, "plugins"));
856
- const hasSkillsDir = isDirectory(path2.join(dir, "skills"));
857
- if (!hasRootPluginManifest && !hasPluginsDir && !hasSkillsDir) {
858
- continue;
859
- }
860
- discovered.push({
861
- name: dependency,
862
- dir,
863
- hasRootPluginManifest,
864
- hasPluginsDir,
865
- hasSkillsDir
866
- });
867
- }
868
- return discovered;
869
- }
870
- function discoverInstalledPluginPackageContent(cwd = process.cwd()) {
871
- const manifestRoots = [];
872
- const skillRoots2 = [];
873
- const tracingIncludes = [];
874
- for (const pkg of discoverInstalledJuniorContentPackages(cwd)) {
875
- const base = `./node_modules/${pkg.name}`;
876
- if (pkg.hasRootPluginManifest) {
877
- manifestRoots.push(pkg.dir);
878
- tracingIncludes.push(`${base}/plugin.yaml`);
879
- }
880
- if (pkg.hasPluginsDir) {
881
- manifestRoots.push(path2.join(pkg.dir, "plugins"));
882
- tracingIncludes.push(`${base}/plugins/**/*`);
883
- }
884
- if (pkg.hasSkillsDir) {
885
- skillRoots2.push(path2.join(pkg.dir, "skills"));
886
- tracingIncludes.push(`${base}/skills/**/*`);
887
- }
888
- }
889
- return {
890
- manifestRoots: uniqueSorted(manifestRoots),
891
- skillRoots: uniqueSorted(skillRoots2),
892
- tracingIncludes: uniqueSorted(tracingIncludes)
893
- };
894
- }
895
-
896
937
  // src/chat/plugins/registry.ts
897
938
  var PLUGIN_NAME_RE = /^[a-z][a-z0-9-]*$/;
898
939
  var SHORT_CAPABILITY_RE = /^[a-z0-9]+(\.[a-z0-9-]+)*$/;
@@ -900,6 +941,15 @@ var SHORT_CONFIG_KEY_RE = /^[a-z0-9]+(\.[a-z0-9-]+)*$/;
900
941
  var AUTH_TOKEN_ENV_RE = /^[A-Z][A-Z0-9_]*$/;
901
942
  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])?$/;
902
943
  var RUNTIME_POSTINSTALL_CMD_RE = /^[A-Za-z0-9._/-]+$/;
944
+ var RESERVED_AUTHORIZE_PARAM_KEYS = /* @__PURE__ */ new Set([
945
+ "client_id",
946
+ "scope",
947
+ "state",
948
+ "redirect_uri",
949
+ "response_type"
950
+ ]);
951
+ var FORBIDDEN_API_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
952
+ var FORBIDDEN_TOKEN_HEADER_NAMES = /* @__PURE__ */ new Set(["authorization"]);
903
953
  function toRecord(value, errorMessage) {
904
954
  if (!value || typeof value !== "object" || Array.isArray(value)) {
905
955
  throw new Error(errorMessage);
@@ -914,14 +964,24 @@ function requireStringField(record, field, errorMessage) {
914
964
  return value.trim();
915
965
  }
916
966
  function requireEnvVarField(record, field, pluginName) {
917
- const value = requireStringField(record, field, `Plugin ${pluginName} ${field} must be a non-empty string`);
967
+ const value = requireStringField(
968
+ record,
969
+ field,
970
+ `Plugin ${pluginName} ${field} must be a non-empty string`
971
+ );
918
972
  if (!AUTH_TOKEN_ENV_RE.test(value)) {
919
- throw new Error(`Plugin ${pluginName} ${field} must be an uppercase env var name`);
973
+ throw new Error(
974
+ `Plugin ${pluginName} ${field} must be an uppercase env var name`
975
+ );
920
976
  }
921
977
  return value;
922
978
  }
923
979
  function requireHttpsUrlField(record, field, pluginName) {
924
- const value = requireStringField(record, field, `Plugin ${pluginName} oauth.${field} must be a non-empty string`);
980
+ const value = requireStringField(
981
+ record,
982
+ field,
983
+ `Plugin ${pluginName} oauth.${field} must be a non-empty string`
984
+ );
925
985
  let parsed;
926
986
  try {
927
987
  parsed = new URL(value);
@@ -936,26 +996,79 @@ function requireHttpsUrlField(record, field, pluginName) {
936
996
  function normalizeApiDomain(rawDomain, name) {
937
997
  const domain = typeof rawDomain === "string" ? rawDomain.trim().toLowerCase() : "";
938
998
  if (!domain) {
939
- throw new Error(`Plugin ${name} credentials.api-domains entries must be non-empty strings`);
999
+ throw new Error(
1000
+ `Plugin ${name} credentials.api-domains entries must be non-empty strings`
1001
+ );
940
1002
  }
941
1003
  if (!API_DOMAIN_RE.test(domain)) {
942
- throw new Error(`Plugin ${name} credentials.api-domains entries must be valid domain names`);
1004
+ throw new Error(
1005
+ `Plugin ${name} credentials.api-domains entries must be valid domain names`
1006
+ );
943
1007
  }
944
1008
  return domain;
945
1009
  }
1010
+ function parseStringMap(data, errorLabel, options = {}) {
1011
+ if (data === void 0) {
1012
+ return void 0;
1013
+ }
1014
+ const record = toRecord(
1015
+ data,
1016
+ `${errorLabel} must be an object when provided`
1017
+ );
1018
+ const entries = Object.entries(record);
1019
+ if (entries.length === 0) {
1020
+ return void 0;
1021
+ }
1022
+ const result = {};
1023
+ const seen = /* @__PURE__ */ new Set();
1024
+ for (const [rawKey, rawValue] of entries) {
1025
+ const key = rawKey.trim();
1026
+ if (!key) {
1027
+ throw new Error(`${errorLabel} keys must be non-empty strings`);
1028
+ }
1029
+ if (typeof rawValue !== "string" || !rawValue.trim()) {
1030
+ throw new Error(`${errorLabel}.${key} must be a non-empty string`);
1031
+ }
1032
+ const normalizedKey = key.toLowerCase();
1033
+ if (options.reservedKeys?.has(normalizedKey)) {
1034
+ throw new Error(`${errorLabel}.${key} is reserved by the runtime`);
1035
+ }
1036
+ if (options.forbiddenKeys?.has(normalizedKey)) {
1037
+ throw new Error(`${errorLabel}.${key} is not allowed`);
1038
+ }
1039
+ if (seen.has(normalizedKey)) {
1040
+ throw new Error(`${errorLabel}.${key} is duplicated`);
1041
+ }
1042
+ seen.add(normalizedKey);
1043
+ result[key] = rawValue.trim();
1044
+ }
1045
+ return Object.keys(result).length > 0 ? result : void 0;
1046
+ }
946
1047
  function parseBaseCredentialFields(data, name) {
947
1048
  const rawDomains = data["api-domains"];
948
1049
  if (!Array.isArray(rawDomains) || rawDomains.length === 0) {
949
- throw new Error(`Plugin ${name} credentials.api-domains must be a non-empty array of strings`);
1050
+ throw new Error(
1051
+ `Plugin ${name} credentials.api-domains must be a non-empty array of strings`
1052
+ );
950
1053
  }
951
- const apiDomains = rawDomains.map((rawDomain) => normalizeApiDomain(rawDomain, name));
1054
+ const apiDomains = rawDomains.map(
1055
+ (rawDomain) => normalizeApiDomain(rawDomain, name)
1056
+ );
1057
+ const apiHeaders = parseStringMap(
1058
+ data["api-headers"],
1059
+ `Plugin ${name} credentials.api-headers`,
1060
+ { forbiddenKeys: FORBIDDEN_API_HEADER_NAMES }
1061
+ );
952
1062
  const authTokenEnv = requireEnvVarField(data, "auth-token-env", name);
953
1063
  const authTokenPlaceholderRaw = data["auth-token-placeholder"];
954
1064
  if (authTokenPlaceholderRaw !== void 0 && (typeof authTokenPlaceholderRaw !== "string" || !authTokenPlaceholderRaw.trim())) {
955
- throw new Error(`Plugin ${name} credentials.auth-token-placeholder must be a non-empty string when provided`);
1065
+ throw new Error(
1066
+ `Plugin ${name} credentials.auth-token-placeholder must be a non-empty string when provided`
1067
+ );
956
1068
  }
957
1069
  return {
958
1070
  apiDomains,
1071
+ ...apiHeaders ? { apiHeaders } : {},
959
1072
  authTokenEnv,
960
1073
  ...typeof authTokenPlaceholderRaw === "string" ? { authTokenPlaceholder: authTokenPlaceholderRaw.trim() } : {}
961
1074
  };
@@ -970,8 +1083,18 @@ function parseCredentials(data, name) {
970
1083
  const base = parseBaseCredentialFields(data, name);
971
1084
  const appIdEnv = requireEnvVarField(data, "app-id-env", name);
972
1085
  const privateKeyEnv = requireEnvVarField(data, "private-key-env", name);
973
- const installationIdEnv = requireEnvVarField(data, "installation-id-env", name);
974
- return { type: "github-app", ...base, appIdEnv, privateKeyEnv, installationIdEnv };
1086
+ const installationIdEnv = requireEnvVarField(
1087
+ data,
1088
+ "installation-id-env",
1089
+ name
1090
+ );
1091
+ return {
1092
+ type: "github-app",
1093
+ ...base,
1094
+ appIdEnv,
1095
+ privateKeyEnv,
1096
+ installationIdEnv
1097
+ };
975
1098
  }
976
1099
  throw new Error(`Plugin ${name} has unsupported credentials.type: "${type}"`);
977
1100
  }
@@ -986,7 +1109,9 @@ function parseRuntimeDependencies(data, name) {
986
1109
  const seen = /* @__PURE__ */ new Set();
987
1110
  for (const entry of data) {
988
1111
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
989
- throw new Error(`Plugin ${name} runtime-dependencies entries must be objects`);
1112
+ throw new Error(
1113
+ `Plugin ${name} runtime-dependencies entries must be objects`
1114
+ );
990
1115
  }
991
1116
  const record = entry;
992
1117
  const type = record.type;
@@ -995,20 +1120,28 @@ function parseRuntimeDependencies(data, name) {
995
1120
  const version = record.version;
996
1121
  const sha256 = record.sha256;
997
1122
  if (typeof type !== "string" || type !== "npm" && type !== "system") {
998
- throw new Error(`Plugin ${name} runtime dependency type must be "npm" or "system"`);
1123
+ throw new Error(
1124
+ `Plugin ${name} runtime dependency type must be "npm" or "system"`
1125
+ );
999
1126
  }
1000
1127
  const normalizedPackage = typeof packageName === "string" ? packageName.trim() : "";
1001
1128
  const normalizedUrl = typeof packageUrl === "string" ? packageUrl.trim() : "";
1002
1129
  if (type === "npm") {
1003
1130
  if (!normalizedPackage) {
1004
- throw new Error(`Plugin ${name} runtime dependency package must be a non-empty string`);
1131
+ throw new Error(
1132
+ `Plugin ${name} runtime dependency package must be a non-empty string`
1133
+ );
1005
1134
  }
1006
1135
  if (packageUrl !== void 0 || sha256 !== void 0) {
1007
- throw new Error(`Plugin ${name} npm runtime dependencies must only include package/version fields`);
1136
+ throw new Error(
1137
+ `Plugin ${name} npm runtime dependencies must only include package/version fields`
1138
+ );
1008
1139
  }
1009
1140
  const normalizedVersion = typeof version === "string" ? version.trim() : "latest";
1010
1141
  if (!normalizedVersion) {
1011
- throw new Error(`Plugin ${name} runtime dependency version must be a non-empty string when provided`);
1142
+ throw new Error(
1143
+ `Plugin ${name} runtime dependency version must be a non-empty string when provided`
1144
+ );
1012
1145
  }
1013
1146
  const dedupeKey2 = `${type}:${normalizedPackage}:${normalizedVersion}`;
1014
1147
  if (seen.has(dedupeKey2)) {
@@ -1023,17 +1156,25 @@ function parseRuntimeDependencies(data, name) {
1023
1156
  continue;
1024
1157
  }
1025
1158
  if (version !== void 0) {
1026
- throw new Error(`Plugin ${name} system runtime dependencies must not include a version`);
1159
+ throw new Error(
1160
+ `Plugin ${name} system runtime dependencies must not include a version`
1161
+ );
1027
1162
  }
1028
1163
  if (normalizedPackage && normalizedUrl) {
1029
- throw new Error(`Plugin ${name} system runtime dependencies must specify either package or url, not both`);
1164
+ throw new Error(
1165
+ `Plugin ${name} system runtime dependencies must specify either package or url, not both`
1166
+ );
1030
1167
  }
1031
1168
  if (!normalizedPackage && !normalizedUrl) {
1032
- throw new Error(`Plugin ${name} system runtime dependencies must specify package or url`);
1169
+ throw new Error(
1170
+ `Plugin ${name} system runtime dependencies must specify package or url`
1171
+ );
1033
1172
  }
1034
1173
  if (normalizedPackage) {
1035
1174
  if (sha256 !== void 0) {
1036
- throw new Error(`Plugin ${name} system runtime dependency package entries must not include sha256`);
1175
+ throw new Error(
1176
+ `Plugin ${name} system runtime dependency package entries must not include sha256`
1177
+ );
1037
1178
  }
1038
1179
  const dedupeKey2 = `${type}:package:${normalizedPackage}`;
1039
1180
  if (seen.has(dedupeKey2)) {
@@ -1047,11 +1188,15 @@ function parseRuntimeDependencies(data, name) {
1047
1188
  continue;
1048
1189
  }
1049
1190
  if (!/^https:\/\//i.test(normalizedUrl)) {
1050
- throw new Error(`Plugin ${name} system runtime dependency url must be an https URL`);
1191
+ throw new Error(
1192
+ `Plugin ${name} system runtime dependency url must be an https URL`
1193
+ );
1051
1194
  }
1052
1195
  const normalizedSha256 = typeof sha256 === "string" ? sha256.trim().toLowerCase() : "";
1053
1196
  if (!/^[a-f0-9]{64}$/.test(normalizedSha256)) {
1054
- throw new Error(`Plugin ${name} system runtime dependency url entries must include a valid sha256`);
1197
+ throw new Error(
1198
+ `Plugin ${name} system runtime dependency url entries must include a valid sha256`
1199
+ );
1055
1200
  }
1056
1201
  const dedupeKey = `${type}:url:${normalizedUrl}:${normalizedSha256}`;
1057
1202
  if (seen.has(dedupeKey)) {
@@ -1076,12 +1221,16 @@ function parseRuntimePostinstall(data, name) {
1076
1221
  const parsed = [];
1077
1222
  for (const entry of data) {
1078
1223
  if (!entry || typeof entry !== "object" || Array.isArray(entry)) {
1079
- throw new Error(`Plugin ${name} runtime-postinstall entries must be objects`);
1224
+ throw new Error(
1225
+ `Plugin ${name} runtime-postinstall entries must be objects`
1226
+ );
1080
1227
  }
1081
1228
  const record = entry;
1082
1229
  const cmd = typeof record.cmd === "string" ? record.cmd.trim() : "";
1083
1230
  if (!cmd) {
1084
- throw new Error(`Plugin ${name} runtime-postinstall cmd must be a non-empty string`);
1231
+ throw new Error(
1232
+ `Plugin ${name} runtime-postinstall cmd must be a non-empty string`
1233
+ );
1085
1234
  }
1086
1235
  if (!RUNTIME_POSTINSTALL_CMD_RE.test(cmd)) {
1087
1236
  throw new Error(
@@ -1090,11 +1239,15 @@ function parseRuntimePostinstall(data, name) {
1090
1239
  }
1091
1240
  const argsRaw = record.args;
1092
1241
  if (argsRaw !== void 0 && (!Array.isArray(argsRaw) || !argsRaw.every((arg) => typeof arg === "string"))) {
1093
- throw new Error(`Plugin ${name} runtime-postinstall args must be an array of strings when provided`);
1242
+ throw new Error(
1243
+ `Plugin ${name} runtime-postinstall args must be an array of strings when provided`
1244
+ );
1094
1245
  }
1095
1246
  const sudoRaw = record.sudo;
1096
1247
  if (sudoRaw !== void 0 && typeof sudoRaw !== "boolean") {
1097
- throw new Error(`Plugin ${name} runtime-postinstall sudo must be a boolean when provided`);
1248
+ throw new Error(
1249
+ `Plugin ${name} runtime-postinstall sudo must be a boolean when provided`
1250
+ );
1098
1251
  }
1099
1252
  const normalizedArgs = Array.isArray(argsRaw) ? argsRaw.map((arg) => arg.trim()).filter((arg) => arg.length > 0) : void 0;
1100
1253
  parsed.push({
@@ -1106,7 +1259,10 @@ function parseRuntimePostinstall(data, name) {
1106
1259
  return parsed.length > 0 ? parsed : void 0;
1107
1260
  }
1108
1261
  function parseManifest(raw, dir) {
1109
- const data = toRecord(parseYaml(raw), `Invalid plugin manifest in ${dir}: expected an object`);
1262
+ const data = toRecord(
1263
+ parseYaml(raw),
1264
+ `Invalid plugin manifest in ${dir}: expected an object`
1265
+ );
1110
1266
  const rawName = data.name;
1111
1267
  if (typeof rawName !== "string" || !PLUGIN_NAME_RE.test(rawName)) {
1112
1268
  throw new Error(`Invalid plugin name in ${dir}: "${rawName}"`);
@@ -1119,7 +1275,9 @@ function parseManifest(raw, dir) {
1119
1275
  const description = rawDescription;
1120
1276
  const rawCapabilities = data.capabilities;
1121
1277
  if (rawCapabilities !== void 0 && !Array.isArray(rawCapabilities)) {
1122
- throw new Error(`Plugin ${name} capabilities must be an array when provided`);
1278
+ throw new Error(
1279
+ `Plugin ${name} capabilities must be an array when provided`
1280
+ );
1123
1281
  }
1124
1282
  const capabilities = [];
1125
1283
  for (const cap of rawCapabilities ?? []) {
@@ -1130,7 +1288,9 @@ function parseManifest(raw, dir) {
1130
1288
  }
1131
1289
  const rawConfigKeys = data["config-keys"];
1132
1290
  if (rawConfigKeys !== void 0 && !Array.isArray(rawConfigKeys)) {
1133
- throw new Error(`Plugin ${name} config-keys must be an array when provided`);
1291
+ throw new Error(
1292
+ `Plugin ${name} config-keys must be an array when provided`
1293
+ );
1134
1294
  }
1135
1295
  const configKeys = [];
1136
1296
  for (const key of rawConfigKeys ?? []) {
@@ -1141,11 +1301,20 @@ function parseManifest(raw, dir) {
1141
1301
  }
1142
1302
  const credentialsRaw = data.credentials;
1143
1303
  if (credentialsRaw !== void 0) {
1144
- toRecord(credentialsRaw, `Plugin ${name} credentials must be an object when provided`);
1304
+ toRecord(
1305
+ credentialsRaw,
1306
+ `Plugin ${name} credentials must be an object when provided`
1307
+ );
1145
1308
  }
1146
1309
  const credentials = credentialsRaw ? parseCredentials(credentialsRaw, name) : void 0;
1147
- const runtimeDependencies = parseRuntimeDependencies(data["runtime-dependencies"], name);
1148
- const runtimePostinstall = parseRuntimePostinstall(data["runtime-postinstall"], name);
1310
+ const runtimeDependencies = parseRuntimeDependencies(
1311
+ data["runtime-dependencies"],
1312
+ name
1313
+ );
1314
+ const runtimePostinstall = parseRuntimePostinstall(
1315
+ data["runtime-postinstall"],
1316
+ name
1317
+ );
1149
1318
  const manifest = {
1150
1319
  name,
1151
1320
  description,
@@ -1161,14 +1330,54 @@ function parseManifest(raw, dir) {
1161
1330
  throw new Error(`Plugin ${name} oauth requires credentials`);
1162
1331
  }
1163
1332
  if (credentials.type !== "oauth-bearer") {
1164
- throw new Error(`Plugin ${name} oauth requires credentials.type "oauth-bearer"`);
1333
+ throw new Error(
1334
+ `Plugin ${name} oauth requires credentials.type "oauth-bearer"`
1335
+ );
1336
+ }
1337
+ const authorizeParams = parseStringMap(
1338
+ oauthRaw["authorize-params"],
1339
+ `Plugin ${name} oauth.authorize-params`,
1340
+ { reservedKeys: RESERVED_AUTHORIZE_PARAM_KEYS }
1341
+ );
1342
+ const tokenExtraHeaders = parseStringMap(
1343
+ oauthRaw["token-extra-headers"],
1344
+ `Plugin ${name} oauth.token-extra-headers`,
1345
+ { forbiddenKeys: FORBIDDEN_TOKEN_HEADER_NAMES }
1346
+ );
1347
+ const tokenAuthMethodRaw = oauthRaw["token-auth-method"];
1348
+ let tokenAuthMethod;
1349
+ if (tokenAuthMethodRaw !== void 0) {
1350
+ const parsedTokenAuthMethod = requireStringField(
1351
+ oauthRaw,
1352
+ "token-auth-method",
1353
+ `Plugin ${name} oauth.token-auth-method must be a non-empty string`
1354
+ );
1355
+ if (parsedTokenAuthMethod !== "body" && parsedTokenAuthMethod !== "basic") {
1356
+ throw new Error(
1357
+ `Plugin ${name} oauth.token-auth-method must be "body" or "basic"`
1358
+ );
1359
+ }
1360
+ tokenAuthMethod = parsedTokenAuthMethod;
1165
1361
  }
1166
1362
  manifest.oauth = {
1167
1363
  clientIdEnv: requireEnvVarField(oauthRaw, "client-id-env", name),
1168
1364
  clientSecretEnv: requireEnvVarField(oauthRaw, "client-secret-env", name),
1169
- authorizeEndpoint: requireHttpsUrlField(oauthRaw, "authorize-endpoint", name),
1365
+ authorizeEndpoint: requireHttpsUrlField(
1366
+ oauthRaw,
1367
+ "authorize-endpoint",
1368
+ name
1369
+ ),
1170
1370
  tokenEndpoint: requireHttpsUrlField(oauthRaw, "token-endpoint", name),
1171
- scope: requireStringField(oauthRaw, "scope", `Plugin ${name} oauth.scope must be a non-empty string`)
1371
+ ...oauthRaw.scope !== void 0 ? {
1372
+ scope: requireStringField(
1373
+ oauthRaw,
1374
+ "scope",
1375
+ `Plugin ${name} oauth.scope must be a non-empty string`
1376
+ )
1377
+ } : {},
1378
+ ...authorizeParams ? { authorizeParams } : {},
1379
+ ...tokenAuthMethod ? { tokenAuthMethod } : {},
1380
+ ...tokenExtraHeaders ? { tokenExtraHeaders } : {}
1172
1381
  };
1173
1382
  }
1174
1383
  const targetRaw = data.target ? toRecord(data.target, `Plugin ${name} target must be an object`) : void 0;
@@ -1178,14 +1387,20 @@ function parseManifest(raw, dir) {
1178
1387
  }
1179
1388
  const rawConfigKey = targetRaw["config-key"];
1180
1389
  if (typeof rawConfigKey !== "string" || !rawConfigKey.trim()) {
1181
- throw new Error(`Plugin ${name} target.config-key must be a non-empty string`);
1390
+ throw new Error(
1391
+ `Plugin ${name} target.config-key must be a non-empty string`
1392
+ );
1182
1393
  }
1183
1394
  if (!SHORT_CONFIG_KEY_RE.test(rawConfigKey)) {
1184
- throw new Error(`Plugin ${name} target.config-key "${rawConfigKey}" is invalid`);
1395
+ throw new Error(
1396
+ `Plugin ${name} target.config-key "${rawConfigKey}" is invalid`
1397
+ );
1185
1398
  }
1186
1399
  const qualifiedKey = `${name}.${rawConfigKey}`;
1187
1400
  if (!configKeys.includes(qualifiedKey)) {
1188
- throw new Error(`Plugin ${name} target.config-key "${rawConfigKey}" must be listed in config-keys`);
1401
+ throw new Error(
1402
+ `Plugin ${name} target.config-key "${rawConfigKey}" must be listed in config-keys`
1403
+ );
1189
1404
  }
1190
1405
  manifest.target = { type: "repo", configKey: qualifiedKey };
1191
1406
  }
@@ -1204,13 +1419,15 @@ function registerPluginManifest(raw, pluginDir) {
1204
1419
  }
1205
1420
  for (const cap of manifest.capabilities) {
1206
1421
  if (capabilityToPlugin.has(cap)) {
1207
- throw new Error(`Duplicate capability "${cap}" in plugin "${manifest.name}"`);
1422
+ throw new Error(
1423
+ `Duplicate capability "${cap}" in plugin "${manifest.name}"`
1424
+ );
1208
1425
  }
1209
1426
  }
1210
1427
  const definition = {
1211
1428
  manifest,
1212
1429
  dir: pluginDir,
1213
- skillsDir: path3.join(pluginDir, "skills")
1430
+ skillsDir: path2.join(pluginDir, "skills")
1214
1431
  };
1215
1432
  pluginDefinitions.push(definition);
1216
1433
  pluginsByName.set(manifest.name, definition);
@@ -1231,24 +1448,29 @@ function loadPlugins() {
1231
1448
  let entries;
1232
1449
  let rootStat;
1233
1450
  try {
1234
- rootStat = statSync2(pluginsRoot);
1451
+ rootStat = statSync(pluginsRoot);
1235
1452
  } catch (error) {
1236
- logWarn("plugin_root_read_failed", {}, {
1237
- "file.directory": pluginsRoot,
1238
- "error.message": error instanceof Error ? error.message : String(error)
1239
- }, "Failed to read plugin root");
1453
+ logWarn(
1454
+ "plugin_root_read_failed",
1455
+ {},
1456
+ {
1457
+ "file.directory": pluginsRoot,
1458
+ "error.message": error instanceof Error ? error.message : String(error)
1459
+ },
1460
+ "Failed to read plugin root"
1461
+ );
1240
1462
  continue;
1241
1463
  }
1242
1464
  if (rootStat.isDirectory()) {
1243
- const manifestPath = path3.join(pluginsRoot, "plugin.yaml");
1465
+ const manifestPath = path2.join(pluginsRoot, "plugin.yaml");
1244
1466
  let hasRootManifest = false;
1245
1467
  try {
1246
- hasRootManifest = statSync2(manifestPath).isFile();
1468
+ hasRootManifest = statSync(manifestPath).isFile();
1247
1469
  } catch {
1248
1470
  hasRootManifest = false;
1249
1471
  }
1250
1472
  if (hasRootManifest) {
1251
- const rawRootManifest = readFileSync2(manifestPath, "utf8");
1473
+ const rawRootManifest = readFileSync(manifestPath, "utf8");
1252
1474
  registerPluginManifest(rawRootManifest, pluginsRoot);
1253
1475
  continue;
1254
1476
  }
@@ -1256,24 +1478,29 @@ function loadPlugins() {
1256
1478
  try {
1257
1479
  entries = readdirSync(pluginsRoot);
1258
1480
  } catch (error) {
1259
- logWarn("plugin_root_read_failed", {}, {
1260
- "file.directory": pluginsRoot,
1261
- "error.message": error instanceof Error ? error.message : String(error)
1262
- }, "Failed to read plugin root");
1481
+ logWarn(
1482
+ "plugin_root_read_failed",
1483
+ {},
1484
+ {
1485
+ "file.directory": pluginsRoot,
1486
+ "error.message": error instanceof Error ? error.message : String(error)
1487
+ },
1488
+ "Failed to read plugin root"
1489
+ );
1263
1490
  continue;
1264
1491
  }
1265
1492
  for (const entry of entries.sort()) {
1266
- const pluginDir = path3.join(pluginsRoot, entry);
1493
+ const pluginDir = path2.join(pluginsRoot, entry);
1267
1494
  try {
1268
- const stat = statSync2(pluginDir);
1495
+ const stat = statSync(pluginDir);
1269
1496
  if (!stat.isDirectory()) continue;
1270
1497
  } catch {
1271
1498
  continue;
1272
1499
  }
1273
- const manifestPath = path3.join(pluginDir, "plugin.yaml");
1500
+ const manifestPath = path2.join(pluginDir, "plugin.yaml");
1274
1501
  let raw;
1275
1502
  try {
1276
- raw = readFileSync2(manifestPath, "utf8");
1503
+ raw = readFileSync(manifestPath, "utf8");
1277
1504
  } catch {
1278
1505
  continue;
1279
1506
  }
@@ -1295,8 +1522,12 @@ function loadPlugins() {
1295
1522
  "Loaded plugins"
1296
1523
  );
1297
1524
  }
1525
+ function ensurePluginsLoaded() {
1526
+ loadPlugins();
1527
+ }
1298
1528
  loadPlugins();
1299
1529
  function getPluginCapabilityProviders() {
1530
+ ensurePluginsLoaded();
1300
1531
  return pluginDefinitions.map((plugin) => ({
1301
1532
  provider: plugin.manifest.name,
1302
1533
  capabilities: [...plugin.manifest.capabilities],
@@ -1305,9 +1536,11 @@ function getPluginCapabilityProviders() {
1305
1536
  }));
1306
1537
  }
1307
1538
  function getPluginProviders() {
1539
+ ensurePluginsLoaded();
1308
1540
  return [...pluginDefinitions];
1309
1541
  }
1310
1542
  function getPluginRuntimeDependencies() {
1543
+ ensurePluginsLoaded();
1311
1544
  const seen = /* @__PURE__ */ new Set();
1312
1545
  const deps = [];
1313
1546
  for (const plugin of pluginDefinitions) {
@@ -1336,6 +1569,7 @@ function getPluginRuntimeDependencies() {
1336
1569
  });
1337
1570
  }
1338
1571
  function getPluginRuntimePostinstall() {
1572
+ ensurePluginsLoaded();
1339
1573
  const commands = [];
1340
1574
  for (const plugin of pluginDefinitions) {
1341
1575
  for (const command of plugin.manifest.runtimePostinstall ?? []) {
@@ -1349,6 +1583,7 @@ function getPluginRuntimePostinstall() {
1349
1583
  return commands;
1350
1584
  }
1351
1585
  function getPluginOAuthConfig(provider) {
1586
+ ensurePluginsLoaded();
1352
1587
  const plugin = pluginsByName.get(provider);
1353
1588
  if (!plugin?.manifest.oauth) return void 0;
1354
1589
  const oauth = plugin.manifest.oauth;
@@ -1357,17 +1592,28 @@ function getPluginOAuthConfig(provider) {
1357
1592
  clientSecretEnv: oauth.clientSecretEnv,
1358
1593
  authorizeEndpoint: oauth.authorizeEndpoint,
1359
1594
  tokenEndpoint: oauth.tokenEndpoint,
1360
- scope: oauth.scope,
1595
+ ...oauth.scope ? { scope: oauth.scope } : {},
1596
+ ...oauth.authorizeParams ? { authorizeParams: { ...oauth.authorizeParams } } : {},
1597
+ ...oauth.tokenAuthMethod ? { tokenAuthMethod: oauth.tokenAuthMethod } : {},
1598
+ ...oauth.tokenExtraHeaders ? { tokenExtraHeaders: { ...oauth.tokenExtraHeaders } } : {},
1361
1599
  callbackPath: `/api/oauth/callback/${plugin.manifest.name}`
1362
1600
  };
1363
1601
  }
1364
1602
  function getPluginSkillRoots() {
1365
- return [.../* @__PURE__ */ new Set([...pluginDefinitions.map((plugin) => plugin.skillsDir), ...packageSkillRoots])];
1603
+ ensurePluginsLoaded();
1604
+ return [
1605
+ .../* @__PURE__ */ new Set([
1606
+ ...pluginDefinitions.map((plugin) => plugin.skillsDir),
1607
+ ...packageSkillRoots
1608
+ ])
1609
+ ];
1366
1610
  }
1367
1611
  function isPluginProvider(provider) {
1612
+ ensurePluginsLoaded();
1368
1613
  return pluginsByName.has(provider);
1369
1614
  }
1370
1615
  function createPluginBroker(provider, deps) {
1616
+ ensurePluginsLoaded();
1371
1617
  const plugin = pluginsByName.get(provider);
1372
1618
  if (!plugin) {
1373
1619
  throw new Error(`Unknown plugin provider: "${provider}"`);
@@ -1467,6 +1713,9 @@ function buildDependencyProfile(runtime) {
1467
1713
  postinstall
1468
1714
  };
1469
1715
  }
1716
+ function getRuntimeDependencyProfileHash(runtime) {
1717
+ return buildDependencyProfile(runtime)?.profileHash;
1718
+ }
1470
1719
  function shouldRebuildCachedSnapshot(profile, cached) {
1471
1720
  if (!profile.hasFloatingVersions) {
1472
1721
  return false;
@@ -1497,7 +1746,11 @@ async function getCachedSnapshot(profileHash) {
1497
1746
  async function setCachedSnapshot(entry) {
1498
1747
  const state = getStateAdapter();
1499
1748
  await state.connect();
1500
- await state.set(profileCacheKey(entry.profileHash), JSON.stringify(entry), SNAPSHOT_CACHE_TTL_MS);
1749
+ await state.set(
1750
+ profileCacheKey(entry.profileHash),
1751
+ JSON.stringify(entry),
1752
+ SNAPSHOT_CACHE_TTL_MS
1753
+ );
1501
1754
  }
1502
1755
  async function withSnapshotSpan(name, op, attributes, callback) {
1503
1756
  return await withSpan(name, op, {}, callback, attributes);
@@ -1532,7 +1785,11 @@ async function installGhCliViaDnf(sandbox) {
1532
1785
  }
1533
1786
  const dnf5Repo = await tryRun(sandbox, {
1534
1787
  cmd: "dnf",
1535
- args: ["config-manager", "addrepo", "--from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo"],
1788
+ args: [
1789
+ "config-manager",
1790
+ "addrepo",
1791
+ "--from-repofile=https://cli.github.com/packages/rpm/gh-cli.repo"
1792
+ ],
1536
1793
  sudo: true
1537
1794
  });
1538
1795
  if (!dnf5Repo.ok) {
@@ -1549,7 +1806,11 @@ async function installGhCliViaDnf(sandbox) {
1549
1806
  sandbox,
1550
1807
  {
1551
1808
  cmd: "dnf",
1552
- args: ["config-manager", "--add-repo", "https://cli.github.com/packages/rpm/gh-cli.repo"],
1809
+ args: [
1810
+ "config-manager",
1811
+ "--add-repo",
1812
+ "https://cli.github.com/packages/rpm/gh-cli.repo"
1813
+ ],
1553
1814
  sudo: true
1554
1815
  },
1555
1816
  "dnf config-manager --add-repo gh-cli.repo"
@@ -1580,8 +1841,12 @@ function runtimeDependencyFilePath(url, sha256) {
1580
1841
  return `/tmp/junior-runtime-${sha256.slice(0, 12)}-${sanitizedBasename}`;
1581
1842
  }
1582
1843
  async function installRuntimeDependencies(sandbox, deps) {
1583
- const systemDeps = deps.filter((dep) => dep.type === "system");
1584
- const npmPackages = deps.filter((dep) => dep.type === "npm").map((dep) => `${dep.package}@${dep.version}`);
1844
+ const systemDeps = deps.filter(
1845
+ (dep) => dep.type === "system"
1846
+ );
1847
+ const npmPackages = deps.filter(
1848
+ (dep) => dep.type === "npm"
1849
+ ).map((dep) => `${dep.package}@${dep.version}`);
1585
1850
  if (systemDeps.length > 0) {
1586
1851
  await withSnapshotSpan(
1587
1852
  "sandbox.snapshot.install_system",
@@ -1608,7 +1873,9 @@ async function installRuntimeDependencies(sandbox, deps) {
1608
1873
  const checksumStdout = (await checksumResult.stdout()).trim();
1609
1874
  const checksumStderr = (await checksumResult.stderr()).trim();
1610
1875
  if (checksumResult.exitCode !== 0) {
1611
- throw new Error(`sha256sum failed: ${checksumStderr || checksumStdout || "command failed"}`);
1876
+ throw new Error(
1877
+ `sha256sum failed: ${checksumStderr || checksumStdout || "command failed"}`
1878
+ );
1612
1879
  }
1613
1880
  const actualChecksum = checksumStdout.split(/\s+/)[0]?.toLowerCase();
1614
1881
  if (!actualChecksum) {
@@ -1685,7 +1952,10 @@ async function runRuntimePostinstall(sandbox, commands) {
1685
1952
  },
1686
1953
  async () => {
1687
1954
  for (const command of commands) {
1688
- const invocation = [JSON.stringify(command.cmd), ...(command.args ?? []).map((arg) => JSON.stringify(arg))].join(" ");
1955
+ const invocation = [
1956
+ JSON.stringify(command.cmd),
1957
+ ...(command.args ?? []).map((arg) => JSON.stringify(arg))
1958
+ ].join(" ");
1689
1959
  const pathPrefix = `${SANDBOX_WORKSPACE_ROOT}/.junior/bin:$PATH`;
1690
1960
  await runOrThrow(
1691
1961
  sandbox,
@@ -1840,7 +2110,9 @@ async function resolveRuntimeDependencySnapshot(params) {
1840
2110
  };
1841
2111
  }
1842
2112
  const cached = await getCachedSnapshot(profile.profileHash);
1843
- const cachedNeedsRebuild = Boolean(cached?.snapshotId && shouldRebuildCachedSnapshot(profile, cached));
2113
+ const cachedNeedsRebuild = Boolean(
2114
+ cached?.snapshotId && shouldRebuildCachedSnapshot(profile, cached)
2115
+ );
1844
2116
  if (!params.forceRebuild && cached?.snapshotId && !cachedNeedsRebuild) {
1845
2117
  await params.onProgress?.("cache_hit");
1846
2118
  return {
@@ -1866,34 +2138,50 @@ async function resolveRuntimeDependencySnapshot(params) {
1866
2138
  }
1867
2139
  return !shouldRebuildCachedSnapshot(profile, candidate);
1868
2140
  };
1869
- const lockResult = await withBuildLock(profile.profileHash, async () => {
1870
- const latest = await getCachedSnapshot(profile.profileHash);
1871
- if (latest?.snapshotId && canUseCachedSnapshot(latest)) {
1872
- await params.onProgress?.("cache_hit");
1873
- return { snapshotId: latest.snapshotId, source: "callback_cache" };
1874
- }
1875
- await params.onProgress?.("building_snapshot");
1876
- const nextSnapshotId = await createDependencySnapshot(profile, params.runtime, params.timeoutMs);
1877
- await setCachedSnapshot({
1878
- profileHash: profile.profileHash,
1879
- snapshotId: nextSnapshotId,
1880
- runtime: params.runtime,
1881
- createdAtMs: Date.now(),
1882
- dependencyCount: profile.dependencyCount
1883
- });
1884
- await params.onProgress?.("build_complete");
1885
- return { snapshotId: nextSnapshotId, source: "built" };
1886
- }, canUseCachedSnapshot, {
1887
- onWaitingForLock: async () => {
1888
- await params.onProgress?.("waiting_for_lock");
2141
+ const lockResult = await withBuildLock(
2142
+ profile.profileHash,
2143
+ async () => {
2144
+ const latest = await getCachedSnapshot(profile.profileHash);
2145
+ if (latest?.snapshotId && canUseCachedSnapshot(latest)) {
2146
+ await params.onProgress?.("cache_hit");
2147
+ return {
2148
+ snapshotId: latest.snapshotId,
2149
+ source: "callback_cache"
2150
+ };
2151
+ }
2152
+ await params.onProgress?.("building_snapshot");
2153
+ const nextSnapshotId = await createDependencySnapshot(
2154
+ profile,
2155
+ params.runtime,
2156
+ params.timeoutMs
2157
+ );
2158
+ await setCachedSnapshot({
2159
+ profileHash: profile.profileHash,
2160
+ snapshotId: nextSnapshotId,
2161
+ runtime: params.runtime,
2162
+ createdAtMs: Date.now(),
2163
+ dependencyCount: profile.dependencyCount
2164
+ });
2165
+ await params.onProgress?.("build_complete");
2166
+ return { snapshotId: nextSnapshotId, source: "built" };
2167
+ },
2168
+ canUseCachedSnapshot,
2169
+ {
2170
+ onWaitingForLock: async () => {
2171
+ await params.onProgress?.("waiting_for_lock");
2172
+ }
1889
2173
  }
1890
- });
2174
+ );
1891
2175
  return {
1892
2176
  snapshotId: lockResult.snapshotId,
1893
2177
  profileHash: profile.profileHash,
1894
2178
  dependencyCount: profile.dependencyCount,
1895
2179
  cacheHit: lockResult.source !== "built",
1896
- resolveOutcome: toResolveOutcome(Boolean(params.forceRebuild), lockResult.source, lockResult.waitedForLock),
2180
+ resolveOutcome: toResolveOutcome(
2181
+ Boolean(params.forceRebuild),
2182
+ lockResult.source,
2183
+ lockResult.waitedForLock
2184
+ ),
1897
2185
  ...rebuildReason ? { rebuildReason } : {}
1898
2186
  };
1899
2187
  }
@@ -1910,6 +2198,8 @@ export {
1910
2198
  soulPathCandidates,
1911
2199
  resolveAuthTokenPlaceholder,
1912
2200
  CredentialUnavailableError,
2201
+ buildOAuthTokenRequest,
2202
+ parseOAuthTokenResponse,
1913
2203
  getPluginCapabilityProviders,
1914
2204
  getPluginProviders,
1915
2205
  getPluginOAuthConfig,
@@ -1935,6 +2225,7 @@ export {
1935
2225
  SANDBOX_WORKSPACE_ROOT,
1936
2226
  SANDBOX_SKILLS_ROOT,
1937
2227
  sandboxSkillDir,
2228
+ getRuntimeDependencyProfileHash,
1938
2229
  resolveRuntimeDependencySnapshot,
1939
2230
  isSnapshotMissingError
1940
2231
  };