@envarai/envarpay 0.2.0-alpha.2 → 0.2.0-alpha.20

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.
Files changed (85) hide show
  1. package/README.md +18 -56
  2. package/dist/commerce/agent-setup.d.ts +19 -0
  3. package/dist/commerce/agent-setup.js +210 -0
  4. package/dist/commerce/alerts.d.ts +27 -0
  5. package/dist/commerce/alerts.js +71 -0
  6. package/dist/commerce/card.js +3 -3
  7. package/dist/commerce/cli.js +91 -119
  8. package/dist/commerce/config.d.ts +3 -2
  9. package/dist/commerce/config.js +19 -77
  10. package/dist/commerce/configure.d.ts +2 -0
  11. package/dist/commerce/configure.js +78 -0
  12. package/dist/commerce/discovery.d.ts +20 -0
  13. package/dist/commerce/discovery.js +162 -0
  14. package/dist/commerce/envar-balance.d.ts +25 -0
  15. package/dist/commerce/envar-balance.js +65 -0
  16. package/dist/commerce/envar-runtime.d.ts +3 -1
  17. package/dist/commerce/envar-runtime.js +28 -1
  18. package/dist/commerce/envar.d.ts +13 -31
  19. package/dist/commerce/envar.js +35 -83
  20. package/dist/commerce/external-agent.d.ts +26 -0
  21. package/dist/commerce/external-agent.js +129 -0
  22. package/dist/commerce/external-events.d.ts +76 -0
  23. package/dist/commerce/external-events.js +273 -0
  24. package/dist/commerce/external-mcp.d.ts +10 -0
  25. package/dist/commerce/external-mcp.js +199 -0
  26. package/dist/commerce/external-oauth.d.ts +12 -0
  27. package/dist/commerce/external-oauth.js +230 -0
  28. package/dist/commerce/external-runtime.d.ts +25 -0
  29. package/dist/commerce/external-runtime.js +244 -0
  30. package/dist/commerce/index.d.ts +1 -0
  31. package/dist/commerce/index.js +1 -0
  32. package/dist/commerce/managed.d.ts +3 -0
  33. package/dist/commerce/managed.js +143 -0
  34. package/dist/commerce/native-inventory.d.ts +9 -0
  35. package/dist/commerce/native-inventory.js +75 -0
  36. package/dist/commerce/native-skills.d.ts +32 -0
  37. package/dist/commerce/native-skills.js +174 -0
  38. package/dist/commerce/openclaw-setup.d.ts +28 -0
  39. package/dist/commerce/openclaw-setup.js +43 -0
  40. package/dist/commerce/operations.d.ts +8 -3
  41. package/dist/commerce/operations.js +21 -18
  42. package/dist/commerce/pricing.d.ts +0 -1
  43. package/dist/commerce/pricing.js +3 -5
  44. package/dist/commerce/runtime.d.ts +4 -2
  45. package/dist/commerce/runtime.js +4 -2
  46. package/dist/commerce/server.d.ts +3 -2
  47. package/dist/commerce/server.js +26 -10
  48. package/dist/commerce/setup.d.ts +25 -0
  49. package/dist/commerce/setup.js +379 -0
  50. package/dist/commerce/skill-gate.d.ts +54 -0
  51. package/dist/commerce/skill-gate.js +153 -0
  52. package/dist/commerce/store.d.ts +11 -1
  53. package/dist/commerce/store.js +36 -2
  54. package/dist/commerce/types.d.ts +6 -57
  55. package/examples/envar-integration-policy.json +1 -9
  56. package/examples/hermes-setup-probe.py +109 -0
  57. package/examples/hermes-skill-inventory.py +108 -0
  58. package/examples/native-skill-runner.py +197 -0
  59. package/examples/seller.json +143 -26
  60. package/package.json +5 -16
  61. package/schemas/envarpay.config.schema.json +28 -87
  62. package/dist/commerce/buyer-store.d.ts +0 -101
  63. package/dist/commerce/buyer-store.js +0 -168
  64. package/dist/commerce/buyer.d.ts +0 -132
  65. package/dist/commerce/buyer.js +0 -755
  66. package/dist/commerce/chain.d.ts +0 -24
  67. package/dist/commerce/chain.js +0 -146
  68. package/dist/commerce/client-runtime.d.ts +0 -8
  69. package/dist/commerce/client-runtime.js +0 -7
  70. package/dist/commerce/management.d.ts +0 -14
  71. package/dist/commerce/management.js +0 -138
  72. package/dist/commerce/mpp-client.d.ts +0 -59
  73. package/dist/commerce/mpp-client.js +0 -24
  74. package/dist/commerce/mpp-runtime.d.ts +0 -3
  75. package/dist/commerce/mpp-runtime.js +0 -3
  76. package/dist/commerce/mpp.d.ts +0 -53
  77. package/dist/commerce/mpp.js +0 -163
  78. package/dist/commerce/peer-proxy.d.ts +0 -44
  79. package/dist/commerce/peer-proxy.js +0 -673
  80. package/dist/commerce/stripe-provider.d.ts +0 -9
  81. package/dist/commerce/stripe-provider.js +0 -97
  82. package/dist/commerce/x402.d.ts +0 -37
  83. package/dist/commerce/x402.js +0 -174
  84. package/examples/buyer-policy.json +0 -18
  85. package/schemas/buyer-policy.schema.json +0 -253
