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
41 changes: 41 additions & 0 deletions scripts/security/skillspector-exception-diagnostic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,47 @@ it("rejects unknown modules and labels and drops unrelated raw fields", () => {
).toThrow();
});

it("identifies only the exact known mock serializer error without exporting its message", async () => {
const code = `
import importlib.util,json,sys
spec=importlib.util.spec_from_file_location("diagnostic",sys.argv[1]);m=importlib.util.module_from_spec(spec);spec.loader.exec_module(m)
known="'MockValSer' object cannot be converted to 'SchemaSerializer'"
cases=[TypeError(known),TypeError(known+" PRIVATE"),ValueError(known),TypeError("PRIVATE"),TypeError(known,"PRIVATE")]
print(json.dumps([m.project_exception(exc) for exc in cases]))
`;
const result = await promisify(execFile)(
"python3",
["-B", "-c", code, new URL("./skillspector_exception_diagnostic.py", import.meta.url).pathname],
{ env: { PATH: process.env.PATH } },
);
expect(result.stdout).not.toContain("PRIVATE");
expect(result.stdout).not.toContain("MockValSer");
const chains = JSON.parse(result.stdout);
expect(
chains.map((chain: Array<{ mockSchemaSerializer: boolean }>) => chain[0].mockSchemaSerializer),
).toEqual([true, false, false, false, false]);
const event = { rootIndex: 4, kind: "failure", chain: chains[0] };
expect(projectSkillSpectorExceptionEvents(JSON.stringify(event))[0]).toMatchObject({
chain: [{ class: "TypeError", mockSchemaSerializer: true }],
});
expect(() =>
projectSkillSpectorExceptionEvents(
JSON.stringify({
...event,
chain: [{ ...chains[0][0], mockSchemaSerializer: "PRIVATE" }],
}),
),
).toThrow();
expect(() =>
projectSkillSpectorExceptionEvents(
JSON.stringify({
...event,
chain: [{ ...chains[0][0], class: "ValueError", mockSchemaSerializer: true }],
}),
),
).toThrow();
});

it("installs only over the qualified venv entrypoint and reads private regular files only", async () => {
const root = await mkdtemp(join(tmpdir(), "exception-observer-"));
roots.push(root);
Expand Down
4 changes: 4 additions & 0 deletions scripts/security/skillspector-exception-diagnostic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@ export function projectSkillSpectorExceptionEvents(text: string) {
!value ||
typeof value.class !== "string" ||
!classes.has(value.class) ||
(value.mockSchemaSerializer !== undefined &&
typeof value.mockSchemaSerializer !== "boolean") ||
(value.mockSchemaSerializer === true && value.class !== "TypeError") ||
!(
value.statusCode === null ||
(integer(value.statusCode, 599) && value.statusCode >= 100)
Expand All @@ -127,6 +130,7 @@ export function projectSkillSpectorExceptionEvents(text: string) {
throw new Error("Invalid exception projection");
return {
class: value.class,
mockSchemaSerializer: value.mockSchemaSerializer === true,
statusCode: value.statusCode as number | null,
unknownFrames: value.unknownFrames,
framesTruncated: value.framesTruncated,
Expand Down
5 changes: 5 additions & 0 deletions scripts/security/skillspector_exception_diagnostic.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,11 @@ def project_exception(exc):
code = getattr(exc, "status_code", None)
chain.append({
"class": type(exc).__name__ if type(exc).__name__ in CLASSES else "other",
"mockSchemaSerializer": (
type(exc) is TypeError and len(exc.args) == 1
and type(exc.args[0]) is str
and exc.args[0] == "'MockValSer' object cannot be converted to 'SchemaSerializer'"
),
"statusCode": code if type(code) is int and 100 <= code <= 599 else None,
"frames": frames[-8:],
"unknownFrames": unknown,
Expand Down
5 changes: 4 additions & 1 deletion specs/security-moderation.md
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,10 @@ See also: [acceptable-usage.md](./acceptable-usage.md) for the marketplace polic
and interpreter. It calls the original failure classifier first and preserves
its result, exceptions and retry behavior. Exported exception data is limited
to fixed module IDs, qualified line numbers, allowlisted classes, bounded HTTP
status codes, root indices, counts and lifecycle flags. Frame provenance must
status codes, root indices, counts and lifecycle flags. One boolean may identify
the exact known Pydantic MockValSer/SchemaSerializer TypeError; matching must
inspect only its single string argument without stringifying arbitrary values
or exporting any message text. Frame provenance must
match the loaded module's globals and source filename. Export limits are three
chained exceptions, eight frames per exception and eight failure records per
root; traversal is capped at 64 frames, line numbers at 1,000,000 and the private
Expand Down
Loading