release-skill 0.1.9 → 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 +53 -0
- package/INSTALL.md +4 -4
- package/INSTALL.zh-CN.md +4 -4
- package/README.md +18 -33
- package/README.zh-CN.md +17 -23
- 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 +2740 -1844
- 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 +2740 -1844
- 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 +2740 -1844
- 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 +2740 -1844
- 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 -730
- 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 +16 -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
- package/src/snapshot/frozen.mjs +29 -5
|
@@ -0,0 +1,514 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Kimi Code platform protocol (T2.2 step 2).
|
|
3
|
+
*
|
|
4
|
+
* Kimi Code protocol-gap modeling (BLOCKER-1 / MAJOR-1 / MAJOR-4 / MINOR-1).
|
|
5
|
+
*
|
|
6
|
+
* Kimi Code has NO scriptable plugin install/list CLI: plugin management is
|
|
7
|
+
* interactive-only (`/plugins install <path-or-url>` in the TUI). There is no
|
|
8
|
+
* `kimi plugins ...` subcommand and no `--json` output protocol. Therefore the
|
|
9
|
+
* kimi-marketplace-install action is modeled as a protocol capability gap:
|
|
10
|
+
*
|
|
11
|
+
* - execute NEVER execs a kimi CLI. It emits an actionable, version-pinned
|
|
12
|
+
* manual-install requirement bound to the frozen plan digest + identity.
|
|
13
|
+
* - observe consumes a structured human attestation (written after the
|
|
14
|
+
* operator runs the interactive install) plus read-only verification of
|
|
15
|
+
* the installed managed copy. Missing/expired/mismatched/escaping proof
|
|
16
|
+
* fails closed, so a kimi unit can never reach VERIFIED without it.
|
|
17
|
+
*
|
|
18
|
+
* This module is the kimi half of the platform registry's strategy table
|
|
19
|
+
* (registry.mjs references these functions); the plugin-marketplace adapter
|
|
20
|
+
* consumes the attestation path from here. The shared adapter primitives
|
|
21
|
+
* (safe-id pattern, frozen-timeout validation, atomic evidence writes) live
|
|
22
|
+
* in adapters/contract.mjs.
|
|
23
|
+
*
|
|
24
|
+
* T2.2 step 2 moved this closure verbatim out of plugin-marketplace.mjs:
|
|
25
|
+
* every error message, field name, and file layout is byte-for-byte the
|
|
26
|
+
* legacy behaviour (frozen by platform-golden.test.mjs and the BLOCKER-1
|
|
27
|
+
* suite).
|
|
28
|
+
*
|
|
29
|
+
* @module platforms/kimi
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { readFile, mkdir } from 'node:fs/promises';
|
|
33
|
+
import { join, resolve, relative, isAbsolute } from 'node:path';
|
|
34
|
+
|
|
35
|
+
import {
|
|
36
|
+
ActionType,
|
|
37
|
+
ActionStatus,
|
|
38
|
+
createResult,
|
|
39
|
+
resolveTimeoutMs,
|
|
40
|
+
SAFE_ID_RE,
|
|
41
|
+
writeEvidenceAtomic,
|
|
42
|
+
} from '../adapters/contract.mjs';
|
|
43
|
+
import { computePlanDigest } from '../core/plan.mjs';
|
|
44
|
+
import { canonicalJson } from '../core/digest.mjs';
|
|
45
|
+
|
|
46
|
+
/** Structured manual-install requirement written by kimi execute. */
|
|
47
|
+
export const KIMI_REQUIREMENT_FILE = 'release-skill-kimi-manual-install.json';
|
|
48
|
+
/** Structured human attestation consumed by kimi observe. */
|
|
49
|
+
export const KIMI_ATTESTATION_FILE = 'release-skill-kimi-attestation.json';
|
|
50
|
+
/** Kimi Code managed install layout: $KIMI_CODE_HOME/plugins/managed/<id>/. */
|
|
51
|
+
export const KIMI_MANAGED_SUBPATH = join('plugins', 'managed');
|
|
52
|
+
/** Maximum attestation validity window (mirrors the 24h approval expiry). */
|
|
53
|
+
export const KIMI_MAX_ATTESTATION_VALIDITY_MS = 24 * 60 * 60 * 1000;
|
|
54
|
+
|
|
55
|
+
/** 64-char lowercase hex plan/payload digest pattern. */
|
|
56
|
+
export const HEX_DIGEST_RE = /^[a-f0-9]{64}$/;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Ordered authoritative kimi plugin manifest candidates.
|
|
60
|
+
* `kimi.plugin.json` at the plugin root takes priority over
|
|
61
|
+
* `.kimi-plugin/plugin.json` (official precedence). Single source for both
|
|
62
|
+
* the readManifest strategy and the registry's manifestPaths descriptor.
|
|
63
|
+
*/
|
|
64
|
+
export const KIMI_MANIFEST_CANDIDATES = Object.freeze([
|
|
65
|
+
'kimi.plugin.json',
|
|
66
|
+
join('.kimi-plugin', 'plugin.json'),
|
|
67
|
+
]);
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Normalize a plan back to its frozen form for digest comparison.
|
|
71
|
+
*
|
|
72
|
+
* Only lifecycle status fields are reset: the top-level `status` returns to
|
|
73
|
+
* "PREPARED" and every `externalActions[].status` returns to "PENDING". Every
|
|
74
|
+
* other field is preserved verbatim. publish/reconcile/verify mutate exactly
|
|
75
|
+
* these status fields in memory as the saga progresses, so normalizing them
|
|
76
|
+
* recovers the frozen digest while leaving all security-relevant fields
|
|
77
|
+
* (baseline, units, action parameters/expected, production config, …) intact.
|
|
78
|
+
*
|
|
79
|
+
* @param {object} plan
|
|
80
|
+
* @returns {object} the lifecycle-normalized plan
|
|
81
|
+
*/
|
|
82
|
+
function normalizePlanForDigest(plan) {
|
|
83
|
+
const normalized = { ...plan, status: 'PREPARED' };
|
|
84
|
+
if (Array.isArray(plan.externalActions)) {
|
|
85
|
+
normalized.externalActions = plan.externalActions.map((action) => (
|
|
86
|
+
action && typeof action === 'object' && !Array.isArray(action)
|
|
87
|
+
? { ...action, status: 'PENDING' }
|
|
88
|
+
: action
|
|
89
|
+
));
|
|
90
|
+
}
|
|
91
|
+
return normalized;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Resolve and verify the genuine frozen plan digest from the adapter context.
|
|
96
|
+
*
|
|
97
|
+
* The kimi manual-install requirement and attestation bind to the REAL frozen
|
|
98
|
+
* plan digest (`context.plan.digest`) — never to `action.manifestDigest`, which
|
|
99
|
+
* is only the snapshot payload digest.
|
|
100
|
+
*
|
|
101
|
+
* Integrity model: the carried `context.plan.digest` is recomputed from the
|
|
102
|
+
* lifecycle-normalized plan and must match EXACTLY. Status transitions
|
|
103
|
+
* (top-level status, per-action checkpoint status) are normalized away, but any
|
|
104
|
+
* other field tamper changes the recomputed digest and fails closed. This
|
|
105
|
+
* proves the attestation is bound to the genuine frozen plan, not to a spoofed
|
|
106
|
+
* or mutated stand-in.
|
|
107
|
+
*
|
|
108
|
+
* @param {object} context - adapter context (must carry the frozen `plan`).
|
|
109
|
+
* @returns {string} the verified frozen plan digest.
|
|
110
|
+
* @throws {Error} when the plan is absent or the digest does not match.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveBoundPlanDigest(context) {
|
|
113
|
+
const plan = context?.plan;
|
|
114
|
+
if (!plan || typeof plan !== 'object' || Array.isArray(plan)) {
|
|
115
|
+
throw new Error('context.plan is required to bind the kimi plan digest');
|
|
116
|
+
}
|
|
117
|
+
const carried = plan.digest;
|
|
118
|
+
if (typeof carried !== 'string' || !HEX_DIGEST_RE.test(carried)) {
|
|
119
|
+
throw new Error('context.plan.digest must be a 64-char lowercase hex frozen plan digest');
|
|
120
|
+
}
|
|
121
|
+
const normalized = normalizePlanForDigest(plan);
|
|
122
|
+
if (computePlanDigest(normalized) !== carried) {
|
|
123
|
+
throw new Error('context.plan.digest does not match the normalized frozen plan (a non-lifecycle field was tampered)');
|
|
124
|
+
}
|
|
125
|
+
return carried;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Authoritative, cross-run attestation directory for a kimi install.
|
|
130
|
+
*
|
|
131
|
+
* Lives at a stable root-fixed location keyed by the verified frozen plan
|
|
132
|
+
* digest and plugin id:
|
|
133
|
+
* <root>/.release-skill/kimi-attestations/<planDigest>/<plugin>/
|
|
134
|
+
*
|
|
135
|
+
* This survives the publish -> manual install -> reconcile -> verify chain,
|
|
136
|
+
* where each command otherwise uses a fresh runDir (an attestation written to a
|
|
137
|
+
* publish runDir would be invisible to reconcile/verify). Both the requirement
|
|
138
|
+
* and the human attestation live here. The segments are pre-validated (planDigest
|
|
139
|
+
* is 64-hex, plugin matches SAFE_ID_RE) and the resolved path is contained
|
|
140
|
+
* within the authority base, so no path escape is possible.
|
|
141
|
+
*
|
|
142
|
+
* @param {object} context - adapter context (needs `root`).
|
|
143
|
+
* @param {string} planDigest - verified frozen plan digest (64-hex).
|
|
144
|
+
* @param {string} plugin - plugin id (SAFE_ID_RE).
|
|
145
|
+
* @returns {string} absolute authority directory.
|
|
146
|
+
*/
|
|
147
|
+
export function kimiAuthorityDir(context, planDigest, plugin) {
|
|
148
|
+
if (!context?.root) {
|
|
149
|
+
throw new Error('context.root is required for the kimi attestation authority');
|
|
150
|
+
}
|
|
151
|
+
if (!HEX_DIGEST_RE.test(planDigest)) {
|
|
152
|
+
throw new Error('kimi attestation authority requires a 64-hex plan digest');
|
|
153
|
+
}
|
|
154
|
+
if (!SAFE_ID_RE.test(plugin)) {
|
|
155
|
+
throw new Error(`kimi attestation authority requires a safe plugin id: "${plugin}"`);
|
|
156
|
+
}
|
|
157
|
+
const base = resolve(context.root, '.release-skill', 'kimi-attestations');
|
|
158
|
+
const dir = resolve(base, planDigest, plugin);
|
|
159
|
+
const rel = relative(base, dir);
|
|
160
|
+
const sep = process.platform === 'win32' ? '\\' : '/';
|
|
161
|
+
if (
|
|
162
|
+
rel === '' || rel === '..' || isAbsolute(rel) || rel.startsWith(`..${sep}`)
|
|
163
|
+
|| rel.split(sep).some((segment) => segment === '..' || segment === '')
|
|
164
|
+
) {
|
|
165
|
+
throw new Error('kimi attestation authority path escapes its base');
|
|
166
|
+
}
|
|
167
|
+
return dir;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Build the official, version-pinned install URL for a frozen Git ref.
|
|
172
|
+
*
|
|
173
|
+
* Prefers the GitHub release-tag URL (`/releases/tag/<ref>`), which pins the
|
|
174
|
+
* exact published ref; `/tree/<ref>` is the documented equivalent. A bare
|
|
175
|
+
* repository URL is NOT acceptable because it installs the latest release (or
|
|
176
|
+
* default branch), which need not equal the frozen version.
|
|
177
|
+
*
|
|
178
|
+
* @param {string} repo - owner/repo
|
|
179
|
+
* @param {string} ref - frozen Git ref (tag)
|
|
180
|
+
* @returns {string}
|
|
181
|
+
*/
|
|
182
|
+
function buildKimiInstallUrl(repo, ref) {
|
|
183
|
+
return `https://github.com/${repo}/releases/tag/${ref}`;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Human-facing, actionable manual-install closed-loop instructions for Kimi Code.
|
|
188
|
+
*
|
|
189
|
+
* @param {{installUrl:string, plugin:string, version:string, ref:string, isolatedHome:string, attestationDir:string}} p
|
|
190
|
+
* @returns {string[]}
|
|
191
|
+
*/
|
|
192
|
+
function buildKimiManualInstructions({ installUrl, plugin, version, ref, isolatedHome, attestationDir }) {
|
|
193
|
+
return [
|
|
194
|
+
`Kimi Code has no scriptable plugin-install CLI; installation is a manual, interactive step.`,
|
|
195
|
+
`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).`,
|
|
196
|
+
`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}/".`,
|
|
197
|
+
`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).`,
|
|
198
|
+
`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.`,
|
|
199
|
+
` Required fields: consumer="kimi", plugin, version, entrySkill, repo, ref, installPath, planDigest, payloadDigest, attestedBy, attestedAt, expiresAt.`,
|
|
200
|
+
`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.`,
|
|
201
|
+
`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.`,
|
|
202
|
+
];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* Read the authoritative Kimi plugin manifest from a verified plugin root.
|
|
207
|
+
*
|
|
208
|
+
* `kimi.plugin.json` at the root takes priority over `.kimi-plugin/plugin.json`
|
|
209
|
+
* when both exist (official precedence). Returns the parsed manifest and the
|
|
210
|
+
* root-relative manifest path. Throws when no valid manifest is present.
|
|
211
|
+
*
|
|
212
|
+
* @param {string} pluginRootReal - realpath of the verified plugin root.
|
|
213
|
+
* @returns {Promise<{manifest:object, manifestRelative:string}>}
|
|
214
|
+
*/
|
|
215
|
+
export async function readKimiManifest(pluginRootReal) {
|
|
216
|
+
for (const manifestRelative of KIMI_MANIFEST_CANDIDATES) {
|
|
217
|
+
const manifestPath = resolve(pluginRootReal, manifestRelative);
|
|
218
|
+
let content;
|
|
219
|
+
try {
|
|
220
|
+
content = await readFile(manifestPath, 'utf8');
|
|
221
|
+
} catch {
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
let manifest;
|
|
225
|
+
try {
|
|
226
|
+
manifest = JSON.parse(content);
|
|
227
|
+
} catch {
|
|
228
|
+
throw new Error(`kimi plugin manifest ${manifestRelative} is not valid JSON`);
|
|
229
|
+
}
|
|
230
|
+
if (!manifest || typeof manifest !== 'object' || Array.isArray(manifest)) {
|
|
231
|
+
throw new Error(`kimi plugin manifest ${manifestRelative} is not an object`);
|
|
232
|
+
}
|
|
233
|
+
return { manifest, manifestRelative };
|
|
234
|
+
}
|
|
235
|
+
throw new Error('no kimi plugin manifest found (expected kimi.plugin.json or .kimi-plugin/plugin.json)');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Validate a structured kimi manual-install attestation against the frozen
|
|
240
|
+
* action and the verified frozen plan digest.
|
|
241
|
+
*
|
|
242
|
+
* Bindings (fail closed on any mismatch):
|
|
243
|
+
* - `planDigest` binds to the REAL frozen plan digest (`boundPlanDigest`, from
|
|
244
|
+
* `context.plan.digest`) — NOT to `action.manifestDigest`.
|
|
245
|
+
* - `payloadDigest` binds separately to `action.manifestDigest` (the sealed
|
|
246
|
+
* snapshot payload digest).
|
|
247
|
+
* - plugin identity, version, entry skill, repo, and frozen ref must match.
|
|
248
|
+
* - Time bounds: `attestedAt` must not be in the future, the validity window
|
|
249
|
+
* (`expiresAt - attestedAt`) must not exceed 24h, and the attestation must
|
|
250
|
+
* not be expired relative to `isoNow`.
|
|
251
|
+
*
|
|
252
|
+
* @param {object} attestation - parsed attestation JSON.
|
|
253
|
+
* @param {object} action - the expanded kimi action (top-level fields).
|
|
254
|
+
* @param {string} isoNow - current ISO timestamp.
|
|
255
|
+
* @param {string} boundPlanDigest - verified frozen plan digest.
|
|
256
|
+
* @returns {{valid:boolean, error:string|null}}
|
|
257
|
+
*/
|
|
258
|
+
export function validateKimiAttestation(attestation, action, isoNow, boundPlanDigest) {
|
|
259
|
+
if (!attestation || typeof attestation !== 'object' || Array.isArray(attestation)) {
|
|
260
|
+
return { valid: false, error: 'kimi attestation is not an object' };
|
|
261
|
+
}
|
|
262
|
+
const requiredStrings = ['plugin', 'version', 'entrySkill', 'repo', 'ref', 'installPath', 'payloadDigest', 'planDigest', 'attestedBy', 'attestedAt', 'expiresAt'];
|
|
263
|
+
for (const field of requiredStrings) {
|
|
264
|
+
if (typeof attestation[field] !== 'string' || attestation[field].length === 0) {
|
|
265
|
+
return { valid: false, error: `kimi attestation missing required field "${field}"` };
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (attestation.consumer !== 'kimi') {
|
|
269
|
+
return { valid: false, error: `kimi attestation consumer "${attestation.consumer}" must be "kimi"` };
|
|
270
|
+
}
|
|
271
|
+
if (!HEX_DIGEST_RE.test(attestation.planDigest)) {
|
|
272
|
+
return { valid: false, error: 'kimi attestation planDigest must be a 64-char lowercase hex digest' };
|
|
273
|
+
}
|
|
274
|
+
if (attestation.planDigest !== boundPlanDigest) {
|
|
275
|
+
return { valid: false, error: 'kimi attestation planDigest does not match the frozen plan digest' };
|
|
276
|
+
}
|
|
277
|
+
if (attestation.plugin !== action.plugin) {
|
|
278
|
+
return { valid: false, error: `kimi attestation plugin "${attestation.plugin}" does not match action plugin "${action.plugin}"` };
|
|
279
|
+
}
|
|
280
|
+
if (attestation.version !== action.version) {
|
|
281
|
+
return { valid: false, error: `kimi attestation version "${attestation.version}" does not match action version "${action.version}"` };
|
|
282
|
+
}
|
|
283
|
+
if (attestation.entrySkill !== action.entrySkill) {
|
|
284
|
+
return { valid: false, error: `kimi attestation entrySkill "${attestation.entrySkill}" does not match action entrySkill "${action.entrySkill}"` };
|
|
285
|
+
}
|
|
286
|
+
if (attestation.repo !== action.repo) {
|
|
287
|
+
return { valid: false, error: `kimi attestation repo "${attestation.repo}" does not match action repo "${action.repo}"` };
|
|
288
|
+
}
|
|
289
|
+
const expectedRef = action.ref ?? `v${action.version}`;
|
|
290
|
+
if (attestation.ref !== expectedRef) {
|
|
291
|
+
return { valid: false, error: `kimi attestation ref "${attestation.ref}" does not match frozen ref "${expectedRef}"` };
|
|
292
|
+
}
|
|
293
|
+
if (attestation.payloadDigest !== action.manifestDigest) {
|
|
294
|
+
return { valid: false, error: 'kimi attestation payloadDigest does not match the frozen payload digest' };
|
|
295
|
+
}
|
|
296
|
+
const attestedMs = Date.parse(attestation.attestedAt);
|
|
297
|
+
const expiresMs = Date.parse(attestation.expiresAt);
|
|
298
|
+
const nowMs = Date.parse(isoNow);
|
|
299
|
+
if (!Number.isFinite(attestedMs) || !Number.isFinite(expiresMs) || !Number.isFinite(nowMs)) {
|
|
300
|
+
return { valid: false, error: 'kimi attestation attestedAt/expiresAt must be valid ISO timestamps' };
|
|
301
|
+
}
|
|
302
|
+
if (attestedMs > nowMs) {
|
|
303
|
+
return { valid: false, error: 'kimi attestation attestedAt is in the future' };
|
|
304
|
+
}
|
|
305
|
+
if (expiresMs <= attestedMs) {
|
|
306
|
+
return { valid: false, error: 'kimi attestation expiresAt must be after attestedAt' };
|
|
307
|
+
}
|
|
308
|
+
if (expiresMs - attestedMs > KIMI_MAX_ATTESTATION_VALIDITY_MS) {
|
|
309
|
+
return { valid: false, error: 'kimi attestation validity must not exceed 24 hours' };
|
|
310
|
+
}
|
|
311
|
+
if (nowMs > expiresMs) {
|
|
312
|
+
return { valid: false, error: 'kimi attestation has expired' };
|
|
313
|
+
}
|
|
314
|
+
return { valid: true, error: null };
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Kimi Code protocol capability gap (BLOCKER-1): there is NO scriptable
|
|
319
|
+
* `kimi plugins install/list` CLI and no `--json` protocol. execute NEVER execs
|
|
320
|
+
* a kimi command. Instead it emits an actionable, version-pinned manual-install
|
|
321
|
+
* requirement bound to the real frozen plan digest + identity, and leaves
|
|
322
|
+
* success to observe, which consumes only a trusted human attestation plus
|
|
323
|
+
* read-only verification. Without that proof the checkpoint fails closed and can
|
|
324
|
+
* never reach VERIFIED.
|
|
325
|
+
*
|
|
326
|
+
* Isolation model (B/C): the kimi home is a STABLE, plan-digest-keyed directory
|
|
327
|
+
* under the attestation authority (`<authorityDir>/kimi-home`), not the per-run
|
|
328
|
+
* runDir consumer dir. The operator launches Kimi Code with that KIMI_CODE_HOME
|
|
329
|
+
* so the managed copy lands at `<kimiHome>/plugins/managed/<plugin>/`, a
|
|
330
|
+
* location that is identical across publish/reconcile/verify run dirs. execute
|
|
331
|
+
* creates ONLY the managed parent (`plugins/managed`), never `managed/<plugin>`
|
|
332
|
+
* (the operator's interactive install creates that). The requirement write is
|
|
333
|
+
* idempotent: an identical existing requirement is left untouched, a divergent
|
|
334
|
+
* one fails closed.
|
|
335
|
+
*
|
|
336
|
+
* Referenced from the registry as the kimi strategy.buildManualRequirement —
|
|
337
|
+
* the automatable=false manual-requirement path.
|
|
338
|
+
*
|
|
339
|
+
* @param {object} action - expanded kimi action (validated params already).
|
|
340
|
+
* @param {object} context - adapter context (root, runDir, plan).
|
|
341
|
+
* @returns {Promise<import('../adapters/contract.mjs').AdapterResult>}
|
|
342
|
+
*/
|
|
343
|
+
export async function executeKimiManualRequirement(action, context) {
|
|
344
|
+
const actionType = ActionType.KIMI_MARKETPLACE_INSTALL;
|
|
345
|
+
|
|
346
|
+
// (A) Bind to the REAL frozen plan digest via strict normalized recompute.
|
|
347
|
+
let planDigest;
|
|
348
|
+
try {
|
|
349
|
+
planDigest = resolveBoundPlanDigest(context);
|
|
350
|
+
} catch (planErr) {
|
|
351
|
+
return createResult({
|
|
352
|
+
actionType,
|
|
353
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
354
|
+
error: `cannot bind kimi requirement to the frozen plan: ${planErr.message}`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Validate the frozen timeout. Kimi execs no CLI, but the frozen-timeout
|
|
359
|
+
// fail-closed invariant still holds for every marketplace action.
|
|
360
|
+
try {
|
|
361
|
+
resolveTimeoutMs(action);
|
|
362
|
+
} catch (timeoutErr) {
|
|
363
|
+
return createResult({
|
|
364
|
+
actionType,
|
|
365
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
366
|
+
error: timeoutErr.message,
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const ref = action.ref ?? `v${action.version}`;
|
|
371
|
+
const installUrl = buildKimiInstallUrl(action.repo, ref);
|
|
372
|
+
|
|
373
|
+
// (B) Stable, plan-digest-keyed authority dir — the ONLY kimi home, shared
|
|
374
|
+
// across publish/reconcile/verify run dirs.
|
|
375
|
+
let attestationDir;
|
|
376
|
+
try {
|
|
377
|
+
attestationDir = kimiAuthorityDir(context, planDigest, action.plugin);
|
|
378
|
+
} catch (dirErr) {
|
|
379
|
+
return createResult({
|
|
380
|
+
actionType,
|
|
381
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
382
|
+
error: dirErr.message,
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
const kimiHome = resolve(attestationDir, 'kimi-home');
|
|
386
|
+
const managedParent = resolve(kimiHome, KIMI_MANAGED_SUBPATH); // plugins/managed
|
|
387
|
+
// plugins/managed/<plugin> — created by the operator's interactive install.
|
|
388
|
+
const managedInstallRoot = resolve(managedParent, action.plugin);
|
|
389
|
+
|
|
390
|
+
const instructions = buildKimiManualInstructions({
|
|
391
|
+
installUrl,
|
|
392
|
+
plugin: action.plugin,
|
|
393
|
+
version: action.version,
|
|
394
|
+
ref,
|
|
395
|
+
isolatedHome: kimiHome,
|
|
396
|
+
attestationDir,
|
|
397
|
+
});
|
|
398
|
+
|
|
399
|
+
const requirement = {
|
|
400
|
+
kind: 'kimi-manual-install-requirement',
|
|
401
|
+
consumer: 'kimi',
|
|
402
|
+
plugin: action.plugin,
|
|
403
|
+
version: action.version,
|
|
404
|
+
entrySkill: action.entrySkill,
|
|
405
|
+
repo: action.repo,
|
|
406
|
+
ref,
|
|
407
|
+
installUrl,
|
|
408
|
+
// (A) planDigest binds to the real frozen plan digest;
|
|
409
|
+
// expectedPayloadDigest binds separately to the snapshot payload digest.
|
|
410
|
+
planDigest,
|
|
411
|
+
expectedPayloadDigest: action.manifestDigest,
|
|
412
|
+
isolatedHome: kimiHome,
|
|
413
|
+
kimiCodeHome: kimiHome,
|
|
414
|
+
managedInstallRoot,
|
|
415
|
+
attestationDir,
|
|
416
|
+
attestationFile: KIMI_ATTESTATION_FILE,
|
|
417
|
+
attestationTemplate: {
|
|
418
|
+
consumer: 'kimi',
|
|
419
|
+
plugin: action.plugin,
|
|
420
|
+
version: action.version,
|
|
421
|
+
entrySkill: action.entrySkill,
|
|
422
|
+
repo: action.repo,
|
|
423
|
+
ref,
|
|
424
|
+
installPath: managedInstallRoot,
|
|
425
|
+
planDigest,
|
|
426
|
+
payloadDigest: action.manifestDigest,
|
|
427
|
+
attestedBy: '<person responsible for the manual install>',
|
|
428
|
+
attestedAt: '<ISO 8601 now; must not be in the future>',
|
|
429
|
+
expiresAt: '<ISO 8601; within 24h of attestedAt>',
|
|
430
|
+
},
|
|
431
|
+
instructions,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
// Create ONLY the managed parent (plugins/managed); never pre-create
|
|
435
|
+
// managed/<plugin> — the operator's interactive install creates that.
|
|
436
|
+
try {
|
|
437
|
+
await mkdir(managedParent, { recursive: true, mode: 0o700 });
|
|
438
|
+
} catch (mkdirErr) {
|
|
439
|
+
return createResult({
|
|
440
|
+
actionType,
|
|
441
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
442
|
+
error: `cannot create kimi managed parent directory: ${mkdirErr.message}`,
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
// Idempotent requirement write: an identical existing requirement is left
|
|
447
|
+
// untouched; a divergent existing requirement fails closed (never silently
|
|
448
|
+
// overwritten). `createdAt` is volatile and excluded from the comparison.
|
|
449
|
+
const requirementPath = resolve(attestationDir, KIMI_REQUIREMENT_FILE);
|
|
450
|
+
let existing = null;
|
|
451
|
+
let requirementMissing = false;
|
|
452
|
+
try {
|
|
453
|
+
const existingRaw = await readFile(requirementPath, 'utf8');
|
|
454
|
+
try {
|
|
455
|
+
existing = JSON.parse(existingRaw);
|
|
456
|
+
} catch (parseErr) {
|
|
457
|
+
return createResult({
|
|
458
|
+
actionType,
|
|
459
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
460
|
+
error: `existing kimi manual-install requirement is invalid JSON; refusing to overwrite: ${parseErr.message}`,
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
} catch (readErr) {
|
|
464
|
+
if (readErr?.code === 'ENOENT') {
|
|
465
|
+
requirementMissing = true;
|
|
466
|
+
} else {
|
|
467
|
+
return createResult({
|
|
468
|
+
actionType,
|
|
469
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
470
|
+
error: `existing kimi manual-install requirement cannot be read; refusing to overwrite: ${readErr.message}`,
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
if (!requirementMissing) {
|
|
475
|
+
if (!existing || typeof existing !== 'object' || Array.isArray(existing)) {
|
|
476
|
+
return createResult({
|
|
477
|
+
actionType,
|
|
478
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
479
|
+
error: 'existing kimi manual-install requirement is not an object; refusing to overwrite',
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
const { createdAt: _existingCreatedAt, ...existingBody } = existing;
|
|
483
|
+
if (canonicalJson(existingBody) !== canonicalJson(requirement)) {
|
|
484
|
+
return createResult({
|
|
485
|
+
actionType,
|
|
486
|
+
status: ActionStatus.EXECUTE_FAILED,
|
|
487
|
+
error: 'existing kimi manual-install requirement conflicts with the current frozen action; refusing to overwrite',
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
} else {
|
|
491
|
+
await writeEvidenceAtomic(requirementPath, { ...requirement, createdAt: new Date().toISOString() });
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
return createResult({
|
|
495
|
+
actionType,
|
|
496
|
+
status: ActionStatus.EXECUTED,
|
|
497
|
+
observation: {
|
|
498
|
+
installed: false,
|
|
499
|
+
manualInstallRequired: true,
|
|
500
|
+
consumer: 'kimi',
|
|
501
|
+
plugin: action.plugin,
|
|
502
|
+
version: action.version,
|
|
503
|
+
entrySkill: action.entrySkill,
|
|
504
|
+
repo: action.repo,
|
|
505
|
+
ref,
|
|
506
|
+
installUrl,
|
|
507
|
+
planDigest,
|
|
508
|
+
attestationDir,
|
|
509
|
+
kimiCodeHome: kimiHome,
|
|
510
|
+
managedInstallRoot,
|
|
511
|
+
instructions,
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
}
|