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
11 changes: 11 additions & 0 deletions docs-yml.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4751,6 +4751,17 @@
}
],
"description": "Additional content sources that Ask Fern should index and search."
},
"mask-pii": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "When enabled, personally identifiable information (PII) in user messages is masked before\nbeing sent to Ask Fern. Disabled by default."
}
},
"additionalProperties": false
Expand Down
6 changes: 6 additions & 0 deletions fern-yml.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -1846,6 +1846,9 @@
],
"additionalProperties": false
}
},
"mask-pii": {
"type": "boolean"
}
},
"additionalProperties": false
Expand Down Expand Up @@ -1892,6 +1895,9 @@
],
"additionalProperties": false
}
},
"mask-pii": {
"type": "boolean"
}
},
"additionalProperties": false
Expand Down
5 changes: 5 additions & 0 deletions fern/apis/docs-yml/definition/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -575,6 +575,11 @@ types:
datasources:
type: optional<list<AIChatDatasource>>
docs: Additional content sources that Ask Fern should index and search.
"mask-pii":
type: optional<boolean>
docs: |
When enabled, personally identifiable information (PII) in user messages is masked before
being sent to Ask Fern. Disabled by default.

AIChatLocation:
enum:
Expand Down
7 changes: 7 additions & 0 deletions packages/cli/cli/changes/unreleased/add-ask-ai-mask-pii.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# yaml-language-server: $schema=../../../../../fern-changes-yml.schema.json

- summary: |
Add an opt-in `mask-pii` setting under `ai-search` in docs.yml. When enabled, personally
identifiable information (PII) in user messages is masked before being sent to Ask Fern.
Disabled by default.
type: feat
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { docsYml } from "@fern-api/configuration";
import { AbsoluteFilePath } from "@fern-api/fs-utils";
import { createMockTaskContext } from "@fern-api/task-context";
import { describe, expect, it } from "vitest";

import { parseDocsConfiguration } from "../parseDocsConfiguration.js";

const FAKE_FERN_DIR = "/fern" as AbsoluteFilePath;
const FAKE_CONFIG_PATH = "/fern/docs.yml" as AbsoluteFilePath;

async function parseRawDocsYml(rawDocsYml: unknown): Promise<docsYml.ParsedDocsConfiguration> {
// mirrors loadDocsWorkspace: kebab-case docs.yml keys -> camelCase raw config
const rawDocsConfiguration = docsYml.RawSchemas.Serializer.DocsConfiguration.parseOrThrow(rawDocsYml);
return await parseDocsConfiguration({
rawDocsConfiguration,
absolutePathToFernFolder: FAKE_FERN_DIR,
absoluteFilepathToDocsConfig: FAKE_CONFIG_PATH,
context: createMockTaskContext()
});
}

describe("parseDocsConfiguration — ai-search.mask-pii", () => {
it("is undefined (masking off) when the ai-search key is omitted", async () => {
const parsed = await parseRawDocsYml({ instances: [], navigation: [] });
expect(parsed.aiChatConfig?.maskPii).toBeUndefined();
});

it("is undefined (masking off) when mask-pii is omitted", async () => {
const parsed = await parseRawDocsYml({
instances: [],
navigation: [],
"ai-search": {}
});
expect(parsed.aiChatConfig?.maskPii).toBeUndefined();
});

it("maps the kebab-case mask-pii key to the camelCase maskPii field", async () => {
const enabled = await parseRawDocsYml({
instances: [],
navigation: [],
"ai-search": { "mask-pii": true }
});
expect(enabled.aiChatConfig?.maskPii).toBe(true);

const disabled = await parseRawDocsYml({
instances: [],
navigation: [],
"ai-search": { "mask-pii": false }
});
expect(disabled.aiChatConfig?.maskPii).toBe(false);
});

it("also honors mask-pii under the deprecated ai-chat key", async () => {
const parsed = await parseRawDocsYml({
instances: [],
navigation: [],
"ai-chat": { "mask-pii": true }
});
expect(parsed.aiChatConfig?.maskPii).toBe(true);
});
});
3 changes: 2 additions & 1 deletion packages/cli/configuration/src/docs-yml/DocsYmlSchemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,8 @@ export const AIChatConfig = z.object({
model: AIChatModel.optional(),
"system-prompt": z.string().optional(),
location: z.array(AIChatLocation).optional(),
datasources: z.array(AIChatDatasource).optional()
datasources: z.array(AIChatDatasource).optional(),
"mask-pii": z.boolean().optional()
});

// ===== Font schemas =====
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { describe, expect, it } from "vitest";
import { z } from "zod";

