@sellable/install 0.1.358 → 0.1.359-phase111.20260720052618

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 (31) hide show
  1. package/bin/sellable-install.mjs +0 -2
  2. package/package.json +2 -11
  3. package/skill-templates/refill-sends-evergreen.md +5 -0
  4. package/skill-templates/refill-sends-v2.md +23 -1
  5. package/skill-templates/refill-sends.md +14 -1
  6. package/bin/sellable-agent-egress-proxy.mjs +0 -31
  7. package/bin/sellable-agent-host-bootstrap.mjs +0 -26
  8. package/bin/sellable-agent-host-worker.mjs +0 -9
  9. package/bin/sellable-agent-runtime-helper.mjs +0 -37
  10. package/bin/sellable-agent-runtime-launcher.mjs +0 -151
  11. package/bin/sellable-agent-runtime-probe.mjs +0 -163
  12. package/bin/sellable-agent-sandbox-init.mjs +0 -93
  13. package/container/Dockerfile +0 -37
  14. package/container/README.md +0 -15
  15. package/container/compose.yaml +0 -22
  16. package/container/entrypoint.sh +0 -13
  17. package/lib/sellable-agent/containment-contract.mjs +0 -472
  18. package/lib/sellable-agent/external-runtime-builder.mjs +0 -272
  19. package/lib/sellable-agent/hermes-bridge.mjs +0 -497
  20. package/lib/sellable-agent/host-bootstrap.mjs +0 -458
  21. package/lib/sellable-agent/host-worker.mjs +0 -2067
  22. package/lib/sellable-agent/profile-materializer.mjs +0 -1410
  23. package/lib/sellable-agent/provisioning-adapter.mjs +0 -992
  24. package/lib/sellable-agent/runtime-boundary.mjs +0 -635
  25. package/lib/sellable-agent/runtime-egress-proxy.mjs +0 -241
  26. package/lib/sellable-agent/runtime-helper.mjs +0 -1428
  27. package/lib/sellable-agent/runtime-reconciler.mjs +0 -683
  28. package/lib/sellable-agent/service-installer.mjs +0 -441
  29. package/services/s6/sellable-agent-egress-proxy/run +0 -15
  30. package/services/s6/sellable-agent-host-worker/run +0 -4
  31. package/services/s6/sellable-agent-runtime/run +0 -19
