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.
- package/README.md +53 -21
- package/dist/actor-contract.d.ts +11 -1
- package/dist/actor-contract.js.map +1 -1
- package/dist/computer-use-actor.d.ts +4 -0
- package/dist/computer-use-actor.js +3 -1
- package/dist/computer-use-actor.js.map +1 -1
- package/dist/computer-use.d.ts +16 -0
- package/dist/computer-use.js +59 -6
- package/dist/computer-use.js.map +1 -1
- package/dist/concurrent-shared-world-lab.js +1 -0
- package/dist/concurrent-shared-world-lab.js.map +1 -1
- package/dist/cua-actor-lab.d.ts +33 -3
- package/dist/cua-actor-lab.js +169 -2
- package/dist/cua-actor-lab.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/lab-config.d.ts +12 -0
- package/dist/lab-config.js +12 -0
- package/dist/lab-config.js.map +1 -1
- package/dist/observer-assets.js +37 -3
- package/dist/observer-assets.js.map +1 -1
- package/dist/observer-data.d.ts +7 -1
- package/dist/observer-data.js +1 -0
- package/dist/observer-data.js.map +1 -1
- package/dist/observer-library.d.ts +5 -1
- package/dist/observer-library.js +9 -7
- package/dist/observer-library.js.map +1 -1
- package/dist/observer-serve.d.ts +11 -23
- package/dist/observer-serve.js +12 -92
- package/dist/observer-serve.js.map +1 -1
- package/dist/observer.d.ts +6 -0
- package/dist/observer.js +52 -1
- package/dist/observer.js.map +1 -1
- package/dist/pricing.d.ts +78 -0
- package/dist/pricing.js +100 -0
- package/dist/pricing.js.map +1 -0
- package/dist/program.js +294 -122
- package/dist/program.js.map +1 -1
- package/dist/run.d.ts +54 -0
- package/dist/run.js +86 -0
- package/dist/run.js.map +1 -1
- package/dist/serve-exposure.d.ts +62 -0
- package/dist/serve-exposure.js +129 -0
- package/dist/serve-exposure.js.map +1 -0
- package/dist/serve-http.d.ts +8 -0
- package/dist/serve-http.js +37 -0
- package/dist/serve-http.js.map +1 -0
- package/dist/serve-tunnel.d.ts +6 -2
- package/dist/serve-tunnel.js +9 -0
- package/dist/serve-tunnel.js.map +1 -1
- package/docs/architecture/actor-contract.md +41 -3
- package/docs/architecture/observer.md +30 -0
- package/docs/architecture/serve.md +138 -82
- package/docs/contracts/run-bundle.md +23 -0
- package/docs/contracts/schemas.md +111 -15
- package/docs/goals/current.md +1 -1
- package/docs/principles/invariants-and-defaults.md +2 -1
- package/docs/ramp/README.md +1 -1
- package/package.json +1 -1
- package/dist/observer-auth.d.ts +0 -21
- package/dist/observer-auth.js +0 -92
- package/dist/observer-auth.js.map +0 -1
|
@@ -2,10 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Date: 2026-08-02
|
|
4
4
|
|
|
5
|
-
Status: shipped
|
|
6
|
-
(`src/observer-serve.ts`, `src/observer-
|
|
7
|
-
`src/serve-tunnel.ts`; CLI wiring in `src/program.ts`).
|
|
8
|
-
control-plane namespace is reserved and answers `501`; no
|
|
5
|
+
Status: shipped — `loopback`, `exposed` (edge-authed), and `share-safe-open`
|
|
6
|
+
modes (`src/observer-serve.ts`, `src/observer-library.ts`, `src/serve-http.ts`,
|
|
7
|
+
`src/serve-exposure.ts`, `src/serve-tunnel.ts`; CLI wiring in `src/program.ts`).
|
|
8
|
+
The `/_humanish/api/*` control-plane namespace is reserved and answers `501`; no
|
|
9
|
+
mutating route ships.
|
|
10
|
+
|
|
11
|
+
Exposure auth is **tunnel-edge only**. As of 0.18.0 humanish carries NO
|
|
12
|
+
in-process auth: the hand-rolled capability-link (cookie/token/TTL, the whole
|
|
13
|
+
`observer-auth.ts` module and the `serve --auth link|none` flags) was removed as
|
|
14
|
+
a pre-1.0 breaking change. The gate now lives entirely at the edge — ngrok
|
|
15
|
+
`--oauth google` (with `--allow-email`/`--allow-domain` allow rules), or an
|
|
16
|
+
operator-secured `--public-url` (Cloudflare Access, Tailscale, a reverse proxy
|
|
17
|
+
you own).
|
|
9
18
|
|
|
10
19
|
## What serve is
|
|
11
20
|
|
|
@@ -13,69 +22,107 @@ control-plane namespace is reserved and answers `501`; no mutating route ships.
|
|
|
13
22
|
the LIBRARY rather than a run:
|
|
14
23
|
|
|
15
24
|
- `humanish watch` — one ATTACHED run: the process that created the run serves
|
|
16
|
-
it and may inject runtime stream URLs for live following
|
|
25
|
+
it and may inject runtime stream URLs for live following (see the stream-URL
|
|
26
|
+
doctrine below and `watch --expose`);
|
|
17
27
|
- `humanish observe` — one FINISHED run, re-served read-only;
|
|
18
28
|
- `humanish serve` — the whole local library under `.humanish/runs/`: a
|
|
19
29
|
library index, per-run Observer pages, `/_humanish/history.json` polling, and
|
|
20
|
-
optional exposure beyond the machine.
|
|
30
|
+
optional edge-authenticated exposure beyond the machine.
|
|
21
31
|
|
|
22
32
|
The server binds `127.0.0.1` unconditionally (`serveObserverLibrary`); exposure
|
|
23
33
|
only ever happens through a tunnel or proxy forwarding to the loopback port.
|
|
24
|
-
`--expose`
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
34
|
+
`--expose` never changes the bind — it declares intent and requires an
|
|
35
|
+
authenticated edge (or `--safe`, see the fail-closed matrix).
|
|
36
|
+
|
|
37
|
+
## Fail-closed exposure matrix
|
|
38
|
+
|
|
39
|
+
One shared validator (`validateExposure` in `src/serve-exposure.ts`) governs
|
|
40
|
+
both `serve` and `watch`. `--expose` must ALWAYS resolve to a reachable public
|
|
41
|
+
origin (a `--tunnel` or a `--public-url`) — even under `--safe`, since an
|
|
42
|
+
origin-less exposed server is an unreachable loopback no-op. With an origin
|
|
43
|
+
present, exposure requires EITHER edge auth (`--oauth` on the ngrok edge, or a
|
|
44
|
+
`--public-url` you secure) OR `--safe` (share_ready runs only). A tunnel with
|
|
45
|
+
neither is a wide-open public URL to local bundles and is refused.
|
|
46
|
+
|
|
47
|
+
| `--expose` | `--tunnel` | `--oauth` | `--public-url` | `--safe` | Outcome |
|
|
48
|
+
| --- | --- | --- | --- | --- | --- |
|
|
49
|
+
| — | — | — | — | any | `loopback` (no exposure) |
|
|
50
|
+
| ✓ | ✓ | ✓ | — | any | OK → `exposed` (edge-authed; all runs unless `--safe`) |
|
|
51
|
+
| ✓ | ✓ | — | — | ✓ | OK → `share-safe-open` (public, share_ready only) |
|
|
52
|
+
| ✓ | ✓ | — | — | — | **REFUSED** `HUMANISH_SERVE_EXPOSE_REQUIRES_EDGE_AUTH_OR_SAFE` |
|
|
53
|
+
| ✓ | — | — | ✓ | any | OK → `exposed` (operator-secured edge) |
|
|
54
|
+
| ✓ | — | — | — | any | **REFUSED** `HUMANISH_SERVE_EXPOSE_REQUIRES_ORIGIN` (no reachable origin, even with `--safe`) |
|
|
55
|
+
| ✓ | — | ✓ | — | any | **REFUSED** `HUMANISH_SERVE_OAUTH_REQUIRES_TUNNEL` |
|
|
56
|
+
| ✓ | ✓ | ✓ | ✓ | any | **REFUSED** `HUMANISH_SERVE_OPTION_CONFLICT` (tunnel + public-url) |
|
|
57
|
+
|
|
58
|
+
Guard order (all before any bind/spawn): `--allow-email`/`--allow-domain`
|
|
59
|
+
without `--oauth` → `HUMANISH_SERVE_ALLOW_REQUIRES_OAUTH`; `--oauth` without
|
|
60
|
+
`--tunnel` → `HUMANISH_SERVE_OAUTH_REQUIRES_TUNNEL`; `--tunnel`+`--public-url` →
|
|
61
|
+
conflict; `--tunnel-domain` without `--tunnel` → conflict; `--expose` with no
|
|
62
|
+
tunnel and no `--public-url` (even under `--safe`) →
|
|
63
|
+
`HUMANISH_SERVE_EXPOSE_REQUIRES_ORIGIN`; `--expose` with an origin but without
|
|
64
|
+
edge auth and without `--safe` →
|
|
65
|
+
`HUMANISH_SERVE_EXPOSE_REQUIRES_EDGE_AUTH_OR_SAFE`; a tunnel/public-url without
|
|
66
|
+
`--expose` → `HUMANISH_SERVE_TUNNEL_REQUIRES_EXPOSE`/conflict. `--oauth google`
|
|
67
|
+
with NO allow rule is ALLOWED (any Google account authenticates) but pushes a
|
|
68
|
+
prominent warning recommending at least one `--allow-email`/`--allow-domain`.
|
|
69
|
+
|
|
70
|
+
The `watch` surface reuses the same validator but is stricter: a live,
|
|
71
|
+
in-progress run is never `share_ready` (raw, unverified screenshots), so `--safe`
|
|
72
|
+
would admit nothing. `watch --expose --safe` is therefore REFUSED outright with
|
|
73
|
+
`HUMANISH_WATCH_SAFE_NOT_APPLICABLE` (rather than silently ignoring the flag);
|
|
74
|
+
`watch --expose` ALWAYS requires edge auth (`--tunnel --oauth` or `--public-url`),
|
|
75
|
+
and is additionally refused with `--dry-run`/`--detach`/`--json` (no live desktop
|
|
76
|
+
/ no attached follow). An exposed watch serves ONLY the attached live run: its
|
|
77
|
+
`/_humanish/history.json` lists just that run and every other run id 404s
|
|
78
|
+
byte-identically to a nonexistent one, so a remote viewer can never enumerate or
|
|
79
|
+
reach any prior run's raw evidence (loopback watch still serves the full library).
|
|
80
|
+
|
|
81
|
+
## ngrok edge OAuth
|
|
82
|
+
|
|
83
|
+
`startNgrokTunnel` builds `ngrok http <port> [--url <domain>] [--oauth google
|
|
84
|
+
[--oauth-allow-email <addr>]… [--oauth-allow-domain <domain>]…] --log stdout
|
|
85
|
+
--log-format json`. ngrok authenticates the viewer at its edge before any request
|
|
86
|
+
reaches the loopback port; the stdout JSON parser skips every line except
|
|
87
|
+
`msg:"started tunnel"`, so ngrok's `--oauth has been deprecated` info line (it is
|
|
88
|
+
still accepted and functional on ngrok 3.39.x) is ignored automatically. The
|
|
89
|
+
forward-compatible path if ngrok removes the flags is a Traffic Policy YAML — a
|
|
90
|
+
documented fast-follow, out of scope for 0.18.0.
|
|
28
91
|
|
|
29
92
|
## Threat model
|
|
30
93
|
|
|
31
|
-
**
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
mint requires a valid session, loopback peer or not.
|
|
37
|
-
|
|
38
|
-
**Referer and unfurler leakage.** The capability URL embeds the secret in the
|
|
39
|
-
path. Every response carries `referrer-policy: no-referrer` (plus `no-store`
|
|
40
|
-
and `x-robots-tag: noindex`), so a link inside a served page can never leak the
|
|
41
|
-
origin or token onward. The residual channel is the operator pasting the link
|
|
42
|
-
into a chat app whose unfurler prefetches it server-side — which is why the
|
|
43
|
-
link is deliberately NOT single-use (a preview fetch must not burn it before
|
|
44
|
-
the phone taps it), and why the README steers sharing toward AirDrop or manual
|
|
45
|
-
entry.
|
|
46
|
-
|
|
47
|
-
**Host-only cookie scope on shared tunnel domains.** The session cookie sets
|
|
48
|
-
no `Domain` attribute (`buildSessionCookie`), so it is host-only. On a shared
|
|
49
|
-
tunnel apex (many customers under one provider domain), a `Domain` cookie
|
|
50
|
-
would be presented to sibling subdomains an attacker can rent; a host-only
|
|
51
|
-
cookie can never leave the exact public origin the operator declared.
|
|
94
|
+
**No in-process secret to leak.** The old capability-link posture (a token in
|
|
95
|
+
the URL path, an HttpOnly cookie, unfurler/history leakage, host-only cookie
|
|
96
|
+
scope, TTL/revocation) is gone with the module. There is no secret in any URL
|
|
97
|
+
served by humanish; the operator's edge owns authentication and session
|
|
98
|
+
lifetime.
|
|
52
99
|
|
|
53
100
|
**DNS rebinding and the strict Host allowlist.** A malicious page can point an
|
|
54
101
|
attacker-controlled DNS name at 127.0.0.1 and read a permissive local server
|
|
55
102
|
from the victim's browser. Serve keeps a strict Host allowlist in ALL modes —
|
|
56
|
-
loopback names plus the declared tunnel/public origin only — and answers
|
|
57
|
-
|
|
58
|
-
|
|
103
|
+
loopback names plus the declared tunnel/public origin only — and answers `421
|
|
104
|
+
Misdirected Request` otherwise. Even the unauthenticated loopback default never
|
|
105
|
+
trusts an arbitrary Host header. The live `serveObserver` server gains the same
|
|
106
|
+
allowlist + security headers under its new `exposed` option (see observer.md), so
|
|
107
|
+
`watch --expose` is not a header-less, rebinding-vulnerable surface.
|
|
59
108
|
|
|
60
|
-
**
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
process runs, a leaked history entry is a live credential. This risk is
|
|
65
|
-
accepted and documented, not solved.
|
|
109
|
+
**Security headers.** Every response carries `cache-control: no-store`,
|
|
110
|
+
`referrer-policy: no-referrer`, `x-content-type-options: nosniff`,
|
|
111
|
+
`x-frame-options: DENY`, and `x-robots-tag: noindex, nofollow`
|
|
112
|
+
(`buildServeSecurityHeaders`).
|
|
66
113
|
|
|
67
114
|
## Mode-to-boundary mapping
|
|
68
115
|
|
|
69
116
|
| Mode | Invocation | Boundary class |
|
|
70
117
|
| --- | --- | --- |
|
|
71
118
|
| `loopback` | `humanish serve` | Capture-side trust: readable only by whoever can already read gitignored `.humanish/` on this machine; no new boundary is crossed. |
|
|
72
|
-
| `
|
|
73
|
-
| `share-safe-open` | `--expose --safe --
|
|
119
|
+
| `exposed` | `--expose --tunnel ngrok --oauth google …`, or `--expose --public-url <origin>` | Edge-authed exposure: only viewers who clear the edge OAuth (or the operator's own edge) reach the loopback server, which then serves everything it grants unless `--safe` composes in. The gate is the edge, not humanish. |
|
|
120
|
+
| `share-safe-open` | `--expose --safe --tunnel ngrok` (no `--oauth`) | Genuine publishing behind the feedback-grade `share_ready` gate: only runs that pass verify are served — admission is re-checked when a bundle changes and re-verified within a bounded window (default 30s); everything else is absent, 404ing byte-identically to a nonexistent run (no existence oracle). |
|
|
74
121
|
|
|
75
122
|
## Stream-URL doctrine
|
|
76
123
|
|
|
77
|
-
Live desktop stream URLs (auth-bearing hosted-VNC links) are never served by
|
|
78
|
-
|
|
124
|
+
Live desktop stream URLs (auth-bearing hosted-VNC links) are never served by the
|
|
125
|
+
LIBRARY surface, in any mode, and the guarantee is layered:
|
|
79
126
|
|
|
80
127
|
- **structural** — runtime stream URLs live in a `WeakMap` keyed by the watch
|
|
81
128
|
process's in-memory `ObserverResult` (`src/observer.ts`) and are never
|
|
@@ -87,54 +134,63 @@ this surface, in any mode, and the guarantee is layered:
|
|
|
87
134
|
- **tested** — the serve suite pins a no-injection test: observer data served
|
|
88
135
|
through the library surface carries no runtime stream URLs.
|
|
89
136
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
the
|
|
137
|
+
`watch --expose` is the ONE surface that deliberately serves runtime E2B stream
|
|
138
|
+
URLs — the attached watch process genuinely holds them, and streaming the live
|
|
139
|
+
desktop is the whole point of watching from a phone. It is safe only because the
|
|
140
|
+
edge authenticates first, and the URLs are still never persisted (they are
|
|
141
|
+
injected into the in-memory bundle, not disk). See observer.md.
|
|
95
142
|
|
|
96
143
|
## The share_ready doctrine
|
|
97
144
|
|
|
98
145
|
`share_ready` was designed as the bar for FEEDBACK payloads: evidence eligible
|
|
99
|
-
to leave the machine inside a public issue draft. Serve's open mode
|
|
100
|
-
that same gate to arbitrary-audience BROWSING, which is a broader
|
|
101
|
-
the same artifacts. The honest caveat carries over unchanged:
|
|
102
|
-
verify` does not yet detect free-form PII/PHI (names, emails, medical
|
|
146
|
+
to leave the machine inside a public issue draft. Serve's `share-safe-open` mode
|
|
147
|
+
extends that same gate to arbitrary-audience BROWSING, which is a broader
|
|
148
|
+
exposure of the same artifacts. The honest caveat carries over unchanged:
|
|
149
|
+
`humanish verify` does not yet detect free-form PII/PHI (names, emails, medical
|
|
103
150
|
identifiers — see the README's public-safety boundary and issue #108), so
|
|
104
|
-
`share_ready` means the automated secret/path scan passed, not that a human
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
151
|
+
`share_ready` means the automated secret/path scan passed, not that a human would
|
|
152
|
+
publish every pixel. Maintainers should treat open mode accordingly: synthetic
|
|
153
|
+
data upstream, review before exposing, and treat `--safe` without edge auth as
|
|
154
|
+
publishing because it is.
|
|
108
155
|
|
|
109
156
|
## v2 control-plane seam contract
|
|
110
157
|
|
|
111
158
|
The reserved `/_humanish/api/*` namespace answers `501` with
|
|
112
|
-
`HUMANISH_SERVE_CONTROL_PLANE_DISABLED`
|
|
113
|
-
auth gate
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
-
|
|
120
|
-
|
|
121
|
-
- mutating routes require
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
159
|
+
`HUMANISH_SERVE_CONTROL_PLANE_DISABLED` to any request. Because the in-process
|
|
160
|
+
auth gate is gone, a request that clears the edge (or a loopback caller) reaches
|
|
161
|
+
the `501` directly — there is no `401`-first anymore. No run artifact can ever
|
|
162
|
+
shadow the namespace. The seam is already typed: `createServeRequestHandler`
|
|
163
|
+
accepts an optional `ServeControlPlane`, and v1 always passes `undefined`. Before
|
|
164
|
+
any mutating route ships, the contract is:
|
|
165
|
+
|
|
166
|
+
- an operator identity distinct from a viewer — but sourced from the edge/control
|
|
167
|
+
plane, not a humanish-minted cookie;
|
|
168
|
+
- mutating routes require CSRF defenses appropriate to the chosen edge session;
|
|
169
|
+
- the spend rule is invariant 3 applied to remote hands: a phone-initiated LIVE
|
|
170
|
+
run needs its own affirmative declaration at serve startup (an explicit opt-in
|
|
171
|
+
naming the lab and budget), never a default the viewer UI can reach.
|
|
172
|
+
|
|
173
|
+
## Why not Better Auth here
|
|
174
|
+
|
|
175
|
+
Better Auth is a strong TypeScript auth framework, but it is deliberately NOT
|
|
176
|
+
used for this CLI serve surface: serve is an ephemeral, no-database,
|
|
177
|
+
per-invocation loopback server whose only job is to hand persisted evidence to an
|
|
178
|
+
already-authenticated edge. A password/session/social-login framework with a
|
|
179
|
+
schema and a datastore is the wrong shape for a process that lives for the length
|
|
180
|
+
of a `Ctrl-C`. The right home for Better Auth is a FUTURE hosted humanish
|
|
181
|
+
dashboard / control-plane — a persistent, multi-user service with a database —
|
|
182
|
+
where accounts, org membership, and durable sessions actually exist.
|
|
126
183
|
|
|
127
184
|
## Future work
|
|
128
185
|
|
|
129
|
-
- **
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
would land in shell history and process listings.
|
|
186
|
+
- **Traffic Policy for ngrok.** Migrate off the deprecated `--oauth*` flags to a
|
|
187
|
+
generated Traffic Policy YAML once ngrok requires it (the `yaml` dep is already
|
|
188
|
+
available); pin the behavior in `serve-tunnel` tests first.
|
|
133
189
|
- **Google Fonts inlining for per-run observer pages.** The library index is
|
|
134
|
-
self-contained; the per-run observer HTML still references remote Google
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
190
|
+
self-contained; the per-run observer HTML still references remote Google Fonts,
|
|
191
|
+
which degrade gracefully offline but should be inlined (or dropped) so a served
|
|
192
|
+
run page makes no third-party requests from a viewer's browser.
|
|
193
|
+
- **Shipped in 0.18.0: `watch --expose`.** Remote LIVE following of a run,
|
|
194
|
+
including its live E2B desktop stream, behind the same edge auth — the one
|
|
195
|
+
surface that deliberately serves runtime stream URLs. See observer.md and
|
|
196
|
+
live_watch_wiring.
|
|
@@ -112,6 +112,29 @@ instead (a dirty working tree cannot be commit-pinned), and `app-url` carries
|
|
|
112
112
|
no code pin at all. No path, basename, or other host-machine string ever
|
|
113
113
|
enters this field; identity is digests, a sha, a boolean, and counts.
|
|
114
114
|
|
|
115
|
+
## Cost Estimate (advisory)
|
|
116
|
+
|
|
117
|
+
`cost` is optional and additive (`humanish.run-cost-summary.v1`): the
|
|
118
|
+
computer-use lane's run-level cost ESTIMATE — the sum of each lane's
|
|
119
|
+
token-derived model cost plus one aggregate E2B desktop-minute figure. It is an
|
|
120
|
+
ESTIMATE, never authoritative: every dollar is a rate-table multiply from the
|
|
121
|
+
operator-editable `src/pricing.ts`, carries the pricing `ratesAsOf` date and
|
|
122
|
+
`source`, and is surfaced with the "estimated (rates as of `<date>`)" label —
|
|
123
|
+
never a bare charge. It follows the same **declared-absent** discipline as the
|
|
124
|
+
terminal cost ledger: an unpriceable line stays present with
|
|
125
|
+
`estimatedCostUsd: null` + a `reason` and contributes nothing;
|
|
126
|
+
`estimatedTotalUsd` is `null` iff every line is null (never coerced to `0`).
|
|
127
|
+
Dry-runs and lanes that spend nothing omit `cost` entirely, so pre-existing
|
|
128
|
+
bundles stay byte-stable. Each lane's own estimate also rides its
|
|
129
|
+
`stream.actor.estimatedCost` (`humanish.actor-estimated-cost.v1`), kept distinct
|
|
130
|
+
from the reserved provider-returned `tokenUsage.costUsd`. See
|
|
131
|
+
[`schemas.md`](schemas.md) → Run Cost Summary And Estimated Actor Cost.
|
|
132
|
+
|
|
133
|
+
`humanish verify` treats cost as ADVISORY on magnitude and FAIL-CLOSED on
|
|
134
|
+
labeling: absence passes, but a claimed dollar figure without its `ratesAsOf`
|
|
135
|
+
date + `source`, or a total that does not match its known lines, fails. Verify
|
|
136
|
+
never inspects the magnitude — a correctly-labeled large estimate still passes.
|
|
137
|
+
|
|
115
138
|
## Adapter Score
|
|
116
139
|
|
|
117
140
|
`adapterScore` is optional and namespaced. It lets a downstream adapter summarize
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Date: 2026-06-02 (current-state note updated 2026-07-14)
|
|
4
4
|
|
|
5
5
|
Status: reference map for the major contracts shipped through source version
|
|
6
|
-
`0.
|
|
6
|
+
`0.19.0`; it is not an exhaustive inventory of command/result envelopes. Exported types,
|
|
7
7
|
schema constants, parsers, and validators in `src/` are authoritative. Rows
|
|
8
8
|
marked "reserved" name layering intent only — no code emits or validates them
|
|
9
9
|
yet. Do not emit a reserved schema.
|
|
@@ -43,6 +43,9 @@ workflow without leaking private upstream truth into core.
|
|
|
43
43
|
| Feedback | `humanish.feedback.v1` | `public-safe-feedback` |
|
|
44
44
|
| Terminal cost ledger | `humanish.terminal-cost-ledger.v1` | see Terminal Cost Ledger below |
|
|
45
45
|
| Terminal no-spend proof | `humanish.terminal-no-spend-proof.v1` | see Terminal Cost Ledger below |
|
|
46
|
+
| Pricing (operator-editable rates) | `humanish.pricing.v1` (`src/pricing.ts`; dated per-model + E2B desktop rates) | see Run Cost Summary And Estimated Actor Cost below |
|
|
47
|
+
| Run cost summary | `humanish.run-cost-summary.v1` (additive `RunBundle.cost`; estimate, never a charge) | see Run Cost Summary And Estimated Actor Cost below |
|
|
48
|
+
| Estimated actor cost | `humanish.actor-estimated-cost.v1` (additive `ActorTrace.estimatedCost`) | see Run Cost Summary And Estimated Actor Cost below |
|
|
46
49
|
| Adapter score | `humanish.adapter-score.v1` (`RunBundle.adapterScore`; namespaced; route-specific acceptance semantics) | see Product-Adapter Extension Seam below |
|
|
47
50
|
| Adapter artifact | `humanish.adapter-artifact.v1` (`RunBundle.adapterArtifacts[]`; namespaced; local relative proof references) | see Product-Adapter Extension Seam below |
|
|
48
51
|
| Shared-world evidence | `humanish.shared-world.v1` (additive `RunBundle.sharedWorld` + `RunBundle.attributionClass`; `topologyMode: sequential \| concurrent`) | see Shared-World Evidence below |
|
|
@@ -601,8 +604,20 @@ Core-owned fields:
|
|
|
601
604
|
- `startedAt` / `completedAt` / `durationMs`
|
|
602
605
|
- `status` / `completionReason` / `reason` (`completionReason` includes
|
|
603
606
|
`step_failed`: a deterministic scripted step/expectation evaluated false —
|
|
604
|
-
the subject failed the script while the harness executed faithfully
|
|
607
|
+
the subject failed the script while the harness executed faithfully; and
|
|
608
|
+
`budget_reached`: an open-ended watch session that hit the wall-clock time
|
|
609
|
+
budget AFTER productive activity — status `passed`, a NON-FAILURE completion,
|
|
610
|
+
distinct from `timed_out`, which stays reserved for a zero-progress deadline
|
|
611
|
+
hit and remains a failure)
|
|
605
612
|
- `ids`, `counts`, `items[]`, optional `tokenUsage`, `capabilities`
|
|
613
|
+
- optional `estimatedCost` (`humanish.actor-estimated-cost.v1`): a token-derived
|
|
614
|
+
cost ESTIMATE for this lane (see Run Cost Summary And Estimated Actor Cost).
|
|
615
|
+
It is deliberately a DIFFERENT field from `tokenUsage.costUsd`: a bare
|
|
616
|
+
`costUsd` is RESERVED for a real provider-returned charge, while
|
|
617
|
+
`estimatedCost.estimatedCostUsd` is a rate-table multiply, named honestly as
|
|
618
|
+
an estimate so a reader can never confuse the two (invariant 6). Absent on
|
|
619
|
+
codex/scripted lanes and on every pre-existing bundle; a `null`
|
|
620
|
+
`estimatedCostUsd` is DECLARED ABSENT (unknown rate / no usage), never 0.
|
|
606
621
|
|
|
607
622
|
Unexpected actor-loop diagnostics live inside `items[]` as
|
|
608
623
|
`kind: notice`, `status: error` rows. They are public-safe evidence, not crash
|
|
@@ -652,19 +667,24 @@ inside run bundles (per-stream transport and status) and lab execution config
|
|
|
652
667
|
|
|
653
668
|
`humanish serve` reports `humanish.serve-result.v1`. The exported `ServeResult`
|
|
654
669
|
type and `SERVE_SCHEMA` constant in `src/observer-serve.ts` are authoritative:
|
|
655
|
-
mode (`loopback |
|
|
656
|
-
|
|
657
|
-
`
|
|
658
|
-
operator's
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
670
|
+
mode (`loopback | exposed | share-safe-open`), the loopback host/port,
|
|
671
|
+
`publicUrl`, the `tunnel` provider/url, an `oauth` echo (`provider`,
|
|
672
|
+
`allowEmails`, `allowDomains` — operator-supplied allow rules, public-safe to
|
|
673
|
+
echo to the operator's own stdout, never persisted into any bundle), runs
|
|
674
|
+
listed, computed warnings, and the `ServeErrorCode` union. Exposure auth is
|
|
675
|
+
tunnel-edge only — as of 0.19.0 there are no `capabilityUrl`/`publicCapabilityUrl`
|
|
676
|
+
/`ttlMinutes` fields, no `--auth`/`--ttl` flags, and no `capability-link` mode
|
|
677
|
+
(the in-process `observer-auth.ts` capability-link was removed as a pre-1.0
|
|
678
|
+
breaking change).
|
|
679
|
+
|
|
680
|
+
Reserved: `/_humanish/api/*` is the serve control-plane namespace. Any request
|
|
681
|
+
under it answers `501` with error code `HUMANISH_SERVE_CONTROL_PLANE_DISABLED`.
|
|
682
|
+
Because the in-process auth gate is gone, a request that clears the edge (or a
|
|
683
|
+
loopback caller) reaches the `501` directly — there is no `401`-first anymore.
|
|
684
|
+
The typed `ServeControlPlane` parameter exists in the handler options and is
|
|
685
|
+
always `undefined` in v1; no code dispatches into it yet. Do not build against
|
|
686
|
+
the namespace; the reservation guarantees only that no run artifact or observer
|
|
687
|
+
asset will ever be served under it. See
|
|
668
688
|
[`docs/architecture/serve.md`](../architecture/serve.md) for the v2 seam
|
|
669
689
|
contract.
|
|
670
690
|
|
|
@@ -734,6 +754,82 @@ measure) and never grant a green pass (they surface as unmeasured). `verifyRun`
|
|
|
734
754
|
fails closed when a live bundle lacks the cost ledger or no-spend proof, when the
|
|
735
755
|
proof claims zero on a `null` line, or when known spend exceeds the declared cap.
|
|
736
756
|
|
|
757
|
+
## Run Cost Summary And Estimated Actor Cost
|
|
758
|
+
|
|
759
|
+
The computer-use (CUA) lane surfaces an ADVISORY, additive cost ESTIMATE. It is
|
|
760
|
+
never authoritative: every dollar figure is a rate-table multiply, labeled
|
|
761
|
+
"estimated (rates as of `<date>`)", and is NEVER presented as a provider charge
|
|
762
|
+
(invariant 6). Three new `.v1` schema tags ship, all additive and optional so
|
|
763
|
+
`humanish.run-bundle.v1` stays v1 and every pre-existing bundle is byte-stable:
|
|
764
|
+
|
|
765
|
+
- `humanish.pricing.v1` — the OPERATOR-EDITABLE rate table in `src/pricing.ts`:
|
|
766
|
+
dated per-model input/output USD-per-token rates and an E2B desktop
|
|
767
|
+
USD-per-minute rate, each with a public pricing-page `source` and an `asOf`
|
|
768
|
+
date. A prominent banner says these are estimates to update when providers
|
|
769
|
+
change pricing. Some entries are `placeholder: true` stand-ins (the shipped
|
|
770
|
+
`gpt-5.5` model rate and the E2B desktop rate) — an operator MUST confirm them
|
|
771
|
+
before trusting the magnitude; the flag propagates into every estimate so a
|
|
772
|
+
stand-in is never mistaken for a live rate. An UNKNOWN model/desktop rate is
|
|
773
|
+
DECLARED ABSENT (`estimatedCostUsd: null` + a `reason`), never guessed.
|
|
774
|
+
- `humanish.actor-estimated-cost.v1` — `ActorTrace.estimatedCost`: one lane's
|
|
775
|
+
token-derived model cost, with `estimatedCostUsd` (or `null` + `reason`
|
|
776
|
+
`no_rate_for_model`/`no_token_usage`), `ratesAsOf`, `source`, `modelId`,
|
|
777
|
+
optional `placeholder`, and a `breakdown`.
|
|
778
|
+
- `humanish.run-cost-summary.v1` — `RunBundle.cost`: the sum of every lane's
|
|
779
|
+
`model-tokens` line PLUS one aggregate `desktop-minutes` line.
|
|
780
|
+
|
|
781
|
+
The summary follows the SAME null discipline as the terminal cost ledger above.
|
|
782
|
+
`estimatedTotalUsd` sums ONLY the non-null `breakdown` lines and is `null` iff
|
|
783
|
+
EVERY line is null (never coerced to `0`); a present-but-unpriceable line stays
|
|
784
|
+
in `breakdown` with `estimatedCostUsd: null` + a `reason` (it records that we
|
|
785
|
+
tried and could not price it) and contributes nothing. `fullyEstimated` is
|
|
786
|
+
`false` when any applicable line is null (the total is then a lower bound);
|
|
787
|
+
`placeholder` is true when any contributing rate is a stand-in; `ratesAsOf` is
|
|
788
|
+
the MIN (oldest) `asOf` across contributing rates — an aggregate is only as fresh
|
|
789
|
+
as its stalest input, so MAX would overclaim freshness (each `breakdown` line
|
|
790
|
+
keeps its own true `asOf`). `desktopMinutes` is a HOST-SIDE
|
|
791
|
+
create→teardown span — an approximation of E2B's server-side billed lifetime, so
|
|
792
|
+
the desktop dollar figure is doubly an estimate.
|
|
793
|
+
|
|
794
|
+
```yaml
|
|
795
|
+
schema: humanish.run-cost-summary.v1
|
|
796
|
+
currency: usd
|
|
797
|
+
estimatedTotalUsd: 11.60167 # sum of KNOWN lines only; null iff every line null
|
|
798
|
+
ratesAsOf: "2026-08-01"
|
|
799
|
+
fullyEstimated: true # both breakdown lines are priced (no null line)
|
|
800
|
+
placeholder: true # a stand-in rate contributed
|
|
801
|
+
breakdown:
|
|
802
|
+
- { kind: model-tokens, laneId: lane-01, modelId: computer-use-preview,
|
|
803
|
+
estimatedCostUsd: 11.60, ratesAsOf: "2026-08-01", source: "openai.com/api/pricing" }
|
|
804
|
+
- { kind: desktop-minutes, estimatedCostUsd: 0.00167, ratesAsOf: "2026-08-01",
|
|
805
|
+
source: "…e2b.dev/pricing", placeholder: true }
|
|
806
|
+
tokenUsage: { input: 3843523, output: 5869, total: 3849392 }
|
|
807
|
+
desktopMinutes: 1
|
|
808
|
+
note: "Estimated 11.60167 USD total…"
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
`verifyRun` asserts LABELING/provenance, never MAGNITUDE. Absence PASSES
|
|
812
|
+
(fail-open on display): a bundle with no cost, a null estimate, or a lane without
|
|
813
|
+
`estimatedCost` verifies fine. A CLAIMED number FAILS closed when it lacks its
|
|
814
|
+
`ratesAsOf` date or `source`, or when `estimatedTotalUsd` does not equal the
|
|
815
|
+
rounded sum of its non-null lines (a null line coerced to 0 is a mechanism
|
|
816
|
+
mismatch). A correctly-labeled huge estimate still passes. Cost is neither a
|
|
817
|
+
secret nor a share-blocker, so it never affects `shareSafety`.
|
|
818
|
+
|
|
819
|
+
**Fail-closed spend cap.** `execution.caps.maxUsd` (the terminal lane's
|
|
820
|
+
`LabScenarioCaps` shape, consumed on the CUA route) aborts a session the moment
|
|
821
|
+
its running ESTIMATED spend crosses the cap — the runaway-retry guard. It is a
|
|
822
|
+
**PER-LANE** cap: enforced INSIDE each lane's loop independently, so an N-lane
|
|
823
|
+
fan-out can spend up to N × `maxUsd` before any lane aborts (the run bundle
|
|
824
|
+
warns with the true ~N × cap ceiling; a shared run-level budget is future work).
|
|
825
|
+
A lane that does real work THEN crosses its cap ends `budget_reached` (passed); a
|
|
826
|
+
zero-action runaway that crosses it ends `gave_up` (failed) — the cap classifies
|
|
827
|
+
its outcome honestly rather than greenlighting the runaway it exists to catch.
|
|
828
|
+
Absent = uncapped (the historical behavior); `maxUsd: 0` = no-spend. A cap on a
|
|
829
|
+
model `src/pricing.ts` cannot price is REFUSED at preflight
|
|
830
|
+
(`HUMANISH_CUA_LAB_UNPRICED_CAP`) before any sandbox rather than run uncapped —
|
|
831
|
+
an unenforceable cap is more dangerous than none.
|
|
832
|
+
|
|
737
833
|
## Product-Adapter Extension Seam
|
|
738
834
|
|
|
739
835
|
The terminal-product and browser/computer-use lanes let an adopter attach
|
package/docs/goals/current.md
CHANGED
|
@@ -16,7 +16,7 @@ Humanish should be the open-source CLI that lets a maintainer ask:
|
|
|
16
16
|
The answer should be observable, verifiable, public-safe, and easy to turn into
|
|
17
17
|
actionable feedback.
|
|
18
18
|
|
|
19
|
-
## Current Program Truth (source `0.
|
|
19
|
+
## Current Program Truth (source `0.19.0`)
|
|
20
20
|
|
|
21
21
|
The package source and repository implementation in this tree agree on these
|
|
22
22
|
points:
|
|
@@ -74,7 +74,8 @@ silently drifting from one is not.
|
|
|
74
74
|
| Single lane | Cost + evidence simplicity | Declared fan-out where the backend supports it — `actors[0].count: N` (homogeneous), explicit `actors[0].lanes[]` (differentiated persona/device/instruction), or compact `actors[0].roster[]` groups that normalize into lanes on the computer-use E2B route (per-lane worlds, cap 16; `execution.concurrency` bounds concurrent paid lanes) |
|
|
75
75
|
| Stock `desktop` template | The stock E2B desktop image is right for most subjects; absent `execution.desktop.template` keeps `Sandbox.create(opts)` byte-stable | `execution.desktop.template` names a custom E2B desktop image (any name/id, no allowlist) for a subject needing baked-in runtimes the stock image lacks (e.g. node/bun/a local Postgres) — threaded to `Sandbox.create(template, opts)` on every desktop-creating route and recorded in the bundle as `desktopTemplate` (public-safe) |
|
|
76
76
|
| Desktop default URL opener | Preserve the route/image's historical browser/default opener behavior when unset | `execution.desktop.browser: chrome | chromium | firefox` makes hosted CUA/shared-world browser choice explicit, fail-closed, and recorded as `desktopBrowser` in the run bundle |
|
|
77
|
-
| Loopback-only observer serving | Run bundles are local by default (public-safety plank 3); the serve
|
|
77
|
+
| Loopback-only observer serving | Run bundles are local by default (public-safety plank 3); the serve/watch surfaces bind `127.0.0.1` unconditionally, and exposing them is publishing-adjacent, so exposure is fail-closed and carries declared friction | `humanish serve --expose` behind TUNNEL-EDGE auth — ngrok `--oauth google` (`--allow-email`/`--allow-domain` allow rules) or an operator `--public-url` you secure — OR `--expose --safe` (open, gated on verify-`share_ready` runs only). `watch --expose` streams a live run behind edge auth (a live run is never `share_ready`, so `--safe` alone is refused). humanish carries no in-process auth; both print computed warnings naming exactly what became reachable |
|
|
78
|
+
| Generous wall-clock safety cap | `execution.timeoutMs` bounds runaway spend, not a goal; ~30 min is reasonable for open-ended "watch it play" | A session that reaches the cap AFTER productive activity is recorded as `budget_reached` (status `passed`, a NON-FAILURE completion, exit `0`), distinct from a zero-progress `timed_out` (a failure, exit `2`). Goal-directed labs set a tight `timeoutMs` and read `completionReason` to confirm the goal, not the cap |
|
|
78
79
|
|
|
79
80
|
## The placement rule (worked example)
|
|
80
81
|
|
package/docs/ramp/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Status: public-safe contributor and agent ramp.
|
|
4
4
|
|
|
5
|
-
Package/source version in this tree: `0.
|
|
5
|
+
Package/source version in this tree: `0.19.0` (2026-08-02). The containment boundary introduced in
|
|
6
6
|
`0.15.1` remains in force: managed run and output paths bind to validated
|
|
7
7
|
physical filesystem identities, and stored provider IDs are evidence, not
|
|
8
8
|
cleanup authority. The bundled OSS meta-lab is dry-run only until
|
package/package.json
CHANGED
package/dist/observer-auth.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
export declare const SERVE_COOKIE_NAME = "humanish_serve";
|
|
2
|
-
export interface ServeSessionStore {
|
|
3
|
-
mint(): {
|
|
4
|
-
cookieValue: string;
|
|
5
|
-
};
|
|
6
|
-
validate(cookieHeader: string | undefined): boolean;
|
|
7
|
-
revokeAll(): void;
|
|
8
|
-
sessionCount(): number;
|
|
9
|
-
}
|
|
10
|
-
export declare function mintServeToken(): string;
|
|
11
|
-
export declare function sha256Digest(value: string): Buffer;
|
|
12
|
-
export declare function verifyTokenDigest(candidate: string, digest: Buffer): boolean;
|
|
13
|
-
export declare function createServeSessionStore(options: {
|
|
14
|
-
ttlMs: number;
|
|
15
|
-
now?: () => number;
|
|
16
|
-
}): ServeSessionStore;
|
|
17
|
-
export declare function parseCookies(header: string | undefined): Record<string, string>;
|
|
18
|
-
export declare function buildSessionCookie(cookieValue: string, options: {
|
|
19
|
-
ttlSeconds: number;
|
|
20
|
-
secure: boolean;
|
|
21
|
-
}): string;
|
package/dist/observer-auth.js
DELETED
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
|
-
export const SERVE_COOKIE_NAME = "humanish_serve";
|
|
3
|
-
export function mintServeToken() {
|
|
4
|
-
return randomBytes(32).toString("base64url");
|
|
5
|
-
}
|
|
6
|
-
export function sha256Digest(value) {
|
|
7
|
-
return createHash("sha256").update(value, "utf8").digest();
|
|
8
|
-
}
|
|
9
|
-
// Hash-first comparison: both sides become fixed-length digests before
|
|
10
|
-
// timingSafeEqual, so an attacker-controlled candidate can neither throw on
|
|
11
|
-
// length mismatch nor learn the token length.
|
|
12
|
-
export function verifyTokenDigest(candidate, digest) {
|
|
13
|
-
return timingSafeEqual(sha256Digest(candidate), digest);
|
|
14
|
-
}
|
|
15
|
-
export function createServeSessionStore(options) {
|
|
16
|
-
const now = options.now ?? (() => Date.now());
|
|
17
|
-
const sessions = new Map();
|
|
18
|
-
return {
|
|
19
|
-
mint() {
|
|
20
|
-
// Sweep expired records on each mint so the map cannot grow without bound
|
|
21
|
-
// when many links are tapped: without this, a record is only pruned when
|
|
22
|
-
// its own cookie is presented again, which an abandoned session never is.
|
|
23
|
-
const currentTime = now();
|
|
24
|
-
for (const [digest, record] of sessions) {
|
|
25
|
-
if (record.expiresAt <= currentTime) {
|
|
26
|
-
sessions.delete(digest);
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
const cookieValue = randomBytes(32).toString("base64url");
|
|
30
|
-
sessions.set(sha256Digest(cookieValue).toString("hex"), {
|
|
31
|
-
expiresAt: currentTime + options.ttlMs,
|
|
32
|
-
scope: "viewer"
|
|
33
|
-
});
|
|
34
|
-
return { cookieValue };
|
|
35
|
-
},
|
|
36
|
-
validate(cookieHeader) {
|
|
37
|
-
const cookieValue = parseCookies(cookieHeader)[SERVE_COOKIE_NAME];
|
|
38
|
-
if (!cookieValue) {
|
|
39
|
-
return false;
|
|
40
|
-
}
|
|
41
|
-
const record = sessions.get(sha256Digest(cookieValue).toString("hex"));
|
|
42
|
-
if (!record || record.scope !== "viewer") {
|
|
43
|
-
return false;
|
|
44
|
-
}
|
|
45
|
-
if (record.expiresAt <= now()) {
|
|
46
|
-
sessions.delete(sha256Digest(cookieValue).toString("hex"));
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
return true;
|
|
50
|
-
},
|
|
51
|
-
revokeAll() {
|
|
52
|
-
sessions.clear();
|
|
53
|
-
},
|
|
54
|
-
sessionCount() {
|
|
55
|
-
return sessions.size;
|
|
56
|
-
}
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
export function parseCookies(header) {
|
|
60
|
-
if (!header) {
|
|
61
|
-
return {};
|
|
62
|
-
}
|
|
63
|
-
const cookies = {};
|
|
64
|
-
for (const part of header.split(";")) {
|
|
65
|
-
const separator = part.indexOf("=");
|
|
66
|
-
if (separator <= 0) {
|
|
67
|
-
continue;
|
|
68
|
-
}
|
|
69
|
-
const name = part.slice(0, separator).trim();
|
|
70
|
-
const value = part.slice(separator + 1).trim();
|
|
71
|
-
if (name) {
|
|
72
|
-
cookies[name] = value;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
return cookies;
|
|
76
|
-
}
|
|
77
|
-
export function buildSessionCookie(cookieValue, options) {
|
|
78
|
-
const attributes = [
|
|
79
|
-
`${SERVE_COOKIE_NAME}=${cookieValue}`,
|
|
80
|
-
"Path=/",
|
|
81
|
-
"HttpOnly",
|
|
82
|
-
"SameSite=Lax",
|
|
83
|
-
`Max-Age=${options.ttlSeconds}`
|
|
84
|
-
];
|
|
85
|
-
if (options.secure) {
|
|
86
|
-
attributes.push("Secure");
|
|
87
|
-
}
|
|
88
|
-
// Deliberately no Domain attribute: a host-only cookie on the public origin
|
|
89
|
-
// can never leak to sibling subdomains of a shared tunnel domain.
|
|
90
|
-
return attributes.join("; ");
|
|
91
|
-
}
|
|
92
|
-
//# sourceMappingURL=observer-auth.js.map
|