@hue-run/sdk 0.3.2 → 0.4.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.
Files changed (43) hide show
  1. package/CLI.md +270 -47
  2. package/ENVIRONMENTS.md +10 -0
  3. package/README.md +19 -3
  4. package/dist/client.d.ts +5 -5
  5. package/dist/client.js +13 -6
  6. package/dist/environment/tools.d.ts +6 -1
  7. package/dist/environment/tools.js +7 -1
  8. package/dist/environment/types.d.ts +6 -1
  9. package/dist/receipt.js +36 -8
  10. package/dist/setup/application.d.ts +74 -0
  11. package/dist/setup/application.js +766 -0
  12. package/dist/setup/backend.d.ts +229 -0
  13. package/dist/setup/backend.js +855 -0
  14. package/dist/setup/checkpoint.js +100 -30
  15. package/dist/setup/cli.js +20 -4
  16. package/dist/setup/configure.d.ts +13 -0
  17. package/dist/setup/configure.js +454 -0
  18. package/dist/setup/credential.d.ts +2 -0
  19. package/dist/setup/credential.js +9 -0
  20. package/dist/setup/detect.js +4 -1
  21. package/dist/setup/installation.d.ts +118 -0
  22. package/dist/setup/installation.js +605 -0
  23. package/dist/setup/lock.d.ts +2 -0
  24. package/dist/setup/lock.js +38 -0
  25. package/dist/setup/machine.d.ts +1 -10
  26. package/dist/setup/machine.js +8 -7
  27. package/dist/setup/render.d.ts +3 -1
  28. package/dist/setup/render.js +209 -6
  29. package/dist/setup/runner.d.ts +26 -76
  30. package/dist/setup/runner.js +320 -45
  31. package/dist/setup/socket.d.ts +7 -0
  32. package/dist/setup/socket.js +144 -0
  33. package/dist/setup/source.d.ts +9 -0
  34. package/dist/setup/source.js +269 -0
  35. package/dist/setup/types.d.ts +16 -9
  36. package/dist/setup/types.js +1 -1
  37. package/dist/setup.d.ts +6 -2
  38. package/dist/setup.js +3 -0
  39. package/dist/types.d.ts +24 -0
  40. package/dist/version.d.ts +1 -1
  41. package/dist/version.js +1 -1
  42. package/package.json +2 -1
  43. package/setup-events.schema.json +16 -9
package/CLI.md CHANGED
@@ -1,52 +1,275 @@
1
- # Hue setup-session CLI contract
1
+ # Hue setup CLI
2
2
 
3
- The local setup-session CLI core shipped in TypeScript `0.3.1` and is available from npm:
3
+ The local setup-session CLI core shipped in TypeScript `0.3.1`. TypeScript `0.4.0` is an unreleased,
4
+ activation-gated candidate for the real one-command flow:
4
5
 
5
6
  ```sh
6
- hue setup
7
- hue setup --agent
8
- hue resume
9
- hue status
10
- hue claim # reports account attachment unavailable in this local core
7
+ npx --yes @hue-run/sdk@latest setup --agent
8
+ # Default Terminal mode, when a person is present:
9
+ npx @hue-run/sdk@latest setup
11
10
  ```
12
11
 
