metergraph-cli 0.2.0-preview.5 → 0.2.0-preview.6

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
@@ -52,7 +52,7 @@ npx --yes metergraph-cli@next traces --limit 20 # one page of trace metadata
52
52
  ```
53
53
 
54
54
  To install it globally instead, run `npm install -g metergraph-cli@next`. The command is
55
- `metergraph`. Pin an exact version, such as `metergraph-cli@0.2.0-preview.5`, when a
55
+ `metergraph`. Pin an exact version, such as `metergraph-cli@0.2.0-preview.6`, when a
56
56
  script must not change with the `next` tag.
57
57
 
58
58
  ## Requirements
@@ -80,11 +80,11 @@ The CLI is built to be run by Codex, Claude Code and Cursor as well as by people
80
80
  network access:
81
81
 
82
82
  ```sh
83
- npx --yes metergraph-cli@next skill install --client codex --runtime local
83
+ npx --yes metergraph-cli@next skill install --client codex
84
84
  ```
85
85
 
86
86
  Python users can run `pip install metergraph-skills` and
87
- `metergraph-skills install --client codex --runtime local` instead
87
+ `metergraph-skills install --client codex` instead
88
88
  ([details](https://github.com/metergraph/cli/blob/main/python/metergraph-skills/README.md)).
89
89
  Both write the same files and recognise each other's installs.
90
90
  - **Model-swap skills.** `skills install` adds the workflow skills from
@@ -94,17 +94,26 @@ The CLI is built to be run by Codex, Claude Code and Cursor as well as by people
94
94
  the `metergraph/skills` plugin marketplace instead.
95
95
 
96
96
  ```sh
97
- npx --yes metergraph-cli@next skills install --client claude --runtime local
97
+ npx --yes metergraph-cli@next skills install --client claude
98
98
  ```
99
99
 
100
+ `--runtime` defaults to `local`. Pass `--runtime cloud` when the client runs in a
101
+ cloud environment with a checkout of the project.
102
+
100
103
  - **Machine output.** Every command takes `--json` and then prints exactly one JSON
101
104
  line on stdout and nothing on stderr. All results share one envelope:
102
105
  `schema_version`, `command`, `ok`, `outcome`, `exit_code`, `data` and `error`.
103
106
  Failures carry a fixed `error.reason` and, where useful, a `data.next_action` that
104
107
  tells the agent what to do next.
105
108
  - **No hidden prompts.** No command reads stdin or asks a question. `--no-browser`
106
- prints the sign in URL for a person to open instead of launching a browser; with
107
- `--json`, a step that needs approval exits 6 with a `run_in_terminal` next action.
109
+ prints the sign in URL for a person to open instead of launching a browser. With
110
+ `--json`, a step that needs approval exits within seconds with 18
111
+ `action_required` and `data.next_action` set to
112
+ `{"kind": "open_url", "url": "...", "timeout_seconds": N, "message": "..."}`, so an
113
+ agent can show the URL to the person. A background process waits for the approval
114
+ until `--timeout-ms`; afterwards, run the same command again to continue from the
115
+ saved state
116
+ ([details](https://github.com/metergraph/cli/blob/main/docs/reference.md#approval-from-an-agent)).
108
117
  - **MCP.** Workspace tools for agents are served by the hosted MCP endpoint, not by this
109
118
  CLI. See the [MCP server guide](https://www.metergraph.dev/docs/guides/mcp-server/)
110
119
  and the [agent access guide](https://www.metergraph.dev/docs/guides/agent-access/).
@@ -141,7 +150,7 @@ writes, and full JSON examples.
141
150
 
142
151
  ## Exit codes
143
152
 
144
- Exit codes are stable. Changing one is a breaking change. Codes 10 to 17 are not in
153
+ Exit codes are stable. Changing one is a breaking change. Codes 10 to 18 are not in
145
154
  `0.1.0`.
146
155
 
147
156
  | Code | Outcome | Meaning |
@@ -152,7 +161,7 @@ Exit codes are stable. Changing one is a breaking change. Codes 10 to 17 are not
152
161
  | 3 | `authentication_required` | Service is reachable, healthy and supported, and requires authentication. No workspace is connected. |
153
162
  | 4 | `connection_failed` | The origin could not be reached, the connection failed, or the probe timed out. For `verify`, the deadline expired before any valid answer arrived. A trace that is still pending is exit 11, not 4. |
154
163
  | 5 | `unhealthy` | The service answered but reported that it is not healthy, or answered with a server error. |
155
- | 6 | `unsupported` | The service answered with a response, deployment profile or status this CLI does not support, or the skill client or runtime cannot use project skill files, or sign in cannot run in this environment, or `--json --no-browser` needs a browser approval it cannot show (`no_browser_requires_terminal`). Nothing was written. |
164
+ | 6 | `unsupported` | The service answered with a response, deployment profile or status this CLI does not support, or the skill client or runtime cannot use project skill files, or sign in cannot run in this environment. Nothing was written. |
156
165
  | 7 | `redirect_rejected` | The service answered with a redirect. Redirects are never followed. |
157
166
  | 8 | `conflict` | The skill target is not owned by this CLI, was modified, is unsafe, is locked or needs an explicit update, or the project is bound to a different origin or workspace. Nothing was changed. |
158
167
  | 9 | `filesystem_error` | Project or credential files could not be read or written. Partial changes were rolled back unless the message says otherwise. |
@@ -164,6 +173,7 @@ Exit codes are stable. Changing one is a breaking change. Codes 10 to 17 are not
164
173
  | 15 | `permission_denied` | The service refused this read for the signed in grant, for example for a missing scope or permission. |
165
174
  | 16 | `rate_limited` | The service asked the CLI to slow down. Nothing was retried. Try again later. |
166
175
  | 17 | `cancelled` | A read command was interrupted before it finished. Read commands never change workspace configuration or telemetry. |
176
+ | 18 | `action_required` | A person must act before the command can finish; `data.next_action` says what to do. `login` and `setup` with `--json --no-browser` exit with this as soon as an approval URL exists (`next_action.kind: "open_url"`), while a background process waits for the approval until `--timeout-ms`. After approval, run the same command again to continue. |
167
177
 
168
178
  ## Security and privacy
169
179
 
@@ -194,9 +204,11 @@ report. See [SECURITY.md](https://github.com/metergraph/cli/blob/main/SECURITY.m
194
204
 
195
205
  `--url ORIGIN` selects the service; the default is `https://app.metergraph.dev`. It
196
206
  accepts a bare `https://` origin, or `http://` on `localhost`, `127.0.0.1` or `[::1]`.
