@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.
- package/README.md +18 -7
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/gh-tool/api.d.ts.map +1 -1
- package/dist/gh-tool/environment.d.ts +2 -0
- package/dist/gh-tool/environment.d.ts.map +1 -0
- package/dist/gh-tool/service.d.ts.map +1 -1
- package/dist/gh-tool/text-input.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/credential-guard/index.ts +903 -36
- package/src/gh-tool/api.ts +50 -24
- package/src/gh-tool/environment.ts +50 -0
- package/src/gh-tool/service.ts +3 -1
- package/src/gh-tool/text-input.ts +7 -1
package/src/gh-tool/api.ts
CHANGED
|
@@ -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
|
-
|
|
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(
|
|
75
|
+
if (Array.isArray(value)) return value.map(decodedBodyText).join("\n");
|
|
73
76
|
if (value !== null && typeof value === "object")
|
|
74
|
-
return Object.
|
|
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>(
|
|
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
|
-
...(
|
|
99
|
+
...(serializedBody === undefined ? {} : { "Content-Type": "application/json" }),
|
|
103
100
|
},
|
|
104
|
-
...(
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
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
|
+
);
|
package/src/gh-tool/service.ts
CHANGED
|
@@ -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 ? {
|
|
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);
|