惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

L
LangChain Blog
有赞技术团队
有赞技术团队
博客园_首页
IT之家
IT之家
爱范儿
爱范儿
量子位
小众软件
小众软件
Jina AI
Jina AI
WordPress大学
WordPress大学
酷 壳 – CoolShell
酷 壳 – CoolShell
博客园 - 聂微东
The Cloudflare Blog
博客园 - 司徒正美
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
大猫的无限游戏
大猫的无限游戏
月光博客
月光博客
雷峰网
雷峰网
V
Visual Studio Blog
博客园 - Franky
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
美团技术团队
Last Week in AI
Last Week in AI
S
SegmentFault 最新的问题

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
ci: verify docker release attestations · openclaw/opencla...
steipete · 2026-04-27 · via Recent Commits to openclaw:main

@@ -0,0 +1,202 @@

1+

#!/usr/bin/env node

2+3+

import { execFileSync } from "node:child_process";

4+

import process from "node:process";

5+6+

const ATTESTATION_REFERENCE_TYPE = "attestation-manifest";

7+

const REQUIRED_PREDICATES = ["https://spdx.dev/Document", "https://slsa.dev/provenance/v1"];

8+9+

export function imageRefForDigest(imageRef, digest) {

10+

const atIndex = imageRef.indexOf("@");

11+

if (atIndex >= 0) {

12+

return `${imageRef.slice(0, atIndex)}@${digest}`;

13+

}

14+

const lastSlash = imageRef.lastIndexOf("/");

15+

const tagIndex = imageRef.indexOf(":", lastSlash + 1);

16+

const base = tagIndex >= 0 ? imageRef.slice(0, tagIndex) : imageRef;

17+

return `${base}@${digest}`;

18+

}

19+20+

export function parsePlatform(value) {

21+

const [os, architecture, variant] = value.split("/");

22+

if (!os || !architecture || value.split("/").length > 3) {

23+

throw new Error(`Invalid platform ${JSON.stringify(value)}. Expected os/architecture.`);

24+

}

25+

return { architecture, os, variant };

26+

}

27+28+

function formatPlatform(platform) {

29+

return platform.variant

30+

? `${platform.os}/${platform.architecture}/${platform.variant}`

31+

: `${platform.os}/${platform.architecture}`;

32+

}

33+34+

function platformMatches(actual, expected) {

35+

return (

36+

actual?.os === expected.os &&

37+

actual?.architecture === expected.architecture &&

38+

(expected.variant ? actual?.variant === expected.variant : true)

39+

);

40+

}

41+42+

function parseJson(raw, label) {

43+

try {

44+

return JSON.parse(raw);

45+

} catch (error) {

46+

const reason = error instanceof Error ? error.message : String(error);

47+

throw new Error(`Failed to parse ${label}: ${reason}`, { cause: error });

48+

}

49+

}

50+51+

export function collectDockerAttestationErrors(params) {

52+

const {

53+

imageRef,

54+

index,

55+

inspectAttestation,

56+

requiredPlatforms,

57+

requiredPredicates = REQUIRED_PREDICATES,

58+

} = params;

59+

const errors = [];

60+

const manifests = Array.isArray(index?.manifests) ? index.manifests : [];

61+

if (manifests.length === 0) {

62+

return [`${imageRef}: expected an image index with manifest descriptors`];

63+

}

64+65+

for (const platform of requiredPlatforms) {

66+

const platformLabel = formatPlatform(platform);

67+

const imageManifest = manifests.find((entry) => platformMatches(entry.platform, platform));

68+

if (!imageManifest?.digest) {

69+

errors.push(`${imageRef}: missing image manifest for ${platformLabel}`);

70+

continue;

71+

}

72+73+

const attestationDescriptors = manifests.filter(

74+

(entry) =>

75+

entry?.annotations?.["vnd.docker.reference.type"] === ATTESTATION_REFERENCE_TYPE &&

76+

entry?.annotations?.["vnd.docker.reference.digest"] === imageManifest.digest &&

77+

typeof entry.digest === "string" &&

78+

entry.digest.length > 0,

79+

);

80+

if (attestationDescriptors.length === 0) {

81+

errors.push(`${imageRef}: missing attestation manifest for ${platformLabel}`);

82+

continue;

83+

}

84+85+

const predicates = new Set();

86+

for (const descriptor of attestationDescriptors) {

87+

const attestation = inspectAttestation(descriptor.digest);

88+

if (attestation?.artifactType !== "application/vnd.docker.attestation.manifest.v1+json") {

89+

errors.push(

90+

`${imageRef}: ${platformLabel} attestation ${descriptor.digest} has unexpected artifactType ${JSON.stringify(

91+

attestation?.artifactType,

92+

)}`,

93+

);

94+

}

95+

for (const layer of attestation?.layers ?? []) {

96+

const predicate = layer?.annotations?.["in-toto.io/predicate-type"];

97+

if (typeof predicate === "string") {

98+

predicates.add(predicate);

99+

}

100+

}

101+

}

102+103+

for (const predicate of requiredPredicates) {

104+

if (!predicates.has(predicate)) {

105+

errors.push(`${imageRef}: ${platformLabel} missing predicate ${predicate}`);

106+

}

107+

}

108+

}

109+110+

return errors;

111+

}

