@shipfox/api-integration-github 12.3.0 → 12.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (53) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +19 -0
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +15 -9
  5. package/dist/api/client.js.map +1 -1
  6. package/dist/api/github-octokit.d.ts +7 -0
  7. package/dist/api/github-octokit.d.ts.map +1 -0
  8. package/dist/api/github-octokit.js +32 -0
  9. package/dist/api/github-octokit.js.map +1 -0
  10. package/dist/api/installation-token-envelope.d.ts +5 -1
  11. package/dist/api/installation-token-envelope.d.ts.map +1 -1
  12. package/dist/api/installation-token-envelope.js +16 -5
  13. package/dist/api/installation-token-envelope.js.map +1 -1
  14. package/dist/api/installation-token-provider.d.ts.map +1 -1
  15. package/dist/api/installation-token-provider.js +4 -2
  16. package/dist/api/installation-token-provider.js.map +1 -1
  17. package/dist/api/shared-installation-token-cache.d.ts.map +1 -1
  18. package/dist/api/shared-installation-token-cache.js +10 -4
  19. package/dist/api/shared-installation-token-cache.js.map +1 -1
  20. package/dist/config.d.ts +1 -0
  21. package/dist/config.d.ts.map +1 -1
  22. package/dist/config.js +8 -0
  23. package/dist/config.js.map +1 -1
  24. package/dist/core/agent-tools.d.ts +11 -4
  25. package/dist/core/agent-tools.d.ts.map +1 -1
  26. package/dist/core/agent-tools.js +235 -24
  27. package/dist/core/agent-tools.js.map +1 -1
  28. package/dist/core/github-agent-tool-catalog.js +1 -1
  29. package/dist/core/github-agent-tool-catalog.js.map +1 -1
  30. package/dist/metrics/instance.d.ts +1 -0
  31. package/dist/metrics/instance.d.ts.map +1 -1
  32. package/dist/metrics/instance.js +19 -0
  33. package/dist/metrics/instance.js.map +1 -1
  34. package/dist/tsconfig.test.tsbuildinfo +1 -1
  35. package/package.json +2 -2
  36. package/src/api/client.test.ts +66 -10
  37. package/src/api/client.ts +44 -7
  38. package/src/api/github-octokit.test.ts +115 -0
  39. package/src/api/github-octokit.ts +49 -0
  40. package/src/api/installation-token-envelope.ts +24 -2
  41. package/src/api/installation-token-provider.test.ts +44 -5
  42. package/src/api/installation-token-provider.ts +5 -2
  43. package/src/api/shared-installation-token-cache.test.ts +28 -0
  44. package/src/api/shared-installation-token-cache.ts +7 -0
  45. package/src/config.ts +5 -0
  46. package/src/core/agent-tools.test.ts +1049 -8
  47. package/src/core/agent-tools.ts +388 -25
  48. package/src/core/github-agent-tool-catalog.ts +1 -1
  49. package/src/metrics/instance.ts +25 -0
  50. package/test/env.ts +1 -0
  51. package/test/fixtures/github-installation-token.ts +8 -0
  52. package/test/index.ts +4 -0
  53. package/tsconfig.build.tsbuildinfo +1 -1
@@ -55,7 +55,7 @@ export class SharedInstallationTokenCache {
55
55
  return tokenFromEnvelope(envelope);
56
56
  }
57
57
  recordInstallationTokenLookup('backoff');
