humanish 0.138.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 +21 -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/routes/computer-use/bundle-parts.d.ts +3 -0
- package/dist/routes/computer-use/bundle-parts.js +9 -1
- package/dist/routes/computer-use/bundle.js +3 -4
- package/dist/routes/computer-use/e2b-desktop/prepare.js +8 -4
- package/dist/routes/computer-use/fanout-bundle.js +3 -2
- package/dist/routes/computer-use/participant-deps.d.ts +1 -1
- package/dist/routes/computer-use/plan.js +66 -19
- package/dist/routes/computer-use/setup.js +14 -7
- package/dist/routes/computer-use/types.d.ts +3 -0
- package/dist/run/bundle-shape.js +1 -1
- package/dist/run/bundle.d.ts +3 -2
- package/dist/run/feedback-shape.js +1 -0
- package/dist/study/composition-rules.js +3 -2
- package/dist/study/doctor.d.ts +3 -0
- package/dist/study/doctor.js +11 -1
- package/dist/study/parse/execution.js +2 -1
- package/dist/study/plan-base.d.ts +1 -0
- package/dist/study/plan-base.js +2 -0
- package/dist/study/plan-types.d.ts +2 -0
- package/dist/study/types.d.ts +1 -1
- package/dist/study/warnings.js +3 -1
- package/dist/substrates/e2b/sdk.d.ts +5 -0
- package/dist/substrates/e2b/sdk.js +12 -0
- package/dist/substrates/modal/commands.d.ts +16 -0
- package/dist/substrates/modal/commands.js +174 -0
- package/dist/substrates/modal/desktop.d.ts +16 -0
- package/dist/substrates/modal/desktop.js +195 -0
- package/dist/substrates/modal/image.d.ts +2 -0
- package/dist/substrates/modal/image.js +36 -1
- package/dist/substrates/modal/sandbox.d.ts +4 -0
- package/dist/substrates/modal/sandbox.js +68 -110
- package/docs/contracts/run-bundle.md +2 -1
- package/docs/contracts/schemas.md +4 -4
- package/package.json +1 -1
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.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
|
+
|
|
21
|
+
## 0.139.0: Computer-use studies can run on Modal desktops (2026-10-19)
|
|
22
|
+
|
|
23
|
+
humanish 0.139.0 can run a computer-use study's desktops on Modal. Set
|
|
24
|
+
`execution.target: modal-desktop` on an `app-url` study and each participant gets its own Modal
|
|
25
|
+
sandbox running the same desktop the E2B route uses: Xvfb, XFCE, xdotool input, screenshots, a
|
|
26
|
+
live noVNC stream, Chromium and recordings. The same study file runs on E2B or Modal
|
|
27
|
+
with only the target changed. Clone and local-tree subjects, shared worlds, E2B templates and
|
|
28
|
+
camera or speech media stay on E2B.
|
|
29
|
+
|
|
30
|
+
[Release notes](https://github.com/danielgwilson/humanish/releases/tag/v0.139.0)
|
|
31
|
+
|
|
11
32
|
## 0.138.0: A terminal study can run on Modal (2026-10-19)
|
|
12
33
|
|
|
13
34
|
humanish 0.138.0 can run a terminal study's shell on Modal as well as E2B. 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)
|
|
@@ -15,7 +15,10 @@ export declare function describeSubjectState(state: RunSubjectProvenance["state"
|
|
|
15
15
|
/** Human-readable provenance line for the single-participant subject.provenance event of a clone
|
|
16
16
|
* or local-tree subject: claims "cloned/packed and served" only when it actually happened. */
|
|
17
17
|
export declare function subjectProvenanceMessage(provenance: RunSubjectProvenance, publicAppUrl: string, dryRun: boolean, hasSession: boolean): string;
|
|
18
|
+
/** The label a run records for the desktop `execution.target` names. */
|
|
19
|
+
export declare function desktopSubstrate(target: string | undefined): "local-desktop" | "modal-desktop" | "e2b-desktop";
|
|
18
20
|
export declare function providerResourcesForOutcome(args: {
|
|
21
|
+
provider: "e2b-desktop" | "modal-desktop";
|
|
19
22
|
outcome: ParticipantRunOutcome | undefined;
|
|
20
23
|
createdAt: string;
|
|
21
24
|
ids: ParticipantIds;
|
|
@@ -73,6 +73,14 @@ export function subjectProvenanceMessage(provenance, publicAppUrl, dryRun, hasSe
|
|
|
73
73
|
}
|
|
74
74
|
return "Subject local-tree packing attempted; archive digest unresolved (provisioning failed before resolution)";
|
|
75
75
|
}
|
|
76
|
+
/** The label a run records for the desktop `execution.target` names. */
|
|
77
|
+
export function desktopSubstrate(target) {
|
|
78
|
+
return target === "local"
|
|
79
|
+
? "local-desktop"
|
|
80
|
+
: target === "modal-desktop"
|
|
81
|
+
? "modal-desktop"
|
|
82
|
+
: "e2b-desktop";
|
|
83
|
+
}
|
|
76
84
|
export function providerResourcesForOutcome(args) {
|
|
77
85
|
if (args.outcome?.sandboxId === undefined) {
|
|
78
86
|
return [];
|
|
@@ -80,7 +88,7 @@ export function providerResourcesForOutcome(args) {
|
|
|
80
88
|
return [
|
|
81
89
|
{
|
|
82
90
|
schema: "humanish.provider-resource.v1",
|
|
83
|
-
provider:
|
|
91
|
+
provider: args.provider,
|
|
84
92
|
kind: "sandbox",
|
|
85
93
|
id: args.outcome.sandboxId,
|
|
86
94
|
owner: "humanish",
|
|
@@ -6,7 +6,7 @@ import { judgeOneParticipant, judgeParticipants } from "../../run/judge.js";
|
|
|
6
6
|
import { renderReviewMarkdown } from "../../run/review-markdown.js";
|
|
7
7
|
import { desktopSpanToMinutes } from "../../run/cost-summary.js";
|
|
8
8
|
import { e2bDesktopTemplate } from "../../substrates/e2b/sandbox.js";
|
|
9
|
-
import { providerResourcesForOutcome } from "./bundle-parts.js";
|
|
9
|
+
import { desktopSubstrate, providerResourcesForOutcome } from "./bundle-parts.js";
|
|
10
10
|
import { participantFactsOf } from "./participant-facts.js";
|
|
11
11
|
import { buildCuaFanoutBundle } from "./fanout-bundle.js";
|
|
12
12
|
import { buildSingleParticipantBundle } from "./single-bundle.js";
|
|
@@ -96,9 +96,7 @@ function singleParticipantArgs(base, state) {
|
|
|
96
96
|
desktopRoute: base.plan.runner.desktop !== "in-process",
|
|
97
97
|
substrate: base.plan.runner.desktop === "in-process"
|
|
98
98
|
? "local-filesystem"
|
|
99
|
-
: plan.residual.execution?.target
|
|
100
|
-
? "local-desktop"
|
|
101
|
-
: "e2b-desktop",
|
|
99
|
+
: desktopSubstrate(plan.residual.execution?.target),
|
|
102
100
|
...(outcome?.desktopGeometry === undefined ? {} : { desktopGeometry: outcome.desktopGeometry }),
|
|
103
101
|
...(outcome?.recording === undefined ? {} : { recording: outcome.recording }),
|
|
104
102
|
...(outcome?.networkLog === undefined ? {} : { network: outcome.networkLog }),
|
|
@@ -127,6 +125,7 @@ function singleParticipantArgs(base, state) {
|
|
|
127
125
|
...(desktopTemplate === undefined ? {} : { desktopTemplate }),
|
|
128
126
|
...(outcome?.desktopBrowser === undefined ? {} : { desktopBrowser: outcome.desktopBrowser }),
|
|
129
127
|
providerResources: providerResourcesForOutcome({
|
|
128
|
+
provider: base.plan.residual.execution?.target === "modal-desktop" ? "modal-desktop" : "e2b-desktop",
|
|
130
129
|
outcome,
|
|
131
130
|
createdAt: base.run.createdAt,
|
|
132
131
|
ids: spec,
|
|
@@ -11,6 +11,7 @@ import { observeDesktopResources } from "../../../substrates/e2b/desktop-resourc
|
|
|
11
11
|
import { acquireE2BDesktopSandbox, e2bDesktopTemplate, withOneRetryOnTransientE2BError, } from "../../../substrates/e2b/sandbox.js";
|
|
12
12
|
import { loadE2BDesktopModule } from "../../../substrates/e2b/sdk.js";
|
|
13
13
|
import { e2bShell } from "../../../substrates/e2b/shell.js";
|
|
14
|
+
import { loadModalDesktopModule } from "../../../substrates/modal/sandbox.js";
|
|
14
15
|
import { attachReceivingInbox, participantCommsEnv, startCommsCatch } from "./comms.js";
|
|
15
16
|
import { participantSubjectEnv } from "../types.js";
|
|
16
17
|
import { plural } from "../../../run/text.js";
|
|
@@ -30,11 +31,14 @@ export async function acquireParticipantDesktop(ctx, state) {
|
|
|
30
31
|
// Off-app comms: the base-URL env is injected at sandbox create, so the app reads it at
|
|
31
32
|
// boot; the catch starts right after create.
|
|
32
33
|
const commsEnv = participantCommsEnv(ctx.comms);
|
|
33
|
-
const
|
|
34
|
+
const provider = deps.sandboxProvider ?? "e2b";
|
|
35
|
+
const desktopModule = await (deps.desktopModule ??
|
|
36
|
+
(provider === "modal" ? () => loadModalDesktopModule(env) : loadE2BDesktopModule))();
|
|
34
37
|
// An explicit template wins. Speech gets the versioned media image; ordinary
|
|
35
|
-
// browser studies retain the SDK default desktop.
|
|
38
|
+
// browser studies retain the SDK default desktop. A Modal desktop has one image.
|
|
36
39
|
const acquired = await acquireE2BDesktopSandbox({
|
|
37
40
|
module: desktopModule,
|
|
41
|
+
provider,
|
|
38
42
|
options: {
|
|
39
43
|
apiKey: deps.e2bApiKey,
|
|
40
44
|
requestTimeoutMs: deps.requestTimeoutMs,
|
|
@@ -67,7 +71,7 @@ export async function acquireParticipantDesktop(ctx, state) {
|
|
|
67
71
|
dpi: 96,
|
|
68
72
|
lifecycle: { onTimeout: "kill" },
|
|
69
73
|
},
|
|
70
|
-
template: e2bDesktopTemplate(residual),
|
|
74
|
+
template: provider === "modal" ? undefined : e2bDesktopTemplate(residual),
|
|
71
75
|
retry: {
|
|
72
76
|
// The default loader reclaims an acquired handle before retrying failed desktop startup.
|
|
73
77
|
// Its error names the cleanup outcome; pre-construction allocation failures remain unowned.
|
|
@@ -92,7 +96,7 @@ export async function acquireParticipantDesktop(ctx, state) {
|
|
|
92
96
|
state.sandboxId = acquired.allocation.resourceId;
|
|
93
97
|
// The billed span starts the instant the sandbox exists.
|
|
94
98
|
state.sandboxCreatedAtMs = deps.now();
|
|
95
|
-
const desktopResources = await observeDesktopResources(desktop);
|
|
99
|
+
const desktopResources = await observeDesktopResources(desktop, undefined, provider === "modal" ? "modal.create" : "e2b.getInfo");
|
|
96
100
|
state.desktopResources = desktopResources;
|
|
97
101
|
if ("reason" in desktopResources) {
|
|
98
102
|
warnings.push(`Desktop resource size unavailable (${desktopResources.reason}); compute cost remains unpriced.`);
|
|
@@ -5,7 +5,7 @@ import { cuaGoalSource } from "../../actors/goal-source.js";
|
|
|
5
5
|
import { redactEncodedText } from "../../evidence/encoded-text.js";
|
|
6
6
|
import { REVIEW_SCHEMA, bundleArtifacts, bundleHead, } from "../../run/bundle.js";
|
|
7
7
|
import { aggregateTaskFunnels, formatParticipantOutcomes, formatRunTaskFunnel, gapsListClause, skippedSentence, withCuaReviewProvenance, } from "../../run/outcomes.js";
|
|
8
|
-
import { providerResourcesForOutcome, publicSafeAppUrlLabel, streakGaps } from "./bundle-parts.js";
|
|
8
|
+
import { desktopSubstrate, providerResourcesForOutcome, publicSafeAppUrlLabel, streakGaps, } from "./bundle-parts.js";
|
|
9
9
|
import { subjectName } from "../../run/subject-name.js";
|
|
10
10
|
import { participantFeedbackCandidates } from "./participant-feedback.js";
|
|
11
11
|
import { participantFactsOf } from "./participant-facts.js";
|
|
@@ -179,7 +179,7 @@ function fanoutFeedbackCandidates(args) {
|
|
|
179
179
|
runId: args.run.runId,
|
|
180
180
|
scenarioId: `cua-${plan.studyId}`,
|
|
181
181
|
adapterId: plan.studyId,
|
|
182
|
-
substrate: plan.residual.execution?.target
|
|
182
|
+
substrate: desktopSubstrate(plan.residual.execution?.target),
|
|
183
183
|
participants: specs.map((spec, index) => {
|
|
184
184
|
const outcome = outcomes?.[index];
|
|
185
185
|
return {
|
|
@@ -241,6 +241,7 @@ export function buildCuaFanoutBundle(args) {
|
|
|
241
241
|
const desktopTemplate = e2bDesktopTemplate(plan.residual);
|
|
242
242
|
const desktopBrowser = fanoutDesktopBrowser(args);
|
|
243
243
|
const providerResources = (outcomes ?? []).flatMap((outcome) => providerResourcesForOutcome({
|
|
244
|
+
provider: plan.residual.execution?.target === "modal-desktop" ? "modal-desktop" : "e2b-desktop",
|
|
244
245
|
outcome,
|
|
245
246
|
createdAt: args.run.createdAt,
|
|
246
247
|
ids: outcome.spec,
|