112+113+

function inspectRaw(imageRef) {

114+

return execFileSync("docker", ["buildx", "imagetools", "inspect", "--raw", imageRef], {

115+

encoding: "utf8",

116+

maxBuffer: 20 * 1024 * 1024,

117+

stdio: ["ignore", "pipe", "pipe"],

118+

});

119+

}

120+121+

function parseArgs(argv) {

122+

const imageRefs = [];

123+

const requiredPlatforms = [];

124+

for (let i = 0; i < argv.length; i += 1) {

125+

const arg = argv[i];

126+

if (arg === "--platform") {

127+

const value = argv[i + 1];

128+

if (!value) {

129+

throw new Error("--platform requires a value");

130+

}

131+

requiredPlatforms.push(parsePlatform(value));

132+

i += 1;

133+

continue;

134+

}

135+

if (arg === "--help" || arg === "-h") {

136+

return { help: true, imageRefs, requiredPlatforms };

137+

}

138+

if (arg?.startsWith("-")) {

139+

throw new Error(`Unknown option: ${arg}`);

140+

}

141+

imageRefs.push(arg);

142+

}

143+

return { help: false, imageRefs, requiredPlatforms };

144+

}

145+146+

function printHelp() {

147+

console.log(

148+

`Usage: node scripts/verify-docker-attestations.mjs --platform linux/amd64 --platform linux/arm64 IMAGE...`,

149+

);

150+

}

151+152+

async function main() {

153+

const parsed = parseArgs(process.argv.slice(2));

154+

if (parsed.help) {

155+

printHelp();

156+

return;

157+

}

158+

if (parsed.imageRefs.length === 0) {

159+

throw new Error("At least one image reference is required.");

160+

}

161+

if (parsed.requiredPlatforms.length === 0) {

162+

throw new Error("At least one --platform is required.");

163+

}

164+165+

const allErrors = [];

166+

for (const imageRef of parsed.imageRefs) {

167+

const index = parseJson(inspectRaw(imageRef), `${imageRef} index`);

168+

const errors = collectDockerAttestationErrors({

169+

imageRef,

170+

index,

171+

requiredPlatforms: parsed.requiredPlatforms,

172+

inspectAttestation(digest) {

173+

return parseJson(

174+

inspectRaw(imageRefForDigest(imageRef, digest)),

175+

`${imageRef} attestation ${digest}`,

176+

);

177+

},

178+

});

179+

if (errors.length === 0) {

180+

console.log(

181+

`Verified Docker attestations for ${imageRef}: ${parsed.requiredPlatforms

182+

.map(formatPlatform)

183+

.join(", ")}`,

184+

);

185+

}

186+

allErrors.push(...errors);

187+

}

188+189+

if (allErrors.length > 0) {

190+

for (const error of allErrors) {

191+

console.error(`[docker-attestations] ${error}`);

192+

}

193+

process.exit(1);

194+

}

195+

}

196+197+

if (import.meta.url === `file://${process.argv[1]}`) {

198+

main().catch((error) => {

199+

console.error(error instanceof Error ? error.message : String(error));

200+

process.exit(1);

201+

});

202+

}