Skip to content

Commit 9d7f482

Browse files
committed
fix(github-workflows): require own automation fields
1 parent 7ff5464 commit 9d7f482

2 files changed

Lines changed: 85 additions & 12 deletions

File tree

packages/github-workflows/src/discover.test.ts

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,32 @@ function writeMarkdown(directory: string, fileName: string, content: string): vo
4343
fsState.files.set(`${directory}/${fileName}`, content);
4444
}
4545

46+
async function withObjectPrototypeProperties<T>(
47+
properties: Record<string, unknown>,
48+
callback: () => Promise<T>
49+
): Promise<T> {
50+
const originals = new Map<string, PropertyDescriptor | undefined>();
51+
for (const [key, value] of Object.entries(properties)) {
52+
originals.set(key, Object.getOwnPropertyDescriptor(Object.prototype, key));
53+
Object.defineProperty(Object.prototype, key, {
54+
configurable: true,
55+
value
56+
});
57+
}
58+
59+
try {
60+
return await callback();
61+
} finally {
62+
for (const [key, descriptor] of originals) {
63+
if (descriptor === undefined) {
64+
delete (Object.prototype as Record<string, unknown>)[key];
65+
} else {
66+
Object.defineProperty(Object.prototype, key, descriptor);
67+
}
68+
}
69+
}
70+
}
71+
4672
describe("discoverAutomations", () => {
4773
beforeEach(() => {
4874
fsState.directories.clear();
@@ -129,6 +155,40 @@ describe("loadAutomation", () => {
129155
});
130156
});
131157

158+
it("ignores inherited automation fields", async () => {
159+
writeMarkdown("/built-in", "triage.md", ["---", "{}", "---"].join("\n"));
160+
161+
await withObjectPrototypeProperties(
162+
{
163+
prompt: "Polluted prompt",
164+
agent: "polluted-agent",
165+
allow: ["OWNER"],
166+
prefix: "/poe"
167+
},
168+
async () => {
169+
await expect(loadAutomation("triage", ["/built-in"])).resolves.toEqual({
170+
name: "triage",
171+
prompt: "",
172+
agent: "codex"
173+
});
174+
}
175+
);
176+
});
177+
178+
it("does not accept inherited mcp server fields", async () => {
179+
writeMarkdown(
180+
"/built-in",
181+
"triage.md",
182+
["---", "mcp:", " server: {}", "---", "Prompt"].join("\n")
183+
);
184+
185+
await withObjectPrototypeProperties({ command: "polluted-command" }, async () => {
186+
await expect(loadAutomation("triage", ["/built-in"])).rejects.toThrow(
187+
'Automation "triage.md" has invalid "mcp.server.command" frontmatter. Expected a string.'
188+
);
189+
});
190+
});
191+
132192
it("falls back to unprefixed filename for backward compatibility", async () => {
133193
writeMarkdown("/project", "triage.md", "# Project triage");
134194
writeMarkdown("/built-in", "triage.md", ["---", "allow:", " - OWNER", "---", "# Built-in triage"].join("\n"));

packages/github-workflows/src/discover.ts

Lines changed: 25 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ async function readAutomation(
109109

110110
return {
111111
name,
112-
prompt: readPrompt(resolved.data.prompt, fileName),
112+
prompt: readPrompt(getOwnEntry(resolved.data, "prompt"), fileName),
113113
...readAutomationFields(resolved.data, fileName)
114114
};
115115
}
@@ -130,12 +130,12 @@ function readAutomationFields(
130130
frontmatter: Record<string, unknown>,
131131
fileName: string
132132
): Omit<AutomationDefinition, "name" | "prompt"> {
133-
const label = readOptionalString(frontmatter.label, "label", fileName);
134-
const source = readOptionalString(frontmatter.source, "source", fileName);
135-
const agent = readOptionalString(frontmatter.agent, "agent", fileName);
136-
const mcp = readOptionalMcp(frontmatter.mcp, fileName);
137-
const allow = readOptionalStringArray(frontmatter.allow, "allow", fileName);
138-
const prefix = readOptionalPrefix(frontmatter.prefix, fileName);
133+
const label = readOptionalString(getOwnEntry(frontmatter, "label"), "label", fileName);
134+
const source = readOptionalString(getOwnEntry(frontmatter, "source"), "source", fileName);
135+
const agent = readOptionalString(getOwnEntry(frontmatter, "agent"), "agent", fileName);
136+
const mcp = readOptionalMcp(getOwnEntry(frontmatter, "mcp"), fileName);
137+
const allow = readOptionalStringArray(getOwnEntry(frontmatter, "allow"), "allow", fileName);
138+
const prefix = readOptionalPrefix(getOwnEntry(frontmatter, "prefix"), fileName);
139139

140140
return {
141141
...(label === undefined ? {} : { label }),
@@ -259,32 +259,32 @@ function readOptionalMcp(
259259
);
260260
}
261261

262-
const command = serverValue.command;
262+
const command = getOwnEntry(serverValue, "command");
263263
if (typeof command !== "string") {
264264
throw new Error(
265265
`Automation "${fileName}" has invalid "mcp.${serverName}.command" frontmatter. Expected a string.`
266266
);
267267
}
268268

269-
const args = serverValue.args;
269+
const args = getOwnEntry(serverValue, "args");
270270
if (args !== undefined && (!Array.isArray(args) || args.some((item) => typeof item !== "string"))) {
271271
throw new Error(
272272
`Automation "${fileName}" has invalid "mcp.${serverName}.args" frontmatter. Expected an array of strings.`
273273
);
274274
}
275275

276-
const env = serverValue.env;
276+
const env = getOwnEntry(serverValue, "env");
277277
if (env !== undefined && !isStringRecord(env)) {
278278
throw new Error(
279279
`Automation "${fileName}" has invalid "mcp.${serverName}.env" frontmatter. Expected an object of strings.`
280280
);
281281
}
282282

283-
mcp[serverName] = {
283+
defineDataProperty(mcp, serverName, {
284284
command,
285285
...(args === undefined ? {} : { args }),
286286
...(env === undefined ? {} : { env })
287-
};
287+
});
288288
}
289289

290290
return mcp;
@@ -302,6 +302,19 @@ function isRecord(value: unknown): value is Record<string, unknown> {
302302
return typeof value === "object" && value !== null && !Array.isArray(value);
303303
}
304304

305+
function getOwnEntry(record: Record<string, unknown>, key: string): unknown {
306+
return Object.prototype.hasOwnProperty.call(record, key) ? record[key] : undefined;
307+
}
308+
309+
function defineDataProperty(object: Record<string, unknown>, key: string, value: unknown): void {
310+
Object.defineProperty(object, key, {
311+
configurable: true,
312+
enumerable: true,
313+
value,
314+
writable: true
315+
});
316+
}
317+
305318
function isStringRecord(value: unknown): value is Record<string, string> {
306319
if (!isRecord(value)) {
307320
return false;

0 commit comments

Comments
 (0)