humanish 0.17.0 → 0.19.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 (63) hide show
  1. package/README.md +53 -21
  2. package/dist/actor-contract.d.ts +11 -1
  3. package/dist/actor-contract.js.map +1 -1
  4. package/dist/computer-use-actor.d.ts +4 -0
  5. package/dist/computer-use-actor.js +3 -1
  6. package/dist/computer-use-actor.js.map +1 -1
  7. package/dist/computer-use.d.ts +16 -0
  8. package/dist/computer-use.js +59 -6
  9. package/dist/computer-use.js.map +1 -1
  10. package/dist/concurrent-shared-world-lab.js +1 -0
  11. package/dist/concurrent-shared-world-lab.js.map +1 -1
  12. package/dist/cua-actor-lab.d.ts +33 -3
  13. package/dist/cua-actor-lab.js +169 -2
  14. package/dist/cua-actor-lab.js.map +1 -1
  15. package/dist/index.d.ts +3 -1
  16. package/dist/index.js +1 -0
  17. package/dist/index.js.map +1 -1
  18. package/dist/lab-config.d.ts +12 -0
  19. package/dist/lab-config.js +12 -0
  20. package/dist/lab-config.js.map +1 -1
  21. package/dist/observer-assets.js +37 -3
  22. package/dist/observer-assets.js.map +1 -1
  23. package/dist/observer-data.d.ts +7 -1
  24. package/dist/observer-data.js +1 -0
  25. package/dist/observer-data.js.map +1 -1
  26. package/dist/observer-library.d.ts +5 -1
  27. package/dist/observer-library.js +9 -7
  28. package/dist/observer-library.js.map +1 -1
  29. package/dist/observer-serve.d.ts +11 -23
  30. package/dist/observer-serve.js +12 -92
  31. package/dist/observer-serve.js.map +1 -1
  32. package/dist/observer.d.ts +6 -0
  33. package/dist/observer.js +52 -1
  34. package/dist/observer.js.map +1 -1
  35. package/dist/pricing.d.ts +78 -0
  36. package/dist/pricing.js +100 -0
  37. package/dist/pricing.js.map +1 -0
  38. package/dist/program.js +294 -122
  39. package/dist/program.js.map +1 -1
  40. package/dist/run.d.ts +54 -0
  41. package/dist/run.js +86 -0
  42. package/dist/run.js.map +1 -1
  43. package/dist/serve-exposure.d.ts +62 -0
  44. package/dist/serve-exposure.js +129 -0
  45. package/dist/serve-exposure.js.map +1 -0
  46. package/dist/serve-http.d.ts +8 -0
  47. package/dist/serve-http.js +37 -0
  48. package/dist/serve-http.js.map +1 -0
  49. package/dist/serve-tunnel.d.ts +6 -2
  50. package/dist/serve-tunnel.js +9 -0
  51. package/dist/serve-tunnel.js.map +1 -1
  52. package/docs/architecture/actor-contract.md +41 -3
  53. package/docs/architecture/observer.md +30 -0
  54. package/docs/architecture/serve.md +138 -82
  55. package/docs/contracts/run-bundle.md +23 -0
  56. package/docs/contracts/schemas.md +111 -15
  57. package/docs/goals/current.md +1 -1
  58. package/docs/principles/invariants-and-defaults.md +2 -1
  59. package/docs/ramp/README.md +1 -1
  60. package/package.json +1 -1
  61. package/dist/observer-auth.d.ts +0 -21
  62. package/dist/observer-auth.js +0 -92
  63. package/dist/observer-auth.js.map +0 -1
