humanish 0.139.0 → 0.139.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,16 @@ The Unreleased section holds the full notes for the next version until it is tag
8
8
 
9
9
  ## Unreleased
10
10
 
11
+ ## 0.139.1: Hosted Codex participants work with an AGENTS.md in the Codex home (2026-10-21)
12
+
13
+ humanish 0.139.1 fixes hosted Codex participants for operators whose Codex home
14
+ (`CODEX_HOME`, or `~/.codex`) holds an `AGENTS.md`. Codex adds that file to every thread, and
15
+ humanish refused the launch as `codex_unsafe_configuration` with no reason. humanish now admits
16
+ that file, leaves it unchanged, and says the participant also received it. Every other
17
+ `codex_unsafe_configuration` refusal names the setting it refused.
18
+
19
+ [Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.139.1)
20
+
11
21
  ## 0.139.0: Computer-use studies can run on Modal desktops (2026-10-19)
12
22
 
13
23
  humanish 0.139.0 can run a computer-use study's desktops on Modal. Set
@@ -0,0 +1,5 @@
1
+ /**
2
+ * The note for an instruction file Codex added from the operator's Codex home: the participant
3
+ * also read it, so its persona may follow the operator's instructions.
4
+ */
5
+ export declare function operatorInstructionsWarning(files: readonly string[] | undefined): string | undefined;
@@ -0,0 +1,9 @@
1
+ /**
2
+ * The note for an instruction file Codex added from the operator's Codex home: the participant
3
+ * also read it, so its persona may follow the operator's instructions.
4
+ */
5
+ export function operatorInstructionsWarning(files) {
6
+ if (files === undefined || files.length === 0)
7
+ return undefined;
8
+ return `Codex also gave this participant ${files.join(" and ")} from your Codex home (CODEX_HOME, or ~/.codex), as it does for every Codex thread. The participant may follow those instructions as well as its persona. humanish does not change that file.`;
9
+ }
@@ -53,15 +53,20 @@ export interface LaunchState {
53
53
  protocolIncompatibilities: string[] | undefined;
54
54
  /** Schema values beyond the baseline, recorded at launch. */
55
55
  protocolAdditions: string[] | undefined;
56
+ /** What in the operator's Codex setup a refusal objects to, without paths or values. */
57
+ refusalReason: string | undefined;
58
+ /** The instruction file names Codex added from the operator's Codex home (operator auth). */
59
+ operatorInstructionFiles: string[] | undefined;
56
60
  }
57
61
  /** An admitted value, or the refusal a launch stops with. */
58
62
  export type Admission<T> = {
59
63
  readonly value: T;
60
64
  } | {
61
65
  readonly refusal: RestrictedCodexAnalysisErrorCode;
66
+ readonly reason?: string;
62
67
  };
63
- /** The admitted value; a refusal stops the launch. */
64
- export declare function admitted<T>(admission: Admission<T>): T;
68
+ /** The admitted value; a refusal stops the launch and records its reason on the state. */
69
+ export declare function admitted<T>(admission: Admission<T>, state: Pick<LaunchState, "refusalReason">): T;
65
70
  /** `-c key=value` arguments for an operator-auth launch, which keeps the operator's config file. */
66
71
  export declare function configArguments(overrides: Record<string, unknown>): string[];
67
72
  /** initialize: the admitted release, this platform, and (isolated auth) the private home. */
@@ -109,9 +114,12 @@ export declare function admitThread(reply: Record<string, unknown>, expected: {
109
114
  cwd: string;
110
115
  reasoningEffort: ReasoningEffort;
111
116
  cliVersion: string;
117
+ /** Operator auth: the Codex home directories whose own instruction file is admitted. */
118
+ operatorHomes?: readonly string[];
112
119
  }): Admission<{
113
120
  model: string;
114
121
  threadId: string;
122
+ instructionFiles: string[];
115
123
  }>;
116
124
  /** mcpServerStatus/list (isolated auth): no MCP server is running for the thread. */
117
125
  export declare function admitMcpStatus(reply: Record<string, unknown>): Admission<void>;
@@ -10,12 +10,14 @@ import path from "node:path";
10
10
  import { checkVersion, childEnvironment, generateProtocolSchema, resolveExecutable, } from "./restricted-executable.js";