13
- These commands belong only to an installer setup session. They do not create or launch a Hue Run,
14
- Scenario, evaluation, or worker. `claim` only means attaching the anonymous setup project to an
15
- account; there is no generic `connect` command or local-agent connection in this CLI.
16
-
17
- `setup` only inspects bounded manifest and lockfile metadata. It does not execute repository code,
18
- change project files, open a browser, ask a question, create a trial, or contact Hue. `claim` also
19
- makes no network request in this build and reports that account attachment is unavailable. `resume`
20
- deterministically continues the same setup-session checkpoint; `status` reads it without changing
21
- it.
22
-
23
- Checkpoints are secret-free JSON files outside the project, under the operating system's user state
24
- directory. On POSIX, directories use mode `0700` and files use mode `0600`; Windows uses its
25
- per-user local state directory without interpreting POSIX mode bits. Writes are atomic, and a
26
- configured state location inside the project is rejected. Checkpoints contain project categories
27
- and hashes, never environment values, credentials, source contents, or claim URLs.
28
-
29
- `--agent` is explicitly noninteractive JSONL. It never uses ANSI, stdin, or a browser, and each
30
- invocation emits exactly one terminal `run.completed` or `run.failed` installer event. Those names
31
- describe the setup-session lifecycle, not a Hue Run. Human output is an append-only inline
32
- transcript. Plain and JSONL output contain no ANSI; `NO_COLOR`, `TERM=dumb`, CI, and non-TTY output
33
- select plain mode automatically.
34
-
35
- Every JSONL record carries `contractVersion: 1`. The TypeScript union is exported from
36
- `@hue-run/sdk/setup`; the JSON Schema is exported as
37
- `@hue-run/sdk/setup-events.schema.json`. Consumers must ignore neither unknown versions nor terminal
38
- failures.
39
-
40
- The future Fern implementation plugs into `SetupBackendAdapter`. Its three installer operations
41
- create an anonymous setup trial hard-pinned to `trial_metadata_v1`, verify instrumentation-only
42
- receipt evidence, and read account-claim state. Inputs carry deterministic idempotency keys and an
43
- optional abort signal. Adapter results must use bounded, non-secret IDs; claim URLs may be sensitive
44
- and therefore must never be checkpointed. Receipt verification does not prove that task or
45
- environment content was captured and must never authorize Scenario publication. The adapter must
46
- not create a Scenario, evaluation, worker, or Hue Run. No live implementation ships in this slice.
47
-
48
- The V1 handoff is deliberately staged: setup verifies the anonymous instrumentation trace; `hue claim`
49
- preserves the project and trace history; then the user performs an explicit content-approved capture
50
- or rerun, with a prepared tester as the first golden path. Only that content-approved trace passes to
51
- the separate review/publication flow for a Scenario. The actual URL and that handoff contract remain
52
- deferred. Setup itself does not capture content, create, publish, or run a Scenario.
12
+ These public commands are not working onboarding promises until candidate publication, Fern
13
+ activation, production acceptance and `latest` promotion all pass. The automatic matrix is limited
14
+ to Express with npm, Express with Bun, and Flask with uv in a single application package with one recognizable
15
+ entrypoint, a literal existing GET route and `PORT` supplied by the environment. The command uses the
16
+ detected manager to install an exact runtime, inserts two owned middleware marker blocks without
17
+ rewriting business logic, starts that exact entrypoint without a shell, authenticates its loopback
18
+ socket before sending HTTP, makes one request on that same socket, flushes, and verifies its exact trace/span receipt.
19
+ A delayed listener, failing handler, missing telemetry or receipt failure never authorizes replay of
20
+ business work. A setup probe is separate transport evidence and cannot establish application
21
+ instrumentation.
22
+
23
+ Workspace roots, mixed languages/managers, custom Hue versions, edited marker blocks, unfamiliar
24
+ entrypoints and ambiguous routes stop with `action.required`. Follow its specific local action and
25
+ rerun the same installation; select the intended application package with `--project` when needed.
26
+ Setup does not choose a monorepo package or silently broaden its supported shapes. Preserve the
27
+ application's business logic when completing an integration manually.
28
+
29
+ The bounded fixture contract is ESM JavaScript/TypeScript Express with `const app = express();`,
30
+ one literal `app.get(...)`, an environment-selected listen port, and a start script containing only
31
+ `node path/to/server.mjs` (or `.js`, `.ts`, `.mts`) or `bun path/to/server.mjs`. A Bun start script
32
+ requires the Bun manager. Plain JavaScript does not need a TypeScript dependency. Flask uses root
33
+ `app.py`, `app = Flask(__name__)`, one literal `@app.get(...)` and a port read from
34
+ `os.environ["PORT"]`. Routes cannot contain network-path prefixes, escapes or dynamic segments.
35
+ JavaScript/TypeScript shebang/BOM entrypoints require manual integration. Python UTF-8/ASCII
36
+ shebang, encoding, comments, module docstring (including parenthesized forms) and future imports
37
+ are preserved using an isolated stdlib Python 3 syntax parser. Other encodings and ambiguous
38
+ statement layouts are refused. Package roots nested in an npm/Bun workspace or beneath any Python
39
+ project manifest are refused because managers can update ancestor locks; Python build-system/custom-source projects are also outside this matrix.
40
+ `uv` uses binary-only dependency installation and starts with no implicit sync/build.
41
+
42
+ The generated `hue.setup.mjs` or `hue_setup.py` always selects `captureContent: false` /
43
+ `capture_content=False`. For a supported application, setup installs the dependency and adds the
44
+ managed import and middleware registration to the existing entrypoint; an unreferenced helper is
45
+ not a completed integration. TypeScript uses `@hue-run/sdk@0.4.1`, `@opentelemetry/api@1.9.1` and
46
+ `@opentelemetry/context-async-hooks@2.11.0`; Python uses `hue-run==0.2.2`.
47
+ Content capture requires an ordinary account-managed key and a later explicit application decision.
48
+
49
+ The generated bootstrap supplies standard active SERVER-span context across asynchronous/streaming
50
+ handlers; the core SDK's global-provider/context ownership is unchanged. It preserves a working
51
+ caller context manager and disposes only its own instance. Unknown local imports, custom/late manager
52
+ bootstraps, runtime preloads and conflicting OTel dependencies require manual review before any
53
+ mutation or provisioning. Automatic Express entrypoints import only Express, the standard OTel API,
54
+ or the supported `node:fs`, `node:fs/promises`, `node:timers/promises` and `node:stream` modules.
55
+ Syntax parsing, not matches inside comments/strings/templates/regular expressions, establishes the
56
+ constructor and literal route. Custom middleware, alternate route methods, app aliases/escapes and
57
+ extra Flask handler decorators are outside the automatic matrix. The one selected request must finish with a 2xx response; a 404 or
58
+ telemetry failure never authorizes a business retry.
59
+
60
+ Bun runtime checks and application launch use an explicit empty config and disable dotenv loading.
61
+ Local runtime/preload settings, dotenv files and a global `.bunfig.toml` require manual review before
62
+ any Bun invocation. The only supported local `bunfig.toml` is an `[install]` section with one
63
+ credential-free registry origin for package installation; this does not become runtime config.
64
+
65
+ Automatic Express listeners must be the single top-level
66
+ `app.listen(Number(process.env.PORT), "127.0.0.1")` (the direct `process.env.PORT` argument
67
+ is also recognized). Flask permits only `app.run(port=int(os.environ["PORT"]))`, optionally
68
+ with `host="127.0.0.1"`, at module level or under the usual main guard. Custom callbacks,
69
+ server handles, socket metadata access, reloaders, workers and listener options require manual
70
+ integration. Flask dotenv/runtime bootstrap configuration also requires review.
71
+ Before dependency or credential changes, an isolated compile-only check validates the selected
72
+ Node/Bun entrypoint without executing it; Node-unsupported TypeScript syntax is refused.
73
+
74
+ During this setup-owned invocation only, a bounded server-first proof authenticates the retained
75
+ loopback connection. HTTP uses that exact socket once, with no redirect, redial or retry. Express
76
+ uses a private inner listener and a one-connection forwarding listener; Flask uses its standard
77
+ request-handler seam. The per-attempt secret stays in memory/private child transport, never in
78
+ events, checkpoints or receipts. Ordinary app starts have no handshake and retain their original
79
+ listener behavior. This prevents accidental requests to an unrelated listener, not access by
80
+ malicious code running as the same local user.
81
+
82
+ Setup creates no Scenario, Hue Run, evaluation, source capture, worker or remote execution. Package
83
+ manager lifecycle scripts are disabled. The supported existing application entrypoint is executed
84
+ directly with fixed argv solely for its bounded local HTTP verification; no shell command is accepted.
85
+
86
+ ## Technical preflight and privacy disclosure
87
+
88
+ Read-only project detection and conflict checks precede project mutation. Public
89
+ `GET /api/v1/setup/preflight` reports `available` or `inactive`, `metadata-only-v1`, trial limits and
90
+ lifetime, and the published privacy and security information. An inactive deployment remains
91
+ unverified. An available deployment proceeds through runtime installation, private installation and
92
+ credential provisioning, managed application wiring, one application request and exact receipt
93
+ verification.
94
+
95
+ Before telemetry, `privacy.notice` presents `https://hue.run/privacy`, effective date `2026-08-24`,
96
+ and `https://trust.hue.run/`. This is a non-blocking disclosure. Setup has no acceptance step or
97
+ acceptance fields and does not infer a person's acknowledgement. It does not invent notice content.
98
+
99
+ The anonymous ingestion window is 24 hours with lifetime limits of 100 traces, 1,000 spans and 2 MiB
100
+ of sanitized stored data. Unclaimed data is purged seven days after expiry. Claim preserves the
101
+ project and its data; the setup-issued replacement key remains metadata-only.
102
+
103
+ ## Commands and modes
104
+
105
+ ```sh
106
+ hue setup # provision/configure/verify, then prepare a private owner handoff
107
+ hue resume # continue the same project/origin installation
108
+ hue status # read status; after claim, reconcile generation 1 if necessary
109
+ hue claim # open the owner-only handoff, or reconcile after browser claim
110
+ hue claim --restart # explicit owner recovery; requires an interactive local terminal
111
+ hue setup --agent # noninteractive version-2 JSONL events
112
+ hue setup --format human # explicit append-only terminal rendering
113
+ hue setup --origin http://127.0.0.1:PORT # isolated loopback tests only
114
+ ```
115
+
116
+ Hosted setup accepts HTTPS origins only. HTTP is accepted solely for `localhost`, `127.0.0.1` and
117
+ `[::1]` test origins. Origins with credentials, paths, queries or fragments are refused, and
118
+ redirects are never followed.
119
+
120
+ `--agent` never reads stdin or opens a browser. It emits exactly one terminal `run.completed` or
121
+ `run.failed` event and only a generic owner action. No setup mode prints a bearer claim URL or token.
122
+ The CLI saves the capability in an ignored owner-only local handoff and human Terminal mode opens
123
+ only that local file; the browser consumes the short-lived one-time capability. A repository-reading
124
+ agent can read local project files, so this boundary prevents durable transcript/log disclosure, not
125
+ a malicious local process. The browser owns the claim cookie; the CLI never reads it. After claim
126
+ completes, rerun `hue claim` or `hue status` to fetch credential generation 1, atomically replace the
127
+ locally managed key, verify the preserved original receipt and confirm the old key receives `401`.
128
+ The superseded key is retained only in private managed state until that check succeeds, so an
129
+ interruption does not lose the revocation check. Reconciliation does not replay the application
130
+ request or require a second one.
131
+
132
+ A pending handoff expires after at most ten minutes and is exchanged once for a distinct browser
133
+ session lasting at most thirty minutes; both deadlines are bounded by trial expiry. Status polling
134
+ never creates or rotates a handoff. Retrying or reopening a pending handoff keeps its original ID
135
+ and expiry; a consumed handoff with an active browser session remains intact. Only the owner can
136
+ explicitly replace an expired or lost handoff with `hue claim --restart`; this persists a new ID and
137
+ uses the observed predecessor for compare-and-swap. A conflict refreshes status once and stops.
138
+ At most 32 distinct handoff IDs exist per installation; `SETUP_HANDOFF_LIMIT` is terminal. Replacing a
139
+ handoff does not create a trial, reset quota or rerun business work.
140
+
141
+ ## Local state and conflicts
142
+
143
+ Before its first network write, setup creates a lowercase UUIDv4 and a 32-byte random installation
144
+ secret. They live with the current telemetry credential in `.hue/installation-<origin-hash>.json`.
145
+ The file is added to `.gitignore`, written atomically with mode `0600`, and scoped to this exact
146
+ project and Hue origin. A pending browser capability is stored only in
147
+ `.hue/claim-handoff-<origin-hash>.html`, also ignored and mode `0600`; `.hue` uses mode `0700`.
148
+ Setup rejects symlinks, unsafe paths, oversized or invalid state, custom files at its managed config
149
+ paths, and unexpected edits to files it previously managed. It preserves unrelated `.gitignore`,
150
+ manifests, lockfiles and project files.
151
+
152
+ Secret-free checkpoints remain in the operating system's per-user state directory and support
153
+ interruption/resume. Claim capabilities never enter checkpoints or public events. Credential retries are idempotent; a lost
154
+ credential response is recovered by asking for the same generation. `SETUP_CHANGED` triggers one
155
+ status refresh. `SETUP_REVOKED`, expired/purged installations, custom credential conflicts and
156
+ exhausted lifetime quota fail closed instead of silently replacing an installation or key.
157
+
158
+ Commands serialize all origins for one canonical project and reload current state under that lock.
159
+ Every invocation re-detects current manifests, managers and managed blocks before mutation. Normal
160
+ interruptions release the lock; after a forcible process kill, the owner must inspect the stale
161
+ non-secret lock under canonical `/tmp` on Linux/macOS
162
+ (`hue-setup-locks-<uid>/<sha256(canonical-project-root)>`) and remove only that lock after confirming
163
+ no setup command remains active. Its location does not depend on `TMPDIR`, `HOME` or state-directory
164
+ overrides; project aliases and different Hue origins share ownership. Other operating systems fail
165
+ closed for automatic setup. Setup never breaks an unexplained lock automatically or retries
166
+ business work.
167
+
168
+ Both setup credential generations use exactly
169
+ `^hue_setup_(live|test)_setup-([a-f0-9]{24})_([A-Za-z0-9_-]{43})$`, with `keyId` equal to
170
+ `setup-` plus the token's 24-character identifier, fixed kind `anonymous_trial`, and the sole
171
+ capability `setup_telemetry_write`.
172
+ Normal project credentials and unknown token shapes are refused in setup responses and private
173
+ managed state. These credentials authorize metadata-only OTLP at `/api/v1/otlp/v1/traces` and exact
174
+ content-free receipt verification at `/api/v1/setup/traces/{traceId}/receipt`. They do not authorize
175
+ generic project, receipt, evaluation, log or browsing APIs. Python `0.2.2` can export with the setup
176
+ credential; the CLI verifies the dedicated setup receipt instead of Python's generic receipt helper.
177
+
178
+ Each command uses bounded timeouts and retries. Provisioning records at most five attempts per local
179
+ installation in an hour and makes at most two attempts in one invocation; the live service's stricter
180
+ per-network admission remains authoritative. Receipt polling honors the SDK's bounded deadline and
181
+ `Retry-After`. A missing or late receipt is reported as resumable and unverified, never as success.
182
+
183
+ The TypeScript event union is exported from `@hue-run/sdk/setup`; its JSON Schema is exported as
184
+ `@hue-run/sdk/setup-events.schema.json`. Event `run.*` names describe only CLI invocations, not Hue
185
+ Runs.
186
+
187
+ ## Public machine events and actions
188
+
189
+ Events carry `contractVersion: 2`, a non-secret invocation ID, sequence and timestamp. The closed
190
+ event set is `run.started`, `project.detected`, `plan.ready`, `step.started`, `step.completed`,
191
+ `file.changed`, `diagnostic`, `privacy.notice`, `action.required`, `trial.created`,
192
+ `receipt.verified`, `claim.required`, `claim.completed`, `run.completed` and `run.failed`.
193
+
194
+ Event version 1 shipped in TypeScript `0.3.1` and `0.3.2`; its public claim URL, action set and receipt
195
+ step are incompatible with this flow. The v2 schema shipped in TypeScript `0.4.0` with identity
196
+ `https://hue.run/schemas/setup-events-v2.json`. Event version 2 is independent of Setup HTTP
197
+ `protocolVersion: 1` and the checkpoint format.
198
+
199
+ `privacy.notice` has only `privacyUrl`, `effectiveDate` and `securityUrl` beyond the event envelope,
200
+ with the literal values above. `receipt.verified` carries non-secret `receiptId`, `traceId` and
201
+ `source: "repository-http-boundary"`. It confirms the exercised application boundary and every
202
+ expected span, positive stored span count, empty missing IDs and absent input/output content.
203
+ It does not establish instrumentation of unexercised application paths. `claim.required` and
204
+ `claim.completed` carry only the non-secret `claimId`; neither contains a URL or capability.
205
+
206
+ The closed `action.required.action` set is:
207
+
208
+ | Action | Caller response |
209
+ | --- | --- |
210
+ | `claim-project` | Defer account linkage to the project owner. |
211
+ | `configure` | Resolve the reported availability or managed-configuration issue. |
212
+ | `select-project` | Select one supported application package explicitly with `--project`. |
213
+ | `integrate-application` | Resolve the reported unsupported or ambiguous integration safely. |
214
+ | `run-instrumented-request` | Inspect the prior attempt and missing evidence; do not automatically replay business work. |
215
+ | `open-claim-handoff` | Ask the owner to run `hue claim` in an interactive local terminal. |
216
+ | `restart-claim-handoff` | Ask the owner to explicitly run `hue claim --restart` if recovery is needed. |
217
+
218
+ A coding agent follows the non-secret message and optional command, reports unverified steps, and
219
+ leaves browser account linkage for the human. It must not read or copy the private handoff into a
220
+ transcript. `run.completed` with `action_required` is a successful pause, not proof that all setup
221
+ steps completed. `claim.completed` requires reconciliation and the retained evidence checks.
222
+
223
+ ## Staging/live acceptance runner
224
+
225
+ Fern owns hosted acceptance and its private browser/inbox orchestration. SDK verification builds and
226
+ retains the exact tarball; it does not spend a live admission or claim hosted success. The Fern job
227
+ first accepts the reviewed PR archive, then after merge downloads the exact `publish=false` Release
228
+ SDK archive without repacking and repeats acceptance. It uses two persistent installations and at
229
+ most four provisioning admissions. Local compatibility fixtures do not spend hosted admissions.
230
+
231
+ The local/manual diagnostic runner accepts an already prepared supported application fixture. Its `--language`
232
+ scaffolding does not create such an application, and its human-output option does not allocate a PTY.
233
+ It records CLI outcomes, not independent original-handler span binding or server evidence. It always
234
+ exits nonzero: exit `2` means the diagnostic finished but acceptance remains unverified; malformed
235
+ events, private output and child failures also fail closed. Even a valid version-2 application receipt
236
+ event followed by `ready` cannot make this runner an acceptance gate. Do not use it to authorize a
237
+ release, count it as hosted acceptance, or suppress its exit code in a release gate:
238
+
239
+ ```sh
240
+ node packages/sdk-typescript/scripts/verify-package.mjs --artifacts-dir .artifacts/typescript
241
+ # Set project to an existing supported fixture; use the same directory on resume.
242
+ project=/absolute/path/to/supported-fixture
243
+ node packages/sdk-typescript/scripts/verify-setup-live.mjs \
244
+ --archive .artifacts/typescript/hue-run-sdk-0.4.1.tgz \
245
+ --origin https://STAGING_ORIGIN \
246
+ --project "$project" --command setup \
247
+ --evidence .context/setup-staging-before-claim.json
248
+ ```
249
+
250
+ After the approved backend is available, run `hue claim` from an interactive owner terminal to open
251
+ the private local handoff and finish the real browser claim, then reconcile the same project:
252
+
253
+ ```sh
254
+ node packages/sdk-typescript/scripts/verify-setup-live.mjs \
255
+ --archive .artifacts/typescript/hue-run-sdk-0.4.1.tgz \
256
+ --origin https://STAGING_ORIGIN \
257
+ --project "$project" --command claim \
258
+ --evidence .context/setup-staging-after-claim.json
259
+ ```
260
+
261
+ Repeat from an existing supported Flask/uv fixture when that diagnostic pass is budgeted. Evidence files explicitly
262
+ record `purpose: "diagnostic-only"`, `independentlyVerifiedApplication: false` and `accepted: false`,
263
+ with the archive hash, bounded event names and terminal outcome—never the claim capability, installation proof,
264
+ telemetry key, cookie, verification URL, project path or trace/span IDs. The runner installs and
265
+ executes the exact tarball; it does not establish hosted acceptance, registry publication or
266
+ production activation.
267
+
268
+ Hosted acceptance separately executes installed Express/npm, Express/Bun and Flask/uv fixture
269
+ coverage, with Agent JSONL and a real default-mode PTY. It independently verifies exact stored IDs,
270
+ the privacy disclosure, one handler invocation, verified-account adoption and owner transfer,
271
+ preserved project/data, replacement receipt access and old-key `401`. Browser orchestration opens
272
+ only the private local file and produces no secret-bearing screenshots, traces, logs or artifacts.
273
+ Publication under a candidate dist-tag, registry acceptance, production activation, promotion of
274
+ the same version to `latest`, and clean-project literal `@latest` smoke remain separate gates; see
275
+ [RELEASING.md](../../RELEASING.md).
package/ENVIRONMENTS.md CHANGED
@@ -161,6 +161,16 @@ await tools.refund_charge!.execute({ charge_id: "ch_2" });
161
161
  await client.finishRun(run.id, { idempotencyKey: randomUUID(), status: "completed" });
