Skip to content

Commit 8794dca

Browse files
authored
Merge pull request #11 from pascalorg/fix/lifecycle-upgrade-headers
Send lifecycle protocol headers and verify bootstrap recovery
2 parents 4bcf992 + 638399c commit 8794dca

10 files changed

Lines changed: 93 additions & 27 deletions

File tree

.claude-plugin/marketplace.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,14 @@
77
},
88
"metadata": {
99
"description": "Verified Solution Cards retrieved from an agent-authored conceptual problem statement.",
10-
"version": "0.5.1"
10+
"version": "0.5.2"
1111
},
1212
"plugins": [
1313
{
1414
"name": "blaze",
1515
"source": "./plugins/claude-code",
1616
"description": "Retrieves a verified Solution Card after an agent prepares a privacy-bounded conceptual query.",
17-
"version": "0.5.1",
17+
"version": "0.5.2",
1818
"keywords": [
1919
"memory",
2020
"retrieval",

install.md

Lines changed: 28 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Install Blaze 0.5.1
1+
# Install Blaze 0.5.2
22

33
Blaze shares verified coding lessons through an explicitly invoked client.
44
Installing it does not authorize uploading prompts, source or transcripts.
@@ -31,17 +31,30 @@ source commit, and the sizes and SHA-256 hashes of exactly two artifacts:
3131
`SKILL.md` and `blaze-client.mjs`. Review the corresponding public source release
3232
when deciding whether to trust it. A hash verifies bytes, not the publisher.
3333

34-
Download `{BLAZE_URL}/blaze-client.mjs` to a temporary private file over HTTPS,
35-
with redirects disabled. Inspect it before running it. Do not pipe a remote
36-
response into a shell. An example download is:
34+
Copy the version and the helper's SHA-256 from that reviewed manifest into the
35+
variables below. Download its immutable artifact to a temporary private file over
36+
HTTPS with redirects disabled, then verify its hash before inspecting or running
37+
it. Do not pipe a remote response into a shell.
3738

3839
```bash
3940
umask 077
4041
blaze_bootstrap_dir=$(mktemp -d)
41-
curl --fail --silent --show-error --proto '=https' --max-redirs 0 --max-time 15 --max-filesize 524288 '{BLAZE_URL}/blaze-client.mjs' --output "$blaze_bootstrap_dir/blaze-client.mjs"
42+
blaze_release_version='<reviewed version>'
43+
blaze_client_sha256='<reviewed helper SHA-256>'
44+
curl --fail --silent --show-error --proto '=https' --max-redirs 0 --max-time 15 --max-filesize 524288 "{BLAZE_URL}/releases/$blaze_release_version/$blaze_client_sha256/blaze-client.mjs" --output "$blaze_bootstrap_dir/blaze-client.mjs"
45+
node --input-type=module - "$blaze_bootstrap_dir/blaze-client.mjs" "$blaze_client_sha256" <<'NODE'
46+
import { readFileSync } from 'node:fs';
47+
import { createHash } from 'node:crypto';
48+
const [path, expected] = process.argv.slice(2);
49+
if (!/^[0-9a-f]{64}$/.test(expected ?? '') ||
50+
createHash('sha256').update(readFileSync(path)).digest('hex') !== expected) {
51+
throw new Error('Bootstrap hash mismatch; do not execute this download.');
52+
}
53+
NODE
4254
```
4355

44-
After reviewing the download, invoke it, replacing `codex` with the current host:
56+
Continue only after the hash check succeeds and you have reviewed the download.
57+
Invoke it, replacing `codex` with the current host:
4558

4659
```bash
4760
node "$blaze_bootstrap_dir/blaze-client.mjs" install --tool codex --origin '{BLAZE_URL}'
@@ -98,6 +111,15 @@ API calls also receive small version hints. No prompt hook fetches metadata or
98111
updates files. Updating requires authorization and respects pins. The direct
99112
updater refuses to modify manager or marketplace installations.
100113

114+
The direct 0.5.0 and 0.5.1 updaters can receive HTTP 426 while checking the saved
115+
installation, before activating a newer release.
116+
For that recorded direct installation, follow the reviewed download and hash checks
117+
under [Direct installation](#direct-installation), then run the freshly downloaded
118+
helper with `install --tool <host> --origin '{BLAZE_URL}'`. It preserves the existing
119+
credential, receipts, ownership record and pin; an active pin still blocks replacement.
120+
Use the installed helper to unpin only when that change is separately authorized.
121+
Manager and marketplace installations must still update through their manager.
122+
101123
Rollback restores the immediately preceding checked direct release and pins it.
102124
The first upgrade from a legacy bundle cannot automatically roll back to the
103125
older state layout; its private backup remains available for deliberate recovery.

plugins/claude-code/.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"$schema": "https://anthropic.com/claude-code/plugin.schema.json",
33
"name": "blaze",
44
"displayName": "Blaze",
5-
"version": "0.5.1",
5+
"version": "0.5.2",
66
"description": "Retrieves a verified Solution Card after an agent prepares a privacy-bounded conceptual query.",
77
"author": {
88
"name": "Blaze"

plugins/claude-code/blaze-client.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const TOKEN = /^blz_[A-Za-z0-9_-]{43}$/;
4343
const CARD_ID = idPattern(ID_PREFIXES.card);
4444
const AUTHORED_SLUG = /^[a-z0-9][a-z0-9-]{2,62}$/;
4545
const DEFAULT_ORIGIN = "https://blaze.pascal.app";
46-
export const CLIENT_VERSION = "0.5.1";
46+
export const CLIENT_VERSION = "0.5.2";
4747
export const CLIENT_CONTRACT = 2;
4848
export const API_VERSION = "2026-09-07";
4949
export const CLIENT_TOOLS = ["claude", "codex", "opencode", "cursor", "openclaw", "agent"];
@@ -786,6 +786,8 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
786786
return Buffer.concat(chunks,size);
787787
}
788788
const parseJSON = (value) => { try {return JSON.parse(value.toString("utf8"));} catch {throw new Error("Blaze returned invalid JSON");} };
789+
const serviceHeaders = headers => ({...headers,
790+
"Blaze-Version":API_VERSION,"Blaze-Client-Version":CLIENT_VERSION,"Blaze-Client-Contract":String(CLIENT_CONTRACT)});
789791
async function release() { return validateRelease(parseJSON(await bytes("/api/skill-release",16*1024)),base); }
790792
function ownedInvocation(meta) { return meta && resolve(dirname(helperPath)) === resolve(paths.root); }
791793
function status() {
@@ -824,7 +826,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
824826
const existing = readToolCredential(tool,home);
825827
if (existing.token) {
826828
if (existing.origin!==base) throw new Error("Keep the existing credential with its original service");
827-
const result = parseJSON(await bytes("/api/stats",32*1024,{headers:{authorization:`Bearer ${existing.token}`}}));
829+
const result = parseJSON(await bytes("/api/stats",32*1024,{headers:serviceHeaders({authorization:`Bearer ${existing.token}`})}));
828830
if (result?.cards!==null && (!Number.isSafeInteger(result?.cards)||result.cards<0)) throw new Error("Invalid service status");
829831
return {credential:"reused"};
830832
}
@@ -833,7 +835,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
833835
exactKeys(pending,new Set(["version","origin","token"]),"Pending registration");
834836
if (pending.version!==1 || pending.origin!==base || !TOKEN.test(pending.token ?? "")) throw new Error("Pending registration belongs to another service or is invalid");
835837
save(pendingPath,pending);
836-
const data = parseJSON(await bytes("/api/installations",16*1024,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${pending.token}`,"Idempotency-Key":sha256(pending.token)},body:JSON.stringify({tool})}));
838+
const data = parseJSON(await bytes("/api/installations",16*1024,{method:"POST",headers:serviceHeaders({"content-type":"application/json",authorization:`Bearer ${pending.token}`,"Idempotency-Key":sha256(pending.token)}),body:JSON.stringify({tool})}));
837839
const installId = responseId(data, "install", "installation");
838840
if (!TOKEN.test(data?.token ?? "") || !installId || data.bootstrap_contract!==2 || data.token!==pending.token) {
839841
throw new Error("This service does not support retryable registration; keep the saved pending credential");

plugins/claude-code/skills/blaze/SKILL.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name: blaze
33
description: Quietly 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 inspect, install, update, contribute to, or link Blaze. Check applicability, verify locally, and close the lookup with an honest outcome and contribution disposition without adding routine user-visible narration.
44
compatibility: Requires Node.js 20 or later and explicit HTTPS access to the configured Blaze service. Local model-context hooks need no network access. No model provider credentials are needed.
55
metadata:
6-
version: "0.5.1"
6+
version: "0.5.2"
77
---
88

99
# Blaze

plugins/claude-code/skills/blaze/blaze-client.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const TOKEN = /^blz_[A-Za-z0-9_-]{43}$/;
4343
const CARD_ID = idPattern(ID_PREFIXES.card);
4444
const AUTHORED_SLUG = /^[a-z0-9][a-z0-9-]{2,62}$/;
4545
const DEFAULT_ORIGIN = "https://blaze.pascal.app";
46-
export const CLIENT_VERSION = "0.5.1";
46+
export const CLIENT_VERSION = "0.5.2";
4747
export const CLIENT_CONTRACT = 2;
4848
export const API_VERSION = "2026-09-07";
4949
export const CLIENT_TOOLS = ["claude", "codex", "opencode", "cursor", "openclaw", "agent"];
@@ -786,6 +786,8 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
786786
return Buffer.concat(chunks,size);
787787
}
788788
const parseJSON = (value) => { try {return JSON.parse(value.toString("utf8"));} catch {throw new Error("Blaze returned invalid JSON");} };
789+
const serviceHeaders = headers => ({...headers,
790+
"Blaze-Version":API_VERSION,"Blaze-Client-Version":CLIENT_VERSION,"Blaze-Client-Contract":String(CLIENT_CONTRACT)});
789791
async function release() { return validateRelease(parseJSON(await bytes("/api/skill-release",16*1024)),base); }
790792
function ownedInvocation(meta) { return meta && resolve(dirname(helperPath)) === resolve(paths.root); }
791793
function status() {
@@ -824,7 +826,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
824826
const existing = readToolCredential(tool,home);
825827
if (existing.token) {
826828
if (existing.origin!==base) throw new Error("Keep the existing credential with its original service");
827-
const result = parseJSON(await bytes("/api/stats",32*1024,{headers:{authorization:`Bearer ${existing.token}`}}));
829+
const result = parseJSON(await bytes("/api/stats",32*1024,{headers:serviceHeaders({authorization:`Bearer ${existing.token}`})}));
828830
if (result?.cards!==null && (!Number.isSafeInteger(result?.cards)||result.cards<0)) throw new Error("Invalid service status");
829831
return {credential:"reused"};
830832
}
@@ -833,7 +835,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
833835
exactKeys(pending,new Set(["version","origin","token"]),"Pending registration");
834836
if (pending.version!==1 || pending.origin!==base || !TOKEN.test(pending.token ?? "")) throw new Error("Pending registration belongs to another service or is invalid");
835837
save(pendingPath,pending);
836-
const data = parseJSON(await bytes("/api/installations",16*1024,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${pending.token}`,"Idempotency-Key":sha256(pending.token)},body:JSON.stringify({tool})}));
838+
const data = parseJSON(await bytes("/api/installations",16*1024,{method:"POST",headers:serviceHeaders({"content-type":"application/json",authorization:`Bearer ${pending.token}`,"Idempotency-Key":sha256(pending.token)}),body:JSON.stringify({tool})}));
837839
const installId = responseId(data, "install", "installation");
838840
if (!TOKEN.test(data?.token ?? "") || !installId || data.bootstrap_contract!==2 || data.token!==pending.token) {
839841
throw new Error("This service does not support retryable registration; keep the saved pending credential");

plugins/client/blaze-client.mjs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ const TOKEN = /^blz_[A-Za-z0-9_-]{43}$/;
4343
const CARD_ID = idPattern(ID_PREFIXES.card);
4444
const AUTHORED_SLUG = /^[a-z0-9][a-z0-9-]{2,62}$/;
4545
const DEFAULT_ORIGIN = "https://blaze.pascal.app";
46-
export const CLIENT_VERSION = "0.5.1";
46+
export const CLIENT_VERSION = "0.5.2";
4747
export const CLIENT_CONTRACT = 2;
4848
export const API_VERSION = "2026-09-07";
4949
export const CLIENT_TOOLS = ["claude", "codex", "opencode", "cursor", "openclaw", "agent"];
@@ -786,6 +786,8 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
786786
return Buffer.concat(chunks,size);
787787
}
788788
const parseJSON = (value) => { try {return JSON.parse(value.toString("utf8"));} catch {throw new Error("Blaze returned invalid JSON");} };
789+
const serviceHeaders = headers => ({...headers,
790+
"Blaze-Version":API_VERSION,"Blaze-Client-Version":CLIENT_VERSION,"Blaze-Client-Contract":String(CLIENT_CONTRACT)});
789791
async function release() { return validateRelease(parseJSON(await bytes("/api/skill-release",16*1024)),base); }
790792
function ownedInvocation(meta) { return meta && resolve(dirname(helperPath)) === resolve(paths.root); }
791793
function status() {
@@ -824,7 +826,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
824826
const existing = readToolCredential(tool,home);
825827
if (existing.token) {
826828
if (existing.origin!==base) throw new Error("Keep the existing credential with its original service");
827-
const result = parseJSON(await bytes("/api/stats",32*1024,{headers:{authorization:`Bearer ${existing.token}`}}));
829+
const result = parseJSON(await bytes("/api/stats",32*1024,{headers:serviceHeaders({authorization:`Bearer ${existing.token}`})}));
828830
if (result?.cards!==null && (!Number.isSafeInteger(result?.cards)||result.cards<0)) throw new Error("Invalid service status");
829831
return {credential:"reused"};
830832
}
@@ -833,7 +835,7 @@ export function createLifecycle({tool, home = homedir(), origin, helperPath = fi
833835
exactKeys(pending,new Set(["version","origin","token"]),"Pending registration");
834836
if (pending.version!==1 || pending.origin!==base || !TOKEN.test(pending.token ?? "")) throw new Error("Pending registration belongs to another service or is invalid");
835837
save(pendingPath,pending);
836-
const data = parseJSON(await bytes("/api/installations",16*1024,{method:"POST",headers:{"content-type":"application/json",authorization:`Bearer ${pending.token}`,"Idempotency-Key":sha256(pending.token)},body:JSON.stringify({tool})}));
838+
const data = parseJSON(await bytes("/api/installations",16*1024,{method:"POST",headers:serviceHeaders({"content-type":"application/json",authorization:`Bearer ${pending.token}`,"Idempotency-Key":sha256(pending.token)}),body:JSON.stringify({tool})}));
837839
const installId = responseId(data, "install", "installation");
838840
if (!TOKEN.test(data?.token ?? "") || !installId || data.bootstrap_contract!==2 || data.token!==pending.token) {
839841
throw new Error("This service does not support retryable registration; keep the saved pending credential");

0 commit comments

Comments
 (0)