@blogic-cz/agent-tools 1.5.8 → 1.5.10

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,7 @@ import { retryTransient } from "#shared/retry-transient";
4
4
 
5
5
  import { GitHubAuthError, GitHubCommandError, GitHubNotFoundError } from "./errors";
6
6
  import type { GitHubApiError } from "./errors";
7
+ import { ghEnvironment } from "./environment";
7
8
  import { validateOutboundText } from "./text-input";
8
9
 
9
10
  // Direct HTTP, not `gh api`: the CLI collapses every failure into a non-zero exit and
@@ -44,6 +45,7 @@ export const resolveGitHubToken = Effect.fn("gh.resolveGitHubToken")(function* (
44
45
  const proc = Bun.spawn(["gh", "auth", "token", "--hostname", "github.com"], {
45
46
  stdout: "pipe",
46
47
  stderr: "ignore",
48
+ env: ghEnvironment(),
47
49
  });
48
50
  const stdout = await new Response(proc.stdout).text();
49
51
  const exitCode = await proc.exited;
@@ -67,27 +69,22 @@ export type GitHubApiRequest = {
67
69
 
68
70
  const MAX_API_RETRIES = 2;
69
71
 
70
- const outboundBodyText = (value: unknown): string => {
72
+ // Traverse only parsed JSON, never the caller's getters or toJSON hooks.
73
+ const decodedBodyText = (value: unknown): string => {
71
74
  if (typeof value === "string") return value;
72
- if (Array.isArray(value)) return value.map(outboundBodyText).join("\n");
75
+ if (Array.isArray(value)) return value.map(decodedBodyText).join("\n");
73
76
  if (value !== null && typeof value === "object")
74
- return Object.values(value).map(outboundBodyText).join("\n");
77
+ return Object.entries(value)
78
+ .map(([key, item]) => `${key}\n${decodedBodyText(item)}`)
79
+ .join("\n");
75
80
  return "";
76
81
  };
77
82
 
78
- const githubApiAttempt = Effect.fn("gh.githubApiAttempt")(function* <T>(opts: GitHubApiRequest) {
83
+ const githubApiAttempt = Effect.fn("gh.githubApiAttempt")(function* <T>(
84
+ opts: GitHubApiRequest,
85
+ serializedBody: string | undefined,
86
+ ) {
79
87
  const method = opts.method ?? "GET";
80
- const bodyText = yield* Effect.try({
81
- try: () => outboundBodyText(opts.body),
82
- catch: () =>
83
- new GitHubCommandError({
84
- command: `gh-tool ${method} ${opts.path}`,
85
- exitCode: 1,
86
- stderr: "Refusing to publish an invalid request body",
87
- message: "Refusing to publish an invalid request body",
88
- }),
89
- });
90
- yield* validateOutboundText(`${opts.path}\n${bodyText}`, `gh-tool ${method} ${opts.path}`);
91
88
  const token = yield* resolveGitHubToken();
92
89
  const url = `${GITHUB_API_ROOT}/${opts.path.replace(/^\//, "")}`;
93
90
 
@@ -99,9 +96,9 @@ const githubApiAttempt = Effect.fn("gh.githubApiAttempt")(function* <T>(opts: Gi
99
96
  Accept: GITHUB_ACCEPT,
100
97
  Authorization: `Bearer ${token}`,
101
98
  "X-GitHub-Api-Version": GITHUB_API_VERSION,
102
- ...(opts.body === undefined ? {} : { "Content-Type": "application/json" }),
99
+ ...(serializedBody === undefined ? {} : { "Content-Type": "application/json" }),
103
100
  },
104
- ...(opts.body === undefined ? {} : { body: JSON.stringify(opts.body) }),
101
+ ...(serializedBody === undefined ? {} : { body: serializedBody }),
105
102
  }),
106
103
  catch: (cause) =>
107
104
  new GitHubCommandError({
@@ -158,13 +155,42 @@ const githubApiAttempt = Effect.fn("gh.githubApiAttempt")(function* <T>(opts: Gi
158
155
  export const githubApi = <T>(
159
156
  opts: GitHubApiRequest,
160
157
  ): Effect.Effect<GitHubApiResponse<T>, GitHubApiError> =>
161
- retryTransient({
162
- attempt: () => githubApiAttempt<T>(opts),
163
- isTransient: (error) =>
164
- error._tag === "GitHubCommandError" &&
165
- error.retryable === true &&
166
- (opts.method ?? "GET") === "GET",
167
- maxRetries: MAX_API_RETRIES,
158
+ Effect.gen(function* () {
159
+ const snapshot = yield* Effect.try({
160
+ try: () => {
161
+ const path = opts.path;
162
+ const method = opts.method ?? "GET";
163
+ const alsoAcceptStatus = opts.alsoAcceptStatus;
164
+ const request = {
165
+ path,
166
+ method,
167
+ ...(alsoAcceptStatus === undefined ? {} : { alsoAcceptStatus: [...alsoAcceptStatus] }),
168
+ };
169
+ const body = opts.body;
170
+ if (body === undefined) return { request, serialized: undefined, decoded: "" };
171
+ const serialized = JSON.stringify(body);
172
+ if (serialized === undefined) throw new Error("Body is not JSON serializable");
173
+ return { request, serialized, decoded: decodedBodyText(JSON.parse(serialized)) };
174
+ },
175
+ catch: () =>
176
+ new GitHubCommandError({
177
+ command: "gh-tool API",
178
+ exitCode: 1,
179
+ stderr: "Refusing to publish an invalid request body",
180
+ message: "Refusing to publish an invalid request body",
181
+ }),
182
+ });
183
+ const { request } = snapshot;
184
+ yield* validateOutboundText(
185
+ `${request.path}\n${snapshot.serialized ?? ""}\n${snapshot.decoded}`,
186
+ `gh-tool ${request.method} ${request.path}`,
187
+ );
188
+ return yield* retryTransient({
189
+ attempt: () => githubApiAttempt<T>(request, snapshot.serialized),
190
+ isTransient: (error) =>
191
+ error._tag === "GitHubCommandError" && error.retryable === true && request.method === "GET",
192
+ maxRetries: MAX_API_RETRIES,
193
+ });
168
194
  });
169
195
 
170
196
  const safeJsonParse = (text: string): unknown => {
@@ -0,0 +1,50 @@
1
+ // Exact names only: GH children need their own auth/config, not unrelated credentials
2
+ // or executable-loader, editor, and pager settings from the parent process.
3
+ const GH_ENVIRONMENT_NAMES = [
4
+ "PATH",
5
+ "HOME",
6
+ "USERPROFILE",
7
+ "APPDATA",
8
+ "AppData",
9
+ "HOMEDRIVE",
10
+ "HOMEPATH",
11
+ "SYSTEMROOT",
12
+ "SystemRoot",
13
+ "WINDIR",
14
+ "TMPDIR",
15
+ "TMP",
16
+ "TEMP",
17
+ "LANG",
18
+ "LC_ALL",
19
+ "LC_CTYPE",
20
+ "TERM",
21
+ "XDG_CONFIG_HOME",
22
+ "DBUS_SESSION_BUS_ADDRESS",
23
+ "GH_CONFIG_DIR",
24
+ "GH_HOST",
25
+ "GH_REPO",
26
+ "GH_TOKEN",
27
+ "GITHUB_TOKEN",
28
+ "GH_ENTERPRISE_TOKEN",
29
+ "GITHUB_ENTERPRISE_TOKEN",
30
+ "HTTP_PROXY",
31
+ "http_proxy",
32
+ "HTTPS_PROXY",
33
+ "https_proxy",
34
+ "ALL_PROXY",
35
+ "all_proxy",
36
+ "NO_PROXY",
37
+ "no_proxy",
38
+ "SSL_CERT_FILE",
39
+ "SSL_CERT_DIR",
40
+ ] as const;
41
+
42
+ export const ghEnvironment = (
43
+ environment: Record<string, string | undefined> = process.env,
44
+ ): Record<string, string> =>
45
+ Object.fromEntries(
46
+ GH_ENVIRONMENT_NAMES.flatMap((name) => {
47
+ const value = environment[name];
48
+ return value === undefined ? [] : [[name, value]];
49
+ }),
50
+ );
@@ -5,6 +5,7 @@ import type { GitHubRepoConfig } from "#config";
5
5
  import type { RepoInfo } from "./types";
6
6
 
7
7
  import { GH_BINARY } from "./config";
8
+ import { ghEnvironment } from "./environment";
8
9
  import { GitHubAuthError, GitHubCommandError, GitHubNotFoundError } from "./errors";
9
10
  import { retryTransient } from "#shared/retry-transient";
10
11
  import { githubApi } from "./api";
@@ -165,7 +166,8 @@ export class GitHubService extends Context.Service<
165
166
  const command = ChildProcess.make(GH_BINARY, args, {
166
167
  stdout: "pipe",
167
168
  stderr: "pipe",
168
- ...(ghRepo ? { env: { GH_REPO: ghRepo }, extendEnv: true } : {}),
169
+ env: { ...ghEnvironment(), ...(ghRepo ? { GH_REPO: ghRepo } : {}) },
170
+ extendEnv: false,
169
171
  });
170
172
 
171
173
  const proc = yield* executor.spawn(command);
@@ -12,13 +12,19 @@ const SENSITIVE_PATH_PATTERNS = [
12
12
  /\.envrc$/,
13
13
  /\.(pem|key|p12|pfx|cer|crt)$/i,
14
14
  /(?:^|[\\/])(credentials?|passwd|shadow)$/i,
15
+ /(?:^|\/)(?:\.ssh|\.aws|\.kube|\.azure|secrets?|credentials?)(?:\/|$)/i,
16
+ /(?:^|\/)(?:\.npmrc|\.netrc|\.git-credentials|\.sentryclirc|\.pypirc|\.pgpass)$/i,
17
+ /(?:^|\/)\.docker\/config\.json$/i,
18
+ /(?:^|\/)\.config\/gh\/hosts\.yml$/i,
19
+ /(?:^|\/)GitHub CLI\/hosts\.yml$/i,
20
+ /(?:^|\/)(?:kubeconfig(?:[.-].+)?|[^/]+\.kubeconfig)$/i,
15
21
  ];
16
22
  const MissingMode = Schema.Literals(["error", "null", "default"]);
17
23
 
18
24
  const readTextFromStdin = () => Bun.stdin.text();
19
25
 
20
26
  export const isSensitivePath = (filePath: string) =>
21
- SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(filePath));
27
+ SENSITIVE_PATH_PATTERNS.some((pattern) => pattern.test(filePath.replaceAll("\\", "/")));
22
28
 
23
29
  export const validateOutboundText = (text: string, command: string) => {
24
30
  const reason = unsafeOutboundTextReason(text);