162
162
  ```
163
163
 
164
+ Each bound call is an ordinary `hue.tool` span. When the catalog names an MCP server, the span
165
+ also carries `mcp.server.name`. Wrap any MCP client the same way, using `serverInfo` from
166
+ `initialize` — this is not specific to Hue-hosted Gmail or Slack:
167
+
168
+ ```ts
169
+ await hue.tool(name, args, () => client.callTool({ name, arguments: args }), {
170
+ mcp: client.getServerVersion(),
171
+ });
172
+ ```
173
+
164
174
  An observation with `status: "error"` is a recorded world answer, not a transport exception.
165
175
  Run mutations retry with stable invocation/idempotency identities. Registry writes do not retry
166
176
  automatically because identity creation and publication have no request key.
package/README.md CHANGED
@@ -65,6 +65,18 @@ HTTPS is required except for loopback HTTP or the explicit
65
65
  [`allowInsecureHttp`](#local-development-without-a-hue-account) opt-in. Redirects are refused for both
66
66
  project checks and exports.
67
67
 
68
+ The setup CLI shipped in TypeScript `0.4.0` also prepares the `hue` executable. The unreleased
69
+ `npx --yes @hue-run/sdk@latest setup --agent` path supports Express with npm, Express with Bun, and
70
+ Flask with uv in one application package with an unambiguous entrypoint and existing GET route. It
71
+ installs the runtime, wires the application, makes one request and verifies that request's exact
72
+ trace/span receipt. Unsupported or ambiguous repositories receive a structured action. Technical
73
+ preflight checks availability and presents the published [privacy notice](https://hue.run/privacy)
74
+ and [security information](https://trust.hue.run/) before telemetry. Anonymous trials last 24 hours
75
+ and are limited to 100 traces, 1,000 spans and 2 MiB. A private owner-only browser handoff supports
76
+ account linkage; the original request evidence is retained and business work is never replayed on
77
+ claim. Setup never enables content capture or creates a Scenario, Hue Run, evaluation, source capture
78
+ or remote execution. See the [setup CLI contract](./CLI.md) for the supported shapes and release gates.
79
+
68
80
  `checkConnection()` rejects with `HueConnectionError`: its fixed message is safe to log, `status`
69
81
  carries the HTTP status when Hue answered, and `cause` carries the underlying network, timeout or
70
82
  parsing error. `serviceVersion` and `resourceAttributes` (for example
@@ -120,7 +132,10 @@ options come after the callback and also accept `name`, `sessionId`, `userId`, `
120
132
  nonnegative integer `gen_ai.usage.input_tokens` / `output_tokens`; other values are omitted and
