metergraph-cli 0.2.1 → 0.2.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.
@@ -18,8 +18,25 @@ only with the tools the capabilities advertise.
18
18
  ## 1. Read it
19
19
 
20
20
  1. `metergraph_get_analysis_run` with the run ID. Continue only when the status
21
- is final. If `report.available` is false on a completed run, the report is
22
- missing: say so and give the run's page (below).
21
+ is final. If it is not, show where the run is, as described next, and stop. If
22
+ `report.available` is false on a completed run, the report is missing: say
23
+ so and give the run's page (below).
24
+
25
+ For a run that is not final, draw `run.stages.items` in the order returned,
26
+ using each item's `state` exactly as given: `✓` completed, `●` current, `✗`
27
+ failed, `–` skipped, `○` not started. For example:
28
+
29
+ ```
30
+ ✓ queued → ✓ exporting → ● analyzing → ○ classified → ○ importing → ○ completed
31
+ ```
32
+
33
+ Under it, say in plain text which stages are done, which one is current and
34
+ since when (its `entered_at`, checked at `provenance.generated_at`), and
35
+ which are still to come. Never give a percentage, an ETA or a time remaining.
36
+ When `run.stages.available` is false, or `stages` is missing, draw nothing:
37
+ say the stage breakdown is unavailable (`unavailable_reason`, when present)
38
+ and report the `status` and timestamps instead. `metergraph-analyze`, when
39
+ installed, describes the same progress report in more detail.
23
40
  2. `metergraph_get_report` with `report.analysis_id`. Read the `outcome`, each
24
41
  workload's `state`, `sample_count` and `reason`, each `model_swap`