@@ -0,0 +1,244 @@
1
+ import { ExternalAgentOAuth } from "./external-oauth.js";
2
+ import { createServer } from "node:http";
3
+ import { Readable } from "node:stream";
4
+ import { join, dirname, resolve } from "node:path";
5
+ import { readFileSync, lstatSync, chmodSync, mkdirSync, writeFileSync, } from "node:fs";
6
+ import { randomBytes } from "node:crypto";
7
+ import { CommerceStore } from "./store.js";
8
+ import { CommerceServer, bearerAuthenticator } from "./server.js";
9
+ import { loadCommerceConfig } from "./config.js";
10
+ import { ExternalAgentExecutor } from "./external-agent.js";
11
+ import { ExternalAgentEvents, publicWebhook, } from "./external-events.js";
12
+ import { ExternalAgentMcp, externalAgentSkill } from "./external-mcp.js";
13
+ import { CredentialVault } from "./vault.js";
14
+ import { CommerceError } from "./types.js";
15
+ export function setupExternalAgent(directory, origin) {
16
+ directory = resolve(directory);
17
+ const u = new URL(origin);
18
+ if (u.protocol !== "https:" || u.origin !== origin)
19
+ throw new CommerceError("external_origin", "Provide the exact public HTTPS origin");
20
+ mkdirSync(directory, { recursive: true, mode: 0o700 });
21
+ const config = {
22
+ configVersion: 1,
23
+ agent: { id: "external-agent", name: "External Agent" },
24
+ paymentProfiles: {},
25
+ services: [
26
+ {
27
+ id: "text-answer",
28
+ name: "Bounded text answer",
29
+ revision: 1,
30
+ execution: { type: "a2a", cardUrl: origin + "/agent-card.json" },
31
+ contract: {
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ request: { type: "string", minLength: 1, maxLength: 2000 },
36
+ },
37
+ required: ["request"],
38
+ additionalProperties: false,
39
+ },
40
+ deliverables: ["external-agent-result.txt"],
41
+ targetDurationSeconds: 600,
42
+ includedRevisions: 0,
43
+ },
44
+ offers: [
45
+ {
46
+ id: "free",
47
+ pricing: { kind: "free" },
48
+ collection: { kind: "none" },
49
+ },
50
+ ],
51
+ },
52
+ ],
53
+ };
54
+ loadCommerceConfig(config);
55
+ const agentToken = randomBytes(32).toString("base64url"), buyerToken = randomBytes(32).toString("base64url");
56
+ const credentials = {
57
+ principal: "external-agent",
58
+ services: ["text-answer"],
59
+ agentTokens: { [agentToken]: "external-agent" },
60
+ callers: { [buyerToken]: "owner" },
61
+ vaultKeyFile: join(directory, "events.key"),
62
+ };
63
+ for (const [name, value] of Object.entries({
64
+ "seller.json": JSON.stringify(config, null, 2),
65
+ "credentials.json": JSON.stringify(credentials, null, 2),
66
+ "events.key": randomBytes(32),
67
+ "agent.token": agentToken,
68
+ "pairing.key": randomBytes(32).toString("base64url"),
69
+ "buyer.token": buyerToken,
70
+ "SKILL.md": externalAgentSkill,
71
+ "connection.json": JSON.stringify({
72
+ url: origin + "/services/text-answer/v1/offers/free/agent-card.json",
73
+ authToken: buyerToken,
74
+ mcpUrl: origin + "/mcp",
75
+ }),
76
+ }))
77
+ writeFileSync(join(directory, name), value, { mode: 0o600, flag: "wx" });
78
+ return {
79
+ directory,
80
+ cardUrl: origin + "/services/text-answer/v1/offers/free/agent-card.json",
81
+ mcpUrl: origin + "/mcp",
82
+ credentialsFile: join(directory, "credentials.json"),
83
+ paymentsEnabled: false,
84
+ };
85
+ }
86
+ export function serveExternalAgent(options) {
87
+ const privateBytes = (path) => {
88
+ const s = lstatSync(path);
89
+ if (!s.isFile() || s.isSymbolicLink() || s.mode & 0o077 || s.size > 65536)
90
+ throw new CommerceError("external_private_file", "Use private regular config/credential files");
91
+ return readFileSync(path);
92
+ };
93
+ const privateJson = (path) => JSON.parse(privateBytes(path).toString("utf8"));
94
+ const config = loadCommerceConfig(privateJson(options.configFile)), secret = privateJson(options.credentialsFile);
95
+ if (config.services.some((s) => s.execution.type !== "a2a" ||
96
+ s.offers.some((o) => o.pricing.kind !== "free")) ||
97
+ Object.keys(config.paymentProfiles).length)
98
+ throw new CommerceError("external_free_only", "External setup currently supports explicit free task acceptance only");
99
+ if (Object.keys(secret.agentTokens).some((token) => Object.hasOwn(secret.callers, token)))
100
+ throw new CommerceError("external_token_scope", "External Agent tokens must differ from buyer credentials");
101
+ const store = new CommerceStore(options.stateFile), executor = new ExternalAgentExecutor(store, secret.principal, secret.services), vault = new CredentialVault(join(dirname(options.stateFile), "event-secrets"), privateBytes(secret.vaultKeyFile));
102
+ const pairingFile = join(dirname(options.credentialsFile), "pairing.key"), oauth = new ExternalAgentOAuth(options.stateFile + ".oauth.sqlite3", options.origin, secret.principal, privateBytes(pairingFile).toString("utf8").trim());
103
+ const agentAuth = bearerAuthenticator(secret.agentTokens);
104
+ const eventsPath = options.stateFile + ".events.sqlite3", events = new ExternalAgentEvents(eventsPath, executor, vault, (url, body, headers) => publicWebhook(url, body, headers, options.callbackLookup));
105
+ chmodSync(eventsPath, 0o600);
106
+ const mcp = new ExternalAgentMcp(executor, events, async (r) => oauth.authenticate(r) ?? (await agentAuth(r)));
107
+ const server = new CommerceServer({
108
+ config,
109
+ store,
110
+ origin: options.origin,
111
+ authenticate: bearerAuthenticator(secret.callers),
112
+ execute: executor.execute,
113
+ });
114
+ const expected = new URL(options.origin).host;
115
+ async function externalA2a(request) {
116
+ const path = new URL(request.url).pathname;
117
+ let normalized = request;
118
+ if (request.method === "POST" && path.endsWith("/a2a")) {
119
+ const rawText = await request.text();
120
+ if (Buffer.byteLength(rawText) > 65536)
121
+ return Response.json({ error: "request_too_large" }, { status: 413 });
122
+ let raw;
123
+ try {
124
+ raw = JSON.parse(rawText);
125
+ }
126
+ catch {
127
+ return Response.json({ error: "invalid_json" }, { status: 400 });
128
+ }
129
+ if (raw.method === "SendMessage") {
130
+ const message = raw.params?.message;
131
+ if (message?.parts?.length === 1 &&
132
+ typeof message.parts[0]?.text === "string" &&
133
+ Object.keys(message.parts[0]).every((k) => ["text", "mediaType"].includes(k))) {
134
+ const text = message.parts[0].text;
135
+ // This opt-in external service advertises a bounded plain-text request.
136
+ // A JSON-text request with exactly that schema is also accepted.
137
+ let input = { request: text };
138
+ try {
139
+ const parsed = JSON.parse(text);
140
+ if (parsed &&
141
+ !Array.isArray(parsed) &&
142
+ Object.keys(parsed).length === 1 &&
143
+ typeof parsed.request === "string")
144
+ input = parsed;
145
+ }
146
+ catch { }
147
+ message.parts = [{ data: input }];
148
+ }
149
+ raw.params.configuration = {
150
+ ...raw.params.configuration,
151
+ returnImmediately: true,
152
+ };
153
+ }
154
+ normalized = new Request(request.url, {
155
+ method: "POST",
156
+ headers: request.headers,
157
+ body: JSON.stringify(raw),
158
+ });
159
+ }
160
+ const response = await server.handle(normalized);
161
+ if (response.status !== 200)
162
+ return response;
163
+ const value = await response.json();
164
+ if (request.method === "GET" && path.endsWith("/agent-card.json")) {
165
+ value.defaultInputModes = ["application/json", "text/plain"];
166
+ }
167
+ // Internally the completed dispatch queue waits for an external claim.
168
+ // A2A buyers see WORKING and can poll until this original Task completes.
169
+ for (const task of [value.result?.task, value.result]) {
170
+ if (task?.status?.state === "TASK_STATE_INPUT_REQUIRED" &&
171
+ task.metadata?.externalAgent) {
172
+ task.status.state = "TASK_STATE_WORKING";
173
+ delete task.status.message;
174
+ }
175
+ }
176
+ return Response.json(value, {
177
+ status: response.status,
178
+ headers: response.headers,
179
+ });
180
+ }
181
+ const http = createServer(async (req, res) => {
182
+ try {
183
+ if (req.headers.host !== expected) {
184
+ res.writeHead(421);
185
+ res.end();
186
+ return;
187
+ }
188
+ const headers = new Headers();
189
+ for (const [k, v] of Object.entries(req.headers))
190
+ if (v !== undefined)
191
+ headers.set(k, Array.isArray(v) ? v.join(",") : v);
192
+ const request = new Request(new URL(req.url ?? "/", options.origin), {
193
+ method: req.method,
194
+ headers,
195
+ ...(!["GET", "HEAD"].includes(req.method ?? "GET")
196
+ ? { body: Readable.toWeb(req), duplex: "half" }
197
+ : {}),
198
+ });
199
+ let response = await oauth.handle(request);
200
+ if (!response)
201
+ response =
202
+ new URL(request.url).pathname === "/mcp"
203
+ ? await mcp.handle(request)
204
+ : await externalA2a(request);
205
+ if (response.status === 401 && new URL(request.url).pathname === "/mcp")
206
+ response = new Response(response.body, {
207
+ status: 401,
208
+ headers: {
209
+ ...Object.fromEntries(response.headers),
210
+ "WWW-Authenticate": oauth.challenge(),
211
+ },
212
+ });
213
+ res.writeHead(response.status, Object.fromEntries(response.headers));
214
+ if (response.body)
215
+ Readable.fromWeb(response.body).pipe(res);
216
+ else
217
+ res.end();
218
+ }
219
+ catch {
220
+ if (!res.headersSent)
221
+ res.writeHead(500);
222
+ res.end('{"error":"request_failed"}');
223
+ }
224
+ });
225
+ http.requestTimeout = 30000;
226
+ http.headersTimeout = 15000;
227
+ http.listen(options.port, options.host);
228
+ events.start();
229
+ return {
230
+ http,
231
+ store,
232
+ events,
233
+ server,
234
+ close: async () => {
235
+ oauth.close();
236
+ await events.close();
237
+ server.stop();
238
+ await new Promise((resolve) => http.close(() => resolve()));
239
+ while (server.isRunning)
240
+ await new Promise((r) => setTimeout(r, 10));
241
+ store.close();
242
+ },
243
+ };
244
+ }
@@ -2,3 +2,4 @@ export * from './types.js';
2
2
  export * from './config.js';