58
- throw providerErrorFromBackoff(envelope?.backoffReason ?? 'provider-unavailable', (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime());
58
+ throw providerErrorFromBackoff(envelope?.backoffReason ?? 'provider-unavailable', (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(), envelope?.backoffError);
59
59
  }
60
60
  let token;
61
61
  try {
@@ -73,7 +73,13 @@ export class SharedInstallationTokenCache {
73
73
  expiresAt: envelope?.expiresAt,
74
74
  permissions: envelope?.permissions,
75
75
  backoffUntil: until,
76
- backoffReason: classified.reason
76
+ backoffReason: classified.reason,
77
+ backoffError: {
78
+ message: providerError.message,
79
+ ...providerError.status === undefined ? {} : {
80
+ status: providerError.status
81
+ }
82
+ }
77
83
  }).catch((writeError)=>{
78
84
  logger().warn({
79
85
  installationId: params.installationId,
@@ -142,7 +148,7 @@ export class SharedInstallationTokenCache {
142
148
  }
143
149
  if (activeBackoff(params.envelope, initialNow)) {
144
150
  recordInstallationTokenLookup('backoff');
145
- throw providerErrorFromBackoff(params.envelope.backoffReason, params.envelope.backoffUntil.getTime() - initialNow.getTime());
151
+ throw providerErrorFromBackoff(params.envelope.backoffReason, params.envelope.backoffUntil.getTime() - initialNow.getTime(), params.envelope.backoffError);
146
152
  }
147
153
  for (const delayMs of this.pollDelaysMs){
148
154
  await this.sleep(delayMs);
@@ -154,7 +160,7 @@ export class SharedInstallationTokenCache {
154
160
  }
155
161
  if (backoffActive(envelope, now)) {
156
162
  recordInstallationTokenLookup('backoff');
157
- throw providerErrorFromBackoff(envelope?.backoffReason ?? 'provider-unavailable', (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime());
163
+ throw providerErrorFromBackoff(envelope?.backoffReason ?? 'provider-unavailable', (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(), envelope?.backoffError);
158
164
  }
159
165
  }
160
166
  throw new GithubIntegrationProviderError('provider-unavailable', 'GitHub installation token mint is still in progress', 1);
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/api/shared-installation-token-cache.ts"],"sourcesContent":["import {setTimeout as sleepTimeout} from 'node:timers/promises';\nimport {reportError} from '@shipfox/node-error-monitoring';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {GithubIntegrationProviderError} from '#core/errors.js';\nimport {\n recordInstallationTokenBackoff,\n recordInstallationTokenLookup,\n recordInstallationTokenMint,\n} from '#metrics/index.js';\nimport type {GithubInstallationAccessToken} from './client.js';\nimport {\n backoffActive,\n backoffMs,\n classifyMintError,\n type InstallationTokenEnvelope,\n mintErrorClassForReason,\n parseInstallationTokenEnvelope,\n providerErrorFromBackoff,\n stillValid,\n toProviderError,\n usable,\n} from './installation-token-envelope.js';\n\nexport interface InstallationTokenCache {\n getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken>;\n}\n\nexport type InstallationTokenLockResult<T> = {acquired: true; value: T} | {acquired: false};\n\nexport interface InstallationTokenSecretStore {\n read(workspaceId: string, installationId: number): Promise<string | null>;\n write(\n workspaceId: string,\n installationId: number,\n envelope: InstallationTokenEnvelope,\n ): Promise<void>;\n}\n\nexport interface SharedInstallationTokenCacheOptions {\n secretStore: InstallationTokenSecretStore;\n withLock: <T>(\n installationId: number,\n fn: () => Promise<T>,\n ) => Promise<InstallationTokenLockResult<T>>;\n resolveWorkspaceId: (installationId: number) => Promise<string>;\n now?: (() => Date) | undefined;\n sleep?: ((ms: number) => Promise<void>) | undefined;\n pollDelaysMs?: number[] | undefined;\n workspaceCacheTtlMs?: number | undefined;\n mintTimeoutMs?: number | undefined;\n}\n\nconst DEFAULT_POLL_DELAYS_MS = [100, 200, 400, 500, 800];\nconst DEFAULT_WORKSPACE_CACHE_TTL_MS = 10 * 60 * 1000;\nconst DEFAULT_MINT_TIMEOUT_MS = 30 * 1000;\n\nexport class SharedInstallationTokenCache implements InstallationTokenCache {\n private readonly workspaceIds = new Map<number, {workspaceId: string; expiresAtMs: number}>();\n private readonly now: () => Date;\n private readonly sleep: (ms: number) => Promise<void>;\n private readonly pollDelaysMs: number[];\n private readonly workspaceCacheTtlMs: number;\n private readonly mintTimeoutMs: number;\n\n constructor(private readonly options: SharedInstallationTokenCacheOptions) {\n this.now = options.now ?? (() => new Date());\n this.sleep = options.sleep ?? ((ms) => sleepTimeout(ms).then(() => undefined));\n this.pollDelaysMs = options.pollDelaysMs ?? DEFAULT_POLL_DELAYS_MS;\n this.workspaceCacheTtlMs = options.workspaceCacheTtlMs ?? DEFAULT_WORKSPACE_CACHE_TTL_MS;\n this.mintTimeoutMs = options.mintTimeoutMs ?? DEFAULT_MINT_TIMEOUT_MS;\n }\n\n async getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n const workspaceId = await this.resolveWorkspaceId(installationId);\n const envelope = await this.readEnvelope(workspaceId, installationId);\n if (usable(envelope, this.now())) {\n recordInstallationTokenLookup('db-hit');\n return tokenFromEnvelope(envelope);\n }\n\n const result = await this.options.withLock(installationId, () =>\n this.mintUnderLock({workspaceId, installationId, mint}),\n );\n if (result.acquired) return result.value;\n\n return await this.serveStaleOrPoll({workspaceId, installationId, envelope});\n }\n\n private async mintUnderLock(params: {\n workspaceId: string;\n installationId: number;\n mint: () => Promise<GithubInstallationAccessToken>;\n }): Promise<GithubInstallationAccessToken> {\n const envelope = await this.readEnvelope(params.workspaceId, params.installationId);\n const now = this.now();\n if (usable(envelope, now)) {\n recordInstallationTokenLookup('db-hit');\n return tokenFromEnvelope(envelope);\n }\n\n if (activeBackoff(envelope, now)) {\n if (canServeStale(envelope, now)) {\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(envelope);\n }\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n envelope?.backoffReason ?? 'provider-unavailable',\n (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(),\n );\n }\n\n let token: GithubInstallationAccessToken;\n try {\n token = await this.recordMint(params.mint);\n } catch (error) {\n const providerError = toProviderError(error);\n const classified = classifyMintError(providerError);\n const until = new Date(this.now().getTime() + backoffMs(classified));\n recordInstallationTokenBackoff({reason: classified.reason, class: classified.class});\n\n await this.writeEnvelope(params.workspaceId, params.installationId, {\n token: envelope?.token,\n expiresAt: envelope?.expiresAt,\n permissions: envelope?.permissions,\n backoffUntil: until,\n backoffReason: classified.reason,\n }).catch((writeError) => {\n logger().warn(\n {installationId: params.installationId, reason: classified.reason, error: writeError},\n 'github installation token backoff write failed',\n );\n reportError(writeError, {\n boundary: 'integration.cache',\n operation: 'write-backoff-envelope',\n extra: {installationId: params.installationId},\n });\n });\n\n if (\n classified.class === 'transient' &&\n envelope?.token &&\n stillValid(envelope.expiresAt, this.now())\n ) {\n logger().warn(\n {\n installationId: params.installationId,\n expiresAt: envelope.expiresAt?.toISOString(),\n reason: classified.reason,\n backoffUntil: until.toISOString(),\n },\n 'github installation token mint failed; serving stale token',\n );\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(envelope);\n }\n\n logger().warn(\n {\n installationId: params.installationId,\n reason: classified.reason,\n backoffUntil: until.toISOString(),\n error: providerError,\n },\n 'github installation token mint failed; backoff recorded',\n );\n recordInstallationTokenLookup('backoff');\n throw providerError;\n }\n\n try {\n await this.writeEnvelope(params.workspaceId, params.installationId, {\n token: token.token,\n expiresAt: token.expiresAt,\n permissions: token.permissions,\n });\n } catch (error) {\n logger().warn(\n {installationId: params.installationId, expiresAt: token.expiresAt.toISOString(), error},\n 'github installation token cache write failed after mint',\n );\n reportError(error, {\n boundary: 'integration.cache',\n operation: 'write-minted-token',\n extra: {installationId: params.installationId},\n });\n }\n\n logger().info(\n {installationId: params.installationId, expiresAt: token.expiresAt.toISOString()},\n 'github installation token minted',\n );\n recordInstallationTokenLookup('minted');\n return token;\n }\n\n private async serveStaleOrPoll(params: {\n workspaceId: string;\n installationId: number;\n envelope: InstallationTokenEnvelope | undefined;\n }): Promise<GithubInstallationAccessToken> {\n const initialNow = this.now();\n if (canServeStale(params.envelope, initialNow)) {\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(params.envelope);\n }\n if (activeBackoff(params.envelope, initialNow)) {\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n params.envelope.backoffReason,\n params.envelope.backoffUntil.getTime() - initialNow.getTime(),\n );\n }\n\n for (const delayMs of this.pollDelaysMs) {\n await this.sleep(delayMs);\n const envelope = await this.readEnvelope(params.workspaceId, params.installationId);\n const now = this.now();\n if (usable(envelope, now)) {\n recordInstallationTokenLookup('contended-poll');\n return tokenFromEnvelope(envelope);\n }\n if (backoffActive(envelope, now)) {\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n envelope?.backoffReason ?? 'provider-unavailable',\n (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(),\n );\n }\n }\n\n throw new GithubIntegrationProviderError(\n 'provider-unavailable',\n 'GitHub installation token mint is still in progress',\n 1,\n );\n }\n\n private async recordMint(\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n const startedAt = Date.now();\n try {\n const token = await withTimeout(mint(), this.mintTimeoutMs);\n recordInstallationTokenMint({outcome: 'success', durationMs: Date.now() - startedAt});\n return token;\n } catch (error) {\n recordInstallationTokenMint({outcome: 'failure', durationMs: Date.now() - startedAt});\n throw error;\n }\n }\n\n private async readEnvelope(\n workspaceId: string,\n installationId: number,\n ): Promise<InstallationTokenEnvelope | undefined> {\n const raw = await this.options.secretStore.read(workspaceId, installationId);\n if (raw === null) return undefined;\n\n const envelope = parseInstallationTokenEnvelope(raw);\n if (envelope === undefined) {\n logger().warn({installationId}, 'github installation token cache envelope failed to decode');\n }\n return envelope;\n }\n\n private async writeEnvelope(\n workspaceId: string,\n installationId: number,\n envelope: InstallationTokenEnvelope,\n ): Promise<void> {\n await this.options.secretStore.write(workspaceId, installationId, envelope);\n }\n\n private async resolveWorkspaceId(installationId: number): Promise<string> {\n const nowMs = this.now().getTime();\n const cached = this.workspaceIds.get(installationId);\n if (cached && cached.expiresAtMs > nowMs) return cached.workspaceId;\n\n const workspaceId = await this.options.resolveWorkspaceId(installationId);\n this.workspaceIds.set(installationId, {\n workspaceId,\n expiresAtMs: nowMs + this.workspaceCacheTtlMs,\n });\n return workspaceId;\n }\n}\n\ntype ActiveBackoffEnvelope = InstallationTokenEnvelope & {\n backoffUntil: Date;\n backoffReason: NonNullable<InstallationTokenEnvelope['backoffReason']>;\n};\n\ntype TokenEnvelope = InstallationTokenEnvelope & {token: string; expiresAt: Date};\n\nfunction activeBackoff(\n envelope: InstallationTokenEnvelope | undefined,\n now: Date,\n): envelope is ActiveBackoffEnvelope {\n return (\n backoffActive(envelope, now) &&\n envelope?.backoffUntil !== undefined &&\n envelope.backoffReason !== undefined\n );\n}\n\nfunction canServeStale(\n envelope: InstallationTokenEnvelope | undefined,\n now: Date,\n): envelope is TokenEnvelope {\n const terminalBackoff =\n activeBackoff(envelope, now) && mintErrorClassForReason(envelope.backoffReason) === 'terminal';\n return (\n envelope?.token !== undefined &&\n envelope.expiresAt !== undefined &&\n stillValid(envelope.expiresAt, now) &&\n !terminalBackoff\n );\n}\n\nfunction tokenFromEnvelope(envelope: InstallationTokenEnvelope): GithubInstallationAccessToken {\n if (!envelope.token || !envelope.expiresAt) {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub installation token cache envelope is missing a token or expiry',\n );\n }\n return {\n token: envelope.token,\n expiresAt: envelope.expiresAt,\n ...(envelope.permissions === undefined ? {} : {permissions: envelope.permissions}),\n };\n}\n\nfunction withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<T>((_, reject) => {\n timer = setTimeout(() => {\n reject(\n new GithubIntegrationProviderError(\n 'timeout',\n 'Timed out minting GitHub installation access token',\n ),\n );\n }, timeoutMs);\n });\n\n return Promise.race([promise, timeout]).finally(() => {\n if (timer) clearTimeout(timer);\n });\n}\n"],"names":["setTimeout","sleepTimeout","reportError","logger","GithubIntegrationProviderError","recordInstallationTokenBackoff","recordInstallationTokenLookup","recordInstallationTokenMint","backoffActive","backoffMs","classifyMintError","mintErrorClassForReason","parseInstallationTokenEnvelope","providerErrorFromBackoff","stillValid","toProviderError","usable","DEFAULT_POLL_DELAYS_MS","DEFAULT_WORKSPACE_CACHE_TTL_MS","DEFAULT_MINT_TIMEOUT_MS","SharedInstallationTokenCache","options","workspaceIds","Map","now","Date","sleep","ms","then","undefined","pollDelaysMs","workspaceCacheTtlMs","mintTimeoutMs","getOrMint","installationId","mint","workspaceId","resolveWorkspaceId","envelope","readEnvelope","tokenFromEnvelope","result","withLock","mintUnderLock","acquired","value","serveStaleOrPoll","params","activeBackoff","canServeStale","backoffReason","backoffUntil","getTime","token","recordMint","error","providerError","classified","until","reason","class","writeEnvelope","expiresAt","permissions","catch","writeError","warn","boundary","operation","extra","toISOString","info","initialNow","delayMs","startedAt","withTimeout","outcome","durationMs","raw","secretStore","read","write","nowMs","cached","get","expiresAtMs","set","terminalBackoff","promise","timeoutMs","timer","timeout","Promise","_","reject","race","finally","clearTimeout"],"mappings":"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"}
1
+ {"version":3,"sources":["../../src/api/shared-installation-token-cache.ts"],"sourcesContent":["import {setTimeout as sleepTimeout} from 'node:timers/promises';\nimport {reportError} from '@shipfox/node-error-monitoring';\nimport {logger} from '@shipfox/node-opentelemetry';\nimport {GithubIntegrationProviderError} from '#core/errors.js';\nimport {\n recordInstallationTokenBackoff,\n recordInstallationTokenLookup,\n recordInstallationTokenMint,\n} from '#metrics/index.js';\nimport type {GithubInstallationAccessToken} from './client.js';\nimport {\n backoffActive,\n backoffMs,\n classifyMintError,\n type InstallationTokenEnvelope,\n mintErrorClassForReason,\n parseInstallationTokenEnvelope,\n providerErrorFromBackoff,\n stillValid,\n toProviderError,\n usable,\n} from './installation-token-envelope.js';\n\nexport interface InstallationTokenCache {\n getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken>;\n}\n\nexport type InstallationTokenLockResult<T> = {acquired: true; value: T} | {acquired: false};\n\nexport interface InstallationTokenSecretStore {\n read(workspaceId: string, installationId: number): Promise<string | null>;\n write(\n workspaceId: string,\n installationId: number,\n envelope: InstallationTokenEnvelope,\n ): Promise<void>;\n}\n\nexport interface SharedInstallationTokenCacheOptions {\n secretStore: InstallationTokenSecretStore;\n withLock: <T>(\n installationId: number,\n fn: () => Promise<T>,\n ) => Promise<InstallationTokenLockResult<T>>;\n resolveWorkspaceId: (installationId: number) => Promise<string>;\n now?: (() => Date) | undefined;\n sleep?: ((ms: number) => Promise<void>) | undefined;\n pollDelaysMs?: number[] | undefined;\n workspaceCacheTtlMs?: number | undefined;\n mintTimeoutMs?: number | undefined;\n}\n\nconst DEFAULT_POLL_DELAYS_MS = [100, 200, 400, 500, 800];\nconst DEFAULT_WORKSPACE_CACHE_TTL_MS = 10 * 60 * 1000;\nconst DEFAULT_MINT_TIMEOUT_MS = 30 * 1000;\n\nexport class SharedInstallationTokenCache implements InstallationTokenCache {\n private readonly workspaceIds = new Map<number, {workspaceId: string; expiresAtMs: number}>();\n private readonly now: () => Date;\n private readonly sleep: (ms: number) => Promise<void>;\n private readonly pollDelaysMs: number[];\n private readonly workspaceCacheTtlMs: number;\n private readonly mintTimeoutMs: number;\n\n constructor(private readonly options: SharedInstallationTokenCacheOptions) {\n this.now = options.now ?? (() => new Date());\n this.sleep = options.sleep ?? ((ms) => sleepTimeout(ms).then(() => undefined));\n this.pollDelaysMs = options.pollDelaysMs ?? DEFAULT_POLL_DELAYS_MS;\n this.workspaceCacheTtlMs = options.workspaceCacheTtlMs ?? DEFAULT_WORKSPACE_CACHE_TTL_MS;\n this.mintTimeoutMs = options.mintTimeoutMs ?? DEFAULT_MINT_TIMEOUT_MS;\n }\n\n async getOrMint(\n installationId: number,\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n const workspaceId = await this.resolveWorkspaceId(installationId);\n const envelope = await this.readEnvelope(workspaceId, installationId);\n if (usable(envelope, this.now())) {\n recordInstallationTokenLookup('db-hit');\n return tokenFromEnvelope(envelope);\n }\n\n const result = await this.options.withLock(installationId, () =>\n this.mintUnderLock({workspaceId, installationId, mint}),\n );\n if (result.acquired) return result.value;\n\n return await this.serveStaleOrPoll({workspaceId, installationId, envelope});\n }\n\n private async mintUnderLock(params: {\n workspaceId: string;\n installationId: number;\n mint: () => Promise<GithubInstallationAccessToken>;\n }): Promise<GithubInstallationAccessToken> {\n const envelope = await this.readEnvelope(params.workspaceId, params.installationId);\n const now = this.now();\n if (usable(envelope, now)) {\n recordInstallationTokenLookup('db-hit');\n return tokenFromEnvelope(envelope);\n }\n\n if (activeBackoff(envelope, now)) {\n if (canServeStale(envelope, now)) {\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(envelope);\n }\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n envelope?.backoffReason ?? 'provider-unavailable',\n (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(),\n envelope?.backoffError,\n );\n }\n\n let token: GithubInstallationAccessToken;\n try {\n token = await this.recordMint(params.mint);\n } catch (error) {\n const providerError = toProviderError(error);\n const classified = classifyMintError(providerError);\n const until = new Date(this.now().getTime() + backoffMs(classified));\n recordInstallationTokenBackoff({reason: classified.reason, class: classified.class});\n\n await this.writeEnvelope(params.workspaceId, params.installationId, {\n token: envelope?.token,\n expiresAt: envelope?.expiresAt,\n permissions: envelope?.permissions,\n backoffUntil: until,\n backoffReason: classified.reason,\n backoffError: {\n message: providerError.message,\n ...(providerError.status === undefined ? {} : {status: providerError.status}),\n },\n }).catch((writeError) => {\n logger().warn(\n {installationId: params.installationId, reason: classified.reason, error: writeError},\n 'github installation token backoff write failed',\n );\n reportError(writeError, {\n boundary: 'integration.cache',\n operation: 'write-backoff-envelope',\n extra: {installationId: params.installationId},\n });\n });\n\n if (\n classified.class === 'transient' &&\n envelope?.token &&\n stillValid(envelope.expiresAt, this.now())\n ) {\n logger().warn(\n {\n installationId: params.installationId,\n expiresAt: envelope.expiresAt?.toISOString(),\n reason: classified.reason,\n backoffUntil: until.toISOString(),\n },\n 'github installation token mint failed; serving stale token',\n );\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(envelope);\n }\n\n logger().warn(\n {\n installationId: params.installationId,\n reason: classified.reason,\n backoffUntil: until.toISOString(),\n error: providerError,\n },\n 'github installation token mint failed; backoff recorded',\n );\n recordInstallationTokenLookup('backoff');\n throw providerError;\n }\n\n try {\n await this.writeEnvelope(params.workspaceId, params.installationId, {\n token: token.token,\n expiresAt: token.expiresAt,\n permissions: token.permissions,\n });\n } catch (error) {\n logger().warn(\n {installationId: params.installationId, expiresAt: token.expiresAt.toISOString(), error},\n 'github installation token cache write failed after mint',\n );\n reportError(error, {\n boundary: 'integration.cache',\n operation: 'write-minted-token',\n extra: {installationId: params.installationId},\n });\n }\n\n logger().info(\n {installationId: params.installationId, expiresAt: token.expiresAt.toISOString()},\n 'github installation token minted',\n );\n recordInstallationTokenLookup('minted');\n return token;\n }\n\n private async serveStaleOrPoll(params: {\n workspaceId: string;\n installationId: number;\n envelope: InstallationTokenEnvelope | undefined;\n }): Promise<GithubInstallationAccessToken> {\n const initialNow = this.now();\n if (canServeStale(params.envelope, initialNow)) {\n recordInstallationTokenLookup('served-stale');\n return tokenFromEnvelope(params.envelope);\n }\n if (activeBackoff(params.envelope, initialNow)) {\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n params.envelope.backoffReason,\n params.envelope.backoffUntil.getTime() - initialNow.getTime(),\n params.envelope.backoffError,\n );\n }\n\n for (const delayMs of this.pollDelaysMs) {\n await this.sleep(delayMs);\n const envelope = await this.readEnvelope(params.workspaceId, params.installationId);\n const now = this.now();\n if (usable(envelope, now)) {\n recordInstallationTokenLookup('contended-poll');\n return tokenFromEnvelope(envelope);\n }\n if (backoffActive(envelope, now)) {\n recordInstallationTokenLookup('backoff');\n throw providerErrorFromBackoff(\n envelope?.backoffReason ?? 'provider-unavailable',\n (envelope?.backoffUntil?.getTime() ?? now.getTime()) - now.getTime(),\n envelope?.backoffError,\n );\n }\n }\n\n throw new GithubIntegrationProviderError(\n 'provider-unavailable',\n 'GitHub installation token mint is still in progress',\n 1,\n );\n }\n\n private async recordMint(\n mint: () => Promise<GithubInstallationAccessToken>,\n ): Promise<GithubInstallationAccessToken> {\n const startedAt = Date.now();\n try {\n const token = await withTimeout(mint(), this.mintTimeoutMs);\n recordInstallationTokenMint({outcome: 'success', durationMs: Date.now() - startedAt});\n return token;\n } catch (error) {\n recordInstallationTokenMint({outcome: 'failure', durationMs: Date.now() - startedAt});\n throw error;\n }\n }\n\n private async readEnvelope(\n workspaceId: string,\n installationId: number,\n ): Promise<InstallationTokenEnvelope | undefined> {\n const raw = await this.options.secretStore.read(workspaceId, installationId);\n if (raw === null) return undefined;\n\n const envelope = parseInstallationTokenEnvelope(raw);\n if (envelope === undefined) {\n logger().warn({installationId}, 'github installation token cache envelope failed to decode');\n }\n return envelope;\n }\n\n private async writeEnvelope(\n workspaceId: string,\n installationId: number,\n envelope: InstallationTokenEnvelope,\n ): Promise<void> {\n await this.options.secretStore.write(workspaceId, installationId, envelope);\n }\n\n private async resolveWorkspaceId(installationId: number): Promise<string> {\n const nowMs = this.now().getTime();\n const cached = this.workspaceIds.get(installationId);\n if (cached && cached.expiresAtMs > nowMs) return cached.workspaceId;\n\n const workspaceId = await this.options.resolveWorkspaceId(installationId);\n this.workspaceIds.set(installationId, {\n workspaceId,\n expiresAtMs: nowMs + this.workspaceCacheTtlMs,\n });\n return workspaceId;\n }\n}\n\ntype ActiveBackoffEnvelope = InstallationTokenEnvelope & {\n backoffUntil: Date;\n backoffReason: NonNullable<InstallationTokenEnvelope['backoffReason']>;\n};\n\ntype TokenEnvelope = InstallationTokenEnvelope & {token: string; expiresAt: Date};\n\nfunction activeBackoff(\n envelope: InstallationTokenEnvelope | undefined,\n now: Date,\n): envelope is ActiveBackoffEnvelope {\n return (\n backoffActive(envelope, now) &&\n envelope?.backoffUntil !== undefined &&\n envelope.backoffReason !== undefined\n );\n}\n\nfunction canServeStale(\n envelope: InstallationTokenEnvelope | undefined,\n now: Date,\n): envelope is TokenEnvelope {\n const terminalBackoff =\n activeBackoff(envelope, now) && mintErrorClassForReason(envelope.backoffReason) === 'terminal';\n return (\n envelope?.token !== undefined &&\n envelope.expiresAt !== undefined &&\n stillValid(envelope.expiresAt, now) &&\n !terminalBackoff\n );\n}\n\nfunction tokenFromEnvelope(envelope: InstallationTokenEnvelope): GithubInstallationAccessToken {\n if (!envelope.token || !envelope.expiresAt) {\n throw new GithubIntegrationProviderError(\n 'malformed-provider-response',\n 'GitHub installation token cache envelope is missing a token or expiry',\n );\n }\n return {\n token: envelope.token,\n expiresAt: envelope.expiresAt,\n ...(envelope.permissions === undefined ? {} : {permissions: envelope.permissions}),\n };\n}\n\nfunction withTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<T>((_, reject) => {\n timer = setTimeout(() => {\n reject(\n new GithubIntegrationProviderError(\n 'timeout',\n 'Timed out minting GitHub installation access token',\n ),\n );\n }, timeoutMs);\n });\n\n return Promise.race([promise, timeout]).finally(() => {\n if (timer) clearTimeout(timer);\n });\n}\n"],"names":["setTimeout","sleepTimeout","reportError","logger","GithubIntegrationProviderError","recordInstallationTokenBackoff","recordInstallationTokenLookup","recordInstallationTokenMint","backoffActive","backoffMs","classifyMintError","mintErrorClassForReason","parseInstallationTokenEnvelope","providerErrorFromBackoff","stillValid","toProviderError","usable","DEFAULT_POLL_DELAYS_MS","DEFAULT_WORKSPACE_CACHE_TTL_MS","DEFAULT_MINT_TIMEOUT_MS","SharedInstallationTokenCache","options","workspaceIds","Map","now","Date","sleep","ms","then","undefined","pollDelaysMs","workspaceCacheTtlMs","mintTimeoutMs","getOrMint","installationId","mint","workspaceId","resolveWorkspaceId","envelope","readEnvelope","tokenFromEnvelope","result","withLock","mintUnderLock","acquired","value","serveStaleOrPoll","params","activeBackoff","canServeStale","backoffReason","backoffUntil","getTime","backoffError","token","recordMint","error","providerError","classified","until","reason","class","writeEnvelope","expiresAt","permissions","message","status","catch","writeError","warn","boundary","operation","extra","toISOString","info","initialNow","delayMs","startedAt","withTimeout","outcome","durationMs","raw","secretStore","read","write","nowMs","cached","get","expiresAtMs","set","terminalBackoff","promise","timeoutMs","timer","timeout","Promise","_","reject","race","finally","clearTimeout"],"mappings":"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"}
package/dist/config.d.ts CHANGED
@@ -7,6 +7,7 @@ export declare const config: Readonly<{
7
7
  GITHUB_APP_SLUG: string;
8
8
  GITHUB_APP_USERNAME: string | undefined;
9
9
  GITHUB_API_BASE_URL: string;
10
+ GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE: "disabled" | "enabled" | undefined;
10
11
  GITHUB_INSTALL_STATE_SECRET: string;
11
12
  } & import("@shipfox/config").CleanedEnvAccessors>;
12
13
  export declare function normalizedGithubPrivateKey(): string;
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM;;;;;;;;;;kDA8BjB,CAAC;AAEH,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAIA,eAAO,MAAM,MAAM;;;;;;;;;;;kDAmCjB,CAAC;AAEH,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD;AAED,wBAAgB,yBAAyB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAEjE;AAED,wBAAgB,0BAA0B,IAAI,MAAM,CAEnD"}
package/dist/config.js CHANGED
@@ -27,6 +27,14 @@ export const config = createConfig({
27
27
  desc: 'Base URL used for GitHub REST API requests. Set this only for GitHub Enterprise Server or a compatible test server.',
28
28
  default: 'https://api.github.com'
29
29
  }),
30
+ GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE: str({
31
+ desc: 'Temporary GitHub installation token format override. Set this to enabled to request stateless tokens, disabled to request stateful tokens, or leave it unset to follow the GitHub rollout.',
32
+ choices: [
33
+ 'enabled',
34
+ 'disabled'
35
+ ],
36
+ default: undefined
37
+ }),
30
38
  GITHUB_INSTALL_STATE_SECRET: str({
31
39
  desc: 'Secret used to sign the state token that protects the GitHub App install flow. Required.'
32
40
  })
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, str, url} from '@shipfox/config';\n\nconst trailingSlashesPattern = /\\/+$/u;\n\nexport const config = createConfig({\n GITHUB_APP_ID: str({\n desc: \"Numeric ID of the GitHub App, found on the app's settings page. Required.\",\n }),\n GITHUB_APP_PRIVATE_KEY: str({\n desc: 'Private key of the GitHub App in PEM format, used to sign API requests. Newlines may be written as \\\\n and are restored at runtime. Required.',\n }),\n GITHUB_APP_CLIENT_ID: str({\n desc: 'OAuth client ID of the GitHub App, used for user sign-in. Required.',\n }),\n GITHUB_APP_CLIENT_SECRET: str({\n desc: 'OAuth client secret of the GitHub App. Required.',\n }),\n GITHUB_APP_WEBHOOK_SECRET: str({\n desc: 'Secret used to verify the signature of incoming GitHub webhooks. Must match the value set on the GitHub App. Required.',\n }),\n GITHUB_APP_SLUG: str({\n desc: 'URL slug of the GitHub App, used to build install and callback links. Required.',\n }),\n GITHUB_APP_USERNAME: str({\n desc: 'GitHub App username used as the Git commit author when checkout credentials are persisted. Set this to the app username, such as my-app. The [bot] suffix is added automatically. Leave unset to keep Git author identity unset.',\n default: undefined,\n }),\n GITHUB_API_BASE_URL: url({\n desc: 'Base URL used for GitHub REST API requests. Set this only for GitHub Enterprise Server or a compatible test server.',\n default: 'https://api.github.com',\n }),\n GITHUB_INSTALL_STATE_SECRET: str({\n desc: 'Secret used to sign the state token that protects the GitHub App install flow. Required.',\n }),\n});\n\nexport function normalizedGithubPrivateKey(): string {\n return config.GITHUB_APP_PRIVATE_KEY.replaceAll('\\\\n', '\\n');\n}\n\nexport function normalizeGithubApiBaseUrl(baseUrl: string): string {\n return baseUrl.replace(trailingSlashesPattern, '');\n}\n\nexport function normalizedGithubApiBaseUrl(): string {\n return normalizeGithubApiBaseUrl(config.GITHUB_API_BASE_URL);\n}\n"],"names":["createConfig","str","url","trailingSlashesPattern","config","GITHUB_APP_ID","desc","GITHUB_APP_PRIVATE_KEY","GITHUB_APP_CLIENT_ID","GITHUB_APP_CLIENT_SECRET","GITHUB_APP_WEBHOOK_SECRET","GITHUB_APP_SLUG","GITHUB_APP_USERNAME","default","undefined","GITHUB_API_BASE_URL","GITHUB_INSTALL_STATE_SECRET","normalizedGithubPrivateKey","replaceAll","normalizeGithubApiBaseUrl","baseUrl","replace","normalizedGithubApiBaseUrl"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAEvD,MAAMC,yBAAyB;AAE/B,OAAO,MAAMC,SAASJ,aAAa;IACjCK,eAAeJ,IAAI;QACjBK,MAAM;IACR;IACAC,wBAAwBN,IAAI;QAC1BK,MAAM;IACR;IACAE,sBAAsBP,IAAI;QACxBK,MAAM;IACR;IACAG,0BAA0BR,IAAI;QAC5BK,MAAM;IACR;IACAI,2BAA2BT,IAAI;QAC7BK,MAAM;IACR;IACAK,iBAAiBV,IAAI;QACnBK,MAAM;IACR;IACAM,qBAAqBX,IAAI;QACvBK,MAAM;QACNO,SAASC;IACX;IACAC,qBAAqBb,IAAI;QACvBI,MAAM;QACNO,SAAS;IACX;IACAG,6BAA6Bf,IAAI;QAC/BK,MAAM;IACR;AACF,GAAG;AAEH,OAAO,SAASW;IACd,OAAOb,OAAOG,sBAAsB,CAACW,UAAU,CAAC,OAAO;AACzD;AAEA,OAAO,SAASC,0BAA0BC,OAAe;IACvD,OAAOA,QAAQC,OAAO,CAAClB,wBAAwB;AACjD;AAEA,OAAO,SAASmB;IACd,OAAOH,0BAA0Bf,OAAOW,mBAAmB;AAC7D"}
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {createConfig, str, url} from '@shipfox/config';\n\nconst trailingSlashesPattern = /\\/+$/u;\n\nexport const config = createConfig({\n GITHUB_APP_ID: str({\n desc: \"Numeric ID of the GitHub App, found on the app's settings page. Required.\",\n }),\n GITHUB_APP_PRIVATE_KEY: str({\n desc: 'Private key of the GitHub App in PEM format, used to sign API requests. Newlines may be written as \\\\n and are restored at runtime. Required.',\n }),\n GITHUB_APP_CLIENT_ID: str({\n desc: 'OAuth client ID of the GitHub App, used for user sign-in. Required.',\n }),\n GITHUB_APP_CLIENT_SECRET: str({\n desc: 'OAuth client secret of the GitHub App. Required.',\n }),\n GITHUB_APP_WEBHOOK_SECRET: str({\n desc: 'Secret used to verify the signature of incoming GitHub webhooks. Must match the value set on the GitHub App. Required.',\n }),\n GITHUB_APP_SLUG: str({\n desc: 'URL slug of the GitHub App, used to build install and callback links. Required.',\n }),\n GITHUB_APP_USERNAME: str({\n desc: 'GitHub App username used as the Git commit author when checkout credentials are persisted. Set this to the app username, such as my-app. The [bot] suffix is added automatically. Leave unset to keep Git author identity unset.',\n default: undefined,\n }),\n GITHUB_API_BASE_URL: url({\n desc: 'Base URL used for GitHub REST API requests. Set this only for GitHub Enterprise Server or a compatible test server.',\n default: 'https://api.github.com',\n }),\n GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE: str({\n desc: 'Temporary GitHub installation token format override. Set this to enabled to request stateless tokens, disabled to request stateful tokens, or leave it unset to follow the GitHub rollout.',\n choices: ['enabled', 'disabled'] as const,\n default: undefined,\n }),\n GITHUB_INSTALL_STATE_SECRET: str({\n desc: 'Secret used to sign the state token that protects the GitHub App install flow. Required.',\n }),\n});\n\nexport function normalizedGithubPrivateKey(): string {\n return config.GITHUB_APP_PRIVATE_KEY.replaceAll('\\\\n', '\\n');\n}\n\nexport function normalizeGithubApiBaseUrl(baseUrl: string): string {\n return baseUrl.replace(trailingSlashesPattern, '');\n}\n\nexport function normalizedGithubApiBaseUrl(): string {\n return normalizeGithubApiBaseUrl(config.GITHUB_API_BASE_URL);\n}\n"],"names":["createConfig","str","url","trailingSlashesPattern","config","GITHUB_APP_ID","desc","GITHUB_APP_PRIVATE_KEY","GITHUB_APP_CLIENT_ID","GITHUB_APP_CLIENT_SECRET","GITHUB_APP_WEBHOOK_SECRET","GITHUB_APP_SLUG","GITHUB_APP_USERNAME","default","undefined","GITHUB_API_BASE_URL","GITHUB_INSTALLATION_TOKEN_FORMAT_OVERRIDE","choices","GITHUB_INSTALL_STATE_SECRET","normalizedGithubPrivateKey","replaceAll","normalizeGithubApiBaseUrl","baseUrl","replace","normalizedGithubApiBaseUrl"],"mappings":"AAAA,SAAQA,YAAY,EAAEC,GAAG,EAAEC,GAAG,QAAO,kBAAkB;AAEvD,MAAMC,yBAAyB;AAE/B,OAAO,MAAMC,SAASJ,aAAa;IACjCK,eAAeJ,IAAI;QACjBK,MAAM;IACR;IACAC,wBAAwBN,IAAI;QAC1BK,MAAM;IACR;IACAE,sBAAsBP,IAAI;QACxBK,MAAM;IACR;IACAG,0BAA0BR,IAAI;QAC5BK,MAAM;IACR;IACAI,2BAA2BT,IAAI;QAC7BK,MAAM;IACR;IACAK,iBAAiBV,IAAI;QACnBK,MAAM;IACR;IACAM,qBAAqBX,IAAI;QACvBK,MAAM;QACNO,SAASC;IACX;IACAC,qBAAqBb,IAAI;QACvBI,MAAM;QACNO,SAAS;IACX;IACAG,2CAA2Cf,IAAI;QAC7CK,MAAM;QACNW,SAAS;YAAC;YAAW;SAAW;QAChCJ,SAASC;IACX;IACAI,6BAA6BjB,IAAI;QAC/BK,MAAM;IACR;AACF,GAAG;AAEH,OAAO,SAASa;IACd,OAAOf,OAAOG,sBAAsB,CAACa,UAAU,CAAC,OAAO;AACzD;AAEA,OAAO,SAASC,0BAA0BC,OAAe;IACvD,OAAOA,QAAQC,OAAO,CAACpB,wBAAwB;AACjD;AAEA,OAAO,SAASqB;IACd,OAAOH,0BAA0BjB,OAAOW,mBAAmB;AAC7D"}
@@ -1,7 +1,7 @@
1
1
  import type { AgentToolSelectionCatalog, AgentToolSession, AgentToolsProvider, IntegrationConnection, OpenAgentToolsSessionInput } from '@shipfox/api-integration-spi';
2
2
  import { type GithubInstallationTokenProvider } from '#api/installation-token-provider.js';
3
3
  import type { GithubInstallation } from '#db/installations.js';
4
- import { type GithubAgentToolCatalogEntry, type GithubAgentToolRequiredScope } from './github-agent-tool-catalog.js';
4
+ import { type GithubAgentToolCatalogEntry, type GithubAgentToolId, type GithubAgentToolRequiredScope } from './github-agent-tool-catalog.js';
5
5
  export type { GithubAgentToolCatalogEntry, GithubAgentToolCategory, GithubAgentToolId, GithubAgentToolPermission, GithubAgentToolPermissionAccess, GithubAgentToolRequiredPermission, GithubAgentToolRequiredScope, GithubAgentToolSensitivity, } from './github-agent-tool-catalog.js';
6
6
  export { buildGithubAgentToolSelectionCatalog, DEFAULT_JOB_LOG_TAIL_LINES, githubAgentToolCatalog, githubAgentToolSelectionCatalog, } from './github-agent-tool-catalog.js';
7
7
  type GithubIntegrationConnection = IntegrationConnection<'github'>;
@@ -26,10 +26,17 @@ export interface GithubAgentToolsProviderOptions {
26
26
  tokenProvider?: GithubInstallationTokenProvider | undefined;
27
27
  createClient?: GithubToolClientFactory | undefined;
28
28
  }
29
+ export interface GithubToolResponse {
30
+ data: unknown;
31
+ headers?: Record<string, string | number | undefined> | undefined;
32
+ status?: number | undefined;
33
+ url?: string | undefined;
34
+ }
29
35
  export interface GithubToolClient {
30
- request(route: string, parameters: Record<string, unknown>): Promise<{
31
- data: unknown;
32
- }>;
36
+ request(route: string, parameters: Record<string, unknown>): Promise<GithubToolResponse>;
37
+ graphql?: ((query: string, variables: Record<string, unknown>) => Promise<unknown>) | undefined;
33
38
  }
34
39
  export type GithubToolClientFactory = (token: string) => GithubToolClient;
40
+ export declare function githubOperationRoute(toolId: GithubAgentToolId, method: string | undefined, args: Record<string, unknown>): string | undefined;
41
+ export declare function projectGithubOperationParameters(toolId: GithubAgentToolId, method: string | undefined, args: Record<string, unknown>): Record<string, unknown>;
35
42
  //# sourceMappingURL=agent-tools.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"agent-tools.d.ts","sourceRoot":"","sources":["../../src/core/agent-tools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAGV,yBAAyB,EACzB,gBAAgB,EAChB,kBAAkB,EAClB,qBAAqB,EACrB,0BAA0B,EAC3B,MAAM,8BAA8B,CAAC;AAEtC,OAAO,EAEL,KAAK,+BAA+B,EACrC,MAAM,qCAAqC,CAAC;AAE7C,OAAO,KAAK,EAAC,kBAAkB,EAAC,MAAM,sBAAsB,CAAC;AAE7D,OAAO,EACL,KAAK,2BAA2B,EAEhC,KAAK,4BAA4B,EAGlC,MAAM,gCAAgC,CAAC;AAExC,YAAY,EACV,2BAA2B,EAC3B,uBAAuB,EACvB,iBAAiB,EACjB,yBAAyB,EACzB,+BAA+B,EAC/B,iCAAiC,EACjC,4BAA4B,EAC5B,0BAA0B,GAC3B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EACL,oCAAoC,EACpC,0BAA0B,EAC1B,sBAAsB,EACtB,+BAA+B,GAChC,MAAM,gCAAgC,CAAC;AAExC,KAAK,2BAA2B,GAAG,qBAAqB,CAAC,QAAQ,CAAC,CAAC;AAEnE,KAAK,oBAAoB,GAAG;IAC1B,OAAO,CAAC,EAAE,OAAO,GAAG,SAAS,CAAC;IAC9B,OAAO,EAAE,SAAS;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAC,EAAE,CAAC;IACjD,iBAAiB,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,SAAS,CAAC;CACzD,CAAC;AAEF,qBAAa,wBACX,YACE,kBAAkB,CAChB,2BAA2B,EAC3B,4BAA4B,EAC5B,OAAO,EACP,oBAAoB,CACrB;IAIS,OAAO,CAAC,QAAQ,CAAC,OAAO;IAFpC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAkC;IAEhE,YAA6B,OAAO,GAAE,+BAAoC,EAEzE;IAED,OAAO,IAAI,SAAS,2BAA2B,EAAE,CAEhD;IAED,gBAAgB,IAAI,yBAAyB,CAE5C;IAEK,WAAW,CACf,KAAK,EAAE,0BAA0B,CAAC,2BAA2B,EAAE,4BAA4B,CAAC,GAC3F,OAAO,CAAC,gBAAgB,CAAC,oBAAoB,CAAC,CAAC,CA8CjD;CACF;AAED,MAAM,WAAW,+BAA+B;IAC9C,6BAA6B,CAAC,EAC1B,CAAC,CAAC,YAAY,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC,GACnE,SAAS,CAAC;IACd,aAAa,CAAC,EAAE,+BAA+B,GAAG,SAAS,CAAC;IAC5D,YAAY,CAAC,EAAE,uBAAuB,GAAG,SAAS,CAAC;CACpD;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;QAAC,IAAI,EAAE,OAAO,CAAA;KAAC,CAAC,CAAC;CACvF;AAED,MAAM,MAAM,uBAAuB,GAAG,CAAC,KAAK,EAAE,MAAM,KAAK,gBAAgB,CAAC"}
1
+ {"version":3,"file":"agent-tools.d.ts","sourceRoot":"","sources":["../../src/core/agent-tools.ts"],"names":[],"mappings":"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"}
@@ -1,9 +1,30 @@
1
1
  import { Octokit } from 'octokit';
2
+ import { mapGithubError } from '#api/client.js';
2
3
  import { createGithubInstallationTokenProvider } from '#api/installation-token-provider.js';
3
- import { normalizedGithubApiBaseUrl } from '#config.js';
4
+ import { config, normalizedGithubApiBaseUrl } from '#config.js';
4
5
  import { GithubIntegrationProviderError } from './errors.js';
5
6
  import { githubAgentToolCatalog, githubAgentToolSelectionCatalog } from './github-agent-tool-catalog.js';
6
7
  export { buildGithubAgentToolSelectionCatalog, DEFAULT_JOB_LOG_TAIL_LINES, githubAgentToolCatalog, githubAgentToolSelectionCatalog } from './github-agent-tool-catalog.js';
8
+ const GITHUB_GRAPHQL_ROUTE = 'POST /graphql';
9
+ const GITHUB_ARTIFACT_ARCHIVE_FORMAT = 'zip';
10
+ const GITHUB_ARTIFACT_DOWNLOAD_ROUTE = `GET /repos/{owner}/{repo}/actions/artifacts/{resource_id}/${GITHUB_ARTIFACT_ARCHIVE_FORMAT}`;
11
+ const GITHUB_ARTIFACT_DOWNLOAD_TIMEOUT_MS = 30_000;
12
+ const GITHUB_APP_BOT_SUFFIX = '[bot]';
13
+ const PENDING_REVIEW_PAGE_SIZE = 100;
14
+ const PENDING_REVIEW_MAX_PAGE_REQUESTS = 5;
15
+ const PENDING_REVIEW_LOOKUP_TIMEOUT_MS = 15_000;
16
+ const PENDING_REVIEW_PAGE_TIMEOUT_MS = 5_000;
17
+ const PENDING_REVIEW_PAGE_PATTERN = /[?&]page=(\d+)/u;
18
+ const NO_PENDING_REVIEW_MESSAGE = 'No pending pull request review found for the authenticated GitHub user.';
19
+ const ADD_PENDING_REVIEW_COMMENT_MUTATION = `
20
+ mutation AddCommentToPendingReview($input: AddPullRequestReviewThreadInput!) {
21
+ addPullRequestReviewThread(input: $input) {
22
+ thread {
23
+ id
24
+ }
25
+ }
26
+ }
27
+ `;
7
28
  export class GithubAgentToolsProvider {
8
29
  constructor(options = {}){
9
30
  this.options = options;
@@ -28,24 +49,28 @@ export class GithubAgentToolsProvider {
28
49
  return {
29
50
  call: async (call)=>{
30
51
  const tool = input.tools.find((candidate)=>candidate.id === call.toolId);
31
- if (!tool) return githubToolError(`Unknown GitHub tool: ${call.toolId}`);
52
+ if (!tool) return githubToolError(`Unknown GitHub tool: ${call.toolId}`, 'invalid-request');
32
53
  const operation = resolveGithubOperation(tool, call);
33
- if (operation === undefined) return githubToolError('Unknown GitHub tool operation');
54
+ if (operation === undefined) return githubToolError('Unknown GitHub tool operation', 'invalid-request');
34
55
  const validationError = validateGithubToolArguments(tool, call.arguments);
35
- if (validationError) return githubToolError(validationError);
56
+ if (validationError) return githubToolError(validationError, 'invalid-request');
36
57
  tokenPromise ??= this.tokenProvider.getInstallationAccessToken(installationId);
37
58
  const token = await tokenPromise;
38
59
  if (!hasGrantedPermissions(token.permissions ?? {}, tool, call)) {
39
- return githubToolError('GitHub installation token is missing permission for this operation');
60
+ return githubToolError('GitHub installation token is missing permission for this operation', 'access-denied');
40
61
  }
41
62
  const client = (this.options.createClient ?? createOctokitClient)(token.token);
42
- try {
43
- const response = await client.request(operation.route, operation.parameters);
44
- return githubToolResult(tool.id, response.data);
45
- } catch (error) {
46
- if (error instanceof GithubIntegrationProviderError) return githubToolError(error.message);
47
- throw error;
63
+ const method = typeof call.arguments.method === 'string' ? call.arguments.method : undefined;
64
+ if (operation.kind === 'graphql') {
65
+ const data = await mapGithubError(()=>addCommentToPendingReview(client, operation.parameters));
66
+ return data === undefined ? githubToolError(NO_PENDING_REVIEW_MESSAGE, 'provider-rejected') : githubToolResult(tool.id, data);
67
+ }
68
+ const operationParameters = await mapGithubError(()=>resolvePendingReviewParameters(client, operation.parameters, tool.id, method));
69
+ if (operationParameters === undefined) {
70
+ return githubToolError(NO_PENDING_REVIEW_MESSAGE, 'provider-rejected');
48
71
  }
72
+ const response = await mapGithubError(()=>client.request(operation.route, operationParameters));
73
+ return githubToolResult(tool.id, response.data, response, operationParameters, operation.route);
49
74
  }
50
75
  };
51
76
  }
@@ -59,7 +84,26 @@ function createOctokitClient(token) {
59
84
  }
60
85
  });
61
86
  return {
62
- request: async (route, parameters)=>await octokit.request(route, parameters)
87
+ request: async (route, parameters)=>{
88
+ if (route !== GITHUB_ARTIFACT_DOWNLOAD_ROUTE) {
89
+ return await octokit.request(route, parameters);
90
+ }
91
+ const abortController = new AbortController();
92
+ const timeout = setTimeout(()=>abortController.abort(), GITHUB_ARTIFACT_DOWNLOAD_TIMEOUT_MS);
93
+ try {
94
+ return await octokit.request(route, {
95
+ ...parameters,
96
+ request: {
97
+ redirect: 'manual',
98
+ parseSuccessResponseBody: false,
99
+ signal: abortController.signal
100
+ }
101
+ });
102
+ } finally{
103
+ clearTimeout(timeout);
104
+ }
105
+ },
106
+ graphql: async (query, variables)=>await octokit.graphql(query, variables)
63
107
  };
64
108
  }
65
109
  function resolveGithubOperation(tool, call) {
@@ -74,10 +118,11 @@ function resolveGithubOperation(tool, call) {
74
118
  const route = githubOperationRoute(toolId, method, params);
75
119
  return route === undefined ? undefined : {
76
120
  route,
77
- parameters: projectGithubOperationParameters(toolId, method, params)
121
+ parameters: projectGithubOperationParameters(toolId, method, params),
122
+ kind: route === GITHUB_GRAPHQL_ROUTE ? 'graphql' : 'rest'
78
123
  };
79
124
  }
80
- function githubOperationRoute(toolId, method, args) {
125
+ export function githubOperationRoute(toolId, method, args) {
81
126
  const owner = '{owner}';
82
127
  const repo = '{repo}';
83
128
  const issue = '{issue_number}';
@@ -114,7 +159,7 @@ function githubOperationRoute(toolId, method, args) {
114
159
  case 'sub_issue_write.remove':
115
160
  return `DELETE ${repoPath}/issues/${issue}/sub_issues/{sub_issue_id}`;
116
161
  case 'sub_issue_write.reprioritize':
117
- return `PATCH ${repoPath}/issues/${issue}/sub_issues/{sub_issue_id}`;
162
+ return `PATCH ${repoPath}/issues/${issue}/sub_issues/priority`;
118
163
  case 'pull_request_read.get':
119
164
  return `GET ${repoPath}/pulls/${pull}`;
120
165
  case 'pull_request_read.get_diff':
@@ -142,7 +187,7 @@ function githubOperationRoute(toolId, method, args) {
142
187
  case 'update_pull_request.':
143
188
  return `PATCH ${repoPath}/pulls/${pull}`;
144
189
  case 'add_reply_to_pull_request_comment.':
145
- return `POST ${repoPath}/pulls/{comment_id}/replies`;
190
+ return args.reaction !== undefined && args.body === undefined ? `POST ${repoPath}/pulls/comments/{comment_id}/reactions` : `POST ${repoPath}/pulls/${pull}/comments/{comment_id}/replies`;
146
191
  case 'merge_pull_request.':
147
192
  return `PUT ${repoPath}/pulls/${pull}/merge`;
148
193
  case 'update_pull_request_branch.':
@@ -154,7 +199,7 @@ function githubOperationRoute(toolId, method, args) {
154
199
  case 'pull_request_review_write.delete_pending':
155
200
  return `DELETE ${repoPath}/pulls/${pull}/reviews/{review_id}`;
156
201
  case 'add_comment_to_pending_review.':
157
- return `POST ${repoPath}/pulls/${pull}/comments`;
202
+ return GITHUB_GRAPHQL_ROUTE;
158
203
  case 'actions_list.list_workflows':
159
204
  return `GET ${repoPath}/actions/workflows`;
160
205
  case 'actions_list.list_workflow_runs':
@@ -170,7 +215,7 @@ function githubOperationRoute(toolId, method, args) {
170
215
  case 'actions_get.get_workflow_job':
171
216
  return `GET ${repoPath}/actions/jobs/${resource}`;
172
217
  case 'actions_get.download_workflow_run_artifact':
173
- return `GET ${repoPath}/actions/artifacts/${resource}/{archive_format}`;
218
+ return GITHUB_ARTIFACT_DOWNLOAD_ROUTE;
174
219
  case 'actions_get.get_workflow_run_usage':
175
220
  return `GET ${repoPath}/actions/runs/${resource}/timing`;
176
221
  case 'actions_get.get_workflow_run_logs_url':
@@ -191,7 +236,30 @@ function githubOperationRoute(toolId, method, args) {
191
236
  return undefined;
192
237
  }
193
238
  }
194
- function projectGithubOperationParameters(toolId, method, args) {
239
+ async function addCommentToPendingReview(client, args) {
240
+ if (client.graphql === undefined) {
241
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub client does not support GraphQL operations');
242
+ }
243
+ const review = await latestPendingReview(client, args, 'nodeId');
244
+ if (review === undefined) return undefined;
245
+ if (review.nodeId === undefined) {
246
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub pending pull request review did not include a node ID');
247
+ }
248
+ const input = {
249
+ pullRequestReviewId: review.nodeId,
250
+ path: args.path,
251
+ body: args.body,
252
+ subjectType: args.subject_type
253
+ };
254
+ if (args.line !== undefined) input.line = args.line;
255
+ if (args.side !== undefined) input.side = args.side;
256
+ if (args.start_line !== undefined) input.startLine = args.start_line;
257
+ if (args.start_side !== undefined) input.startSide = args.start_side;
258
+ return await client.graphql(ADD_PENDING_REVIEW_COMMENT_MUTATION, {
259
+ input
260
+ });
261
+ }
262
+ export function projectGithubOperationParameters(toolId, method, args) {
195
263
  const parameters = {
196
264
  ...args
197
265
  };
@@ -207,8 +275,129 @@ function projectGithubOperationParameters(toolId, method, args) {
207
275
  }
208
276
  return parameters;
209
277
  }
210
- function githubToolResult(toolId, data) {
211
- const structuredContent = projectGithubToolOutput(toolId, data);
278
+ async function resolvePendingReviewParameters(client, parameters, toolId, method) {
279
+ if (!isPendingReviewOperation(toolId, method)) return parameters;
280
+ const review = await latestPendingReview(client, parameters, 'id');
281
+ if (review === undefined) return undefined;
282
+ if (review.id === undefined) {
283
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub pending pull request review did not include a numeric ID');
284
+ }
285
+ return {
286
+ ...parameters,
287
+ review_id: review.id
288
+ };
289
+ }
290
+ function isPendingReviewOperation(toolId, method) {
291
+ return toolId === 'pull_request_review_write' && (method === 'submit_pending' || method === 'delete_pending');
292
+ }
293
+ async function latestPendingReview(client, parameters, requiredIdentifier) {
294
+ const lookupController = new AbortController();
295
+ const lookupTimeout = setTimeout(()=>lookupController.abort(), PENDING_REVIEW_LOOKUP_TIMEOUT_MS);
296
+ try {
297
+ return await latestPendingReviewBeforeDeadline(client, parameters, requiredIdentifier, lookupController.signal);
298
+ } finally{
299
+ clearTimeout(lookupTimeout);
300
+ }
301
+ }
302
+ async function latestPendingReviewBeforeDeadline(client, parameters, requiredIdentifier, lookupSignal) {
303
+ const firstPage = await requestPendingReviewPage(client, parameters, 1, lookupSignal);
304
+ const lastPage = pendingReviewLastPage(firstPage.headers);
305
+ let requests = 1;
306
+ let malformed = false;
307
+ for(let page = lastPage; page >= 1; page -= 1){
308
+ let response = firstPage;
309
+ if (page !== 1) {
310
+ if (requests >= PENDING_REVIEW_MAX_PAGE_REQUESTS) {
311
+ throw new GithubIntegrationProviderError('content-too-large', 'GitHub pull request review history exceeded the pending review lookup limit');
312
+ }
313
+ response = await requestPendingReviewPage(client, parameters, page, lookupSignal);
314
+ requests += 1;
315
+ }
316
+ if (!Array.isArray(response.data)) {
317
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub pull request review list response was malformed');
318
+ }
319
+ const result = latestPendingReviewOnPage(response.data, requiredIdentifier);
320
+ if (result.review !== undefined) return result.review;
321
+ malformed ||= result.malformed;
322
+ }
323
+ if (malformed) {
324
+ throw new GithubIntegrationProviderError('malformed-provider-response', requiredIdentifier === 'nodeId' ? 'GitHub pending pull request review did not include a node ID' : 'GitHub pending pull request review did not include a numeric ID');
325
+ }
326
+ return undefined;
327
+ }
328
+ async function requestPendingReviewPage(client, parameters, page, lookupSignal) {
329
+ const pageController = new AbortController();
330
+ const abortPage = ()=>pageController.abort();
331
+ if (lookupSignal.aborted) abortPage();
332
+ else lookupSignal.addEventListener('abort', abortPage, {
333
+ once: true
334
+ });
335
+ const pageTimeout = setTimeout(abortPage, PENDING_REVIEW_PAGE_TIMEOUT_MS);
336
+ try {
337
+ return await client.request('GET /repos/{owner}/{repo}/pulls/{pull_number}/reviews', {
338
+ owner: parameters.owner,
339
+ repo: parameters.repo,
340
+ pull_number: parameters.pull_number,
341
+ per_page: PENDING_REVIEW_PAGE_SIZE,
342
+ page,
343
+ request: {
344
+ signal: pageController.signal
345
+ }
346
+ });
347
+ } finally{
348
+ clearTimeout(pageTimeout);
349
+ lookupSignal.removeEventListener('abort', abortPage);
350
+ }
351
+ }
352
+ function pendingReviewLastPage(headers) {
353
+ const link = headers?.link;
354
+ if (typeof link !== 'string') return 1;
355
+ const lastLink = link.split(',').find((part)=>part.includes('rel="last"'));
356
+ if (lastLink === undefined) return 1;
357
+ const match = PENDING_REVIEW_PAGE_PATTERN.exec(lastLink);
358
+ const page = match?.[1] === undefined ? Number.NaN : Number.parseInt(match[1], 10);
359
+ if (!Number.isSafeInteger(page) || page < 1) {
360
+ throw new GithubIntegrationProviderError('malformed-provider-response', 'GitHub pull request review pagination response was malformed');
361
+ }
362
+ return page;
363
+ }
364
+ function latestPendingReviewOnPage(data, requiredIdentifier) {
365
+ let malformed = false;
366
+ const appLogin = githubAppBotLogin().toLowerCase();
367
+ for(let index = data.length - 1; index >= 0; index -= 1){
368
+ const review = data[index];
369
+ if (!isRecord(review) || review.state !== 'PENDING') continue;
370
+ const userLogin = isRecord(review.user) ? review.user.login : undefined;
371
+ if (typeof userLogin !== 'string' || userLogin.trim().length === 0) {
372
+ malformed = true;
373
+ continue;
374
+ }
375
+ if (userLogin.trim().toLowerCase() !== appLogin) continue;
376
+ const id = typeof review.id === 'number' && Number.isSafeInteger(review.id) && review.id > 0 ? review.id : undefined;
377
+ const nodeId = typeof review.node_id === 'string' && review.node_id.trim().length > 0 ? review.node_id.trim() : undefined;
378
+ const reference = {
379
+ id,
380
+ nodeId
381
+ };
382
+ if (reference[requiredIdentifier] !== undefined) return {
383
+ malformed,
384
+ review: reference
385
+ };
386
+ malformed = true;
387
+ }
388
+ return {
389
+ malformed
390
+ };
391
+ }
392
+ function githubAppBotLogin() {
393
+ const configuredUsername = config.GITHUB_APP_USERNAME?.trim() || config.GITHUB_APP_SLUG.trim();
394
+ return configuredUsername.toLowerCase().endsWith(GITHUB_APP_BOT_SUFFIX) ? configuredUsername : `${configuredUsername}${GITHUB_APP_BOT_SUFFIX}`;
395
+ }
396
+ function githubToolResult(toolId, data, response, parameters, route) {
397
+ const structuredContent = projectGithubToolOutput(toolId, data, response, parameters, route);
398
+ if (structuredContent === undefined) {
399
+ return githubToolError('GitHub artifact download did not return a download URL', 'malformed-provider-response');
400
+ }
212
401
  return {
213
402
  content: [
214
403
  {
@@ -219,7 +408,10 @@ function githubToolResult(toolId, data) {
219
408
  structuredContent
220
409
  };
221
410
  }
222
- function projectGithubToolOutput(toolId, data) {
411
+ function projectGithubToolOutput(toolId, data, response, parameters, route) {
412
+ if (route === GITHUB_ARTIFACT_DOWNLOAD_ROUTE) {
413
+ return projectGithubArtifactDownloadOutput(response, parameters);
414
+ }
223
415
  switch(toolId){
224
416
  case 'list_issue_types':
225
417
  return {
@@ -256,10 +448,26 @@ function projectGithubToolOutput(toolId, data) {
256
448
  };
257
449
  }
258
450
  }
451
+ function projectGithubArtifactDownloadOutput(response, parameters) {
452
+ if (response === undefined) return undefined;
453
+ const downloadUrl = response.headers?.location;
454
+ if (typeof downloadUrl !== 'string' || downloadUrl.length === 0) return undefined;
455
+ const output = {
456
+ archive_format: GITHUB_ARTIFACT_ARCHIVE_FORMAT,
457
+ download_url: downloadUrl
458
+ };
459
+ if (typeof parameters?.resource_id === 'string') output.artifact_id = parameters.resource_id;
460
+ const contentType = response.headers?.['content-type'];
461
+ if (typeof contentType === 'string') output.content_type = contentType;
462
+ const contentLength = response.headers?.['content-length'];
463
+ const sizeBytes = typeof contentLength === 'number' ? contentLength : Number(contentLength);
464
+ if (Number.isSafeInteger(sizeBytes) && sizeBytes >= 0) output.size_bytes = sizeBytes;
465
+ return output;
466
+ }
259
467
  function githubSearchItems(data) {
260
468
  return isRecord(data) ? data.items : data;
261
469
  }
262
- function githubToolError(message) {
470
+ function githubToolError(message, code) {
263
471
  return {
264
472
  isError: true,
265
473
  content: [
@@ -267,7 +475,10 @@ function githubToolError(message) {
267
475
  type: 'text',
268
476
  text: message
269
477
  }
270
- ]
478
+ ],
479
+ structuredContent: {
480
+ code
481
+ }
271
482
  };
272
483
  }
273
484
  function isRecord(value) {