@sellable/install 0.1.588 → 0.1.590-wip.phase187.20260811.3
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 +4 -4
- package/container/README.md +2 -2
- package/lib/sellable-agent/fly-admin-image/admin-runtime.mjs +16 -1
- package/lib/sellable-agent/fly-customer-image/Dockerfile +1 -1
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +1 -1
- package/lib/sellable-agent/fly-skills-bridge.mjs +4 -1
- package/lib/sellable-agent/hermes-memory-snapshot.mjs +284 -55
- package/lib/sellable-agent/host-bootstrap.mjs +1 -1
- package/lib/sellable-agent/host-worker.mjs +1 -1
- package/lib/sellable-agent/profile-materializer.mjs +1 -1
- package/lib/sellable-agent/provisioning-adapter.mjs +4 -4
- package/package.json +1 -1
package/container/Dockerfile
CHANGED
|
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
|
|
|
3
3
|
|
|
4
4
|
USER root
|
|
5
5
|
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
6
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.589
|
|
7
7
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.860
|
|
8
8
|
ARG INSTALLER_INTEGRITY
|
|
9
9
|
ARG MCP_INTEGRITY=sha512-jRA1KsCV2d4Zcuo5/UmJiP8P0NaoC3ZDvUFRQOc2AMSNDVmUR6gWvtkyAiqb1lukbFWKzfVlz2/4EWzxvb7hkQ==
|
|
10
10
|
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.
|
|
11
|
+
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.589" \
|
|
12
12
|
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.860" \
|
|
13
13
|
&& test -n "${INSTALLER_INTEGRITY}" \
|
|
14
14
|
&& test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
|
|
@@ -23,9 +23,9 @@ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.588" \
|
|
|
23
23
|
&& mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
|
|
24
24
|
&& npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
|
|
25
25
|
&& npm install --prefix /opt/sellable-agent/mcp-root --omit=dev --ignore-scripts --no-audit --no-fund "${MCP_PACKAGE}" \
|
|
26
|
-
&& node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.
|
|
26
|
+
&& node --input-type=module -e "import { readFileSync } from 'node:fs'; const install = JSON.parse(readFileSync('/opt/sellable-agent/install-root/node_modules/@sellable/install/package.json')); const mcp = JSON.parse(readFileSync('/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp/package.json')); if (install.name !== '@sellable/install' || install.version !== '0.1.589' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.860') throw new Error('package identity rejected');" \
|
|
27
27
|
&& node --input-type=module -e "import { buildExternalRuntimeClosure } from '/opt/sellable-agent/install-root/node_modules/@sellable/install/lib/sellable-agent/external-runtime-builder.mjs'; const built = buildExternalRuntimeClosure({ mcpPackageRoot: '/opt/sellable-agent/mcp-root/node_modules/@sellable/mcp', mcpNodeModulesRoot: '/opt/sellable-agent/mcp-root/node_modules', hermesSourceRoot: '/opt/hermes', ownerUid: 0, ownerGid: 0 }); if (!built.closureDigest) throw new Error('runtime closure rejected');" \
|
|
28
|
-
&& node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.
|
|
28
|
+
&& node --input-type=module -e "import { chmodSync, writeFileSync } from 'node:fs'; const release = { installerPackage: '@sellable/install@0.1.589', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.860', mcpIntegrity: process.argv[2] }; writeFileSync('/opt/sellable-agent/release.json', JSON.stringify(release) + '\\n', { mode: 0o444 }); chmodSync('/opt/sellable-agent/release.json', 0o444);" "${INSTALLER_INTEGRITY}" "${MCP_INTEGRITY}" \
|
|
29
29
|
&& npm cache clean --force
|
|
30
30
|
|
|
31
31
|
COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
|
package/container/README.md
CHANGED
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
|
|
4
4
|
|
|
5
5
|
Build from this directory after
|
|
6
|
-
`@sellable/install@0.1.
|
|
6
|
+
`@sellable/install@0.1.589` is
|
|
7
7
|
published:
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
docker build --platform linux/amd64 \
|
|
11
11
|
--build-arg INSTALLER_INTEGRITY='sha512-…' \
|
|
12
|
-
--tag sellable-agent-host:0.1.
|
|
12
|
+
--tag sellable-agent-host:0.1.589 .
|
|
13
13
|
```
|
|
14
14
|
|
|
15
15
|
Create a private `bootstrap/` directory owned by root with mode `0700`. It must contain `bootstrap.json`, `provider-auth`, and `slack-app`, each owned by root with mode `0600`. `bootstrap.json` contains only non-secret identities, hashes, package integrities, and the control-plane URL. The other two files contain the provider JSON and global Slack app token whose hashes are pinned by `bootstrap.json`.
|
|
@@ -67,6 +67,7 @@ import {
|
|
|
67
67
|
HERMES_SNAPSHOT_HOOKS,
|
|
68
68
|
HERMES_SNAPSHOT_HOOK_ALLOWLIST_BYTES,
|
|
69
69
|
applyHermesHostedBackgroundReviewPolicy,
|
|
70
|
+
ensureHermesOpportunitySequencesCron,
|
|
70
71
|
ensureHermesSnapshotCron,
|
|
71
72
|
markHermesSnapshotDirty,
|
|
72
73
|
mergeHermesState,
|
|
@@ -696,6 +697,10 @@ const ADMIN_LIVE_SKILLS_CONFIG_KEYS = Object.freeze([
|
|
|
696
697
|
"disabled",
|
|
697
698
|
"platform_disabled",
|
|
698
699
|
]);
|
|
700
|
+
const ADMIN_SEALED_SKILL_DIRECTORIES = new Set([
|
|
701
|
+
"sellable",
|
|
702
|
+
"opportunity-sequences",
|
|
703
|
+
]);
|
|
699
704
|
|
|
700
705
|
function adminPreservedSkillsIdentity(profileRoot) {
|
|
701
706
|
if (!existsSync(profileRoot)) return null;
|
|
@@ -704,7 +709,7 @@ function adminPreservedSkillsIdentity(profileRoot) {
|
|
|
704
709
|
const walk = (directory, relativePath = "") => {
|
|
705
710
|
for (const name of readdirSync(directory).sort()) {
|
|
706
711
|
const nextRelative = relativePath ? `${relativePath}/${name}` : name;
|
|
707
|
-
if (nextRelative
|
|
712
|
+
if (ADMIN_SEALED_SKILL_DIRECTORIES.has(nextRelative)) continue;
|
|
708
713
|
const path = join(directory, name);
|
|
709
714
|
const stat = lstatSync(path);
|
|
710
715
|
if (stat.isSymbolicLink()) {
|
|
@@ -961,12 +966,22 @@ async function materializeAdminProfile({
|
|
|
961
966
|
hermesCli: HERMES,
|
|
962
967
|
});
|
|
963
968
|
if (!memoryCron.ok) throw new Error(memoryCron.code);
|
|
969
|
+
const opportunitySequencesCron = await ensureHermesOpportunitySequencesCron({
|
|
970
|
+
profileId: profile,
|
|
971
|
+
profileRoot: profileHome,
|
|
972
|
+
hermesRoot: DATA_ROOT,
|
|
973
|
+
statePath: layout.statePath,
|
|
974
|
+
});
|
|
975
|
+
if (!opportunitySequencesCron.ok) {
|
|
976
|
+
throw new Error(opportunitySequencesCron.code);
|
|
977
|
+
}
|
|
964
978
|
return {
|
|
965
979
|
stage,
|
|
966
980
|
promotion: promoted.promotion,
|
|
967
981
|
profile,
|
|
968
982
|
profileHome,
|
|
969
983
|
adminHome,
|
|
984
|
+
opportunitySequencesCron,
|
|
970
985
|
observedSoulRawSha256: promoted.observedSoulRawSha256,
|
|
971
986
|
servers: [serverProofs.sellable, serverProofs["sellable-admin"]].map(
|
|
972
987
|
({ tools: _tools, ...proof }) => proof
|
|
@@ -6,7 +6,7 @@ USER root
|
|
|
6
6
|
|
|
7
7
|
ARG MCP_PACKAGE=@sellable/mcp@0.1.860
|
|
8
8
|
ARG MCP_INTEGRITY=sha512-jRA1KsCV2d4Zcuo5/UmJiP8P0NaoC3ZDvUFRQOc2AMSNDVmUR6gWvtkyAiqb1lukbFWKzfVlz2/4EWzxvb7hkQ==
|
|
9
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
9
|
+
ARG INSTALLER_PACKAGE=@sellable/install@0.1.589
|
|
10
10
|
ARG TUS_JS_CLIENT_VERSION=4.3.1
|
|
11
11
|
ARG YAML_VERSION=2.9.0
|
|
12
12
|
ARG HERMES_VERSION=0.20.0
|
|
@@ -93,7 +93,7 @@ const RUNTIME_GENERATION = Number(
|
|
|
93
93
|
const BOOT_SESSION_ID = randomUUID();
|
|
94
94
|
const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.0";
|
|
95
95
|
const INSTALLER_PACKAGE =
|
|
96
|
-
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.
|
|
96
|
+
process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.589";
|
|
97
97
|
const MCP_PACKAGE =
|
|
98
98
|
process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.860";
|
|
99
99
|
|
|
@@ -353,6 +353,7 @@ function publicInventory(inventory) {
|
|
|
353
353
|
|
|
354
354
|
const NATIVE_SCRIPT = String.raw`
|
|
355
355
|
import json, os, sys
|
|
356
|
+
from pathlib import Path
|
|
356
357
|
request = json.loads(sys.stdin.read())
|
|
357
358
|
from tools.skills_tool import _find_all_skills, skill_view
|
|
358
359
|
from tools.skill_manager_tool import _find_skill, _create_skill, _edit_skill, _delete_skill, _write_file, _remove_file
|
|
@@ -390,7 +391,9 @@ elif op == "read":
|
|
|
390
391
|
output = {"nativeView": skill_view(request["readName"], file_path=request.get("readFile")) if request.get("readFile") else skill_view(request["readName"])}
|
|
391
392
|
elif op == "scan":
|
|
392
393
|
from tools.skills_guard import scan_skill, should_allow_install
|
|
393
|
-
|
|
394
|
+
# Phase 170 production receipt skills_apply_failed_rolled_back proved that
|
|
395
|
+
# Hermes 0.20.0 rejects a string here; keep the native boundary path-typed.
|
|
396
|
+
path = Path(request["path"])
|
|
394
397
|
scan = scan_skill(path, source="agent-created")
|
|
395
398
|
allowed, reason = should_allow_install(scan)
|
|
396
399
|
output = {"ok": allowed is True, "reason": str(reason)}
|
|
@@ -55,9 +55,20 @@ export const HERMES_SNAPSHOT_CRON = Object.freeze({
|
|
|
55
55
|
installedScript: "scripts/hermes-memory-reconcile.sh",
|
|
56
56
|
noAgent: true,
|
|
57
57
|
});
|
|
58
|
+
export const HERMES_OPPORTUNITY_SEQUENCES_CRON = Object.freeze({
|
|
59
|
+
name: "sellable-opportunity-sequences",
|
|
60
|
+
schedule: "every 30m",
|
|
61
|
+
deliver: "slack",
|
|
62
|
+
skills: Object.freeze(["opportunity-sequences"]),
|
|
63
|
+
enabledToolsets: Object.freeze(["safe", "sellable", "mcp"]),
|
|
64
|
+
noAgent: false,
|
|
65
|
+
});
|
|
66
|
+
export const HERMES_OPPORTUNITY_SEQUENCES_JOURNAL_ROOT =
|
|
67
|
+
"/opt/data/.sellable-agent/admin-runtime/opportunity-sequences";
|
|
58
68
|
const LEGACY_HERMES_SNAPSHOT_SCRIPT = "scripts/hermes-memory-reconcile.sh";
|
|
59
69
|
|
|
60
70
|
const HERMES = "/opt/hermes/.venv/bin/hermes";
|
|
71
|
+
const HERMES_PYTHON = "/opt/hermes/.venv/bin/python";
|
|
61
72
|
const VERSION = "0.20.0";
|
|
62
73
|
const ID = /^[A-Za-z0-9][A-Za-z0-9_-]{0,127}$/;
|
|
63
74
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
@@ -259,6 +270,68 @@ function cronJobs(profileRoot) {
|
|
|
259
270
|
return jobs;
|
|
260
271
|
}
|
|
261
272
|
|
|
273
|
+
const sameJson = (left, right) =>
|
|
274
|
+
JSON.stringify(left) === JSON.stringify(right);
|
|
275
|
+
|
|
276
|
+
export async function ensureHermesNamedCron({
|
|
277
|
+
profileRoot,
|
|
278
|
+
name,
|
|
279
|
+
isExpected,
|
|
280
|
+
isUpdatable = () => true,
|
|
281
|
+
create,
|
|
282
|
+
update,
|
|
283
|
+
codePrefix = "hermes_named_cron",
|
|
284
|
+
}) {
|
|
285
|
+
if (
|
|
286
|
+
!isAbsolute(profileRoot) ||
|
|
287
|
+
!ID.test(name ?? "") ||
|
|
288
|
+
typeof isExpected !== "function" ||
|
|
289
|
+
typeof create !== "function" ||
|
|
290
|
+
typeof update !== "function"
|
|
291
|
+
) {
|
|
292
|
+
return failed(`${codePrefix}_identity_rejected`);
|
|
293
|
+
}
|
|
294
|
+
try {
|
|
295
|
+
const before = cronJobs(profileRoot);
|
|
296
|
+
const named = before.filter((job) => job?.name === name);
|
|
297
|
+
if (named.length > 1 || (named[0] && !isUpdatable(named[0]))) {
|
|
298
|
+
return failed(`${codePrefix}_inventory_rejected`);
|
|
299
|
+
}
|
|
300
|
+
if (named[0] && isExpected(named[0])) {
|
|
301
|
+
return {
|
|
302
|
+
ok: true,
|
|
303
|
+
status: "REUSED",
|
|
304
|
+
jobId: named[0].id,
|
|
305
|
+
job: named[0],
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
const siblings = before.filter((job) => job?.name !== name);
|
|
309
|
+
const mutation = named[0] ? await update(named[0]) : await create();
|
|
310
|
+
if (childFailed(mutation)) {
|
|
311
|
+
return failed(`${codePrefix}_${named[0] ? "update" : "create"}_failed`);
|
|
312
|
+
}
|
|
313
|
+
const after = cronJobs(profileRoot);
|
|
314
|
+
const readback = after.filter((job) => job?.name === name);
|
|
315
|
+
const afterSiblings = after.filter((job) => job?.name !== name);
|
|
316
|
+
if (!sameJson(afterSiblings, siblings)) {
|
|
317
|
+
return failed(`${codePrefix}_sibling_readback_rejected`);
|
|
318
|
+
}
|
|
319
|
+
if (readback.length !== 1 || !isExpected(readback[0])) {
|
|
320
|
+
return failed(
|
|
321
|
+
`${codePrefix}_${named[0] ? "update_readback" : "readback"}_rejected`
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return {
|
|
325
|
+
ok: true,
|
|
326
|
+
status: named[0] ? "UPDATED" : "CREATED",
|
|
327
|
+
jobId: readback[0].id,
|
|
328
|
+
job: readback[0],
|
|
329
|
+
};
|
|
330
|
+
} catch {
|
|
331
|
+
return failed(`${codePrefix}_install_failed`);
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
262
335
|
function isCron(job) {
|
|
263
336
|
return Boolean(
|
|
264
337
|
job?.id &&
|
|
@@ -430,25 +503,23 @@ export async function ensureHermesSnapshotCron({
|
|
|
430
503
|
readFileSync(reconcileTemplatePath),
|
|
431
504
|
0o700
|
|
432
505
|
);
|
|
433
|
-
const
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
}
|
|
442
|
-
if (before[0]) {
|
|
443
|
-
if (isLegacyCron(before[0])) {
|
|
444
|
-
const migrated = await childRunner(runChild)(
|
|
506
|
+
const result = await ensureHermesNamedCron({
|
|
507
|
+
profileRoot,
|
|
508
|
+
name: HERMES_SNAPSHOT_CRON.name,
|
|
509
|
+
isExpected: isCron,
|
|
510
|
+
isUpdatable: (job) => isCron(job) || isLegacyCron(job),
|
|
511
|
+
codePrefix: "hermes_snapshot_cron",
|
|
512
|
+
create: () =>
|
|
513
|
+
childRunner(runChild)(
|
|
445
514
|
invocation(
|
|
446
515
|
[
|
|
447
516
|
"--profile",
|
|
448
517
|
profileId,
|
|
449
518
|
"cron",
|
|
450
|
-
"
|
|
451
|
-
|
|
519
|
+
"create",
|
|
520
|
+
HERMES_SNAPSHOT_CRON.schedule,
|
|
521
|
+
"--name",
|
|
522
|
+
HERMES_SNAPSHOT_CRON.name,
|
|
452
523
|
"--script",
|
|
453
524
|
HERMES_SNAPSHOT_CRON.script,
|
|
454
525
|
"--no-agent",
|
|
@@ -456,53 +527,211 @@ export async function ensureHermesSnapshotCron({
|
|
|
456
527
|
hermesRoot,
|
|
457
528
|
hermesCli
|
|
458
529
|
)
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
);
|
|
466
|
-
return afterMigration.length === 1 && isCron(afterMigration[0])
|
|
467
|
-
? {
|
|
468
|
-
ok: true,
|
|
469
|
-
status: "MIGRATED",
|
|
470
|
-
jobId: afterMigration[0].id,
|
|
530
|
+
),
|
|
531
|
+
update: (job) =>
|
|
532
|
+
childRunner(runChild)(
|
|
533
|
+
invocation(
|
|
534
|
+
[
|
|
535
|
+
"--profile",
|
|
471
536
|
profileId,
|
|
472
|
-
|
|
473
|
-
|
|
537
|
+
"cron",
|
|
538
|
+
"edit",
|
|
539
|
+
job.id,
|
|
540
|
+
"--script",
|
|
541
|
+
HERMES_SNAPSHOT_CRON.script,
|
|
542
|
+
"--no-agent",
|
|
543
|
+
],
|
|
544
|
+
hermesRoot,
|
|
545
|
+
hermesCli
|
|
546
|
+
)
|
|
547
|
+
),
|
|
548
|
+
});
|
|
549
|
+
if (!result.ok) {
|
|
550
|
+
if (result.code === "hermes_snapshot_cron_update_failed") {
|
|
551
|
+
return failed("hermes_snapshot_cron_migrate_failed");
|
|
552
|
+
}
|
|
553
|
+
if (result.code === "hermes_snapshot_cron_update_readback_rejected") {
|
|
554
|
+
return failed("hermes_snapshot_cron_migrate_readback_rejected");
|
|
474
555
|
}
|
|
475
|
-
return
|
|
556
|
+
return result;
|
|
476
557
|
}
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
558
|
+
return {
|
|
559
|
+
ok: true,
|
|
560
|
+
status: result.status === "UPDATED" ? "MIGRATED" : result.status,
|
|
561
|
+
jobId: result.jobId,
|
|
562
|
+
profileId,
|
|
563
|
+
};
|
|
564
|
+
} catch {
|
|
565
|
+
return failed("hermes_snapshot_cron_install_failed");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
const OPPORTUNITY_SEQUENCE_ASSETS = Object.freeze([
|
|
570
|
+
"SKILL.md",
|
|
571
|
+
"references/outbound-multichannel-v1.md",
|
|
572
|
+
"references/positive-reply-to-agreed-meeting-v1.md",
|
|
573
|
+
]);
|
|
574
|
+
const SLACK_USER_ID = /^U[A-Z0-9]{8,20}$/;
|
|
575
|
+
const OPPORTUNITY_SEQUENCE_CRON_PYTHON = [
|
|
576
|
+
"import json,sys",
|
|
577
|
+
"from cron.jobs import create_job, update_job",
|
|
578
|
+
"value=json.loads(sys.argv[1])",
|
|
579
|
+
"job_id=value.pop('jobId', None)",
|
|
580
|
+
"enabled=value.pop('enabled')",
|
|
581
|
+
"result=update_job(job_id, {**value, 'enabled': enabled}) if job_id else create_job(**value)",
|
|
582
|
+
"print(json.dumps({'id': result.get('id') if result else None}))",
|
|
583
|
+
].join("\n");
|
|
584
|
+
|
|
585
|
+
function opportunitySequenceIdentity(profileRoot) {
|
|
586
|
+
const root = join(profileRoot, "skills", "opportunity-sequences");
|
|
587
|
+
return Object.fromEntries(
|
|
588
|
+
OPPORTUNITY_SEQUENCE_ASSETS.map((relative) => {
|
|
589
|
+
const path = join(root, relative);
|
|
590
|
+
regularFile(path);
|
|
591
|
+
return [relative, sha256(readFileSync(path))];
|
|
592
|
+
})
|
|
593
|
+
);
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function opportunitySequencePrompt({
|
|
597
|
+
notificationSlackUserId,
|
|
598
|
+
journalRoot,
|
|
599
|
+
hashes,
|
|
600
|
+
}) {
|
|
601
|
+
return [
|
|
602
|
+
"Run the installed opportunity-sequences skill for one bounded page.",
|
|
603
|
+
`Use the private journal at ${journalRoot}.`,
|
|
604
|
+
`Require these installed asset hashes: ${JSON.stringify(hashes)}.`,
|
|
605
|
+
`End with a compact native Slack summary addressed to <@${notificationSlackUserId}>.`,
|
|
606
|
+
"Never send prospect outreach; never claim exactly-once delivery; never use a direct Slack or Lightfield transport outside the installed skill and Product MCP.",
|
|
607
|
+
].join(" ");
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
export async function ensureHermesOpportunitySequencesCron({
|
|
611
|
+
profileId,
|
|
612
|
+
profileRoot,
|
|
613
|
+
hermesRoot,
|
|
614
|
+
statePath,
|
|
615
|
+
journalRoot = HERMES_OPPORTUNITY_SEQUENCES_JOURNAL_ROOT,
|
|
616
|
+
hermesPython = HERMES_PYTHON,
|
|
617
|
+
runChild,
|
|
618
|
+
}) {
|
|
619
|
+
if (
|
|
620
|
+
!validProfilePath(profileId, profileRoot, hermesRoot) ||
|
|
621
|
+
!existsSync(profileRoot) ||
|
|
622
|
+
!isAbsolute(statePath) ||
|
|
623
|
+
!isAbsolute(journalRoot)
|
|
624
|
+
) {
|
|
625
|
+
return failed("hermes_opportunity_sequences_cron_identity_rejected");
|
|
626
|
+
}
|
|
627
|
+
try {
|
|
628
|
+
const stateStat = regularFile(statePath);
|
|
629
|
+
if ((stateStat.mode & 0o077) !== 0) {
|
|
630
|
+
return failed("hermes_opportunity_sequences_cron_state_mode_rejected");
|
|
631
|
+
}
|
|
632
|
+
const state = JSON.parse(readFileSync(statePath, "utf8"));
|
|
633
|
+
const notificationSlackUserId =
|
|
634
|
+
state?.opportunitySequences?.notificationSlackUserId;
|
|
635
|
+
if (!SLACK_USER_ID.test(notificationSlackUserId ?? "")) {
|
|
636
|
+
return failed("hermes_opportunity_sequences_cron_notification_rejected");
|
|
637
|
+
}
|
|
638
|
+
mkdirSync(journalRoot, { recursive: true, mode: 0o700 });
|
|
639
|
+
chmodSync(journalRoot, 0o700);
|
|
640
|
+
const journalStat = lstatSync(journalRoot);
|
|
641
|
+
if (
|
|
642
|
+
journalStat.isSymbolicLink() ||
|
|
643
|
+
!journalStat.isDirectory() ||
|
|
644
|
+
(journalStat.mode & 0o077) !== 0
|
|
645
|
+
) {
|
|
646
|
+
return failed("hermes_opportunity_sequences_cron_journal_rejected");
|
|
647
|
+
}
|
|
648
|
+
const hashes = opportunitySequenceIdentity(profileRoot);
|
|
649
|
+
const prompt = opportunitySequencePrompt({
|
|
650
|
+
notificationSlackUserId,
|
|
651
|
+
journalRoot,
|
|
652
|
+
hashes,
|
|
653
|
+
});
|
|
654
|
+
const expected = (job) =>
|
|
655
|
+
Boolean(
|
|
656
|
+
job?.id &&
|
|
657
|
+
job.name === HERMES_OPPORTUNITY_SEQUENCES_CRON.name &&
|
|
658
|
+
job.prompt === prompt &&
|
|
659
|
+
job.no_agent === false &&
|
|
660
|
+
job.enabled === true &&
|
|
661
|
+
job.deliver === HERMES_OPPORTUNITY_SEQUENCES_CRON.deliver &&
|
|
662
|
+
sameJson(job.skills, HERMES_OPPORTUNITY_SEQUENCES_CRON.skills) &&
|
|
663
|
+
sameJson(
|
|
664
|
+
job.enabled_toolsets,
|
|
665
|
+
HERMES_OPPORTUNITY_SEQUENCES_CRON.enabledToolsets
|
|
666
|
+
) &&
|
|
667
|
+
job.schedule?.kind === "interval" &&
|
|
668
|
+
job.schedule?.minutes === 30
|
|
669
|
+
);
|
|
670
|
+
const mutate = (jobId = null) => {
|
|
671
|
+
const spec = {
|
|
672
|
+
prompt,
|
|
673
|
+
schedule: HERMES_OPPORTUNITY_SEQUENCES_CRON.schedule,
|
|
674
|
+
name: HERMES_OPPORTUNITY_SEQUENCES_CRON.name,
|
|
675
|
+
deliver: HERMES_OPPORTUNITY_SEQUENCES_CRON.deliver,
|
|
676
|
+
skills: [...HERMES_OPPORTUNITY_SEQUENCES_CRON.skills],
|
|
677
|
+
enabled_toolsets: [
|
|
678
|
+
...HERMES_OPPORTUNITY_SEQUENCES_CRON.enabledToolsets,
|
|
490
679
|
],
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
680
|
+
no_agent: false,
|
|
681
|
+
enabled: true,
|
|
682
|
+
...(jobId ? { jobId } : {}),
|
|
683
|
+
};
|
|
684
|
+
return childRunner(runChild)({
|
|
685
|
+
command: hermesPython,
|
|
686
|
+
args: ["-c", OPPORTUNITY_SEQUENCE_CRON_PYTHON, JSON.stringify(spec)],
|
|
687
|
+
env: {
|
|
688
|
+
...process.env,
|
|
689
|
+
HOME: hermesRoot,
|
|
690
|
+
HERMES_HOME: profileRoot,
|
|
691
|
+
HERMES_PROFILE: profileId,
|
|
692
|
+
NO_COLOR: "1",
|
|
693
|
+
},
|
|
694
|
+
shell: false,
|
|
695
|
+
timeoutMs: NATIVE_COMMAND_TIMEOUT_MS,
|
|
696
|
+
maxOutputBytes: 64 * 1024,
|
|
697
|
+
});
|
|
698
|
+
};
|
|
699
|
+
const result = await ensureHermesNamedCron({
|
|
700
|
+
profileRoot,
|
|
701
|
+
name: HERMES_OPPORTUNITY_SEQUENCES_CRON.name,
|
|
702
|
+
isExpected: expected,
|
|
703
|
+
create: () => mutate(),
|
|
704
|
+
update: (job) => mutate(job.id),
|
|
705
|
+
codePrefix: "hermes_opportunity_sequences_cron",
|
|
706
|
+
});
|
|
707
|
+
if (!result.ok) return result;
|
|
708
|
+
const jobsMode =
|
|
709
|
+
lstatSync(join(profileRoot, "cron", "jobs.json")).mode & 0o777;
|
|
710
|
+
if ((jobsMode & 0o077) !== 0) {
|
|
711
|
+
return failed("hermes_opportunity_sequences_cron_jobs_mode_rejected");
|
|
497
712
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
:
|
|
713
|
+
return {
|
|
714
|
+
ok: true,
|
|
715
|
+
status: result.status,
|
|
716
|
+
jobId: result.jobId,
|
|
717
|
+
name: result.job.name,
|
|
718
|
+
prompt: result.job.prompt,
|
|
719
|
+
schedule: result.job.schedule,
|
|
720
|
+
profileId,
|
|
721
|
+
enabledToolsets: result.job.enabled_toolsets,
|
|
722
|
+
skills: result.job.skills,
|
|
723
|
+
assetHashes: hashes,
|
|
724
|
+
notificationSlackUserIdSource:
|
|
725
|
+
"config-state.json:opportunitySequences.notificationSlackUserId",
|
|
726
|
+
journalRoot,
|
|
727
|
+
modes: {
|
|
728
|
+
state: stateStat.mode & 0o777,
|
|
729
|
+
journal: journalStat.mode & 0o777,
|
|
730
|
+
jobs: jobsMode,
|
|
731
|
+
},
|
|
732
|
+
};
|
|
504
733
|
} catch {
|
|
505
|
-
return failed("
|
|
734
|
+
return failed("hermes_opportunity_sequences_cron_install_failed");
|
|
506
735
|
}
|
|
507
736
|
}
|
|
508
737
|
|
|
@@ -39,7 +39,7 @@ const PACKAGE_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
|
|
|
39
39
|
const VERSION = "sellable-agent-host-bootstrap/v1";
|
|
40
40
|
const RECEIPT_VERSION = "sellable-agent-host-registration/v1";
|
|
41
41
|
const INSTALLER_PACKAGE =
|
|
42
|
-
"@sellable/install@0.1.
|
|
42
|
+
"@sellable/install@0.1.589";
|
|
43
43
|
const MCP_PACKAGE = "@sellable/mcp@0.1.860";
|
|
44
44
|
const HERMES_VERSION = "0.18.0";
|
|
45
45
|
const SHA256 = /^[a-f0-9]{64}$/;
|
|
@@ -1546,7 +1546,7 @@ export function compileClaimToProfileDesired(activeClaim, config) {
|
|
|
1546
1546
|
]) ||
|
|
1547
1547
|
pinned.hermesCli !== "hermes" ||
|
|
1548
1548
|
pinned.hermesVersion !== "0.18.0" ||
|
|
1549
|
-
pinned.installerPackage !== "@sellable/install@0.1.
|
|
1549
|
+
pinned.installerPackage !== "@sellable/install@0.1.589" ||
|
|
1550
1550
|
pinned.mcpPackage !== "@sellable/mcp@0.1.860" ||
|
|
1551
1551
|
!Array.isArray(policy.toolInclude) ||
|
|
1552
1552
|
policy.toolInclude.length === 0 ||
|
|
@@ -202,7 +202,7 @@ function validateDesired(desired) {
|
|
|
202
202
|
desired.serviceCredentialGeneration < 1 ||
|
|
203
203
|
desired.hermesCli !== "hermes" ||
|
|
204
204
|
desired.hermesVersion !== "0.18.0" ||
|
|
205
|
-
desired.installerPackage !== "@sellable/install@0.1.
|
|
205
|
+
desired.installerPackage !== "@sellable/install@0.1.589" ||
|
|
206
206
|
desired.mcpPackage !== "@sellable/mcp@0.1.860" ||
|
|
207
207
|
!Array.isArray(desired.toolInclude) ||
|
|
208
208
|
desired.toolInclude.length === 0 ||
|
|
@@ -25,7 +25,7 @@ import { fileURLToPath } from "node:url";
|
|
|
25
25
|
import { deriveContainedProfileId } from "./profile-materializer.mjs";
|
|
26
26
|
|
|
27
27
|
export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
|
|
28
|
-
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.
|
|
28
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.589";
|
|
29
29
|
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.860";
|
|
30
30
|
|
|
31
31
|
export function deriveAgentProfileId(workspaceId, agentId, hostId) {
|
|
@@ -611,7 +611,7 @@ function inspectProvisionedProfile({
|
|
|
611
611
|
!matchesReadback(manifest, request) ||
|
|
612
612
|
manifest.serviceCredentialReference !== serviceCredentialReference ||
|
|
613
613
|
manifest.installerPackage !== PINNED_INSTALL_PACKAGE ||
|
|
614
|
-
manifest.installerVersion !== "0.1.
|
|
614
|
+
manifest.installerVersion !== "0.1.589" ||
|
|
615
615
|
manifest.mcpPackage !== PINNED_MCP_PACKAGE ||
|
|
616
616
|
manifest.credentialKeyVersion !== credentialKeyVersion ||
|
|
617
617
|
!/^[a-f0-9]{16}$/.test(manifest.credentialFingerprint) ||
|
|
@@ -825,7 +825,7 @@ function successReceipt({
|
|
|
825
825
|
subject: `agent-profile:${observed.profileId}`,
|
|
826
826
|
cli: {
|
|
827
827
|
installPackage: PINNED_INSTALL_PACKAGE,
|
|
828
|
-
installVersion: "0.1.
|
|
828
|
+
installVersion: "0.1.589",
|
|
829
829
|
mcpPackage: PINNED_MCP_PACKAGE,
|
|
830
830
|
command: "hermes profile bootstrap",
|
|
831
831
|
},
|
|
@@ -873,7 +873,7 @@ function failure(code, stages, extra = {}, request = null) {
|
|
|
873
873
|
: "agent-profile:unbound",
|
|
874
874
|
cli: {
|
|
875
875
|
installPackage: PINNED_INSTALL_PACKAGE,
|
|
876
|
-
installVersion: "0.1.
|
|
876
|
+
installVersion: "0.1.589",
|
|
877
877
|
mcpPackage: PINNED_MCP_PACKAGE,
|
|
878
878
|
command: "hermes profile bootstrap",
|
|
879
879
|
},
|