121
133
  counted as instrumentation failures. Unknown usage stays absent. `hue.tool(name, input, execute)`
122
134
  creates an `execute_tool {name}` span with `gen_ai.tool.name`, arguments and result; an optional
123
- fourth argument `{ callId }` records the provider's tool call id as `gen_ai.tool.call.id`. Content
135
+ fourth argument `{ callId }` records the provider's tool call id as `gen_ai.tool.call.id`. When the
136
+ tool came from an MCP server, pass `{ mcp: client.getServerVersion() }` (the MCP `initialize`
137
+ `serverInfo`) to record `mcp.server.name` and `mcp.server.version` so a generic verb such as
138
+ `get_thread` is attributed to that server. Content
124
139
  helpers (`setInput`, `setOutput`, `tool` arguments and results, `recordMessages`,
125
140
  `SpanOptions.input`) accept any value and encode plain JSON data (`JsonValue`) at runtime; a value
126
141
  that is not JSON, such as a `Date` or a class instance, is omitted with an instrumentation failure
@@ -412,8 +427,9 @@ Scorer deferral shipped in TypeScript `0.3.1`. Only built-ins
412
427
  and bound `local_code` callbacks run here; other pins remain pending for their authorized executor.
413
428
  See [scorer execution](EVALUATIONS.md#hosted-and-manual-scorer-pins).
414
429
 
415
- The [setup CLI](CLI.md) is a resumable local inspection core. Existing customers connect their
416
- agents with `runLocalAgent()`; setup does not register workers or launch Scenarios.
430
+ The published [setup CLI](CLI.md) is a resumable local inspection core; the unreleased `0.4.0`
431
+ candidate adds the bounded application onboarding flow described above. Existing customers connect
432
+ their agents with `runLocalAgent()`; setup does not register workers or launch Scenarios.
417
433
 
418
434
  ## Managed targets
419
435
 
package/dist/client.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type Context, type Span, type Tracer } from "@opentelemetry/api";
2
2
  import { HueTransport } from "./transport.js";
