@dot-skill/core 0.4.3 → 0.5.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/README.md +38 -0
- package/dist/compile.js +394 -5
- package/dist/hash.d.ts +10 -0
- package/dist/hash.js +70 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/mint.d.ts +41 -5
- package/dist/mint.js +236 -30
- package/dist/validate.d.ts +4 -0
- package/dist/validate.js +61 -2
- package/package.json +19 -4
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# `@dot-skill/core`
|
|
2
|
+
|
|
3
|
+
Compile, pack, validate, and mint `.skill` packages for the [Open `.skill` Protocol](https://github.com/dot-skill/dot-skill).
|
|
4
|
+
|
|
5
|
+
Turns a **SkillSource** / **SkillContract** into a deterministic ZIP with digests, privacy scrubbing, completeness gates, and optional mint attestation.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm i @dot-skill/core
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Capabilities
|
|
14
|
+
|
|
15
|
+
| Function | Role |
|
|
16
|
+
|----------|------|
|
|
17
|
+
| Compile | SkillSource → package files; release profile refuses if incomplete |
|
|
18
|
+
| Pack / unpack | Deterministic ZIP container |
|
|
19
|
+
| Validate | Structure + SHA-256 digests |
|
|
20
|
+
| Mint | Creation attestation (reference HMAC is development-only) |
|
|
21
|
+
| Inspect helpers | Manifest and seal visibility without execution |
|
|
22
|
+
|
|
23
|
+
## Profiles
|
|
24
|
+
|
|
25
|
+
- **`continuity`** — handoff draft; soft gaps allowed; not mintable
|
|
26
|
+
- **`release`** — complete or `compile_refused`; mintable when approved
|
|
27
|
+
|
|
28
|
+
## Related
|
|
29
|
+
|
|
30
|
+
- [`@dot-skill/protocol`](https://www.npmjs.com/package/@dot-skill/protocol) — types & contract
|
|
31
|
+
- [`@dot-skill/runtime`](https://www.npmjs.com/package/@dot-skill/runtime) — inspect / dry-run / execute
|
|
32
|
+
- [`skillerr`](https://www.npmjs.com/package/skillerr) — public install (`skill` CLI)
|
|
33
|
+
|
|
34
|
+
Docs: [PROTOCOL.md](https://github.com/dot-skill/dot-skill/blob/main/docs/PROTOCOL.md) · [MINT.md](https://github.com/dot-skill/dot-skill/blob/main/docs/MINT.md)
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT
|
package/dist/compile.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { DEFAULT_SKILL_POLICY, CONTAINER_VERSION, PROTOCOL_VERSION, WORKFLOW_DIALECT_VERSION, isValidAgentHost, recipeToSkillSource, } from "@dot-skill/protocol";
|
|
2
|
+
import { DEFAULT_SKILL_POLICY, CONTAINER_VERSION, PROTOCOL_VERSION, WORKFLOW_DIALECT_VERSION, assessSkillContract, isValidAgentHost, recipeToSkillSource, } from "@dot-skill/protocol";
|
|
3
3
|
import { packSkill, finalizeManifest, buildFileMap } from "./pack.js";
|
|
4
4
|
const PLACEHOLDER_RE = /\{\{([a-zA-Z_][a-zA-Z0-9_]*)\}\}|<([A-Z][A-Z0-9_]+)>|\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}/g;
|
|
5
5
|
const SECRET_PATTERNS = [
|
|
@@ -135,14 +135,391 @@ export function assessCompleteness(source, opts) {
|
|
|
135
135
|
complete: requiredMissing.length === 0,
|
|
136
136
|
present,
|
|
137
137
|
missing: requiredMissing,
|
|
138
|
-
hints: requiredMissing.map((m) => HINTS[m]),
|
|
138
|
+
hints: requiredMissing.map((m) => HINTS[m] ?? `Complete the ${m} declaration.`),
|
|
139
139
|
};
|
|
140
140
|
}
|
|
141
141
|
function sectionHasWorkflowAction(sections) {
|
|
142
142
|
return sections.some((s) => ["integration", "prompt", "implementation_note", "workflow_note", "code", "config"].includes(s.type));
|
|
143
143
|
}
|
|
144
|
+
function declaredItems(declaration) {
|
|
145
|
+
if (!declaration)
|
|
146
|
+
return [];
|
|
147
|
+
return declaration.status === "specified" ? declaration.items ?? [] : [];
|
|
148
|
+
}
|
|
149
|
+
function contractCompleteness(assessment, profile) {
|
|
150
|
+
const missing = [...new Set(assessment.issues.map((issue) => {
|
|
151
|
+
const root = issue.field.split(".")[0];
|
|
152
|
+
return (root === "contract" ? "semantic_contract" : root);
|
|
153
|
+
}))];
|
|
154
|
+
const contractParts = [
|
|
155
|
+
"semantic_contract",
|
|
156
|
+
"intent",
|
|
157
|
+
"triggers",
|
|
158
|
+
"inputs",
|
|
159
|
+
"preconditions",
|
|
160
|
+
"steps",
|
|
161
|
+
"branches",
|
|
162
|
+
"human_decisions",
|
|
163
|
+
"capabilities",
|
|
164
|
+
"permissions",
|
|
165
|
+
"forbidden_actions",
|
|
166
|
+
"outputs",
|
|
167
|
+
"recovery",
|
|
168
|
+
"verification",
|
|
169
|
+
"corrections",
|
|
170
|
+
"provenance",
|
|
171
|
+
];
|
|
172
|
+
return {
|
|
173
|
+
kind: "completeness_report",
|
|
174
|
+
profile,
|
|
175
|
+
complete: assessment.complete,
|
|
176
|
+
present: contractParts.filter((part) => !missing.includes(part)),
|
|
177
|
+
missing,
|
|
178
|
+
hints: assessment.issues.map((issue) => `${issue.field}: ${issue.fix}`),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
function compileContractStep(step) {
|
|
182
|
+
const base = {
|
|
183
|
+
id: step.id,
|
|
184
|
+
title: step.title,
|
|
185
|
+
optional: step.optional,
|
|
186
|
+
next: step.next,
|
|
187
|
+
on_fail: step.on_failure,
|
|
188
|
+
};
|
|
189
|
+
switch (step.kind) {
|
|
190
|
+
case "instruct":
|
|
191
|
+
return { ...base, kind: "instruct", text: step.instruction ?? "" };
|
|
192
|
+
case "prompt":
|
|
193
|
+
return { ...base, kind: "prompt", template: step.instruction ?? "" };
|
|
194
|
+
case "tool":
|
|
195
|
+
return {
|
|
196
|
+
...base,
|
|
197
|
+
kind: "tool",
|
|
198
|
+
capability: step.capability ?? "",
|
|
199
|
+
arguments: step.arguments,
|
|
200
|
+
argument_bindings: step.argument_bindings,
|
|
201
|
+
result_as: step.result_as,
|
|
202
|
+
};
|
|
203
|
+
case "transform":
|
|
204
|
+
return {
|
|
205
|
+
...base,
|
|
206
|
+
kind: "transform",
|
|
207
|
+
expression: step.instruction ?? "identity",
|
|
208
|
+
result_as: step.result_as,
|
|
209
|
+
};
|
|
210
|
+
case "checkpoint":
|
|
211
|
+
return { ...base, kind: "checkpoint", message: step.instruction };
|
|
212
|
+
case "human_decision":
|
|
213
|
+
return {
|
|
214
|
+
...base,
|
|
215
|
+
kind: "human_decision",
|
|
216
|
+
prompt: step.instruction ?? step.decision ?? step.title,
|
|
217
|
+
result_as: step.result_as,
|
|
218
|
+
};
|
|
219
|
+
case "verify":
|
|
220
|
+
return { ...base, kind: "verify", assertions: step.assertions ?? [] };
|
|
221
|
+
case "emit":
|
|
222
|
+
return {
|
|
223
|
+
...base,
|
|
224
|
+
kind: "emit",
|
|
225
|
+
output: step.output ?? "",
|
|
226
|
+
from: step.from ?? "",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
function compileNativeContract(source, opts, profile) {
|
|
231
|
+
const contract = source.contract;
|
|
232
|
+
if (!isValidAgentHost(source.agent.host) || !source.journey?.summary?.trim()) {
|
|
233
|
+
const missing = [];
|
|
234
|
+
const hints = [];
|
|
235
|
+
if (!isValidAgentHost(source.agent.host)) {
|
|
236
|
+
missing.push("agent_context");
|
|
237
|
+
hints.push(HINTS.agent_context);
|
|
238
|
+
}
|
|
239
|
+
if (!source.journey?.summary?.trim()) {
|
|
240
|
+
missing.push("journey");
|
|
241
|
+
hints.push(HINTS.journey);
|
|
242
|
+
}
|
|
243
|
+
throw new CompileRefusalError({
|
|
244
|
+
kind: "completeness_report",
|
|
245
|
+
profile,
|
|
246
|
+
complete: false,
|
|
247
|
+
present: [],
|
|
248
|
+
missing,
|
|
249
|
+
hints,
|
|
250
|
+
});
|
|
251
|
+
}
|
|
252
|
+
const assessment = assessSkillContract(contract, profile);
|
|
253
|
+
const completeness = contractCompleteness(assessment, profile);
|
|
254
|
+
if (!assessment.complete && !opts.allow_incomplete && profile === "release") {
|
|
255
|
+
throw new CompileRefusalError(completeness);
|
|
256
|
+
}
|
|
257
|
+
const skillId = opts.skill_id ?? shortId("skl");
|
|
258
|
+
const inputs = declaredItems(contract.inputs).map((input) => ({
|
|
259
|
+
name: input.name,
|
|
260
|
+
description: input.description,
|
|
261
|
+
schema: structuredClone(input.schema),
|
|
262
|
+
required: input.required,
|
|
263
|
+
...(input.default !== undefined ? { default: structuredClone(input.default) } : {}),
|
|
264
|
+
sensitivity: input.sensitivity,
|
|
265
|
+
source: input.source,
|
|
266
|
+
ask_when: input.ask_when,
|
|
267
|
+
approval: input.approval,
|
|
268
|
+
// Contract review approves the slot definition; human_before_use remains a runtime gate.
|
|
269
|
+
approved: true,
|
|
270
|
+
}));
|
|
271
|
+
const outputs = declaredItems(contract.outputs).map((output) => ({
|
|
272
|
+
name: output.name,
|
|
273
|
+
description: output.description,
|
|
274
|
+
schema: structuredClone(output.schema),
|
|
275
|
+
required: output.required,
|
|
276
|
+
media_type: output.media_type,
|
|
277
|
+
assert: output.assertions,
|
|
278
|
+
}));
|
|
279
|
+
const capabilities = declaredItems(contract.capabilities).map((capability) => ({
|
|
280
|
+
...structuredClone(capability),
|
|
281
|
+
}));
|
|
282
|
+
const permissions = declaredItems(contract.permissions).map((permission) => ({
|
|
283
|
+
side_effect_class: permission.side_effect_class,
|
|
284
|
+
description: permission.description,
|
|
285
|
+
paths: permission.paths,
|
|
286
|
+
hosts: permission.hosts,
|
|
287
|
+
requires_consent: permission.consent === "explicit_human",
|
|
288
|
+
}));
|
|
289
|
+
const knowledge = source.sections.map((section) => ({
|
|
290
|
+
kind: "knowledge",
|
|
291
|
+
id: `k_${section.id.replace(/[^a-zA-Z0-9]/g, "").slice(0, 16)}`,
|
|
292
|
+
type: knowledgeTypeFor(section),
|
|
293
|
+
title: section.title,
|
|
294
|
+
body: redactSecrets(section.body),
|
|
295
|
+
fidelity: "exact",
|
|
296
|
+
pinned: true,
|
|
297
|
+
sensitivity: section.sensitivity === "public" ? "public" : "private",
|
|
298
|
+
provenance: [{ kind: "section", id: section.id, revision: section.revision, hash: source.hash }],
|
|
299
|
+
}));
|
|
300
|
+
const steps = declaredItems(contract.steps).map(compileContractStep);
|
|
301
|
+
for (let i = 0; i < steps.length - 1; i++) {
|
|
302
|
+
if (!steps[i].next)
|
|
303
|
+
steps[i].next = steps[i + 1].id;
|
|
304
|
+
}
|
|
305
|
+
let entrypoint = steps[0]?.id;
|
|
306
|
+
for (const branch of declaredItems(contract.branches)) {
|
|
307
|
+
const branchStep = {
|
|
308
|
+
id: branch.id,
|
|
309
|
+
kind: "branch",
|
|
310
|
+
title: `Conditional branch: ${branch.condition}`,
|
|
311
|
+
cases: [{ when: branch.condition, goto: branch.then }],
|
|
312
|
+
else: branch.otherwise,
|
|
313
|
+
};
|
|
314
|
+
steps.push(branchStep);
|
|
315
|
+
if (branch.after_step) {
|
|
316
|
+
const parent = steps.find((step) => step.id === branch.after_step);
|
|
317
|
+
if (parent)
|
|
318
|
+
parent.next = branch.id;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
for (const decision of declaredItems(contract.human_decisions)) {
|
|
322
|
+
const decisionStep = {
|
|
323
|
+
id: decision.id,
|
|
324
|
+
kind: "human_decision",
|
|
325
|
+
title: decision.prompt,
|
|
326
|
+
prompt: decision.prompt,
|
|
327
|
+
choices: decision.choices,
|
|
328
|
+
result_as: decision.id,
|
|
329
|
+
next: decision.required_before,
|
|
330
|
+
};
|
|
331
|
+
for (const step of steps) {
|
|
332
|
+
if (step.next === decision.required_before)
|
|
333
|
+
step.next = decision.id;
|
|
334
|
+
}
|
|
335
|
+
if (entrypoint === decision.required_before)
|
|
336
|
+
entrypoint = decision.id;
|
|
337
|
+
steps.push(decisionStep);
|
|
338
|
+
}
|
|
339
|
+
for (const input of declaredItems(contract.inputs).filter((item) => item.approval === "human_before_use")) {
|
|
340
|
+
const id = `approve_input_${slug(input.name)}`;
|
|
341
|
+
steps.push({
|
|
342
|
+
id,
|
|
343
|
+
kind: "human_decision",
|
|
344
|
+
title: `Approve input ${input.name}`,
|
|
345
|
+
prompt: `Approve use of input ${input.name}: ${input.description}`,
|
|
346
|
+
choices: ["approve", "deny"],
|
|
347
|
+
result_as: id,
|
|
348
|
+
next: entrypoint,
|
|
349
|
+
});
|
|
350
|
+
entrypoint = id;
|
|
351
|
+
}
|
|
352
|
+
for (const edge of declaredItems(contract.recovery)) {
|
|
353
|
+
const from = steps.find((step) => step.id === edge.from_step);
|
|
354
|
+
if (from && edge.goto)
|
|
355
|
+
from.on_fail = edge.goto;
|
|
356
|
+
}
|
|
357
|
+
if (declaredItems(contract.preconditions).length) {
|
|
358
|
+
const id = "contract_preconditions";
|
|
359
|
+
steps.push({
|
|
360
|
+
id,
|
|
361
|
+
kind: "verify",
|
|
362
|
+
title: "Verify contract preconditions",
|
|
363
|
+
assertions: declaredItems(contract.preconditions).map((item) => `precondition:${item.id}`),
|
|
364
|
+
next: entrypoint,
|
|
365
|
+
});
|
|
366
|
+
entrypoint = id;
|
|
367
|
+
}
|
|
368
|
+
const verification = declaredItems(contract.verification);
|
|
369
|
+
if (verification.length) {
|
|
370
|
+
const id = "contract_verification";
|
|
371
|
+
const terminal = steps.find((step) => !step.next && step.id !== id);
|
|
372
|
+
if (terminal)
|
|
373
|
+
terminal.next = id;
|
|
374
|
+
steps.push({
|
|
375
|
+
id,
|
|
376
|
+
kind: "verify",
|
|
377
|
+
title: "Verify domain assertions",
|
|
378
|
+
assertions: verification.map((item) => `contract_assertion:${item.id}`),
|
|
379
|
+
});
|
|
380
|
+
if (!entrypoint)
|
|
381
|
+
entrypoint = id;
|
|
382
|
+
}
|
|
383
|
+
if (!entrypoint) {
|
|
384
|
+
const id = "contract_noop";
|
|
385
|
+
steps.push({
|
|
386
|
+
id,
|
|
387
|
+
kind: "instruct",
|
|
388
|
+
title: "No operational steps declared",
|
|
389
|
+
text: contract.intent,
|
|
390
|
+
});
|
|
391
|
+
entrypoint = id;
|
|
392
|
+
}
|
|
393
|
+
const constraints = declaredItems(contract.forbidden_actions).map((forbidden) => ({
|
|
394
|
+
kind: "steering_constraint",
|
|
395
|
+
id: forbidden.id,
|
|
396
|
+
verb: "reject",
|
|
397
|
+
effect: "forbidden",
|
|
398
|
+
statement: forbidden.description,
|
|
399
|
+
}));
|
|
400
|
+
const report = {
|
|
401
|
+
kind: "compilation_report",
|
|
402
|
+
skill_id: skillId,
|
|
403
|
+
source_id: source.id,
|
|
404
|
+
profile,
|
|
405
|
+
created_at: new Date().toISOString(),
|
|
406
|
+
mappings: [],
|
|
407
|
+
inferred_inputs: [],
|
|
408
|
+
issues: assessment.issues.map((issue) => ({
|
|
409
|
+
severity: profile === "release" ? "error" : "warning",
|
|
410
|
+
code: `contract_${issue.code}`,
|
|
411
|
+
message: `${issue.field}: ${issue.message}`,
|
|
412
|
+
related: [issue.field],
|
|
413
|
+
})),
|
|
414
|
+
pending_approvals: [],
|
|
415
|
+
approved: contract.provenance.human_review.status === "reviewed",
|
|
416
|
+
completeness,
|
|
417
|
+
semantic_contract: "native_0.5",
|
|
418
|
+
};
|
|
419
|
+
const safeJourney = {
|
|
420
|
+
...source.journey,
|
|
421
|
+
summary: redactSecrets(source.journey.summary),
|
|
422
|
+
open_questions: source.journey.open_questions?.map(redactSecrets),
|
|
423
|
+
decisions: source.journey.decisions?.map(redactSecrets),
|
|
424
|
+
redacted: true,
|
|
425
|
+
};
|
|
426
|
+
const files = {
|
|
427
|
+
manifest: {
|
|
428
|
+
kind: "dot-skill",
|
|
429
|
+
id: skillId,
|
|
430
|
+
version: opts.version ?? "1.0.0",
|
|
431
|
+
title: opts.title ?? contract.title,
|
|
432
|
+
description: opts.description ?? source.summary ?? contract.intent,
|
|
433
|
+
intent: contract.intent,
|
|
434
|
+
contract: structuredClone(contract),
|
|
435
|
+
triggers: declaredItems(contract.triggers),
|
|
436
|
+
preconditions: structuredClone(contract.preconditions),
|
|
437
|
+
branches: structuredClone(contract.branches),
|
|
438
|
+
human_decisions: structuredClone(contract.human_decisions),
|
|
439
|
+
forbidden_actions: structuredClone(contract.forbidden_actions),
|
|
440
|
+
recovery: structuredClone(contract.recovery),
|
|
441
|
+
verification: structuredClone(contract.verification),
|
|
442
|
+
corrections: structuredClone(contract.corrections),
|
|
443
|
+
authors: source.actor ? [source.actor] : undefined,
|
|
444
|
+
container_version: CONTAINER_VERSION,
|
|
445
|
+
protocol_version: PROTOCOL_VERSION,
|
|
446
|
+
entrypoint,
|
|
447
|
+
inputs,
|
|
448
|
+
outputs,
|
|
449
|
+
capabilities,
|
|
450
|
+
permissions,
|
|
451
|
+
policy: { ...DEFAULT_SKILL_POLICY },
|
|
452
|
+
content: [],
|
|
453
|
+
package_digest: "sha256:" + "0".repeat(64),
|
|
454
|
+
provenance_mode: opts.provenance_mode ?? (profile === "continuity" ? "redacted" : "full"),
|
|
455
|
+
compile_profile: profile,
|
|
456
|
+
completeness,
|
|
457
|
+
package_sensitivity: contract.sensitivity,
|
|
458
|
+
mint: { mint_status: "draft" },
|
|
459
|
+
needs_human_review: profile === "continuity" || contract.provenance.human_review.status !== "reviewed",
|
|
460
|
+
},
|
|
461
|
+
workflow: {
|
|
462
|
+
kind: "workflow",
|
|
463
|
+
dialect_version: WORKFLOW_DIALECT_VERSION,
|
|
464
|
+
entrypoint,
|
|
465
|
+
steps,
|
|
466
|
+
constraints,
|
|
467
|
+
preconditions: structuredClone(contract.preconditions),
|
|
468
|
+
branches: structuredClone(contract.branches),
|
|
469
|
+
human_decisions: structuredClone(contract.human_decisions),
|
|
470
|
+
recovery: structuredClone(contract.recovery),
|
|
471
|
+
verification: structuredClone(contract.verification),
|
|
472
|
+
},
|
|
473
|
+
knowledge,
|
|
474
|
+
provenance: {
|
|
475
|
+
source: opts.provenance_mode === "proof_only"
|
|
476
|
+
? undefined
|
|
477
|
+
: {
|
|
478
|
+
id: source.id,
|
|
479
|
+
hash: source.hash,
|
|
480
|
+
title: source.title,
|
|
481
|
+
contract: structuredClone(contract),
|
|
482
|
+
agent: {
|
|
483
|
+
...source.agent,
|
|
484
|
+
endpoint: source.agent.endpoint
|
|
485
|
+
? redactSecrets(source.agent.endpoint)
|
|
486
|
+
: undefined,
|
|
487
|
+
},
|
|
488
|
+
section_ids: source.sections.map((section) => `${section.id}@${section.revision}`),
|
|
489
|
+
},
|
|
490
|
+
journey: safeJourney,
|
|
491
|
+
generation_usage: opts.generation_usage ?? source.generation_usage,
|
|
492
|
+
proof: { source_id: source.id, source_hash: source.hash },
|
|
493
|
+
compilation_report: report,
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
const fileMap = buildFileMap(files);
|
|
497
|
+
files.manifest = finalizeManifest(files.manifest, fileMap);
|
|
498
|
+
const packageBytes = packSkill(files);
|
|
499
|
+
return {
|
|
500
|
+
files,
|
|
501
|
+
report,
|
|
502
|
+
packageBytes,
|
|
503
|
+
pending_approvals: [],
|
|
504
|
+
completeness,
|
|
505
|
+
};
|
|
506
|
+
}
|
|
144
507
|
export function compileSkillSource(source, opts = {}) {
|
|
145
508
|
const profile = opts.profile ?? "release";
|
|
509
|
+
if (source.contract)
|
|
510
|
+
return compileNativeContract(source, opts, profile);
|
|
511
|
+
if (profile === "release") {
|
|
512
|
+
throw new CompileRefusalError({
|
|
513
|
+
kind: "completeness_report",
|
|
514
|
+
profile,
|
|
515
|
+
complete: false,
|
|
516
|
+
present: [],
|
|
517
|
+
missing: ["semantic_contract"],
|
|
518
|
+
hints: [
|
|
519
|
+
"Legacy 0.4 SkillSource/Recipe text is a lossy adapter. Add a protocol 0.5 SkillContract and assess it before release compile; use continuity only for migration.",
|
|
520
|
+
],
|
|
521
|
+
});
|
|
522
|
+
}
|
|
146
523
|
const skillId = opts.skill_id ?? shortId("skl");
|
|
147
524
|
const version = opts.version ?? "1.0.0";
|
|
148
525
|
const issues = [];
|
|
@@ -380,8 +757,12 @@ export function compileSkillSource(source, opts = {}) {
|
|
|
380
757
|
hasInputsDeclared,
|
|
381
758
|
pendingApprovals: pending,
|
|
382
759
|
});
|
|
383
|
-
if (!
|
|
384
|
-
|
|
760
|
+
if (!source.contract) {
|
|
761
|
+
completeness.complete = false;
|
|
762
|
+
if (!completeness.missing.includes("semantic_contract")) {
|
|
763
|
+
completeness.missing.push("semantic_contract");
|
|
764
|
+
}
|
|
765
|
+
completeness.hints.push("Add a 0.5 SkillContract. Legacy text was retained for continuity but structured semantics are unknown.");
|
|
385
766
|
}
|
|
386
767
|
if (!completeness.complete && profile === "continuity") {
|
|
387
768
|
// continuity still requires hard parts
|
|
@@ -429,6 +810,13 @@ export function compileSkillSource(source, opts = {}) {
|
|
|
429
810
|
pending_approvals: pending,
|
|
430
811
|
approved: pending.length === 0,
|
|
431
812
|
completeness,
|
|
813
|
+
semantic_contract: "legacy_lossy",
|
|
814
|
+
losses: [
|
|
815
|
+
"intent/triggers may be inferred from title or summary",
|
|
816
|
+
"inputs inferred from placeholders lose original schema semantics",
|
|
817
|
+
"preconditions, branches, outputs, recovery, and verification may remain prose",
|
|
818
|
+
"human review cannot be reconstructed from legacy text",
|
|
819
|
+
],
|
|
432
820
|
};
|
|
433
821
|
const files = {
|
|
434
822
|
manifest: {
|
|
@@ -440,7 +828,7 @@ export function compileSkillSource(source, opts = {}) {
|
|
|
440
828
|
source.summary ??
|
|
441
829
|
`Skill compiled from source ${source.id}`,
|
|
442
830
|
intent: source.intent ?? source.summary,
|
|
443
|
-
triggers: [source.title],
|
|
831
|
+
triggers: [{ id: "legacy_title", description: source.title }],
|
|
444
832
|
authors: source.actor ? [source.actor] : undefined,
|
|
445
833
|
container_version: CONTAINER_VERSION,
|
|
446
834
|
protocol_version: PROTOCOL_VERSION,
|
|
@@ -476,6 +864,7 @@ export function compileSkillSource(source, opts = {}) {
|
|
|
476
864
|
package_sensitivity: source.sensitivity,
|
|
477
865
|
mint: { mint_status: "draft" },
|
|
478
866
|
needs_human_review: pending.length > 0 || profile === "continuity",
|
|
867
|
+
legacy: true,
|
|
479
868
|
},
|
|
480
869
|
workflow: {
|
|
481
870
|
kind: "workflow",
|
package/dist/hash.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { SealedManifestClaims, SkillManifest } from "@dot-skill/protocol";
|
|
1
2
|
/** RFC 8785-inspired JSON Canonicalization for I-JSON-compatible objects. */
|
|
2
3
|
export declare function canonicalize(value: unknown): string;
|
|
3
4
|
export declare function sha256Hex(data: string | Uint8Array): string;
|
|
@@ -6,3 +7,12 @@ export declare function packageDigestFromContent(content: Array<{
|
|
|
6
7
|
path: string;
|
|
7
8
|
digest: string;
|
|
8
9
|
}>): string;
|
|
10
|
+
/** Public development HMAC key — forgeable; never production trust. */
|
|
11
|
+
export declare const PUBLIC_DEV_MINT_KEY = "dot-skill-dev-mint-key";
|
|
12
|
+
export declare const PUBLIC_DEV_MINT_KEY_ID = "dot-skill-dev-mint-key";
|
|
13
|
+
/**
|
|
14
|
+
* Build the claim set covered by the creation seal.
|
|
15
|
+
* Binds identity, intent, permissions, policy, capabilities, inputs, and content digests.
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildSealedManifestClaims(manifest: SkillManifest): SealedManifestClaims;
|
|
18
|
+
export declare function sealedManifestDigest(manifest: SkillManifest): string;
|
package/dist/hash.js
CHANGED
|
@@ -50,3 +50,73 @@ export function packageDigestFromContent(content) {
|
|
|
50
50
|
}
|
|
51
51
|
return sha256Digest(canonicalize({ paths }));
|
|
52
52
|
}
|
|
53
|
+
/** Public development HMAC key — forgeable; never production trust. */
|
|
54
|
+
export const PUBLIC_DEV_MINT_KEY = "dot-skill-dev-mint-key";
|
|
55
|
+
export const PUBLIC_DEV_MINT_KEY_ID = "dot-skill-dev-mint-key";
|
|
56
|
+
/**
|
|
57
|
+
* Build the claim set covered by the creation seal.
|
|
58
|
+
* Binds identity, intent, permissions, policy, capabilities, inputs, and content digests.
|
|
59
|
+
*/
|
|
60
|
+
export function buildSealedManifestClaims(manifest) {
|
|
61
|
+
const claims = {
|
|
62
|
+
id: manifest.id,
|
|
63
|
+
version: manifest.version,
|
|
64
|
+
title: manifest.title,
|
|
65
|
+
intent: manifest.intent,
|
|
66
|
+
description: manifest.description,
|
|
67
|
+
package_digest: manifest.package_digest,
|
|
68
|
+
permissions: [...manifest.permissions]
|
|
69
|
+
.map((p) => ({
|
|
70
|
+
side_effect_class: p.side_effect_class,
|
|
71
|
+
description: p.description,
|
|
72
|
+
paths: p.paths,
|
|
73
|
+
hosts: p.hosts,
|
|
74
|
+
requires_consent: p.requires_consent,
|
|
75
|
+
}))
|
|
76
|
+
.sort((a, b) => `${a.side_effect_class}:${a.description}`.localeCompare(`${b.side_effect_class}:${b.description}`)),
|
|
77
|
+
policy: {
|
|
78
|
+
require_signatures: manifest.policy.require_signatures,
|
|
79
|
+
require_minted: manifest.policy.require_minted,
|
|
80
|
+
require_anchor: manifest.policy.require_anchor,
|
|
81
|
+
allow_network: manifest.policy.allow_network,
|
|
82
|
+
filesystem_roots: manifest.policy.filesystem_roots
|
|
83
|
+
? [...manifest.policy.filesystem_roots].sort()
|
|
84
|
+
: undefined,
|
|
85
|
+
consent_for: [...manifest.policy.consent_for].sort(),
|
|
86
|
+
trust_profile: manifest.policy.trust_profile,
|
|
87
|
+
max_tool_calls: manifest.policy.max_tool_calls,
|
|
88
|
+
max_runtime_ms: manifest.policy.max_runtime_ms,
|
|
89
|
+
fail_on_unsupported_step: manifest.policy.fail_on_unsupported_step,
|
|
90
|
+
},
|
|
91
|
+
capabilities: [...manifest.capabilities]
|
|
92
|
+
.map((c) => ({
|
|
93
|
+
name: c.name,
|
|
94
|
+
side_effect_class: c.side_effect_class,
|
|
95
|
+
required: c.required,
|
|
96
|
+
}))
|
|
97
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
98
|
+
inputs: [...manifest.inputs]
|
|
99
|
+
.map((i) => ({
|
|
100
|
+
name: i.name,
|
|
101
|
+
sensitivity: i.sensitivity,
|
|
102
|
+
required: i.required,
|
|
103
|
+
source: i.source,
|
|
104
|
+
}))
|
|
105
|
+
.sort((a, b) => a.name.localeCompare(b.name)),
|
|
106
|
+
content: [...manifest.content]
|
|
107
|
+
.map((c) => ({ path: c.path, digest: c.digest, media_type: c.media_type, bytes: c.bytes }))
|
|
108
|
+
.sort((a, b) => a.path.localeCompare(b.path)),
|
|
109
|
+
};
|
|
110
|
+
if (manifest.contract) {
|
|
111
|
+
claims.contract = {
|
|
112
|
+
title: manifest.contract.title,
|
|
113
|
+
intent: manifest.contract.intent,
|
|
114
|
+
skill_kind: manifest.contract.skill_kind,
|
|
115
|
+
sensitivity: manifest.contract.sensitivity,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
return claims;
|
|
119
|
+
}
|
|
120
|
+
export function sealedManifestDigest(manifest) {
|
|
121
|
+
return sha256Digest(canonicalize(buildSealedManifestClaims(manifest)));
|
|
122
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/** @dot-skill/core — pack, unpack, validate, mint, compile .skill packages. */
|
|
2
|
-
export { canonicalize, sha256Hex, sha256Digest, packageDigestFromContent } from "./hash.js";
|
|
2
|
+
export { canonicalize, sha256Hex, sha256Digest, packageDigestFromContent, buildSealedManifestClaims, sealedManifestDigest, PUBLIC_DEV_MINT_KEY, PUBLIC_DEV_MINT_KEY_ID, } from "./hash.js";
|
|
3
3
|
export { normalizePath, assertSafePaths, MAX_ENTRIES, MAX_UNCOMPRESSED_BYTES, MAX_COMPRESSION_RATIO, } from "./paths.js";
|
|
4
4
|
export { buildFileMap, finalizeManifest, packSkill, unpackSkill, } from "./pack.js";
|
|
5
5
|
export type { PackOptions, UnpackResult } from "./pack.js";
|
|
6
6
|
export { validateManifestShape, validateWorkflowShape, validatePackageBytes, inspectSkill, } from "./validate.js";
|
|
7
7
|
export type { ValidationIssue, ValidationResult } from "./validate.js";
|
|
8
8
|
export { migrateLegacySkill, toSkillMdAdapter } from "./migrate.js";
|
|
9
|
-
export { mintSkillPackage, addPermanenceAnchor, verifyMintTrust, } from "./mint.js";
|
|
10
|
-
export type { MintOptions } from "./mint.js";
|
|
9
|
+
export { mintSkillPackage, addPermanenceAnchor, verifyMintTrust, inspectTrustView, } from "./mint.js";
|
|
10
|
+
export type { MintOptions, VerifyMintTrustOptions } from "./mint.js";
|
|
11
11
|
export { compileSkillSource, compileRecipeToSkill, approveCompilation, assessCompleteness, redactSecrets, CompileRefusalError, } from "./compile.js";
|
|
12
12
|
export type { CompileOptions, CompileResult } from "./compile.js";
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
/** @dot-skill/core — pack, unpack, validate, mint, compile .skill packages. */
|
|
2
|
-
export { canonicalize, sha256Hex, sha256Digest, packageDigestFromContent } from "./hash.js";
|
|
2
|
+
export { canonicalize, sha256Hex, sha256Digest, packageDigestFromContent, buildSealedManifestClaims, sealedManifestDigest, PUBLIC_DEV_MINT_KEY, PUBLIC_DEV_MINT_KEY_ID, } from "./hash.js";
|
|
3
3
|
export { normalizePath, assertSafePaths, MAX_ENTRIES, MAX_UNCOMPRESSED_BYTES, MAX_COMPRESSION_RATIO, } from "./paths.js";
|
|
4
4
|
export { buildFileMap, finalizeManifest, packSkill, unpackSkill, } from "./pack.js";
|
|
5
5
|
export { validateManifestShape, validateWorkflowShape, validatePackageBytes, inspectSkill, } from "./validate.js";
|
|
6
6
|
export { migrateLegacySkill, toSkillMdAdapter } from "./migrate.js";
|
|
7
|
-
export { mintSkillPackage, addPermanenceAnchor, verifyMintTrust, } from "./mint.js";
|
|
7
|
+
export { mintSkillPackage, addPermanenceAnchor, verifyMintTrust, inspectTrustView, } from "./mint.js";
|
|
8
8
|
export { compileSkillSource, compileRecipeToSkill, approveCompilation, assessCompleteness, redactSecrets, CompileRefusalError, } from "./compile.js";
|
package/dist/mint.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { CreationAttestation, PermanenceAnchor, SkillPackageFiles, TrustProfile } from "@dot-skill/protocol";
|
|
1
|
+
import type { CreationAttestation, HostClaimBinding, PermanenceAnchor, SkillPackageFiles, TrustProfile, TrustState, TrustView } from "@dot-skill/protocol";
|
|
2
2
|
import { type ValidationIssue } from "./validate.js";
|
|
3
3
|
export interface MintOptions {
|
|
4
4
|
host: string;
|
|
@@ -11,16 +11,47 @@ export interface MintOptions {
|
|
|
11
11
|
endpoint?: string;
|
|
12
12
|
actors?: string[];
|
|
13
13
|
/**
|
|
14
|
-
* HMAC-style digest seal
|
|
15
|
-
*
|
|
16
|
-
* Default key is `dot-skill-dev-mint-key` and must be replaced in production.
|
|
14
|
+
* HMAC-style digest seal.
|
|
15
|
+
* Omit / use the public constant only for local development — never production trust.
|
|
17
16
|
*/
|
|
18
17
|
issuer_secret?: string;
|
|
19
18
|
policy_profile?: TrustProfile;
|
|
19
|
+
/**
|
|
20
|
+
* Evidence that mint was invoked from an agent runtime path (not a bare human shell
|
|
21
|
+
* that only exported SKILL_HOST). Markers are still locally spoofable; residual risk
|
|
22
|
+
* for local LLMs remains.
|
|
23
|
+
*/
|
|
24
|
+
agent_runtime_evidence?: {
|
|
25
|
+
markers?: string[];
|
|
26
|
+
session_id?: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Only set when a non-public issuer key authenticates the host/model claim.
|
|
30
|
+
* Default is always self_reported.
|
|
31
|
+
*/
|
|
32
|
+
host_claim_binding?: HostClaimBinding;
|
|
33
|
+
/** Env snapshot for marker detection (defaults to process.env). */
|
|
34
|
+
env?: Record<string, string | undefined>;
|
|
35
|
+
}
|
|
36
|
+
export interface VerifyMintTrustOptions {
|
|
37
|
+
issuer_secret?: string;
|
|
38
|
+
/**
|
|
39
|
+
* When true, public-dev HMAC may pass structural verification as trust_state=development.
|
|
40
|
+
* Production execute paths must leave this false (fail closed).
|
|
41
|
+
*/
|
|
42
|
+
allow_development_issuer?: boolean;
|
|
43
|
+
/** Accept self_reported host binding as ok for the requested profile. Default false for minted+. */
|
|
44
|
+
allow_self_reported?: boolean;
|
|
20
45
|
}
|
|
21
46
|
/**
|
|
22
47
|
* Seal a draft package as minted.
|
|
23
48
|
* Content under signatures/ may change; package_digest (content) stays fixed after finalize.
|
|
49
|
+
*
|
|
50
|
+
* Trust rules:
|
|
51
|
+
* - Forbidden hosts (human/cli/shell/manual/…) refuse mint entirely.
|
|
52
|
+
* - Env-only SKILL_HOST without agent runtime markers still mints, but as self_reported
|
|
53
|
+
* + public_dev_hmac — never production trust.
|
|
54
|
+
* - Seal binds sealed_manifest_digest (identity + policy + content claims).
|
|
24
55
|
*/
|
|
25
56
|
export declare function mintSkillPackage(pkg: SkillPackageFiles, opts: MintOptions): {
|
|
26
57
|
files: SkillPackageFiles;
|
|
@@ -30,8 +61,13 @@ export declare function mintSkillPackage(pkg: SkillPackageFiles, opts: MintOptio
|
|
|
30
61
|
export declare function addPermanenceAnchor(archive: Uint8Array, anchor: Omit<PermanenceAnchor, "package_digest"> & {
|
|
31
62
|
package_digest?: string;
|
|
32
63
|
}): Uint8Array;
|
|
33
|
-
export declare function verifyMintTrust(archive: Uint8Array, profile?: TrustProfile,
|
|
64
|
+
export declare function verifyMintTrust(archive: Uint8Array, profile?: TrustProfile, issuer_secret_or_opts?: string | VerifyMintTrustOptions): {
|
|
34
65
|
ok: boolean;
|
|
35
66
|
issues: ValidationIssue[];
|
|
36
67
|
attestation?: CreationAttestation;
|
|
68
|
+
trust_state: TrustState;
|
|
37
69
|
};
|
|
70
|
+
/**
|
|
71
|
+
* TrustView from skill.json + signatures + digests only — no compile, no model body ingest.
|
|
72
|
+
*/
|
|
73
|
+
export declare function inspectTrustView(archive: Uint8Array): TrustView;
|
package/dist/mint.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { isValidAgentHost } from "@dot-skill/protocol";
|
|
3
|
-
import { canonicalize, sha256Digest } from "./hash.js";
|
|
2
|
+
import { detectAgentRuntimeMarkers, hasAgentRuntimeEvidence, isValidAgentHost, } from "@dot-skill/protocol";
|
|
3
|
+
import { canonicalize, PUBLIC_DEV_MINT_KEY, PUBLIC_DEV_MINT_KEY_ID, sealedManifestDigest, sha256Digest, } from "./hash.js";
|
|
4
4
|
import { packSkill, unpackSkill } from "./pack.js";
|
|
5
5
|
import { validatePackageBytes } from "./validate.js";
|
|
6
6
|
function loadCoreIdentity() {
|
|
@@ -11,13 +11,38 @@ function loadCoreIdentity() {
|
|
|
11
11
|
return { name: metadata.name, version: metadata.version };
|
|
12
12
|
}
|
|
13
13
|
const CORE_IDENTITY = loadCoreIdentity();
|
|
14
|
+
function resolveIssuerClass(secret, keyId) {
|
|
15
|
+
if (secret === PUBLIC_DEV_MINT_KEY || keyId === PUBLIC_DEV_MINT_KEY_ID) {
|
|
16
|
+
return "public_dev_hmac";
|
|
17
|
+
}
|
|
18
|
+
return "configured_hmac";
|
|
19
|
+
}
|
|
20
|
+
function resolveHostClaimBinding(opts, issuerClass, markers) {
|
|
21
|
+
if (opts.host_claim_binding === "verified_issuer") {
|
|
22
|
+
if (issuerClass === "public_dev_hmac") {
|
|
23
|
+
throw new Error("Cannot mark host_claim_binding=verified_issuer with the public development HMAC key");
|
|
24
|
+
}
|
|
25
|
+
if (!hasAgentRuntimeEvidence(opts.agent_runtime_evidence, opts.env) && markers.length === 0) {
|
|
26
|
+
throw new Error("verified_issuer host binding requires agent runtime evidence (markers or session_id)");
|
|
27
|
+
}
|
|
28
|
+
return "verified_issuer";
|
|
29
|
+
}
|
|
30
|
+
return "self_reported";
|
|
31
|
+
}
|
|
14
32
|
/**
|
|
15
33
|
* Seal a draft package as minted.
|
|
16
34
|
* Content under signatures/ may change; package_digest (content) stays fixed after finalize.
|
|
35
|
+
*
|
|
36
|
+
* Trust rules:
|
|
37
|
+
* - Forbidden hosts (human/cli/shell/manual/…) refuse mint entirely.
|
|
38
|
+
* - Env-only SKILL_HOST without agent runtime markers still mints, but as self_reported
|
|
39
|
+
* + public_dev_hmac — never production trust.
|
|
40
|
+
* - Seal binds sealed_manifest_digest (identity + policy + content claims).
|
|
17
41
|
*/
|
|
18
42
|
export function mintSkillPackage(pkg, opts) {
|
|
19
43
|
if (!isValidAgentHost(opts.host)) {
|
|
20
|
-
throw new Error(`Mint host "${opts.host}" is not a valid AI agent host
|
|
44
|
+
throw new Error(`Mint host "${opts.host}" is not a valid AI agent host (denylisted human/cli/shell/manual). ` +
|
|
45
|
+
`Use an agent host id such as cursor, ollama, lmstudio, llama-cpp, custom-agent.`);
|
|
21
46
|
}
|
|
22
47
|
if (pkg.manifest.needs_human_review) {
|
|
23
48
|
throw new Error("Cannot mint while needs_human_review is true — approve inputs/permissions first");
|
|
@@ -31,6 +56,7 @@ export function mintSkillPackage(pkg, opts) {
|
|
|
31
56
|
const report = pkg.provenance?.compilation_report;
|
|
32
57
|
if (!report ||
|
|
33
58
|
report.profile !== "release" ||
|
|
59
|
+
report.semantic_contract !== "native_0.5" ||
|
|
34
60
|
!report.completeness.complete ||
|
|
35
61
|
!report.approved ||
|
|
36
62
|
report.pending_approvals.length > 0) {
|
|
@@ -50,22 +76,46 @@ export function mintSkillPackage(pkg, opts) {
|
|
|
50
76
|
const package_digest = unpacked.manifest.package_digest;
|
|
51
77
|
const agentRuntime = opts.agent_runtime ?? CORE_IDENTITY.name;
|
|
52
78
|
const agentVersion = opts.agent_version ?? (agentRuntime === CORE_IDENTITY.name ? CORE_IDENTITY.version : "unknown");
|
|
79
|
+
const secret = opts.issuer_secret ?? PUBLIC_DEV_MINT_KEY;
|
|
80
|
+
const keyId = opts.key_id ?? (secret === PUBLIC_DEV_MINT_KEY ? PUBLIC_DEV_MINT_KEY_ID : "configured-issuer");
|
|
81
|
+
const issuer_class = resolveIssuerClass(secret, keyId);
|
|
82
|
+
const markers = [
|
|
83
|
+
...detectAgentRuntimeMarkers(opts.env),
|
|
84
|
+
...(opts.agent_runtime_evidence?.markers ?? []),
|
|
85
|
+
].filter((m, i, arr) => arr.indexOf(m) === i);
|
|
86
|
+
const host_claim_binding = resolveHostClaimBinding(opts, issuer_class, markers);
|
|
87
|
+
// Apply seal-bound policy updates before hashing sealed_manifest_digest so
|
|
88
|
+
// attestation covers the final identity/policy/content claims.
|
|
89
|
+
const sealedManifestBase = {
|
|
90
|
+
...unpacked.manifest,
|
|
91
|
+
policy: {
|
|
92
|
+
...unpacked.manifest.policy,
|
|
93
|
+
require_signatures: true,
|
|
94
|
+
require_minted: true,
|
|
95
|
+
trust_profile: opts.policy_profile ?? "minted",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
const sealed_manifest_digest = sealedManifestDigest(sealedManifestBase);
|
|
53
99
|
const attestation = {
|
|
54
100
|
kind: "creation_attestation",
|
|
55
101
|
package_digest,
|
|
102
|
+
sealed_manifest_digest,
|
|
56
103
|
skill_id: pkg.manifest.id,
|
|
57
104
|
skill_version: pkg.manifest.version,
|
|
58
105
|
minted_at: new Date().toISOString(),
|
|
59
106
|
agent: {
|
|
60
107
|
runtime: agentRuntime,
|
|
61
108
|
version: agentVersion,
|
|
62
|
-
key_id:
|
|
109
|
+
key_id: keyId,
|
|
63
110
|
},
|
|
64
111
|
host: opts.host,
|
|
65
112
|
provider: opts.provider,
|
|
66
113
|
model: opts.model,
|
|
67
114
|
deployment: opts.deployment,
|
|
68
115
|
endpoint: opts.endpoint,
|
|
116
|
+
host_claim_binding,
|
|
117
|
+
issuer_class,
|
|
118
|
+
agent_runtime_markers: markers.length ? markers : undefined,
|
|
69
119
|
journey: {
|
|
70
120
|
source_id: pkg.provenance?.compilation_report?.source_id,
|
|
71
121
|
source_hash: pkg.provenance?.proof &&
|
|
@@ -98,12 +148,6 @@ export function mintSkillPackage(pkg, opts) {
|
|
|
98
148
|
};
|
|
99
149
|
const payload = canonicalize(attestation);
|
|
100
150
|
const payloadDigest = sha256Digest(payload);
|
|
101
|
-
/**
|
|
102
|
-
* REFERENCE IMPLEMENTATION ONLY.
|
|
103
|
-
* The default key "dot-skill-dev-mint-key" is a public constant for testing.
|
|
104
|
-
* Replace issuer_secret with a real private key in any production issuer.
|
|
105
|
-
*/
|
|
106
|
-
const secret = opts.issuer_secret ?? "dot-skill-dev-mint-key";
|
|
107
151
|
const sig = sha256Digest(`${secret}:${payloadDigest}`);
|
|
108
152
|
const dsse = {
|
|
109
153
|
payloadType: "application/vnd.dot-skill.creation-attestation+json",
|
|
@@ -114,7 +158,7 @@ export function mintSkillPackage(pkg, opts) {
|
|
|
114
158
|
const minted = {
|
|
115
159
|
...unpacked.raw,
|
|
116
160
|
manifest: {
|
|
117
|
-
...
|
|
161
|
+
...sealedManifestBase,
|
|
118
162
|
mint: {
|
|
119
163
|
mint_status: "minted",
|
|
120
164
|
minted_at: attestation.minted_at,
|
|
@@ -122,12 +166,7 @@ export function mintSkillPackage(pkg, opts) {
|
|
|
122
166
|
content_id: package_digest,
|
|
123
167
|
},
|
|
124
168
|
attestation_digest: payloadDigest,
|
|
125
|
-
|
|
126
|
-
...unpacked.manifest.policy,
|
|
127
|
-
require_signatures: true,
|
|
128
|
-
require_minted: true,
|
|
129
|
-
trust_profile: opts.policy_profile ?? "minted",
|
|
130
|
-
},
|
|
169
|
+
sealed_manifest_digest,
|
|
131
170
|
},
|
|
132
171
|
attestation,
|
|
133
172
|
signatures: {
|
|
@@ -167,14 +206,30 @@ export function addPermanenceAnchor(archive, anchor) {
|
|
|
167
206
|
};
|
|
168
207
|
return packSkill(files);
|
|
169
208
|
}
|
|
170
|
-
|
|
209
|
+
function extractAttestation(unpacked) {
|
|
210
|
+
const envelope = unpacked.raw.signatures?.["creation.dsse.json"];
|
|
211
|
+
const attestation = envelope?.attestation ?? unpacked.raw.attestation;
|
|
212
|
+
return { attestation, envelope };
|
|
213
|
+
}
|
|
214
|
+
function classifyTrustState(attestation, signedOk) {
|
|
215
|
+
if (!attestation || !signedOk)
|
|
216
|
+
return "untrusted";
|
|
217
|
+
if (attestation.issuer_class === "public_dev_hmac")
|
|
218
|
+
return "development";
|
|
219
|
+
if (attestation.host_claim_binding === "verified_issuer")
|
|
220
|
+
return "verified_issuer";
|
|
221
|
+
return "self_reported";
|
|
222
|
+
}
|
|
223
|
+
export function verifyMintTrust(archive, profile = "minted", issuer_secret_or_opts) {
|
|
224
|
+
const opts = typeof issuer_secret_or_opts === "string"
|
|
225
|
+
? { issuer_secret: issuer_secret_or_opts }
|
|
226
|
+
: (issuer_secret_or_opts ?? {});
|
|
171
227
|
const base = validatePackageBytes(archive);
|
|
172
228
|
const issues = [...base.issues];
|
|
173
229
|
const unpacked = unpackSkill(archive);
|
|
174
230
|
const mintStatus = unpacked.manifest.mint?.mint_status ?? "draft";
|
|
175
|
-
const attestation = unpacked
|
|
176
|
-
|
|
177
|
-
const envelope = unpacked.raw.signatures?.["creation.dsse.json"];
|
|
231
|
+
const { attestation, envelope } = extractAttestation(unpacked);
|
|
232
|
+
let signedOk = false;
|
|
178
233
|
if (profile !== "open") {
|
|
179
234
|
if (mintStatus !== "minted") {
|
|
180
235
|
issues.push({
|
|
@@ -205,6 +260,30 @@ export function verifyMintTrust(archive, profile = "minted", issuer_secret) {
|
|
|
205
260
|
});
|
|
206
261
|
}
|
|
207
262
|
else {
|
|
263
|
+
const expectedSealed = sealedManifestDigest(unpacked.manifest);
|
|
264
|
+
const sealed = attestation.sealed_manifest_digest ?? unpacked.manifest.sealed_manifest_digest;
|
|
265
|
+
if (!sealed) {
|
|
266
|
+
issues.push({
|
|
267
|
+
severity: "error",
|
|
268
|
+
code: "missing_sealed_manifest_digest",
|
|
269
|
+
message: "Attestation must bind sealed_manifest_digest over identity/policy/content claims",
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
else if (sealed !== expectedSealed) {
|
|
273
|
+
issues.push({
|
|
274
|
+
severity: "error",
|
|
275
|
+
code: "sealed_manifest_digest_mismatch",
|
|
276
|
+
message: "sealed_manifest_digest does not match current manifest claims",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
if (unpacked.manifest.sealed_manifest_digest &&
|
|
280
|
+
unpacked.manifest.sealed_manifest_digest !== sealed) {
|
|
281
|
+
issues.push({
|
|
282
|
+
severity: "error",
|
|
283
|
+
code: "manifest_sealed_digest_mismatch",
|
|
284
|
+
message: "Manifest sealed_manifest_digest does not match attestation",
|
|
285
|
+
});
|
|
286
|
+
}
|
|
208
287
|
const payloadDigest = sha256Digest(canonicalize(attestation));
|
|
209
288
|
if (envelope.payload_digest !== payloadDigest) {
|
|
210
289
|
issues.push({
|
|
@@ -213,26 +292,68 @@ export function verifyMintTrust(archive, profile = "minted", issuer_secret) {
|
|
|
213
292
|
message: "DSSE payload_digest does not match CreationAttestation",
|
|
214
293
|
});
|
|
215
294
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
295
|
+
const issuerClass = attestation.issuer_class ??
|
|
296
|
+
(attestation.agent.key_id === PUBLIC_DEV_MINT_KEY_ID
|
|
297
|
+
? "public_dev_hmac"
|
|
298
|
+
: "configured_hmac");
|
|
299
|
+
// Fail closed: public-dev HMAC is never production trust unless explicitly allowed.
|
|
300
|
+
if (issuerClass === "public_dev_hmac" && !opts.allow_development_issuer) {
|
|
221
301
|
issues.push({
|
|
222
302
|
severity: "error",
|
|
223
|
-
code: "
|
|
224
|
-
message: "
|
|
303
|
+
code: "public_dev_issuer_untrusted",
|
|
304
|
+
message: "Seal uses the public development HMAC key — not production trust. " +
|
|
305
|
+
"Pass allow_development_issuer only for local testing, or mint with a configured issuer secret.",
|
|
225
306
|
});
|
|
226
307
|
}
|
|
227
|
-
|
|
308
|
+
const hostBinding = attestation.host_claim_binding ?? "self_reported";
|
|
309
|
+
if (hostBinding === "self_reported" &&
|
|
310
|
+
!opts.allow_self_reported &&
|
|
311
|
+
!opts.allow_development_issuer) {
|
|
312
|
+
issues.push({
|
|
313
|
+
severity: "error",
|
|
314
|
+
code: "self_reported_host_untrusted",
|
|
315
|
+
message: "Host/model claims are self_reported (e.g. SKILL_HOST env alone). " +
|
|
316
|
+
"Not treated as verified_issuer trust.",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
const secret = opts.issuer_secret ??
|
|
320
|
+
(issuerClass === "public_dev_hmac" ? PUBLIC_DEV_MINT_KEY : undefined);
|
|
321
|
+
if (!secret) {
|
|
322
|
+
issues.push({
|
|
323
|
+
severity: "error",
|
|
324
|
+
code: "issuer_secret_required",
|
|
325
|
+
message: "Configured issuer seal requires a matching issuer_secret in the trust store",
|
|
326
|
+
});
|
|
327
|
+
}
|
|
328
|
+
else {
|
|
329
|
+
const expected = sha256Digest(`${secret}:${payloadDigest}`);
|
|
330
|
+
const sig = envelope?.signatures?.[0]?.sig;
|
|
331
|
+
if (sig !== expected) {
|
|
332
|
+
issues.push({
|
|
333
|
+
severity: "error",
|
|
334
|
+
code: "attestation_sig_invalid",
|
|
335
|
+
message: "CreationAttestation signature failed verification",
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
else {
|
|
339
|
+
signedOk = true;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (issuerClass === "public_dev_hmac") {
|
|
228
343
|
issues.push({
|
|
229
344
|
severity: "warning",
|
|
230
345
|
code: "development_attestation",
|
|
231
|
-
message: "Attestation uses the public development key
|
|
346
|
+
message: "Attestation uses the public development key — labeled development, never production identity",
|
|
232
347
|
});
|
|
233
348
|
}
|
|
234
349
|
}
|
|
235
350
|
}
|
|
351
|
+
else if (attestation && envelope?.signatures?.[0]?.sig) {
|
|
352
|
+
// open profile: still classify if a seal is present
|
|
353
|
+
const payloadDigest = sha256Digest(canonicalize(attestation));
|
|
354
|
+
const secret = opts.issuer_secret ?? PUBLIC_DEV_MINT_KEY;
|
|
355
|
+
signedOk = envelope.signatures[0].sig === sha256Digest(`${secret}:${payloadDigest}`);
|
|
356
|
+
}
|
|
236
357
|
if (profile === "anchored") {
|
|
237
358
|
const anchors = unpacked.manifest.anchors ?? [];
|
|
238
359
|
if (!anchors.length) {
|
|
@@ -253,9 +374,94 @@ export function verifyMintTrust(archive, profile = "minted", issuer_secret) {
|
|
|
253
374
|
});
|
|
254
375
|
}
|
|
255
376
|
}
|
|
377
|
+
const trust_state = classifyTrustState(attestation, signedOk);
|
|
256
378
|
return {
|
|
257
379
|
ok: !issues.some((i) => i.severity === "error"),
|
|
258
380
|
issues,
|
|
259
381
|
attestation,
|
|
382
|
+
trust_state,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* TrustView from skill.json + signatures + digests only — no compile, no model body ingest.
|
|
387
|
+
*/
|
|
388
|
+
export function inspectTrustView(archive) {
|
|
389
|
+
const base = validatePackageBytes(archive);
|
|
390
|
+
const warnings = [];
|
|
391
|
+
if (!base.manifest) {
|
|
392
|
+
return {
|
|
393
|
+
trust_state: "untrusted",
|
|
394
|
+
mint_status: "draft",
|
|
395
|
+
signed: false,
|
|
396
|
+
package_digest: "",
|
|
397
|
+
label: "INVALID — package failed validation",
|
|
398
|
+
warnings: [],
|
|
399
|
+
issues: base.issues,
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const unpacked = unpackSkill(archive);
|
|
403
|
+
const m = unpacked.manifest;
|
|
404
|
+
const mint_status = m.mint?.mint_status ?? "draft";
|
|
405
|
+
const { attestation, envelope } = extractAttestation(unpacked);
|
|
406
|
+
const signed = Boolean(envelope?.signatures?.[0]?.sig && attestation);
|
|
407
|
+
let trust_state = "untrusted";
|
|
408
|
+
let label = "UNSIGNED / OPEN — untrusted";
|
|
409
|
+
if (mint_status === "draft" || !signed) {
|
|
410
|
+
trust_state = "untrusted";
|
|
411
|
+
label = "UNSIGNED / OPEN — untrusted (do not execute without --allow-untrusted)";
|
|
412
|
+
warnings.push("Package has no verified creation seal");
|
|
413
|
+
}
|
|
414
|
+
else {
|
|
415
|
+
const verify = verifyMintTrust(archive, "minted", {
|
|
416
|
+
allow_development_issuer: true,
|
|
417
|
+
allow_self_reported: true,
|
|
418
|
+
});
|
|
419
|
+
trust_state = verify.trust_state;
|
|
420
|
+
if (trust_state === "development") {
|
|
421
|
+
label = "DEVELOPMENT seal (public-dev HMAC) — not production trust";
|
|
422
|
+
warnings.push("Public development HMAC is forgeable; treat as untrusted for production execute");
|
|
423
|
+
}
|
|
424
|
+
else if (trust_state === "self_reported") {
|
|
425
|
+
label = "SELF-REPORTED agent host claims — signed but not verified_issuer";
|
|
426
|
+
warnings.push("Host/provider/model are self-asserted; local LLMs can lie about authorship");
|
|
427
|
+
}
|
|
428
|
+
else if (trust_state === "verified_issuer") {
|
|
429
|
+
label = "VERIFIED ISSUER seal — host claims bound by configured issuer";
|
|
430
|
+
warnings.push("Issuer key authenticity is established; model honesty (esp. local LLMs) remains a residual risk");
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
label = "UNTRUSTED — seal present but verification failed";
|
|
434
|
+
}
|
|
435
|
+
for (const issue of verify.issues) {
|
|
436
|
+
if (issue.severity === "warning")
|
|
437
|
+
warnings.push(issue.message);
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const expectedSealed = sealedManifestDigest(m);
|
|
441
|
+
return {
|
|
442
|
+
trust_state,
|
|
443
|
+
mint_status,
|
|
444
|
+
signed,
|
|
445
|
+
issuer: attestation?.agent.runtime ?? m.mint?.mint_issuer,
|
|
446
|
+
issuer_class: attestation?.issuer_class,
|
|
447
|
+
host_claim_binding: attestation?.host_claim_binding,
|
|
448
|
+
agent: attestation
|
|
449
|
+
? {
|
|
450
|
+
host: attestation.host,
|
|
451
|
+
provider: attestation.provider,
|
|
452
|
+
model: attestation.model,
|
|
453
|
+
runtime: attestation.agent.runtime,
|
|
454
|
+
version: attestation.agent.version,
|
|
455
|
+
key_id: attestation.agent.key_id,
|
|
456
|
+
deployment: attestation.deployment,
|
|
457
|
+
markers: attestation.agent_runtime_markers,
|
|
458
|
+
}
|
|
459
|
+
: undefined,
|
|
460
|
+
package_digest: m.package_digest,
|
|
461
|
+
sealed_manifest_digest: attestation?.sealed_manifest_digest ?? m.sealed_manifest_digest ?? expectedSealed,
|
|
462
|
+
attestation_digest: m.attestation_digest,
|
|
463
|
+
label,
|
|
464
|
+
warnings,
|
|
465
|
+
issues: base.issues,
|
|
260
466
|
};
|
|
261
467
|
}
|
package/dist/validate.d.ts
CHANGED
|
@@ -21,12 +21,16 @@ export declare function inspectSkill(archive: Uint8Array): {
|
|
|
21
21
|
version: string;
|
|
22
22
|
title: string;
|
|
23
23
|
description: string;
|
|
24
|
+
intent?: string;
|
|
24
25
|
inputs: string[];
|
|
25
26
|
permissions: string[];
|
|
26
27
|
capabilities: string[];
|
|
27
28
|
package_digest: string;
|
|
29
|
+
sealed_manifest_digest?: string;
|
|
28
30
|
mint_status?: string;
|
|
29
31
|
needs_human_review?: boolean;
|
|
32
|
+
trust_label?: string;
|
|
33
|
+
trust_state?: string;
|
|
30
34
|
};
|
|
31
35
|
issues: ValidationIssue[];
|
|
32
36
|
};
|
package/dist/validate.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONTAINER_VERSION, PROTOCOL_VERSION, WORKFLOW_DIALECT_VERSION, } from "@dot-skill/protocol";
|
|
1
|
+
import { assessSkillContract, CONTAINER_VERSION, PROTOCOL_VERSION, WORKFLOW_DIALECT_VERSION, } from "@dot-skill/protocol";
|
|
2
2
|
import { packageDigestFromContent, sha256Digest } from "./hash.js";
|
|
3
3
|
import { unpackSkill } from "./pack.js";
|
|
4
4
|
export function validateManifestShape(manifest) {
|
|
@@ -38,6 +38,25 @@ export function validateManifestShape(manifest) {
|
|
|
38
38
|
message: `Unsupported protocol_version ${manifest.protocol_version}; expected ${PROTOCOL_VERSION}`,
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
+
if (manifest.compile_profile === "release") {
|
|
42
|
+
if (!manifest.contract) {
|
|
43
|
+
issues.push({
|
|
44
|
+
severity: "error",
|
|
45
|
+
code: "release_contract_missing",
|
|
46
|
+
message: "Release packages require a native 0.5 authoring contract",
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
for (const issue of assessSkillContract(manifest.contract, "release").issues) {
|
|
51
|
+
issues.push({
|
|
52
|
+
severity: "error",
|
|
53
|
+
code: `contract_${issue.code}`,
|
|
54
|
+
message: `${issue.field}: ${issue.message}`,
|
|
55
|
+
path: issue.field,
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
41
60
|
if (manifest.mint?.mint_status === "minted") {
|
|
42
61
|
if (manifest.compile_profile !== "release") {
|
|
43
62
|
issues.push({
|
|
@@ -94,6 +113,23 @@ export function validateWorkflowShape(workflow, entrypoint) {
|
|
|
94
113
|
message: "Each step needs id and kind",
|
|
95
114
|
});
|
|
96
115
|
}
|
|
116
|
+
const refs = [
|
|
117
|
+
...(typeof step.next === "string" ? [step.next] : step.next ?? []),
|
|
118
|
+
...(step.on_fail ? [step.on_fail] : []),
|
|
119
|
+
...(step.kind === "branch"
|
|
120
|
+
? [...step.cases.map((branch) => branch.goto), ...(step.else ? [step.else] : [])]
|
|
121
|
+
: []),
|
|
122
|
+
];
|
|
123
|
+
for (const ref of refs) {
|
|
124
|
+
if (!ids.has(ref)) {
|
|
125
|
+
issues.push({
|
|
126
|
+
severity: "error",
|
|
127
|
+
code: "step_reference_missing",
|
|
128
|
+
message: `Step ${step.id} references missing step ${ref}`,
|
|
129
|
+
path: step.id,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
97
133
|
}
|
|
98
134
|
return issues;
|
|
99
135
|
}
|
|
@@ -191,11 +227,30 @@ export function inspectSkill(archive) {
|
|
|
191
227
|
permissions: [],
|
|
192
228
|
capabilities: [],
|
|
193
229
|
package_digest: "",
|
|
230
|
+
trust_label: "INVALID",
|
|
231
|
+
trust_state: "untrusted",
|
|
194
232
|
},
|
|
195
233
|
issues: result.issues,
|
|
196
234
|
};
|
|
197
235
|
}
|
|
198
236
|
const m = result.manifest;
|
|
237
|
+
const mint_status = m.mint?.mint_status ?? "draft";
|
|
238
|
+
const signed = Boolean((result.manifest &&
|
|
239
|
+
Object.keys(
|
|
240
|
+
// signatures live outside validate result — infer from mint + attestation_digest
|
|
241
|
+
m.attestation_digest ? { signed: true } : {}).length) ||
|
|
242
|
+
mint_status === "minted");
|
|
243
|
+
// Lightweight label without full TrustView import cycle; CLI --trust uses inspectTrustView.
|
|
244
|
+
let trust_label = "UNSIGNED / OPEN — untrusted";
|
|
245
|
+
let trust_state = "untrusted";
|
|
246
|
+
if (mint_status === "minted" && m.attestation_digest) {
|
|
247
|
+
trust_label = "SEALED — use skill inspect --trust for issuer/host details";
|
|
248
|
+
trust_state = "self_reported";
|
|
249
|
+
}
|
|
250
|
+
else if (!signed || mint_status === "draft") {
|
|
251
|
+
trust_label = "UNSIGNED / OPEN — untrusted";
|
|
252
|
+
trust_state = "untrusted";
|
|
253
|
+
}
|
|
199
254
|
return {
|
|
200
255
|
ok: result.ok,
|
|
201
256
|
summary: {
|
|
@@ -203,12 +258,16 @@ export function inspectSkill(archive) {
|
|
|
203
258
|
version: m.version,
|
|
204
259
|
title: m.title,
|
|
205
260
|
description: m.description,
|
|
261
|
+
intent: m.intent,
|
|
206
262
|
inputs: m.inputs.filter((i) => i.required).map((i) => i.name),
|
|
207
263
|
permissions: m.permissions.map((p) => p.side_effect_class),
|
|
208
264
|
capabilities: m.capabilities.map((c) => c.name),
|
|
209
265
|
package_digest: m.package_digest,
|
|
210
|
-
|
|
266
|
+
sealed_manifest_digest: m.sealed_manifest_digest,
|
|
267
|
+
mint_status,
|
|
211
268
|
needs_human_review: m.needs_human_review,
|
|
269
|
+
trust_label,
|
|
270
|
+
trust_state,
|
|
212
271
|
},
|
|
213
272
|
issues: result.issues,
|
|
214
273
|
};
|
package/package.json
CHANGED
|
@@ -1,12 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dot-skill/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.5.0",
|
|
4
|
+
"description": "Compile, pack, validate, and mint portable .skill packages",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Bharat Dudeja",
|
|
8
8
|
"url": "https://github.com/bharatdudeja13-cmd"
|
|
9
9
|
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"skill",
|
|
12
|
+
"dot-skill",
|
|
13
|
+
".skill",
|
|
14
|
+
"ai-agents",
|
|
15
|
+
"agent-skills",
|
|
16
|
+
"compile",
|
|
17
|
+
"mint"
|
|
18
|
+
],
|
|
10
19
|
"type": "module",
|
|
11
20
|
"main": "./dist/index.js",
|
|
12
21
|
"types": "./dist/index.d.ts",
|
|
@@ -18,8 +27,14 @@
|
|
|
18
27
|
"url": "https://github.com/dot-skill/dot-skill.git",
|
|
19
28
|
"directory": "packages/core"
|
|
20
29
|
},
|
|
30
|
+
"homepage": "https://github.com/dot-skill/dot-skill#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/dot-skill/dot-skill/issues"
|
|
33
|
+
},
|
|
21
34
|
"files": [
|
|
22
|
-
"dist"
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
23
38
|
],
|
|
24
39
|
"exports": {
|
|
25
40
|
".": {
|
|
@@ -33,7 +48,7 @@
|
|
|
33
48
|
"clean": "rm -rf dist"
|
|
34
49
|
},
|
|
35
50
|
"dependencies": {
|
|
36
|
-
"@dot-skill/protocol": "^0.
|
|
51
|
+
"@dot-skill/protocol": "^0.5.0",
|
|
37
52
|
"fflate": "^0.8.2"
|
|
38
53
|
},
|
|
39
54
|
"devDependencies": {
|