import { DocsConfiguration, ProductFileConfig, VersionFileConfig } from "../DocsYmlSchemas.js";
import { AIChatConfig, DocsConfiguration, ProductFileConfig, VersionFileConfig } from "../DocsYmlSchemas.js";

describe("DocsYmlSchemas", () => {
it("should produce JSON Schema for DocsConfiguration", () => {
Expand Down Expand Up @@ -41,6 +41,16 @@ describe("DocsYmlSchemas", () => {
expect(properties["check"]).toBeDefined();
});

it("AIChatConfig defaults mask-pii to undefined (masking off by default)", () => {
const parsed = AIChatConfig.parse({});
expect(parsed["mask-pii"]).toBeUndefined();
});

it("AIChatConfig preserves an explicit mask-pii opt-in", () => {
expect(AIChatConfig.parse({ "mask-pii": true })["mask-pii"]).toBe(true);
expect(AIChatConfig.parse({ "mask-pii": false })["mask-pii"]).toBe(false);
});

it("should preserve explicitly configured check rule severities", () => {
const parsed = DocsConfiguration.parse({
instances: [],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,9 @@ export interface AiChatConfig {
location?: FernDocsConfig.AiChatLocation[];
/** Additional content sources that Ask Fern should index and search. */
datasources?: FernDocsConfig.AiChatDatasource[];
/**
* When enabled, personally identifiable information (PII) in user messages is masked before
* being sent to Ask Fern. Disabled by default.
*/
maskPii?: boolean;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export const AiChatConfig: core.serialization.ObjectSchema<serializers.AiChatCon
systemPrompt: core.serialization.property("system-prompt", core.serialization.string().optional()),
location: core.serialization.list(AiChatLocation).optional(),
datasources: core.serialization.list(AiChatDatasource).optional(),
maskPii: core.serialization.property("mask-pii", core.serialization.boolean().optional()),
});

export declare namespace AiChatConfig {
Expand All @@ -21,5 +22,6 @@ export declare namespace AiChatConfig {
"system-prompt"?: string | null;
location?: AiChatLocation.Raw[] | null;
datasources?: AiChatDatasource.Raw[] | null;
"mask-pii"?: boolean | null;
}
}
11 changes: 9 additions & 2 deletions packages/cli/docs-resolver/src/DocsDefinitionResolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,12 @@ interface DocsConfigWithTranslations extends DocsV1Write.DocsConfig {
translations: DocsTranslationsConfig | undefined;
}

// TODO: Remove this shim once the published @fern-api/fdr-sdk type for
// DocsV1Write.AIChatConfig includes the maskPii field.
type AIChatConfigWithMaskPii = NonNullable<DocsV1Write.DocsConfig["aiChatConfig"]> & {
maskPii?: boolean;
};

import { ApiReferenceNodeConverter } from "./ApiReferenceNodeConverter.js";
import { ChangelogNodeConverter } from "./ChangelogNodeConverter.js";
import { NodeIdGenerator } from "./NodeIdGenerator.js";
Expand Down Expand Up @@ -965,8 +971,9 @@ export class DocsDefinitionResolver {
datasources: this.parsedDocsConfig.aiChatConfig.datasources?.map((ds) => ({
url: ds.url,
title: ds.title
}))
} as DocsV1Write.DocsConfig["aiChatConfig"])
})),
maskPii: this.parsedDocsConfig.aiChatConfig.maskPii

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔴 critical

this.parsedDocsConfig.aiChatConfig originates from the zod AIChatConfig schema, where the field is "mask-pii", not maskPii. Unless there's a serialization step between the zod-parsed config and parsedDocsConfig that converts to camelCase (the generated SDK serialization does, but the zod schema doesn't), this.parsedDocsConfig.aiChatConfig.maskPii will always be undefined and the setting will silently never propagate. Verify which type parsedDocsConfig.aiChatConfig actually is and use the matching key.

} as AIChatConfigWithMaskPii as DocsV1Write.DocsConfig["aiChatConfig"])
: undefined,
hideNavLinks: undefined,
title: this.parsedDocsConfig.title,
Expand Down
11 changes: 11 additions & 0 deletions packages/cli/workspace/loader/src/docs-yml.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -4751,6 +4751,17 @@
}
],
"description": "Additional content sources that Ask Fern should index and search."
},
"mask-pii": {
"oneOf": [
{
"type": "boolean"
},
{
"type": "null"
}
],
"description": "When enabled, personally identifiable information (PII) in user messages is masked before\nbeing sent to Ask Fern. Disabled by default."
}
},
"additionalProperties": false
Expand Down
Loading