Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/gittensory-engine/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,16 @@ export { resolvePlanOverallStatus, type PlanOverallStatus } from "./plan-overall
export { hasPlanReadySteps } from "./plan-ready.js";
export { isPlanTerminated } from "./plan-terminated.js";
export * from "./plan-templates.js";
export {
PROMPT_PACKET_REDACTED_PATH,
PROMPT_PACKET_REDACTED_TERM,
PROMPT_PACKET_TEXT_FIELDS,
buildPromptPacket,
sanitizePromptPacketField,
type PromptPacket,
type PromptPacketInput,
type PromptPacketTextField,
} from "./prompt-packet.js";
export * from "./portfolio/queue.js";
export {
applyAiPolicyFatigueToRankInput,
Expand Down
47 changes: 47 additions & 0 deletions packages/gittensory-engine/src/prompt-packet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// Metadata-only prompt-packet builder (#2321): four analyze-phase text fields scrubbed with the same PUBLIC_UNSAFE_TERMS / PUBLIC_LOCAL_PATH_INLINE vocabulary as src/signals/redaction.ts (duplicated here so gittensory-engine stays standalone).

/** Canonical economic/identity term vocabulary (alternation source only — mirrors `PUBLIC_UNSAFE_TERMS`). */
const PUBLIC_UNSAFE_TERMS = String.raw`(?:reward|score|wallet|hotkey|coldkey|mnemonic|payout|ranking)\w*|farming|raw[-_\s]?trust|trust[-_\s]?score|private[-_\s]?reviewability|reviewability`;

/** Canonical local-filesystem-root vocabulary (alternation source only — mirrors `PUBLIC_LOCAL_PATH_INLINE`). */
const PUBLIC_LOCAL_PATH_INLINE = String.raw`/Users/|/home/|/root/|/var/|/opt/|/tmp/|/private/|[A-Za-z]:[\\/]Users[\\/]|[A-Za-z]:[\\/]Program Files[\\/]`;

const UNSAFE_TERM_SCRUB = new RegExp(String.raw`\b(${PUBLIC_UNSAFE_TERMS})\b`, "gi");
const LOCAL_PATH_SCRUB = new RegExp(String.raw`(?:${PUBLIC_LOCAL_PATH_INLINE})[^\s"',;)]*`, "gi");

export const PROMPT_PACKET_REDACTED_TERM = "[redacted]";
export const PROMPT_PACKET_REDACTED_PATH = "<local-path>";

/** The four free-text fields the analyze prompt packet exposes to a coding agent. */
export type PromptPacketTextField = "taskBrief" | "feasibilityNotes" | "retrievalContext" | "constraints";

export const PROMPT_PACKET_TEXT_FIELDS: readonly PromptPacketTextField[] = Object.freeze([
"taskBrief",
"feasibilityNotes",
"retrievalContext",
"constraints",
]);

export type PromptPacketInput = Record<PromptPacketTextField, string>;
export type PromptPacket = PromptPacketInput;

function emptyPromptPacketInput(): PromptPacketInput {
return {
taskBrief: "",
feasibilityNotes: "",
retrievalContext: "",
constraints: "",
};
}

/** Scrub unsafe economic/identity terms and absolute local paths from one packet field. */
export function sanitizePromptPacketField(value: string): string {
return value.replace(LOCAL_PATH_SCRUB, PROMPT_PACKET_REDACTED_PATH).replace(UNSAFE_TERM_SCRUB, PROMPT_PACKET_REDACTED_TERM);
}

/** Build a public-safe analyze prompt packet from metadata-only inputs. Clean fields pass through byte-identical; unsafe terms and local paths are redacted. */
export function buildPromptPacket(input: PromptPacketInput): PromptPacket {
const packet = emptyPromptPacketInput();
for (const field of PROMPT_PACKET_TEXT_FIELDS) packet[field] = sanitizePromptPacketField(input[field]);
return packet;
}
146 changes: 146 additions & 0 deletions test/unit/prompt-packet-redaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import { describe, expect, it } from "vitest";
import {
PROMPT_PACKET_REDACTED_PATH,
PROMPT_PACKET_REDACTED_TERM,
PROMPT_PACKET_TEXT_FIELDS,
buildPromptPacket,
type PromptPacketInput,
} from "../../packages/gittensory-engine/src/prompt-packet";
import { PUBLIC_LOCAL_PATH_INLINE, PUBLIC_UNSAFE_TERMS } from "../../src/signals/redaction";

function cleanPacketInput(over: Partial<PromptPacketInput> = {}): PromptPacketInput {
return {
taskBrief: "Add retry logic to the cache reconnect path.",
feasibilityNotes: "Linked issue is open and unassigned.",
retrievalContext: "See src/cache/reconnect.ts for the existing handler.",
constraints: "Match house style; run npm run test:ci before push.",
...over,
};
}

function splitTopLevelAlternation(source: string): string[] {
const branches: string[] = [];
let depth = 0;
let current = "";
for (const char of source) {
if (char === "(") depth += 1;
if (depth > 0) current += char;
else if (char === "|") {
branches.push(current);
current = "";
} else {
current += char;
}
if (char === ")") depth -= 1;
}
if (current.length > 0) branches.push(current);
return branches;
}

/** Enumerate every unsafe-term family branch from the canonical alternation source (do not hand-copy). */
function enumerateUnsafeTermFamilies(source: string): Array<{ id: string; sample: string }> {
const branches = splitTopLevelAlternation(source);
const families: Array<{ id: string; sample: string }> = [];

for (const branch of branches) {
const pluralizable = branch.match(/^\(\?:([^)]+)\)\\w\*$/) ?? branch.match(/^\(([^)]+)\)\\w\*$/);
if (pluralizable) {
for (const term of splitTopLevelAlternation(pluralizable[1]!)) {
families.push({ id: term, sample: term });
}
continue;
}
if (branch === "raw[-_\\s]?trust") {
families.push({ id: "raw-trust", sample: "raw-trust" });
continue;
}
if (branch === "trust[-_\\s]?score") {
families.push({ id: "trust-score", sample: "trust_score" });
continue;
}
if (branch === "private[-_\\s]?reviewability") {
families.push({ id: "private-reviewability", sample: "private-reviewability" });
continue;
}
families.push({ id: branch, sample: branch });
}

