@sellable/install 0.1.578 → 0.1.581
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/container/Dockerfile +7 -7
- package/container/README.md +2 -2
- package/lib/sellable-agent/external-runtime-builder.mjs +1 -1
- package/lib/sellable-agent/fly-admin-image/Dockerfile +13 -0
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +97 -1
- package/lib/sellable-agent/fly-customer-image/Dockerfile +13 -4
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +29 -4
- package/lib/sellable-agent/fly-skills-bridge-exec.mjs +45 -0
- package/lib/sellable-agent/fly-skills-bridge.mjs +977 -0
- package/lib/sellable-agent/hermes-bridge.mjs +154 -19
- package/lib/sellable-agent/host-bootstrap.mjs +2 -2
- package/lib/sellable-agent/host-worker.mjs +12 -5
- package/lib/sellable-agent/profile-materializer.mjs +169 -14
- package/lib/sellable-agent/provisioning-adapter.mjs +5 -8
- package/package.json +1 -1
|
@@ -0,0 +1,977 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
4
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
5
|
+
import {
|
|
6
|
+
closeSync,
|
|
7
|
+
constants,
|
|
8
|
+
existsSync,
|
|
9
|
+
fstatSync,
|
|
10
|
+
lstatSync,
|
|
11
|
+
mkdirSync,
|
|
12
|
+
openSync,
|
|
13
|
+
readFileSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
realpathSync,
|
|
16
|
+
renameSync,
|
|
17
|
+
rmSync,
|
|
18
|
+
writeFileSync,
|
|
19
|
+
} from "node:fs";
|
|
20
|
+
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
21
|
+
|
|
22
|
+
import {
|
|
23
|
+
SOUL_BRIDGE_DATA_ROOT,
|
|
24
|
+
discoverLiveHermesGateway,
|
|
25
|
+
revalidateLiveHermesGateway,
|
|
26
|
+
soulProfilePaths,
|
|
27
|
+
withProfileSoulLock,
|
|
28
|
+
} from "./fly-soul-bridge.mjs";
|
|
29
|
+
import { markHermesSnapshotDirty } from "./hermes-memory-snapshot.mjs";
|
|
30
|
+
|
|
31
|
+
export const SKILLS_BRIDGE_SCHEMA_VERSION = "sellable-agent-skills-bridge/v1";
|
|
32
|
+
export const SKILLS_PROOF_SCHEMA_VERSION = "sellable-agent-skills-proof/v1";
|
|
33
|
+
export const SKILLS_MAX_COUNT = 256;
|
|
34
|
+
export const SKILL_MAX_FILES = 256;
|
|
35
|
+
export const SKILL_MAX_TOTAL_BYTES = 2 * 1024 * 1024;
|
|
36
|
+
export const SKILL_MAX_TEXT_BYTES = 256 * 1024;
|
|
37
|
+
export const SKILLS_REQUEST_MAX_BYTES = 512 * 1024;
|
|
38
|
+
|
|
39
|
+
const PROFILE_ID = /^[a-z0-9][a-z0-9_-]{0,127}$/;
|
|
40
|
+
const SHA256 = /^[a-f0-9]{64}$/;
|
|
41
|
+
const SAFE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
42
|
+
const PYTHON = "/opt/hermes/.venv/bin/python3";
|
|
43
|
+
const SUPPORT_ROOTS = new Set(["references", "templates"]);
|
|
44
|
+
const SOURCE_MANAGED = new Set(["bundled", "hub", "plugin", "org", "external"]);
|
|
45
|
+
|
|
46
|
+
const sha256 = (value) => createHash("sha256").update(value).digest("hex");
|
|
47
|
+
const stableJson = (value) => {
|
|
48
|
+
if (Array.isArray(value)) return `[${value.map(stableJson).join(",")}]`;
|
|
49
|
+
if (value && typeof value === "object") {
|
|
50
|
+
return `{${Object.keys(value)
|
|
51
|
+
.sort()
|
|
52
|
+
.map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`)
|
|
53
|
+
.join(",")}}`;
|
|
54
|
+
}
|
|
55
|
+
return JSON.stringify(value);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
export class FlySkillsBridgeError extends Error {
|
|
59
|
+
constructor(code) {
|
|
60
|
+
super(code);
|
|
61
|
+
this.name = "FlySkillsBridgeError";
|
|
62
|
+
this.code = code;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function fail(code) {
|
|
67
|
+
throw new FlySkillsBridgeError(code);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function validProfileId(value) {
|
|
71
|
+
if (typeof value !== "string" || !PROFILE_ID.test(value)) {
|
|
72
|
+
fail("skills_profile_id_rejected");
|
|
73
|
+
}
|
|
74
|
+
return value;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function utf8(bytes, code = "skills_utf8_rejected") {
|
|
78
|
+
try {
|
|
79
|
+
return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
|
|
80
|
+
} catch {
|
|
81
|
+
fail(code);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function safeRelativePath(value, { supporting = false } = {}) {
|
|
86
|
+
if (
|
|
87
|
+
typeof value !== "string" ||
|
|
88
|
+
value.length < 1 ||
|
|
89
|
+
value.length > 256 ||
|
|
90
|
+
value.includes("\\") ||
|
|
91
|
+
value.includes("\0") ||
|
|
92
|
+
isAbsolute(value)
|
|
93
|
+
) {
|
|
94
|
+
fail("skills_file_path_rejected");
|
|
95
|
+
}
|
|
96
|
+
const parts = value.split("/");
|
|
97
|
+
if (parts.some((part) => !part || part === "." || part === "..")) {
|
|
98
|
+
fail("skills_file_path_rejected");
|
|
99
|
+
}
|
|
100
|
+
if (supporting && (!SUPPORT_ROOTS.has(parts[0]) || parts.length < 2)) {
|
|
101
|
+
fail("skills_support_path_rejected");
|
|
102
|
+
}
|
|
103
|
+
return parts.join("/");
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function regularFile(path, maxBytes = SKILL_MAX_TOTAL_BYTES) {
|
|
107
|
+
const link = lstatSync(path);
|
|
108
|
+
if (
|
|
109
|
+
link.isSymbolicLink() ||
|
|
110
|
+
!link.isFile() ||
|
|
111
|
+
link.nlink !== 1 ||
|
|
112
|
+
link.size > maxBytes
|
|
113
|
+
) {
|
|
114
|
+
fail("skills_file_rejected");
|
|
115
|
+
}
|
|
116
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
117
|
+
try {
|
|
118
|
+
const opened = fstatSync(fd);
|
|
119
|
+
if (
|
|
120
|
+
!opened.isFile() ||
|
|
121
|
+
opened.dev !== link.dev ||
|
|
122
|
+
opened.ino !== link.ino ||
|
|
123
|
+
opened.nlink !== 1 ||
|
|
124
|
+
opened.size !== link.size
|
|
125
|
+
) {
|
|
126
|
+
fail("skills_file_changed");
|
|
127
|
+
}
|
|
128
|
+
return readFileSync(fd);
|
|
129
|
+
} finally {
|
|
130
|
+
closeSync(fd);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function contained(root, candidate) {
|
|
135
|
+
const rel = relative(root, candidate);
|
|
136
|
+
return (
|
|
137
|
+
rel !== "" &&
|
|
138
|
+
rel !== ".." &&
|
|
139
|
+
!rel.startsWith(`..${sep}`) &&
|
|
140
|
+
!isAbsolute(rel)
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function inspectTree(skillRoot, limits) {
|
|
145
|
+
const entries = [];
|
|
146
|
+
let totalBytes = 0;
|
|
147
|
+
let incompleteReason = null;
|
|
148
|
+
const walk = (directory) => {
|
|
149
|
+
for (const name of readdirSync(directory).sort()) {
|
|
150
|
+
const path = join(directory, name);
|
|
151
|
+
const link = lstatSync(path);
|
|
152
|
+
if (link.isSymbolicLink()) {
|
|
153
|
+
incompleteReason ??= "symlink";
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
if (link.isDirectory()) {
|
|
157
|
+
walk(path);
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (!link.isFile() || link.nlink !== 1) {
|
|
161
|
+
incompleteReason ??= "non_regular";
|
|
162
|
+
continue;
|
|
163
|
+
}
|
|
164
|
+
if (entries.length >= limits.maxFiles) {
|
|
165
|
+
incompleteReason ??= "file_count";
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
if (totalBytes + link.size > limits.maxTotalBytes) {
|
|
169
|
+
incompleteReason ??= "total_bytes";
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
const bytes = regularFile(path, limits.maxTotalBytes);
|
|
173
|
+
totalBytes += bytes.byteLength;
|
|
174
|
+
entries.push({
|
|
175
|
+
relativePath: relative(skillRoot, path).split(sep).join("/"),
|
|
176
|
+
byteLength: bytes.byteLength,
|
|
177
|
+
fileSha256: sha256(bytes),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
walk(skillRoot);
|
|
182
|
+
return { entries, totalBytes, incompleteReason };
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function normalizedProvenance(item, editable) {
|
|
186
|
+
const upstream = String(
|
|
187
|
+
item.provenance ?? item.source ?? "unknown"
|
|
188
|
+
).toLowerCase();
|
|
189
|
+
if (!editable) {
|
|
190
|
+
if (String(item.name).toLowerCase() === "sellable") return "sellable";
|
|
191
|
+
return SOURCE_MANAGED.has(upstream) ? upstream : "external";
|
|
192
|
+
}
|
|
193
|
+
return "profile";
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function skillId(item, provenance, relativeDirectory) {
|
|
197
|
+
return sha256(
|
|
198
|
+
stableJson({
|
|
199
|
+
name: String(item.name),
|
|
200
|
+
provenance,
|
|
201
|
+
relativeDirectory,
|
|
202
|
+
})
|
|
203
|
+
);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function disabledBytes(native) {
|
|
207
|
+
if (typeof native.disabledConfig === "string") {
|
|
208
|
+
return Buffer.from(native.disabledConfig, "utf8");
|
|
209
|
+
}
|
|
210
|
+
return Buffer.from(
|
|
211
|
+
`${stableJson([...new Set(native.disabledNames ?? [])].sort())}\n`,
|
|
212
|
+
"utf8"
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function normalizeEnumeration(native, paths, limits = {}) {
|
|
217
|
+
const maxSkills = limits.maxSkills ?? SKILLS_MAX_COUNT;
|
|
218
|
+
const treeLimits = {
|
|
219
|
+
maxFiles: limits.maxFiles ?? SKILL_MAX_FILES,
|
|
220
|
+
maxTotalBytes: limits.maxTotalBytes ?? SKILL_MAX_TOTAL_BYTES,
|
|
221
|
+
};
|
|
222
|
+
if (
|
|
223
|
+
!native ||
|
|
224
|
+
!Array.isArray(native.skills) ||
|
|
225
|
+
!Array.isArray(native.disabledNames)
|
|
226
|
+
) {
|
|
227
|
+
fail("skills_native_enumeration_rejected");
|
|
228
|
+
}
|
|
229
|
+
const disabled = new Set(native.disabledNames.map(String));
|
|
230
|
+
const profileSkillsRoot = join(paths.profileRoot, "skills");
|
|
231
|
+
const canonicalProfileSkillsRoot = existsSync(profileSkillsRoot)
|
|
232
|
+
? realpathSync(profileSkillsRoot)
|
|
233
|
+
: profileSkillsRoot;
|
|
234
|
+
const skills = [];
|
|
235
|
+
let incompleteReason =
|
|
236
|
+
native.complete === false ? String(native.reason ?? "native") : null;
|
|
237
|
+
for (const item of native.skills.slice(0, maxSkills)) {
|
|
238
|
+
if (
|
|
239
|
+
!item ||
|
|
240
|
+
!SAFE_NAME.test(String(item.name ?? "")) ||
|
|
241
|
+
typeof item.path !== "string"
|
|
242
|
+
) {
|
|
243
|
+
incompleteReason ??= "native_entry";
|
|
244
|
+
continue;
|
|
245
|
+
}
|
|
246
|
+
let canonicalRoot;
|
|
247
|
+
try {
|
|
248
|
+
const link = lstatSync(item.path);
|
|
249
|
+
if (link.isSymbolicLink() || !link.isDirectory()) {
|
|
250
|
+
incompleteReason ??= "symlink";
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
canonicalRoot = realpathSync(item.path);
|
|
254
|
+
} catch {
|
|
255
|
+
incompleteReason ??= "missing";
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const relativeDirectory = contained(
|
|
259
|
+
canonicalProfileSkillsRoot,
|
|
260
|
+
canonicalRoot
|
|
261
|
+
)
|
|
262
|
+
? relative(canonicalProfileSkillsRoot, canonicalRoot).split(sep).join("/")
|
|
263
|
+
: null;
|
|
264
|
+
const upstream = String(
|
|
265
|
+
item.provenance ?? item.source ?? "unknown"
|
|
266
|
+
).toLowerCase();
|
|
267
|
+
const editable = Boolean(
|
|
268
|
+
relativeDirectory &&
|
|
269
|
+
!relativeDirectory.split("/").includes(".org") &&
|
|
270
|
+
!SOURCE_MANAGED.has(upstream) &&
|
|
271
|
+
String(item.name).toLowerCase() !== "sellable"
|
|
272
|
+
);
|
|
273
|
+
const provenance = normalizedProvenance(item, editable);
|
|
274
|
+
const tree = inspectTree(canonicalRoot, treeLimits);
|
|
275
|
+
incompleteReason ??= tree.incompleteReason;
|
|
276
|
+
const skillMd = tree.entries.find(
|
|
277
|
+
(entry) => entry.relativePath === "SKILL.md"
|
|
278
|
+
);
|
|
279
|
+
if (!skillMd) {
|
|
280
|
+
incompleteReason ??= "manifest_missing";
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
let manifestContent = null;
|
|
284
|
+
if (skillMd.byteLength <= SKILL_MAX_TEXT_BYTES) {
|
|
285
|
+
try {
|
|
286
|
+
manifestContent = utf8(
|
|
287
|
+
regularFile(join(canonicalRoot, "SKILL.md"), SKILL_MAX_TEXT_BYTES)
|
|
288
|
+
);
|
|
289
|
+
} catch {
|
|
290
|
+
incompleteReason ??= "manifest_utf8";
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
incompleteReason ??= "manifest_bytes";
|
|
294
|
+
}
|
|
295
|
+
const manifestIdentity = {
|
|
296
|
+
provenance,
|
|
297
|
+
relativeDirectory,
|
|
298
|
+
entries: tree.entries,
|
|
299
|
+
};
|
|
300
|
+
skills.push({
|
|
301
|
+
skillId: skillId(item, provenance, relativeDirectory),
|
|
302
|
+
name: String(item.name),
|
|
303
|
+
description: String(item.description ?? "").slice(0, 2000),
|
|
304
|
+
category:
|
|
305
|
+
item.category == null ? null : String(item.category).slice(0, 128),
|
|
306
|
+
provenance,
|
|
307
|
+
editable,
|
|
308
|
+
enabled: !disabled.has(String(item.name)),
|
|
309
|
+
usage:
|
|
310
|
+
Number.isSafeInteger(item.usage) && item.usage >= 0 ? item.usage : 0,
|
|
311
|
+
manifestContent,
|
|
312
|
+
fileSha256: skillMd.fileSha256,
|
|
313
|
+
skillManifestSha256: sha256(stableJson(manifestIdentity)),
|
|
314
|
+
manifest: tree.entries,
|
|
315
|
+
_canonicalRoot: canonicalRoot,
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
if (native.skills.length > maxSkills) incompleteReason ??= "skill_count";
|
|
319
|
+
skills.sort((left, right) => left.skillId.localeCompare(right.skillId));
|
|
320
|
+
const disabledConfigSha256 = sha256(disabledBytes(native));
|
|
321
|
+
const inventorySha256 = sha256(
|
|
322
|
+
stableJson(
|
|
323
|
+
skills.map(
|
|
324
|
+
({ _canonicalRoot: _private, manifestContent: _content, ...skill }) =>
|
|
325
|
+
skill
|
|
326
|
+
)
|
|
327
|
+
)
|
|
328
|
+
);
|
|
329
|
+
return {
|
|
330
|
+
complete: incompleteReason === null,
|
|
331
|
+
...(incompleteReason === null
|
|
332
|
+
? {}
|
|
333
|
+
: {
|
|
334
|
+
code: "inventory_incomplete",
|
|
335
|
+
truncation: {
|
|
336
|
+
reason: incompleteReason,
|
|
337
|
+
observedSkillCount: native.skills.length,
|
|
338
|
+
returnedSkillCount: skills.length,
|
|
339
|
+
},
|
|
340
|
+
}),
|
|
341
|
+
disabledConfigSha256,
|
|
342
|
+
inventorySha256,
|
|
343
|
+
skills,
|
|
344
|
+
};
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function publicInventory(inventory) {
|
|
348
|
+
return {
|
|
349
|
+
...inventory,
|
|
350
|
+
skills: inventory.skills.map(({ _canonicalRoot, ...skill }) => skill),
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
const NATIVE_SCRIPT = String.raw`
|
|
355
|
+
import json, os, sys
|
|
356
|
+
request = json.loads(sys.stdin.read())
|
|
357
|
+
from tools.skills_tool import _find_all_skills, skill_view
|
|
358
|
+
from tools.skill_manager_tool import _find_skill, _create_skill, _edit_skill, _delete_skill, _write_file, _remove_file
|
|
359
|
+
from hermes_cli.config import load_config
|
|
360
|
+
from hermes_cli.skills_config import get_disabled_skills, save_disabled_skills
|
|
361
|
+
|
|
362
|
+
def inventory(read_name=None, read_file=None):
|
|
363
|
+
rows = _find_all_skills(skip_disabled=True)
|
|
364
|
+
config = load_config()
|
|
365
|
+
disabled = sorted(get_disabled_skills(config))
|
|
366
|
+
result = []
|
|
367
|
+
try:
|
|
368
|
+
from tools.skill_usage import _read_bundled_manifest_names, _read_hub_installed_names, activity_count, load_usage
|
|
369
|
+
bundled, hub, usage = _read_bundled_manifest_names(), _read_hub_installed_names(), load_usage()
|
|
370
|
+
except Exception:
|
|
371
|
+
bundled, hub, usage = set(), set(), {}
|
|
372
|
+
for row in rows:
|
|
373
|
+
name = str(row.get("name") or "")
|
|
374
|
+
found = _find_skill(name)
|
|
375
|
+
if not found:
|
|
376
|
+
continue
|
|
377
|
+
provenance = "hub" if name in hub else "bundled" if name in bundled else "agent"
|
|
378
|
+
item = dict(row)
|
|
379
|
+
item.update({"path": str(found["path"]), "provenance": provenance, "usage": activity_count(usage.get(name, {})) if usage else 0})
|
|
380
|
+
result.append(item)
|
|
381
|
+
viewed = None
|
|
382
|
+
if read_name:
|
|
383
|
+
viewed = skill_view(read_name, file_path=read_file) if read_file else skill_view(read_name)
|
|
384
|
+
return {"complete": True, "skills": result, "disabledNames": disabled, "disabledConfig": json.dumps(disabled, separators=(",", ":")), "nativeView": viewed}
|
|
385
|
+
|
|
386
|
+
op = request.get("operation")
|
|
387
|
+
if op == "enumerate":
|
|
388
|
+
output = inventory(request.get("readName"), request.get("readFile"))
|
|
389
|
+
elif op == "read":
|
|
390
|
+
output = {"nativeView": skill_view(request["readName"], file_path=request.get("readFile")) if request.get("readFile") else skill_view(request["readName"])}
|
|
391
|
+
elif op == "scan":
|
|
392
|
+
from tools.skills_guard import scan_skill, should_allow_install
|
|
393
|
+
path = request["path"]
|
|
394
|
+
scan = scan_skill(path, source="agent-created")
|
|
395
|
+
allowed, reason = should_allow_install(scan)
|
|
396
|
+
output = {"ok": allowed is True, "reason": str(reason)}
|
|
397
|
+
elif op == "mutate":
|
|
398
|
+
action = request["action"]
|
|
399
|
+
name = request["name"]
|
|
400
|
+
if action == "create": output = _create_skill(name, request["content"], request.get("category"))
|
|
401
|
+
elif action == "edit": output = _edit_skill(name, request["content"])
|
|
402
|
+
elif action == "delete": output = _delete_skill(name, "")
|
|
403
|
+
elif action == "write_file": output = _write_file(name, request["filePath"], request["content"])
|
|
404
|
+
elif action == "remove_file": output = _remove_file(name, request["filePath"])
|
|
405
|
+
elif action == "toggle":
|
|
406
|
+
config = load_config(); disabled = get_disabled_skills(config)
|
|
407
|
+
if request["enabled"]: disabled.discard(name)
|
|
408
|
+
else: disabled.add(name)
|
|
409
|
+
save_disabled_skills(config, disabled)
|
|
410
|
+
output = {"success": True}
|
|
411
|
+
else: raise RuntimeError("operation")
|
|
412
|
+
else:
|
|
413
|
+
raise RuntimeError("operation")
|
|
414
|
+
sys.stdout.write(json.dumps(output, separators=(",", ":")))
|
|
415
|
+
`;
|
|
416
|
+
|
|
417
|
+
function runNative(payload, paths, { python = PYTHON } = {}) {
|
|
418
|
+
const child = spawnSync(python, ["-c", NATIVE_SCRIPT], {
|
|
419
|
+
input: JSON.stringify(payload),
|
|
420
|
+
encoding: "utf8",
|
|
421
|
+
timeout: 20_000,
|
|
422
|
+
maxBuffer: 4 * 1024 * 1024,
|
|
423
|
+
env: {
|
|
424
|
+
HOME: paths.dataRoot,
|
|
425
|
+
HERMES_HOME: paths.profileRoot,
|
|
426
|
+
HERMES_PROFILE: paths.profileId,
|
|
427
|
+
SELLABLE_HERMES_PROFILE_ID: paths.profileId,
|
|
428
|
+
SELLABLE_AGENT_RUNTIME: "1",
|
|
429
|
+
PYTHONDONTWRITEBYTECODE: "1",
|
|
430
|
+
PATH: "/opt/hermes/.venv/bin:/usr/local/bin:/usr/bin:/bin",
|
|
431
|
+
},
|
|
432
|
+
});
|
|
433
|
+
if (child.status !== 0 || child.signal || child.error)
|
|
434
|
+
fail("skills_native_call_failed");
|
|
435
|
+
try {
|
|
436
|
+
return JSON.parse(child.stdout);
|
|
437
|
+
} catch {
|
|
438
|
+
fail("skills_native_output_rejected");
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
function defaultAdapter(paths) {
|
|
443
|
+
return {
|
|
444
|
+
enumerate: ({ readName, readFile } = {}) =>
|
|
445
|
+
runNative({ operation: "enumerate", readName, readFile }, paths),
|
|
446
|
+
read: ({ readName, readFile } = {}) =>
|
|
447
|
+
runNative({ operation: "read", readName, readFile }, paths),
|
|
448
|
+
scan: (path) => runNative({ operation: "scan", path }, paths),
|
|
449
|
+
mutate: (mutation) =>
|
|
450
|
+
runNative({ operation: "mutate", ...mutation }, paths),
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function assertExactKeys(value, allowed) {
|
|
455
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
456
|
+
fail("skills_request_rejected");
|
|
457
|
+
if (Object.keys(value).some((key) => !allowed.has(key)))
|
|
458
|
+
fail("skills_request_rejected");
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function assertRequest(request) {
|
|
462
|
+
const common = ["operation"];
|
|
463
|
+
const shapes = {
|
|
464
|
+
list: common,
|
|
465
|
+
read: [...common, "skillId", "filePath"],
|
|
466
|
+
edit: [...common, "skillId", "content", "expectedFileSha256"],
|
|
467
|
+
toggle: [...common, "skillId", "enabled", "disabledConfigSha256"],
|
|
468
|
+
create: [
|
|
469
|
+
...common,
|
|
470
|
+
"name",
|
|
471
|
+
"content",
|
|
472
|
+
"category",
|
|
473
|
+
"expectedFileAbsent",
|
|
474
|
+
"inventorySha256",
|
|
475
|
+
],
|
|
476
|
+
delete: [...common, "skillId", "skillManifestSha256"],
|
|
477
|
+
write_file: [
|
|
478
|
+
...common,
|
|
479
|
+
"skillId",
|
|
480
|
+
"filePath",
|
|
481
|
+
"content",
|
|
482
|
+
"expectedFileSha256",
|
|
483
|
+
"expectedFileAbsent",
|
|
484
|
+
"skillManifestSha256",
|
|
485
|
+
],
|
|
486
|
+
remove_file: [
|
|
487
|
+
...common,
|
|
488
|
+
"skillId",
|
|
489
|
+
"filePath",
|
|
490
|
+
"expectedFileSha256",
|
|
491
|
+
"skillManifestSha256",
|
|
492
|
+
],
|
|
493
|
+
};
|
|
494
|
+
const keys = shapes[request?.operation];
|
|
495
|
+
if (!keys) fail("skills_operation_rejected");
|
|
496
|
+
assertExactKeys(request, new Set(keys));
|
|
497
|
+
if (
|
|
498
|
+
request.operation === "create" &&
|
|
499
|
+
(!SAFE_NAME.test(request.name ?? "") ||
|
|
500
|
+
(request.category != null && !SAFE_NAME.test(request.category)))
|
|
501
|
+
) {
|
|
502
|
+
fail("skills_category_rejected");
|
|
503
|
+
}
|
|
504
|
+
if (request.skillId !== undefined && !SHA256.test(request.skillId))
|
|
505
|
+
fail("skills_identity_rejected");
|
|
506
|
+
if (request.content !== undefined) {
|
|
507
|
+
if (
|
|
508
|
+
typeof request.content !== "string" ||
|
|
509
|
+
Buffer.byteLength(request.content) > SKILL_MAX_TEXT_BYTES
|
|
510
|
+
) {
|
|
511
|
+
fail("skills_content_rejected");
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
if (request.filePath !== undefined)
|
|
515
|
+
safeRelativePath(request.filePath, {
|
|
516
|
+
supporting: request.operation !== "read",
|
|
517
|
+
});
|
|
518
|
+
for (const field of [
|
|
519
|
+
"expectedFileSha256",
|
|
520
|
+
"disabledConfigSha256",
|
|
521
|
+
"inventorySha256",
|
|
522
|
+
"skillManifestSha256",
|
|
523
|
+
]) {
|
|
524
|
+
if (request[field] !== undefined && !SHA256.test(request[field]))
|
|
525
|
+
fail("skills_cas_rejected");
|
|
526
|
+
}
|
|
527
|
+
return request;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
function findSkill(inventory, id, { editable = false } = {}) {
|
|
531
|
+
const skill = inventory.skills.find((entry) => entry.skillId === id);
|
|
532
|
+
if (!skill) fail("skills_identity_not_found");
|
|
533
|
+
if (editable && (!inventory.complete || !skill.editable))
|
|
534
|
+
fail("skills_mutation_refused");
|
|
535
|
+
return skill;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function backupTree(skillRoot) {
|
|
539
|
+
const snapshot = [];
|
|
540
|
+
const walk = (directory) => {
|
|
541
|
+
for (const name of readdirSync(directory).sort()) {
|
|
542
|
+
const path = join(directory, name);
|
|
543
|
+
const stat = lstatSync(path);
|
|
544
|
+
if (stat.isSymbolicLink()) fail("skills_backup_rejected");
|
|
545
|
+
if (stat.isDirectory()) walk(path);
|
|
546
|
+
else if (stat.isFile())
|
|
547
|
+
snapshot.push({
|
|
548
|
+
relativePath: relative(skillRoot, path),
|
|
549
|
+
bytes: regularFile(path),
|
|
550
|
+
mode: stat.mode & 0o777,
|
|
551
|
+
});
|
|
552
|
+
else fail("skills_backup_rejected");
|
|
553
|
+
}
|
|
554
|
+
};
|
|
555
|
+
walk(skillRoot);
|
|
556
|
+
return snapshot;
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function restoreTree(skillRoot, snapshot) {
|
|
560
|
+
rmSync(skillRoot, { recursive: true, force: true });
|
|
561
|
+
mkdirSync(skillRoot, { recursive: true, mode: 0o700 });
|
|
562
|
+
for (const entry of snapshot) {
|
|
563
|
+
const path = join(skillRoot, entry.relativePath);
|
|
564
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
565
|
+
writeFileSync(path, entry.bytes, { mode: entry.mode });
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
function atomicProof(paths, inventory) {
|
|
570
|
+
const root = join(paths.profileRoot, ".sellable-agent");
|
|
571
|
+
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
572
|
+
const path = join(root, "skills-proof.json");
|
|
573
|
+
const temporary = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
574
|
+
const value = {
|
|
575
|
+
schemaVersion: SKILLS_PROOF_SCHEMA_VERSION,
|
|
576
|
+
inventorySha256: inventory.inventorySha256,
|
|
577
|
+
disabledConfigSha256: inventory.disabledConfigSha256,
|
|
578
|
+
};
|
|
579
|
+
writeFileSync(temporary, `${JSON.stringify(value)}\n`, { mode: 0o600 });
|
|
580
|
+
renameSync(temporary, path);
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
function executeLocked(request, paths, adapter, options) {
|
|
584
|
+
const beforeNative = adapter.enumerate();
|
|
585
|
+
const before = normalizeEnumeration(beforeNative, paths, options.limits);
|
|
586
|
+
if (request.operation === "list") {
|
|
587
|
+
atomicProof(paths, before);
|
|
588
|
+
return {
|
|
589
|
+
ok: true,
|
|
590
|
+
schemaVersion: SKILLS_BRIDGE_SCHEMA_VERSION,
|
|
591
|
+
status: "proven",
|
|
592
|
+
...publicInventory(before),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
if (request.operation === "read") {
|
|
596
|
+
const skill = findSkill(before, request.skillId);
|
|
597
|
+
const relativePath = request.filePath
|
|
598
|
+
? safeRelativePath(request.filePath)
|
|
599
|
+
: "SKILL.md";
|
|
600
|
+
const manifest = skill.manifest.find(
|
|
601
|
+
(entry) => entry.relativePath === relativePath
|
|
602
|
+
);
|
|
603
|
+
if (!manifest)
|
|
604
|
+
return {
|
|
605
|
+
ok: false,
|
|
606
|
+
code: before.complete
|
|
607
|
+
? "skills_file_not_found"
|
|
608
|
+
: "inventory_incomplete",
|
|
609
|
+
};
|
|
610
|
+
if (manifest.byteLength > SKILL_MAX_TEXT_BYTES)
|
|
611
|
+
return { ok: false, code: "skills_file_too_large" };
|
|
612
|
+
const content = utf8(
|
|
613
|
+
regularFile(
|
|
614
|
+
join(skill._canonicalRoot, relativePath),
|
|
615
|
+
SKILL_MAX_TEXT_BYTES
|
|
616
|
+
)
|
|
617
|
+
);
|
|
618
|
+
const proof = adapter.read({
|
|
619
|
+
readName: skill.name,
|
|
620
|
+
readFile: relativePath === "SKILL.md" ? undefined : relativePath,
|
|
621
|
+
});
|
|
622
|
+
if (typeof proof.nativeView !== "string")
|
|
623
|
+
fail("skills_native_readback_failed");
|
|
624
|
+
atomicProof(paths, before);
|
|
625
|
+
return {
|
|
626
|
+
ok: true,
|
|
627
|
+
schemaVersion: SKILLS_BRIDGE_SCHEMA_VERSION,
|
|
628
|
+
status: "proven",
|
|
629
|
+
skill: publicInventory({ ...before, skills: [skill] }).skills[0],
|
|
630
|
+
filePath: relativePath,
|
|
631
|
+
content,
|
|
632
|
+
fileSha256: sha256(Buffer.from(content, "utf8")),
|
|
633
|
+
inventorySha256: before.inventorySha256,
|
|
634
|
+
disabledConfigSha256: before.disabledConfigSha256,
|
|
635
|
+
};
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
if (!before.complete)
|
|
639
|
+
return {
|
|
640
|
+
ok: false,
|
|
641
|
+
code: "inventory_incomplete",
|
|
642
|
+
inventorySha256: before.inventorySha256,
|
|
643
|
+
};
|
|
644
|
+
let skill =
|
|
645
|
+
request.operation === "create"
|
|
646
|
+
? null
|
|
647
|
+
: findSkill(before, request.skillId, { editable: true });
|
|
648
|
+
if (request.operation === "create") {
|
|
649
|
+
if (
|
|
650
|
+
!SAFE_NAME.test(request.name ?? "") ||
|
|
651
|
+
request.expectedFileAbsent !== true ||
|
|
652
|
+
request.inventorySha256 !== before.inventorySha256
|
|
653
|
+
) {
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
code: "skills_conflict",
|
|
657
|
+
inventorySha256: before.inventorySha256,
|
|
658
|
+
};
|
|
659
|
+
}
|
|
660
|
+
if (before.skills.some((entry) => entry.name === request.name))
|
|
661
|
+
return { ok: false, code: "skills_absent_conflict" };
|
|
662
|
+
}
|
|
663
|
+
if (
|
|
664
|
+
request.operation === "edit" &&
|
|
665
|
+
request.expectedFileSha256 !== skill.fileSha256
|
|
666
|
+
)
|
|
667
|
+
return {
|
|
668
|
+
ok: false,
|
|
669
|
+
code: "skills_file_conflict",
|
|
670
|
+
fileSha256: skill.fileSha256,
|
|
671
|
+
};
|
|
672
|
+
if (
|
|
673
|
+
request.operation === "toggle" &&
|
|
674
|
+
request.disabledConfigSha256 !== before.disabledConfigSha256
|
|
675
|
+
)
|
|
676
|
+
return {
|
|
677
|
+
ok: false,
|
|
678
|
+
code: "skills_disabled_config_conflict",
|
|
679
|
+
disabledConfigSha256: before.disabledConfigSha256,
|
|
680
|
+
};
|
|
681
|
+
if (
|
|
682
|
+
["delete", "write_file", "remove_file"].includes(request.operation) &&
|
|
683
|
+
request.skillManifestSha256 !== skill.skillManifestSha256
|
|
684
|
+
)
|
|
685
|
+
return {
|
|
686
|
+
ok: false,
|
|
687
|
+
code: "skills_manifest_conflict",
|
|
688
|
+
skillManifestSha256: skill.skillManifestSha256,
|
|
689
|
+
};
|
|
690
|
+
let supportingExisting = null;
|
|
691
|
+
if (["write_file", "remove_file"].includes(request.operation)) {
|
|
692
|
+
const relativePath = safeRelativePath(request.filePath, {
|
|
693
|
+
supporting: true,
|
|
694
|
+
});
|
|
695
|
+
supportingExisting =
|
|
696
|
+
skill.manifest.find((entry) => entry.relativePath === relativePath) ??
|
|
697
|
+
null;
|
|
698
|
+
if (request.operation === "remove_file" || supportingExisting) {
|
|
699
|
+
if (
|
|
700
|
+
!supportingExisting ||
|
|
701
|
+
request.expectedFileSha256 !== supportingExisting.fileSha256
|
|
702
|
+
)
|
|
703
|
+
return {
|
|
704
|
+
ok: false,
|
|
705
|
+
code: "skills_file_conflict",
|
|
706
|
+
fileSha256: supportingExisting?.fileSha256 ?? null,
|
|
707
|
+
};
|
|
708
|
+
} else if (request.expectedFileAbsent !== true)
|
|
709
|
+
return { ok: false, code: "skills_absent_conflict" };
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const skillRoot =
|
|
713
|
+
skill?._canonicalRoot ??
|
|
714
|
+
join(
|
|
715
|
+
paths.profileRoot,
|
|
716
|
+
"skills",
|
|
717
|
+
request.category ? join(request.category, request.name) : request.name
|
|
718
|
+
);
|
|
719
|
+
const skillExisted = existsSync(skillRoot);
|
|
720
|
+
const treeBackup = skillExisted ? backupTree(skillRoot) : [];
|
|
721
|
+
const disabledPath = join(paths.profileRoot, "config.yaml");
|
|
722
|
+
const disabledBackup = existsSync(disabledPath)
|
|
723
|
+
? regularFile(disabledPath)
|
|
724
|
+
: null;
|
|
725
|
+
const proofPath = join(
|
|
726
|
+
paths.profileRoot,
|
|
727
|
+
".sellable-agent",
|
|
728
|
+
"skills-proof.json"
|
|
729
|
+
);
|
|
730
|
+
const proofBackup = existsSync(proofPath)
|
|
731
|
+
? regularFile(proofPath, 4096)
|
|
732
|
+
: null;
|
|
733
|
+
const rollback = () => {
|
|
734
|
+
if (skillExisted) restoreTree(skillRoot, treeBackup);
|
|
735
|
+
else rmSync(skillRoot, { recursive: true, force: true });
|
|
736
|
+
if (disabledBackup !== null)
|
|
737
|
+
writeFileSync(disabledPath, disabledBackup, { mode: 0o600 });
|
|
738
|
+
if (proofBackup !== null)
|
|
739
|
+
writeFileSync(proofPath, proofBackup, { mode: 0o600 });
|
|
740
|
+
else rmSync(proofPath, { force: true });
|
|
741
|
+
};
|
|
742
|
+
try {
|
|
743
|
+
const scanTarget = skillRoot;
|
|
744
|
+
if (skillExisted) {
|
|
745
|
+
const scan = adapter.scan(scanTarget);
|
|
746
|
+
if (scan?.ok !== true) fail("skills_scan_refused");
|
|
747
|
+
}
|
|
748
|
+
const noEffect =
|
|
749
|
+
(request.operation === "edit" &&
|
|
750
|
+
sha256(Buffer.from(request.content, "utf8")) === skill.fileSha256) ||
|
|
751
|
+
(request.operation === "toggle" && request.enabled === skill.enabled) ||
|
|
752
|
+
(request.operation === "write_file" &&
|
|
753
|
+
supportingExisting !== null &&
|
|
754
|
+
sha256(Buffer.from(request.content, "utf8")) ===
|
|
755
|
+
supportingExisting.fileSha256);
|
|
756
|
+
if (noEffect) {
|
|
757
|
+
atomicProof(paths, before);
|
|
758
|
+
return {
|
|
759
|
+
ok: true,
|
|
760
|
+
schemaVersion: SKILLS_BRIDGE_SCHEMA_VERSION,
|
|
761
|
+
status: "noop",
|
|
762
|
+
operation: request.operation,
|
|
763
|
+
inventory: publicInventory(before),
|
|
764
|
+
detail: publicInventory({ ...before, skills: [skill] }).skills[0],
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
const mutation = {
|
|
768
|
+
action: request.operation,
|
|
769
|
+
name: skill?.name ?? request.name,
|
|
770
|
+
...(request.content !== undefined ? { content: request.content } : {}),
|
|
771
|
+
...(request.category !== undefined ? { category: request.category } : {}),
|
|
772
|
+
...(request.filePath !== undefined ? { filePath: request.filePath } : {}),
|
|
773
|
+
...(request.enabled !== undefined ? { enabled: request.enabled } : {}),
|
|
774
|
+
};
|
|
775
|
+
const result = adapter.mutate(mutation);
|
|
776
|
+
if (result?.success !== true)
|
|
777
|
+
return { ok: false, code: "skills_native_refused" };
|
|
778
|
+
if (existsSync(skillRoot)) {
|
|
779
|
+
const scan = adapter.scan(skillRoot);
|
|
780
|
+
if (scan?.ok !== true) fail("skills_scan_refused");
|
|
781
|
+
}
|
|
782
|
+
const afterNative = adapter.enumerate({
|
|
783
|
+
readName: request.operation === "delete" ? undefined : mutation.name,
|
|
784
|
+
});
|
|
785
|
+
const after = normalizeEnumeration(afterNative, paths, options.limits);
|
|
786
|
+
if (
|
|
787
|
+
!after.complete ||
|
|
788
|
+
(request.operation !== "delete" &&
|
|
789
|
+
typeof afterNative.nativeView !== "string")
|
|
790
|
+
)
|
|
791
|
+
fail("skills_readback_failed");
|
|
792
|
+
const afterSkill =
|
|
793
|
+
after.skills.find((entry) => entry.name === mutation.name) ?? null;
|
|
794
|
+
if (
|
|
795
|
+
request.operation === "delete" ? afterSkill !== null : afterSkill === null
|
|
796
|
+
)
|
|
797
|
+
fail("skills_readback_failed");
|
|
798
|
+
if (
|
|
799
|
+
(request.operation === "edit" &&
|
|
800
|
+
afterSkill.fileSha256 !==
|
|
801
|
+
sha256(Buffer.from(request.content, "utf8"))) ||
|
|
802
|
+
(request.operation === "toggle" &&
|
|
803
|
+
(afterSkill.enabled !== request.enabled ||
|
|
804
|
+
after.disabledConfigSha256 === before.disabledConfigSha256)) ||
|
|
805
|
+
(request.operation === "create" &&
|
|
806
|
+
afterSkill.fileSha256 !==
|
|
807
|
+
sha256(Buffer.from(request.content, "utf8"))) ||
|
|
808
|
+
(request.operation === "write_file" &&
|
|
809
|
+
!afterSkill.manifest.some(
|
|
810
|
+
(entry) =>
|
|
811
|
+
entry.relativePath === request.filePath &&
|
|
812
|
+
entry.fileSha256 === sha256(Buffer.from(request.content, "utf8"))
|
|
813
|
+
)) ||
|
|
814
|
+
(request.operation === "remove_file" &&
|
|
815
|
+
afterSkill.manifest.some(
|
|
816
|
+
(entry) => entry.relativePath === request.filePath
|
|
817
|
+
))
|
|
818
|
+
) {
|
|
819
|
+
fail("skills_readback_failed");
|
|
820
|
+
}
|
|
821
|
+
atomicProof(paths, after);
|
|
822
|
+
options.markDirty(paths.profileRoot);
|
|
823
|
+
return {
|
|
824
|
+
ok: true,
|
|
825
|
+
schemaVersion: SKILLS_BRIDGE_SCHEMA_VERSION,
|
|
826
|
+
status: "applied",
|
|
827
|
+
operation: request.operation,
|
|
828
|
+
effectId: randomUUID(),
|
|
829
|
+
inventory: publicInventory(after),
|
|
830
|
+
detail: afterSkill
|
|
831
|
+
? publicInventory({ ...after, skills: [afterSkill] }).skills[0]
|
|
832
|
+
: null,
|
|
833
|
+
};
|
|
834
|
+
} catch (error) {
|
|
835
|
+
rollback();
|
|
836
|
+
try {
|
|
837
|
+
adapter.enumerate();
|
|
838
|
+
} catch {
|
|
839
|
+
return { ok: false, code: "skills_rollback_unproven" };
|
|
840
|
+
}
|
|
841
|
+
return {
|
|
842
|
+
ok: false,
|
|
843
|
+
code: "skills_apply_failed_rolled_back",
|
|
844
|
+
reason:
|
|
845
|
+
error instanceof FlySkillsBridgeError
|
|
846
|
+
? error.code
|
|
847
|
+
: "skills_bridge_failed",
|
|
848
|
+
};
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/**
|
|
853
|
+
* @param {any} request
|
|
854
|
+
* @param {{
|
|
855
|
+
* profileId?: string,
|
|
856
|
+
* dataRoot?: string,
|
|
857
|
+
* nativeAdapter?: {enumerate: Function, read: Function, scan: Function, mutate: Function} | null,
|
|
858
|
+
* markDirty?: (profileRoot: string) => void,
|
|
859
|
+
* lockHeld?: boolean,
|
|
860
|
+
* limits?: any,
|
|
861
|
+
* }} [options]
|
|
862
|
+
*/
|
|
863
|
+
export function executeSkillsBridgeOperation(
|
|
864
|
+
request,
|
|
865
|
+
{
|
|
866
|
+
profileId = process.env.SELLABLE_HERMES_PROFILE_ID,
|
|
867
|
+
dataRoot = SOUL_BRIDGE_DATA_ROOT,
|
|
868
|
+
nativeAdapter = null,
|
|
869
|
+
markDirty = markHermesSnapshotDirty,
|
|
870
|
+
lockHeld = false,
|
|
871
|
+
limits = {},
|
|
872
|
+
} = {}
|
|
873
|
+
) {
|
|
874
|
+
assertRequest(request);
|
|
875
|
+
const id = validProfileId(profileId);
|
|
876
|
+
const paths = soulProfilePaths(id, { dataRoot });
|
|
877
|
+
const adapter = nativeAdapter ?? defaultAdapter(paths);
|
|
878
|
+
if (
|
|
879
|
+
!adapter ||
|
|
880
|
+
!["enumerate", "read", "scan", "mutate"].every(
|
|
881
|
+
(name) => typeof adapter[name] === "function"
|
|
882
|
+
)
|
|
883
|
+
)
|
|
884
|
+
fail("skills_native_adapter_rejected");
|
|
885
|
+
if (typeof markDirty !== "function") fail("skills_dirty_marker_rejected");
|
|
886
|
+
const run = () =>
|
|
887
|
+
executeLocked(request, paths, adapter, { markDirty, limits });
|
|
888
|
+
return lockHeld ? run() : withProfileSoulLock(id, run, { dataRoot });
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* @param {any} request
|
|
893
|
+
* @param {{
|
|
894
|
+
* procRoot?: string,
|
|
895
|
+
* dataRoot?: string,
|
|
896
|
+
* nativeAdapter?: {enumerate: Function, read: Function, scan: Function, mutate: Function} | null,
|
|
897
|
+
* markDirty?: (profileRoot: string) => void,
|
|
898
|
+
* callerProfileId?: string,
|
|
899
|
+
* limits?: any,
|
|
900
|
+
* }} [options]
|
|
901
|
+
*/
|
|
902
|
+
export function executeExternalSkillsBridgeOperation(
|
|
903
|
+
request,
|
|
904
|
+
{
|
|
905
|
+
procRoot = "/proc",
|
|
906
|
+
dataRoot = SOUL_BRIDGE_DATA_ROOT,
|
|
907
|
+
nativeAdapter = null,
|
|
908
|
+
markDirty = markHermesSnapshotDirty,
|
|
909
|
+
callerProfileId = process.env.SELLABLE_HERMES_PROFILE_ID,
|
|
910
|
+
limits = {},
|
|
911
|
+
} = {}
|
|
912
|
+
) {
|
|
913
|
+
const gateway = discoverLiveHermesGateway({ procRoot, dataRoot });
|
|
914
|
+
if (callerProfileId && callerProfileId !== gateway.profileId)
|
|
915
|
+
fail("skills_gateway_caller_profile_mismatch");
|
|
916
|
+
revalidateLiveHermesGateway(gateway, { procRoot, dataRoot });
|
|
917
|
+
const result = executeSkillsBridgeOperation(request, {
|
|
918
|
+
profileId: gateway.profileId,
|
|
919
|
+
dataRoot,
|
|
920
|
+
nativeAdapter,
|
|
921
|
+
markDirty,
|
|
922
|
+
limits,
|
|
923
|
+
});
|
|
924
|
+
revalidateLiveHermesGateway(gateway, { procRoot, dataRoot });
|
|
925
|
+
return Object.freeze({
|
|
926
|
+
...result,
|
|
927
|
+
profileId: gateway.profileId,
|
|
928
|
+
gatewayPid: gateway.pid,
|
|
929
|
+
gatewayStartTicks: gateway.startTicks,
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
|
|
933
|
+
export function computeSkillsConfigIdentity(profileRoot) {
|
|
934
|
+
if (!isAbsolute(profileRoot) || !existsSync(profileRoot))
|
|
935
|
+
fail("skills_profile_root_rejected");
|
|
936
|
+
const canonical = realpathSync(profileRoot);
|
|
937
|
+
const skillsRoot = join(canonical, "skills");
|
|
938
|
+
const tree = existsSync(skillsRoot)
|
|
939
|
+
? inspectTree(skillsRoot, {
|
|
940
|
+
maxFiles: 4096,
|
|
941
|
+
maxTotalBytes: 64 * 1024 * 1024,
|
|
942
|
+
})
|
|
943
|
+
: { entries: [], incompleteReason: null };
|
|
944
|
+
if (tree.incompleteReason) fail("skills_profile_identity_incomplete");
|
|
945
|
+
const configPath = join(canonical, "config.yaml");
|
|
946
|
+
const configSha256 = existsSync(configPath)
|
|
947
|
+
? sha256(regularFile(configPath, 2 * 1024 * 1024))
|
|
948
|
+
: sha256("");
|
|
949
|
+
return Object.freeze({
|
|
950
|
+
inventorySha256: sha256(stableJson(tree.entries)),
|
|
951
|
+
disabledConfigSha256: configSha256,
|
|
952
|
+
});
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
export function readSkillsProofObservation(profileRoot) {
|
|
956
|
+
try {
|
|
957
|
+
const path = join(
|
|
958
|
+
realpathSync(profileRoot),
|
|
959
|
+
".sellable-agent",
|
|
960
|
+
"skills-proof.json"
|
|
961
|
+
);
|
|
962
|
+
const value = JSON.parse(regularFile(path, 4096).toString("utf8"));
|
|
963
|
+
if (
|
|
964
|
+
value.schemaVersion !== SKILLS_PROOF_SCHEMA_VERSION ||
|
|
965
|
+
!SHA256.test(value.inventorySha256 ?? "") ||
|
|
966
|
+
!SHA256.test(value.disabledConfigSha256 ?? "") ||
|
|
967
|
+
Object.keys(value).length !== 3
|
|
968
|
+
)
|
|
969
|
+
return null;
|
|
970
|
+
return Object.freeze({
|
|
971
|
+
inventorySha256: value.inventorySha256,
|
|
972
|
+
disabledConfigSha256: value.disabledConfigSha256,
|
|
973
|
+
});
|
|
974
|
+
} catch {
|
|
975
|
+
return null;
|
|
976
|
+
}
|
|
977
|
+
}
|