3
3
  export * from './pricing.js';
4
4
  export * from './card.js';
5
+ export * from './skill-gate.js';
@@ -2,3 +2,4 @@ export * from './types.js';
2
2
  export * from './config.js';
3
3
  export * from './pricing.js';
4
4
  export * from './card.js';
5
+ export * from './skill-gate.js';
@@ -0,0 +1,3 @@
1
+ export declare function managedProfile(root: string): string;
2
+ export declare function activateManagedProfile(root: string, agentId: string, profile: string): void;
3
+ export declare function serveManaged(root: string, origin: string, host: string, port: number): Promise<void>;
@@ -0,0 +1,143 @@
1
+ /** A persistent native-runtime host for the existing seller CLI. No wallet authority is added. */
2
+ import { chownSync, closeSync, existsSync, fsyncSync, linkSync, lstatSync, openSync, readFileSync, readdirSync, unlinkSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { spawn } from 'node:child_process';
6
+ import { once } from 'node:events';
7
+ import { DatabaseSync } from 'node:sqlite';
8
+ import { CommerceError } from './types.js';
9
+ const idPattern = /^[0-9a-f]{8}(?:-[0-9a-f]{4}){3}-[0-9a-f]{12}$/;
10
+ function rootDirectory(root) {
11
+ const path = resolve(root), stat = lstatSync(path);
12
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077))
13
+ throw new CommerceError('managed_directory', 'Use the existing private managed EnvarPay directory');
14
+ return path;
15
+ }
16
+ export function managedProfile(root) {
17
+ const base = rootDirectory(root), file = join(base, 'active-profile.json');
18
+ const profiles = join(base, 'profiles');
19
+ if (existsSync(profiles)) {
20
+ const stat = lstatSync(profiles);
21
+ if (!stat.isDirectory() || stat.isSymbolicLink() || (stat.mode & 0o077))
22
+ throw new CommerceError('managed_profile', 'Use a private profile directory without links');
23
+ }
24
+ if (!existsSync(file))
25
+ return base;
26
+ const stat = lstatSync(file);
27
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1024 || (stat.mode & 0o077))
28
+ throw new CommerceError('managed_profile', 'Invalid managed profile');
29
+ const value = JSON.parse(readFileSync(file, 'utf8'));
30
+ if (value.version !== 1 || !idPattern.test(value.agentId))
31
+ throw new CommerceError('managed_profile', 'Invalid managed Agent identity');
32
+ const path = join(base, 'profiles', value.agentId), p = lstatSync(path);
33
+ if (!p.isDirectory() || p.isSymbolicLink() || (p.mode & 0o077))
34
+ throw new CommerceError('managed_profile', 'Use the original private profile');
35
+ return path;
36
+ }
37
+ export function activateManagedProfile(root, agentId, profile) {
38
+ const base = rootDirectory(root);
39
+ if (!idPattern.test(agentId) || resolve(profile) !== join(base, 'profiles', agentId))
40
+ throw new CommerceError('managed_profile', 'Profile does not belong to this managed Agent');
41
+ const previous = managedProfile(base);
42
+ if (previous !== base && previous !== resolve(profile))
43
+ throw new CommerceError('managed_bound', 'This runtime is already bound to another Agent; preserve its ledger');
44
+ if (previous === base) {
45
+ const config = JSON.parse(readFileSync(join(base, 'seller.json'), 'utf8'));
46
+ if (config.services?.length || Object.keys(config.paymentProfiles ?? {}).length)
47
+ throw new CommerceError('managed_in_use', 'Existing seller configuration requires review');
48
+ const ledger = join(base, 'seller.sqlite3');
49
+ if (existsSync(ledger)) {
50
+ const db = new DatabaseSync(ledger, { readOnly: true });
51
+ try {
52
+ for (const table of ['commerce_orders', 'commerce_service_revisions']) {
53
+ if (db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(table) && Number(db.prepare(`SELECT count(*) AS n FROM ${table}`).get()?.n))
54
+ throw new CommerceError('managed_in_use', 'Keep the existing seller ledger and inspect its operations before connecting');
55
+ }
56
+ }
57
+ finally {
58
+ db.close();
59
+ }
60
+ }
61
+ }
62
+ const owner = lstatSync(base);
63
+ function ownership(path) {
64
+ const s = lstatSync(path);
65
+ if (s.isSymbolicLink() || (!s.isDirectory() && !s.isFile()))
66
+ throw new CommerceError('managed_profile', 'Managed profiles cannot contain links or special files');
67
+ if (process.getuid?.() === 0)
68
+ chownSync(path, owner.uid, owner.gid);
69
+ if (s.isDirectory())
70
+ for (const name of readdirSync(path))
71
+ ownership(join(path, name));
72
+ }
73
+ ownership(join(base, 'profiles'));
74
+ const connection = JSON.parse(readFileSync(join(profile, 'connection.json'), 'utf8'));
75
+ if (connection.agentId !== agentId)
76
+ throw new CommerceError('managed_profile', 'Connection belongs to another Agent');
77
+ if (previous === resolve(profile))
78
+ return;
79
+ const temp = join(base, `.active-${process.pid}.json`);
80
+ writeFileSync(temp, JSON.stringify({ version: 1, agentId }), { mode: 0o600, flag: 'wx', flush: true });
81
+ if (process.getuid?.() === 0)
82
+ chownSync(temp, owner.uid, owner.gid);
83
+ try {
84
+ // Atomic create prevents two setup processes from replacing one another's binding.
85
+ linkSync(temp, join(base, 'active-profile.json'));
86
+ }
87
+ catch (error) {
88
+ if (error.code !== 'EEXIST' || managedProfile(base) !== resolve(profile))
89
+ throw new CommerceError('managed_bound', 'This runtime was bound by another setup; preserve its original profile');
90
+ }
91
+ finally {
92
+ unlinkSync(temp);
93
+ }
94
+ const directory = openSync(base, 'r');
95
+ try {
96
+ fsyncSync(directory);
97
+ }
98
+ finally {
99
+ closeSync(directory);
100
+ }
101
+ }
102
+ export async function serveManaged(root, origin, host, port) {
103
+ const base = rootDirectory(root), cli = fileURLToPath(new URL('./cli.js', import.meta.url));
104
+ let child, stopping = false;
105
+ const stop = () => { stopping = true; child?.kill('SIGTERM'); };
106
+ process.once('SIGTERM', stop);
107
+ process.once('SIGINT', stop);
108
+ try {
109
+ while (!stopping) {
110
+ const profile = managedProfile(base);
111
+ const stamp = () => ['credentials.json', 'envar.json', 'envar.token'].map(name => { const p = join(profile, name); return existsSync(p) ? lstatSync(p).mtimeMs : 0; }).join(':');
112
+ const initialStamp = stamp();
113
+ const args = [cli, 'serve', '--config', join(profile, 'seller.json'), '--credentials', join(profile, 'credentials.json'), '--state', join(profile, 'seller.sqlite3'), '--origin', origin, '--host', host, '--port', String(port)];
114
+ if (profile !== base)
115
+ args.push('--envar-config', join(profile, 'envar.json'), '--envar-credentials', join(profile, 'envar.token'));
116
+ child = spawn(process.execPath, args, { stdio: 'inherit' });
117
+ let switching = false;
118
+ const timer = setInterval(() => {
119
+ try {
120
+ if (!switching && (managedProfile(base) !== profile || stamp() !== initialStamp)) {
121
+ switching = true;
122
+ child?.kill('SIGTERM');
123
+ }
124
+ }
125
+ catch {
126
+ stop();
127
+ }
128
+ }, 1000);
129
+ try {
130
+ const [code] = await once(child, 'exit');
131
+ if (!stopping && !switching)
132
+ throw new CommerceError('managed_runtime_exit', `Seller stopped (${code}); keep its profile and inspect its configuration`);
133
+ }
134
+ finally {
135
+ clearInterval(timer);
136
+ }
137
+ }
138
+ }
139
+ finally {
140
+ process.removeListener('SIGTERM', stop);
141
+ process.removeListener('SIGINT', stop);
142
+ }
143
+ }
@@ -0,0 +1,9 @@
1
+ export declare const isSkillInstallerMetadata: (path: string) => path is ".clawhub/origin.json" | ".openclaw/source-origin.json";
2
+ export declare function readSkillInventory(roots: string[]): {
3
+ name: string;
4
+ path: string;
5
+ description: string;
6
+ supported: boolean;
7
+ digest: string;
8
+ files: number;
9
+ }[];
@@ -0,0 +1,75 @@
1
+ import { existsSync, lstatSync, readFileSync, readdirSync } from 'node:fs';
2
+ import { homedir } from 'node:os';
3
+ import { extname, join, resolve } from 'node:path';
4
+ import { createHash } from 'node:crypto';
5
+ import { parse } from 'yaml';
6
+ const namePattern = /^[a-z][a-z0-9-]{0,63}$/;
7
+ // Native installer provenance is included in the source digest, but never
8
+ // copied into the executable service package or supplied to the model.
9
+ export const isSkillInstallerMetadata = (path) => path === '.clawhub/origin.json' || path === '.openclaw/source-origin.json';
10
+ export function readSkillInventory(roots) {
11
+ const found = [];
12
+ const seen = new Set();
13
+ function visit(folder, depth) {
14
+ if (depth > 10 || found.length >= 128 || !existsSync(folder))
15
+ return;
16
+ const stat = lstatSync(folder);
17
+ if (!stat.isDirectory() || stat.isSymbolicLink())
18
+ return;
19
+ const entry = join(folder, 'SKILL.md');
20
+ if (!existsSync(entry)) {
21
+ for (const child of readdirSync(folder).sort())
22
+ if (!child.startsWith('.'))
23
+ visit(join(folder, child), depth + 1);
24
+ return;
25
+ }
26
+ const info = lstatSync(entry);
27
+ if (!info.isFile() || info.isSymbolicLink() || info.size > 48000)
28
+ return;
29
+ const raw = readFileSync(entry, 'utf8'), header = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw)?.[1];
30
+ let meta;
31
+ try {
32
+ meta = parse(header ?? '') ?? {};
33
+ }
34
+ catch {
35
+ return;
36
+ }
37
+ if (typeof meta.name !== 'string' || !namePattern.test(meta.name) || seen.has(meta.name))
38
+ return;
39
+ const files = [];
40
+ let size = 0, supported = true;
41
+ function collect(path, prefix = '', level = 0) {
42
+ if (level > 10) {
43
+ supported = false;
44
+ return;
45
+ }
46
+ for (const name of readdirSync(path).sort()) {
47
+ const file = join(path, name), s = lstatSync(file);
48
+ if (s.isSymbolicLink()) {
49
+ supported = false;
50
+ continue;
51
+ }
52
+ if (s.isDirectory()) {
53
+ collect(file, prefix + name + '/', level + 1);
54
+ continue;
55
+ }
56
+ if (!s.isFile() || s.size > 512 * 1024 || (size += s.size) > 1024 * 1024 || files.length >= 128) {
57
+ supported = false;
58
+ continue;
59
+ }
60
+ if (!['.md', '.txt'].includes(extname(name).toLowerCase()) && name !== 'LICENSE' && !isSkillInstallerMetadata(prefix + name))
61
+ supported = false;
62
+ files.push([prefix + name, createHash('sha256').update(readFileSync(file)).digest('hex')]);
63
+ }
64
+ }
65
+ collect(folder);
66
+ files.sort((a, b) => a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
67
+ const canonical = JSON.stringify(files).replace(/[\u007f-\uffff]/g, c => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'));
68
+ seen.add(meta.name);
69
+ found.push({ name: meta.name, path: resolve(folder), description: String(meta.description ?? '').slice(0, 500), supported, digest: createHash('sha256').update(canonical).digest('hex'), files: files.length });
70
+ }
71
+ for (const root of roots.slice(0, 16))
72
+ if (typeof root === 'string')
73
+ visit(resolve(root.replace(/^~(?=\/|$)/, homedir())), 0);
74
+ return found;
75
+ }
@@ -0,0 +1,32 @@
1
+ import { type SkillDescriptor } from './skill-gate.js';
2
+ import type { ExecuteOrder } from './server.js';
3
+ export interface NativeSkillSettings {
4
+ framework: 'hermes' | 'openclaw';
5
+ skillsDirectory: string;
6
+ freeSkills: string[];
7
+ stateDirectory: string;
8
+ python: string;
9
+ command?: string[];
10
+ model: string;
11
+ baseUrl: string;
12
+ apiKeyFile: string;
13
+ /** Host-side Docker CLI, never a Docker socket mounted into the Agent. */
14
+ dockerContainer?: string;
15
+ dockerStateDirectory?: string;
16
+ discovery?: {
17
+ hermesHome: string;
18
+ enabledFromWeb: true;
19
+ roots?: string[];
20
+ };
21
+ }
22
+ interface InstalledSkill {
23
+ descriptor: SkillDescriptor;
24
+ instructions: string;
25
+ }
26
+ /** Hash every resource; reject symlinks, unbounded packages and executable skill requirements. */
27
+ export declare function readInstalledSkills(root: string, freeSkills: readonly string[], cardUrl: string): InstalledSkill[];
28
+ export declare function createNativeSkillExecutor(settings: NativeSkillSettings, origin: string, agentName: string): {
29
+ execute: ExecuteOrder;
30
+ close(): Promise<void>;
31
+ };
32
+ export {};