3
- import type { ExportReport, FlushableLoggerProvider, FlushableTracerProvider, HueOptions, HueSpan, ModelOptions, ProjectConnection, SpanOptions, VerifyTraceOptions, TraceVerification, SafeLifecycleOptions, SafeLifecycleResult } from "./types.js";
3
+ import type { ExportReport, FlushableLoggerProvider, FlushableTracerProvider, HueOptions, HueSpan, ModelOptions, ProjectConnection, SpanOptions, ToolOptions, VerifyTraceOptions, TraceVerification, SafeLifecycleOptions, SafeLifecycleResult } from "./types.js";
4
4
  /**
5
5
  * Attach mode: the application owns its OpenTelemetry providers and passes the transport whose
6
6
  * processors it attached to them. The client flushes these providers but never shuts them down.
@@ -73,11 +73,11 @@ export declare class HueClient {
73
73
  * are recorded as `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result` when `captureContent`
74
74
  * is true; values that are not JSON-encodable are omitted with an instrumentation failure.
75
75
  * `options.callId` is recorded as `gen_ai.tool.call.id`, like the Python `call_id=` keyword.
76
+ * `options.mcp` records the MCP `initialize` `serverInfo` as `mcp.server.name` /
77
+ * `mcp.server.version` so a generic tool name can be attributed to the server that
78
+ * handled it. Pass `client.getServerVersion()`.
76
79
  */
