release-skill 0.9.1 → 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 +49 -0
- package/INSTALL.md +2 -2
- package/INSTALL.zh-CN.md +2 -2
- package/README.md +24 -26
- package/README.zh-CN.md +20 -23
- 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 +7 -7
- package/adapters/claude/bin/error-codes.json +1 -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 +4068 -1962
- package/adapters/claude/bin/rules.json +1 -1
- 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 +2 -0
- package/adapters/claude/skills/release-help/SKILL.md +11 -3
- package/adapters/claude/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/claude/skills/release-prepare/SKILL.md +2 -0
- package/adapters/claude/skills/release-publish/SKILL.md +17 -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/plugin-verification-request.schema.json +147 -22
- package/adapters/claude/src/schemas/plugin-verification-result.schema.json +436 -18
- 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 +7 -7
- package/adapters/codex/bin/error-codes.json +1 -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 +4068 -1962
- package/adapters/codex/bin/rules.json +1 -1
- 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 +2 -0
- package/adapters/codex/skills/release-help/SKILL.md +11 -3
- package/adapters/codex/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/codex/skills/release-prepare/SKILL.md +2 -0
- package/adapters/codex/skills/release-publish/SKILL.md +17 -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/plugin-verification-request.schema.json +147 -22
- package/adapters/codex/src/schemas/plugin-verification-result.schema.json +436 -18
- 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 +7 -7
- package/adapters/kimi/bin/error-codes.json +1 -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 +4068 -1962
- package/adapters/kimi/bin/rules.json +1 -1
- 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 +2 -0
- package/adapters/kimi/skills/release-help/SKILL.md +11 -3
- package/adapters/kimi/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/kimi/skills/release-prepare/SKILL.md +2 -0
- package/adapters/kimi/skills/release-publish/SKILL.md +17 -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/plugin-verification-request.schema.json +147 -22
- package/adapters/kimi/src/schemas/plugin-verification-result.schema.json +436 -18
- 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 +7 -7
- package/adapters/workbuddy/bin/error-codes.json +1 -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 +4068 -1962
- package/adapters/workbuddy/bin/rules.json +1 -1
- 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 +2 -0
- package/adapters/workbuddy/skills/release-help/SKILL.md +11 -3
- package/adapters/workbuddy/skills/release-marketplace/SKILL.md +3 -1
- package/adapters/workbuddy/skills/release-prepare/SKILL.md +2 -0
- package/adapters/workbuddy/skills/release-publish/SKILL.md +17 -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/plugin-verification-request.schema.json +147 -22
- package/adapters/workbuddy/src/schemas/plugin-verification-result.schema.json +436 -18
- 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 +7 -7
- package/bin/error-codes.json +1 -1
- package/bin/foundation-resource-binding.json +1 -1
- package/bin/registry.json +22 -1
- package/bin/release-skill-cli.mjs +32 -0
- package/bin/release-skill.bundle.mjs +4068 -1962
- package/bin/rules.json +1 -1
- package/package.json +5 -5
- package/platform-manifest.json +4 -4
- 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 +2 -0
- package/skills/release-help/SKILL.md +11 -3
- package/skills/release-marketplace/SKILL.md +3 -1
- package/skills/release-prepare/SKILL.md +2 -0
- package/skills/release-publish/SKILL.md +17 -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 +2 -0
- package/skills-src/release-help/SKILL.md +11 -3
- package/skills-src/release-marketplace/SKILL.md +3 -1
- package/skills-src/release-prepare/SKILL.md +2 -0
- package/skills-src/release-publish/SKILL.md +17 -10
- package/skills-src/release-verify/SKILL.md +2 -0
- package/src/commands/prepare.mjs +139 -5
- package/src/commands/route.mjs +597 -18
- package/src/commands/setup.mjs +2 -0
- package/src/commands/ship.mjs +19 -0
- package/src/commands/verify.mjs +21 -0
- package/src/core/adoption-assessment.mjs +59 -0
- 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 +3 -3
- package/src/schemas/executable-identity-observation.schema.json +162 -0
- package/src/schemas/filesystem-tree-observation.schema.json +18 -2
- package/src/schemas/plugin-verification-request.schema.json +147 -22
- package/src/schemas/plugin-verification-result.schema.json +436 -18
- 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
|
@@ -315,6 +315,25 @@ export async function advanceShip(options = {}, injected = {}) {
|
|
|
315
315
|
? injected
|
|
316
316
|
: await defaultDependencies();
|
|
317
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
|
+
}
|
|
318
337
|
if (state?.gitTransport) {
|
|
319
338
|
process.env.RELEASE_SKILL_GIT_TRANSPORT = state.gitTransport;
|
|
320
339
|
}
|
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: '部分接入(存在必选缺口)',
|
|
@@ -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
|
}
|