mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-30 05:25:08 +02:00
fix: gate ECC node qualification before spawn
This commit is contained in:
@@ -1228,9 +1228,10 @@ skill](skills/ito-compute/SKILL.md) for the full RFQ authority and MCP setup
|
||||
contract.
|
||||
|
||||
`find` submits a live authenticated RFQ. It does not reserve capacity.
|
||||
`evals` requires the canonical CLI's double opt-in, a separately installed
|
||||
`sixtytwo-cli==0.3.33`, an explicit node list, and an existing absolute
|
||||
configuration directory. It cannot rent, launch, recover, repair, or purchase.
|
||||
`evals` requires both `ITO_ENABLE_SIXTYTWO_LIVE=1` and `--live-sixtytwo`, a
|
||||
separately installed `sixtytwo-cli==0.3.33`, an explicit node list, and an
|
||||
existing absolute configuration directory. It cannot rent, launch, recover,
|
||||
repair, or purchase.
|
||||
ECC exposes no quote lock, purchase, workload, or inference path, and it never
|
||||
replaces a missing client or failed live call with a local result.
|
||||
|
||||
|
||||
@@ -70,7 +70,8 @@ environment. It does not inspect or log the key.
|
||||
or agent must gather every hard topology/economic constraint and obtain
|
||||
explicit buyer authority before invoking it.
|
||||
- `status` reads current RFQ and procurement status.
|
||||
- `evals` runs only the canonical CLI's double-opt-in
|
||||
- `evals` requires both `ITO_ENABLE_SIXTYTWO_LIVE=1` and
|
||||
`--live-sixtytwo`, then runs only the canonical CLI's pinned
|
||||
`sixtytwo-cli==0.3.33` qualification adapter against an explicit node list
|
||||
and existing absolute configuration directory. It receives no `ITO_API_KEY`
|
||||
or unrelated cloud/model credentials and cannot rent, launch, recover,
|
||||
|
||||
+5
-12
@@ -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:
|
||||
@@ -227,22 +227,15 @@ function runCommand(commandName, args) {
|
||||
if (!command) {
|
||||
throw new Error(`Unknown command: ${commandName}`);
|
||||
}
|
||||
const itoSubcommand = args[0] === '--json' ? args[1] : args[0];
|
||||
const isItoNodeQualification = commandName === 'ito' && itoSubcommand === 'evals';
|
||||
|
||||
const result = spawnSync(
|
||||
process.execPath,
|
||||
[path.join(__dirname, command.script), ...args],
|
||||
{
|
||||
cwd: process.cwd(),
|
||||
env: commandName === 'ito'
|
||||
? {
|
||||
...createSafeItoEnvironment(process.env, {
|
||||
includeControls: true,
|
||||
includeItoRuntime: !isItoNodeQualification,
|
||||
includeItoEvals: isItoNodeQualification,
|
||||
}),
|
||||
}
|
||||
? createSafeItoInvocationEnvironment(process.env, args, {
|
||||
includeControls: true,
|
||||
})
|
||||
: process.env,
|
||||
encoding: 'utf8',
|
||||
maxBuffer: 10 * 1024 * 1024,
|
||||
|
||||
+63
-11
@@ -5,7 +5,9 @@
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { spawnSync } = require("child_process");
|
||||
const { createSafeItoEnvironment } = require("./lib/ito-environment");
|
||||
const {
|
||||
createSafeItoInvocationEnvironment,
|
||||
} = require("./lib/ito-environment");
|
||||
|
||||
const SUPPORTED_COMMANDS = Object.freeze(["auth", "find", "status", "evals"]);
|
||||
const CANONICAL_REPOSITORY = "https://github.com/Ito-Markets/ito-cloud-runtime.git";
|
||||
@@ -18,9 +20,10 @@ 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:
|
||||
@@ -63,15 +66,64 @@ 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. The canonical CLI requires sixtytwo-cli==0.3.33 and fails closed.
|
||||
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 || 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");
|
||||
requiredOptionValue(args, "--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 (
|
||||
@@ -101,6 +153,9 @@ function parseArgs(argv, environment = process.env) {
|
||||
`Unsupported Itô command "${command || "(missing)"}"; ECC permits only auth, find, status, and evals.`
|
||||
);
|
||||
}
|
||||
if (command === "evals") {
|
||||
validateNodeQualificationArgs(withoutJson, environment);
|
||||
}
|
||||
|
||||
return Object.freeze({
|
||||
help: false,
|
||||
@@ -199,13 +254,9 @@ function invokeIto(executable, args, environment = process.env) {
|
||||
const result = spawnSync(invocation.executable, invocation.args, {
|
||||
cwd: process.cwd(),
|
||||
encoding: "utf8",
|
||||
env: {
|
||||
...createSafeItoEnvironment(environment, {
|
||||
includeItoRuntime: !isNodeQualification,
|
||||
includeItoEvals: isNodeQualification,
|
||||
}),
|
||||
},
|
||||
env: createSafeItoInvocationEnvironment(environment, args),
|
||||
maxBuffer: MAX_OUTPUT_BYTES,
|
||||
timeout: isNodeQualification ? NODE_QUALIFICATION_TIMEOUT_MS : undefined,
|
||||
shell: false,
|
||||
windowsHide: true,
|
||||
});
|
||||
@@ -245,6 +296,7 @@ module.exports = Object.freeze({
|
||||
CANONICAL_PACKAGE_PATH,
|
||||
CANONICAL_REPOSITORY,
|
||||
EXECUTABLE_OVERRIDE,
|
||||
NODE_QUALIFICATION_TIMEOUT_MS,
|
||||
SUPPORTED_COMMANDS,
|
||||
buildInvocation,
|
||||
invokeIto,
|
||||
|
||||
@@ -42,6 +42,7 @@ const ECC_ITO_CONTROL_KEYS = Object.freeze([
|
||||
"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") {
|
||||
@@ -79,10 +80,24 @@ function createSafeItoEnvironment(source = process.env, options = {}) {
|
||||
return Object.freeze(safe);
|
||||
}
|
||||
|
||||
function createSafeItoInvocationEnvironment(
|
||||
source = process.env,
|
||||
args = [],
|
||||
options = {},
|
||||
) {
|
||||
const command = args.filter((value) => value !== "--json")[0];
|
||||
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,
|
||||
});
|
||||
|
||||
@@ -13,7 +13,14 @@ const { spawnSync } = require("child_process");
|
||||
|
||||
const REPO_ROOT = path.join(__dirname, "..", "..");
|
||||
const ECC_SCRIPT = path.join(REPO_ROOT, "scripts", "ecc.js");
|
||||
const ITO_SCRIPT = path.join(REPO_ROOT, "scripts", "ito.js");
|
||||
const CANONICAL_PACKAGE = "Ito-Markets/ito-cloud-runtime/cli/ito-compute-cli";
|
||||
const {
|
||||
NODE_QUALIFICATION_TIMEOUT_MS,
|
||||
} = require("../../scripts/ito");
|
||||
const {
|
||||
createSafeItoInvocationEnvironment,
|
||||
} = require("../../scripts/lib/ito-environment");
|
||||
|
||||
function runCli(args, environment = {}) {
|
||||
return spawnSync(process.execPath, [ECC_SCRIPT, ...args], {
|
||||
@@ -80,8 +87,8 @@ function main() {
|
||||
console.log("\n=== Testing ECC × Itô real CLI bridge ===\n");
|
||||
|
||||
const tests = [
|
||||
["forwards only the reviewed CLI surface to an explicit local executable", () => {
|
||||
for (const command of ["auth", "find", "status", "evals"]) {
|
||||
["forwards only the reviewed RFQ CLI surface to an explicit local executable", () => {
|
||||
for (const command of ["auth", "find", "status"]) {
|
||||
const probe = makeItoProbe();
|
||||
try {
|
||||
const result = runCli(["ito", command], {
|
||||
@@ -156,6 +163,8 @@ function main() {
|
||||
const probe = makeItoProbe();
|
||||
try {
|
||||
const configDirectory = path.join(probe.directory, "qualification");
|
||||
fs.mkdirSync(configDirectory);
|
||||
fs.writeFileSync(path.join(configDirectory, "sixtytwo.yaml"), "suite: full\n");
|
||||
const result = runCli([
|
||||
"ito",
|
||||
"evals",
|
||||
@@ -170,6 +179,7 @@ function main() {
|
||||
ITO_INVENTORY_URL: "https://edge.example.test",
|
||||
ITO_ENABLE_SIXTYTWO_LIVE: "1",
|
||||
SIXTYTWO_API_TOKEN: "sixtytwo-test-token",
|
||||
SIXTYTWO_TOKEN: "sixtytwo-legacy-test-token",
|
||||
SSH_AUTH_SOCK: "/tmp/ecc-test-agent.sock",
|
||||
ITO_CLI_DEMO: "1",
|
||||
ITO_CLI_STATE_DIR: "/tmp/forbidden-paper-state",
|
||||
@@ -187,6 +197,7 @@ function main() {
|
||||
]);
|
||||
assert.strictEqual(invocation.env.ITO_ENABLE_SIXTYTWO_LIVE, "1");
|
||||
assert.strictEqual(invocation.env.SIXTYTWO_API_TOKEN, "sixtytwo-test-token");
|
||||
assert.strictEqual(invocation.env.SIXTYTWO_TOKEN, "sixtytwo-legacy-test-token");
|
||||
assert.strictEqual(invocation.env.SSH_AUTH_SOCK, "/tmp/ecc-test-agent.sock");
|
||||
assert.strictEqual(invocation.env.ITO_API_KEY, undefined);
|
||||
assert.strictEqual(invocation.env.ITO_API_URL, undefined);
|
||||
@@ -199,6 +210,115 @@ function main() {
|
||||
fs.rmSync(probe.directory, { recursive: true, force: true });
|
||||
}
|
||||
}],
|
||||
["rejects every incomplete live qualification before spawning", () => {
|
||||
const validArgs = [
|
||||
"ito",
|
||||
"evals",
|
||||
"--cluster", "clu_prod_example",
|
||||
"--live-sixtytwo",
|
||||
"--nodes", "gpu-01,gpu-02",
|
||||
];
|
||||
const cases = [
|
||||
{
|
||||
label: "missing environment opt-in",
|
||||
args: [...validArgs, "--config-dir", "__CONFIG__"],
|
||||
env: {},
|
||||
error: /ITO_ENABLE_SIXTYTWO_LIVE=1/,
|
||||
},
|
||||
{
|
||||
label: "missing live flag",
|
||||
args: validArgs.filter((value) => value !== "--live-sixtytwo")
|
||||
.concat("--config-dir", "__CONFIG__"),
|
||||
env: { ITO_ENABLE_SIXTYTWO_LIVE: "1" },
|
||||
error: /--live-sixtytwo/,
|
||||
},
|
||||
{
|
||||
label: "missing nodes",
|
||||
args: [
|
||||
"ito", "evals",
|
||||
"--cluster", "clu_prod_example",
|
||||
"--live-sixtytwo",
|
||||
"--config-dir", "__CONFIG__",
|
||||
],
|
||||
env: { ITO_ENABLE_SIXTYTWO_LIVE: "1" },
|
||||
error: /--nodes/,
|
||||
},
|
||||
{
|
||||
label: "missing cluster",
|
||||
args: [
|
||||
"ito", "evals",
|
||||
"--live-sixtytwo",
|
||||
"--nodes", "gpu-01",
|
||||
"--config-dir", "__CONFIG__",
|
||||
],
|
||||
env: { ITO_ENABLE_SIXTYTWO_LIVE: "1" },
|
||||
error: /--cluster/,
|
||||
},
|
||||
{
|
||||
label: "relative config directory",
|
||||
args: [...validArgs, "--config-dir", "relative/config"],
|
||||
env: { ITO_ENABLE_SIXTYTWO_LIVE: "1" },
|
||||
error: /absolute/,
|
||||
},
|
||||
{
|
||||
label: "missing config directory",
|
||||
args: [...validArgs, "--config-dir", "__MISSING_CONFIG__"],
|
||||
env: { ITO_ENABLE_SIXTYTWO_LIVE: "1" },
|
||||
error: /sixtytwo\.yaml/,
|
||||
},
|
||||
];
|
||||
|
||||
for (const testCase of cases) {
|
||||
const probe = makeItoProbe();
|
||||
try {
|
||||
const configDirectory = path.join(probe.directory, "qualification");
|
||||
fs.mkdirSync(configDirectory);
|
||||
fs.writeFileSync(path.join(configDirectory, "sixtytwo.yaml"), "suite: full\n");
|
||||
const args = testCase.args.map((value) => (
|
||||
value === "__CONFIG__"
|
||||
? configDirectory
|
||||
: value === "__MISSING_CONFIG__"
|
||||
? path.join(probe.directory, "missing")
|
||||
: value
|
||||
));
|
||||
const result = runCli(args, {
|
||||
ECC_ITO_CLI_EXECUTABLE: probe.executable,
|
||||
...testCase.env,
|
||||
});
|
||||
assert.notStrictEqual(result.status, 0, testCase.label);
|
||||
assert.match(result.stderr, testCase.error, testCase.label);
|
||||
assert.ok(
|
||||
!fs.existsSync(probe.log),
|
||||
`${testCase.label} must not spawn the canonical Itô CLI`,
|
||||
);
|
||||
} finally {
|
||||
fs.rmSync(probe.directory, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
}],
|
||||
["classifies Itō child environments once and fails closed on unknown prefixes", () => {
|
||||
const safe = createSafeItoInvocationEnvironment(
|
||||
{
|
||||
PATH: process.env.PATH,
|
||||
ECC_ITO_CLI_EXECUTABLE: "/operator/canonical/ito.js",
|
||||
ITO_API_KEY: "must-not-cross",
|
||||
SIXTYTWO_TOKEN: "must-not-cross",
|
||||
},
|
||||
["--future-ecc-flag", "evals"],
|
||||
{ includeControls: true },
|
||||
);
|
||||
assert.strictEqual(safe.ECC_ITO_CLI_EXECUTABLE, "/operator/canonical/ito.js");
|
||||
assert.strictEqual(safe.ITO_API_KEY, undefined);
|
||||
assert.strictEqual(safe.SIXTYTWO_TOKEN, undefined);
|
||||
}],
|
||||
["bounds the outer node-qualification process beyond the canonical timeout", () => {
|
||||
assert.strictEqual(NODE_QUALIFICATION_TIMEOUT_MS, 31 * 60 * 1000);
|
||||
const source = fs.readFileSync(ITO_SCRIPT, "utf8");
|
||||
assert.match(
|
||||
source,
|
||||
/timeout: isNodeQualification \? NODE_QUALIFICATION_TIMEOUT_MS : undefined/,
|
||||
);
|
||||
}],
|
||||
["rejects unsupported browser, paper, and execution operations before spawning", () => {
|
||||
for (const command of ["rent", "lock", "run", "inference", "mcp"]) {
|
||||
const probe = makeItoProbe();
|
||||
|
||||
Reference in New Issue
Block a user