@@ -0,0 +1,62 @@
1
+ import { type ServeMode } from "./serve-http.js";
2
+ import { type ServeTunnel, type StartNgrokTunnelOptions } from "./serve-tunnel.js";
3
+ export type ExposureSurface = "serve" | "watch";
4
+ export type ExposureErrorCode = "HUMANISH_SERVE_ALLOW_REQUIRES_OAUTH" | "HUMANISH_SERVE_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_SERVE_OPTION_CONFLICT" | "HUMANISH_SERVE_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_SERVE_EXPOSE_REQUIRES_EDGE_AUTH_OR_SAFE" | "HUMANISH_SERVE_EXPOSE_REQUIRES_ORIGIN" | "HUMANISH_WATCH_ALLOW_REQUIRES_OAUTH" | "HUMANISH_WATCH_OAUTH_REQUIRES_TUNNEL" | "HUMANISH_WATCH_OPTION_CONFLICT" | "HUMANISH_WATCH_TUNNEL_REQUIRES_EXPOSE" | "HUMANISH_WATCH_EXPOSE_REQUIRES_EDGE_AUTH" | "HUMANISH_WATCH_EXPOSE_REQUIRES_LIVE_FOLLOW" | "HUMANISH_WATCH_SAFE_NOT_APPLICABLE";
5
+ export interface ExposureRequest {
6
+ expose: boolean;
7
+ tunnel?: "ngrok" | undefined;
8
+ tunnelDomain?: string | undefined;
9
+ oauth?: "google" | undefined;
10
+ allowEmails: string[];
11
+ allowDomains: string[];
12
+ publicUrl?: string | undefined;
13
+ safe: boolean;
14
+ }
15
+ export interface WatchLiveContext {
16
+ dryRun: boolean;
17
+ detach: boolean;
18
+ json: boolean;
19
+ }
20
+ export interface ExposurePlan {
21
+ exposed: boolean;
22
+ edgeAuthed: boolean;
23
+ mode: ServeMode;
24
+ safe: boolean;
25
+ tunnel?: "ngrok";
26
+ tunnelDomain?: string;
27
+ oauth?: {
28
+ provider: "google";
29
+ allowEmails: string[];
30
+ allowDomains: string[];
31
+ };
32
+ publicOrigin?: {
33
+ origin: string;
34
+ host: string;
35
+ scheme: "http" | "https";
36
+ };
37
+ warnings: string[];
38
+ }
39
+ export type ExposureValidation = {
40
+ ok: true;
41
+ plan: ExposurePlan;
42
+ } | {
43
+ ok: false;
44
+ error: {
45
+ code: ExposureErrorCode;
46
+ message: string;
47
+ };
48
+ };
49
+ export declare function validateExposure(surface: ExposureSurface, request: ExposureRequest, live?: WatchLiveContext): ExposureValidation;
50
+ export interface ExposableServer {
51
+ readonly port: number;
52
+ readonly url: string;
53
+ addPublicOrigin(origin: string): void;
54
+ }
55
+ export interface ExposureResult {
56
+ tunnel?: ServeTunnel;
57
+ publicUrl?: string;
58
+ warnings: string[];
59
+ }
60
+ export declare function startExposedObserver(server: ExposableServer, plan: ExposurePlan, deps?: {
61
+ startTunnel?: (options: StartNgrokTunnelOptions) => Promise<ServeTunnel>;
62
+ }): Promise<ExposureResult>;
@@ -0,0 +1,129 @@
1
+ // Shared fail-closed exposure validation + tunnel orchestration for BOTH `serve` (a library of
2
+ // finished runs) and `watch` (one live run). Exposure auth is TUNNEL-EDGE only: humanish carries no
3
+ // in-process auth. Exposure is admitted only behind edge auth (ngrok --oauth google, or an operator
4
+ // --public-url they secure) OR, for serve, behind --safe (share_ready runs only). A live watch run
5
+ // is never share_ready (raw, unverified screenshots), so watch --expose ALWAYS requires edge auth.
6
+ //
7
+ // This module is pure with respect to the fail-closed matrix (validateExposure) and thin over the
8
+ // tunnel launcher (startExposedObserver), so the CLI just maps flags in and results out.
9
+ import { parsePublicOrigin } from "./serve-http.js";
10
+ import { startNgrokTunnel } from "./serve-tunnel.js";
11
+ function code(surface, suffix) {
12
+ return `HUMANISH_${surface.toUpperCase()}_${suffix}`;
13
+ }
14
+ export function validateExposure(surface, request, live) {
15
+ const fail = (suffix, message) => ({
16
+ ok: false,
17
+ error: { code: code(surface, suffix), message }
18
+ });
19
+ // Structural guards (both surfaces), in the fail-closed order documented in the matrix. These run
20
+ // before any bind/spawn so a mis-configured exposure aborts before sandbox/provider spend.
21
+ if ((request.allowEmails.length > 0 || request.allowDomains.length > 0) && !request.oauth) {
22
+ return fail("ALLOW_REQUIRES_OAUTH", "--allow-email/--allow-domain configure the ngrok edge OAuth allow-list; they require --oauth google.");
23
+ }
24
+ if (request.oauth && !request.tunnel) {
25
+ return fail("OAUTH_REQUIRES_TUNNEL", "--oauth turns on edge OAuth on the ngrok tunnel; it requires --tunnel ngrok (a --public-url operator brings their own edge auth).");
26
+ }
27
+ if (request.tunnel && request.publicUrl !== undefined) {
28
+ return fail("OPTION_CONFLICT", "Use either --tunnel or --public-url as the public origin, not both.");
29
+ }
30
+ if (request.tunnelDomain !== undefined && !request.tunnel) {
31
+ return fail("OPTION_CONFLICT", "--tunnel-domain requires --tunnel.");
32
+ }
33
+ const publicOrigin = request.publicUrl !== undefined ? parsePublicOrigin(request.publicUrl) : null;
34
+ if (request.publicUrl !== undefined && !publicOrigin) {
35
+ return fail("OPTION_CONFLICT", "--public-url must be an http(s) origin like https://observer.example.com.");
36
+ }
37
+ if (!request.expose) {
38
+ // Exposure flags without --expose are refused (no silent wide-open). --safe is orthogonal and
39
+ // stays valid without --expose (a loopback share_ready filter).
40
+ if (request.tunnel) {
41
+ return fail("TUNNEL_REQUIRES_EXPOSE", "--tunnel exposes the surface; declare that intent with --expose.");
42
+ }
43
+ if (request.publicUrl !== undefined) {
44
+ return fail("OPTION_CONFLICT", "--public-url only applies with --expose.");
45
+ }
46
+ return {
47
+ ok: true,
48
+ plan: { exposed: false, edgeAuthed: false, mode: "loopback", safe: request.safe, warnings: [] }
49
+ };
50
+ }
51
+ const oauth = request.oauth
52
+ ? { provider: "google", allowEmails: request.allowEmails, allowDomains: request.allowDomains }
53
+ : undefined;
54
+ const edgeAuthed = Boolean(request.oauth) || Boolean(publicOrigin);
55
+ const warnings = [];
56
+ if (surface === "watch") {
57
+ // A live, in-progress run is never share_ready (raw screenshots, unverified), so --safe would
58
+ // admit nothing and expose nothing: watch --expose ALWAYS requires edge auth, and the live-follow
59
+ // preconditions are checked first so `--dry-run`/`--detach`/`--json` fail with the clearer reason.
60
+ if (live && (live.dryRun || live.detach || live.json)) {
61
+ return fail("EXPOSE_REQUIRES_LIVE_FOLLOW", "watch --expose streams a live desktop over an attached follow channel; it cannot combine with --dry-run, --detach, or --json.");
62
+ }
63
+ // --safe is a share_ready LIBRARY filter for `serve`; watch streams a single live run that is
64
+ // never share_ready, so --safe would silently do nothing here. Reject it rather than ignore it.
65
+ if (request.safe) {
66
+ return fail("SAFE_NOT_APPLICABLE", "watch streams a single live run that is never share_ready; --safe (a share_ready library filter) applies to `serve`, not `watch`. Restrict viewers with edge auth: --allow-email / --allow-domain.");
67
+ }
68
+ if (!edgeAuthed) {
69
+ return fail("EXPOSE_REQUIRES_EDGE_AUTH", "watch --expose serves a live run that is never share_ready, so --safe cannot gate it; require edge auth: --tunnel ngrok --oauth google, or an operator-secured --public-url.");
70
+ }
71
+ }
72
+ else {
73
+ // serve: --expose must ALWAYS resolve to a reachable public origin (a tunnel or a --public-url),
74
+ // even under --safe. Without one the exposed server is an unreachable loopback no-op, so fail
75
+ // closed before the origin-less bind. When an origin IS present, keep the edge-auth-OR-safe gate.
76
+ if (!request.tunnel && !publicOrigin) {
77
+ return fail("EXPOSE_REQUIRES_ORIGIN", "--expose needs a declared public origin: pass --tunnel ngrok or --public-url <origin>.");
78
+ }
79
+ if (!edgeAuthed && !request.safe) {
80
+ return fail("EXPOSE_REQUIRES_EDGE_AUTH_OR_SAFE", "--expose opens a public URL to local run bundles; require edge auth (--oauth google with --tunnel, or a --public-url you secure) OR --safe (share_ready runs only).");
81
+ }
82
+ }
83
+ if (request.oauth && request.allowEmails.length === 0 && request.allowDomains.length === 0) {
84
+ warnings.push("ngrok --oauth google with NO --allow-email/--allow-domain lets ANY Google account that reaches the URL in; add at least one allow rule to restrict who can watch.");
85
+ }
86
+ const mode = edgeAuthed ? "exposed" : "share-safe-open";
87
+ return {
88
+ ok: true,
89
+ plan: {
90
+ exposed: true,
91
+ edgeAuthed,
92
+ mode,
93
+ safe: request.safe,
94
+ ...(request.tunnel ? { tunnel: request.tunnel } : {}),
95
+ ...(request.tunnelDomain ? { tunnelDomain: request.tunnelDomain } : {}),
96
+ ...(oauth ? { oauth } : {}),
97
+ ...(publicOrigin ? { publicOrigin } : {}),
98
+ warnings
99
+ }
100
+ };
101
+ }
102
+ // Orchestrate the edge in front of an already-bound loopback server: spawn the ngrok tunnel (with
103
+ // oauth/allow args) or declare the operator's --public-url, then extend the server's Host allowlist.
104
+ // May throw a ServeTunnelError (ngrok missing/failed); callers close the loopback server on throw.
105
+ export async function startExposedObserver(server, plan, deps = {}) {
106
+ const warnings = [...plan.warnings];
107
+ if (plan.tunnel === "ngrok") {
108
+ const startTunnel = deps.startTunnel ?? startNgrokTunnel;
109
+ const tunnel = await startTunnel({
110
+ port: server.port,
111
+ ...(plan.tunnelDomain ? { domain: plan.tunnelDomain } : {}),
112
+ ...(plan.oauth
113
+ ? {
114
+ oauthProvider: plan.oauth.provider,
115
+ oauthAllowEmails: plan.oauth.allowEmails,
116
+ oauthAllowDomains: plan.oauth.allowDomains
117
+ }
118
+ : {})
119
+ });
120
+ server.addPublicOrigin(tunnel.url);
121
+ return { tunnel, publicUrl: tunnel.url.replace(/\/$/, ""), warnings };
122
+ }
123
+ if (plan.publicOrigin) {
124
+ server.addPublicOrigin(plan.publicOrigin.origin);
125
+ return { publicUrl: plan.publicOrigin.origin, warnings };
126
+ }
127
+ return { warnings };
128
+ }
129
+ //# sourceMappingURL=serve-exposure.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-exposure.js","sourceRoot":"","sources":["../src/serve-exposure.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,8 @@
1
+ export type ServeMode = "loopback" | "exposed" | "share-safe-open";
2
+ export declare function buildServeSecurityHeaders(): Record<string, string>;
3
+ export declare function hostAllowed(hostHeader: string | undefined, allowlist: ReadonlySet<string>): boolean;
4
+ export declare function parsePublicOrigin(value: string): {
5
+ origin: string;
6
+ host: string;
7
+ scheme: "http" | "https";
8
+ } | null;
@@ -0,0 +1,37 @@
1
+ // Shared HTTP hardening primitives for the serve surfaces. Extracted here so BOTH the live
2
+ // Observer server (src/observer.ts, exposed mode) and the run-library server
3
+ // (src/observer-serve.ts) can enforce the identical Host allowlist + security-header posture
4
+ // without a module cycle. This file imports nothing from the serve modules.
5
+ export function buildServeSecurityHeaders() {
6
+ return {
7
+ "cache-control": "no-store",
8
+ "referrer-policy": "no-referrer",
9
+ "x-content-type-options": "nosniff",
10
+ "x-frame-options": "DENY",
11
+ "x-robots-tag": "noindex, nofollow"
12
+ };
13
+ }
14
+ export function hostAllowed(hostHeader, allowlist) {
15
+ return typeof hostHeader === "string" && allowlist.has(hostHeader.trim().toLowerCase());
16
+ }
17
+ export function parsePublicOrigin(value) {
18
+ let parsed;
19
+ try {
20
+ parsed = new URL(value);
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") {
26
+ return null;
27
+ }
28
+ if ((parsed.pathname !== "/" && parsed.pathname !== "") || parsed.search || parsed.hash || !parsed.host) {
29
+ return null;
30
+ }
31
+ return {
32
+ origin: parsed.origin,
33
+ host: parsed.host.toLowerCase(),
34
+ scheme: parsed.protocol === "https:" ? "https" : "http"
35
+ };
36
+ }
37
+ //# sourceMappingURL=serve-http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serve-http.js","sourceRoot":"","sources":["../src/serve-http.ts"],"names":[],"mappings":"AAAA,2FAA2F;AAC3F,6EAA6E;AAC7E,6FAA6F;AAC7F,4EAA4E;AAS5E,MAAM,UAAU,yBAAyB;IACvC,OAAO;QACL,eAAe,EAAE,UAAU;QAC3B,iBAAiB,EAAE,aAAa;QAChC,wBAAwB,EAAE,SAAS;QACnC,iBAAiB,EAAE,MAAM;QACzB,cAAc,EAAE,mBAAmB;KACpC,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,WAAW,CAAC,UAA8B,EAAE,SAA8B;IACxF,OAAO,OAAO,UAAU,KAAK,QAAQ,IAAI,SAAS,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC,CAAC;AAC1F,CAAC;AAED,MAAM,UAAU,iBAAiB,CAAC,KAAa;IAC7C,IAAI,MAAW,CAAC;IAChB,IAAI,CAAC;QACH,MAAM,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC1B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,MAAM,CAAC,QAAQ,KAAK,OAAO,IAAI,MAAM,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;QAChE,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC,MAAM,CAAC,QAAQ,KAAK,GAAG,IAAI,MAAM,CAAC,QAAQ,KAAK,EAAE,CAAC,IAAI,MAAM,CAAC,MAAM,IAAI,MAAM,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,EAAE,CAAC;QACxG,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO;QACL,MAAM,EAAE,MAAM,CAAC,MAAM;QACrB,IAAI,EAAE,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE;QAC/B,MAAM,EAAE,MAAM,CAAC,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM;KACxD,CAAC;AACJ,CAAC"}
@@ -8,9 +8,13 @@ export interface ServeTunnel {
8
8
  url: string;
9
9
  close(): Promise<void>;
10
10
  }