11
11
  import { checkProtocol, loadProtocolSchema } from "./protocol-compat.js";
12
12
  import { protocolContract } from "./protocol-contract.js";
13
- import { admitsRestrictedCodexConfig, admitsRestrictedCodexThread, codexRecord, restrictedCodexConfig, } from "./restricted-policy.js";
13
+ import { restrictedCodexConfigViolation, admitsRestrictedCodexThread, codexRecord, restrictedCodexConfig, } from "./restricted-policy.js";
14
14
  import { RestrictedCodexDeadline, RestrictedCodexStop, RestrictedCodexTransport, ownCodexProcess, } from "./restricted-transport.js";
15
- /** The admitted value; a refusal stops the launch. */
16
- export function admitted(admission) {
17
- if ("refusal" in admission)
15
+ /** The admitted value; a refusal stops the launch and records its reason on the state. */
16
+ export function admitted(admission, state) {
17
+ if ("refusal" in admission) {
18
+ state.refusalReason = admission.reason;
18
19
  throw new RestrictedCodexStop(admission.refusal);
20
+ }
19
21
  return admission.value;
20
22
  }
21
23
  /** `-c key=value` arguments for an operator-auth launch, which keeps the operator's config file. */
@@ -37,8 +39,12 @@ export function admitInitialize(reply, expected) {
37
39
  }
38
40
  /** config/read: the restricted configuration, and the model it selects, if any. */
39
41
  export function admitEffectiveConfig(reply, configPath, requestedModel, mode) {
40
- if (!admitsRestrictedCodexConfig(reply, configPath, requestedModel, mode))
41
- return { refusal: "codex_unsafe_configuration" };
42
+ const violation = restrictedCodexConfigViolation(reply, configPath, requestedModel, mode);
43
+ if (violation !== null)
44
+ return {
45
+ refusal: "codex_unsafe_configuration",
46
+ reason: `in the effective Codex config, ${violation}`,
47
+ };
42
48
  const configuredModel = codexRecord(reply.config).model;
43
49
  return { value: typeof configuredModel === "string" ? configuredModel : undefined };
44
50
  }
@@ -60,8 +66,16 @@ export function admitAccount(reply, operatorAuth) {
60
66
  */
61
67
  export function threadConfigFor(effectiveConfig, overrides) {
62
68
  const mcpNames = Object.keys(codexRecord(codexRecord(effectiveConfig.config).mcp_servers));
63
- if (mcpNames.length > 100 || mcpNames.some((name) => !/^[A-Za-z0-9_-]{1,200}$/.test(name)))
64
- return { refusal: "codex_unsafe_configuration" };
69
+ if (mcpNames.length > 100)
70
+ return {
71
+ refusal: "codex_unsafe_configuration",
72
+ reason: "more than 100 MCP servers are configured",
73
+ };
74
+ if (mcpNames.some((name) => !/^[A-Za-z0-9_-]{1,200}$/.test(name)))
75
+ return {
76
+ refusal: "codex_unsafe_configuration",
77
+ reason: "an MCP server name uses characters other than letters, digits, _ and -",
78
+ };
65
79
  const threadConfig = { ...overrides };
66
80
  for (const name of mcpNames)
67
81
  threadConfig[`mcp_servers.${name}.enabled`] = false;
@@ -101,14 +115,46 @@ export function threadStartParams(args) {
101
115
  }
102
116
  /** thread/start's reply: the model it runs (the selected one, when one was) and the thread id. */
103
117
  export function admitThread(reply, expected) {
118
+ const sources = Array.isArray(reply.instructionSources)
119
+ ? reply.instructionSources
120
+ : [];
121
+ // Codex adds the instruction file in its home to every thread, with no setting to skip it, and
122
+ // humanish never edits the operator's files. An operator-auth launch admits that one file and
123
+ // reports it; anything else stays refused. Basenames keep the operator's paths out of the run.
124
+ const homes = expected.operatorHomes ?? [];
125
+ const homeFile = (source) => typeof source === "string" &&
126
+ homes.includes(path.dirname(source)) &&
127
+ ["AGENTS.md", "AGENTS.override.md"].includes(path.basename(source));
128
+ const other = sources.filter((source) => !homeFile(source));
129
+ if (other.length > 0)
130
+ return {
131
+ refusal: "codex_unsafe_configuration",
132
+ reason: `Codex loaded instructions from ${other
133
+ .map((source) => (typeof source === "string" ? path.basename(source) : "a file"))
134
+ .join(", ")}`,
135
+ };
104
136
  const returnedModel = codexRecord(reply.thread).model;
105
137
  if (typeof returnedModel !== "string" ||
106
138
  returnedModel.length === 0 ||
107
139
  returnedModel.length > 200 ||
108
140
  (expected.selectedModel !== undefined && returnedModel !== expected.selectedModel) ||
109
- !admitsRestrictedCodexThread(reply, returnedModel, expected.cwd, expected.reasoningEffort, expected.cliVersion))
141
+ !admitsRestrictedCodexThread({ ...reply, instructionSources: [] }, returnedModel, expected.cwd, expected.reasoningEffort, expected.cliVersion))
110
142
  return { refusal: "codex_unsafe_configuration" };
111
- return { value: { model: returnedModel, threadId: String(codexRecord(reply.thread).id) } };
143
+ return {
144
+ value: {
145
+ model: returnedModel,
146
+ threadId: String(codexRecord(reply.thread).id),
147
+ instructionFiles: sources.filter(homeFile).map((source) => path.basename(source)),
148
+ },
149
+ };
150
+ }
151
+ /** The operator's Codex home as configured and as resolved, for matching its instruction file. */
152
+ async function operatorCodexHomes(options, sourceEnv) {
153
+ const configured = options.authHome ?? sourceEnv.CODEX_HOME ?? path.join(sourceEnv.HOME ?? homedir(), ".codex");
154
+ if (!path.isAbsolute(configured))
155
+ return [];
156
+ const resolved = await realpath(configured).catch(() => configured);
157
+ return [...new Set([configured, resolved])];
112
158
  }
113
159
  /** mcpServerStatus/list (isolated auth): no MCP server is running for the thread. */
114
160
  export function admitMcpStatus(reply) {
@@ -223,15 +269,15 @@ export async function launchAdmittedAppServer(settings, state, request, deadline
223
269
  home,
224
270
  operatorAuth,
225
271
  platform: settings.platform,
226
- }));
272
+ }), state);
227
273
  launched.notify("initialized", {});