25
42
  opportunity (`recommendation_status`, `quality_vs_reference`,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "metergraph-cli",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "description": "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,157 @@
1
+ import { verifyContext } from "./auth-oauth.js";
2
+ import { DEFAULT_ORIGIN, MAX_BODY_BYTES, METADATA_SCOPE } from "./constants.js";
3
+ import { CREDENTIAL_MIN_LENGTH, readCredentialFile } from "./deployment-credential.js";
4
+ import { checkDeployment } from "./doctor.js";
5
+ import { AGENT_TOKEN_ENV, AGENT_URL_ENV } from "./environment.js";
6
+ import { parseOrigin } from "./origin.js";
7
+ import { send } from "./transport.js";
8
+
9
+ // The read commands' second credential: a coding-agent key with Metadata
10
+ // access that a person created on the Keys page and handed to the agent's
11
+ // environment as a secret. It is for agents where browser sign in cannot
12
+ // finish (SSH, cloud workspaces, CI, some containers).
13
+ //
14
+ // The key comes only from the METERGRAPH_AGENT_TOKEN environment variable or
15
+ // from a private file named by --agent-token-file (checked as setup checks
16
+ // its agent token file). There is no argv or stdin form. The key is never
17
+ // written anywhere, never printed, and sent only as a bearer header to the
18
+ // chosen origin, and only after that origin has answered the unauthenticated
19
+ // deployment probe as a supported Metergraph service. Before any read, the
20
+ // service's own workspace and capabilities documents must show exactly the
21
+ // Metadata scope, no content access, the expected deployment profile and,
22
+ // when one is pinned, the expected workspace. A broader key fails closed:
23
+ // there is no fallback to Debug or Replay access.
24
+
25
+ const TOKEN = new RegExp(`^[\\x21-\\x7e]{${CREDENTIAL_MIN_LENGTH},8192}$`);
26
+
27
+ const stop = (outcome, reason) => ({ ok: false, outcome, reason });
28
+
29
+ // Which key source this run has, or null. An explicit file wins over the
30
+ // environment variable.
31
+ export function agentKeySource(options, env) {
32
+ if (typeof options.agentTokenFile === "string") return { kind: "file", file: options.agentTokenFile };
33
+ if (typeof env[AGENT_TOKEN_ENV] === "string" && env[AGENT_TOKEN_ENV] !== "") return { kind: "env" };
34
+ return null;
35
+ }
36
+
37
+ // Where the key may be sent and what it must prove, before any request.
38
+ // A project binding pins the origin, workspace and profile; --url (or
39
+ // METERGRAPH_URL) and --workspace may repeat them but never contradict them.
40
+ // Without a binding, --workspace pins the workspace; without that, the
41
+ // key's own workspace is used and reported as unpinned.
42
+ // originConfirmed is true when this machine holds a saved sign in for the
43
+ // binding's origin, which proves the person chose that origin here.
44
+ export function keyTarget({ binding, options, env, originConfirmed = false }) {
45
+ let requested = options.origin ?? null;
46
+ let source = requested === null ? null : "flag";
47
+ const rawEnv = env[AGENT_URL_ENV];
48
+ if (requested === null && typeof rawEnv === "string" && rawEnv !== "") {
49
+ requested = parseOrigin(rawEnv);
50
+ if (requested === null) return stop("invalid_input", "metergraph_url_invalid");
51
+ source = "env";
52
+ }
53
+ const pinned = options.workspace ?? null;
54
+ if (binding !== null) {
55
+ if (requested !== null && requested !== binding.origin) return stop("conflict", "bound_to_other_origin");
56
+ // The binding is repository content, so it may come from anyone who can
57
+ // commit. A key is sent to the origin it names only when that is the
58
+ // hosted service, this machine signed in to it, or the person confirmed
59
+ // the same origin with --url or METERGRAPH_URL; otherwise a committed
60
+ // file could direct the key to another server.
61
+ if (requested === null && binding.origin !== DEFAULT_ORIGIN && !originConfirmed) {
62
+ return stop("conflict", "agent_key_origin_unconfirmed");
63
+ }
64
+ if (pinned !== null && pinned !== binding.workspace_id) return stop("conflict", "bound_to_other_workspace");
65
+ return {
66
+ ok: true,
67
+ origin: binding.origin,
68
+ workspaceId: binding.workspace_id,
69
+ profile: binding.deployment_profile,
70
+ originSource: "binding",
71
+ };
72
+ }
73
+ return {
74
+ ok: true,
75
+ origin: requested ?? DEFAULT_ORIGIN,
76
+ workspaceId: pinned,
77
+ profile: null,
78
+ originSource: source ?? "default",
79
+ };
80
+ }
81
+
82
+ // Resolves with the same shape as auth-session's sessionFor:
83
+ // { ok: true, session, documents, knownCredentials }
84
+ // { ok: false, outcome, reason }
85
+ // verifiedProfile is the profile the caller already read from GET
86
+ // /v1/deployment under the same deadline, or null to read it here.
87
+ export async function keySession({ target, source, env, signal, verifiedProfile = null }) {
88
+ // A key already in the environment is checked for shape before any
89
+ // request; a key file is opened only once the origin has proven itself.
90
+ let key = source.kind === "env" ? readAgentKey(source, env) : null;
91
+ if (key !== null && !key.ok) return key;
92
+ let profile = verifiedProfile;
93
+ if (profile === null) {
94
+ const deployment = checkDeployment(
95
+ await send(target.origin, "/v1/deployment", { signal, maxBytes: MAX_BODY_BYTES }),
96
+ );
97
+ if (deployment.outcome !== null) return stop(deployment.outcome, deployment.reason);
98
+ profile = deployment.profile;
99
+ }
100
+ if (target.profile !== null && profile !== target.profile) return stop("verification_failed", "profile_mismatch");
101
+
102
+ // Only now, with the origin proven to be a supported Metergraph service,
103
+ // is the key read.
104
+ if (key === null) key = readAgentKey(source, env);
105
+ if (!key.ok) return key;
106
+ const { token } = key;
107
+
108
+ const verified = await verifyContext(
109
+ target.origin,
110
+ token,
111
+ { profile, workspaceId: target.workspaceId, anyWorkspace: target.workspaceId === null },
112
+ signal,
113
+ );
114
+ if (!verified.ok) {
115
+ if (signal?.aborted) return stop("authorization_failed", "cancelled");
116
+ // 401: the key is unknown, expired or revoked. 403: the key is valid but
117
+ // lacks Metadata access to this workspace (for example an ingest key).
118
+ if (verified.denied && verified.status === 403) return stop("permission_denied", "agent_key_insufficient_scope");
119
+ if (verified.denied) return stop("login_required", "agent_key_rejected");
120
+ return stop(verified.outcome, verified.reason);
121
+ }
122
+ return {
123
+ ok: true,
124
+ session: {
125
+ origin: target.origin,
126
+ workspaceId: verified.workspaceId,
127
+ profile,
128
+ scopes: [METADATA_SCOPE],
129
+ accessToken: token,
130
+ },
131
+ documents: verified.documents,
132
+ // Internal only, for checking output before it is printed.
133
+ knownCredentials: [token],
134
+ };
135
+ }
136
+
137
+ // { ok: true, token } or { ok: false, outcome, reason } with a fixed reason.
138
+ // No path, value or system error text is ever returned.
139
+ export function readAgentKey(source, env) {
140
+ if (source.kind === "file") {
141
+ const read = readCredentialFile(source.file);
142
+ if (read.ok) return read;
143
+ // The file's own reasons, renamed so they are never mistaken for the
144
+ // private sign in store's reasons: credential_missing becomes
145
+ // agent_token_file_missing, and so on.
146
+ const reason = read.reason.startsWith("credential_") ? read.reason.slice("credential_".length) : read.reason;
147
+ return stop(read.outcome, read.reason === "platform_credential_handoff" ? read.reason : `agent_token_file_${reason}`);
148
+ }
149
+ let value = env[AGENT_TOKEN_ENV];
150
+ if (typeof value !== "string") return stop("login_required", "agent_token_missing");
151
+ if (value.endsWith("\r\n")) value = value.slice(0, -2);
152
+ else if (value.endsWith("\n")) value = value.slice(0, -1);
153
+ // One token only: whitespace means a label such as "Bearer", a second
154
+ // token or a second line.
155
+ if (!TOKEN.test(value)) return stop("invalid_input", "agent_token_malformed");
156
+ return { ok: true, token: value };
157
+ }
@@ -19,6 +19,11 @@ import { envelope, toJsonLine } from "./output.js";
19
19
  const BIN = fileURLToPath(new URL("../bin/metergraph.js", import.meta.url));
20
20
  const WAITER = "METERGRAPH_APPROVAL_WAITER";
21
21
 
22
+ // True in the detached waiter that handOff starts.
23
+ export function isApprovalWaiter(env = process.env) {
24
+ return env[WAITER] === "1";
25
+ }
26
+
22
27
  export function handsOff(argv, env = process.env) {
23
28
  if (env[WAITER] === "1") return false;
24
29
  const parsed = parseArgs(argv);