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/artifacts/policy.mjs
CHANGED
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { readFile } from 'node:fs/promises';
|
|
13
|
-
import { readFileSync } from 'node:fs';
|
|
14
13
|
import { join } from 'node:path';
|
|
15
14
|
import { createHash } from 'node:crypto';
|
|
16
15
|
import YAML from 'yaml';
|
|
@@ -22,21 +21,19 @@ import {
|
|
|
22
21
|
PATH_UNSAFE,
|
|
23
22
|
} from '../core/errors.mjs';
|
|
24
23
|
import { canonicalArtifactPath } from './path-key.mjs';
|
|
24
|
+
import { readTrustedPackageResourceSync } from '../core/trusted-resource.mjs';
|
|
25
25
|
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
// Schema validation
|
|
28
28
|
// ---------------------------------------------------------------------------
|
|
29
29
|
|
|
30
|
-
const POLICY_SCHEMA_PATH = new URL(
|
|
31
|
-
'../../schemas/artifact-policy.schema.json',
|
|
32
|
-
import.meta.url,
|
|
33
|
-
);
|
|
34
|
-
|
|
35
30
|
const ajv = new Ajv({ allErrors: true, strict: false });
|
|
36
31
|
addFormats(ajv);
|
|
37
32
|
|
|
38
33
|
// Compile schema once at module load (synchronous).
|
|
39
|
-
const _validate = ajv.compile(JSON.parse(
|
|
34
|
+
const _validate = ajv.compile(JSON.parse(readTrustedPackageResourceSync(
|
|
35
|
+
'schemas/artifact-policy.schema.json',
|
|
36
|
+
).toString('utf8')));
|
|
40
37
|
|
|
41
38
|
/**
|
|
42
39
|
* Validate a policy object against the artifact-policy JSON Schema.
|
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
|
|
11
11
|
import { createRequire } from 'node:module';
|
|
12
12
|
import { dirname, resolve } from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
13
14
|
import { realpath } from 'node:fs/promises';
|
|
14
15
|
import { readFileSync, realpathSync, lstatSync } from 'node:fs';
|
|
15
16
|
import { createHash } from 'node:crypto';
|
|
16
|
-
import { fileURLToPath } from 'node:url';
|
|
17
17
|
|
|
18
18
|
import {
|
|
19
19
|
ReleaseError,
|
|
@@ -25,17 +25,71 @@ import {
|
|
|
25
25
|
// Constants
|
|
26
26
|
// ---------------------------------------------------------------------------
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const
|
|
28
|
+
// Keep this internal loader self-contained. Besides making the native trust
|
|
29
|
+
// boundary auditable, this lets isolated consumers test/copy it together with
|
|
30
|
+
// errors.mjs only. In the bundle the banner supplies __bundlePkgRoot; in source
|
|
31
|
+
// mode the module's own URL deterministically identifies the package root.
|
|
32
|
+
const PKG_ROOT = typeof __bundlePkgRoot !== 'undefined'
|
|
33
|
+
? __bundlePkgRoot
|
|
34
|
+
: resolve(dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
35
|
+
const NATIVE_BASE = resolve(PKG_ROOT, 'native/safe-write');
|
|
36
|
+
const PREBUILDS_PATH = resolve(NATIVE_BASE, 'prebuilds.json');
|
|
37
|
+
const PREBUILDS_RESOURCE = 'native/safe-write/prebuilds.json';
|
|
38
|
+
const DEFAULT_ADDON_PATH = resolve(NATIVE_BASE, 'build/Release/safe_write.node');
|
|
33
39
|
|
|
34
40
|
const EXPECTED_EXPORTS = Object.freeze([
|
|
35
41
|
'openRoot', 'openDir', 'readEntry', 'readFile',
|
|
36
42
|
'createTemp', 'rename', 'abortTemp', 'mkdir', 'rmdir', 'unlink', 'chmod', 'fsync', 'close',
|
|
37
43
|
]);
|
|
38
44
|
|
|
45
|
+
function failPrebuildManifest(reason, message = 'prebuilds manifest not readable') {
|
|
46
|
+
throw new ReleaseError(
|
|
47
|
+
SAFE_WRITE_UNAVAILABLE,
|
|
48
|
+
message,
|
|
49
|
+
{ resource: PREBUILDS_RESOURCE, reason },
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function readTrustedPrebuildManifest() {
|
|
54
|
+
let lexicalStat;
|
|
55
|
+
try {
|
|
56
|
+
lexicalStat = lstatSync(PREBUILDS_PATH, { bigint: true });
|
|
57
|
+
} catch {
|
|
58
|
+
failPrebuildManifest('MISSING');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (lexicalStat.isSymbolicLink()) {
|
|
62
|
+
failPrebuildManifest('SYMLINK', 'prebuilds manifest must not be a symlink');
|
|
63
|
+
}
|
|
64
|
+
if (!lexicalStat.isFile()) {
|
|
65
|
+
failPrebuildManifest('NOT_REGULAR_FILE', 'prebuilds manifest is not a regular file');
|
|
66
|
+
}
|
|
67
|
+
if (lexicalStat.nlink !== 1n) {
|
|
68
|
+
failPrebuildManifest('UNEXPECTED_HARDLINK_COUNT', 'prebuilds manifest has invalid nlink count');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let physicalBase;
|
|
72
|
+
let physicalManifest;
|
|
73
|
+
try {
|
|
74
|
+
physicalBase = realpathSync(NATIVE_BASE);
|
|
75
|
+
physicalManifest = realpathSync(PREBUILDS_PATH);
|
|
76
|
+
} catch {
|
|
77
|
+
failPrebuildManifest('REALPATH_FAILED');
|
|
78
|
+
}
|
|
79
|
+
if (!physicalManifest.startsWith(physicalBase + '/') && physicalManifest !== physicalBase) {
|
|
80
|
+
failPrebuildManifest(
|
|
81
|
+
'PHYSICAL_PATH_ESCAPE',
|
|
82
|
+
'prebuilds manifest physical path escapes base directory',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
try {
|
|
87
|
+
return JSON.parse(readFileSync(physicalManifest, 'utf8'));
|
|
88
|
+
} catch {
|
|
89
|
+
failPrebuildManifest('READ_FAILED');
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
39
93
|
// ---------------------------------------------------------------------------
|
|
40
94
|
// Error sanitization
|
|
41
95
|
// ---------------------------------------------------------------------------
|
|
@@ -58,7 +112,7 @@ function sanitizeError(err) {
|
|
|
58
112
|
* Returns the verified physical path on success, or null on failure.
|
|
59
113
|
*/
|
|
60
114
|
function isWithinPackageBuildDir(addonPath) {
|
|
61
|
-
const buildDir = resolve(
|
|
115
|
+
const buildDir = resolve(NATIVE_BASE, 'build/');
|
|
62
116
|
const resolved = resolve(addonPath);
|
|
63
117
|
if (!resolved.startsWith(buildDir + '/') && resolved !== buildDir) {
|
|
64
118
|
return null;
|
|
@@ -106,8 +160,8 @@ function validateManifestEntry(entry, key) {
|
|
|
106
160
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest entry missing exports for ${key}`);
|
|
107
161
|
}
|
|
108
162
|
|
|
109
|
-
const resolved = resolve(
|
|
110
|
-
const baseDir =
|
|
163
|
+
const resolved = resolve(NATIVE_BASE, entry.path);
|
|
164
|
+
const baseDir = NATIVE_BASE;
|
|
111
165
|
if (!resolved.startsWith(baseDir + '/') && resolved !== baseDir) {
|
|
112
166
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, `manifest path escapes base directory for ${key}`);
|
|
113
167
|
}
|
|
@@ -126,7 +180,8 @@ function validateManifestEntry(entry, key) {
|
|
|
126
180
|
|
|
127
181
|
function requireAddon(addonPath) {
|
|
128
182
|
try {
|
|
129
|
-
|
|
183
|
+
// Use PKG_ROOT-relative resolution so bundled mode can find the addon.
|
|
184
|
+
const require = createRequire(resolve(PKG_ROOT, 'package.json'));
|
|
130
185
|
return require(addonPath);
|
|
131
186
|
} catch (err) {
|
|
132
187
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, sanitizeError(err));
|
|
@@ -149,14 +204,7 @@ function requireAddon(addonPath) {
|
|
|
149
204
|
|
|
150
205
|
export function readStableAddon(options = {}) {
|
|
151
206
|
const hooks = options._hooks || {};
|
|
152
|
-
|
|
153
|
-
const prebuildsPath = resolve(__dirname, PREBUILDS_REL);
|
|
154
|
-
let manifest;
|
|
155
|
-
try {
|
|
156
|
-
manifest = JSON.parse(readFileSync(prebuildsPath, 'utf8'));
|
|
157
|
-
} catch {
|
|
158
|
-
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilds manifest not readable');
|
|
159
|
-
}
|
|
207
|
+
const manifest = readTrustedPrebuildManifest();
|
|
160
208
|
|
|
161
209
|
if (!manifest.prebuilds || typeof manifest.prebuilds !== 'object') {
|
|
162
210
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilds manifest has invalid structure');
|
|
@@ -174,7 +222,7 @@ export function readStableAddon(options = {}) {
|
|
|
174
222
|
validateManifestEntry(entry, key);
|
|
175
223
|
|
|
176
224
|
// Resolve lexical candidate path
|
|
177
|
-
const addonFile = resolve(
|
|
225
|
+
const addonFile = resolve(NATIVE_BASE, entry.path);
|
|
178
226
|
|
|
179
227
|
// Step 1: lstat LEXICAL path FIRST — reject symlink, non-regular, nlink!=1
|
|
180
228
|
// Must lstat the original path to detect symlinks; lstat on a resolved path
|
|
@@ -207,7 +255,7 @@ export function readStableAddon(options = {}) {
|
|
|
207
255
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilt addon not accessible');
|
|
208
256
|
}
|
|
209
257
|
|
|
210
|
-
const nativeBaseDir = realpathSync(
|
|
258
|
+
const nativeBaseDir = realpathSync(NATIVE_BASE);
|
|
211
259
|
if (!physicalAddonFile.startsWith(nativeBaseDir + '/') && physicalAddonFile !== nativeBaseDir) {
|
|
212
260
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'prebuilt addon physical path escapes base directory');
|
|
213
261
|
}
|
|
@@ -305,7 +353,7 @@ export function loadNativeAddon(addonPath) {
|
|
|
305
353
|
|
|
306
354
|
// 2. Development mode
|
|
307
355
|
if (process.env.RELEASE_SKILL_NATIVE_DEV === '1') {
|
|
308
|
-
const devPath =
|
|
356
|
+
const devPath = DEFAULT_ADDON_PATH;
|
|
309
357
|
const physicalPath = isWithinPackageBuildDir(devPath);
|
|
310
358
|
if (!physicalPath) {
|
|
311
359
|
throw new ReleaseError(SAFE_WRITE_UNAVAILABLE, 'dev addon path is outside build directory');
|
package/src/commands/prepare.mjs
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
buildFrozenGitRepository,
|
|
38
38
|
buildFrozenNpmTarball,
|
|
39
39
|
computeFrozenSnapshot,
|
|
40
|
+
normalizeGitTimestamp,
|
|
40
41
|
sealFrozenSnapshot,
|
|
41
42
|
} from '../snapshot/frozen.mjs';
|
|
42
43
|
import { ReleaseError, GATE_FAILED, CONFIG_INVALID, FORBIDDEN_CONTENT_DETECTED } from '../core/errors.mjs';
|
|
@@ -514,7 +515,11 @@ async function buildProductionAssets(
|
|
|
514
515
|
runDir,
|
|
515
516
|
unitBaselineResults,
|
|
516
517
|
buildGitRepository = buildFrozenGitRepository,
|
|
518
|
+
freezeTimestamp,
|
|
517
519
|
) {
|
|
520
|
+
// The plan freeze timestamp is sampled exactly once by prepareRelease.
|
|
521
|
+
// Every unit reuses this single value; the wall clock is never re-read.
|
|
522
|
+
const canonicalFreezeTimestamp = normalizeGitTimestamp(freezeTimestamp, 'plan freeze timestamp');
|
|
518
523
|
for (const { unit } of unitResults) {
|
|
519
524
|
const npmDistribution = (unit.distributions ?? []).find((distribution) => distribution.type === 'npm');
|
|
520
525
|
if (npmDistribution && !['public', 'restricted'].includes(npmDistribution.access)) {
|
|
@@ -561,6 +566,7 @@ async function buildProductionAssets(
|
|
|
561
566
|
version,
|
|
562
567
|
expectedSnapshotDigest: sealed.digest,
|
|
563
568
|
parent,
|
|
569
|
+
commitTimestamp: canonicalFreezeTimestamp,
|
|
564
570
|
});
|
|
565
571
|
|
|
566
572
|
let npm = null;
|
|
@@ -586,6 +592,7 @@ async function buildProductionAssets(
|
|
|
586
592
|
gitObjectDir: relative(root, repositoryDir),
|
|
587
593
|
commit: git.commit,
|
|
588
594
|
tree: git.tree,
|
|
595
|
+
commitTimestamp: canonicalFreezeTimestamp,
|
|
589
596
|
branchStrategy,
|
|
590
597
|
...(git.parentCommit ? { parentCommit: git.parentCommit } : {}),
|
|
591
598
|
branch,
|
|
@@ -683,6 +690,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
683
690
|
const claudeDist = (unit.distributions ?? []).find((d) => d.type === 'claude-plugin');
|
|
684
691
|
if (claudeDist) {
|
|
685
692
|
const identity = marketplaceIdentity(claudeDist);
|
|
693
|
+
const claudeTimeoutMs = Number.isInteger(claudeDist.timeoutMs) ? claudeDist.timeoutMs : 300000;
|
|
686
694
|
actions.push({
|
|
687
695
|
id: `claude-marketplace-install-${unit.id}`,
|
|
688
696
|
type: 'claude-marketplace-install',
|
|
@@ -695,6 +703,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
695
703
|
repo: unit.publicRepo,
|
|
696
704
|
version,
|
|
697
705
|
entrySkill: identity.entrySkill,
|
|
706
|
+
timeoutMs: claudeTimeoutMs,
|
|
698
707
|
},
|
|
699
708
|
expected: {
|
|
700
709
|
installed: true,
|
|
@@ -709,6 +718,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
709
718
|
const codexDist = (unit.distributions ?? []).find((d) => d.type === 'codex-plugin');
|
|
710
719
|
if (codexDist) {
|
|
711
720
|
const identity = marketplaceIdentity(codexDist);
|
|
721
|
+
const codexTimeoutMs = Number.isInteger(codexDist.timeoutMs) ? codexDist.timeoutMs : 300000;
|
|
712
722
|
actions.push({
|
|
713
723
|
id: `codex-marketplace-install-${unit.id}`,
|
|
714
724
|
type: 'codex-marketplace-install',
|
|
@@ -721,6 +731,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
721
731
|
repo: unit.publicRepo,
|
|
722
732
|
version,
|
|
723
733
|
entrySkill: identity.entrySkill,
|
|
734
|
+
timeoutMs: codexTimeoutMs,
|
|
724
735
|
},
|
|
725
736
|
expected: {
|
|
726
737
|
installed: true,
|
|
@@ -877,6 +888,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
877
888
|
const claudeDist = (unit.distributions ?? []).find((d) => d.type === 'claude-plugin');
|
|
878
889
|
if (claudeDist) {
|
|
879
890
|
const identity = marketplaceIdentity(claudeDist);
|
|
891
|
+
const claudeTimeoutMs = Number.isInteger(claudeDist.timeoutMs) ? claudeDist.timeoutMs : 300000;
|
|
880
892
|
actions.push({
|
|
881
893
|
id: `claude-marketplace-install-${unit.id}`,
|
|
882
894
|
type: 'claude-marketplace-install',
|
|
@@ -892,6 +904,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
892
904
|
entrySkill: identity.entrySkill,
|
|
893
905
|
snapshotPath: asset.snapshotPath,
|
|
894
906
|
manifestDigest: asset.manifestDigest,
|
|
907
|
+
timeoutMs: claudeTimeoutMs,
|
|
895
908
|
},
|
|
896
909
|
expected: {
|
|
897
910
|
installed: true,
|
|
@@ -911,6 +924,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
911
924
|
const codexDist = (unit.distributions ?? []).find((d) => d.type === 'codex-plugin');
|
|
912
925
|
if (codexDist) {
|
|
913
926
|
const identity = marketplaceIdentity(codexDist);
|
|
927
|
+
const codexTimeoutMs = Number.isInteger(codexDist.timeoutMs) ? codexDist.timeoutMs : 300000;
|
|
914
928
|
actions.push({
|
|
915
929
|
id: `codex-marketplace-install-${unit.id}`,
|
|
916
930
|
type: 'codex-marketplace-install',
|
|
@@ -926,6 +940,7 @@ function buildExternalActions(unitResults, resolvedVersions, productionAssets) {
|
|
|
926
940
|
entrySkill: identity.entrySkill,
|
|
927
941
|
snapshotPath: asset.snapshotPath,
|
|
928
942
|
manifestDigest: asset.manifestDigest,
|
|
943
|
+
timeoutMs: codexTimeoutMs,
|
|
929
944
|
},
|
|
930
945
|
expected: {
|
|
931
946
|
installed: true,
|
|
@@ -1443,6 +1458,18 @@ export async function prepareRelease(options) {
|
|
|
1443
1458
|
// --- Step 7: Build plan object ---
|
|
1444
1459
|
await evidence.append({ phase: 'plan-assembly', status: 'started' });
|
|
1445
1460
|
|
|
1461
|
+
// Production plans sample their freeze timestamp exactly once, before the
|
|
1462
|
+
// first frozen Git object exists. This single canonical value becomes
|
|
1463
|
+
// GIT_AUTHOR_DATE/GIT_COMMITTER_DATE for every unit's frozen commit,
|
|
1464
|
+
// every unit's frozenSnapshot.commitTimestamp, and plan.createdAt. It is
|
|
1465
|
+
// thereby bound by the plan digest and the approval record; publish,
|
|
1466
|
+
// retry, and reconcile consume it from the frozen plan and never re-read
|
|
1467
|
+
// the wall clock. A missing or invalid injected value fails closed here,
|
|
1468
|
+
// before any Git write.
|
|
1469
|
+
const freezeTimestamp = production
|
|
1470
|
+
? normalizeGitTimestamp(clock ? clock() : new Date().toISOString(), 'plan freeze timestamp')
|
|
1471
|
+
: null;
|
|
1472
|
+
|
|
1446
1473
|
const productionAssets = production
|
|
1447
1474
|
? await buildProductionAssets(
|
|
1448
1475
|
unitResults,
|
|
@@ -1451,6 +1478,7 @@ export async function prepareRelease(options) {
|
|
|
1451
1478
|
runDir,
|
|
1452
1479
|
unitBaselineResults,
|
|
1453
1480
|
options.buildFrozenGitRepositoryFn ?? buildFrozenGitRepository,
|
|
1481
|
+
freezeTimestamp,
|
|
1454
1482
|
)
|
|
1455
1483
|
: null;
|
|
1456
1484
|
|
|
@@ -1474,6 +1502,7 @@ export async function prepareRelease(options) {
|
|
|
1474
1502
|
branchStrategy: productionAssets[idx].branchStrategy,
|
|
1475
1503
|
commit: productionAssets[idx].commit,
|
|
1476
1504
|
tree: productionAssets[idx].tree,
|
|
1505
|
+
commitTimestamp: productionAssets[idx].commitTimestamp,
|
|
1477
1506
|
...(productionAssets[idx].parentCommit
|
|
1478
1507
|
? { parentCommit: productionAssets[idx].parentCommit }
|
|
1479
1508
|
: {}),
|
|
@@ -1512,7 +1541,7 @@ export async function prepareRelease(options) {
|
|
|
1512
1541
|
} : {}),
|
|
1513
1542
|
units,
|
|
1514
1543
|
externalActions,
|
|
1515
|
-
createdAt: (clock ? clock() : new Date().toISOString()),
|
|
1544
|
+
createdAt: production ? freezeTimestamp : (clock ? clock() : new Date().toISOString()),
|
|
1516
1545
|
};
|
|
1517
1546
|
|
|
1518
1547
|
await evidence.append({
|
package/src/commands/publish.mjs
CHANGED
|
@@ -389,6 +389,7 @@ export async function publishRelease(options) {
|
|
|
389
389
|
gitObjectDir: frozen.gitObjectDir,
|
|
390
390
|
commit: frozen.commit,
|
|
391
391
|
tree: frozen.tree,
|
|
392
|
+
commitTimestamp: frozen.commitTimestamp,
|
|
392
393
|
});
|
|
393
394
|
assertInsideAssetRoot(assetRoot, git.gitDir, 'frozen git object directory');
|
|
394
395
|
if (frozen.npm) {
|
|
@@ -222,7 +222,10 @@ export async function reconcileRelease(options) {
|
|
|
222
222
|
// =======================================================================
|
|
223
223
|
await evidence.append({ phase: 'safety-gate', gate: 'action-completeness', status: 'started' });
|
|
224
224
|
|
|
225
|
-
|
|
225
|
+
// Use legacyCompatibility: old PARTIAL plans (pre-v0.1.5) lack
|
|
226
|
+
// parameters.timeoutMs. Reconcile must still pass these plans,
|
|
227
|
+
// while strict mode (prepare/approve/publish) rejects them.
|
|
228
|
+
const completenessResult = validatePlanActionCompleteness(plan, { legacyCompatibility: true });
|
|
226
229
|
if (!completenessResult.passed) {
|
|
227
230
|
await evidence.append({
|
|
228
231
|
phase: 'safety-gate',
|