@skillerr/runtime 0.6.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/LICENSE +21 -0
- package/README.md +36 -0
- package/dist/index.d.ts +86 -0
- package/dist/index.js +811 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Bharat Dudeja
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# `@skillerr/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 @skillerr/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 `@skillerr/core` (dev HMAC ≠ production PKI)
|
|
26
|
+
|
|
27
|
+
## Related
|
|
28
|
+
|
|
29
|
+
- [`@skillerr/core`](https://www.npmjs.com/package/@skillerr/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
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import type { CapabilityAdapterHint, CapabilityRequirement, InputSlot, RuntimeMode, SkillPackageFiles, SkillRun, TrustProfile } from "@skillerr/protocol";
|
|
2
|
+
import { inspectSkill, inspectTrustView, unpackSkill, validatePackageBytes } from "@skillerr/core";
|
|
3
|
+
export interface CapabilityAdapter {
|
|
4
|
+
name: string;
|
|
5
|
+
supports: (cap: CapabilityRequirement) => boolean;
|
|
6
|
+
invoke: (cap: CapabilityRequirement, args: Record<string, unknown>) => Promise<{
|
|
7
|
+
ok: boolean;
|
|
8
|
+
result?: unknown;
|
|
9
|
+
error?: string;
|
|
10
|
+
adapter: CapabilityAdapterHint;
|
|
11
|
+
}>;
|
|
12
|
+
}
|
|
13
|
+
export interface RuntimeHost {
|
|
14
|
+
askInputs?: (slots: InputSlot[]) => Promise<Record<string, unknown>>;
|
|
15
|
+
consent?: (plan: {
|
|
16
|
+
title: string;
|
|
17
|
+
permissions: string[];
|
|
18
|
+
steps: string[];
|
|
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
|
+
}>;
|
|
42
|
+
resolveSecret?: (ref: string) => Promise<string>;
|
|
43
|
+
env?: Record<string, string>;
|
|
44
|
+
adapters?: CapabilityAdapter[];
|
|
45
|
+
model?: string;
|
|
46
|
+
host?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface RunOptions {
|
|
49
|
+
mode?: RuntimeMode;
|
|
50
|
+
inputs?: Record<string, unknown>;
|
|
51
|
+
resume_from?: string;
|
|
52
|
+
checkpoint_state?: Record<string, unknown>;
|
|
53
|
+
/** Defaults to manifest policy. Use open only for untrusted drafts. */
|
|
54
|
+
trust_profile?: TrustProfile;
|
|
55
|
+
/** Reference verifier only; production runtimes should use a real trust store. */
|
|
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;
|
|
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>;
|
|
72
|
+
export declare function explainPackage(pkg: SkillPackageFiles): {
|
|
73
|
+
title: string;
|
|
74
|
+
description: string;
|
|
75
|
+
inputs: InputSlot[];
|
|
76
|
+
permissions: string[];
|
|
77
|
+
steps: Array<{
|
|
78
|
+
id: string;
|
|
79
|
+
kind: string;
|
|
80
|
+
title?: string;
|
|
81
|
+
}>;
|
|
82
|
+
constraints: string[];
|
|
83
|
+
};
|
|
84
|
+
export declare function runSkillPackage(pkg: SkillPackageFiles, host?: RuntimeHost, options?: RunOptions): Promise<SkillRun>;
|
|
85
|
+
export declare function runSkillArchive(archive: Uint8Array, host?: RuntimeHost, options?: RunOptions): Promise<SkillRun>;
|
|
86
|
+
export { inspectSkill, inspectTrustView, validatePackageBytes, unpackSkill, explainPackage as explain, };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { inspectSkill, inspectTrustView, unpackSkill, validatePackageBytes, verifyMintTrust, sha256Digest, } from "@skillerr/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 @skillerr/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
|
+
}
|
|
88
|
+
function substitute(template, inputs) {
|
|
89
|
+
return template.replace(/\{\{\s*([a-zA-Z_][a-zA-Z0-9_]*)\s*\}\}/g, (_, name) => {
|
|
90
|
+
const v = inputs[name];
|
|
91
|
+
return v === undefined || v === null ? `{{${name}}}` : String(v);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
function missingInputs(slots, provided, env) {
|
|
95
|
+
return slots.filter((slot) => {
|
|
96
|
+
if (!slot.required && slot.ask_when !== "always")
|
|
97
|
+
return false;
|
|
98
|
+
if (slot.ask_when === "never")
|
|
99
|
+
return false;
|
|
100
|
+
if (provided[slot.name] !== undefined)
|
|
101
|
+
return false;
|
|
102
|
+
if (slot.default !== undefined)
|
|
103
|
+
return false;
|
|
104
|
+
if (slot.source === "environment" && env[slot.name] !== undefined)
|
|
105
|
+
return false;
|
|
106
|
+
if (slot.ask_when === "if_missing" || slot.ask_when === "always" || slot.required) {
|
|
107
|
+
return true;
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
function resolveInputsSync(slots, provided, env) {
|
|
113
|
+
const resolved = { ...provided };
|
|
114
|
+
const secret_refs = {};
|
|
115
|
+
for (const slot of slots) {
|
|
116
|
+
if (resolved[slot.name] !== undefined) {
|
|
117
|
+
if (slot.sensitivity === "secret") {
|
|
118
|
+
secret_refs[slot.name] = String(resolved[slot.name]);
|
|
119
|
+
resolved[slot.name] = `secret:${slot.name}`;
|
|
120
|
+
}
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
if (slot.source === "environment" && env[slot.name] !== undefined) {
|
|
124
|
+
resolved[slot.name] = env[slot.name];
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (slot.default !== undefined) {
|
|
128
|
+
resolved[slot.name] = slot.default;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return { resolved, secret_refs, missing: missingInputs(slots, resolved, env) };
|
|
132
|
+
}
|
|
133
|
+
export function explainPackage(pkg) {
|
|
134
|
+
return {
|
|
135
|
+
title: pkg.manifest.title,
|
|
136
|
+
description: pkg.manifest.description,
|
|
137
|
+
inputs: pkg.manifest.inputs,
|
|
138
|
+
permissions: pkg.manifest.permissions.map((p) => `${p.side_effect_class}: ${p.description}`),
|
|
139
|
+
steps: pkg.workflow.steps.map((s) => ({ id: s.id, kind: s.kind, title: s.title })),
|
|
140
|
+
constraints: (pkg.workflow.constraints ?? []).map((c) => `${c.effect}: ${c.statement}`),
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
export async function runSkillPackage(pkg, host = {}, options = {}) {
|
|
144
|
+
const mode = options.mode ?? "execute";
|
|
145
|
+
const started = new Date().toISOString();
|
|
146
|
+
const runId = `run_${randomUUID().replace(/-/g, "").slice(0, 16)}`;
|
|
147
|
+
const stepRecords = [];
|
|
148
|
+
const verifications = [];
|
|
149
|
+
const outputs = {};
|
|
150
|
+
const stepOutputs = { ...(options.checkpoint_state ?? {}) };
|
|
151
|
+
const env = host.env ?? Object.fromEntries(Object.entries(process.env).filter((e) => e[1] !== undefined));
|
|
152
|
+
let { resolved, secret_refs, missing } = resolveInputsSync(pkg.manifest.inputs, options.inputs ?? {}, env);
|
|
153
|
+
if (missing.length && host.askInputs && mode !== "inspect" && mode !== "explain") {
|
|
154
|
+
const answered = await host.askInputs(missing);
|
|
155
|
+
({ resolved, secret_refs, missing } = resolveInputsSync(pkg.manifest.inputs, { ...resolved, ...answered }, env));
|
|
156
|
+
}
|
|
157
|
+
if (mode === "inspect" || mode === "explain") {
|
|
158
|
+
return {
|
|
159
|
+
kind: "skill_run",
|
|
160
|
+
id: runId,
|
|
161
|
+
skill_id: pkg.manifest.id,
|
|
162
|
+
skill_version: pkg.manifest.version,
|
|
163
|
+
package_digest: pkg.manifest.package_digest,
|
|
164
|
+
status: missing.length ? "paused" : "succeeded",
|
|
165
|
+
mode,
|
|
166
|
+
resolved_inputs: resolved,
|
|
167
|
+
secret_refs,
|
|
168
|
+
steps: [],
|
|
169
|
+
outputs: { explanation: explainPackage(pkg), missing_inputs: missing.map((m) => m.name) },
|
|
170
|
+
verifications: [],
|
|
171
|
+
runtime: {
|
|
172
|
+
name: RUNTIME_NAME,
|
|
173
|
+
version: RUNTIME_VERSION,
|
|
174
|
+
host: host.host,
|
|
175
|
+
model: host.model,
|
|
176
|
+
},
|
|
177
|
+
started_at: started,
|
|
178
|
+
finished_at: new Date().toISOString(),
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
if (missing.length) {
|
|
182
|
+
return {
|
|
183
|
+
kind: "skill_run",
|
|
184
|
+
id: runId,
|
|
185
|
+
skill_id: pkg.manifest.id,
|
|
186
|
+
skill_version: pkg.manifest.version,
|
|
187
|
+
package_digest: pkg.manifest.package_digest,
|
|
188
|
+
status: "paused",
|
|
189
|
+
mode,
|
|
190
|
+
resolved_inputs: resolved,
|
|
191
|
+
secret_refs,
|
|
192
|
+
steps: [],
|
|
193
|
+
verifications: [],
|
|
194
|
+
runtime: {
|
|
195
|
+
name: RUNTIME_NAME,
|
|
196
|
+
version: RUNTIME_VERSION,
|
|
197
|
+
host: host.host,
|
|
198
|
+
model: host.model,
|
|
199
|
+
},
|
|
200
|
+
started_at: started,
|
|
201
|
+
finished_at: new Date().toISOString(),
|
|
202
|
+
error: `Missing required inputs: ${missing.map((m) => m.name).join(", ")}`,
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
const consentNeeded = new Set(pkg.manifest.permissions
|
|
206
|
+
.filter((p) => p.requires_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({
|
|
219
|
+
title: pkg.manifest.title,
|
|
220
|
+
permissions: [...consentNeeded],
|
|
221
|
+
steps: pkg.workflow.steps.map((s) => `${s.id}:${s.kind}`),
|
|
222
|
+
});
|
|
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) {
|
|
227
|
+
return {
|
|
228
|
+
kind: "skill_run",
|
|
229
|
+
id: runId,
|
|
230
|
+
skill_id: pkg.manifest.id,
|
|
231
|
+
skill_version: pkg.manifest.version,
|
|
232
|
+
package_digest: pkg.manifest.package_digest,
|
|
233
|
+
status: "cancelled",
|
|
234
|
+
mode,
|
|
235
|
+
resolved_inputs: resolved,
|
|
236
|
+
secret_refs,
|
|
237
|
+
steps: [],
|
|
238
|
+
verifications: [],
|
|
239
|
+
runtime: {
|
|
240
|
+
name: RUNTIME_NAME,
|
|
241
|
+
version: RUNTIME_VERSION,
|
|
242
|
+
host: host.host,
|
|
243
|
+
model: host.model,
|
|
244
|
+
},
|
|
245
|
+
started_at: started,
|
|
246
|
+
finished_at: new Date().toISOString(),
|
|
247
|
+
error: "User denied permissions",
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
const adapters = host.adapters ?? [];
|
|
252
|
+
for (const cap of pkg.manifest.capabilities) {
|
|
253
|
+
if (!cap.required)
|
|
254
|
+
continue;
|
|
255
|
+
const ok = adapters.some((a) => a.supports(cap));
|
|
256
|
+
if (!ok && cap.fallback === "fail") {
|
|
257
|
+
return {
|
|
258
|
+
kind: "skill_run",
|
|
259
|
+
id: runId,
|
|
260
|
+
skill_id: pkg.manifest.id,
|
|
261
|
+
skill_version: pkg.manifest.version,
|
|
262
|
+
package_digest: pkg.manifest.package_digest,
|
|
263
|
+
status: "failed",
|
|
264
|
+
mode,
|
|
265
|
+
resolved_inputs: resolved,
|
|
266
|
+
secret_refs,
|
|
267
|
+
steps: [],
|
|
268
|
+
verifications: [],
|
|
269
|
+
runtime: {
|
|
270
|
+
name: RUNTIME_NAME,
|
|
271
|
+
version: RUNTIME_VERSION,
|
|
272
|
+
host: host.host,
|
|
273
|
+
model: host.model,
|
|
274
|
+
},
|
|
275
|
+
started_at: started,
|
|
276
|
+
finished_at: new Date().toISOString(),
|
|
277
|
+
error: `Required capability unavailable: ${cap.name}`,
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const byId = new Map(pkg.workflow.steps.map((s) => [s.id, s]));
|
|
282
|
+
let current = options.resume_from ?? pkg.workflow.entrypoint ?? pkg.manifest.entrypoint;
|
|
283
|
+
let toolCalls = 0;
|
|
284
|
+
const dry = mode === "dry_run";
|
|
285
|
+
while (current) {
|
|
286
|
+
const step = byId.get(current);
|
|
287
|
+
if (!step) {
|
|
288
|
+
return failRun(runId, pkg, mode, resolved, secret_refs, stepRecords, verifications, started, host, `Unknown step ${current}`);
|
|
289
|
+
}
|
|
290
|
+
const record = {
|
|
291
|
+
step_id: step.id,
|
|
292
|
+
kind: step.kind,
|
|
293
|
+
status: "pending",
|
|
294
|
+
started_at: new Date().toISOString(),
|
|
295
|
+
};
|
|
296
|
+
try {
|
|
297
|
+
if (step.kind === "checkpoint" && step.require_human && mode === "execute") {
|
|
298
|
+
record.status = "waiting";
|
|
299
|
+
record.finished_at = new Date().toISOString();
|
|
300
|
+
stepRecords.push(record);
|
|
301
|
+
return {
|
|
302
|
+
kind: "skill_run",
|
|
303
|
+
id: runId,
|
|
304
|
+
skill_id: pkg.manifest.id,
|
|
305
|
+
skill_version: pkg.manifest.version,
|
|
306
|
+
package_digest: pkg.manifest.package_digest,
|
|
307
|
+
status: "paused",
|
|
308
|
+
mode,
|
|
309
|
+
resolved_inputs: resolved,
|
|
310
|
+
secret_refs,
|
|
311
|
+
steps: stepRecords,
|
|
312
|
+
outputs,
|
|
313
|
+
verifications,
|
|
314
|
+
runtime: {
|
|
315
|
+
name: RUNTIME_NAME,
|
|
316
|
+
version: RUNTIME_VERSION,
|
|
317
|
+
host: host.host,
|
|
318
|
+
model: host.model,
|
|
319
|
+
},
|
|
320
|
+
checkpoints: [
|
|
321
|
+
{
|
|
322
|
+
id: `cp_${step.id}`,
|
|
323
|
+
step_id: step.id,
|
|
324
|
+
at: new Date().toISOString(),
|
|
325
|
+
state_digest: sha256Digest(JSON.stringify(stepOutputs)),
|
|
326
|
+
},
|
|
327
|
+
],
|
|
328
|
+
started_at: started,
|
|
329
|
+
finished_at: new Date().toISOString(),
|
|
330
|
+
};
|
|
331
|
+
}
|
|
332
|
+
const result = await executeStep(step, {
|
|
333
|
+
inputs: resolved,
|
|
334
|
+
secret_refs,
|
|
335
|
+
stepOutputs,
|
|
336
|
+
pkg,
|
|
337
|
+
adapters,
|
|
338
|
+
dry,
|
|
339
|
+
host,
|
|
340
|
+
});
|
|
341
|
+
if (result.toolCall)
|
|
342
|
+
toolCalls += 1;
|
|
343
|
+
if (toolCalls > pkg.manifest.policy.max_tool_calls) {
|
|
344
|
+
throw new Error("max_tool_calls exceeded");
|
|
345
|
+
}
|
|
346
|
+
record.status = result.skipped ? "skipped" : "succeeded";
|
|
347
|
+
record.adapter = result.adapter;
|
|
348
|
+
record.output_digest = result.output !== undefined
|
|
349
|
+
? sha256Digest(JSON.stringify(result.output))
|
|
350
|
+
: undefined;
|
|
351
|
+
if (result.output !== undefined) {
|
|
352
|
+
stepOutputs[step.id] = result.output;
|
|
353
|
+
if (result.resultAs)
|
|
354
|
+
stepOutputs[result.resultAs] = result.output;
|
|
355
|
+
}
|
|
356
|
+
if (result.emit) {
|
|
357
|
+
outputs[result.emit.name] = result.emit.value;
|
|
358
|
+
}
|
|
359
|
+
if (result.verifications)
|
|
360
|
+
verifications.push(...result.verifications);
|
|
361
|
+
record.finished_at = new Date().toISOString();
|
|
362
|
+
stepRecords.push(record);
|
|
363
|
+
current = result.next ?? nextStepId(step);
|
|
364
|
+
}
|
|
365
|
+
catch (e) {
|
|
366
|
+
record.status = "failed";
|
|
367
|
+
record.error = e instanceof Error ? e.message : String(e);
|
|
368
|
+
record.finished_at = new Date().toISOString();
|
|
369
|
+
stepRecords.push(record);
|
|
370
|
+
if (step.on_fail) {
|
|
371
|
+
current = step.on_fail;
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (step.optional) {
|
|
375
|
+
current = nextStepId(step);
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
return failRun(runId, pkg, mode, resolved, secret_refs, stepRecords, verifications, started, host, record.error);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
for (const out of pkg.manifest.outputs) {
|
|
382
|
+
if (out.required && outputs[out.name] === undefined) {
|
|
383
|
+
const last = stepRecords.filter((s) => s.status === "succeeded").at(-1);
|
|
384
|
+
if (last && stepOutputs[last.step_id] !== undefined) {
|
|
385
|
+
outputs[out.name] = stepOutputs[last.step_id];
|
|
386
|
+
}
|
|
387
|
+
else {
|
|
388
|
+
verifications.push({
|
|
389
|
+
assertion: `output:${out.name}`,
|
|
390
|
+
passed: false,
|
|
391
|
+
detail: "Required output missing",
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const failedVerify = verifications.some((v) => !v.passed);
|
|
397
|
+
return {
|
|
398
|
+
kind: "skill_run",
|
|
399
|
+
id: runId,
|
|
400
|
+
skill_id: pkg.manifest.id,
|
|
401
|
+
skill_version: pkg.manifest.version,
|
|
402
|
+
package_digest: pkg.manifest.package_digest,
|
|
403
|
+
status: failedVerify ? "failed" : "succeeded",
|
|
404
|
+
mode,
|
|
405
|
+
resolved_inputs: resolved,
|
|
406
|
+
secret_refs,
|
|
407
|
+
steps: stepRecords,
|
|
408
|
+
outputs,
|
|
409
|
+
verifications,
|
|
410
|
+
runtime: {
|
|
411
|
+
name: RUNTIME_NAME,
|
|
412
|
+
version: RUNTIME_VERSION,
|
|
413
|
+
host: host.host,
|
|
414
|
+
model: host.model,
|
|
415
|
+
},
|
|
416
|
+
started_at: started,
|
|
417
|
+
finished_at: new Date().toISOString(),
|
|
418
|
+
error: failedVerify ? "Verification failed" : undefined,
|
|
419
|
+
};
|
|
420
|
+
}
|
|
421
|
+
function nextStepId(step) {
|
|
422
|
+
if (!step.next)
|
|
423
|
+
return undefined;
|
|
424
|
+
return Array.isArray(step.next) ? step.next[0] : step.next;
|
|
425
|
+
}
|
|
426
|
+
function failRun(runId, pkg, mode, resolved, secret_refs, steps, verifications, started, host, error) {
|
|
427
|
+
return {
|
|
428
|
+
kind: "skill_run",
|
|
429
|
+
id: runId,
|
|
430
|
+
skill_id: pkg.manifest.id,
|
|
431
|
+
skill_version: pkg.manifest.version,
|
|
432
|
+
package_digest: pkg.manifest.package_digest,
|
|
433
|
+
status: "failed",
|
|
434
|
+
mode,
|
|
435
|
+
resolved_inputs: resolved,
|
|
436
|
+
secret_refs,
|
|
437
|
+
steps,
|
|
438
|
+
verifications,
|
|
439
|
+
runtime: {
|
|
440
|
+
name: RUNTIME_NAME,
|
|
441
|
+
version: RUNTIME_VERSION,
|
|
442
|
+
host: host.host,
|
|
443
|
+
model: host.model,
|
|
444
|
+
},
|
|
445
|
+
started_at: started,
|
|
446
|
+
finished_at: new Date().toISOString(),
|
|
447
|
+
error,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
async function executeStep(step, ctx) {
|
|
451
|
+
switch (step.kind) {
|
|
452
|
+
case "instruct": {
|
|
453
|
+
const text = substitute(step.text, ctx.inputs);
|
|
454
|
+
const constraints = (ctx.pkg.workflow.constraints ?? [])
|
|
455
|
+
.map((c) => `- [${c.effect}] ${c.statement}`)
|
|
456
|
+
.join("\n");
|
|
457
|
+
return {
|
|
458
|
+
output: {
|
|
459
|
+
instruction: text,
|
|
460
|
+
knowledge_refs: step.knowledge_refs,
|
|
461
|
+
constraints,
|
|
462
|
+
dry_run: ctx.dry,
|
|
463
|
+
},
|
|
464
|
+
};
|
|
465
|
+
}
|
|
466
|
+
case "prompt": {
|
|
467
|
+
const rendered = substitute(step.template, {
|
|
468
|
+
...ctx.inputs,
|
|
469
|
+
...Object.fromEntries(Object.entries(step.input_bindings ?? {}).map(([k, v]) => [
|
|
470
|
+
k,
|
|
471
|
+
ctx.inputs[v] ?? ctx.stepOutputs[v],
|
|
472
|
+
])),
|
|
473
|
+
});
|
|
474
|
+
return { output: { prompt: rendered } };
|
|
475
|
+
}
|
|
476
|
+
case "tool": {
|
|
477
|
+
const cap = ctx.pkg.manifest.capabilities.find((c) => c.name === step.capability);
|
|
478
|
+
if (!cap)
|
|
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);
|
|
486
|
+
if (ctx.dry) {
|
|
487
|
+
return {
|
|
488
|
+
output: { dry_run: true, capability: cap.name, arguments: step.arguments },
|
|
489
|
+
resultAs: step.result_as,
|
|
490
|
+
toolCall: true,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
const adapter = ctx.adapters.find((a) => a.supports(cap));
|
|
494
|
+
if (!adapter) {
|
|
495
|
+
if (cap.fallback === "skip_if_optional" || step.optional) {
|
|
496
|
+
return { skipped: true };
|
|
497
|
+
}
|
|
498
|
+
if (cap.fallback === "ask_human") {
|
|
499
|
+
throw new Error(`Capability ${cap.name} requires human/tool adapter`);
|
|
500
|
+
}
|
|
501
|
+
throw new Error(`No adapter for capability ${cap.name}`);
|
|
502
|
+
}
|
|
503
|
+
const inv = await adapter.invoke(cap, args);
|
|
504
|
+
if (!inv.ok)
|
|
505
|
+
throw new Error(inv.error ?? "tool failed");
|
|
506
|
+
return {
|
|
507
|
+
output: inv.result,
|
|
508
|
+
resultAs: step.result_as,
|
|
509
|
+
toolCall: true,
|
|
510
|
+
adapter: inv.adapter,
|
|
511
|
+
};
|
|
512
|
+
}
|
|
513
|
+
case "transform": {
|
|
514
|
+
const input = step.input_from
|
|
515
|
+
? ctx.stepOutputs[step.input_from] ?? ctx.inputs[step.input_from]
|
|
516
|
+
: ctx.stepOutputs;
|
|
517
|
+
if (step.expression === "identity") {
|
|
518
|
+
return { output: input, resultAs: step.result_as };
|
|
519
|
+
}
|
|
520
|
+
if (step.expression.startsWith("jsonpath:")) {
|
|
521
|
+
const key = step.expression.slice("jsonpath:".length).replace(/^\$\.?/, "");
|
|
522
|
+
const val = input && typeof input === "object"
|
|
523
|
+
? input[key]
|
|
524
|
+
: undefined;
|
|
525
|
+
return { output: val, resultAs: step.result_as };
|
|
526
|
+
}
|
|
527
|
+
throw new Error(`Unsupported transform: ${step.expression}`);
|
|
528
|
+
}
|
|
529
|
+
case "branch": {
|
|
530
|
+
for (const c of step.cases) {
|
|
531
|
+
if (evalWhen(c.when, ctx.inputs, ctx.stepOutputs)) {
|
|
532
|
+
return { next: c.goto, output: { branched: c.goto } };
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
if (step.else)
|
|
536
|
+
return { next: step.else, output: { branched: step.else } };
|
|
537
|
+
return { output: { branched: null } };
|
|
538
|
+
}
|
|
539
|
+
case "iterate": {
|
|
540
|
+
const collection = ctx.inputs[step.over] ?? ctx.stepOutputs[step.over];
|
|
541
|
+
if (!Array.isArray(collection))
|
|
542
|
+
throw new Error(`iterate.over ${step.over} is not an array`);
|
|
543
|
+
const results = [];
|
|
544
|
+
for (const item of collection) {
|
|
545
|
+
results.push({ [step.as]: item, body: step.body });
|
|
546
|
+
}
|
|
547
|
+
return { output: results };
|
|
548
|
+
}
|
|
549
|
+
case "delegate": {
|
|
550
|
+
if (ctx.dry)
|
|
551
|
+
return { output: { dry_run: true, task: step.task }, resultAs: step.result_as };
|
|
552
|
+
throw new Error("delegate requires an A2A adapter (not configured)");
|
|
553
|
+
}
|
|
554
|
+
case "checkpoint": {
|
|
555
|
+
return { output: { checkpoint: true, message: step.message } };
|
|
556
|
+
}
|
|
557
|
+
case "human_decision": {
|
|
558
|
+
if (ctx.dry) {
|
|
559
|
+
return {
|
|
560
|
+
output: { awaiting_human: true, prompt: step.prompt, choices: step.choices },
|
|
561
|
+
resultAs: step.result_as,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
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`);
|
|
574
|
+
}
|
|
575
|
+
return { output: evidence, resultAs: step.result_as };
|
|
576
|
+
}
|
|
577
|
+
case "verify": {
|
|
578
|
+
const results = [];
|
|
579
|
+
for (const assertion of step.assertions) {
|
|
580
|
+
if (assertion === "all_required_inputs_resolved") {
|
|
581
|
+
const missing = ctx.pkg.manifest.inputs.filter((i) => i.required && ctx.inputs[i.name] === undefined);
|
|
582
|
+
results.push({
|
|
583
|
+
assertion,
|
|
584
|
+
passed: missing.length === 0,
|
|
585
|
+
detail: missing.length ? missing.map((m) => m.name).join(",") : undefined,
|
|
586
|
+
});
|
|
587
|
+
}
|
|
588
|
+
else if (assertion.startsWith("constraint_present:") ||
|
|
589
|
+
assertion.startsWith("honor:")) {
|
|
590
|
+
const cid = assertion.slice(assertion.indexOf(":") + 1);
|
|
591
|
+
const c = ctx.pkg.workflow.constraints?.find((x) => x.id === cid);
|
|
592
|
+
results.push({
|
|
593
|
+
assertion,
|
|
594
|
+
passed: Boolean(c),
|
|
595
|
+
detail: c
|
|
596
|
+
? `Constraint is present (behavioral compliance is host responsibility): ${c.statement}`
|
|
597
|
+
: "Constraint missing",
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
else if (assertion.startsWith("exists:")) {
|
|
601
|
+
const key = assertion.slice("exists:".length);
|
|
602
|
+
const val = ctx.stepOutputs[key] ?? ctx.inputs[key];
|
|
603
|
+
results.push({ assertion, passed: val !== undefined });
|
|
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
|
+
}
|
|
633
|
+
else {
|
|
634
|
+
results.push({
|
|
635
|
+
assertion,
|
|
636
|
+
passed: !ctx.pkg.manifest.policy.fail_on_unsupported_step,
|
|
637
|
+
detail: ctx.pkg.manifest.policy.fail_on_unsupported_step
|
|
638
|
+
? "Unsupported assertion"
|
|
639
|
+
: "Unrecognized assertion treated as advisory",
|
|
640
|
+
});
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
if (results.some((r) => !r.passed)) {
|
|
644
|
+
throw new Error(`Verification failed: ${results.filter((r) => !r.passed).map((r) => r.assertion).join(", ")}`);
|
|
645
|
+
}
|
|
646
|
+
return { output: { verified: true }, verifications: results };
|
|
647
|
+
}
|
|
648
|
+
case "emit": {
|
|
649
|
+
const value = ctx.stepOutputs[step.from] ?? ctx.inputs[step.from];
|
|
650
|
+
return { output: value, emit: { name: step.output, value } };
|
|
651
|
+
}
|
|
652
|
+
case "subskill": {
|
|
653
|
+
throw new Error(`subskill ${step.skill_id} not resolved in this runtime invocation`);
|
|
654
|
+
}
|
|
655
|
+
default: {
|
|
656
|
+
const _exhaustive = step;
|
|
657
|
+
void _exhaustive;
|
|
658
|
+
throw new Error("Unsupported step kind");
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
function evalWhen(expr, inputs, stepOutputs) {
|
|
663
|
+
const m = expr.match(/^input:([a-zA-Z0-9_]+)(?:==(.*))?$/);
|
|
664
|
+
if (!m) {
|
|
665
|
+
throw new Error(`Unsupported branch expression ${JSON.stringify(expr)}; supported form is input:name or input:name==value`);
|
|
666
|
+
}
|
|
667
|
+
const val = inputs[m[1]] ?? stepOutputs[m[1]];
|
|
668
|
+
if (m[2] === undefined)
|
|
669
|
+
return Boolean(val);
|
|
670
|
+
return String(val) === m[2];
|
|
671
|
+
}
|
|
672
|
+
export async function runSkillArchive(archive, host = {}, options = {}) {
|
|
673
|
+
const validation = validatePackageBytes(archive);
|
|
674
|
+
if (!validation.ok) {
|
|
675
|
+
const started = new Date().toISOString();
|
|
676
|
+
return {
|
|
677
|
+
kind: "skill_run",
|
|
678
|
+
id: `run_${randomUUID().replace(/-/g, "").slice(0, 16)}`,
|
|
679
|
+
skill_id: validation.manifest?.id ?? "invalid",
|
|
680
|
+
skill_version: validation.manifest?.version ?? "0.0.0",
|
|
681
|
+
package_digest: validation.manifest?.package_digest ?? "",
|
|
682
|
+
status: "failed",
|
|
683
|
+
mode: options.mode ?? "execute",
|
|
684
|
+
resolved_inputs: {},
|
|
685
|
+
steps: [],
|
|
686
|
+
verifications: validation.issues.map((i) => ({
|
|
687
|
+
assertion: i.code,
|
|
688
|
+
passed: false,
|
|
689
|
+
detail: i.message,
|
|
690
|
+
})),
|
|
691
|
+
runtime: { name: RUNTIME_NAME, version: RUNTIME_VERSION, host: host.host, model: host.model },
|
|
692
|
+
started_at: started,
|
|
693
|
+
finished_at: new Date().toISOString(),
|
|
694
|
+
error: "Package validation failed",
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
const unpacked = unpackSkill(archive);
|
|
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
|
+
}
|
|
735
|
+
const trustProfile = options.trust_profile ??
|
|
736
|
+
(manifest.policy.require_anchor
|
|
737
|
+
? "anchored"
|
|
738
|
+
: manifest.policy.require_minted || manifest.policy.require_signatures
|
|
739
|
+
? "minted"
|
|
740
|
+
: manifest.policy.trust_profile ?? "open");
|
|
741
|
+
if (trustProfile !== "open") {
|
|
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
|
+
});
|
|
749
|
+
if (!trust.ok) {
|
|
750
|
+
const started = new Date().toISOString();
|
|
751
|
+
return {
|
|
752
|
+
kind: "skill_run",
|
|
753
|
+
id: `run_${randomUUID().replace(/-/g, "").slice(0, 16)}`,
|
|
754
|
+
skill_id: manifest.id,
|
|
755
|
+
skill_version: manifest.version,
|
|
756
|
+
package_digest: manifest.package_digest,
|
|
757
|
+
status: "failed",
|
|
758
|
+
mode,
|
|
759
|
+
resolved_inputs: {},
|
|
760
|
+
steps: [],
|
|
761
|
+
verifications: trust.issues.map((issue) => ({
|
|
762
|
+
assertion: issue.code,
|
|
763
|
+
passed: issue.severity !== "error",
|
|
764
|
+
detail: issue.message,
|
|
765
|
+
})),
|
|
766
|
+
runtime: {
|
|
767
|
+
name: RUNTIME_NAME,
|
|
768
|
+
version: RUNTIME_VERSION,
|
|
769
|
+
host: host.host,
|
|
770
|
+
model: host.model,
|
|
771
|
+
},
|
|
772
|
+
started_at: started,
|
|
773
|
+
finished_at: new Date().toISOString(),
|
|
774
|
+
error: "Package trust verification failed",
|
|
775
|
+
};
|
|
776
|
+
}
|
|
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
|
+
}
|
|
809
|
+
return runSkillPackage(unpacked.raw, host, options);
|
|
810
|
+
}
|
|
811
|
+
export { inspectSkill, inspectTrustView, validatePackageBytes, unpackSkill, explainPackage as explain, };
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@skillerr/runtime",
|
|
3
|
+
"version": "0.6.0",
|
|
4
|
+
"description": "Inspect, dry-run, and execute portable .skill packages",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Bharat Dudeja",
|
|
8
|
+
"url": "https://github.com/bharatdudeja13-cmd"
|
|
9
|
+
},
|
|
10
|
+
"keywords": [
|
|
11
|
+
"skill",
|
|
12
|
+
".skill",
|
|
13
|
+
"ai-agents",
|
|
14
|
+
"agent-skills",
|
|
15
|
+
"runtime",
|
|
16
|
+
"dry-run"
|
|
17
|
+
],
|
|
18
|
+
"type": "module",
|
|
19
|
+
"main": "./dist/index.js",
|
|
20
|
+
"types": "./dist/index.d.ts",
|
|
21
|
+
"publishConfig": {
|
|
22
|
+
"access": "public",
|
|
23
|
+
"provenance": true
|
|
24
|
+
},
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "https://github.com/dot-skill/dot-skill.git",
|
|
28
|
+
"directory": "packages/runtime"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://skillerr.com",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/dot-skill/dot-skill/issues"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"dist",
|
|
36
|
+
"README.md",
|
|
37
|
+
"LICENSE"
|
|
38
|
+
],
|
|
39
|
+
"exports": {
|
|
40
|
+
".": {
|
|
41
|
+
"import": "./dist/index.js",
|
|
42
|
+
"types": "./dist/index.d.ts"
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "rm -rf dist && tsc",
|
|
47
|
+
"prepack": "npm run build",
|
|
48
|
+
"clean": "rm -rf dist"
|
|
49
|
+
},
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"@skillerr/core": "^0.6.0",
|
|
52
|
+
"@skillerr/protocol": "^0.6.0"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@types/node": "^26.1.1",
|
|
56
|
+
"typescript": "^5.4.5"
|
|
57
|
+
}
|
|
58
|
+
}
|