release-skill 0.1.7 → 0.1.9
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 +27 -0
- package/CHANGELOG.md +56 -0
- package/INSTALL.md +73 -2
- package/INSTALL.zh-CN.md +94 -99
- package/README.md +65 -39
- package/README.zh-CN.md +92 -325
- 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 +1114 -183
- package/adapters/claude/schemas/.render-manifest.json +4 -4
- package/adapters/claude/schemas/release-plan.schema.json +20 -2
- package/adapters/claude/schemas/release-project.schema.json +22 -4
- package/adapters/claude/schemas/release-run.schema.json +1 -0
- package/adapters/codex/.codex-plugin/plugin.json +2 -2
- package/adapters/codex/bin/release-skill.bundle.mjs +1114 -183
- package/adapters/codex/schemas/.render-manifest.json +4 -4
- package/adapters/codex/schemas/release-plan.schema.json +20 -2
- package/adapters/codex/schemas/release-project.schema.json +22 -4
- package/adapters/codex/schemas/release-run.schema.json +1 -0
- package/adapters/kimi/.kimi-plugin/plugin.json +27 -0
- package/adapters/kimi/bin/release-skill.bundle.mjs +84467 -0
- package/adapters/kimi/bin/release-skill.mjs +54 -0
- package/adapters/kimi/native/safe-write/binding.gyp +41 -0
- package/adapters/kimi/native/safe-write/prebuilds/darwin-arm64/safe_write.node +0 -0
- package/adapters/kimi/native/safe-write/prebuilds.json +24 -0
- package/adapters/kimi/native/safe-write/src/safe_write.cc +2032 -0
- package/adapters/kimi/schemas/.render-manifest.json +37 -0
- package/adapters/kimi/schemas/approval-record.schema.json +115 -0
- package/adapters/kimi/schemas/artifact-lock.schema.json +111 -0
- package/adapters/kimi/schemas/artifact-plan.schema.json +52 -0
- package/adapters/kimi/schemas/artifact-policy.schema.json +76 -0
- package/adapters/kimi/schemas/evidence-event.schema.json +89 -0
- package/adapters/kimi/schemas/release-plan.schema.json +878 -0
- package/adapters/kimi/schemas/release-project.schema.json +895 -0
- package/adapters/kimi/schemas/release-run.schema.json +343 -0
- package/adapters/kimi/skills/release-assess/SKILL.md +58 -0
- package/adapters/kimi/skills/release-help/SKILL.md +84 -0
- package/adapters/kimi/skills/release-prepare/SKILL.md +99 -0
- package/adapters/kimi/skills/release-publish/SKILL.md +64 -0
- package/adapters/kimi/skills/release-reconcile/SKILL.md +80 -0
- package/adapters/kimi/skills/release-setup/SKILL.md +102 -0
- package/adapters/kimi/skills/release-verify/SKILL.md +77 -0
- package/bin/release-skill-cli.mjs +16 -4
- package/bin/release-skill.bundle.mjs +1114 -183
- package/package.json +4 -2
- package/schemas/.render-manifest.json +4 -4
- package/schemas/release-plan.schema.json +20 -2
- package/schemas/release-project.schema.json +22 -4
- package/schemas/release-run.schema.json +1 -0
- package/src/adapters/contract.mjs +1 -0
- package/src/adapters/plugin-marketplace.mjs +1109 -49
- package/src/commands/assess.mjs +50 -1
- package/src/commands/prepare.mjs +65 -0
- package/src/commands/publish.mjs +3 -0
- package/src/commands/reconcile.mjs +3 -0
- package/src/commands/setup.mjs +10 -5
- package/src/commands/verify.mjs +16 -6
- package/src/core/plan.mjs +118 -0
- package/src/core/verification-gates.mjs +1 -1
- package/src/producers/build-adapters.mjs +38 -8
- package/src/snapshot/frozen.mjs +4 -3
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
|
|
29
29
|
import { createHash } from 'node:crypto';
|
|
30
30
|
import { computeFrozenSnapshot, resolveFrozenPath } from '../snapshot/frozen.mjs';
|
|
31
|
+
import { computePlanDigest } from '../core/plan.mjs';
|
|
32
|
+
import { canonicalJson } from '../core/digest.mjs';
|
|
31
33
|
|
|
32
34
|
const execFile = promisify(execFileCb);
|
|
33
35
|
|
|
@@ -46,6 +48,93 @@ function transportPayload(entries) {
|
|
|
46
48
|
}));
|
|
47
49
|
}
|
|
48
50
|
|
|
51
|
+
// Consumer-owned transport metadata written into the plugin install root
|
|
52
|
+
// that is not part of the published payload. Codex checks out the
|
|
53
|
+
// repository (root `.git` metadata); Claude marks in-use plugin checkouts
|
|
54
|
+
// with an empty root `.in_use` marker. Exclusions apply to root entries
|
|
55
|
+
// only; all payload paths keep the fail-closed file checks.
|
|
56
|
+
function consumerTransportExclusions(consumer) {
|
|
57
|
+
if (consumer === 'claude') return ['.in_use'];
|
|
58
|
+
if (consumer === 'codex' || consumer === 'kimi') return ['.git'];
|
|
59
|
+
return [];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extract the marketplace plugin entry's declared source as a validated,
|
|
64
|
+
* normalized snapshot-relative subpath ("." for root layouts).
|
|
65
|
+
*
|
|
66
|
+
* The rejection set is preserved verbatim from the preflight safety checks:
|
|
67
|
+
* non-empty string, no absolute paths, no ".." traversal (substring check,
|
|
68
|
+
* deliberately stricter than per-segment), no backslashes, no remote URLs.
|
|
69
|
+
* Normalization runs AFTER validation and collapses "./", ".", and trailing
|
|
70
|
+
* slashes. Throws with the preflight's exact error messages.
|
|
71
|
+
*/
|
|
72
|
+
function extractDeclaredPluginSource(consumer, entry) {
|
|
73
|
+
const rawSource = consumer === 'claude'
|
|
74
|
+
? entry.source
|
|
75
|
+
: entry.source?.source === 'local' ? entry.source?.path : null;
|
|
76
|
+
if (typeof rawSource !== 'string' || rawSource.length === 0) {
|
|
77
|
+
throw new Error(`marketplace plugin entry source must be a non-empty relative path${consumer === 'codex' ? ' (object with source:"local")' : ''}, got ${JSON.stringify(entry.source)}`);
|
|
78
|
+
}
|
|
79
|
+
if (
|
|
80
|
+
rawSource.startsWith('/') ||
|
|
81
|
+
rawSource.includes('..') ||
|
|
82
|
+
rawSource.includes('\\') ||
|
|
83
|
+
/^https?:\/\//i.test(rawSource)
|
|
84
|
+
) {
|
|
85
|
+
throw new Error(`marketplace plugin entry source "${rawSource}" is not a safe relative path`);
|
|
86
|
+
}
|
|
87
|
+
const segments = rawSource.split('/').filter((segment) => segment !== '' && segment !== '.');
|
|
88
|
+
// Redundant post-normalization invariant: ".." was already rejected by the
|
|
89
|
+
// substring check above; fail closed if it ever survives normalization.
|
|
90
|
+
if (segments.some((segment) => segment === '..')) {
|
|
91
|
+
throw new Error(`marketplace plugin entry source "${rawSource}" is not a safe relative path`);
|
|
92
|
+
}
|
|
93
|
+
return segments.length === 0 ? '.' : segments.join('/');
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve the payload subpath the consumer CLI installs for this action,
|
|
98
|
+
* read from the marketplace manifest inside the digest-verified frozen
|
|
99
|
+
* snapshot. Returns "." when the whole snapshot is the installed payload
|
|
100
|
+
* (root layouts, and kimi which has no marketplace manifest).
|
|
101
|
+
*
|
|
102
|
+
* Throws (fail closed) if the manifest is absent from the verified entries,
|
|
103
|
+
* unreadable, names a different marketplace, or does not declare exactly
|
|
104
|
+
* one plugins[] entry for action.plugin. The manifest itself lives inside
|
|
105
|
+
* the digest-sealed snapshot, so the declared subpath is authority-bound:
|
|
106
|
+
* tampering with it fails the snapshot digest revalidation first.
|
|
107
|
+
*/
|
|
108
|
+
async function resolveInstalledPayloadSubpath(snapshotDir, sourceEntries, action, consumer) {
|
|
109
|
+
if (consumer === 'kimi') return '.';
|
|
110
|
+
const marketplaceRelative = consumer === 'claude'
|
|
111
|
+
? '.claude-plugin/marketplace.json'
|
|
112
|
+
: '.agents/plugins/marketplace.json';
|
|
113
|
+
// Anchor the manifest read to the digest-verified entry walk: the target
|
|
114
|
+
// must be one of the regular files that already passed the fail-closed
|
|
115
|
+
// read checks (O_NOFOLLOW, single link, before/after stat stability).
|
|
116
|
+
const anchored = sourceEntries.some((entry) => entry.type === 'file' && entry.path === marketplaceRelative);
|
|
117
|
+
if (!anchored) {
|
|
118
|
+
throw new Error(`frozen snapshot is missing the marketplace manifest ${marketplaceRelative}`);
|
|
119
|
+
}
|
|
120
|
+
const result = await validateManifestFile(resolve(snapshotDir, marketplaceRelative), ['name', 'plugins']);
|
|
121
|
+
if (!result.valid) {
|
|
122
|
+
throw new Error(`frozen snapshot ${marketplaceRelative} invalid: ${result.error}`);
|
|
123
|
+
}
|
|
124
|
+
if (result.manifest.name !== action.marketplace) {
|
|
125
|
+
throw new Error(`marketplace manifest name "${result.manifest.name}" does not match action marketplace "${action.marketplace}"`);
|
|
126
|
+
}
|
|
127
|
+
const plugins = result.manifest.plugins;
|
|
128
|
+
if (!Array.isArray(plugins)) {
|
|
129
|
+
throw new Error(`${marketplaceRelative} must have a plugins[] array`);
|
|
130
|
+
}
|
|
131
|
+
const matches = plugins.filter((entry) => entry.name === action.plugin);
|
|
132
|
+
if (matches.length !== 1) {
|
|
133
|
+
throw new Error(`expected exactly one plugins[] entry with name "${action.plugin}", found ${matches.length}`);
|
|
134
|
+
}
|
|
135
|
+
return extractDeclaredPluginSource(consumer, matches[0]);
|
|
136
|
+
}
|
|
137
|
+
|
|
49
138
|
async function verifyInstalledMarketplacePayload(action, context, installPath, consumer) {
|
|
50
139
|
const sourcePath = await resolveFrozenPath(
|
|
51
140
|
context.root,
|
|
@@ -56,11 +145,34 @@ async function verifyInstalledMarketplacePayload(action, context, installPath, c
|
|
|
56
145
|
if (sourceSnapshot.digest !== action.manifestDigest) {
|
|
57
146
|
throw new Error('frozen marketplace snapshot digest no longer matches the plan');
|
|
58
147
|
}
|
|
148
|
+
// Consumer marketplaces install only the plugin entry's declared source
|
|
149
|
+
// subtree (e.g. "./adapters/claude"), not the whole unit snapshot. The
|
|
150
|
+
// sealed whole-snapshot digest above remains the authority; bind the
|
|
151
|
+
// installed payload to that snapshot's declared subtree. Root layouts
|
|
152
|
+
// and kimi keep the whole-tree comparison ("." subpath, no filtering).
|
|
153
|
+
const payloadSubpath = await resolveInstalledPayloadSubpath(
|
|
154
|
+
sourcePath,
|
|
155
|
+
sourceSnapshot.entries,
|
|
156
|
+
action,
|
|
157
|
+
consumer,
|
|
158
|
+
);
|
|
159
|
+
const prefix = payloadSubpath === '.' ? null : `${payloadSubpath}/`;
|
|
160
|
+
const authorityEntries = prefix === null
|
|
161
|
+
? sourceSnapshot.entries
|
|
162
|
+
: sourceSnapshot.entries
|
|
163
|
+
// The trailing slash keeps sibling directories (e.g.
|
|
164
|
+
// "adapters/claude-x") out of the comparison set. Prefix removal on
|
|
165
|
+
// a path-sorted array is order-preserving, so no re-sort is needed.
|
|
166
|
+
.filter((entry) => entry.path.startsWith(prefix))
|
|
167
|
+
.map((entry) => ({ ...entry, path: entry.path.slice(prefix.length) }));
|
|
168
|
+
if (authorityEntries.length === 0) {
|
|
169
|
+
throw new Error('frozen snapshot contains no payload under the declared marketplace source');
|
|
170
|
+
}
|
|
59
171
|
const installedSnapshot = await computeFrozenSnapshot(installPath, {
|
|
60
|
-
excludeRootEntries: consumer
|
|
172
|
+
excludeRootEntries: consumerTransportExclusions(consumer),
|
|
61
173
|
});
|
|
62
174
|
if (
|
|
63
|
-
JSON.stringify(transportPayload(
|
|
175
|
+
JSON.stringify(transportPayload(authorityEntries))
|
|
64
176
|
!== JSON.stringify(transportPayload(installedSnapshot.entries))
|
|
65
177
|
) {
|
|
66
178
|
throw new Error('installed marketplace payload differs in path, bytes, size, or non-write mode bits');
|
|
@@ -81,11 +193,592 @@ async function writeEvidenceAtomic(filePath, value) {
|
|
|
81
193
|
}
|
|
82
194
|
}
|
|
83
195
|
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
// Kimi Code protocol-gap modeling (BLOCKER-1 / MAJOR-1 / MAJOR-4 / MINOR-1).
|
|
198
|
+
//
|
|
199
|
+
// Kimi Code has NO scriptable plugin install/list CLI: plugin management is
|
|
200
|
+
// interactive-only (`/plugins install <path-or-url>` in the TUI). There is no
|
|
201
|
+
// `kimi plugins ...` subcommand and no `--json` output protocol. Therefore the
|
|
202
|
+
// kimi-marketplace-install action is modeled as a protocol capability gap:
|
|
203
|
+
//
|
|
204
|
+
// - execute NEVER execs a kimi CLI. It emits an actionable, version-pinned
|
|
205
|
+
// manual-install requirement bound to the frozen plan digest + identity.
|
|
206
|
+
// - observe consumes a structured human attestation (written after the
|
|
207
|
+
// operator runs the interactive install) plus read-only verification of
|
|
208
|
+
// the installed managed copy. Missing/expired/mismatched/escaping proof
|
|
209
|
+
// fails closed, so a kimi unit can never reach VERIFIED without it.
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
/** Structured manual-install requirement written by kimi execute. */
|
|
213
|
+
const KIMI_REQUIREMENT_FILE = 'release-skill-kimi-manual-install.json';
|
|
214
|
+
/** Structured human attestation consumed by kimi observe. */
|
|
215
|
+
const KIMI_ATTESTATION_FILE = 'release-skill-kimi-attestation.json';
|
|
216
|
+
/** Kimi Code managed install layout: $KIMI_CODE_HOME/plugins/managed/<id>/. */
|
|
217
|
+
const KIMI_MANAGED_SUBPATH = join('plugins', 'managed');
|
|
218
|
+
/** Maximum attestation validity window (mirrors the 24h approval expiry). */
|
|
219
|
+
const KIMI_MAX_ATTESTATION_VALIDITY_MS = 24 * 60 * 60 * 1000;
|
|
220
|
+
|
|
221
|
+
/** 64-char lowercase hex plan/payload digest pattern. */
|
|
222
|
+
const HEX_DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Normalize a plan back to its frozen form for digest comparison.
|
|
226
|
+
*
|
|
227
|
+
* Only lifecycle status fields are reset: the top-level `status` returns to
|
|
228
|
+
* "PREPARED" and every `externalActions[].status` returns to "PENDING". Every
|
|
229
|
+
* other field is preserved verbatim. publish/reconcile/verify mutate exactly
|
|
230
|
+
* these status fields in memory as the saga progresses, so normalizing them
|
|
231
|
+
* recovers the frozen digest while leaving all security-relevant fields
|
|
232
|
+
* (baseline, units, action parameters/expected, production config, …) intact.
|
|
233
|
+
*
|
|
234
|
+
* @param {object} plan
|
|
235
|
+
* @returns {object} the lifecycle-normalized plan
|
|
236
|
+
*/
|
|
237
|
+
function normalizePlanForDigest(plan) {
|
|
238
|
+
const normalized = { ...plan, status: 'PREPARED' };
|
|
239
|
+
if (Array.isArray(plan.externalActions)) {
|
|
240
|
+
normalized.externalActions = plan.externalActions.map((action) => (
|
|
241
|
+
action && typeof action === 'object' && !Array.isArray(action)
|
|
242
|
+
? { ...action, status: 'PENDING' }
|
|
243
|
+
: action
|
|
244
|
+
));
|
|
245
|
+
}
|
|
246
|
+
return normalized;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Resolve and verify the genuine frozen plan digest from the adapter context.
|
|
251
|
+
*
|
|
252
|
+
* The kimi manual-install requirement and attestation bind to the REAL frozen
|
|
253
|
+
* plan digest (`context.plan.digest`) — never to `action.manifestDigest`, which
|
|
254
|
+
* is only the snapshot payload digest.
|
|
255
|
+
*
|
|
256
|
+
* Integrity model: the carried `context.plan.digest` is recomputed from the
|
|
257
|
+
* lifecycle-normalized plan and must match EXACTLY. Status transitions
|
|
258
|
+
* (top-level status, per-action checkpoint status) are normalized away, but any
|
|
259
|
+
* other field tamper changes the recomputed digest and fails closed. This
|
|
260
|
+
* proves the attestation is bound to the genuine frozen plan, not to a spoofed
|
|
261
|
+
* or mutated stand-in.
|
|
262
|
+
*
|
|
263
|
+
* @param {object} context - adapter context (must carry the frozen `plan`).
|
|
264
|
+
* @returns {string} the verified frozen plan digest.
|
|
265
|
+
* @throws {Error} when the plan is absent or the digest does not match.
|
|
266
|
+
*/
|
|
267
|
+
function resolveBoundPlanDigest(context) {
|
|
268
|
+
const plan = context?.plan;
|
|
269
|
+
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
270
|
+
throw new Error('context.plan is required to bind the kimi plan digest');
|
|
271
|
+
}
|
|
272
|
+
const carried = plan.digest;
|
|
273
|
+
if (typeof carried !== 'string' || !HEX_DIGEST_RE.test(carried)) {
|
|
274
|
+
throw new Error('context.plan.digest must be a 64-char lowercase hex frozen plan digest');
|
|
275
|
+
}
|
|
276
|
+
const normalized = normalizePlanForDigest(plan);
|
|
277
|
+
if (computePlanDigest(normalized) !== carried) {
|
|
278
|
+
throw new Error('context.plan.digest does not match the normalized frozen plan (a non-lifecycle field was tampered)');
|
|
279
|
+
}
|
|
280
|
+
return carried;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Authoritative, cross-run attestation directory for a kimi install.
|
|
285
|
+
*
|
|
286
|
+
* Lives at a stable root-fixed location keyed by the verified frozen plan
|
|
287
|
+
* digest and plugin id:
|
|
288
|
+
* <root>/.release-skill/kimi-attestations/<planDigest>/<plugin>/
|
|
289
|
+
*
|
|
290
|
+
* This survives the publish -> manual install -> reconcile -> verify chain,
|
|
291
|
+
* where each command otherwise uses a fresh runDir (an attestation written to a
|
|
292
|
+
* publish runDir would be invisible to reconcile/verify). Both the requirement
|
|
293
|
+
* and the human attestation live here. The segments are pre-validated (planDigest
|
|
294
|
+
* is 64-hex, plugin matches SAFE_ID_RE) and the resolved path is contained
|
|
295
|
+
* within the authority base, so no path escape is possible.
|
|
296
|
+
*
|
|
297
|
+
* @param {object} context - adapter context (needs `root`).
|
|
298
|
+
* @param {string} planDigest - verified frozen plan digest (64-hex).
|
|
299
|
+
* @param {string} plugin - plugin id (SAFE_ID_RE).
|
|
300
|
+
* @returns {string} absolute authority directory.
|
|
301
|
+
*/
|
|
302
|
+
function kimiAuthorityDir(context, planDigest, plugin) {
|
|
303
|
+
if (!context?.root) {
|
|
304
|
+
throw new Error('context.root is required for the kimi attestation authority');
|
|
305
|
+
}
|
|
306
|
+
if (!HEX_DIGEST_RE.test(planDigest)) {
|
|
307
|
+
throw new Error('kimi attestation authority requires a 64-hex plan digest');
|
|
308
|
+
}
|
|
309
|
+
if (!SAFE_ID_RE.test(plugin)) {
|
|
310
|
+
throw new Error(`kimi attestation authority requires a safe plugin id: "${plugin}"`);
|
|
311
|
+
}
|
|
312
|
+
const base = resolve(context.root, '.release-skill', 'kimi-attestations');
|
|
313
|
+
const dir = resolve(base, planDigest, plugin);
|
|
314
|
+
const rel = relative(base, dir);
|
|
315
|
+
const sep = process.platform === 'win32' ? '\\' : '/';
|
|
316
|
+
if (
|
|
317
|
+
rel === '' || rel === '..' || isAbsolute(rel) || rel.startsWith(`..${sep}`)
|
|
318
|
+
|| rel.split(sep).some((segment) => segment === '..' || segment === '')
|
|
319
|
+
) {
|
|
320
|
+
throw new Error('kimi attestation authority path escapes its base');
|
|
321
|
+
}
|
|
322
|
+
return dir;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* Build the official, version-pinned install URL for a frozen Git ref.
|
|
327
|
+
*
|
|
328
|
+
* Prefers the GitHub release-tag URL (`/releases/tag/<ref>`), which pins the
|
|
329
|
+
* exact published ref; `/tree/<ref>` is the documented equivalent. A bare
|
|
330
|
+
* repository URL is NOT acceptable because it installs the latest release (or
|
|
331
|
+
* default branch), which need not equal the frozen version.
|
|
332
|
+
*
|
|
333
|
+
* @param {string} repo - owner/repo
|
|
334
|
+
* @param {string} ref - frozen Git ref (tag)
|
|
335
|
+
* @returns {string}
|
|
336
|
+
*/
|
|
337
|
+
function buildKimiInstallUrl(repo, ref) {
|
|
338
|
+
return `https://github.com/${repo}/releases/tag/${ref}`;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Human-facing, actionable manual-install closed-loop instructions for Kimi Code.
|
|
343
|
+
*
|
|
344
|
+
* @param {{installUrl:string, plugin:string, version:string, ref:string, isolatedHome:string, attestationDir:string}} p
|
|
345
|
+
* @returns {string[]}
|
|
346
|
+
*/
|
|
347
|
+
function buildKimiManualInstructions({ installUrl, plugin, version, ref, isolatedHome, attestationDir }) {
|
|
348
|
+
return [
|
|
349
|
+
`Kimi Code has no scriptable plugin-install CLI; installation is a manual, interactive step.`,
|
|
350
|
+
`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).`,
|
|
351
|
+
`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}/".`,
|
|
352
|
+
`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).`,
|
|
353
|
+
`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.`,
|
|
354
|
+
` Required fields: consumer="kimi", plugin, version, entrySkill, repo, ref, installPath, planDigest, payloadDigest, attestedBy, attestedAt, expiresAt.`,
|
|
355
|
+
`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.`,
|
|
356
|
+
`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.`,
|
|
357
|
+
];
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Read the authoritative Kimi plugin manifest from a verified plugin root.
|
|
362
|
+
*
|
|
363
|
+
* `kimi.plugin.json` at the root takes priority over `.kimi-plugin/plugin.json`
|
|
364
|
+
* when both exist (official precedence). Returns the parsed manifest and the
|
|
365
|
+
* root-relative manifest path. Throws when no valid manifest is present.
|
|
366
|
+
*
|
|
367
|
+
* @param {string} pluginRootReal - realpath of the verified plugin root.
|
|
368
|
+
* @returns {Promise<{manifest:object, manifestRelative:string}>}
|
|
369
|
+
*/
|
|
370
|
+
async function readKimiManifest(pluginRootReal) {
|
|
371
|
+
const candidates = [
|
|
372
|
+
'kimi.plugin.json',
|
|
373
|
+
join('.kimi-plugin', 'plugin.json'),
|
|
374
|
+
];
|
|
375
|
+
for (const manifestRelative of candidates) {
|
|
376
|
+
const manifestPath = resolve(pluginRootReal, manifestRelative);
|
|
377
|
+
let content;
|
|
378
|
+
try {
|
|
379
|
+
content = await readFile(manifestPath, 'utf8');
|
|
380
|
+
} catch {
|
|
381
|
+
continue;
|
|
382
|
+
}
|
|
383
|
+
let manifest;
|
|
384
|
+
try {
|
|
385
|
+
manifest = JSON.parse(content);
|
|
386
|
+
} catch {
|
|
387
|
+
throw new Error(`kimi plugin manifest ${manifestRelative} is not valid JSON`);
|
|
388
|
+
}
|
|
389
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
390
|
+
throw new Error(`kimi plugin manifest ${manifestRelative} is not an object`);
|
|
391
|
+
}
|
|
392
|
+
return { manifest, manifestRelative };
|
|
393
|
+
}
|
|
394
|
+
throw new Error('no kimi plugin manifest found (expected kimi.plugin.json or .kimi-plugin/plugin.json)');
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Validate that a manifest `skills` value is a safe plugin-root-relative path.
|
|
399
|
+
*
|
|
400
|
+
* Accepts "./" or "./some/dir/" forms. Rejects absolute paths, ".." traversal,
|
|
401
|
+
* backslashes, and URLs. Returns the normalized root-relative path (no leading
|
|
402
|
+
* "./"). Throws on unsafe values.
|
|
403
|
+
*
|
|
404
|
+
* @param {string} skillsRaw
|
|
405
|
+
* @returns {string} normalized relative path ('' for the plugin root itself)
|
|
406
|
+
*/
|
|
407
|
+
function normalizeKimiSkillsRel(skillsRaw) {
|
|
408
|
+
if (typeof skillsRaw !== 'string' || skillsRaw.length === 0) {
|
|
409
|
+
throw new Error('kimi manifest skills must be a non-empty relative path when present');
|
|
410
|
+
}
|
|
411
|
+
if (
|
|
412
|
+
skillsRaw.startsWith('/') ||
|
|
413
|
+
skillsRaw.includes('..') ||
|
|
414
|
+
skillsRaw.includes('\\') ||
|
|
415
|
+
/^https?:\/\//i.test(skillsRaw)
|
|
416
|
+
) {
|
|
417
|
+
throw new Error(`kimi manifest skills "${skillsRaw}" is not a safe relative path`);
|
|
418
|
+
}
|
|
419
|
+
let rel = skillsRaw.replace(/^\.\//, '');
|
|
420
|
+
rel = rel.replace(/\/+$/, '');
|
|
421
|
+
if (rel.split('/').some((segment) => segment === '' || segment === '.' || segment === '..')) {
|
|
422
|
+
throw new Error(`kimi manifest skills "${skillsRaw}" is not a safe relative path`);
|
|
423
|
+
}
|
|
424
|
+
return rel;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Resolve the entry SKILL.md for a Kimi plugin from its authoritative manifest.
|
|
429
|
+
*
|
|
430
|
+
* - When the manifest declares `skills`, the entry skill resolves under that
|
|
431
|
+
* skills root (validated + realpath-contained within the plugin root).
|
|
432
|
+
* - When `skills` is omitted, Kimi's official single-skill semantics apply: the
|
|
433
|
+
* plugin root's own SKILL.md is the single skill.
|
|
434
|
+
*
|
|
435
|
+
* The returned path is realpath-contained within pluginRootReal and is a
|
|
436
|
+
* regular, non-symlink file. Throws on missing/escaping/invalid layouts so the
|
|
437
|
+
* caller fails closed.
|
|
438
|
+
*
|
|
439
|
+
* @param {string} pluginRootReal - realpath of the verified plugin root.
|
|
440
|
+
* @param {object} manifest - parsed kimi plugin manifest.
|
|
441
|
+
* @param {string} entrySkill - expected entry skill id.
|
|
442
|
+
* @returns {Promise<string>} realpath of the entry SKILL.md
|
|
443
|
+
*/
|
|
444
|
+
async function resolveKimiEntrySkillFile(pluginRootReal, manifest, entrySkill) {
|
|
445
|
+
if (!entrySkill || typeof entrySkill !== 'string' || !SAFE_ID_RE.test(entrySkill)) {
|
|
446
|
+
throw new Error(`unsafe entrySkill: "${entrySkill}"`);
|
|
447
|
+
}
|
|
448
|
+
let entryAbs;
|
|
449
|
+
if (manifest.skills === undefined || manifest.skills === null) {
|
|
450
|
+
// Official single-skill semantics: root SKILL.md is the sole skill.
|
|
451
|
+
entryAbs = resolve(pluginRootReal, 'SKILL.md');
|
|
452
|
+
} else {
|
|
453
|
+
const skillsRel = normalizeKimiSkillsRel(manifest.skills);
|
|
454
|
+
const skillsRootAbs = skillsRel === '' ? pluginRootReal : resolve(pluginRootReal, skillsRel);
|
|
455
|
+
const skillsRootReal = await realpath(skillsRootAbs).catch(() => null);
|
|
456
|
+
if (!skillsRootReal) {
|
|
457
|
+
throw new Error(`kimi manifest skills root does not exist: ${manifest.skills}`);
|
|
458
|
+
}
|
|
459
|
+
const skillsContainment = relative(pluginRootReal, skillsRootReal);
|
|
460
|
+
const sepK = process.platform === 'win32' ? '\\' : '/';
|
|
461
|
+
if (
|
|
462
|
+
skillsContainment !== '' &&
|
|
463
|
+
(isAbsolute(skillsContainment) || skillsContainment === '..' || skillsContainment.startsWith(`..${sepK}`))
|
|
464
|
+
) {
|
|
465
|
+
throw new Error(`kimi manifest skills "${manifest.skills}" escapes the plugin root after symlink resolution`);
|
|
466
|
+
}
|
|
467
|
+
entryAbs = resolve(skillsRootReal, entrySkill, 'SKILL.md');
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// lstat the LEXICAL entry BEFORE realpath. A symlinked SKILL.md must be
|
|
471
|
+
// rejected outright; lstat-ing the realpath target instead would observe the
|
|
472
|
+
// resolved regular file and silently miss the symlink.
|
|
473
|
+
let entryLexicalStat;
|
|
474
|
+
try {
|
|
475
|
+
entryLexicalStat = await lstat(entryAbs);
|
|
476
|
+
} catch {
|
|
477
|
+
throw new Error(`kimi entry skill not found: ${relative(pluginRootReal, entryAbs) || 'SKILL.md'}`);
|
|
478
|
+
}
|
|
479
|
+
if (entryLexicalStat.isSymbolicLink()) {
|
|
480
|
+
throw new Error('kimi entry skill must not be a symlink');
|
|
481
|
+
}
|
|
482
|
+
if (!entryLexicalStat.isFile()) {
|
|
483
|
+
throw new Error('kimi entry skill is not a regular file');
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
const entryReal = await realpath(entryAbs).catch(() => null);
|
|
487
|
+
if (!entryReal) {
|
|
488
|
+
throw new Error(`kimi entry skill not found: ${relative(pluginRootReal, entryAbs) || 'SKILL.md'}`);
|
|
489
|
+
}
|
|
490
|
+
const entryContainment = relative(pluginRootReal, entryReal);
|
|
491
|
+
const sepE = process.platform === 'win32' ? '\\' : '/';
|
|
492
|
+
if (
|
|
493
|
+
entryContainment !== '' &&
|
|
494
|
+
(isAbsolute(entryContainment) || entryContainment === '..' || entryContainment.startsWith(`..${sepE}`))
|
|
495
|
+
) {
|
|
496
|
+
throw new Error('kimi entry skill escapes the plugin root after symlink resolution');
|
|
497
|
+
}
|
|
498
|
+
return entryReal;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Validate a structured kimi manual-install attestation against the frozen
|
|
503
|
+
* action and the verified frozen plan digest.
|
|
504
|
+
*
|
|
505
|
+
* Bindings (fail closed on any mismatch):
|
|
506
|
+
* - `planDigest` binds to the REAL frozen plan digest (`boundPlanDigest`, from
|
|
507
|
+
* `context.plan.digest`) — NOT to `action.manifestDigest`.
|
|
508
|
+
* - `payloadDigest` binds separately to `action.manifestDigest` (the sealed
|
|
509
|
+
* snapshot payload digest).
|
|
510
|
+
* - plugin identity, version, entry skill, repo, and frozen ref must match.
|
|
511
|
+
* - Time bounds: `attestedAt` must not be in the future, the validity window
|
|
512
|
+
* (`expiresAt - attestedAt`) must not exceed 24h, and the attestation must
|
|
513
|
+
* not be expired relative to `isoNow`.
|
|
514
|
+
*
|
|
515
|
+
* @param {object} attestation - parsed attestation JSON.
|
|
516
|
+
* @param {object} action - the expanded kimi action (top-level fields).
|
|
517
|
+
* @param {string} isoNow - current ISO timestamp.
|
|
518
|
+
* @param {string} boundPlanDigest - verified frozen plan digest.
|
|
519
|
+
* @returns {{valid:boolean, error:string|null}}
|
|
520
|
+
*/
|
|
521
|
+
function validateKimiAttestation(attestation, action, isoNow, boundPlanDigest) {
|
|
522
|
+
if (!attestation || typeof attestation !== 'object' || Array.isArray(attestation)) {
|
|
523
|
+
return { valid: false, error: 'kimi attestation is not an object' };
|
|
524
|
+
}
|
|
525
|
+
const requiredStrings = ['plugin', 'version', 'entrySkill', 'repo', 'ref', 'installPath', 'payloadDigest', 'planDigest', 'attestedBy', 'attestedAt', 'expiresAt'];
|
|
526
|
+
for (const field of requiredStrings) {
|
|
527
|
+
if (typeof attestation[field] !== 'string' || attestation[field].length === 0) {
|
|
528
|
+
return { valid: false, error: `kimi attestation missing required field "${field}"` };
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
if (attestation.consumer !== 'kimi') {
|
|
532
|
+
return { valid: false, error: `kimi attestation consumer "${attestation.consumer}" must be "kimi"` };
|
|
533
|
+
}
|
|
534
|
+
if (!HEX_DIGEST_RE.test(attestation.planDigest)) {
|
|
535
|
+
return { valid: false, error: 'kimi attestation planDigest must be a 64-char lowercase hex digest' };
|
|
536
|
+
}
|
|
537
|
+
if (attestation.planDigest !== boundPlanDigest) {
|
|
538
|
+
return { valid: false, error: 'kimi attestation planDigest does not match the frozen plan digest' };
|
|
539
|
+
}
|
|
540
|
+
if (attestation.plugin !== action.plugin) {
|
|
541
|
+
return { valid: false, error: `kimi attestation plugin "${attestation.plugin}" does not match action plugin "${action.plugin}"` };
|
|
542
|
+
}
|
|
543
|
+
if (attestation.version !== action.version) {
|
|
544
|
+
return { valid: false, error: `kimi attestation version "${attestation.version}" does not match action version "${action.version}"` };
|
|
545
|
+
}
|
|
546
|
+
if (attestation.entrySkill !== action.entrySkill) {
|
|
547
|
+
return { valid: false, error: `kimi attestation entrySkill "${attestation.entrySkill}" does not match action entrySkill "${action.entrySkill}"` };
|
|
548
|
+
}
|
|
549
|
+
if (attestation.repo !== action.repo) {
|
|
550
|
+
return { valid: false, error: `kimi attestation repo "${attestation.repo}" does not match action repo "${action.repo}"` };
|
|
551
|
+
}
|
|
552
|
+
const expectedRef = action.ref ?? `v${action.version}`;
|
|
553
|
+
if (attestation.ref !== expectedRef) {
|
|
554
|
+
return { valid: false, error: `kimi attestation ref "${attestation.ref}" does not match frozen ref "${expectedRef}"` };
|
|
555
|
+
}
|
|
556
|
+
if (attestation.payloadDigest !== action.manifestDigest) {
|
|
557
|
+
return { valid: false, error: 'kimi attestation payloadDigest does not match the frozen payload digest' };
|
|
558
|
+
}
|
|
559
|
+
const attestedMs = Date.parse(attestation.attestedAt);
|
|
560
|
+
const expiresMs = Date.parse(attestation.expiresAt);
|
|
561
|
+
const nowMs = Date.parse(isoNow);
|
|
562
|
+
if (!Number.isFinite(attestedMs) || !Number.isFinite(expiresMs) || !Number.isFinite(nowMs)) {
|
|
563
|
+
return { valid: false, error: 'kimi attestation attestedAt/expiresAt must be valid ISO timestamps' };
|
|
564
|
+
}
|
|
565
|
+
if (attestedMs > nowMs) {
|
|
566
|
+
return { valid: false, error: 'kimi attestation attestedAt is in the future' };
|
|
567
|
+
}
|
|
568
|
+
if (expiresMs <= attestedMs) {
|
|
569
|
+
return { valid: false, error: 'kimi attestation expiresAt must be after attestedAt' };
|
|
570
|
+
}
|
|
571
|
+
if (expiresMs - attestedMs > KIMI_MAX_ATTESTATION_VALIDITY_MS) {
|
|
572
|
+
return { valid: false, error: 'kimi attestation validity must not exceed 24 hours' };
|
|
573
|
+
}
|
|
574
|
+
if (nowMs > expiresMs) {
|
|
575
|
+
return { valid: false, error: 'kimi attestation has expired' };
|
|
576
|
+
}
|
|
577
|
+
return { valid: true, error: null };
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* Kimi Code protocol capability gap (BLOCKER-1): there is NO scriptable
|
|
582
|
+
* `kimi plugins install/list` CLI and no `--json` protocol. execute NEVER execs
|
|
583
|
+
* a kimi command. Instead it emits an actionable, version-pinned manual-install
|
|
584
|
+
* requirement bound to the real frozen plan digest + identity, and leaves
|
|
585
|
+
* success to observe, which consumes only a trusted human attestation plus
|
|
586
|
+
* read-only verification. Without that proof the checkpoint fails closed and can
|
|
587
|
+
* never reach VERIFIED.
|
|
588
|
+
*
|
|
589
|
+
* Isolation model (B/C): the kimi home is a STABLE, plan-digest-keyed directory
|
|
590
|
+
* under the attestation authority (`<authorityDir>/kimi-home`), not the per-run
|
|
591
|
+
* runDir consumer dir. The operator launches Kimi Code with that KIMI_CODE_HOME
|
|
592
|
+
* so the managed copy lands at `<kimiHome>/plugins/managed/<plugin>/`, a
|
|
593
|
+
* location that is identical across publish/reconcile/verify run dirs. execute
|
|
594
|
+
* creates ONLY the managed parent (`plugins/managed`), never `managed/<plugin>`
|
|
595
|
+
* (the operator's interactive install creates that). The requirement write is
|
|
596
|
+
* idempotent: an identical existing requirement is left untouched, a divergent
|
|
597
|
+
* one fails closed.
|
|
598
|
+
*
|
|
599
|
+
* @param {object} action - expanded kimi action (validated params already).
|
|
600
|
+
* @param {object} context - adapter context (root, runDir, plan).
|
|
601
|
+
* @returns {Promise<import('./contract.mjs').AdapterResult>}
|
|
602
|
+
*/
|
|
603
|
+
async function executeKimiManualRequirement(action, context) {
|
|
604
|
+
const actionType = ActionType.KIMI_MARKETPLACE_INSTALL;
|
|
605
|
+
|
|
606
|
+
// (A) Bind to the REAL frozen plan digest via strict normalized recompute.
|
|
607
|
+
let planDigest;
|
|
608
|
+
try {
|
|
609
|
+
planDigest = resolveBoundPlanDigest(context);
|
|
610
|
+
} catch (planErr) {
|
|
611
|
+
return createResult({
|
|
612
|
+
actionType,
|
|
613
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
614
|
+
error: `cannot bind kimi requirement to the frozen plan: ${planErr.message}`,
|
|
615
|
+
});
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// Validate the frozen timeout. Kimi execs no CLI, but the frozen-timeout
|
|
619
|
+
// fail-closed invariant still holds for every marketplace action.
|
|
620
|
+
try {
|
|
621
|
+
resolveTimeoutMs(action);
|
|
622
|
+
} catch (timeoutErr) {
|
|
623
|
+
return createResult({
|
|
624
|
+
actionType,
|
|
625
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
626
|
+
error: timeoutErr.message,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
const ref = action.ref ?? `v${action.version}`;
|
|
631
|
+
const installUrl = buildKimiInstallUrl(action.repo, ref);
|
|
632
|
+
|
|
633
|
+
// (B) Stable, plan-digest-keyed authority dir — the ONLY kimi home, shared
|
|
634
|
+
// across publish/reconcile/verify run dirs.
|
|
635
|
+
let attestationDir;
|
|
636
|
+
try {
|
|
637
|
+
attestationDir = kimiAuthorityDir(context, planDigest, action.plugin);
|
|
638
|
+
} catch (dirErr) {
|
|
639
|
+
return createResult({
|
|
640
|
+
actionType,
|
|
641
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
642
|
+
error: dirErr.message,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
const kimiHome = resolve(attestationDir, 'kimi-home');
|
|
646
|
+
const managedParent = resolve(kimiHome, KIMI_MANAGED_SUBPATH); // plugins/managed
|
|
647
|
+
// plugins/managed/<plugin> — created by the operator's interactive install.
|
|
648
|
+
const managedInstallRoot = resolve(managedParent, action.plugin);
|
|
649
|
+
|
|
650
|
+
const instructions = buildKimiManualInstructions({
|
|
651
|
+
installUrl,
|
|
652
|
+
plugin: action.plugin,
|
|
653
|
+
version: action.version,
|
|
654
|
+
ref,
|
|
655
|
+
isolatedHome: kimiHome,
|
|
656
|
+
attestationDir,
|
|
657
|
+
});
|
|
658
|
+
|
|
659
|
+
const requirement = {
|
|
660
|
+
kind: 'kimi-manual-install-requirement',
|
|
661
|
+
consumer: 'kimi',
|
|
662
|
+
plugin: action.plugin,
|
|
663
|
+
version: action.version,
|
|
664
|
+
entrySkill: action.entrySkill,
|
|
665
|
+
repo: action.repo,
|
|
666
|
+
ref,
|
|
667
|
+
installUrl,
|
|
668
|
+
// (A) planDigest binds to the real frozen plan digest;
|
|
669
|
+
// expectedPayloadDigest binds separately to the snapshot payload digest.
|
|
670
|
+
planDigest,
|
|
671
|
+
expectedPayloadDigest: action.manifestDigest,
|
|
672
|
+
isolatedHome: kimiHome,
|
|
673
|
+
kimiCodeHome: kimiHome,
|
|
674
|
+
managedInstallRoot,
|
|
675
|
+
attestationDir,
|
|
676
|
+
attestationFile: KIMI_ATTESTATION_FILE,
|
|
677
|
+
attestationTemplate: {
|
|
678
|
+
consumer: 'kimi',
|
|
679
|
+
plugin: action.plugin,
|
|
680
|
+
version: action.version,
|
|
681
|
+
entrySkill: action.entrySkill,
|
|
682
|
+
repo: action.repo,
|
|
683
|
+
ref,
|
|
684
|
+
installPath: managedInstallRoot,
|
|
685
|
+
planDigest,
|
|
686
|
+
payloadDigest: action.manifestDigest,
|
|
687
|
+
attestedBy: '<person responsible for the manual install>',
|
|
688
|
+
attestedAt: '<ISO 8601 now; must not be in the future>',
|
|
689
|
+
expiresAt: '<ISO 8601; within 24h of attestedAt>',
|
|
690
|
+
},
|
|
691
|
+
instructions,
|
|
692
|
+
};
|
|
693
|
+
|
|
694
|
+
// Create ONLY the managed parent (plugins/managed); never pre-create
|
|
695
|
+
// managed/<plugin> — the operator's interactive install creates that.
|
|
696
|
+
try {
|
|
697
|
+
await mkdir(managedParent, { recursive: true, mode: 0o700 });
|
|
698
|
+
} catch (mkdirErr) {
|
|
699
|
+
return createResult({
|
|
700
|
+
actionType,
|
|
701
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
702
|
+
error: `cannot create kimi managed parent directory: ${mkdirErr.message}`,
|
|
703
|
+
});
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
// Idempotent requirement write: an identical existing requirement is left
|
|
707
|
+
// untouched; a divergent existing requirement fails closed (never silently
|
|
708
|
+
// overwritten). `createdAt` is volatile and excluded from the comparison.
|
|
709
|
+
const requirementPath = resolve(attestationDir, KIMI_REQUIREMENT_FILE);
|
|
710
|
+
let existing = null;
|
|
711
|
+
let requirementMissing = false;
|
|
712
|
+
try {
|
|
713
|
+
const existingRaw = await readFile(requirementPath, 'utf8');
|
|
714
|
+
try {
|
|
715
|
+
existing = JSON.parse(existingRaw);
|
|
716
|
+
} catch (parseErr) {
|
|
717
|
+
return createResult({
|
|
718
|
+
actionType,
|
|
719
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
720
|
+
error: `existing kimi manual-install requirement is invalid JSON; refusing to overwrite: ${parseErr.message}`,
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
} catch (readErr) {
|
|
724
|
+
if (readErr?.code === 'ENOENT') {
|
|
725
|
+
requirementMissing = true;
|
|
726
|
+
} else {
|
|
727
|
+
return createResult({
|
|
728
|
+
actionType,
|
|
729
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
730
|
+
error: `existing kimi manual-install requirement cannot be read; refusing to overwrite: ${readErr.message}`,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (!requirementMissing) {
|
|
735
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
736
|
+
return createResult({
|
|
737
|
+
actionType,
|
|
738
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
739
|
+
error: 'existing kimi manual-install requirement is not an object; refusing to overwrite',
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
const { createdAt: _existingCreatedAt, ...existingBody } = existing;
|
|
743
|
+
if (canonicalJson(existingBody) !== canonicalJson(requirement)) {
|
|
744
|
+
return createResult({
|
|
745
|
+
actionType,
|
|
746
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
747
|
+
error: 'existing kimi manual-install requirement conflicts with the current frozen action; refusing to overwrite',
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
} else {
|
|
751
|
+
await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: new Date().toISOString() });
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
return createResult({
|
|
755
|
+
actionType,
|
|
756
|
+
status: ActionStatus.EXECUTED,
|
|
757
|
+
observation: {
|
|
758
|
+
installed: false,
|
|
759
|
+
manualInstallRequired: true,
|
|
760
|
+
consumer: 'kimi',
|
|
761
|
+
plugin: action.plugin,
|
|
762
|
+
version: action.version,
|
|
763
|
+
entrySkill: action.entrySkill,
|
|
764
|
+
repo: action.repo,
|
|
765
|
+
ref,
|
|
766
|
+
installUrl,
|
|
767
|
+
planDigest,
|
|
768
|
+
attestationDir,
|
|
769
|
+
kimiCodeHome: kimiHome,
|
|
770
|
+
managedInstallRoot,
|
|
771
|
+
instructions,
|
|
772
|
+
},
|
|
773
|
+
});
|
|
774
|
+
}
|
|
775
|
+
|
|
84
776
|
const SUPPORTED_TYPES = [
|
|
85
777
|
ActionType.PLUGIN_MANIFEST_VALIDATE,
|
|
86
778
|
ActionType.PLUGIN_INSTALL_CHECK,
|
|
87
779
|
ActionType.CLAUDE_MARKETPLACE_INSTALL,
|
|
88
780
|
ActionType.CODEX_MARKETPLACE_INSTALL,
|
|
781
|
+
ActionType.KIMI_MARKETPLACE_INSTALL,
|
|
89
782
|
];
|
|
90
783
|
|
|
91
784
|
/** Safe identifier pattern: lowercase alphanumeric, hyphens, dots, underscores. */
|
|
@@ -163,13 +856,22 @@ function validateMarketplaceParams(params) {
|
|
|
163
856
|
return { valid: false, error: 'parameters must be an object' };
|
|
164
857
|
}
|
|
165
858
|
const { consumer, plugin, marketplace, repo, version, entrySkill } = params;
|
|
166
|
-
if (!['claude', 'codex'].includes(consumer)) {
|
|
859
|
+
if (!['claude', 'codex', 'kimi'].includes(consumer)) {
|
|
167
860
|
return { valid: false, error: `invalid consumer: "${consumer}"` };
|
|
168
861
|
}
|
|
169
862
|
if (!plugin || !SAFE_ID_RE.test(plugin)) {
|
|
170
863
|
return { valid: false, error: `unsafe plugin identifier: "${plugin}"` };
|
|
171
864
|
}
|
|
172
|
-
|
|
865
|
+
// marketplace is a required identity field for Claude/Codex, which have
|
|
866
|
+
// scriptable marketplace add/install interfaces. Kimi Code has an interactive
|
|
867
|
+
// marketplace but NO non-interactive install API, so `marketplace` carries no
|
|
868
|
+
// executable meaning for kimi and is optional (validated only if present); it
|
|
869
|
+
// must not become a required identity condition for kimi execution/observe.
|
|
870
|
+
if (consumer === 'kimi') {
|
|
871
|
+
if (marketplace !== undefined && marketplace !== null && !SAFE_ID_RE.test(marketplace)) {
|
|
872
|
+
return { valid: false, error: `unsafe marketplace identifier: "${marketplace}"` };
|
|
873
|
+
}
|
|
874
|
+
} else if (!marketplace || !SAFE_ID_RE.test(marketplace)) {
|
|
173
875
|
return { valid: false, error: `unsafe marketplace identifier: "${marketplace}"` };
|
|
174
876
|
}
|
|
175
877
|
if (!repo || !SAFE_REPO_RE.test(repo)) {
|
|
@@ -373,7 +1075,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
373
1075
|
// Marketplace install preflight: fail-closed validation
|
|
374
1076
|
if (
|
|
375
1077
|
actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
|
|
376
|
-
actionType === ActionType.CODEX_MARKETPLACE_INSTALL
|
|
1078
|
+
actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
|
|
1079
|
+
actionType === ActionType.KIMI_MARKETPLACE_INSTALL
|
|
377
1080
|
) {
|
|
378
1081
|
// 1. Validate all parameters for injection safety
|
|
379
1082
|
const validation = validateMarketplaceParams(action);
|
|
@@ -449,6 +1152,9 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
449
1152
|
// 6. Verify frozen snapshot exists and contains required marketplace files
|
|
450
1153
|
const consumer = action.consumer;
|
|
451
1154
|
let snapshotDirReal;
|
|
1155
|
+
// Authoritative kimi manifest (from the frozen snapshot), used to
|
|
1156
|
+
// resolve the entry skill via the manifest-declared skills root.
|
|
1157
|
+
let kimiSnapshotManifest = null;
|
|
452
1158
|
try {
|
|
453
1159
|
snapshotDirReal = await resolveFrozenPath(context.root, snapshotPath, 'frozen snapshot path');
|
|
454
1160
|
} catch (frozenErr) {
|
|
@@ -459,6 +1165,40 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
459
1165
|
});
|
|
460
1166
|
}
|
|
461
1167
|
|
|
1168
|
+
// Kimi has no non-interactive marketplace/install API: the whole repo
|
|
1169
|
+
// is installed as one plugin. The authoritative manifest is read from
|
|
1170
|
+
// the verified snapshot root with official precedence
|
|
1171
|
+
// (kimi.plugin.json over .kimi-plugin/plugin.json).
|
|
1172
|
+
if (consumer === 'kimi') {
|
|
1173
|
+
let kimiManifestResult;
|
|
1174
|
+
try {
|
|
1175
|
+
kimiManifestResult = await readKimiManifest(snapshotDirReal);
|
|
1176
|
+
} catch (manifestErr) {
|
|
1177
|
+
return createResult({
|
|
1178
|
+
actionType,
|
|
1179
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1180
|
+
error: `frozen snapshot kimi manifest invalid: ${manifestErr.message}`,
|
|
1181
|
+
});
|
|
1182
|
+
}
|
|
1183
|
+
const kimiManifest = kimiManifestResult.manifest;
|
|
1184
|
+
if (typeof kimiManifest.name !== 'string' || kimiManifest.name !== action.plugin) {
|
|
1185
|
+
return createResult({
|
|
1186
|
+
actionType,
|
|
1187
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1188
|
+
error: `plugin manifest name "${kimiManifest.name}" does not match action plugin "${action.plugin}"`,
|
|
1189
|
+
});
|
|
1190
|
+
}
|
|
1191
|
+
if (typeof kimiManifest.version !== 'string' || kimiManifest.version !== action.version) {
|
|
1192
|
+
return createResult({
|
|
1193
|
+
actionType,
|
|
1194
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1195
|
+
error: `plugin manifest version "${kimiManifest.version}" does not match action version "${action.version}"`,
|
|
1196
|
+
});
|
|
1197
|
+
}
|
|
1198
|
+
kimiSnapshotManifest = kimiManifest;
|
|
1199
|
+
}
|
|
1200
|
+
|
|
1201
|
+
if (consumer !== 'kimi') {
|
|
462
1202
|
// Verify marketplace files exist.
|
|
463
1203
|
// marketplace.json is at the snapshot root; plugin manifest is
|
|
464
1204
|
// resolved relative to the entry's declared source path.
|
|
@@ -509,27 +1249,17 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
509
1249
|
// Entry source must be a safe relative path within the snapshot.
|
|
510
1250
|
// Accepts "./" (root-level), "./adapters/claude" (subdirectory),
|
|
511
1251
|
// etc. Rejects absolute paths, ".." traversal, remote URLs, and
|
|
512
|
-
// empty strings.
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
status: ActionStatus.PREFLIGHT_FAILED,
|
|
520
|
-
error: `marketplace plugin entry source must be a non-empty relative path${consumer === 'codex' ? ' (object with source:"local")' : ''}, got ${JSON.stringify(entry.source)}`,
|
|
521
|
-
});
|
|
522
|
-
}
|
|
523
|
-
if (
|
|
524
|
-
sourcePath.startsWith('/') ||
|
|
525
|
-
sourcePath.includes('..') ||
|
|
526
|
-
sourcePath.includes('\\') ||
|
|
527
|
-
/^https?:\/\//i.test(sourcePath)
|
|
528
|
-
) {
|
|
1252
|
+
// empty strings. Normalized to "." for root layouts; the same
|
|
1253
|
+
// helper backs verify-side payload subtree resolution, so both
|
|
1254
|
+
// paths can never drift apart.
|
|
1255
|
+
let sourcePath;
|
|
1256
|
+
try {
|
|
1257
|
+
sourcePath = extractDeclaredPluginSource(consumer, entry);
|
|
1258
|
+
} catch (sourceErr) {
|
|
529
1259
|
return createResult({
|
|
530
1260
|
actionType,
|
|
531
1261
|
status: ActionStatus.PREFLIGHT_FAILED,
|
|
532
|
-
error:
|
|
1262
|
+
error: sourceErr.message,
|
|
533
1263
|
});
|
|
534
1264
|
}
|
|
535
1265
|
// Verify the declared source directory exists and contains the
|
|
@@ -605,17 +1335,35 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
605
1335
|
error: `plugin manifest version "${pluginManifestResult.manifest.version}" does not match action version "${action.version}"`,
|
|
606
1336
|
});
|
|
607
1337
|
}
|
|
1338
|
+
}
|
|
608
1339
|
|
|
609
|
-
// Verify
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
})
|
|
1340
|
+
// Verify the entry skill exists in the snapshot.
|
|
1341
|
+
// Claude/Codex manifests always declare ./skills/, so the fixed
|
|
1342
|
+
// skills/<entrySkill>/SKILL.md layout is authoritative for them.
|
|
1343
|
+
// Kimi resolves the entry skill via the manifest-declared skills root
|
|
1344
|
+
// (MAJOR-4): the root is validated + realpath-contained, and omitted
|
|
1345
|
+
// `skills` means the official single-skill root SKILL.md.
|
|
1346
|
+
if (consumer === 'kimi') {
|
|
1347
|
+
try {
|
|
1348
|
+
await resolveKimiEntrySkillFile(snapshotDirReal, kimiSnapshotManifest, action.entrySkill);
|
|
1349
|
+
} catch (entryErr) {
|
|
1350
|
+
return createResult({
|
|
1351
|
+
actionType,
|
|
1352
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1353
|
+
error: `entry skill not resolvable in snapshot: ${entryErr.message}`,
|
|
1354
|
+
});
|
|
1355
|
+
}
|
|
1356
|
+
} else {
|
|
1357
|
+
const entrySkillFile = resolve(snapshotDirReal, 'skills', action.entrySkill, 'SKILL.md');
|
|
1358
|
+
try {
|
|
1359
|
+
await stat(entrySkillFile);
|
|
1360
|
+
} catch {
|
|
1361
|
+
return createResult({
|
|
1362
|
+
actionType,
|
|
1363
|
+
status: ActionStatus.PREFLIGHT_FAILED,
|
|
1364
|
+
error: `entry skill not found in snapshot: skills/${action.entrySkill}/SKILL.md`,
|
|
1365
|
+
});
|
|
1366
|
+
}
|
|
619
1367
|
}
|
|
620
1368
|
|
|
621
1369
|
// Verify manifestDigest matches actual snapshot content using frozen algorithm
|
|
@@ -763,7 +1511,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
763
1511
|
// Marketplace install execute
|
|
764
1512
|
if (
|
|
765
1513
|
actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
|
|
766
|
-
actionType === ActionType.CODEX_MARKETPLACE_INSTALL
|
|
1514
|
+
actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
|
|
1515
|
+
actionType === ActionType.KIMI_MARKETPLACE_INSTALL
|
|
767
1516
|
) {
|
|
768
1517
|
try {
|
|
769
1518
|
assertIsolatedConsumerWritesAuthorized(context, actionType);
|
|
@@ -793,6 +1542,15 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
793
1542
|
});
|
|
794
1543
|
}
|
|
795
1544
|
|
|
1545
|
+
// Kimi has NO scriptable install CLI. It is handled entirely by the
|
|
1546
|
+
// manual-requirement helper, which uses a stable plan-digest-keyed
|
|
1547
|
+
// home and deliberately SKIPS the per-run isolated consumer dir and
|
|
1548
|
+
// its runDir containment check (that model only fits claude/codex,
|
|
1549
|
+
// which exec a real CLI into a per-run HOME).
|
|
1550
|
+
if (action.consumer === 'kimi') {
|
|
1551
|
+
return executeKimiManualRequirement(action, context);
|
|
1552
|
+
}
|
|
1553
|
+
|
|
796
1554
|
const consumer = action.consumer;
|
|
797
1555
|
const runDir = context.runDir;
|
|
798
1556
|
const isolatedHome = resolve(runDir, 'consumers', `${consumer}-${action.plugin}`);
|
|
@@ -826,7 +1584,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
826
1584
|
? { HOME: isolatedHome, CLAUDE_CONFIG_DIR: resolve(isolatedHome, '.claude') }
|
|
827
1585
|
: { HOME: isolatedHome, CODEX_HOME: isolatedHome }),
|
|
828
1586
|
};
|
|
829
|
-
// Ensure real HOME/CODEX_HOME don't leak back (already overridden
|
|
1587
|
+
// Ensure real HOME/CODEX_HOME don't leak back (already overridden
|
|
1588
|
+
// above).
|
|
830
1589
|
|
|
831
1590
|
// Resolve frozen timeoutMs from the expanded action (top-level,
|
|
832
1591
|
// not action.parameters -- the publish/reconcile/verify call path
|
|
@@ -845,9 +1604,10 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
845
1604
|
});
|
|
846
1605
|
}
|
|
847
1606
|
|
|
848
|
-
// Step 1: Add marketplace
|
|
1607
|
+
// Step 1: Add marketplace (claude/codex only; kimi returned above)
|
|
849
1608
|
const ref = action.ref ?? `v${action.version}`;
|
|
850
|
-
let addOutput;
|
|
1609
|
+
let addOutput = null;
|
|
1610
|
+
if (consumer !== 'kimi') {
|
|
851
1611
|
const marketplaceArgs = consumer === 'claude'
|
|
852
1612
|
? ['plugin', 'marketplace', 'add', `${action.repo}@${ref}`]
|
|
853
1613
|
: ['plugin', 'marketplace', 'add', action.repo, '--ref', ref, '--json'];
|
|
@@ -885,8 +1645,10 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
885
1645
|
error: `marketplace add failed: ${addErr.message}`,
|
|
886
1646
|
});
|
|
887
1647
|
}
|
|
1648
|
+
}
|
|
888
1649
|
|
|
889
|
-
// Step 2: Install plugin
|
|
1650
|
+
// Step 2: Install plugin (claude/codex only; kimi has no install CLI
|
|
1651
|
+
// and returned the manual-install requirement above).
|
|
890
1652
|
let installOutput;
|
|
891
1653
|
const installArgs = consumer === 'claude'
|
|
892
1654
|
? ['plugin', 'install', `${action.plugin}@${action.marketplace}`]
|
|
@@ -1038,6 +1800,10 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1038
1800
|
* For Codex: uses pluginId === "plugin@marketplace" match in installed array,
|
|
1039
1801
|
* reads installedPath from add/install output or list, verifies install dir
|
|
1040
1802
|
* is inside isolated HOME, computes real manifestDigest.
|
|
1803
|
+
*
|
|
1804
|
+
* For Kimi: uses name === plugin match in installed array, reads
|
|
1805
|
+
* installedPath from validated install evidence, verifies install dir
|
|
1806
|
+
* is inside isolated HOME, computes real manifestDigest.
|
|
1041
1807
|
*/
|
|
1042
1808
|
async observe(action, context) {
|
|
1043
1809
|
const { actionType } = action;
|
|
@@ -1085,7 +1851,8 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1085
1851
|
// Marketplace install observe
|
|
1086
1852
|
if (
|
|
1087
1853
|
actionType === ActionType.CLAUDE_MARKETPLACE_INSTALL ||
|
|
1088
|
-
actionType === ActionType.CODEX_MARKETPLACE_INSTALL
|
|
1854
|
+
actionType === ActionType.CODEX_MARKETPLACE_INSTALL ||
|
|
1855
|
+
actionType === ActionType.KIMI_MARKETPLACE_INSTALL
|
|
1089
1856
|
) {
|
|
1090
1857
|
const consumer = action.consumer;
|
|
1091
1858
|
const runDir = context.runDir;
|
|
@@ -1097,13 +1864,15 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1097
1864
|
});
|
|
1098
1865
|
}
|
|
1099
1866
|
const isolatedHome = resolve(runDir, 'consumers', `${consumer}-${action.plugin}`);
|
|
1100
|
-
const cliCmd = consumer === 'claude' ? 'claude' : 'codex';
|
|
1867
|
+
const cliCmd = consumer === 'claude' ? 'claude' : consumer === 'codex' ? 'codex' : 'kimi';
|
|
1101
1868
|
const baseEnv = { ...process.env, ...(context.env ?? {}) };
|
|
1102
1869
|
const env = {
|
|
1103
1870
|
...baseEnv,
|
|
1104
1871
|
...(consumer === 'claude'
|
|
1105
1872
|
? { HOME: isolatedHome, CLAUDE_CONFIG_DIR: resolve(isolatedHome, '.claude') }
|
|
1106
|
-
:
|
|
1873
|
+
: consumer === 'codex'
|
|
1874
|
+
? { HOME: isolatedHome, CODEX_HOME: isolatedHome }
|
|
1875
|
+
: { HOME: isolatedHome, KIMI_CODE_HOME: isolatedHome }),
|
|
1107
1876
|
};
|
|
1108
1877
|
|
|
1109
1878
|
// Resolve frozen timeoutMs from the expanded action (top-level).
|
|
@@ -1120,6 +1889,293 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1120
1889
|
});
|
|
1121
1890
|
}
|
|
1122
1891
|
|
|
1892
|
+
// Kimi Code protocol capability gap (BLOCKER-1): there is NO
|
|
1893
|
+
// `kimi plugins list --json` interface. observe never execs a kimi
|
|
1894
|
+
// command. Instead it consumes a structured human attestation (written
|
|
1895
|
+
// after the interactive install) bound to the frozen plan digest and
|
|
1896
|
+
// expected identity, then performs read-only verification of the
|
|
1897
|
+
// installed managed copy: payload digest vs the sealed authority,
|
|
1898
|
+
// entry skill resolved via the manifest skills root (MAJOR-4), and
|
|
1899
|
+
// manifest name/version. Missing/expired/mismatched/escaping proof
|
|
1900
|
+
// fails closed so a kimi unit can never reach VERIFIED without it.
|
|
1901
|
+
if (consumer === 'kimi') {
|
|
1902
|
+
const expectedRef = action.ref ?? `v${action.version}`;
|
|
1903
|
+
|
|
1904
|
+
// Bind to the REAL frozen plan digest (A). Fail closed if the
|
|
1905
|
+
// context does not carry an intact frozen plan.
|
|
1906
|
+
let boundPlanDigest;
|
|
1907
|
+
try {
|
|
1908
|
+
boundPlanDigest = resolveBoundPlanDigest(context);
|
|
1909
|
+
} catch (planErr) {
|
|
1910
|
+
return createResult({
|
|
1911
|
+
actionType,
|
|
1912
|
+
status: ActionStatus.OBSERVED,
|
|
1913
|
+
observation: {
|
|
1914
|
+
installed: false,
|
|
1915
|
+
error: `cannot bind kimi observation to the frozen plan: ${planErr.message}`,
|
|
1916
|
+
},
|
|
1917
|
+
});
|
|
1918
|
+
}
|
|
1919
|
+
|
|
1920
|
+
// Stable, cross-run attestation authority (B). The requirement and
|
|
1921
|
+
// attestation live here, keyed by the verified plan digest + plugin,
|
|
1922
|
+
// so they survive publish PARTIAL -> reconcile -> verify (each of
|
|
1923
|
+
// which uses a fresh runDir).
|
|
1924
|
+
let attestationDir;
|
|
1925
|
+
try {
|
|
1926
|
+
attestationDir = kimiAuthorityDir(context, boundPlanDigest, action.plugin);
|
|
1927
|
+
} catch (dirErr) {
|
|
1928
|
+
return createResult({
|
|
1929
|
+
actionType,
|
|
1930
|
+
status: ActionStatus.OBSERVED,
|
|
1931
|
+
observation: { installed: false, error: dirErr.message },
|
|
1932
|
+
});
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
// The execute-emitted requirement must exist and bind to the action
|
|
1936
|
+
// and the frozen plan digest.
|
|
1937
|
+
let requirement = null;
|
|
1938
|
+
try {
|
|
1939
|
+
requirement = JSON.parse(await readFile(resolve(attestationDir, KIMI_REQUIREMENT_FILE), 'utf8'));
|
|
1940
|
+
} catch {
|
|
1941
|
+
return createResult({
|
|
1942
|
+
actionType,
|
|
1943
|
+
status: ActionStatus.OBSERVED,
|
|
1944
|
+
observation: {
|
|
1945
|
+
installed: false,
|
|
1946
|
+
manualInstallRequired: true,
|
|
1947
|
+
error: 'kimi manual-install requirement is missing; run execute first',
|
|
1948
|
+
},
|
|
1949
|
+
});
|
|
1950
|
+
}
|
|
1951
|
+
if (
|
|
1952
|
+
requirement.planDigest !== boundPlanDigest ||
|
|
1953
|
+
requirement.plugin !== action.plugin ||
|
|
1954
|
+
requirement.version !== action.version ||
|
|
1955
|
+
requirement.entrySkill !== action.entrySkill ||
|
|
1956
|
+
requirement.repo !== action.repo ||
|
|
1957
|
+
requirement.ref !== expectedRef
|
|
1958
|
+
) {
|
|
1959
|
+
return createResult({
|
|
1960
|
+
actionType,
|
|
1961
|
+
status: ActionStatus.OBSERVED,
|
|
1962
|
+
observation: {
|
|
1963
|
+
installed: false,
|
|
1964
|
+
error: 'kimi manual-install requirement does not match the frozen plan/action',
|
|
1965
|
+
},
|
|
1966
|
+
});
|
|
1967
|
+
}
|
|
1968
|
+
|
|
1969
|
+
// The trusted human attestation is mandatory and is read from the
|
|
1970
|
+
// stable authority dir. Without it the interactive install has not
|
|
1971
|
+
// been proven: fail closed.
|
|
1972
|
+
let attestation = null;
|
|
1973
|
+
try {
|
|
1974
|
+
attestation = JSON.parse(await readFile(resolve(attestationDir, KIMI_ATTESTATION_FILE), 'utf8'));
|
|
1975
|
+
} catch {
|
|
1976
|
+
return createResult({
|
|
1977
|
+
actionType,
|
|
1978
|
+
status: ActionStatus.OBSERVED,
|
|
1979
|
+
observation: {
|
|
1980
|
+
installed: false,
|
|
1981
|
+
manualInstallRequired: true,
|
|
1982
|
+
installUrl: requirement.installUrl,
|
|
1983
|
+
attestationDir,
|
|
1984
|
+
error: `kimi attestation is missing; write ${resolve(attestationDir, KIMI_ATTESTATION_FILE)} after the interactive install (${requirement.installUrl})`,
|
|
1985
|
+
},
|
|
1986
|
+
});
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
const attestationCheck = validateKimiAttestation(attestation, action, new Date().toISOString(), boundPlanDigest);
|
|
1990
|
+
if (!attestationCheck.valid) {
|
|
1991
|
+
return createResult({
|
|
1992
|
+
actionType,
|
|
1993
|
+
status: ActionStatus.OBSERVED,
|
|
1994
|
+
observation: { installed: false, error: attestationCheck.error },
|
|
1995
|
+
});
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// The attested install path must live in the documented managed
|
|
1999
|
+
// layout under the STABLE, plan-digest-keyed isolated home (B). The
|
|
2000
|
+
// managed root is derived from the authority dir (itself derived from
|
|
2001
|
+
// the verified plan digest + plugin), so it is identical across
|
|
2002
|
+
// publish/reconcile/verify run dirs. No lexical-containment fallback
|
|
2003
|
+
// (C): every path must realpath-resolve and stay contained; a missing
|
|
2004
|
+
// home/root, a symlink, or an escape fails closed.
|
|
2005
|
+
const kimiCodeHome = resolve(attestationDir, 'kimi-home');
|
|
2006
|
+
const kimiCodeHomeReal = await realpath(kimiCodeHome).catch(() => null);
|
|
2007
|
+
if (!kimiCodeHomeReal) {
|
|
2008
|
+
return createResult({
|
|
2009
|
+
actionType,
|
|
2010
|
+
status: ActionStatus.OBSERVED,
|
|
2011
|
+
observation: {
|
|
2012
|
+
installed: false,
|
|
2013
|
+
error: `KIMI_CODE_HOME does not exist or cannot be resolved: ${kimiCodeHome}`,
|
|
2014
|
+
},
|
|
2015
|
+
});
|
|
2016
|
+
}
|
|
2017
|
+
const managedRootReal = await realpath(resolve(kimiCodeHomeReal, KIMI_MANAGED_SUBPATH, action.plugin)).catch(() => null);
|
|
2018
|
+
if (!managedRootReal) {
|
|
2019
|
+
return createResult({
|
|
2020
|
+
actionType,
|
|
2021
|
+
status: ActionStatus.OBSERVED,
|
|
2022
|
+
observation: {
|
|
2023
|
+
installed: false,
|
|
2024
|
+
error: `kimi managed plugin root does not exist: ${resolve(kimiCodeHomeReal, KIMI_MANAGED_SUBPATH, action.plugin)}`,
|
|
2025
|
+
},
|
|
2026
|
+
});
|
|
2027
|
+
}
|
|
2028
|
+
// installPath must be a real directory (not a symlink) that resolves
|
|
2029
|
+
// inside the managed root.
|
|
2030
|
+
const installPath = resolve(attestation.installPath);
|
|
2031
|
+
let installPathStat;
|
|
2032
|
+
try {
|
|
2033
|
+
installPathStat = await lstat(installPath);
|
|
2034
|
+
} catch {
|
|
2035
|
+
return createResult({
|
|
2036
|
+
actionType,
|
|
2037
|
+
status: ActionStatus.OBSERVED,
|
|
2038
|
+
observation: { installed: false, error: `kimi install path does not exist: ${attestation.installPath}` },
|
|
2039
|
+
});
|
|
2040
|
+
}
|
|
2041
|
+
if (installPathStat.isSymbolicLink()) {
|
|
2042
|
+
return createResult({
|
|
2043
|
+
actionType,
|
|
2044
|
+
status: ActionStatus.OBSERVED,
|
|
2045
|
+
observation: { installed: false, error: `kimi install path must not be a symlink: ${attestation.installPath}` },
|
|
2046
|
+
});
|
|
2047
|
+
}
|
|
2048
|
+
if (!installPathStat.isDirectory()) {
|
|
2049
|
+
return createResult({
|
|
2050
|
+
actionType,
|
|
2051
|
+
status: ActionStatus.OBSERVED,
|
|
2052
|
+
observation: { installed: false, error: `kimi install path must be a directory: ${attestation.installPath}` },
|
|
2053
|
+
});
|
|
2054
|
+
}
|
|
2055
|
+
const installPathReal = await realpath(installPath).catch(() => null);
|
|
2056
|
+
if (!installPathReal) {
|
|
2057
|
+
return createResult({
|
|
2058
|
+
actionType,
|
|
2059
|
+
status: ActionStatus.OBSERVED,
|
|
2060
|
+
observation: { installed: false, error: `kimi install path cannot be resolved: ${attestation.installPath}` },
|
|
2061
|
+
});
|
|
2062
|
+
}
|
|
2063
|
+
const sepK = process.platform === 'win32' ? '\\' : '/';
|
|
2064
|
+
const relToManaged = relative(managedRootReal, installPathReal);
|
|
2065
|
+
if (
|
|
2066
|
+
relToManaged !== '' &&
|
|
2067
|
+
(isAbsolute(relToManaged) || relToManaged === '..' || relToManaged.startsWith(`..${sepK}`))
|
|
2068
|
+
) {
|
|
2069
|
+
return createResult({
|
|
2070
|
+
actionType,
|
|
2071
|
+
status: ActionStatus.OBSERVED,
|
|
2072
|
+
observation: {
|
|
2073
|
+
installed: false,
|
|
2074
|
+
error: `kimi install path escapes the managed root (${managedRootReal}): ${attestation.installPath}`,
|
|
2075
|
+
},
|
|
2076
|
+
});
|
|
2077
|
+
}
|
|
2078
|
+
|
|
2079
|
+
// Read-only payload binding: the installed managed copy must match
|
|
2080
|
+
// the sealed frozen authority exactly (transport-normalized).
|
|
2081
|
+
let manifestDigest;
|
|
2082
|
+
try {
|
|
2083
|
+
manifestDigest = await verifyInstalledMarketplacePayload(action, context, installPathReal, consumer);
|
|
2084
|
+
} catch (digestErr) {
|
|
2085
|
+
return createResult({
|
|
2086
|
+
actionType,
|
|
2087
|
+
status: ActionStatus.OBSERVED,
|
|
2088
|
+
observation: {
|
|
2089
|
+
installed: true,
|
|
2090
|
+
installPath: installPathReal,
|
|
2091
|
+
error: `failed to bind installed kimi payload to frozen authority: ${digestErr.message}`,
|
|
2092
|
+
},
|
|
2093
|
+
});
|
|
2094
|
+
}
|
|
2095
|
+
if (manifestDigest !== action.manifestDigest) {
|
|
2096
|
+
return createResult({
|
|
2097
|
+
actionType,
|
|
2098
|
+
status: ActionStatus.OBSERVED,
|
|
2099
|
+
observation: {
|
|
2100
|
+
installed: true,
|
|
2101
|
+
installPath: installPathReal,
|
|
2102
|
+
error: 'installed kimi payload digest does not match the frozen plan digest',
|
|
2103
|
+
},
|
|
2104
|
+
});
|
|
2105
|
+
}
|
|
2106
|
+
|
|
2107
|
+
// Entry skill must resolve via the installed manifest's skills root
|
|
2108
|
+
// (MAJOR-4), with official manifest precedence.
|
|
2109
|
+
let installedManifest;
|
|
2110
|
+
let entrySkillFound = false;
|
|
2111
|
+
try {
|
|
2112
|
+
const readManifest = await readKimiManifest(installPathReal);
|
|
2113
|
+
installedManifest = readManifest.manifest;
|
|
2114
|
+
await resolveKimiEntrySkillFile(installPathReal, installedManifest, action.entrySkill);
|
|
2115
|
+
entrySkillFound = true;
|
|
2116
|
+
} catch (entryErr) {
|
|
2117
|
+
return createResult({
|
|
2118
|
+
actionType,
|
|
2119
|
+
status: ActionStatus.OBSERVED,
|
|
2120
|
+
observation: {
|
|
2121
|
+
installed: true,
|
|
2122
|
+
installPath: installPathReal,
|
|
2123
|
+
manifestDigest,
|
|
2124
|
+
entrySkillFound: false,
|
|
2125
|
+
error: `kimi entry skill not resolvable in installed copy: ${entryErr.message}`,
|
|
2126
|
+
},
|
|
2127
|
+
});
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
// Installed manifest identity must match the frozen action.
|
|
2131
|
+
if (installedManifest.name !== action.plugin) {
|
|
2132
|
+
return createResult({
|
|
2133
|
+
actionType,
|
|
2134
|
+
status: ActionStatus.OBSERVED,
|
|
2135
|
+
observation: {
|
|
2136
|
+
installed: true,
|
|
2137
|
+
installPath: installPathReal,
|
|
2138
|
+
manifestDigest,
|
|
2139
|
+
entrySkillFound: true,
|
|
2140
|
+
error: `installed kimi manifest name "${installedManifest.name}" does not match action plugin "${action.plugin}"`,
|
|
2141
|
+
},
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
if (installedManifest.version !== action.version) {
|
|
2145
|
+
return createResult({
|
|
2146
|
+
actionType,
|
|
2147
|
+
status: ActionStatus.OBSERVED,
|
|
2148
|
+
observation: {
|
|
2149
|
+
installed: true,
|
|
2150
|
+
installPath: installPathReal,
|
|
2151
|
+
manifestDigest,
|
|
2152
|
+
entrySkillFound: true,
|
|
2153
|
+
error: `installed kimi manifest version "${installedManifest.version}" does not match action version "${action.version}"`,
|
|
2154
|
+
},
|
|
2155
|
+
});
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
// Build the observation from independently verified fields only.
|
|
2159
|
+
// marketplace is intentionally NOT a kimi identity field (MINOR-1).
|
|
2160
|
+
const observation = {
|
|
2161
|
+
installed: true,
|
|
2162
|
+
installPath: installPathReal,
|
|
2163
|
+
entrySkillFound: true,
|
|
2164
|
+
entrySkill: action.entrySkill,
|
|
2165
|
+
manifestDigest,
|
|
2166
|
+
consumer,
|
|
2167
|
+
plugin: installedManifest.name,
|
|
2168
|
+
version: installedManifest.version,
|
|
2169
|
+
repo: action.repo,
|
|
2170
|
+
ref: expectedRef,
|
|
2171
|
+
};
|
|
2172
|
+
return createResult({
|
|
2173
|
+
actionType,
|
|
2174
|
+
status: ActionStatus.OBSERVED,
|
|
2175
|
+
observation,
|
|
2176
|
+
});
|
|
2177
|
+
}
|
|
2178
|
+
|
|
1123
2179
|
// Read execute evidence — mandatory for observe validation
|
|
1124
2180
|
let evidence = null;
|
|
1125
2181
|
try {
|
|
@@ -1155,10 +2211,9 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1155
2211
|
});
|
|
1156
2212
|
}
|
|
1157
2213
|
|
|
1158
|
-
// Run list command to verify installation
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
: ['plugin', 'list', '--json'];
|
|
2214
|
+
// Run list command to verify installation (claude/codex only; kimi
|
|
2215
|
+
// has no list CLI and returned via the attestation path above).
|
|
2216
|
+
const listArgs = ['plugin', 'list', '--json'];
|
|
1162
2217
|
|
|
1163
2218
|
let listOutput;
|
|
1164
2219
|
try {
|
|
@@ -1213,7 +2268,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1213
2268
|
});
|
|
1214
2269
|
}
|
|
1215
2270
|
installPath = found.installPath;
|
|
1216
|
-
} else {
|
|
2271
|
+
} else if (consumer === 'codex') {
|
|
1217
2272
|
// Codex: installPath comes from validated evidence, not from list
|
|
1218
2273
|
installPath = evidence.installOutput?.installedPath;
|
|
1219
2274
|
if (!installPath) {
|
|
@@ -1335,7 +2390,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1335
2390
|
// is returned and verify therefore fails closed.
|
|
1336
2391
|
try {
|
|
1337
2392
|
const installedSnapshot = await computeFrozenSnapshot(installPath, {
|
|
1338
|
-
excludeRootEntries: consumer
|
|
2393
|
+
excludeRootEntries: consumerTransportExclusions(consumer),
|
|
1339
2394
|
});
|
|
1340
2395
|
manifestDigest = installedSnapshot.digest;
|
|
1341
2396
|
} catch {
|
|
@@ -1354,14 +2409,15 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1354
2409
|
consumer,
|
|
1355
2410
|
};
|
|
1356
2411
|
|
|
1357
|
-
// Fields from CLI evidence only
|
|
2412
|
+
// Fields from CLI evidence only (claude/codex; kimi observe returns
|
|
2413
|
+
// via the attestation path above and never reaches this point).
|
|
1358
2414
|
if (consumer === 'claude') {
|
|
1359
2415
|
// Claude list may not have name; extract plugin/marketplace from id
|
|
1360
2416
|
const idParts = found.id.split('@');
|
|
1361
2417
|
observation.plugin = idParts[0];
|
|
1362
2418
|
observation.marketplace = idParts.slice(1).join('@');
|
|
1363
2419
|
if (found.version) observation.version = found.version;
|
|
1364
|
-
} else {
|
|
2420
|
+
} else if (consumer === 'codex') {
|
|
1365
2421
|
if (found.name) observation.plugin = found.name;
|
|
1366
2422
|
if (found.marketplaceName) observation.marketplace = found.marketplaceName;
|
|
1367
2423
|
if (found.version) observation.version = found.version;
|
|
@@ -1510,3 +2566,7 @@ export function createPluginMarketplaceAdapter(deps = {}) {
|
|
|
1510
2566
|
},
|
|
1511
2567
|
});
|
|
1512
2568
|
}
|
|
2569
|
+
|
|
2570
|
+
// Test-support exports: white-box regression tests assert the Kimi entry-skill
|
|
2571
|
+
// resolution and manifest-reading contracts directly (FU-3 / MAJOR-4).
|
|
2572
|
+
export { resolveKimiEntrySkillFile, readKimiManifest };
|