Skip to content
Open
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
42 changes: 41 additions & 1 deletion integrations/filesystem-watcher/watcher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,45 @@ const DEFAULT_IGNORE = [

const MAX_PREVIEW_BYTES = 4096;
const DEBOUNCE_MS = 500;
const REDACTED = "[REDACTED]";

function isDotEnvPath(path) {
const name = basename(path).toLowerCase();
return name === ".env" || name.startsWith(".env.");
}

function isSensitiveKey(key) {
const normalized = key.replace(/[^a-z0-9]/gi, "").toLowerCase();
return [
"apikey",
"accesstoken",
"accesskey",
"authorization",
"bearer",
"clientsecret",
"password",
"passwd",
"privatekey",
"pwd",
"secret",
"token",
].some((needle) => normalized.includes(needle));
}

function redactSensitiveLine(line) {
const assignment = line.match(
/^(\s*(?:export\s+)?["']?([A-Za-z_][A-Za-z0-9_.-]*)["']?\s*([=:])\s*)(.*)$/,
);
if (assignment && isSensitiveKey(assignment[2])) {
const bearer = assignment[3] === ":" ? assignment[4].match(/^(Bearer\s+).+/i) : null;
return `${assignment[1]}${bearer ? bearer[1] : ""}${REDACTED}`;
}
return line.replace(/\b(Bearer\s+)[A-Za-z0-9._~+/=-]{8,}\b/gi, `$1${REDACTED}`);
}

function redactSensitivePreview(preview) {
return preview.split("\n").map(redactSensitiveLine).join("\n");
}

export class FilesystemWatcher {
constructor(config = {}) {
Expand All @@ -54,7 +93,7 @@ export class FilesystemWatcher {
isTextFile(path) {
if (this.allowBinary) return true;
const ext = extname(path).toLowerCase();
return TEXT_EXTENSIONS.has(ext);
return TEXT_EXTENSIONS.has(ext) || isDotEnvPath(path);
}

async readPreview(path) {
Expand Down Expand Up @@ -121,6 +160,7 @@ export class FilesystemWatcher {
let preview = null;
if (exists && this.isTextFile(absPath)) {
preview = await this.readPreview(absPath);
if (preview !== null) preview = redactSensitivePreview(preview);
}
const truncated = exists && size > MAX_PREVIEW_BYTES;
const payload = {
Expand Down
67 changes: 67 additions & 0 deletions test/fs-watcher.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,73 @@ describe("FilesystemWatcher", () => {
}
});

it("redacts sensitive dotenv preview values before sending observations", async () => {
writeFileSync(
join(root, ".env"),
[
"OPENAI_API_KEY=sk-test-secret-value",
"PUBLIC_FLAG=enabled",
"AUTHORIZATION=Bearer live-token-value",
].join("\n"),
);
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});

await w.flush(root, ".env");

expect(captured).toHaveLength(1);
const content = (captured[0].body as { data: { content: string } }).data.content;
expect(content).toContain("OPENAI_API_KEY=[REDACTED]");
expect(content).toContain("PUBLIC_FLAG=enabled");
expect(content).toContain("AUTHORIZATION=[REDACTED]");
expect(content).not.toContain("sk-test-secret-value");
expect(content).not.toContain("live-token-value");
});

it("redacts quoted JSON-style sensitive keys before sending observations", async () => {
writeFileSync(
join(root, "settings.json"),
[
'{',
' "api_key": "json-preview-secret",',
' "public_flag": "enabled"',
'}',
].join("\n"),
);
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});

await w.flush(root, "settings.json");

expect(captured).toHaveLength(1);
const content = (captured[0].body as { data: { content: string } }).data.content;
expect(content).toContain('"api_key": [REDACTED]');
expect(content).toContain('"public_flag": "enabled"');
expect(content).not.toContain("json-preview-secret");
});

it("redacts bearer tokens from regular text previews before sending observations", async () => {
writeFileSync(join(root, "request.txt"), "Authorization: Bearer plaintext-token-value\n");
const w = new FilesystemWatcher({
roots: [root],
baseUrl: "http://localhost:3111",
logger: { info: vi.fn(), warn: vi.fn(), error: vi.fn() },
});

await w.flush(root, "request.txt");

expect(captured).toHaveLength(1);
const content = (captured[0].body as { data: { content: string } }).data.content;
expect(content).toContain("Authorization: Bearer [REDACTED]");
expect(content).not.toContain("plaintext-token-value");
});

it("debounces rapid writes to a single observation", async () => {
const w = new FilesystemWatcher({
roots: [root],
Expand Down