@@ -1,497 +0,0 @@
1
- import { createHash } from "node:crypto";
2
- import {
3
- closeSync,
4
- constants,
5
- existsSync,
6
- fchmodSync,
7
- fchownSync,
8
- fsyncSync,
9
- lstatSync,
10
- openSync,
11
- readFileSync,
12
- realpathSync,
13
- renameSync,
14
- unlinkSync,
15
- writeFileSync,
16
- } from "node:fs";
17
- import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
18
-
19
- export const HERMES_AGENT_BRIDGE_ID = "sellable-agent-hermes-bridge/v1";
20
- export const HERMES_AGENT_BRIDGE_MARKER = "SELLABLE_AGENT_HERMES_BRIDGE_V1";
21
-
22
- const sha256 = (value) => createHash("sha256").update(value).digest("hex");
23
- const isSha256 = (value) => typeof value === "string" && /^[a-f0-9]{64}$/.test(value);
24
-
25
- export class HermesAgentBridgeError extends Error {
26
- constructor(code, message, details = {}) {
27
- super(message);
28
- this.name = "HermesAgentBridgeError";
29
- this.code = code;
30
- this.details = details;
31
- }
32
- }
33
-
34
- function replaceExactlyOnce(source, needle, replacement, label) {
35
- const first = source.indexOf(needle);
36
- const last = source.lastIndexOf(needle);
37
- if (first < 0 || first !== last) {
38
- throw new HermesAgentBridgeError(
39
- "HERMES_BRIDGE_PATCH_ANCHOR_MISMATCH",
40
- `Expected exactly one ${label} patch anchor`,
41
- { label },
42
- );
43
- }
44
- return `${source.slice(0, first)}${replacement}${source.slice(first + needle.length)}`;
45
- }
46
-
47
- const SECRET_FD_HELPER = `
48
- # ${HERMES_AGENT_BRIDGE_MARKER}: inherited Slack secret reader.
49
- _SELLABLE_AGENT_SECRET_CACHE = {}
50
-
51
- def _sellable_agent_read_slack_secret(fd: int, env_name: str, prefix: str):
52
- if os.getenv("SELLABLE_AGENT_RUNTIME", "") != "1":
53
- return os.getenv(env_name)
54
- if env_name in os.environ:
55
- raise RuntimeError(f"{env_name} must not be present in the Sellable runtime environment")
56
- cache_key = (fd, env_name, prefix)
57
- if cache_key in _SELLABLE_AGENT_SECRET_CACHE:
58
- return _SELLABLE_AGENT_SECRET_CACHE[cache_key]
59
- try:
60
- duplicate = os.dup(fd)
61
- except OSError as exc:
62
- raise RuntimeError(f"required inherited descriptor {fd} is unavailable") from exc
63
- try:
64
- chunks = []
65
- size = 0
66
- while True:
67
- chunk = os.read(duplicate, min(1024, 4097 - size))
68
- if not chunk:
69
- break
70
- chunks.append(chunk)
71
- size += len(chunk)
72
- if size > 4096:
73
- raise RuntimeError(f"secret on descriptor {fd} exceeds the size limit")
74
- finally:
75
- os.close(duplicate)
76
- try:
77
- value = b"".join(chunks).decode("utf-8").strip()
78
- except UnicodeDecodeError as exc:
79
- raise RuntimeError(f"secret on descriptor {fd} is not valid UTF-8") from exc
80
- if not value or "\\x00" in value or "\\n" in value or "\\r" in value:
81
- raise RuntimeError(f"secret on descriptor {fd} is malformed")
82
- if not value.startswith(prefix):
83
- raise RuntimeError(f"secret on descriptor {fd} has the wrong credential class")
84
- _SELLABLE_AGENT_SECRET_CACHE[cache_key] = value
85
- return value
86
- `;
87
-
88
- const MCP_META_HELPER = `
89
- # ${HERMES_AGENT_BRIDGE_MARKER}: request-scoped Sellable MCP metadata.
90
- def _sellable_agent_call_meta():
91
- if os.getenv("SELLABLE_AGENT_RUNTIME", "") != "1":
92
- return None
93
- import json
94
- gate_path = os.getenv("SELLABLE_AGENT_RUNTIME_GATE_FILE", "")
95
- if not gate_path.startswith("/"):
96
- raise RuntimeError("Sellable runtime gate path is not pinned")
97
- try:
98
- with open(gate_path, "r", encoding="utf-8") as gate_file:
99
- gates = json.load(gate_file)
100
- except (OSError, ValueError) as exc:
101
- raise RuntimeError("Sellable runtime gates are unavailable") from exc
102
- if gates != {"lifecycle": "ON", "dispatch": "ENABLED", "route": "OPEN", "output": "OPEN"}:
103
- raise RuntimeError("Sellable runtime dispatch is fenced")
104
- from gateway.session_context import get_session_env, session_context_engaged
105
- if not session_context_engaged():
106
- raise RuntimeError("Sellable MCP calls require an active Hermes session context")
107
- legacy_names = (
108
- "HERMES_SESSION_PLATFORM",
109
- "HERMES_SESSION_CHAT_ID",
110
- "HERMES_SESSION_USER_ID",
111
- "HERMES_SESSION_MESSAGE_ID",
112
- )
113
- if any(os.getenv(name) for name in legacy_names):
114
- raise RuntimeError("Sellable MCP identity must not come from process-global session variables")
115
- platform = get_session_env("HERMES_SESSION_PLATFORM", "").strip().lower()
116
- channel_id = get_session_env("HERMES_SESSION_CHAT_ID", "").strip()
117
- requester_id = get_session_env("HERMES_SESSION_USER_ID", "").strip()
118
- message_id = get_session_env("HERMES_SESSION_MESSAGE_ID", "").strip()
119
- if platform != "slack" or not channel_id or not requester_id or not message_id:
120
- raise RuntimeError("Sellable MCP calls require a bound Slack requester, channel, and message")
121
- return {
122
- "sellableAgent": {
123
- "requesterId": requester_id,
124
- "channelId": channel_id,
125
- "providerRequestId": message_id,
126
- }
127
- }
128
- `;
129
-
130
- export function patchHermesSourceFile(patchKind, source) {
131
- if (typeof source !== "string") {
132
- throw new HermesAgentBridgeError("HERMES_BRIDGE_INVALID_SOURCE", "Hermes source must be UTF-8 text");
133
- }
134
- if (source.includes(HERMES_AGENT_BRIDGE_MARKER)) {
135
- throw new HermesAgentBridgeError(
136
- "HERMES_BRIDGE_ALREADY_PATCHED_SOURCE",
137
- "Refusing to transform source that already contains the bridge marker",
138
- { patchKind },
139
- );
140
- }
141
-
142
- if (patchKind === "gateway-config") {
143
- let patched = replaceExactlyOnce(
144
- source,
145
- "logger = logging.getLogger(__name__)\n",
146
- `logger = logging.getLogger(__name__)\n${SECRET_FD_HELPER}`,
147
- "gateway helper",
148
- );
149
- patched = replaceExactlyOnce(
150
- patched,
151
- ' slack_token = os.getenv("SLACK_BOT_TOKEN")',
152
- ' slack_token = _sellable_agent_read_slack_secret(9, "SLACK_BOT_TOKEN", "xoxb-")',
153
- "Slack bot token",
154
- );
155
- return patched;
156
- }
157
-
158
- if (patchKind === "slack-adapter") {
159
- let patched = replaceExactlyOnce(
160
- source,
161
- "logger = logging.getLogger(__name__)\n",
162
- `logger = logging.getLogger(__name__)\n${SECRET_FD_HELPER}`,
163
- "Slack adapter helper",
164
- );
165
- patched = replaceExactlyOnce(
166
- patched,
167
- ' app_token = os.getenv("SLACK_APP_TOKEN")',
168
- ' app_token = _sellable_agent_read_slack_secret(8, "SLACK_APP_TOKEN", "xapp-")',
169
- "Slack app token",
170
- );
171
- patched = replaceExactlyOnce(
172
- patched,
173
- " tokens_file = get_hermes_home() / \"slack_tokens.json\"\n if tokens_file.exists():",
174
- " tokens_file = get_hermes_home() / \"slack_tokens.json\"\n if tokens_file.exists() and os.getenv(\"SELLABLE_AGENT_RUNTIME\", \"\") != \"1\":",
175
- "legacy Slack token file",
176
- );
177
- return patched;
178
- }
179
-
180
- if (patchKind === "mcp-tool") {
181
- let patched = replaceExactlyOnce(
182
- source,
183
- "logger = logging.getLogger(__name__)\n",
184
- `logger = logging.getLogger(__name__)\n${MCP_META_HELPER}`,
185
- "MCP metadata helper",
186
- );
187
- patched = replaceExactlyOnce(
188
- patched,
189
- " result = await server.session.call_tool(tool_name, arguments=args)",
190
- [
191
- " sellable_meta = _sellable_agent_call_meta()",
192
- " call_kwargs = {\"arguments\": args}",
193
- " if sellable_meta is not None:",
194
- " call_kwargs[\"meta\"] = sellable_meta",
195
- " result = await server.session.call_tool(tool_name, **call_kwargs)",
196
- ].join("\n"),
197
- "MCP call metadata",
198
- );
199
- return patched;
200
- }
201
-
202
- throw new HermesAgentBridgeError(
203
- "HERMES_BRIDGE_UNKNOWN_PATCH_KIND",
204
- `Unknown Hermes bridge patch kind: ${patchKind}`,
205
- { patchKind },
206
- );
207
- }
208
-
209
- function normalizeContract(input) {
210
- if (!input || typeof input !== "object") {
211
- throw new HermesAgentBridgeError("HERMES_BRIDGE_INVALID_CONTRACT", "Bridge contract is required");
212
- }
213
- const versionFile = input.versionFile;
214
- if (
215
- !input.hermesVersion ||
216
- !versionFile ||
217
- typeof versionFile.path !== "string" ||
218
- !isSha256(versionFile.sha256) ||
219
- !Array.isArray(input.files) ||
220
- input.files.length === 0
221
- ) {
222
- throw new HermesAgentBridgeError("HERMES_BRIDGE_INVALID_CONTRACT", "Bridge contract is malformed");
223
- }
224
- const seen = new Set([versionFile.path]);
225
- const files = input.files.map((file) => {
226
- if (
227
- !file ||
228
- typeof file.path !== "string" ||
229
- typeof file.patchKind !== "string" ||
230
- !isSha256(file.sourceSha256) ||
231
- !isSha256(file.patchedSha256) ||
232
- seen.has(file.path)
233
- ) {
234
- throw new HermesAgentBridgeError("HERMES_BRIDGE_INVALID_CONTRACT", "Bridge file contract is malformed");
235
- }
236
- seen.add(file.path);
237
- return Object.freeze({ ...file });
238
- });
239
- const normalized = {
240
- bridgeId: HERMES_AGENT_BRIDGE_ID,
241
- hermesVersion: String(input.hermesVersion),
242
- versionFile: Object.freeze({ ...versionFile }),
243
- files: Object.freeze(files),
244
- };
245
- return Object.freeze({
246
- ...normalized,
247
- contractDigest: sha256(JSON.stringify(normalized)),
248
- });
249
- }
250
-
251
- export function createHermesBridgeContractFromSources({
252
- hermesVersion,
253
- versionFilePath = "hermes_cli/__init__.py",
254
- versionFileSource,
255
- files,
256
- }) {
257
- if (typeof versionFileSource !== "string" || !Array.isArray(files)) {
258
- throw new HermesAgentBridgeError("HERMES_BRIDGE_INVALID_CONTRACT", "Source-backed contract is malformed");
259
- }
260
- return normalizeContract({
261
- hermesVersion,
262
- versionFile: { path: versionFilePath, sha256: sha256(versionFileSource) },
263
- files: files.map((file) => ({
264
- path: file.path,
265
- patchKind: file.patchKind,
266
- sourceSha256: sha256(file.source),
267
- patchedSha256: sha256(patchHermesSourceFile(file.patchKind, file.source)),
268
- })),
269
- });
270
- }
271
-
272
- export const HERMES_AGENT_BRIDGE_CONTRACT = normalizeContract({
273
- hermesVersion: "0.18.0",
274
- versionFile: {
275
- path: "hermes_cli/__init__.py",
276
- sha256: "58b0e216b05a3db1ec84e6eff8087a611261b78f0d3004c31cd83d93c257bbc3",
277
- },
278
- files: [
279
- {
280
- path: "gateway/config.py",
281
- patchKind: "gateway-config",
282
- sourceSha256: "a08cf991bc6614539389936573b0d179fbe8169db05037df496123308f651b50",
283
- patchedSha256: "6c629d4a95597c97e115a9d92f85d6f4da6312be7e5ffec81a110d0651961586",
284
- },
285
- {
286
- path: "plugins/platforms/slack/adapter.py",
287
- patchKind: "slack-adapter",
288
- sourceSha256: "20b12d9bbff0f5b280e0be8e795c7327f4089c3fa8b04d7a6d346dabae591f2a",
289
- patchedSha256: "548ab70f515e22830cbcbf6b0691768272dd708fd0118f7c601e1e1232bb016b",
290
- },
291
- {
292
- path: "tools/mcp_tool.py",
293
- patchKind: "mcp-tool",
294
- sourceSha256: "1bcceccaeb8ec8894c83819d389a64258517f4b48db1c8fea5c486633e56fe8b",
295
- patchedSha256: "44bf5dfeca47bf91fc5e68ecd6455c33f41afd2b04fa16d213f59115ddb38f21",
296
- },
297
- ],
298
- });
299
-
300
- function assertRelativeContractPath(path) {
301
- if (!path || isAbsolute(path) || path.split(/[\\/]+/).some((part) => part === ".." || part === "")) {
302
- throw new HermesAgentBridgeError("HERMES_BRIDGE_UNSAFE_PATH", "Bridge contract path is unsafe", { path });
303
- }
304
- }
305
-
306
- function resolveConfined(root, path) {
307
- assertRelativeContractPath(path);
308
- const candidate = resolve(root, path);
309
- const rel = relative(root, candidate);
310
- if (!rel || rel === ".." || rel.startsWith(`..${sep}`) || isAbsolute(rel)) {
311
- throw new HermesAgentBridgeError("HERMES_BRIDGE_UNSAFE_PATH", "Bridge path escapes source root", { path });
312
- }
313
- return candidate;
314
- }
315
-
316
- function assertSafeRegularFile(root, path) {
317
- const candidate = resolveConfined(root, path);
318
- let cursor = root;
319
- for (const component of relative(root, candidate).split(sep)) {
320
- cursor = join(cursor, component);
321
- const info = lstatSync(cursor);
322
- if (info.isSymbolicLink()) {
323
- throw new HermesAgentBridgeError("HERMES_BRIDGE_SYMLINK", "Hermes bridge paths may not contain symlinks", { path });
324
- }
325
- if (cursor !== candidate && (!info.isDirectory() || (info.mode & 0o022) !== 0)) {
326
- throw new HermesAgentBridgeError(
327
- "HERMES_BRIDGE_UNSAFE_DIRECTORY",
328
- "Hermes bridge parent directories must be real and not group/world writable",
329
- { path },
330
- );
331
- }
332
- }
333
- const info = lstatSync(candidate);
334
- if (!info.isFile()) {
335
- throw new HermesAgentBridgeError("HERMES_BRIDGE_NOT_REGULAR", "Hermes bridge target is not a regular file", { path });
336
- }
337
- if ((info.mode & 0o022) !== 0) {
338
- throw new HermesAgentBridgeError("HERMES_BRIDGE_WRITABLE_SOURCE", "Hermes bridge target is group/world writable", { path });
339
- }
340
- return { candidate, info };
341
- }
342
-
343
- function validateSourceRoot(sourceRoot) {
344
- if (typeof sourceRoot !== "string" || !isAbsolute(sourceRoot) || resolve(sourceRoot) !== sourceRoot) {
345
- throw new HermesAgentBridgeError("HERMES_BRIDGE_UNSAFE_ROOT", "Hermes source root must be an absolute normalized path");
346
- }
347
- const rootInfo = lstatSync(sourceRoot);
348
- if (!rootInfo.isDirectory() || rootInfo.isSymbolicLink() || realpathSync(sourceRoot) !== sourceRoot) {
349
- throw new HermesAgentBridgeError("HERMES_BRIDGE_UNSAFE_ROOT", "Hermes source root must be a real directory");
350
- }
351
- if ((rootInfo.mode & 0o022) !== 0) {
352
- throw new HermesAgentBridgeError("HERMES_BRIDGE_WRITABLE_ROOT", "Hermes source root is group/world writable");
353
- }
354
- return sourceRoot;
355
- }
356
-
357
- function readVersion(source) {
358
- const match = source.match(/^__version__\s*=\s*["']([^"']+)["']/m);
359
- return match?.[1] ?? null;
360
- }
361
-
362
- function publicFileReceipts(files) {
363
- return files.map(({ path, sha256: digest }) => ({ path, sha256: digest }));
364
- }
365
-
366
- export function inspectHermesAgentBridge({ sourceRoot, contract = HERMES_AGENT_BRIDGE_CONTRACT }) {
367
- const pinned = normalizeContract(contract);
368
- const root = validateSourceRoot(sourceRoot);
369
- const versionTarget = assertSafeRegularFile(root, pinned.versionFile.path);
370
- const versionBytes = readFileSync(versionTarget.candidate);
371
- const versionSource = versionBytes.toString("utf8");
372
- const versionDigest = sha256(versionBytes);
373
- const version = readVersion(versionSource);
374
- const reasons = [];
375
- if (versionDigest !== pinned.versionFile.sha256) reasons.push("VERSION_FILE_HASH_MISMATCH");
376
- if (version !== pinned.hermesVersion) reasons.push("HERMES_VERSION_MISMATCH");
377
-
378
- const files = pinned.files.map((file) => {
379
- const target = assertSafeRegularFile(root, file.path);
380
- const digest = sha256(readFileSync(target.candidate));
381
- const state = digest === file.sourceSha256 ? "source" : digest === file.patchedSha256 ? "patched" : "unknown";
382
- if (state === "unknown") reasons.push(`UNKNOWN_SOURCE:${file.path}`);
383
- return { ...file, absolutePath: target.candidate, mode: target.info.mode & 0o777, sha256: digest, state };
384
- });
385
- const knownStates = new Set(files.map((file) => file.state));
386
- if (knownStates.has("source") && knownStates.has("patched")) reasons.push("MIXED_BRIDGE_STATE");
387
-
388
- let state = "blocked";
389
- if (reasons.length === 0 && knownStates.size === 1 && knownStates.has("source")) state = "needs_install";
390
- if (reasons.length === 0 && knownStates.size === 1 && knownStates.has("patched")) state = "installed";
391
- return Object.freeze({
392
- bridgeId: pinned.bridgeId,
393
- contractDigest: pinned.contractDigest,
394
- hermesVersion: version,
395
- state,
396
- reasons: Object.freeze(reasons),
397
- files: Object.freeze(publicFileReceipts(files)),
398
- });
399
- }
400
-
401
- function fsyncDirectory(path) {
402
- const fd = openSync(path, constants.O_RDONLY);
403
- try {
404
- fsyncSync(fd);
405
- } finally {
406
- closeSync(fd);
407
- }
408
- }
409
-
410
- function atomicReplace(candidate, bytes, { mode, uid, gid }) {
411
- const temp = join(dirname(candidate), `.${candidate.split(sep).at(-1)}.sellable-agent-${process.pid}-${Date.now()}.tmp`);
412
- let fd;
413
- try {
414
- fd = openSync(temp, constants.O_CREAT | constants.O_EXCL | constants.O_WRONLY | constants.O_NOFOLLOW, mode);
415
- writeFileSync(fd, bytes);
416
- fchmodSync(fd, mode);
417
- fchownSync(fd, uid, gid);
418
- fsyncSync(fd);
419
- closeSync(fd);
420
- fd = undefined;
421
- renameSync(temp, candidate);
422
- fsyncDirectory(dirname(candidate));
423
- } finally {
424
- if (fd !== undefined) closeSync(fd);
425
- if (existsSync(temp)) unlinkSync(temp);
426
- }
427
- }
428
-
429
- export function installHermesAgentBridge({ sourceRoot, contract = HERMES_AGENT_BRIDGE_CONTRACT }) {
430
- const pinned = normalizeContract(contract);
431
- const before = inspectHermesAgentBridge({ sourceRoot, contract: pinned });
432
- if (before.state === "installed") return Object.freeze({ ...before, status: "already_current" });
433
- if (before.state !== "needs_install") {
434
- throw new HermesAgentBridgeError(
435
- "HERMES_BRIDGE_SOURCE_REJECTED",
436
- "Hermes source does not match the pinned bridge preimage",
437
- { state: before.state, reasons: before.reasons },
438
- );
439
- }
440
-
441
- const staged = pinned.files.map((file) => {
442
- const { candidate, info } = assertSafeRegularFile(sourceRoot, file.path);
443
- const original = readFileSync(candidate);
444
- const patched = Buffer.from(patchHermesSourceFile(file.patchKind, original.toString("utf8")), "utf8");
445
- const digest = sha256(patched);
446
- if (digest !== file.patchedSha256) {
447
- throw new HermesAgentBridgeError(
448
- "HERMES_BRIDGE_PATCH_DIGEST_MISMATCH",
449
- "Generated Hermes patch does not match the pinned postimage",
450
- { path: file.path },
451
- );
452
- }
453
- return { ...file, candidate, ownership: { mode: info.mode & 0o777, uid: info.uid, gid: info.gid }, original, patched };
454
- });
455
-
456
- const replaced = [];
457
- try {
458
- for (const file of staged) {
459
- atomicReplace(file.candidate, file.patched, file.ownership);
460
- replaced.push(file);
461
- }
462
- } catch (error) {
463
- const rollbackErrors = [];
464
- for (const file of replaced.reverse()) {
465
- try {
466
- atomicReplace(file.candidate, file.original, file.ownership);
467
- } catch (rollbackError) {
468
- rollbackErrors.push({ path: file.path, message: rollbackError instanceof Error ? rollbackError.message : String(rollbackError) });
469
- }
470
- }
471
- throw new HermesAgentBridgeError(
472
- rollbackErrors.length ? "HERMES_BRIDGE_ROLLBACK_FAILED" : "HERMES_BRIDGE_INSTALL_FAILED",
473
- rollbackErrors.length ? "Hermes bridge install and rollback failed" : "Hermes bridge install failed and was rolled back",
474
- { rollbackErrors, cause: error instanceof Error ? error.message : String(error) },
475
- );
476
- }
477
-
478
- const after = inspectHermesAgentBridge({ sourceRoot, contract: pinned });
479
- if (after.state !== "installed") {
480
- throw new HermesAgentBridgeError("HERMES_BRIDGE_VERIFY_FAILED", "Hermes bridge post-install verification failed", {
481
- state: after.state,
482
- reasons: after.reasons,
483
- });
484
- }
485
- return Object.freeze({ ...after, status: "installed" });
486
- }
487
-
488
- export function verifyHermesAgentBridge({ sourceRoot, contract = HERMES_AGENT_BRIDGE_CONTRACT }) {
489
- const observation = inspectHermesAgentBridge({ sourceRoot, contract });
490
- if (observation.state !== "installed") {
491
- throw new HermesAgentBridgeError("HERMES_BRIDGE_NOT_INSTALLED", "Pinned Hermes bridge is not installed", {
492
- state: observation.state,
493
- reasons: observation.reasons,
494
- });
495
- }
496
- return Object.freeze({ ...observation, status: "verified" });
497
- }