release-skill 0.9.0 → 0.9.3
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 +1 -1
- package/.codebuddy-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +81 -0
- package/INSTALL.md +22 -9
- package/INSTALL.zh-CN.md +15 -8
- package/README.md +40 -35
- package/README.zh-CN.md +31 -29
- package/adapters/claude/.claude-plugin/marketplace.json +1 -1
- package/adapters/claude/.claude-plugin/plugin.json +1 -1
- package/adapters/claude/bin/consumer-contract-vectors.json +110 -0
- package/adapters/claude/bin/error-codes.json +8 -1
- package/adapters/claude/bin/foundation-resource-binding.json +1 -1
- package/adapters/claude/bin/registry.json +22 -1
- package/adapters/claude/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/claude/bin/rules.json +1 -1
- package/adapters/claude/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/claude/schemas/release-project.schema.json +4 -0
- package/adapters/claude/schemas/release-run.schema.json +41 -1
- package/adapters/claude/skills/release-config/SKILL.md +5 -3
- package/adapters/claude/skills/release-docs/SKILL.md +4 -2
- package/adapters/claude/skills/release-finish/SKILL.md +6 -4
- package/adapters/claude/skills/release-help/SKILL.md +14 -5
- package/adapters/claude/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/claude/skills/release-prepare/SKILL.md +4 -2
- package/adapters/claude/skills/release-publish/SKILL.md +19 -10
- package/adapters/claude/skills/release-verify/SKILL.md +2 -0
- package/adapters/claude/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/claude/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/claude/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/claude/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/claude/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/claude/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/claude/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/claude/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/consumer-contract-vectors.json +110 -0
- package/adapters/codex/bin/error-codes.json +8 -1
- package/adapters/codex/bin/foundation-resource-binding.json +1 -1
- package/adapters/codex/bin/registry.json +22 -1
- package/adapters/codex/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/codex/bin/rules.json +1 -1
- package/adapters/codex/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/codex/schemas/release-project.schema.json +4 -0
- package/adapters/codex/schemas/release-run.schema.json +41 -1
- package/adapters/codex/skills/release-config/SKILL.md +5 -3
- package/adapters/codex/skills/release-docs/SKILL.md +4 -2
- package/adapters/codex/skills/release-finish/SKILL.md +6 -4
- package/adapters/codex/skills/release-help/SKILL.md +14 -5
- package/adapters/codex/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +4 -2
- package/adapters/codex/skills/release-publish/SKILL.md +19 -10
- package/adapters/codex/skills/release-verify/SKILL.md +2 -0
- package/adapters/codex/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/codex/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/codex/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/codex/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/codex/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/codex/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/codex/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/codex/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/consumer-contract-vectors.json +110 -0
- package/adapters/kimi/bin/error-codes.json +8 -1
- package/adapters/kimi/bin/foundation-resource-binding.json +1 -1
- package/adapters/kimi/bin/registry.json +22 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/kimi/bin/rules.json +1 -1
- package/adapters/kimi/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/kimi/schemas/release-project.schema.json +4 -0
- package/adapters/kimi/schemas/release-run.schema.json +41 -1
- package/adapters/kimi/skills/release-config/SKILL.md +5 -3
- package/adapters/kimi/skills/release-docs/SKILL.md +4 -2
- package/adapters/kimi/skills/release-finish/SKILL.md +6 -4
- package/adapters/kimi/skills/release-help/SKILL.md +14 -5
- package/adapters/kimi/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/kimi/skills/release-prepare/SKILL.md +4 -2
- package/adapters/kimi/skills/release-publish/SKILL.md +19 -10
- package/adapters/kimi/skills/release-verify/SKILL.md +2 -0
- package/adapters/kimi/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/kimi/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/kimi/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/kimi/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/kimi/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/kimi/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/kimi/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/kimi/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/adapters/workbuddy/.codebuddy-plugin/plugin.json +1 -1
- package/adapters/workbuddy/bin/consumer-contract-vectors.json +110 -0
- package/adapters/workbuddy/bin/error-codes.json +8 -1
- package/adapters/workbuddy/bin/foundation-resource-binding.json +1 -1
- package/adapters/workbuddy/bin/registry.json +22 -1
- package/adapters/workbuddy/bin/release-skill.bundle.mjs +4772 -2238
- package/adapters/workbuddy/bin/rules.json +1 -1
- package/adapters/workbuddy/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/adapters/workbuddy/schemas/release-project.schema.json +4 -0
- package/adapters/workbuddy/schemas/release-run.schema.json +41 -1
- package/adapters/workbuddy/skills/release-config/SKILL.md +5 -3
- package/adapters/workbuddy/skills/release-docs/SKILL.md +4 -2
- package/adapters/workbuddy/skills/release-finish/SKILL.md +6 -4
- package/adapters/workbuddy/skills/release-help/SKILL.md +14 -5
- package/adapters/workbuddy/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +4 -2
- package/adapters/workbuddy/skills/release-publish/SKILL.md +19 -10
- package/adapters/workbuddy/skills/release-verify/SKILL.md +2 -0
- package/adapters/workbuddy/src/schemas/executable-identity-observation.schema.json +162 -0
- package/adapters/workbuddy/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/adapters/workbuddy/src/schemas/migration-manifest.schema.json +271 -7
- package/adapters/workbuddy/src/schemas/plugin-verification-request.schema.json +147 -22
- package/adapters/workbuddy/src/schemas/plugin-verification-result.schema.json +436 -18
- package/adapters/workbuddy/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/adapters/workbuddy/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/adapters/workbuddy/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
- package/bin/consumer-contract-vectors.json +110 -0
- package/bin/error-codes.json +8 -1
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/registry.json +22 -1
- package/bin/release-skill-cli.mjs +72 -4
- package/bin/release-skill.bundle.mjs +4772 -2238
- package/bin/rules.json +1 -1
- package/bin/schemas/consumer-contract-vector.schema.json +149 -0
- package/package.json +5 -5
- package/platform-manifest.json +5 -5
- package/references/01-state-machine.md +16 -1
- package/references/05-evidence-and-errors.md +2 -2
- package/schemas/release-project.schema.json +4 -0
- package/schemas/release-run.schema.json +41 -1
- package/skills/release-config/SKILL.md +5 -3
- package/skills/release-docs/SKILL.md +4 -2
- package/skills/release-finish/SKILL.md +6 -4
- package/skills/release-help/SKILL.md +14 -5
- package/skills/release-marketplace/SKILL.md +3 -1
- package/skills/release-prepare/SKILL.md +4 -2
- package/skills/release-publish/SKILL.md +19 -10
- package/skills/release-verify/SKILL.md +2 -0
- package/skills-src/release-config/SKILL.md +5 -3
- package/skills-src/release-docs/SKILL.md +4 -2
- package/skills-src/release-finish/SKILL.md +6 -4
- package/skills-src/release-help/SKILL.md +14 -5
- package/skills-src/release-marketplace/SKILL.md +3 -1
- package/skills-src/release-prepare/SKILL.md +4 -2
- package/skills-src/release-publish/SKILL.md +19 -10
- package/skills-src/release-verify/SKILL.md +2 -0
- package/src/commands/post-release-local.mjs +249 -58
- package/src/commands/prepare.mjs +178 -9
- package/src/commands/route.mjs +597 -18
- package/src/commands/setup.mjs +2 -0
- package/src/commands/ship.mjs +143 -16
- package/src/commands/verify.mjs +21 -0
- package/src/core/adoption-assessment.mjs +59 -0
- package/src/core/derived-artifact-gates.mjs +32 -5
- package/src/core/foundation-plugin-verification.mjs +213 -1
- package/src/core/hook-cache.mjs +318 -26
- package/src/core/hooks.mjs +8 -1
- package/src/producers/foundation-resource-projection.mjs +16 -3
- package/src/schemas/executable-identity-observation.schema.json +162 -0
- package/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/src/schemas/migration-manifest.schema.json +271 -7
- package/src/schemas/plugin-verification-request.schema.json +147 -22
- package/src/schemas/plugin-verification-result.schema.json +436 -18
- package/src/schemas/profile-adoption-declaration.schema.json +8 -0
- package/src/schemas/skill-family-directory-verification-request.schema.json +100 -0
- package/src/schemas/skill-family-directory-verification-result.schema.json +193 -0
package/src/commands/setup.mjs
CHANGED
|
@@ -46,6 +46,7 @@ import {
|
|
|
46
46
|
deriveGateSuggestions,
|
|
47
47
|
deriveHookDurations,
|
|
48
48
|
deriveLongHookSuggestions,
|
|
49
|
+
derivePreHookPublicSurfaceFinding,
|
|
49
50
|
scanGateDeclarationFindings,
|
|
50
51
|
FINDING_CATEGORY,
|
|
51
52
|
ASSESSMENT_STATUS,
|
|
@@ -3209,6 +3210,7 @@ export async function assessAdoption({ root } = {}) {
|
|
|
3209
3210
|
currentProducerVersion: resolveProducerVersion(),
|
|
3210
3211
|
});
|
|
3211
3212
|
findings.push(...deriveLongHookSuggestions(hookDurations));
|
|
3213
|
+
findings.push(derivePreHookPublicSurfaceFinding({ config, hookDurations }));
|
|
3212
3214
|
findings.push(...deriveCheckOnlySuggestions(config.hooks));
|
|
3213
3215
|
|
|
3214
3216
|
// --- Gate suggestions (scenarios 5 & 6) ---
|
package/src/commands/ship.mjs
CHANGED
|
@@ -2,6 +2,7 @@ import { readFile, lstat, mkdir } from 'node:fs/promises';
|
|
|
2
2
|
import { basename, dirname, resolve } from 'node:path';
|
|
3
3
|
import { publishFileOrReplace } from 'skill-family-harness-node';
|
|
4
4
|
|
|
5
|
+
import { MAX_APPROVAL_MS } from '../core/approval.mjs';
|
|
5
6
|
import { canonicalJson, sha256Hex } from '../core/digest.mjs';
|
|
6
7
|
import {
|
|
7
8
|
effectiveHookRequiresApproval,
|
|
@@ -88,6 +89,7 @@ async function defaultDependencies() {
|
|
|
88
89
|
postVerifyRelease: postverifyModule.postVerifyRelease,
|
|
89
90
|
preflightGitTransports: transportModule.preflightGitTransports,
|
|
90
91
|
updatePreviousPublicBaselines: metadataModule.updatePreviousPublicBaselines,
|
|
92
|
+
readMaxApprovalMs: () => MAX_APPROVAL_MS,
|
|
91
93
|
};
|
|
92
94
|
}
|
|
93
95
|
|
|
@@ -116,34 +118,135 @@ function publicState(state) {
|
|
|
116
118
|
...(state.manualFollowUps ? { manualFollowUps: state.manualFollowUps } : {}),
|
|
117
119
|
...(state.metadataUpdate ? { metadataUpdate: state.metadataUpdate } : {}),
|
|
118
120
|
...(state.postRelease ? { postRelease: state.postRelease } : {}),
|
|
119
|
-
|
|
120
|
-
|
|
121
|
+
externalActionsIncludedInPlanApproval: true,
|
|
122
|
+
verificationGatesIncludedInPlanApproval: true,
|
|
123
|
+
postPublishCheckpointApprovalsIncludedInPlanApproval: false,
|
|
121
124
|
};
|
|
122
125
|
}
|
|
123
126
|
|
|
127
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
128
|
+
|
|
129
|
+
const ACTION_TARGET_FIELDS = Object.freeze({
|
|
130
|
+
'push-commit': Object.freeze(['remote', 'branch']),
|
|
131
|
+
'push-snapshot': Object.freeze(['repo', 'branch', 'branchStrategy', 'commit']),
|
|
132
|
+
'create-tag': Object.freeze(['repo', 'tag', 'commit']),
|
|
133
|
+
'npm-publish': Object.freeze(['package', 'version', 'registry', 'tag', 'access', 'provenance']),
|
|
134
|
+
'github-release': Object.freeze(['repo', 'tag', 'name', 'commit']),
|
|
135
|
+
'claude-marketplace-install': Object.freeze(['consumer', 'plugin', 'marketplace', 'repo', 'ref', 'version', 'entrySkill']),
|
|
136
|
+
'codex-marketplace-install': Object.freeze(['consumer', 'plugin', 'marketplace', 'repo', 'ref', 'version', 'entrySkill']),
|
|
137
|
+
'kimi-marketplace-install': Object.freeze(['consumer', 'plugin', 'repo', 'ref', 'version', 'entrySkill']),
|
|
138
|
+
'codebuddy-marketplace-install': Object.freeze(['consumer', 'plugin', 'repo', 'ref', 'version', 'entrySkill']),
|
|
139
|
+
'set-default-branch': Object.freeze(['repo', 'oldBranch', 'newBranch', 'expectedNewBranchCommit']),
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
function deriveApprovalWindowHours(maxApprovalMs) {
|
|
143
|
+
if (
|
|
144
|
+
!Number.isSafeInteger(maxApprovalMs)
|
|
145
|
+
|| maxApprovalMs <= 0
|
|
146
|
+
|| maxApprovalMs % HOUR_MS !== 0
|
|
147
|
+
) {
|
|
148
|
+
throw new ReleaseError(
|
|
149
|
+
GATE_FAILED,
|
|
150
|
+
'core approval window must be a positive whole number of hours',
|
|
151
|
+
{ maxApprovalMs },
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
return maxApprovalMs / HOUR_MS;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function projectActionTarget(action) {
|
|
158
|
+
const parameters = action?.parameters ?? {};
|
|
159
|
+
return Object.fromEntries(
|
|
160
|
+
(ACTION_TARGET_FIELDS[action?.type] ?? [])
|
|
161
|
+
.filter((field) => parameters[field] !== undefined)
|
|
162
|
+
.map((field) => [field, parameters[field]]),
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
124
166
|
/**
|
|
125
167
|
* Build a human-readable approval summary from the frozen plan.
|
|
126
|
-
* Lists
|
|
127
|
-
*
|
|
168
|
+
* Lists every publish target and every checkpoint approval separately. All
|
|
169
|
+
* fields are mechanically projected from the frozen plan; unreadable plans
|
|
170
|
+
* fail closed instead of producing an empty or partial summary.
|
|
128
171
|
*
|
|
129
172
|
* @param {string} planPath - Path to the frozen release plan.
|
|
130
173
|
* @returns {Promise<object>} The approval summary.
|
|
131
174
|
*/
|
|
132
|
-
async function buildApprovalSummary(planPath) {
|
|
175
|
+
async function buildApprovalSummary(planPath, maxApprovalMs = MAX_APPROVAL_MS) {
|
|
133
176
|
try {
|
|
134
177
|
const plan = JSON.parse(await readFile(planPath, 'utf8'));
|
|
135
|
-
const
|
|
136
|
-
id: unit.id,
|
|
137
|
-
targetVersion: unit.targetVersion ?? unit.version,
|
|
138
|
-
}));
|
|
178
|
+
const approvalWindowHours = deriveApprovalWindowHours(maxApprovalMs);
|
|
139
179
|
const actions = (plan.externalActions ?? []).map((action) => ({
|
|
140
180
|
id: action.id,
|
|
141
181
|
type: action.type,
|
|
142
182
|
unitId: action.unitId,
|
|
183
|
+
target: projectActionTarget(action),
|
|
143
184
|
}));
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
185
|
+
const units = (plan.units ?? []).map((unit) => {
|
|
186
|
+
const targetVersion = unit.targetVersion ?? unit.version;
|
|
187
|
+
const githubRelease = (plan.externalActions ?? [])
|
|
188
|
+
.find((action) => action.unitId === unit.id && action.type === 'github-release');
|
|
189
|
+
return {
|
|
190
|
+
id: unit.id,
|
|
191
|
+
targetVersion,
|
|
192
|
+
publicRepo: unit.publicRepo,
|
|
193
|
+
branch: unit.frozenSnapshot?.branch
|
|
194
|
+
?? unit.productionConfig?.branchTemplate
|
|
195
|
+
?.replaceAll('{tag}', unit.tagTemplate?.replace('{version}', targetVersion) ?? '')
|
|
196
|
+
.replaceAll('{version}', targetVersion)
|
|
197
|
+
.replaceAll('{unit}', unit.id),
|
|
198
|
+
branchStrategy: unit.frozenSnapshot?.branchStrategy ?? unit.productionConfig?.branchStrategy,
|
|
199
|
+
tag: unit.tagTemplate?.replace('{version}', targetVersion),
|
|
200
|
+
npm: (unit.distributions ?? [])
|
|
201
|
+
.filter((distribution) => distribution.type === 'npm')
|
|
202
|
+
.map((distribution) => ({
|
|
203
|
+
package: distribution.package,
|
|
204
|
+
version: targetVersion,
|
|
205
|
+
registry: distribution.registry,
|
|
206
|
+
distTag: distribution.tag,
|
|
207
|
+
access: distribution.access,
|
|
208
|
+
provenance: distribution.provenance,
|
|
209
|
+
})),
|
|
210
|
+
...(githubRelease ? {
|
|
211
|
+
githubRelease: {
|
|
212
|
+
repo: githubRelease.parameters?.repo ?? githubRelease.parameters?.publicRepo,
|
|
213
|
+
tag: githubRelease.parameters?.tag,
|
|
214
|
+
name: githubRelease.parameters?.name,
|
|
215
|
+
},
|
|
216
|
+
} : {}),
|
|
217
|
+
};
|
|
218
|
+
});
|
|
219
|
+
const postPublishCheckpointApprovals = normalizePostPublishView(plan)
|
|
220
|
+
.flatMap((declaration) => (declaration.hooks ?? [])
|
|
221
|
+
.filter((hook) => effectiveHookRequiresApproval(hook))
|
|
222
|
+
.map((hook) => ({
|
|
223
|
+
unitId: declaration.unitId,
|
|
224
|
+
hookId: hook.id,
|
|
225
|
+
actionId: postPublishActionId({
|
|
226
|
+
planVersion: plan.planVersion,
|
|
227
|
+
unitId: declaration.unitId,
|
|
228
|
+
localId: hook.id,
|
|
229
|
+
}),
|
|
230
|
+
phase: hook.phase ?? 'distribute',
|
|
231
|
+
...(hook.preset !== undefined ? { preset: hook.preset } : {}),
|
|
232
|
+
...(hook.config?.delivery !== undefined ? { delivery: hook.config.delivery } : {}),
|
|
233
|
+
requiresApproval: true,
|
|
234
|
+
includedInPlanApproval: false,
|
|
235
|
+
approvalWindowHours,
|
|
236
|
+
binding: { planDigest: plan.digest, hookId: hook.id },
|
|
237
|
+
})));
|
|
238
|
+
return {
|
|
239
|
+
units,
|
|
240
|
+
actions,
|
|
241
|
+
waivers: Array.isArray(plan.waivers) ? plan.waivers : [],
|
|
242
|
+
postPublishCheckpointApprovals,
|
|
243
|
+
};
|
|
244
|
+
} catch (error) {
|
|
245
|
+
throw new ReleaseError(
|
|
246
|
+
GATE_FAILED,
|
|
247
|
+
`cannot build ship approval summary from frozen plan: ${error.message}`,
|
|
248
|
+
{ planPath },
|
|
249
|
+
);
|
|
147
250
|
}
|
|
148
251
|
}
|
|
149
252
|
|
|
@@ -197,9 +300,11 @@ async function allUnclosedPostVerifyHooksUngated(state, postVerifyHooks, planVer
|
|
|
197
300
|
* resumes instead of reconstructing authority from terminal/chat output.
|
|
198
301
|
*
|
|
199
302
|
* New flow (v0.4+): ship directly runs configured hooks and verification gates
|
|
200
|
-
* without a separate hook authorization step.
|
|
201
|
-
*
|
|
202
|
-
*
|
|
303
|
+
* without a separate hook authorization step. Plan approval is the only
|
|
304
|
+
* normal release-level gate. A postPublish hook whose effective
|
|
305
|
+
* requiresApproval is true still needs its independent checkpoint approval.
|
|
306
|
+
* Kimi/CodeBuddy installations are non-blocking manual follow-up tasks when
|
|
307
|
+
* the plan declares humanConsumersStrategy: 'manualFollowUps'.
|
|
203
308
|
*/
|
|
204
309
|
export async function advanceShip(options = {}, injected = {}) {
|
|
205
310
|
const root = resolve(options.root ?? process.cwd());
|
|
@@ -210,6 +315,25 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
210
315
|
? injected
|
|
211
316
|
: await defaultDependencies();
|
|
212
317
|
let state = await readState(statePath);
|
|
318
|
+
const requestedTargetVersion = options.targetVersion ?? null;
|
|
319
|
+
if (state && requestedTargetVersion !== null) {
|
|
320
|
+
const stateTargetVersion = state.targetVersion ?? null;
|
|
321
|
+
if (stateTargetVersion === null || stateTargetVersion !== requestedTargetVersion) {
|
|
322
|
+
const suffix = stateTargetVersion === null
|
|
323
|
+
? 'state targetVersion is missing; provide a new --state path to start the requested version'
|
|
324
|
+
: `state targetVersion ${stateTargetVersion} does not match requested targetVersion ${requestedTargetVersion}`;
|
|
325
|
+
throw new ReleaseError(
|
|
326
|
+
GATE_FAILED,
|
|
327
|
+
`ship state target version mismatch: ${suffix}`,
|
|
328
|
+
{
|
|
329
|
+
reason: 'SHIP_STATE_TARGET_VERSION_MISMATCH',
|
|
330
|
+
statePath,
|
|
331
|
+
stateTargetVersion,
|
|
332
|
+
requestedTargetVersion,
|
|
333
|
+
},
|
|
334
|
+
);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
213
337
|
if (state?.gitTransport) {
|
|
214
338
|
process.env.RELEASE_SKILL_GIT_TRANSPORT = state.gitTransport;
|
|
215
339
|
}
|
|
@@ -268,7 +392,10 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
268
392
|
transportPreflight = await deps.preflightGitTransports(frozenPlan);
|
|
269
393
|
process.env.RELEASE_SKILL_GIT_TRANSPORT = transportPreflight.transport;
|
|
270
394
|
}
|
|
271
|
-
const
|
|
395
|
+
const maxApprovalMs = typeof deps.readMaxApprovalMs === 'function'
|
|
396
|
+
? await deps.readMaxApprovalMs()
|
|
397
|
+
: MAX_APPROVAL_MS;
|
|
398
|
+
const approvalSummary = await buildApprovalSummary(prepared.planPath, maxApprovalMs);
|
|
272
399
|
state = {
|
|
273
400
|
...state,
|
|
274
401
|
status: 'NEEDS_PLAN_APPROVAL',
|
package/src/commands/verify.mjs
CHANGED
|
@@ -1045,6 +1045,27 @@ export async function verifyRelease(options) {
|
|
|
1045
1045
|
requireDigest: Boolean(plan.production),
|
|
1046
1046
|
...(plan.production ? { authorityPlanPath: planPath } : {}),
|
|
1047
1047
|
});
|
|
1048
|
+
|
|
1049
|
+
// `distribute` runs are deliberately not valid verify sources. Report the
|
|
1050
|
+
// actionable lineage correction before the generic lineage validator's
|
|
1051
|
+
// unsupported-command error, while keeping the existing source authority
|
|
1052
|
+
// and automatic distribute discovery semantics unchanged.
|
|
1053
|
+
if (sourceRun.command === 'distribute') {
|
|
1054
|
+
throw new ReleaseError(
|
|
1055
|
+
GATE_FAILED,
|
|
1056
|
+
'verify --run must point to a PUBLISHED publish or reconcile run from the same plan; distribute runs are automatically discovered by verify and do not become the verify authority',
|
|
1057
|
+
{
|
|
1058
|
+
sourceRunCommand: sourceRun.command,
|
|
1059
|
+
sourceRunId: sourceRun.runId,
|
|
1060
|
+
sourceRunStatus: sourceRun.status,
|
|
1061
|
+
expectedSourceCommands: ['publish', 'reconcile'],
|
|
1062
|
+
requiredSourceStatus: 'PUBLISHED',
|
|
1063
|
+
distributeRunsAutoDiscovered: true,
|
|
1064
|
+
verifyAuthority: 'publish-or-reconcile-source-run',
|
|
1065
|
+
},
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
|
|
1048
1069
|
await validateRunLineage(sourceRun, {
|
|
1049
1070
|
plan,
|
|
1050
1071
|
planPath,
|
|
@@ -506,6 +506,65 @@ export function deriveLongHookSuggestions(hookDurations) {
|
|
|
506
506
|
return findings;
|
|
507
507
|
}
|
|
508
508
|
|
|
509
|
+
/**
|
|
510
|
+
* Report the opt-in R93-01 pre-hook public-surface check without changing the
|
|
511
|
+
* base adoption conclusion. A candidate is based only on hookDurations that
|
|
512
|
+
* already passed the current-producer, command/cwd-matching and real
|
|
513
|
+
* started/completed-pair checks above; timeout, cache hits and labels are not
|
|
514
|
+
* consulted here.
|
|
515
|
+
*
|
|
516
|
+
* @param {object} options
|
|
517
|
+
* @param {object} options.config - Loaded project configuration.
|
|
518
|
+
* @param {Array<object>} options.hookDurations - Trusted duration facts.
|
|
519
|
+
* @returns {object} One adoption finding for the policy field.
|
|
520
|
+
*/
|
|
521
|
+
export function derivePreHookPublicSurfaceFinding({ config, hookDurations }) {
|
|
522
|
+
const fieldPath = 'policy.preHookPublicSurfaceCheck';
|
|
523
|
+
if (config?.policy?.preHookPublicSurfaceCheck === true) {
|
|
524
|
+
return createFinding({
|
|
525
|
+
category: FINDING_CATEGORY.SATISFIED,
|
|
526
|
+
code: 'PRE_HOOK_PUBLIC_SURFACE_CHECK_ADOPTED',
|
|
527
|
+
fieldPath,
|
|
528
|
+
message: '已启用 Hook 前公开表面检查;prepare 会在首个 Hook 前验证候选公开表面,并在 Hook 后重新执行正式检查。',
|
|
529
|
+
evidence: { enabled: true },
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const candidate = (hookDurations ?? [])
|
|
534
|
+
.filter((duration) => (
|
|
535
|
+
duration?.basis === 'evidence'
|
|
536
|
+
&& Number.isFinite(duration.durationMs)
|
|
537
|
+
&& duration.durationMs >= LONG_HOOK_THRESHOLD_MS
|
|
538
|
+
&& duration.realRuns >= 1
|
|
539
|
+
))
|
|
540
|
+
.sort((a, b) => (
|
|
541
|
+
(b.durationMs - a.durationMs) || a.hookName.localeCompare(b.hookName)
|
|
542
|
+
))[0];
|
|
543
|
+
if (candidate) {
|
|
544
|
+
return createFinding({
|
|
545
|
+
category: FINDING_CATEGORY.OPTIONAL_SUGGESTION,
|
|
546
|
+
code: 'PRE_HOOK_PUBLIC_SURFACE_CHECK_CANDIDATE',
|
|
547
|
+
fieldPath,
|
|
548
|
+
message: `当前 hook "${candidate.hookName}" 有可信的实际运行证据(至少 30 分钟);可人工评估是否启用 Hook 前公开表面检查。`,
|
|
549
|
+
evidence: {
|
|
550
|
+
hookName: candidate.hookName,
|
|
551
|
+
durationMs: candidate.durationMs,
|
|
552
|
+
realRuns: candidate.realRuns,
|
|
553
|
+
lastObservedAt: candidate.lastObservedAt,
|
|
554
|
+
},
|
|
555
|
+
action: '确认公开文件不会由 Hook 生成或修复后,再在配置中将 policy.preHookPublicSurfaceCheck 设为 true;评估不会自动修改配置。',
|
|
556
|
+
severity: 'suggestion',
|
|
557
|
+
});
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
return createFinding({
|
|
561
|
+
category: FINDING_CATEGORY.NOT_APPLICABLE,
|
|
562
|
+
code: 'PRE_HOOK_PUBLIC_SURFACE_CHECK_NOT_APPLICABLE',
|
|
563
|
+
fieldPath,
|
|
564
|
+
message: '当前没有可信且匹配当前 Hook 的至少 30 分钟实际运行证据,暂不建议启用 Hook 前公开表面检查。',
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
509
568
|
const STATUS_LABEL = Object.freeze({
|
|
510
569
|
NOT_CONFIGURED: '尚未配置',
|
|
511
570
|
PARTIALLY_ADOPTED: '部分接入(存在必选缺口)',
|
|
@@ -5,17 +5,20 @@
|
|
|
5
5
|
* In the 0.6.1 cycle two prepares burned the full ~80s test hook before
|
|
6
6
|
* surfacing drift that a sub-second check could have caught: adapter trees
|
|
7
7
|
* out of sync with skills-src/, and self-bootstrap fact pins still bound to
|
|
8
|
-
* the previous version. This module promotes those
|
|
9
|
-
* prepare's earliest stage, alongside the bundle
|
|
8
|
+
* the previous version. This module promotes those checks and the platform
|
|
9
|
+
* manifest freshness check to prepare's earliest stage, alongside the bundle
|
|
10
|
+
* freshness gate:
|
|
10
11
|
*
|
|
11
12
|
* - adapter gate: runs `scripts/build-adapters.mjs --check` (drift list,
|
|
12
13
|
* exit 1 on drift — the same supported check the scripts surface offers);
|
|
14
|
+
* - platform manifest gate: runs
|
|
15
|
+
* `scripts/generate-platform-manifest.mjs --check`;
|
|
13
16
|
* - fact-pin gate: runs the version fact pins of
|
|
14
17
|
* `test/release-docs-self-bootstrap.test.mjs` — exactly its hermetic
|
|
15
18
|
* section 1 (`[self-bootstrap 1*]`: byte-level version assertions plus the
|
|
16
19
|
* in-process read-only planner), scoped via `--test-name-pattern`.
|
|
17
20
|
*
|
|
18
|
-
*
|
|
21
|
+
* All three promote the CANONICAL check logic — the gates and the full pipeline
|
|
19
22
|
* can never disagree about what "in sync" means. The facts gate is scoped to
|
|
20
23
|
* the suite's hermetic fact-pin section deliberately: the suite's remaining
|
|
21
24
|
* sections shell out to npm/git and drive fixture prepares, which would make
|
|
@@ -88,6 +91,15 @@ const GATES = Object.freeze({
|
|
|
88
91
|
'Rebuild the existing adapters with: node scripts/build-adapters.mjs --apply ' +
|
|
89
92
|
`(or run the one-click derived-artifact sync from the workspace root: ${DERIVED_SYNC_COMMAND}).`,
|
|
90
93
|
}),
|
|
94
|
+
'platform-manifest': Object.freeze({
|
|
95
|
+
artifact: 'platform-manifest',
|
|
96
|
+
marker: join('scripts', 'generate-platform-manifest.mjs'),
|
|
97
|
+
argv: (pkgRoot) => [join(pkgRoot, 'scripts', 'generate-platform-manifest.mjs'), '--check'],
|
|
98
|
+
timeoutMs: 120000,
|
|
99
|
+
remediation:
|
|
100
|
+
'Regenerate the platform manifest with: node scripts/generate-platform-manifest.mjs ' +
|
|
101
|
+
`(or run the one-click derived-artifact sync from the workspace root: ${DERIVED_SYNC_COMMAND}).`,
|
|
102
|
+
}),
|
|
91
103
|
'self-bootstrap-facts': Object.freeze({
|
|
92
104
|
artifact: 'self-bootstrap-facts',
|
|
93
105
|
marker: join('test', 'release-docs-self-bootstrap.test.mjs'),
|
|
@@ -118,7 +130,7 @@ async function isFile(path) {
|
|
|
118
130
|
/**
|
|
119
131
|
* Run one derived-artifact check (pure decision, never throws).
|
|
120
132
|
*
|
|
121
|
-
* @param {'adapters' | 'self-bootstrap-facts'} kind - Gate to run.
|
|
133
|
+
* @param {'adapters' | 'platform-manifest' | 'self-bootstrap-facts'} kind - Gate to run.
|
|
122
134
|
* @param {string} pkgRoot - Absolute package root of the running checkout.
|
|
123
135
|
* @param {object} [options]
|
|
124
136
|
* @param {Function} [options.execFileFn] - execFile seam (tests).
|
|
@@ -202,6 +214,11 @@ export function checkAdapterFreshness(pkgRoot, options = {}) {
|
|
|
202
214
|
return checkDerivedArtifactGate('adapters', pkgRoot, options);
|
|
203
215
|
}
|
|
204
216
|
|
|
217
|
+
/** Platform manifest freshness decision (generate-platform-manifest --check). */
|
|
218
|
+
export function checkPlatformManifestFreshness(pkgRoot, options = {}) {
|
|
219
|
+
return checkDerivedArtifactGate('platform-manifest', pkgRoot, options);
|
|
220
|
+
}
|
|
221
|
+
|
|
205
222
|
/** Self-bootstrap fact-pin decision (single-file test). */
|
|
206
223
|
export function checkSelfBootstrapFacts(pkgRoot, options = {}) {
|
|
207
224
|
return checkDerivedArtifactGate('self-bootstrap-facts', pkgRoot, options);
|
|
@@ -215,7 +232,9 @@ async function assertGate(kind, pkgRoot, options = {}) {
|
|
|
215
232
|
const gate = GATES[kind];
|
|
216
233
|
const subject = kind === 'adapters'
|
|
217
234
|
? 'adapters/ is out of sync with its sources (build-adapters --check reported drift)'
|
|
218
|
-
:
|
|
235
|
+
: kind === 'platform-manifest'
|
|
236
|
+
? 'platform-manifest.json is out of sync with the current package tree (generate-platform-manifest --check reported drift)'
|
|
237
|
+
: 'the release-docs-self-bootstrap fact pins are stale (the hermetic fact-pin check failed)';
|
|
219
238
|
throw new ReleaseError(
|
|
220
239
|
DERIVED_ARTIFACT_STALE,
|
|
221
240
|
`${subject}. ${DERIVED_ARTIFACT_PREGATE_NOTE} ${gate.remediation}`,
|
|
@@ -243,6 +262,14 @@ export function assertAdapterFreshness(pkgRoot, options = {}) {
|
|
|
243
262
|
return assertGate('adapters', pkgRoot, options);
|
|
244
263
|
}
|
|
245
264
|
|
|
265
|
+
/**
|
|
266
|
+
* Fail-closed platform manifest pre-gate used by prepare's earliest stage.
|
|
267
|
+
* Not-applicable layouts return quietly; drift throws DERIVED_ARTIFACT_STALE.
|
|
268
|
+
*/
|
|
269
|
+
export function assertPlatformManifestFreshness(pkgRoot, options = {}) {
|
|
270
|
+
return assertGate('platform-manifest', pkgRoot, options);
|
|
271
|
+
}
|
|
272
|
+
|
|
246
273
|
/**
|
|
247
274
|
* Fail-closed self-bootstrap fact-pin pre-gate used by prepare's earliest
|
|
248
275
|
* stage. Not-applicable layouts return quietly; drift throws
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Release-domain thin adapter for Foundation 0.
|
|
2
|
+
* Release-domain thin adapter for Foundation 0.15 complete-plugin observation.
|
|
3
3
|
*
|
|
4
4
|
* The frozen snapshot remains the only payload authority. This module derives
|
|
5
5
|
* transient Foundation inputs from that tree, calls the released Kit API, and
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
} from 'skill-family-engineering-kit';
|
|
20
20
|
import {
|
|
21
21
|
createFilesystemRootBinding,
|
|
22
|
+
digestBytes,
|
|
23
|
+
observeFilesystemTree,
|
|
22
24
|
readFileBound,
|
|
23
25
|
} from 'skill-family-harness-node';
|
|
24
26
|
|
|
@@ -30,6 +32,176 @@ import { ReleaseError, POST_PUBLISH_VERIFY_FAILED } from './errors.mjs';
|
|
|
30
32
|
// contract at 1.0.0. The real API integration test intentionally exercises
|
|
31
33
|
// this value so an upstream contract change fails closed during adoption.
|
|
32
34
|
const FOUNDATION_PLUGIN_DRIVER_VERSION = '1.0.0';
|
|
35
|
+
const SHA256_PATTERN = /^[0-9a-f]{64}$/u;
|
|
36
|
+
const BASE64_PATTERN = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
|
|
37
|
+
|
|
38
|
+
function isSafeRelativePath(value) {
|
|
39
|
+
return typeof value === 'string'
|
|
40
|
+
&& value.length > 0
|
|
41
|
+
&& !value.startsWith('/')
|
|
42
|
+
&& !value.includes('\\')
|
|
43
|
+
&& !value.includes(':')
|
|
44
|
+
&& !value.includes('\0')
|
|
45
|
+
&& value.split('/').every((part) => part.length > 0 && part !== '.' && part !== '..');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function decodeCanonicalBase64(value) {
|
|
49
|
+
if (typeof value !== 'string' || !BASE64_PATTERN.test(value)) return null;
|
|
50
|
+
const decoded = Buffer.from(value, 'base64');
|
|
51
|
+
return decoded.toString('base64') === value ? decoded : null;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function hasExactKeys(value, keys) {
|
|
55
|
+
return Object.keys(value).sort().join(',') === keys.join(',');
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function compareUnicodeCodePoints(left, right) {
|
|
59
|
+
const leftPoints = Array.from(left, (character) => character.codePointAt(0));
|
|
60
|
+
const rightPoints = Array.from(right, (character) => character.codePointAt(0));
|
|
61
|
+
const length = Math.min(leftPoints.length, rightPoints.length);
|
|
62
|
+
for (let index = 0; index < length; index += 1) {
|
|
63
|
+
if (leftPoints[index] !== rightPoints[index]) return leftPoints[index] - rightPoints[index];
|
|
64
|
+
}
|
|
65
|
+
return leftPoints.length - rightPoints.length;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function installCommandExited(result) {
|
|
69
|
+
const commands = result?.facts?.install?.commands;
|
|
70
|
+
if (!Array.isArray(commands)) return false;
|
|
71
|
+
return commands.every((command) => {
|
|
72
|
+
const execution = command?.execution;
|
|
73
|
+
return execution?.exitStatus === 0 && execution.processStatus === 'exited';
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function assertStableInstallTreeState(state) {
|
|
78
|
+
if (state === undefined) return;
|
|
79
|
+
if (!state || typeof state !== 'object' || Array.isArray(state)) {
|
|
80
|
+
throw fail('Foundation install tree state is invalid');
|
|
81
|
+
}
|
|
82
|
+
if (
|
|
83
|
+
state.commandExited === false
|
|
84
|
+
|| state.isolated === false
|
|
85
|
+
|| state.stable === false
|
|
86
|
+
|| state.namespaceWriterActive === true
|
|
87
|
+
) {
|
|
88
|
+
throw fail('Foundation install tree is not a stable isolated post-command tree', {
|
|
89
|
+
commandExited: state.commandExited ?? null,
|
|
90
|
+
isolated: state.isolated ?? null,
|
|
91
|
+
stable: state.stable ?? null,
|
|
92
|
+
namespaceWriterActive: state.namespaceWriterActive ?? null,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function compareRecordedInstallTree(observation, expectedMembers) {
|
|
98
|
+
if (
|
|
99
|
+
!observation
|
|
100
|
+
|| typeof observation !== 'object'
|
|
101
|
+
|| observation.schemaVersion !== 1
|
|
102
|
+
|| observation.kind !== 'skill-family.filesystem-tree-observation'
|
|
103
|
+
|| !/^[0-9a-f]{64}$/u.test(observation.membersDigest ?? '')
|
|
104
|
+
|| !Array.isArray(observation.members)
|
|
105
|
+
) {
|
|
106
|
+
throw fail('Foundation install tree observation has an invalid shape');
|
|
107
|
+
}
|
|
108
|
+
if (digestDocument(observation.members) !== observation.membersDigest) {
|
|
109
|
+
throw fail('Foundation install tree observation digest does not match its members');
|
|
110
|
+
}
|
|
111
|
+
const actual = new Map();
|
|
112
|
+
const foldedPaths = new Set();
|
|
113
|
+
let previousPath = null;
|
|
114
|
+
for (const member of observation.members) {
|
|
115
|
+
const foldedPath = member?.path?.toLowerCase();
|
|
116
|
+
if (
|
|
117
|
+
!member
|
|
118
|
+
|| !isSafeRelativePath(member.path)
|
|
119
|
+
|| !['directory', 'file', 'symlink'].includes(member.type)
|
|
120
|
+
|| actual.has(member.path)
|
|
121
|
+
|| foldedPaths.has(foldedPath)
|
|
122
|
+
|| previousPath !== null && compareUnicodeCodePoints(previousPath, member.path) >= 0
|
|
123
|
+
) {
|
|
124
|
+
throw fail('Foundation install tree observation contains duplicate or invalid paths');
|
|
125
|
+
}
|
|
126
|
+
actual.set(member.path, member);
|
|
127
|
+
foldedPaths.add(foldedPath);
|
|
128
|
+
const requiredKeys = member.type === 'directory'
|
|
129
|
+
? ['path', 'statMode', 'type']
|
|
130
|
+
: member.type === 'file'
|
|
131
|
+
? ['bytes', 'contentBase64', 'path', 'sha256', 'statMode', 'type']
|
|
132
|
+
: ['bytes', 'path', 'statMode', 'targetBase64', 'type'];
|
|
133
|
+
if (!hasExactKeys(member, requiredKeys)) {
|
|
134
|
+
throw fail('Foundation install tree member has unknown or missing fields', { path: member.path });
|
|
135
|
+
}
|
|
136
|
+
previousPath = member.path;
|
|
137
|
+
if (
|
|
138
|
+
typeof member.statMode !== 'number'
|
|
139
|
+
|| !Number.isInteger(member.statMode)
|
|
140
|
+
|| member.statMode < 0
|
|
141
|
+
|| member.statMode > 0xffff
|
|
142
|
+
) {
|
|
143
|
+
throw fail('Foundation install tree member mode is invalid', { path: member.path });
|
|
144
|
+
}
|
|
145
|
+
if (member.type === 'file') {
|
|
146
|
+
const content = decodeCanonicalBase64(member.contentBase64);
|
|
147
|
+
if (
|
|
148
|
+
!content
|
|
149
|
+
|| typeof member.sha256 !== 'string'
|
|
150
|
+
|| !SHA256_PATTERN.test(member.sha256)
|
|
151
|
+
|| !Number.isSafeInteger(member.bytes)
|
|
152
|
+
|| member.bytes < 0
|
|
153
|
+
|| member.bytes !== content.length
|
|
154
|
+
|| member.sha256 !== digestBytes(content)
|
|
155
|
+
) {
|
|
156
|
+
throw fail('Foundation install tree file record is invalid', { path: member.path });
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
if (member.type === 'symlink') {
|
|
160
|
+
const target = decodeCanonicalBase64(member.targetBase64);
|
|
161
|
+
if (
|
|
162
|
+
!target
|
|
163
|
+
|| target.length === 0
|
|
164
|
+
|| !Number.isSafeInteger(member.bytes)
|
|
165
|
+
|| member.bytes < 1
|
|
166
|
+
|| member.bytes !== target.length
|
|
167
|
+
) {
|
|
168
|
+
throw fail('Foundation install tree link record is invalid', { path: member.path });
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
const expected = new Map(expectedMembers.map((member) => [member.path, member]));
|
|
173
|
+
for (const [path, expectedMember] of expected) {
|
|
174
|
+
const member = actual.get(path);
|
|
175
|
+
if (!member || member.type === 'symlink' || member.type !== expectedMember.type) {
|
|
176
|
+
throw fail('declared install tree member is missing or unsafe', { path });
|
|
177
|
+
}
|
|
178
|
+
if (member.type === 'file' && (
|
|
179
|
+
member.sha256 !== expectedMember.sha256
|
|
180
|
+
|| member.bytes !== expectedMember.bytes
|
|
181
|
+
|| ((member.statMode & 0o111) !== 0) !== expectedMember.executable
|
|
182
|
+
)) {
|
|
183
|
+
throw fail('declared install tree member drifted from the frozen payload', { path });
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
const extras = [...actual.values()].filter((member) => !expected.has(member.path));
|
|
187
|
+
const extraInstalledPaths = extras
|
|
188
|
+
.map((member) => member.path)
|
|
189
|
+
.sort(compareUnicodeCodePoints);
|
|
190
|
+
const extraInstalledLinks = extras
|
|
191
|
+
.filter((member) => member.type === 'symlink')
|
|
192
|
+
.map((member) => {
|
|
193
|
+
// Keep the target bytes supplied by Foundation. No path resolution or
|
|
194
|
+
// target read occurs here, so an added link can never be followed.
|
|
195
|
+
return {
|
|
196
|
+
path: member.path,
|
|
197
|
+
targetBase64: member.targetBase64,
|
|
198
|
+
bytes: member.bytes,
|
|
199
|
+
statMode: member.statMode,
|
|
200
|
+
};
|
|
201
|
+
})
|
|
202
|
+
.sort((left, right) => compareUnicodeCodePoints(left.path, right.path));
|
|
203
|
+
return { extraInstalledPaths, extraInstalledLinks };
|
|
204
|
+
}
|
|
33
205
|
|
|
34
206
|
function fail(message, details = {}) {
|
|
35
207
|
return new ReleaseError(POST_PUBLISH_VERIFY_FAILED, message, details);
|
|
@@ -135,6 +307,8 @@ export async function verifyFrozenPluginWithFoundation({
|
|
|
135
307
|
root,
|
|
136
308
|
runDir,
|
|
137
309
|
runPluginVerificationFn = runPluginVerification,
|
|
310
|
+
observeFilesystemTreeFn = observeFilesystemTree,
|
|
311
|
+
installTreeState,
|
|
138
312
|
clock = () => new Date().toISOString(),
|
|
139
313
|
} = {}) {
|
|
140
314
|
const unit = (plan?.units ?? []).find((candidate) => candidate.id === action?.unitId);
|
|
@@ -231,6 +405,38 @@ export async function verifyFrozenPluginWithFoundation({
|
|
|
231
405
|
const result = await runPluginVerificationFn({ request, bindings, hostsRoot });
|
|
232
406
|
assertResultMatchesRequest(result, request);
|
|
233
407
|
|
|
408
|
+
// A3: the installation command (if Foundation reports one) must have
|
|
409
|
+
// exited before the isolated tree is observed. The install-only Foundation
|
|
410
|
+
// path reports no commands, which is the completed local materialization.
|
|
411
|
+
if (!installCommandExited(result)) {
|
|
412
|
+
throw fail('Foundation install tree observation requires an exited install command');
|
|
413
|
+
}
|
|
414
|
+
assertStableInstallTreeState(installTreeState);
|
|
415
|
+
|
|
416
|
+
// Foundation's local install-only contract materializes the payload at the
|
|
417
|
+
// stable child of the isolated install container. Observe that payload root
|
|
418
|
+
// rather than the container, whose private siblings are not plugin bytes.
|
|
419
|
+
const installRoot = join(runDir, 'install', 'payload');
|
|
420
|
+
let treeObservation;
|
|
421
|
+
try {
|
|
422
|
+
const rootBinding = await createFilesystemRootBinding(installRoot);
|
|
423
|
+
treeObservation = await observeFilesystemTreeFn({
|
|
424
|
+
root: installRoot,
|
|
425
|
+
rootBinding,
|
|
426
|
+
symlinkPolicy: { mode: 'record' },
|
|
427
|
+
});
|
|
428
|
+
if (treeObservation?.rootBinding?.digest !== rootBinding.digest) {
|
|
429
|
+
throw fail('Foundation install tree observation root binding drifted');
|
|
430
|
+
}
|
|
431
|
+
} catch (error) {
|
|
432
|
+
if (error instanceof ReleaseError) throw error;
|
|
433
|
+
throw fail('Foundation install tree observation failed closed', {
|
|
434
|
+
cause: error?.message ?? String(error),
|
|
435
|
+
code: error?.code ?? null,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
const extraInstallAudit = compareRecordedInstallTree(treeObservation, sourceMembers);
|
|
439
|
+
|
|
234
440
|
return {
|
|
235
441
|
planDigest: plan.digest,
|
|
236
442
|
actionId: action.id,
|
|
@@ -243,5 +449,11 @@ export async function verifyFrozenPluginWithFoundation({
|
|
|
243
449
|
payloadMatches: result.facts.install.payloadMatches,
|
|
244
450
|
observationDigest: result.facts.install.observationDigest,
|
|
245
451
|
observedAt: clock(),
|
|
452
|
+
...(extraInstallAudit.extraInstalledPaths.length > 0
|
|
453
|
+
? { extraInstalledPaths: extraInstallAudit.extraInstalledPaths }
|
|
454
|
+
: {}),
|
|
455
|
+
...(extraInstallAudit.extraInstalledLinks.length > 0
|
|
456
|
+
? { extraInstalledLinks: extraInstallAudit.extraInstalledLinks }
|
|
457
|
+
: {}),
|
|
246
458
|
};
|
|
247
459
|
}
|