Add username log censor setting

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
dotta
2026-03-20 08:00:39 -05:00
parent 3de7d63ea9
commit 39878fcdfe
33 changed files with 10841 additions and 146 deletions

View File

@@ -117,7 +117,7 @@ describe("codex_local ui stdout parser", () => {
{
kind: "system",
ts,
text: "file changes: update /Users/[]/project/ui/src/pages/AgentDetail.tsx",
text: "file changes: update /Users/paperclipuser/project/ui/src/pages/AgentDetail.tsx",
},
]);
});

View File

@@ -5,7 +5,9 @@ import { errorHandler } from "../middleware/index.js";
import { instanceSettingsRoutes } from "../routes/instance-settings.js";
const mockInstanceSettingsService = vi.hoisted(() => ({
getGeneral: vi.fn(),
getExperimental: vi.fn(),
updateGeneral: vi.fn(),
updateExperimental: vi.fn(),
listCompanyIds: vi.fn(),
}));
@@ -31,9 +33,18 @@ function createApp(actor: any) {
describe("instance settings routes", () => {
beforeEach(() => {
vi.clearAllMocks();
mockInstanceSettingsService.getGeneral.mockResolvedValue({
censorUsernameInLogs: false,
});
mockInstanceSettingsService.getExperimental.mockResolvedValue({
enableIsolatedWorkspaces: false,
});
mockInstanceSettingsService.updateGeneral.mockResolvedValue({
id: "instance-settings-1",
general: {
censorUsernameInLogs: true,
},
});
mockInstanceSettingsService.updateExperimental.mockResolvedValue({
id: "instance-settings-1",
experimental: {
@@ -66,6 +77,29 @@ describe("instance settings routes", () => {
expect(mockLogActivity).toHaveBeenCalledTimes(2);
});
it("allows local board users to read and update general settings", async () => {
const app = createApp({
type: "board",
userId: "local-board",
source: "local_implicit",
isInstanceAdmin: true,
});
const getRes = await request(app).get("/api/instance/settings/general");
expect(getRes.status).toBe(200);
expect(getRes.body).toEqual({ censorUsernameInLogs: false });
const patchRes = await request(app)
.patch("/api/instance/settings/general")
.send({ censorUsernameInLogs: true });
expect(patchRes.status).toBe(200);
expect(mockInstanceSettingsService.updateGeneral).toHaveBeenCalledWith({
censorUsernameInLogs: true,
});
expect(mockLogActivity).toHaveBeenCalledTimes(2);
});
it("rejects non-admin board users", async () => {
const app = createApp({
type: "board",
@@ -75,10 +109,10 @@ describe("instance settings routes", () => {
companyIds: ["company-1"],
});
const res = await request(app).get("/api/instance/settings/experimental");
const res = await request(app).get("/api/instance/settings/general");
expect(res.status).toBe(403);
expect(mockInstanceSettingsService.getExperimental).not.toHaveBeenCalled();
expect(mockInstanceSettingsService.getGeneral).not.toHaveBeenCalled();
});
it("rejects agent callers", async () => {
@@ -90,10 +124,10 @@ describe("instance settings routes", () => {
});
const res = await request(app)
.patch("/api/instance/settings/experimental")
.send({ enableIsolatedWorkspaces: true });
.patch("/api/instance/settings/general")
.send({ censorUsernameInLogs: true });
expect(res.status).toBe(403);
expect(mockInstanceSettingsService.updateExperimental).not.toHaveBeenCalled();
expect(mockInstanceSettingsService.updateGeneral).not.toHaveBeenCalled();
});
});

View File

@@ -1,6 +1,6 @@
import { describe, expect, it } from "vitest";
import {
CURRENT_USER_REDACTION_TOKEN,
maskUserNameForLogs,
redactCurrentUserText,
redactCurrentUserValue,
} from "../log-redaction.js";
@@ -8,6 +8,7 @@ import {
describe("log redaction", () => {
it("redacts the active username inside home-directory paths", () => {
const userName = "paperclipuser";
const maskedUserName = maskUserNameForLogs(userName);
const input = [
`cwd=/Users/${userName}/paperclip`,
`home=/home/${userName}/workspace`,
@@ -19,14 +20,15 @@ describe("log redaction", () => {
homeDirs: [`/Users/${userName}`, `/home/${userName}`, `C:\\Users\\${userName}`],
});
expect(result).toContain(`cwd=/Users/${CURRENT_USER_REDACTION_TOKEN}/paperclip`);
expect(result).toContain(`home=/home/${CURRENT_USER_REDACTION_TOKEN}/workspace`);
expect(result).toContain(`win=C:\\Users\\${CURRENT_USER_REDACTION_TOKEN}\\paperclip`);
expect(result).toContain(`cwd=/Users/${maskedUserName}/paperclip`);
expect(result).toContain(`home=/home/${maskedUserName}/workspace`);
expect(result).toContain(`win=C:\\Users\\${maskedUserName}\\paperclip`);
expect(result).not.toContain(userName);
});
it("redacts standalone username mentions without mangling larger tokens", () => {
const userName = "paperclipuser";
const maskedUserName = maskUserNameForLogs(userName);
const result = redactCurrentUserText(
`user ${userName} said ${userName}/project should stay but apaperclipuserz should not change`,
{
@@ -36,12 +38,13 @@ describe("log redaction", () => {
);
expect(result).toBe(
`user ${CURRENT_USER_REDACTION_TOKEN} said ${CURRENT_USER_REDACTION_TOKEN}/project should stay but apaperclipuserz should not change`,
`user ${maskedUserName} said ${maskedUserName}/project should stay but apaperclipuserz should not change`,
);
});
it("recursively redacts nested event payloads", () => {
const userName = "paperclipuser";
const maskedUserName = maskUserNameForLogs(userName);
const result = redactCurrentUserValue({
cwd: `/Users/${userName}/paperclip`,
prompt: `open /Users/${userName}/paperclip/ui`,
@@ -55,12 +58,17 @@ describe("log redaction", () => {
});
expect(result).toEqual({
cwd: `/Users/${CURRENT_USER_REDACTION_TOKEN}/paperclip`,
prompt: `open /Users/${CURRENT_USER_REDACTION_TOKEN}/paperclip/ui`,
cwd: `/Users/${maskedUserName}/paperclip`,
prompt: `open /Users/${maskedUserName}/paperclip/ui`,
nested: {
author: CURRENT_USER_REDACTION_TOKEN,
author: maskedUserName,
},
values: [CURRENT_USER_REDACTION_TOKEN, `/home/${CURRENT_USER_REDACTION_TOKEN}/project`],
values: [maskedUserName, `/home/${maskedUserName}/project`],
});
});
it("skips redaction when disabled", () => {
const input = "cwd=/Users/paperclipuser/paperclip";
expect(redactCurrentUserText(input, { enabled: false })).toBe(input);
});
});