return families;
}

/** Enumerate every local-path root prefix from the canonical alternation source (do not hand-copy). */
function enumerateLocalPathSamples(source: string): Array<{ id: string; sample: string }> {
return splitTopLevelAlternation(source).map((prefix) => {
if (prefix.startsWith("[A-Za-z]:[\\\\/]Users[\\\\/]")) {
return { id: "windows-users", sample: "C:\\Users\\alice\\repo\\main.ts" };
}
if (prefix.startsWith("[A-Za-z]:[\\\\/]Program Files[\\\\/]")) {
return { id: "windows-program-files", sample: "C:\\Program Files\\App\\config.json" };
}
const id = prefix.replace(/\/$/, "").replace(/^\//, "");
return { id, sample: `${prefix}alice/project` };
});
}

const UNSAFE_TERM_FAMILIES = enumerateUnsafeTermFamilies(PUBLIC_UNSAFE_TERMS);
const LOCAL_PATH_SAMPLES = enumerateLocalPathSamples(PUBLIC_LOCAL_PATH_INLINE);

describe("buildPromptPacket redaction (#2321 adversarial allowlist)", () => {
it("enumerates every unsafe-term family from PUBLIC_UNSAFE_TERMS", () => {
expect(UNSAFE_TERM_FAMILIES.map((entry) => entry.id).sort()).toEqual(
["coldkey", "farming", "hotkey", "mnemonic", "payout", "private-reviewability", "ranking", "raw-trust", "reviewability", "reward", "score", "trust-score", "wallet"].sort(),
);
});

it("enumerates every local-path root from PUBLIC_LOCAL_PATH_INLINE", () => {
expect(LOCAL_PATH_SAMPLES.map((entry) => entry.id).sort()).toEqual(
["Users", "home", "opt", "private", "root", "tmp", "var", "windows-program-files", "windows-users"].sort(),
);
});

it.each(UNSAFE_TERM_FAMILIES.flatMap(({ id, sample }) =>
PROMPT_PACKET_TEXT_FIELDS.map((field) => ({ id, field, sample })),
))("strips unsafe term family '$id' injected into $field", ({ field, sample }) => {
const input = cleanPacketInput({ [field]: `prefix ${sample} suffix` });
const packet = buildPromptPacket(input);

expect(packet[field]).toContain(PROMPT_PACKET_REDACTED_TERM);
expect(packet[field]).not.toMatch(new RegExp(String.raw`\b${sample.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\b`, "i"));
});

it.each(LOCAL_PATH_SAMPLES.flatMap(({ id, sample }) =>
PROMPT_PACKET_TEXT_FIELDS.map((field) => ({ id, field, sample })),
))("strips local-path prefix '$id' injected into $field", ({ field, sample }) => {
const input = cleanPacketInput({ [field]: `clone failed at ${sample} during setup` });
const packet = buildPromptPacket(input);

expect(packet[field]).toContain(PROMPT_PACKET_REDACTED_PATH);
expect(packet[field]).not.toContain(sample);
});

it("applies both unsafe-term and local-path filters in the same field (double jeopardy)", () => {
const packet = buildPromptPacket(
cleanPacketInput({ taskBrief: "wallet backup stored at /home/alice/secrets before retry" }),
);

expect(packet.taskBrief).toContain(PROMPT_PACKET_REDACTED_TERM);
expect(packet.taskBrief).toContain(PROMPT_PACKET_REDACTED_PATH);
expect(packet.taskBrief).not.toMatch(/\bwallet\b/i);
expect(packet.taskBrief).not.toMatch(/\/home\//);
});

it("leaves fields with zero unsafe content byte-identical", () => {
const input = cleanPacketInput();
const packet = buildPromptPacket(input);

for (const field of PROMPT_PACKET_TEXT_FIELDS) {
expect(packet[field]).toBe(input[field]);
}
});

it("strips a field that contains only an unsafe term instead of forwarding it verbatim", () => {
const packet = buildPromptPacket(cleanPacketInput({ constraints: "wallet" }));

expect(packet.constraints).toBe(PROMPT_PACKET_REDACTED_TERM);
});
});
4 changes: 2 additions & 2 deletions test/unit/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7242,7 +7242,7 @@ describe("queue processors", () => {
} finally {
errors.mockRestore();
}
});
}, 60_000);

it("REGRESSION (#orb-retry-storm): a repair dispatch under the attempt cap records a repair_attempt audit event, and a second sweep tick does not duplicate the repair_exhausted event once already flagged", async () => {
const sent: import("../../src/types").JobMessage[] = [];
Expand Down Expand Up @@ -7287,7 +7287,7 @@ describe("queue processors", () => {
.bind("agent.sweep.regate.repair_exhausted", targetKey)
.first<{ n: number }>();
expect(exhausted?.n).toBe(1);
});
}, 60_000);

it("agent re-gate sweep fail-opens when current Gate check reads fail during repair priority selection", async () => {
const sent: import("../../src/types").JobMessage[] = [];
Expand Down
2 changes: 1 addition & 1 deletion test/unit/rag-wiring.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -413,7 +413,7 @@ describe("RAG wired into the AI reviewer (flag GITTENSORY_REVIEW_RAG)", () => {
notesReferencedRetrievedPath: false,
findingReferencedRetrievedPath: false,
});
});
}, 60_000);

it("FLAG-OFF (default): the prompt is byte-identical to the no-RAG prompt (ragContext undefined)", async () => {
// The flag-OFF call site leaves ragContext undefined; the prompt must equal the no-RAG prompt.
Expand Down