228
274
  enter("config/read");
229
275
  const effective = await launched.rpc("config/read", { includeLayers: true, cwd: state.cwd });
230
- const selectedModel = admitted(admitEffectiveConfig(effective, configPath, request.model, configMode));
276
+ const selectedModel = admitted(admitEffectiveConfig(effective, configPath, request.model, configMode), state);
231
277
  enter("account/read");
232
278
  const account = await launched.rpc("account/read", { refreshToken: false });
233
- state.authentication = admitted(admitAccount(account, operatorAuth));
234
- const threadConfig = admitted(threadConfigFor(effective, config.overrides));
279
+ state.authentication = admitted(admitAccount(account, operatorAuth), state);
280
+ const threadConfig = admitted(threadConfigFor(effective, config.overrides), state);
235
281
  enter("thread/start");
236
282
  const thread = await launched.rpc("thread/start", threadStartParams({
237
283
  cwd: state.cwd,
@@ -245,12 +291,15 @@ export async function launchAdmittedAppServer(settings, state, request, deadline
245
291
  cwd: state.cwd,
246
292
  reasoningEffort,
247
293
  cliVersion: state.cliVersion,
248
- }));
294
+ ...(operatorAuth ? { operatorHomes: await operatorCodexHomes(options, sourceEnv) } : {}),
295
+ }), state);
249
296
  state.resolvedModel = started.model;
297
+ if (started.instructionFiles.length > 0)
298
+ state.operatorInstructionFiles = started.instructionFiles;
250
299
  state.threadId = started.threadId;
