humanish 0.139.0 → 0.140.0
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 +21 -0
- package/README.md +5 -4
- 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/analysis/automatic-completion.js +24 -0
- package/dist/analysis/automatic-config.d.ts +15 -6
- package/dist/analysis/automatic-config.js +25 -9
- package/dist/cli/commands/study-route-computer-use.js +2 -3
- package/dist/cli/commands/study-route-scripted.js +3 -4
- package/dist/cli/commands/study-route-shared-world.js +2 -3
- package/dist/cli/commands/study-route-terminal.js +3 -4
- package/dist/cli/commands/study-run.js +1 -1
- package/dist/cli/doctor.d.ts +2 -0
- package/dist/cli/doctor.js +3 -0
- package/dist/cli/findings.d.ts +17 -14
- package/dist/cli/findings.js +37 -31
- package/dist/cli/io.d.ts +5 -1
- package/dist/cli/io.js +11 -2
- package/dist/cli/run-findings.d.ts +19 -0
- package/dist/cli/run-findings.js +106 -0
- package/dist/guest/bootstrap.d.ts +4 -2
- package/dist/guest/bootstrap.js +15 -7
- package/dist/guest/display-config.d.ts +31 -0
- package/dist/guest/display-config.js +35 -0
- package/dist/guest/runtime-desktop.d.ts +9 -3
- package/dist/guest/runtime-desktop.js +59 -25
- package/dist/guest/runtime.d.ts +2 -1
- package/dist/guest/runtime.js +1 -1
- package/dist/guest-runtime-main.js +2 -1
- package/dist/routes/computer-use/e2b-desktop/fidelity.js +10 -17
- package/dist/routes/computer-use/local-vm.js +34 -1
- package/dist/routes/computer-use/plan.js +1 -1
- package/dist/routes/scripted/plan.js +1 -1
- package/dist/routes/shared-world/plan.js +1 -1
- package/dist/routes/terminal/plan.js +1 -1
- package/dist/run/route-shell.js +1 -1
- package/dist/study/doctor.d.ts +3 -0
- package/dist/study/doctor.js +22 -7
- package/dist/study/init-templates.js +9 -3
- package/dist/study/mobile-emulation.d.ts +27 -0
- package/dist/study/mobile-emulation.js +60 -0
- package/dist/study/plan.js +1 -1
- package/dist/study/preflight-result.js +1 -1
- package/dist/study/summary.js +1 -1
- package/dist/study/warnings.js +5 -2
- package/dist/substrates/e2b/cdp-probe.d.ts +2 -6
- package/dist/substrates/e2b/desktop-cdp.d.ts +0 -2
- package/dist/substrates/e2b/desktop-cdp.js +0 -2
- package/dist/substrates/local/firecracker-desktop.d.ts +5 -0
- package/dist/substrates/local/firecracker-desktop.js +7 -1
- package/dist/substrates/local/runtime-config.d.ts +12 -0
- package/dist/substrates/local/runtime-config.js +55 -13
- package/dist/substrates/local/runtime-release.js +8 -8
- package/dist/substrates/local/runtime.js +1 -0
- package/docs/contracts/study-analysis.md +14 -8
- package/docs/principles/invariants-and-defaults.md +1 -1
- package/docs/product/automatic-analysis.md +46 -11
- package/package.json +1 -1
- package/skills/humanish/SKILL.md +45 -26
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,27 @@ The Unreleased section holds the full notes for the next version until it is tag
|
|
|
8
8
|
|
|
9
9
|
## Unreleased
|
|
10
10
|
|
|
11
|
+
## 0.140.0: One local study can test phone, tablet and desktop, and every run ends with findings (2026-10-21)
|
|
12
|
+
|
|
13
|
+
humanish 0.140.0 lets a local browser study give each participant its own device. One study with
|
|
14
|
+
`mobile`, `tablet` and `desktop` participants runs them side by side on your machine, with mobile
|
|
15
|
+
emulation for the phone, as hosted desktops already did; `humanish runtime setup` installs the new
|
|
16
|
+
local runtime. A study with a Codex participant is now analyzed by the same Codex account on every
|
|
17
|
+
target, so a run ends with findings without an OpenAI key; the default OpenAI analysis cap is $10;
|
|
18
|
+
a skipped analysis names the command that runs it; and `run --json` carries the findings.
|
|
19
|
+
|
|
20
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.140.0)
|
|
21
|
+
|
|
22
|
+
## 0.139.1: Hosted Codex participants work with an AGENTS.md in the Codex home (2026-10-21)
|
|
23
|
+
|
|
24
|
+
humanish 0.139.1 fixes hosted Codex participants for operators whose Codex home
|
|
25
|
+
(`CODEX_HOME`, or `~/.codex`) holds an `AGENTS.md`. Codex adds that file to every thread, and
|
|
26
|
+
humanish refused the launch as `codex_unsafe_configuration` with no reason. humanish now admits
|
|
27
|
+
that file, leaves it unchanged, and says the participant also received it. Every other
|
|
28
|
+
`codex_unsafe_configuration` refusal names the setting it refused.
|
|
29
|
+
|
|
30
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.139.1)
|
|
31
|
+
|
|
11
32
|
## 0.139.0: Computer-use studies can run on Modal desktops (2026-10-19)
|
|
12
33
|
|
|
13
34
|
humanish 0.139.0 can run a computer-use study's desktops on Modal. Set
|
package/README.md
CHANGED
|
@@ -235,18 +235,19 @@ a separately verified copy while preserving the readable original.
|
|
|
235
235
|
participant's report, task outcomes, costs, and how to turn a finding into an issue.
|
|
236
236
|
|
|
237
237
|
For a ranked review of a completed live study, run
|
|
238
|
-
`npx humanish analyze --run latest --max-cost
|
|
238
|
+
`npx humanish analyze --run latest --max-cost 10 --dry-run --json` to inspect
|
|
239
239
|
admission, then remove `--dry-run` to generate findings. This sends selected
|
|
240
240
|
retained evidence to OpenAI and requires `OPENAI_API_KEY`. Findings appear
|
|
241
241
|
beside Participants in Observer, with evidence links and separate participant
|
|
242
242
|
feedback. You can explicitly select a restricted Codex account analyst with
|
|
243
243
|
`npx humanish analyze --run latest --provider codex`; it uses remote inference,
|
|
244
244
|
requires the qualified CLI and account login, and reports dollar cost as unknown.
|
|
245
|
-
|
|
245
|
+
A study whose participant is a Codex `local-agent` uses that analyst by default. See the [analysis contract](docs/contracts/study-analysis.md) for
|
|
246
246
|
coverage limits, estimated cost controls, corrections and sharing behavior.
|
|
247
247
|
|
|
248
|
-
Supported live studies generate findings automatically after participants finish
|
|
249
|
-
|
|
248
|
+
Supported live studies generate findings automatically after participants finish.
|
|
249
|
+
A Codex participant's run is analyzed by the same Codex account; every other run uses
|
|
250
|
+
`gpt-6-astra` with high reasoning and a separate $10 cap on the expected cost.
|
|
250
251
|
This is additional to participant execution costs, and is not a hard provider
|
|
251
252
|
billing cap. To disable the extra request:
|
|
252
253
|
|
|
@@ -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)
|
|
@@ -96,10 +96,34 @@ function defaultAnalysisOverBudget(result) {
|
|
|
96
96
|
value.reason === "AUTOMATIC_ANALYSIS_ADMISSION_REFUSED" &&
|
|
97
97
|
value.result?.error?.code === "analysis_budget_exceeded");
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Codex refusals that stop the analyst before any request: the account or CLI cannot start it here.
|
|
101
|
+
* A hosted Codex participant can run where the analyst cannot, for example with a keyring-only
|
|
102
|
+
* login, since the analyst reads auth.json from a private home.
|
|
103
|
+
*/
|
|
104
|
+
const CODEX_ANALYST_SETUP_CODES = [
|
|
105
|
+
"analysis_codex_unavailable",
|
|
106
|
+
"analysis_codex_unsupported_version",
|
|
107
|
+
"analysis_codex_incompatible_release",
|
|
108
|
+
"analysis_codex_unsupported_platform",
|
|
109
|
+
"analysis_codex_login_required",
|
|
110
|
+
"analysis_codex_unsupported_auth",
|
|
111
|
+
"analysis_codex_busy",
|
|
112
|
+
];
|
|
113
|
+
/** True when a default Codex analyst could not start, so the study's author never asked for it. */
|
|
114
|
+
function defaultCodexAnalystUnavailable(result) {
|
|
115
|
+
const value = result.automaticAnalysis;
|
|
116
|
+
return (result.automaticAnalysisTrigger === "default" &&
|
|
117
|
+
value?.state === "failed" &&
|
|
118
|
+
value.reason === "AUTOMATIC_ANALYSIS_CODEX_UNAVAILABLE" &&
|
|
119
|
+
CODEX_ANALYST_SETUP_CODES.includes(value.result?.error?.code ?? ""));
|
|
120
|
+
}
|
|
99
121
|
export function automaticAnalysisSucceeded(result) {
|
|
100
122
|
const value = result.automaticAnalysis;
|
|
101
123
|
if (value === undefined)
|
|
102
124
|
return true;
|
|
125
|
+
if (defaultCodexAnalystUnavailable(result))
|
|
126
|
+
return true;
|
|
103
127
|
if (value.state === "skipped")
|
|
104
128
|
return (value.reason === "AUTOMATIC_ANALYSIS_DRY_RUN" ||
|
|
105
129
|
(result.automaticAnalysisTrigger === "default" &&
|
|
@@ -17,10 +17,18 @@ export type StudyAnalysis = StudyAnalysisSettings & ({
|
|
|
17
17
|
maxCostUsd?: null;
|
|
18
18
|
maxOutputTokens?: null;
|
|
19
19
|
});
|
|
20
|
-
/** The spend cap on an analysis the study did not declare. */
|
|
21
|
-
export declare const DEFAULT_ANALYSIS_MAX_COST_USD =
|
|
22
|
-
/**
|
|
23
|
-
export
|
|
20
|
+
/** The spend cap on an OpenAI analysis the study did not declare. */
|
|
21
|
+
export declare const DEFAULT_ANALYSIS_MAX_COST_USD = 10;
|
|
22
|
+
/** The study's participant, which picks the analyst when the study declares none. */
|
|
23
|
+
export interface AnalysisParticipant {
|
|
24
|
+
readonly type?: string;
|
|
25
|
+
readonly localAgent?: "codex" | "claude";
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Called for parsed manifests and direct library configs, before participant execution. An
|
|
29
|
+
* undefined `raw` is the study's default, chosen by its participant (the study's `actor`).
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveAutomaticAnalysis(raw: unknown, participant?: AnalysisParticipant): {
|
|
24
32
|
ok: true;
|
|
25
33
|
config: AnalysisConfig | undefined;
|
|
26
34
|
preferLargerOutput?: boolean;
|
|
@@ -48,8 +56,9 @@ export interface AutomaticAnalysisBudget {
|
|
|
48
56
|
refusedFromUsd?: number;
|
|
49
57
|
}
|
|
50
58
|
/** Metadata only: resolving the future live-run budget never reads keys or dispatches. With a
|
|
51
|
-
* participant count it also gives the expected cost range for that many participants
|
|
52
|
-
|
|
59
|
+
* participant count it also gives the expected cost range for that many participants, and with the
|
|
60
|
+
* study's participant it resolves an undeclared analysis the way the run will. */
|
|
61
|
+
export declare function automaticAnalysisBudget(raw: unknown, route: StudyRoute, participants?: number, participant?: AnalysisParticipant): AutomaticAnalysisBudget | undefined;
|
|
53
62
|
/**
|
|
54
63
|
* An analysis's requests in words, from their count: undefined for one request. The plan's
|
|
55
64
|
* analysis line and the dry run's worst case both say it.
|