@byokit/openclaw 0.1.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/CHANGELOG.md +47 -0
- package/LICENSE +202 -0
- package/README.md +174 -0
- package/dist/approvals.d.ts +29 -0
- package/dist/approvals.js +200 -0
- package/dist/bridge.d.ts +78 -0
- package/dist/bridge.js +345 -0
- package/dist/classify.d.ts +4 -0
- package/dist/classify.js +14 -0
- package/dist/config.d.ts +13 -0
- package/dist/config.js +74 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.js +13 -0
- package/dist/device.d.ts +63 -0
- package/dist/device.js +192 -0
- package/dist/engine.d.ts +43 -0
- package/dist/engine.js +212 -0
- package/dist/generated/events.d.ts +61 -0
- package/dist/generated/events.js +1 -0
- package/dist/generated/methods.d.ts +2368 -0
- package/dist/generated/methods.js +1 -0
- package/dist/generated/params.d.ts +1717 -0
- package/dist/generated/params.js +5 -0
- package/dist/generated/report.json +1356 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/kit.d.ts +108 -0
- package/dist/kit.js +655 -0
- package/dist/link.d.ts +24 -0
- package/dist/link.js +273 -0
- package/dist/members.d.ts +11 -0
- package/dist/members.js +23 -0
- package/dist/migrate.d.ts +22 -0
- package/dist/migrate.js +118 -0
- package/dist/notices.d.ts +9 -0
- package/dist/notices.js +75 -0
- package/dist/routes.d.ts +9 -0
- package/dist/routes.js +15 -0
- package/dist/routes.json +398 -0
- package/dist/runs.d.ts +14 -0
- package/dist/runs.js +110 -0
- package/dist/signin.d.ts +24 -0
- package/dist/signin.js +251 -0
- package/dist/testing/contract.d.ts +12 -0
- package/dist/testing/contract.js +250 -0
- package/dist/testing/fake-gateway.d.ts +19 -0
- package/dist/testing/fake-gateway.js +378 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +4 -0
- package/dist/testing/model-stub.d.ts +29 -0
- package/dist/testing/model-stub.js +182 -0
- package/dist/transport.d.ts +22 -0
- package/dist/transport.js +94 -0
- package/dist/types.d.ts +125 -0
- package/dist/types.js +2 -0
- package/dist/words.d.ts +12 -0
- package/dist/words.js +26 -0
- package/dist/words.json +20 -0
- package/engine/package-lock.json +4680 -0
- package/engine/package.json +1 -0
- package/package.json +68 -0
- package/plugin/index.js +183 -0
- package/plugin/package.json +1 -0
- package/policy/policy.mjs +197 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"byokit-openclaw-engine","version":"0.1.0","type":"module","private":true,"dependencies":{"openclaw":"2026.8.1"}}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@byokit/openclaw",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "The OpenClaw runtime kit: the pinned engine's full operator surface with plain-words helpers for members, sign-in, runs and approvals.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/umeranjum17/byokit.git",
|
|
10
|
+
"directory": "packages/openclaw"
|
|
11
|
+
},
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=22.18"
|
|
14
|
+
},
|
|
15
|
+
"exports": {
|
|
16
|
+
".": {
|
|
17
|
+
"types": "./dist/index.d.ts",
|
|
18
|
+
"default": "./dist/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./device": {
|
|
21
|
+
"react-native": {
|
|
22
|
+
"types": "./dist/device.d.ts",
|
|
23
|
+
"default": "./dist/device.js"
|
|
24
|
+
},
|
|
25
|
+
"browser": {
|
|
26
|
+
"types": "./dist/device.d.ts",
|
|
27
|
+
"default": "./dist/device.js"
|
|
28
|
+
},
|
|
29
|
+
"types": "./dist/device.d.ts",
|
|
30
|
+
"default": "./dist/device.js"
|
|
31
|
+
},
|
|
32
|
+
"./link": {
|
|
33
|
+
"types": "./dist/link.d.ts",
|
|
34
|
+
"default": "./dist/link.js"
|
|
35
|
+
},
|
|
36
|
+
"./testing": {
|
|
37
|
+
"types": "./dist/testing/index.d.ts",
|
|
38
|
+
"default": "./dist/testing/index.js"
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"engine",
|
|
44
|
+
"plugin",
|
|
45
|
+
"policy",
|
|
46
|
+
"README.md",
|
|
47
|
+
"LICENSE",
|
|
48
|
+
"CHANGELOG.md"
|
|
49
|
+
],
|
|
50
|
+
"scripts": {
|
|
51
|
+
"prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
|
|
52
|
+
},
|
|
53
|
+
"dependencies": {
|
|
54
|
+
"@byokit/link": "0.3.2",
|
|
55
|
+
"@byokit/relay": "0.2.1",
|
|
56
|
+
"@byokit/reach": "0.2.0",
|
|
57
|
+
"@byokit/seal": "0.1.0",
|
|
58
|
+
"@openclaw/gateway-client": "2026.8.1",
|
|
59
|
+
"@openclaw/gateway-protocol": "2026.8.1",
|
|
60
|
+
"ws": "8.21.3"
|
|
61
|
+
},
|
|
62
|
+
"devDependencies": {
|
|
63
|
+
"@byokit/ui-core": "0.2.0"
|
|
64
|
+
},
|
|
65
|
+
"publishConfig": {
|
|
66
|
+
"access": "public"
|
|
67
|
+
}
|
|
68
|
+
}
|
package/plugin/index.js
ADDED
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
// The BYOKit bridge plugin (docs/runtime-kits.md 5.9): tool calls gated by the fail-closed unix-socket bridge.
|
|
2
|
+
// Runs in the gateway process. Reads its tool table from the sibling tools.json written by writePlugin, so
|
|
3
|
+
// schemas and descriptions stay app data. No imports beyond node builtins: nothing here needs resolving in-gateway.
|
|
4
|
+
import { connect } from 'node:net';
|
|
5
|
+
import { readFileSync } from 'node:fs';
|
|
6
|
+
|
|
7
|
+
/** TypeBox's Kind is a global symbol, so these schemas are valid TypeBox without importing typebox. */
|
|
8
|
+
const Kind = Symbol.for('TypeBox.Kind');
|
|
9
|
+
|
|
10
|
+
const table = JSON.parse(readFileSync(new URL('./tools.json', import.meta.url), 'utf8'));
|
|
11
|
+
const RUN_PARAM = table.runParam;
|
|
12
|
+
const PERMIT_PARAM = table.permitParam;
|
|
13
|
+
const TOOLS = new Map(table.tools.map((t) => [t.name, t]));
|
|
14
|
+
// Absent (a table from before the flag) reads as true: fail closed.
|
|
15
|
+
const GATE_BUILTINS = table.gateBuiltins !== false;
|
|
16
|
+
|
|
17
|
+
/** Minimal JSON Schema -> TypeBox-marked schema converter; output is both valid TypeBox and valid JSON Schema. */
|
|
18
|
+
function toTypeBox(schema) {
|
|
19
|
+
if (schema === true) return { [Kind]: 'Any' };
|
|
20
|
+
if (typeof schema !== 'object' || schema === null || Array.isArray(schema)) return { [Kind]: 'Any' };
|
|
21
|
+
const out = { ...schema };
|
|
22
|
+
const kind =
|
|
23
|
+
schema.const !== undefined ? 'Literal'
|
|
24
|
+
: Array.isArray(schema.enum) ? 'Union'
|
|
25
|
+
: schema.anyOf !== undefined ? 'Union'
|
|
26
|
+
: schema.oneOf !== undefined ? 'Union'
|
|
27
|
+
: schema.allOf !== undefined ? 'Intersect'
|
|
28
|
+
: schema.type === 'object' || (schema.type === undefined && (schema.properties !== undefined || schema.required !== undefined)) ? 'Object'
|
|
29
|
+
: schema.type === 'array' ? 'Array'
|
|
30
|
+
: schema.type === 'string' ? 'String'
|
|
31
|
+
: schema.type === 'number' ? 'Number'
|
|
32
|
+
: schema.type === 'integer' ? 'Integer'
|
|
33
|
+
: schema.type === 'boolean' ? 'Boolean'
|
|
34
|
+
: schema.type === 'null' ? 'Null'
|
|
35
|
+
: 'Any';
|
|
36
|
+
out[Kind] = kind;
|
|
37
|
+
if (kind === 'Literal') out.const = schema.const;
|
|
38
|
+
if (Array.isArray(schema.enum)) out.anyOf = schema.enum.map((v) => ({ [Kind]: 'Literal', const: v }));
|
|
39
|
+
if (Array.isArray(schema.anyOf)) out.anyOf = schema.anyOf.map(toTypeBox);
|
|
40
|
+
if (Array.isArray(schema.oneOf)) out.oneOf = schema.oneOf.map(toTypeBox);
|
|
41
|
+
if (Array.isArray(schema.allOf)) out.allOf = schema.allOf.map(toTypeBox);
|
|
42
|
+
if (out.properties && typeof out.properties === 'object') {
|
|
43
|
+
out.properties = Object.fromEntries(Object.entries(out.properties).map(([k, v]) => [k, toTypeBox(v)]));
|
|
44
|
+
}
|
|
45
|
+
if (out.items !== undefined) {
|
|
46
|
+
out.items = Array.isArray(out.items) ? out.items.map(toTypeBox) : toTypeBox(out.items);
|
|
47
|
+
}
|
|
48
|
+
if (out.additionalProperties !== undefined && typeof out.additionalProperties === 'object') {
|
|
49
|
+
out.additionalProperties = toTypeBox(out.additionalProperties);
|
|
50
|
+
}
|
|
51
|
+
return out;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One newline-framed request over the bridge socket: one connection, one request, then close. */
|
|
55
|
+
function bridgeRequest(message, { timeoutMs, signal } = {}) {
|
|
56
|
+
const path = process.env.BYOKIT_BRIDGE_SOCK;
|
|
57
|
+
if (!path) return Promise.reject(new Error("can't check this action right now"));
|
|
58
|
+
return new Promise((resolve, reject) => {
|
|
59
|
+
const socket = connect(path);
|
|
60
|
+
let buffer = '';
|
|
61
|
+
let done = false;
|
|
62
|
+
let timer;
|
|
63
|
+
const fail = (error) => {
|
|
64
|
+
if (done) return;
|
|
65
|
+
done = true;
|
|
66
|
+
clearTimeout(timer);
|
|
67
|
+
socket.destroy();
|
|
68
|
+
reject(error);
|
|
69
|
+
};
|
|
70
|
+
const abort = () => fail(new Error('the call was cancelled'));
|
|
71
|
+
if (signal) {
|
|
72
|
+
if (signal.aborted) return abort();
|
|
73
|
+
signal.addEventListener('abort', abort, { once: true });
|
|
74
|
+
}
|
|
75
|
+
socket.once('error', () => fail(new Error("can't check this action right now")));
|
|
76
|
+
socket.on('data', (chunk) => {
|
|
77
|
+
buffer += String(chunk);
|
|
78
|
+
if (buffer.length > 1_000_000) return fail(new Error("can't check this action right now"));
|
|
79
|
+
const end = buffer.indexOf('\n');
|
|
80
|
+
if (end < 0) return;
|
|
81
|
+
done = true;
|
|
82
|
+
clearTimeout(timer);
|
|
83
|
+
signal?.removeEventListener('abort', abort);
|
|
84
|
+
socket.end();
|
|
85
|
+
try {
|
|
86
|
+
resolve(JSON.parse(buffer.slice(0, end)));
|
|
87
|
+
} catch {
|
|
88
|
+
fail(new Error("can't check this action right now"));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
socket.once('connect', () => {
|
|
92
|
+
timer = setTimeout(() => fail(new Error("can't check this action right now")), timeoutMs ?? 195_000);
|
|
93
|
+
socket.write(JSON.stringify(message) + '\n');
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function gate(key, tool, input, signal) {
|
|
99
|
+
const reply = await bridgeRequest({ kind: 'gate', key, tool, input }, { timeoutMs: 195_000, signal });
|
|
100
|
+
return reply && typeof reply === 'object' ? reply : { allow: false, reason: "can't check this action right now" };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default {
|
|
104
|
+
id: table.id,
|
|
105
|
+
name: 'BYOKit bridge',
|
|
106
|
+
description: 'Gates tool calls through the host app before they run.',
|
|
107
|
+
configSchema: {
|
|
108
|
+
safeParse(value) {
|
|
109
|
+
if (value === undefined) return { success: true, data: undefined };
|
|
110
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return { success: false, error: 'expected config object' };
|
|
111
|
+
if (Object.keys(value).length > 0) return { success: false, error: 'config must be empty' };
|
|
112
|
+
return { success: true, data: value };
|
|
113
|
+
},
|
|
114
|
+
jsonSchema: { type: 'object', additionalProperties: false, properties: {} },
|
|
115
|
+
},
|
|
116
|
+
register(api) {
|
|
117
|
+
for (const spec of table.tools) {
|
|
118
|
+
api.registerTool({
|
|
119
|
+
name: spec.name,
|
|
120
|
+
label: spec.name,
|
|
121
|
+
description: spec.description,
|
|
122
|
+
parameters: toTypeBox(spec.parameters),
|
|
123
|
+
async execute(_toolCallId, params, signal) {
|
|
124
|
+
const input = { ...(params ?? {}) };
|
|
125
|
+
const run = input[RUN_PARAM];
|
|
126
|
+
const permit = input[PERMIT_PARAM];
|
|
127
|
+
delete input[RUN_PARAM];
|
|
128
|
+
delete input[PERMIT_PARAM];
|
|
129
|
+
// The run key is always required; the permit only rides along when the gate minted one (N1).
|
|
130
|
+
if (typeof run !== 'string') {
|
|
131
|
+
throw new Error('this tool needs approval before it runs');
|
|
132
|
+
}
|
|
133
|
+
const call = { kind: 'call', key: run, tool: spec.name, input };
|
|
134
|
+
if (typeof permit === 'string') call.permit = permit;
|
|
135
|
+
let reply;
|
|
136
|
+
try {
|
|
137
|
+
reply = await bridgeRequest(call, { timeoutMs: 195_000, signal });
|
|
138
|
+
} catch (error) {
|
|
139
|
+
throw new Error(error instanceof Error ? error.message : "can't check this action right now");
|
|
140
|
+
}
|
|
141
|
+
if (reply && typeof reply === 'object' && reply.ok === true && typeof reply.text === 'string') {
|
|
142
|
+
return { content: [{ type: 'text', text: reply.text }], details: {} };
|
|
143
|
+
}
|
|
144
|
+
const reason =
|
|
145
|
+
reply && typeof reply === 'object' && typeof reply.reason === 'string'
|
|
146
|
+
? reply.reason
|
|
147
|
+
: "can't check this action right now";
|
|
148
|
+
throw new Error(reason);
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
api.on('before_tool_call', async (event, ctx) => {
|
|
153
|
+
// Engine builtins (web_fetch, memory, ...) are gated too unless the app opted out; they run in-engine and
|
|
154
|
+
// never call back, so an allow passes them through unchanged.
|
|
155
|
+
const builtin = !TOOLS.has(event.toolName);
|
|
156
|
+
if (builtin && !GATE_BUILTINS) return undefined;
|
|
157
|
+
const key = ctx?.sessionKey;
|
|
158
|
+
if (typeof key !== 'string' || !key) return { block: true, blockReason: "can't check this action right now" };
|
|
159
|
+
let decision;
|
|
160
|
+
try {
|
|
161
|
+
decision = await gate(key, event.toolName, event.params ?? {}, ctx?.abortSignal);
|
|
162
|
+
} catch (error) {
|
|
163
|
+
return { block: true, blockReason: error instanceof Error ? error.message : "can't check this action right now" };
|
|
164
|
+
}
|
|
165
|
+
if (!decision.allow) {
|
|
166
|
+
return {
|
|
167
|
+
block: true,
|
|
168
|
+
blockReason: typeof decision.reason === 'string' ? decision.reason : "can't check this action right now",
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (builtin) return undefined;
|
|
172
|
+
// The bridge mints a permit for permitted tools and admits the rest ticket-side; either way the run key
|
|
173
|
+
// rides along and execute sends back only what the gate gave it (N1).
|
|
174
|
+
return {
|
|
175
|
+
params: {
|
|
176
|
+
...(event.params ?? {}),
|
|
177
|
+
[RUN_PARAM]: key,
|
|
178
|
+
...(typeof decision.permit === 'string' ? { [PERMIT_PARAM]: decision.permit } : {}),
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
});
|
|
182
|
+
},
|
|
183
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"name":"byokit-openclaw-bridge","version":"0.1.0","type":"module","openclaw":{"extensions":["./index.js"]}}
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
// The operator install policy (docs/runtime-kits.md 5.9): run as a command by OpenClaw's security.installPolicy,
|
|
2
|
+
// not imported. Reads one protocol-1 request on stdin, prints one protocol-1 envelope on stdout. Fail closed:
|
|
3
|
+
// anything unverifiable is a block.
|
|
4
|
+
//
|
|
5
|
+
// Inputs from the environment (set by the kit's installPolicy config):
|
|
6
|
+
// BYOKIT_TRUSTED_SKILLS path to the trusted-skills JSON file (Crewhouse's trusted-skills.json shape):
|
|
7
|
+
// either a list [{ id|name, version, sha256|hash }] or a map { "<id>": { version, sha256 } }.
|
|
8
|
+
// BYOKIT_OWN_ROOTS JSON array of own content roots; skills sourced under them are the app's own.
|
|
9
|
+
// Rules: any request kind containing 'depend' is blocked everywhere (own roots included);
|
|
10
|
+
// dependency installers (a skill installSpec carrying bins/formula/package/module/url/archive) are blocked;
|
|
11
|
+
// skill installs are allowed only from own roots or with an exact id + version + SKILL.md sha256 trusted match
|
|
12
|
+
// (the version is request.origin.version when present, else the SKILL.md frontmatter version);
|
|
13
|
+
// plugin installs are allowed only in the @openclaw/ scope.
|
|
14
|
+
import { createHash } from 'node:crypto';
|
|
15
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
16
|
+
import { relative, resolve } from 'node:path';
|
|
17
|
+
|
|
18
|
+
const allow = (findings) => ({ protocolVersion: 1, decision: 'allow', ...(findings ? { findings } : {}) });
|
|
19
|
+
const block = (reason, findings) => ({
|
|
20
|
+
protocolVersion: 1,
|
|
21
|
+
decision: 'block',
|
|
22
|
+
reason,
|
|
23
|
+
...(findings && findings.length > 0 ? { findings } : {}),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const isRecord = (v) => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
27
|
+
|
|
28
|
+
/** The trusted-skills file, tolerated in list or map shape with several key spellings. */
|
|
29
|
+
function loadTrusted() {
|
|
30
|
+
const path = process.env.BYOKIT_TRUSTED_SKILLS;
|
|
31
|
+
if (!path || !existsSync(path)) return [];
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'));
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
const entries = [];
|
|
39
|
+
const push = (id, entry) => {
|
|
40
|
+
if (typeof id !== 'string' || !id) return;
|
|
41
|
+
const version =
|
|
42
|
+
isRecord(entry) && (typeof entry.version === 'string' ? entry.version : undefined);
|
|
43
|
+
const sha =
|
|
44
|
+
(isRecord(entry) && typeof entry.sha256 === 'string' && entry.sha256) ||
|
|
45
|
+
(isRecord(entry) && typeof entry.hash === 'string' && entry.hash) ||
|
|
46
|
+
(typeof entry === 'string' && entry) ||
|
|
47
|
+
undefined;
|
|
48
|
+
entries.push({ id, version, sha });
|
|
49
|
+
};
|
|
50
|
+
if (Array.isArray(parsed)) {
|
|
51
|
+
for (const entry of parsed) {
|
|
52
|
+
if (typeof entry === 'string') push(entry, undefined);
|
|
53
|
+
else if (isRecord(entry)) push(entry.id ?? entry.name, entry);
|
|
54
|
+
}
|
|
55
|
+
} else if (isRecord(parsed)) {
|
|
56
|
+
for (const [id, entry] of Object.entries(parsed)) push(id, entry);
|
|
57
|
+
}
|
|
58
|
+
return entries;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function ownRoots() {
|
|
62
|
+
try {
|
|
63
|
+
const roots = JSON.parse(process.env.BYOKIT_OWN_ROOTS ?? '[]');
|
|
64
|
+
return Array.isArray(roots) ? roots.filter((r) => typeof r === 'string').map((r) => resolve(r)) : [];
|
|
65
|
+
} catch {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const inside = (root, p) => {
|
|
71
|
+
const rel = relative(root, resolve(p));
|
|
72
|
+
return rel !== '' && !rel.startsWith('..');
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/** First `name:`/`version:` scalars of the SKILL.md frontmatter, plus the sha256 of the file bytes. */
|
|
76
|
+
function skillIdentity(sourcePath) {
|
|
77
|
+
let bytes;
|
|
78
|
+
try {
|
|
79
|
+
const file = resolve(sourcePath, 'SKILL.md');
|
|
80
|
+
if (!statSync(file).isFile()) return undefined;
|
|
81
|
+
bytes = readFileSync(file);
|
|
82
|
+
} catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const text = bytes.toString('utf8');
|
|
86
|
+
let name;
|
|
87
|
+
let version;
|
|
88
|
+
if (text.startsWith('---')) {
|
|
89
|
+
const end = text.indexOf('\n---', 3);
|
|
90
|
+
const front = text.slice(3, end < 0 ? 0 : end);
|
|
91
|
+
for (const line of front.split('\n')) {
|
|
92
|
+
const m = /^\s*(name|version)\s*:\s*(.+?)\s*$/.exec(line);
|
|
93
|
+
if (m) {
|
|
94
|
+
const value = m[2].replace(/^['"]|['"]$/g, '');
|
|
95
|
+
if (m[1] === 'name') name = value;
|
|
96
|
+
else version = value;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return { name, version, sha256: createHash('sha256').update(bytes).digest('hex') };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const INSTALLER_FIELDS = ['bins', 'formula', 'package', 'module', 'url', 'archive'];
|
|
104
|
+
|
|
105
|
+
function decideSkill(request, trusted, roots) {
|
|
106
|
+
const kind =
|
|
107
|
+
isRecord(request.request) && typeof request.request.kind === 'string' ? request.request.kind : undefined;
|
|
108
|
+
if (kind !== undefined && /depend/i.test(kind)) {
|
|
109
|
+
return block(`dependency installs are not allowed: ${kind}`, [
|
|
110
|
+
{ ruleId: 'dependency-installer', severity: 'critical', message: `request kind ${kind} fetches dependencies` },
|
|
111
|
+
]);
|
|
112
|
+
}
|
|
113
|
+
const spec = isRecord(request.skill) && isRecord(request.skill.installSpec) ? request.skill.installSpec : {};
|
|
114
|
+
for (const field of INSTALLER_FIELDS) {
|
|
115
|
+
if (spec[field] !== undefined) {
|
|
116
|
+
return block(`skill installs that fetch ${field} are not allowed`, [
|
|
117
|
+
{ ruleId: 'dependency-installer', severity: 'critical', message: `installSpec carries ${field}` },
|
|
118
|
+
]);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const sourcePath = typeof request.sourcePath === 'string' ? request.sourcePath : undefined;
|
|
122
|
+
if (!sourcePath) return block('skill source is not verifiable');
|
|
123
|
+
if (roots.some((root) => inside(root, sourcePath) || resolve(sourcePath) === root)) return allow();
|
|
124
|
+
const identity = skillIdentity(sourcePath);
|
|
125
|
+
if (!identity) return block('skill SKILL.md is not verifiable');
|
|
126
|
+
// Bundled skills carry no frontmatter version; the reviewed version travels on request.origin.version.
|
|
127
|
+
const originVersion =
|
|
128
|
+
isRecord(request.origin) && typeof request.origin.version === 'string' ? request.origin.version : undefined;
|
|
129
|
+
const candidateVersion = originVersion ?? identity.version;
|
|
130
|
+
const ids = [spec.id, request.targetName, identity.name].filter((v) => typeof v === 'string');
|
|
131
|
+
const hit = trusted.find(
|
|
132
|
+
(entry) =>
|
|
133
|
+
ids.includes(entry.id) &&
|
|
134
|
+
(entry.version === undefined && candidateVersion === undefined || entry.version === candidateVersion) &&
|
|
135
|
+
entry.sha !== undefined &&
|
|
136
|
+
entry.sha.toLowerCase() === identity.sha256.toLowerCase(),
|
|
137
|
+
);
|
|
138
|
+
if (!hit) return block(`skill ${ids[0] ?? request.targetName} is not a trusted skill`);
|
|
139
|
+
return allow();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function decidePlugin(request) {
|
|
143
|
+
const plugin = isRecord(request.plugin) ? request.plugin : {};
|
|
144
|
+
const scope = [plugin.packageName, plugin.pluginId, request.targetName].find((v) => typeof v === 'string');
|
|
145
|
+
if (!scope || !scope.startsWith('@openclaw/')) {
|
|
146
|
+
return block(`only the @openclaw/ plugin scope is allowed, not ${scope ?? request.targetName}`);
|
|
147
|
+
}
|
|
148
|
+
// N4: the scope name above is self-declared (the candidate manifest's own
|
|
149
|
+
// `name`). Allow only with engine proof of registry origin on the pinned
|
|
150
|
+
// engine: source.kind 'npm' (engine-derived from the install request kind;
|
|
151
|
+
// npm installs always carry authority 'third-party') plus the
|
|
152
|
+
// operator-requested specifier in the @openclaw/ scope. Anything else is a
|
|
153
|
+
// local/archive/git/file candidate spoofing the name. Fail closed.
|
|
154
|
+
const source = isRecord(request.source) ? request.source : undefined;
|
|
155
|
+
const specifier =
|
|
156
|
+
isRecord(request.request) && typeof request.request.requestedSpecifier === 'string'
|
|
157
|
+
? request.request.requestedSpecifier
|
|
158
|
+
: undefined;
|
|
159
|
+
if (source?.kind === 'npm' && specifier !== undefined && specifier.startsWith('@openclaw/')) return allow();
|
|
160
|
+
return block(`@openclaw/ plugin install is not from the npm registry: ${specifier ?? request.targetName}`, [
|
|
161
|
+
{ ruleId: 'plugin-source', severity: 'critical', message: 'self-declared @openclaw/ name without registry source proof' },
|
|
162
|
+
]);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function decide(request) {
|
|
166
|
+
if (!isRecord(request) || request.protocolVersion !== 1) return block('unknown policy protocol');
|
|
167
|
+
if (request.targetType === 'plugin') return decidePlugin(request);
|
|
168
|
+
if (request.targetType === 'skill') return decideSkill(request, loadTrusted(), ownRoots());
|
|
169
|
+
return block(`unknown install target ${String(request.targetType)}`);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function main() {
|
|
173
|
+
let input = '';
|
|
174
|
+
const fail = (reason) => {
|
|
175
|
+
process.stdout.write(JSON.stringify(block(reason)) + '\n');
|
|
176
|
+
};
|
|
177
|
+
try {
|
|
178
|
+
input = readFileSync(0, 'utf8');
|
|
179
|
+
} catch {
|
|
180
|
+
fail('policy could not read its request');
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
if (Buffer.byteLength(input, 'utf8') > 1024 * 1024) {
|
|
184
|
+
fail('policy request too large');
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
let request;
|
|
188
|
+
try {
|
|
189
|
+
request = JSON.parse(input);
|
|
190
|
+
} catch {
|
|
191
|
+
fail('policy request is not JSON');
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
process.stdout.write(JSON.stringify(decide(request)) + '\n');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
main();
|