197
- `setup --deployment` supports the hosted service (`managed`), the commercial
198
- `customer-local` bundle, customer-owned cloud (`byoc`) and the open source server
199
- (`oss`). See [self-hosting](https://www.metergraph.dev/docs/self-host/local/) and the
207
+ `setup --deployment` routes to the hosted service (`managed`), the commercial
208
+ `customer-local` bundle, customer-owned cloud (`byoc`) or the open source server
209
+ (`oss`). The hosted service and released customer-local bundle are the release
210
+ targets; `byoc` and `oss` are not yet verified against a live deployment. See
211
+ [self-hosting](https://www.metergraph.dev/docs/self-host/local/) and the
200
212
  [setup reference](https://github.com/metergraph/cli/blob/main/docs/reference.md#setup).
201
213
 
202
214
  ## Links
@@ -36,6 +36,15 @@ package does not support. A CLI login or health probe alone does not prove
36
36
  application traffic. Verification needs an exact trace or request ID from an
37
37
  application invocation, its time window and the intended workspace.
38
38
 
39
+ `login` and `setup` need the person to approve in a browser on this machine. If the
40
+ installed CLI's help lists exit code 18 `action_required`, run them with
41
+ `--json --no-browser`. When a step needs approval, the command exits 18 within
42
+ seconds. Show `data.next_action.url` to the person and ask them to approve; a
43
+ background process waits for the approval. After they approve, run the same command
44
+ again. A new project takes three runs: sign in, ingest approval, then a final check.
45
+ An older CLI exits 6 `no_browser_requires_terminal` instead. Then the person has to
46
+ run setup in their own terminal.
47
+
39
48
  Repository identity is optional for capture but keeps traces attributable when
40
49
  several repositories share a workspace. If the installed CLI's `setup` help lists
41
50
  `--repository`, setup records `owner/name` from the git remote in a committed
@@ -68,7 +77,7 @@ For hosted Claude Desktop on an individual account, guide the human through Cust
68
77
 
69
78
  For a fresh hosted workspace, first ask whether the human already has a Metergraph account and workspace. Direct them to https://app.metergraph.dev/ and the signup, invitation or sign-in flow actually offered. The hosted UI can offer "Get a free API key" for self-service signup, but do not assume that button or a workspace invitation is available to everyone. If access is unavailable, direct them to their workspace owner or Metergraph. Confirm the workspace before creating a coding-agent key.
70
79
 
71
- For the commercial customer-local bundle, direct them to https://www.metergraph.dev/docs/self-host/local/#the-customer-local-bundle and the signed public release. They need a separate pull-only registry credential from Metergraph to pull the images; the public bundle download does not grant registry access. Verify the release before installation. Local admin credentials from the bundle's private configuration provide dashboard sign-in; create a coding-agent key in that installation's Keys page. Do not substitute hosted signup or a hosted key. Do not clone the private implementation or run development seed commands.
80
+ For the commercial customer-local bundle, direct them to https://www.metergraph.dev/docs/self-host/local/#the-customer-local-bundle and the signed public release. They need a separate pull-only registry credential from Metergraph to pull the images; the public bundle download does not grant registry access. Verify the release before installation. Local admin credentials from the bundle's private configuration provide dashboard sign-in; create a coding-agent key in that installation's Keys page. Do not substitute hosted signup or a hosted key. Do not clone the private implementation or run development seed commands. Use `metergraph-local-bundle` to stand the bundle up when that skill is installed.
72
81
 
73
82
  For customer-owned AWS, direct them to https://www.metergraph.dev/docs/self-host/aws/. An operator provisions the installation, identity and membership. There is no public signup. Ask for the installation's actual reachable address and use a coding-agent key from its Keys page. Its default internal load balancer is private, and its Agent Access surface excludes trace content and replay.
74
83
 
@@ -3,7 +3,7 @@
3
3
  "name": "metergraph",
4
4
  "file": "SKILL.md",
5
5
  "source_url": "https://www.metergraph.dev/SKILL.md",
6
- "sha256": "92f7955f6f8761a20a1886ad81f22dfcee239afddf8e9f1e761c35eb8e70f218",
7
- "size": 12980,
8
- "revision": "sha256-92f7955f6f87"
6
+ "sha256": "e7fc4eb7a22eeb16c357bede9d0a080ad3b415aaab1d9c72f164d5350d48c4b1",
7
+ "size": 13664,
8
+ "revision": "sha256-e7fc4eb7a22e"
9
9
  }
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 1,
3
3
  "repository": "metergraph/skills",
4
- "commit": "25a62e420589cb805e3dd7d9bb3e3a35d85e2ece",
4
+ "commit": "dda4d5f130448f12a7fb2549ed264503d8b51d8d",
5
5
  "skills": [
6
6
  {
7
7
  "name": "metergraph-analyze",
@@ -33,6 +33,12 @@
33
33
  "size": 3789,
34
34
  "revision": "sha256-9ce5a2cf527e"
35
35
  },
36
+ {
37
+ "name": "metergraph-local-bundle",
38
+ "sha256": "692cd8952a8a8896ec2c06091b329a7a01912e261e87d62a9458452172a7c9b0",
39
+ "size": 12245,
40
+ "revision": "sha256-692cd8952a8a"
41
+ },
36
42
  {
37
43
  "name": "metergraph-model-swap",
38
44
  "sha256": "d77ee37f882ab1e50dc4a093e64a2c3661f4cd9bb59f184da6dccbd76fd51a18",
@@ -41,9 +47,9 @@
41
47
  },
42
48
  {
43
49
  "name": "metergraph-onboarding",
44
- "sha256": "15be9083f5b0d18cc6fe0d225f0921e2f16c5b4fb3d811c3984cef5cbc7941ba",
45
- "size": 10208,
46
- "revision": "sha256-15be9083f5b0"
50
+ "sha256": "6e6f418933f37b34437035ab3171344d43f37dee3770815b19f97efb62e5f1d1",
51
+ "size": 10267,
52
+ "revision": "sha256-6e6f418933f3"
47
53
  },
48
54
  {
49
55
  "name": "metergraph-report",
@@ -0,0 +1,283 @@
1
+ ---
2
+ name: metergraph-local-bundle
3
+ description: Verify and start the signed Metergraph customer-local bundle on a person's own machine, preserve existing local data and secrets, then hand over to metergraph setup. Use when customer-local setup reports that the bundle_started_verified prerequisite is incomplete or when installing the commercial local bundle from a public release.
4
+ ---
5
+
6
+ # Start the signed customer-local bundle
7
+
8
+ Stand up the released bundle only on the person's own machine, then run
9
+ `metergraph setup`. The CLI does not provision it, and
10
+ `--confirm-prerequisites` records confirmation rather than installing anything.
11
+
12
+ Keep registry, admin and database secrets out of chat, command arguments and
13
+ output. Never read or print `.env` values, `~/.docker/config.json`,
14
+ `docker compose config` output or container environment from `docker inspect`.
15
+ Do not configure model-provider keys.
16
+
17
+ ## Stop in remote runtimes
18
+
19
+ Before changing anything, determine where the agent executes. Check only
20
+ whether these variables exist, never their values:
21
+
22
+ ```sh
23
+ for name in SSH_CONNECTION SSH_CLIENT SSH_TTY CODESPACES GITPOD_WORKSPACE_ID CLOUD_SHELL CI; do
24
+ if printenv "$name" >/dev/null 2>&1; then printf '%s is set\n' "$name"; fi
25
+ done
26
+ ```
27
+
28
+ If any is set, or this is otherwise a hosted/cloud runtime, download, verify,
29
+ extract and start nothing. Prefer an installed `metergraph` whose `--help`
30
+ lists `setup`; otherwise state that the fallback fetches the preview CLI and
31
+ use `npx --yes metergraph-cli@next` as the prefix. Run only:
32
+
33
+ ```sh
34
+ metergraph setup --runtime cloud --deployment customer-local --url http://localhost:8080 --workspace 11111111-1111-4111-8111-111111111111 --skip-skill --json
35
+ ```
36
+
37
+ Relay its `data.next_action.kind: "run_on_customer_machine"` handoff and tell
38
+ the person to run this skill on their own machine. The placeholder workspace is
39
+ intentional: the runtime check refuses before making a request or writing a
40
+ file. When an SSH, cloud or CI marker is set, even `--runtime local` is refused
41
+ with reason `run_on_local_machine` and the same handoff. Valid runtimes are
42
+ `local`, `cloud` and `cloud-no-shell`. Never expose localhost to the remote
43
+ session.
44
+
45
+ ## Preflight and reuse
46
+
47
+ Ask for the coding client and bundle version. Default to the newest version at
48
+ https://www.metergraph.dev/docs/releases/ and record its `v`-prefixed value.
49
+ Record the application repository directory to return to for setup. Run:
50
+
51
+ ```sh
52
+ docker info >/dev/null
53
+ docker compose version
54
+ cosign version
55
+ curl -fsS http://localhost:8080/v1/deployment
56
+ lsof -nP -iTCP:8080 -sTCP:LISTEN
57
+ ```
58
+
59
+ Docker with Compose must be running and `cosign` installed. If the deployment
60
+ endpoint returns the `local` profile, reuse that stack and download nothing.
61
+ Find its bundle directory using only the Compose working-directory label:
62
+
63
+ ```sh
64
+ container_id="$(docker ps --filter publish=8080 --format '{{.ID}}' | head -n 1)"
65
+ bundle_dir="$(docker inspect --format '{{ index .Config.Labels "com.docker.compose.project.working_dir" }}' "$container_id")"
66
+ (cd "$bundle_dir" && ./bin/status)
67
+ ```
68
+
69
+ Do not inspect container environment. Read the workspace ID from the
70
+ `workspace` check and skip to **Run setup**. If the directory cannot be found,
71
+ ask for the workspace ID from Settings. If nothing listens on 8080, continue.
72
+ If anything else owns it, stop and ask; never stop or reconfigure it.
73
+
74
+ Use `~/metergraph/<version>` and set
75
+ `bundle_dir="$HOME/metergraph/$version/local"`. If it contains `.env`, reuse
76
+ that existing installation: never overwrite, delete or read the file, and
77
+ never extract over `local/`. Continue at **Log in and start**. Otherwise check:
78
+
79
+ ```sh
80
+ docker volume inspect local_pgdata >/dev/null 2>&1
81
+ ```
82
+
83
+ Because the extracted directory and Compose project are `local`, an existing
84
+ volume is `local_pgdata`. If it exists without the matching `local/.env`, stop
85
+ and ask for the prior bundle/configuration. Postgres fixed its password when
86
+ the volume was created; a new `.env` can cause `password authentication failed`.
87
+ Never remove the volume, delete `.env` or use `down --volumes`.
88
+
89
+ ## Download and verify
90
+
91
+ Require a fresh, empty version directory. Before creating it, tell the person
92
+ the directory and four public files that will be written. No account is needed.
93
+ Set `version` to the recorded version and run:
94
+
95
+ ```sh
96
+ release_dir="$HOME/metergraph/$version"; mkdir -p "$release_dir"; cd "$release_dir"
97
+ archive="metergraph-customer-local-$version.tar.gz"
98
+ base="https://www.metergraph.dev/releases/$version"
99
+ curl -fsSLO "$base/$archive"
100
+ curl -fsSLO "$base/metergraph-byoc-release.json"
101
+ curl -fsSLO "$base/metergraph-byoc-release.json.sig"
102
+ curl -fsSLO "$base/metergraph-byoc-release.json.pem"
103
+ ```
104
+
105
+ Do not list, extract or run archive contents. Verify the manifest signature
106
+ with exactly this command. The issuer and identity are pinned here, not read
107
+ from the release page, so a changed download site cannot change who must have
108
+ signed the release:
109
+
110
+ ```sh
111
+ cosign verify-blob \
112
+ --certificate metergraph-byoc-release.json.pem \
113
+ --signature metergraph-byoc-release.json.sig \
114
+ --certificate-oidc-issuer https://token.actions.githubusercontent.com \
115
+ --certificate-identity-regexp '^https://github\.com/PioneerSquareLabs/metergraph-internal/\.github/workflows/byoc-release\.yml@refs/(heads/main|tags/v.+)$' \
116
+ metergraph-byoc-release.json
117
+ ```
118
+
119
+ Never loosen or drop the issuer or identity, and never add
120
+ `--insecure-ignore-tlog` or a similar bypass. If the release page publishes a
121
+ different identity, stop and tell the person rather than using it.
122
+
123
+ If cosign fails, stop: "The manifest signature did not verify. Do not extract
124
+ or run this bundle." After it succeeds, check signed fields:
125
+
126
+ ```sh
127
+ python3 - "$version" "$archive" <<'EOF'
128
+ import json, sys
129
+ version, archive = sys.argv[1:]
130
+ m = json.load(open("metergraph-byoc-release.json"))
131
+ required = ("release", "local_bundle", "local_bundle_sha256",
132
+ "local_image_digest", "local_analysis_image_digest")
133
+ missing = [key for key in required if not m.get(key)]
134
+ if missing: sys.exit("Signed manifest is missing: " + ", ".join(missing))
135
+ if m["release"] != version: sys.exit("Signed manifest release mismatch. Do not run it.")
136
+ if m["local_bundle"] != archive: sys.exit("Signed archive name mismatch. Do not run it.")
137
+ print("Signed manifest release, archive, and image digests are present.")
138
+ EOF
139
+ ```
140
+
141
+ Then run this digest check, still before any `tar` command:
142
+
143
+ ```sh
144
+ python3 - <<'EOF'
145
+ import hashlib, json, sys
146
+ manifest = json.load(open("metergraph-byoc-release.json"))
147
+ name = manifest["local_bundle"]
148
+ actual = hashlib.sha256(open(name, "rb").read()).hexdigest()
149
+ if actual != manifest["local_bundle_sha256"]:
150
+ sys.exit(f"{name} does not match the signed manifest. Do not run it.")
151
+ print(f"{name} matches the signed manifest.")
152
+ EOF
153
+ ```
154
+
155
+ Stop on failure. After both checks pass, list and validate archive paths:
156
+
157
+ ```sh
158
+ tar -tzf "$archive"
159
+ python3 - "$archive" <<'EOF'
160
+ from pathlib import PurePosixPath
161
+ import sys, tarfile
162
+ bad = []
163
+ with tarfile.open(sys.argv[1], "r:gz") as bundle:
164
+ for member in bundle.getmembers():
165
+ path = PurePosixPath(member.name)
166
+ if path.is_absolute() or not path.parts or path.parts[0] != "local" or ".." in path.parts:
167
+ bad.append(member.name)
168
+ if bad: sys.exit("Unsafe archive paths; do not extract: " + ", ".join(bad[:10]))
169
+ print("Every archive entry is beneath local/ with a safe relative path.")
170
+ EOF
171
+ ```
172
+
173
+ Only now tell the person extraction will create `local/`, then run:
174
+
175
+ ```sh
176
+ tar xzf "$archive"
177
+ mv metergraph-byoc-release.json* local/
178
+ cd local
179
+ ```
180
+
181
+ ## Configure without exposing passwords
182
+
183
+ Reuse an existing `.env` unchanged. For a new installation, state that a
184
+ mode-0600 file with random admin/database passwords and empty provider keys
185
+ will be created. Confirm `local_pgdata` did not exist, then run:
186
+
187
+ ```sh
188
+ umask 077; cp .env.example .env; chmod 600 .env
189
+ python3 - <<'EOF'
190
+ from pathlib import Path
191
+ import secrets
192
+ path = Path(".env")
193
+ keys = ("METERGRAPH_LOCAL_ADMIN_PASSWORD", "METERGRAPH_DB_PASSWORD")
194
+ values = {key: secrets.token_urlsafe(24) for key in keys}
195
+ lines, found = path.read_text().splitlines(), set()
196
+ for i, line in enumerate(lines):
197
+ for key in keys:
198
+ if line.startswith(key + "="):
199
+ lines[i], found = key + "=" + values[key], found | {key}
200
+ if found != set(keys): raise SystemExit("Password keys missing from .env.example")
201
+ path.write_text("\n".join(lines) + "\n")
202
+ print("Set random values for: " + ", ".join(keys))
203
+ EOF
204
+ ```
205
+
206
+ Do not display, source or parse `.env`. Leave `AI_GATEWAY_API_KEY`,
207
+ `FIREWORKS_API_KEY`, `ANTHROPIC_API_KEY`, `OPENAI_API_KEY` and `GOOGLE_API_KEY`
208
+ empty.
209
+
210
+ ## Log in and start
211
+
212
+ Stop for the person to log in to the registry in their own terminal. They use
213
+ the pull-only invitation values, never chat or an agent-visible command:
214
+
215
+ ```sh
216
+ echo "$METERGRAPH_PULL_TOKEN" | docker login ghcr.io -u "$METERGRAPH_PULL_USER" --password-stdin
217
+ ```
218
+
219
+ Wait for confirmation only; never read Docker's credential file. State that
220
+ `./bin/start` will re-verify the manifest and both image digests, pull by digest,
221
+ start containers, and on a new database send one synthetic demo trace labeled
222
+ `environment=local-demo`. Then run:
223
+
224
+ ```sh
225
+ ./bin/start
226
+ ./bin/status
227
+ ```
228
+
229
+ An image-pull failure usually means login is missing or expired. On other
230
+ failure, read only the log named by `./bin/start` and `./bin/status`; never read
231
+ rendered Compose configuration or container environment. On password
232
+ authentication failure with `local_pgdata`, stop and ask for its matching
233
+ `.env`. Never delete data. Read the workspace ID and suggested command from the
234
+ status JSON's `workspace` check. The service listens only on localhost:8080.
235
+
236
+ ## Run setup
237
+
238
+ Return to the application repository. Prefer installed `metergraph` only when
239
+ its help lists `setup`; otherwise state that `npx --yes metergraph-cli@next`
240
+ fetches the preview CLI and use it as the prefix. Run with `--json`:
241
+
242
+ ```sh
243
+ metergraph setup --runtime local --deployment customer-local --url http://localhost:8080 --workspace 11111111-1111-4111-8111-111111111111 --confirm-prerequisites --client <client> --json
244
+ ```
245
+
246
+ Replace the example UUID and client only. Setup writes an ingest key to the
247
+ application `.env`, installs the skill and requests Metadata access only; never
248
+ print that file. Setup opens the deployment's browser consent page and waits;
249
+ the person signs in as the local admin and completes two approvals. If the
250
+ browser cannot open, follow the `metergraph` skill's guidance for handing the
251
+ person the approval URL. Continue until `data.status` is `ok`. For
252
+ `operator_handoff`, relay `data.next_action.kind` (`complete_prerequisite`,
253
+ `run_on_customer_machine`, `connection_guide`, or other); never report a
254
+ handoff as success.
255
+
256
+ The agent never runs a command that reads `.env`. To see the admin login without
257
+ printing it, the person runs these no-output pipelines themselves, one at a
258
+ time, in their own terminal or in Claude Code with the `!` prefix. Fill in the
259
+ absolute bundle directory for them; their shell does not have `bundle_dir`:
260
+
261
+ ```sh
262
+ sed -n 's/^METERGRAPH_LOCAL_ADMIN_EMAIL=//p' ~/metergraph/<version>/local/.env | pbcopy
263
+ sed -n 's/^METERGRAPH_LOCAL_ADMIN_PASSWORD=//p' ~/metergraph/<version>/local/.env | pbcopy
264
+ ```
265
+
266
+ They may instead use their own editor. Never use `cat`, `grep` or any other
267
+ command that prints `.env`, particularly with `!`, because `!` output lands in
268
+ the transcript. A new configuration keeps `admin@example.com` as the email.
269
+
270
+ ## Recovery and stopping
271
+
272
+ | State | Action |
273
+ | --- | --- |
274
+ | Signature, manifest, digest or path check fails | Stop; extract and run nothing |
275
+ | Port 8080 belongs to something else | Stop and ask; do not alter it |
276
+ | Registry pull is denied | Person repeats private login; never request the token |
277
+ | Existing `.env` | Reuse unchanged; never regenerate passwords |
278
+ | `local_pgdata` lacks its matching `.env` | Stop and find prior configuration; never remove it |
279
+ | Startup fails | Use status and only the named log; preserve secrets and data |
280
+ | Setup returns a handoff | Relay exact status/next action; do not report success |
281
+
282
+ To stop later, follow the bundle README's `stop`. Prefer it over `down`, which
283
+ loses raw payloads in local object storage. Never use `down --volumes`.
@@ -34,8 +34,9 @@ skill installation; a later installed version may offer `login`, `setup` and
34
34
  https://www.metergraph.dev/docs/start/first-trace/ and its manual setup steps.
35
35
  Do not infer that a server endpoint or source-only CLI command has shipped.
36
36
 
37
- For customer-local MeterGraph, use the released bundle and its included
38
- README, signed manifest and verification files. The human obtains the current
37
+ For customer-local MeterGraph, use `metergraph-local-bundle` to stand up the
38
+ released bundle, then continue here. Use its included README, signed manifest
39
+ and verification files. The human obtains the current
39
40
  pull-only registry credential; registry access is not yet universally self-serve.
40
41
  Use password-stdin and the local secret store for registry login. Verify the
41
42
  archive before running its scripts. Configure the bundle's local settings,
package/bin/metergraph.js CHANGED
@@ -1,11 +1,11 @@
1
1
  #!/usr/bin/env node
2
+ import { handOff, handsOff } from "../src/approval-handoff.js";
2
3
  import { main } from "../src/cli.js";
3
4
 
4
5
  // A closed pipe on stdout (for example "metergraph --help | head -1") is not
5
6
  // a CLI failure.
6
7
  process.stdout.on("error", () => {});
7
8
 
8
- process.exitCode = await main(process.argv.slice(2), {
9
- stdout: process.stdout,
10
- stderr: process.stderr,
11
- });
9
+ const argv = process.argv.slice(2);
10
+ const io = { stdout: process.stdout, stderr: process.stderr };
11
+ process.exitCode = handsOff(argv) ? await handOff(argv, io) : await main(argv, io);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metergraph-cli",
3
- "version": "0.2.0-preview.5",
3
+ "version": "0.2.0-preview.6",
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": {
@@ -0,0 +1,91 @@
1
+ import childProcess from "node:child_process";
2
+ import { fileURLToPath } from "node:url";
3
+
4
+ import { parseArgs } from "./args.js";
5
+ import { envelope, toJsonLine } from "./output.js";
6
+
7
+ // login and setup with --json --no-browser may need a person to approve in a
8
+ // browser. The approval returns to a loopback listener, so something must
9
+ // keep listening after the agent has the URL. Agent shells usually show a
10
+ // command's output only after it exits, so that cannot be this process.
11
+ //
12
+ // This process runs the same command as a detached waiter and relays the
13
+ // waiter's one JSON line. When the line is action_required, it exits with it
14
+ // at once and leaves the waiter listening until approval or --timeout-ms; the
15
+ // waiter saves what the approval completes and prints nothing more. Any other
16
+ // line is the command's result, and this process waits for the waiter to
17
+ // finish. The arguments carry no credential, and the waiter's environment is
18
+ // this one plus a marker.
19
+ const BIN = fileURLToPath(new URL("../bin/metergraph.js", import.meta.url));
20
+ const WAITER = "METERGRAPH_APPROVAL_WAITER";
21
+
22
+ export function handsOff(argv, env = process.env) {
23
+ if (env[WAITER] === "1") return false;
24
+ const parsed = parseArgs(argv);
25
+ return parsed.ok && (parsed.command === "login" || parsed.command === "setup") && parsed.json && parsed.noBrowser;
26
+ }
27
+
28
+ export function handOff(argv, { stdout }) {
29
+ return new Promise((resolve) => {
30
+ let child;
31
+ try {
32
+ child = childProcess.spawn(process.execPath, [...process.execArgv, BIN, ...argv], {
33
+ env: { ...process.env, [WAITER]: "1" },
34
+ stdio: ["ignore", "pipe", "ignore"],
35
+ detached: true,
36
+ windowsHide: true,
37
+ });
38
+ } catch {
39
+ resolve(failed(argv, stdout));
40
+ return;
41
+ }
42
+ let text = "";
43
+ let settled = false;
44
+ // Ctrl+C before the line arrives cancels the waiter, which reports it.
45
+ const forward = (signal) => child.kill(signal);
46
+ const signals = ["SIGINT", "SIGTERM"];
47
+ for (const signal of signals) process.on(signal, forward);
48
+ const finish = (code) => {
49
+ if (settled) return;
50
+ settled = true;
51
+ for (const signal of signals) process.removeListener(signal, forward);
52
+ resolve(code);
53
+ };
54
+ child.once("error", () => finish(failed(argv, stdout)));
55
+ child.stdout.setEncoding("utf8");
56
+ child.stdout.on("data", (chunk) => {
57
+ if (settled) return;
58
+ text += chunk;
59
+ const end = text.indexOf("\n");
60
+ if (end === -1) return;
61
+ let line;
62
+ try { line = JSON.parse(text.slice(0, end)); } catch { line = null; }
63
+ if (line?.outcome !== "action_required") return;
64
+ stdout.write(text.slice(0, end + 1));
65
+ child.stdout.destroy();
66
+ child.unref();
67
+ finish(line.exit_code);
68
+ });
69
+ child.once("close", (code) => {
70
+ if (settled) return;
71
+ if (text.endsWith("\n") && text.indexOf("\n") === text.length - 1) {
72
+ stdout.write(text);
73
+ finish(code ?? 1);
74
+ } else {
75
+ finish(failed(argv, stdout));
76
+ }
77
+ });
78
+ });
79
+ }
80
+
81
+ function failed(argv, stdout) {
82
+ const parsed = parseArgs(argv);
83
+ const result = envelope({
84
+ command: parsed.command ?? null,
85
+ outcome: "internal_error",
86
+ reason: "unexpected_failure",
87
+ message: "The CLI failed unexpectedly. No details are shown to avoid exposing sensitive input.",
88
+ });
89
+ stdout.write(toJsonLine(result));
90
+ return result.exit_code;
91
+ }
package/src/args.js CHANGED
@@ -279,9 +279,10 @@ function parseVerify(values, flags, json, fail) {
279
279
  open: flags.has("--open"), noBrowser: flags.has("--no-browser"), json };
280
280
  }
281
281
 
282
- // Client and runtime are required, so a script states where the skill is
283
- // used instead of the CLI guessing. Values for clients and runtimes that
284
- // cannot use project skill files are accepted here and handed off later.
282
+ // The client is required, so a script states which client loads the skill.
283
+ // The runtime defaults to local, the runtime setup installs the skill for;
284
+ // pass --runtime cloud for a cloud checkout. Values for clients and runtimes
285
+ // that cannot use project skill files are accepted here and handed off later.
285
286
  function parseSkill(action, values, json, fail) {
286
287
  if (action === null) {
287
288
  return fail(
@@ -296,10 +297,7 @@ function parseSkill(action, values, json, fail) {
296
297
  if (!Object.hasOwn(SKILL_CLIENTS, client) && !Object.hasOwn(HANDOFF_SKILL_CLIENTS, client)) {
297
298
  return fail("invalid_client", "--client must be codex, claude or cursor.");
298
299
  }
299
- const runtime = values["--runtime"];
300
- if (runtime === undefined) {
301
- return fail("missing_runtime", "--runtime is required. Use local or cloud.");
302
- }
300
+ const runtime = values["--runtime"] ?? "local";
303
301
  if (!SKILL_RUNTIMES.includes(runtime) && !HANDOFF_SKILL_RUNTIMES.includes(runtime)) {
304
302
  return fail("invalid_runtime", "--runtime must be local or cloud.");
305
303
  }
@@ -442,7 +440,8 @@ function parseSetup(values, flags, json, fail) {
442
440
  parseTimeout(rawTimeout, LOGIN_MIN_TIMEOUT_MS, LOGIN_MAX_TIMEOUT_MS);
443
441
  if (timeoutMs === null) return fail("invalid_timeout", `--timeout-ms must be a whole number from ${LOGIN_MIN_TIMEOUT_MS} to ${LOGIN_MAX_TIMEOUT_MS}.`);
444
442
  return { ok: true, command: "setup", runtime, origin, originExplicit: values["--url"] !== undefined,
445
- deployment, confirmPrerequisites: flags.has("--confirm-prerequisites"), agentTokenFile,
443
+ deployment, deploymentExplicit: values["--deployment"] !== undefined,
444
+ confirmPrerequisites: flags.has("--confirm-prerequisites"), agentTokenFile,
446
445
  workspace: rawWorkspace === undefined ? null : rawWorkspace.toLowerCase(),
447
446
  project: paths.project, configDir: paths.configDir, envFile, client, skipSkill: flags.has("--skip-skill"),
448
447
  timeoutMs, noBrowser: flags.has("--no-browser"), repair: flags.has("--repair"),
package/src/auth-login.js CHANGED
@@ -39,6 +39,11 @@ import { deadline, trapSignals } from "./transport.js";
39
39
  // server-verified workspace UUID. Token values, the authorization code, the
40
40
  // PKCE verifier, absolute paths and server text never leave this module.
41
41
  // progress(text) prints human progress lines; it is a no-op with --json.
42
+ // options.announce(data), given only with --json --no-browser, receives the
43
+ // login data with an open_url next action as soon as the authorization URL
44
+ // exists, while the listener keeps waiting. The caller prints it as the one
45
+ // JSON line, so an agent can hand the URL to a person; approval-handoff.js
46
+ // lets the agent's command exit then while this process keeps waiting.
42
47
 
43
48
  export async function runLogin(options, progress) {
44
49
  const ctx = {
@@ -58,21 +63,12 @@ export async function runLogin(options, progress) {
58
63
  if (options.runtime !== "local") return end("unsupported", "runtime_not_supported", GUIDE);
59
64
  const remote = detectRemoteSession();
60
65
  if (remote !== null) return end("unsupported", remote, GUIDE);
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;
68
-
69
66
  const trap = trapSignals();
70
67
  try {
71
68
  const root = resolveProject(options.project);
72
69
  const configDir = resolveConfigDir(options.configDir);
73
70
  const existing = readBinding(root);
74
71
  const bound = existing?.binding ?? null;
75
- if (urlUnshowable && (bound === null || options.reconnect)) return noBrowserHandoff();
76
72
  // Setup pins a deployment model before sign in. A stale binding for a
77
73
  // different profile must never be silently reused or reconnected.
78
74
  if (options.expectedProfile && bound !== null && !options.reconnect &&
@@ -102,9 +98,11 @@ export async function runLogin(options, progress) {
102
98
  }
103
99
  if (session.outcome !== "login_required") return end(session.outcome, session.reason);
104
100
  }
105
- if (urlUnshowable) return noBrowserHandoff();
106
101
 
107
- const flow = await authorize(options, ctx, trap.signal, progress);
102
+ const announce = options.announce
103
+ ? (next) => options.announce(loginData({ ...ctx, status: "approval_pending" }, next))
104
+ : null;
105
+ const flow = await authorize(options, ctx, trap.signal, progress, announce);
108
106
  if (!flow.ok) return end(flow.outcome, flow.reason, flow.next ?? null);
109
107
  const { endpoints, clientId, grant } = flow;
110
108
 
@@ -169,7 +167,7 @@ export async function runLogin(options, progress) {
169
167
  // written here. A validated grant that is not accepted is sent for revocation
170
168
  // before returning; that is best effort, and a token response that fails
171
169
  // validation cannot be revoked.
172
- async function authorize(options, ctx, cancel, progress) {
170
+ async function authorize(options, ctx, cancel, progress, announce) {
173
171
  const preflight = await runDoctor({ origin: ctx.origin, timeoutMs: AUTH_HTTP_TIMEOUT_MS });
174
172
  if (cancel.aborted) return fail("authorization_failed", "cancelled");
175
173
  if (preflight.outcome !== "authentication_required") return fail(preflight.outcome, preflight.reason);
@@ -217,7 +215,9 @@ async function authorize(options, ctx, cancel, progress) {
217
215
  const seconds = Math.round(options.timeoutMs / 1000);
218
216
  // A wait armed after Ctrl+C has already settled; do not show the URL.
219
217
  if (cancel.aborted) return fail("authorization_failed", "cancelled");
220
- if (options.noBrowser) {
218
+ if (options.noBrowser && announce) {
219
+ announce(openUrl(url, seconds, "sign in with Metadata access"));
220
+ } else if (options.noBrowser) {
221
221
  progress(
222
222
  "Open this URL in a browser on this machine to sign in:\n" +
223
223
  `${url}\n` +
@@ -375,6 +375,20 @@ function fail(outcome, reason, next = null) {
375
375
  return { ok: false, outcome, reason, next };
376
376
  }
377
377
 
378
+ // The next action a --json --no-browser run prints while it waits. The URL is
379
+ // the authorization request: public client ID, loopback redirect, state and
380
+ // PKCE challenge. It carries no token, code or verifier.
381
+ export function openUrl(url, seconds, purpose) {
382
+ return {
383
+ kind: "open_url",
384
+ url,
385
+ timeout_seconds: seconds,
386
+ message: `Show this URL to the person now and ask them to open it in a browser on this machine to ` +
387
+ `${purpose}. A background process waits up to ${seconds} seconds for their approval. ` +
388
+ "After they approve, run the same command again to continue.",
389
+ };
390
+ }
391
+
378
392
  const GUIDE = { kind: "connection_guide", url: CONNECTION_GUIDE_URL };
379
393
  const RECONNECT = {
380
394
  kind: "reconnect",
package/src/cli.js CHANGED
@@ -18,7 +18,7 @@ import { runRead } from "./read.js";
18
18
  import { readMessage, readText } from "./read-output.js";
19
19
  import { listSkillPack, runSkill, runSkillPack } from "./skill.js";
20
20
  import { runSetup } from "./setup.js";
21
- import { containsKnownCredential, preflightNonHostedSetup } from "./setup-deployment.js";
21
+ import { containsKnownCredential, preflightNonHostedSetup, prerequisiteGuide, resumeNonHostedRoute } from "./setup-deployment.js";
22
22
  import { VERSION } from "./constants.js";
23
23
  import { runVerify } from "./verify.js";
24
24
  import { verifyMessage, verifyText } from "./verify-output.js";
@@ -75,8 +75,10 @@ async function run(argv, { stdout, stderr }) {
75
75
  // Human progress goes to stderr, never with --json, so stdout carries
76
76
  // only the final result.
77
77
  const progress = parsed.json ? () => {} : (text) => stderr.write(`${text}\n`);
78
+ const pending = announcer(parsed, stdout, (data) => data);
78
79
  const { outcome, reason, data } =
79
- parsed.command === "login" ? await runLogin(parsed, progress) : await runLogout(parsed);
80
+ parsed.command === "login" ? await runLogin({ ...parsed, announce: pending.announce }, progress) : await runLogout(parsed);
81
+ if (pending.result) return pending.result;
80
82
  const message = authMessage(outcome, reason);
81
83
  const result = envelope({
82
84
  command: parsed.command,
@@ -90,14 +92,18 @@ async function run(argv, { stdout, stderr }) {
90
92
  }
91
93
 
92
94
  if (parsed.command === "setup") {
93
- const routed = parsed.deployment === "managed"
95
+ const options = resumeNonHostedRoute(parsed);
96
+ const routed = options.deployment === "managed"
94
97
  ? { proceed: true, profile: "managed" }
95
- : await preflightNonHostedSetup(parsed);
98
+ : await preflightNonHostedSetup(options);
96
99
  const progress = parsed.json ? () => {} : (line) =>
97
100
  stderr.write(containsKnownCredential(line, routed) ? "Setup progress.\n" : `${line}\n`);
101
+ // The announced line is checked for known credentials like the final one.
102
+ const pending = announcer(parsed, stdout, (data) => containsKnownCredential(data, routed) ? null : data);
98
103
  const response = routed.proceed
99
- ? await runSetup({ ...parsed, expectedProfile: routed.profile ?? null }, progress)
104
+ ? await runSetup({ ...options, expectedProfile: routed.profile ?? null, announce: pending.announce }, progress)
100
105
  : routed;
106
+ if (pending.result) return pending.result;
101
107
  const { outcome, reason, data } = response;
102
108
  let result = envelope({ command: "setup", outcome, reason, data,
103
109
  message: outcome === "ok" ? null : "Setup did not complete. Review the reason and retry safely." });
@@ -107,7 +113,8 @@ async function run(argv, { stdout, stderr }) {
107
113
  const receipt = result.data?.receipt;
108
114
  const summary = receipt ? `Workspace: ${receipt.workspace_id}\nDeployment: ${receipt.deployment_profile} at ${receipt.origin}\nCompleted: ${receipt.completed_steps.join(", ")}\nPending: ${receipt.pending_steps.join(", ")}\n` : "";
109
115
  const identity = repositoryLine(result.data?.repository);
110
- const human = `Metergraph setup: ${result.data?.status ?? "not_ready"}\n${summary}${identity}Application traffic verified: no\n${result.ok ? "Next: instrument your application and verify an exact trace.\n" : `Result: ${result.outcome} (${result.error.reason})\n`}`;
116
+ const guide = handoffLines(result.data);
117
+ const human = `Metergraph setup: ${result.data?.status ?? "not_ready"}\n${summary}${identity}${guide}Application traffic verified: no\n${result.ok ? "Next: instrument your application and verify an exact trace.\n" : `Result: ${result.outcome} (${result.error.reason})\n`}`;
111
118
  if (containsKnownCredential(human, routed)) {
112
119
  result = envelope({ command: "setup", outcome: "unsupported", reason: "credential_echo" });
113
120
  stdout.write(parsed.json ? toJsonLine(result) : "Setup stopped.\n");
@@ -181,6 +188,47 @@ async function run(argv, { stdout, stderr }) {
181
188
  return result;
182
189
  }
183
190
 
191
+ // With --json --no-browser, login and setup print their one JSON line as
192
+ // soon as a person must open an approval URL, then keep waiting for the
193
+ // callback; bin/metergraph.js has already moved this process to the
194
+ // background (approval-handoff.js). Whatever the run does afterwards, that
195
+ // line is its result; the agent reruns the command to continue. screen(data) returns
196
+ // the data to print, or null to print a credential_echo refusal instead.
197
+ function announcer(parsed, stdout, screen) {
198
+ const pending = { announce: null, result: null };
199
+ if (!parsed.json || !parsed.noBrowser) return pending;
200
+ pending.announce = (data) => {
201
+ if (pending.result) return;
202
+ const safe = screen(data);
203
+ pending.result = safe === null
204
+ ? envelope({ command: parsed.command, outcome: "unsupported", reason: "credential_echo" })
205
+ : envelope({ command: parsed.command, outcome: "action_required", reason: "browser_approval_required",
206
+ data: safe, message: authMessage("action_required", "browser_approval_required") });
207
+ stdout.write(toJsonLine(pending.result));
208
+ };
209
+ return pending;
210
+ }
211
+
212
+ // What an operator handoff is waiting for, in fixed text: the route, each
213
+ // unconfirmed prerequisite with its guidance, and the next action.
214
+ function handoffLines(data) {
215
+ if (data?.status !== "operator_handoff") return "";
216
+ let text = data.origin ? `Deployment: ${data.deployment_profile} at ${data.origin}\n` : "";
217
+ if (data.workspace_id) text += `Workspace: ${data.workspace_id}\n`;
218
+ if (data.pending_prerequisites.length > 0) {
219
+ text += "Confirm each prerequisite, then rerun with --confirm-prerequisites:\n";
220
+ for (const name of data.pending_prerequisites) {
221
+ text += ` - ${name}: ${prerequisiteGuide(name, data.deployment_profile) ?? "see the guide"}\n`;
222
+ }
223
+ }
224
+ const next = data.next_action;
225
+ if (next?.message && !(next.kind === "complete_prerequisite" && data.pending_prerequisites.includes(next.prerequisite))) {
226
+ text += `Next: ${next.message}\n`;
227
+ }
228
+ if (next?.url) text += `Guide: ${next.url}\n`;
229
+ return text;
230
+ }
231
+
184
232
  // One line on the repository identity setup found or recorded. The value is
185
233
  // owner/name only; a remote URL is never shown.
186
234
  function repositoryLine(repository) {
package/src/constants.js CHANGED
@@ -141,6 +141,7 @@ export const EXIT_CODES = Object.freeze({
141
141
  permission_denied: 15,
142
142
  rate_limited: 16,
143
143
  cancelled: 17,
144
+ action_required: 18,
144
145
  });
145
146
 
146
147
  export const EXIT_CODE_MEANINGS = Object.freeze({
@@ -177,4 +178,8 @@ export const EXIT_CODE_MEANINGS = Object.freeze({
177
178
  rate_limited: "The service asked the CLI to slow down. Nothing was retried. Try again later.",
178
179
  cancelled:
179
180
  "A read command was interrupted before it finished. Read commands never change workspace configuration or telemetry.",
181
+ action_required:
182
+ "A person must act before this command can finish; data.next_action says what to do. " +
183
+ "With --json --no-browser the command exits as soon as the approval URL exists, and a background " +
184
+ "process waits for approval until --timeout-ms. After approval, run the same command again to continue.",
180
185
  });
@@ -117,7 +117,7 @@ const DEPLOYMENT_PATH = "/v1/deployment";
117
117
  const MIN_TIMEOUT_MS = 100;
118
118
  const MAX_TIMEOUT_MS = 60000;
119
119
 
120
- function action(kind, prerequisite = null) {
120
+ export function action(kind, prerequisite = null) {
121
121
  // Kinds are constants from this module, so this is a programming error.
122
122
  if (!ACTION_KINDS.includes(kind)) throw new Error("unknown next action kind");
123
123
  return { kind, prerequisite, url: CONNECTION_GUIDE_URL };
package/src/output.js CHANGED
@@ -73,20 +73,20 @@ const SKILL_OPTIONS = [
73
73
  {
74
74
  name: "--runtime",
75
75
  value: "RUNTIME",
76
- summary: "Required. local or cloud: where the client runs. Recorded, not detected.",
76
+ summary: "local (default) or cloud: where the client runs. Recorded, not detected.",
77
77
  },
78
78
  { name: "--project", value: "DIR", summary: "Existing project directory. Default: the current directory." },
79
79
  { name: "--json", value: null, summary: "Print one JSON line on stdout." },
80
80
  ];
81
81
 
82
82
  const SKILL_USAGE = [
83
- "metergraph skill install --client CLIENT --runtime RUNTIME [--project DIR] [--json]",
84
- "metergraph skill update --client CLIENT --runtime RUNTIME [--project DIR] [--json]",
83
+ "metergraph skill install --client CLIENT [--runtime RUNTIME] [--project DIR] [--json]",
84
+ "metergraph skill update --client CLIENT [--runtime RUNTIME] [--project DIR] [--json]",
85
85
  ];
86
86
 
87
87
  const SKILLS_USAGE = [
88
- "metergraph skills install --client CLIENT --runtime RUNTIME [--project DIR] [--json]",
89
- "metergraph skills update --client CLIENT --runtime RUNTIME [--project DIR] [--json]",
88
+ "metergraph skills install --client CLIENT [--runtime RUNTIME] [--project DIR] [--json]",
89
+ "metergraph skills update --client CLIENT [--runtime RUNTIME] [--project DIR] [--json]",
90
90
  "metergraph skills list [--project DIR] [--json]",
91
91
  ];
92
92
 
@@ -106,6 +106,15 @@ const SKILLS_STATE_LABELS = {
106
106
  unsafe_path: "unsafe path",
107
107
  };
108
108
 
109
+ // How a coding agent gets an approval URL from login or setup. Many agent
110
+ // shells show output only after the command exits, which is too late.
111
+ const AGENT_APPROVAL_HELP = [
112
+ "From a coding agent: with --json --no-browser, a step that needs approval exits at once",
113
+ "with exit 18 action_required and the URL in data.next_action.url, while a background",
114
+ "process keeps waiting for the approval until --timeout-ms. Show the URL to the person,",
115
+ "and after they approve, run the same command again to continue.",
116
+ ];
117
+
109
118
  const CONFIG_DIR_OPTION = {
110
119
  name: "--config-dir",
111
120
  value: "DIR",
@@ -134,7 +143,7 @@ const LOGIN_OPTIONS = [
134
143
  summary: `Time to wait for the browser, ${LOGIN_MIN_TIMEOUT_MS} to ${LOGIN_MAX_TIMEOUT_MS}. Default ${LOGIN_DEFAULT_TIMEOUT_MS}.`,
135
144
  },
136
145
  { name: "--signup", value: null, summary: "Start at the hosted sign up page. Managed service only." },
137
- { name: "--no-browser", value: null, summary: "Print the sign in URL on stderr instead of opening a browser. With --json, only a rerun that needs no sign in succeeds." },
146
+ { name: "--no-browser", value: null, summary: "Print the sign in URL on stderr instead of opening a browser. With --json, exit 18 with it in data.next_action (open_url)." },
138
147
  { name: "--reconnect", value: null, summary: "Allow switching a bound project to another origin or workspace." },
139
148
  { name: "--json", value: null, summary: "Print one JSON line on stdout." },
140
149
  ];
@@ -353,7 +362,7 @@ export function helpData(topic) {
353
362
  { name: "--timeout-ms", value: "N", summary: "Time to wait for browser approval." },
354
363
  { name: "--signup", value: null, summary: "Start at hosted sign up when the project needs login." },
355
364
  { name: "--reconnect", value: null, summary: "Permit switching an existing project binding." },
356
- { name: "--no-browser", value: null, summary: "Print approval URL on stderr. With --json, only a rerun that needs no approval succeeds." },
365
+ { name: "--no-browser", value: null, summary: "Print approval URL on stderr. With --json, exit 18 with it in data.next_action (open_url); rerun after approval." },
357
366
  { name: "--repair", value: null, summary: "Explicitly approve replacement of an acknowledged key that no longer verifies." },
358
367
  { name: "--deployment", value: "MODEL", summary: "managed, customer-local, byoc or oss. Default managed." },
359
368
  { name: "--confirm-prerequisites", value: null, summary: "Attest deployment prerequisites are met; it does not verify bundle publication." },
@@ -417,6 +426,7 @@ export function helpText(topic) {
417
426
  const flag = option.value ? `${option.name} ${option.value}` : option.name;
418
427
  lines.push(` ${flag.padEnd(25)}${option.summary}`);
419
428
  }
429
+ lines.push("", ...AGENT_APPROVAL_HELP);
420
430
  } else if (topic === "login" || topic === "logout") {
421
431
  const login = topic === "login";
422
432
  lines.push(`Usage: ${login ? LOGIN_USAGE : LOGOUT_USAGE}`, "");
@@ -449,6 +459,7 @@ export function helpText(topic) {
449
459
  const flag = option.value ? `${option.name} ${option.value}` : option.name;
450
460
  lines.push(` ${flag.padEnd(18)}${option.summary}`);
451
461
  }
462
+ if (login) lines.push("", ...AGENT_APPROVAL_HELP);
452
463
  } else if (topic === "verify") {
453
464
  lines.push(`Usage: ${VERIFY_USAGE}`, "", "Uses the saved Metadata grant to poll for one exact, processed trace.",
454
465
  "The source label is supplied by the caller. A Metadata match alone does not prove application traffic.",
@@ -719,9 +730,9 @@ const AUTH_MESSAGES = {
719
730
  "so sign in cannot finish here. Follow the connection guide instead. Nothing was written.",
720
731
  ci_environment:
721
732
  "This is a CI environment, where no person can approve sign in in a browser. Nothing was written.",
722
- no_browser_requires_terminal:
723
- "Approval is needed, and --no-browser prints its URL for a person to open, which --json cannot do. " +
724
- "Run the same command without --json in a terminal. Nothing was done.",
733
+ browser_approval_required:
734
+ "A person must approve in a browser. Open data.next_action.url on this machine; a background process " +
735
+ "waits for the approval until the timeout. After approval, run the same command again to continue.",
725
736
  bound_to_other_origin:
726
737
  "This project is bound to a different origin. Nothing was changed. Use --reconnect to switch it.",
727
738
  bound_to_other_workspace:
@@ -737,7 +748,7 @@ const AUTH_MESSAGES = {
737
748
  timeout: "The operation did not finish within the time limit. Nothing was saved.",
738
749
  cancelled: "Sign in was cancelled. Nothing was saved.",
739
750
  browser_unavailable:
740
- "The browser could not be opened. Run login again with --no-browser, without --json, and open the URL it prints.",
751
+ "The browser could not be opened. Run login again with --no-browser and open the URL it prints.",
741
752
  oauth_metadata_missing: "This service does not support CLI sign in yet. Nothing was written.",
742
753
  metadata_scope_unsupported:
743
754
  "This service does not offer Metadata-only access for the CLI yet. The CLI does not fall back to broader access.",
@@ -1,6 +1,9 @@
1
+ import { detectRemoteSession } from "./auth-browser.js";
1
2
  import { runDoctor } from "./doctor.js";
2
- import { planDeploymentRoute, verifyDeploymentRoute } from "./deployment-route.js";
3
+ import { action, planDeploymentRoute, verifyDeploymentRoute } from "./deployment-route.js";
3
4
  import { AUTH_HTTP_TIMEOUT_MS } from "./constants.js";
5
+ import { resolveProject } from "./auth-binding.js";
6
+ import { readSetupState } from "./setup-state.js";
4
7
 
5
8
  // Setup routing uses the same deployment model and Metadata credential checks
6
9
  // as the standalone routing contract. Operator prerequisites are assertions,
@@ -24,6 +27,55 @@ const PREREQUISITES = Object.freeze({
24
27
 
25
28
  const EXPECTED_PROFILE = Object.freeze({ "customer-local": "local", byoc: "byoc-core", oss: "oss" });
26
29
 
30
+ const DEPLOYMENT_FOR_PROFILE = Object.freeze({ local: "customer-local", "byoc-core": "byoc" });
31
+
32
+ // Fixed guidance for each prerequisite, so a person or agent can act on a
33
+ // handoff without the docs. None of it names a credential value, and none of
34
+ // it asks for a credential to be passed to this CLI.
35
+ const PREREQUISITE_GUIDE = Object.freeze({
36
+ released_signed_bundle: "Download the released customer-local bundle and its signed manifest, and verify the signature before starting it. A bundle built from source is not a release.",
37
+ registry_invitation: "Accept the workspace's registry invitation so the bundle can pull its images. The registry pull credential is only for pulling images: never give it to this CLI or put it in the project env file.",
38
+ bundle_started_verified: "Start the bundle with bin/start and wait until bin/status reports it healthy. Pass its origin as --url and the workspace ID that bin/status shows as --workspace.",
39
+ local_admin_configured: "Sign in to the installation once as its local admin, in a browser on this machine. Setup asks that account to approve sign in and an ingest-only key.",
40
+ operator_provisioning: "The operator must already have provisioned the customer-owned AWS deployment. Setup connects to it and never provisions anything.",
41
+ private_network_reachability: "Run setup from a machine that reaches the deployment's private HTTPS origin, for example over the customer VPN. Setup never opens a tunnel or a public endpoint.",
42
+ identity_membership_configured: "The operator must already have given your identity membership of the workspace through the deployment's configured identity provider.",
43
+ server_distribution_installed: "The open source server must already be installed and running under the operator's management.",
44
+ deployment_discovery_supported: "The open source server must answer GET /v1/deployment with the oss profile. If it does not, ask the operator to upgrade it.",
45
+ metadata_scope_supported: "The open source server must advertise the agent:metadata scope in its OAuth metadata. If it does not, ask the operator to upgrade it.",
46
+ ingestion_tokens_configured: "The operator configures ingestion tokens in MG_TOKENS and gives the application one as METERGRAPH_APP_TOKEN. Setup never issues or writes open source ingest tokens.",
47
+ agent_read_tokens_configured: "The operator configures agent read tokens in MG_AGENT_TOKENS, separately from MG_TOKENS. Never reuse an ingestion token for agent reads.",
48
+ });
49
+ const METADATA_CREDENTIAL_GUIDE = Object.freeze({
50
+ local: "Setup obtains its own Metadata-only sign in through the browser. --agent-token-file is optional and must hold a Metadata-only agent token.",
51
+ "byoc-core": "Setup obtains its own Metadata-only sign in through the browser. --agent-token-file is optional and must hold a Metadata-only agent token.",
52
+ oss: "Put one MG_AGENT_TOKENS value in a private file (mode 0600) and pass its absolute path as --agent-token-file.",
53
+ });
54
+ const ACTION_GUIDE = Object.freeze({
55
+ complete_prerequisite: "Complete the named prerequisite, then rerun setup with --confirm-prerequisites.",
56
+ oss_operator_handoff: "Ask the open source server operator to complete this step. This CLI does not create open source credentials or change the server.",
57
+ run_on_customer_machine: "Run setup in a terminal on the machine that reaches the deployment. Setup does not tunnel or forward credentials from a remote or cloud session.",
58
+ use_https_origin: "Pass the deployment's HTTPS origin. A customer-owned AWS deployment is never reached over plain HTTP.",
59
+ connection_guide: "Check that --url is the installation's own origin and that it reports the expected deployment profile.",
60
+ check_private_network: "This machine cannot reach the private origin. Connect to the deployment's network, for example over the customer VPN, and rerun. Do not expose the deployment publicly.",
61
+ platform_credential_handoff: "This platform cannot prove the agent token file is private. Rerun without --agent-token-file, or on macOS or Linux.",
62
+ fix_credential_file: "Make --agent-token-file an absolute path to a regular file you own, with mode 0600, holding one token.",
63
+ use_metadata_only_credential: "The agent token was refused or has more than Metadata access. Use a Metadata-only agent token for this workspace.",
64
+ retry: "Rerun the same command.",
65
+ });
66
+
67
+ export function prerequisiteGuide(name, profile) {
68
+ if (name === "metadata_agent_credential") return METADATA_CREDENTIAL_GUIDE[profile] ?? null;
69
+ return PREREQUISITE_GUIDE[name] ?? null;
70
+ }
71
+
72
+ function guided(nextAction, profile) {
73
+ if (nextAction === null) return null;
74
+ const message = nextAction.kind === "complete_prerequisite" && nextAction.prerequisite !== null
75
+ ? prerequisiteGuide(nextAction.prerequisite, profile) : ACTION_GUIDE[nextAction.kind] ?? null;
76
+ return { ...nextAction, message };
77
+ }
78
+
27
79
  const handoff = (outcome, reason, nextAction = null, details = {}) => ({
28
80
  proceed: false,
29
81
  outcome,
@@ -38,10 +90,21 @@ const handoff = (outcome, reason, nextAction = null, details = {}) => ({
38
90
  pending_prerequisites: details.pendingPrerequisites ?? [],
39
91
  metadata_access: details.metadataAccess ?? "not_checked",
40
92
  ingest_credential: "not_checked",
41
- next_action: nextAction,
93
+ next_action: guided(nextAction, details.profile ?? null),
42
94
  },
43
95
  });
44
96
 
97
+ // The next action when the service did not answer as a healthy deployment.
98
+ // An unreachable customer-local origin usually means the bundle is not
99
+ // running; an unreachable BYOC origin is private networking. Neither is
100
+ // tunnelled or relaxed.
101
+ function unreachedAction(deployment, outcome) {
102
+ if (deployment === "oss") return action("oss_operator_handoff");
103
+ if (outcome !== "connection_failed") return action("connection_guide");
104
+ if (deployment === "byoc") return action("check_private_network");
105
+ return action("complete_prerequisite", "bundle_started_verified");
106
+ }
107
+
45
108
  // The static agent token can be any printable string of at least 16 bytes.
46
109
  // Even a fixed status such as "operator_handoff" might equal that token. Keep
47
110
  // the verifier's non-enumerable knownCredentials on every later result and
@@ -94,13 +157,21 @@ export function planNonHostedSetup(options) {
94
157
  return { proceed: true, plan: planned.plan, prerequisites };
95
158
  }
96
159
 
97
- export async function preflightNonHostedSetup(options) {
160
+ export async function preflightNonHostedSetup(options, { env = process.env } = {}) {
98
161
  const planned = planNonHostedSetup(options);
99
162
  if (!planned.proceed) return planned;
100
163
  const { plan, prerequisites } = planned;
164
+ // Setup itself refuses a remote session, but only after this preflight.
165
+ // Refuse here first, so a remote shell sends no request and never reads the
166
+ // separate agent token file.
167
+ if (detectRemoteSession(env) !== null) {
168
+ return handoff("unsupported", "run_on_local_machine", action("run_on_customer_machine"), {
169
+ profile: plan.deployment_profile, origin: plan.origin, workspaceId: plan.workspace_id,
170
+ });
171
+ }
101
172
  const timeoutMs = Math.min(options.timeoutMs, AUTH_HTTP_TIMEOUT_MS);
102
173
  if (options.deployment === "oss" && !options.agentTokenFile) {
103
- return handoff("unsupported", "oss_agent_token_file_required", "oss_operator_handoff", {
174
+ return handoff("unsupported", "oss_agent_token_file_required", action("oss_operator_handoff"), {
104
175
  profile: plan.deployment_profile, origin: plan.origin, workspaceId: plan.workspace_id,
105
176
  prerequisites: "operator_confirmed",
106
177
  });
@@ -123,7 +194,7 @@ export async function preflightNonHostedSetup(options) {
123
194
  // The OSS operator provisions MG_TOKENS and MG_AGENT_TOKENS separately.
124
195
  // No released OSS server implements the purpose-bound ingest bootstrap,
125
196
  // and a Metadata agent token cannot issue a key. Keep this a handoff.
126
- return guardKnownCredentials(handoff("unsupported", "oss_ingest_operator_handoff", "oss_operator_handoff", {
197
+ return guardKnownCredentials(handoff("unsupported", "oss_ingest_operator_handoff", action("oss_operator_handoff"), {
127
198
  profile: plan.deployment_profile, origin: plan.origin, workspaceId: plan.workspace_id,
128
199
  prerequisites: "operator_confirmed", metadataAccess: "verified",
129
200
  }), verified);
@@ -134,13 +205,13 @@ export async function preflightNonHostedSetup(options) {
134
205
  // and unsafe network responses and reports only a fixed profile token.
135
206
  const doctor = await runDoctor({ origin: plan.origin, timeoutMs });
136
207
  if (doctor.report.deployment_profile !== null && doctor.report.deployment_profile !== plan.deployment_profile) {
137
- return handoff("unsupported", "deployment_profile_mismatch", "connection_guide", {
208
+ return handoff("unsupported", "deployment_profile_mismatch", action("connection_guide"), {
138
209
  profile: plan.deployment_profile, origin: plan.origin, workspaceId: plan.workspace_id,
139
210
  prerequisites: "operator_confirmed",
140
211
  });
141
212
  }
142
213
  if (doctor.outcome !== "authentication_required") {
143
- return handoff(doctor.outcome, doctor.reason, options.deployment === "oss" ? "oss_operator_handoff" : "connection_guide", {
214
+ return handoff(doctor.outcome, doctor.reason, unreachedAction(options.deployment, doctor.outcome), {
144
215
  profile: plan.deployment_profile, origin: plan.origin, workspaceId: plan.workspace_id,
145
216
  prerequisites: "operator_confirmed",
146
217
  });
@@ -176,3 +247,28 @@ export async function preflightNonHostedSetup(options) {
176
247
  metadataAccess: "pending_login",
177
248
  };
178
249
  }
250
+
251
+ // A rerun of non-hosted setup may omit the route it was first given, as a
252
+ // hosted rerun does. Only a saved setup state for a customer-local or BYOC
253
+ // route is resumed, and only to that same origin and workspace. The earlier
254
+ // run's operator confirmation stands for that route; every live check still
255
+ // runs again. Anything else is left for setup's own conflict checks.
256
+ export function resumeNonHostedRoute(options) {
257
+ let saved;
258
+ try {
259
+ saved = readSetupState(resolveProject(options.project));
260
+ } catch {
261
+ return options;
262
+ }
263
+ const deployment = DEPLOYMENT_FOR_PROFILE[saved?.value.deployment_profile];
264
+ if (deployment === undefined) return options;
265
+ const { origin, workspace_id: workspaceId } = saved.value;
266
+ const sameRoute = (!options.originExplicit || options.origin === origin) &&
267
+ (options.workspace === null || options.workspace === workspaceId);
268
+ if (!sameRoute) return options;
269
+ if (!options.deploymentExplicit) {
270
+ return { ...options, deployment, origin, originExplicit: true, workspace: workspaceId, confirmPrerequisites: true };
271
+ }
272
+ if (options.deployment === deployment && options.originExplicit) return { ...options, confirmPrerequisites: true };
273
+ return options;
274
+ }
package/src/setup.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import { detectRemoteSession, openBrowser } from "./auth-browser.js";
2
2
  import { readBinding, resolveProject } from "./auth-binding.js";
3
3
  import { startCallbackListener } from "./auth-callback.js";
4
- import { runLogin } from "./auth-login.js";
4
+ import { openUrl, runLogin } from "./auth-login.js";
5
5
  import { clientFor, endpointsFor, newPkce, offeredClientId } from "./auth-oauth.js";
6
6
  import { Stop } from "./auth-store.js";
7
7
  import { verifiedSession } from "./auth-session.js";
@@ -24,26 +24,26 @@ const PURPOSE = "ingest-bootstrap-v1";
24
24
  const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
25
25
  const RECEIPT = /^mgbs_[A-Za-z0-9_-]{32,256}$/;
26
26
  const KEY = (value) => typeof value === "string" && UUID.test(value);
27
- // --json cannot show a person the --no-browser approval URL. The refusal names
28
- // a fixed, secret-free next step instead of only a reason token.
29
- const RUN_IN_TERMINAL = Object.freeze({
30
- kind: "run_in_terminal",
31
- message: "Run the same setup command in a terminal without --json to see the approval URL.",
32
- });
33
- const fail = (outcome, reason, status = "not_ready", receipt = null) => ({ outcome, reason,
34
- data: { status, application_traffic_verified: false, receipt,
35
- next_action: reason === "no_browser_requires_terminal" ? RUN_IN_TERMINAL : null } });
27
+ const fail = (outcome, reason, status = "not_ready", receipt = null, next = null) => ({ outcome, reason,
28
+ data: { status, application_traffic_verified: false, receipt, next_action: next } });
36
29
 
37
30
  // This command never receives an ingest key from argv, stdin, an environment
38
31
  // variable or a Metadata OAuth grant. Only the purpose-bound redemption
39
32
  // response can supply it. Every returned string is fixed and non-secret.
33
+ //
34
+ // With --json --no-browser the caller passes options.announce(data). The
35
+ // first approval this run needs (sign in, or ingest-only setup approval) is
36
+ // announced as setup data with an open_url next action while the listener
37
+ // keeps waiting; stdout has room for one line, so a second approval in the
38
+ // same run is not started. The person approves, the run saves what it
39
+ // finished, and the agent reruns setup, which resumes from the saved state.
40
40
  export async function runSetup(options, progress = () => {}) {
41
41
  if (options.runtime !== "local" || detectRemoteSession() !== null) return fail("unsupported", "run_on_local_machine");
42
42
  const trap = trapSignals();
43
43
  let receipt = null;
44
44
  let loginVerified = false;
45
45
  let credentialVerified = false;
46
- const stop = (outcome, reason, status = "not_ready") => {
46
+ const currentReceipt = () => {
47
47
  let current = receipt;
48
48
  if (current !== null && (!loginVerified || !credentialVerified)) {
49
49
  const completed = loginVerified ? ["login"] : [];
@@ -52,8 +52,14 @@ export async function runSetup(options, progress = () => {}) {
52
52
  ...(!loginVerified ? ["login"] : []), "credential", "skill", "instrument", "verify", "view",
53
53
  ] };
54
54
  }
55
- return fail(outcome, reason, status, current);
55
+ return current;
56
56
  };
57
+ const stop = (outcome, reason, status = "not_ready") => fail(outcome, reason, status, currentReceipt());
58
+ let announced = false;
59
+ const announce = options.announce ? (status, next) => {
60
+ announced = true;
61
+ options.announce(fail("action_required", "browser_approval_required", status, currentReceipt(), next).data);
62
+ } : null;
57
63
  try {
58
64
  const root = resolveProject(options.project);
59
65
  const saved = readSetupState(root);
@@ -67,6 +73,13 @@ export async function runSetup(options, progress = () => {}) {
67
73
  (bound !== null && (bound.origin !== saved.value.origin || bound.workspace_id !== saved.value.workspace_id)))) {
68
74
  return stop("conflict", "setup_binding_changed");
69
75
  }
76
+ // An env key this setup does not own stops before any sign in, so the
77
+ // refusal costs no approval and leaves the binding untouched. The check
78
+ // after sign in stays, against a fresh read.
79
+ if (!saved) {
80
+ const early = currentEnvValues(preflightEnv({ project: root, envFile: options.envFile, signal: trap.signal }));
81
+ if (early.token !== null) return stop("conflict", "existing_ingest_key_unowned");
82
+ }
70
83
  const bindingDiffers = bound !== null && (bound.origin !== intendedOrigin ||
71
84
  (intendedWorkspace !== null && bound.workspace_id !== intendedWorkspace));
72
85
  if (bindingDiffers && !options.reconnect) return stop("conflict", "bound_to_other_workspace");
@@ -74,7 +87,8 @@ export async function runSetup(options, progress = () => {}) {
74
87
  await verifiedSession({ project: root, configDir: options.configDir, cancel: trap.signal });
75
88
  if (!signedIn.ok && signedIn.outcome === "login_required") {
76
89
  const login = await runLogin({ ...options, origin: intendedOrigin, workspace: intendedWorkspace,
77
- project: root, reconnect: options.reconnect, expectedProfile: options.expectedProfile ?? null }, progress);
90
+ project: root, reconnect: options.reconnect, expectedProfile: options.expectedProfile ?? null,
91
+ announce: announce && ((data) => announce("login_pending", data.next_action)) }, progress);
78
92
  if (login.outcome !== "ok") return stop(login.outcome, login.reason, "login_pending");
79
93
  signedIn = await verifiedSession({ project: root, configDir: options.configDir, cancel: trap.signal });
80
94
  }
@@ -146,8 +160,9 @@ export async function runSetup(options, progress = () => {}) {
146
160
  }
147
161
 
148
162
  // A verified rerun returned above without approval. From here a person
149
- // must open an approval URL, which --json cannot show with --no-browser.
150
- if (options.json && options.noBrowser) return stop("unsupported", "no_browser_requires_terminal");
163
+ // must open an approval URL. A run that already announced its sign in URL
164
+ // has used its one JSON line, so it stops here and the rerun continues.
165
+ if (announced) return stop("action_required", "browser_approval_required", "approval_pending");
151
166
  const setup = await discoverSetup(origin, profile, trap.signal);
152
167
  if (!setup.ok) return stop(setup.outcome, setup.reason);
153
168
 
@@ -183,7 +198,9 @@ export async function runSetup(options, progress = () => {}) {
183
198
  if (intent === "repair") params.expected_key_id = state.key_id;
184
199
  url.search = new URLSearchParams(params).toString();
185
200
  if (trap.signal.aborted) return stop("authorization_failed", "cancelled");
186
- if (options.noBrowser) progress(`Open this URL on this machine to approve ingest-only setup:\n${url.href}`);
201
+ if (options.noBrowser && announce) {
202
+ announce("approval_pending", openUrl(url.href, Math.round(options.timeoutMs / 1000), "approve ingest-only setup"));
203
+ } else if (options.noBrowser) progress(`Open this URL on this machine to approve ingest-only setup:\n${url.href}`);
187
204
  else if (!(await openBrowser(url.href))) return stop("authorization_failed", "browser_unavailable");
188
205
  else progress("Opened your browser for ingest-only setup approval.");
189
206
  callback = await pending;
@@ -5,7 +5,7 @@ import { readFileSync } from "node:fs";
5
5
  // public skill at the manifest's source_url. The source has no version of its
6
6
  // own, so the revision is derived from the content hash. The hash is pinned
7
7
  // here as well as in the manifest, so editing either file alone is detected.
8
- const PINNED_SHA256 = "92f7955f6f8761a20a1886ad81f22dfcee239afddf8e9f1e761c35eb8e70f218";
8
+ const PINNED_SHA256 = "e7fc4eb7a22eeb16c357bede9d0a080ad3b415aaab1d9c72f164d5350d48c4b1";
9
9
 
10
10
  const ASSET_DIR = new URL("../assets/skill/", import.meta.url);
11
11
  const MANIFEST_KEYS = ["file", "manifest_version", "name", "revision", "sha256", "size", "source_url"];
@@ -55,7 +55,7 @@ export function loadBundledSkill() {
55
55
  // The workflow skills that "skills install" writes, vendored from the public
56
56
  // skills repository at one commit. The manifest's own hash is pinned here, and
57
57
  // the manifest pins each skill's hash, so changing any file alone is detected.
58
- const PINNED_PACK_SHA256 = "c8e4d9b0d90bd201e90758e16ab0827b8ea36e3620a28223a7e73e91c035d835";
58
+ const PINNED_PACK_SHA256 = "f08a188abba2018770dc3ba071509ff22f3af76ebc5c30a9f3a2ec2b2eae75b5";
59
59
 
60
60
  const PACK_DIR = new URL("../assets/skills/", import.meta.url);
61
61
  const PACK_KEYS = ["commit", "manifest_version", "repository", "skills"];