metergraph-cli 0.2.3 → 0.2.5
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 +78 -14
- package/assets/skill/SKILL.md +124 -31
- package/assets/skill/manifest.json +3 -3
- package/assets/skills/manifest.json +37 -31
- package/assets/skills/metergraph-analyze/SKILL.md +161 -58
- package/assets/skills/metergraph-candidates/SKILL.md +50 -8
- package/assets/skills/metergraph-evals/SKILL.md +83 -16
- package/assets/skills/metergraph-investigate/SKILL.md +90 -34
- package/assets/skills/metergraph-iterate/SKILL.md +82 -12
- package/assets/skills/metergraph-local-bundle/SKILL.md +146 -128
- package/assets/skills/metergraph-model-swap/SKILL.md +142 -20
- package/assets/skills/metergraph-onboarding/SKILL.md +93 -49
- package/assets/skills/metergraph-price-check/SKILL.md +219 -0
- package/assets/skills/metergraph-report/SKILL.md +60 -15
- package/assets/skills/metergraph-workloads/SKILL.md +97 -13
- package/native/README.md +85 -0
- package/native/anchored-fs.c +243 -0
- package/native/bin/anchored-fs-darwin-arm64 +0 -0
- package/native/bin/anchored-fs-darwin-x64 +0 -0
- package/native/bin/anchored-fs-linux-arm64 +0 -0
- package/native/bin/anchored-fs-linux-x64 +0 -0
- package/native/build.mjs +55 -0
- package/native/manifest.json +9 -0
- package/native/targets.mjs +24 -0
- package/package.json +10 -3
- package/src/action-help.js +130 -0
- package/src/agent-key.js +21 -14
- package/src/anchored-fs.js +163 -0
- package/src/args.js +125 -17
- package/src/auth-binding.js +12 -7
- package/src/auth-callback.js +10 -2
- package/src/auth-login.js +111 -28
- package/src/auth-oauth.js +89 -45
- package/src/auth-session.js +19 -12
- package/src/auth-store.js +28 -7
- package/src/cli.js +52 -8
- package/src/constants.js +37 -9
- package/src/experiments-output.js +7 -6
- package/src/experiments.js +26 -17
- package/src/output.js +274 -79
- package/src/read-contract.js +42 -8
- package/src/read-output.js +66 -13
- package/src/read.js +90 -15
- package/src/repository-identity.js +2 -2
- package/src/setup-env-git.js +1 -1
- package/src/setup-env.js +16 -9
- package/src/setup-interactive.js +19 -4
- package/src/setup-refusal.js +2 -2
- package/src/setup-state.js +9 -4
- package/src/setup.js +15 -3
- package/src/skill-bundle.js +2 -2
- package/src/skill-trusted.js +56 -0
- package/src/skill.js +28 -9
- package/src/style.js +33 -7
- package/src/tools-output.js +73 -0
- package/src/tools.js +249 -0
- package/src/trace-contract.js +1 -1
- package/src/transport.js +21 -0
- package/src/verify-output.js +2 -2
- package/src/verify.js +5 -2
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// Human action help; command execution and JSON envelopes remain independent.
|
|
2
|
+
const option = (name, value, summary) => ({ name, value, summary });
|
|
3
|
+
const selectors = [
|
|
4
|
+
option("--model", "ID", "Model ID."),
|
|
5
|
+
option("--provider", "ID", "Provider; use instead of --channel."),
|
|
6
|
+
option("--channel", "ID", "Billing channel; use instead of --provider."),
|
|
7
|
+
option("--region", "ID", "Billing region."),
|
|
8
|
+
];
|
|
9
|
+
const pricingCommon = [
|
|
10
|
+
option("--url", "ORIGIN", "Service origin. Default: https://app.metergraph.dev."),
|
|
11
|
+
option("--timeout-ms", "N", "Request deadline, 100–30000 ms. Default: 10000."),
|
|
12
|
+
option("--api", "API", "public (default) or reference. Reference requires a loopback origin."),
|
|
13
|
+
option("--json", null, "Print JSON."),
|
|
14
|
+
];
|
|
15
|
+
const tokens = ["input", "output", "cache-read", "cache-write", "cache-write-5m", "cache-write-1h"].map(
|
|
16
|
+
name => option(`--${name}-tokens`, "N", name.startsWith("cache-write")
|
|
17
|
+
? "Anthropic cache-write tokens."
|
|
18
|
+
: "Token count. Omitted counts stay unknown."));
|
|
19
|
+
const pricing = {
|
|
20
|
+
models: ["Discover available models", "[--provider ID | --channel ID] [--limit N]", "metergraph pricing models --limit 10",
|
|
21
|
+
[...selectors, option("--limit", "N", "Models to show. Default: 20; max: 100.")],
|
|
22
|
+
"Current catalog. No account required."],
|
|
23
|
+
lookup: ["Look up a model's prices", "--model ID [--provider ID | --channel ID]", "metergraph pricing lookup --model gpt-5 --provider openai",
|
|
24
|
+
[...selectors, option("--at", "YYYY-MM-DD", "Price date (UTC). Default: now.")],
|
|
25
|
+
"Public catalog prices. No account required."],
|
|
26
|
+
calculate: ["Estimate cost from token counts", "--model ID [--input-tokens N] [--output-tokens N]", "metergraph pricing calculate --model gpt-5 --provider openai --input-tokens 1000 --output-tokens 200",
|
|
27
|
+
[...selectors, ...tokens, option("--batch", null, "Use batch rates."),
|
|
28
|
+
option("--at", "YYYY-MM-DD", "Price date (UTC). Default: now."),
|
|
29
|
+
option("--trace-format", "FORMAT", "Token format: anthropic.messages, openai.responses, openai.chat_completions or otel.gen_ai.")],
|
|
30
|
+
"Only supplied counts are sent. Partial costs are known subtotals. OpenAI cache reads cannot exceed total input. No model calls are made."],
|
|
31
|
+
market: ["Compare current rates on one billing channel", "--channel ID [--region ID] [--limit N]", "metergraph pricing market --channel openai-api --limit 10",
|
|
32
|
+
selectors.filter(o => ["--channel", "--region"].includes(o.name)).concat(option("--limit", "N", "Models to show. Default: 20; max: 100.")),
|
|
33
|
+
"Current prices, without historical or quality rankings. No account required."],
|
|
34
|
+
history: ["Inspect historical catalog prices", "--model ID --channel ID --api reference --url ORIGIN", "metergraph pricing history --model openai/gpt-5.6-terra --channel openai-api --api reference --url http://127.0.0.1:8765 --format chart",
|
|
35
|
+
selectors.filter(o => o.name !== "--provider").concat([
|
|
36
|
+
option("--api", "reference", "Required. History currently uses the local reference adapter."),
|
|
37
|
+
option("--at", "YYYY-MM-DD", "Reference lookup date at UTC midnight."),
|
|
38
|
+
option("--format", "FORMAT", "chart (default), table or html. Cannot combine with --json."),
|
|
39
|
+
]), "Requires a loopback reference service. Historical data is not available from the public API."],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export function actionHelpData(topic, action, commands) {
|
|
43
|
+
if (topic === "pricing") {
|
|
44
|
+
const canonical = action === "cost" ? "calculate" : action;
|
|
45
|
+
const entry = pricing[canonical];
|
|
46
|
+
if (!entry) return null;
|
|
47
|
+
const [summary, usage, example, options, note] = entry;
|
|
48
|
+
const focused = options.map(o => {
|
|
49
|
+
if (o.name === "--model") return { ...o, summary: canonical === "models" ? "Filter by model ID." : "Required. Model ID." };
|
|
50
|
+
if (o.name === "--channel" && ["market", "history"].includes(canonical)) return { ...o, summary: "Required. Billing channel." };
|
|
51
|
+
if (o.name === "--region" && ["market", "history"].includes(canonical)) return { ...o, summary: "Billing region. Default: global." };
|
|
52
|
+
return o;
|
|
53
|
+
});
|
|
54
|
+
return { summary, usage: `metergraph pricing ${action} ${usage} [options]`,
|
|
55
|
+
options: [...focused, ...pricingCommon.filter(o => !options.some(specific => specific.name === o.name)
|
|
56
|
+
&& !(o.name === "--api" && ["models", "calculate"].includes(action)))], examples: [example.replace("pricing calculate", `pricing ${action}`)], note };
|
|
57
|
+
}
|
|
58
|
+
if (topic === "experiments") {
|
|
59
|
+
const entry = commands.find(c => c.name === topic);
|
|
60
|
+
const names = { list: ["--workload", "--limit"], get: ["--id"],
|
|
61
|
+
estimate: ["--workload", "--treatment", "--last-n", "--sample-model", "--track"],
|
|
62
|
+
start: ["--workload", "--treatment", "--last-n", "--sample-model", "--track"] }[action];
|
|
63
|
+
if (!names) return null;
|
|
64
|
+
const specific = ["--workload", "--limit", "--id", "--treatment", "--last-n", "--sample-model", "--track"];
|
|
65
|
+
const options = entry.options.filter(o => !specific.includes(o.name) || names.includes(o.name)).map(o => {
|
|
66
|
+
if (o.name === "--agent-token-file") return { ...o, summary: action === "start"
|
|
67
|
+
? "Private (0600) Debug key file with Allow analysis starts. Or set METERGRAPH_AGENT_TOKEN."
|
|
68
|
+
: "Private (0600) Metadata key file. Or use saved sign in or METERGRAPH_AGENT_TOKEN." };
|
|
69
|
+
const summaries = {
|
|
70
|
+
"--workload": action === "list" ? "Filter by workload ID." : "Required. Workload ID.",
|
|
71
|
+
"--id": "Required. Experiment ID.", "--limit": "Experiments to show. Default: 20; max: 50.",
|
|
72
|
+
"--treatment": "Required. PROVIDER/MODEL; repeat for up to 3 treatments.",
|
|
73
|
+
"--last-n": "Calls to replay. Default: 20; range: 10–40.",
|
|
74
|
+
"--sample-model": "Filter production calls by model.", "--track": "JSON path to compare; repeat up to 10 times.",
|
|
75
|
+
};
|
|
76
|
+
return { ...o, summary: summaries[o.name] ?? o.summary };
|
|
77
|
+
});
|
|
78
|
+
const invocation = { list: "[--workload ID] [--limit N]", get: "--id UUID",
|
|
79
|
+
estimate: "--workload ID --treatment PROVIDER/MODEL", start: "--workload ID --treatment PROVIDER/MODEL" }[action];
|
|
80
|
+
return { summary: { list: "List recent experiments", get: "Read an experiment's results",
|
|
81
|
+
estimate: "Estimate a model comparison before starting it", start: "Start a budgeted model comparison" }[action],
|
|
82
|
+
usage: `metergraph experiments ${action} ${invocation} [options]`, options,
|
|
83
|
+
examples: [`metergraph experiments ${action} ${{ list: "--limit 10", get: "--id UUID", estimate: "--workload ROUTE --treatment PROVIDER/MODEL", start: "--workload ROUTE --treatment PROVIDER/MODEL --agent-token-file /path/to/private-key" }[action]}`],
|
|
84
|
+
note: action === "start"
|
|
85
|
+
? "Replays captured production prompts on the selected providers and incurs provider costs. Requires a Debug key with Allow analysis starts. Estimate first. Each start creates a new experiment; never retry a successful start."
|
|
86
|
+
: { list: "Uses Metadata access. Returns experiment summaries.",
|
|
87
|
+
get: "Uses Metadata access. Returns results without prompts or responses.",
|
|
88
|
+
estimate: "Uses Metadata access. Estimates provider cost without starting a replay." }[action] };
|
|
89
|
+
}
|
|
90
|
+
const entry = commands.find(c => c.name === `${topic} ${action}`);
|
|
91
|
+
if (!entry) return null;
|
|
92
|
+
const label = topic === "skill" ? "setup skill" : "workflow skills";
|
|
93
|
+
return { summary: action === "list" ? "List installed workflow skills"
|
|
94
|
+
: `${action === "install" ? "Install" : "Update"} the Metergraph ${label}`,
|
|
95
|
+
usage: `metergraph ${topic} ${action}${action === "list" ? "" : " --client codex|claude|cursor"} [options]`,
|
|
96
|
+
options: entry.options, examples: [`metergraph ${topic} ${action}${action === "list" ? "" : " --client codex"}`],
|
|
97
|
+
note: action === "list" ? "Lists installed workflow skills for all clients. Makes no changes."
|
|
98
|
+
: (action === "install" ? "Existing skills are left alone. No network requests." : "Replaces only unchanged skills owned by this CLI. No network requests.") };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Connection details remain available offline without dominating everyday help.
|
|
102
|
+
const ADVANCED_CONNECTION = new Set(["--url", "--config-dir", "--workspace", "--agent-token-file", "--timeout-ms", "--api"]);
|
|
103
|
+
const SHORT_OPTIONS = {
|
|
104
|
+
"--project": "Project directory. Default: current directory.",
|
|
105
|
+
"--config-dir": "Credential directory. Default: per-user config directory.",
|
|
106
|
+
"--url": "Service origin. Default: project binding or hosted service.",
|
|
107
|
+
"--workspace": "Require this workspace ID.",
|
|
108
|
+
"--json": "Print JSON.",
|
|
109
|
+
"--client": "Agent: codex, claude or cursor.",
|
|
110
|
+
"--runtime": "local, cloud or cloud-no-shell. Inferred in a local terminal.",
|
|
111
|
+
"--skip-skill": "Skip agent skill installation.",
|
|
112
|
+
"--env-file": "Private env file. Default: .env.",
|
|
113
|
+
"--no-browser": "Print the approval link instead of opening it.",
|
|
114
|
+
"--signup": "Open sign up during browser approval.",
|
|
115
|
+
"--reconnect": "Allow switching the connected workspace.",
|
|
116
|
+
"--repair": "Approve replacement of a key that no longer verifies.",
|
|
117
|
+
"--deployment": "managed (default), customer-local, byoc or oss.",
|
|
118
|
+
"--confirm-prerequisites": "Confirm operator prerequisites are met.",
|
|
119
|
+
"--repository": "Repository identity as OWNER/NAME.",
|
|
120
|
+
"--no-repository": "Leave repository identity unset.",
|
|
121
|
+
};
|
|
122
|
+
export function displayOptions(options, topic, action, advanced = false) {
|
|
123
|
+
return options.filter(o => advanced || !ADVANCED_CONNECTION.has(o.name)
|
|
124
|
+
// History cannot run without these explicit connection selectors.
|
|
125
|
+
|| (topic === "pricing" && action === "history" && ["--api", "--url"].includes(o.name)))
|
|
126
|
+
.map(o => ({ ...o, summary: advanced ? o.summary
|
|
127
|
+
: o.name === "--runtime" && ["skill", "skills"].includes(topic) ? "local (default) or cloud."
|
|
128
|
+
: o.name === "--url" && topic === "pricing" ? o.summary
|
|
129
|
+
: SHORT_OPTIONS[o.name] ?? o.summary }));
|
|
130
|
+
}
|
package/src/agent-key.js
CHANGED
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { verifyContext } from "./auth-oauth.js";
|
|
2
|
-
import { DEFAULT_ORIGIN, MAX_BODY_BYTES
|
|
2
|
+
import { DEFAULT_ORIGIN, MAX_BODY_BYTES } from "./constants.js";
|
|
3
3
|
import { CREDENTIAL_MIN_LENGTH, readCredentialFile } from "./deployment-credential.js";
|
|
4
4
|
import { checkDeployment } from "./doctor.js";
|
|
5
5
|
import { AGENT_TOKEN_ENV, AGENT_URL_ENV } from "./environment.js";
|
|
6
6
|
import { parseOrigin } from "./origin.js";
|
|
7
7
|
import { send } from "./transport.js";
|
|
8
8
|
|
|
9
|
-
// The read commands' second credential: a coding-agent key
|
|
10
|
-
//
|
|
11
|
-
// environment as a secret. It
|
|
12
|
-
//
|
|
9
|
+
// The read commands' second credential: a coding-agent key that a person
|
|
10
|
+
// created on the Keys page (or with a bundle's bin/agent-key) and handed to
|
|
11
|
+
// the agent's environment as a secret. It has the Metadata role, or the
|
|
12
|
+
// Debug role with workflow permissions. It is how agents read where browser
|
|
13
|
+
// sign in cannot finish (SSH, cloud workspaces, CI, some containers), and how
|
|
14
|
+
// they get the Debug role and permissions.
|
|
13
15
|
//
|
|
14
16
|
// The key comes only from the METERGRAPH_AGENT_TOKEN environment variable or
|
|
15
17
|
// from a private file named by --agent-token-file (checked as setup checks
|
|
@@ -17,10 +19,11 @@ import { send } from "./transport.js";
|
|
|
17
19
|
// written anywhere, never printed, and sent only as a bearer header to the
|
|
18
20
|
// chosen origin, and only after that origin has answered the unauthenticated
|
|
19
21
|
// deployment probe as a supported Metergraph service. Before any read, the
|
|
20
|
-
// service's own workspace and capabilities documents must show
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
22
|
+
// service's own workspace and capabilities documents must show Metadata, or
|
|
23
|
+
// Debug with workflow permissions, the expected deployment profile and, when
|
|
24
|
+
// one is pinned, the expected workspace. A Replay key, or one with any other
|
|
25
|
+
// scope, fails closed. The read commands send only Metadata requests,
|
|
26
|
+
// whichever key they use.
|
|
24
27
|
|
|
25
28
|
const TOKEN = new RegExp(`^[\\x21-\\x7e]{${CREDENTIAL_MIN_LENGTH},8192}$`);
|
|
26
29
|
|
|
@@ -66,6 +69,8 @@ export function keyTarget({ binding, options, env, originConfirmed = false }) {
|
|
|
66
69
|
ok: true,
|
|
67
70
|
origin: binding.origin,
|
|
68
71
|
workspaceId: binding.workspace_id,
|
|
72
|
+
// A slug or name to match against the verified workspace.
|
|
73
|
+
workspaceText: options.workspaceText ?? null,
|
|
69
74
|
profile: binding.deployment_profile,
|
|
70
75
|
originSource: "binding",
|
|
71
76
|
};
|
|
@@ -74,6 +79,7 @@ export function keyTarget({ binding, options, env, originConfirmed = false }) {
|
|
|
74
79
|
ok: true,
|
|
75
80
|
origin: requested ?? DEFAULT_ORIGIN,
|
|
76
81
|
workspaceId: pinned,
|
|
82
|
+
workspaceText: options.workspaceText ?? null,
|
|
77
83
|
profile: null,
|
|
78
84
|
originSource: source ?? "default",
|
|
79
85
|
};
|
|
@@ -84,10 +90,10 @@ export function keyTarget({ binding, options, env, originConfirmed = false }) {
|
|
|
84
90
|
// { ok: false, outcome, reason }
|
|
85
91
|
// verifiedProfile is the profile the caller already read from GET
|
|
86
92
|
// /v1/deployment under the same deadline, or null to read it here.
|
|
87
|
-
// access is "
|
|
88
|
-
// experiments start, which needs Debug access and the start grant
|
|
89
|
-
// (verifyContext)
|
|
90
|
-
export async function keySession({ target, source, env, signal, verifiedProfile = null, access = "
|
|
93
|
+
// access is "read" for the read commands, or "experiment_start" for
|
|
94
|
+
// experiments start, which needs Debug access and the start grant
|
|
95
|
+
// (verifyContext).
|
|
96
|
+
export async function keySession({ target, source, env, signal, verifiedProfile = null, access = "read" }) {
|
|
91
97
|
// A key already in the environment is checked for shape before any
|
|
92
98
|
// request; a key file is opened only once the origin has proven itself.
|
|
93
99
|
let key = source.kind === "env" ? readAgentKey(source, env) : null;
|
|
@@ -128,7 +134,8 @@ export async function keySession({ target, source, env, signal, verifiedProfile
|
|
|
128
134
|
origin: target.origin,
|
|
129
135
|
workspaceId: verified.workspaceId,
|
|
130
136
|
profile,
|
|
131
|
-
|
|
137
|
+
access: verified.access,
|
|
138
|
+
scopes: verified.scopes,
|
|
132
139
|
accessToken: token,
|
|
133
140
|
},
|
|
134
141
|
documents: verified.documents,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
7
|
+
|
|
8
|
+
// A process-local directory capability service. The shipped helper owns all
|
|
9
|
+
// descriptors; only its inherited anonymous pipes carry requests and contents.
|
|
10
|
+
// An absent or unverified backend is an error, never a pathname-write fallback.
|
|
11
|
+
const MAX = 2 * 1024 * 1024;
|
|
12
|
+
const base = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../native");
|
|
13
|
+
const errnoNames = new Map(Object.entries(os.constants.errno).map(([name, number]) => [number, name]));
|
|
14
|
+
let service = null;
|
|
15
|
+
let rollbackDepth = 0;
|
|
16
|
+
const aliases = new Map(process.platform === "darwin" ? [["/var", "/private/var"], ["/tmp", "/private/tmp"]] : []);
|
|
17
|
+
function failure(code) { const error = new Error(`Secure filesystem operation failed (${code}).`); error.code = code; return error; }
|
|
18
|
+
function verifiedHelper() {
|
|
19
|
+
const name = `anchored-fs-${process.platform}-${process.arch}`;
|
|
20
|
+
let file;
|
|
21
|
+
try {
|
|
22
|
+
const manifest = JSON.parse(fs.readFileSync(path.join(base, "manifest.json"), "utf8"));
|
|
23
|
+
file = path.join(base, "bin", name);
|
|
24
|
+
const bytes = fs.readFileSync(file);
|
|
25
|
+
if (manifest.schema !== 1 || typeof manifest.artifacts[name] !== "string" ||
|
|
26
|
+
createHash("sha256").update(bytes).digest("hex") !== manifest.artifacts[name]) throw failure("ENOTSUP");
|
|
27
|
+
} catch { throw failure("ENOTSUP"); }
|
|
28
|
+
return file;
|
|
29
|
+
}
|
|
30
|
+
// Test and diagnostic capability discovery verifies the same packaged bytes as
|
|
31
|
+
// start(). It never enables a platform merely because its name is recognised.
|
|
32
|
+
export function hasVerifiedHelper() {
|
|
33
|
+
try { verifiedHelper(); return true; } catch { return false; }
|
|
34
|
+
}
|
|
35
|
+
function start() {
|
|
36
|
+
if (service !== null) return service;
|
|
37
|
+
const file = verifiedHelper();
|
|
38
|
+
const child = spawn(file, [], { stdio: ["pipe", "pipe", "ignore"], windowsHide: true, env: {} });
|
|
39
|
+
child.on("error", () => { /* synchronous pipe operations report backend loss */ });
|
|
40
|
+
const input = child.stdin?._handle;
|
|
41
|
+
const output = child.stdout?._handle;
|
|
42
|
+
if (!input || !output || typeof input.setBlocking !== "function" || typeof output.setBlocking !== "function") {
|
|
43
|
+
child.kill("SIGKILL"); throw failure("ENOTSUP");
|
|
44
|
+
}
|
|
45
|
+
input.setBlocking(true); output.setBlocking(true);
|
|
46
|
+
service = { child, input: input.fd, output: output.fd };
|
|
47
|
+
child.unref(); child.stdin.unref(); child.stdout.unref();
|
|
48
|
+
return service;
|
|
49
|
+
}
|
|
50
|
+
function writeAll(fd, bytes) { let offset = 0; while (offset < bytes.length) { const count = fs.writeSync(fd, bytes, offset, bytes.length - offset); if (count === 0) throw failure("EIO"); offset += count; } }
|
|
51
|
+
function readAll(fd, bytes) { let offset = 0; while (offset < bytes.length) { const count = fs.readSync(fd, bytes, offset, bytes.length - offset, null); if (count === 0) throw failure("EIO"); offset += count; } }
|
|
52
|
+
function request(operation, args = [], payload = Buffer.alloc(0)) {
|
|
53
|
+
const encoded = args.map(value => Buffer.from(String(value)));
|
|
54
|
+
if (encoded.some(value => value.includes(0))) throw failure("EINVAL");
|
|
55
|
+
const size = 2 + encoded.reduce((n, value) => n + 4 + value.length, 0) + payload.length;
|
|
56
|
+
if (size > MAX) throw failure("EFBIG");
|
|
57
|
+
const body = Buffer.alloc(4 + size); body.writeUInt32BE(size); body[4] = operation; body[5] = args.length;
|
|
58
|
+
let offset = 6;
|
|
59
|
+
for (const value of encoded) { body.writeUInt32BE(value.length, offset); offset += 4; value.copy(body, offset); offset += value.length; }
|
|
60
|
+
payload.copy(body, offset);
|
|
61
|
+
const pipe = start(); writeAll(pipe.input, body);
|
|
62
|
+
const header = Buffer.alloc(8); readAll(pipe.output, header);
|
|
63
|
+
const error = header.readUInt32BE(); const length = header.readUInt32BE(4);
|
|
64
|
+
if (length > MAX) throw failure("EIO");
|
|
65
|
+
const result = Buffer.alloc(length); readAll(pipe.output, result);
|
|
66
|
+
if (error) throw failure(errnoNames.get(error) ?? "EIO");
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
function canonical(value) {
|
|
70
|
+
if (typeof value !== "string" || value.includes("\0")) throw failure("EINVAL");
|
|
71
|
+
let absolute = path.resolve(value);
|
|
72
|
+
const choices = [...aliases].sort((a,b) => b[0].length - a[0].length);
|
|
73
|
+
for (const [alias, target] of choices) if (absolute === alias || absolute.startsWith(`${alias}/`)) { absolute = target + absolute.slice(alias.length); break; }
|
|
74
|
+
return absolute;
|
|
75
|
+
}
|
|
76
|
+
function descriptor(value) { if (!Number.isSafeInteger(value) || value > -1001) throw failure("EBADF"); return -value - 1001; }
|
|
77
|
+
function statResult(bytes, options) {
|
|
78
|
+
if (bytes.length !== 88) throw failure("EIO");
|
|
79
|
+
const keys = ["dev", "ino", "mode", "nlink", "uid", "gid", "size"];
|
|
80
|
+
const result = {};
|
|
81
|
+
keys.forEach((key, index) => { const n = bytes.readBigUInt64BE(index*8); result[key] = options?.bigint ? n : Number(n); });
|
|
82
|
+
for (const [name, index] of [["mtime", 7], ["ctime", 9]]) {
|
|
83
|
+
const seconds = bytes.readBigInt64BE(index * 8);
|
|
84
|
+
const nanoseconds = bytes.readBigUInt64BE((index + 1) * 8);
|
|
85
|
+
if (nanoseconds >= 1000000000n) throw failure("EIO");
|
|
86
|
+
const ns = seconds * 1000000000n + nanoseconds;
|
|
87
|
+
result[`${name}Ms`] = options?.bigint ? ns / 1000000n : Number(seconds) * 1000 + Number(nanoseconds) / 1000000;
|
|
88
|
+
if (options?.bigint) result[`${name}Ns`] = ns;
|
|
89
|
+
result[name] = new Date(Number(ns / 1000000n));
|
|
90
|
+
}
|
|
91
|
+
const mode = Number(result.mode) & 0o170000;
|
|
92
|
+
return Object.assign(result, { isFile: () => mode === 0o100000, isDirectory: () => mode === 0o040000,
|
|
93
|
+
isSymbolicLink: () => mode === 0o120000, isFIFO: () => mode === 0o010000,
|
|
94
|
+
isSocket: () => mode === 0o140000, isBlockDevice: () => mode === 0o060000, isCharacterDevice: () => mode === 0o020000 });
|
|
95
|
+
}
|
|
96
|
+
export function validatePath(value) { request(13, [canonical(value)]); return true; }
|
|
97
|
+
export function withRollback(callback) {
|
|
98
|
+
if (rollbackDepth++ === 0) request(14, [1]);
|
|
99
|
+
try { return callback(); } finally { if (--rollbackDepth === 0) request(14, [0]); }
|
|
100
|
+
}
|
|
101
|
+
export function closeAnchoredFs() {
|
|
102
|
+
if (service !== null) { service.child.stdin.destroy(); service.child.stdout.destroy(); service.child.kill("SIGKILL"); service = null; }
|
|
103
|
+
}
|
|
104
|
+
const anchored = {
|
|
105
|
+
constants: fs.constants,
|
|
106
|
+
lstatSync(value, options) { return statResult(request(1, [canonical(value)]), options); },
|
|
107
|
+
statSync(value, options) { const result = this.lstatSync(value, options); if (result.isSymbolicLink()) throw failure("ELOOP"); return result; },
|
|
108
|
+
realpathSync(value) {
|
|
109
|
+
// Root selection is read-only. Pin the resolved identity immediately; every
|
|
110
|
+
// subsequent operation uses the helper's retained directory descriptors.
|
|
111
|
+
const absolute = path.resolve(value); const real = fs.realpathSync(absolute);
|
|
112
|
+
const prior = aliases.get(absolute);
|
|
113
|
+
if (prior !== undefined && prior !== real) throw failure("ESTALE");
|
|
114
|
+
// Freeze both the selected spelling and the capability spelling. A later
|
|
115
|
+
// root resolution cannot rebind an existing capability to another tree.
|
|
116
|
+
const pinned = aliases.get(real);
|
|
117
|
+
if (pinned !== undefined && pinned !== real) throw failure("ESTALE");
|
|
118
|
+
validatePath(real); aliases.set(real, real); aliases.set(absolute, real); return real;
|
|
119
|
+
},
|
|
120
|
+
existsSync(value) { try { this.lstatSync(value); return true; } catch (error) { if (["ENOENT", "ENOTDIR"].includes(error.code)) return false; throw error; } },
|
|
121
|
+
openSync(value, flags, mode = 0o666) {
|
|
122
|
+
let kind;
|
|
123
|
+
const extras = (fs.constants.O_NOFOLLOW ?? 0) | (fs.constants.O_NONBLOCK ?? 0);
|
|
124
|
+
const stripped = typeof flags === "number" ? flags & ~extras : flags;
|
|
125
|
+
if (flags === "r" || stripped === fs.constants.O_RDONLY) kind = 0;
|
|
126
|
+
else if (flags === "r+" || stripped === fs.constants.O_RDWR) kind = 1;
|
|
127
|
+
else if (flags === "wx" || flags === "wx+" || stripped === (fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL)) kind = 2;
|
|
128
|
+
else throw failure("ENOTSUP");
|
|
129
|
+
return -1001 - request(2, [canonical(value), kind, mode]).readUInt32BE();
|
|
130
|
+
},
|
|
131
|
+
fstatSync(fd, options) { return statResult(request(3, [descriptor(fd)]), options); },
|
|
132
|
+
readFileSync(value, options) {
|
|
133
|
+
const own = typeof value !== "number"; const fd = own ? this.openSync(value, "r") : value;
|
|
134
|
+
try { const bytes = request(4, [descriptor(fd)]); const encoding = typeof options === "string" ? options : options?.encoding; return encoding ? bytes.toString(encoding) : bytes; }
|
|
135
|
+
finally { if (own) this.closeSync(fd); }
|
|
136
|
+
},
|
|
137
|
+
readSync(fd, buffer, offset, length, position) {
|
|
138
|
+
const bytes = request(16, [descriptor(fd), length, position ?? 0]); bytes.copy(buffer, offset); return bytes.length;
|
|
139
|
+
},
|
|
140
|
+
writeFileSync(fd, value, options) {
|
|
141
|
+
if (typeof fd !== "number") throw failure("ENOTSUP");
|
|
142
|
+
const encoding = typeof options === "string" ? options : options?.encoding;
|
|
143
|
+
request(5, [descriptor(fd)], Buffer.isBuffer(value) ? value : Buffer.from(value, encoding));
|
|
144
|
+
},
|
|
145
|
+
fchmodSync(fd, mode) { request(6, [descriptor(fd), mode]); },
|
|
146
|
+
chmodSync(value, mode) { const fd = this.openSync(value, "r"); try { this.fchmodSync(fd, mode); } finally { this.closeSync(fd); } },
|
|
147
|
+
fsyncSync(fd) { request(7, [descriptor(fd)]); },
|
|
148
|
+
closeSync(fd) { request(8, [descriptor(fd)]); },
|
|
149
|
+
mkdirSync(value, options = {}) {
|
|
150
|
+
const absolute = canonical(value); const mode = typeof options === "number" ? options : options.mode ?? 0o777;
|
|
151
|
+
try { request(9, [absolute, mode]); }
|
|
152
|
+
catch (error) {
|
|
153
|
+
if (!options.recursive) throw error;
|
|
154
|
+
if (error.code === "ENOENT") { this.mkdirSync(path.dirname(absolute), {recursive:true, mode}); request(9, [absolute, mode]); }
|
|
155
|
+
else if (error.code !== "EEXIST" || !this.statSync(absolute).isDirectory()) throw error;
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
renameSync(from, to) { request(10, [canonical(from), canonical(to)]); },
|
|
159
|
+
unlinkSync(value) { request(11, [canonical(value)]); },
|
|
160
|
+
rmdirSync(value) { request(12, [canonical(value)]); },
|
|
161
|
+
linkSync(from, to) { request(15, [canonical(from), canonical(to)]); },
|
|
162
|
+
};
|
|
163
|
+
export default anchored;
|
package/src/args.js
CHANGED
|
@@ -39,13 +39,24 @@ import {
|
|
|
39
39
|
USAGE_DIMENSIONS,
|
|
40
40
|
USAGE_MAX_FILTER_VALUES,
|
|
41
41
|
USAGE_SORTS,
|
|
42
|
+
WORKFLOW_PERMISSIONS,
|
|
42
43
|
} from "./constants.js";
|
|
43
44
|
import { parseOrigin } from "./origin.js";
|
|
44
45
|
import { groupedRequest, hasUnsafeText, isCursor, isSafeFilter, readQuery } from "./read-contract.js";
|
|
46
|
+
import { TOOL_ARGS_MAX_BYTES, TOOL_NAME } from "./tools.js";
|
|
45
47
|
|
|
46
48
|
export const READ_COMMANDS = Object.freeze(["status", "context", "capabilities", "usage", "routes", "traces"]);
|
|
47
|
-
const COMMANDS = new Set(["pricing", "doctor", "help", "skill", "skills", "login", "logout", "setup", "verify", "experiments", ...READ_COMMANDS]);
|
|
48
|
-
const HELP_TOPICS = new Set(["pricing", "doctor", "skill", "skills", "login", "logout", "setup", "verify", "experiments", ...READ_COMMANDS]);
|
|
49
|
+
const COMMANDS = new Set(["pricing", "doctor", "help", "skill", "skills", "login", "logout", "setup", "verify", "experiments", "tools", ...READ_COMMANDS]);
|
|
50
|
+
const HELP_TOPICS = new Set(["agents", "exit-codes", "pricing", "doctor", "skill", "skills", "login", "logout", "setup", "verify", "experiments", "tools", ...READ_COMMANDS]);
|
|
51
|
+
export const TOOLS_ACTIONS = Object.freeze(["list", "call"]);
|
|
52
|
+
// The roles login asks for: Metadata, or Debug, to which --allow-* adds the
|
|
53
|
+
// workflow permissions a workspace owner approves. Replay is never asked for.
|
|
54
|
+
export const LOGIN_ROLES = Object.freeze(["metadata", "debug"]);
|
|
55
|
+
// Each --allow-* switch and the scope it asks for, by the names bin/agent-key
|
|
56
|
+
// and the Keys page use.
|
|
57
|
+
const LOGIN_PERMISSIONS = Object.freeze(
|
|
58
|
+
WORKFLOW_PERMISSIONS.map(({ field, scope }) => [`--${field.replaceAll("_", "-")}`, scope]),
|
|
59
|
+
);
|
|
49
60
|
export const EXPERIMENT_ACTIONS = Object.freeze(["list", "estimate", "start", "get"]);
|
|
50
61
|
const SKILL_ACTIONS = new Set(["install", "update"]);
|
|
51
62
|
const SKILLS_ACTIONS = new Set(["install", "update", "list"]);
|
|
@@ -59,7 +70,7 @@ const OPTIONS = {
|
|
|
59
70
|
doctor: new Set(["--url", "--timeout-ms"]),
|
|
60
71
|
skill: new Set(["--client", "--runtime", "--project"]),
|
|
61
72
|
skills: new Set(["--client", "--runtime", "--project"]),
|
|
62
|
-
login: new Set(["--runtime", "--url", "--workspace", "--project", "--config-dir", "--timeout-ms"]),
|
|
73
|
+
login: new Set(["--runtime", "--url", "--workspace", "--role", "--project", "--config-dir", "--timeout-ms"]),
|
|
63
74
|
setup: new Set(["--runtime", "--url", "--workspace", "--project", "--config-dir", "--env-file", "--client", "--timeout-ms", "--deployment", "--agent-token-file", "--repository"]),
|
|
64
75
|
logout: new Set(["--project", "--config-dir"]),
|
|
65
76
|
status: new Set([...READ_BASE, ...READ_KEY]),
|
|
@@ -77,6 +88,7 @@ const OPTIONS = {
|
|
|
77
88
|
experiments: new Set([
|
|
78
89
|
...READ_BASE, ...READ_KEY, "--workload", "--limit", "--treatment", "--last-n", "--sample-model", "--track", "--id",
|
|
79
90
|
]),
|
|
91
|
+
tools: new Set([...READ_BASE, ...READ_KEY, "--name", "--args", "--args-file"]),
|
|
80
92
|
};
|
|
81
93
|
// Which experiments options each action takes, beyond READ_BASE and READ_KEY.
|
|
82
94
|
const EXPERIMENT_OPTIONS = {
|
|
@@ -129,10 +141,11 @@ const USAGE_GROUPED = [
|
|
|
129
141
|
// Options that take no value.
|
|
130
142
|
const FLAGS = {
|
|
131
143
|
pricing: new Set(["--batch"]),
|
|
132
|
-
login: new Set(["--signup", "--no-browser", "--reconnect"]),
|
|
144
|
+
login: new Set(["--signup", "--no-browser", "--reconnect", ...LOGIN_PERMISSIONS.map(([flag]) => flag)]),
|
|
133
145
|
setup: new Set(["--no-browser", "--repair", "--signup", "--reconnect", "--skip-skill", "--confirm-prerequisites", "--no-repository"]),
|
|
134
146
|
verify: new Set(["--open", "--no-browser"]),
|
|
135
147
|
usage: new Set(["--today"]),
|
|
148
|
+
tools: new Set(["--yes"]),
|
|
136
149
|
};
|
|
137
150
|
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
138
151
|
|
|
@@ -156,13 +169,24 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
|
156
169
|
export function parseArgs(argv) {
|
|
157
170
|
const json = argv.includes("--json");
|
|
158
171
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
172
|
+
const helpCommand = argv.find(arg => arg !== "--json") === "help";
|
|
173
|
+
if (argv.includes("--help") || argv.includes("-h") || helpCommand) {
|
|
174
|
+
const parts = argv.filter(arg => !["--json", "--help", "-h", "--advanced"].includes(arg));
|
|
175
|
+
if (parts[0] === "help") parts.shift();
|
|
176
|
+
const topic = HELP_TOPICS.has(parts[0]) ? parts[0] : null;
|
|
177
|
+
const actions = { pricing: PRICING_ACTIONS, experiments: EXPERIMENT_ACTIONS,
|
|
178
|
+
skill: [...SKILL_ACTIONS], skills: [...SKILLS_ACTIONS] };
|
|
179
|
+
const action = actions[topic]?.includes(parts[1]) ? parts[1] : null;
|
|
180
|
+
if (helpCommand && (parts.length > (action ? 2 : topic ? 1 : 0))) {
|
|
181
|
+
return { ok: false, command: "help", json, code: "unknown_help_topic", outcome: "invalid_input",
|
|
182
|
+
message: 'Unknown help topic. Run "metergraph --help" for commands.' };
|
|
183
|
+
}
|
|
184
|
+
return { ok: true, command: "help", topic, ...(action ? { action } : {}), ...(argv.includes("--advanced") ? { advanced: true } : {}), json };
|
|
162
185
|
}
|
|
163
186
|
|
|
164
187
|
let command = null;
|
|
165
188
|
let action = null;
|
|
189
|
+
let toolName = null;
|
|
166
190
|
let topic = null;
|
|
167
191
|
let version = false;
|
|
168
192
|
const values = {};
|
|
@@ -170,7 +194,7 @@ export function parseArgs(argv) {
|
|
|
170
194
|
|
|
171
195
|
const fail = (code, message, outcome = "invalid_input") => ({
|
|
172
196
|
ok: false,
|
|
173
|
-
command: (command === "skill" || command === "skills" || command === "experiments") && action !== null
|
|
197
|
+
command: (command === "skill" || command === "skills" || command === "experiments" || command === "tools") && action !== null
|
|
174
198
|
? `${command} ${action}`
|
|
175
199
|
: command ?? (version ? "version" : null),
|
|
176
200
|
json,
|
|
@@ -213,6 +237,14 @@ export function parseArgs(argv) {
|
|
|
213
237
|
action = arg;
|
|
214
238
|
continue;
|
|
215
239
|
}
|
|
240
|
+
if (command === "tools" && action === null && TOOLS_ACTIONS.includes(arg)) {
|
|
241
|
+
action = arg;
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (command === "tools" && action === "call" && toolName === null && !arg.startsWith("-")) {
|
|
245
|
+
toolName = arg;
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
216
248
|
|
|
217
249
|
if (READ_COMMANDS.includes(command)) {
|
|
218
250
|
const equals = arg.indexOf("=");
|
|
@@ -280,6 +312,9 @@ export function parseArgs(argv) {
|
|
|
280
312
|
`Unknown experiments subcommand at argument ${position}. Use list, estimate, start or get.`,
|
|
281
313
|
);
|
|
282
314
|
}
|
|
315
|
+
if (command === "tools" && action === null && !arg.startsWith("-")) {
|
|
316
|
+
return fail("unknown_subcommand", `Unknown tools subcommand at argument ${position}. Use list or call.`);
|
|
317
|
+
}
|
|
283
318
|
return fail(
|
|
284
319
|
"unknown_argument",
|
|
285
320
|
`Unrecognized argument at position ${position}. Run "metergraph --help" for usage.`,
|
|
@@ -297,6 +332,7 @@ export function parseArgs(argv) {
|
|
|
297
332
|
if (command === "setup") return parseSetup(values, flags, json, fail);
|
|
298
333
|
if (command === "verify") return parseVerify(values, flags, json, fail);
|
|
299
334
|
if (command === "experiments") return parseExperiments(action, values, json, fail);
|
|
335
|
+
if (command === "tools") return parseTools(action, toolName, values, flags, json, fail);
|
|
300
336
|
if (READ_COMMANDS.includes(command)) return parseRead(command, values, flags, json, fail);
|
|
301
337
|
if (command === "logout") {
|
|
302
338
|
const paths = parsePaths(values, fail);
|
|
@@ -322,16 +358,29 @@ export function parseArgs(argv) {
|
|
|
322
358
|
|
|
323
359
|
// --url, --workspace and --agent-token-file, shared by the read commands and
|
|
324
360
|
// experiments: { ok: true, origin, workspace, agentTokenFile } or a failure.
|
|
361
|
+
// --workspace names a workspace by its slug (on its Settings page, and in
|
|
362
|
+
// "metergraph context"), by its name, or by its ID. Returns { workspace,
|
|
363
|
+
// workspaceText }: workspace is an ID, workspaceText a slug or name, which is
|
|
364
|
+
// matched against the workspace the service reports. null for a bad value.
|
|
365
|
+
function workspaceOption(raw) {
|
|
366
|
+
if (raw === undefined) return { workspace: null, workspaceText: null };
|
|
367
|
+
if (UUID.test(raw)) return { workspace: raw.toLowerCase(), workspaceText: null };
|
|
368
|
+
const text = raw.trim();
|
|
369
|
+
if (text.length === 0 || text.length > MAX_WORKSPACE_TEXT || hasUnsafeText(text)) return null;
|
|
370
|
+
return { workspace: null, workspaceText: text };
|
|
371
|
+
}
|
|
372
|
+
const MAX_WORKSPACE_TEXT = 200;
|
|
373
|
+
const INVALID_WORKSPACE =
|
|
374
|
+
"--workspace takes the workspace's slug, shown on its Settings page and by \"metergraph context\", or its name.";
|
|
375
|
+
|
|
325
376
|
function parseKeyTarget(values, fail) {
|
|
326
377
|
let origin = null;
|
|
327
378
|
if (values["--url"] !== undefined) {
|
|
328
379
|
origin = parseOrigin(values["--url"]);
|
|
329
380
|
if (origin === null) return fail("invalid_url", INVALID_URL);
|
|
330
381
|
}
|
|
331
|
-
const
|
|
332
|
-
if (
|
|
333
|
-
return fail("invalid_workspace", "--workspace must be a workspace ID in UUID form.");
|
|
334
|
-
}
|
|
382
|
+
const named = workspaceOption(values["--workspace"]);
|
|
383
|
+
if (named === null) return fail("invalid_workspace", INVALID_WORKSPACE);
|
|
335
384
|
const agentTokenFile = values["--agent-token-file"] ?? null;
|
|
336
385
|
if (agentTokenFile !== null && (agentTokenFile === "" || agentTokenFile.includes("\0"))) {
|
|
337
386
|
return fail("invalid_agent_token_file", "--agent-token-file must name a private absolute file.");
|
|
@@ -339,11 +388,61 @@ function parseKeyTarget(values, fail) {
|
|
|
339
388
|
return {
|
|
340
389
|
ok: true,
|
|
341
390
|
origin,
|
|
342
|
-
|
|
391
|
+
...named,
|
|
343
392
|
agentTokenFile,
|
|
344
393
|
};
|
|
345
394
|
}
|
|
346
395
|
|
|
396
|
+
// tools list [--name TOOL] | call TOOL [--args JSON | --args-file PATH] [--yes].
|
|
397
|
+
// The credential options are the read commands'. Arguments are checked for
|
|
398
|
+
// shape only; the service validates them against the tool's schema.
|
|
399
|
+
function parseTools(action, toolName, values, flags, json, fail) {
|
|
400
|
+
if (action === null) {
|
|
401
|
+
return fail("missing_subcommand", 'tools requires a subcommand: list or call. Run "metergraph help tools" for usage.');
|
|
402
|
+
}
|
|
403
|
+
const allowed = action === "list" ? ["--name"] : ["--args", "--args-file"];
|
|
404
|
+
for (const name of Object.keys(values)) {
|
|
405
|
+
if (!READ_BASE.includes(name) && !READ_KEY.includes(name) && !allowed.includes(name)) {
|
|
406
|
+
return fail("unknown_argument", `tools ${action} does not take ${name}. Run "metergraph help tools" for usage.`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (action === "list" && flags.has("--yes")) {
|
|
410
|
+
return fail("unknown_argument", 'tools list does not take --yes. Run "metergraph help tools" for usage.');
|
|
411
|
+
}
|
|
412
|
+
const name = action === "list" ? values["--name"] ?? null : toolName;
|
|
413
|
+
if (action === "call" && name === null) {
|
|
414
|
+
return fail("missing_tool", 'tools call needs a tool name, such as metergraph_list_classified_workloads. Run "metergraph tools list" to see them.');
|
|
415
|
+
}
|
|
416
|
+
if (name !== null && !TOOL_NAME.test(name)) {
|
|
417
|
+
return fail("invalid_tool", "A tool name is metergraph_ followed by lower case letters, digits and underscores.");
|
|
418
|
+
}
|
|
419
|
+
if (values["--args"] !== undefined && values["--args-file"] !== undefined) {
|
|
420
|
+
return fail("conflicting_options", "Give --args or --args-file, not both.");
|
|
421
|
+
}
|
|
422
|
+
if (values["--args"] !== undefined && Buffer.byteLength(values["--args"]) > TOOL_ARGS_MAX_BYTES) {
|
|
423
|
+
return fail("args_too_large", "--args is larger than 1 MiB. Use --args-file.");
|
|
424
|
+
}
|
|
425
|
+
if (values["--args-file"] !== undefined && (values["--args-file"] === "" || values["--args-file"].includes("\0"))) {
|
|
426
|
+
return fail("invalid_args_file", "--args-file must name a file.");
|
|
427
|
+
}
|
|
428
|
+
const paths = parsePaths(values, fail);
|
|
429
|
+
if (!paths.ok) return paths;
|
|
430
|
+
const rawTimeout = values["--timeout-ms"];
|
|
431
|
+
const timeoutMs = rawTimeout === undefined
|
|
432
|
+
? READ_DEFAULT_TIMEOUT_MS
|
|
433
|
+
: parseTimeout(rawTimeout, READ_MIN_TIMEOUT_MS, READ_MAX_TIMEOUT_MS);
|
|
434
|
+
if (timeoutMs === null) {
|
|
435
|
+
return fail("invalid_timeout", `--timeout-ms must be a whole number from ${READ_MIN_TIMEOUT_MS} to ${READ_MAX_TIMEOUT_MS}.`);
|
|
436
|
+
}
|
|
437
|
+
const target = parseKeyTarget(values, fail);
|
|
438
|
+
if (!target.ok) return target;
|
|
439
|
+
delete target.ok;
|
|
440
|
+
return {
|
|
441
|
+
ok: true, command: "tools", action, ...target, project: paths.project, configDir: paths.configDir, timeoutMs, json,
|
|
442
|
+
tool: { name, args: values["--args"] ?? null, argsFile: values["--args-file"] ?? null, yes: flags.has("--yes") },
|
|
443
|
+
};
|
|
444
|
+
}
|
|
445
|
+
|
|
347
446
|
// experiments list | estimate | start | get. estimate and start take the same
|
|
348
447
|
// request, so an agent starts exactly what it priced: the workload, 1 to 3
|
|
349
448
|
// --treatment PROVIDER/MODEL[,label=..][,temperature=..][,max_tokens=..]
|
|
@@ -596,9 +695,16 @@ function parseLogin(values, flags, json, fail) {
|
|
|
596
695
|
}
|
|
597
696
|
const origin = parseUrlOption(values);
|
|
598
697
|
if (origin === null) return fail("invalid_url", INVALID_URL);
|
|
599
|
-
const
|
|
600
|
-
if (
|
|
601
|
-
|
|
698
|
+
const named = workspaceOption(values["--workspace"]);
|
|
699
|
+
if (named === null) return fail("invalid_workspace", INVALID_WORKSPACE);
|
|
700
|
+
const role = values["--role"] ?? "metadata";
|
|
701
|
+
if (!LOGIN_ROLES.includes(role)) return fail("invalid_role", "--role must be metadata or debug.");
|
|
702
|
+
const permissions = LOGIN_PERMISSIONS.filter(([flag]) => flags.has(flag)).map(([, scope]) => scope);
|
|
703
|
+
if (permissions.length > 0 && role !== "debug") {
|
|
704
|
+
return fail(
|
|
705
|
+
"permission_needs_debug",
|
|
706
|
+
"--allow-evaluation-writes, --allow-evaluation-previews and --allow-analysis-starts need --role debug.",
|
|
707
|
+
);
|
|
602
708
|
}
|
|
603
709
|
const paths = parsePaths(values, fail);
|
|
604
710
|
if (!paths.ok) return paths;
|
|
@@ -618,7 +724,9 @@ function parseLogin(values, flags, json, fail) {
|
|
|
618
724
|
command: "login",
|
|
619
725
|
runtime,
|
|
620
726
|
origin,
|
|
621
|
-
|
|
727
|
+
...named,
|
|
728
|
+
role,
|
|
729
|
+
permissions,
|
|
622
730
|
project: paths.project,
|
|
623
731
|
configDir: paths.configDir,
|
|
624
732
|
timeoutMs,
|