@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.
@@ -3,12 +3,12 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.588
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.588" \
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.588' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.860') throw new Error('package identity rejected');" \
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.588', 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}" \
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
@@ -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.588` is
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.588 .
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 === "sellable") continue;
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.588
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.588";
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
- path = request["path"]
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 before = cronJobs(profileRoot).filter(
434
- (job) => job?.name === HERMES_SNAPSHOT_CRON.name
435
- );
436
- if (
437
- before.length > 1 ||
438
- (before[0] && !isCron(before[0]) && !isLegacyCron(before[0]))
439
- ) {
440
- return failed("hermes_snapshot_cron_inventory_rejected");
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
- "edit",
451
- before[0].id,
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
- if (childFailed(migrated)) {
461
- return failed("hermes_snapshot_cron_migrate_failed");
462
- }
463
- const afterMigration = cronJobs(profileRoot).filter(
464
- (job) => job?.name === HERMES_SNAPSHOT_CRON.name
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
- : failed("hermes_snapshot_cron_migrate_readback_rejected");
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 { ok: true, status: "REUSED", jobId: before[0].id, profileId };
556
+ return result;
476
557
  }
477
- const result = await childRunner(runChild)(
478
- invocation(
479
- [
480
- "--profile",
481
- profileId,
482
- "cron",
483
- "create",
484
- HERMES_SNAPSHOT_CRON.schedule,
485
- "--name",
486
- HERMES_SNAPSHOT_CRON.name,
487
- "--script",
488
- HERMES_SNAPSHOT_CRON.script,
489
- "--no-agent",
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
- hermesRoot,
492
- hermesCli
493
- )
494
- );
495
- if (childFailed(result)) {
496
- return failed("hermes_snapshot_cron_create_failed");
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
- const after = cronJobs(profileRoot).filter(
499
- (job) => job?.name === HERMES_SNAPSHOT_CRON.name
500
- );
501
- return after.length === 1 && isCron(after[0])
502
- ? { ok: true, status: "CREATED", jobId: after[0].id, profileId }
503
- : failed("hermes_snapshot_cron_readback_rejected");
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("hermes_snapshot_cron_install_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.588";
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.588" ||
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.588" ||
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.588";
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.588" ||
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.588",
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.588",
876
+ installVersion: "0.1.589",
877
877
  mcpPackage: PINNED_MCP_PACKAGE,
878
878
  command: "hermes profile bootstrap",
879
879
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.588",
3
+ "version": "0.1.590-wip.phase187.20260811.3",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {