release-skill 0.1.10 → 0.2.0
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/.codex-plugin/plugin.json +2 -2
- package/.kimi-plugin/plugin.json +1 -1
- package/CHANGELOG.md +19 -0
- package/INSTALL.md +4 -4
- package/INSTALL.zh-CN.md +4 -4
- package/README.md +17 -32
- package/README.zh-CN.md +17 -25
- 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 +2721 -1843
- package/adapters/claude/schemas/.render-manifest.json +8 -8
- package/adapters/claude/schemas/approval-record.schema.json +1 -1
- package/adapters/claude/schemas/release-plan.schema.json +6 -2
- package/adapters/claude/schemas/release-project.schema.json +14 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +2721 -1843
- package/adapters/codex/schemas/.render-manifest.json +8 -8
- package/adapters/codex/schemas/approval-record.schema.json +1 -1
- package/adapters/codex/schemas/release-plan.schema.json +6 -2
- package/adapters/codex/schemas/release-project.schema.json +14 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +1 -1
- package/adapters/kimi/bin/release-skill.bundle.mjs +2721 -1843
- package/adapters/kimi/schemas/.render-manifest.json +8 -8
- package/adapters/kimi/schemas/approval-record.schema.json +1 -1
- package/adapters/kimi/schemas/release-plan.schema.json +6 -2
- package/adapters/kimi/schemas/release-project.schema.json +14 -0
- package/bin/release-skill-cli.mjs +3 -0
- package/bin/release-skill.bundle.mjs +2721 -1843
- package/package.json +8 -2
- package/references/.render-manifest.json +8 -8
- package/references/01-state-machine.md +5 -5
- package/references/02-project-config.md +1 -1
- package/references/05-evidence-and-errors.md +1 -1
- package/references/06-adapter-contract.md +41 -1
- package/schemas/.render-manifest.json +8 -8
- package/schemas/approval-record.schema.json +1 -1
- package/schemas/release-plan.schema.json +6 -2
- package/schemas/release-project.schema.json +14 -0
- package/scripts/sync-public-files.mjs +462 -0
- package/src/adapters/contract.mjs +60 -0
- package/src/adapters/plugin-marketplace.mjs +289 -736
- package/src/commands/prepare.mjs +195 -182
- package/src/commands/publish.mjs +438 -122
- package/src/commands/reconcile.mjs +369 -191
- package/src/commands/verify.mjs +13 -2
- package/src/core/approval.mjs +72 -45
- package/src/core/baseline.mjs +5 -0
- package/src/core/checkpoints.mjs +143 -0
- package/src/core/evidence.mjs +30 -3
- package/src/core/hook-cache.mjs +254 -0
- package/src/core/hooks.mjs +37 -1
- package/src/core/observe-retry.mjs +223 -0
- package/src/core/plan.mjs +162 -253
- package/src/platforms/kimi.mjs +514 -0
- package/src/platforms/registry.mjs +393 -0
- package/src/producers/build-adapters.mjs +14 -22
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
|
|
15
15
|
import { execFile as execFileCb } from 'node:child_process';
|
|
16
16
|
import { promisify } from 'node:util';
|
|
17
|
-
import { readFile, stat, mkdir,
|
|
17
|
+
import { readFile, stat, mkdir, readdir, realpath, lstat } from 'node:fs/promises';
|
|
18
18
|
import { join, resolve, relative, isAbsolute, basename } from 'node:path';
|
|
19
19
|
|
|
20
20
|
import {
|
|
@@ -24,12 +24,23 @@ import {
|
|
|
24
24
|
assertWritesAuthorized,
|
|
25
25
|
assertIsolatedConsumerWritesAuthorized,
|
|
26
26
|
matchObservation,
|
|
27
|
+
resolveTimeoutMs,
|
|
28
|
+
SAFE_ID_RE,
|
|
29
|
+
writeEvidenceAtomic,
|
|
27
30
|
} from './contract.mjs';
|
|
28
31
|
|
|
29
32
|
import { createHash } from 'node:crypto';
|
|
30
33
|
import { computeFrozenSnapshot, resolveFrozenPath } from '../snapshot/frozen.mjs';
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
34
|
+
import { PLATFORMS, getPlatform } from '../platforms/registry.mjs';
|
|
35
|
+
import {
|
|
36
|
+
KIMI_REQUIREMENT_FILE,
|
|
37
|
+
KIMI_ATTESTATION_FILE,
|
|
38
|
+
KIMI_MANAGED_SUBPATH,
|
|
39
|
+
resolveBoundPlanDigest,
|
|
40
|
+
kimiAuthorityDir,
|
|
41
|
+
validateKimiAttestation,
|
|
42
|
+
readKimiManifest,
|
|
43
|
+
} from '../platforms/kimi.mjs';
|
|
33
44
|
|
|
34
45
|
const execFile = promisify(execFileCb);
|
|
35
46
|
|
|
@@ -48,22 +59,26 @@ function transportPayload(entries) {
|
|
|
48
59
|
}));
|
|
49
60
|
}
|
|
50
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Payload verification contract marker written into marketplace install
|
|
64
|
+
* action parameters by prepare. Plans declaring this contract verify the
|
|
65
|
+
* installed payload by declared-manifest containment (every authority file
|
|
66
|
+
* present and byte-identical; host-added files recorded, not failed).
|
|
67
|
+
* Actions without the marker keep the legacy full-tree equality semantics.
|
|
68
|
+
*/
|
|
69
|
+
const PAYLOAD_CONTRACT_DECLARED_MANIFEST = 'declared-manifest-v1';
|
|
70
|
+
/** Audit cap: at most this many extra installed paths are recorded. */
|
|
71
|
+
const EXTRA_INSTALLED_PATHS_CAP = 200;
|
|
72
|
+
/** Diagnostic cap: at most this many conflict paths are listed per error. */
|
|
73
|
+
const PAYLOAD_CONFLICT_REPORT_CAP = 10;
|
|
74
|
+
|
|
51
75
|
// Consumer-owned transport metadata written into the plugin install root
|
|
52
|
-
// that is not part of the published payload.
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
// the exact CLI-generated subtree. All other payload paths keep the
|
|
59
|
-
// fail-closed file checks — the exemption must never widen to
|
|
60
|
-
// ".codex-plugin/*" or arbitrary extra files.
|
|
61
|
-
function consumerTransportExclusions(consumer) {
|
|
62
|
-
if (consumer === 'claude') return ['.in_use'];
|
|
63
|
-
if (consumer === 'codex') return ['.git', '.codex-plugin/migrated-command-skills'];
|
|
64
|
-
if (consumer === 'kimi') return ['.git'];
|
|
65
|
-
return [];
|
|
66
|
-
}
|
|
76
|
+
// that is not part of the published payload (e.g. codex's root `.git`
|
|
77
|
+
// checkout and `.codex-plugin/migrated-command-skills/`, claude's `.in_use`
|
|
78
|
+
// marker) lives in each platform's `knownHostArtifacts` registry data. Only
|
|
79
|
+
// the legacy payload path (frozen plans without a `payloadContract` marker)
|
|
80
|
+
// applies that list; declared-manifest-v1 verification never excludes
|
|
81
|
+
// anything — host-added files are recorded as `extraInstalledPaths` instead.
|
|
67
82
|
|
|
68
83
|
/**
|
|
69
84
|
* Extract the marketplace plugin entry's declared source as a validated,
|
|
@@ -76,11 +91,16 @@ function consumerTransportExclusions(consumer) {
|
|
|
76
91
|
* slashes. Throws with the preflight's exact error messages.
|
|
77
92
|
*/
|
|
78
93
|
function extractDeclaredPluginSource(consumer, entry) {
|
|
79
|
-
const
|
|
94
|
+
const platform = getPlatform(consumer);
|
|
95
|
+
// Source form is registry data: claude declares a plain string, codex an
|
|
96
|
+
// {source:"local",path} object; any other form has no raw source.
|
|
97
|
+
const rawSource = platform.marketplaceSourceForm === 'string'
|
|
80
98
|
? entry.source
|
|
81
|
-
:
|
|
99
|
+
: platform.marketplaceSourceForm === 'local-path-object'
|
|
100
|
+
? (entry.source?.source === 'local' ? entry.source?.path : null)
|
|
101
|
+
: null;
|
|
82
102
|
if (typeof rawSource !== 'string' || rawSource.length === 0) {
|
|
83
|
-
throw new Error(`marketplace plugin entry source must be a non-empty relative path${
|
|
103
|
+
throw new Error(`marketplace plugin entry source must be a non-empty relative path${platform.marketplaceSourceForm === 'local-path-object' ? ' (object with source:"local")' : ''}, got ${JSON.stringify(entry.source)}`);
|
|
84
104
|
}
|
|
85
105
|
if (
|
|
86
106
|
rawSource.startsWith('/') ||
|
|
@@ -112,10 +132,10 @@ function extractDeclaredPluginSource(consumer, entry) {
|
|
|
112
132
|
* tampering with it fails the snapshot digest revalidation first.
|
|
113
133
|
*/
|
|
114
134
|
async function resolveInstalledPayloadSubpath(snapshotDir, sourceEntries, action, consumer) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
// Platforms without a marketplace manifest (kimi) install the whole
|
|
136
|
+
// snapshot as the payload.
|
|
137
|
+
const marketplaceRelative = getPlatform(consumer).manifestPaths.marketplace;
|
|
138
|
+
if (marketplaceRelative === null) return '.';
|
|
119
139
|
// Anchor the manifest read to the digest-verified entry walk: the target
|
|
120
140
|
// must be one of the regular files that already passed the fail-closed
|
|
121
141
|
// read checks (O_NOFOLLOW, single link, before/after stat stability).
|
|
@@ -174,8 +194,72 @@ async function verifyInstalledMarketplacePayload(action, context, installPath, c
|
|
|
174
194
|
if (authorityEntries.length === 0) {
|
|
175
195
|
throw new Error('frozen snapshot contains no payload under the declared marketplace source');
|
|
176
196
|
}
|
|
197
|
+
// Contract selection is bound by the frozen plan: prepare writes
|
|
198
|
+
// `payloadContract` into marketplace install action parameters; frozen
|
|
199
|
+
// plans without the marker keep the legacy full-tree equality semantics
|
|
200
|
+
// byte-for-byte (including the consumer transport exclusion list).
|
|
201
|
+
const payloadContract = action.payloadContract;
|
|
202
|
+
if (payloadContract !== undefined && payloadContract !== PAYLOAD_CONTRACT_DECLARED_MANIFEST) {
|
|
203
|
+
throw new Error(`unsupported marketplace payload contract: ${JSON.stringify(payloadContract)}`);
|
|
204
|
+
}
|
|
205
|
+
if (payloadContract === PAYLOAD_CONTRACT_DECLARED_MANIFEST) {
|
|
206
|
+
// declared-manifest-v1: every authority entry must exist in the installed
|
|
207
|
+
// payload and agree in type/size/bytes/non-write mode bits. Host-added
|
|
208
|
+
// files are NOT failures — they are recorded (relative paths, capped) as
|
|
209
|
+
// audit evidence so host evolution can never break a release, while any
|
|
210
|
+
// missing or altered declared file still fails closed.
|
|
211
|
+
const installedSnapshot = await computeFrozenSnapshot(installPath);
|
|
212
|
+
const authorityPayload = transportPayload(authorityEntries);
|
|
213
|
+
const installedByPath = new Map(
|
|
214
|
+
transportPayload(installedSnapshot.entries).map((entry) => [entry.path, entry]),
|
|
215
|
+
);
|
|
216
|
+
const conflicts = [];
|
|
217
|
+
for (const authorityEntry of authorityPayload) {
|
|
218
|
+
const installedEntry = installedByPath.get(authorityEntry.path);
|
|
219
|
+
if (!installedEntry) {
|
|
220
|
+
conflicts.push(`missing: ${authorityEntry.path}`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
if (installedEntry.type !== authorityEntry.type) {
|
|
224
|
+
conflicts.push(`type mismatch: ${authorityEntry.path}`);
|
|
225
|
+
} else if (
|
|
226
|
+
installedEntry.size !== authorityEntry.size ||
|
|
227
|
+
installedEntry.contentDigest !== authorityEntry.contentDigest
|
|
228
|
+
) {
|
|
229
|
+
conflicts.push(`content mismatch: ${authorityEntry.path}`);
|
|
230
|
+
} else if (installedEntry.mode !== authorityEntry.mode) {
|
|
231
|
+
conflicts.push(`mode mismatch: ${authorityEntry.path}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
if (conflicts.length > 0) {
|
|
235
|
+
const listed = conflicts.slice(0, PAYLOAD_CONFLICT_REPORT_CAP).join('; ');
|
|
236
|
+
const overflow = conflicts.length > PAYLOAD_CONFLICT_REPORT_CAP
|
|
237
|
+
? `; and ${conflicts.length - PAYLOAD_CONFLICT_REPORT_CAP} more conflicting path(s)`
|
|
238
|
+
: '';
|
|
239
|
+
throw new Error(
|
|
240
|
+
`installed marketplace payload differs in path, bytes, size, or non-write mode bits (${PAYLOAD_CONTRACT_DECLARED_MANIFEST}): ${listed}${overflow}`,
|
|
241
|
+
);
|
|
242
|
+
}
|
|
243
|
+
const authorityPaths = new Set(authorityPayload.map((entry) => entry.path));
|
|
244
|
+
const extraPaths = installedSnapshot.entries
|
|
245
|
+
.map((entry) => entry.path)
|
|
246
|
+
.filter((path) => !authorityPaths.has(path));
|
|
247
|
+
const extraInstalledPaths = extraPaths.slice(0, EXTRA_INSTALLED_PATHS_CAP);
|
|
248
|
+
// This is not an expected-value backfill: the sealed authority digest was
|
|
249
|
+
// revalidated above and every declared file was independently compared.
|
|
250
|
+
return {
|
|
251
|
+
manifestDigest: action.manifestDigest,
|
|
252
|
+
extraInstalledPaths,
|
|
253
|
+
...(extraPaths.length > EXTRA_INSTALLED_PATHS_CAP
|
|
254
|
+
? { extraInstalledPathsTotal: extraPaths.length }
|
|
255
|
+
: {}),
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
// Legacy contract (frozen plans without payloadContract): full-tree
|
|
259
|
+
// equality against the consumer-exclusion-filtered install tree. Behavior
|
|
260
|
+
// is frozen byte-for-byte; do not change.
|
|
177
261
|
const installedSnapshot = await computeFrozenSnapshot(installPath, {
|
|
178
|
-
excludeRootEntries:
|
|
262
|
+
excludeRootEntries: getPlatform(consumer).knownHostArtifacts,
|
|
179
263
|
});
|
|
180
264
|
if (
|
|
181
265
|
JSON.stringify(transportPayload(authorityEntries))
|
|
@@ -183,222 +267,29 @@ async function verifyInstalledMarketplacePayload(action, context, installPath, c
|
|
|
183
267
|
) {
|
|
184
268
|
throw new Error('installed marketplace payload differs in path, bytes, size, or non-write mode bits');
|
|
185
269
|
}
|
|
186
|
-
|
|
187
|
-
// revalidated above and the installed payload was independently compared.
|
|
188
|
-
return action.manifestDigest;
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
async function writeEvidenceAtomic(filePath, value) {
|
|
192
|
-
const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
|
|
193
|
-
try {
|
|
194
|
-
await writeFile(tempPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: 'utf8', mode: 0o600, flag: 'wx' });
|
|
195
|
-
await rename(tempPath, filePath);
|
|
196
|
-
} catch (err) {
|
|
197
|
-
await rm(tempPath, { force: true }).catch(() => {});
|
|
198
|
-
throw err;
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
// ---------------------------------------------------------------------------
|
|
203
|
-
// Kimi Code protocol-gap modeling (BLOCKER-1 / MAJOR-1 / MAJOR-4 / MINOR-1).
|
|
204
|
-
//
|
|
205
|
-
// Kimi Code has NO scriptable plugin install/list CLI: plugin management is
|
|
206
|
-
// interactive-only (`/plugins install <path-or-url>` in the TUI). There is no
|
|
207
|
-
// `kimi plugins ...` subcommand and no `--json` output protocol. Therefore the
|
|
208
|
-
// kimi-marketplace-install action is modeled as a protocol capability gap:
|
|
209
|
-
//
|
|
210
|
-
// - execute NEVER execs a kimi CLI. It emits an actionable, version-pinned
|
|
211
|
-
// manual-install requirement bound to the frozen plan digest + identity.
|
|
212
|
-
// - observe consumes a structured human attestation (written after the
|
|
213
|
-
// operator runs the interactive install) plus read-only verification of
|
|
214
|
-
// the installed managed copy. Missing/expired/mismatched/escaping proof
|
|
215
|
-
// fails closed, so a kimi unit can never reach VERIFIED without it.
|
|
216
|
-
// ---------------------------------------------------------------------------
|
|
217
|
-
|
|
218
|
-
/** Structured manual-install requirement written by kimi execute. */
|
|
219
|
-
const KIMI_REQUIREMENT_FILE = 'release-skill-kimi-manual-install.json';
|
|
220
|
-
/** Structured human attestation consumed by kimi observe. */
|
|
221
|
-
const KIMI_ATTESTATION_FILE = 'release-skill-kimi-attestation.json';
|
|
222
|
-
/** Kimi Code managed install layout: $KIMI_CODE_HOME/plugins/managed/<id>/. */
|
|
223
|
-
const KIMI_MANAGED_SUBPATH = join('plugins', 'managed');
|
|
224
|
-
/** Maximum attestation validity window (mirrors the 24h approval expiry). */
|
|
225
|
-
const KIMI_MAX_ATTESTATION_VALIDITY_MS = 24 * 60 * 60 * 1000;
|
|
226
|
-
|
|
227
|
-
/** 64-char lowercase hex plan/payload digest pattern. */
|
|
228
|
-
const HEX_DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Normalize a plan back to its frozen form for digest comparison.
|
|
232
|
-
*
|
|
233
|
-
* Only lifecycle status fields are reset: the top-level `status` returns to
|
|
234
|
-
* "PREPARED" and every `externalActions[].status` returns to "PENDING". Every
|
|
235
|
-
* other field is preserved verbatim. publish/reconcile/verify mutate exactly
|
|
236
|
-
* these status fields in memory as the saga progresses, so normalizing them
|
|
237
|
-
* recovers the frozen digest while leaving all security-relevant fields
|
|
238
|
-
* (baseline, units, action parameters/expected, production config, …) intact.
|
|
239
|
-
*
|
|
240
|
-
* @param {object} plan
|
|
241
|
-
* @returns {object} the lifecycle-normalized plan
|
|
242
|
-
*/
|
|
243
|
-
function normalizePlanForDigest(plan) {
|
|
244
|
-
const normalized = { ...plan, status: 'PREPARED' };
|
|
245
|
-
if (Array.isArray(plan.externalActions)) {
|
|
246
|
-
normalized.externalActions = plan.externalActions.map((action) => (
|
|
247
|
-
action && typeof action === 'object' && !Array.isArray(action)
|
|
248
|
-
? { ...action, status: 'PENDING' }
|
|
249
|
-
: action
|
|
250
|
-
));
|
|
251
|
-
}
|
|
252
|
-
return normalized;
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Resolve and verify the genuine frozen plan digest from the adapter context.
|
|
257
|
-
*
|
|
258
|
-
* The kimi manual-install requirement and attestation bind to the REAL frozen
|
|
259
|
-
* plan digest (`context.plan.digest`) — never to `action.manifestDigest`, which
|
|
260
|
-
* is only the snapshot payload digest.
|
|
261
|
-
*
|
|
262
|
-
* Integrity model: the carried `context.plan.digest` is recomputed from the
|
|
263
|
-
* lifecycle-normalized plan and must match EXACTLY. Status transitions
|
|
264
|
-
* (top-level status, per-action checkpoint status) are normalized away, but any
|
|
265
|
-
* other field tamper changes the recomputed digest and fails closed. This
|
|
266
|
-
* proves the attestation is bound to the genuine frozen plan, not to a spoofed
|
|
267
|
-
* or mutated stand-in.
|
|
268
|
-
*
|
|
269
|
-
* @param {object} context - adapter context (must carry the frozen `plan`).
|
|
270
|
-
* @returns {string} the verified frozen plan digest.
|
|
271
|
-
* @throws {Error} when the plan is absent or the digest does not match.
|
|
272
|
-
*/
|
|
273
|
-
function resolveBoundPlanDigest(context) {
|
|
274
|
-
const plan = context?.plan;
|
|
275
|
-
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
276
|
-
throw new Error('context.plan is required to bind the kimi plan digest');
|
|
277
|
-
}
|
|
278
|
-
const carried = plan.digest;
|
|
279
|
-
if (typeof carried !== 'string' || !HEX_DIGEST_RE.test(carried)) {
|
|
280
|
-
throw new Error('context.plan.digest must be a 64-char lowercase hex frozen plan digest');
|
|
281
|
-
}
|
|
282
|
-
const normalized = normalizePlanForDigest(plan);
|
|
283
|
-
if (computePlanDigest(normalized) !== carried) {
|
|
284
|
-
throw new Error('context.plan.digest does not match the normalized frozen plan (a non-lifecycle field was tampered)');
|
|
285
|
-
}
|
|
286
|
-
return carried;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/**
|
|
290
|
-
* Authoritative, cross-run attestation directory for a kimi install.
|
|
291
|
-
*
|
|
292
|
-
* Lives at a stable root-fixed location keyed by the verified frozen plan
|
|
293
|
-
* digest and plugin id:
|
|
294
|
-
* <root>/.release-skill/kimi-attestations/<planDigest>/<plugin>/
|
|
295
|
-
*
|
|
296
|
-
* This survives the publish -> manual install -> reconcile -> verify chain,
|
|
297
|
-
* where each command otherwise uses a fresh runDir (an attestation written to a
|
|
298
|
-
* publish runDir would be invisible to reconcile/verify). Both the requirement
|
|
299
|
-
* and the human attestation live here. The segments are pre-validated (planDigest
|
|
300
|
-
* is 64-hex, plugin matches SAFE_ID_RE) and the resolved path is contained
|
|
301
|
-
* within the authority base, so no path escape is possible.
|
|
302
|
-
*
|
|
303
|
-
* @param {object} context - adapter context (needs `root`).
|
|
304
|
-
* @param {string} planDigest - verified frozen plan digest (64-hex).
|
|
305
|
-
* @param {string} plugin - plugin id (SAFE_ID_RE).
|
|
306
|
-
* @returns {string} absolute authority directory.
|
|
307
|
-
*/
|
|
308
|
-
function kimiAuthorityDir(context, planDigest, plugin) {
|
|
309
|
-
if (!context?.root) {
|
|
310
|
-
throw new Error('context.root is required for the kimi attestation authority');
|
|
311
|
-
}
|
|
312
|
-
if (!HEX_DIGEST_RE.test(planDigest)) {
|
|
313
|
-
throw new Error('kimi attestation authority requires a 64-hex plan digest');
|
|
314
|
-
}
|
|
315
|
-
if (!SAFE_ID_RE.test(plugin)) {
|
|
316
|
-
throw new Error(`kimi attestation authority requires a safe plugin id: "${plugin}"`);
|
|
317
|
-
}
|
|
318
|
-
const base = resolve(context.root, '.release-skill', 'kimi-attestations');
|
|
319
|
-
const dir = resolve(base, planDigest, plugin);
|
|
320
|
-
const rel = relative(base, dir);
|
|
321
|
-
const sep = process.platform === 'win32' ? '\\' : '/';
|
|
322
|
-
if (
|
|
323
|
-
rel === '' || rel === '..' || isAbsolute(rel) || rel.startsWith(`..${sep}`)
|
|
324
|
-
|| rel.split(sep).some((segment) => segment === '..' || segment === '')
|
|
325
|
-
) {
|
|
326
|
-
throw new Error('kimi attestation authority path escapes its base');
|
|
327
|
-
}
|
|
328
|
-
return dir;
|
|
270
|
+
return { manifestDigest: action.manifestDigest };
|
|
329
271
|
}
|
|
330
272
|
|
|
331
273
|
/**
|
|
332
|
-
* Build the
|
|
333
|
-
*
|
|
334
|
-
*
|
|
335
|
-
* exact published ref; `/tree/<ref>` is the documented equivalent. A bare
|
|
336
|
-
* repository URL is NOT acceptable because it installs the latest release (or
|
|
337
|
-
* default branch), which need not equal the frozen version.
|
|
338
|
-
*
|
|
339
|
-
* @param {string} repo - owner/repo
|
|
340
|
-
* @param {string} ref - frozen Git ref (tag)
|
|
341
|
-
* @returns {string}
|
|
274
|
+
* Build the audit fields recorded when a declared-manifest-v1 verification
|
|
275
|
+
* observes host-added files in the install tree. Empty for legacy bindings
|
|
276
|
+
* (and for failed bindings), so legacy observations stay byte-identical.
|
|
342
277
|
*/
|
|
343
|
-
function
|
|
344
|
-
return
|
|
278
|
+
function extraInstalledPathsAudit(binding) {
|
|
279
|
+
if (!binding || !Array.isArray(binding.extraInstalledPaths)) return {};
|
|
280
|
+
return {
|
|
281
|
+
extraInstalledPaths: binding.extraInstalledPaths,
|
|
282
|
+
...(binding.extraInstalledPathsTotal !== undefined
|
|
283
|
+
? { extraInstalledPathsTotal: binding.extraInstalledPathsTotal }
|
|
284
|
+
: {}),
|
|
285
|
+
};
|
|
345
286
|
}
|
|
346
287
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
*/
|
|
353
|
-
function buildKimiManualInstructions({ installUrl, plugin, version, ref, isolatedHome, attestationDir }) {
|
|
354
|
-
return [
|
|
355
|
-
`Kimi Code has no scriptable plugin-install CLI; installation is a manual, interactive step.`,
|
|
356
|
-
`1) publish fails closed at this kimi checkpoint and leaves the run PARTIAL (the automated Git branch/tag, npm, and GitHub Release writes still complete first).`,
|
|
357
|
-
`2) Launch Kimi Code with the ISOLATED home from this requirement so the managed copy lands inside it: set HOME="${isolatedHome}" and KIMI_CODE_HOME="${isolatedHome}". The plugin installs to "${isolatedHome}/plugins/managed/${plugin}/".`,
|
|
358
|
-
`3) In that isolated Kimi Code session run: /plugins install ${installUrl} (pinned to frozen ref "${ref}", version ${version}; never install the bare repository URL). Confirm the trust prompt for plugin "${plugin}", then run /plugins reload (or /new).`,
|
|
359
|
-
`4) Write the attestation JSON to: ${attestationDir}/${KIMI_ATTESTATION_FILE}. planDigest MUST be the frozen plan digest; payloadDigest MUST be the frozen snapshot payload digest; installPath MUST be the isolated managed directory above. attestedAt must not be in the future and expiresAt must be within 24 hours of attestedAt.`,
|
|
360
|
-
` Required fields: consumer="kimi", plugin, version, entrySkill, repo, ref, installPath, planDigest, payloadDigest, attestedBy, attestedAt, expiresAt.`,
|
|
361
|
-
`5) Re-run release-skill reconcile (promotes PARTIAL -> PUBLISHED) and then verify (-> VERIFIED). Both read the attestation from this same plan-digest-keyed authority directory, so a fresh run directory does not lose the proof.`,
|
|
362
|
-
`An install into the ordinary ~/.kimi-code is NOT acceptable proof: the attested installPath must resolve inside this requirement's isolated KIMI_CODE_HOME managed root, otherwise verification fails closed.`,
|
|
363
|
-
];
|
|
364
|
-
}
|
|
365
|
-
|
|
366
|
-
/**
|
|
367
|
-
* Read the authoritative Kimi plugin manifest from a verified plugin root.
|
|
368
|
-
*
|
|
369
|
-
* `kimi.plugin.json` at the root takes priority over `.kimi-plugin/plugin.json`
|
|
370
|
-
* when both exist (official precedence). Returns the parsed manifest and the
|
|
371
|
-
* root-relative manifest path. Throws when no valid manifest is present.
|
|
372
|
-
*
|
|
373
|
-
* @param {string} pluginRootReal - realpath of the verified plugin root.
|
|
374
|
-
* @returns {Promise<{manifest:object, manifestRelative:string}>}
|
|
375
|
-
*/
|
|
376
|
-
async function readKimiManifest(pluginRootReal) {
|
|
377
|
-
const candidates = [
|
|
378
|
-
'kimi.plugin.json',
|
|
379
|
-
join('.kimi-plugin', 'plugin.json'),
|
|
380
|
-
];
|
|
381
|
-
for (const manifestRelative of candidates) {
|
|
382
|
-
const manifestPath = resolve(pluginRootReal, manifestRelative);
|
|
383
|
-
let content;
|
|
384
|
-
try {
|
|
385
|
-
content = await readFile(manifestPath, 'utf8');
|
|
386
|
-
} catch {
|
|
387
|
-
continue;
|
|
388
|
-
}
|
|
389
|
-
let manifest;
|
|
390
|
-
try {
|
|
391
|
-
manifest = JSON.parse(content);
|
|
392
|
-
} catch {
|
|
393
|
-
throw new Error(`kimi plugin manifest ${manifestRelative} is not valid JSON`);
|
|
394
|
-
}
|
|
395
|
-
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
396
|
-
throw new Error(`kimi plugin manifest ${manifestRelative} is not an object`);
|
|
397
|
-
}
|
|
398
|
-
return { manifest, manifestRelative };
|
|
399
|
-
}
|
|
400
|
-
throw new Error('no kimi plugin manifest found (expected kimi.plugin.json or .kimi-plugin/plugin.json)');
|
|
401
|
-
}
|
|
288
|
+
// The kimi protocol closure (manual-install requirement, human attestation
|
|
289
|
+
// validation, manifest reading, shared constants) lives in
|
|
290
|
+
// ../platforms/kimi.mjs and is referenced from the platform registry's kimi
|
|
291
|
+
// strategy table. Shared adapter primitives (SAFE_ID_RE, resolveTimeoutMs,
|
|
292
|
+
// writeEvidenceAtomic) live in ./contract.mjs.
|
|
402
293
|
|
|
403
294
|
/**
|
|
404
295
|
* Validate that a manifest `skills` value is a safe plugin-root-relative path.
|
|
@@ -504,281 +395,6 @@ async function resolveKimiEntrySkillFile(pluginRootReal, manifest, entrySkill) {
|
|
|
504
395
|
return entryReal;
|
|
505
396
|
}
|
|
506
397
|
|
|
507
|
-
/**
|
|
508
|
-
* Validate a structured kimi manual-install attestation against the frozen
|
|
509
|
-
* action and the verified frozen plan digest.
|
|
510
|
-
*
|
|
511
|
-
* Bindings (fail closed on any mismatch):
|
|
512
|
-
* - `planDigest` binds to the REAL frozen plan digest (`boundPlanDigest`, from
|
|
513
|
-
* `context.plan.digest`) — NOT to `action.manifestDigest`.
|
|
514
|
-
* - `payloadDigest` binds separately to `action.manifestDigest` (the sealed
|
|
515
|
-
* snapshot payload digest).
|
|
516
|
-
* - plugin identity, version, entry skill, repo, and frozen ref must match.
|
|
517
|
-
* - Time bounds: `attestedAt` must not be in the future, the validity window
|
|
518
|
-
* (`expiresAt - attestedAt`) must not exceed 24h, and the attestation must
|
|
519
|
-
* not be expired relative to `isoNow`.
|
|
520
|
-
*
|
|
521
|
-
* @param {object} attestation - parsed attestation JSON.
|
|
522
|
-
* @param {object} action - the expanded kimi action (top-level fields).
|
|
523
|
-
* @param {string} isoNow - current ISO timestamp.
|
|
524
|
-
* @param {string} boundPlanDigest - verified frozen plan digest.
|
|
525
|
-
* @returns {{valid:boolean, error:string|null}}
|
|
526
|
-
*/
|
|
527
|
-
function validateKimiAttestation(attestation, action, isoNow, boundPlanDigest) {
|
|
528
|
-
if (!attestation || typeof attestation !== 'object' || Array.isArray(attestation)) {
|
|
529
|
-
return { valid: false, error: 'kimi attestation is not an object' };
|
|
530
|
-
}
|
|
531
|
-
const requiredStrings = ['plugin', 'version', 'entrySkill', 'repo', 'ref', 'installPath', 'payloadDigest', 'planDigest', 'attestedBy', 'attestedAt', 'expiresAt'];
|
|
532
|
-
for (const field of requiredStrings) {
|
|
533
|
-
if (typeof attestation[field] !== 'string' || attestation[field].length === 0) {
|
|
534
|
-
return { valid: false, error: `kimi attestation missing required field "${field}"` };
|
|
535
|
-
}
|
|
536
|
-
}
|
|
537
|
-
if (attestation.consumer !== 'kimi') {
|
|
538
|
-
return { valid: false, error: `kimi attestation consumer "${attestation.consumer}" must be "kimi"` };
|
|
539
|
-
}
|
|
540
|
-
if (!HEX_DIGEST_RE.test(attestation.planDigest)) {
|
|
541
|
-
return { valid: false, error: 'kimi attestation planDigest must be a 64-char lowercase hex digest' };
|
|
542
|
-
}
|
|
543
|
-
if (attestation.planDigest !== boundPlanDigest) {
|
|
544
|
-
return { valid: false, error: 'kimi attestation planDigest does not match the frozen plan digest' };
|
|
545
|
-
}
|
|
546
|
-
if (attestation.plugin !== action.plugin) {
|
|
547
|
-
return { valid: false, error: `kimi attestation plugin "${attestation.plugin}" does not match action plugin "${action.plugin}"` };
|
|
548
|
-
}
|
|
549
|
-
if (attestation.version !== action.version) {
|
|
550
|
-
return { valid: false, error: `kimi attestation version "${attestation.version}" does not match action version "${action.version}"` };
|
|
551
|
-
}
|
|
552
|
-
if (attestation.entrySkill !== action.entrySkill) {
|
|
553
|
-
return { valid: false, error: `kimi attestation entrySkill "${attestation.entrySkill}" does not match action entrySkill "${action.entrySkill}"` };
|
|
554
|
-
}
|
|
555
|
-
if (attestation.repo !== action.repo) {
|
|
556
|
-
return { valid: false, error: `kimi attestation repo "${attestation.repo}" does not match action repo "${action.repo}"` };
|
|
557
|
-
}
|
|
558
|
-
const expectedRef = action.ref ?? `v${action.version}`;
|
|
559
|
-
if (attestation.ref !== expectedRef) {
|
|
560
|
-
return { valid: false, error: `kimi attestation ref "${attestation.ref}" does not match frozen ref "${expectedRef}"` };
|
|
561
|
-
}
|
|
562
|
-
if (attestation.payloadDigest !== action.manifestDigest) {
|
|
563
|
-
return { valid: false, error: 'kimi attestation payloadDigest does not match the frozen payload digest' };
|
|
564
|
-
}
|
|
565
|
-
const attestedMs = Date.parse(attestation.attestedAt);
|
|
566
|
-
const expiresMs = Date.parse(attestation.expiresAt);
|
|
567
|
-
const nowMs = Date.parse(isoNow);
|
|
568
|
-
if (!Number.isFinite(attestedMs) || !Number.isFinite(expiresMs) || !Number.isFinite(nowMs)) {
|
|
569
|
-
return { valid: false, error: 'kimi attestation attestedAt/expiresAt must be valid ISO timestamps' };
|
|
570
|
-
}
|
|
571
|
-
if (attestedMs > nowMs) {
|
|
572
|
-
return { valid: false, error: 'kimi attestation attestedAt is in the future' };
|
|
573
|
-
}
|
|
574
|
-
if (expiresMs <= attestedMs) {
|
|
575
|
-
return { valid: false, error: 'kimi attestation expiresAt must be after attestedAt' };
|
|
576
|
-
}
|
|
577
|
-
if (expiresMs - attestedMs > KIMI_MAX_ATTESTATION_VALIDITY_MS) {
|
|
578
|
-
return { valid: false, error: 'kimi attestation validity must not exceed 24 hours' };
|
|
579
|
-
}
|
|
580
|
-
if (nowMs > expiresMs) {
|
|
581
|
-
return { valid: false, error: 'kimi attestation has expired' };
|
|
582
|
-
}
|
|
583
|
-
return { valid: true, error: null };
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Kimi Code protocol capability gap (BLOCKER-1): there is NO scriptable
|
|
588
|
-
* `kimi plugins install/list` CLI and no `--json` protocol. execute NEVER execs
|
|
589
|
-
* a kimi command. Instead it emits an actionable, version-pinned manual-install
|
|
590
|
-
* requirement bound to the real frozen plan digest + identity, and leaves
|
|
591
|
-
* success to observe, which consumes only a trusted human attestation plus
|
|
592
|
-
* read-only verification. Without that proof the checkpoint fails closed and can
|
|
593
|
-
* never reach VERIFIED.
|
|
594
|
-
*
|
|
595
|
-
* Isolation model (B/C): the kimi home is a STABLE, plan-digest-keyed directory
|
|
596
|
-
* under the attestation authority (`<authorityDir>/kimi-home`), not the per-run
|
|
597
|
-
* runDir consumer dir. The operator launches Kimi Code with that KIMI_CODE_HOME
|
|
598
|
-
* so the managed copy lands at `<kimiHome>/plugins/managed/<plugin>/`, a
|
|
599
|
-
* location that is identical across publish/reconcile/verify run dirs. execute
|
|
600
|
-
* creates ONLY the managed parent (`plugins/managed`), never `managed/<plugin>`
|
|
601
|
-
* (the operator's interactive install creates that). The requirement write is
|
|
602
|
-
* idempotent: an identical existing requirement is left untouched, a divergent
|
|
603
|
-
* one fails closed.
|
|
604
|
-
*
|
|
605
|
-
* @param {object} action - expanded kimi action (validated params already).
|
|
606
|
-
* @param {object} context - adapter context (root, runDir, plan).
|
|
607
|
-
* @returns {Promise<import('./contract.mjs').AdapterResult>}
|
|
608
|
-
*/
|
|
609
|
-
async function executeKimiManualRequirement(action, context) {
|
|
610
|
-
const actionType = ActionType.KIMI_MARKETPLACE_INSTALL;
|
|
611
|
-
|
|
612
|
-
// (A) Bind to the REAL frozen plan digest via strict normalized recompute.
|
|
613
|
-
let planDigest;
|
|
614
|
-
try {
|
|
615
|
-
planDigest = resolveBoundPlanDigest(context);
|
|
616
|
-
} catch (planErr) {
|
|
617
|
-
return createResult({
|
|
618
|
-
actionType,
|
|
619
|
-
status: ActionStatus.EXECUTE_FAILED,
|
|
620
|
-
error: `cannot bind kimi requirement to the frozen plan: ${planErr.message}`,
|
|
621
|
-
});
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Validate the frozen timeout. Kimi execs no CLI, but the frozen-timeout
|
|
625
|
-
// fail-closed invariant still holds for every marketplace action.
|
|
626
|
-
try {
|
|
627
|
-
resolveTimeoutMs(action);
|
|
628
|
-
} catch (timeoutErr) {
|
|
629
|
-
return createResult({
|
|
630
|
-
actionType,
|
|
631
|
-
status: ActionStatus.EXECUTE_FAILED,
|
|
632
|
-
error: timeoutErr.message,
|
|
633
|
-
});
|
|
634
|
-
}
|
|
635
|
-
|
|
636
|
-
const ref = action.ref ?? `v${action.version}`;
|
|
637
|
-
const installUrl = buildKimiInstallUrl(action.repo, ref);
|
|
638
|
-
|
|
639
|
-
// (B) Stable, plan-digest-keyed authority dir — the ONLY kimi home, shared
|
|
640
|
-
// across publish/reconcile/verify run dirs.
|
|
641
|
-
let attestationDir;
|
|
642
|
-
try {
|
|
643
|
-
attestationDir = kimiAuthorityDir(context, planDigest, action.plugin);
|
|
644
|
-
} catch (dirErr) {
|
|
645
|
-
return createResult({
|
|
646
|
-
actionType,
|
|
647
|
-
status: ActionStatus.EXECUTE_FAILED,
|
|
648
|
-
error: dirErr.message,
|
|
649
|
-
});
|
|
650
|
-
}
|
|
651
|
-
const kimiHome = resolve(attestationDir, 'kimi-home');
|
|
652
|
-
const managedParent = resolve(kimiHome, KIMI_MANAGED_SUBPATH); // plugins/managed
|
|
653
|
-
// plugins/managed/<plugin> — created by the operator's interactive install.
|
|
654
|
-
const managedInstallRoot = resolve(managedParent, action.plugin);
|
|
655
|
-
|
|
656
|
-
const instructions = buildKimiManualInstructions({
|
|
657
|
-
installUrl,
|
|
658
|
-
plugin: action.plugin,
|
|
659
|
-
version: action.version,
|
|
660
|
-
ref,
|
|
661
|
-
isolatedHome: kimiHome,
|
|
662
|
-
attestationDir,
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
const requirement = {
|
|
666
|
-
kind: 'kimi-manual-install-requirement',
|
|
667
|
-
consumer: 'kimi',
|
|
668
|
-
plugin: action.plugin,
|
|
669
|
-
version: action.version,
|
|
670
|
-
entrySkill: action.entrySkill,
|
|
671
|
-
repo: action.repo,
|
|
672
|
-
ref,
|
|
673
|
-
installUrl,
|
|
674
|
-
// (A) planDigest binds to the real frozen plan digest;
|
|
675
|
-
// expectedPayloadDigest binds separately to the snapshot payload digest.
|
|
676
|
-
planDigest,
|
|
677
|
-
expectedPayloadDigest: action.manifestDigest,
|
|
678
|
-
isolatedHome: kimiHome,
|
|
679
|
-
kimiCodeHome: kimiHome,
|
|
680
|
-
managedInstallRoot,
|
|
681
|
-
attestationDir,
|
|
682
|
-
attestationFile: KIMI_ATTESTATION_FILE,
|
|
683
|
-
attestationTemplate: {
|
|
684
|
-
consumer: 'kimi',
|
|
685
|
-
plugin: action.plugin,
|
|
686
|
-
version: action.version,
|
|
687
|
-
entrySkill: action.entrySkill,
|
|
688
|
-
repo: action.repo,
|
|
689
|
-
ref,
|
|
690
|
-
installPath: managedInstallRoot,
|
|
691
|
-
planDigest,
|
|
692
|
-
payloadDigest: action.manifestDigest,
|
|
693
|
-
attestedBy: '<person responsible for the manual install>',
|
|
694
|
-
attestedAt: '<ISO 8601 now; must not be in the future>',
|
|
695
|
-
expiresAt: '<ISO 8601; within 24h of attestedAt>',
|
|
696
|
-
},
|
|
697
|
-
instructions,
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
// Create ONLY the managed parent (plugins/managed); never pre-create
|
|
701
|
-
// managed/<plugin> — the operator's interactive install creates that.
|
|
702
|
-
try {
|
|
703
|
-
await mkdir(managedParent, { recursive: true, mode: 0o700 });
|
|
704
|
-
} catch (mkdirErr) {
|
|
705
|
-
return createResult({
|
|
706
|
-
actionType,
|
|
707
|
-
status: ActionStatus.EXECUTE_FAILED,
|
|
708
|
-
error: `cannot create kimi managed parent directory: ${mkdirErr.message}`,
|
|
709
|
-
});
|
|
710
|
-
}
|
|
711
|
-
|
|
712
|
-
// Idempotent requirement write: an identical existing requirement is left
|
|
713
|
-
// untouched; a divergent existing requirement fails closed (never silently
|
|
714
|
-
// overwritten). `createdAt` is volatile and excluded from the comparison.
|
|
715
|
-
const requirementPath = resolve(attestationDir, KIMI_REQUIREMENT_FILE);
|
|
716
|
-
let existing = null;
|
|
717
|
-
let requirementMissing = false;
|
|
718
|
-
try {
|
|
719
|
-
const existingRaw = await readFile(requirementPath, 'utf8');
|
|
720
|
-
try {
|
|
721
|
-
existing = JSON.parse(existingRaw);
|
|
722
|
-
} catch (parseErr) {
|
|
723
|
-
return createResult({
|
|
724
|
-
actionType,
|
|
725
|
-
status: ActionStatus.EXECUTE_FAILED,
|
|
726
|
-
error: `existing kimi manual-install requirement is invalid JSON; refusing to overwrite: ${parseErr.message}`,
|
|
727
|
-
});
|
|
728
|
-
}
|
|
729
|
-
} catch (readErr) {
|
|
730
|
-
if (readErr?.code === 'ENOENT') {
|
|
731
|
-
requirementMissing = true;
|
|
732
|
-
} else {
|
|
733
|
-
return createResult({
|
|
734
|
-
actionType,
|
|
735
|
-
status: ActionStatus.EXECUTE_FAILED,
|
|
736
|
-
error: `existing kimi manual-install requirement cannot be read; refusing to overwrite: ${readErr.message}`,
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
}
|
|
740
|
-
if (!requirementMissing) {
|
|
741
|
-
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
742
|
-
return createResult({
|
|
743
|
-
actionType,
|
|
744
|
-
status: ActionStatus.EXECUTE_FAILED,
|
|
745
|
-
error: 'existing kimi manual-install requirement is not an object; refusing to overwrite',
|
|
746
|
-
});
|
|
747
|
-
}
|
|
748
|
-
const { createdAt: _existingCreatedAt, ...existingBody } = existing;
|
|
749
|
-
if (canonicalJson(existingBody) !== canonicalJson(requirement)) {
|
|
750
|
-
return createResult({
|
|
751
|
-
actionType,
|
|
752
|
-
status: ActionStatus.EXECUTE_FAILED,
|
|
753
|
-
error: 'existing kimi manual-install requirement conflicts with the current frozen action; refusing to overwrite',
|
|
754
|
-
});
|
|
755
|
-
}
|
|
756
|
-
} else {
|
|
757
|
-
await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: new Date().toISOString() });
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
return createResult({
|
|
761
|
-
actionType,
|
|
762
|
-
status: ActionStatus.EXECUTED,
|
|
763
|
-
observation: {
|
|
764
|
-
installed: false,
|
|
765
|
-
manualInstallRequired: true,
|
|
766
|
-
consumer: 'kimi',
|
|
767
|
-
plugin: action.plugin,
|
|
768
|
-
version: action.version,
|
|
769
|
-
entrySkill: action.entrySkill,
|
|
770
|
-
repo: action.repo,
|
|
771
|
-
ref,
|
|
772
|
-
installUrl,
|
|
773
|
-
planDigest,
|
|
774
|
-
attestationDir,
|
|
775
|
-
kimiCodeHome: kimiHome,
|
|
776
|
-
managedInstallRoot,
|
|
777
|
-
instructions,
|
|
778
|
-
},
|
|
779
|
-
});
|
|
780
|
-
}
|
|
781
|
-
|
|
782
398
|
const SUPPORTED_TYPES = [
|
|
783
399
|
ActionType.PLUGIN_MANIFEST_VALIDATE,
|
|
784
400
|
ActionType.PLUGIN_INSTALL_CHECK,
|
|
@@ -787,12 +403,12 @@ const SUPPORTED_TYPES = [
|
|
|
787
403
|
ActionType.KIMI_MARKETPLACE_INSTALL,
|
|
788
404
|
];
|
|
789
405
|
|
|
790
|
-
/** Safe identifier pattern: lowercase alphanumeric, hyphens, dots, underscores. */
|
|
791
|
-
const SAFE_ID_RE = /^[a-z0-9][a-z0-9._-]*$/;
|
|
792
|
-
|
|
793
406
|
/** Safe repo pattern: owner/repo with alphanumeric, hyphens, dots, underscores. */
|
|
794
407
|
const SAFE_REPO_RE = /^[a-zA-Z0-9][a-zA-Z0-9._-]*\/[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
|
|
795
408
|
|
|
409
|
+
/** Valid consumer ids, derived from the platform registry (single source). */
|
|
410
|
+
const CONSUMER_IDS = new Set(PLATFORMS.map((p) => p.id));
|
|
411
|
+
|
|
796
412
|
/**
|
|
797
413
|
* Strict semver pattern: supports prerelease and build metadata.
|
|
798
414
|
* Matches: 1.0.0, 1.0.0-beta.1, 1.0.0-rc.1+build.123
|
|
@@ -862,7 +478,7 @@ function validateMarketplaceParams(params) {
|
|
|
862
478
|
return { valid: false, error: 'parameters must be an object' };
|
|
863
479
|
}
|
|
864
480
|
const { consumer, plugin, marketplace, repo, version, entrySkill } = params;
|
|
865
|
-
if (!
|
|
481
|
+
if (!CONSUMER_IDS.has(consumer)) {
|
|
866
482
|
return { valid: false, error: `invalid consumer: "${consumer}"` };
|
|
867
483
|
}
|
|
868
484
|
if (!plugin || !SAFE_ID_RE.test(plugin)) {
|
|
@@ -873,7 +489,7 @@ function validateMarketplaceParams(params) {
|
|
|
873
489
|
// marketplace but NO non-interactive install API, so `marketplace` carries no
|
|
874
490
|
// executable meaning for kimi and is optional (validated only if present); it
|
|
875
491
|
// must not become a required identity condition for kimi execution/observe.
|
|
876
|
-
if (consumer
|
|
492
|
+
if (!getPlatform(consumer).automatable) {
|
|
877
493
|
if (marketplace !== undefined && marketplace !== null && !SAFE_ID_RE.test(marketplace)) {
|
|
878
494
|
return { valid: false, error: `unsafe marketplace identifier: "${marketplace}"` };
|
|
879
495
|
}
|
|
@@ -893,42 +509,6 @@ function validateMarketplaceParams(params) {
|
|
|
893
509
|
}
|
|
894
510
|
|
|
895
511
|
|
|
896
|
-
/**
|
|
897
|
-
* Resolve and validate the frozen timeoutMs from the expanded adapter action.
|
|
898
|
-
*
|
|
899
|
-
* The publish/reconcile/verify call path expands plan actions as
|
|
900
|
-
* `{ actionType, ...action.parameters }`, so `parameters.timeoutMs` in the
|
|
901
|
-
* plan becomes `action.timeoutMs` at the adapter level. This function reads
|
|
902
|
-
* from the top-level action, not from a nested `parameters` sub-object.
|
|
903
|
-
*
|
|
904
|
-
* Rules:
|
|
905
|
-
* - Missing field (undefined): returns 300000 default (legacy compatibility).
|
|
906
|
-
* - Present but null/invalid (null, string, NaN, Infinity, non-integer,
|
|
907
|
-
* out of range): fail-closed, throws.
|
|
908
|
-
* - Valid integer in [30000, 900000]: returns the value as-is.
|
|
909
|
-
*
|
|
910
|
-
* @param {object} action - The expanded adapter action (top-level).
|
|
911
|
-
* @returns {number} Validated timeout in milliseconds.
|
|
912
|
-
* @throws {Error} If the value is present but invalid.
|
|
913
|
-
*/
|
|
914
|
-
function resolveTimeoutMs(action) {
|
|
915
|
-
const raw = action?.timeoutMs;
|
|
916
|
-
if (raw === undefined) {
|
|
917
|
-
return 300000;
|
|
918
|
-
}
|
|
919
|
-
if (raw === null || typeof raw !== 'number' || !Number.isFinite(raw) || !Number.isInteger(raw)) {
|
|
920
|
-
throw new Error(
|
|
921
|
-
`action.timeoutMs must be a finite integer, got: ${JSON.stringify(raw)}`,
|
|
922
|
-
);
|
|
923
|
-
}
|
|
924
|
-
if (raw < 30000 || raw > 900000) {
|
|
925
|
-
throw new Error(
|
|
926
|
-
`action.timeoutMs must be between 30000 and 900000, got: ${raw}`,
|
|
927
|
-
);
|
|
928
|
-
}
|
|
929
|
-
return raw;
|
|
930
|
-
}
|
|
931
|
-
|
|
932
512
|
/**
|
|
933
513
|
* Run a CLI command using execFile (never shell: true).
|
|
934
514
|
*/
|
|
@@ -1157,6 +737,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1157
737
|
|
|
1158
738
|
// 6. Verify frozen snapshot exists and contains required marketplace files
|
|
1159
739
|
const consumer = action.consumer;
|
|
740
|
+
const platform = getPlatform(consumer);
|
|
1160
741
|
let snapshotDirReal;
|
|
1161
742
|
// Authoritative kimi manifest (from the frozen snapshot), used to
|
|
1162
743
|
// resolve the entry skill via the manifest-declared skills root.
|
|
@@ -1171,14 +752,15 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1171
752
|
});
|
|
1172
753
|
}
|
|
1173
754
|
|
|
1174
|
-
//
|
|
1175
|
-
//
|
|
1176
|
-
//
|
|
755
|
+
// A non-automatable platform (kimi) has no non-interactive
|
|
756
|
+
// marketplace/install API: the whole repo is installed as one
|
|
757
|
+
// plugin. The authoritative manifest is read from the verified
|
|
758
|
+
// snapshot root via the platform strategy's official precedence
|
|
1177
759
|
// (kimi.plugin.json over .kimi-plugin/plugin.json).
|
|
1178
|
-
if (
|
|
760
|
+
if (!platform.automatable) {
|
|
1179
761
|
let kimiManifestResult;
|
|
1180
762
|
try {
|
|
1181
|
-
kimiManifestResult = await
|
|
763
|
+
kimiManifestResult = await platform.strategy.readManifest(snapshotDirReal);
|
|
1182
764
|
} catch (manifestErr) {
|
|
1183
765
|
return createResult({
|
|
1184
766
|
actionType,
|
|
@@ -1204,13 +786,11 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1204
786
|
kimiSnapshotManifest = kimiManifest;
|
|
1205
787
|
}
|
|
1206
788
|
|
|
1207
|
-
if (
|
|
789
|
+
if (platform.automatable) {
|
|
1208
790
|
// Verify marketplace files exist.
|
|
1209
791
|
// marketplace.json is at the snapshot root; plugin manifest is
|
|
1210
792
|
// resolved relative to the entry's declared source path.
|
|
1211
|
-
const marketplaceRelative =
|
|
1212
|
-
? '.claude-plugin/marketplace.json'
|
|
1213
|
-
: '.agents/plugins/marketplace.json';
|
|
793
|
+
const marketplaceRelative = platform.manifestPaths.marketplace;
|
|
1214
794
|
|
|
1215
795
|
const marketplacePath = resolve(snapshotDirReal, marketplaceRelative);
|
|
1216
796
|
|
|
@@ -1294,9 +874,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1294
874
|
// snapshot/.claude-plugin/plugin.json
|
|
1295
875
|
// For subdirectory layouts (source: "./adapters/claude"), this resolves to
|
|
1296
876
|
// snapshot/adapters/claude/.claude-plugin/plugin.json
|
|
1297
|
-
const manifestRelative =
|
|
1298
|
-
? join(sourcePath, '.claude-plugin', 'plugin.json')
|
|
1299
|
-
: join(sourcePath, '.codex-plugin', 'plugin.json');
|
|
877
|
+
const manifestRelative = join(sourcePath, platform.manifestPaths.plugin);
|
|
1300
878
|
const manifestPath = resolve(snapshotDirReal, manifestRelative);
|
|
1301
879
|
|
|
1302
880
|
const manifestResult = await validateManifestFile(manifestPath, ['name', 'version']);
|
|
@@ -1308,9 +886,12 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1308
886
|
});
|
|
1309
887
|
}
|
|
1310
888
|
|
|
1311
|
-
//
|
|
1312
|
-
//
|
|
1313
|
-
|
|
889
|
+
// Whether the marketplace entry itself carries the authoritative
|
|
890
|
+
// version is a platform protocol split (registry data): claude
|
|
891
|
+
// binds entry.version to the action version, codex keeps the
|
|
892
|
+
// authoritative version in .codex-plugin/plugin.json (the entry
|
|
893
|
+
// version is never bound), kimi has no marketplace.
|
|
894
|
+
if (platform.marketplaceEntryCarriesVersion && entry.version !== action.version) {
|
|
1314
895
|
return createResult({
|
|
1315
896
|
actionType,
|
|
1316
897
|
status: ActionStatus.PREFLIGHT_FAILED,
|
|
@@ -1344,12 +925,13 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1344
925
|
}
|
|
1345
926
|
|
|
1346
927
|
// Verify the entry skill exists in the snapshot.
|
|
1347
|
-
//
|
|
1348
|
-
// skills/<entrySkill>/SKILL.md layout is authoritative for
|
|
1349
|
-
//
|
|
1350
|
-
// (MAJOR-4): the root is
|
|
1351
|
-
// `skills` means the
|
|
1352
|
-
|
|
928
|
+
// Automatable platforms' manifests always declare ./skills/, so the
|
|
929
|
+
// fixed skills/<entrySkill>/SKILL.md layout is authoritative for
|
|
930
|
+
// them. A non-automatable platform (kimi) resolves the entry skill
|
|
931
|
+
// via the manifest-declared skills root (MAJOR-4): the root is
|
|
932
|
+
// validated + realpath-contained, and omitted `skills` means the
|
|
933
|
+
// official single-skill root SKILL.md.
|
|
934
|
+
if (!platform.automatable) {
|
|
1353
935
|
try {
|
|
1354
936
|
await resolveKimiEntrySkillFile(snapshotDirReal, kimiSnapshotManifest, action.entrySkill);
|
|
1355
937
|
} catch (entryErr) {
|
|
@@ -1548,13 +1130,15 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1548
1130
|
});
|
|
1549
1131
|
}
|
|
1550
1132
|
|
|
1551
|
-
//
|
|
1552
|
-
// manual-requirement
|
|
1553
|
-
// home and deliberately SKIPS the
|
|
1554
|
-
//
|
|
1555
|
-
//
|
|
1556
|
-
|
|
1557
|
-
|
|
1133
|
+
// A non-automatable platform (kimi) has NO scriptable install CLI.
|
|
1134
|
+
// It is handled entirely by its manual-requirement strategy, which
|
|
1135
|
+
// uses a stable plan-digest-keyed home and deliberately SKIPS the
|
|
1136
|
+
// per-run isolated consumer dir and its runDir containment check
|
|
1137
|
+
// (that model only fits automatable platforms, which exec a real
|
|
1138
|
+
// CLI into a per-run HOME).
|
|
1139
|
+
const platform = getPlatform(action.consumer);
|
|
1140
|
+
if (!platform.automatable) {
|
|
1141
|
+
return platform.strategy.buildManualRequirement(action, context);
|
|
1558
1142
|
}
|
|
1559
1143
|
|
|
1560
1144
|
const consumer = action.consumer;
|
|
@@ -1574,21 +1158,18 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1574
1158
|
});
|
|
1575
1159
|
}
|
|
1576
1160
|
|
|
1577
|
-
// Create isolated HOME and
|
|
1161
|
+
// Create isolated HOME and the consumer state subdirectories the
|
|
1162
|
+
// registry declares for this platform.
|
|
1578
1163
|
await mkdir(isolatedHome, { recursive: true, mode: 0o700 });
|
|
1579
|
-
|
|
1580
|
-
await mkdir(resolve(isolatedHome,
|
|
1581
|
-
} else {
|
|
1582
|
-
await mkdir(resolve(isolatedHome, '.codex'), { recursive: true, mode: 0o700 });
|
|
1164
|
+
for (const subdir of platform.isolationSubdirs) {
|
|
1165
|
+
await mkdir(resolve(isolatedHome, subdir), { recursive: true, mode: 0o700 });
|
|
1583
1166
|
}
|
|
1584
1167
|
|
|
1585
|
-
const cliCmd =
|
|
1168
|
+
const cliCmd = platform.cli.binary;
|
|
1586
1169
|
const baseEnv = { ...process.env, ...context.env };
|
|
1587
1170
|
const env = {
|
|
1588
1171
|
...baseEnv,
|
|
1589
|
-
...(
|
|
1590
|
-
? { HOME: isolatedHome, CLAUDE_CONFIG_DIR: resolve(isolatedHome, '.claude') }
|
|
1591
|
-
: { HOME: isolatedHome, CODEX_HOME: isolatedHome }),
|
|
1172
|
+
...platform.isolationEnv(isolatedHome),
|
|
1592
1173
|
};
|
|
1593
1174
|
// Ensure real HOME/CODEX_HOME don't leak back (already overridden
|
|
1594
1175
|
// above).
|
|
@@ -1610,16 +1191,14 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1610
1191
|
});
|
|
1611
1192
|
}
|
|
1612
1193
|
|
|
1613
|
-
// Step 1: Add marketplace (
|
|
1194
|
+
// Step 1: Add marketplace (automatable platforms only; the
|
|
1195
|
+
// non-automatable manual-requirement path returned above)
|
|
1614
1196
|
const ref = action.ref ?? `v${action.version}`;
|
|
1615
1197
|
let addOutput = null;
|
|
1616
|
-
|
|
1617
|
-
const marketplaceArgs = consumer === 'claude'
|
|
1618
|
-
? ['plugin', 'marketplace', 'add', `${action.repo}@${ref}`]
|
|
1619
|
-
: ['plugin', 'marketplace', 'add', action.repo, '--ref', ref, '--json'];
|
|
1198
|
+
const marketplaceArgs = platform.cli.marketplaceAdd(action.repo, ref);
|
|
1620
1199
|
try {
|
|
1621
1200
|
const addResult = await exec(cliCmd, marketplaceArgs, { env, cwd: context.root, timeout: frozenTimeoutMs });
|
|
1622
|
-
if (
|
|
1201
|
+
if (platform.jsonProtocol.marketplaceAddOutput === 'json') {
|
|
1623
1202
|
try {
|
|
1624
1203
|
addOutput = JSON.parse(addResult.stdout);
|
|
1625
1204
|
if (!addOutput || typeof addOutput !== 'object') {
|
|
@@ -1651,17 +1230,14 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1651
1230
|
error: `marketplace add failed: ${addErr.message}`,
|
|
1652
1231
|
});
|
|
1653
1232
|
}
|
|
1654
|
-
}
|
|
1655
1233
|
|
|
1656
|
-
// Step 2: Install plugin (
|
|
1657
|
-
//
|
|
1234
|
+
// Step 2: Install plugin (the non-automatable manual-requirement
|
|
1235
|
+
// path returned above; it has no install CLI).
|
|
1658
1236
|
let installOutput;
|
|
1659
|
-
const installArgs =
|
|
1660
|
-
? ['plugin', 'install', `${action.plugin}@${action.marketplace}`]
|
|
1661
|
-
: ['plugin', 'add', `${action.plugin}@${action.marketplace}`, '--json'];
|
|
1237
|
+
const installArgs = platform.cli.install(action.plugin, action.marketplace);
|
|
1662
1238
|
try {
|
|
1663
1239
|
const installResult = await exec(cliCmd, installArgs, { env, cwd: context.root, timeout: frozenTimeoutMs });
|
|
1664
|
-
if (
|
|
1240
|
+
if (platform.jsonProtocol.pluginInstallOutput === 'json') {
|
|
1665
1241
|
try {
|
|
1666
1242
|
installOutput = JSON.parse(installResult.stdout);
|
|
1667
1243
|
if (!installOutput || typeof installOutput !== 'object') {
|
|
@@ -1729,6 +1305,29 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1729
1305
|
});
|
|
1730
1306
|
}
|
|
1731
1307
|
|
|
1308
|
+
// Bind the installed payload to the sealed authority before writing
|
|
1309
|
+
// evidence so the evidence file can carry the declared-manifest
|
|
1310
|
+
// audit fields (host-added paths). Best-effort: binding failure is
|
|
1311
|
+
// caught at verify time. Claude's install CLI reports no
|
|
1312
|
+
// installedPath, so claude binds only at observe (via `plugin
|
|
1313
|
+
// list`); codex binds here from the validated install JSON.
|
|
1314
|
+
const installPath = installOutput?.installedPath;
|
|
1315
|
+
let executeManifestDigest = null;
|
|
1316
|
+
let executeBinding = null;
|
|
1317
|
+
if (installPath) {
|
|
1318
|
+
try {
|
|
1319
|
+
executeBinding = await verifyInstalledMarketplacePayload(
|
|
1320
|
+
action,
|
|
1321
|
+
context,
|
|
1322
|
+
installPath,
|
|
1323
|
+
consumer,
|
|
1324
|
+
);
|
|
1325
|
+
executeManifestDigest = executeBinding.manifestDigest;
|
|
1326
|
+
} catch {
|
|
1327
|
+
// Digest computation failure is caught at verify time
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1732
1331
|
// Build and write structured evidence for observe cross-validation
|
|
1733
1332
|
const evidence = {
|
|
1734
1333
|
isolatedHome,
|
|
@@ -1741,6 +1340,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1741
1340
|
addOutput,
|
|
1742
1341
|
installOutput,
|
|
1743
1342
|
executedAt: new Date().toISOString(),
|
|
1343
|
+
...extraInstalledPathsAudit(executeBinding),
|
|
1744
1344
|
};
|
|
1745
1345
|
|
|
1746
1346
|
// Write evidence file to runDir/evidence/ (outside isolatedHome/installPath digest scope)
|
|
@@ -1749,24 +1349,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1749
1349
|
const evidencePath = resolve(evidenceDir, 'release-skill-install-evidence.json');
|
|
1750
1350
|
await writeEvidenceAtomic(evidencePath, evidence);
|
|
1751
1351
|
|
|
1752
|
-
//
|
|
1753
|
-
// expected-compatible observation for executeCheckpoint's
|
|
1352
|
+
// Build expected-compatible observation for executeCheckpoint's
|
|
1754
1353
|
// matchObservation check.
|
|
1755
|
-
const installPath = installOutput?.installedPath;
|
|
1756
|
-
let executeManifestDigest = null;
|
|
1757
|
-
if (installPath) {
|
|
1758
|
-
try {
|
|
1759
|
-
executeManifestDigest = await verifyInstalledMarketplacePayload(
|
|
1760
|
-
action,
|
|
1761
|
-
context,
|
|
1762
|
-
installPath,
|
|
1763
|
-
consumer,
|
|
1764
|
-
);
|
|
1765
|
-
} catch {
|
|
1766
|
-
// Digest computation failure is caught at verify time
|
|
1767
|
-
}
|
|
1768
|
-
}
|
|
1769
|
-
|
|
1770
1354
|
const executeObservation = {
|
|
1771
1355
|
...evidence,
|
|
1772
1356
|
installed: true,
|
|
@@ -1870,15 +1454,19 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1870
1454
|
});
|
|
1871
1455
|
}
|
|
1872
1456
|
const isolatedHome = resolve(runDir, 'consumers', `${consumer}-${action.plugin}`);
|
|
1873
|
-
|
|
1457
|
+
// Registry-driven platform data. observe historically applies no
|
|
1458
|
+
// consumer validation gate (execute/preflight validate it), so an
|
|
1459
|
+
// unregistered consumer keeps the legacy fall-through shape: a
|
|
1460
|
+
// kimi-shaped env, no attestation branch, and no CLI binary — it
|
|
1461
|
+
// still fails closed on the missing execute evidence below.
|
|
1462
|
+
const platform = PLATFORMS.find((p) => p.id === consumer) ?? null;
|
|
1463
|
+
const cliCmd = platform ? (platform.cli ? platform.cli.binary : null) : 'kimi';
|
|
1874
1464
|
const baseEnv = { ...process.env, ...(context.env ?? {}) };
|
|
1875
1465
|
const env = {
|
|
1876
1466
|
...baseEnv,
|
|
1877
|
-
...(
|
|
1878
|
-
?
|
|
1879
|
-
:
|
|
1880
|
-
? { HOME: isolatedHome, CODEX_HOME: isolatedHome }
|
|
1881
|
-
: { HOME: isolatedHome, KIMI_CODE_HOME: isolatedHome }),
|
|
1467
|
+
...(platform
|
|
1468
|
+
? platform.isolationEnv(isolatedHome)
|
|
1469
|
+
: { HOME: isolatedHome, KIMI_CODE_HOME: isolatedHome }),
|
|
1882
1470
|
};
|
|
1883
1471
|
|
|
1884
1472
|
// Resolve frozen timeoutMs from the expanded action (top-level).
|
|
@@ -1895,16 +1483,17 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1895
1483
|
});
|
|
1896
1484
|
}
|
|
1897
1485
|
|
|
1898
|
-
//
|
|
1899
|
-
// `kimi plugins list --json` interface. observe never
|
|
1900
|
-
// command. Instead it consumes a structured human
|
|
1901
|
-
// after the interactive install) bound to the
|
|
1902
|
-
// expected identity, then performs read-only
|
|
1903
|
-
// installed managed copy: payload digest vs the
|
|
1904
|
-
// entry skill resolved via the manifest skills
|
|
1905
|
-
// manifest name/version.
|
|
1906
|
-
// fails closed so a kimi
|
|
1907
|
-
|
|
1486
|
+
// Non-automatable platform protocol capability gap (BLOCKER-1):
|
|
1487
|
+
// there is NO `kimi plugins list --json` interface. observe never
|
|
1488
|
+
// execs a kimi command. Instead it consumes a structured human
|
|
1489
|
+
// attestation (written after the interactive install) bound to the
|
|
1490
|
+
// frozen plan digest and expected identity, then performs read-only
|
|
1491
|
+
// verification of the installed managed copy: payload digest vs the
|
|
1492
|
+
// sealed authority, entry skill resolved via the manifest skills
|
|
1493
|
+
// root (MAJOR-4), and manifest name/version.
|
|
1494
|
+
// Missing/expired/mismatched/escaping proof fails closed so a kimi
|
|
1495
|
+
// unit can never reach VERIFIED without it.
|
|
1496
|
+
if (platform && !platform.automatable) {
|
|
1908
1497
|
const expectedRef = action.ref ?? `v${action.version}`;
|
|
1909
1498
|
|
|
1910
1499
|
// Bind to the REAL frozen plan digest (A). Fail closed if the
|
|
@@ -2085,8 +1674,10 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2085
1674
|
// Read-only payload binding: the installed managed copy must match
|
|
2086
1675
|
// the sealed frozen authority exactly (transport-normalized).
|
|
2087
1676
|
let manifestDigest;
|
|
1677
|
+
let payloadBinding = null;
|
|
2088
1678
|
try {
|
|
2089
|
-
|
|
1679
|
+
payloadBinding = await verifyInstalledMarketplacePayload(action, context, installPathReal, consumer);
|
|
1680
|
+
manifestDigest = payloadBinding.manifestDigest;
|
|
2090
1681
|
} catch (digestErr) {
|
|
2091
1682
|
return createResult({
|
|
2092
1683
|
actionType,
|
|
@@ -2115,7 +1706,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2115
1706
|
let installedManifest;
|
|
2116
1707
|
let entrySkillFound = false;
|
|
2117
1708
|
try {
|
|
2118
|
-
const readManifest = await
|
|
1709
|
+
const readManifest = await platform.strategy.readManifest(installPathReal);
|
|
2119
1710
|
installedManifest = readManifest.manifest;
|
|
2120
1711
|
await resolveKimiEntrySkillFile(installPathReal, installedManifest, action.entrySkill);
|
|
2121
1712
|
entrySkillFound = true;
|
|
@@ -2174,6 +1765,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2174
1765
|
version: installedManifest.version,
|
|
2175
1766
|
repo: action.repo,
|
|
2176
1767
|
ref: expectedRef,
|
|
1768
|
+
...extraInstalledPathsAudit(payloadBinding),
|
|
2177
1769
|
};
|
|
2178
1770
|
return createResult({
|
|
2179
1771
|
actionType,
|
|
@@ -2217,8 +1809,9 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2217
1809
|
});
|
|
2218
1810
|
}
|
|
2219
1811
|
|
|
2220
|
-
// Run list command to verify installation (
|
|
2221
|
-
// has no list CLI and returned via
|
|
1812
|
+
// Run list command to verify installation (automatable platforms
|
|
1813
|
+
// only; a non-automatable platform has no list CLI and returned via
|
|
1814
|
+
// the attestation path above).
|
|
2222
1815
|
const listArgs = ['plugin', 'list', '--json'];
|
|
2223
1816
|
|
|
2224
1817
|
let listOutput;
|
|
@@ -2240,98 +1833,57 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2240
1833
|
let found = null;
|
|
2241
1834
|
let installPath = null;
|
|
2242
1835
|
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
}
|
|
2255
|
-
found = listOutput.find((p) => p.id === pluginId);
|
|
2256
|
-
if (!found) {
|
|
2257
|
-
return createResult({
|
|
2258
|
-
actionType,
|
|
2259
|
-
status: ActionStatus.OBSERVED,
|
|
2260
|
-
observation: {
|
|
2261
|
-
installed: false,
|
|
2262
|
-
error: `plugin "${pluginId}" not found in Claude plugin list`,
|
|
2263
|
-
},
|
|
2264
|
-
});
|
|
2265
|
-
}
|
|
2266
|
-
if (!found.installPath) {
|
|
2267
|
-
return createResult({
|
|
2268
|
-
actionType,
|
|
2269
|
-
status: ActionStatus.OBSERVED,
|
|
2270
|
-
observation: {
|
|
2271
|
-
installed: false,
|
|
2272
|
-
error: `plugin "${pluginId}" found but missing installPath`,
|
|
2273
|
-
},
|
|
2274
|
-
});
|
|
2275
|
-
}
|
|
2276
|
-
installPath = found.installPath;
|
|
2277
|
-
} else if (consumer === 'codex') {
|
|
2278
|
-
// Codex: installPath comes from validated evidence, not from list
|
|
2279
|
-
installPath = evidence.installOutput?.installedPath;
|
|
2280
|
-
if (!installPath) {
|
|
1836
|
+
// Protocol differences live in the platform strategy functions.
|
|
1837
|
+
// Where the install path comes from install evidence (codex) that
|
|
1838
|
+
// check runs BEFORE the list shape check (legacy ordering); where it
|
|
1839
|
+
// comes from the parsed list entry (claude), extractInstallPath is
|
|
1840
|
+
// only reached after parseListOutput returned ok — which has already
|
|
1841
|
+
// fail-closed on a missing installPath, so the lenient
|
|
1842
|
+
// claudeExtractInstallPath boundary can never observe an incomplete
|
|
1843
|
+
// listParsed (slice-1 review leftover).
|
|
1844
|
+
if (platform && platform.jsonProtocol.installPathSource === 'install-output') {
|
|
1845
|
+
const extracted = platform.strategy.extractInstallPath({ execEvidence: evidence });
|
|
1846
|
+
if (!extracted.ok) {
|
|
2281
1847
|
return createResult({
|
|
2282
1848
|
actionType,
|
|
2283
1849
|
status: ActionStatus.OBSERVED,
|
|
2284
|
-
observation: {
|
|
2285
|
-
installed: false,
|
|
2286
|
-
error: 'evidence install JSON missing installedPath',
|
|
2287
|
-
},
|
|
1850
|
+
observation: { installed: false, error: extracted.error },
|
|
2288
1851
|
});
|
|
2289
1852
|
}
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
actionType,
|
|
2296
|
-
status: ActionStatus.OBSERVED,
|
|
2297
|
-
observation: {
|
|
2298
|
-
installed: false,
|
|
2299
|
-
error: 'Codex plugin list did not return {installed: [...]}',
|
|
2300
|
-
},
|
|
2301
|
-
});
|
|
2302
|
-
}
|
|
2303
|
-
found = installed.find((p) => p.pluginId === pluginId);
|
|
2304
|
-
if (!found) {
|
|
2305
|
-
return createResult({
|
|
2306
|
-
actionType,
|
|
2307
|
-
status: ActionStatus.OBSERVED,
|
|
2308
|
-
observation: {
|
|
2309
|
-
installed: false,
|
|
2310
|
-
error: `plugin "${pluginId}" not found in Codex installed list`,
|
|
2311
|
-
},
|
|
2312
|
-
});
|
|
2313
|
-
}
|
|
2314
|
-
// Cross-validate: list fields must match evidence/action
|
|
2315
|
-
if (found.name !== action.plugin) {
|
|
1853
|
+
installPath = extracted.installPath;
|
|
1854
|
+
}
|
|
1855
|
+
if (platform && platform.strategy.parseListOutput) {
|
|
1856
|
+
const listParsed = platform.strategy.parseListOutput(listOutput, pluginId);
|
|
1857
|
+
if (!listParsed.ok) {
|
|
2316
1858
|
return createResult({
|
|
2317
1859
|
actionType,
|
|
2318
1860
|
status: ActionStatus.OBSERVED,
|
|
2319
|
-
observation: { installed: false, error:
|
|
1861
|
+
observation: { installed: false, error: listParsed.error },
|
|
2320
1862
|
});
|
|
2321
1863
|
}
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2327
|
-
|
|
1864
|
+
found = listParsed.found;
|
|
1865
|
+
if (platform.jsonProtocol.installPathSource === 'list') {
|
|
1866
|
+
const extracted = platform.strategy.extractInstallPath({ listParsed });
|
|
1867
|
+
if (!extracted.ok) {
|
|
1868
|
+
return createResult({
|
|
1869
|
+
actionType,
|
|
1870
|
+
status: ActionStatus.OBSERVED,
|
|
1871
|
+
observation: { installed: false, error: extracted.error },
|
|
1872
|
+
});
|
|
1873
|
+
}
|
|
1874
|
+
installPath = extracted.installPath;
|
|
2328
1875
|
}
|
|
2329
|
-
|
|
2330
|
-
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
1876
|
+
// Cross-validate list identity fields against the frozen action
|
|
1877
|
+
// where the platform protocol requires it (codex).
|
|
1878
|
+
if (platform.strategy.crossValidateListEntry) {
|
|
1879
|
+
const crossCheck = platform.strategy.crossValidateListEntry(found, action);
|
|
1880
|
+
if (!crossCheck.ok) {
|
|
1881
|
+
return createResult({
|
|
1882
|
+
actionType,
|
|
1883
|
+
status: ActionStatus.OBSERVED,
|
|
1884
|
+
observation: { installed: false, error: crossCheck.error },
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
2335
1887
|
}
|
|
2336
1888
|
}
|
|
2337
1889
|
|
|
@@ -2383,20 +1935,26 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2383
1935
|
// normalizing only transport-restored write permission bits.
|
|
2384
1936
|
let manifestDigest;
|
|
2385
1937
|
let manifestError = null;
|
|
1938
|
+
let payloadBinding = null;
|
|
2386
1939
|
try {
|
|
2387
|
-
|
|
1940
|
+
payloadBinding = await verifyInstalledMarketplacePayload(
|
|
2388
1941
|
action,
|
|
2389
1942
|
context,
|
|
2390
1943
|
installPath,
|
|
2391
1944
|
consumer,
|
|
2392
1945
|
);
|
|
1946
|
+
manifestDigest = payloadBinding.manifestDigest;
|
|
2393
1947
|
} catch (digestErr) {
|
|
2394
1948
|
// Preserve independently observed fields for diagnostics. This
|
|
2395
1949
|
// raw digest is not accepted as plan authority because the error
|
|
2396
|
-
// is returned and verify therefore fails closed.
|
|
1950
|
+
// is returned and verify therefore fails closed. The legacy
|
|
1951
|
+
// contract filters consumer-owned transport metadata; the
|
|
1952
|
+
// declared-manifest contract never excludes anything.
|
|
2397
1953
|
try {
|
|
2398
1954
|
const installedSnapshot = await computeFrozenSnapshot(installPath, {
|
|
2399
|
-
excludeRootEntries:
|
|
1955
|
+
excludeRootEntries: action.payloadContract === undefined
|
|
1956
|
+
? getPlatform(consumer).knownHostArtifacts
|
|
1957
|
+
: [],
|
|
2400
1958
|
});
|
|
2401
1959
|
manifestDigest = installedSnapshot.digest;
|
|
2402
1960
|
} catch {
|
|
@@ -2413,20 +1971,15 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2413
1971
|
entrySkill: action.entrySkill,
|
|
2414
1972
|
manifestDigest,
|
|
2415
1973
|
consumer,
|
|
1974
|
+
...extraInstalledPathsAudit(payloadBinding),
|
|
2416
1975
|
};
|
|
2417
1976
|
|
|
2418
|
-
// Fields from CLI evidence only
|
|
2419
|
-
//
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
observation.
|
|
2424
|
-
observation.marketplace = idParts.slice(1).join('@');
|
|
2425
|
-
if (found.version) observation.version = found.version;
|
|
2426
|
-
} else if (consumer === 'codex') {
|
|
2427
|
-
if (found.name) observation.plugin = found.name;
|
|
2428
|
-
if (found.marketplaceName) observation.marketplace = found.marketplaceName;
|
|
2429
|
-
if (found.version) observation.version = found.version;
|
|
1977
|
+
// Fields from CLI evidence only, extracted by the platform strategy
|
|
1978
|
+
// (a non-automatable platform observe returns via the attestation
|
|
1979
|
+
// path above and never reaches this point). Key insertion order is
|
|
1980
|
+
// strategy-owned and mirrors the legacy backfill.
|
|
1981
|
+
if (platform && platform.strategy.extractListIdentity) {
|
|
1982
|
+
Object.assign(observation, platform.strategy.extractListIdentity(found));
|
|
2430
1983
|
}
|
|
2431
1984
|
|
|
2432
1985
|
// Cross-validate version: evidence vs CLI
|
|
@@ -2446,7 +1999,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
2446
1999
|
|
|
2447
2000
|
// Verify installed manifest name/version matches CLI/evidence
|
|
2448
2001
|
try {
|
|
2449
|
-
const installedManifestPath = resolve(installPath,
|
|
2002
|
+
const installedManifestPath = resolve(installPath, platform.manifestPaths.plugin);
|
|
2450
2003
|
const installedManifestContent = await readFile(installedManifestPath, 'utf8');
|
|
2451
2004
|
const installedManifest = JSON.parse(installedManifestContent);
|
|
2452
2005
|
const expectedName = observation.plugin;
|