Skip to content

Commit 2b18ef7

Browse files
steipeteEVA
andauthored
feat(codex): surface native questions and goals (#109724)
* feat(codex): bridge native questions and goals Co-authored-by: EVA <[email protected]> * fix(ui): preserve native question text encoding * fix(codex): keep secret answers off action paths * fix(codex): validate native action keys exactly * fix(codex): isolate new goals and free-form answers * style(codex): satisfy extension lint * fix(codex): keep goal replacement non-destructive * fix(codex): separate native goals from continuation * fix(codex): preserve exact native answer labels * test(codex): assert goal continuation stays disabled * fix(codex): remove unused bridge exports * test(codex): refresh goal feature snapshots --------- Co-authored-by: EVA <[email protected]>
1 parent 264a95c commit 2b18ef7

32 files changed

Lines changed: 1412 additions & 20 deletions

docs/plugins/codex-harness-runtime.md

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -227,9 +227,12 @@ cwd creates a fresh approval.
227227

228228
Codex MCP tool approval elicitations route through OpenClaw's plugin approval
229229
flow when Codex marks `_meta.codex_approval_kind` as `"mcp_tool_call"`. Codex
230-
`request_user_input` prompts are sent back to the originating chat, and the
231-
next queued follow-up message answers that native server request instead of
232-
being steered as extra context. Other MCP elicitation requests fail closed.
230+
`request_user_input` prompts are sent back to the originating chat. A single
231+
non-secret choice uses typed channel buttons when the channel supports them,
232+
and the Control UI shows non-secret questions as a structured card. The next
233+
queued follow-up message answers that native server request instead of being
234+
steered as extra context. Secret questions stay on the warned text-reply path.
235+
Other MCP elicitation requests fail closed.
233236

234237
For the general plugin approval flow that carries these prompts, see
235238
[Plugin permission requests](/plugins/plugin-permission-requests).

docs/plugins/codex-harness.md

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -293,6 +293,7 @@ Keep provider refs and runtime policy separate:
293293
| Attach the current chat | `/codex bind [thread-id] [--cwd <path>] [--model <model>] [--provider <provider>]` |
294294
| Resume an existing Codex thread | `/codex resume <thread-id>` |
295295
| List or filter Codex threads | `/codex threads [filter]` |
296+
| Read or update the bound thread's native goal | `/codex goal [status\|set <objective>\|pause\|resume\|block\|complete\|clear]` |
296297
| List native Codex plugins | `/codex plugins list` |
297298
| Enable or disable a configured native Codex plugin | `/codex plugins enable <name>`, `/codex plugins disable <name>` |
298299
| Resume a stored Codex CLI session as a paired-node turn | `/codex sessions --host <node> [filter]`, then `/codex resume <session-id> --host <node> --bind here` |
@@ -495,14 +496,16 @@ Native execution and control require an owner or an `operator.admin`
495496
Gateway client: binding or resuming threads, sending or stopping turns,
496497
changing model, fast-mode, or permission state, compacting or reviewing, and
497498
detaching a binding. Other authorized senders keep read-only status, help,
498-
account, model, thread, MCP server, skill, and binding inspection commands.
499+
account, model, thread, native goal, MCP server, skill, and binding inspection
500+
commands.
499501

500502
Common forms:
501503

502504
- `/codex status` checks app-server connectivity, models, account, rate
503505
limits, MCP servers, and skills.
504506
- `/codex models` lists live Codex app-server models.
505507
- `/codex threads [filter]` lists recent Codex app-server threads.
508+
- `/codex goal` reads or updates the attached thread's native Codex goal. Codex automatic goal continuation stays disabled; OpenClaw does not own autonomous follow-on turns yet.
506509
- `/codex resume <thread-id>` attaches the current OpenClaw session to an
507510
existing Codex thread.
508511
- `/codex bind [thread-id] [--cwd <path>] [--model <model>] [--provider <provider>]`
@@ -629,7 +632,9 @@ normal user-home state.
629632
Codex dynamic tools default to `searchable` loading. OpenClaw normally does
630633
not expose dynamic tools that duplicate Codex-native workspace operations:
631634
`read`, `write`, `edit`, `apply_patch`, `exec`, `process`, `update_plan`,
632-
`tool_call`, `tool_describe`, `tool_search`, and `tool_search_code`. Most
635+
`get_goal`, `create_goal`, `update_goal`, `tool_call`, `tool_describe`,
636+
`tool_search`, and `tool_search_code`. Goal operations stay native to Codex,
637+
so OpenClaw does not project a second goal store into Codex turns. Most
633638
remaining OpenClaw integration tools, such as messaging, media, cron,
634639
browser, nodes, gateway, and `heartbeat_respond`, are available through
635640
Codex tool search under the `openclaw` namespace, keeping the initial model

extensions/codex/src/app-server/capabilities.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ export const CODEX_CONTROL_METHODS = {
2020
resumeThread: "thread/resume",
2121
review: "review/start",
2222
unarchiveThread: "thread/unarchive",
23+
getThreadGoal: "thread/goal/get",
24+
setThreadGoal: "thread/goal/set",
25+
clearThreadGoal: "thread/goal/clear",
2326
} as const;
2427

2528
type CodexControlName = keyof typeof CODEX_CONTROL_METHODS;

extensions/codex/src/app-server/dynamic-tool-build.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,9 @@ describe("Codex app-server dynamic tool build", () => {
192192
"exec",
193193
"process",
194194
"update_plan",
195+
"get_goal",
196+
"create_goal",
197+
"update_goal",
195198
"tool_call",
196199
"tool_describe",
197200
"tool_search",
@@ -503,7 +506,9 @@ describe("Codex app-server dynamic tool build", () => {
503506
});
504507

505508
it("exposes app-server-owned tools directly for forced private QA Codex runtime", () => {
506-
const tools = ["read", "write", "image_generate", "message"].map((name) => ({ name }));
509+
const tools = ["read", "write", "get_goal", "image_generate", "message"].map((name) => ({
510+
name,
511+
}));
507512
const privateQaCodexEnv = {
508513
OPENCLAW_BUILD_PRIVATE_QA: "1",
509514
OPENCLAW_QA_FORCE_RUNTIME: "codex",

extensions/codex/src/app-server/dynamic-tool-profile.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ const CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES = [
2121
"tool_search",
2222
"tool_search_code",
2323
] as const;
24+
const CODEX_NATIVE_GOAL_TOOL_EXCLUDES = ["get_goal", "create_goal", "update_goal"] as const;
2425
const CODEX_APP_SERVER_OWNED_SHELL_TOOL_EXCLUDES = new Set(["exec", "process"]);
2526

2627
const DYNAMIC_TOOL_NAME_ALIASES: Record<string, string> = {
@@ -138,6 +139,9 @@ function filterCodexDynamicToolsWithOptions<T extends { name: string }>(
138139
options: { preserveOpenClawShell: boolean },
139140
): T[] {
140141
const excludes = new Set<string>();
142+
for (const name of CODEX_NATIVE_GOAL_TOOL_EXCLUDES) {
143+
excludes.add(name);
144+
}
141145
if (!isForcedPrivateQaCodexRuntime(env)) {
142146
for (const name of CODEX_APP_SERVER_OWNED_DYNAMIC_TOOL_EXCLUDES) {
143147
if (options.preserveOpenClawShell && CODEX_APP_SERVER_OWNED_SHELL_TOOL_EXCLUDES.has(name)) {

extensions/codex/src/app-server/protocol.ts

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,38 @@ export type CodexThreadResumeResponse = {
256256
modelProvider?: string | null;
257257
};
258258

259+
type CodexThreadGoalStatus =
260+
| "active"
261+
| "paused"
262+
| "blocked"
263+
| "usageLimited"
264+
| "budgetLimited"
265+
| "complete";
266+
267+
type CodexThreadGoal = {
268+
threadId: string;
269+
objective: string;
270+
status: CodexThreadGoalStatus;
271+
tokenBudget: number | null;
272+
tokensUsed: number;
273+
timeUsedSeconds: number;
274+
createdAt: number;
275+
updatedAt: number;
276+
};
277+
278+
type CodexThreadGoalSetParams = JsonObject & {
279+
threadId: string;
280+
objective?: string;
281+
status?: CodexThreadGoalStatus;
282+
tokenBudget?: number | null;
283+
};
284+
285+
type CodexThreadGoalGetParams = JsonObject & { threadId: string };
286+
type CodexThreadGoalClearParams = JsonObject & { threadId: string };
287+
type CodexThreadGoalSetResponse = { goal: CodexThreadGoal };
288+
type CodexThreadGoalGetResponse = { goal: CodexThreadGoal | null };
289+
type CodexThreadGoalClearResponse = { cleared: boolean };
290+
259291
type CodexThreadInjectItemsParams = JsonObject & {
260292
threadId: string;
261293
items: JsonValue[];
@@ -702,6 +734,9 @@ type CodexAppServerRequestParamsOverride = {
702734
"thread/start": CodexThreadStartParams;
703735
"thread/unarchive": CodexThreadArchiveParams;
704736
"thread/unsubscribe": CodexThreadUnsubscribeParams;
737+
"thread/goal/set": CodexThreadGoalSetParams;
738+
"thread/goal/get": CodexThreadGoalGetParams;
739+
"thread/goal/clear": CodexThreadGoalClearParams;
705740
"turn/interrupt": CodexTurnInterruptParams;
706741
};
707742

@@ -739,6 +774,9 @@ type CodexAppServerRequestResultMap = {
739774
"thread/start": CodexThreadStartResponse;
740775
"thread/unarchive": CodexThreadUnarchiveResponse;
741776
"thread/unsubscribe": JsonValue;
777+
"thread/goal/set": CodexThreadGoalSetResponse;
778+
"thread/goal/get": CodexThreadGoalGetResponse;
779+
"thread/goal/clear": CodexThreadGoalClearResponse;
742780
"turn/interrupt": JsonValue;
743781
"turn/start": CodexTurnStartResponse;
744782
"turn/steer": JsonValue;

extensions/codex/src/app-server/thread-lifecycle.test.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,13 +79,15 @@ describe("Codex ring-zero thread config", () => {
7979
cwd: "/repo",
8080
dynamicTools: [],
8181
hostSystemAgentActive: false,
82+
config: { "features.goals": true },
8283
});
8384
expect(normal.baseInstructions).toBeUndefined();
85+
expect(normal.config?.["features.goals"]).toBe(false);
8486
});
8587
});
8688

8789
describe("Codex delegation capability", () => {
88-
it("disables native delegation on start and resume without disabling other tools", () => {
90+
it("disables native delegation and goal continuation on start and resume", () => {
8991
const params = createAttemptParams({ provider: "openai" });
9092
params.delegationCapability = "report_only";
9193
const appServer = createAppServerOptions() as never;
@@ -110,7 +112,7 @@ describe("Codex delegation capability", () => {
110112
for (const request of [start, resume]) {
111113
expect(request.config?.["features.multi_agent"]).toBe(false);
112114
expect(request.config?.["features.multi_agent_v2"]).toBe(false);
113-
expect(request.config?.["features.goals"]).toBe(true);
115+
expect(request.config?.["features.goals"]).toBe(false);
114116
}
115117
});
116118
});
@@ -638,6 +640,7 @@ describe("Codex app-server native code mode config", () => {
638640
},
639641
"features.code_mode": true,
640642
"features.code_mode_only": false,
643+
"features.goals": false,
641644
"features.apply_patch_streaming_events": true,
642645
"features.standalone_web_search": false,
643646
web_search: "cached",
@@ -782,6 +785,7 @@ describe("Codex app-server native code mode config", () => {
782785
expect(request.config).toEqual({
783786
"features.code_mode": true,
784787
"features.code_mode_only": false,
788+
"features.goals": false,
785789
"features.apply_patch_streaming_events": true,
786790
"features.multi_agent": false,
787791
"features.standalone_web_search": false,
@@ -893,6 +897,7 @@ describe("Codex app-server native code mode config", () => {
893897
expect(request.config).toEqual({
894898
"features.code_mode": true,
895899
"features.code_mode_only": true,
900+
"features.goals": false,
896901
"features.apply_patch_streaming_events": true,
897902
"features.standalone_web_search": false,
898903
web_search: "cached",
@@ -914,6 +919,7 @@ describe("Codex app-server native code mode config", () => {
914919
expect(request.config).toEqual({
915920
"features.code_mode": true,
916921
"features.code_mode_only": true,
922+
"features.goals": false,
917923
"features.apply_patch_streaming_events": true,
918924
"features.standalone_web_search": false,
919925
web_search: "cached",
@@ -971,6 +977,7 @@ describe("Codex app-server native code mode config", () => {
971977
expect(request.config).toEqual({
972978
"features.code_mode": true,
973979
"features.code_mode_only": false,
980+
"features.goals": false,
974981
"features.apply_patch_streaming_events": true,
975982
"features.standalone_web_search": false,
976983
web_search: "cached",
@@ -995,6 +1002,7 @@ describe("Codex app-server native code mode config", () => {
9951002
expect(request.config).toEqual({
9961003
"features.code_mode": false,
9971004
"features.code_mode_only": false,
1005+
"features.goals": false,
9981006
"features.standalone_web_search": false,
9991007
web_search: "disabled",
10001008
});
@@ -1014,6 +1022,7 @@ describe("Codex app-server native code mode config", () => {
10141022
expect(request.config).toEqual({
10151023
"features.code_mode": false,
10161024
"features.code_mode_only": false,
1025+
"features.goals": false,
10171026
"features.standalone_web_search": false,
10181027
web_search: "disabled",
10191028
});
@@ -1043,6 +1052,7 @@ describe("Codex app-server native code mode config", () => {
10431052
"features.hooks": true,
10441053
"features.code_mode": true,
10451054
"features.code_mode_only": false,
1055+
"features.goals": false,
10461056
"features.apply_patch_streaming_events": true,
10471057
"features.standalone_web_search": false,
10481058
web_search: "cached",
@@ -1066,6 +1076,7 @@ describe("Codex app-server native code mode config", () => {
10661076
project_doc_max_bytes: 64_000,
10671077
"features.code_mode": true,
10681078
"features.code_mode_only": false,
1079+
"features.goals": false,
10691080
"features.apply_patch_streaming_events": true,
10701081
"features.standalone_web_search": false,
10711082
web_search: "cached",
@@ -1218,6 +1229,7 @@ describe("Codex app-server turn params", () => {
12181229
config: {
12191230
"features.code_mode": true,
12201231
"features.code_mode_only": false,
1232+
"features.goals": false,
12211233
"features.apply_patch_streaming_events": true,
12221234
"features.standalone_web_search": false,
12231235
web_search: "cached",

extensions/codex/src/app-server/thread-requests.ts

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ const CODEX_CODE_MODE_THREAD_CONFIG: JsonObject = {
3838
"features.apply_patch_streaming_events": true,
3939
};
4040

41+
const CODEX_GOAL_CONTINUATION_DISABLED_THREAD_CONFIG: JsonObject = {
42+
"features.goals": false,
43+
};
44+
4145
const CODEX_CODE_MODE_DISABLED_THREAD_CONFIG: JsonObject = {
4246
"features.code_mode": false,
4347
"features.code_mode_only": false,
@@ -265,6 +269,8 @@ export function buildCodexRuntimeThreadConfig(
265269
directOnlyToolNamespaces?: readonly string[];
266270
} = {},
267271
): JsonObject {
272+
// Native goal RPCs remain available through app-server, but the Codex goals
273+
// feature also starts autonomous turns. Keep it disabled until a run owner exists.
268274
const codeModeConfig: JsonObject = {
269275
...CODEX_CODE_MODE_THREAD_CONFIG,
270276
"features.code_mode_only": options.nativeCodeModeOnlyEnabled === true,
@@ -273,6 +279,7 @@ export function buildCodexRuntimeThreadConfig(
273279
const disabledConfig = mergeCodexThreadConfigs(
274280
config,
275281
CODEX_CODE_MODE_DISABLED_THREAD_CONFIG,
282+
CODEX_GOAL_CONTINUATION_DISABLED_THREAD_CONFIG,
276283
) ?? {
277284
...CODEX_CODE_MODE_DISABLED_THREAD_CONFIG,
278285
};
@@ -282,16 +289,27 @@ export function buildCodexRuntimeThreadConfig(
282289
return disabledConfig;
283290
}
284291
if (options.nativeCodeModeOnlyEnabled === true) {
285-
const merged = mergeCodexThreadConfigs(codeModeConfig, config, {
286-
"features.code_mode_only": true,
287-
}) ?? {
292+
const merged = mergeCodexThreadConfigs(
293+
codeModeConfig,
294+
config,
295+
CODEX_GOAL_CONTINUATION_DISABLED_THREAD_CONFIG,
296+
{
297+
"features.code_mode_only": true,
298+
},
299+
) ?? {
288300
...codeModeConfig,
301+
...CODEX_GOAL_CONTINUATION_DISABLED_THREAD_CONFIG,
289302
"features.code_mode_only": true,
290303
};
291304
return ensureDirectOnlyToolNamespaces(merged, options.directOnlyToolNamespaces);
292305
}
293-
const merged = mergeCodexThreadConfigs(codeModeConfig, config) ?? {
306+
const merged = mergeCodexThreadConfigs(
307+
codeModeConfig,
308+
config,
309+
CODEX_GOAL_CONTINUATION_DISABLED_THREAD_CONFIG,
310+
) ?? {
294311
...codeModeConfig,
312+
...CODEX_GOAL_CONTINUATION_DISABLED_THREAD_CONFIG,
295313
};
296314
return ensureDirectOnlyToolNamespaces(merged, options.directOnlyToolNamespaces);
297315
}

0 commit comments

Comments
 (0)