diff --git a/README.md b/README.md index 0a18dbeeb..25c14d345 100644 --- a/README.md +++ b/README.md @@ -375,11 +375,11 @@ Kimi Code discovers the installed `.kimi-code/AGENTS.md` instructions and `.kimi ### Itô compute CLI bridge -`ecc ito` delegates to the separately installed canonical Itô client; ECC does not maintain a second API client. `ecc ito login [--no-browser]` performs device authorization, opens the Itô verification page by default, and persists a device token in macOS Keychain; `--no-browser` suppresses the page handoff. ECC itself does no browser automation. `ecc ito auth` is validation-only and rejects `--no-browser`. The available operations are `ecc ito login`, `ecc ito auth`, `ecc ito find`, `ecc ito status`, and the separately gated `ecc ito evals`. The matching MCP tools remain `ito_auth`, `ito_find`, and `ito_status`; `ito_auth` validates existing credentials and node qualification is CLI-only. +`ecc ito` delegates to the separately installed canonical Itô client; ECC does not maintain a second API client. `ecc ito login [--no-browser]` performs device authorization, opens the Itô verification page by default, and persists a device token in macOS Keychain; `--no-browser` suppresses the page handoff. ECC itself does no browser automation. `ecc ito auth` is validation-only and rejects `--no-browser`. The bridge validates the installed local-only `ito.cli.capabilities.v1` contract before every operation. Available operations include `ecc ito capabilities`, `login`, `logout`, `auth`, `find`, `status`, the no-side-effect `workload-status`, and the separately gated `evals`. The matching MCP tools remain `ito_auth`, `ito_find`, and `ito_status`; `ito_auth` validates existing credentials and node qualification is CLI-only. -The `ito-compute-cli` package is currently unpublished. Build it locally from the Itô runtime repo (private while the desk hardens; design partners get access) under `cli/ito-compute-cli`, run `npm ci` and `npm run check`, then set `ECC_ITO_CLI_EXECUTABLE` to that build's absolute `dist/bin/ito.js` path. Login never inherits `ITO_API_KEY`; auth, find, and status forward `ITO_API_KEY` directly when configured, and `ITO_AUTH_MODE=legacy` is not required. `ecc ito logout` revokes the current device credential and retains its local copy if remote revocation cannot be confirmed. Device tokens use macOS Keychain by default; explicit file fallback must retain owner-only directory/file permissions. ECC does not discover this credential-bearing client through `PATH`. See the [`ito-compute` skill](skills/ito-compute/SKILL.md) for the full RFQ authority and MCP setup contract. +The `ito-compute-cli` package is currently unpublished. Build it locally from the Itô runtime repo (private while the desk hardens; design partners get access) under `cli/ito-compute-cli`, run `npm ci` and `npm run check`, then set `ECC_ITO_CLI_EXECUTABLE` to that build's absolute `dist/bin/ito.js` path. Login never inherits `ITO_API_KEY`; auth, find, status, and workload-status forward `ITO_API_KEY` directly when configured, and `ITO_AUTH_MODE=legacy` is not required. `ecc ito logout` revokes the current device credential and retains its local copy if remote revocation cannot be confirmed. Device tokens use macOS Keychain by default; explicit file fallback must retain owner-only directory/file permissions. ECC does not discover this credential-bearing client through `PATH`. See the [`ito-compute` 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 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. +`find` submits a live authenticated RFQ. It does not reserve capacity. `workload-status` can inspect an already existing entitled run, but ECC blocks workload start, cancel, cleanup, quote lock, purchase, and inference effects even if the canonical CLI advertises them. `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 never replaces a missing client or failed live call with a local result. ## Advanced Install Options diff --git a/docs/design/ecc-ito-compute-integration.md b/docs/design/ecc-ito-compute-integration.md index c346428a9..ce1144b89 100644 --- a/docs/design/ecc-ito-compute-integration.md +++ b/docs/design/ecc-ito-compute-integration.md @@ -103,7 +103,15 @@ Itô platform. ECC adds no shadow store. ## Unsupported in this slice -ECC exposes no quote lock, purchase, workload execution, or inference command. +ECC validates the canonical CLI's local-only `ito.cli.capabilities.v1` manifest +before forwarding. Supported commands with `side_effect: none` and `authority: +none` are derived from that manifest; this currently +adds `capabilities` and read-only `workload-status` without a handwritten +allowlist update. + +ECC exposes no quote lock, purchase, workload start, cancel, cleanup, or +inference command. Those advertised effects remain outside the explicit ECC +policy. Node qualification is live-only through the separately gated canonical adapter; the ECC bridge does not expose its paper fixture mode. @@ -164,7 +172,7 @@ after review. The local contract suite proves: -- only the six supported operations spawn; +- capability discovery plus seven reviewed target operations spawn; - RFQ arguments are forwarded without economic reinterpretation; - only approved Itô runtime or isolated node-qualification variables cross the process boundary; diff --git a/manifests/install-components.json b/manifests/install-components.json index 971f86607..74ba99829 100644 --- a/manifests/install-components.json +++ b/manifests/install-components.json @@ -197,7 +197,7 @@ { "id": "capability:ito-compute", "family": "capability", - "description": "Authenticated Itô GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", + "description": "Capability-validated Itô GPU inventory, RFQ, status, read-only workload inspection, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", "modules": [ "ito-compute" ] diff --git a/scripts/ecc.js b/scripts/ecc.js index 8a92fa302..e20502261 100755 --- a/scripts/ecc.js +++ b/scripts/ecc.js @@ -275,6 +275,7 @@ function runCommand(commandName, args) { env: commandName === 'ito' ? { ...createSafeItoInvocationEnvironment(process.env, args, { + deferCapabilityPolicy: true, includeControls: true, }), } diff --git a/scripts/ito.js b/scripts/ito.js index e981d85ee..c781c1733 100755 --- a/scripts/ito.js +++ b/scripts/ito.js @@ -6,11 +6,15 @@ const fs = require("fs"); const path = require("path"); const { spawnSync } = require("child_process"); const { + createSafeItoEnvironment, createSafeItoInvocationEnvironment, getInvocationCommand, } = require("./lib/ito-environment"); +const { + authorizeEccCapability, + parseItoCapabilities, +} = require("./lib/ito-capabilities"); -const SUPPORTED_COMMANDS = Object.freeze(["login", "logout", "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([ @@ -21,6 +25,7 @@ const CANONICAL_ENTRY_SEGMENTS = Object.freeze([ ]); const EXECUTABLE_OVERRIDE = "ECC_ITO_CLI_EXECUTABLE"; const MAX_OUTPUT_BYTES = 10 * 1024 * 1024; +const CAPABILITY_TIMEOUT_MS = 10 * 1000; const NODE_QUALIFICATION_TIMEOUT_MS = 31 * 60 * 1000; function showHelp() { @@ -33,8 +38,10 @@ Usage: ecc ito auth ecc ito find ecc ito status + ecc ito workload-status + ecc ito capabilities ecc ito evals --cluster --live-sixtytwo --nodes --config-dir - ecc ito --json + 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 @@ -50,6 +57,10 @@ 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. + - "workload-status" reads an existing entitled workload without changing it. + - The installed canonical CLI must advertise every command through the local-only + ito.cli.capabilities.v1 contract. ECC derives read-only forwarding from that + contract and blocks workload start, cancel, cleanup, purchase, and unknown effects. - "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. @@ -162,10 +173,8 @@ function parseArgs(argv, environment = process.env) { } const withoutJson = args.filter((value) => value !== "--json"); 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.` - ); + if (typeof command !== "string" || !/^[a-z][a-z0-9-]{0,63}$/.test(command)) { + throw new Error(`Invalid Itô command ${JSON.stringify(command || "(missing)")}.`); } if (command === "auth" && withoutJson.includes("--no-browser")) { throw new Error("--no-browser is valid only for ecc ito login; auth is validation-only."); @@ -176,6 +185,7 @@ function parseArgs(argv, environment = process.env) { return Object.freeze({ help: false, + command, invocationArgs: Object.freeze([ ...(jsonIndexes.length === 1 ? ["--json"] : []), command, @@ -264,7 +274,28 @@ function buildInvocation(executable, args) { }); } -function invokeIto(executable, args, environment = process.env) { +function discoverItoCapabilities(executable, environment = process.env) { + const invocation = buildInvocation(executable, ["capabilities", "--json"]); + const result = spawnSync(invocation.executable, invocation.args, { + cwd: process.cwd(), + encoding: "utf8", + env: { ...createSafeItoEnvironment(environment) }, + stdio: ["ignore", "pipe", "pipe"], + maxBuffer: MAX_OUTPUT_BYTES, + timeout: CAPABILITY_TIMEOUT_MS, + shell: false, + windowsHide: true, + }); + if (result.error) { + throw new Error(`The local Itô capability contract could not be read: ${result.error.message}`); + } + if (result.status !== 0) { + throw new Error("The local Itô CLI rejected credential-free capability discovery."); + } + return parseItoCapabilities(result.stdout || ""); +} + +function invokeIto(executable, args, environment = process.env, capability) { const invocation = buildInvocation(executable, args); const command = getInvocationCommand(args); const isNodeQualification = command === "evals"; @@ -274,7 +305,9 @@ function invokeIto(executable, args, environment = process.env) { encoding: "utf8", // Keep policy helpers immutable for callers, but give child-process // instrumentation its own mutable copy (for example NODE_V8_COVERAGE). - env: { ...createSafeItoInvocationEnvironment(environment, args) }, + env: { + ...createSafeItoInvocationEnvironment(environment, args, { capability }), + }, stdio: isDeviceLogin ? "inherit" : ["pipe", "pipe", "pipe"], maxBuffer: MAX_OUTPUT_BYTES, timeout: isNodeQualification ? NODE_QUALIFICATION_TIMEOUT_MS : undefined, @@ -302,7 +335,9 @@ function main(argv = process.argv.slice(2), environment = process.env) { return 0; } const executable = resolveItoExecutable(environment); - return invokeIto(executable, parsed.invocationArgs, environment); + const manifest = discoverItoCapabilities(executable, environment); + const capability = authorizeEccCapability(manifest, parsed.command); + return invokeIto(executable, parsed.invocationArgs, environment, capability); } catch (error) { console.error(`Error: ${error.message}`); return 1; @@ -316,10 +351,11 @@ if (require.main === module) { module.exports = Object.freeze({ CANONICAL_PACKAGE_PATH, CANONICAL_REPOSITORY, + CAPABILITY_TIMEOUT_MS, EXECUTABLE_OVERRIDE, NODE_QUALIFICATION_TIMEOUT_MS, - SUPPORTED_COMMANDS, buildInvocation, + discoverItoCapabilities, invokeIto, main, parseArgs, diff --git a/scripts/lib/ito-capabilities.js b/scripts/lib/ito-capabilities.js new file mode 100644 index 000000000..1ba7a21f6 --- /dev/null +++ b/scripts/lib/ito-capabilities.js @@ -0,0 +1,209 @@ +"use strict"; + +const CONTRACT_VERSION = "ito.cli.capabilities.v1"; +const MAX_COMMANDS = 64; +const COMMAND_PATTERN = /^[a-z][a-z0-9-]{0,63}$/; +const ENUMS = Object.freeze({ + availability: new Set(["supported", "unsupported"]), + auth: new Set(["none", "required", "device_bootstrap"]), + network: new Set(["none", "ito_api", "explicit_nodes"]), + side_effect: new Set([ + "none", + "credential_write", + "credential_revoke", + "rfq_submit", + "workload_start", + "workload_cancel", + "workload_cleanup", + "provisioning_reconcile", + "node_qualification", + ]), + authority: new Set([ + "none", + "device_owner", + "buyer_rfq", + "entitled_workload", + "named_node_operator", + ]), +}); +const EXPLICIT_EFFECT_POLICY = Object.freeze({ + login: Object.freeze({ + auth: "device_bootstrap", + network: "ito_api", + side_effect: "credential_write", + authority: "device_owner", + }), + logout: Object.freeze({ + auth: "none", + network: "ito_api", + side_effect: "credential_revoke", + authority: "device_owner", + }), + find: Object.freeze({ + auth: "required", + network: "ito_api", + side_effect: "rfq_submit", + authority: "buyer_rfq", + }), + status: Object.freeze({ + auth: "required", + network: "ito_api", + side_effect: "provisioning_reconcile", + authority: "buyer_rfq", + }), + evals: Object.freeze({ + auth: "none", + network: "explicit_nodes", + side_effect: "node_qualification", + authority: "named_node_operator", + }), +}); + +function assertPlainObject(value, label) { + if (!value || typeof value !== "object" || Array.isArray(value)) { + throw new Error(`Invalid Itô capability contract: ${label} must be an object.`); + } + return value; +} + +function assertExactKeys(value, expected, label) { + const actual = Object.keys(value).sort(); + const wanted = [...expected].sort(); + if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) { + throw new Error(`Invalid Itô capability contract: ${label} has unknown or missing fields.`); + } +} + +function assertEnum(value, field) { + if (!ENUMS[field].has(value)) { + throw new Error(`Invalid Itô capability contract: unsupported ${field}.`); + } +} + +function parseItoCapabilities(stdout) { + let envelope; + try { + envelope = JSON.parse(stdout); + } catch { + throw new Error("Invalid Itô capability contract: canonical CLI did not emit JSON."); + } + assertPlainObject(envelope, "envelope"); + assertExactKeys(envelope, ["ok", "live_api_contacted", "notice", "data"], "envelope"); + if ( + envelope.ok !== true + || envelope.live_api_contacted !== false + || typeof envelope.notice !== "string" + || envelope.notice.length === 0 + ) { + throw new Error("Invalid Itô capability contract: discovery must be local-only success."); + } + + const data = assertPlainObject(envelope.data, "data"); + assertExactKeys( + data, + ["contract_version", "cli", "commands", "mcp_tools", "output_contract"], + "data", + ); + if (data.contract_version !== CONTRACT_VERSION) { + throw new Error(`Unsupported Itô capability contract ${JSON.stringify(data.contract_version)}.`); + } + + const cli = assertPlainObject(data.cli, "cli"); + assertExactKeys(cli, ["name", "version", "private"], "cli"); + if ( + cli.name !== "ito-compute-cli" + || typeof cli.version !== "string" + || !cli.version + || cli.private !== true + ) { + throw new Error("Invalid Itô capability contract: unexpected private CLI identity."); + } + + const outputContract = assertPlainObject(data.output_contract, "output_contract"); + assertExactKeys( + outputContract, + ["json_success_envelope", "errors_on_stderr", "unsupported_commands_contact_nothing"], + "output_contract", + ); + if ( + outputContract.json_success_envelope !== true + || outputContract.errors_on_stderr !== true + || outputContract.unsupported_commands_contact_nothing !== true + ) { + throw new Error("Invalid Itô capability contract: required output guarantees are absent."); + } + + if ( + !Array.isArray(data.mcp_tools) + || data.mcp_tools.join("\n") !== "ito_auth\nito_find\nito_status" + ) { + throw new Error("Invalid Itô capability contract: MCP tool surface changed."); + } + if (!Array.isArray(data.commands) || data.commands.length < 1 || data.commands.length > MAX_COMMANDS) { + throw new Error("Invalid Itô capability contract: commands must be a bounded non-empty array."); + } + + const seen = new Set(); + const commands = data.commands.map((entry, index) => { + const command = assertPlainObject(entry, `commands[${index}]`); + assertExactKeys( + command, + ["name", "availability", "auth", "network", "side_effect", "authority"], + `commands[${index}]`, + ); + if (typeof command.name !== "string" || !COMMAND_PATTERN.test(command.name)) { + throw new Error("Invalid Itô capability contract: malformed command name."); + } + if (seen.has(command.name)) { + throw new Error(`Invalid Itô capability contract: duplicate command ${command.name}.`); + } + seen.add(command.name); + for (const field of ["availability", "auth", "network", "side_effect", "authority"]) { + assertEnum(command[field], field); + } + return Object.freeze({ ...command }); + }); + + return Object.freeze({ + contract_version: CONTRACT_VERSION, + cli: Object.freeze({ ...cli }), + commands: Object.freeze(commands), + mcp_tools: Object.freeze([...data.mcp_tools]), + output_contract: Object.freeze({ ...outputContract }), + }); +} + +function commandCapability(manifest, requestedCommand) { + const capability = manifest.commands.find(({ name }) => name === requestedCommand); + if (!capability || capability.availability !== "supported") { + throw new Error( + `The installed canonical Itô CLI does not advertise ${JSON.stringify(requestedCommand)} as supported.`, + ); + } + return capability; +} + +function authorizeEccCapability(manifest, requestedCommand) { + const capability = commandCapability(manifest, requestedCommand); + const policy = EXPLICIT_EFFECT_POLICY[requestedCommand]; + if (policy) { + if (Object.entries(policy).every(([field, expected]) => capability[field] === expected)) { + return capability; + } + throw new Error( + `Itô command ${JSON.stringify(requestedCommand)} is outside ECC's safe policy: ${capability.side_effect}.`, + ); + } + if (capability.side_effect === "none" && capability.authority === "none") { + return capability; + } + throw new Error( + `Itô command ${JSON.stringify(requestedCommand)} is outside ECC's safe policy: ${capability.side_effect}.`, + ); +} + +module.exports = Object.freeze({ + CONTRACT_VERSION, + authorizeEccCapability, + parseItoCapabilities, +}); diff --git a/scripts/lib/ito-environment.js b/scripts/lib/ito-environment.js index e9a6c909c..955015cc2 100644 --- a/scripts/lib/ito-environment.js +++ b/scripts/lib/ito-environment.js @@ -94,11 +94,27 @@ function createSafeItoInvocationEnvironment( options = {}, ) { const command = getInvocationCommand(args); + const capability = options.capability; + const deferCapabilityPolicy = options.deferCapabilityPolicy === true; + const includeItoRuntime = deferCapabilityPolicy + ? command !== "evals" + : capability + ? capability.network === "ito_api" + : ITO_RUNTIME_COMMANDS.has(command); + const includeItoApiKey = deferCapabilityPolicy + ? !["login", "logout", "evals"].includes(command) + : capability + ? capability.auth === "required" + : ["auth", "find", "status"].includes(command); return createSafeItoEnvironment(source, { includeControls: options.includeControls === true, - includeItoRuntime: ITO_RUNTIME_COMMANDS.has(command), - includeItoApiKey: ["auth", "find", "status"].includes(command), - includeItoEvals: command === "evals", + includeItoRuntime, + includeItoApiKey, + includeItoEvals: deferCapabilityPolicy + ? command === "evals" + : capability + ? capability.network === "explicit_nodes" + : command === "evals", }); } diff --git a/skills/ito-compute/SKILL.md b/skills/ito-compute/SKILL.md index c81bd97a3..494176157 100644 --- a/skills/ito-compute/SKILL.md +++ b/skills/ito-compute/SKILL.md @@ -1,6 +1,6 @@ --- name: ito-compute -description: Query live GPU inventory, submit an authenticated Itô fixed-rate RFQ, inspect RFQ or procurement status, revoke device credentials, and run explicitly gated node qualification through the separately installed canonical CLI. Use when a user asks to find H100/H200 capacity, request a fixed compute rate, check Itô compute status, validate GPU nodes, revoke Itô access, or rent or purchase GPU compute and needs the supported boundary explained. +description: Query live GPU inventory, submit an authenticated Itô fixed-rate RFQ, inspect RFQ, procurement, or existing workload status, revoke device credentials, and run explicitly gated node qualification through the separately installed canonical CLI. Use when a user asks to find H100/H200 capacity, request a fixed compute rate, check Itô compute or workload status, validate GPU nodes, revoke Itô access, or rent or purchase GPU compute and needs the supported boundary explained. --- # Itô Compute @@ -29,10 +29,17 @@ Set `ECC_ITO_CLI_EXECUTABLE` to the explicit absolute built entry: ECC never discovers this credential-bearing client through `PATH`. `ecc ito login` performs device authorization and never inherits `ITO_API_KEY`. -The validation-only `auth`, plus `find` and `status`, forward `ITO_API_KEY` -directly when configured; `ITO_AUTH_MODE=legacy` is not required. Never put a +The validation-only `auth`, plus `find`, `status`, and `workload-status`, forward +`ITO_API_KEY` directly when configured; `ITO_AUTH_MODE=legacy` is not required. Never put a key or token in arguments, tracked files, MCP results, logs, or chat. +Before every forwarded operation, ECC invokes `capabilities --json` with a +credential-free environment and validates the closed `ito.cli.capabilities.v1` +contract. Run `ecc ito capabilities --json` to inspect that installed contract +directly. ECC automatically accepts supported no-side-effect commands and keeps +explicit policy only for login, logout, RFQ submission, and node qualification. +Workload start, cancel, and cleanup effects remain blocked. + ## CLI workflow 1. Run `ecc ito login` before the first operation. ECC delegates this to the @@ -73,7 +80,9 @@ key or token in arguments, tracked files, MCP results, logs, or chat. 5. Run `ecc ito status` to inspect RFQs and procurement orders. After an ambiguous transport failure, check status before repeating `find`. -6. Run `ecc ito logout` when the user explicitly asks to revoke this device. +6. Run `ecc ito workload-status ` to inspect an already existing + entitled workload without starting, stopping, or cleaning it up. +7. Run `ecc ito logout` when the user explicitly asks to revoke this device. The canonical CLI keeps the local credential when remote revocation fails so the operator can retry; never delete the token manually as a substitute. @@ -144,8 +153,10 @@ operate it as a substitute for a missing CLI capability. ## Unsupported operations -The supported client surface cannot lock quotes, reserve capacity, execute -workloads, or serve inference. The MCP server does not expose qualification; +The ECC bridge cannot lock quotes, reserve capacity, start, cancel, or clean up +workloads, or serve inference. It may inspect an already existing workload when +the installed v1 capability contract advertises that no-side-effect command. +The MCP server does not expose qualification; use the explicit CLI command above. Do not invent additional tools or a purchase path. Do not substitute a browser or fixture when the local CLI is missing or a live operation fails. Report the missing capability and stop. diff --git a/tests/ci/ito-compute-skill.test.js b/tests/ci/ito-compute-skill.test.js index 9df529456..a6f513e1a 100644 --- a/tests/ci/ito-compute-skill.test.js +++ b/tests/ci/ito-compute-skill.test.js @@ -40,6 +40,8 @@ function main() { "ecc ito auth", "ecc ito find", "ecc ito status", + "ecc ito workload-status", + "ecc ito capabilities", "ecc ito evals", ]) { assert.match(skill, new RegExp(command.replace(" ", "\\s+"))); @@ -67,6 +69,8 @@ function main() { assert.match(skill, /--no-browser/); assert.match(skill, /macOS Keychain/i); assert.match(skill, /(?:auth|find|status).*ITO_API_KEY/i); + assert.match(skill, /ito\.cli\.capabilities\.v1/); + assert.match(skill, /workload start, cancel, and cleanup effects remain blocked/i); assert.match(skill, /ITO_AUTH_MODE=legacy[^.]*not required/i); assert.match(skill, /ECC (?:itself )?(?:does|performs) no browser automation/i); assert.match(skill, /ITO_ENABLE_SIXTYTWO_LIVE/); @@ -93,6 +97,8 @@ function main() { assert.doesNotMatch(source, /ecc ito auth --no-browser/i, relativePath); assert.match(source, /ITO_API_KEY.*(?:auth|find|status)/is, relativePath); assert.match(source, /ITO_AUTH_MODE=legacy[^.]*not required/i, relativePath); + assert.match(source, /ito\.cli\.capabilities\.v1/i, relativePath); + assert.match(source, /workload-status/i, relativePath); } }], ["registers one opt-in install module and capability", () => { @@ -117,7 +123,7 @@ function main() { { id: "capability:ito-compute", family: "capability", - description: "Authenticated Itô GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", + description: "Capability-validated Itô GPU inventory, RFQ, status, read-only workload inspection, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", modules: ["ito-compute"], } ); diff --git a/tests/ci/ito-inference-skill.test.js b/tests/ci/ito-inference-skill.test.js index 0899158c7..a8a5e489c 100644 --- a/tests/ci/ito-inference-skill.test.js +++ b/tests/ci/ito-inference-skill.test.js @@ -4,9 +4,8 @@ const assert = require("assert"); const fs = require("fs"); -const os = require("os"); const path = require("path"); -const { spawnSync } = require("child_process"); +const { authorizeEccCapability } = require("../../scripts/lib/ito-capabilities"); const REPO_ROOT = path.join(__dirname, "..", ".."); @@ -64,32 +63,19 @@ const results = [ 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 }); - } + test("keeps workload-serving effects outside the executable policy", () => { + const capability = Object.freeze({ + name: "serve", + availability: "supported", + auth: "required", + network: "ito_api", + side_effect: "workload_start", + authority: "entitled_workload", + }); + assert.throws( + () => authorizeEccCapability({ commands: [capability] }, "serve"), + /outside ECC's safe policy: workload_start/, + ); }), test("ships canonical inference through the existing opt-in compute module", () => { const modules = readJson("manifests/install-modules.json").modules; @@ -110,7 +96,7 @@ const results = [ { id: "capability:ito-compute", family: "capability", - description: "Authenticated Itô GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", + description: "Capability-validated Itô GPU inventory, RFQ, status, read-only workload inspection, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", modules: ["ito-compute"], } ); diff --git a/tests/ci/ito-training-skill.test.js b/tests/ci/ito-training-skill.test.js index fd0a4b30e..6acd889bb 100644 --- a/tests/ci/ito-training-skill.test.js +++ b/tests/ci/ito-training-skill.test.js @@ -7,9 +7,8 @@ const assert = require("assert"); const fs = require("fs"); -const os = require("os"); const path = require("path"); -const { spawnSync } = require("child_process"); +const { authorizeEccCapability } = require("../../scripts/lib/ito-capabilities"); const REPO_ROOT = path.join(__dirname, "..", ".."); @@ -77,32 +76,19 @@ test("labels backend stages as future and keeps eval gates human-honest", () => assert.match(skill, /Loss-spike restart is a proposed, human-gated action/i); }); -test("keeps unsupported training 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]+train/); - assert.match(bridge, /Unsupported Itô command/); - - const fixtureRoot = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-ito-train-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", "train", - "--booking", "booking_test", "--model-size", "8B", - ], { - encoding: "utf8", - env: { ...process.env, ECC_ITO_CLI_EXECUTABLE: executable }, - }); - assert.notStrictEqual(result.status, 0); - assert.match(result.stderr, /Unsupported Itô command "train"/); - assert.ok(!fs.existsSync(marker), "unsupported train spawned the canonical child"); - } finally { - fs.rmSync(fixtureRoot, { recursive: true, force: true }); - } +test("keeps workload-training effects outside the executable policy", () => { + const capability = Object.freeze({ + name: "train", + availability: "supported", + auth: "required", + network: "ito_api", + side_effect: "workload_start", + authority: "entitled_workload", + }); + assert.throws( + () => authorizeEccCapability({ commands: [capability] }, "train"), + /outside ECC's safe policy: workload_start/, + ); }); test("ships through the existing opt-in compute module and npm package", () => { diff --git a/tests/scripts/ito-cli-bridge.test.js b/tests/scripts/ito-cli-bridge.test.js index e6e4c7d2c..2de6c9bbe 100644 --- a/tests/scripts/ito-cli-bridge.test.js +++ b/tests/scripts/ito-cli-bridge.test.js @@ -23,6 +23,10 @@ const { getInvocationCommand, ITO_RUNTIME_ENVIRONMENT_KEYS, } = require("../../scripts/lib/ito-environment"); +const { + authorizeEccCapability, + parseItoCapabilities, +} = require("../../scripts/lib/ito-capabilities"); function runCli(args, environment = {}) { return spawnSync(process.execPath, [ECC_SCRIPT, ...args], { @@ -64,9 +68,44 @@ function runCliAndObserveFirstOutput(args, environment = {}) { }); } -function makeItoProbe(exitCode = 0) { +function capabilityEnvelope(commands) { + return { + ok: true, + live_api_contacted: false, + notice: "Static CLI capability contract; no credential or network access occurred.", + data: { + contract_version: "ito.cli.capabilities.v1", + cli: { name: "ito-compute-cli", version: "0.1.1", private: true }, + commands, + mcp_tools: ["ito_auth", "ito_find", "ito_status"], + output_contract: { + json_success_envelope: true, + errors_on_stderr: true, + unsupported_commands_contact_nothing: true, + }, + }, + }; +} + +const DEFAULT_CAPABILITY_COMMANDS = Object.freeze([ + { name: "capabilities", availability: "supported", auth: "none", network: "none", side_effect: "none", authority: "none" }, + { name: "login", availability: "supported", auth: "device_bootstrap", network: "ito_api", side_effect: "credential_write", authority: "device_owner" }, + { name: "logout", availability: "supported", auth: "none", network: "ito_api", side_effect: "credential_revoke", authority: "device_owner" }, + { name: "auth", availability: "supported", auth: "required", network: "ito_api", side_effect: "none", authority: "none" }, + { name: "find", availability: "supported", auth: "required", network: "ito_api", side_effect: "rfq_submit", authority: "buyer_rfq" }, + { name: "status", availability: "supported", auth: "required", network: "ito_api", side_effect: "provisioning_reconcile", authority: "buyer_rfq" }, + { name: "serve", availability: "supported", auth: "required", network: "ito_api", side_effect: "workload_start", authority: "entitled_workload" }, + { name: "train", availability: "supported", auth: "required", network: "ito_api", side_effect: "workload_start", authority: "entitled_workload" }, + { name: "workload-status", availability: "supported", auth: "required", network: "ito_api", side_effect: "none", authority: "none" }, + { name: "workload-cancel", availability: "supported", auth: "required", network: "ito_api", side_effect: "workload_cancel", authority: "entitled_workload" }, + { name: "workload-cleanup", availability: "supported", auth: "required", network: "ito_api", side_effect: "workload_cleanup", authority: "entitled_workload" }, + { name: "evals", availability: "supported", auth: "none", network: "explicit_nodes", side_effect: "node_qualification", authority: "named_node_operator" }, +]); + +function makeItoProbe(exitCode = 0, options = {}) { const directory = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-ito-cli-")); const log = path.join(directory, "invocation.json"); + const capabilityLog = path.join(directory, "capability-invocation.json"); const script = path.join( directory, "ito-cloud-runtime", @@ -84,6 +123,13 @@ function makeItoProbe(exitCode = 0) { `#!${process.execPath}`, '"use strict";', 'const fs = require("fs");', + 'const argv = process.argv.slice(2);', + 'const command = argv.filter((value) => value !== "--json")[0];', + 'if (command === "capabilities") {', + ` fs.writeFileSync(${JSON.stringify(capabilityLog)}, JSON.stringify({ argv, env: process.env }));`, + ` process.stdout.write(${JSON.stringify(`${JSON.stringify(options.capabilityEnvelope ?? capabilityEnvelope(DEFAULT_CAPABILITY_COMMANDS))}\n`)});`, + ' process.exit(0);', + '}', `fs.writeFileSync(${JSON.stringify(log)}, JSON.stringify({ argv: process.argv.slice(2), env: process.env }));`, 'process.stdout.write(`ito-probe:${process.argv.slice(2).join("|")}\\n`);', 'process.stderr.write("ito-probe-stderr\\n");', @@ -94,7 +140,7 @@ function makeItoProbe(exitCode = 0) { if (process.platform !== "win32") { fs.chmodSync(script, 0o755); } - return Object.freeze({ directory, executable, log }); + return Object.freeze({ capabilityLog, directory, executable, log }); } function readInvocation(probe) { @@ -132,6 +178,92 @@ async function main() { } } }], + ["derives a newly supported read-only command from a credential-free capability probe", () => { + const probe = makeItoProbe(); + try { + const result = runCli(["ito", "workload-status", "run_123", "--json"], { + ECC_ITO_CLI_EXECUTABLE: probe.executable, + ITO_API_KEY: "ito_test_key", + ITO_API_URL: "https://compute.example.test", + AWS_SECRET_ACCESS_KEY: "must-not-cross", + }); + assert.strictEqual(result.status, 0, result.stderr); + const capabilityInvocation = JSON.parse(fs.readFileSync(probe.capabilityLog, "utf8")); + assert.deepStrictEqual(capabilityInvocation.argv, ["capabilities", "--json"]); + assert.strictEqual(capabilityInvocation.env.ITO_API_KEY, undefined); + assert.strictEqual(capabilityInvocation.env.ITO_API_URL, undefined); + assert.strictEqual(capabilityInvocation.env.AWS_SECRET_ACCESS_KEY, undefined); + assert.strictEqual(capabilityInvocation.env.ECC_ITO_CLI_EXECUTABLE, undefined); + const targetInvocation = readInvocation(probe); + assert.deepStrictEqual(targetInvocation.argv, ["--json", "workload-status", "run_123"]); + assert.strictEqual(targetInvocation.env.ITO_API_KEY, "ito_test_key"); + assert.strictEqual(targetInvocation.env.ITO_API_URL, "https://compute.example.test"); + } finally { + fs.rmSync(probe.directory, { recursive: true, force: true }); + } + }], + ["blocks advertised workload mutations after capability discovery", () => { + const probe = makeItoProbe(); + try { + const result = runCli([ + "ito", "serve", "--model", "org/model", "--entitlement", "ent_123", "--confirm", + ], { ECC_ITO_CLI_EXECUTABLE: probe.executable }); + assert.notStrictEqual(result.status, 0); + assert.match(result.stderr, /workload_start|safe policy/i); + assert.ok(fs.existsSync(probe.capabilityLog), "capability discovery did not run"); + assert.ok(!fs.existsSync(probe.log), "blocked workload command reached the target invocation"); + } finally { + fs.rmSync(probe.directory, { recursive: true, force: true }); + } + }], + ["requires exact tuples for every explicitly reviewed effectful command", () => { + for (const commandName of ["login", "logout", "find", "evals"]) { + const commands = DEFAULT_CAPABILITY_COMMANDS.map((command) => ( + command.name === commandName + ? { ...command, auth: "none", network: "none", side_effect: "none", authority: "none" } + : command + )); + const manifest = parseItoCapabilities(JSON.stringify(capabilityEnvelope(commands))); + assert.throws( + () => authorizeEccCapability(manifest, commandName), + /outside ECC's safe policy/i, + commandName, + ); + } + }], + ["allows status only under the exact provisioning-reconcile tuple", () => { + const manifest = parseItoCapabilities(JSON.stringify(capabilityEnvelope(DEFAULT_CAPABILITY_COMMANDS))); + assert.strictEqual(authorizeEccCapability(manifest, "status").side_effect, "provisioning_reconcile"); + const mislabeled = DEFAULT_CAPABILITY_COMMANDS.map((command) => ( + command.name === "status" + ? { ...command, side_effect: "none", authority: "none" } + : command + )); + assert.throws( + () => authorizeEccCapability( + parseItoCapabilities(JSON.stringify(capabilityEnvelope(mislabeled))), + "status", + ), + /outside ECC's safe policy/i, + ); + }], + ["fails closed on duplicate command records in the installed manifest", () => { + const duplicateAuth = DEFAULT_CAPABILITY_COMMANDS.find(({ name }) => name === "auth"); + const probe = makeItoProbe(0, { + capabilityEnvelope: capabilityEnvelope([duplicateAuth, duplicateAuth]), + }); + try { + const result = runCli(["ito", "auth"], { + ECC_ITO_CLI_EXECUTABLE: probe.executable, + ITO_API_KEY: "must-not-cross-after-invalid-contract", + }); + assert.notStrictEqual(result.status, 0); + assert.match(result.stderr, /duplicate|capabilit/i); + assert.ok(!fs.existsSync(probe.log), "invalid capability contract reached target invocation"); + } finally { + fs.rmSync(probe.directory, { recursive: true, force: true }); + } + }], ["forwards logout with device-token settings but never an API key", () => { const probe = makeItoProbe(); try { @@ -263,6 +395,11 @@ async function main() { probe.executable, [ '"use strict";', + 'const argv = process.argv.slice(2);', + 'if (argv.filter((value) => value !== "--json")[0] === "capabilities") {', + ` process.stdout.write(${JSON.stringify(`${JSON.stringify(capabilityEnvelope(DEFAULT_CAPABILITY_COMMANDS))}\n`)});`, + ' process.exit(0);', + '}', 'process.stdout.write("device-code-now\\n");', 'setTimeout(() => process.exit(7), 500);', "", @@ -472,7 +609,7 @@ async function main() { /timeout: isNodeQualification \? NODE_QUALIFICATION_TIMEOUT_MS : undefined/, ); }], - ["rejects unsupported browser, paper, and execution operations before spawning", () => { + ["rejects unsupported browser, paper, and execution operations after safe discovery", () => { for (const command of ["rent", "lock", "purchase", "run", "inference", "mcp"]) { const probe = makeItoProbe(); try { @@ -480,8 +617,9 @@ async function main() { 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.ok(!fs.existsSync(probe.log), `${command} must not spawn the Itô CLI`); + assert.match(result.stderr, /does not advertise|safe policy/i); + assert.ok(fs.existsSync(probe.capabilityLog), `${command} must inspect capabilities`); + assert.ok(!fs.existsSync(probe.log), `${command} must not reach target execution`); } finally { fs.rmSync(probe.directory, { recursive: true, force: true }); }