77
- tool<T>(name: string, input: unknown, execute: () => Promise<T> | T, options?: Pick<SpanOptions, "parentContext"> & {
78
- /** Provider-issued identifier of this tool call, recorded as `gen_ai.tool.call.id`. */
79
- callId?: string;
80
- }): Promise<T>;
80
+ tool<T>(name: string, input: unknown, execute: () => Promise<T> | T, options?: ToolOptions): Promise<T>;
81
81
  /**
82
82
  * Runs `callback` inside a GenAI client span for one direct provider call, named
83
83
  * `{operation} {model}` unless `options.name` is given and carrying `gen_ai.operation.name`,
package/dist/client.js CHANGED
@@ -298,20 +298,27 @@ export class HueClient {
298
298
  * are recorded as `gen_ai.tool.call.arguments` / `gen_ai.tool.call.result` when `captureContent`
299
299
  * is true; values that are not JSON-encodable are omitted with an instrumentation failure.
300
300
  * `options.callId` is recorded as `gen_ai.tool.call.id`, like the Python `call_id=` keyword.
301
+ * `options.mcp` records the MCP `initialize` `serverInfo` as `mcp.server.name` /
302
+ * `mcp.server.version` so a generic tool name can be attributed to the server that
303
+ * handled it. Pass `client.getServerVersion()`.
301
304
  */
