mirror of
https://github.com/Jeuners/ECC.git
synced 2026-09-09 15:02:30 +02:00
feat(ito): expose guarded live node qualification
Expose the canonical Itō CLI's pinned sixtytwo node-qualification path through ECC with double opt-in, explicit node/config gates, credential isolation, and no new MCP or execution authority. Validated across the full Linux, macOS, and Windows Node/package-manager matrix, hosted coverage, CodeQL, security, lint, and focused bridge tests.
This commit is contained in:
parent
34fbe007f0
commit
33c7dbb7d6
11 changed files with 384 additions and 42 deletions
|
|
@ -4,7 +4,7 @@ const { spawnSync } = require('child_process');
|
|||
const path = require('path');
|
||||
const { listAvailableLanguages } = require('./lib/install-executor');
|
||||
const { getComputeSponsorCopy } = require('./lib/compute-sponsor');
|
||||
const { createSafeItoEnvironment } = require('./lib/ito-environment');
|
||||
const { createSafeItoInvocationEnvironment } = require('./lib/ito-environment');
|
||||
|
||||
const COMMANDS = {
|
||||
install: {
|
||||
|
|
@ -107,7 +107,7 @@ const PRIMARY_COMMANDS = [
|
|||
];
|
||||
|
||||
function showHelp(exitCode = 0) {
|
||||
console.log(`
|
||||
process.stdout.write(`
|
||||
ECC selective-install CLI
|
||||
|
||||
Usage:
|
||||
|
|
@ -141,6 +141,7 @@ Examples:
|
|||
ecc ito auth
|
||||
ecc ito find --gpu h200 --count 8 --nodes 1 --gpus-per-node 8 --days 30 --storage-tb 1 --start-window 2099-08-15 --max-rate 3.00 --form-factor bare_metal --contract-type reservation --fabric infiniband --region us-east-1
|
||||
ecc ito status --json
|
||||
ecc ito evals --cluster clu_prod_example --live-sixtytwo --nodes gpu-01,gpu-02 --config-dir /absolute/path/to/qualification-config
|
||||
ecc list-installed --json
|
||||
ecc doctor --target cursor
|
||||
ecc repair --dry-run
|
||||
|
|
@ -226,7 +227,6 @@ function runCommand(commandName, args) {
|
|||
if (!command) {
|
||||
throw new Error(`Unknown command: ${commandName}`);
|
||||
}
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(__dirname, command.script), ...args],
|
||||
|
|
@ -234,9 +234,8 @@ function runCommand(commandName, args) {
|
|||
cwd: process.cwd(),
|
||||
env: commandName === 'ito'
|
||||
? {
|
||||
...createSafeItoEnvironment(process.env, {
|
||||
...createSafeItoInvocationEnvironment(process.env, args, {
|
||||
includeControls: true,
|
||||
includeItoRuntime: true,
|
||||
}),
|
||||
}
|
||||
: process.env,
|
||||
|
|
|
|||
|
|
@ -5,9 +5,12 @@
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
const { createSafeItoEnvironment } = require("./lib/ito-environment");
|
||||
const {
|
||||
createSafeItoInvocationEnvironment,
|
||||
getInvocationCommand,
|
||||
} = require("./lib/ito-environment");
|
||||
|
||||
const SUPPORTED_COMMANDS = Object.freeze(["auth", "find", "status"]);
|
||||
const SUPPORTED_COMMANDS = Object.freeze(["auth", "find", "status", "evals"]);
|
||||
const CANONICAL_REPOSITORY = "https://github.com/Ito-Markets/ito-cloud-runtime.git";
|
||||
const CANONICAL_PACKAGE_PATH = "cli/ito-compute-cli";
|
||||
const CANONICAL_ENTRY_SEGMENTS = Object.freeze([
|
||||
|
|
@ -18,25 +21,30 @@ const CANONICAL_ENTRY_SEGMENTS = Object.freeze([
|
|||
]);
|
||||
const EXECUTABLE_OVERRIDE = "ECC_ITO_CLI_EXECUTABLE";
|
||||
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
||||
const NODE_QUALIFICATION_TIMEOUT_MS = 31 * 60 * 1000;
|
||||
|
||||
function showHelp() {
|
||||
console.log(`
|
||||
process.stdout.write(`
|
||||
ECC × Itô local CLI bridge
|
||||
|
||||
Usage:
|
||||
ecc ito auth
|
||||
ecc ito find <all required RFQ options>
|
||||
ecc ito status
|
||||
ecc ito <auth|find|status> --json
|
||||
ecc ito evals --cluster <id> --live-sixtytwo --nodes <list> --config-dir <dir>
|
||||
ecc ito <auth|find|status|evals> --json
|
||||
|
||||
The bridge invokes the separately installed canonical Itô CLI and returns its
|
||||
real stdout, stderr, and exit code unchanged. It performs no browser navigation
|
||||
and adds no lock, workload, inference, evaluation, or purchase path.
|
||||
and adds no lock, workload, inference, or purchase path.
|
||||
|
||||
Important:
|
||||
- "find" reads live inventory and submits an authenticated RFQ.
|
||||
- Obtain explicit buyer authority and every hard constraint before invoking it.
|
||||
- "status" reads live RFQ and procurement status.
|
||||
- "evals" invokes only the canonical CLI's double-opt-in, pinned
|
||||
sixtytwo-cli node-qualification adapter against explicit nodes.
|
||||
- Node qualification cannot rent, launch, recover, repair, or purchase.
|
||||
- Inventory and RFQs are not reservations; only a returned firm quote is firm.
|
||||
|
||||
The canonical package is currently unpublished. Install it locally:
|
||||
|
|
@ -59,11 +67,67 @@ The same package's MCP server exposes only:
|
|||
Configure the MCP command as "node" with this absolute argument:
|
||||
/absolute/path/to/ito-cloud-runtime/${CANONICAL_PACKAGE_PATH}/dist/bin/ito-mcp.js
|
||||
|
||||
Inject ITO_API_KEY into the child process from 1Password or the launching
|
||||
environment. Never put the key in arguments, tracked files, or chat.
|
||||
For auth, find, and status, inject ITO_API_KEY into the child process from
|
||||
1Password or the launching environment. Never put the key in arguments,
|
||||
tracked files, or chat.
|
||||
|
||||
Live node qualification requires ITO_ENABLE_SIXTYTWO_LIVE=1,
|
||||
--live-sixtytwo, an explicit node list, and an existing absolute config
|
||||
directory. It forwards only named SIXTYTWO_API_TOKEN/SIXTYTWO_TOKEN and SSH
|
||||
agent state; ITO_API_KEY is intentionally excluded. The canonical CLI requires
|
||||
sixtytwo-cli==0.3.33 and fails closed.
|
||||
`);
|
||||
}
|
||||
|
||||
function requiredOptionValue(args, option) {
|
||||
const indexes = args
|
||||
.map((value, index) => (value === option ? index : -1))
|
||||
.filter((index) => index >= 0);
|
||||
if (indexes.length !== 1) {
|
||||
throw new Error(`${option} is required exactly once for live node qualification.`);
|
||||
}
|
||||
const value = args[indexes[0] + 1];
|
||||
if (!value?.trim() || value.startsWith("--")) {
|
||||
throw new Error(`${option} requires a non-empty value for live node qualification.`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function validateNodeQualificationArgs(args, environment) {
|
||||
if (environment.ITO_ENABLE_SIXTYTWO_LIVE !== "1") {
|
||||
throw new Error(
|
||||
"Live node qualification requires ITO_ENABLE_SIXTYTWO_LIVE=1 before any process is started."
|
||||
);
|
||||
}
|
||||
if (args.filter((value) => value === "--live-sixtytwo").length !== 1) {
|
||||
throw new Error(
|
||||
"Live node qualification requires --live-sixtytwo exactly once before any process is started."
|
||||
);
|
||||
}
|
||||
requiredOptionValue(args, "--cluster");
|
||||
const nodes = requiredOptionValue(args, "--nodes");
|
||||
if (!nodes.split(",").every((node) => node.trim().length > 0)) {
|
||||
throw new Error("--nodes must explicitly list one or more non-empty nodes.");
|
||||
}
|
||||
const configDirectory = requiredOptionValue(args, "--config-dir");
|
||||
if (!path.isAbsolute(configDirectory)) {
|
||||
throw new Error("--config-dir must be an existing absolute directory.");
|
||||
}
|
||||
try {
|
||||
const resolved = fs.realpathSync.native(configDirectory);
|
||||
if (
|
||||
!fs.statSync(resolved).isDirectory()
|
||||
|| !fs.statSync(path.join(resolved, "sixtytwo.yaml")).isFile()
|
||||
) {
|
||||
throw new Error("invalid qualification configuration");
|
||||
}
|
||||
} catch {
|
||||
throw new Error(
|
||||
"--config-dir must exist and contain a regular sixtytwo.yaml before any process is started."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function parseArgs(argv, environment = process.env) {
|
||||
const args = [...argv];
|
||||
if (
|
||||
|
|
@ -90,9 +154,12 @@ function parseArgs(argv, environment = process.env) {
|
|||
const command = withoutJson.shift();
|
||||
if (!SUPPORTED_COMMANDS.includes(command)) {
|
||||
throw new Error(
|
||||
`Unsupported Itô command "${command || "(missing)"}"; ECC permits only auth, find, and status.`
|
||||
`Unsupported Itô command "${command || "(missing)"}"; ECC permits only auth, find, status, and evals.`
|
||||
);
|
||||
}
|
||||
if (command === "evals") {
|
||||
validateNodeQualificationArgs(withoutJson, environment);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
help: false,
|
||||
|
|
@ -186,13 +253,16 @@ function buildInvocation(executable, args) {
|
|||
|
||||
function invokeIto(executable, args, environment = process.env) {
|
||||
const invocation = buildInvocation(executable, args);
|
||||
const command = getInvocationCommand(args);
|
||||
const isNodeQualification = command === "evals";
|
||||
const result = spawnSync(invocation.executable, invocation.args, {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...createSafeItoEnvironment(environment, { includeItoRuntime: true }),
|
||||
},
|
||||
// Keep policy helpers immutable for callers, but give child-process
|
||||
// instrumentation its own mutable copy (for example NODE_V8_COVERAGE).
|
||||
env: { ...createSafeItoInvocationEnvironment(environment, args) },
|
||||
maxBuffer: MAX_OUTPUT_BYTES,
|
||||
timeout: isNodeQualification ? NODE_QUALIFICATION_TIMEOUT_MS : undefined,
|
||||
shell: false,
|
||||
windowsHide: true,
|
||||
});
|
||||
|
|
@ -232,6 +302,7 @@ module.exports = Object.freeze({
|
|||
CANONICAL_PACKAGE_PATH,
|
||||
CANONICAL_REPOSITORY,
|
||||
EXECUTABLE_OVERRIDE,
|
||||
NODE_QUALIFICATION_TIMEOUT_MS,
|
||||
SUPPORTED_COMMANDS,
|
||||
buildInvocation,
|
||||
invokeIto,
|
||||
|
|
|
|||
|
|
@ -29,11 +29,20 @@ const ITO_RUNTIME_ENVIRONMENT_KEYS = Object.freeze([
|
|||
"ITO_INVENTORY_URL",
|
||||
]);
|
||||
|
||||
const ITO_EVAL_ENVIRONMENT_KEYS = Object.freeze([
|
||||
"ITO_ENABLE_SIXTYTWO_LIVE",
|
||||
"SIXTYTWO_API_TOKEN",
|
||||
"SIXTYTWO_TOKEN",
|
||||
"SSH_AUTH_SOCK",
|
||||
"SSH_AGENT_PID",
|
||||
]);
|
||||
|
||||
const ECC_ITO_CONTROL_KEYS = Object.freeze([
|
||||
"ECC_DRY_RUN",
|
||||
"ECC_ITO_CLI_EXECUTABLE",
|
||||
"NODE_ENV",
|
||||
]);
|
||||
const ITO_RUNTIME_COMMANDS = new Set(["auth", "find", "status"]);
|
||||
|
||||
function copyDefined(source, target, key) {
|
||||
if (typeof source[key] === "string") {
|
||||
|
|
@ -56,6 +65,12 @@ function createSafeItoEnvironment(source = process.env, options = {}) {
|
|||
}
|
||||
}
|
||||
|
||||
if (options.includeItoEvals) {
|
||||
for (const key of ITO_EVAL_ENVIRONMENT_KEYS) {
|
||||
copyDefined(source, safe, key);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.includeControls) {
|
||||
for (const key of ECC_ITO_CONTROL_KEYS) {
|
||||
copyDefined(source, safe, key);
|
||||
|
|
@ -65,9 +80,29 @@ function createSafeItoEnvironment(source = process.env, options = {}) {
|
|||
return Object.freeze(safe);
|
||||
}
|
||||
|
||||
function getInvocationCommand(args = []) {
|
||||
return args.filter((value) => value !== "--json")[0];
|
||||
}
|
||||
|
||||
function createSafeItoInvocationEnvironment(
|
||||
source = process.env,
|
||||
args = [],
|
||||
options = {},
|
||||
) {
|
||||
const command = getInvocationCommand(args);
|
||||
return createSafeItoEnvironment(source, {
|
||||
includeControls: options.includeControls === true,
|
||||
includeItoRuntime: ITO_RUNTIME_COMMANDS.has(command),
|
||||
includeItoEvals: command === "evals",
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = Object.freeze({
|
||||
ECC_ITO_CONTROL_KEYS,
|
||||
ITO_EVAL_ENVIRONMENT_KEYS,
|
||||
ITO_RUNTIME_ENVIRONMENT_KEYS,
|
||||
SYSTEM_ENVIRONMENT_KEYS,
|
||||
createSafeItoEnvironment,
|
||||
createSafeItoInvocationEnvironment,
|
||||
getInvocationCommand,
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue