mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-22 02:56:52 +03:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca40a4900f | ||
|
|
107de24e64 |
@@ -358,31 +358,30 @@ As you run commands, Impeccable writes working files under `.impeccable/`: criti
|
||||
```gitignore
|
||||
# impeccable-ignore-start
|
||||
# Ephemeral output, runtime state, and per-dev overrides.
|
||||
# Unanchored: .impeccable may sit at the repo root or under a nested
|
||||
# workspace (apps/web/.impeccable/...); anchored patterns would miss it.
|
||||
# The **/ prefix covers .impeccable at the repo root or in a nested workspace.
|
||||
# Shared artifacts stay tracked: config.json, live/config.json,
|
||||
# design.json, surfaces/*.md, critique/*.md.
|
||||
.impeccable/config.local.json
|
||||
.impeccable/hook.cache.json
|
||||
.impeccable/hook.pending.json
|
||||
.impeccable/*.png
|
||||
.impeccable/review/
|
||||
.impeccable/questions/
|
||||
.impeccable/live/server.json
|
||||
.impeccable/live/sessions/
|
||||
.impeccable/live/previews/
|
||||
.impeccable/live/annotations/
|
||||
.impeccable/live/cache/
|
||||
.impeccable/live/manual-edit-apply-transaction.json
|
||||
.impeccable/live/manual-edit-events.jsonl
|
||||
.impeccable/live/manual-edit-evidence/
|
||||
.impeccable/live/pending-manual-edits.json
|
||||
.impeccable/live/deferred-svelte-component-accepts.json
|
||||
.impeccable/live/*.png
|
||||
**/.impeccable/config.local.json
|
||||
**/.impeccable/hook.cache.json
|
||||
**/.impeccable/hook.pending.json
|
||||
**/.impeccable/*.png
|
||||
**/.impeccable/review/
|
||||
**/.impeccable/questions/
|
||||
**/.impeccable/live/server.json
|
||||
**/.impeccable/live/sessions/
|
||||
**/.impeccable/live/previews/
|
||||
**/.impeccable/live/annotations/
|
||||
**/.impeccable/live/cache/
|
||||
**/.impeccable/live/manual-edit-apply-transaction.json
|
||||
**/.impeccable/live/manual-edit-events.jsonl
|
||||
**/.impeccable/live/manual-edit-evidence/
|
||||
**/.impeccable/live/pending-manual-edits.json
|
||||
**/.impeccable/live/deferred-svelte-component-accepts.json
|
||||
**/.impeccable/live/*.png
|
||||
# impeccable-ignore-end
|
||||
```
|
||||
|
||||
The block is wrapped in `# impeccable-ignore-start` / `# impeccable-ignore-end` markers so you can recognize and refresh it later. Patterns are unanchored on purpose: in a monorepo the active project (and its `.impeccable/` directory) often lives under a nested workspace path like `apps/web/`, and a root-anchored pattern would miss it.
|
||||
The block is wrapped in `# impeccable-ignore-start` / `# impeccable-ignore-end` markers so you can recognize and refresh it later. The `**/` prefix makes each pattern match whether the active project's `.impeccable/` directory is at the repository root or under a nested workspace path like `apps/web/`.
|
||||
|
||||
**Keep these tracked** (they are shared project artifacts, do not add them to `.gitignore`):
|
||||
|
||||
|
||||
@@ -52,7 +52,7 @@ fn run(args: &[String], io: &mut Io) -> i32 {
|
||||
"detect" => impeccable_detect::run_detect(rest, io, &engines()),
|
||||
"ignores" | "ignore" => impeccable_detect::run_ignores(rest, io),
|
||||
"skills" => impeccable_skills::run(rest, io),
|
||||
"help" | "install" | "link" | "update" | "check" | "verify-bundle" => impeccable_skills::run(args, io),
|
||||
"help" | "install" | "link" | "update" | "check" => impeccable_skills::run(args, io),
|
||||
// skill scripts
|
||||
"context" => impeccable_context::run_context(rest, io),
|
||||
"pin" => impeccable_context::run_pin(rest, io),
|
||||
|
||||
@@ -67,7 +67,6 @@ Commands:
|
||||
link Symlink skills from a local checkout or submodule
|
||||
update Update skills to the latest version
|
||||
check Check if skill updates are available
|
||||
verify-bundle Verify a local skill bundle against the pinned signing keys
|
||||
|
||||
Options:
|
||||
--help Show this help message
|
||||
|
||||
@@ -31,15 +31,15 @@ pub(crate) fn release_version(location: &str) -> Result<String, String> {
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase", deny_unknown_fields)]
|
||||
pub(crate) struct Envelope {
|
||||
struct Envelope {
|
||||
schema: u32,
|
||||
pub(crate) key_id: String,
|
||||
pub(crate) version: String,
|
||||
pub(crate) artifact: String,
|
||||
pub(crate) size: u64,
|
||||
pub(crate) sha256: String,
|
||||
key_id: String,
|
||||
version: String,
|
||||
artifact: String,
|
||||
size: u64,
|
||||
sha256: String,
|
||||
signature: String,
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ pub(crate) fn verify_reader(
|
||||
signature: &[u8],
|
||||
version: &str,
|
||||
keys: &TrustedKeys,
|
||||
) -> Result<Envelope, String> {
|
||||
) -> Result<(), String> {
|
||||
if signature.len() as u64 > MAX_SIGNATURE_BYTES {
|
||||
return Err("Bundle signature is too large".into());
|
||||
}
|
||||
@@ -108,7 +108,7 @@ pub(crate) fn verify_reader(
|
||||
if size != envelope.size || format!("{:x}", hash.finalize()) != envelope.sha256 {
|
||||
return Err("Bundle digest or size does not match its signature".into());
|
||||
}
|
||||
Ok(envelope)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -55,7 +55,6 @@ pub fn run(args: &[String], io: &mut Io) -> R<()> {
|
||||
"link" => link(&rest, io),
|
||||
"update" => update(&rest, io),
|
||||
"check" => check(io),
|
||||
"verify-bundle" => crate::verify_bundle::run(&rest, io),
|
||||
other => {
|
||||
io.err(&format!("Unknown skills command: {other}\n"));
|
||||
io.err("Run 'impeccable --help' for available commands.\n");
|
||||
|
||||
@@ -34,7 +34,6 @@ pub mod hook_manifest;
|
||||
pub mod prompt;
|
||||
pub mod providers;
|
||||
pub mod util;
|
||||
mod verify_bundle;
|
||||
|
||||
use impeccable_common::Io;
|
||||
|
||||
|
||||
@@ -1,230 +0,0 @@
|
||||
//! Offline verification of local release assets using the installer's trust roots.
|
||||
use std::fs::File;
|
||||
use std::io::{BufReader, Read};
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use impeccable_common::Io;
|
||||
|
||||
use crate::bundle_signature::{self, Envelope, TrustedKeys, MAX_SIGNATURE_BYTES};
|
||||
use crate::{Flow, R};
|
||||
|
||||
const USAGE: &str = "Usage: impeccable verify-bundle <zip> --version <expected-version> [options]
|
||||
|
||||
Verify a local skill bundle's signature and SHA-256 using the pinned signing keys.
|
||||
Runs offline without extracting files, installing skills, or enabling hooks.
|
||||
|
||||
Options:
|
||||
--version <version> Required expected skill version (for example, 4.3.1)
|
||||
--signature <path> Signature manifest (default: <zip>.sig.json)
|
||||
--json Print verified metadata as JSON
|
||||
-h, --help Show this help message
|
||||
|
||||
Exit codes: 0 verified, 1 verification or file error, 2 invalid arguments.
|
||||
";
|
||||
|
||||
struct Options {
|
||||
bundle: PathBuf,
|
||||
signature: PathBuf,
|
||||
version: String,
|
||||
json: bool,
|
||||
}
|
||||
|
||||
fn parse(args: &[String]) -> Result<Options, String> {
|
||||
let (mut bundle, mut signature, mut version) = (None, None, None);
|
||||
let mut json = false;
|
||||
let mut positional = false;
|
||||
let mut args = args.iter();
|
||||
while let Some(arg) = args.next() {
|
||||
if !positional && arg == "--" {
|
||||
positional = true;
|
||||
} else if !positional && arg == "--json" {
|
||||
json = true;
|
||||
} else if !positional
|
||||
&& (arg == "--version"
|
||||
|| arg == "--signature"
|
||||
|| arg.starts_with("--version=")
|
||||
|| arg.starts_with("--signature="))
|
||||
{
|
||||
let (name, value) = match arg.split_once('=') {
|
||||
Some(pair) => pair,
|
||||
None => (arg.as_str(), args.next().map(String::as_str).unwrap_or("")),
|
||||
};
|
||||
if value.is_empty() || value.starts_with('-') {
|
||||
return Err(format!("{name} requires a value"));
|
||||
}
|
||||
let slot = if name == "--version" {
|
||||
&mut version
|
||||
} else {
|
||||
&mut signature
|
||||
};
|
||||
if slot.replace(value.to_string()).is_some() {
|
||||
return Err(format!("{name} may only be specified once"));
|
||||
}
|
||||
} else if !positional && arg.starts_with('-') {
|
||||
return Err(format!("Unknown option: {arg}"));
|
||||
} else if bundle.replace(PathBuf::from(arg)).is_some() {
|
||||
return Err("Expected exactly one local bundle path".into());
|
||||
}
|
||||
}
|
||||
let bundle = bundle.ok_or("A local bundle path is required")?;
|
||||
let version = version.ok_or("--version is required; specify the expected skill release")?;
|
||||
// Reuse the installer's exact release-version grammar.
|
||||
bundle_signature::release_version(&format!(
|
||||
"https://github.com/pbakaus/impeccable/releases/download/skill-v{version}/universal.zip"
|
||||
))?;
|
||||
let signature = signature.map(PathBuf::from).unwrap_or_else(|| {
|
||||
let mut path = bundle.as_os_str().to_os_string();
|
||||
path.push(".sig.json");
|
||||
PathBuf::from(path)
|
||||
});
|
||||
Ok(Options {
|
||||
bundle,
|
||||
signature,
|
||||
version,
|
||||
json,
|
||||
})
|
||||
}
|
||||
|
||||
fn verify(options: &Options, cwd: &Path, keys: &TrustedKeys) -> Result<Envelope, String> {
|
||||
let read_error = |path: &Path, error| format!("Could not read {}: {error}", path.display());
|
||||
let file =
|
||||
File::open(cwd.join(&options.signature)).map_err(|e| read_error(&options.signature, e))?;
|
||||
let mut signature = Vec::new();
|
||||
file.take(MAX_SIGNATURE_BYTES + 1)
|
||||
.read_to_end(&mut signature)
|
||||
.map_err(|e| read_error(&options.signature, e))?;
|
||||
let file = File::open(cwd.join(&options.bundle)).map_err(|e| read_error(&options.bundle, e))?;
|
||||
bundle_signature::verify_reader(
|
||||
&mut BufReader::new(file),
|
||||
&signature,
|
||||
&options.version,
|
||||
keys,
|
||||
)
|
||||
}
|
||||
|
||||
pub(crate) fn run(args: &[String], io: &mut Io) -> R<()> {
|
||||
if args
|
||||
.iter()
|
||||
.take_while(|a| a.as_str() != "--")
|
||||
.any(|a| a == "--help" || a == "-h")
|
||||
{
|
||||
io.out(USAGE);
|
||||
return Ok(());
|
||||
}
|
||||
let options = parse(args).map_err(|e| {
|
||||
io.err(&format!("{e}\n\n{USAGE}"));
|
||||
Flow::Exit(2)
|
||||
})?;
|
||||
let verified = bundle_signature::trusted_keys()
|
||||
.and_then(|keys| verify(&options, &io.cwd, &keys))
|
||||
.map_err(|e| Flow::Throw(format!("{}{e}", bundle_signature::ERROR_PREFIX)))?;
|
||||
if options.json {
|
||||
io.out(&format!(
|
||||
"{}\n",
|
||||
serde_json::json!({
|
||||
"verified": true, "version": verified.version, "artifact": verified.artifact,
|
||||
"keyId": verified.key_id, "size": verified.size, "sha256": verified.sha256,
|
||||
})
|
||||
));
|
||||
} else {
|
||||
io.out(&format!(
|
||||
"Verified {} (skill-v{}).\nSigning key: {}\nSHA-256: {}\nSize: {} bytes\n",
|
||||
verified.artifact, verified.version, verified.key_id, verified.sha256, verified.size
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn verifies_local_pair_and_rejects_tampering_without_writes() {
|
||||
let fixture: serde_json::Value = serde_json::from_str(include_str!(
|
||||
"../../../tests/fixtures/bundle-signature.json"
|
||||
))
|
||||
.unwrap();
|
||||
let root = crate::util::mkdtemp(
|
||||
&std::env::temp_dir()
|
||||
.join("verify-bundle-")
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.unwrap();
|
||||
let root = Path::new(&root);
|
||||
let bundle = fixture["bundle"].as_str().unwrap().as_bytes();
|
||||
let signature = serde_json::to_vec(&fixture["envelope"]).unwrap();
|
||||
std::fs::write(root.join("renamed.zip"), bundle).unwrap();
|
||||
std::fs::write(root.join("manifest.json"), &signature).unwrap();
|
||||
let options = Options {
|
||||
bundle: "renamed.zip".into(),
|
||||
signature: "manifest.json".into(),
|
||||
version: "4.2.0".into(),
|
||||
json: true,
|
||||
};
|
||||
let keys = serde_json::from_value(fixture["keys"].clone()).unwrap();
|
||||
let result = verify(&options, root, &keys).unwrap();
|
||||
assert_eq!(result.version, "4.2.0");
|
||||
assert_eq!(result.sha256, fixture["envelope"]["sha256"]);
|
||||
assert!(verify(&options, root, &bundle_signature::trusted_keys().unwrap()).is_err());
|
||||
let wrong_release = Options {
|
||||
version: "4.2.1".into(),
|
||||
..options
|
||||
};
|
||||
assert!(verify(&wrong_release, root, &keys).is_err());
|
||||
let options = Options {
|
||||
version: "4.2.0".into(),
|
||||
..wrong_release
|
||||
};
|
||||
std::fs::write(root.join("renamed.zip"), b"tampered").unwrap();
|
||||
assert!(verify(&options, root, &keys).is_err());
|
||||
std::fs::write(root.join("renamed.zip"), bundle).unwrap();
|
||||
std::fs::write(
|
||||
root.join("manifest.json"),
|
||||
vec![b' '; MAX_SIGNATURE_BYTES as usize + 1],
|
||||
)
|
||||
.unwrap();
|
||||
assert!(verify(&options, root, &keys)
|
||||
.unwrap_err()
|
||||
.contains("too large"));
|
||||
std::fs::remove_file(root.join("manifest.json")).unwrap();
|
||||
assert!(verify(&options, root, &keys)
|
||||
.unwrap_err()
|
||||
.contains("Could not read"));
|
||||
assert_eq!(std::fs::read(root.join("renamed.zip")).unwrap(), bundle);
|
||||
assert_eq!(std::fs::read_dir(root).unwrap().count(), 1);
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arguments_are_strict_and_default_to_adjacent_signature() {
|
||||
let parse_args =
|
||||
|args: &[&str]| parse(&args.iter().map(|a| a.to_string()).collect::<Vec<_>>());
|
||||
let options = parse_args(&["bundle.zip", "--version=4.3.1", "--json"]).unwrap();
|
||||
assert_eq!(options.signature, PathBuf::from("bundle.zip.sig.json"));
|
||||
assert!(options.json);
|
||||
let options = parse_args(&[
|
||||
"--version",
|
||||
"4.3.1",
|
||||
"--signature",
|
||||
"sig.json",
|
||||
"--",
|
||||
"-bundle.zip",
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(options.bundle, PathBuf::from("-bundle.zip"));
|
||||
assert_eq!(options.signature, PathBuf::from("sig.json"));
|
||||
for args in [
|
||||
vec![],
|
||||
vec!["bundle.zip"],
|
||||
vec!["bundle.zip", "--version"],
|
||||
vec!["bundle.zip", "--version=04.3.1"],
|
||||
vec!["bundle.zip", "--version=4.3.1", "--version=4.2.0"],
|
||||
vec!["bundle.zip", "--version=4.3.1", "extra.zip"],
|
||||
vec!["bundle.zip", "--version=4.3.1", "--skip-signature"],
|
||||
vec!["bundle.zip", "--version=4.3.1", "--signature="],
|
||||
] {
|
||||
assert!(parse_args(&args).is_err(), "{args:?}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
use impeccable_common::Io;
|
||||
|
||||
#[test]
|
||||
fn verify_bundle_help_is_offline() {
|
||||
let (mut io, capture) = Io::captured("", std::env::temp_dir(), Default::default());
|
||||
let code = impeccable_skills::run(&["verify-bundle".into(), "--help".into()], &mut io);
|
||||
assert_eq!(code, 0);
|
||||
assert!(String::from_utf8(capture.stdout.borrow().clone())
|
||||
.unwrap()
|
||||
.contains("--version"));
|
||||
assert!(String::from_utf8(capture.stdout.borrow().clone())
|
||||
.unwrap()
|
||||
.contains("--signature"));
|
||||
assert!(String::from_utf8(capture.stderr.borrow().clone())
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_bundle_requires_an_independent_expected_version() {
|
||||
let (mut io, capture) = Io::captured("", std::env::temp_dir(), Default::default());
|
||||
let code = impeccable_skills::run(&["verify-bundle".into(), "universal.zip".into()], &mut io);
|
||||
assert_eq!(code, 2);
|
||||
assert!(String::from_utf8(capture.stderr.borrow().clone())
|
||||
.unwrap()
|
||||
.contains("--version"));
|
||||
assert!(String::from_utf8(capture.stdout.borrow().clone())
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_override_cannot_bypass_signature_verification_in_json_mode() {
|
||||
let fixture: serde_json::Value = serde_json::from_str(include_str!(
|
||||
"../../../tests/fixtures/bundle-signature.json"
|
||||
))
|
||||
.unwrap();
|
||||
let root = impeccable_skills::util::mkdtemp(
|
||||
&std::env::temp_dir()
|
||||
.join("verify-command-")
|
||||
.to_string_lossy(),
|
||||
)
|
||||
.unwrap();
|
||||
let root = std::path::PathBuf::from(root);
|
||||
std::fs::write(root.join("bundle.zip"), fixture["bundle"].as_str().unwrap()).unwrap();
|
||||
std::fs::write(
|
||||
root.join("bundle.zip.sig.json"),
|
||||
serde_json::to_vec(&fixture["envelope"]).unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
let env = [(
|
||||
"IMPECCABLE_BUNDLE_PATH".into(),
|
||||
root.to_string_lossy().into_owned(),
|
||||
)]
|
||||
.into();
|
||||
let (mut io, capture) = Io::captured("", root.clone(), env);
|
||||
let code = impeccable_skills::run(
|
||||
&[
|
||||
"verify-bundle".into(),
|
||||
"bundle.zip".into(),
|
||||
"--version=4.2.0".into(),
|
||||
"--json".into(),
|
||||
],
|
||||
&mut io,
|
||||
);
|
||||
assert_eq!(code, 1);
|
||||
assert!(capture.stdout.borrow().is_empty());
|
||||
assert!(String::from_utf8(capture.stderr.borrow().clone())
|
||||
.unwrap()
|
||||
.contains("Unknown bundle signing key"));
|
||||
assert_eq!(std::fs::read_dir(&root).unwrap().count(), 2);
|
||||
std::fs::remove_dir_all(root).unwrap();
|
||||
}
|
||||
+4
-41
@@ -13,45 +13,6 @@ HTTPS. Missing signatures, unknown keys, changed metadata, and changed ZIP
|
||||
bytes stop the operation before extraction or writes to installed skills.
|
||||
The temporary download directory is removed on failure.
|
||||
|
||||
## Verify a downloaded release offline
|
||||
|
||||
Use an approved engine that includes `verify-bundle`, and obtain the ZIP and
|
||||
`universal.zip.sig.json` from the same versioned skill release. Then run:
|
||||
|
||||
```sh
|
||||
impeccable verify-bundle /path/to/universal.zip --version 4.3.1
|
||||
```
|
||||
|
||||
The expected version is required and must be the skill version, not the CLI
|
||||
or engine version. The signature defaults to `<zip>.sig.json`; use
|
||||
`--signature /path/to/manifest.json` when stored separately. Both
|
||||
`--version=4.3.1` and `--version 4.3.1` are accepted.
|
||||
|
||||
For an audit record:
|
||||
|
||||
```sh
|
||||
impeccable verify-bundle /path/to/universal.zip --version 4.3.1 --json
|
||||
```
|
||||
|
||||
Successful JSON contains `verified: true`, `version`, `artifact`, `keyId`,
|
||||
`size`, and `sha256`. Only authenticated metadata is printed. Exit codes are
|
||||
0 for successful verification, 1 for verification or file errors, and 2 for
|
||||
invalid arguments. Errors go to stderr, including with `--json`; stdout is
|
||||
empty on failure.
|
||||
|
||||
This command reads local files only. It does not download, extract, install,
|
||||
or enable hooks, and it ignores local bundle overrides. It uses the same
|
||||
compiled-in public keys and verifier as remote installation; there is no
|
||||
custom-key or skip-verification option. Approve the engine and its keyring
|
||||
through your organization's trust process first. Invoking through `npx` may
|
||||
still download the npm package or engine; use an already provisioned native
|
||||
binary for a fully offline workflow.
|
||||
|
||||
Verification authenticates the bytes and their declared release. It does not
|
||||
inspect ZIP contents or prove that skill instructions are safe. Requiring an
|
||||
expected version rejects a different release, but cannot tell you whether the
|
||||
version you chose is the newest. Verify again if the files change before use.
|
||||
|
||||
## Sign a release
|
||||
|
||||
Install the 1Password CLI and enable its desktop app integration. The signing
|
||||
@@ -92,10 +53,12 @@ review the exact released `universal.zip`, then run:
|
||||
node scripts/sign-bundle.mjs 4.2.0 /path/to/universal.zip
|
||||
```
|
||||
|
||||
Check the resulting sidecar against the verifier and compiled public key:
|
||||
Check the resulting sidecar against the Rust verifier and compiled public key:
|
||||
|
||||
```sh
|
||||
impeccable verify-bundle /path/to/universal.zip --version 4.2.0
|
||||
IMPECCABLE_VERIFY_BUNDLE=/path/to/universal.zip \
|
||||
IMPECCABLE_VERIFY_BUNDLE_VERSION=4.2.0 \
|
||||
cargo test -p impeccable-skills verifies_reviewed_release_with_production_keyring -- --ignored
|
||||
```
|
||||
|
||||
This creates only the local sidecar. It neither uploads it nor replaces the
|
||||
|
||||
@@ -1831,23 +1831,3 @@ Conventions: every script's "run directly" guard is `process.argv[1]` ending wit
|
||||
|
||||
#### E2E harness contract (`tests/live-e2e.test.mjs`, `tests/live-e2e/*`)
|
||||
- Fake agent polls `GET /poll?token&timeout=5000` (no lease override → 30 s lease), replies via `POST /poll` with `{token,type:'done',sourceEventType:'generate',id,file}`, `steer_done {message,file}`, `error`, accept/discard completions with `data:{carbonize:true,_acceptResult}`/`{_acceptResult}`, manual apply via `live-poll.mjs --reply <id> done --data <json>`. Variant format: 3 variants (font-weights 300/900/600 for render proof), params `lightness` (range), `face` (steps), `italic` (toggle). Scenarios: core, manual, annotations, exit, missed-done, params, mount-failure, republish, storage-loss (fixtures README). Fixture `runtime` block schema is authoritative for what a reimplementation must satisfy end-to-end.
|
||||
|
||||
### `verify-bundle`: offline release verification
|
||||
|
||||
`impeccable verify-bundle <zip> --version <expected-version>` (also under
|
||||
`impeccable skills`) authenticates local bytes with the remote installer's
|
||||
compiled-in Ed25519 keyring before reporting success. No network, extraction,
|
||||
installation, hooks, or writes occur. `IMPECCABLE_BUNDLE_PATH` does not bypass
|
||||
verification. The expected skill version is required; no `v` or `skill-v`
|
||||
prefix. The default signature path is `<zip>.sig.json`; `--signature <path>`
|
||||
overrides it. Value options also accept `=`, and `--` ends option parsing.
|
||||
Unknown options, duplicate value options, extra paths, and invalid versions
|
||||
exit 2. `--help` / `-h` prints static help and exits 0.
|
||||
|
||||
Success exits 0 and prints the authenticated artifact, version, key ID,
|
||||
SHA-256, and size. `--json` instead prints one object with `verified: true`,
|
||||
`version`, `artifact`, `keyId`, `size`, and `sha256`. File/signature failures
|
||||
exit 1 with `Could not verify skill bundle: ...` on stderr and empty stdout,
|
||||
including in JSON mode. Signature reads are capped at 16 KiB plus one byte to
|
||||
detect oversized input; bundle hashing streams through the shared verifier.
|
||||
See [BUNDLE-SIGNING.md](BUNDLE-SIGNING.md) for trust scope and examples.
|
||||
|
||||
@@ -164,10 +164,3 @@ installed. The binary's `CLI_VERSION` moves from `3.6.0` to `4.0.0` with the
|
||||
CLI 4.0.0 release; it is what the binary prints when run directly.
|
||||
|
||||
- `cli-version`.
|
||||
|
||||
## Recorded 2026-09-18: offline skill bundle verification
|
||||
|
||||
- `cli-help`: adds the `verify-bundle` command to root help.
|
||||
- `skills-verify-bundle-help`, `skills-verify-bundle-namespace-help`, and
|
||||
`skills-verify-bundle-version-required`: new offline command help and
|
||||
required expected-version behavior. Existing installer output is unchanged.
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
* the top-level verb and the legacy `skills` namespace.
|
||||
*/
|
||||
export default [
|
||||
{ id: 'skills-verify-bundle-help', verb: 'verify-bundle', args: ['--help'] },
|
||||
{ id: 'skills-verify-bundle-version-required', verb: 'verify-bundle', args: ['universal.zip'] },
|
||||
{ id: 'skills-verify-bundle-namespace-help', verb: 'skills', args: ['verify-bundle', '--help'] },
|
||||
{ id: 'skills-install-help', verb: 'install', args: ['--help'] },
|
||||
{ id: 'skills-install-help-short', verb: 'install', args: ['-h'] },
|
||||
{ id: 'skills-link-help', verb: 'link', args: ['--help'] },
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"stdout": "Usage: impeccable <command> [options]\n\nCommands:\n detect [file-or-dir-or-url...] Scan for UI anti-patterns and design quality issues\n ignores Manage detector ignore rules, files, and values\n help List all available skills and commands\n install Install impeccable skills into your project or global harness\n link Symlink skills from a local checkout or submodule\n update Update skills to the latest version\n check Check if skill updates are available\n verify-bundle Verify a local skill bundle against the pinned signing keys\n\nOptions:\n --help Show this help message\n --version Show version number\n\nCompatibility:\n impeccable skills <command> Legacy namespace; still supported.\n",
|
||||
"stdout": "Usage: impeccable <command> [options]\n\nCommands:\n detect [file-or-dir-or-url...] Scan for UI anti-patterns and design quality issues\n ignores Manage detector ignore rules, files, and values\n help List all available skills and commands\n install Install impeccable skills into your project or global harness\n link Symlink skills from a local checkout or submodule\n update Update skills to the latest version\n check Check if skill updates are available\n\nOptions:\n --help Show this help message\n --version Show version number\n\nCompatibility:\n impeccable skills <command> Legacy namespace; still supported.\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"stdout": "Usage: impeccable verify-bundle <zip> --version <expected-version> [options]\n\nVerify a local skill bundle's signature and SHA-256 using the pinned signing keys.\nRuns offline without extracting files, installing skills, or enabling hooks.\n\nOptions:\n --version <version> Required expected skill version (for example, 4.3.1)\n --signature <path> Signature manifest (default: <zip>.sig.json)\n --json Print verified metadata as JSON\n -h, --help Show this help message\n\nExit codes: 0 verified, 1 verification or file error, 2 invalid arguments.\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"stdout": "Usage: impeccable verify-bundle <zip> --version <expected-version> [options]\n\nVerify a local skill bundle's signature and SHA-256 using the pinned signing keys.\nRuns offline without extracting files, installing skills, or enabling hooks.\n\nOptions:\n --version <version> Required expected skill version (for example, 4.3.1)\n --signature <path> Signature manifest (default: <zip>.sig.json)\n --json Print verified metadata as JSON\n -h, --help Show this help message\n\nExit codes: 0 verified, 1 verification or file error, 2 invalid arguments.\n",
|
||||
"stderr": "",
|
||||
"exit": 0,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"stdout": "",
|
||||
"stderr": "--version is required; specify the expected skill release\n\nUsage: impeccable verify-bundle <zip> --version <expected-version> [options]\n\nVerify a local skill bundle's signature and SHA-256 using the pinned signing keys.\nRuns offline without extracting files, installing skills, or enabling hooks.\n\nOptions:\n --version <version> Required expected skill version (for example, 4.3.1)\n --signature <path> Signature manifest (default: <zip>.sig.json)\n --json Print verified metadata as JSON\n -h, --help Show this help message\n\nExit codes: 0 verified, 1 verification or file error, 2 invalid arguments.\n",
|
||||
"exit": 2,
|
||||
"signal": null,
|
||||
"files": {}
|
||||
}
|
||||
@@ -26,15 +26,25 @@ describe('README gitignore block', () => {
|
||||
writeFileSync(join(tmp, '.gitignore'), block);
|
||||
execFileSync('git', ['init'], { cwd: tmp });
|
||||
|
||||
const ignored = execFileSync('git', [
|
||||
'check-ignore',
|
||||
for (const rel of [
|
||||
'.impeccable/review/desktop.png',
|
||||
'.impeccable/questions/fb63f8a6.log',
|
||||
], { cwd: tmp, encoding: 'utf-8' });
|
||||
assert.match(ignored, /\.impeccable\/review\/desktop\.png/);
|
||||
assert.match(ignored, /\.impeccable\/questions\/fb63f8a6\.log/);
|
||||
'apps/web/.impeccable/review/desktop.png',
|
||||
'apps/web/.impeccable/questions/fb63f8a6.log',
|
||||
]) {
|
||||
const ignored = execFileSync('git', ['check-ignore', rel], {
|
||||
cwd: tmp,
|
||||
encoding: 'utf-8',
|
||||
});
|
||||
assert.equal(ignored.trim(), rel, `${rel} should be ignored independently`);
|
||||
}
|
||||
|
||||
for (const rel of ['.impeccable/config.json', '.impeccable/critique/report.md']) {
|
||||
for (const rel of [
|
||||
'.impeccable/config.json',
|
||||
'.impeccable/critique/report.md',
|
||||
'apps/web/.impeccable/config.json',
|
||||
'apps/web/.impeccable/critique/report.md',
|
||||
]) {
|
||||
const result = spawnSync('git', ['check-ignore', rel], { cwd: tmp });
|
||||
assert.notEqual(result.status, 0, `${rel} should not be ignored`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user