diff --git a/README.md b/README.md index 9624a26de..0bccf07ce 100644 --- a/README.md +++ b/README.md @@ -566,7 +566,7 @@ Kimi Code discovers the installed `.kimi-code/AGENTS.md` instructions and `.kimi `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. -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. 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, 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. `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. diff --git a/docs/design/ecc-ito-compute-integration.md b/docs/design/ecc-ito-compute-integration.md index 7a4840032..a21c9bdfd 100644 --- a/docs/design/ecc-ito-compute-integration.md +++ b/docs/design/ecc-ito-compute-integration.md @@ -24,10 +24,11 @@ ECC delegates to the canonical Itô package in `Ito-Markets/ito-cloud-runtime/cli/ito-compute-cli`. ECC does not maintain a second API client or response schema. -The wrapper exposes only the canonical CLI's `login`, `auth`, `find`, `status`, and `evals` +The wrapper exposes only the canonical CLI's `login`, `logout`, `auth`, `find`, `status`, and `evals` operations: ecc ito login [--no-browser] + ecc ito logout ecc ito auth ecc ito find ecc ito status @@ -76,6 +77,9 @@ directory and 0600 token-file permissions. ECC does not inspect or log secrets. - `login` starts canonical device authorization, with `--no-browser` available when the operator does not want the CLI to open the verification page. +- `logout` revokes the current device credential and removes the local copy only + after confirmed remote revocation; a failed revocation keeps the local copy + for retry. - `auth` validates existing credentials only. - `find` reads live inventory and submits a live authenticated RFQ. An operator or agent must gather every hard topology/economic constraint and obtain @@ -132,7 +136,7 @@ after review. The local contract suite proves: -- only the four supported operations spawn; +- only the six supported 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/docs/testing/ecc-ito-real-cli-bridge.tdd.md b/docs/testing/ecc-ito-real-cli-bridge.tdd.md index 824d822d3..92713e9d9 100644 --- a/docs/testing/ecc-ito-real-cli-bridge.tdd.md +++ b/docs/testing/ecc-ito-real-cli-bridge.tdd.md @@ -8,7 +8,8 @@ handoff. No external plan file was executed. ## User journeys 1. As an ECC operator, I can explicitly invoke streaming device `login`, then - use validation-only `auth`, `find`, and `status` without a duplicate client. + use validation-only `auth`, `find`, and `status`, or revoke the device with + `logout`, without a duplicate client. 2. As a security reviewer, I can prove unsupported operations, missing local installs, and ECC dry-run requests fail before any child process or network operation. @@ -56,7 +57,7 @@ module. No dependency installation was performed. | Guarantee | Test | Type | Result | |---|---|---|---| -| `login`, `auth`, `find`, and `status` forward only their reviewed surfaces | `tests/scripts/ito-cli-bridge.test.js` | end-to-end process contract | PASS | +| `login`, `logout`, `auth`, `find`, and `status` forward only their reviewed surfaces | `tests/scripts/ito-cli-bridge.test.js` | end-to-end process contract | PASS | | Login output streams before completion and its exit status propagates | `tests/scripts/ito-cli-bridge.test.js` | async process contract | PASS | | `auth --no-browser` fails before spawn | `tests/scripts/ito-cli-bridge.test.js` | negative process contract | PASS | | Full RFQ arguments cross unchanged | `tests/scripts/ito-cli-bridge.test.js` | integration | PASS | diff --git a/manifests/install-components.json b/manifests/install-components.json index 70409a86d..a5f976a94 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, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", + "description": "Authenticated Itô GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", "modules": [ "ito-compute" ] diff --git a/manifests/install-modules.json b/manifests/install-modules.json index cb0015181..e18922bf0 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -605,7 +605,7 @@ { "id": "ito-compute", "kind": "skills", - "description": "Authenticated Itô GPU inventory, RFQ, status, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", + "description": "Authenticated Itô GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", "paths": [ "skills/ito-compute", "skills/ito-inference", diff --git a/scripts/ecc.js b/scripts/ecc.js index a80db16d9..3caff5735 100755 --- a/scripts/ecc.js +++ b/scripts/ecc.js @@ -164,6 +164,7 @@ Examples: ecc consult "security reviews" ecc control-pane --port 8765 ecc ito login [--no-browser] + ecc ito logout ecc ito auth ecc ito find --gpu h200 --count 8 --nodes 1 --gpus-per-node 8 --days 30 --storage-tb 1 --start-window 2099-08-15 --max-rate 3.00 --form-factor bare_metal --contract-type reservation --fabric infiniband --region us-east-1 ecc ito status --json diff --git a/scripts/ito.js b/scripts/ito.js index 592f9f2f0..e981d85ee 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", "auth", "find", "status", "evals"]); +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([ @@ -29,11 +29,12 @@ ECC × Itô local CLI bridge Usage: ecc ito login [--no-browser] + ecc ito logout ecc ito auth ecc ito find ecc ito status 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 @@ -42,6 +43,8 @@ and persists its device token in macOS Keychain. Pass --no-browser to suppress that handoff. ECC itself performs no browser automation and adds no lock, workload, inference, or purchase path. "ecc ito auth" is validation-only and never starts device login. +"ecc ito logout" asks the canonical CLI to revoke the current device credential +and remove its local copy only after remote revocation is confirmed. Important: - "find" reads live inventory and submits an authenticated RFQ. @@ -161,7 +164,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, auth, find, status, and evals.` + `Unsupported Itô command "${command || "(missing)"}"; ECC permits only login, logout, auth, find, status, and evals.` ); } if (command === "auth" && withoutJson.includes("--no-browser")) { diff --git a/scripts/lib/ito-environment.js b/scripts/lib/ito-environment.js index d23741c18..e9a6c909c 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", "auth", "find", "status"]); +const ITO_RUNTIME_COMMANDS = new Set(["login", "logout", "auth", "find", "status"]); function copyDefined(source, target, key) { if (typeof source[key] === "string") { diff --git a/skills/ito-compute/SKILL.md b/skills/ito-compute/SKILL.md index 05c0c96d1..c81bd97a3 100644 --- a/skills/ito-compute/SKILL.md +++ b/skills/ito-compute/SKILL.md @@ -1,8 +1,6 @@ --- name: ito-compute -description: Query live GPU inventory, submit an authenticated Itô fixed-rate RFQ, inspect RFQ or procurement status, 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, or validate GPU nodes. -metadata: - origin: ECC +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. --- # Itô Compute @@ -41,7 +39,9 @@ key or token in arguments, tracked files, MCP results, logs, or chat. canonical CLI's device authorization, which opens the Itô verification page by default and persists a device token in macOS Keychain. Use `ecc ito login --no-browser` to suppress the page handoff. ECC itself does no - browser automation. + browser automation. If the originating agent cannot complete the signed-in + browser step, hand the exact command to the user; after approval finishes, + return to the originating task and continue with `ecc ito auth`. Device tokens use macOS Keychain by default. File-token fallback is explicit and its directory and token file must remain owner-only (0700 and 0600). 2. Run `ecc ito auth` to validate existing credentials; it never starts login @@ -73,6 +73,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. + The canonical CLI keeps the local credential when remote revocation fails so + the operator can retry; never delete the token manually as a substitute. Inventory prices are indicative. An RFQ is not reserved capacity. Treat a rate as fixed only when the canonical result contains a non-null firm quote. @@ -131,6 +134,14 @@ The server exposes only: `ito_auth`, gather explicit buyer authority and every hard constraint, call `ito_find`, then poll with `ito_status` when needed. +## Rent or purchase semantics + +`find` submits an RFQ and may return a firm quote, but it does not rent, +purchase, reserve, provision, or move funds. `status` is read-oriented, though +the provider endpoint may reconcile an existing procurement order. The passive +dashboard link in ECC help is a separate user-operated web route; do not open or +operate it as a substitute for a missing CLI capability. + ## Unsupported operations The supported client surface cannot lock quotes, reserve capacity, execute diff --git a/skills/ito-compute/agents/openai.yaml b/skills/ito-compute/agents/openai.yaml new file mode 100644 index 000000000..c6b965cba --- /dev/null +++ b/skills/ito-compute/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Itô Compute" + short_description: "GPU inventory, RFQs, status, and revocation" + default_prompt: "Use $ito-compute to request a live GPU RFQ, inspect status, or revoke this device safely." diff --git a/tests/ci/ito-compute-skill.test.js b/tests/ci/ito-compute-skill.test.js index 8534d3149..9df529456 100644 --- a/tests/ci/ito-compute-skill.test.js +++ b/tests/ci/ito-compute-skill.test.js @@ -36,6 +36,7 @@ function main() { const skill = read("skills/ito-compute/SKILL.md"); for (const command of [ "ecc ito login", + "ecc ito logout", "ecc ito auth", "ecc ito find", "ecc ito status", @@ -59,6 +60,9 @@ function main() { assert.match(skill, /explicit absolute built entry/); assert.match(skill, /never discovers[^\n]*through `PATH`/); assert.match(skill, /ecc ito login --no-browser/); + assert.match(skill, /return to the originating (?:agent|task)/i); + assert.match(skill, /revok/i); + assert.match(skill, /rent or purchase/i); assert.match(skill, /auth.*validat/i); assert.match(skill, /--no-browser/); assert.match(skill, /macOS Keychain/i); @@ -70,6 +74,11 @@ function main() { assert.match(skill, /explicit node/i); assert.match(skill, /cannot (?:rent|launch|recover|repair)/i); assert.doesNotMatch(skill, /npm link/); + const frontmatter = skill.match(/^---\n([\s\S]*?)\n---/)[1]; + assert.doesNotMatch(frontmatter, /^metadata:/m); + const interfaceMetadata = read("skills/ito-compute/agents/openai.yaml"); + assert.match(interfaceMetadata, /display_name: "Itô Compute"/); + assert.match(interfaceMetadata, /default_prompt: .*\$ito-compute/); }], ["keeps README and integration docs aligned with the separated auth contract", () => { for (const relativePath of [ @@ -108,7 +117,7 @@ function main() { { 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.", + description: "Authenticated Itô GPU inventory, RFQ, status, device revocation, and explicitly gated node-qualification workflows through the separately installed canonical CLI.", modules: ["ito-compute"], } ); diff --git a/tests/scripts/ito-cli-bridge.test.js b/tests/scripts/ito-cli-bridge.test.js index f79634e7d..e6e4c7d2c 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", "auth", "find", "status"]) { + for (const command of ["login", "logout", "auth", "find", "status"]) { const probe = makeItoProbe(); try { const result = runCli(["ito", command], { @@ -132,6 +132,27 @@ async function main() { } } }], + ["forwards logout with device-token settings but never an API key", () => { + const probe = makeItoProbe(); + try { + const result = runCli(["ito", "logout", "--json"], { + ECC_ITO_CLI_EXECUTABLE: probe.executable, + ITO_API_KEY: "must-not-cross-into-device-revocation", + ITO_ALLOW_FILE_TOKEN: "1", + ITO_TOKEN_FILE: "/tmp/ito-device-token", + ITO_API_URL: "https://compute.example.test", + }); + assert.strictEqual(result.status, 0, result.stderr); + const invocation = readInvocation(probe); + assert.deepStrictEqual(invocation.argv, ["--json", "logout"]); + assert.strictEqual(invocation.env.ITO_API_KEY, undefined); + assert.strictEqual(invocation.env.ITO_ALLOW_FILE_TOKEN, "1"); + assert.strictEqual(invocation.env.ITO_TOKEN_FILE, "/tmp/ito-device-token"); + assert.strictEqual(invocation.env.ITO_API_URL, "https://compute.example.test"); + } finally { + fs.rmSync(probe.directory, { recursive: true, force: true }); + } + }], ["forwards the canonical login browser opt-out without performing browser automation", () => { const probe = makeItoProbe(); try { @@ -459,7 +480,7 @@ async function main() { ECC_ITO_CLI_EXECUTABLE: probe.executable, }); assert.notStrictEqual(result.status, 0, command); - assert.match(result.stderr, /only login, auth, find, status, and evals/i); + 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`); } finally { fs.rmSync(probe.directory, { recursive: true, force: true }); @@ -632,6 +653,7 @@ async function main() { }); assert.strictEqual(result.status, 0, result.stderr); assert.match(result.stdout, /ecc ito login \[--no-browser\]/); + assert.match(result.stdout, /ecc ito logout/); assert.match(result.stdout, /ecc ito auth/); assert.match(result.stdout, /ecc ito find/); assert.match(result.stdout, /ecc ito status/);