302
305
  async tool(name, input, execute, options = {}) {
303
306
  const attributes = {
304
307
  "gen_ai.operation.name": "execute_tool",
305
308
  "gen_ai.tool.name": name,
306
309
  };
307
- const callId = options.callId;
308
- if (callId !== undefined) {
309
- // A blank or non-string id is omitted and counted; the tool call itself still runs.
310
- if (isLabel(callId))
311
- attributes["gen_ai.tool.call.id"] = callId;
310
+ const stamp = (key, value) => {
311
+ if (value === undefined)
312
+ return;
313
+ // A blank or non-string label is omitted and counted; the tool call itself still runs.
314
+ if (isLabel(value))
315
+ attributes[key] = value;
312
316
  else if (this.enabled && !this.closed)
313
317
  this.transport.instrumentationFailure();
314
- }
318
+ };
319
+ stamp("gen_ai.tool.call.id", options.callId);
320
+ stamp("mcp.server.name", options.mcp?.name);
321
+ stamp("mcp.server.version", options.mcp?.version);
315
322
  return this.withSpan(`execute_tool ${name}`, async ({ span }) => {
316
323
  this.setContent(span, "gen_ai.tool.call.arguments", input);
317
324
  const result = await execute();
@@ -26,5 +26,10 @@ export interface BindEnvironmentToolsOptions {
26
26
  /** Optional durable invocation-ID factory for caller-owned resume state. */
27
27
  invocationId?(action: string): string;
28
28
  }
29
- /** Binds a run's generated catalog to plain local callables without changing the agent framework. */
29
+ /**
30
+ * Binds a run's generated catalog to plain local callables without changing the agent
31
+ * framework. Each call is recorded through {@link HueClient.tool}. Catalog entries that
32
+ * include `mcp` stamp `mcp.server.name` / `mcp.server.version` so a generic verb is
33
+ * attributed to that MCP server.
34
+ */
30
35
  export declare function bindEnvironmentTools(options: BindEnvironmentToolsOptions): Record<string, EnvironmentTool>;
@@ -1,5 +1,10 @@
1
1
  import { randomUUID } from "node:crypto";
2
- /** Binds a run's generated catalog to plain local callables without changing the agent framework. */
2
+ /**
3
+ * Binds a run's generated catalog to plain local callables without changing the agent
4
+ * framework. Each call is recorded through {@link HueClient.tool}. Catalog entries that
5
+ * include `mcp` stamp `mcp.server.name` / `mcp.server.version` so a generic verb is
6
+ * attributed to that MCP server.
7
+ */
3
8
  export function bindEnvironmentTools(options) {
4
9
  const tools = {};
5
10
  for (const action of options.run.actions) {
@@ -16,6 +21,7 @@ export function bindEnvironmentTools(options) {
16
21
  })).observation;
17
22
  return options.hue.tool(action.name, args, execute, {
18
23
  parentContext: options.parentContext,
24
+ ...(action.mcp === undefined ? {} : { mcp: action.mcp }),
19
25
  });
20
26
  },
21
27
  };
@@ -1,4 +1,4 @@
1
- import type { JsonValue } from "../types.js";
1
+ import type { JsonValue, McpServerInfo } from "../types.js";
2
2
  export type { JsonValue } from "../types.js";
3
3
  /** Human-readable identity used when creating an environment. */
4
4
  export interface EnvironmentIdentity {
@@ -251,6 +251,11 @@ export interface ActionDefinition {
251
251
  description?: string;
252
252
  /** Generated input contract. */
253
253
  inputSchema: ActionSchema;
254
+ /**
255
+ * MCP `initialize` identity when this action is served by one MCP surface.
256
+ * Bound environment tools record it on the tool span as `mcp.server.name`.
257
+ */
258
+ mcp?: McpServerInfo;
254
259
  }
255
260
  /** Recorded answer returned by the simulated world. */
256
261
  export interface Observation {
package/dist/receipt.js CHANGED
@@ -31,7 +31,7 @@ function validId(value, length) {
31
31
  function record(value) {
32
32
  return value !== null && typeof value === "object" && !Array.isArray(value);
33
33
  }
34
- function parseReceipt(value, traceId, expected, origin) {
34
+ function parseReceipt(value, traceId, expected, origin, setup = false) {
35
35
  if (!record(value) ||
36
36
  value.traceId !== traceId ||
37
37
  !Number.isSafeInteger(value.spanCount) ||
@@ -52,6 +52,24 @@ function parseReceipt(value, traceId, expected, origin) {
52
52
  }
53
53
  if (traceUrl.origin !== origin || traceUrl.username || traceUrl.password)
54
54
  invalidResponse();
55
+ if (setup) {
56
+ const uuid = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/u;
57
+ if (traceUrl.href !== value.traceUrl ||
58
+ traceUrl.hash ||
59
+ !traceUrl.pathname.startsWith("/traces/") ||
60
+ !uuid.test(traceUrl.pathname.slice(8)))
61
+ invalidResponse();
62
+ const queryNames = [];
63
+ traceUrl.searchParams.forEach((value, name) => {
64
+ if (!["projectId", "organizationId"].includes(name) ||
65
+ queryNames.includes(name) ||
66
+ !uuid.test(value))
67
+ invalidResponse();
68
+ queryNames.push(name);
69
+ });
70
+ if (queryNames.length !== 2)
71
+ invalidResponse();
72
+ }
55
73
  const matched = value.matchedSpanIds, missing = value.missingSpanIds;
56
74
  if (!Array.isArray(matched) ||
57
75
  !Array.isArray(missing) ||
@@ -128,6 +146,13 @@ async function pause(milliseconds, signal) {
128
146
  }
129
147
  /** Observe persisted evidence after the application and its exporter have finished. */
130
148
  export async function verifyTrace(connection, traceId, options = {}) {
149
+ return verifyTraceAtPath(connection, traceId, options, false, fetch);
150
+ }
151
+ /** @internal Dedicated receipt path for setup credentials; ordinary clients stay unchanged. */
152
+ export async function verifySetupTrace(connection, traceId, options, fetcher, signal) {
153
+ return verifyTraceAtPath(connection, traceId, options, true, fetcher, signal);
154
+ }
155
+ async function verifyTraceAtPath(connection, traceId, options, setup, fetcher, signal) {
131
156
  if (!validId(traceId, 32))
132
157
  throw new TypeError("traceId must be a nonzero lowercase 32-character OpenTelemetry trace ID");
133
158
  if (options === null || typeof options !== "object" || Array.isArray(options))
@@ -148,27 +173,28 @@ export async function verifyTrace(connection, traceId, options = {}) {
148
173
  throw new TypeError("timeoutMillis must be greater than zero and at most 60000");
149
174
  // Snapshot caller arrays so concurrent mutation cannot alter the verification criteria.
150
175
  const expectedIds = [...expected], requiredFields = [...required];
151
- const url = new URL(`/api/v1/traces/${traceId}/receipt`, connection.baseUrl);
176
+ const url = new URL(`/api/v1/${setup ? "setup/" : ""}traces/${traceId}/receipt`, connection.baseUrl);
152
177
  for (const id of expectedIds)
153
178
  url.searchParams.append("expectedSpanId", id);
154
179
  const controller = new AbortController();
155
180
  const deadline = performance.now() + timeout;
156
181
  const timer = setTimeout(() => controller.abort(), timeout);
182
+ const requestSignal = signal ? AbortSignal.any([signal, controller.signal]) : controller.signal;
157
183
  let receipt = null;
158
184
  let delay = 250;
159
185
  try {
160
- while (!controller.signal.aborted && performance.now() < deadline) {
161
- const response = await fetch(url, {
186
+ while (!requestSignal.aborted && performance.now() < deadline) {
187
+ const response = await fetcher(url, {
162
188
  headers: { Authorization: `Bearer ${connection.apiKey}`, Accept: "application/json" },
163
189
  redirect: "manual",
164
190
  credentials: "omit",
165
191
  cache: "no-store",
166
- signal: controller.signal,
192
+ signal: requestSignal,
167
193
  });
168
194
  let retryAfter = 0;
169
195
  if (response.status === 200) {
170
- receipt = parseReceipt(await readJson(response), traceId, expectedIds, url.origin);
171
- if (!controller.signal.aborted &&
196
+ receipt = parseReceipt(await readJson(response), traceId, expectedIds, url.origin, setup);
197
+ if (!requestSignal.aborted &&
172
198
  performance.now() < deadline &&
173
199
  receipt.missingSpanIds.length === 0 &&
174
200
  requiredFields.every((field) => receipt.fields[field]))
@@ -195,7 +221,7 @@ export async function verifyTrace(connection, traceId, options = {}) {
195
221
  if (remaining <= 0)
196
222
  break;
197
223
  const wait = Math.max(delay, retryAfter);
198
- await pause(Math.min(wait, remaining), controller.signal);
224
+ await pause(Math.min(wait, remaining), requestSignal);
199
225
  // A truncated backoff exhausts this call even if a timer wakes just early.
200
226
  if (wait >= remaining)
201
227
  break;
@@ -203,6 +229,8 @@ export async function verifyTrace(connection, traceId, options = {}) {
203
229
  }
204
230
  }
205
231
  catch (error) {
232
+ if (signal?.aborted)
233
+ throw new Error("Setup interrupted");
206
234
  if (!controller.signal.aborted && performance.now() < deadline) {
207
235
  if (error instanceof HueTraceVerificationError)
208
236
  throw error;