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.
- package/README.md +12 -7
- package/assets/skill/SKILL.md +52 -3
- package/assets/skill/manifest.json +3 -3
- package/assets/skills/manifest.json +13 -13
- package/assets/skills/metergraph-analyze/SKILL.md +62 -4
- package/assets/skills/metergraph-investigate/SKILL.md +222 -35
- package/assets/skills/metergraph-onboarding/SKILL.md +7 -0
- package/assets/skills/metergraph-report/SKILL.md +19 -2
- package/package.json +1 -1
- package/src/agent-key.js +157 -0
- package/src/approval-handoff.js +5 -0
- package/src/args.js +373 -14
- package/src/auth-browser.js +5 -19
- package/src/auth-callback.js +24 -15
- package/src/auth-login.js +55 -12
- package/src/auth-oauth.js +20 -3
- package/src/callback-page.js +58 -0
- package/src/cli.js +94 -37
- package/src/constants.js +32 -3
- package/src/environment.js +169 -0
- package/src/origin.js +6 -0
- package/src/output.js +325 -130
- package/src/read-contract.js +953 -13
- package/src/read-output.js +449 -30
- package/src/read.js +297 -59
- package/src/setup-deployment.js +4 -3
- package/src/setup-refusal.js +172 -0
- package/src/setup-state.js +4 -2
- package/src/setup.js +227 -53
- package/src/skill-bundle.js +2 -2
- package/src/style.js +101 -0
- package/src/transport.js +8 -4
- package/src/usage-aggregates.js +197 -0
- package/src/verify-output.js +21 -11
|
@@ -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
|
|
22
|
-
|
|
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.
|
|
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": {
|
package/src/agent-key.js
ADDED
|
@@ -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
|
+
}
|
package/src/approval-handoff.js
CHANGED
|
@@ -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);
|