251
300
  if (!operatorAuth) {
252
301
  enter("mcpServerStatus/list");
253
- admitted(admitMcpStatus(await launched.rpc("mcpServerStatus/list", { limit: 100 })));
302
+ admitted(admitMcpStatus(await launched.rpc("mcpServerStatus/list", { limit: 100 })), state);
254
303
  }
255
304
  state.identity = {
256
305
  requestedModel: request.model,
@@ -35,6 +35,10 @@ export interface ParticipantReadiness {
35
35
  protocolIncompatibilities?: readonly string[];
36
36
  /** Schema values beyond the baseline, recorded by a launch that passed. */
37
37
  protocolAdditions?: readonly string[];
38
+ /** What in the operator's Codex setup the launch refused, without paths or values. */
39
+ refusalReason?: string;
40
+ /** The instruction file names Codex added from the operator's Codex home. */
41
+ operatorInstructionFiles?: readonly string[];
38
42
  /** The model thread/start resolved from the operator's configuration or the declared model. */
39
43
  resolvedModel?: string;
40
44
  authentication?: "chatgpt-account" | "api-key";
@@ -7,6 +7,7 @@ import { resolveWaitSettings } from "../computer-use/wait.js";
7
7
  import { createRestrictedCodexSession, } from "./restricted-session.js";
8
8
  import { PARTICIPANT_PROFILE, PARTICIPANT_LIMITS as L, PARTICIPANT_FINAL_SCHEMA, participantToolSchema, parseParticipantTool, parseParticipantFinal, } from "./restricted-participant-policy.js";
9
9
  import { truncatedFrameWarning, unknownNotificationsWarning } from "./restricted-notifications.js";
10
+ import { operatorInstructionsWarning } from "./operator-instructions.js";
10
11
  import { protocolAdditionsWarning, protocolIncompatibilityMessage } from "./protocol-compat.js";
11
12
  const codeOf = (code) => {
12
13
  if (code === "cancelled" || code === "timeout" || code === "refusal")
@@ -236,6 +237,10 @@ export async function checkRestrictedCodexParticipantReadiness(options) {
236
237
  ...(session.protocolAdditions === undefined || session.protocolAdditions.length === 0
237
238
  ? {}
238
239
  : { protocolAdditions: session.protocolAdditions }),
240
+ ...(session.refusalReason === undefined ? {} : { refusalReason: session.refusalReason }),
241
+ ...(session.operatorInstructionFiles === undefined
242
+ ? {}
243
+ : { operatorInstructionFiles: session.operatorInstructionFiles }),
239
244
  ...(session.resolvedModel === undefined ? {} : { resolvedModel: session.resolvedModel }),
240
245
  ...(session.authentication === undefined ? {} : { authentication: session.authentication }),
241
246
  };
@@ -298,6 +303,10 @@ function sessionReport(session, operator) {
298
303
  ? undefined
299
304
  : protocolIncompatibilityMessage(session.cliVersion, session.protocolIncompatibilities),
300
305
  protocolAdditionsWarning(session.cliVersion, session.protocolAdditions),
306
+ session.refusalReason === undefined
307
+ ? undefined
308
+ : `humanish stopped the Codex launch because ${session.refusalReason}.`,
309
+ operatorInstructionsWarning(session.operatorInstructionFiles),
301
310
  unknownNotificationsWarning(session.unknownNotifications, session.cliVersion),
302
311
  truncatedFrameWarning(session.truncatedFrameBytes),
303
312
  ].filter((warning) => warning !== undefined);
@@ -64,8 +64,11 @@ export declare function restrictedCodexConfig(model: string | undefined, mode?:
64
64
  toml: string;
65
65
  overrides: Record<string, unknown>;
66
66
  };
67
- /** Inspect before thread/start, which could otherwise start inherited integrations. */
68
- export declare function admitsRestrictedCodexConfig(raw: unknown, configPath: string, model: string | undefined, mode?: RestrictedCodexConfigMode): boolean;
67
+ /**
68
+ * Inspect before thread/start, which could otherwise start inherited integrations. Returns the
69
+ * first setting that does not match, named by its config key and never by its value, or null.
70
+ */
71
+ export declare function restrictedCodexConfigViolation(raw: unknown, configPath: string, model: string | undefined, mode?: RestrictedCodexConfigMode): string | null;
69
72
  export declare function admitsRestrictedCodexThread(raw: unknown, model: string, cwd: string, reasoningEffort: ReasoningEffort, cliVersion: string): boolean;
70
73
  export declare function restrictedCodexRequestError(request: RestrictedCodexRequest, operatorParticipant?: boolean): RestrictedCodexAnalysisErrorCode | null;
71
74
  export declare function restrictedCodexUsage(raw: unknown): RestrictedCodexUsage | null;
@@ -98,11 +98,14 @@ max_depth = 1
98
98
  `;
99
99
  return { toml, overrides };
100
100
  }
101
- /** Inspect before thread/start, which could otherwise start inherited integrations. */
102
- export function admitsRestrictedCodexConfig(raw, configPath, model, mode = {}) {
101
+ /**
102
+ * Inspect before thread/start, which could otherwise start inherited integrations. Returns the
103
+ * first setting that does not match, named by its config key and never by its value, or null.
104
+ */
105
+ export function restrictedCodexConfigViolation(raw, configPath, model, mode = {}) {
103
106
  const root = codexRecord(raw), config = codexRecord(root.config), features = codexRecord(config.features);
104
107
  if (!Array.isArray(root.layers) || root.layers.length === 0)
105
- return false;
108
+ return "config/read reported no config layers";
106
109
  if (!mode.operatorAuth) {
107
110
  let ownLayer = false;
108
111
  for (const rawLayer of root.layers) {
@@ -112,14 +115,14 @@ export function admitsRestrictedCodexConfig(raw, configPath, model, mode = {}) {
112
115
  name.profile === null &&
113
116
  (layer.disabledReason === null || layer.disabledReason === undefined)) {
114
117
  if (ownLayer)
115
- return false;
118
+ return "humanish's config layer appears twice";
116
119
  ownLayer = true;
117
120
  }
118
121
  else if (name.type !== "system" || !empty(layer.config))
119
- return false;
122
+ return `a ${String(name.type)} config layer is active`;
120
123
  }
121
124
  if (!ownLayer)
122
- return false;
125
+ return "humanish's config layer is missing";
123
126
  }
124
127
  const reasoningEffort = mode.reasoningEffort ?? "low", codeMode = mode.participantCodeMode === true;
125
128
  const inheritedModel = config.model;
@@ -130,77 +133,92 @@ export function admitsRestrictedCodexConfig(raw, configPath, model, mode = {}) {
130
133
  inheritedModel.length > 0 &&
131
134
  inheritedModel.length <= 200)
132
135
  : inheritedModel === model;
133
- if (!modelAdmitted ||
134
- config.model_provider !== "openai" ||
135
- config.model_reasoning_effort !== reasoningEffort ||
136
- (!mode.operatorAuth && config.forced_login_method !== "chatgpt") ||
137
- config.sandbox_mode !== "read-only" ||
138
- config.approval_policy !== "never" ||
139
- config.project_doc_max_bytes !== 0 ||
140
- config.web_search !== "disabled" ||
141
- features.skip_host_skill_discovery !== true ||
142
- codexRecord(config.history).persistence !== "none" ||
143
- codexRecord(config.analytics).enabled !== false ||
144
- disabledFeatures.some((name) => features[name] !== false) ||
145
- codeModeFeatures.some((name) => features[name] !== codeMode))
146
- return false;
147
136
  const agents = codexRecord(config.agents), skills = codexRecord(config.skills);
148
- if (agents.enabled !== false ||
149
- agents.max_concurrent_threads_per_session !== 1 ||
150
- agents.max_depth !== 1 ||
151
- codexRecord(skills.bundled).enabled !== false ||
152
- Object.keys(skills).some((key) => key !== "bundled"))
153
- return false;
137
+ const pinned = [
138
+ ["model", modelAdmitted],
139
+ ["model_provider", config.model_provider === "openai"],
140
+ ["model_reasoning_effort", config.model_reasoning_effort === reasoningEffort],
141
+ ["forced_login_method", mode.operatorAuth === true || config.forced_login_method === "chatgpt"],
142
+ ["sandbox_mode", config.sandbox_mode === "read-only"],
143
+ ["approval_policy", config.approval_policy === "never"],
144
+ ["project_doc_max_bytes", config.project_doc_max_bytes === 0],
145
+ ["web_search", config.web_search === "disabled"],
146
+ ["features.skip_host_skill_discovery", features.skip_host_skill_discovery === true],
147
+ ["history.persistence", codexRecord(config.history).persistence === "none"],
148
+ ["analytics.enabled", codexRecord(config.analytics).enabled === false],
149
+ ...disabledFeatures.map((name) => [
150
+ `features.${name}`,
151
+ features[name] === false,
152
+ ]),
153
+ ...codeModeFeatures.map((name) => [
154
+ `features.${name}`,
155
+ features[name] === codeMode,
156
+ ]),
157
+ ["agents.enabled", agents.enabled === false],
158
+ ["agents.max_threads", agents.max_concurrent_threads_per_session === 1],
159
+ ["agents.max_depth", agents.max_depth === 1],
160
+ ["skills.bundled.enabled", codexRecord(skills.bundled).enabled === false],
161
+ ];
162
+ const mismatch = pinned.find(([, ok]) => !ok)?.[0];
163
+ if (mismatch !== undefined)
164
+ return `${mismatch} is not the value humanish sets`;
165
+ const extraSkills = Object.keys(skills).find((key) => key !== "bundled");
166
+ if (extraSkills !== undefined)
167
+ return `skills.${extraSkills} is set`;
154
168
  if (!mode.operatorAuth) {
155
- for (const key of [
156
- "mcp_servers",
157
- "plugins",
158
- "hooks",
159
- "notify",
160
- "instructions",
161
- "developer_instructions",
162
- "permissions",
163
- "profiles",
164
- "projects",
165
- "model_providers",
166
- "experimental_compact_prompt_file",
167
- "experimental_model_instructions_file",
168
- "model_instructions_file",
169
- "model_catalog_json",
170
- "openai_base_url",
171
- "orchestrator",
172
- "experimental_thread_store",
173
- "experimental_thread_store_endpoint",
174
- "experimental_realtime_ws_base_url",
175
- "experimental_realtime_webrtc_call_base_url",
176
- "forced_chatgpt_workspace_id",
177
- "log_dir",
178
- "sqlite_home",
179
- "otel",
180
- "marketplaces",
181
- "js_repl_node_path",
182
- "js_repl_node_module_dirs",
183
- "responses_api_metadata",
184
- "compact_prompt",
185
- "profile",
186
- "default_permissions",
187
- "auto_review",
188
- "apps",
189
- "browser_use",
190
- "computer_use",
191
- "desktop",
192
- "memories",
193
- "realtime",
194
- ])
195
- if (!empty(config[key]))
196
- return false;
169
+ const inherited = isolatedUnsetKeys.find((key) => !empty(config[key]));
170
+ if (inherited !== undefined)
171
+ return `${inherited} is set`;
197
172
  if (Object.values(codexRecord(config.shell_environment_policy)).some((value) => !empty(value)))
198
- return false;
173
+ return "shell_environment_policy is set";
174
+ if (config.cli_auth_credentials_store !== "file")
175
+ return "cli_auth_credentials_store is not file";
176
+ if (config.chatgpt_base_url !== "https://chatgpt.com/backend-api/")
177
+ return "chatgpt_base_url is not the ChatGPT backend";
199
178
  }
200
- return (mode.operatorAuth ||
201
- (config.cli_auth_credentials_store === "file" &&
202
- config.chatgpt_base_url === "https://chatgpt.com/backend-api/"));
179
+ return null;
203
180
  }
181
+ /** Settings an isolated launch requires to be unset: each could add tools, instructions or egress. */
182
+ const isolatedUnsetKeys = [
183
+ "mcp_servers",
184
+ "plugins",
185
+ "hooks",
186
+ "notify",
187
+ "instructions",
188
+ "developer_instructions",
189
+ "permissions",
190
+ "profiles",
191
+ "projects",
192
+ "model_providers",
193
+ "experimental_compact_prompt_file",
194
+ "experimental_model_instructions_file",
195
+ "model_instructions_file",
196
+ "model_catalog_json",
197
+ "openai_base_url",
198
+ "orchestrator",
199
+ "experimental_thread_store",
200
+ "experimental_thread_store_endpoint",
201
+ "experimental_realtime_ws_base_url",
202
+ "experimental_realtime_webrtc_call_base_url",
203
+ "forced_chatgpt_workspace_id",
204
+ "log_dir",
205
+ "sqlite_home",
206
+ "otel",
207
+ "marketplaces",
208
+ "js_repl_node_path",
209
+ "js_repl_node_module_dirs",
210
+ "responses_api_metadata",
211
+ "compact_prompt",
212
+ "profile",
213
+ "default_permissions",
214
+ "auto_review",
215
+ "apps",
216
+ "browser_use",
217
+ "computer_use",
218
+ "desktop",
219
+ "memories",
220
+ "realtime",
221
+ ];
204
222
  export function admitsRestrictedCodexThread(raw, model, cwd, reasoningEffort, cliVersion) {
205
223
  const value = codexRecord(raw), thread = codexRecord(value.thread), sandbox = codexRecord(value.sandbox);
206
224
  return (typeof thread.id === "string" &&
@@ -53,6 +53,10 @@ export interface RestrictedCodexSession {
53
53
  readonly protocolIncompatibilities?: readonly string[] | undefined;
54
54
  /** Schema values beyond the baseline humanish knows, recorded at launch. */
55
55
  readonly protocolAdditions?: readonly string[] | undefined;
56
+ /** What in the operator's Codex setup the launch refused, without paths or values. */
57
+ readonly refusalReason?: string | undefined;
58
+ /** The instruction file names Codex added from the operator's Codex home. */
59
+ readonly operatorInstructionFiles?: readonly string[] | undefined;
56
60
  run(request: RestrictedCodexRequest, readinessOnly?: boolean): Promise<RestrictedCodexResult>;
57
61
  close(): Promise<boolean>;
58
62
  }
@@ -356,6 +356,8 @@ export function createRestrictedCodexSession(options = {}) {
356
356
  unknownNotifications: new Map(),
357
357
  protocolIncompatibilities: undefined,
358
358
  protocolAdditions: undefined,
359
+ refusalReason: undefined,
360
+ operatorInstructionFiles: undefined,
359
361
  activeDeadline: undefined,
360
362
  interrupt: undefined,
361
363
  closed: false,
@@ -399,6 +401,12 @@ export function createRestrictedCodexSession(options = {}) {
399
401
  get protocolAdditions() {
400
402
  return state.protocolAdditions;
401
403
  },
404
+ get refusalReason() {
405
+ return state.refusalReason;
406
+ },
407
+ get operatorInstructionFiles() {
408
+ return state.operatorInstructionFiles;
409
+ },
402
410
  run(request, readinessOnly = false) {
403
411
  const refusal = refusedRun(settings, state, request, () => pending !== undefined);
404
412
  if (refusal)
@@ -22,10 +22,13 @@ type CodexReadiness = {
22
22
  protocolIncompatibilities?: readonly string[];
23
23
  /** Schema values beyond the baseline, recorded by a launch that passed. */
24
24
  protocolAdditions?: readonly string[];
25
+ /** What in the operator's Codex setup the launch refused, without paths or values. */
26
+ refusalReason?: string;
25
27
  };
26
28
  /** What doctor's hosted Codex participant check found: a readiness result and what it admitted. */
27
29
  type HostedCodexReadiness = CodexReadiness & {
28
30
  cliVersion?: string;
31
+ operatorInstructionFiles?: readonly string[];
29
32
  resolvedModel?: string;
30
33
  authentication?: "chatgpt-account" | "api-key";
31
34
  };
@@ -5,6 +5,7 @@ import { capacityShortfall, describeCapacity, } from "../substrates/local/capaci
5
5
  import { keyNamesOf, requiredKeys, requiredSubjectEnv } from "./requirements.js";
6
6
  import { localAgentReady, localAgentVersionRefusal, } from "../actors/local-agent/cli.js";
7
7
  import { protocolAdditionsWarning, protocolIncompatibilityMessage, } from "../actors/codex/protocol-compat.js";
8
+ import { operatorInstructionsWarning } from "../actors/codex/operator-instructions.js";
8
9
  import { admissionRule } from "../analysis/admission.js";
9
10
  import { automaticAnalysisBudget } from "../analysis/automatic-config.js";
10
11
  import { externalCatchHealthy } from "../comms/sandbox-catch.js";
@@ -90,6 +91,8 @@ function codexRecovery(readiness, fallback) {
90
91
  return codexVersionRecovery(readiness.detectedCliVersion, readiness.installation);
91
92
  case "codex_incompatible_release":
92
93
  return `${protocolIncompatibilityMessage(readiness.detectedCliVersion, readiness.protocolIncompatibilities ?? [])} ${codexInstallAdvice(readiness.installation)} Then sign in with a ChatGPT account (\`codex login\`).`;
94
+ case "codex_unsafe_configuration":
95
+ return unsafeConfigurationRecovery(readiness.refusalReason, fallback);
93
96
  case "codex_login_required":
94
97
  return "Codex is installed but not signed in. Run `codex login` and sign in with a ChatGPT account.";
95
98
  case "codex_unsupported_auth":
@@ -107,6 +110,12 @@ function codexRecovery(readiness, fallback) {
107
110
  return fallback;
108
111
  }
109
112
  }
113
+ /** What the refused setting is and how to change it; the fallback when no reason was recorded. */
114
+ function unsafeConfigurationRecovery(reason, fallback) {
115
+ if (reason === undefined)
116
+ return fallback;
117
+ return `humanish stopped the Codex launch because ${reason}. Change that setting in the Codex config (~/.codex/config.toml, a profile, or a system config) if you want hosted participants to use it, then rerun doctor.`;
118
+ }
110
119
  /** Shared read-only Codex account check for local participants in doctor and the TUI. */
111
120
  export async function localCodexParticipantCheck(args) {
112
121
  const readiness = await (args.readiness ?? codexAccountReadiness)(args.env).catch(() => ({ ready: false, errorCode: "codex_unavailable" }));
@@ -320,6 +329,7 @@ async function hostedCodexParticipantCheck(args, actor) {
320
329
  ...(actor?.reasoningEffort === undefined ? {} : { reasoningEffort: actor.reasoningEffort }),
321
330
  };
322
331
  const readiness = await (args.codexParticipantReadiness ?? hostedCodexReadiness)(args.env, declared).catch(() => ({ ready: false, errorCode: "codex_unavailable" }));
332
+ const operatorNote = operatorInstructionsWarning(readiness.operatorInstructionFiles);
323
333
  const account = readiness.authentication === "api-key"
324
334
  ? "an API key, which bills its usage to that key"
325
335
  : "a ChatGPT account";
@@ -327,7 +337,7 @@ async function hostedCodexParticipantCheck(args, actor) {
327
337
  name: "local participant authentication",
328
338
  ok: readiness.ready,
329
339
  message: readiness.ready
330
- ? readyMessage(`Codex CLI ${readiness.cliVersion ?? "(unknown release)"} passed the operator handshake without a turn (initialize, config/read, account/read and an ephemeral thread/start): model ${readiness.resolvedModel ?? "(not reported)"} on ${account}. E2B supplies the desktop; no OpenAI API key is required for this participant.`, readiness)
340
+ ? readyMessage(`Codex CLI ${readiness.cliVersion ?? "(unknown release)"} passed the operator handshake without a turn (initialize, config/read, account/read and an ephemeral thread/start): model ${readiness.resolvedModel ?? "(not reported)"} on ${account}. E2B supplies the desktop; no OpenAI API key is required for this participant.${operatorNote === undefined ? "" : ` ${operatorNote}`}`, readiness)
331
341
  : `Hosted Codex participant setup is unavailable (${readiness.errorCode}). ${codexRecovery(readiness, "Check `codex login status` and the operator's Codex configuration, then rerun doctor.")} No API fallback is used.`,
332
342
  };
333
343
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "humanish",
3
- "version": "0.139.0",
3
+ "version": "0.139.1",
4
4
  "description": "Synthetic user research for apps, CLIs, and agent-facing product flows.",
5
5
  "keywords": [
6
6
  "cli",