release-skill 0.1.3 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +3 -3
- package/CHANGELOG.md +56 -0
- package/INSTALL.md +69 -32
- package/INSTALL.zh-CN.md +64 -26
- package/README.md +111 -37
- package/README.zh-CN.md +95 -28
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +79371 -0
- package/adapters/claude/bin/release-skill.mjs +34 -0
- package/adapters/claude/native/safe-write/binding.gyp +40 -0
- package/adapters/claude/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/claude/native/safe-write/prebuilds.json +24 -0
- package/adapters/claude/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/claude/schemas/.render-manifest.json +37 -0
- package/adapters/claude/schemas/approval-record.schema.json +115 -0
- package/adapters/claude/schemas/artifact-lock.schema.json +111 -0
- package/adapters/claude/schemas/artifact-plan.schema.json +52 -0
- package/adapters/claude/schemas/artifact-policy.schema.json +76 -0
- package/adapters/claude/schemas/evidence-event.schema.json +89 -0
- package/adapters/claude/schemas/release-plan.schema.json +860 -0
- package/adapters/claude/schemas/release-project.schema.json +759 -0
- package/adapters/claude/schemas/release-run.schema.json +342 -0
- package/adapters/claude/skills/release-assess/SKILL.md +5 -6
- package/adapters/claude/skills/release-help/SKILL.md +11 -18
- package/adapters/claude/skills/release-prepare/SKILL.md +6 -6
- package/adapters/claude/skills/release-publish/SKILL.md +1 -4
- package/adapters/claude/skills/release-reconcile/SKILL.md +5 -5
- package/adapters/claude/skills/release-setup/SKILL.md +64 -80
- package/adapters/claude/skills/release-verify/SKILL.md +4 -7
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +79371 -0
- package/adapters/codex/bin/release-skill.mjs +34 -0
- package/adapters/codex/native/safe-write/binding.gyp +40 -0
- package/adapters/codex/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/codex/native/safe-write/prebuilds.json +24 -0
- package/adapters/codex/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/codex/schemas/.render-manifest.json +37 -0
- package/adapters/codex/schemas/approval-record.schema.json +115 -0
- package/adapters/codex/schemas/artifact-lock.schema.json +111 -0
- package/adapters/codex/schemas/artifact-plan.schema.json +52 -0
- package/adapters/codex/schemas/artifact-policy.schema.json +76 -0
- package/adapters/codex/schemas/evidence-event.schema.json +89 -0
- package/adapters/codex/schemas/release-plan.schema.json +860 -0
- package/adapters/codex/schemas/release-project.schema.json +759 -0
- package/adapters/codex/schemas/release-run.schema.json +342 -0
- package/adapters/codex/skills/release-assess/SKILL.md +12 -6
- package/adapters/codex/skills/release-help/SKILL.md +18 -18
- package/adapters/codex/skills/release-prepare/SKILL.md +13 -6
- package/adapters/codex/skills/release-publish/SKILL.md +8 -4
- package/adapters/codex/skills/release-reconcile/SKILL.md +12 -5
- package/adapters/codex/skills/release-setup/SKILL.md +71 -80
- package/adapters/codex/skills/release-verify/SKILL.md +11 -7
- package/bin/release-skill-cli.mjs +807 -0
- package/bin/release-skill.bundle.mjs +79371 -0
- package/bin/release-skill.mjs +23 -788
- package/package.json +6 -2
- package/references/02-project-config.md +2 -2
- package/schemas/release-plan.schema.json +7 -1
- package/schemas/release-project.schema.json +31 -0
- package/scripts/build-bundle.mjs +133 -0
- package/skills/release-assess/SKILL.md +5 -6
- package/skills/release-help/SKILL.md +11 -18
- package/skills/release-prepare/SKILL.md +6 -6
- package/skills/release-publish/SKILL.md +1 -4
- package/skills/release-reconcile/SKILL.md +5 -5
- package/skills/release-setup/SKILL.md +64 -80
- package/skills/release-verify/SKILL.md +4 -7
- package/skills-src/release-assess/SKILL.md +5 -6
- package/skills-src/release-help/SKILL.md +11 -18
- package/skills-src/release-prepare/SKILL.md +6 -6
- package/skills-src/release-publish/SKILL.md +1 -4
- package/skills-src/release-reconcile/SKILL.md +5 -5
- package/skills-src/release-setup/SKILL.md +64 -80
- package/skills-src/release-verify/SKILL.md +4 -7
- package/src/adapters/npm.mjs +5 -13
- package/src/adapters/plugin-marketplace.mjs +137 -34
- package/src/artifacts/policy.mjs +4 -7
- package/src/artifacts/safe-fs-backend-internal.mjs +69 -21
- package/src/commands/prepare.mjs +30 -1
- package/src/commands/publish.mjs +1 -0
- package/src/commands/reconcile.mjs +4 -1
- package/src/commands/setup.mjs +667 -28
- package/src/commands/verify.mjs +4 -1
- package/src/core/approval.mjs +4 -6
- package/src/core/config.mjs +8 -8
- package/src/core/pkg-root.mjs +22 -0
- package/src/core/plan.mjs +68 -5
- package/src/core/run.mjs +4 -4
- package/src/core/trusted-resource.mjs +96 -0
- package/src/docs/version-gate.mjs +164 -0
- package/src/producers/build-adapters.mjs +512 -55
- package/src/snapshot/frozen.mjs +133 -3
- package/src/snapshot/public-map.mjs +7 -4
- package/src/snapshot/scan.mjs +2 -1
package/src/snapshot/frozen.mjs
CHANGED
|
@@ -169,13 +169,47 @@ export async function verifyFrozenDirectoryStructure(directory, label = 'frozen
|
|
|
169
169
|
await walk(directory);
|
|
170
170
|
}
|
|
171
171
|
|
|
172
|
-
|
|
172
|
+
/**
|
|
173
|
+
* Verify an already-frozen Git object directory against the plan.
|
|
174
|
+
*
|
|
175
|
+
* Consumption only: this never rebuilds objects and never reads the wall
|
|
176
|
+
* clock. When `commitTimestamp` (the plan's `frozenSnapshot.commitTimestamp`)
|
|
177
|
+
* is provided, the frozen commit's real `%aI`/`%cI` dates must match it
|
|
178
|
+
* exactly, so a commit carrying any other author/committer time can never
|
|
179
|
+
* pass pre-publish verification.
|
|
180
|
+
*/
|
|
181
|
+
export async function verifyFrozenGitRepository({
|
|
182
|
+
root,
|
|
183
|
+
gitObjectDir,
|
|
184
|
+
commit,
|
|
185
|
+
tree,
|
|
186
|
+
parentCommit,
|
|
187
|
+
commitTimestamp,
|
|
188
|
+
exec = execFile,
|
|
189
|
+
}) {
|
|
173
190
|
const gitDir = await resolveFrozenPath(root, gitObjectDir, 'frozen git object directory');
|
|
174
191
|
await verifyFrozenDirectoryStructure(gitDir, 'frozen git object directory');
|
|
175
192
|
const { stdout } = await exec('git', ['--git-dir', gitDir, 'rev-parse', `${commit}^{tree}`], { shell: false });
|
|
176
193
|
if (stdout.trim() !== tree) {
|
|
177
194
|
throw frozenError('frozen git object tree mismatch', { commit, expectedTree: tree, observedTree: stdout.trim() });
|
|
178
195
|
}
|
|
196
|
+
if (commitTimestamp !== undefined) {
|
|
197
|
+
const expectedTimestamp = normalizeGitTimestamp(commitTimestamp, 'frozen commit timestamp');
|
|
198
|
+
const { stdout: datesOut } = await exec(
|
|
199
|
+
'git',
|
|
200
|
+
['--git-dir', gitDir, 'show', '-s', '--format=%aI%n%cI', commit],
|
|
201
|
+
{ shell: false },
|
|
202
|
+
);
|
|
203
|
+
const [authorDate, committerDate] = datesOut.trim().split('\n');
|
|
204
|
+
if (authorDate !== expectedTimestamp || committerDate !== expectedTimestamp) {
|
|
205
|
+
throw frozenError('frozen git commit dates do not match the plan freeze timestamp', {
|
|
206
|
+
commit,
|
|
207
|
+
expectedTimestamp,
|
|
208
|
+
observedAuthorDate: authorDate,
|
|
209
|
+
observedCommitterDate: committerDate,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
}
|
|
179
213
|
if (parentCommit) {
|
|
180
214
|
if (!/^[a-f0-9]{40,64}$/.test(parentCommit)) {
|
|
181
215
|
throw frozenError('frozen Git parent must be a full hexadecimal object id');
|
|
@@ -242,6 +276,85 @@ async function verifyGitTreeContent({ snapshotDir, repositoryDir, commit, expect
|
|
|
242
276
|
}
|
|
243
277
|
}
|
|
244
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Strict ISO 8601 timestamp with an explicit UTC offset (Z or ±HH:MM),
|
|
281
|
+
* optional fractional seconds. Naive/local timestamps (no offset) are rejected
|
|
282
|
+
* so the frozen commit time is never interpreted in the builder's local zone.
|
|
283
|
+
*/
|
|
284
|
+
const GIT_ISO8601_STRICT_RE =
|
|
285
|
+
/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/;
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Validate and canonicalize a plan freeze timestamp.
|
|
289
|
+
*
|
|
290
|
+
* The value must be a non-empty, strict ISO 8601 timestamp with an explicit UTC
|
|
291
|
+
* offset and a parseable calendar date. It is normalized to UTC with second
|
|
292
|
+
* precision and a `+00:00` offset, which is exactly the form Git stores and
|
|
293
|
+
* re-emits via `%aI`/`%cI`. Missing, empty, malformed, or out-of-range values
|
|
294
|
+
* throw a closed `GATE_FAILED` error.
|
|
295
|
+
*
|
|
296
|
+
* Normalization is idempotent: re-normalizing an already-canonical value returns
|
|
297
|
+
* the identical string, so prepare and the Git builder always agree byte-for-byte.
|
|
298
|
+
*
|
|
299
|
+
* @param {unknown} value - Candidate timestamp.
|
|
300
|
+
* @param {string} [label] - Label used in error messages.
|
|
301
|
+
* @returns {string} Canonical `YYYY-MM-DDTHH:MM:SS+00:00` timestamp.
|
|
302
|
+
* @throws {ReleaseError} GATE_FAILED when the value is not a valid timestamp.
|
|
303
|
+
*/
|
|
304
|
+
export function normalizeGitTimestamp(value, label = 'freeze timestamp') {
|
|
305
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
306
|
+
throw frozenError(`${label} must be a non-empty strict ISO 8601 timestamp string`, {
|
|
307
|
+
received: typeof value === 'string' ? value : typeof value,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const trimmed = value.trim();
|
|
311
|
+
const match = GIT_ISO8601_STRICT_RE.exec(trimmed);
|
|
312
|
+
if (!match) {
|
|
313
|
+
throw frozenError(`${label} must be strict ISO 8601 with an explicit UTC offset`, {
|
|
314
|
+
received: trimmed,
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
const [, yearStr, monthStr, dayStr, hourStr, minuteStr, secondStr, zone] = match;
|
|
318
|
+
const inputYear = Number(yearStr);
|
|
319
|
+
const inputMonth = Number(monthStr);
|
|
320
|
+
const inputDay = Number(dayStr);
|
|
321
|
+
const inputHour = Number(hourStr);
|
|
322
|
+
const inputMinute = Number(minuteStr);
|
|
323
|
+
const inputSecond = Number(secondStr);
|
|
324
|
+
if (inputYear < 1000 || inputYear > 9999) {
|
|
325
|
+
throw frozenError(`${label} year must be a four-digit ISO year`, { received: trimmed });
|
|
326
|
+
}
|
|
327
|
+
// Reject impossible calendar dates and out-of-range time fields. Date.UTC
|
|
328
|
+
// silently rolls values over (Feb 30 -> Mar 2), so compare the constructed
|
|
329
|
+
// UTC fields against the declared input fields instead of trusting parsing.
|
|
330
|
+
const probeMs = Date.UTC(inputYear, inputMonth - 1, inputDay, inputHour, inputMinute, inputSecond);
|
|
331
|
+
const probe = new Date(probeMs);
|
|
332
|
+
if (
|
|
333
|
+
probe.getUTCFullYear() !== inputYear || probe.getUTCMonth() !== inputMonth - 1 ||
|
|
334
|
+
probe.getUTCDate() !== inputDay || probe.getUTCHours() !== inputHour ||
|
|
335
|
+
probe.getUTCMinutes() !== inputMinute || probe.getUTCSeconds() !== inputSecond
|
|
336
|
+
) {
|
|
337
|
+
throw frozenError(`${label} is not a valid calendar date`, { received: trimmed });
|
|
338
|
+
}
|
|
339
|
+
let offsetMs = 0;
|
|
340
|
+
if (zone !== 'Z') {
|
|
341
|
+
const sign = zone.startsWith('-') ? -1 : 1;
|
|
342
|
+
const zoneHours = Number(zone.slice(1, 3));
|
|
343
|
+
const zoneMinutes = Number(zone.slice(4, 6));
|
|
344
|
+
if (zoneHours > 23 || zoneMinutes > 59) {
|
|
345
|
+
throw frozenError(`${label} has an invalid UTC offset`, { received: trimmed });
|
|
346
|
+
}
|
|
347
|
+
offsetMs = sign * (zoneHours * 60 + zoneMinutes) * 60_000;
|
|
348
|
+
}
|
|
349
|
+
const date = new Date(probeMs - offsetMs);
|
|
350
|
+
if (date.getUTCFullYear() < 1000 || date.getUTCFullYear() > 9999) {
|
|
351
|
+
throw frozenError(`${label} normalizes outside the four-digit ISO year range`, { received: trimmed });
|
|
352
|
+
}
|
|
353
|
+
const pad = (num) => String(num).padStart(2, '0');
|
|
354
|
+
return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())}` +
|
|
355
|
+
`T${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())}+00:00`;
|
|
356
|
+
}
|
|
357
|
+
|
|
245
358
|
function publicGitRemote({ repo, githubHost = 'github.com' }) {
|
|
246
359
|
if (typeof repo !== 'string' || !/^[A-Za-z0-9._-]+\/[A-Za-z0-9._-]+$/.test(repo)) {
|
|
247
360
|
throw frozenError('Git parent repo must use owner/name format');
|
|
@@ -255,14 +368,31 @@ function publicGitRemote({ repo, githubHost = 'github.com' }) {
|
|
|
255
368
|
return `https://${githubHost}/${repo}.git`;
|
|
256
369
|
}
|
|
257
370
|
|
|
371
|
+
/**
|
|
372
|
+
* Build the frozen release Git objects for a sealed public snapshot.
|
|
373
|
+
*
|
|
374
|
+
* `commitTimestamp` is the plan freeze timestamp sampled exactly once during
|
|
375
|
+
* production prepare. It is strictly validated BEFORE any filesystem or Git
|
|
376
|
+
* write (missing, empty, malformed, or impossible values fail closed), then
|
|
377
|
+
* written verbatim to both GIT_AUTHOR_DATE and GIT_COMMITTER_DATE, so the
|
|
378
|
+
* frozen commit's `%aI` and `%cI` always equal the plan's
|
|
379
|
+
* `frozenSnapshot.commitTimestamp` byte-for-byte. Identical inputs therefore
|
|
380
|
+
* yield identical tree and commit identifiers, and a different freeze
|
|
381
|
+
* timestamp alone yields a different commit with an identical tree.
|
|
382
|
+
*/
|
|
258
383
|
export async function buildFrozenGitRepository({
|
|
259
384
|
snapshotDir,
|
|
260
385
|
repositoryDir,
|
|
261
386
|
version,
|
|
262
387
|
expectedSnapshotDigest,
|
|
263
388
|
parent,
|
|
389
|
+
commitTimestamp,
|
|
264
390
|
exec = execFile,
|
|
265
391
|
}) {
|
|
392
|
+
// Fail closed before creating the object directory or running any Git
|
|
393
|
+
// command: a frozen release commit may never inherit the builder's wall
|
|
394
|
+
// clock, an ancient constant, or a value Git would reinterpret.
|
|
395
|
+
const canonicalTimestamp = normalizeGitTimestamp(commitTimestamp, 'frozen commit timestamp');
|
|
266
396
|
if (!expectedSnapshotDigest) throw frozenError('Git object build requires the sealed snapshot digest');
|
|
267
397
|
await mkdir(repositoryDir, { recursive: true });
|
|
268
398
|
await exec('git', ['init', '--bare', repositoryDir], { shell: false });
|
|
@@ -312,8 +442,8 @@ export async function buildFrozenGitRepository({
|
|
|
312
442
|
GIT_AUTHOR_EMAIL: 'release-skill@localhost',
|
|
313
443
|
GIT_COMMITTER_NAME: 'release-skill',
|
|
314
444
|
GIT_COMMITTER_EMAIL: 'release-skill@localhost',
|
|
315
|
-
GIT_AUTHOR_DATE:
|
|
316
|
-
GIT_COMMITTER_DATE:
|
|
445
|
+
GIT_AUTHOR_DATE: canonicalTimestamp,
|
|
446
|
+
GIT_COMMITTER_DATE: canonicalTimestamp,
|
|
317
447
|
};
|
|
318
448
|
const commitArgs = ['--git-dir', repositoryDir, 'commit-tree', tree];
|
|
319
449
|
if (parentCommit) commitArgs.push('-p', parentCommit);
|
|
@@ -281,6 +281,7 @@ export function normalizePublicMappings({ unit } = {}) {
|
|
|
281
281
|
return Object.freeze(
|
|
282
282
|
unit.publicFiles.map((mapping) => {
|
|
283
283
|
const from = canonicalPublicPath(mapping.from).path;
|
|
284
|
+
const sourceScope = mapping.sourceScope ?? 'unit';
|
|
284
285
|
if (isReservedReleaseControlPath(from)) {
|
|
285
286
|
throw new ReleaseError(
|
|
286
287
|
PUBLIC_PATH_FORBIDDEN,
|
|
@@ -291,7 +292,7 @@ export function normalizePublicMappings({ unit } = {}) {
|
|
|
291
292
|
const sourceRelative = posix.relative(sourcePrefix, from);
|
|
292
293
|
|
|
293
294
|
// Reject `from` paths that escape `unit.source`.
|
|
294
|
-
if (sourceRelative.startsWith('../') || sourceRelative === '..') {
|
|
295
|
+
if (sourceScope === 'unit' && (sourceRelative.startsWith('../') || sourceRelative === '..')) {
|
|
295
296
|
throw new ReleaseError(
|
|
296
297
|
PUBLIC_PATH_FORBIDDEN,
|
|
297
298
|
`public file "${mapping.from}" is outside unit source "${unit.source}"`,
|
|
@@ -301,6 +302,7 @@ export function normalizePublicMappings({ unit } = {}) {
|
|
|
301
302
|
|
|
302
303
|
return Object.freeze({
|
|
303
304
|
from,
|
|
305
|
+
sourceScope,
|
|
304
306
|
sourceRelative,
|
|
305
307
|
to: canonicalPublicPath(mapping.to).path,
|
|
306
308
|
mode: mapping.mode,
|
|
@@ -576,7 +578,7 @@ export async function buildPublicStaging({
|
|
|
576
578
|
continue;
|
|
577
579
|
}
|
|
578
580
|
// Also check real containment inside unit root
|
|
579
|
-
if (!isContained(realUnitRoot, realSrc)) {
|
|
581
|
+
if (mapping.sourceScope === 'unit' && !isContained(realUnitRoot, realSrc)) {
|
|
580
582
|
preflightRecords.push({
|
|
581
583
|
ok: false,
|
|
582
584
|
err: new ReleaseError(
|
|
@@ -988,7 +990,8 @@ export async function buildPublicStaging({
|
|
|
988
990
|
);
|
|
989
991
|
}
|
|
990
992
|
|
|
991
|
-
// Verify realpath of the handle is inside
|
|
993
|
+
// Verify realpath of the handle is inside the workspace and, for the
|
|
994
|
+
// default unit scope, inside the unit root as well.
|
|
992
995
|
// This catches a race between preflight and open where a component
|
|
993
996
|
// was replaced with a symlink.
|
|
994
997
|
const handleRealPath = await fs.realpath(srcPath, { stage: 'source-open-realpath' });
|
|
@@ -999,7 +1002,7 @@ export async function buildPublicStaging({
|
|
|
999
1002
|
{ from: mapping.from, realPath: handleRealPath },
|
|
1000
1003
|
);
|
|
1001
1004
|
}
|
|
1002
|
-
if (!isContained(realUnitRoot, handleRealPath)) {
|
|
1005
|
+
if (mapping.sourceScope === 'unit' && !isContained(realUnitRoot, handleRealPath)) {
|
|
1003
1006
|
throw new ReleaseError(
|
|
1004
1007
|
SNAPSHOT_FIDELITY_FAILED,
|
|
1005
1008
|
`source escapes unitRoot containment after open: "${mapping.from}"`,
|
package/src/snapshot/scan.mjs
CHANGED
|
@@ -49,7 +49,8 @@ const BINARY_PROBE_SIZE = 8192;
|
|
|
49
49
|
*/
|
|
50
50
|
const ABSOLUTE_PATH_PATTERNS = [
|
|
51
51
|
/\/(?:Users|home)\/[A-Za-z0-9_-][A-Za-z0-9._-]*(?:\/[^\s"'`<>]*)?/,
|
|
52
|
-
/\/
|
|
52
|
+
/\/root\/[A-Za-z0-9_-][A-Za-z0-9._-]*(?:\/[^\s"'`<>]*)?/,
|
|
53
|
+
/\/tmp\/(?=[^\s"'`<>]*(?:secret|token|credential|password|private[-_]?key|id_rsa))[^\s"'`<>]+/i,
|
|
53
54
|
/(?:^|[^A-Za-z0-9_])[A-Za-z]:\\[A-Za-z0-9_$-][A-Za-z0-9._$-]*(?:\\[^\s"'`<>]*)?/,
|
|
54
55
|
];
|
|
55
56
|
|