metergraph-cli 0.2.0-preview.0 → 0.2.0-preview.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # metergraph-cli
2
2
 
3
- The Metergraph command line tool. This checkout is a **development preview**, version
4
- `0.2.0-preview.0`, which has not been published.
3
+ The Metergraph command line tool. Version `0.2.0-preview.2` is published on the
4
+ `next` npm tag. The `latest` tag remains on `0.1.0`.
5
5
 
6
6
  Install the released preview channel with npm or run it directly:
7
7
 
@@ -11,42 +11,45 @@ npx --yes metergraph-cli@next doctor --json
11
11
  npm install -g metergraph-cli@next
12
12
  ```
13
13
 
14
- The installed command is `metergraph`. Pin `metergraph-cli@0.1.0` for that exact preview.
14
+ The installed command is `metergraph`. Pin `metergraph-cli@0.2.0-preview.2`
15
+ when you need this exact preview rather than whichever version `next` names later.
15
16
 
16
17
  **Availability:**
17
18
 
18
- - The published package, `metergraph-cli@0.1.0`, contains only `doctor` and
19
- `skill install` / `skill update`. It has no sign in commands.
20
- - `login` and `logout`, described below, exist only in this checkout. They are an
21
- upcoming preview: run them from a checkout or a locally packed tarball (see
22
- [Development](#development)). Do not expect them from `npx metergraph-cli` until a
23
- release that includes them is announced.
24
- - They also need a Metergraph service that offers Metadata-only CLI sign in and grant
19
+ - `metergraph-cli@0.1.0` contains only `doctor` and `skill install` /
20
+ `skill update`. It has no sign in commands.
21
+ - `metergraph-cli@0.2.0-preview.0` adds `login`, `logout`, `setup`, `verify`,
22
+ `status`, `context`, `capabilities`, `usage`, `routes` and `traces`.
23
+ - `metergraph-cli@0.2.0-preview.1` makes `setup` write the SDK service root as
24
+ `METERGRAPH_INGEST_URL` and repairs the full ingest endpoint that
25
+ `0.2.0-preview.0` wrote, after checking the saved key. It also keeps validated,
26
+ workspace-bound trace links in Metadata reads.
27
+ - `metergraph-cli@0.2.0-preview.2` makes `verify` exit 11 `verification_failed`
28
+ when its deadline expires after the service reported the trace as pending, lets
29
+ `login` and `setup` reruns that need no approval succeed with `--json
30
+ --no-browser`, and reports why `verify --open` did not launch a browser.
31
+ - Sign in needs a Metergraph service that offers Metadata-only CLI grants and grant
25
32
  revocation. A service without them is reported as unsupported, and the CLI never
26
33
  falls back to broader access.
27
- - The read commands `status`, `context`, `capabilities`, `usage`, `routes` and `traces`
28
- also exist only in this checkout and are part of the same unpublished upcoming preview.
29
- They need a project signed in with `login`.
30
- - `setup` also exists only in this checkout. It guides sign in and workspace choice,
31
- then requires the deployment's separate ingest bootstrap API and browser approval
32
- by a member of that workspace.
33
- - `verify` also exists only in this checkout. It checks one exact trace identity in an
34
- explicit invocation window using Metadata access. It never sends application data.
34
+ - The read commands need a project signed in with `login`. `setup` also needs the
35
+ deployment's ingest bootstrap API and browser approval by a workspace member.
36
+ `verify` checks one exact trace identity in an explicit invocation window using
37
+ Metadata access. It never sends application data.
35
38
 
36
- This checkout includes:
39
+ The preview includes:
37
40
 
38
41
  - `doctor` checks whether a Metergraph service is reachable, healthy and supported.
39
42
  - `skill install` and `skill update` copy the Metergraph agent skill bundled with the
40
43
  CLI into one coding agent's project skill directory.
41
- - `login` and `logout` (checkout only) sign a project in to one workspace through your
44
+ - `login` and `logout` sign a project in to one workspace through your
42
45
  browser with a delegated, Metadata-only grant, and sign it out again.
43
- - The read commands (checkout only) use that grant to read bounded workspace Metadata:
46
+ - The read commands use that grant to read bounded workspace Metadata:
44
47
  connection status, workspace context, capabilities, daily usage, routes and one page
45
48
  of trace metadata.
46
- - `setup` (checkout only) guides browser sign in and workspace choice, asks for
49
+ - `setup` guides browser sign in and workspace choice, asks for
47
50
  ingest-only approval, writes a private project env file, confirms delivery, and
48
51
  installs the selected client skill.
49
- - `verify` (checkout only) polls for one exact processed trace in a bounded window.
52
+ - `verify` polls for one exact processed trace in a bounded window.
50
53
  It does not infer application provenance from a Metadata match.
51
54
 
52
55
  It does not read retained content, replay traces, call model providers or send
@@ -101,7 +104,16 @@ sends a test event.
101
104
  `--open` launches only a server-provided link carrying the exact trace and the
102
105
  verified workspace ID. The dashboard must check that ID against its signed-in
103
106
  workspace before displaying traces. Older links without a workspace remain a
104
- manual handoff; a conflicting workspace or unsafe link is refused.
107
+ manual handoff; a conflicting workspace or unsafe link is refused. The result's
108
+ `browser` field is `launcher_started`, `launcher_unavailable`, `not_requested`
109
+ (no `--open`), or `suppressed_by_json` / `suppressed_by_no_browser` when `--open`
110
+ was given but that flag returned the link instead of launching it.
111
+
112
+ If `--timeout-ms` expires after the service has answered at least once with a
113
+ pending result, `verify` exits 11 `verification_failed` with reason
114
+ `verification_timeout`: the origin is reachable and the trace is not visible yet.
115
+ Exit 4 `connection_failed` with that reason means no valid answer arrived before
116
+ the deadline.
105
117
 
106
118
  ### doctor
107
119
 
@@ -205,7 +217,7 @@ does not match. It never downloads the skill or runs a remote script. A new skil
205
217
  revision ships only in a new CLI release; `skill update` then upgrades projects that
206
218
  hold an unchanged earlier revision.
207
219
 
208
- ### login and logout (checkout only, unreleased)
220
+ ### login and logout
209
221
 
210
222
  `login` binds a project directory to one Metergraph workspace. Your browser does the
211
223
  sign in, sign up, invitation and workspace consent on the service's own pages and
@@ -226,7 +238,7 @@ metergraph logout
226
238
  | `--config-dir DIR` | see below | Private per-user directory for the saved grant. |
227
239
  | `--timeout-ms N` | `300000` | How long to wait for the browser, 1000 to 900000. |
228
240
  | `--signup` | off | Start at the hosted sign up page, which returns to the same authorization request. Managed service only; other profiles exit 6. |
229
- | `--no-browser` | off | Print the authorization URL on stderr for you to open on this machine, then wait. Not with `--json`. |
241
+ | `--no-browser` | off | Print the authorization URL on stderr for you to open on this machine, then wait. With `--json`, a rerun that needs no approval succeeds; one that needs approval exits 6 `no_browser_requires_terminal`. |
230
242
  | `--reconnect` | off | Allow replacing a binding to a different origin or workspace. |
231
243
  | `--json` | off | Print exactly one JSON line on stdout and nothing on stderr. |
232
244
 
@@ -296,7 +308,7 @@ revocation request. If it does not answer `200`, local sign out still happens an
296
308
  command exits 13 with `revocation: "unconfirmed"`. A project that is not signed in
297
309
  exits 0 without any request.
298
310
 
299
- ### setup (checkout only, unreleased)
311
+ ### setup
300
312
 
301
313
  Run setup once from a project directory, choosing the coding client that will use
302
314
  the skill:
@@ -343,6 +355,11 @@ page shows the workspace and the consequence of approval. A signed-in browser
343
355
  on another workspace must switch in Metergraph and rerun; the CLI does not
344
356
  switch it automatically.
345
357
 
358
+ `METERGRAPH_INGEST_URL` is the deployment's service root. The Python SDK
359
+ appends `/v1/ingest` itself. A setup rerun with a verified
360
+ key repairs the endpoint value written by the first preview without minting a
361
+ new key.
362
+
346
363
  The env file must be a project-relative `.env`, `.env.<name>` or `<name>.env`
347
364
  (`--env-file` selects another). The writer refuses tracked files, links,
348
365
  ambiguous dotenv syntax and unsafe paths. It adds a project `.gitignore` rule
@@ -367,11 +384,11 @@ conflict leaves the delivered key in place and reports `credential_ready_skill_p
367
384
  resolve the skill file conflict and rerun setup without another approval.
368
385
  Success means the key was delivered and the project is ready to instrument.
369
386
  It does **not** mean application traffic has arrived. Run your application and
370
- verify one exact trace afterward. This checkout and the matching server slice
371
- are development work; neither their availability on a deployed service nor a
372
- published package has been established by these local tests.
387
+ verify one exact trace afterward. The hosted service advertises the setup
388
+ contract, but each non-hosted deployment must be checked at its own origin.
389
+ Local protocol tests do not prove browser approval or real application traffic.
373
390
 
374
- ### Read commands (checkout only, unreleased)
391
+ ### Read commands
375
392
 
376
393
  The read commands use the grant `login` saved for this project. They never open a
377
394
  browser, never sign in on their own and never request another scope. Each one:
@@ -512,8 +529,8 @@ before the failure, and `notices` lists fixed tokens such as `rows_truncated`,
512
529
 
513
530
  ## Exit codes
514
531
 
515
- Exit codes are stable. Changing one is a breaking change. Codes 10 to 17 exist only in
516
- this checkout.
532
+ Exit codes are stable. Changing one is a breaking change. Codes 10 to 17 were
533
+ added in `0.2.0-preview.0` and are unavailable in `0.1.0`.
517
534
 
518
535
  | Code | Outcome | Meaning |
519
536
  | --- | --- | --- |
