2026-07-23 14:35:57 -07:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
|
|
|
|
|
|
"use strict";
|
|
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
const fs = require("fs");
|
|
|
|
|
|
const path = require("path");
|
2026-07-23 14:35:57 -07:00
|
|
|
|
const { spawnSync } = require("child_process");
|
2026-07-23 22:10:57 -07:00
|
|
|
|
const {
|
|
|
|
|
|
createSafeItoInvocationEnvironment,
|
|
|
|
|
|
getInvocationCommand,
|
|
|
|
|
|
} = require("./lib/ito-environment");
|
2026-07-23 14:35:57 -07:00
|
|
|
|
|
2026-08-07 14:53:13 -04:00
|
|
|
|
const SUPPORTED_COMMANDS = Object.freeze(["login", "logout", "auth", "find", "status", "evals"]);
|
2026-07-23 19:28:55 -07:00
|
|
|
|
const CANONICAL_REPOSITORY = "https://github.com/Ito-Markets/ito-cloud-runtime.git";
|
|
|
|
|
|
const CANONICAL_PACKAGE_PATH = "cli/ito-compute-cli";
|
2026-07-23 21:07:59 -07:00
|
|
|
|
const CANONICAL_ENTRY_SEGMENTS = Object.freeze([
|
|
|
|
|
|
...CANONICAL_PACKAGE_PATH.split("/"),
|
|
|
|
|
|
"dist",
|
|
|
|
|
|
"bin",
|
|
|
|
|
|
"ito.js",
|
|
|
|
|
|
]);
|
2026-07-23 19:28:55 -07:00
|
|
|
|
const EXECUTABLE_OVERRIDE = "ECC_ITO_CLI_EXECUTABLE";
|
|
|
|
|
|
const MAX_OUTPUT_BYTES = 10 * 1024 * 1024;
|
2026-07-23 22:10:57 -07:00
|
|
|
|
const NODE_QUALIFICATION_TIMEOUT_MS = 31 * 60 * 1000;
|
2026-07-23 14:35:57 -07:00
|
|
|
|
|
|
|
|
|
|
function showHelp() {
|
2026-07-23 22:10:57 -07:00
|
|
|
|
process.stdout.write(`
|
2026-07-23 19:28:55 -07:00
|
|
|
|
ECC × Itô local CLI bridge
|
2026-07-23 14:35:57 -07:00
|
|
|
|
|
|
|
|
|
|
Usage:
|
2026-08-05 18:17:10 -04:00
|
|
|
|
ecc ito login [--no-browser]
|
2026-08-07 14:53:13 -04:00
|
|
|
|
ecc ito logout
|
2026-07-23 19:28:55 -07:00
|
|
|
|
ecc ito auth
|
|
|
|
|
|
ecc ito find <all required RFQ options>
|
|
|
|
|
|
ecc ito status
|
2026-07-23 22:10:57 -07:00
|
|
|
|
ecc ito evals --cluster <id> --live-sixtytwo --nodes <list> --config-dir <dir>
|
2026-08-07 14:53:13 -04:00
|
|
|
|
ecc ito <login|logout|auth|find|status|evals> --json
|
2026-07-23 14:35:57 -07:00
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
The bridge invokes the separately installed canonical Itô CLI and returns its
|
2026-08-05 18:17:10 -04:00
|
|
|
|
real stdout, stderr, and exit code unchanged. "ecc ito login" delegates to the
|
|
|
|
|
|
canonical CLI's device authorization. It opens the Itô verification page by default
|
|
|
|
|
|
and persists its device token in macOS Keychain. Pass --no-browser to
|
|
|
|
|
|
suppress that handoff. ECC itself performs no browser automation and adds no
|
|
|
|
|
|
lock, workload, inference, or purchase path.
|
|
|
|
|
|
"ecc ito auth" is validation-only and never starts device login.
|
2026-08-07 14:53:13 -04:00
|
|
|
|
"ecc ito logout" asks the canonical CLI to revoke the current device credential
|
|
|
|
|
|
and remove its local copy only after remote revocation is confirmed.
|
2026-07-23 14:35:57 -07:00
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
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.
|
2026-07-23 22:10:57 -07:00
|
|
|
|
- "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.
|
2026-07-23 19:28:55 -07:00
|
|
|
|
- Inventory and RFQs are not reservations; only a returned firm quote is firm.
|
2026-07-23 14:35:57 -07:00
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
The canonical package is currently unpublished. Install it locally:
|
|
|
|
|
|
Canonical source: Ito-Markets/ito-cloud-runtime/${CANONICAL_PACKAGE_PATH}
|
|
|
|
|
|
git clone ${CANONICAL_REPOSITORY}
|
|
|
|
|
|
cd ito-cloud-runtime/${CANONICAL_PACKAGE_PATH}
|
|
|
|
|
|
npm ci
|
|
|
|
|
|
npm run check
|
|
|
|
|
|
|
|
|
|
|
|
Then set ${EXECUTABLE_OVERRIDE} to the explicit absolute built entry:
|
|
|
|
|
|
/absolute/path/to/ito-cloud-runtime/${CANONICAL_PACKAGE_PATH}/dist/bin/ito.js
|
|
|
|
|
|
|
|
|
|
|
|
For safety, ECC never discovers this credential-bearing client through PATH.
|
|
|
|
|
|
|
|
|
|
|
|
The same package's MCP server exposes only:
|
|
|
|
|
|
ito_auth
|
|
|
|
|
|
ito_find
|
|
|
|
|
|
ito_status
|
|
|
|
|
|
|
|
|
|
|
|
Configure the MCP command as "node" with this absolute argument:
|
|
|
|
|
|
/absolute/path/to/ito-cloud-runtime/${CANONICAL_PACKAGE_PATH}/dist/bin/ito-mcp.js
|
|
|
|
|
|
|
2026-08-05 18:17:10 -04:00
|
|
|
|
Device login never inherits ITO_API_KEY. The auth, find, and status commands
|
|
|
|
|
|
forward ITO_API_KEY directly when configured; ITO_AUTH_MODE=legacy is not
|
|
|
|
|
|
required. The canonical client stores device credentials in macOS Keychain by
|
|
|
|
|
|
default; file-token fallback remains explicit and must use restrictive settings.
|
|
|
|
|
|
Never put a key or token in arguments, tracked files, or chat.
|
2026-07-23 22:10:57 -07:00
|
|
|
|
|
|
|
|
|
|
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.
|
2026-07-23 14:35:57 -07:00
|
|
|
|
`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 22:10:57 -07:00
|
|
|
|
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."
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
function parseArgs(argv, environment = process.env) {
|
2026-07-23 14:35:57 -07:00
|
|
|
|
const args = [...argv];
|
2026-07-23 19:28:55 -07:00
|
|
|
|
if (
|
|
|
|
|
|
args.length === 0
|
|
|
|
|
|
|| args.includes("--help")
|
|
|
|
|
|
|| args.includes("-h")
|
|
|
|
|
|
) {
|
|
|
|
|
|
return Object.freeze({ help: true, invocationArgs: [] });
|
2026-07-23 14:35:57 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
if (environment.ECC_DRY_RUN === "1" || args.includes("--dry-run")) {
|
2026-07-23 14:35:57 -07:00
|
|
|
|
throw new Error(
|
2026-07-23 19:28:55 -07:00
|
|
|
|
"Itô compute has no paper or dry-run success mode. No CLI operation was invoked."
|
2026-07-23 14:35:57 -07:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
const jsonIndexes = args
|
|
|
|
|
|
.map((value, index) => (value === "--json" ? index : -1))
|
|
|
|
|
|
.filter((index) => index >= 0);
|
|
|
|
|
|
if (jsonIndexes.length > 1) {
|
|
|
|
|
|
throw new Error("--json may only be provided once");
|
|
|
|
|
|
}
|
|
|
|
|
|
const withoutJson = args.filter((value) => value !== "--json");
|
|
|
|
|
|
const command = withoutJson.shift();
|
|
|
|
|
|
if (!SUPPORTED_COMMANDS.includes(command)) {
|
|
|
|
|
|
throw new Error(
|
2026-08-07 14:53:13 -04:00
|
|
|
|
`Unsupported Itô command "${command || "(missing)"}"; ECC permits only login, logout, auth, find, status, and evals.`
|
2026-07-23 19:28:55 -07:00
|
|
|
|
);
|
2026-07-23 14:35:57 -07:00
|
|
|
|
}
|
2026-08-05 18:17:10 -04:00
|
|
|
|
if (command === "auth" && withoutJson.includes("--no-browser")) {
|
|
|
|
|
|
throw new Error("--no-browser is valid only for ecc ito login; auth is validation-only.");
|
|
|
|
|
|
}
|
2026-07-23 22:10:57 -07:00
|
|
|
|
if (command === "evals") {
|
|
|
|
|
|
validateNodeQualificationArgs(withoutJson, environment);
|
|
|
|
|
|
}
|
2026-07-23 14:35:57 -07:00
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
return Object.freeze({
|
|
|
|
|
|
help: false,
|
|
|
|
|
|
invocationArgs: Object.freeze([
|
|
|
|
|
|
...(jsonIndexes.length === 1 ? ["--json"] : []),
|
|
|
|
|
|
command,
|
|
|
|
|
|
...withoutJson,
|
|
|
|
|
|
]),
|
2026-07-23 14:35:57 -07:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
function resolveItoExecutable(environment = process.env) {
|
|
|
|
|
|
const configured = environment[EXECUTABLE_OVERRIDE]?.trim();
|
|
|
|
|
|
if (!configured) {
|
|
|
|
|
|
throw new Error([
|
|
|
|
|
|
"The canonical ito-compute-cli is unpublished and ECC will not resolve",
|
|
|
|
|
|
`a credential-bearing "ito" executable from PATH. Build it from`,
|
|
|
|
|
|
`${CANONICAL_REPOSITORY.replace(/\.git$/, "")}/${CANONICAL_PACKAGE_PATH},`,
|
|
|
|
|
|
"run npm ci and npm run check, then set",
|
|
|
|
|
|
`${EXECUTABLE_OVERRIDE} to the explicit absolute dist/bin/ito.js path.`,
|
|
|
|
|
|
].join(" "));
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!path.isAbsolute(configured)) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`${EXECUTABLE_OVERRIDE} must be an absolute path explicitly configured by the operator.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return assertUsableExecutable(configured);
|
2026-07-23 14:35:57 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
function assertUsableExecutable(candidate) {
|
|
|
|
|
|
let canonicalCandidate;
|
2026-07-23 14:35:57 -07:00
|
|
|
|
try {
|
2026-07-23 19:28:55 -07:00
|
|
|
|
canonicalCandidate = fs.realpathSync.native(candidate);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`${EXECUTABLE_OVERRIDE} does not point to a readable local Itô CLI file.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-07-23 21:07:59 -07:00
|
|
|
|
if (!isCanonicalItoEntry(canonicalCandidate)) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`${EXECUTABLE_OVERRIDE} must point to the canonical dist/bin/ito.js entry.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-07-23 19:28:55 -07:00
|
|
|
|
if (!isUsableExecutable(canonicalCandidate)) {
|
|
|
|
|
|
throw new Error(
|
|
|
|
|
|
`${EXECUTABLE_OVERRIDE} does not point to a readable local Itô CLI file.`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return canonicalCandidate;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 21:07:59 -07:00
|
|
|
|
function isCanonicalItoEntry(candidate) {
|
|
|
|
|
|
const pathSegments = path
|
|
|
|
|
|
.normalize(candidate)
|
|
|
|
|
|
.split(path.sep)
|
|
|
|
|
|
.filter(Boolean);
|
|
|
|
|
|
if (pathSegments.length < CANONICAL_ENTRY_SEGMENTS.length) return false;
|
|
|
|
|
|
const candidateTail = pathSegments.slice(-CANONICAL_ENTRY_SEGMENTS.length);
|
|
|
|
|
|
return candidateTail.every((segment, index) => {
|
|
|
|
|
|
const expected = CANONICAL_ENTRY_SEGMENTS[index];
|
|
|
|
|
|
return process.platform === "win32"
|
|
|
|
|
|
? segment.toLowerCase() === expected.toLowerCase()
|
|
|
|
|
|
: segment === expected;
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-07-23 19:28:55 -07:00
|
|
|
|
function isUsableExecutable(candidate) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const info = fs.statSync(candidate);
|
|
|
|
|
|
if (!info.isFile()) return false;
|
2026-07-23 21:07:59 -07:00
|
|
|
|
fs.accessSync(candidate, fs.constants.R_OK);
|
2026-07-23 19:28:55 -07:00
|
|
|
|
return true;
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function buildInvocation(executable, args) {
|
2026-07-23 21:07:59 -07:00
|
|
|
|
if (!isCanonicalItoEntry(executable)) {
|
2026-07-23 19:28:55 -07:00
|
|
|
|
throw new Error(
|
2026-07-23 21:07:59 -07:00
|
|
|
|
`Refusing to invoke an Itô CLI shim. Set ${EXECUTABLE_OVERRIDE} to the absolute dist/bin/ito.js path.`
|
2026-07-23 19:28:55 -07:00
|
|
|
|
);
|
|
|
|
|
|
}
|
2026-07-23 21:07:59 -07:00
|
|
|
|
return Object.freeze({
|
|
|
|
|
|
executable: process.execPath,
|
|
|
|
|
|
args: Object.freeze([executable, ...args]),
|
|
|
|
|
|
});
|
2026-07-23 19:28:55 -07:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function invokeIto(executable, args, environment = process.env) {
|
|
|
|
|
|
const invocation = buildInvocation(executable, args);
|
2026-07-23 22:10:57 -07:00
|
|
|
|
const command = getInvocationCommand(args);
|
|
|
|
|
|
const isNodeQualification = command === "evals";
|
2026-08-05 18:17:10 -04:00
|
|
|
|
const isDeviceLogin = command === "login";
|
2026-07-23 19:28:55 -07:00
|
|
|
|
const result = spawnSync(invocation.executable, invocation.args, {
|
|
|
|
|
|
cwd: process.cwd(),
|
|
|
|
|
|
encoding: "utf8",
|
2026-07-23 22:10:57 -07:00
|
|
|
|
// Keep policy helpers immutable for callers, but give child-process
|
|
|
|
|
|
// instrumentation its own mutable copy (for example NODE_V8_COVERAGE).
|
|
|
|
|
|
env: { ...createSafeItoInvocationEnvironment(environment, args) },
|
2026-08-05 18:17:10 -04:00
|
|
|
|
stdio: isDeviceLogin ? "inherit" : ["pipe", "pipe", "pipe"],
|
2026-07-23 19:28:55 -07:00
|
|
|
|
maxBuffer: MAX_OUTPUT_BYTES,
|
2026-07-23 22:10:57 -07:00
|
|
|
|
timeout: isNodeQualification ? NODE_QUALIFICATION_TIMEOUT_MS : undefined,
|
2026-07-23 19:28:55 -07:00
|
|
|
|
shell: false,
|
|
|
|
|
|
windowsHide: true,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (result.stdout) process.stdout.write(result.stdout);
|
|
|
|
|
|
if (result.stderr) process.stderr.write(result.stderr);
|
|
|
|
|
|
if (result.error) {
|
|
|
|
|
|
throw new Error(`The local Itô CLI could not be started: ${result.error.message}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (typeof result.status === "number") return result.status;
|
|
|
|
|
|
if (result.signal) {
|
|
|
|
|
|
throw new Error(`The local Itô CLI terminated by signal ${result.signal}.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function main(argv = process.argv.slice(2), environment = process.env) {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const parsed = parseArgs(argv, environment);
|
|
|
|
|
|
if (parsed.help) {
|
|
|
|
|
|
showHelp();
|
|
|
|
|
|
return 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
const executable = resolveItoExecutable(environment);
|
|
|
|
|
|
return invokeIto(executable, parsed.invocationArgs, environment);
|
2026-07-23 14:35:57 -07:00
|
|
|
|
} catch (error) {
|
2026-07-23 19:28:55 -07:00
|
|
|
|
console.error(`Error: ${error.message}`);
|
2026-07-23 14:35:57 -07:00
|
|
|
|
return 1;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (require.main === module) {
|
|
|
|
|
|
process.exitCode = main();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
module.exports = Object.freeze({
|
2026-07-23 19:28:55 -07:00
|
|
|
|
CANONICAL_PACKAGE_PATH,
|
|
|
|
|
|
CANONICAL_REPOSITORY,
|
|
|
|
|
|
EXECUTABLE_OVERRIDE,
|
2026-07-23 22:10:57 -07:00
|
|
|
|
NODE_QUALIFICATION_TIMEOUT_MS,
|
2026-07-23 19:28:55 -07:00
|
|
|
|
SUPPORTED_COMMANDS,
|
|
|
|
|
|
buildInvocation,
|
|
|
|
|
|
invokeIto,
|
2026-07-23 14:35:57 -07:00
|
|
|
|
main,
|
|
|
|
|
|
parseArgs,
|
2026-07-23 19:28:55 -07:00
|
|
|
|
resolveItoExecutable,
|
2026-07-23 14:35:57 -07:00
|
|
|
|
});
|