Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,14 @@
},
"metadata": {
"description": "Verified Solution Cards retrieved from an agent-authored conceptual problem statement.",
"version": "0.4.1"
"version": "0.4.2"
},
"plugins": [
{
"name": "blaze",
"source": "./plugins/claude-code",
"description": "Retrieves a verified Solution Card after an agent prepares a privacy-bounded conceptual query.",
"version": "0.4.1",
"version": "0.4.2",
"keywords": [
"memory",
"retrieval",
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,3 +73,8 @@ bytes. Published versions cannot silently change their bytes. The hosted build v
its pinned release; local edited trees produce a draft accepted only on loopback origins.
Digests from the same HTTPS origin detect corruption and mixed downloads, not a
compromised publisher. Manager-owned installations retain their manager's trust boundary.

Activation journals from 0.4.2 record the service origin before swapping files.
Earlier update journals can recover through their prior ownership record. An older
first-install journal with neither an origin nor a prior record is preserved for
operator recovery; the current caller must not assign it a new origin.
2 changes: 1 addition & 1 deletion install.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Install Blaze 0.4.1
# Install Blaze 0.4.2

Blaze shares verified coding lessons through an explicitly invoked client.
Installing it does not authorize uploading prompts, source or transcripts.
Expand Down
2 changes: 1 addition & 1 deletion plugins/claude-code/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
"name": "blaze",
"displayName": "Blaze",
"version": "0.4.1",
"version": "0.4.2",
"description": "Retrieves a verified Solution Card after an agent prepares a privacy-bounded conceptual query.",
"author": {
"name": "Blaze"
Expand Down
19 changes: 11 additions & 8 deletions plugins/claude-code/blaze-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const TOKEN = /^blz_[A-Za-z0-9_-]{43}$/;
const CARD_ID = /^[a-z0-9][a-z0-9-]{2,62}$/;
const DEFAULT_ORIGIN = "https://blaze.pascal.app";
export const CLIENT_VERSION = "0.4.1";
export const CLIENT_VERSION = "0.4.2";
export const CLIENT_CONTRACT = 1;
export const CLIENT_TOOLS = ["claude", "codex", "opencode", "cursor", "openclaw", "agent"];
const RELEASE_FILES = ["SKILL.md", "blaze-client.mjs"];
Expand Down Expand Up @@ -645,13 +645,14 @@ async function locked(path, work) {
export function createLifecycle({tool, home = homedir(), origin, helperPath = fileURLToPath(import.meta.url), fetchImpl = fetch}) {
const paths = toolPaths(tool, home);
const invokedRoot = resolve(dirname(helperPath));
// Hosts can discover another host's global copy. Only an already recorded
// direct bundle (or its interrupted transaction) can establish ownership.
// Hosts can discover another host's global copy. Only a completed direct
// ownership record binds that copy to an origin. The original host must
// recover a first install interrupted before that record was committed.
// Credentials and receipts still belong to the invoking tool's state directory.
if (invokedRoot !== resolve(paths.root) && CLIENT_TOOLS.some(name => resolve(toolPaths(name,home).root) === invokedRoot)) {
const recordedState = join(home,".config/blaze/bundles",sha256(invokedRoot).slice(0,32));
homePath(home,invokedRoot);homePath(home,recordedState);
if (pathStat(join(recordedState,"installation.json")) || pathStat(join(recordedState,"transaction.json"))) paths.root = invokedRoot;
if (pathStat(join(recordedState,"installation.json"))) paths.root = invokedRoot;
}
const base = trustedOrigin(origin ?? readToolCredential(tool, home, false).origin);
const bundleState = join(home, ".config/blaze/bundles", sha256(resolve(paths.root)).slice(0,32));
Expand Down Expand Up @@ -762,10 +763,12 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
}
function recover() {
const journal = loadRequiredIfPresent(journalPath); if (!journal) return;
exactKeys(journal,new Set(["version","id","release","prior"]),"Activation journal");
if (journal.version!==1 || !UUID.test(journal.id ?? "")) throw new Error("Invalid activation journal");
exactKeys(journal,new Set(journal.version===2 ? ["version","id","origin","release","prior"] : ["version","id","release","prior"]),"Activation journal");
if (![1,2].includes(journal.version) || !UUID.test(journal.id ?? "")) throw new Error("Invalid activation journal");
const next = validateRelease(journal.release,base);
const prior = validateMetadata(journal.prior);
const recordedOrigin = journal.version===2 ? trustedOrigin(journal.origin) : prior?.origin;
if (recordedOrigin!==base) throw new Error("Interrupted installation has no matching recorded service origin; preserve its state for recovery");
const stage = join(bundleState,"staging",journal.id), backup = join(bundleState,"backups",journal.id);
homePath(home,stage);homePath(home,backup);homePath(home,paths.root);
if (existsSync(paths.root) && verifyBundle(paths.root,next,false,true)) {
Expand Down Expand Up @@ -842,7 +845,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
if (syntax.status!==0) throw new Error("Release client failed syntax validation");
const credential = await setup();
migrateReceipts();
save(journalPath,{version:1,id,release:next,prior});
save(journalPath,{version:2,id,origin:base,release:next,prior});
if (existsSync(paths.root)) renameSync(paths.root,backup);
mkdirSync(dirname(paths.root),{recursive:true,mode:0o700});
renameSync(stage,paths.root);
Expand Down Expand Up @@ -885,7 +888,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
if (!verifyBundle(paths.root,meta.release) || !verifyBundle(backup,previous)) throw new Error("Rollback bundle was modified");
const id = randomUUID(), stage = join(bundleState,"staging",id);
ensurePrivateDir(dirname(stage));renameSync(backup,stage);
save(journalPath,{version:1,id,release:previous,prior:{...meta,pin:previous.version}});
save(journalPath,{version:2,id,origin:base,release:previous,prior:{...meta,pin:previous.version}});
const currentBackup = join(bundleState,"backups",id);renameSync(paths.root,currentBackup);renameSync(stage,paths.root);recover();
return {version:previous.version,activation:"rolled_back",reload_required:true,pin:previous.version};
});
Expand Down
2 changes: 1 addition & 1 deletion plugins/claude-code/skills/blaze/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: blaze
description: Reuse and improve verified coding lessons across agents. Use for a nontrivial debugging or implementation problem where an earlier solution could help, when a Blaze offer or receipt appears, or when the user asks to install, update, contribute to, or link Blaze. Check applicability, verify locally, and close the lookup with an honest outcome and contribution disposition.
compatibility: Requires Node.js 20 or later and explicit HTTPS access to the configured Blaze service. Local reminder hooks need no network access. No model provider credentials are needed.
metadata:
version: "0.4.1"
version: "0.4.2"
---

# Blaze
Expand Down
19 changes: 11 additions & 8 deletions plugins/claude-code/skills/blaze/blaze-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const TOKEN = /^blz_[A-Za-z0-9_-]{43}$/;
const CARD_ID = /^[a-z0-9][a-z0-9-]{2,62}$/;
const DEFAULT_ORIGIN = "https://blaze.pascal.app";
export const CLIENT_VERSION = "0.4.1";
export const CLIENT_VERSION = "0.4.2";
export const CLIENT_CONTRACT = 1;
export const CLIENT_TOOLS = ["claude", "codex", "opencode", "cursor", "openclaw", "agent"];
const RELEASE_FILES = ["SKILL.md", "blaze-client.mjs"];
Expand Down Expand Up @@ -645,13 +645,14 @@ async function locked(path, work) {
export function createLifecycle({tool, home = homedir(), origin, helperPath = fileURLToPath(import.meta.url), fetchImpl = fetch}) {
const paths = toolPaths(tool, home);
const invokedRoot = resolve(dirname(helperPath));
// Hosts can discover another host's global copy. Only an already recorded
// direct bundle (or its interrupted transaction) can establish ownership.
// Hosts can discover another host's global copy. Only a completed direct
// ownership record binds that copy to an origin. The original host must
// recover a first install interrupted before that record was committed.
// Credentials and receipts still belong to the invoking tool's state directory.
if (invokedRoot !== resolve(paths.root) && CLIENT_TOOLS.some(name => resolve(toolPaths(name,home).root) === invokedRoot)) {
const recordedState = join(home,".config/blaze/bundles",sha256(invokedRoot).slice(0,32));
homePath(home,invokedRoot);homePath(home,recordedState);
if (pathStat(join(recordedState,"installation.json")) || pathStat(join(recordedState,"transaction.json"))) paths.root = invokedRoot;
if (pathStat(join(recordedState,"installation.json"))) paths.root = invokedRoot;
}
const base = trustedOrigin(origin ?? readToolCredential(tool, home, false).origin);
const bundleState = join(home, ".config/blaze/bundles", sha256(resolve(paths.root)).slice(0,32));
Expand Down Expand Up @@ -762,10 +763,12 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
}
function recover() {
const journal = loadRequiredIfPresent(journalPath); if (!journal) return;
exactKeys(journal,new Set(["version","id","release","prior"]),"Activation journal");
if (journal.version!==1 || !UUID.test(journal.id ?? "")) throw new Error("Invalid activation journal");
exactKeys(journal,new Set(journal.version===2 ? ["version","id","origin","release","prior"] : ["version","id","release","prior"]),"Activation journal");
if (![1,2].includes(journal.version) || !UUID.test(journal.id ?? "")) throw new Error("Invalid activation journal");
const next = validateRelease(journal.release,base);
const prior = validateMetadata(journal.prior);
const recordedOrigin = journal.version===2 ? trustedOrigin(journal.origin) : prior?.origin;
if (recordedOrigin!==base) throw new Error("Interrupted installation has no matching recorded service origin; preserve its state for recovery");
const stage = join(bundleState,"staging",journal.id), backup = join(bundleState,"backups",journal.id);
homePath(home,stage);homePath(home,backup);homePath(home,paths.root);
if (existsSync(paths.root) && verifyBundle(paths.root,next,false,true)) {
Expand Down Expand Up @@ -842,7 +845,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
if (syntax.status!==0) throw new Error("Release client failed syntax validation");
const credential = await setup();
migrateReceipts();
save(journalPath,{version:1,id,release:next,prior});
save(journalPath,{version:2,id,origin:base,release:next,prior});
if (existsSync(paths.root)) renameSync(paths.root,backup);
mkdirSync(dirname(paths.root),{recursive:true,mode:0o700});
renameSync(stage,paths.root);
Expand Down Expand Up @@ -885,7 +888,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
if (!verifyBundle(paths.root,meta.release) || !verifyBundle(backup,previous)) throw new Error("Rollback bundle was modified");
const id = randomUUID(), stage = join(bundleState,"staging",id);
ensurePrivateDir(dirname(stage));renameSync(backup,stage);
save(journalPath,{version:1,id,release:previous,prior:{...meta,pin:previous.version}});
save(journalPath,{version:2,id,origin:base,release:previous,prior:{...meta,pin:previous.version}});
const currentBackup = join(bundleState,"backups",id);renameSync(paths.root,currentBackup);renameSync(stage,paths.root);recover();
return {version:previous.version,activation:"rolled_back",reload_required:true,pin:previous.version};
});
Expand Down
19 changes: 11 additions & 8 deletions plugins/client/blaze-client.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
const TOKEN = /^blz_[A-Za-z0-9_-]{43}$/;
const CARD_ID = /^[a-z0-9][a-z0-9-]{2,62}$/;
const DEFAULT_ORIGIN = "https://blaze.pascal.app";
export const CLIENT_VERSION = "0.4.1";
export const CLIENT_VERSION = "0.4.2";
export const CLIENT_CONTRACT = 1;
export const CLIENT_TOOLS = ["claude", "codex", "opencode", "cursor", "openclaw", "agent"];
const RELEASE_FILES = ["SKILL.md", "blaze-client.mjs"];
Expand Down Expand Up @@ -645,13 +645,14 @@ async function locked(path, work) {
export function createLifecycle({tool, home = homedir(), origin, helperPath = fileURLToPath(import.meta.url), fetchImpl = fetch}) {
const paths = toolPaths(tool, home);
const invokedRoot = resolve(dirname(helperPath));
// Hosts can discover another host's global copy. Only an already recorded
// direct bundle (or its interrupted transaction) can establish ownership.
// Hosts can discover another host's global copy. Only a completed direct
// ownership record binds that copy to an origin. The original host must
// recover a first install interrupted before that record was committed.
// Credentials and receipts still belong to the invoking tool's state directory.
if (invokedRoot !== resolve(paths.root) && CLIENT_TOOLS.some(name => resolve(toolPaths(name,home).root) === invokedRoot)) {
const recordedState = join(home,".config/blaze/bundles",sha256(invokedRoot).slice(0,32));
homePath(home,invokedRoot);homePath(home,recordedState);
if (pathStat(join(recordedState,"installation.json")) || pathStat(join(recordedState,"transaction.json"))) paths.root = invokedRoot;
if (pathStat(join(recordedState,"installation.json"))) paths.root = invokedRoot;
}
const base = trustedOrigin(origin ?? readToolCredential(tool, home, false).origin);
const bundleState = join(home, ".config/blaze/bundles", sha256(resolve(paths.root)).slice(0,32));
Expand Down Expand Up @@ -762,10 +763,12 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
}
function recover() {
const journal = loadRequiredIfPresent(journalPath); if (!journal) return;
exactKeys(journal,new Set(["version","id","release","prior"]),"Activation journal");
if (journal.version!==1 || !UUID.test(journal.id ?? "")) throw new Error("Invalid activation journal");
exactKeys(journal,new Set(journal.version===2 ? ["version","id","origin","release","prior"] : ["version","id","release","prior"]),"Activation journal");
if (![1,2].includes(journal.version) || !UUID.test(journal.id ?? "")) throw new Error("Invalid activation journal");
const next = validateRelease(journal.release,base);
const prior = validateMetadata(journal.prior);
const recordedOrigin = journal.version===2 ? trustedOrigin(journal.origin) : prior?.origin;
if (recordedOrigin!==base) throw new Error("Interrupted installation has no matching recorded service origin; preserve its state for recovery");
const stage = join(bundleState,"staging",journal.id), backup = join(bundleState,"backups",journal.id);
homePath(home,stage);homePath(home,backup);homePath(home,paths.root);
if (existsSync(paths.root) && verifyBundle(paths.root,next,false,true)) {
Expand Down Expand Up @@ -842,7 +845,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
if (syntax.status!==0) throw new Error("Release client failed syntax validation");
const credential = await setup();
migrateReceipts();
save(journalPath,{version:1,id,release:next,prior});
save(journalPath,{version:2,id,origin:base,release:next,prior});
if (existsSync(paths.root)) renameSync(paths.root,backup);
mkdirSync(dirname(paths.root),{recursive:true,mode:0o700});
renameSync(stage,paths.root);
Expand Down Expand Up @@ -885,7 +888,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
if (!verifyBundle(paths.root,meta.release) || !verifyBundle(backup,previous)) throw new Error("Rollback bundle was modified");
const id = randomUUID(), stage = join(bundleState,"staging",id);
ensurePrivateDir(dirname(stage));renameSync(backup,stage);
save(journalPath,{version:1,id,release:previous,prior:{...meta,pin:previous.version}});
save(journalPath,{version:2,id,origin:base,release:previous,prior:{...meta,pin:previous.version}});
const currentBackup = join(bundleState,"backups",id);renameSync(paths.root,currentBackup);renameSync(stage,paths.root);recover();
return {version:previous.version,activation:"rolled_back",reload_required:true,pin:previous.version};
});
Expand Down
30 changes: 30 additions & 0 deletions plugins/client/lifecycle.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,36 @@ test("cross-host discovery preserves origin and symlink checks on recorded owner
assert.throws(()=>createLifecycle({...options,tool:"opencode"}).status(),/symbolic links/);assert.equal(requests.length,before);
});

test("an incomplete first install cannot establish cross-host ownership or replace its origin",async t=>{
const {lifecycle,options,state,control}=await fixture(t);await lifecycle.install();
rmSync(join(state,"installation.json"));
put(join(state,"transaction.json"),{version:1,id:randomUUID(),release:control.release.manifest,prior:null});
let calls=0;
const discovered=createLifecycle({...options,tool:"opencode",origin:"https://example.invalid",fetchImpl:async()=>{calls++;return new Response(null,{status:503})}});
assert.equal((await discovered.update()).installation,"managed_or_unrecorded");
assert.equal(calls,0);assert.equal(existsSync(join(state,"installation.json")),false);
});

test("first-install recovery binds the recorded origin even for hosts with the same default directory",async t=>{
const {lifecycle,options,state,control,paths}=await fixture(t);await lifecycle.install();
const credential=readFileSync(paths.token,"utf8");rmSync(join(state,"installation.json"));
put(join(state,"transaction.json"),{version:2,id:randomUUID(),origin:options.origin,release:control.release.manifest,prior:null});
let calls=0;
const other=createLifecycle({...options,tool:"cursor",origin:"https://example.invalid",fetchImpl:async()=>{calls++;return new Response(null,{status:503})}});
await assert.rejects(other.update(),/recorded service origin/);assert.equal(calls,0);assert.equal(existsSync(join(state,"installation.json")),false);
assert.equal((await lifecycle.update()).credential,"reused");
assert.equal(get(join(state,"installation.json")).origin,options.origin);assert.equal(readFileSync(paths.token,"utf8"),credential);
});

test("legacy first-install journals without an origin are preserved instead of assigning the caller's origin",async t=>{
const {lifecycle,state,control,requests}=await fixture(t);await lifecycle.install();
rmSync(join(state,"installation.json"));
put(join(state,"transaction.json"),{version:1,id:randomUUID(),release:control.release.manifest,prior:null});
const before=requests.length;
await assert.rejects(lifecycle.update(),/recorded service origin/);assert.equal(requests.length,before);
assert.equal(existsSync(join(state,"installation.json")),false);assert.equal(existsSync(join(state,"transaction.json")),true);
});

test("intentional requests cache fixed version hints; retired contracts outrank pins",async t=>{
const {lifecycle,paths,options}=await fixture(t);await lifecycle.install();await lifecycle.pin("0.4.0");
let count=0;const client=createClient({origin:options.origin,token:get(paths.token).token,stateDir:join(paths.state,"receipts"),freshnessPath:join(paths.state,"freshness.json"),tool:"codex",
Expand Down
6 changes: 3 additions & 3 deletions release.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": "0.4.1",
"created_at": "2026-09-07T08:58:37.955Z",
"updated_at": "2026-09-07T08:58:37.955Z",
"version": "0.4.2",
"created_at": "2026-09-07T09:12:23.208Z",
"updated_at": "2026-09-07T09:12:23.208Z",
"client_contract": 1,
"minimum_client_contract": 0
}
2 changes: 1 addition & 1 deletion skill.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: blaze
description: Reuse and improve verified coding lessons across agents. Use for a nontrivial debugging or implementation problem where an earlier solution could help, when a Blaze offer or receipt appears, or when the user asks to install, update, contribute to, or link Blaze. Check applicability, verify locally, and close the lookup with an honest outcome and contribution disposition.
compatibility: Requires Node.js 20 or later and explicit HTTPS access to the configured Blaze service. Local reminder hooks need no network access. No model provider credentials are needed.
metadata:
version: "0.4.1"
version: "0.4.2"
---

# Blaze
Expand Down
Loading