humanish 0.111.3 → 0.112.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.
Files changed (112) hide show
  1. package/CHANGELOG.md +33 -0
  2. package/dist/actors/computer-use/openai-conversation.d.ts +91 -0
  3. package/dist/actors/computer-use/openai-conversation.js +209 -0
  4. package/dist/actors/computer-use/openai-provider.js +25 -41
  5. package/dist/actors/contract.d.ts +2 -0
  6. package/dist/actors/scripted-browser/actor.d.ts +5 -0
  7. package/dist/actors/scripted-browser/actor.js +40 -19
  8. package/dist/actors/scripted-browser/journey.d.ts +2 -0
  9. package/dist/actors/scripted-browser/journey.js +2 -1
  10. package/dist/actors/scripted-browser/steps.d.ts +2 -0
  11. package/dist/actors/scripted-browser/steps.js +17 -0
  12. package/dist/analysis/diagnostics.js +7 -12
  13. package/dist/analysis/execute.d.ts +1 -1
  14. package/dist/analysis/execute.js +35 -5
  15. package/dist/analysis/service.js +7 -1
  16. package/dist/analysis/types.d.ts +23 -0
  17. package/dist/analysis/validation.d.ts +80 -35
  18. package/dist/analysis/validation.js +58 -11
  19. package/dist/cli/commands/runs.js +4 -10
  20. package/dist/cli/findings.d.ts +49 -4
  21. package/dist/cli/findings.js +179 -23
  22. package/dist/cli/io.d.ts +2 -1
  23. package/dist/cli/io.js +3 -2
  24. package/dist/evidence/secret-scrub.d.ts +7 -0
  25. package/dist/evidence/secret-scrub.js +21 -12
  26. package/dist/feedback/draft.js +14 -4
  27. package/dist/observer/run-routes.js +8 -4
  28. package/dist/observer-app.html +4 -4
  29. package/dist/routes/computer-use/bundle.d.ts +1 -1
  30. package/dist/routes/computer-use/bundle.js +6 -16
  31. package/dist/routes/computer-use/fanout-bundle.js +40 -37
  32. package/dist/routes/computer-use/fanout-records.d.ts +2 -0
  33. package/dist/routes/computer-use/fanout-records.js +18 -26
  34. package/dist/routes/computer-use/participant-execution.js +4 -11
  35. package/dist/routes/computer-use/participant-facts.d.ts +2 -2
  36. package/dist/routes/computer-use/participant-facts.js +10 -1
  37. package/dist/routes/computer-use/result.js +7 -15
  38. package/dist/routes/computer-use/single-bundle.d.ts +2 -0
  39. package/dist/routes/computer-use/single-bundle.js +81 -37
  40. package/dist/routes/scripted/bundle.d.ts +1 -1
  41. package/dist/routes/scripted/bundle.js +9 -19
  42. package/dist/routes/scripted/route.js +1 -0
  43. package/dist/routes/scripted/setup.js +53 -10
  44. package/dist/routes/scripted/surface-records.d.ts +0 -1
  45. package/dist/routes/scripted/surface-records.js +27 -13
  46. package/dist/routes/scripted/surface-sessions.d.ts +1 -1
  47. package/dist/routes/scripted/surface-sessions.js +2 -0
  48. package/dist/routes/shared-world/bundle-records.d.ts +2 -2
  49. package/dist/routes/shared-world/bundle-records.js +21 -28
  50. package/dist/routes/shared-world/bundle.d.ts +1 -1
  51. package/dist/routes/shared-world/bundle.js +70 -78
  52. package/dist/routes/shared-world/participant-specs.d.ts +1 -2
  53. package/dist/routes/shared-world/participant-specs.js +0 -8
  54. package/dist/routes/shared-world/result.js +8 -12
  55. package/dist/routes/terminal/bundle.d.ts +1 -1
  56. package/dist/routes/terminal/bundle.js +10 -19
  57. package/dist/routes/terminal/credentials.js +14 -1
  58. package/dist/routes/terminal/dry-run.js +1 -0
  59. package/dist/routes/terminal/live-finish.js +7 -4
  60. package/dist/routes/terminal/live-sandbox.js +9 -2
  61. package/dist/routes/terminal/plan.js +1 -0
  62. package/dist/routes/terminal/runtime.d.ts +1 -0
  63. package/dist/routes/terminal/runtime.js +1 -0
  64. package/dist/routes/terminal/session.js +2 -1
  65. package/dist/routes/terminal/trace.d.ts +4 -2
  66. package/dist/routes/terminal/trace.js +30 -35
  67. package/dist/run/costs.js +9 -20
  68. package/dist/run/detail.d.ts +1 -1
  69. package/dist/run/detail.js +10 -1
  70. package/dist/run/display.d.ts +2 -2
  71. package/dist/run/display.js +15 -18
  72. package/dist/run/dry-run.js +10 -25
  73. package/dist/run/judge.d.ts +24 -0
  74. package/dist/run/judge.js +54 -0
  75. package/dist/run/liveness.d.ts +18 -0
  76. package/dist/run/liveness.js +24 -0
  77. package/dist/run/outcomes.d.ts +8 -1
  78. package/dist/run/outcomes.js +58 -12
  79. package/dist/run/participant-caption.d.ts +30 -0
  80. package/dist/run/participant-caption.js +50 -0
  81. package/dist/run/participant-records.d.ts +1 -1
  82. package/dist/run/review-markdown.d.ts +11 -0
  83. package/dist/run/review-markdown.js +22 -0
  84. package/dist/run/run-cost.d.ts +7 -0
  85. package/dist/run/run-cost.js +25 -5
  86. package/dist/run/run-index.d.ts +4 -50
  87. package/dist/run/run-index.js +57 -87
  88. package/dist/run/run.d.ts +2 -2
  89. package/dist/run/run.js +5 -3
  90. package/dist/run/secrets.d.ts +15 -2
  91. package/dist/run/secrets.js +134 -9
  92. package/dist/run/stats.js +2 -12
  93. package/dist/run/transient-comms-secrets.d.ts +8 -3
  94. package/dist/run/transient-comms-secrets.js +21 -1
  95. package/dist/study/keys.d.ts +1 -0
  96. package/dist/study/keys.js +1 -1
  97. package/dist/study/parse/execution.js +7 -1
  98. package/dist/study/plan-types.d.ts +1 -0
  99. package/dist/study/requirements.js +2 -1
  100. package/dist/study/types.d.ts +4 -1
  101. package/dist/study/warnings.d.ts +2 -1
  102. package/dist/study/warnings.js +3 -2
  103. package/dist/tui-app.js +73 -73
  104. package/dist/verify/liveness.d.ts +2 -2
  105. package/dist/verify/liveness.js +7 -4
  106. package/docs/architecture/terminal-product-route.md +14 -0
  107. package/docs/contracts/run-bundle.md +10 -0
  108. package/docs/contracts/schemas.md +9 -1
  109. package/docs/contracts/study-analysis.md +76 -23
  110. package/package.json +2 -1
  111. package/dist/actors/computer-use/openai-requests.d.ts +0 -62
  112. package/dist/actors/computer-use/openai-requests.js +0 -144
