diff --git a/manifests/install-components.json b/manifests/install-components.json index a5f976a94..316ff5a74 100644 --- a/manifests/install-components.json +++ b/manifests/install-components.json @@ -202,6 +202,14 @@ "ito-compute" ] }, + { + "id": "capability:ito-inference", + "family": "capability", + "description": "Gated Itô inference preflight, deployment, observability, and cleanup on existing entitled clusters.", + "modules": [ + "ito-inference" + ] + }, { "id": "capability:social", "family": "capability", diff --git a/manifests/install-modules.json b/manifests/install-modules.json index e18922bf0..5a5d9f590 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -634,6 +634,36 @@ "cost": "light", "stability": "beta" }, + { + "id": "ito-inference", + "kind": "skills", + "description": "Gated Itô inference eligibility, entitlement, deployment, observability, and cleanup on existing clusters.", + "paths": [ + "skills/ito-inference" + ], + "targets": [ + "claude", + "claude-project", + "cursor", + "antigravity", + "codex", + "gemini", + "opencode", + "codebuddy", + "joycode", + "qwen", + "zed", + "hermes", + "openclaw", + "kimi" + ], + "dependencies": [ + "ito-compute" + ], + "defaultInstall": false, + "cost": "light", + "stability": "experimental" + }, { "id": "social-distribution", "kind": "skills", diff --git a/manifests/install-profiles.json b/manifests/install-profiles.json index 15e429943..a6da4d510 100644 --- a/manifests/install-profiles.json +++ b/manifests/install-profiles.json @@ -89,6 +89,7 @@ "optimization-workflows", "prediction-market-skills", "ito-compute", + "ito-inference", "social-distribution", "media-generation", "orchestration", diff --git a/package.json b/package.json index 26ba305aa..3651cb14d 100644 --- a/package.json +++ b/package.json @@ -224,6 +224,7 @@ "skills/inventory-demand-planning/", "skills/ito-basket-compare/", "skills/ito-compute/", + "skills/ito-inference/", "skills/ito-data-atlas-agent/", "skills/ito-inference/", "skills/ito-market-intelligence/", diff --git a/scripts/ito.js b/scripts/ito.js index e981d85ee..913370fdf 100755 --- a/scripts/ito.js +++ b/scripts/ito.js @@ -10,7 +10,7 @@ const { getInvocationCommand, } = require("./lib/ito-environment"); -const SUPPORTED_COMMANDS = Object.freeze(["login", "logout", "auth", "find", "status", "evals"]); +const SUPPORTED_COMMANDS = Object.freeze(["login", "logout", "auth", "find", "status", "evals", "inference"]); 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([ @@ -34,14 +34,18 @@ Usage: ecc ito find ecc ito status ecc ito evals --cluster --live-sixtytwo --nodes --config-dir - ecc ito --json + ecc ito inference preflight --cluster --model --revision --engine vllm --max-runtime-hours + ecc ito inference deploy --confirm-cost-usd + ecc ito inference --deployment + ecc ito inference stop --deployment --confirm + ecc ito --json The bridge invokes the separately installed canonical Itô CLI and returns its 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. +lock, workload, or purchase path. "ecc ito auth" is validation-only and never starts device login. "ecc ito logout" asks the canonical CLI to revoke the current device credential and remove its local copy only after remote revocation is confirmed. @@ -52,6 +56,10 @@ Important: - "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. + - "inference preflight" is read-only; deploy and stop require the canonical + CLI's exact cost and explicit cleanup confirmations. + - Inference operates only on an existing active, entitled cluster. It cannot + find, book, reserve, purchase, resize, or delete compute capacity. - Node qualification cannot rent, launch, recover, repair, or purchase. - Inventory and RFQs are not reservations; only a returned firm quote is firm. @@ -164,7 +172,7 @@ 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 login, logout, auth, find, status, and evals.` + `Unsupported Itô command "${command || "(missing)"}"; ECC permits only login, logout, auth, find, status, evals, and inference.` ); } if (command === "auth" && withoutJson.includes("--no-browser")) { diff --git a/scripts/lib/ito-environment.js b/scripts/lib/ito-environment.js index e9a6c909c..ff2aaff40 100644 --- a/scripts/lib/ito-environment.js +++ b/scripts/lib/ito-environment.js @@ -45,7 +45,7 @@ const ECC_ITO_CONTROL_KEYS = Object.freeze([ "ECC_ITO_CLI_EXECUTABLE", "NODE_ENV", ]); -const ITO_RUNTIME_COMMANDS = new Set(["login", "logout", "auth", "find", "status"]); +const ITO_RUNTIME_COMMANDS = new Set(["login", "logout", "auth", "find", "status", "inference"]); function copyDefined(source, target, key) { if (typeof source[key] === "string") { @@ -97,7 +97,7 @@ function createSafeItoInvocationEnvironment( return createSafeItoEnvironment(source, { includeControls: options.includeControls === true, includeItoRuntime: ITO_RUNTIME_COMMANDS.has(command), - includeItoApiKey: ["auth", "find", "status"].includes(command), + includeItoApiKey: ["auth", "find", "status", "inference"].includes(command), includeItoEvals: command === "evals", }); } diff --git a/skills/ito-inference/SKILL.md b/skills/ito-inference/SKILL.md index 4a95b6c36..6972ffca9 100644 --- a/skills/ito-inference/SKILL.md +++ b/skills/ito-inference/SKILL.md @@ -1,119 +1,98 @@ --- name: ito-inference -description: Inspect the availability of model serving on a completed Itô compute booking and, when the canonical backend becomes available, hand off an explicitly confirmed serving manifest. Use after ito-compute has booked GPU nodes and the user asks for an OpenAI-compatible endpoint, ito-serve, hosted Kimi, or self-hosted open-weights inference. ECC implements no serving stack of its own. -metadata: - origin: ECC - status: scaffold - aliases: ito-serve, hosted-open-weights +description: Preflight, deploy, observe, and stop self-hosted open-weights inference on an existing eligible and entitled Itô compute cluster. Use when a user asks to deploy, host, serve, run inference, check health or logs, or tear down a Kimi or other open-weights model on Itô GPUs. --- # Itô Inference -`ito-inference` is the sole canonical ECC skill for inference serving on Itô -compute. Requests naming `ito-serve` route here; do not create or install a -second `ito-serve` skill. ECC never SSHes to nodes, downloads weights, launches -an engine, or exposes an endpoint; it never books, reserves, or spends. +Use only the reviewed canonical `ecc ito inference` lifecycle. This skill never +finds, books, reserves, resizes, purchases, or deletes compute capacity. Never +substitute SSH, systemd, a local vLLM runner, browser automation, or an invented +API/MCP tool. -## Current production boundary +## Establish intent and authority -Managed serving is unavailable today. The ECC bridge exposes only `login`, -`auth`, `find`, `status`, and explicitly gated `evals`. It has no `serve` verb. -The canonical runtime documents `inference` only as an unsupported compatibility -probe; ECC does not invoke or depend on it. The MCP surface exposes only auth, -find, and status. The locally enforceable guarantee is that ECC rejects `serve` -before resolving or spawning the credential-bearing canonical client. +Require the originating agent's completed Itô booking record: account, cluster +id, active status, nodes, GPU topology, service start, and entitlement context. +Collect an exact Hugging Face `owner/model`, a pinned 7–64 character hexadecimal +revision, `vllm`, quantization (`none`, `awq`, or `gptq`), and a bounded runtime +of 1–168 hours. Return to the originating agent for missing booking data; do not +select another cluster. -Therefore stop before authentication or any command invocation. Report the -missing capability and return to the originating agent. Never substitute a -local runner, SSH helper, browser workflow, purchase endpoint, or any untracked -local `ito-serve` draft. +Deployment and teardown are separate state-changing actions. Obtain explicit +user confirmation immediately before each. A compute booking confirmation does +not authorize serving, and a deployment confirmation does not authorize stop. -## Required entitlement +## Authenticate -When serving is implemented, its first gate is a server-verified completed -booking. Harness memory, an RFQ, a quote, node IPs, or SSH access are not proof -of entitlement. The backend must return fresh serving eligibility bound to the -authenticated account, booking, GPU topology, region, fabric, term, and model -policy. Expired, revoked, mismatched, incomplete, or already-released bookings -fail closed before confirmation. +Run `ecc ito auth --json`. If authentication is missing, run `ecc ito login`, +present the canonical device URL/code handoff, and return control to the +originating agent while the user completes authorization. Then retry +`ecc ito auth --json`. Never place a key, token, or device code in chat, files, +screenshots, or logs. Timeout, denial, or revocation is not success; repeat the +device flow and never silently switch accounts. -## Future CLI and API contract +## Preflight -The intended command name is `serve`; `inference` may remain only as an -explicitly deprecated compatibility alias after the production contract lands. -The future handoff must be equivalent to: +Run the fresh, read-only check: -```sh -ecc ito serve \ - --booking \ - --manifest \ - --confirmation-ref \ - --idempotency-key \ - --json +```text +ecc ito inference preflight --cluster --model --revision --engine vllm --quantization --max-runtime-hours <1-168> --json ``` -The reviewed manifest must identify the model revision, engine and version, -quantization, tensor/pipeline topology, endpoint exposure policy, artifact -checksums, storage ceiling, runtime limits, optional TTFT/TPOT objectives, and -maximum incremental cost. No raw API key, SSH key, node password, or bearer -token belongs in arguments, manifests, logs, MCP results, or chat. +Proceed only when the response says the authenticated account is entitled, the +exact cluster is active, every contracted node is present and up, service has +started, and the model configuration is valid. Record the returned exact +`estimated_cost_usd`. This is a conservative serving ceiling on already-booked +capacity; it is not authority to buy capacity. -The client must canonicalize the manifest path, reject symlinks, open a regular -file without following links, require appropriate ownership and restrictive -permissions, enforce a bounded size, and hash bytes from the opened descriptor. -That digest must exactly equal the digest bound into confirmation before any -workload mutation. A path swap, digest mismatch, oversized file, or mutable -unsafe file fails closed. +## Deploy -The canonical API—not ECC—must own workload creation and return structured JSON -with `ok`, `live_api_contacted`, `notice`, and either `data` or `error`. Serving -data must include stable booking, workload, manifest, and idempotency IDs plus a -state enum; it must not claim an endpoint is live until health and model checks -pass. Errors must include a stable code and safe message without secrets. +After explicit user confirmation, bind that confirmation to the exact current +preflight amount: -## Confirmation and execution gates +```text +ecc ito inference deploy --cluster --model --revision --engine vllm --quantization --max-runtime-hours <1-168> --confirm-cost-usd --json +``` -Before workload creation, require all of the following: +Do not round, edit, or reuse a stale amount. The canonical client supplies an +idempotency key and rejects missing/mismatched confirmation. On timeout or an +ambiguous response, do not deploy again blindly; query status using any returned +deployment id and otherwise report the ambiguity for operator reconciliation. -1. Fresh entitlement and serving eligibility from the canonical backend. -2. A reviewable immutable manifest and deterministic digest. -3. A separate single-use confirmation bound to account, action, manifest, and - cost, with a short expiry and replay protection. CLI arguments carry only an - opaque, non-authorizing confirmation reference; the server resolves and - consumes the bearer capability out of band. -4. A caller-supplied idempotency key reserved atomically with the workload. -5. Server-side fabric, capacity, model-policy, storage, and cost validation. +## Observe -Authentication is identity, not workload authority. A login, API key, quote, -or completed booking never substitutes for the serving confirmation. Inspection -and plan generation must not create a workload. Cancel and cleanup are separate -mutations with their own scoped confirmation and idempotency boundaries. +Read canonical state; never infer readiness from process exit alone: -## Lifecycle and recovery +```text +ecc ito inference status --deployment --json +ecc ito inference logs --deployment --limit <1-500> --json +``` -The production surface is incomplete until the same canonical client exposes -tenant-scoped status, logs, metrics, cancel, and cleanup operations. Every -operation needs bounded connect and overall timeouts, revocation-aware errors, -and structured output. After an ambiguous transport failure, query status by -the idempotency key before retrying; never create a second workload merely -because the first response was lost. A revoked credential stops polling and -returns control to the originating agent without starting login automatically. +Claim readiness only when status reports the pinned model revision, a healthy +deployment, and its endpoint. Redact credentials and signed endpoint material. +Logs are bounded and read-only. A provider timeout, invalid status, or revoked +authentication is an error, not cached success. -Only report `ready` after endpoint health, model identity, and canary inference -all pass. Report intermediate and terminal failure states honestly. Cleanup must -be observable and must not release or modify the underlying booking unless that -separate economic action was explicitly authorized. +## Stop and verify cleanup -## Proposed backend stages +After separate explicit teardown confirmation: -These stages describe the future backend, not code that exists in ECC: +```text +ecc ito inference stop --deployment --confirm --json +ecc ito inference status --deployment --json +``` -1. Verify entitlement, topology, fabric, and cost gates. -2. Fetch checksum-pinned weights into backend-managed storage. -3. Emit and validate a reviewable topology/engine plan. -4. Launch through the provider control plane, never direct root SSH from ECC. -5. Warm up, test health and model identity, run an SLO canary, then register the - endpoint and redacted configuration. +Report cleanup complete only after canonical state is `stopped` with no active +endpoint. Stopping serving must not cancel or delete the underlying compute +booking. If stop is ambiguous, retain `cleanup.required=true` and escalate for +status reconciliation rather than claiming completion. -Until every gate and lifecycle operation above exists in the canonical runtime, -this skill remains a fail-closed availability check and documentation handoff. +## Structured result + +Return one secret-free object with `status` (`PASS` or `BLOCK`), phase, account +and cluster ids, authenticated/eligible/entitled flags, model and revision, +estimated cost, deployment id, health, endpoint, bounded log evidence, cleanup +required/completed, error code, and next action. Distinguish code existence, +local test pass, merge, deployment, and observed live behavior. Use `PASS` only +for the lifecycle stage actually observed. diff --git a/tests/ci/ito-inference-skill.test.js b/tests/ci/ito-inference-skill.test.js index bbc02e920..d49e0be15 100644 --- a/tests/ci/ito-inference-skill.test.js +++ b/tests/ci/ito-inference-skill.test.js @@ -1,130 +1,77 @@ -/** - * Contract tests for the installable, fail-closed Itô inference handoff. - */ +/** Contract tests for the installable, fail-closed Itô inference skill. */ const assert = require("assert"); const fs = require("fs"); -const os = require("os"); const path = require("path"); -const { spawnSync } = require("child_process"); -const REPO_ROOT = path.join(__dirname, "..", ".."); +const ROOT = path.join(__dirname, "..", ".."); +const read = (file) => fs.readFileSync(path.join(ROOT, file), "utf8"); +const json = (file) => JSON.parse(read(file)); +const skill = read("skills/ito-inference/SKILL.md"); -function read(relativePath) { - return fs.readFileSync(path.join(REPO_ROOT, relativePath), "utf8"); -} - -function readJson(relativePath) { - return JSON.parse(read(relativePath)); -} - -function test(name, fn) { - try { - fn(); - console.log(` ✓ ${name}`); - return true; - } catch (error) { - console.log(` ✗ ${name}`); - console.error(` ${error.message}`); - return false; - } -} - -console.log("\n=== Testing Itô inference skill lifecycle ===\n"); - -const results = [ - test("uses the canonical serving trigger and fails closed while unavailable", () => { - const skill = read("skills/ito-inference/SKILL.md"); - assert.match(skill, /^name: ito-inference$/m); - assert.match(skill, /self-host|serve a model|OpenAI-compatible endpoint/i); - assert.match(skill, /requests naming .*ito-serve/i); - assert.match(skill, /completed booking/i); - assert.match(skill, /never books, reserves,\s+or spends/i); - assert.match(skill, /serving is unavailable today/i); - assert.match(skill, /report the\s+missing capability and return/i); - assert.match(skill, /stop before authentication/i); - assert.match(skill, /no `serve` verb/i); - assert.match(skill, /`inference`.*unsupported compatibility\s+probe/i); - assert.match(skill, /never substitute a\s+local runner, SSH helper, browser workflow, purchase endpoint/i); - assert.doesNotMatch(skill, /ssh\s+root@|serve-status\.sh/i); - for (const gate of [ - /server-verified completed\s+booking/i, - /fresh serving eligibility/i, - /single-use confirmation/i, - /account, action, manifest, and\s+cost/i, - /idempotency/i, - /status, logs, metrics, cancel, and cleanup/i, - /structured JSON/i, - /ambiguous transport/i, - /reject symlinks/i, - /without following links/i, - /hash bytes from the opened descriptor/i, - /digest must exactly equal/i, - ]) assert.match(skill, gate); - assert.match(skill, /--confirmation-ref /i); - assert.doesNotMatch(skill, /--confirmation-token|--api-key|--access-token/i); - }), - test("keeps unsupported serving outside the executable bridge", () => { - const bridge = read("scripts/ito.js"); - assert.match(bridge, /SUPPORTED_COMMANDS[^\n]+login[^\n]+auth[^\n]+find[^\n]+status[^\n]+evals/); - assert.doesNotMatch(bridge, /SUPPORTED_COMMANDS[^\n]+serve/); - assert.match(bridge, /Unsupported Itô command/); - - const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-ito-serve-reject-")); - try { - const canonicalDir = path.join(fixtureRoot, "cli", "ito-compute-cli", "dist", "bin"); - fs.mkdirSync(canonicalDir, { recursive: true }); - const marker = path.join(fixtureRoot, "spawned"); - const executable = path.join(canonicalDir, "ito.js"); - fs.writeFileSync(executable, `require("fs").writeFileSync(${JSON.stringify(marker)}, "spawned");\n`); - const result = spawnSync(process.execPath, [ - path.join(REPO_ROOT, "scripts", "ecc.js"), "ito", "serve", - "--booking", "booking_test", "--model", "model_test", - ], { - encoding: "utf8", - env: { ...process.env, ECC_ITO_CLI_EXECUTABLE: executable }, - }); - assert.notStrictEqual(result.status, 0); - assert.match(result.stderr, /Unsupported Itô command "serve"/); - assert.ok(!fs.existsSync(marker), "unsupported serve spawned the canonical child"); - } finally { - fs.rmSync(fixtureRoot, { recursive: true, force: true }); +const tests = [ + ["has trigger-rich two-field frontmatter", () => { + const frontmatter = skill.match(/^---\n([\s\S]*?)\n---/)[1]; + const keys = [...frontmatter.matchAll(/^([a-z_-]+):/gm)].map((match) => match[1]); + assert.deepStrictEqual(keys, ["name", "description"]); + for (const phrase of ["deploy", "host", "serve", "run inference", "check", "tear down"]) { + assert.match(frontmatter, new RegExp(phrase, "i")); } - }), - test("ships canonical inference through the existing opt-in compute module", () => { - const modules = readJson("manifests/install-modules.json").modules; - const module = modules.find((candidate) => candidate.id === "ito-compute"); - assert.ok(module, "ito-compute install module is missing"); - assert.deepStrictEqual(module.paths, [ - "skills/ito-compute", - "skills/ito-inference", - "skills/ito-training", - ]); - assert.deepStrictEqual(module.dependencies, ["platform-configs"]); + }], + ["requires originating booking, exact eligibility, and no capacity selection", () => { + for (const pattern of [ + /originating agent/, /completed Itô booking/, /cluster id/, /active status/, + /entitlement/, /do not\s+select another cluster/, /never\s+finds, books, reserves/, + ]) assert.match(skill, pattern); + }], + ["documents device login return and revocation recovery without secrets", () => { + for (const pattern of [ + /ecc ito auth --json/, /ecc ito login/, /device URL\/code handoff/, + /return control to the\s+originating agent/, /Timeout, denial, or revocation/, + /Never place a key, token, or device code/, + ]) assert.match(skill, pattern); + }], + ["documents the complete canonical lifecycle and validation bounds", () => { + for (const pattern of [ + /inference preflight/, /inference deploy/, /inference status/, + /inference logs/, /inference stop/, /owner\/model/, /pinned 7–64/, + /1–168 hours/, /exact current\s+preflight amount/, /idempotency key/, + ]) assert.match(skill, pattern); + }], + ["enforces separate confirmations, ambiguity recovery, and safe cleanup", () => { + for (const pattern of [ + /separate state-changing actions/, /explicit\s+user confirmation immediately before each/, + /do not deploy again blindly/, /provider timeout[\s\S]*not cached success/, + /separate explicit teardown confirmation/, /must not cancel or delete.*compute\s+booking/, + /cleanup.required=true/, + ]) assert.match(skill, pattern); + }], + ["defines structured results and honest capability-state distinctions", () => { + for (const field of [ + "status", "phase", "cluster", "authenticated", "eligible", "entitled", + "model", "revision", "estimated cost", "deployment id", "health", + "endpoint", "cleanup", "error code", "next action", + ]) assert.match(skill, new RegExp(field, "i")); + assert.match(skill, /code existence,\s+local test pass, merge, deployment, and observed live behavior/); + }], + ["registers package, module, dependency, capability, and full profile", () => { + const module = json("manifests/install-modules.json").modules.find((item) => item.id === "ito-inference"); + assert.deepStrictEqual(module.paths, ["skills/ito-inference"]); + assert.deepStrictEqual(module.dependencies, ["ito-compute"]); assert.strictEqual(module.defaultInstall, false); - assert.strictEqual(module.stability, "beta"); - - const components = readJson("manifests/install-components.json").components; - assert.deepStrictEqual( - components.find((candidate) => candidate.id === "capability:ito-compute"), - { - id: "capability:ito-compute", - family: "capability", - description: "Authenticated Itô GPU inventory, RFQ, status, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", - modules: ["ito-compute"], - } - ); - - const profiles = readJson("manifests/install-profiles.json").profiles; - assert.ok(profiles.full.modules.includes("ito-compute")); - - const packageFiles = readJson("package.json").files; - assert.ok(packageFiles.includes("skills/ito-inference/")); - assert.ok(packageFiles.includes("skills/ito-training/")); - }), + assert.strictEqual(module.stability, "experimental"); + assert.ok(module.targets.includes("codex") && module.targets.includes("kimi")); + assert.deepStrictEqual(json("manifests/install-components.json").components.find((item) => item.id === "capability:ito-inference").modules, ["ito-inference"]); + assert.ok(json("manifests/install-profiles.json").profiles.full.modules.includes("ito-inference")); + assert.ok(json("package.json").files.includes("skills/ito-inference/")); + }], ]; -const failed = results.filter((passed) => !passed).length; -console.log(`\nPassed: ${results.length - failed}`); +let failed = 0; +for (const [name, test] of tests) { + try { test(); console.log(` ✓ ${name}`); } + catch (error) { failed += 1; console.error(` ✗ ${name}: ${error.message}`); } +} +console.log(`Passed: ${tests.length - failed}`); console.log(`Failed: ${failed}`); -process.exit(failed > 0 ? 1 : 0); +process.exitCode = failed ? 1 : 0; diff --git a/tests/scripts/ito-cli-bridge.test.js b/tests/scripts/ito-cli-bridge.test.js index e6e4c7d2c..3329f8a8c 100644 --- a/tests/scripts/ito-cli-bridge.test.js +++ b/tests/scripts/ito-cli-bridge.test.js @@ -118,7 +118,7 @@ async function main() { const tests = [ ["forwards only the reviewed RFQ CLI surface to an explicit local executable", () => { - for (const command of ["login", "logout", "auth", "find", "status"]) { + for (const command of ["login", "logout", "auth", "find", "status", "inference"]) { const probe = makeItoProbe(); try { const result = runCli(["ito", command], { @@ -241,8 +241,8 @@ async function main() { fs.rmSync(probe.directory, { recursive: true, force: true }); } }], - ["forwards ITO_API_KEY directly to auth, find, and status without legacy mode", () => { - for (const command of ["auth", "find", "status"]) { + ["forwards ITO_API_KEY directly to authenticated runtime commands without legacy mode", () => { + for (const command of ["auth", "find", "status", "inference"]) { const probe = makeItoProbe(); try { const result = runCli(["ito", command], { @@ -256,6 +256,33 @@ async function main() { } } }], + ["passes inference lifecycle arguments and explicit confirmation boundaries unchanged", () => { + const probe = makeItoProbe(); + try { + const args = [ + "ito", "inference", "deploy", + "--cluster", "clu_1", + "--model", "Qwen/Qwen3-8B", + "--revision", "a1b2c3d4", + "--engine", "vllm", + "--max-runtime-hours", "2", + "--confirm-cost-usd", "32.00", + "--json", + ]; + const result = runCli(args, { + ECC_ITO_CLI_EXECUTABLE: probe.executable, + ITO_API_KEY: "ito_test_key", + OPENAI_API_KEY: "must-not-cross", + }); + assert.strictEqual(result.status, 0, result.stderr); + const invocation = readInvocation(probe); + assert.deepStrictEqual(invocation.argv, ["--json", ...args.slice(1, -1)]); + assert.strictEqual(invocation.env.ITO_API_KEY, "ito_test_key"); + assert.strictEqual(invocation.env.OPENAI_API_KEY, undefined); + } finally { + fs.rmSync(probe.directory, { recursive: true, force: true }); + } + }], ["streams device login output before completion and propagates its exit status", async () => { const probe = makeItoProbe(7); try { @@ -473,14 +500,14 @@ async function main() { ); }], ["rejects unsupported browser, paper, and execution operations before spawning", () => { - for (const command of ["rent", "lock", "purchase", "run", "inference", "mcp"]) { + for (const command of ["rent", "lock", "purchase", "run", "mcp"]) { const probe = makeItoProbe(); try { const result = runCli(["ito", command], { ECC_ITO_CLI_EXECUTABLE: probe.executable, }); assert.notStrictEqual(result.status, 0, command); - assert.match(result.stderr, /only login, logout, auth, find, status, and evals/i); + assert.match(result.stderr, /only login, logout, auth, find, status, evals, and inference/i); assert.ok(!fs.existsSync(probe.log), `${command} must not spawn the Itô CLI`); } finally { fs.rmSync(probe.directory, { recursive: true, force: true });