@estebanforge/pi-antigravity-bridge 1.7.0 → 1.7.1
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/CHANGELOG.md +6 -0
- package/package.json +1 -1
- package/src/acp/connection.ts +12 -7
- package/src/ask-tool.ts +5 -2
- package/src/daily-log.ts +17 -4
- package/src/driver.ts +4 -1
- package/src/redact.ts +27 -0
- package/src/web-tools.ts +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [1.7.1] - 2026-09-23
|
|
6
|
+
|
|
7
|
+
### Fixed
|
|
8
|
+
|
|
9
|
+
- **Subprocess stderr can no longer leak secrets into logs or the chat.** The `agy` CLI can print auth material (API keys, OAuth tokens, credential headers) to stderr, and six surfaces carried that text verbatim: the daily support log users attach to bug reports, the stream-json turn-failure error, the ACP connection-exit rejection reasons and `/agy doctor` tail, the web tools' failure note, and the AskAntigravity delegation exit note. A key in an auth error therefore landed on disk and in the transcript unchanged. Redaction now runs on the reassembled stderr tail at exit and getter boundaries - never per chunk - so a token split across two pipe events still matches, and the raw tail stays internal. New `redactText()` scrubs four secret-shaped pattern classes (API-key prefixes, OAuth tokens, credential headers, token-ish JSON fields), anchored with a word boundary so ordinary text like `desk-123456789012` survives, and carries no length cap because callers already bound their own output sizes. The daily log scrubber additionally scans string values, not just secret-shaped keys, and routes `Error.message` through the same pass; values are scanned before truncation so a cut token cannot lose the characters a pattern needs. Best-effort by design: a last line against leak paths, not a sandbox. Ten new tests pin the behavior, two of them spawning real processes (a secret-bearing crash and a token split across stderr chunks). 1200 tests, tsc clean.
|
|
10
|
+
|
|
5
11
|
## [1.7.0] - 2026-09-23
|
|
6
12
|
|
|
7
13
|
### Added
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@estebanforge/pi-antigravity-bridge",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.1",
|
|
4
4
|
"description": "Gemini provider for Pi on the Antigravity ACP server (official Google ACP) or the stream-json agy CLI. antigravity/* models in Pi's /model picker, no-patch MCP bridge: agy runs Pi's tools. ToS safe to use.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
package/src/acp/connection.ts
CHANGED
|
@@ -26,6 +26,7 @@ import path from "node:path";
|
|
|
26
26
|
import { parseAuthPort, readLastUrl } from "./browser-capture.js";
|
|
27
27
|
import { frameCarriesUsage } from "./events.js";
|
|
28
28
|
import type { AgyUsage } from "../driver-types.js";
|
|
29
|
+
import { redactText } from "../redact.js";
|
|
29
30
|
import { JsonRpcResponseError, JsonRpcSession } from "./jsonrpc.js";
|
|
30
31
|
|
|
31
32
|
export interface AcpMcpServer {
|
|
@@ -110,7 +111,9 @@ export class AcpConnection {
|
|
|
110
111
|
}
|
|
111
112
|
|
|
112
113
|
get stderrTail(): string {
|
|
113
|
-
|
|
114
|
+
// Redacted at the boundary: the raw tail stays internal, every consumer
|
|
115
|
+
// (doctor display, driver exit logs, failure messages) sees redacted text.
|
|
116
|
+
return redactText(this.#stderrTail);
|
|
114
117
|
}
|
|
115
118
|
|
|
116
119
|
/** Gate B watch: latched true once any session/update frame carried
|
|
@@ -152,8 +155,8 @@ export class AcpConnection {
|
|
|
152
155
|
// try/catch in #write cannot see them. Without this listener an EPIPE
|
|
153
156
|
// (server died mid-handshake) is uncaught and kills pi.
|
|
154
157
|
child.stdin?.on("error", (err) => {
|
|
155
|
-
this.#opts.log("stdin-error", { message: err.message });
|
|
156
|
-
if (!this.#exited && !this.#killed) this.#finish(err.message);
|
|
158
|
+
this.#opts.log("stdin-error", { message: redactText(err.message) });
|
|
159
|
+
if (!this.#exited && !this.#killed) this.#finish(redactText(err.message));
|
|
157
160
|
});
|
|
158
161
|
|
|
159
162
|
const rpc = new JsonRpcSession({
|
|
@@ -169,12 +172,14 @@ export class AcpConnection {
|
|
|
169
172
|
this.#stderrTail = (this.#stderrTail + chunk).slice(-8192);
|
|
170
173
|
});
|
|
171
174
|
child.on("error", (err) => {
|
|
172
|
-
this.#opts.log("spawn-error", { message: err.message });
|
|
173
|
-
this.#finish(err.message);
|
|
175
|
+
this.#opts.log("spawn-error", { message: redactText(err.message) });
|
|
176
|
+
this.#finish(redactText(err.message));
|
|
174
177
|
});
|
|
175
178
|
child.on("exit", (code, signal) => {
|
|
176
179
|
this.#opts.log("exit", { code: code ?? signal ?? "?" });
|
|
177
|
-
|
|
180
|
+
// The reason rides into pending-request rejections, so it must be
|
|
181
|
+
// redacted here (server stderr can carry auth material).
|
|
182
|
+
this.#finish(redactText(this.#stderrTail.trim()));
|
|
178
183
|
});
|
|
179
184
|
|
|
180
185
|
const result = (await this.request(
|
|
@@ -444,7 +449,7 @@ export class AcpConnection {
|
|
|
444
449
|
this.#authUrlWatcher?.close();
|
|
445
450
|
this.#authUrlWatcher = undefined;
|
|
446
451
|
this.abortAll(`connection exited: ${reason || "process gone"}`);
|
|
447
|
-
this.#opts.onExit({ code: null, signal: null, stderrTail: this.#stderrTail });
|
|
452
|
+
this.#opts.onExit({ code: null, signal: null, stderrTail: redactText(this.#stderrTail) });
|
|
448
453
|
}
|
|
449
454
|
}
|
|
450
455
|
|
package/src/ask-tool.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
snapshotConversations,
|
|
26
26
|
} from "./discovery.js";
|
|
27
27
|
import { loadConfig, type AgyMode, type ThinkingTier } from "./config.js";
|
|
28
|
+
import { redactText } from "./redact.js";
|
|
28
29
|
import { acquireBridgeSuppression } from "./mcp-registration.js";
|
|
29
30
|
import { AGY_EFFORT_ORDER, spawnAgyModelsRaw, toAgyEffort } from "./models.js";
|
|
30
31
|
|
|
@@ -742,8 +743,10 @@ export async function registerAskAntigravityTool(
|
|
|
742
743
|
}
|
|
743
744
|
|
|
744
745
|
if (outcome.exitCode !== 0) {
|
|
745
|
-
|
|
746
|
-
|
|
746
|
+
// stderr can carry auth material; the note is chat-visible.
|
|
747
|
+
const stderr = redactText(details.stderr.trim());
|
|
748
|
+
const note = stderr
|
|
749
|
+
? `agy exited with status ${outcome.exitCode}: ${stderr}`
|
|
747
750
|
: `agy exited with status ${outcome.exitCode}`;
|
|
748
751
|
return {
|
|
749
752
|
content: [{ type: "text", text: text ? `${text}\n\n[${note}]` : note }],
|
package/src/daily-log.ts
CHANGED
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
// - One appendFile per record: O_APPEND keeps single-line writes atomic,
|
|
13
13
|
// so two pi tabs sharing the dir stay line-consistent.
|
|
14
14
|
// - No secrets, no prompt content: values of secret-shaped keys are
|
|
15
|
-
// redacted
|
|
15
|
+
// redacted wholesale, secret-shaped material inside string values
|
|
16
|
+
// (stderr tails, error messages) is scrubbed, and long strings are
|
|
17
|
+
// truncated before they reach disk.
|
|
16
18
|
// - Retention: files older than `retentionDays` are pruned once per
|
|
17
19
|
// process, so the dir cannot grow unbounded.
|
|
18
20
|
// - Volume tiers: default installs write ONLY errors — routine logging
|
|
@@ -22,6 +24,7 @@
|
|
|
22
24
|
|
|
23
25
|
import { appendFile, mkdir, readdir, unlink } from "node:fs/promises";
|
|
24
26
|
import path from "node:path";
|
|
27
|
+
import { redactText } from "./redact.js";
|
|
25
28
|
|
|
26
29
|
export type LogLevel = "debug" | "info" | "warn" | "error";
|
|
27
30
|
|
|
@@ -63,14 +66,24 @@ const MAX_RECORD = 4000;
|
|
|
63
66
|
// lives in `value`, which a name-only regex would miss.
|
|
64
67
|
const SECRET_KEY = /token|secret|password|passphrase|authorization|api[-_]?key|cookie|headers/i;
|
|
65
68
|
|
|
66
|
-
/** Redact secret-shaped
|
|
69
|
+
/** Redact secret-shaped keys wholesale, scrub secrets embedded in string
|
|
70
|
+
* values, and cap runaway strings before disk. */
|
|
67
71
|
function scrub(value: unknown, depth: number): unknown {
|
|
68
72
|
if (value === null || value === undefined) return value;
|
|
69
73
|
if (value instanceof Error) {
|
|
70
|
-
|
|
74
|
+
// message goes through the string branch: errors are the main carrier of
|
|
75
|
+
// subprocess stderr text (the exact leak class this scrubber exists for).
|
|
76
|
+
return {
|
|
77
|
+
name: value.name,
|
|
78
|
+
message: scrub(value.message, depth),
|
|
79
|
+
stack: scrub(value.stack, depth),
|
|
80
|
+
};
|
|
71
81
|
}
|
|
72
82
|
if (typeof value === "string") {
|
|
73
|
-
|
|
83
|
+
// Value scan first, truncate second: a cut token could lose the tail
|
|
84
|
+
// characters the pattern needs to match.
|
|
85
|
+
const clean = redactText(value);
|
|
86
|
+
return clean.length > MAX_STRING ? clean.slice(0, MAX_STRING) + "…(truncated)" : clean;
|
|
74
87
|
}
|
|
75
88
|
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
76
89
|
if (depth >= MAX_DEPTH) return "(depth limit)";
|
package/src/driver.ts
CHANGED
|
@@ -19,6 +19,7 @@ import { randomUUID } from "node:crypto";
|
|
|
19
19
|
import { parseAgyLine } from "./stream-events.js";
|
|
20
20
|
import { bridgeMcpConfigDir, bridgeMcpConfigExists } from "./mcp-server.js";
|
|
21
21
|
import { gateHooksStaged } from "./approval-hook.js";
|
|
22
|
+
import { redactText } from "./redact.js";
|
|
22
23
|
import type {
|
|
23
24
|
AgyUsage,
|
|
24
25
|
DriverActivity,
|
|
@@ -403,7 +404,9 @@ export class StreamDriver implements TurnDriver {
|
|
|
403
404
|
} else {
|
|
404
405
|
this.#failTurn(
|
|
405
406
|
turn,
|
|
406
|
-
|
|
407
|
+
// agy stderr can carry auth material; the tail becomes the
|
|
408
|
+
// user-facing error and a daily-log value.
|
|
409
|
+
redactText(this.#stderrTail.trim()) || `agy exited with status ${code ?? "signal"}`,
|
|
407
410
|
);
|
|
408
411
|
}
|
|
409
412
|
}
|
package/src/redact.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
// Secret redaction for free text: stderr tails, error messages, log string
|
|
2
|
+
// values. Patterns ported from pi-antigravity-acp-provider's errors.redact
|
|
3
|
+
// (MIT). No length cap here on purpose: callers already bound their own
|
|
4
|
+
// output sizes (8192 stderr tails, MAX_STRING truncation), and a cap inside
|
|
5
|
+
// this function would silently shorten their output.
|
|
6
|
+
|
|
7
|
+
const SECRET_PATTERNS: Array<[RegExp, string]> = [
|
|
8
|
+
// API-key prefixes: Google (AIza), OpenAI-style (sk-), npm, GitHub. The
|
|
9
|
+
// left word-boundary stops mid-word matches ("desk-123456789012").
|
|
10
|
+
[/\b(?:AIza|sk-|npm_|gh[opurs]_)[A-Za-z0-9_-]{12,}/gu, "<redacted>"],
|
|
11
|
+
// Google OAuth tokens (access ya29., refresh 1//).
|
|
12
|
+
[/\b(?:ya29\.|1\/\/)[A-Za-z0-9._~+/-]{12,}/gu, "<redacted>"],
|
|
13
|
+
// Credential header values; the key text stays (useful context).
|
|
14
|
+
[/((?:authorization|cookie|set-cookie)\s*[:=]\s*(?:bearer\s+)?)[^\s,;]+/giu, "$1<redacted>"],
|
|
15
|
+
// Token-ish JSON fields; the key stays.
|
|
16
|
+
[
|
|
17
|
+
/((?:["']?(?:access_token|refresh_token|id_token|client_secret|api[-_]?key)["']?)\s*[:=]\s*["']?)[^"'\s,;&}]+/giu,
|
|
18
|
+
"$1<redacted>",
|
|
19
|
+
],
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
/** Replace secret-shaped material with `<redacted>`, preserving structure. */
|
|
23
|
+
export function redactText(text: string): string {
|
|
24
|
+
let out = text;
|
|
25
|
+
for (const [pattern, replacement] of SECRET_PATTERNS) out = out.replace(pattern, replacement);
|
|
26
|
+
return out;
|
|
27
|
+
}
|
package/src/web-tools.ts
CHANGED
|
@@ -21,6 +21,7 @@ import { mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync }
|
|
|
21
21
|
import os from "node:os";
|
|
22
22
|
import path from "node:path";
|
|
23
23
|
import { Type } from "typebox";
|
|
24
|
+
import { redactText } from "./redact.js";
|
|
24
25
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
25
26
|
|
|
26
27
|
export const WEB_AGENT_PREFIX = "pi-bridge-web-";
|
|
@@ -119,7 +120,7 @@ export async function runWebAgent(opts: WebRunOptions): Promise<WebRunResult> {
|
|
|
119
120
|
const agentName = `${WEB_AGENT_PREFIX}${process.pid}-${Date.now().toString(36)}-${randomBytes(4).toString("hex")}`;
|
|
120
121
|
const agentDir = path.join(root, agentName);
|
|
121
122
|
mkdirSync(agentDir, { recursive: true, mode: 0o700 });
|
|
122
|
-
// commandExecutionPolicy stays at the
|
|
123
|
+
// commandExecutionPolicy stays at the live-validated "auto": the template's
|
|
123
124
|
// tools list has no command tool and plan mode blocks edits; an unvalidated
|
|
124
125
|
// stricter enum risks agent discovery failing outright.
|
|
125
126
|
writeFileSync(
|
|
@@ -178,7 +179,7 @@ export async function runWebAgent(opts: WebRunOptions): Promise<WebRunResult> {
|
|
|
178
179
|
child.once("close", resolve);
|
|
179
180
|
});
|
|
180
181
|
if (!failed && opts.signal?.aborted) failed = "web run aborted";
|
|
181
|
-
if (!failed && code !== 0) failed = `agy exited with code ${code}${stderrTail ? `: ${stderrTail.trim().slice(-300)}` : ""}`;
|
|
182
|
+
if (!failed && code !== 0) failed = `agy exited with code ${code}${stderrTail ? `: ${redactText(stderrTail.trim()).slice(-300)}` : ""}`;
|
|
182
183
|
if (!failed && !allowedToolUsed) failed = `Antigravity returned an answer without an observed ${opts.gatedTool} step; refusing it as unverified`;
|
|
183
184
|
if (!failed && response === "") failed = "Antigravity web run produced no answer";
|
|
184
185
|
if (failed) {
|