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 +10 -0
- package/dist/actors/codex/operator-instructions.d.ts +5 -0
- package/dist/actors/codex/operator-instructions.js +9 -0
- package/dist/actors/codex/restricted-launch.d.ts +10 -2
- package/dist/actors/codex/restricted-launch.js +65 -16
- package/dist/actors/codex/restricted-participant.d.ts +4 -0
- package/dist/actors/codex/restricted-participant.js +9 -0
- package/dist/actors/codex/restricted-policy.d.ts +5 -2
- package/dist/actors/codex/restricted-policy.js +90 -72
- package/dist/actors/codex/restricted-session.d.ts +4 -0
- package/dist/actors/codex/restricted-session.js +8 -0
- package/dist/study/doctor.d.ts +3 -0
- package/dist/study/doctor.js +11 -1
- package/package.json +1 -1
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 {
|
|
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
|
-
|
|
41
|
-
|
|
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
|
|
64
|
-
return {
|
|
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 {
|
|
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
|
-
/**
|
|
68
|
-
|
|
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
|
-
/**
|
|
102
|
-
|
|
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
|
|
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
|
|
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
|
|
122
|
+
return `a ${String(name.type)} config layer is active`;
|
|
120
123
|
}
|
|
121
124
|
if (!ownLayer)
|
|
122
|
-
return
|
|
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
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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
|
|
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
|
|
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)
|
package/dist/study/doctor.d.ts
CHANGED
|
@@ -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
|
};
|
package/dist/study/doctor.js
CHANGED
|
@@ -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
|
|
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
|
}
|