@blogic-cz/agent-tools 1.5.7 → 1.5.8
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 +9 -1
- package/dist/az-tool/service.d.ts.map +1 -1
- package/dist/azdo-tool/service.d.ts.map +1 -1
- package/dist/credential-guard/index.d.ts +2 -0
- package/dist/credential-guard/index.d.ts.map +1 -1
- package/dist/db-tool/service.d.ts.map +1 -1
- package/dist/gh-tool/gist.d.ts +13 -0
- package/dist/gh-tool/gist.d.ts.map +1 -1
- package/dist/gh-tool/pr/core.d.ts.map +1 -1
- package/dist/gh-tool/pr/review.d.ts.map +1 -1
- package/dist/gh-tool/release.d.ts +46 -1
- package/dist/gh-tool/release.d.ts.map +1 -1
- package/dist/gh-tool/text-input.d.ts +5 -3
- package/dist/gh-tool/text-input.d.ts.map +1 -1
- package/dist/k8s-tool/service.d.ts.map +1 -1
- package/dist/logs-tool/main.d.ts +1 -2
- package/dist/logs-tool/main.d.ts.map +1 -1
- package/dist/observability-tool/shared.d.ts.map +1 -1
- package/dist/session-tool/main.d.ts +1 -2
- package/dist/session-tool/main.d.ts.map +1 -1
- package/dist/shared/audit.d.ts.map +1 -1
- package/dist/shared/content-security.d.ts +6 -0
- package/dist/shared/content-security.d.ts.map +1 -0
- package/dist/shared/credential-patterns.d.ts +11 -0
- package/dist/shared/credential-patterns.d.ts.map +1 -0
- package/dist/shared/effect-version.d.ts.map +1 -1
- package/dist/shared/error-renderer.d.ts.map +1 -1
- package/dist/shared/format.d.ts.map +1 -1
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.d.ts.map +1 -1
- package/dist/shared/output-boundary.d.ts +3 -0
- package/dist/shared/output-boundary.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/audit-tool/main.ts +3 -1
- package/src/az-tool/main.ts +3 -1
- package/src/az-tool/service.ts +9 -0
- package/src/azdo-tool/main.ts +3 -1
- package/src/azdo-tool/service.ts +17 -0
- package/src/credential-guard/claude-hook.ts +2 -1
- package/src/credential-guard/index.ts +25 -56
- package/src/db-tool/main.ts +3 -1
- package/src/db-tool/service.ts +25 -2
- package/src/gh-tool/gist.ts +63 -64
- package/src/gh-tool/main.ts +3 -1
- package/src/gh-tool/pr/core.ts +10 -3
- package/src/gh-tool/pr/review.ts +199 -12
- package/src/gh-tool/release.ts +104 -29
- package/src/gh-tool/text-input.ts +19 -38
- package/src/k8s-tool/main.ts +3 -1
- package/src/k8s-tool/service.ts +25 -0
- package/src/logs-tool/main.ts +7 -2
- package/src/observability-tool/main.ts +5 -7
- package/src/observability-tool/shared.ts +41 -1
- package/src/session-tool/main.ts +18 -3
- package/src/shared/audit.ts +11 -10
- package/src/shared/content-security.ts +91 -0
- package/src/shared/credential-patterns.ts +62 -0
- package/src/shared/effect-version.ts +2 -1
- package/src/shared/error-renderer.ts +15 -2
- package/src/shared/format.ts +33 -6
- package/src/shared/index.ts +5 -0
- package/src/shared/output-boundary.ts +29 -0
|
@@ -15,6 +15,11 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import type { CliToolOverride, CredentialGuardConfig } from "#config/types";
|
|
18
|
+
import { redactSensitiveText } from "#shared/content-security";
|
|
19
|
+
import { SECRET_PATTERNS } from "#shared/credential-patterns";
|
|
20
|
+
|
|
21
|
+
export { findSecretMatches } from "#shared/credential-patterns";
|
|
22
|
+
export { redactSensitiveText } from "#shared/content-security";
|
|
18
23
|
|
|
19
24
|
// ============================================================================
|
|
20
25
|
// TYPES
|
|
@@ -84,61 +89,6 @@ const DEFAULT_ALLOWED_PATH_PATTERNS: RegExp[] = [
|
|
|
84
89
|
/\.env\.sample$/,
|
|
85
90
|
];
|
|
86
91
|
|
|
87
|
-
/** Patterns to detect secrets in content. */
|
|
88
|
-
const SECRET_PATTERNS = [
|
|
89
|
-
{
|
|
90
|
-
name: "AWS Access Key",
|
|
91
|
-
pattern: /(?:AKIA|ABIA|ACCA|ASIA)[A-Z0-9]{16}/,
|
|
92
|
-
},
|
|
93
|
-
{
|
|
94
|
-
name: "GitHub Token",
|
|
95
|
-
pattern: /gh[ps]_[A-Za-z0-9]{36}/,
|
|
96
|
-
},
|
|
97
|
-
{
|
|
98
|
-
name: "GitHub PAT",
|
|
99
|
-
pattern: /github_pat_[A-Za-z0-9]{22}_[A-Za-z0-9]{59}/,
|
|
100
|
-
},
|
|
101
|
-
{ name: "OpenAI Key", pattern: /sk-[A-Za-z0-9]{48}/ },
|
|
102
|
-
{
|
|
103
|
-
name: "Generic API Key",
|
|
104
|
-
pattern: /(?:api[_-]?key|apikey)["\s:=]+["']?([A-Za-z0-9_-]{20,})["']?/i,
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
name: "Generic Secret",
|
|
108
|
-
pattern:
|
|
109
|
-
/(?:secret|token|password|passwd|pwd)[" \t:=]+["']?(?!\$\{|process\.env|z\.|generate|create|read|get|fetch|import|export|const|function|return|Schema)[^\s"']{32,}["']?/i,
|
|
110
|
-
},
|
|
111
|
-
{
|
|
112
|
-
// eslint-disable-next-line eslint/no-useless-concat -- intentionally split to avoid credential guard self-detection
|
|
113
|
-
name: "Priv" + "ate Key",
|
|
114
|
-
pattern: new RegExp("-----BEGIN.*PRIVATE KEY-----"),
|
|
115
|
-
},
|
|
116
|
-
{
|
|
117
|
-
name: "JWT Token",
|
|
118
|
-
pattern: /(?:["'=:\s]|^)eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/,
|
|
119
|
-
},
|
|
120
|
-
{
|
|
121
|
-
name: "Azure SAS Token",
|
|
122
|
-
pattern: /[?&]sig=[A-Za-z0-9%+/=]{20,}/,
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
name: "GCP Service Account Key",
|
|
126
|
-
pattern: /"type"\s*:\s*"service_account"/,
|
|
127
|
-
},
|
|
128
|
-
{
|
|
129
|
-
name: "Slack Webhook URL",
|
|
130
|
-
pattern: /https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/,
|
|
131
|
-
},
|
|
132
|
-
{
|
|
133
|
-
name: "Discord Webhook URL",
|
|
134
|
-
pattern: /https:\/\/discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/,
|
|
135
|
-
},
|
|
136
|
-
{
|
|
137
|
-
name: "Database URL",
|
|
138
|
-
pattern: /(?:postgres(?:ql)?|mysql|mongodb):\/\/(?!\$\{)[^:]+:(?!\$\{)[^@]+@/,
|
|
139
|
-
},
|
|
140
|
-
];
|
|
141
|
-
|
|
142
92
|
/**
|
|
143
93
|
* CLI tools that must use wrapper tools for security and audit.
|
|
144
94
|
*/
|
|
@@ -1425,6 +1375,25 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
1425
1375
|
}
|
|
1426
1376
|
|
|
1427
1377
|
function handleToolExecuteBefore(input: HookInput, output: HookOutput): void {
|
|
1378
|
+
try {
|
|
1379
|
+
enforceToolExecuteBefore(input, output);
|
|
1380
|
+
} catch (error) {
|
|
1381
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1382
|
+
const sanitized = redactSensitiveText(message);
|
|
1383
|
+
if (error instanceof Error) {
|
|
1384
|
+
if (sanitized !== message) {
|
|
1385
|
+
error.message = sanitized;
|
|
1386
|
+
if (error.stack) error.stack = error.stack.replace(message, sanitized);
|
|
1387
|
+
}
|
|
1388
|
+
throw error;
|
|
1389
|
+
}
|
|
1390
|
+
// Do not preserve an unsanitized non-Error value as the new error's cause.
|
|
1391
|
+
// eslint-disable-next-line eslint/preserve-caught-error
|
|
1392
|
+
throw new Error(sanitized);
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
function enforceToolExecuteBefore(input: HookInput, output: HookOutput): void {
|
|
1428
1397
|
// Normalize tool name across platforms:
|
|
1429
1398
|
// - AI coding agents pass capitalized: "Bash", "Read", "Write", "Edit"
|
|
1430
1399
|
// - OpenCode MCP tools pass prefixed: "mcp_bash", "mcp_read", "mcp_write", "mcp_edit"
|
|
@@ -1453,7 +1422,7 @@ export function createCredentialGuard(config?: CredentialGuardConfig): Credentia
|
|
|
1453
1422
|
if (detected) {
|
|
1454
1423
|
throw new Error(
|
|
1455
1424
|
`\u{1F6AB} Secret detected: Potential ${detected.name} found in content.\n\n` +
|
|
1456
|
-
`Matched:
|
|
1425
|
+
`Matched: [REDACTED]\n\n` +
|
|
1457
1426
|
`Never commit secrets to code. Use environment variables or secret managers.\n\n` +
|
|
1458
1427
|
`Think this is a false positive? Review the project repository, fix the pattern, and submit a PR.`,
|
|
1459
1428
|
);
|
package/src/db-tool/main.ts
CHANGED
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
VERSION,
|
|
15
15
|
} from "#shared";
|
|
16
16
|
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
17
|
+
import { withRedactedOutput } from "#shared/output-boundary";
|
|
17
18
|
import { ConfigService, ConfigServiceLayer, getDefaultEnvironment } from "#config";
|
|
18
19
|
import { DbConfigService, makeDbConfigLayer } from "./config-service";
|
|
19
20
|
import { DbConnectionError } from "./errors";
|
|
@@ -238,6 +239,7 @@ const mainCommand = Command.make("db-tool", {}).pipe(
|
|
|
238
239
|
|
|
239
240
|
const cli = Command.run(mainCommand, {
|
|
240
241
|
version: VERSION,
|
|
242
|
+
renderErrors: false,
|
|
241
243
|
});
|
|
242
244
|
|
|
243
245
|
const dbConfigLayer = makeDbConfigLayer(profileArg);
|
|
@@ -252,7 +254,7 @@ const MainLayer = DbService.layer.pipe(
|
|
|
252
254
|
Layer.provideMerge(AuditServiceLayer),
|
|
253
255
|
);
|
|
254
256
|
|
|
255
|
-
const program = withAudit("db", cli).pipe(
|
|
257
|
+
const program = withRedactedOutput(withAudit("db", cli)).pipe(
|
|
256
258
|
Effect.provide(MainLayer),
|
|
257
259
|
Effect.tapCause(renderCauseToStderr),
|
|
258
260
|
);
|
package/src/db-tool/service.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { DbConfig, DbMutationOperation, QueryResult, SchemaMode } from "./t
|
|
|
6
6
|
import { ConfigService } from "#config";
|
|
7
7
|
import { isPrerequisiteRunError } from "#shared/prerequisites/errors";
|
|
8
8
|
import { resolveEnvTemplate } from "#shared/env-template";
|
|
9
|
+
import { unsafeOutboundTextReason } from "#shared/content-security";
|
|
9
10
|
import { resolveEnvironmentScopedPrerequisites } from "#shared/prerequisites/config";
|
|
10
11
|
import { runWithProfilePrerequisites } from "#shared/prerequisites/runtime";
|
|
11
12
|
import { buildApiProbeArgs } from "#shared/k8s-probe";
|
|
@@ -501,8 +502,18 @@ export class DbService extends Context.Service<
|
|
|
501
502
|
readOnly: isFullyReadOnly(config),
|
|
502
503
|
});
|
|
503
504
|
|
|
504
|
-
const
|
|
505
|
-
|
|
505
|
+
const unsafeSqlError = (reason: string) =>
|
|
506
|
+
new DbQueryError({
|
|
507
|
+
message: `Refusing to send SQL containing ${reason}.`,
|
|
508
|
+
sql: "[redacted]",
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const runSql = (config: DbConfig, password: string, sql: string) => {
|
|
512
|
+
const unsafeReason = unsafeOutboundTextReason(sql);
|
|
513
|
+
return unsafeReason === null
|
|
514
|
+
? sqlClient.run(toConnection(config, password), sql)
|
|
515
|
+
: Effect.fail(unsafeSqlError(unsafeReason));
|
|
516
|
+
};
|
|
506
517
|
|
|
507
518
|
const fetchSingleColumn = (config: DbConfig, password: string, sql: string) =>
|
|
508
519
|
runSql(config, password, sql).pipe(
|
|
@@ -772,6 +783,11 @@ export class DbService extends Context.Service<
|
|
|
772
783
|
sql: string,
|
|
773
784
|
limit?: number,
|
|
774
785
|
) {
|
|
786
|
+
const unsafeReason = unsafeOutboundTextReason(sql);
|
|
787
|
+
if (unsafeReason !== null) {
|
|
788
|
+
return yield* unsafeSqlError(unsafeReason);
|
|
789
|
+
}
|
|
790
|
+
|
|
775
791
|
const config = yield* getConfigForEnv(env);
|
|
776
792
|
const startTimeMs = yield* Clock.currentTimeMillis;
|
|
777
793
|
const resolvedConfig = yield* resolveDbConfig(config, env);
|
|
@@ -827,6 +843,13 @@ export class DbService extends Context.Service<
|
|
|
827
843
|
mode: SchemaMode,
|
|
828
844
|
table?: string,
|
|
829
845
|
) {
|
|
846
|
+
if (table !== undefined) {
|
|
847
|
+
const unsafeReason = unsafeOutboundTextReason(table);
|
|
848
|
+
if (unsafeReason !== null) {
|
|
849
|
+
return yield* unsafeSqlError(unsafeReason);
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
830
853
|
const config = yield* getConfigForEnv(env);
|
|
831
854
|
const startTimeMs = yield* Clock.currentTimeMillis;
|
|
832
855
|
const resolvedConfig = yield* resolveDbConfig(config, env);
|
package/src/gh-tool/gist.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { Command, Flag } from "effect/unstable/cli";
|
|
2
2
|
import { Effect, Option } from "effect";
|
|
3
|
+
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
3
4
|
import { join } from "node:path";
|
|
5
|
+
import { tmpdir } from "node:os";
|
|
4
6
|
|
|
5
7
|
import { formatOption, logFormatted } from "#shared";
|
|
6
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
isSensitivePath,
|
|
10
|
+
readValidatedOutboundFile,
|
|
11
|
+
resolveOptionalTextInput,
|
|
12
|
+
} from "#gh/text-input";
|
|
7
13
|
import { GitHubCommandError } from "./errors";
|
|
8
14
|
import { GitHubService } from "./service";
|
|
9
15
|
|
|
@@ -118,29 +124,13 @@ const validateFilePath = (path: string, command: string) =>
|
|
|
118
124
|
// `gh gist create/edit` read content from files only; inline --body is staged in a temp file
|
|
119
125
|
// whose basename becomes the gist filename.
|
|
120
126
|
const stageBody = Effect.fn("gist.stageBody")(function* (opts: {
|
|
121
|
-
body: string;
|
|
127
|
+
body: string | Uint8Array;
|
|
122
128
|
filename: string;
|
|
123
129
|
command: string;
|
|
124
130
|
}) {
|
|
125
131
|
const directory = yield* Effect.acquireRelease(
|
|
126
132
|
Effect.tryPromise({
|
|
127
|
-
try:
|
|
128
|
-
const proc = Bun.spawn(
|
|
129
|
-
["mktemp", "-d", join(process.env.TMPDIR ?? "/tmp", "gh-tool-gist-XXXXXX")],
|
|
130
|
-
{ stdout: "pipe", stderr: "pipe" },
|
|
131
|
-
);
|
|
132
|
-
const [exitCode, stdout, stderr] = await Promise.all([
|
|
133
|
-
proc.exited,
|
|
134
|
-
new Response(proc.stdout).text(),
|
|
135
|
-
new Response(proc.stderr).text(),
|
|
136
|
-
]);
|
|
137
|
-
|
|
138
|
-
if (exitCode !== 0) {
|
|
139
|
-
throw new Error(stderr.trim() || `mktemp exited with code ${exitCode}`);
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
return stdout.trim();
|
|
143
|
-
},
|
|
133
|
+
try: () => mkdtemp(join(process.env.TMPDIR ?? tmpdir(), "gh-tool-gist-")),
|
|
144
134
|
catch: (error) =>
|
|
145
135
|
inputError(
|
|
146
136
|
`Failed to stage gist content: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -149,20 +139,14 @@ const stageBody = Effect.fn("gist.stageBody")(function* (opts: {
|
|
|
149
139
|
}),
|
|
150
140
|
(tempDirectory) =>
|
|
151
141
|
Effect.tryPromise({
|
|
152
|
-
try:
|
|
153
|
-
const proc = Bun.spawn(["rm", "-rf", tempDirectory], {
|
|
154
|
-
stdout: "ignore",
|
|
155
|
-
stderr: "ignore",
|
|
156
|
-
});
|
|
157
|
-
await proc.exited;
|
|
158
|
-
},
|
|
142
|
+
try: () => rm(tempDirectory, { recursive: true, force: true }),
|
|
159
143
|
catch: () => undefined,
|
|
160
144
|
}).pipe(Effect.ignore),
|
|
161
145
|
);
|
|
162
146
|
const path = join(directory, opts.filename);
|
|
163
147
|
|
|
164
148
|
yield* Effect.tryPromise({
|
|
165
|
-
try: () =>
|
|
149
|
+
try: () => writeFile(path, opts.body),
|
|
166
150
|
catch: (error) =>
|
|
167
151
|
inputError(
|
|
168
152
|
`Failed to stage gist content: ${error instanceof Error ? error.message : String(error)}`,
|
|
@@ -293,45 +277,55 @@ export const createGist = Effect.fn("gist.createGist")(function* (opts: {
|
|
|
293
277
|
public: boolean;
|
|
294
278
|
}) {
|
|
295
279
|
const gh = yield* GitHubService;
|
|
280
|
+
return yield* Effect.scoped(
|
|
281
|
+
Effect.gen(function* () {
|
|
282
|
+
const snapshots: string[] = [];
|
|
283
|
+
for (const path of opts.paths) {
|
|
284
|
+
const validated = yield* readValidatedOutboundFile(path, "gh-tool gist create");
|
|
285
|
+
const filename = path.split(/[\\/]/).at(-1) ?? path;
|
|
286
|
+
snapshots.push(
|
|
287
|
+
yield* stageBody({ body: validated.bytes, filename, command: "gh-tool gist create" }),
|
|
288
|
+
);
|
|
289
|
+
}
|
|
296
290
|
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const args = ["gist", "create", ...opts.paths];
|
|
291
|
+
const args = ["gist", "create", ...snapshots];
|
|
300
292
|
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
293
|
+
if (opts.description !== null) {
|
|
294
|
+
args.push("--desc", opts.description);
|
|
295
|
+
}
|
|
304
296
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
297
|
+
if (opts.public) {
|
|
298
|
+
args.push("--public");
|
|
299
|
+
}
|
|
308
300
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
301
|
+
const result = yield* gh.runGh(args);
|
|
302
|
+
const url =
|
|
303
|
+
result.stdout
|
|
304
|
+
.trim()
|
|
305
|
+
.split("\n")
|
|
306
|
+
.map((line) => line.trim())
|
|
307
|
+
.findLast((line) => line.startsWith("https://")) ?? "";
|
|
308
|
+
|
|
309
|
+
if (url === "") {
|
|
310
|
+
return yield* Effect.fail(
|
|
311
|
+
inputError("gh gist create did not return a gist URL", "gh-tool gist create"),
|
|
312
|
+
);
|
|
313
|
+
}
|
|
322
314
|
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
315
|
+
const created: GistCreateResult = {
|
|
316
|
+
created: true,
|
|
317
|
+
id: url.split("/").at(-1) ?? "",
|
|
318
|
+
url,
|
|
319
|
+
public: opts.public,
|
|
320
|
+
files: opts.paths.map((path) => path.split(/[\\/]/).at(-1) ?? path),
|
|
321
|
+
};
|
|
330
322
|
|
|
331
|
-
|
|
323
|
+
return created;
|
|
324
|
+
}),
|
|
325
|
+
);
|
|
332
326
|
});
|
|
333
327
|
|
|
334
|
-
const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
328
|
+
export const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
335
329
|
id: string;
|
|
336
330
|
description: string | null;
|
|
337
331
|
add: string | null;
|
|
@@ -341,15 +335,13 @@ const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
|
341
335
|
}) {
|
|
342
336
|
const gh = yield* GitHubService;
|
|
343
337
|
|
|
344
|
-
for (const path of [opts.sourcePath, opts.add]) {
|
|
345
|
-
if (path !== null) yield* validateOutboundFile(path, "gh-tool gist edit");
|
|
346
|
-
}
|
|
347
|
-
|
|
348
338
|
const args = ["gist", "edit", opts.id];
|
|
349
339
|
const changes: string[] = [];
|
|
350
340
|
|
|
351
341
|
if (opts.sourcePath !== null) {
|
|
352
|
-
|
|
342
|
+
const validated = yield* readValidatedOutboundFile(opts.sourcePath, "gh-tool gist edit");
|
|
343
|
+
const filename = opts.filename ?? opts.sourcePath.split(/[\\/]/).at(-1) ?? "";
|
|
344
|
+
args.push(yield* stageBody({ body: validated.bytes, filename, command: "gh-tool gist edit" }));
|
|
353
345
|
changes.push(`content:${opts.filename ?? opts.sourcePath.split("/").at(-1)}`);
|
|
354
346
|
}
|
|
355
347
|
|
|
@@ -359,7 +351,14 @@ const editGist = Effect.fn("gist.editGist")(function* (opts: {
|
|
|
359
351
|
}
|
|
360
352
|
|
|
361
353
|
if (opts.add !== null) {
|
|
362
|
-
|
|
354
|
+
const validated = yield* readValidatedOutboundFile(opts.add, "gh-tool gist edit");
|
|
355
|
+
const filename = opts.add.split(/[\\/]/).at(-1) ?? opts.add;
|
|
356
|
+
const snapshot = yield* stageBody({
|
|
357
|
+
body: validated.bytes,
|
|
358
|
+
filename,
|
|
359
|
+
command: "gh-tool gist edit",
|
|
360
|
+
});
|
|
361
|
+
args.push("--add", snapshot);
|
|
363
362
|
changes.push(`add:${opts.add.split("/").at(-1)}`);
|
|
364
363
|
}
|
|
365
364
|
|
package/src/gh-tool/main.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { Effect, Layer } from "effect";
|
|
|
5
5
|
|
|
6
6
|
import { makeSchemaCommand, renderCauseToStderr, VERSION } from "#shared";
|
|
7
7
|
import { AuditServiceLayer, withAudit } from "#shared/audit";
|
|
8
|
+
import { withRedactedOutput } from "#shared/output-boundary";
|
|
8
9
|
import { ConfigServiceLayer } from "#config";
|
|
9
10
|
import {
|
|
10
11
|
issueCloseCommand,
|
|
@@ -240,6 +241,7 @@ WORKFLOW FOR AI AGENTS:
|
|
|
240
241
|
|
|
241
242
|
const cli = Command.run(mainCommand, {
|
|
242
243
|
version: VERSION,
|
|
244
|
+
renderErrors: false,
|
|
243
245
|
});
|
|
244
246
|
|
|
245
247
|
const MainLayer = GitHubService.layer.pipe(
|
|
@@ -248,7 +250,7 @@ const MainLayer = GitHubService.layer.pipe(
|
|
|
248
250
|
Layer.provideMerge(ConfigServiceLayer),
|
|
249
251
|
);
|
|
250
252
|
|
|
251
|
-
const program = withAudit("gh", cli).pipe(
|
|
253
|
+
const program = withRedactedOutput(withAudit("gh", cli)).pipe(
|
|
252
254
|
Effect.provide(MainLayer),
|
|
253
255
|
Effect.tapCause(renderCauseToStderr),
|
|
254
256
|
);
|
package/src/gh-tool/pr/core.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { GitHubCommandError, GitHubMergeError } from "#gh/errors";
|
|
|
21
21
|
import { GitHubService } from "#gh/service";
|
|
22
22
|
import type { GhResult } from "#gh/service";
|
|
23
23
|
import { logText } from "#shared";
|
|
24
|
+
import { redactSensitiveText } from "#shared/content-security";
|
|
24
25
|
|
|
25
26
|
import type { ButStatusJson, PRViewJsonResult } from "./helpers";
|
|
26
27
|
import { pollUntilResolved } from "#shared/poll-until-resolved";
|
|
@@ -114,14 +115,18 @@ const resolveMergeSha = Effect.fn("pr.resolveMergeSha")(function* (
|
|
|
114
115
|
Effect.map((sha) => ({ sha, failed: false as const })),
|
|
115
116
|
Effect.catch((error) =>
|
|
116
117
|
Console.error(
|
|
117
|
-
|
|
118
|
+
redactSensitiveText(
|
|
119
|
+
`Warning: PR #${pr} was merged, but its merge commit SHA could not be read: ${error.message}`,
|
|
120
|
+
),
|
|
118
121
|
).pipe(Effect.as({ sha: null, failed: true as const })),
|
|
119
122
|
),
|
|
120
123
|
);
|
|
121
124
|
|
|
122
125
|
if (result.sha === null && !result.failed) {
|
|
123
126
|
yield* Console.error(
|
|
124
|
-
|
|
127
|
+
redactSensitiveText(
|
|
128
|
+
`Warning: PR #${pr} was merged, but its merge commit SHA is still unknown after ${MERGE_SHA_WAIT_SECONDS}s.`,
|
|
129
|
+
),
|
|
125
130
|
);
|
|
126
131
|
}
|
|
127
132
|
|
|
@@ -1088,7 +1093,9 @@ export const mergePR = Effect.fn("pr.mergePR")(function* (opts: {
|
|
|
1088
1093
|
if (error._tag !== "GitHubCommandError") {
|
|
1089
1094
|
return rollbackNote === ""
|
|
1090
1095
|
? Effect.fail(error)
|
|
1091
|
-
: Console.error(rollbackNote.trim()).pipe(
|
|
1096
|
+
: Console.error(redactSensitiveText(rollbackNote.trim())).pipe(
|
|
1097
|
+
Effect.andThen(Effect.fail(error)),
|
|
1098
|
+
);
|
|
1092
1099
|
}
|
|
1093
1100
|
|
|
1094
1101
|
const stderr = error.stderr.toLowerCase();
|