@@ -1,33 +1,158 @@
1
1
  // The literal values one run scrubs from what it writes: the provider keys and subject env values
2
2
  // its route reads, and the values the run provisions while it runs, such as the addresses email
3
3
  // receiving registers. These have no secret shape for pattern redaction to find, so each is
4
- // replaced as written; redactText runs after the scrub and finds secrets by shape.
4
+ // replaced as written and in the encoded forms a URL, a JSON body or a log line gives it;
5
+ // redactText runs after the scrub and finds secrets by shape.
5
6
  import { scrubLiterals } from "../evidence/redaction.js";
7
+ import { encodedForms } from "../evidence/secret-scrub.js";
6
8
  export class RunSecrets {
7
9
  // Private fields stay out of JSON.stringify and object spread, so a Run that reaches a writer
8
10
  // whole cannot carry the values with it.
9
- #held;
11
+ #held = [];
12
+ /** Every held value as written and encoded, longest first. */
13
+ #forms = [];
10
14
  #minLength;
11
15
  /**
12
- * Replaces every held value with the marker. It reads the values on each call, so a value added
13
- * after the scrub was handed to a participant or a subject is scrubbed from then on.
16
+ * Replaces every held value with the marker, as written, in each encoded form, and where a URL
17
+ * carries it partly percent-encoded or a terminal escape sequence splits it. It reads the values
18
+ * on each call, so a value added after the scrub was handed to a participant or a subject is
19
+ * scrubbed from then on.
14
20
  */
15
21
  scrub;
16
22
  /** `seed` keeps its order; values shorter than the floor are dropped. */
17
23
  constructor(seed, options = {}) {
18
24
  // An empty value would match between every character.
19
25
  this.#minLength = Math.max(1, options.minLength ?? 4);
20
- this.#held = seed.filter((value) => value.length >= this.#minLength);
21
- this.scrub = scrubLiterals(this.#held, options.marker);
26
+ this.add(seed);
27
+ const marker = options.marker ?? "[REDACTED_SECRET]";
28
+ // Longest first, so a form that holds a shorter one, such as a JSON-escaped value ending in a
29
+ // backslash, is replaced whole and leaves no stray escape behind.
30
+ const literal = scrubLiterals(this.#forms, marker);
31
+ this.scrub = (text) => {
32
+ const scrubbed = literal(text);
33
+ return replaceSpans(scrubbed, viewSpans(scrubbed, this.#forms), marker);
34
+ };
22
35
  }
23
36
  /** Holds each value the run learns while it runs, unless it is under the floor or already held. */
24
37
  add(values) {
25
- for (const value of values)
26
- if (value.length >= this.#minLength && !this.#held.includes(value))
27
- this.#held.push(value);
38
+ for (const value of values) {
39
+ if (value.length < this.#minLength || this.#held.includes(value))
40
+ continue;
41
+ this.#held.push(value);
42
+ for (const form of encodedForms(value))
43
+ if (!this.#forms.includes(form))
44
+ this.#forms.push(form);
45
+ }
46
+ this.#forms.sort((left, right) => right.length - left.length);
28
47
  }
29
48
  /** The held values, seed first, in one array that later `add` calls extend. */
30
49
  values() {
31
50
  return this.#held;
32
51
  }
52
+ /**
53
+ * Every held value as written and in each encoded form, longest first, in one array that later
54
+ * `add` calls extend. The terminal recorder sizes the output it keeps past its cap from these.
55
+ */
56
+ forms() {
57
+ return this.#forms;
58
+ }
59
+ /**
60
+ * Where `text` holds a held value: each encoded form as written, and each form in the text's
61
+ * view without terminal escape sequences and with percent escapes decoded. Sorted, with
62
+ * overlapping spans merged. A scrub that replaces across chunk boundaries reads these.
63
+ */
64
+ spans(text) {
65
+ const found = viewSpans(text, this.#forms);
66
+ for (const form of this.#forms)
67
+ for (let at = text.indexOf(form); at !== -1; at = text.indexOf(form, at + form.length))
68
+ found.push([at, at + form.length]);
69
+ return mergeSpans(found);
70
+ }
71
+ }
72
+ // What a terminal draws with (operating-system commands, control sequences and two-byte escapes,
73
+ // as written or JSON-escaped, as a JSON event carries a command's colored output) and what a URL
74
+ // encodes with (runs of percent escapes). The view a value is looked for in drops the first and
75
+ // decodes the second.
76
+ const VIEW_SEQUENCE = new RegExp([
77
+ "\\x1b\\][^\\x07]*(?:\\x07|\\x1b\\\\)",
78
+ "\\x1b\\[[0-?]*[ -/]*[@-~]",
79
+ "\\x1b[78=>]",
80
+ "\\\\u001b\\][^\\\\]*(?:\\\\u0007|\\\\u001b\\\\\\\\)",
81
+ "\\\\u001b\\[[0-?]*[ -/]*[@-~]",
82
+ "(?:%[0-9A-Fa-f]{2})+",
83
+ ].join("|"), "g");
84
+ /**
85
+ * Where a value is found in the text's view. A browser encodes a space or a quote in a URL path
86
+ * and leaves a `/` or a `:` as written, so a value with both matches no single encoded form, and a
87
+ * color code inside a value splits it. Each found value maps back to the span of `text` it came
88
+ * from, so the text keeps every other character as written.
89
+ */
90
+ function viewSpans(text, values) {
91
+ if (!text.includes("%") && !text.includes("\x1b") && !text.includes("\\u001b"))
92
+ return [];
93
+ // The view, and for each of its characters the span of `text` it came from. A character decoded
94
+ // from an escape run maps to the whole run.
95
+ let view = "";
96
+ const starts = [];
97
+ const ends = [];
98
+ const copy = (from, to) => {
99
+ for (let at = from; at < to; at += 1) {
100
+ starts.push(at);
101
+ ends.push(at + 1);
102
+ }
103
+ view += text.slice(from, to);
104
+ };
105
+ let cursor = 0;
106
+ let changed = false;
107
+ for (const sequence of text.matchAll(VIEW_SEQUENCE)) {
108
+ let plain = "";
109
+ if (sequence[0].startsWith("%")) {
110
+ try {
111
+ plain = decodeURIComponent(sequence[0]);
112
+ }
113
+ catch {
114
+ // Not UTF-8: the run stays as written and is copied with the text after it.
115
+ continue;
116
+ }
117
+ }
118
+ copy(cursor, sequence.index);
119
+ cursor = sequence.index + sequence[0].length;
120
+ for (let unit = 0; unit < plain.length; unit += 1) {
121
+ starts.push(sequence.index);
122
+ ends.push(cursor);
123
+ }
124
+ view += plain;
125
+ changed = true;
126
+ }
127
+ if (!changed)
128
+ return [];
129
+ copy(cursor, text.length);
130
+ const spans = [];
131
+ for (const value of values)
132
+ for (let at = view.indexOf(value); at !== -1; at = view.indexOf(value, at + value.length))
133
+ spans.push([starts[at] ?? 0, ends[at + value.length - 1] ?? text.length]);
134
+ return spans;
135
+ }
136
+ /** Sorted, with overlapping spans merged; spans that only touch stay apart. */
137
+ function mergeSpans(spans) {
138
+ const merged = [];
139
+ for (const [from, to] of [...spans].sort((left, right) => left[0] - right[0])) {
140
+ const last = merged.at(-1);
141
+ if (last !== undefined && from < last[1])
142
+ last[1] = Math.max(last[1], to);
143
+ else
144
+ merged.push([from, to]);
145
+ }
146
+ return merged;
147
+ }
148
+ function replaceSpans(text, spans, marker) {
149
+ if (spans.length === 0)
150
+ return text;
151
+ let replaced = "";
152
+ let written = 0;
153
+ for (const [from, to] of mergeSpans(spans)) {
154
+ replaced += `${text.slice(written, from)}${marker}`;
155
+ written = to;
156
+ }
157
+ return `${replaced}${text.slice(written)}`;
33
158
  }
package/dist/run/stats.js CHANGED
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { runDisplay, runDisplayLabel } from "./display.js";
6
6
  import { readRunIndex } from "./run-index.js";
7
7
  import { addCostTotals, emptyCostTotals, readCostTotals, } from "./costs.js";
8
+ import { runEstimateUsd } from "./run-cost.js";
8
9
  import { round6 } from "./pricing.js";
9
10
  import { plural } from "./text.js";
10
11
  const STATS_SCHEMA = "humanish.stats.v2";
@@ -27,17 +28,6 @@ function addParticipants(into, entry) {
27
28
  into.reachedGoal += entry.participants.reachedGoal;
28
29
  into.reportedFriction += entry.participants.reportedFriction ?? 0;
29
30
  }
30
- /**
31
- * A run's participant and desktop estimate: its recorded figure, or $0 for a dry run that records
32
- * none, since a dry run makes no model request and creates no desktop. Undefined when the cost is
33
- * unknown, including a dry run that records an unknown (null) figure.
34
- */
35
- function runEstimateUsd(entry) {
36
- const usd = entry.estimatedCostUsd;
37
- if (typeof usd === "number" && Number.isFinite(usd) && usd >= 0)
38
- return usd;
39
- return usd === undefined && entry.mode === "dry-run" ? 0 : undefined;
40
- }
41
31
  function entryTime(entry) {
42
32
  return entry.startedAt ?? entry.completedAt ?? entry.updatedAt;
43
33
  }
@@ -175,7 +165,7 @@ export async function computeStats(cwdInput, options = {}) {
175
165
  totals.dryRun += 1;
176
166
  if (entry.liveness === "running")
177
167
  totals.running += 1;
178
- const runUsd = runEstimateUsd(entry);
168
+ const runUsd = runEstimateUsd(entry) ?? undefined;
179
169
  if (runUsd === undefined)
180
170
  totals.unpricedRuns += 1;
181
171
  else
@@ -4,7 +4,12 @@ export declare function registerTransientCommsSecrets(values: string[]): void;
4
4
  /** Literal, longest-first replacement in one pass; replacement text is never matched recursively. */
5
5
  export declare function scrubTransientCommsText(text: string): string;
6
6
  /**
7
- * The scope's values as a scrubSecretValues scrub, which also finds them percent-encoded, escaped or
8
- * base64-encoded and returns decoded text. Outside a scope it changes nothing.
7
+ * A scrub for text a model writes: each scope value as written and in its encoded forms
8
+ * (percent-encoded, JSON-escaped, base64, base64url, hex), and where escapes split one. Text that
9
+ * holds an encoded value is returned decoded with each value replaced. Text without one keeps its
10
+ * original spelling, so an exact quote that holds an escape still matches its evidence. The literal
11
+ * scrub runs first, before decoding can rewrite a value that itself holds an escape, and last,
12
+ * because the encoded scrub leaves text inside marker-shaped spans alone. Outside a scope it
13
+ * changes nothing.
9
14
  */
10
- export declare function transientCommsEncodedScrub(): (text: string) => string;
15
+ export declare function transientCommsKnownValueScrub(): (text: string) => string;
@@ -1,4 +1,5 @@
1
1
  import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { decodeEscapes } from "../evidence/encoded-text.js";
2
3
  import { scrubSecretValues } from "../evidence/secret-scrub.js";
3
4
  import { escapeRegExp } from "./text.js";
4
5
  const scopes = new AsyncLocalStorage();
@@ -75,10 +76,29 @@ export function scrubTransientCommsText(text) {
75
76
  * The scope's values as a scrubSecretValues scrub, which also finds them percent-encoded, escaped or
76
77
  * base64-encoded and returns decoded text. Outside a scope it changes nothing.
77
78
  */
78
- export function transientCommsEncodedScrub() {
79
+ function transientCommsEncodedScrub() {
79
80
  const scope = scopes.getStore();
80
81
  if (!scope)
81
82
  return (text) => text;
82
83
  usable(scope);
83
84
  return scrubSecretValues([...scope.values]);
84
85
  }
86
+ const REDACTED = "[REDACTED_SECRET]";
87
+ const markers = (text) => text.split(REDACTED).length - 1;
88
+ /**
89
+ * A scrub for text a model writes: each scope value as written and in its encoded forms
90
+ * (percent-encoded, JSON-escaped, base64, base64url, hex), and where escapes split one. Text that
91
+ * holds an encoded value is returned decoded with each value replaced. Text without one keeps its
92
+ * original spelling, so an exact quote that holds an escape still matches its evidence. The literal
93
+ * scrub runs first, before decoding can rewrite a value that itself holds an escape, and last,
94
+ * because the encoded scrub leaves text inside marker-shaped spans alone. Outside a scope it
95
+ * changes nothing.
96
+ */
97
+ export function transientCommsKnownValueScrub() {
98
+ const encoded = transientCommsEncodedScrub();
99
+ return (text) => {
100
+ const literal = scrubTransientCommsText(text);
101
+ const found = encoded(literal);
102
+ return scrubTransientCommsText(markers(found) > markers(decodeEscapes(literal)) ? found : literal);
103
+ };
104
+ }
@@ -168,6 +168,7 @@ export declare const EXECUTION: {
168
168
  terminal: {
169
169
  stdin: true;
170
170
  transport: true;
171
+ doNotTrack: true;
171
172
  };
172
173
  desktop: {
173
174
  template: true;
@@ -108,7 +108,7 @@ export const EXECUTION = {
108
108
  timeoutMs: true,
109
109
  completionTimeoutMs: true,
110
110
  egressAllow: true,
111
- terminal: { stdin: true, transport: true },
111
+ terminal: { stdin: true, transport: true, doNotTrack: true },
112
112
  desktop: {
113
113
  template: true,
114
114
  browser: true,
@@ -75,9 +75,15 @@ function parseTerminal(raw) {
75
75
  return { ok: true, value: undefined };
76
76
  }
77
77
  if (!isRecord(raw)) {
78
- return invalid("`execution.terminal` must be an object ({ transport?, stdin? }).");
78
+ return invalid("`execution.terminal` must be an object ({ transport?, stdin?, doNotTrack? }).");
79
79
  }
80
80
  const terminal = {};
81
+ if (raw.doNotTrack !== undefined) {
82
+ if (typeof raw.doNotTrack !== "boolean") {
83
+ return invalid("`execution.terminal.doNotTrack` must be true or false (unquoted).");
84
+ }
85
+ terminal.doNotTrack = raw.doNotTrack;
86
+ }
81
87
  if (raw.transport !== undefined) {
82
88
  const transport = str(raw.transport);
83
89
  if (transport !== "exec-stream") {
@@ -217,6 +217,7 @@ export type TerminalPlan = PlanBase & {
217
217
  readonly personaId?: string;
218
218
  readonly mission?: string;
219
219
  readonly runtime: {
220
+ readonly doNotTrack: boolean;
220
221
  readonly version?: string;
221
222
  /** Passed to Codex as `--model` on every run, so the bundle can name and price it. */
222
223
  readonly model: string;
@@ -100,7 +100,8 @@ async function signedInAgentSuggestion(env) {
100
100
  if (ready.length === 0)
101
101
  return "";
102
102
  const labels = ready.map((agent) => agent.label).join(" and ");
103
- return ` ${labels} reports authenticated on this machine. Set actor.type: local-agent to use ${ready.length === 1 ? "it" : "one"} instead of a key.`;
103
+ const one = ready.length === 1;
104
+ return ` ${labels} ${one ? "reports" : "report"} authenticated on this machine. Set actor.type: local-agent to use ${one ? "it" : "one"} instead of a key.`;
104
105
  }
105
106
  /**
106
107
  * Refuses a local-agent brain whose CLI is missing, signed out, too old, or cannot honor the
@@ -401,6 +401,8 @@ export interface StudyExecutionTerminal {
401
401
  transport?: StudyTerminalTransport;
402
402
  /** Operator stdin posture. Default "disabled". */
403
403
  stdin?: StudyTerminalStdin;
404
+ /** Send DO_NOT_TRACK=1 to participant commands by default; false leaves it unset. */
405
+ doNotTrack?: boolean;
404
406
  }
405
407
  /**
406
408
  * The terminal agent's runtime-auth channel. "openai-egress" (the default) keeps the raw runtime
@@ -511,7 +513,8 @@ export interface StudyExecution {
511
513
  };
512
514
  /**
513
515
  * `terminal-product` route: outbound routing allowlist passed to E2B with a deny-all fallback.
514
- * Every other route leaves egress open and ignores it with a warning; 0.112.0 refuses it there.
516
+ * Every other route leaves egress open and ignores it with a warning;
517
+ * the first minor release on or after 2026-11-04 refuses it there.
515
518
  * Domain filtering is a routing control, not strict destination isolation on shared hosting.
516
519
  * It does not constrain spending through an allowed runtime provider, including when
517
520
  * openai-egress keeps the raw runtime key outside the sandbox.
@@ -14,7 +14,8 @@ export declare function inertFieldPaths(config: StudyConfig): string[];
14
14
  * terminal route creates its sandbox with the list as an outbound allowlist; on any other route the
15
15
  * study author would trust a list that blocks nothing. `humanish study check` and every run print
16
16
  * it, and the run records it in its bundle.
17
- * Removed in 0.112.0: deprecated on 2026-10-05. 0.112.0 refuses the field off the terminal route.
17
+ * Removed in the first minor release on or after 2026-11-04: deprecated on 2026-10-05. That release refuses the field off the terminal
18
+ * route.
18
19
  */
19
20
  export declare function egressAllowIgnoredWarning(config: StudyConfig): string | undefined;
20
21
  export declare function forwardDeclaredWarnings(config: StudyConfig): string[];
@@ -270,12 +270,13 @@ export function inertFieldPaths(config) {
270
270
  * terminal route creates its sandbox with the list as an outbound allowlist; on any other route the
271
271
  * study author would trust a list that blocks nothing. `humanish study check` and every run print
272
272
  * it, and the run records it in its bundle.
273
- * Removed in 0.112.0: deprecated on 2026-10-05. 0.112.0 refuses the field off the terminal route.
273
+ * Removed in the first minor release on or after 2026-11-04: deprecated on 2026-10-05. That release refuses the field off the terminal
274
+ * route.
274
275
  */
275
276
  export function egressAllowIgnoredWarning(config) {
276
277
  if (config.execution?.egressAllow === undefined || config.route === "terminal")
277
278
  return undefined;
278
- return `\`execution.egressAllow\` is ignored on route: ${config.route}. Only route: terminal creates its sandbox with that outbound allowlist, and this route's browser desktops can reach any site. Remove the field: humanish 0.112.0 refuses it on every route except terminal.`;
279
+ return `\`execution.egressAllow\` is ignored on route: ${config.route}. Only route: terminal creates its sandbox with that outbound allowlist, and this route's browser desktops can reach any site. Remove the field: the first minor release on or after 2026-11-04 refuses it on every route except terminal.`;
279
280
  }
280
281
  // Report fields that are present but not yet consumed by the engine, so a user never trusts a
281
282
  // setting that silently does nothing.