From 0d39ae83ddf7a0857c8fa1f0ffe3c5115f6c4450 Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Sat, 15 Aug 2026 01:57:26 -0400 Subject: [PATCH] feat: add pinned Nasiko control-plane bridge --- manifests/install-components.json | 8 + manifests/install-modules.json | 30 ++ package.json | 3 + scripts/ecc.js | 8 + scripts/lib/nasiko-release.js | 323 ++++++++++++++++++ scripts/nasiko.js | 134 ++++++++ skills/nasiko-control-plane/SKILL.md | 44 +++ .../nasiko-control-plane/agents/openai.yaml | 4 + 8 files changed, 554 insertions(+) create mode 100644 scripts/lib/nasiko-release.js create mode 100644 scripts/nasiko.js create mode 100644 skills/nasiko-control-plane/SKILL.md create mode 100644 skills/nasiko-control-plane/agents/openai.yaml diff --git a/manifests/install-components.json b/manifests/install-components.json index a5f976a94..971f86607 100644 --- a/manifests/install-components.json +++ b/manifests/install-components.json @@ -202,6 +202,14 @@ "ito-compute" ] }, + { + "id": "capability:nasiko-control-plane", + "family": "capability", + "description": "Explicitly gated Nasiko control-plane installation, status, and agent-operations guidance with pinned artifact verification and opt-in telemetry boundaries.", + "modules": [ + "nasiko-control-plane" + ] + }, { "id": "capability:social", "family": "capability", diff --git a/manifests/install-modules.json b/manifests/install-modules.json index 8c0ea11d8..f4560b593 100644 --- a/manifests/install-modules.json +++ b/manifests/install-modules.json @@ -635,6 +635,36 @@ "cost": "light", "stability": "beta" }, + { + "id": "nasiko-control-plane", + "kind": "skills", + "description": "Explicitly gated Nasiko control-plane installation, status, and agent-operations guidance with pinned artifact verification and opt-in telemetry boundaries.", + "paths": [ + "skills/nasiko-control-plane" + ], + "targets": [ + "claude", + "claude-project", + "cursor", + "antigravity", + "codex", + "gemini", + "opencode", + "codebuddy", + "joycode", + "qwen", + "zed", + "hermes", + "openclaw", + "kimi" + ], + "dependencies": [ + "platform-configs" + ], + "defaultInstall": false, + "cost": "light", + "stability": "experimental" + }, { "id": "social-distribution", "kind": "skills", diff --git a/package.json b/package.json index 56ac663db..d4ed22ab2 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,8 @@ "scripts/install-guided.js", "scripts/install-plan.js", "scripts/ito.js", + "scripts/nasiko.js", + "scripts/lib/nasiko-release.js", "scripts/lib/", "scripts/list-installed.js", "scripts/loop-status.js", @@ -233,6 +235,7 @@ "skills/ito-compute/", "skills/ito-inference/", "skills/ito-training/", + "skills/nasiko-control-plane/", "skills/investor-materials/", "skills/investor-outreach/", "skills/iterative-retrieval/", diff --git a/scripts/ecc.js b/scripts/ecc.js index 3caff5735..8a92fa302 100755 --- a/scripts/ecc.js +++ b/scripts/ecc.js @@ -39,6 +39,10 @@ const COMMANDS = { script: 'ito.js', description: 'Invoke the separately installed canonical Itô compute CLI', }, + nasiko: { + script: 'nasiko.js', + description: 'Install or inspect the optional pinned Nasiko control-plane CLI', + }, memory: { script: 'memory.js', description: 'Share durable context across Claude, Codex, Hermes, and other harnesses', @@ -110,6 +114,7 @@ const PRIMARY_COMMANDS = [ 'consult', 'control-pane', 'ito', + 'nasiko', 'memory', 'list-installed', 'doctor', @@ -168,6 +173,9 @@ Examples: 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 + ecc nasiko status --json + ecc nasiko install --version v0.1.0 --dry-run --json + ecc nasiko install --version v0.1.0 --yes --json ecc ito evals --cluster clu_prod_example --live-sixtytwo --nodes gpu-01,gpu-02 --config-dir /absolute/path/to/qualification-config ecc memory init ecc memory handoff --from codex --target claude --title "Continue migration" --stdin diff --git a/scripts/lib/nasiko-release.js b/scripts/lib/nasiko-release.js new file mode 100644 index 000000000..0ffc4b618 --- /dev/null +++ b/scripts/lib/nasiko-release.js @@ -0,0 +1,323 @@ +'use strict'; + +const crypto = require('crypto'); +const fs = require('fs'); +const https = require('https'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); + +const REGISTRY_ORIGIN = 'https://registry.nasiko.dev'; +const REPOSITORY = 'nasiko/nasiko'; +const MAX_MANIFEST_BYTES = 1024 * 1024; +const MAX_ARCHIVE_BYTES = 100 * 1024 * 1024; +const SHA256_PATTERN = /^sha256:[a-f0-9]{64}$/; + +const QUALIFIED_RELEASES = Object.freeze({ + 'v0.1.0': Object.freeze({ + 'linux/amd64': 'sha256:0df748a40f3d714b6b6a3376a1d13a224c05bdb8d1628f31ace5a7bee8ceb9de', + 'linux/arm64': 'sha256:655021a129c7df4621a80d16ea4eab38018530bfe9a20da646641d9f4ac5c249', + 'darwin/amd64': 'sha256:b4188482621efd7da5a2ab630f653665ab5f80b9aceae448b6bb5fc93e003f06', + 'darwin/arm64': 'sha256:ce7e54fa19f989a5d125c4409b3587ca9503bb5a07bc5ff223c60e0fbad437f0', + 'windows/amd64': 'sha256:0760fe1fc98e8fedb66796aaf891a1de9268af1338c5e88b949656fda5d9f045', + }), +}); + +function normalizePlatform(platform = process.platform, architecture = process.arch) { + const osName = platform === 'win32' ? 'windows' : platform; + if (!['linux', 'darwin', 'windows'].includes(osName)) { + throw new Error(`Unsupported platform: ${platform}`); + } + const arch = architecture === 'x64' ? 'amd64' : architecture; + if (!['amd64', 'arm64'].includes(arch)) { + throw new Error(`Unsupported architecture: ${architecture}`); + } + if (osName === 'windows' && arch !== 'amd64') { + throw new Error(`Unsupported architecture for Windows: ${architecture}`); + } + return { + os: osName, + arch, + binaryName: osName === 'windows' ? 'nasiko.exe' : 'nasiko', + }; +} + +function getQualifiedRelease(version, platform = process.platform, architecture = process.arch) { + if (!/^v\d+\.\d+\.\d+$/.test(String(version || ''))) { + throw new Error('Nasiko installation requires a pinned version such as v0.1.0; latest is not allowed.'); + } + const release = QUALIFIED_RELEASES[version]; + if (!release) { + throw new Error(`Nasiko ${version} is not qualified by this ECC release.`); + } + const normalized = normalizePlatform(platform, architecture); + const manifestDigest = release[`${normalized.os}/${normalized.arch}`]; + if (!manifestDigest) { + throw new Error(`Nasiko ${version} is not qualified for ${normalized.os}/${normalized.arch}.`); + } + return { version, ...normalized, manifestDigest }; +} + +function digestBytes(bytes) { + return `sha256:${crypto.createHash('sha256').update(bytes).digest('hex')}`; +} + +function assertDigest(bytes, expectedDigest, label) { + if (!SHA256_PATTERN.test(expectedDigest)) { + throw new Error(`${label} has an invalid expected digest.`); + } + const actualDigest = digestBytes(bytes); + if (actualDigest !== expectedDigest) { + throw new Error(`${label} digest mismatch: expected ${expectedDigest}, got ${actualDigest}.`); + } +} + +function validateManifest(manifestBytes) { + let manifest; + try { + manifest = JSON.parse(manifestBytes.toString('utf8')); + } catch (_error) { + throw new Error('Nasiko manifest is not valid JSON.'); + } + if (manifest.schemaVersion !== 2 || !Array.isArray(manifest.layers) || manifest.layers.length !== 1) { + throw new Error('Nasiko manifest must contain exactly one OCI layer.'); + } + const layer = manifest.layers[0]; + if (layer.mediaType !== 'application/gzip' || !SHA256_PATTERN.test(layer.digest)) { + throw new Error('Nasiko manifest layer is not a qualified gzip artifact.'); + } + if (!Number.isSafeInteger(layer.size) || layer.size <= 0 || layer.size > MAX_ARCHIVE_BYTES) { + throw new Error('Nasiko manifest layer size is outside the allowed range.'); + } + return { digest: layer.digest, size: layer.size }; +} + +function validateArchiveEntries(entries, expectedBinaryName) { + if (!Array.isArray(entries) || entries.length !== 1) { + throw new Error('Unsafe archive: expected exactly one binary file.'); + } + const [entry] = entries; + const normalizedPath = String(entry.path || '').replace(/^\.\//, ''); + if (normalizedPath !== expectedBinaryName || normalizedPath.includes('..') || path.isAbsolute(normalizedPath)) { + throw new Error('Unsafe archive path: expected only the Nasiko binary.'); + } + if (entry.type !== 'file') { + throw new Error('Nasiko archive entry must be a regular file.'); + } + return true; +} + +function fetchBytes(url, options = {}) { + const maxBytes = options.maxBytes || MAX_ARCHIVE_BYTES; + const timeoutMs = options.timeoutMs || 15000; + const parsed = new URL(url); + if (parsed.origin !== REGISTRY_ORIGIN || parsed.protocol !== 'https:') { + return Promise.reject(new Error('Nasiko download origin is not allowed.')); + } + return new Promise((resolve, reject) => { + const request = https.get(parsed, { + headers: options.accept ? { Accept: options.accept } : {}, + }, response => { + if (response.statusCode >= 300 && response.statusCode < 400) { + response.resume(); + reject(new Error('Nasiko registry redirects are not allowed.')); + return; + } + if (response.statusCode !== 200) { + response.resume(); + reject(new Error(`Nasiko registry returned HTTP ${response.statusCode}.`)); + return; + } + const chunks = []; + let totalBytes = 0; + response.on('data', chunk => { + totalBytes += chunk.length; + if (totalBytes > maxBytes) { + request.destroy(new Error('Nasiko registry response exceeded the size limit.')); + return; + } + chunks.push(chunk); + }); + response.on('end', () => resolve(Buffer.concat(chunks))); + response.on('error', reject); + }); + request.setTimeout(timeoutMs, () => request.destroy(new Error('Nasiko registry request timed out.'))); + request.on('error', reject); + }); +} + +function inspectArchive(archivePath) { + const result = spawnSync('tar', ['-tvzf', archivePath], { + encoding: 'utf8', + shell: false, + timeout: 15000, + }); + if (result.status !== 0) { + throw new Error('Nasiko archive inspection failed.'); + } + return result.stdout.split(/\r?\n/).filter(Boolean).map(line => { + const typeMarker = line[0]; + const entryPath = line.trim().split(/\s+/).at(-1); + return { + path: entryPath, + type: typeMarker === '-' ? 'file' : typeMarker === 'l' ? 'symlink' : 'other', + }; + }); +} + +function extractArchive(archivePath, destination) { + const result = spawnSync('tar', ['-xzf', archivePath, '-C', destination], { + encoding: 'utf8', + shell: false, + timeout: 30000, + }); + if (result.status !== 0) { + throw new Error('Nasiko archive extraction failed.'); + } +} + +function runVersion(executable) { + return spawnSync(executable, ['--version'], { + encoding: 'utf8', + shell: false, + timeout: 10000, + }); +} + +function defaultInstallDirectory(normalized, environment = process.env, homeDirectory = os.homedir()) { + if (normalized.os === 'windows') { + if (!environment.LOCALAPPDATA) throw new Error('LOCALAPPDATA is required on Windows.'); + return path.join(environment.LOCALAPPDATA, 'nasiko', 'bin'); + } + return path.join(homeDirectory, '.local', 'bin'); +} + +function validateInstallDirectory(installDirectory) { + if (typeof installDirectory !== 'string' || installDirectory.includes('\0') || !path.isAbsolute(installDirectory)) { + throw new Error('Nasiko install directory must be an absolute path.'); + } + const resolved = path.resolve(installDirectory); + if (resolved === path.parse(resolved).root) { + throw new Error('Nasiko cannot install directly into a filesystem root.'); + } + return resolved; +} + +function assertDirectoryNotSymlink(directoryPath) { + if (!fs.existsSync(directoryPath)) return; + const stats = fs.lstatSync(directoryPath); + if (!stats.isDirectory() || stats.isSymbolicLink()) { + throw new Error('Nasiko install directory must be a real directory, not a symlink.'); + } +} + +async function installNasiko(options = {}, dependencies = {}) { + const version = options.version || 'v0.1.0'; + const qualified = getQualifiedRelease( + version, + dependencies.platform || process.platform, + dependencies.arch || process.arch + ); + const release = dependencies.releaseOverride + ? { ...qualified, ...dependencies.releaseOverride } + : qualified; + const installDirectory = validateInstallDirectory(options.installDir || defaultInstallDirectory( + release, + dependencies.environment || process.env, + dependencies.homeDirectory || os.homedir() + )); + const destination = path.join(installDirectory, release.binaryName); + const plan = { + dryRun: Boolean(options.dryRun), + version, + platform: release.os, + architecture: release.arch, + manifestDigest: release.manifestDigest, + registryOrigin: REGISTRY_ORIGIN, + destination, + }; + if (options.dryRun) return plan; + if (!options.yes) throw new Error('Nasiko installation requires explicit --yes consent.'); + + assertDirectoryNotSymlink(installDirectory); + fs.mkdirSync(installDirectory, { recursive: true, mode: 0o755 }); + assertDirectoryNotSymlink(installDirectory); + if (fs.existsSync(destination)) { + if (fs.lstatSync(destination).isSymbolicLink()) { + throw new Error('Refusing to replace a symlinked Nasiko executable.'); + } + const existing = (dependencies.runVersion || runVersion)(destination); + const output = `${existing.stdout || ''}\n${existing.stderr || ''}`; + if (existing.status === 0 && output.includes(version)) { + return { ...plan, dryRun: false, installed: true, reused: true }; + } + throw new Error('An incompatible Nasiko executable already exists at the destination.'); + } + + const retrieve = dependencies.fetchBytes || fetchBytes; + const manifestUrl = `${REGISTRY_ORIGIN}/v2/${REPOSITORY}/manifests/${release.manifestDigest}`; + const manifestBytes = await retrieve(manifestUrl, { + accept: 'application/vnd.oci.image.manifest.v1+json', + maxBytes: MAX_MANIFEST_BYTES, + }); + assertDigest(manifestBytes, release.manifestDigest, 'Nasiko manifest'); + const layer = validateManifest(manifestBytes); + const archiveUrl = `${REGISTRY_ORIGIN}/v2/${REPOSITORY}/blobs/${layer.digest}`; + const archiveBytes = await retrieve(archiveUrl, { maxBytes: MAX_ARCHIVE_BYTES }); + if (archiveBytes.length !== layer.size) throw new Error('Nasiko archive size mismatch.'); + assertDigest(archiveBytes, layer.digest, 'Nasiko archive'); + + const temporaryDirectory = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-nasiko-install-')); + try { + const archivePath = path.join(temporaryDirectory, 'nasiko.tar.gz'); + const extractionDirectory = path.join(temporaryDirectory, 'extract'); + fs.mkdirSync(extractionDirectory, { mode: 0o700 }); + fs.writeFileSync(archivePath, archiveBytes, { mode: 0o600 }); + const inspect = dependencies.inspectArchive || inspectArchive; + validateArchiveEntries(inspect(archivePath), release.binaryName); + (dependencies.extractArchive || extractArchive)(archivePath, extractionDirectory); + const extractedBinary = path.join(extractionDirectory, release.binaryName); + const extractedStats = fs.lstatSync(extractedBinary); + if (!extractedStats.isFile() || extractedStats.isSymbolicLink()) { + throw new Error('Extracted Nasiko binary is not a regular file.'); + } + fs.chmodSync(extractedBinary, 0o755); + const stagedDestination = path.join(installDirectory, `.${release.binaryName}.tmp-${process.pid}`); + fs.copyFileSync(extractedBinary, stagedDestination, fs.constants.COPYFILE_EXCL); + fs.chmodSync(stagedDestination, 0o755); + fs.renameSync(stagedDestination, destination); + const versionResult = (dependencies.runVersion || runVersion)(destination); + const versionOutput = `${versionResult.stdout || ''}\n${versionResult.stderr || ''}`; + if (versionResult.status !== 0 || !versionOutput.includes(version)) { + fs.rmSync(destination, { force: true }); + throw new Error('Installed Nasiko binary did not report the qualified version.'); + } + const metadata = { + version, + platform: release.os, + architecture: release.arch, + manifestDigest: release.manifestDigest, + artifactDigest: layer.digest, + installedPath: destination, + }; + const metadataPath = path.join(installDirectory, '.ecc-nasiko-install.json'); + const temporaryMetadata = `${metadataPath}.tmp-${process.pid}`; + fs.writeFileSync(temporaryMetadata, `${JSON.stringify(metadata, null, 2)}\n`, { mode: 0o600 }); + fs.renameSync(temporaryMetadata, metadataPath); + return { ...plan, dryRun: false, installed: true, reused: false, artifactDigest: layer.digest }; + } finally { + fs.rmSync(temporaryDirectory, { recursive: true, force: true }); + } +} + +module.exports = { + QUALIFIED_RELEASES, + REGISTRY_ORIGIN, + digestBytes, + fetchBytes, + getQualifiedRelease, + installNasiko, + normalizePlatform, + validateArchiveEntries, + validateInstallDirectory, +}; diff --git a/scripts/nasiko.js b/scripts/nasiko.js new file mode 100644 index 000000000..772b803dc --- /dev/null +++ b/scripts/nasiko.js @@ -0,0 +1,134 @@ +#!/usr/bin/env node +'use strict'; + +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const { spawnSync } = require('child_process'); +const { + installNasiko, + normalizePlatform, + validateInstallDirectory, +} = require('./lib/nasiko-release'); + +function helpText() { + return ` +ECC Nasiko control-plane bridge + +Usage: + ecc nasiko status [--json] + ecc nasiko install --version v0.1.0 --yes [--install-dir ] [--json] + ecc nasiko install --version v0.1.0 --dry-run [--install-dir ] [--json] + +The installer is opt-in, accepts only ECC-qualified pinned releases, downloads +content-addressed OCI artifacts from registry.nasiko.dev, verifies SHA-256 +digests before extraction, and never executes fetched shell or PowerShell code. +`; +} + +function parseInstallArguments(argumentsList) { + let options = { dryRun: false, installDir: undefined, json: false, version: undefined, yes: false }; + for (let index = 0; index < argumentsList.length; index += 1) { + const argument = argumentsList[index]; + if (argument === '--version' || argument === '--install-dir') { + const value = argumentsList[index + 1]; + if (!value || value.startsWith('--')) throw new Error(`Missing value for ${argument}.`); + options = { + ...options, + [argument === '--version' ? 'version' : 'installDir']: value, + }; + index += 1; + } else if (argument === '--yes' || argument === '-y') { + options = { ...options, yes: true }; + } else if (argument === '--dry-run') { + options = { ...options, dryRun: true }; + } else if (argument === '--json') { + options = { ...options, json: true }; + } else { + throw new Error(`Unknown Nasiko install argument: ${argument}`); + } + } + if (!options.version) throw new Error('Nasiko install requires --version v0.1.0.'); + if (options.installDir) validateInstallDirectory(options.installDir); + return options; +} + +function defaultExecutablePath() { + const normalized = normalizePlatform(); + if (normalized.os === 'windows') { + return process.env.LOCALAPPDATA + ? path.join(process.env.LOCALAPPDATA, 'nasiko', 'bin', normalized.binaryName) + : null; + } + return path.join(os.homedir(), '.local', 'bin', normalized.binaryName); +} + +function resolveExecutable() { + const configured = process.env.ECC_NASIKO_CLI_EXECUTABLE; + const candidate = configured || defaultExecutablePath(); + if (!candidate) return null; + if (!path.isAbsolute(candidate)) { + throw new Error('ECC_NASIKO_CLI_EXECUTABLE must be an absolute path.'); + } + if (!fs.existsSync(candidate)) return null; + const stats = fs.lstatSync(candidate); + if (!stats.isFile() || stats.isSymbolicLink()) { + throw new Error('Nasiko executable must be a regular file, not a symlink.'); + } + return candidate; +} + +function readStatus() { + const executable = resolveExecutable(); + if (!executable) return { installed: false, version: null, executable: null }; + const result = spawnSync(executable, ['--version'], { + encoding: 'utf8', + shell: false, + timeout: 10000, + }); + if (result.status !== 0) { + throw new Error('Nasiko executable failed its version check.'); + } + const output = `${result.stdout || ''}\n${result.stderr || ''}`; + const version = output.match(/\bv\d+\.\d+\.\d+\b/)?.[0] || null; + if (!version) throw new Error('Nasiko executable returned an unrecognized version.'); + return { installed: true, version, executable }; +} + +async function main(argumentsList = process.argv.slice(2)) { + const [command, ...rest] = argumentsList; + if (!command || command === '--help' || command === '-h' || command === 'help') { + process.stdout.write(helpText()); + return 0; + } + if (command === 'status') { + const unknown = rest.filter(argument => argument !== '--json'); + if (unknown.length > 0) throw new Error(`Unknown Nasiko status argument: ${unknown[0]}`); + const status = readStatus(); + if (rest.includes('--json')) process.stdout.write(`${JSON.stringify(status, null, 2)}\n`); + else process.stdout.write(status.installed + ? `Nasiko ${status.version} is installed at ${status.executable}.\n` + : 'Nasiko is not installed in the ECC-qualified location.\n'); + return 0; + } + if (command === 'install') { + const options = parseInstallArguments(rest); + const result = await installNasiko(options); + if (options.json) process.stdout.write(`${JSON.stringify(result, null, 2)}\n`); + else if (result.dryRun) process.stdout.write(`Would install Nasiko ${result.version} to ${result.destination}.\n`); + else process.stdout.write(`${result.reused ? 'Using existing' : 'Installed'} Nasiko ${result.version} at ${result.destination}.\n`); + return 0; + } + throw new Error(`Unsupported Nasiko command: ${command}`); +} + +if (require.main === module) { + main().then(code => { + process.exitCode = code; + }).catch(error => { + process.stderr.write(`Error: ${String(error?.message || error).replace(/[\r\n]+/g, ' ')}\n`); + process.exitCode = 1; + }); +} + +module.exports = { main, parseInstallArguments, readStatus, resolveExecutable }; diff --git a/skills/nasiko-control-plane/SKILL.md b/skills/nasiko-control-plane/SKILL.md new file mode 100644 index 000000000..3002c580c --- /dev/null +++ b/skills/nasiko-control-plane/SKILL.md @@ -0,0 +1,44 @@ +--- +name: nasiko-control-plane +description: Install, detect, and operate the optional Nasiko agent control plane through ECC with pinned artifacts, explicit consent, and telemetry and secrets boundaries. +--- + +# Nasiko Control Plane + +Use this skill when a user explicitly asks to install, inspect, or operate the +Nasiko control plane with ECC. + +## Safety contract + +- Begin with `ecc nasiko status --json`. Status is read-only. +- Installation always requires explicit user consent and `--yes`. +- Install only an ECC-qualified pinned version, currently `v0.1.0`. +- Preview first with `ecc nasiko install --version v0.1.0 --dry-run --json`. +- Install with `ecc nasiko install --version v0.1.0 --yes --json` only after the + user reviews the version, registry origin, digest, and destination. +- Never replace the qualified command with a downloaded shell or PowerShell + bootstrap script. +- Never put secrets or credentials in command arguments, logs, skill output, + install metadata, or ECC state. +- Nasiko telemetry and any sharing with Nasiko or Ito must be opt-in and + separately disclosed. Installation is not telemetry consent. + +## Lifecycle boundary + +The initial ECC bridge supports only qualified installation and read-only +status. Use the canonical Nasiko CLI directly for connection, authentication, +launch, deployment, or shutdown until those verbs have their own verified ECC +contracts. Do not guess CLI verbs. + +Installing the CLI does not prove that a control-plane server is running, an +agent is governed, routing or ACLs work, observability is complete, telemetry +was enabled, or Ito compute is connected. Report each state separately. + +## Failure behavior + +- If the platform, architecture, version, manifest, digest, archive, binary, or + destination fails validation, stop without executing the artifact. +- Do not fall back to `latest`. +- Do not search arbitrary `PATH` entries. Use ECC's qualified location or an + explicit absolute `ECC_NASIKO_CLI_EXECUTABLE` for development verification. +- Do not treat a partial or ambiguous installation as success. diff --git a/skills/nasiko-control-plane/agents/openai.yaml b/skills/nasiko-control-plane/agents/openai.yaml new file mode 100644 index 000000000..6168412b7 --- /dev/null +++ b/skills/nasiko-control-plane/agents/openai.yaml @@ -0,0 +1,4 @@ +interface: + display_name: "Nasiko Control Plane" + short_description: "Safely install and inspect the optional Nasiko control plane" + default_prompt: "Use $nasiko-control-plane to inspect or explicitly install the pinned Nasiko CLI without enabling telemetry or exposing secrets."