11
- export declare function startNgrokTunnel(options: {
11
+ export interface StartNgrokTunnelOptions {
12
12
  port: number;
13
13
  domain?: string;
14
+ oauthProvider?: "google";
15
+ oauthAllowEmails?: string[];
16
+ oauthAllowDomains?: string[];
14
17
  timeoutMs?: number;
15
18
  spawnImpl?: typeof spawn;
16
- }): Promise<ServeTunnel>;
19
+ }
20
+ export declare function startNgrokTunnel(options: StartNgrokTunnelOptions): Promise<ServeTunnel>;
@@ -10,6 +10,14 @@ export class ServeTunnelError extends Error {
10
10
  export async function startNgrokTunnel(options) {
11
11
  const spawnImpl = options.spawnImpl ?? spawn;
12
12
  const timeoutMs = options.timeoutMs ?? 15_000;
13
+ const oauthArgs = options.oauthProvider
14
+ ? [
15
+ "--oauth",
16
+ options.oauthProvider,
17
+ ...(options.oauthAllowEmails ?? []).flatMap((email) => ["--oauth-allow-email", email]),
18
+ ...(options.oauthAllowDomains ?? []).flatMap((domain) => ["--oauth-allow-domain", domain])
19
+ ]
20
+ : [];
13
21
  const args = [
14
22
  "http",
15
23
  "--log",
@@ -17,6 +25,7 @@ export async function startNgrokTunnel(options) {
17
25
  "--log-format",
18
26
  "json",
19
27
  ...(options.domain ? ["--url", options.domain] : []),
28
+ ...oauthArgs,
20
29
  String(options.port)
21
30
  ];
22
31
  const child = spawnImpl("ngrok", args, { stdio: ["ignore", "pipe", "ignore"] });
@@ -1 +1 @@
1
- {"version":3,"file":"serve-tunnel.js","sourceRoot":"","sources":["../src/serve-tunnel.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"serve-tunnel.js","sourceRoot":"","sources":["../src/serve-tunnel.ts"],"names":[],"mappings":"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"}
@@ -111,7 +111,10 @@ export type ActorCompletionReason =
111
111
  | "turn_completed" // harness saw an explicit done signal, no predicate
112
112
  | "gave_up" // persona abandoned in character: friction exceeded its tolerance
113
113
  | "blocked_approval" // an action was auto-declined and the actor could not proceed
114
- | "timed_out"
114
+ | "timed_out" // wall-clock deadline hit with ZERO material progress → still a FAILURE
115
+ | "budget_reached" // wall-clock time budget hit AFTER productive activity (>=1 material
116
+ // action) → ActorStatus "passed", a NON-FAILURE open-ended-watch
117
+ // completion; distinct from goal_satisfied (no goal was claimed)
115
118
  | "actor_error"
116
119
  | "step_failed" // a deterministic scripted step/expectation evaluated false: the
117
120
  // SUBJECT failed the script; the harness executed faithfully
@@ -162,6 +165,7 @@ export interface ActorTrace {
162
165
  counts: Record<string, number>;
163
166
  items: ActorTraceItem[];
164
167
  tokenUsage?: { input?: number; output?: number; total?: number; costUsd?: number };
168
+ estimatedCost?: ActorEstimatedCost; // humanish.actor-estimated-cost.v1 (additive)
165
169
  capabilities: ActorCapabilities;
166
170
  }
167
171
 
@@ -229,6 +233,17 @@ export interface Actor {
229
233
  target URLs, or unredacted provider payloads in the trace.
230
234
  - **Capabilities.** Declare them honestly; the registry uses them to refuse
231
235
  unsuitable dispatch.
236
+ - **Cost (estimate vs. charge).** `tokenUsage.costUsd` stays RESERVED for a
237
+ real, provider-returned charge (the codex/agent-SDK path) — a bare `costUsd`
238
+ always means "the provider billed this". The optional `estimatedCost`
239
+ (`humanish.actor-estimated-cost.v1`) is a SEPARATE, differently-named field: a
240
+ token-derived rate-table multiply from the operator-editable `src/pricing.ts`,
241
+ labeled honestly as an estimate and projected up into `RunBundle.cost` (see
242
+ [`../contracts/schemas.md`](../contracts/schemas.md) → Run Cost Summary And
243
+ Estimated Actor Cost). The CUA lab computes and attaches `estimatedCost` at the
244
+ lab boundary before persisting the trace, so the pure computer-use loop never
245
+ depends on the pricing table. An unknown model yields
246
+ `estimatedCostUsd: null` + a `reason`, never a guessed charge.
232
247
 
233
248
  ## The scripted-browser lane (shipped)
234
249
 
@@ -245,8 +260,31 @@ Completion semantics: `goal_satisfied` means the scenario's `expect` blocks —
245
260
  predicate — all held ("the app still affords this exact journey", nothing about user
246
261
  behavior); `step_failed` means a deterministic step or expectation evaluated false (the
247
262
  subject failed the script; the harness ran faithfully); `timed_out` is the journey wall-clock
248
- budget; `harness_error` is a browser that could not launch. `gave_up` and `blocked_approval`
249
- are unreachable — no persona patience, no approvals exist on a deterministic replay.
263
+ budget hit with NO material progress (still a failure); `harness_error` is a browser that could
264
+ not launch. `gave_up` and `blocked_approval` are unreachable — no persona patience, no
265
+ approvals exist on a deterministic replay.
266
+
267
+ ### The time budget vs. a stuck timeout (`budget_reached`)
268
+
269
+ `execution.timeoutMs` is a GENEROUS wall-clock SAFETY cap, not a goal. For an open-ended
270
+ "watch it play" session there is no success predicate — productive play IS the outcome — so the
271
+ computer-use loop distinguishes two ways to hit the cap:
272
+
273
+ - **`budget_reached`** — the deadline was reached AFTER at least one material (non-idle) action.
274
+ This maps to `ActorStatus: "passed"`: a non-failure completion, `laneOutcomeOk` returns true,
275
+ the verdict is `pass`, and the CLI exits `0`. It stays a distinct `completionReason` (never
276
+ `goal_satisfied`), and the trace `reason` says it reached the budget after productive activity,
277
+ so a reviewer of a strictly goal-directed lab sees it hit the cap rather than reaching a goal
278
+ (goal-directed labs should set a tight `timeoutMs`).
279
+ - **`timed_out`** — the deadline was reached with ZERO material actions (a hung provider, an
280
+ idle-only stall). This maps to `ActorStatus: "timed_out"`, `laneOutcomeOk` is false, the
281
+ verdict is `fail`, and the CLI exits `2`. "Made zero progress then timed out" stays a failure.
282
+
283
+ `ActorStatus` intentionally gains NO new member — the honest distinction lives in
284
+ `completionReason`/`reason` — which keeps the change from rippling through ~10 provider mappers.
285
+ `statusForCompletion` is an exhaustive switch with no default, so a new completion reason forces a
286
+ compile-time decision about its status. ~30 min (`1_800_000`) is a reasonable default for
287
+ open-ended watch; the persona still stops early on `goal_satisfied`/`gave_up`/`stopWhen`.
250
288
 
251
289
  Actuation-vs-spend gate: on the scripted lab route `scenario.mode: live` is still required
252
290
  even though provider spend is $0 by mechanism. The gate's justification there is ACTUATION,
@@ -78,6 +78,36 @@ LIBRARY surface — every run under `.humanish/runs/` — and never serves runti
78
78
  stream URLs in any mode; remote viewers see persisted evidence only. See
79
79
  [Serve: the run library surface](serve.md).
80
80
 
81
+ ### Exposed hardening and `watch --expose`
82
+
83
+ The live `serveObserver` server binds `127.0.0.1` and, by default, is a
84
+ permissive local-dev server (no Host allowlist, no security headers). Under its
85
+ `exposed` option — set by `watch --expose` — it enforces the SAME
86
+ DNS-rebinding defense as the library surface: a strict Host allowlist (loopback
87
+ names at bind, extended by `addPublicOrigin(tunnel.url | public-url)`, `421
88
+ Misdirected Request` otherwise) and the shared `buildServeSecurityHeaders()` on
89
+ every response (both live in `src/serve-http.ts`, shared without a module cycle).
90
+ Loopback (non-exposed) behavior is byte-identical to before.
91
+
92
+ Exposed mode also SCOPES the surface to the attached live run (`result.run`): the
93
+ `/_humanish/history.json` index is filtered to that one run, and `/_humanish/runs/<id>/…`
94
+ 404s byte-identically to a nonexistent run for any other id. A remote viewer who
95
+ clears the edge auth can therefore see only the run being watched — never enumerate
96
+ or fetch a prior run's raw, unverified evidence. Loopback keeps the full cross-run
97
+ library (history + any run by id) exactly as before.
98
+
99
+ `watch --expose` is the ONE surface that DELIBERATELY streams the live E2B
100
+ desktop to a remote viewer: the attached watch process genuinely holds the
101
+ runtime stream URLs (in the in-memory `WeakMap`, never persisted), and streaming
102
+ them is the whole point of watching from a phone. It is safe only because the
103
+ ngrok edge (Google OAuth + allow rules) or an operator `--public-url` edge
104
+ authenticates the viewer first — `watch --expose` therefore always requires edge
105
+ auth (a live run is never `share_ready`, so `--safe` alone cannot gate it). The
106
+ attached server comes up DURING the run and survives a `timed_out`/`failed` run
107
+ (serving is not gated on pass/fail), so a failed run's evidence stays inspectable
108
+ to Ctrl-C. `serve` still never injects stream URLs. See
109
+ [Serve: the run library surface](serve.md).
110
+
81
111
  ## UI Shape
82
112
 
83
113
  The Observer shell has: