@dot-skill/runtime 0.4.2 → 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 ADDED
@@ -0,0 +1,36 @@
1
+ # `@dot-skill/runtime`
2
+
3
+ Reference runtime for the [Open `.skill` Protocol](https://github.com/dot-skill/dot-skill).
4
+
5
+ Load a `.skill` archive, verify integrity and trust profile, resolve inputs, and run in **inspect**, **dry_run**, or **execute** modes. Prefer inspect and dry-run before execute.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm i @dot-skill/runtime
11
+ ```
12
+
13
+ ## Lifecycle
14
+
15
+ `LoadAndVerify` → `NegotiateCapabilities` → `ResolveInputs` → `Consent` → `Execute` → `Verify` → `EmitSkillRun`
16
+
17
+ Modes: `inspect` · `explain` · `dry_run` · `execute` · `resume`
18
+
19
+ Fails clearly when required capabilities or minted trust profiles are unmet. Does not silently degrade.
20
+
21
+ ## Trust
22
+
23
+ - Digests and seals are visible without executing workflow steps.
24
+ - Trust profiles: `open` | `minted` | `anchored` | `issuer:<id>`
25
+ - Reference mint verification matches `@dot-skill/core` (dev HMAC ≠ production PKI)
26
+
27
+ ## Related
28
+
29
+ - [`@dot-skill/core`](https://www.npmjs.com/package/@dot-skill/core) — pack / validate / mint
30
+ - [`skillerr`](https://www.npmjs.com/package/skillerr) — `skill inspect` / `skill run`
31
+
32
+ Docs: [RUNTIME.md](https://github.com/dot-skill/dot-skill/blob/main/docs/RUNTIME.md) · [SECURITY.md](https://github.com/dot-skill/dot-skill/blob/main/docs/SECURITY.md)
33
+
34
+ ## License
35
+
36
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { CapabilityAdapterHint, CapabilityRequirement, InputSlot, RuntimeMode, SkillPackageFiles, SkillRun, TrustProfile } from "@dot-skill/protocol";
2
- import { inspectSkill, unpackSkill, validatePackageBytes } from "@dot-skill/core";
2
+ import { inspectSkill, inspectTrustView, unpackSkill, validatePackageBytes } from "@dot-skill/core";
3
3
  export interface CapabilityAdapter {
4
4
  name: string;
5
5
  supports: (cap: CapabilityRequirement) => boolean;
@@ -16,7 +16,29 @@ export interface RuntimeHost {
16
16
  title: string;
17
17
  permissions: string[];
18
18
  steps: string[];
19
- }) => Promise<boolean>;
19
+ }) => Promise<{
20
+ allowed: boolean;
21
+ actor: string;
22
+ at: string;
23
+ }>;
24
+ decide?: (decision: {
25
+ id: string;
26
+ prompt: string;
27
+ choices?: string[];
28
+ }) => Promise<{
29
+ decision: string;
30
+ actor: string;
31
+ at: string;
32
+ }>;
33
+ verifyAssertion?: (assertion: {
34
+ id: string;
35
+ assertion: string;
36
+ check: "runtime" | "capability" | "human" | "agent";
37
+ evidence?: string[];
38
+ }) => Promise<{
39
+ passed: boolean;
40
+ detail?: string;
41
+ }>;
20
42
  resolveSecret?: (ref: string) => Promise<string>;
21
43
  env?: Record<string, string>;
22
44
  adapters?: CapabilityAdapter[];
@@ -32,7 +54,21 @@ export interface RunOptions {
32
54
  trust_profile?: TrustProfile;
33
55
  /** Reference verifier only; production runtimes should use a real trust store. */
34
56
  issuer_secret?: string;
57
+ /**
58
+ * Explicit unsafe opt-in to execute unsigned / development / self_reported packages.
59
+ * Required for execute when TrustView is not verified_issuer.
60
+ */
61
+ allow_untrusted?: boolean;
62
+ /** Allow public-dev HMAC seals for local testing only. */
63
+ allow_development_issuer?: boolean;
35
64
  }
65
+ /**
66
+ * Deny-by-default: refuse undeclared network / filesystem / secret adapter use.
67
+ * Fail closed when consent is required but missing.
68
+ */
69
+ export declare function assertCapabilityAllowed(pkg: SkillPackageFiles, cap: CapabilityRequirement, args?: Record<string, unknown>): void;
70
+ /** Resolve a secret ref only if the input slot declares secret sensitivity/source. */
71
+ export declare function resolveDeclaredSecret(pkg: SkillPackageFiles, slotName: string, host: RuntimeHost): Promise<string>;
36
72
  export declare function explainPackage(pkg: SkillPackageFiles): {
37
73
  title: string;
38
74
  description: string;
@@ -47,4 +83,4 @@ export declare function explainPackage(pkg: SkillPackageFiles): {
47
83
  };
48
84
  export declare function runSkillPackage(pkg: SkillPackageFiles, host?: RuntimeHost, options?: RunOptions): Promise<SkillRun>;
49
85
  export declare function runSkillArchive(archive: Uint8Array, host?: RuntimeHost, options?: RunOptions): Promise<SkillRun>;
50
- export { inspectSkill, validatePackageBytes, unpackSkill, explainPackage as explain };
86
+ export { inspectSkill, inspectTrustView, validatePackageBytes, unpackSkill, explainPackage as explain, };
package/dist/index.js CHANGED
@@ -1,7 +1,90 @@
1
1
  import { randomUUID } from "node:crypto";
2
- import { inspectSkill, unpackSkill, validatePackageBytes, verifyMintTrust, sha256Digest, } from "@dot-skill/core";
3
- const RUNTIME_NAME = "@dot-skill/runtime";
4
- const RUNTIME_VERSION = "0.4.1";
2
+ import { readFileSync } from "node:fs";
3
+ import { inspectSkill, inspectTrustView, unpackSkill, validatePackageBytes, verifyMintTrust, sha256Digest, } from "@dot-skill/core";
4
+ function loadRuntimeIdentity() {
5
+ const metadata = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8"));
6
+ if (typeof metadata.name !== "string" || typeof metadata.version !== "string") {
7
+ throw new Error("Invalid @dot-skill/runtime package metadata");
8
+ }
9
+ return { name: metadata.name, version: metadata.version };
10
+ }
11
+ const { name: RUNTIME_NAME, version: RUNTIME_VERSION } = loadRuntimeIdentity();
12
+ function permissionCovers(permissions, sideEffect) {
13
+ return permissions.find((p) => p.side_effect_class === sideEffect);
14
+ }
15
+ /**
16
+ * Deny-by-default: refuse undeclared network / filesystem / secret adapter use.
17
+ * Fail closed when consent is required but missing.
18
+ */
19
+ export function assertCapabilityAllowed(pkg, cap, args = {}) {
20
+ const policy = pkg.manifest.policy;
21
+ const side = cap.side_effect_class;
22
+ if (side === "network") {
23
+ if (!policy.allow_network) {
24
+ throw new Error(`Denied: capability ${cap.name} requires network but policy.allow_network=false (deny-by-default)`);
25
+ }
26
+ const perm = permissionCovers(pkg.manifest.permissions, "network");
27
+ if (!perm) {
28
+ throw new Error(`Denied: capability ${cap.name} uses network but no network permission is declared`);
29
+ }
30
+ const hostArg = typeof args.host === "string"
31
+ ? args.host
32
+ : typeof args.url === "string"
33
+ ? args.url
34
+ : typeof args.endpoint === "string"
35
+ ? args.endpoint
36
+ : undefined;
37
+ if (perm.hosts?.length && hostArg) {
38
+ const ok = perm.hosts.some((h) => hostArg === h || hostArg.includes(h) || hostArg.startsWith(h));
39
+ if (!ok) {
40
+ throw new Error(`Denied: network host/url not in declared permission.hosts for ${cap.name}`);
41
+ }
42
+ }
43
+ }
44
+ if (side === "read" || side === "write" || side === "destructive") {
45
+ const perm = permissionCovers(pkg.manifest.permissions, side);
46
+ if (!perm && side !== "read") {
47
+ throw new Error(`Denied: capability ${cap.name} uses ${side} but no matching permission is declared`);
48
+ }
49
+ const pathArg = typeof args.path === "string"
50
+ ? args.path
51
+ : typeof args.file === "string"
52
+ ? args.file
53
+ : typeof args.root === "string"
54
+ ? args.root
55
+ : undefined;
56
+ const roots = policy.filesystem_roots;
57
+ if (pathArg && roots?.length) {
58
+ const ok = roots.some((root) => pathArg === root || pathArg.startsWith(root.endsWith("/") ? root : `${root}/`));
59
+ if (!ok) {
60
+ throw new Error(`Denied: path ${pathArg} outside policy.filesystem_roots for ${cap.name}`);
61
+ }
62
+ }
63
+ if (pathArg && perm?.paths?.length) {
64
+ const ok = perm.paths.some((p) => pathArg === p || pathArg.startsWith(p.endsWith("/") ? p : `${p}/`));
65
+ if (!ok) {
66
+ throw new Error(`Denied: path ${pathArg} not in declared permission.paths for ${cap.name}`);
67
+ }
68
+ }
69
+ }
70
+ if (side === "none") {
71
+ // no side effects — allowed
72
+ }
73
+ }
74
+ function assertSecretAccessAllowed(pkg, slotName) {
75
+ const slot = pkg.manifest.inputs.find((i) => i.name === slotName);
76
+ if (!slot || (slot.sensitivity !== "secret" && slot.source !== "secret")) {
77
+ throw new Error(`Denied: secret access for undeclared slot ${slotName} (deny-by-default)`);
78
+ }
79
+ }
80
+ /** Resolve a secret ref only if the input slot declares secret sensitivity/source. */
81
+ export async function resolveDeclaredSecret(pkg, slotName, host) {
82
+ assertSecretAccessAllowed(pkg, slotName);
83
+ if (!host.resolveSecret) {
84
+ throw new Error("Denied: no resolveSecret callback (fail closed for secrets)");
85
+ }
86
+ return host.resolveSecret(slotName);
87
+ }
5
88
  function substitute(template, inputs) {
6
89
  return template.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, (_, name) => {
7
90
  const v = inputs[name];
@@ -119,16 +202,28 @@ export async function runSkillPackage(pkg, host = {}, options = {}) {
119
202
  error: `Missing required inputs: ${missing.map((m) => m.name).join(", ")}`,
120
203
  };
121
204
  }
122
- const consentNeeded = pkg.manifest.permissions
205
+ const consentNeeded = new Set(pkg.manifest.permissions
123
206
  .filter((p) => p.requires_consent)
124
- .map((p) => p.side_effect_class);
125
- if (mode === "execute" && consentNeeded.length && host.consent) {
126
- const allowed = await host.consent({
207
+ .map((p) => p.side_effect_class));
208
+ for (const side of pkg.manifest.policy.consent_for ?? []) {
209
+ if (pkg.manifest.capabilities.some((c) => c.side_effect_class === side) ||
210
+ pkg.manifest.permissions.some((p) => p.side_effect_class === side)) {
211
+ consentNeeded.add(side);
212
+ }
213
+ }
214
+ if (mode === "execute" && consentNeeded.size) {
215
+ if (!host.consent) {
216
+ return failRun(runId, pkg, mode, resolved, secret_refs, stepRecords, verifications, started, host, "Explicit permission consent required; runtime host has no authenticated consent callback (fail closed)");
217
+ }
218
+ const consent = await host.consent({
127
219
  title: pkg.manifest.title,
128
- permissions: consentNeeded,
220
+ permissions: [...consentNeeded],
129
221
  steps: pkg.workflow.steps.map((s) => `${s.id}:${s.kind}`),
130
222
  });
131
- if (!allowed) {
223
+ if (!consent.actor || !consent.at) {
224
+ return failRun(runId, pkg, mode, resolved, secret_refs, stepRecords, verifications, started, host, "Permission consent callback returned invalid actor/timestamp evidence");
225
+ }
226
+ if (!consent.allowed) {
132
227
  return {
133
228
  kind: "skill_run",
134
229
  id: runId,
@@ -382,6 +477,12 @@ async function executeStep(step, ctx) {
382
477
  const cap = ctx.pkg.manifest.capabilities.find((c) => c.name === step.capability);
383
478
  if (!cap)
384
479
  throw new Error(`Unknown capability ${step.capability}`);
480
+ const args = { ...(step.arguments ?? {}) };
481
+ for (const [k, bind] of Object.entries(step.argument_bindings ?? {})) {
482
+ args[k] = ctx.inputs[bind] ?? ctx.stepOutputs[bind];
483
+ }
484
+ // Deny-by-default capability gate (even in dry_run for visibility of refusals).
485
+ assertCapabilityAllowed(ctx.pkg, cap, args);
385
486
  if (ctx.dry) {
386
487
  return {
387
488
  output: { dry_run: true, capability: cap.name, arguments: step.arguments },
@@ -399,10 +500,6 @@ async function executeStep(step, ctx) {
399
500
  }
400
501
  throw new Error(`No adapter for capability ${cap.name}`);
401
502
  }
402
- const args = { ...(step.arguments ?? {}) };
403
- for (const [k, bind] of Object.entries(step.argument_bindings ?? {})) {
404
- args[k] = ctx.inputs[bind] ?? ctx.stepOutputs[bind];
405
- }
406
503
  const inv = await adapter.invoke(cap, args);
407
504
  if (!inv.ok)
408
505
  throw new Error(inv.error ?? "tool failed");
@@ -464,11 +561,18 @@ async function executeStep(step, ctx) {
464
561
  resultAs: step.result_as,
465
562
  };
466
563
  }
467
- const existing = (step.result_as ? ctx.inputs[step.result_as] : undefined) ?? ctx.inputs[step.id];
468
- if (existing !== undefined) {
469
- return { output: existing, resultAs: step.result_as };
564
+ if (!ctx.host.decide) {
565
+ throw new Error(`human_decision ${step.id} unsupported: runtime requires an authenticated decide callback; input values cannot spoof human approval`);
566
+ }
567
+ const evidence = await ctx.host.decide({
568
+ id: step.id,
569
+ prompt: step.prompt,
570
+ choices: step.choices,
571
+ });
572
+ if (!evidence.actor || !evidence.at || !evidence.decision) {
573
+ throw new Error(`human_decision ${step.id} returned invalid approval evidence`);
470
574
  }
471
- throw new Error(`human_decision ${step.id} requires input`);
575
+ return { output: evidence, resultAs: step.result_as };
472
576
  }
473
577
  case "verify": {
474
578
  const results = [];
@@ -498,6 +602,34 @@ async function executeStep(step, ctx) {
498
602
  const val = ctx.stepOutputs[key] ?? ctx.inputs[key];
499
603
  results.push({ assertion, passed: val !== undefined });
500
604
  }
605
+ else if (assertion.startsWith("precondition:") ||
606
+ assertion.startsWith("contract_assertion:")) {
607
+ const id = assertion.slice(assertion.indexOf(":") + 1);
608
+ const definition = ctx.pkg.workflow.preconditions?.status === "specified"
609
+ ? ctx.pkg.workflow.preconditions.items.find((item) => item.id === id)
610
+ : ctx.pkg.workflow.verification?.status === "specified"
611
+ ? ctx.pkg.workflow.verification.items.find((item) => item.id === id)
612
+ : undefined;
613
+ if (!definition) {
614
+ results.push({ assertion, passed: false, detail: "Contract assertion missing" });
615
+ }
616
+ else if (!ctx.host.verifyAssertion) {
617
+ results.push({
618
+ assertion,
619
+ passed: false,
620
+ detail: "Unsupported domain assertion: runtime host must provide verifyAssertion; presence is not proof",
621
+ });
622
+ }
623
+ else {
624
+ const checked = await ctx.host.verifyAssertion({
625
+ id: definition.id,
626
+ assertion: "assertion" in definition ? definition.assertion : "",
627
+ check: definition.check,
628
+ evidence: "evidence" in definition ? definition.evidence : undefined,
629
+ });
630
+ results.push({ assertion, ...checked });
631
+ }
632
+ }
501
633
  else {
502
634
  results.push({
503
635
  assertion,
@@ -529,8 +661,9 @@ async function executeStep(step, ctx) {
529
661
  }
530
662
  function evalWhen(expr, inputs, stepOutputs) {
531
663
  const m = expr.match(/^input:([a-zA-Z0-9_]+)(?:==(.*))?$/);
532
- if (!m)
533
- return false;
664
+ if (!m) {
665
+ throw new Error(`Unsupported branch expression ${JSON.stringify(expr)}; supported form is input:name or input:name==value`);
666
+ }
534
667
  const val = inputs[m[1]] ?? stepOutputs[m[1]];
535
668
  if (m[2] === undefined)
536
669
  return Boolean(val);
@@ -563,6 +696,42 @@ export async function runSkillArchive(archive, host = {}, options = {}) {
563
696
  }
564
697
  const unpacked = unpackSkill(archive);
565
698
  const manifest = unpacked.manifest;
699
+ const mode = options.mode ?? "execute";
700
+ const trustView = inspectTrustView(archive);
701
+ // Execute refuses unsigned / development / self_reported unless explicit unsafe flag.
702
+ if (mode === "execute" || mode === "resume") {
703
+ const trusted = trustView.trust_state === "verified_issuer";
704
+ if (!trusted && !options.allow_untrusted) {
705
+ const started = new Date().toISOString();
706
+ return {
707
+ kind: "skill_run",
708
+ id: `run_${randomUUID().replace(/-/g, "").slice(0, 16)}`,
709
+ skill_id: manifest.id,
710
+ skill_version: manifest.version,
711
+ package_digest: manifest.package_digest,
712
+ status: "failed",
713
+ mode,
714
+ resolved_inputs: {},
715
+ steps: [],
716
+ verifications: [
717
+ {
718
+ assertion: "trust_gate",
719
+ passed: false,
720
+ detail: `${trustView.label}. Pass allow_untrusted / --allow-untrusted to execute anyway.`,
721
+ },
722
+ ],
723
+ runtime: {
724
+ name: RUNTIME_NAME,
725
+ version: RUNTIME_VERSION,
726
+ host: host.host,
727
+ model: host.model,
728
+ },
729
+ started_at: started,
730
+ finished_at: new Date().toISOString(),
731
+ error: `Refusing execute: ${trustView.label}`,
732
+ };
733
+ }
734
+ }
566
735
  const trustProfile = options.trust_profile ??
567
736
  (manifest.policy.require_anchor
568
737
  ? "anchored"
@@ -570,7 +739,13 @@ export async function runSkillArchive(archive, host = {}, options = {}) {
570
739
  ? "minted"
571
740
  : manifest.policy.trust_profile ?? "open");
572
741
  if (trustProfile !== "open") {
573
- const trust = verifyMintTrust(archive, trustProfile, options.issuer_secret);
742
+ const nonExecute = mode === "dry_run" || mode === "inspect" || mode === "explain";
743
+ const trust = verifyMintTrust(archive, trustProfile, {
744
+ issuer_secret: options.issuer_secret,
745
+ allow_development_issuer: options.allow_development_issuer ??
746
+ (options.allow_untrusted === true || nonExecute),
747
+ allow_self_reported: options.allow_untrusted === true || nonExecute,
748
+ });
574
749
  if (!trust.ok) {
575
750
  const started = new Date().toISOString();
576
751
  return {
@@ -580,7 +755,7 @@ export async function runSkillArchive(archive, host = {}, options = {}) {
580
755
  skill_version: manifest.version,
581
756
  package_digest: manifest.package_digest,
582
757
  status: "failed",
583
- mode: options.mode ?? "execute",
758
+ mode,
584
759
  resolved_inputs: {},
585
760
  steps: [],
586
761
  verifications: trust.issues.map((issue) => ({
@@ -600,6 +775,37 @@ export async function runSkillArchive(archive, host = {}, options = {}) {
600
775
  };
601
776
  }
602
777
  }
778
+ else if ((mode === "execute" || mode === "resume") && !options.allow_untrusted) {
779
+ // Open profile packages are explicitly untrusted for execute.
780
+ const started = new Date().toISOString();
781
+ return {
782
+ kind: "skill_run",
783
+ id: `run_${randomUUID().replace(/-/g, "").slice(0, 16)}`,
784
+ skill_id: manifest.id,
785
+ skill_version: manifest.version,
786
+ package_digest: manifest.package_digest,
787
+ status: "failed",
788
+ mode,
789
+ resolved_inputs: {},
790
+ steps: [],
791
+ verifications: [
792
+ {
793
+ assertion: "open_untrusted",
794
+ passed: false,
795
+ detail: "Open/unsigned packages require --allow-untrusted for execute",
796
+ },
797
+ ],
798
+ runtime: {
799
+ name: RUNTIME_NAME,
800
+ version: RUNTIME_VERSION,
801
+ host: host.host,
802
+ model: host.model,
803
+ },
804
+ started_at: started,
805
+ finished_at: new Date().toISOString(),
806
+ error: "Refusing execute of open/untrusted package without --allow-untrusted",
807
+ };
808
+ }
603
809
  return runSkillPackage(unpacked.raw, host, options);
604
810
  }
605
- export { inspectSkill, validatePackageBytes, unpackSkill, explainPackage as explain };
811
+ export { inspectSkill, inspectTrustView, validatePackageBytes, unpackSkill, explainPackage as explain, };
package/package.json CHANGED
@@ -1,12 +1,21 @@
1
1
  {
2
2
  "name": "@dot-skill/runtime",
3
- "version": "0.4.2",
4
- "description": "Open .skill Protocol reference workflow runtime",
3
+ "version": "0.5.0",
4
+ "description": "Inspect, dry-run, and execute 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
+ "runtime",
17
+ "dry-run"
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/runtime"
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,8 +48,8 @@
33
48
  "clean": "rm -rf dist"
34
49
  },
35
50
  "dependencies": {
36
- "@dot-skill/core": "^0.4.2",
37
- "@dot-skill/protocol": "^0.4.2"
51
+ "@dot-skill/core": "^0.5.0",
52
+ "@dot-skill/protocol": "^0.5.0"
38
53
  },
39
54
  "devDependencies": {
40
55
  "@types/node": "^26.1.1",