Skip to content
Draft
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
24 changes: 16 additions & 8 deletions src/bundled/codex/codex-sh.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -429,21 +429,29 @@ it("lets go of the hand-over lock before Codex starts", async () => {
);
await mkdir(f.own, { recursive: true });
await writeFile(join(f.own, "auth.json"), "first", { mode: 0o600 });
let observed!: () => void, failed!: (error: Error) => void;
const boundary = new Promise<void>((resolve, reject) => {
observed = resolve;
failed = reject;
});
const watcher = watch(f.home, (_event, name) => {
if (name === "started") observed();
});
watcher.once("error", failed);
// An app server that stays up, as Buzz's does, must not hold up the next run.
const first = f.step("run");
try {
await expect
.poll(() =>
stat(started).then(
() => true,
() => false,
),
)
.toBe(true);
await Promise.race([
boundary,
first.then(() => {
throw new Error("Codex exited before its startup boundary");
}),
]);
await signIn(f.own, "second");
await f.step("run");
expect(await readFile(join(f.codex, "auth.json"), "utf8")).toBe("second");
} finally {
watcher.close();
first.child.kill();
await first.catch(() => {});
}
Expand Down
3 changes: 3 additions & 0 deletions src/bundled/codex/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ function applied() {
let type: AgentType | undefined;
const subscribe = () => () => undefined;
const ctx = {
agentProtection: { registerModels: vi.fn() },
pluginOwner: { id: "buzz.codex", revision: "r1" },
host: { spawn: vi.fn(), request: vi.fn() },
agents: {
Expand Down Expand Up @@ -124,6 +125,7 @@ it("stops agents that stop running even when the agent list could not be read",
let state = { status: "ready", agents: [agent], running: [agent] };
let notify = () => undefined;
apply({
agentProtection: { registerModels: vi.fn() },
pluginOwner: { id: "buzz.codex" },
host: { spawn: vi.fn(), request: vi.fn() },
agents: {
Expand Down Expand Up @@ -188,6 +190,7 @@ it("starts the server once Codex is updated and checked, only while a Codex agen
};
let cleanup: () => void = () => undefined;
apply({
agentProtection: { registerModels: vi.fn() },
pluginOwner: { id: "buzz.codex" },
host: { spawn: vi.fn(), request: vi.fn() },
agents: {
Expand Down
25 changes: 24 additions & 1 deletion src/bundled/codex/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import { CodexRuntime } from "./runtime";
import { CodexSetup, withScript } from "./setup";
import { CodexProvider, createTabs } from "./tabs";
import { readUsage } from "./usage";
import { reviewModel } from "./security-review";
import { AppServer, listModels } from "./rpc";
/** How often Buzz's copy of Codex looks for a new release. */
const UPDATE_MS = 6 * 60 * 60_000;
export const inject = ["agents", "host"];
export const inject = ["agents", "host", "agentProtection"];
export const apply: PluginModule["apply"] = (ctx) => {
// Only the desktop app can start `codex`, and setup and file uploads run
// `bash` and `base64`, which stock Windows lacks; no unusable type elsewhere.
Expand Down Expand Up @@ -60,7 +62,26 @@ export const apply: PluginModule["apply"] = (ctx) => {
},
}
: undefined,
ctx.host,
(pubkey) => ctx.agents.handle(pubkey),
);
ctx.agentProtection.registerModels({
label: "Codex",
models: async () => {
const rpc = new AppServer();
try {
await rpc.open(start);
return (await listModels(rpc)).map((model) => ({
id: model.model,
label: model.displayName,
}));
} finally {
await rpc.close();
}
},
complete: (model, prompt, signal, protectedSpawn) =>
reviewModel(protectedSpawn, model, prompt, signal),
});
/** A Codex agent is running, so Buzz's copy is needed; unknown until the
* agents have loaded. */
let needed: boolean | undefined;
Expand Down Expand Up @@ -121,6 +142,8 @@ export const apply: PluginModule["apply"] = (ctx) => {
ctx.agents.register<Config>({
id: TYPE_ID,
hooks: { events: CODEX_HOOK_EVENTS },
retire: (pubkey) => runtime.retire(pubkey),
protection: true,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Discover models from the protected engine

When a protection provider supplies a pinned Codex engine whose catalog differs from the ordinary bundled binary, model selection still runs through codexRun → runtime.preview(), which calls this.open() without the agent handle and therefore lists models from the unprotected shared server. The picker can consequently accept a model that the protected server later rejects in prepare() as unavailable, preventing protected turns from starting. Route protected-agent previews through that agent's protected server and scope the preview cache to its protection key.

AGENTS.md reference: AGENTS.md:L81-L85

Useful? React with 👍 / 👎.

title: "Codex",
icon: CodexLogoIcon,
description:
Expand Down
46 changes: 28 additions & 18 deletions src/bundled/codex/rpc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ export class AppServer {
private readonly toolCall?: (call: ToolCall) => Promise<ToolReply>,
) {}

async open(spawn: Spawn, cwd?: string) {
async open(spawn: Spawn, cwd?: string, protectedRuntime = false) {
this.process = await spawn("app-server", {
...(cwd ? { cwd } : {}),
onStdout: (data) => this.read(data),
Expand All @@ -73,24 +73,34 @@ export class AppServer {
await this.close();
throw this.failure;
}
await this.request("initialize", {
clientInfo: {
name: "buzz_codex_plugin",
title: "Buzz Codex",
version: "0.1.0",
const initialized = await this.request<{ userAgent?: string }>(
"initialize",
{
clientInfo: {
name: "buzz_codex_plugin",
title: "Buzz Codex",
version: "0.1.0",
},
capabilities: {
experimentalApi: true,
optOutNotificationMethods: [
"item/agentMessage/delta",
"item/reasoning/summaryTextDelta",
"item/reasoning/textDelta",
"item/plan/delta",
"item/commandExecution/outputDelta",
"item/fileChange/outputDelta",
],
},
},
capabilities: {
experimentalApi: true,
optOutNotificationMethods: [
"item/agentMessage/delta",
"item/reasoning/summaryTextDelta",
"item/reasoning/textDelta",
"item/plan/delta",
"item/commandExecution/outputDelta",
"item/fileChange/outputDelta",
],
},
});
);
if (
protectedRuntime &&
!initialized.userAgent?.includes("buzz-protection-v2")
) {
await this.close();
throw new Error("This Codex build lacks mandatory security hooks");
}
await this.send({ method: "initialized" });
return this;
}
Expand Down
Loading
Loading