@byokit/openclaw 0.14.2 → 0.14.4

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/dist/signin.d.ts CHANGED
@@ -15,6 +15,11 @@ export type SignInCtx = {
15
15
  restarted(authBefore: string | undefined, signal?: AbortSignal): Promise<boolean>;
16
16
  stopSignal?: AbortSignal;
17
17
  fresh?: boolean;
18
+ setProviderHeaders?(provider: string, headers: Record<string, string> | null): Promise<void>;
19
+ listWorkspaces?(key: string, signal: AbortSignal): Promise<{
20
+ id: string;
21
+ name: string;
22
+ }[]>;
18
23
  };
19
24
  /**
20
25
  * Drive the engine's provider-owned login. `on` sees every view the person watches; `done` resolves once, and
package/dist/signin.js CHANGED
@@ -4,6 +4,7 @@ import { randomUUID } from 'node:crypto';
4
4
  import { createServer } from 'node:http';
5
5
  import { activate, LATE_RESTART } from './activation.js';
6
6
  import { authStatus } from './auth-status.js';
7
+ import { LIVE_CATALOGS } from './constants.js';
7
8
  import { routes } from './routes.js';
8
9
  import { signedInProviders } from './runs.js';
9
10
  import { words } from './words.js';
@@ -298,6 +299,55 @@ export function signIn(ctx, member, o, on) {
298
299
  if (!apiKey)
299
300
  return { state: 'failed', via, why: signal.aborted ? 'declined' : 'expired' };
300
301
  secret = apiKey.trim() || undefined;
302
+ // A live-catalog key can act for several workspaces, and the Go gateway refuses every call until one is
303
+ // named (403 Workspace selection is required): the engine carries the workspace only from its provider
304
+ // config headers, so resolve it here the way the kit's key lane does, and write the header before the
305
+ // activation so the activation probe itself carries it. One workspace is used unasked; several ask here,
306
+ // answered through the same paste (an id or an exact name); a key the console refuses (a workspace-bound
307
+ // service key) needs none; a listing that cannot be read fails the sign-in before anything changes.
308
+ if (secret && keyRoute && LIVE_CATALOGS.includes(keyRoute.provider) && ctx.setProviderHeaders && ctx.listWorkspaces) {
309
+ let workspaces;
310
+ try {
311
+ workspaces = await ctx.listWorkspaces(secret, signal);
312
+ }
313
+ catch {
314
+ return { state: 'failed', via, why: 'failed', error: 'The workspace list could not be read. Try again.' };
315
+ }
316
+ if (signal.aborted)
317
+ return cancelled();
318
+ const pickable = workspaces.filter((w) => w.id);
319
+ let orgId = workspaces.length === 1 ? pickable[0]?.id ?? '' : '';
320
+ let retrying = false;
321
+ while (orgId === '' && pickable.length > 1) {
322
+ if (signal.aborted)
323
+ return cancelled();
324
+ // A re-ask says a different sentence, so an app that watches the prompt sees the answer was not taken.
325
+ // Names alone: the ids are unbreakable noise on a phone; the answer may still be an id.
326
+ say({ prompt: `${retrying ? 'That is not one of them. ' : ''}Which workspace? Paste one of: ${pickable.map((w) => w.name).join(', ')}` });
327
+ retrying = true;
328
+ const text = returned ?? await new Promise((resolve) => {
329
+ const stop = (value) => { clearTimeout(timer); signal.removeEventListener('abort', abort); pasteIn = undefined; resolve(value); };
330
+ const abort = () => stop();
331
+ const timer = setTimeout(abort, PASTE_MS);
332
+ pasteIn = stop;
333
+ signal.addEventListener('abort', abort, { once: true });
334
+ if (signal.aborted)
335
+ abort();
336
+ });
337
+ returned = undefined;
338
+ if (!text)
339
+ return { state: 'failed', via, why: signal.aborted ? 'declined' : 'expired' };
340
+ const given = text.trim().toLowerCase();
341
+ const match = pickable.find((w) => w.id.toLowerCase() === given)
342
+ ?? pickable.find((w) => w.name.toLowerCase() === given);
343
+ if (match)
344
+ orgId = match.id;
345
+ }
346
+ if (signal.aborted)
347
+ return cancelled();
348
+ if (orgId)
349
+ await ctx.setProviderHeaders(keyRoute.provider, { 'x-opencode-org-id': orgId });
350
+ }
301
351
  }
302
352
  // The commit point: a phone cancel or host shutdown that landed before it activates nothing. Done waits for the gateway restart an `auth` change causes (activation.ts).
303
353
  // For Claude only host shutdown cuts that wait; a late restart has persisted the route and says saved. A key's cancel still declines its wait; a late key keeps the shared words.
@@ -380,11 +380,15 @@ export function fakeGateway(script, o) {
380
380
  // deleteFiles: false keeps the workspace, as the kit always asks; the agent's own store and model go.
381
381
  'agents.delete': (p) => {
382
382
  const id = String(p.agentId);
383
- if (!agents.delete(id))
383
+ if (!agents.has(id))
384
384
  throw new Error(`agent "${id}" not found`);
385
- profiles.delete(id);
386
- orders.delete(id);
387
- models.delete(id);
385
+ const drop = () => { agents.delete(id); profiles.delete(id); orders.delete(id); models.delete(id); };
386
+ // With rosterApplyMs the fake mirrors the pinned engine: delete answers before the roster hot-reload
387
+ // drops the agent from agents.list (the create side lags the same way).
388
+ if (o?.rosterApplyMs)
389
+ setTimeout(drop, o.rosterApplyMs);
390
+ else
391
+ drop();
388
392
  return { ok: true, agentId: id, removedBindings: 0 };
389
393
  },
390
394
  // model: null clears the agent's primary (config.patch cannot delete a key); a string sets it.
@@ -0,0 +1,150 @@
1
+ {
2
+ "amazon-bedrock": {
3
+ "spec": "@openclaw/amazon-bedrock-provider@2026.8.35",
4
+ "integrity": "sha512-wYOiY7RaRo0SFXxcBVcqWnfwBH4TNc921Vlz7adBS+T5DIhtbnyzcAsMS1XsS9KvrYU3n1yW5SDdexvxop04hA=="
5
+ },
6
+ "amazon-bedrock-mantle": {
7
+ "spec": "@openclaw/amazon-bedrock-mantle-provider@2026.8.35",
8
+ "integrity": "sha512-bolUz+/osnBgarPBAko/+POthZ57JUXZ6T2nGMdQz+TfmdfnDhdZtXBRgy8JFjeDVgnNneWX4P1iuCgRRIsoJQ=="
9
+ },
10
+ "anthropic-vertex": {
11
+ "spec": "@openclaw/anthropic-vertex-provider@2026.8.35",
12
+ "integrity": "sha512-zQ5OYT7WvCgbyUnInz3+al/jCrN3RE2yMlC2wopVgjP9IMg1jANK0wPXArXWecT1p+j8+i2/TvxPI55U10Vdeg=="
13
+ },
14
+ "arcee": {
15
+ "spec": "@openclaw/arcee-provider@2026.8.35",
16
+ "integrity": "sha512-ViygKglSVjq5hQGVkduqe39Iiw9OBht/kTxOctLsgRvUzOWOqZvA9NvLybUtj230xLFCb/AVs1f3iGFRoXMBCA=="
17
+ },
18
+ "baseten": {
19
+ "spec": "@openclaw/baseten-provider@2026.8.35",
20
+ "integrity": "sha512-7zieofesRMB9fp41Ilh4VIFLIKJfRe1eT5ZYp3NKGwyON5rH0X11IcMXL1nRzIbIRyKnodfvl+H1EPO8NC4exA=="
21
+ },
22
+ "byteplus": {
23
+ "spec": "@openclaw/byteplus-provider@2026.8.35",
24
+ "integrity": "sha512-6Z9uHUxVSAt9TNNe5eiZfPhsExgInueEVCOeHxrZ6OybULJwDGx/cBJEY1LAy3MXzwCLX8ayENEtutcajxC0ew=="
25
+ },
26
+ "cerebras": {
27
+ "spec": "@openclaw/cerebras-provider@2026.8.35",
28
+ "integrity": "sha512-rqe2RMXn2hea0IpZuC01KwknMBOoH2fD9spGAvmffL8sX8qai+wSXW0Cn0pSOz9T+i862N5N3RXQP/K35QOvdA=="
29
+ },
30
+ "chutes": {
31
+ "spec": "@openclaw/chutes-provider@2026.8.35",
32
+ "integrity": "sha512-76+x2QS3NEgml0YnxV7fNbS7wO93oRtjlIFwkw0aUBYBzg1744aE3wEXXWkLcGpjVxp9bvgcpJ18Hp+gaW8F0A=="
33
+ },
34
+ "cloudflare-ai-gateway": {
35
+ "spec": "@openclaw/cloudflare-ai-gateway-provider@2026.8.35",
36
+ "integrity": "sha512-TjJJHTRdIiTVAM75w1xOed6VR0hQbAe30gNOc2ighi9G7FWU5wb3VeSQ9CtcJsAHYS5kurQNCf45owJfzkWGTg=="
37
+ },
38
+ "cohere": {
39
+ "spec": "@openclaw/cohere-provider@2026.8.35",
40
+ "integrity": "sha512-lA35F/ztGyYLAyRsrygogmL9WxU3M4X1lRSLCr1zZakXJDj0c6g46XtMmqxjtdQNCTo8TFni0gs4lqowZNq0OQ=="
41
+ },
42
+ "comfy": {
43
+ "spec": "@openclaw/comfy-provider@2026.8.35",
44
+ "integrity": "sha512-KHnjNwTCoy5l8Quq5zu7CwPBfg8XdTlI1W3GLg2CcamR+jksjbLpPzvrIObN8MineWVwpwmPgsdP3PyfyKxIaQ=="
45
+ },
46
+ "deepinfra": {
47
+ "spec": "@openclaw/deepinfra-provider@2026.8.35",
48
+ "integrity": "sha512-r4jktr88r3AFGmZRKtAjLkEOUQG5OAz+bXTwHpZxz4M28pbik3bJU2eBBaHAzahE7QdP9Xi3C3eIxoUl6/yi9w=="
49
+ },
50
+ "deepseek": {
51
+ "spec": "@openclaw/deepseek-provider@2026.8.35",
52
+ "integrity": "sha512-UVT+YusrPGSHLD7xd3YzJ45Hfe+ezFKEVBgaxPfYAUoXKvwN/OftG/kVga+bkjoxxSrp5Q+yI0Ec1kPBDhQ2RQ=="
53
+ },
54
+ "featherless": {
55
+ "spec": "@openclaw/featherless-provider@2026.8.35",
56
+ "integrity": "sha512-3XzG0avloplS8NVcNtq57JN91Sq6YnFcNSVcwz9IN8Q0oKcyxt5pabWfS6jCvsaBfV3sFaRf1Amn2PNrQKgHgQ=="
57
+ },
58
+ "fireworks": {
59
+ "spec": "@openclaw/fireworks-provider@2026.8.35",
60
+ "integrity": "sha512-CUms5nyK72D5VyNyXH5FZdy73gjlLMA3Vz6fpc6Z9ZtR6LxHjET7e91pM2cnjm6AkO4pUbh3sgiOeU/+lCYNPg=="
61
+ },
62
+ "gmi": {
63
+ "spec": "@openclaw/gmi-provider@2026.8.35",
64
+ "integrity": "sha512-OOLyjF58RdhwGgvaIyudhwpKzunSWbaOlyfUUr+oTFkOhb3ZzfLMx9Kqweql0ug04fh/aQ6s9IvX2jK2adAbBA=="
65
+ },
66
+ "groq": {
67
+ "spec": "@openclaw/groq-provider@2026.8.35",
68
+ "integrity": "sha512-/VmTp3GXaz1F06HfG0Xc83E8Ul/tsjQ6DRd6bscjmHOH8JtKv1U+AgQa8Uj5wMPmop9ttbqPXCqHuenMViankg=="
69
+ },
70
+ "kilocode": {
71
+ "spec": "@openclaw/kilocode-provider@2026.8.35",
72
+ "integrity": "sha512-O8hp8BQqAVElY2UXdE5EBBwzba7xevStF7TIi9TE59NTADK2JojK9bStF6rX4y9Ctp5NeMpf+KyYFTiSfm/N1A=="
73
+ },
74
+ "kimi": {
75
+ "spec": "@openclaw/kimi-provider@2026.8.35",
76
+ "integrity": "sha512-jeS3BmTXoa8A3OWXg6TB+vlKj1t+bvJFZQIvbXSGUNm2vIUSNXLYvgzNgmhF24MdwHwyBPdcUnJVZyMCklbEBw=="
77
+ },
78
+ "longcat": {
79
+ "spec": "@openclaw/longcat-provider@2026.8.35",
80
+ "integrity": "sha512-E4hHADSRpkHtiy6FfoHQpYD94usn+J0sVmY2fn94ftNNV4SjVvILqybjBiqfAlVy5FVOPRytL6P2y6hkdd52Cg=="
81
+ },
82
+ "meta": {
83
+ "spec": "@openclaw/meta-provider@2026.8.35",
84
+ "integrity": "sha512-3cEwNKmJW1wIwQE40crK1O4X4eo6n4xzH1v/7raa79DeoTOJ8pc8PWa66stv51ESwWhYDcCFVHU7XtnrbXyUXA=="
85
+ },
86
+ "mistral": {
87
+ "spec": "@openclaw/mistral-provider@2026.8.35",
88
+ "integrity": "sha512-TNULKvqsMMXz1ZNr89UPv9ucqcUYcw1hKLqoOo0yB/yRUazNErepds1qOzYJYA4lksylvhvvruDs9vIUMbfTjA=="
89
+ },
90
+ "moonshot": {
91
+ "spec": "@openclaw/moonshot-provider@2026.8.35",
92
+ "integrity": "sha512-gk9C4Hg/+K9taU/cRgNO77QzmPw46fo7klNB6y7qhefJQwoknbtCJLgnMpVEo97uiAxYxCTYRNvI+fOKF7Em5A=="
93
+ },
94
+ "novita": {
95
+ "spec": "@openclaw/novita-provider@2026.8.35",
96
+ "integrity": "sha512-TNui/LbLCsakaj677E+Xw2D1hRztQMOJWLj86JcqOfS7QzfEM6rA+FRs996HnXUIpqXeaefHc0qdVghKEXhgOw=="
97
+ },
98
+ "opencode": {
99
+ "spec": "@openclaw/opencode-provider@2026.8.35",
100
+ "integrity": "sha512-6wRKxfmgETuusys4K74cB0ixZVqQCgJBvD2g74cQAgx8w7BMr7+2965uYbU1sMjspreOigjgYLfePcMgV5conQ=="
101
+ },
102
+ "pixverse": {
103
+ "spec": "@openclaw/pixverse-provider@2026.8.35",
104
+ "integrity": "sha512-BbAp0Nfphem5LAytazWWAB5TJTM7L5zC7cWd+7r4iHU5iO7CyZYAGS5rjeLV3XEUwD2TAoWcHKBe8TiRXgsbaQ=="
105
+ },
106
+ "qianfan": {
107
+ "spec": "@openclaw/qianfan-provider@2026.8.35",
108
+ "integrity": "sha512-LSSz68Bv5/vRemKRrVNoDXA9zLx7yymMEY94fP5cgat9rsR83Fa6bA5hyEucutExz5wf2FJWmWqEJy2snm0k9A=="
109
+ },
110
+ "qwen": {
111
+ "spec": "@openclaw/qwen-provider@2026.8.35",
112
+ "integrity": "sha512-TaCx2LEOZgcXrWv0FLdBuFHoFvwDVNH+loGcO3MUIwhIBPoQTHD7Q+7DTOcLeK8qrQA/zGvWitT2WismCQJ7pw=="
113
+ },
114
+ "stepfun": {
115
+ "spec": "@openclaw/stepfun-provider@2026.8.35",
116
+ "integrity": "sha512-JBUKhN36cjUsMea8AOfX8yROEb7Zp01YPLpjzI/jnosalq2mhfYJY4kJTLou2RGwblDXx1dj0vB2HWidA+e/yw=="
117
+ },
118
+ "synthetic": {
119
+ "spec": "@openclaw/synthetic-provider@2026.8.35",
120
+ "integrity": "sha512-Lfs2lFTvsMEDNqZG2myoX8n7sbmIEVXMYV/Roa4umnPLsLT2aeqLkvWyesQWl9XRneSnGNtWS6yhbKLaqXU0Ng=="
121
+ },
122
+ "tencent": {
123
+ "spec": "@openclaw/tencent-provider@2026.8.35",
124
+ "integrity": "sha512-8khAe7ZaR0o3KPxiWnnyGaLFwDTF1ft1Cr3v1YrBntL1Nr8OvFM1XP/Lyifn3MJ7SK4lajYxLVsfuq2E18Jxeg=="
125
+ },
126
+ "venice": {
127
+ "spec": "@openclaw/venice-provider@2026.8.35",
128
+ "integrity": "sha512-c/Au2qkMeneJRKKm2o0/Fkf7dpcJR5RVJPmEPZhOsFaOUoq3zAk+Is/42khZ136Yj/UyQ09nFTUJsqmaeKoZVA=="
129
+ },
130
+ "vercel-ai-gateway": {
131
+ "spec": "@openclaw/vercel-ai-gateway-provider@2026.8.35",
132
+ "integrity": "sha512-gL9aGnfVGKVvB9mvMDRmIPOPTqQUqTr11DU60nn7BqDUCpLckoRUSXxzH3Rituxj+5dyoLI1LL5RiA9SPoImRA=="
133
+ },
134
+ "volcengine": {
135
+ "spec": "@openclaw/volcengine-provider@2026.8.35",
136
+ "integrity": "sha512-7dqdTtFgJcEBiSdzt5gLLPop/y6rK5FuobjiAGDwKxbPVENo0hmWJ8HGWbLKzRXerhzK20pvzF/+rr3OGDkRPQ=="
137
+ },
138
+ "vydra": {
139
+ "spec": "@openclaw/vydra-provider@2026.8.35",
140
+ "integrity": "sha512-Z0xHR8t+0Ge2GCgVDIlH1WLq4XeI5WoOgvkMvDvb4CQkxW+c251L0gbg37R6CNPkdfDFn8mh5/yIQrkazNj7dQ=="
141
+ },
142
+ "xiaomi": {
143
+ "spec": "@openclaw/xiaomi-provider@2026.8.35",
144
+ "integrity": "sha512-xtH3ESbMBcPQIciYiJCBP2ijd6MpP6tU/y27dJ3PGF8SluKZlbUfTUrqjT9U/NHo/bwafRISxL/HtAKYSm3Jkg=="
145
+ },
146
+ "zai": {
147
+ "spec": "@openclaw/zai-provider@2026.8.35",
148
+ "integrity": "sha512-g3ceSnLS84lNW2KlHznrO9vPXhX4ul1nQ+Dc0K4ccHdLOVfQ0DjjgV1NbT4CQZiTlN7Z2UFtGzl5EQeD1oCPfQ=="
149
+ }
150
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@byokit/openclaw",
3
- "version": "0.14.2",
3
+ "version": "0.14.4",
4
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
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -60,10 +60,10 @@
60
60
  "prepack": "tsc -b && node ../../scripts/fix-words-dts.cjs"
61
61
  },
62
62
  "dependencies": {
63
- "@byokit/accounts": "0.24.1",
64
- "@byokit/pair": "0.10.1",
65
- "@byokit/relay": "0.7.1",
66
- "@byokit/discover": "0.8.2",
63
+ "@byokit/accounts": "0.24.3",
64
+ "@byokit/pair": "0.10.2",
65
+ "@byokit/relay": "0.8.0",
66
+ "@byokit/discover": "0.8.3",
67
67
  "@byokit/seal": "0.3.0",
68
68
  "@byokit/secrets": "0.5.1",
69
69
  "@openclaw/gateway-client": "2026.8.35",
@@ -71,7 +71,7 @@
71
71
  "ws": "8.22.0"
72
72
  },
73
73
  "devDependencies": {
74
- "@byokit/ui": "0.8.0"
74
+ "@byokit/ui": "0.9.0"
75
75
  },
76
76
  "publishConfig": {
77
77
  "access": "public"
package/policy/policy.mjs CHANGED
@@ -6,11 +6,13 @@
6
6
  // BYOKIT_TRUSTED_SKILLS path to the trusted-skills JSON file (Crewhouse's trusted-skills.json shape):
7
7
  // either a list [{ id|name, version, sha256|hash }] or a map { "<id>": { version, sha256 } }.
8
8
  // BYOKIT_OWN_ROOTS JSON array of own content roots; skills sourced under them are the app's own.
9
+ // BYOKIT_PLUGIN_PINS JSON array [{ spec: '<package>@<version>', integrity }] of the external plugins the kit
10
+ // installs (engine/plugins.json at the engine pin).
9
11
  // Rules: any request kind containing 'depend' is blocked everywhere (own roots included);
10
12
  // dependency installers (a skill installSpec carrying bins/formula/package/module/url/archive) are blocked;
11
13
  // skill installs are allowed only from own roots or with an exact id + version + SKILL.md sha256 trusted match
12
14
  // (the version is request.origin.version when present, else the SKILL.md frontmatter version);
13
- // plugin installs are allowed only in the @openclaw/ scope.
15
+ // plugin installs are allowed only from the npm registry at exactly a pinned spec, with the pinned integrity.
14
16
  import { createHash } from 'node:crypto';
15
17
  import { existsSync, readFileSync, statSync } from 'node:fs';
16
18
  import { relative, resolve } from 'node:path';
@@ -139,27 +141,54 @@ function decideSkill(request, trusted, roots) {
139
141
  return allow();
140
142
  }
141
143
 
144
+ function pins() {
145
+ try {
146
+ const list = JSON.parse(process.env.BYOKIT_PLUGIN_PINS ?? '[]');
147
+ return Array.isArray(list) ? list.filter((p) => isRecord(p) && typeof p.spec === 'string' && typeof p.integrity === 'string') : [];
148
+ } catch {
149
+ return [];
150
+ }
151
+ }
152
+
142
153
  function decidePlugin(request) {
143
154
  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
155
+ // N4: the names in `plugin` are self-declared (the candidate manifest's own `name`, or only its id for the
156
+ // dependency tree). Only engine proof of registry origin on the pinned engine counts: source.kind 'npm'
157
+ // (engine-derived from the install request kind) plus the operator-requested specifier. Anything else is a
153
158
  // local/archive/git/file candidate spoofing the name. Fail closed.
154
159
  const source = isRecord(request.source) ? request.source : undefined;
155
160
  const specifier =
156
161
  isRecord(request.request) && typeof request.request.requestedSpecifier === 'string'
157
162
  ? request.request.requestedSpecifier
158
163
  : 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
- ]);
164
+ const npm = source?.kind === 'npm' && specifier !== undefined;
165
+ const scope = npm ? specifier : [plugin.packageName, plugin.pluginId, request.targetName].find((v) => typeof v === 'string');
166
+ if (!scope || !scope.startsWith('@openclaw/')) {
167
+ return block(`only the @openclaw/ plugin scope is allowed, not ${scope ?? request.targetName}`);
168
+ }
169
+ if (!npm) {
170
+ return block(`@openclaw/ plugin install is not from the npm registry: ${specifier ?? request.targetName}`, [
171
+ { ruleId: 'plugin-source', severity: 'critical', message: 'self-declared @openclaw/ name without registry source proof' },
172
+ ]);
173
+ }
174
+ // B7: exactly a pinned spec (`<package>@<engine version>`), never a range, tag or latest.
175
+ const pin = pins().find((p) => p.spec === specifier);
176
+ if (!pin) return block(`not a pinned plugin version: ${specifier}`);
177
+ // The preflight carries the registry's resolution as a file; the engine then checks the installed package
178
+ // against that resolution's integrity, so a matching pin here holds for the bytes installed.
179
+ if (request.sourcePathKind === 'file') {
180
+ let resolution;
181
+ try {
182
+ resolution = JSON.parse(readFileSync(request.sourcePath, 'utf8')).resolution;
183
+ } catch {
184
+ return block(`pinned plugin integrity unreadable: ${specifier}`);
185
+ }
186
+ if (!isRecord(resolution) || `${resolution.name}@${resolution.version}` !== pin.spec) {
187
+ return block(`not a pinned plugin version: ${specifier}`);
188
+ }
189
+ if (resolution.integrity !== pin.integrity) return block(`pinned plugin integrity mismatch: ${specifier}`);
190
+ }
191
+ return allow();
163
192
  }
164
193
 
165
194
  function decide(request) {