@@ -618,7 +635,7 @@ A successful `skill install`:
618
635
  `receipt_invalid`, `locked`, `invalid_project`, `client_not_supported`,
619
636
  `write_failed` or `bundled_skill_invalid`.
620
637
 
621
- A successful `login` (checkout only):
638
+ A successful `login`:
622
639
 
623
640
  ```json
624
641
  {
@@ -759,7 +776,7 @@ To try a packed artifact without publishing:
759
776
 
760
777
  ```sh
761
778
  npm pack --pack-destination "$(mktemp -d)"
762
- npx --yes --package=/path/to/metergraph-cli-0.2.0-preview.0.tgz -- metergraph --version
779
+ npx --yes --package=/path/to/metergraph-cli-0.2.0-preview.2.tgz -- metergraph --version
763
780
  ```
764
781
 
765
782
  Do not commit tarballs or other generated files.
@@ -768,10 +785,9 @@ Do not commit tarballs or other generated files.
768
785
 
769
786
  The source of truth is the public repository
770
787
  [github.com/metergraph/cli](https://github.com/metergraph/cli), licensed Apache-2.0.
771
- The first preview uses the `next` npm tag. `0.1.0` is the only published version.
772
- This checkout's `0.2.0-preview.0` is not published and must not be published until
773
- the service side of sign in and the Metadata read endpoints are released. Subsequent releases must pass the checks
774
- below before publication.
788
+ `0.2.0-preview.2` is published on the `next` npm tag; `latest` remains on
789
+ `0.1.0`. The hosted service supports sign in, Metadata reads and ingest bootstrap.
790
+ Subsequent releases must pass the checks below before publication.
775
791
 
776
792
  Releases are manual. The `Release CLI` workflow (`.github/workflows/release.yml`) runs
777
793
  only when a maintainer starts it from `main`. It does not run on tags, pushes or a
@@ -803,12 +819,12 @@ commit:
803
819
  - If `main` moves after you copy the SHA, the run fails. Start a new run with the new
804
820
  head. To release an older state, land it on `main` first.
805
821
 
806
- ### First package bootstrap
822
+ ### First package bootstrap (completed for 0.1.0)
807
823
 
808
824
  npm trusted publishing is configured on a package that already exists, so the very
809
- first version cannot come from this workflow. Creating the package is a one time,
810
- human step that a Metergraph maintainer must approve and perform. Nothing in this
811
- repository automates it, and no npm token or secret is stored here.
825
+ first version could not come from this workflow. The steps below describe the
826
+ completed bootstrap of `0.1.0`; they are not part of subsequent releases. No npm
827
+ token or secret is stored here.
812
828
 
813
829
  1. Confirm the intended npm maintainer accounts and that `metergraph-cli` is
814
830
  available. The first approved publish establishes package ownership.
@@ -837,15 +853,13 @@ Trusted publishing requires npm 11.5.1 or newer. The publish job checks this bef
837
853
  publishes. After the trusted publisher works, consider restricting the package to
838
854
  trusted publishing so that long lived tokens cannot publish it.
839
855
 
840
- ### Remaining maintainer setup
856
+ ### Release configuration
841
857
 
842
- The source repository and license are settled. Before any automated release, a
843
- maintainer still has to:
858
+ Before an automated publish, confirm the following settings are still in place:
844
859
 
845
- - complete the [first package bootstrap](#first-package-bootstrap);
846
- - configure the [trusted publisher](#trusted-publishing);
847
- - create the `npm-release` environment with required reviewers;
848
- - set the repository variable `METERGRAPH_CLI_PUBLISH_ENABLED` to `true`.
860
+ - the [trusted publisher](#trusted-publishing) matches this repository and workflow;
861
+ - the `npm-release` environment has required reviewers;
862
+ - the repository variable `METERGRAPH_CLI_PUBLISH_ENABLED` is `true`.
849
863
 
850
864
  Until all of these are done, leave `publish` false.
851
865
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metergraph-cli",
3
- "version": "0.2.0-preview.0",
3
+ "version": "0.2.0-preview.2",
4
4
  "description": "Preview Metergraph command line tool with a read-only connection probe, a project skill installer, Metadata-only sign in and bounded workspace Metadata reads",
5
5
  "type": "module",
6
6
  "bin": {
package/src/auth-login.js CHANGED
@@ -58,12 +58,13 @@ export async function runLogin(options, progress) {
58
58
  if (options.runtime !== "local") return end("unsupported", "runtime_not_supported", GUIDE);
59
59
  const remote = detectRemoteSession();
60
60
  if (remote !== null) return end("unsupported", remote, GUIDE);
61
- if (options.json && options.noBrowser) {
62
- return end("unsupported", "no_browser_requires_terminal", {
63
- kind: "run_in_terminal",
64
- message: "Run the same login command in a terminal without --json to see the sign in URL.",
65
- });
66
- }
61
+ // --json cannot show a person the --no-browser URL. A rerun with a saved,
62
+ // working grant needs no URL, so only refuse once approval is required.
63
+ const noBrowserHandoff = () => end("unsupported", "no_browser_requires_terminal", {
64
+ kind: "run_in_terminal",
65
+ message: "Run the same login command in a terminal without --json to see the sign in URL.",
66
+ });
67
+ const urlUnshowable = options.json && options.noBrowser;
67
68
 
68
69
  const trap = trapSignals();
69
70
  try {
@@ -71,6 +72,7 @@ export async function runLogin(options, progress) {
71
72
  const configDir = resolveConfigDir(options.configDir);
72
73
  const existing = readBinding(root);
73
74
  const bound = existing?.binding ?? null;
75
+ if (urlUnshowable && (bound === null || options.reconnect)) return noBrowserHandoff();
74
76
  // Setup pins a deployment model before sign in. A stale binding for a
75
77
  // different profile must never be silently reused or reconnected.
76
78
  if (options.expectedProfile && bound !== null && !options.reconnect &&
@@ -100,6 +102,7 @@ export async function runLogin(options, progress) {
100
102
  }
101
103
  if (session.outcome !== "login_required") return end(session.outcome, session.reason);
102
104
  }
105
+ if (urlUnshowable) return noBrowserHandoff();
103
106
 
104
107
  const flow = await authorize(options, ctx, trap.signal, progress);
105
108
  if (!flow.ok) return end(flow.outcome, flow.reason, flow.next ?? null);
package/src/output.js CHANGED
@@ -599,8 +599,8 @@ const AUTH_MESSAGES = {
599
599
  ci_environment:
600
600
  "This is a CI environment, where no person can approve sign in in a browser. Nothing was written.",
601
601
  no_browser_requires_terminal:
602
- "--no-browser prints the sign in URL for a person to open, which --json cannot do. " +
603
- "Run login without --json in a terminal. Nothing was done.",
602
+ "Approval is needed, and --no-browser prints its URL for a person to open, which --json cannot do. " +
603
+ "Run the same command without --json in a terminal. Nothing was done.",
604
604
  bound_to_other_origin:
605
605
  "This project is bound to a different origin. Nothing was changed. Use --reconnect to switch it.",
606
606
  bound_to_other_workspace:
@@ -1,5 +1,6 @@
1
1
  import { normalizeUuid } from "./auth-oauth.js";
2
2
  import { AGENT_CONTRACT_VERSION, MAX_CURSOR_LENGTH, METADATA_SCOPE } from "./constants.js";
3
+ import { traceLink } from "./trace-contract.js";
3
4
 
4
5
  // Validators for the service's Metadata read documents. Each one checks the
5
6
  // document against the agent access contract, the bound workspace and the
@@ -530,6 +531,8 @@ export function tracesReport(body, ctx, request, notices) {
530
531
  ) {
531
532
  throw new Invalid("verification_failed", reason);
532
533
  }
534
+ const link = traceLink(row.metergraph_links?.trace ?? null, ctx.origin, row, ctx.workspaceId);
535
+ if (!link.ok) throw new Invalid(link.outcome, link.reason);
533
536
  return {
534
537
  id: row.id,
535
538
  trace_id: row.trace_id,
@@ -546,12 +549,16 @@ export function tracesReport(body, ctx, request, notices) {
546
549
  routes: nameList(row.routes, notices, reason),
547
550
  providers: nameList(row.providers, notices, reason),
548
551
  models: nameList(row.models, notices, reason),
549
- link: null,
552
+ link: link.value,
553
+ link_workspace_bound: link.workspaceBound === true,
550
554
  };
551
555
  });
552
556
  if (!proof.complete) notices.add("evidence_incomplete");
553
557
  if (page.truncated) notices.add("more_pages");
554
- notices.add("trace_links_unavailable");
558
+ const missingLinks = traces.length === 0 || traces.some((trace) => trace.link === null);
559
+ if (missingLinks) notices.add("trace_links_unavailable");
560
+ const unboundLinks = traces.some((trace) => trace.link !== null && !trace.link_workspace_bound);
561
+ if (unboundLinks) notices.add("trace_workspace_binding_unavailable");
555
562
  return {
556
563
  provenance: origin,
557
564
  window: span,
@@ -570,7 +577,8 @@ export function tracesReport(body, ctx, request, notices) {
570
577
  next_cursor: page.next_cursor,
571
578
  complete: proof.complete && !page.truncated,
572
579
  empty: traces.length === 0,
573
- link_status: "server_link_unavailable",
580
+ link_status: missingLinks ? "server_link_unavailable" :
581
+ unboundLinks ? "workspace_binding_unavailable" : "available",
574
582
  traces,
575
583
  };
576
584
  });
@@ -13,14 +13,14 @@ export const ENV_NAMES = Object.freeze({
13
13
  });
14
14
  const NAMES = Object.values(ENV_NAMES);
15
15
 
16
- // Ingest paths the service accepts on an origin. A URL with any other path,
17
- // a query, a fragment or user information is refused.
18
- export const INGEST_PATHS = Object.freeze(["/v1/ingest"]);
16
+ // The SDK expects a service root and appends /v1/ingest itself. Accept the
17
+ // endpoint form only while reading setup files written by the first preview.
18
+ export const INGEST_PATHS = Object.freeze(["", "/v1/ingest"]);
19
19
 
20
20
  // Server-generated application tokens are RFC 3986 unreserved characters
21
21
  // only, so a value can never carry a quote, newline, comment or space.
22
22
  const TOKEN = /^[A-Za-z0-9._~-]{16,1024}$/;
23
- const URL_TEXT = /^(https?:\/\/[^/?#@\\]+)(\/[A-Za-z0-9/_.-]*)$/i;
23
+ const URL_TEXT = /^(https?:\/\/[^/?#@\\]+)(\/[A-Za-z0-9/_.-]*)?$/i;
24
24
  const SAFE_URL = /^[A-Za-z0-9.:/[\]_-]+$/;
25
25
 
26
26
  const ASSIGNMENT = /^([ \t]*)(export[ \t]+)?([A-Za-z_][A-Za-z0-9_.-]*)([ \t]*=[ \t]*)(.*)$/;
@@ -35,15 +35,15 @@ export function isAppToken(value) {
35
35
  return typeof value === "string" && TOKEN.test(value);
36
36
  }
37
37
 
38
- // Returns the normalized ingest URL (origin plus a supported path) or null.
38
+ // Returns the normalized service root or legacy ingest endpoint, or null.
39
39
  // The caller must not echo the raw input when this returns null.
40
40
  export function parseIngestUrl(raw) {
41
41
  if (typeof raw !== "string" || raw.length > 2048) return null;
42
42
  const match = URL_TEXT.exec(raw);
43
- if (match === null || !INGEST_PATHS.includes(match[2])) return null;
43
+ if (match === null || !INGEST_PATHS.includes(match[2] ?? "")) return null;
44
44
  const origin = parseOrigin(match[1]);
45
45
  if (origin === null) return null;
46
- const url = `${origin}${match[2]}`;
46
+ const url = `${origin}${match[2] ?? ""}`;
47
47
  return SAFE_URL.test(url) ? url : null;
48
48
  }
49
49
 
package/src/setup.js CHANGED
@@ -31,7 +31,6 @@ const fail = (outcome, reason, status = "not_ready", receipt = null) => ({ outco
31
31
  // response can supply it. Every returned string is fixed and non-secret.
32
32
  export async function runSetup(options, progress = () => {}) {
33
33
  if (options.runtime !== "local" || detectRemoteSession() !== null) return fail("unsupported", "run_on_local_machine");
34
- if (options.json && options.noBrowser) return fail("unsupported", "no_browser_requires_terminal");
35
34
  const trap = trapSignals();
36
35
  let receipt = null;
37
36
  let loginVerified = false;
@@ -111,7 +110,9 @@ export async function runSetup(options, progress = () => {}) {
111
110
  if (previous && existing.token !== null) {
112
111
  const checked = await checkCredential(origin, existing.token, previous.value, profile, trap.signal);
113
112
  if (checked.ok) {
114
- if (existing.ingestUrl !== `${origin}/v1/ingest`) return stop("conflict", "ingest_url_mismatch");
113
+ if (existing.ingestUrl !== origin && existing.ingestUrl !== `${origin}/v1/ingest`) {
114
+ return stop("conflict", "ingest_url_mismatch");
115
+ }
115
116
  if (previous.value.key_id !== null && previous.value.key_id !== checked.keyId) {
116
117
  return stop("conflict", "setup_key_changed");
117
118
  }
@@ -121,6 +122,13 @@ export async function runSetup(options, progress = () => {}) {
121
122
  if (previous.value.phase !== "delivered" || previous.value.key_id === null) {
122
123
  writeSetupState(root, withSetupState(previous.value, { key_id: checked.keyId, phase: "delivered" }), previous);
123
124
  }
125
+ // The first preview wrote the endpoint, which the SDK appends again.
126
+ // Repair only after verifying this family's saved key, without issuing
127
+ // another key or changing the workspace binding.
128
+ if (existing.ingestUrl !== origin) {
129
+ const repaired = await commitEnv(plan, { ingestUrl: origin, signal: trap.signal });
130
+ return finishSkill(root, options, repaired.receipt.file);
131
+ }
124
132
  return finishSkill(root, options, "unchanged");
125
133
  }
126
134
  if (!options.repair && previous.value.phase !== "redeem_attempted") return stop("verification_failed", "saved_key_unverified");
@@ -129,6 +137,9 @@ export async function runSetup(options, progress = () => {}) {
129
137
  return stop("conflict", "repair_requires_saved_key");
130
138
  }
131
139
 
140
+ // A verified rerun returned above without approval. From here a person
141
+ // must open an approval URL, which --json cannot show with --no-browser.
142
+ if (options.json && options.noBrowser) return stop("unsupported", "no_browser_requires_terminal");
132
143
  const setup = await discoverSetup(origin, profile, trap.signal);
133
144
  if (!setup.ok) return stop(setup.outcome, setup.reason);
134
145
 
@@ -188,7 +199,7 @@ export async function runSetup(options, progress = () => {}) {
188
199
  redirect_uri: listener.redirectUri, code_verifier: pkce.verifier,
189
200
  family_id: state.family_id, workspace_id: workspaceId }, profile, trap.signal);
190
201
  if (!redeemed.ok) return stop(redeemed.outcome, redeemed.reason, "delivery_pending");
191
- const written = await commitEnv(plan, { token: redeemed.token, ingestUrl: `${origin}/v1/ingest`, signal: trap.signal });
202
+ const written = await commitEnv(plan, { token: redeemed.token, ingestUrl: origin, signal: trap.signal });
192
203
  const checked = await checkCredential(origin, redeemed.token, state, profile, trap.signal);
193
204
  if (!checked.ok || checked.keyId !== redeemed.keyId) return stop("verification_failed", "issued_key_unverified", "delivery_pending");
194
205
  const ack = await acknowledge(origin, redeemed.token, state, profile, redeemed.keyId, trap.signal);
package/src/trace-open.js CHANGED
@@ -4,7 +4,8 @@ import { normalizeUuid } from "./auth-oauth.js";
4
4
  import { parseOrigin } from "./origin.js";
5
5
 
6
6
  // Operates only on an already verified, provenance-bound receipt. JSON and
7
- // no-browser mode return its URL without starting a launcher. "opened" means
7
+ // no-browser mode return its URL without starting a launcher, and say which
8
+ // one suppressed an --open request. "opened" means
8
9
  // the OS launcher started; it does not assert a browser rendered the page.
9
10
  export async function openTrace(receipt, options = {}, launch = openBrowser) {
10
11
  if (receipt?.readiness?.metadata_available !== true || receipt?.readiness?.processed !== true ||
@@ -20,7 +21,11 @@ export async function openTrace(receipt, options = {}, launch = openBrowser) {
20
21
  if (link.workspaceBound !== true || receipt.link_workspace_bound !== true || receipt.link_status !== "available") {
21
22
  return { outcome: "unsupported", reason: "link_workspace_binding_unavailable", data: { ...receipt, browser: "not_requested" } };
22
23
  }
23
- if (options.json || options.noBrowser || !options.open) return { outcome: "ok", reason: null, data: { ...receipt, browser: "not_requested" } };
24
+ if (!options.open) return { outcome: "ok", reason: null, data: { ...receipt, browser: "not_requested" } };
25
+ if (options.json || options.noBrowser) {
26
+ const browser = options.json ? "suppressed_by_json" : "suppressed_by_no_browser";
27
+ return { outcome: "ok", reason: null, data: { ...receipt, browser } };
28
+ }
24
29
  if (options.signal?.aborted) return { outcome: "cancelled", reason: "cancelled", data: receipt };
25
30
  const launched = await launchWithinSignal(link.value, launch, options.signal);
26
31
  if (options.signal?.aborted) return { outcome: "cancelled", reason: "cancelled", data: receipt };
package/src/verify.js CHANGED
@@ -33,7 +33,10 @@ export async function runVerify(options, dependencies = {}) {
33
33
  if (holdsKnown(data, known)) return { outcome: "verification_failed", reason: "credential_in_metadata_response", data: null };
34
34
  return { outcome, reason, data: data === null ? null : { ...data, attempts } };
35
35
  };
36
- const interrupted = () => limit.timedOut() ? end("connection_failed", "verification_timeout") : end("cancelled", "cancelled");
36
+ // Once the origin has answered with a valid pending receipt, a deadline
37
+ // means the trace is not visible yet, not that the origin is unreachable.
38
+ const interrupted = () => !limit.timedOut() ? end("cancelled", "cancelled")
39
+ : end(receipt === null ? "connection_failed" : "verification_failed", "verification_timeout");
37
40
  try {
38
41
  const sessionResult = await (dependencies.session ?? verifiedSession)({ project: options.project, configDir: options.configDir, cancel: limit.signal });
39
42
  if (limit.signal.aborted) return interrupted();