From 7b03a834b3cab5ac4e40dc2332f6033252cbd29e Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Thu, 23 Jul 2026 14:35:57 -0700 Subject: [PATCH] =?UTF-8?q?feat:=20add=20read-only=20It=C5=8D=20compute=20?= =?UTF-8?q?handoff=20(#2554)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add read-only Itō compute handoff * fix(ito): keep handoff portable under CI * test(ito): run npm welcome through Windows shell --- docs/design/ecc-ito-compute-integration.md | 24 ++ .../ecc-ito-handoff-evidence-2026-07-23.md | 122 +++++++ package.json | 1 + scripts/ecc.js | 12 +- scripts/ito.js | 321 ++++++++++++++++++ scripts/lib/ito-environment.js | 55 +++ tests/scripts/ito-handoff.test.js | 294 ++++++++++++++++ tests/scripts/npm-publish-surface.test.js | 2 + 8 files changed, 830 insertions(+), 1 deletion(-) create mode 100644 docs/design/ecc-ito-handoff-evidence-2026-07-23.md create mode 100755 scripts/ito.js create mode 100644 scripts/lib/ito-environment.js create mode 100644 tests/scripts/ito-handoff.test.js diff --git a/docs/design/ecc-ito-compute-integration.md b/docs/design/ecc-ito-compute-integration.md index 9e1d4133b..4893df8f0 100644 --- a/docs/design/ecc-ito-compute-integration.md +++ b/docs/design/ecc-ito-compute-integration.md @@ -18,6 +18,30 @@ OAuth integration, billing flow, or rental mutation. No Phase 2 skill, CLI, MCP, OAuth, rental-management, or serving code should be built until Affaan approves the decisions at the end of this document. +### Narrow sandbox handoff authorized on 2026-07-23 + +Affaan separately authorized one smaller CLI slice: + + ecc ito rent --accelerator h100 --count 1 --hours 24 + +That command is implemented as `ito.compute.handoff.v1`. It validates an +explicit H100 intent, records `ecc-cli` provenance, fixes the authority ceiling +at read-only sandbox mode, and opens only the `/desk` page, where sign-in may be +required. `--dry-run` emits the identical structured handoff without opening a +browser. + +The current Itô desk does not expose a supported structured ECC deep link or a +non-mutating external-intake endpoint. The CLI therefore returns +`transport: manual_copy` and `acceptedByIto: false`; it does not pretend that +opening `/desk` delivered the intent. A human may copy the bounded message into +the conversational desk. ECC never reads credentials, files an RFQ, requests +or accepts a quote, calls procurement, approves funds, creates an order, or +contacts a counterparty. The user must stop before `Pay & buy`. + +This narrow authorization does not approve the five skills, MCP, OAuth, rental +management, inference serving, deployment, publication, or any live economic +mutation described later in this document. + ## Thesis The distribution chain is: diff --git a/docs/design/ecc-ito-handoff-evidence-2026-07-23.md b/docs/design/ecc-ito-handoff-evidence-2026-07-23.md new file mode 100644 index 000000000..7d0c8aa3b --- /dev/null +++ b/docs/design/ecc-ito-handoff-evidence-2026-07-23.md @@ -0,0 +1,122 @@ +# ECC × Itô Sandbox Handoff Evidence + +Date: 2026-07-23 + +Branch: `agent/ecc-ito-sandbox-handoff` + +Base commit: `a3130f9ebfaeed075df5d5b52538acb0ee4bcdf8` + +## Result + +The canonical ECC CLI now supports: + +```text +ecc ito rent --accelerator h100 --count 1 --hours 24 +``` + +The command emits an `ito.compute.handoff.v1` envelope containing the exact +accelerator, count, and duration, `ecc-cli` provenance, and a read-only sandbox +authority ceiling. It opens only `https://compute.itomarkets.com/desk`. + +No supported Itô deep link or non-mutating structured external-intake endpoint +exists yet. The contract therefore reports `transport: manual_copy`, +`acceptedByIto: false`, and `signInMayBeRequired: true`. It never claims that +opening the page delivered the intent. + +The CLI does not read credentials, call the network, file an RFQ, request or +accept a quote, call procurement, approve funds, create an order, perform +outreach, deploy, push, or merge. The human boundary remains before +`Pay & buy`. + +## Security boundary + +Both subprocess boundaries use an explicit system-environment allowlist: + +1. `ecc` to the `ito` command. +2. `ito` to the OS browser opener. + +The allowlist retains only runtime essentials such as path, home, temporary +directory, locale, terminal, and desktop-session variables. ECC dry-run and +test controls are passed only to the Itô command where needed. Browser-probe +tests inject API-key, service-token, and password sentinels and prove none +reach the browser child. + +## Eval evidence + +Focused handoff: + +```text +node tests/scripts/ito-handoff.test.js +``` + +Result: 9 passed, 0 failed. + +Covered scenarios: + +- exact H100, count 1, 24-hour mapping; +- provenance and sandbox authority; +- global and local dry-run with zero browser invocation; +- exact allowlisted browser URL and sanitized child environment; +- direct Itô invocation with sanitized browser environment; +- missing, duplicate, unsupported, and out-of-range input rejection; +- absence of credential or mutating transports; +- human-readable approval boundary; +- POSIX-safe npm welcome output. + +CLI and package regressions: + +```text +node tests/scripts/ecc.test.js +node tests/scripts/npm-publish-surface.test.js +``` + +Results: 21 passed and 2 passed, with 0 failures. + +Full repository suite: + +```text +npm test +``` + +Result: 3,155 passed, 0 failed. + +Static and security gates: + +```text +npm run lint +npm run security:ioc-scan -- --root . +git diff --check +``` + +Results: all passed; the IOC scan inspected 207 files. + +Packaged-bin execution: + +```text +npm pack --pack-destination +node /scripts/ecc.js --dry-run ito rent \ + --accelerator h100 --count 1 --hours 24 --json +``` + +Result: the packed `ecc-universal-2.0.0.tgz` included executable `ecc.js` and +`ito.js`; the unpacked command returned the exact intent with `dryRun: true`, +`opened: false`, and `transport: manual_copy`. + +## Changed surfaces + +- `scripts/ito.js` +- `scripts/lib/ito-environment.js` +- `scripts/ecc.js` +- `package.json` +- `tests/scripts/ito-handoff.test.js` +- `tests/scripts/npm-publish-surface.test.js` +- `docs/design/ecc-ito-compute-integration.md` +- this evidence record + +## Remaining limitation + +ECC cannot automatically prefill or deliver this structured intent until Itô +publishes a safe, authenticated, non-mutating intake or deep-link contract. +Adding such a contract belongs in Itô and must preserve its existing human +approval gates. This ECC slice deliberately does not invent another UI, +transport, credential, or procurement path. diff --git a/package.json b/package.json index 3b056e3b3..9f6aef9b2 100644 --- a/package.json +++ b/package.json @@ -110,6 +110,7 @@ "scripts/hooks/", "scripts/install-apply.js", "scripts/install-plan.js", + "scripts/ito.js", "scripts/lib/", "scripts/list-installed.js", "scripts/loop-status.js", diff --git a/scripts/ecc.js b/scripts/ecc.js index 7f1e7bfb8..4fdc23831 100755 --- a/scripts/ecc.js +++ b/scripts/ecc.js @@ -4,6 +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 COMMANDS = { install: { @@ -26,6 +27,10 @@ const COMMANDS = { script: 'control-pane.js', description: 'Run the local ECC2 operator control pane', }, + ito: { + script: 'ito.js', + description: 'Prepare a read-only sandbox handoff to the Itô compute desk', + }, 'install-plan': { script: 'install-plan.js', description: 'Alias for plan', @@ -86,6 +91,7 @@ const PRIMARY_COMMANDS = [ 'catalog', 'consult', 'control-pane', + 'ito', 'list-installed', 'doctor', 'repair', @@ -132,6 +138,8 @@ Examples: ecc catalog show framework:nextjs ecc consult "security reviews" ecc control-pane --port 8765 + ecc ito rent --accelerator h100 --count 1 --hours 24 + ecc --dry-run ito rent --accelerator h100 --count 1 --hours 24 --json ecc list-installed --json ecc doctor --target cursor ecc repair --dry-run @@ -223,7 +231,9 @@ function runCommand(commandName, args) { [path.join(__dirname, command.script), ...args], { cwd: process.cwd(), - env: process.env, + env: commandName === 'ito' + ? { ...createSafeItoEnvironment(process.env, { includeControls: true }) } + : process.env, encoding: 'utf8', maxBuffer: 10 * 1024 * 1024, } diff --git a/scripts/ito.js b/scripts/ito.js new file mode 100755 index 000000000..8f37ee20c --- /dev/null +++ b/scripts/ito.js @@ -0,0 +1,321 @@ +#!/usr/bin/env node + +"use strict"; + +const { spawnSync } = require("child_process"); +const { createSafeItoEnvironment } = require("./lib/ito-environment"); + +const DESK_URL = "https://compute.itomarkets.com/desk"; +const SCHEMA_VERSION = "ito.compute.handoff.v1"; +const SUPPORTED_ACCELERATORS = Object.freeze({ + h100: "h100", + "h100-pcie": "h100-pcie", + "h100-sxm": "h100-sxm", +}); +const REQUIRED_INTENT_OPTIONS = Object.freeze([ + "accelerator", + "count", + "hours", +]); + +function showHelp() { + console.log(` +ECC × Itô compute handoff + +Usage: + ecc ito rent --accelerator --count <1-64> --hours <1-720> [options] + +Options: + --dry-run Emit the exact handoff without opening a browser + --no-open Emit the exact handoff for manual browser navigation + --json Emit the versioned response envelope as JSON + --help Show this help + +Example: + ecc ito rent --accelerator h100 --count 1 --hours 24 + +This command creates a sandbox-only, read-only intent. It opens the Itô desk +for manual copy; sign-in may be required. It does not send the intent into Itô, +file an RFQ, request or accept a quote, call a procurement endpoint, approve +funds, or place an order. Stop before "Pay & buy". +`); +} + +function readValue(args, index, option) { + const value = args[index + 1]; + if (value === undefined || value.startsWith("--")) { + throw new Error(`--${option} requires a value`); + } + return value; +} + +function parseInteger(raw, option, minimum, maximum) { + if (!/^[0-9]+$/.test(raw)) { + throw new Error(`--${option} must be an integer between ${minimum} and ${maximum}`); + } + const value = Number(raw); + if (!Number.isSafeInteger(value) || value < minimum || value > maximum) { + throw new Error(`--${option} must be an integer between ${minimum} and ${maximum}`); + } + return value; +} + +function parseAccelerator(raw) { + const normalized = String(raw).trim().toLowerCase(); + const accelerator = SUPPORTED_ACCELERATORS[normalized]; + if (!accelerator) { + throw new Error( + "--accelerator must be one of h100, h100-pcie, or h100-sxm" + ); + } + return accelerator; +} + +function parseArgs(argv) { + const args = [...argv]; + if (args.includes("--help") || args.includes("-h")) { + return { help: true }; + } + + const command = args.shift(); + if (command !== "rent") { + throw new Error(`unsupported Itô command: ${command || "(missing)"}`); + } + + const values = { + accelerator: null, + count: null, + hours: null, + dryRun: false, + json: false, + noOpen: false, + }; + const seen = new Set(); + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (arg === "--dry-run") { + if (seen.has("dry-run")) throw new Error("--dry-run may only be provided once"); + seen.add("dry-run"); + values.dryRun = true; + continue; + } + if (arg === "--json") { + if (seen.has("json")) throw new Error("--json may only be provided once"); + seen.add("json"); + values.json = true; + continue; + } + if (arg === "--no-open") { + if (seen.has("no-open")) throw new Error("--no-open may only be provided once"); + seen.add("no-open"); + values.noOpen = true; + continue; + } + + const option = arg.startsWith("--") ? arg.slice(2) : ""; + if (!REQUIRED_INTENT_OPTIONS.includes(option)) { + throw new Error(`unsupported option: ${arg}`); + } + if (seen.has(option)) { + throw new Error(`--${option} may only be provided once`); + } + seen.add(option); + const raw = readValue(args, index, option); + if (option === "accelerator") values.accelerator = parseAccelerator(raw); + if (option === "count") values.count = parseInteger(raw, option, 1, 64); + if (option === "hours") values.hours = parseInteger(raw, option, 1, 720); + index += 1; + } + + const missing = REQUIRED_INTENT_OPTIONS.filter((option) => values[option] === null); + if (missing.length > 0) { + throw new Error( + `missing required rental intent option${missing.length === 1 ? "" : "s"}: ` + + missing.map((option) => `--${option}`).join(", ") + ); + } + + return { + help: false, + command, + options: Object.freeze({ + ...values, + dryRun: values.dryRun || process.env.ECC_DRY_RUN === "1", + }), + }; +} + +function buildHandoffMessage(intent) { + const accelerator = intent.accelerator.toUpperCase(); + return [ + "[ECC sandbox-only compute handoff]", + `Source: ECC CLI. Need ${intent.count} × ${accelerator} for ${intent.hours} hours.`, + "Treat this as a read-only requirement for review.", + "Do not file an RFQ, request or accept a quote, place an order, approve funds, or contact a counterparty without a separate explicit human action in Itô.", + ].join(" "); +} + +function createEnvelope({ success, state, data, error }) { + return Object.freeze({ + schemaVersion: SCHEMA_VERSION, + success, + state, + data, + error, + links: Object.freeze({ desk: DESK_URL }), + }); +} + +function createInvalidEnvelope(message) { + return createEnvelope({ + success: false, + state: "invalid_request", + data: null, + error: Object.freeze({ + code: "INVALID_ARGUMENT", + message, + }), + }); +} + +function defaultOpenUrl(url) { + let executable; + let args; + const testExecutable = process.env.NODE_ENV === "test" + ? process.env.ECC_ITO_BROWSER_EXECUTABLE + : null; + + if (testExecutable) { + if (process.platform === "win32" && /\.(?:bat|cmd)$/i.test(testExecutable)) { + executable = process.env.ComSpec || "cmd.exe"; + args = ["/d", "/s", "/c", testExecutable, url]; + } else { + executable = testExecutable; + args = [url]; + } + } else if (process.platform === "darwin") { + executable = "open"; + args = [url]; + } else if (process.platform === "win32") { + executable = "cmd.exe"; + args = ["/d", "/s", "/c", "start", "", url]; + } else { + executable = "xdg-open"; + args = [url]; + } + + const result = spawnSync(executable, args, { + env: { ...createSafeItoEnvironment(process.env) }, + stdio: "ignore", + windowsHide: true, + shell: false, + }); + return !result.error && result.status === 0; +} + +function createHandoff(options, openUrl = defaultOpenUrl) { + const shouldOpen = !options.dryRun && !options.noOpen; + const opened = shouldOpen ? openUrl(DESK_URL) : false; + const intent = Object.freeze({ + accelerator: options.accelerator, + count: options.count, + hours: options.hours, + }); + + return createEnvelope({ + success: true, + state: "manual_handoff", + data: Object.freeze({ + authenticated: null, + dryRun: options.dryRun, + opened, + intent, + provenance: Object.freeze({ + source: "ecc-cli", + command: "ecc ito rent", + }), + authority: Object.freeze({ + environment: "sandbox", + readOnly: true, + liveRfq: false, + procurementMutation: false, + quoteAcceptance: false, + fundsApproval: false, + orderCreation: false, + outreach: false, + }), + handoff: Object.freeze({ + transport: "manual_copy", + destination: "ito-desk", + acceptedByIto: false, + signInMayBeRequired: true, + message: buildHandoffMessage(intent), + limitation: "Itô /desk currently exposes no supported structured ECC intake or deep-link contract.", + }), + approvalGate: 'Stop before "Pay & buy".', + priceQuote: null, + orderId: null, + }), + error: null, + }); +} + +function renderText(payload) { + const { data } = payload; + const browserState = data.dryRun + ? "Dry-run: browser not opened." + : data.opened + ? "Opened the Itô desk; sign-in may be required." + : "Browser not opened; sign-in may be required at the Itô desk."; + return [ + "ECC × Itô sandbox compute handoff", + "", + browserState, + `Desk: ${payload.links.desk}`, + "Transport: manual copy; Itô has not accepted this intent.", + `Message: ${data.handoff.message}`, + `Limitation: ${data.handoff.limitation}`, + `Approval gate: ${data.approvalGate}`, + "", + "ECC does not file an RFQ, request or accept a quote, use credentials, approve funds, create an order, or contact a counterparty.", + ].join("\n"); +} + +function main(argv = process.argv.slice(2), dependencies = {}) { + let parsed; + try { + parsed = parseArgs(argv); + } catch (error) { + const payload = createInvalidEnvelope(error.message); + if (argv.includes("--json")) console.log(JSON.stringify(payload, null, 2)); + else console.error(`Error: ${error.message}`); + return 1; + } + + if (parsed.help) { + showHelp(); + return 0; + } + + const payload = createHandoff(parsed.options, dependencies.openUrl); + if (parsed.options.json) console.log(JSON.stringify(payload, null, 2)); + else console.log(renderText(payload)); + return 0; +} + +if (require.main === module) { + process.exitCode = main(); +} + +module.exports = Object.freeze({ + DESK_URL, + SCHEMA_VERSION, + buildHandoffMessage, + createHandoff, + createInvalidEnvelope, + main, + parseAccelerator, + parseArgs, + renderText, +}); diff --git a/scripts/lib/ito-environment.js b/scripts/lib/ito-environment.js new file mode 100644 index 000000000..3be2f5be2 --- /dev/null +++ b/scripts/lib/ito-environment.js @@ -0,0 +1,55 @@ +"use strict"; + +const SYSTEM_ENVIRONMENT_KEYS = Object.freeze([ + "CI", + "ComSpec", + "DISPLAY", + "FORCE_COLOR", + "HOME", + "LANG", + "LC_ALL", + "NO_COLOR", + "PATH", + "PATHEXT", + "SHELL", + "SystemRoot", + "TEMP", + "TERM", + "TMP", + "TMPDIR", + "USERPROFILE", + "WAYLAND_DISPLAY", + "WINDIR", + "XDG_RUNTIME_DIR", +]); + +function copyDefined(source, target, key) { + if (typeof source[key] === "string") { + target[key] = source[key]; + } +} + +function createSafeItoEnvironment(source = process.env, options = {}) { + const safe = {}; + for (const key of SYSTEM_ENVIRONMENT_KEYS) { + copyDefined(source, safe, key); + } + for (const [key] of Object.entries(source)) { + if (key.startsWith("LC_")) copyDefined(source, safe, key); + } + + if (options.includeControls) { + copyDefined(source, safe, "ECC_DRY_RUN"); + copyDefined(source, safe, "NODE_ENV"); + if (source.NODE_ENV === "test") { + copyDefined(source, safe, "ECC_ITO_BROWSER_EXECUTABLE"); + } + } + + return Object.freeze(safe); +} + +module.exports = Object.freeze({ + SYSTEM_ENVIRONMENT_KEYS, + createSafeItoEnvironment, +}); diff --git a/tests/scripts/ito-handoff.test.js b/tests/scripts/ito-handoff.test.js new file mode 100644 index 000000000..349ee5386 --- /dev/null +++ b/tests/scripts/ito-handoff.test.js @@ -0,0 +1,294 @@ +/** + * End-to-end contract tests for ECC's read-only Itô compute handoff. + */ + +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 ECC_SCRIPT = path.join(REPO_ROOT, "scripts", "ecc.js") +const ITO_SCRIPT = path.join(REPO_ROOT, "scripts", "ito.js") +const DESK_URL = "https://compute.itomarkets.com/desk" + +function runCli(args, options = {}) { + return spawnSync(process.execPath, [ECC_SCRIPT, ...args], { + cwd: REPO_ROOT, + encoding: "utf8", + env: { + ...process.env, + NODE_ENV: "test", + ...(options.env || {}), + }, + }) +} + +function parseJson(result, expectedStatus = 0) { + assert.strictEqual(result.status, expectedStatus, result.stderr) + return JSON.parse(result.stdout) +} + +function runTest(name, fn) { + try { + fn() + console.log(` ✓ ${name}`) + return true + } catch (error) { + console.log(` ✗ ${name}`) + console.error(` ${error.message}`) + return false + } +} + +function makeBrowserProbe() { + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "ecc-ito-browser-")) + const log = path.join(dir, "opened-url.txt") + const envLog = path.join(dir, "browser-env.txt") + const executable = path.join(dir, process.platform === "win32" ? "browser-probe.cmd" : "browser-probe") + if (process.platform === "win32") { + fs.writeFileSync(executable, `@echo off\r\n"${log}"\r\nset >"${envLog}"\r\n`) + } else { + fs.writeFileSync(executable, `#!/bin/sh\nprintf '%s' "$1" > "${log}"\nenv > "${envLog}"\n`) + fs.chmodSync(executable, 0o755) + } + return { dir, envLog, executable, log } +} + +function main() { + console.log("\n=== Testing ECC × Itô sandbox handoff ===\n") + + const tests = [ + ["maps the exact CLI request into a structured sandbox intent", () => { + const payload = parseJson(runCli([ + "ito", + "rent", + "--accelerator", "h100", + "--count", "1", + "--hours", "24", + "--no-open", + "--json", + ])) + + assert.strictEqual(payload.schemaVersion, "ito.compute.handoff.v1") + assert.strictEqual(payload.success, true) + assert.strictEqual(payload.state, "manual_handoff") + assert.deepStrictEqual(payload.data.intent, { + accelerator: "h100", + count: 1, + hours: 24, + }) + assert.deepStrictEqual(payload.data.provenance, { + source: "ecc-cli", + command: "ecc ito rent", + }) + assert.strictEqual(payload.data.authority.environment, "sandbox") + assert.strictEqual(payload.data.authority.readOnly, true) + assert.strictEqual(payload.data.authority.liveRfq, false) + assert.strictEqual(payload.data.authority.procurementMutation, false) + assert.strictEqual(payload.data.authority.quoteAcceptance, false) + assert.strictEqual(payload.data.authority.fundsApproval, false) + assert.strictEqual(payload.data.authority.orderCreation, false) + assert.strictEqual(payload.data.handoff.transport, "manual_copy") + assert.strictEqual(payload.data.handoff.destination, "ito-desk") + assert.strictEqual(payload.data.handoff.acceptedByIto, false) + assert.strictEqual(payload.data.handoff.signInMayBeRequired, true) + assert.match(payload.data.handoff.message, /1 × H100/) + assert.match(payload.data.handoff.message, /24 hours/) + assert.match(payload.data.handoff.message, /ECC CLI/) + assert.match(payload.data.handoff.message, /sandbox-only/) + assert.strictEqual(payload.links.desk, DESK_URL) + assert.strictEqual(payload.data.opened, false) + assert.strictEqual(payload.data.priceQuote, null) + assert.strictEqual(payload.data.orderId, null) + }], + ["global dry-run emits the same handoff and never opens a browser", () => { + const probe = makeBrowserProbe() + try { + const payload = parseJson(runCli([ + "--dry-run", + "ito", + "rent", + "--accelerator", "h100", + "--count", "1", + "--hours", "24", + "--json", + ], { + env: { ECC_ITO_BROWSER_EXECUTABLE: probe.executable }, + })) + + assert.strictEqual(payload.data.dryRun, true) + assert.strictEqual(payload.data.opened, false) + assert.ok(!fs.existsSync(probe.log), "dry-run must not invoke the browser executable") + } finally { + fs.rmSync(probe.dir, { recursive: true, force: true }) + } + }], + ["local dry-run is equivalent and rejects an open request", () => { + const probe = makeBrowserProbe() + try { + const payload = parseJson(runCli([ + "ito", + "rent", + "--accelerator", "h100", + "--count", "1", + "--hours", "24", + "--dry-run", + "--json", + ], { + env: { ECC_ITO_BROWSER_EXECUTABLE: probe.executable }, + })) + + assert.strictEqual(payload.data.dryRun, true) + assert.strictEqual(payload.data.opened, false) + assert.ok(!fs.existsSync(probe.log), "local dry-run must not invoke the browser executable") + } finally { + fs.rmSync(probe.dir, { recursive: true, force: true }) + } + }], + ["browser handoff opens only the allowlisted desk URL without inherited secrets", () => { + const probe = makeBrowserProbe() + try { + const payload = parseJson(runCli([ + "ito", + "rent", + "--accelerator", "h100", + "--count", "1", + "--hours", "24", + "--json", + ], { + env: { + ECC_ITO_BROWSER_EXECUTABLE: probe.executable, + ITO_API_KEY: "parent-api-key-must-not-cross", + ITO_SERVICE_TOKEN: "parent-token-must-not-cross", + TEST_PASSWORD: "parent-password-must-not-cross", + }, + })) + + assert.strictEqual(payload.data.opened, true) + assert.strictEqual(fs.readFileSync(probe.log, "utf8"), DESK_URL) + assert.strictEqual(payload.links.desk, DESK_URL) + const childEnvironment = fs.readFileSync(probe.envLog, "utf8") + assert.doesNotMatch(childEnvironment, /parent-api-key-must-not-cross/) + assert.doesNotMatch(childEnvironment, /parent-token-must-not-cross/) + assert.doesNotMatch(childEnvironment, /parent-password-must-not-cross/) + } finally { + fs.rmSync(probe.dir, { recursive: true, force: true }) + } + }], + ["direct Ito execution strips secrets from the browser child", () => { + const probe = makeBrowserProbe() + try { + const result = spawnSync(process.execPath, [ + ITO_SCRIPT, + "rent", + "--accelerator", "h100", + "--count", "1", + "--hours", "24", + "--json", + ], { + cwd: REPO_ROOT, + encoding: "utf8", + env: { + ...process.env, + NODE_ENV: "test", + ECC_ITO_BROWSER_EXECUTABLE: probe.executable, + ITO_API_KEY: "direct-api-key-must-not-cross", + ITO_SERVICE_TOKEN: "direct-token-must-not-cross", + TEST_PASSWORD: "direct-password-must-not-cross", + }, + }) + const payload = parseJson(result) + assert.strictEqual(payload.data.opened, true) + const childEnvironment = fs.readFileSync(probe.envLog, "utf8") + assert.doesNotMatch(childEnvironment, /direct-api-key-must-not-cross/) + assert.doesNotMatch(childEnvironment, /direct-token-must-not-cross/) + assert.doesNotMatch(childEnvironment, /direct-password-must-not-cross/) + } finally { + fs.rmSync(probe.dir, { recursive: true, force: true }) + } + }], + ["fails closed on missing, unsupported, duplicate, and out-of-range intent", () => { + for (const args of [ + ["ito", "rent", "--count", "1", "--hours", "24", "--no-open", "--json"], + ["ito", "rent", "--accelerator", "a100", "--count", "1", "--hours", "24", "--no-open", "--json"], + ["ito", "rent", "--accelerator", "h100", "--count", "0", "--hours", "24", "--no-open", "--json"], + ["ito", "rent", "--accelerator", "h100", "--count", "1", "--hours", "721", "--no-open", "--json"], + ["ito", "rent", "--accelerator", "h100", "--accelerator", "h100-sxm", "--count", "1", "--hours", "24", "--no-open", "--json"], + ["ito", "dashboard", "--accelerator", "h100", "--count", "1", "--hours", "24", "--no-open", "--json"], + ]) { + const result = runCli(args) + assert.notStrictEqual(result.status, 0, args.join(" ")) + const payload = JSON.parse(result.stdout) + assert.strictEqual(payload.success, false) + assert.strictEqual(payload.state, "invalid_request") + assert.strictEqual(payload.data, null) + assert.strictEqual(payload.error.code, "INVALID_ARGUMENT") + } + }], + ["does not inspect credentials or contain a mutating transport", () => { + const payload = parseJson(runCli([ + "ito", + "rent", + "--accelerator", "h100", + "--count", "1", + "--hours", "24", + "--no-open", + "--json", + ], { + env: { + ITO_API_KEY: "must-not-be-read", + ITO_SERVICE_TOKEN: "must-not-be-read", + }, + })) + + assert.strictEqual(payload.data.authenticated, null) + const source = fs.readFileSync(path.join(REPO_ROOT, "scripts", "ito.js"), "utf8") + assert.doesNotMatch(source, /ITO_API_KEY|ITO_SERVICE_TOKEN|procurement\/orders|authorization/i) + assert.doesNotMatch(source, /\bfetch\s*\(|https?\.request|child_process\.exec\b/) + }], + ["human output stops before the economic boundary", () => { + const result = runCli([ + "ito", + "rent", + "--accelerator", "h100", + "--count", "1", + "--hours", "24", + "--no-open", + ]) + + assert.strictEqual(result.status, 0, result.stderr) + assert.match(result.stdout, /manual copy/i) + assert.match(result.stdout, /sandbox-only/i) + assert.match(result.stdout, /sign-in may be required/i) + assert.doesNotMatch(result.stdout, /signed-in/i) + assert.match(result.stdout, /does not file an RFQ/i) + assert.match(result.stdout, /Stop before ["“]Pay & buy["”]/i) + assert.match(result.stdout, /no supported structured ECC intake/i) + }], + ["npm welcome remains POSIX-safe around apostrophes", () => { + const npmExecutable = process.platform === "win32" ? "npm.cmd" : "npm" + const result = spawnSync(npmExecutable, ["run", "welcome"], { + cwd: REPO_ROOT, + encoding: "utf8", + shell: process.platform === "win32", + }) + assert.strictEqual(result.status, 0, result.stderr) + assert.match(result.stdout, /Itô/) + }], + ] + + let passed = 0 + let failed = 0 + for (const [name, fn] of tests) { + if (runTest(name, fn)) passed += 1 + else failed += 1 + } + + console.log(`\nPassed: ${passed}`) + console.log(`Failed: ${failed}`) + process.exit(failed > 0 ? 1 : 0) +} + +main() diff --git a/tests/scripts/npm-publish-surface.test.js b/tests/scripts/npm-publish-surface.test.js index cb556c446..c00205ee1 100644 --- a/tests/scripts/npm-publish-surface.test.js +++ b/tests/scripts/npm-publish-surface.test.js @@ -55,6 +55,7 @@ function buildExpectedPublishPaths(repoRoot) { "scripts/work-items.js", "scripts/install-apply.js", "scripts/install-plan.js", + "scripts/ito.js", "scripts/list-installed.js", "scripts/loop-status.js", "scripts/observability-readiness.js", @@ -143,6 +144,7 @@ function main() { "scripts/ci/supply-chain-advisory-sources.js", "scripts/consult.js", "scripts/control-pane.js", + "scripts/ito.js", "scripts/discussion-audit.js", "scripts/operator-readiness-dashboard.js", "scripts/preview-pack-smoke.js",