@sellable/install 0.1.411 → 0.1.413

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.411
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.413
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.644
8
8
  ARG INSTALLER_INTEGRITY
9
9
  ARG MCP_INTEGRITY=sha512-Q2+bBeQCKy/tZ9hMslOZ1Tw/kTLKBjiv77zQqX38IFrNevLiOhRG4rsxvVPqfHkOIIOSKFl/ECTgFD0pcmhAXg==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.411" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.413" \
12
12
  && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.644" \
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.411" \
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.411' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.644') 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.413' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.644') 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.411', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.644', 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.413', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.644', 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.411` is
6
+ `@sellable/install@0.1.413` 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.411 .
12
+ --tag sellable-agent-host:0.1.413 .
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`.
@@ -3,9 +3,9 @@ FROM ${HERMES_IMAGE}
3
3
 
4
4
  USER root
5
5
 
6
- ARG MCP_PACKAGE=@sellable/mcp@0.1.710-uat.refill-zero-shot.20260723123117
7
- ARG MCP_INTEGRITY=sha512-KvIRcIPQR/3iviWNcpq4EgrR/gEuOOqV9fngbaF/Ch/E7bBx0oAsFDu+0YrKvPc+l95ia/ddyvxWmpuRpMGHGw==
8
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.411
6
+ ARG MCP_PACKAGE=@sellable/mcp@0.1.745
7
+ ARG MCP_INTEGRITY=sha512-3PhNu92HGbWN38vPmpvxpb2lu0C4TQpjhNdk8+2RsVCTMBHcRoFSSsaMkmlhpGHeH7fftf2uMqFH+04sxS374Q==
8
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.413
9
9
  ARG HERMES_VERSION=0.19.0
10
10
  ARG MCP_TRUST_ROOT_B64
11
11
  ARG TIRITH_VERSION=0.3.3
@@ -11,20 +11,21 @@ import {
11
11
  openSync,
12
12
  readFileSync,
13
13
  renameSync,
14
+ unlinkSync,
14
15
  writeFileSync,
15
16
  } from "node:fs";
16
17
  import { dirname, join } from "node:path";
17
- import {
18
- buildNativeHermesLaunch,
19
- clearCustomerRuntimeSecrets,
20
- installCustomerRuntimeSecrets,
21
- } from "../fly-customer-worker.mjs";
22
18
  import {
23
19
  FLY_CUSTOMER_MODEL_ID,
24
20
  FLY_CUSTOMER_MODEL_PROVIDER,
25
21
  FLY_CUSTOMER_MODEL_REASONING_EFFORT,
26
22
  parseFlyCustomerModelCredential,
27
23
  } from "../fly-customer-model.mjs";
24
+ import {
25
+ buildNativeHermesLaunch,
26
+ clearCustomerRuntimeSecrets,
27
+ installCustomerRuntimeSecrets,
28
+ } from "../fly-customer-worker.mjs";
28
29
  import {
29
30
  createFlyCustomerControlClient,
30
31
  dropFlyRuntimePrivileges,
@@ -46,6 +47,7 @@ const PROFILES_ROOT = join(DATA_ROOT, "profiles");
46
47
  const SECRET_ROOT = "/run/sellable-agent";
47
48
  const TRUST_ROOT = "/usr/local/share/sellable-agent/mcp-turn-trust-root.pem";
48
49
  const HERMES = "/opt/hermes/.venv/bin/hermes";
50
+ const HERMES_PYTHON = "/opt/hermes/.venv/bin/python3";
49
51
  const MCP =
50
52
  "/usr/local/lib/sellable-agent/external-runtime/mcp/bin/sellable-mcp";
51
53
  const RUNTIME_ID = process.env.SELLABLE_RUNTIME_ID ?? "";
@@ -60,11 +62,9 @@ const RUNTIME_GENERATION = Number(
60
62
  const BOOT_SESSION_ID = randomUUID();
61
63
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.19.0";
62
64
  const INSTALLER_PACKAGE =
63
- process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ??
64
- "@sellable/install@0.1.411";
65
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.413";
65
66
  const MCP_PACKAGE =
66
- process.env.SELLABLE_AGENT_MCP_PACKAGE ??
67
- "@sellable/mcp@0.1.710-uat.refill-zero-shot.20260723123117";
67
+ process.env.SELLABLE_AGENT_MCP_PACKAGE ?? "@sellable/mcp@0.1.745";
68
68
 
69
69
  const sha256 = (value) => createHash("sha256").update(value).digest("hex");
70
70
  const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
@@ -328,6 +328,42 @@ function writeRuntimeHermesConfig({
328
328
  const runtimeConfigRoot = profileRoot;
329
329
  mkdirSync(runtimeConfigRoot, { recursive: true, mode: 0o700 });
330
330
  chmodSync(runtimeConfigRoot, 0o700);
331
+ const priorIdentityPath = join(runtimeConfigRoot, "runtime-identity.json");
332
+ let priorModelCredentialGeneration = null;
333
+ if (existsSync(priorIdentityPath)) {
334
+ try {
335
+ const priorIdentity = JSON.parse(readFileSync(priorIdentityPath, "utf8"));
336
+ if (
337
+ Number.isSafeInteger(priorIdentity.modelCredentialGeneration) &&
338
+ priorIdentity.modelCredentialGeneration > 0
339
+ ) {
340
+ priorModelCredentialGeneration =
341
+ priorIdentity.modelCredentialGeneration;
342
+ }
343
+ } catch {
344
+ priorModelCredentialGeneration = null;
345
+ }
346
+ }
347
+ const localHermesAuthStorePath = join(runtimeConfigRoot, "auth.json");
348
+ const hermesRootAuthStorePath = join(DATA_ROOT, "auth.json");
349
+ if (existsSync(localHermesAuthStorePath)) {
350
+ const localAuthStore = lstatSync(localHermesAuthStorePath);
351
+ if (localAuthStore.isSymbolicLink() || !localAuthStore.isFile()) {
352
+ throw new Error("customer_model_auth_store_rejected");
353
+ }
354
+ unlinkSync(localHermesAuthStorePath);
355
+ }
356
+ if (
357
+ priorModelCredentialGeneration !== null &&
358
+ priorModelCredentialGeneration !== desired.modelCredentialGeneration &&
359
+ existsSync(hermesRootAuthStorePath)
360
+ ) {
361
+ const authStore = lstatSync(hermesRootAuthStorePath);
362
+ if (authStore.isSymbolicLink() || !authStore.isFile()) {
363
+ throw new Error("customer_model_auth_store_rejected");
364
+ }
365
+ unlinkSync(hermesRootAuthStorePath);
366
+ }
331
367
  const mcpConfig = JSON.parse(
332
368
  readFileSync(join(profileRoot, "sellable", "mcp.json"), "utf8")
333
369
  );
@@ -362,6 +398,21 @@ function writeRuntimeHermesConfig({
362
398
  ? process.env.SELLABLE_AGENT_TEST_MODEL_ORIGIN
363
399
  : null;
364
400
  const modelCredential = parseFlyCustomerModelCredential(secrets.modelCodex);
401
+ const modelAuthPath = join(secretRoot, "auth.json");
402
+ atomicFile(modelAuthPath, modelCredential.authJson, 0o600);
403
+ if (!existsSync(hermesRootAuthStorePath)) {
404
+ atomicFile(
405
+ hermesRootAuthStorePath,
406
+ modelCredential.hermesRootAuthJson,
407
+ 0o600
408
+ );
409
+ } else {
410
+ const authStore = lstatSync(hermesRootAuthStorePath);
411
+ if (authStore.isSymbolicLink() || !authStore.isFile()) {
412
+ throw new Error("customer_model_auth_store_rejected");
413
+ }
414
+ chmodSync(hermesRootAuthStorePath, 0o600);
415
+ }
365
416
  atomicJson(runtimeConfigPath, {
366
417
  ...baseConfig,
367
418
  ...(testModelOrigin
@@ -439,6 +490,8 @@ function writeRuntimeHermesConfig({
439
490
  runtimeConfigPath,
440
491
  runtimeMcpPath,
441
492
  gatesPath,
493
+ modelAuthPath,
494
+ hermesRootAuthStorePath,
442
495
  modelCredential,
443
496
  };
444
497
  }
@@ -477,24 +530,25 @@ function applicationRuntimeEnv({ desired, runtimeConfig, secrets }) {
477
530
  HOME: DATA_ROOT,
478
531
  HERMES_HOME: DATA_ROOT,
479
532
  HERMES_CONFIG_PATH: runtimeConfig.runtimeConfigPath,
480
- CODEX_HOME: join(SECRET_ROOT, desired.profileId),
533
+ CODEX_HOME: dirname(runtimeConfig.modelAuthPath),
481
534
  SELLABLE_AGENT_RUNTIME: "1",
482
535
  SELLABLE_AGENT_RUNTIME_GATE_FILE: runtimeConfig.gatesPath,
483
536
  SLACK_HOME_CHANNEL: desired.defaultChannelId,
484
537
  SLACK_ALLOWED_CHANNELS: desired.selectedChannelIds.join(","),
485
538
  SLACK_ALLOWED_USERS: desired.memberAllowlist.join(","),
486
539
  SLACK_REQUIRE_MENTION: "true",
487
- SLACK_STRICT_MENTION: "true",
540
+ SLACK_STRICT_MENTION: "false",
488
541
  SLACK_ALLOW_BOTS: "false",
489
542
  ...(allowTestProviders()
490
543
  ? {
491
544
  SELLABLE_AGENT_TEST_MODEL_KEY:
492
- runtimeConfig.modelCredential.apiKey,
545
+ runtimeConfig.modelCredential.testAccessToken,
493
546
  }
494
- : { OPENAI_API_KEY: runtimeConfig.modelCredential.apiKey }),
547
+ : {}),
495
548
  NO_COLOR: "1",
496
549
  };
497
550
  delete env.OPENAI_BASE_URL;
551
+ delete env.OPENAI_API_KEY;
498
552
  delete env.OPENROUTER_API_KEY;
499
553
  delete env.SLACK_APP_TOKEN;
500
554
  delete env.SLACK_BOT_TOKEN;
@@ -636,23 +690,61 @@ async function proveProvisionedApplication({
636
690
  profileDigest,
637
691
  }) {
638
692
  const runtimeEnv = applicationRuntimeEnv({ desired, runtimeConfig, secrets });
639
- const [hermesVersion, mcpVersion] = await Promise.all([
693
+ const [hermesVersion, mcpVersion, modelAuth] = await Promise.all([
640
694
  run(HERMES, ["--version"], { env: runtimeEnv, timeoutMs: 180_000 }),
641
695
  run(MCP, ["--version"], { env: runtimeEnv, timeoutMs: 180_000 }),
696
+ run(
697
+ HERMES_PYTHON,
698
+ [
699
+ "-c",
700
+ [
701
+ "import json",
702
+ "from pathlib import Path",
703
+ "from hermes_constants import get_default_hermes_root, get_hermes_home",
704
+ "from hermes_cli.auth import resolve_codex_runtime_credentials",
705
+ "value = resolve_codex_runtime_credentials(refresh_if_expiring=False)",
706
+ 'root_auth = get_default_hermes_root() / "auth.json"',
707
+ 'local_auth = get_hermes_home() / "auth.json"',
708
+ 'print(json.dumps({"provider": value.get("provider"), "source": value.get("source"), "source_class": "root_fallback" if root_auth.is_file() and not local_auth.exists() else "unexpected", "auth_mode": value.get("auth_mode"), "credential_present": bool(value.get("api_key")), "local_auth_entry_count": 0 if not local_auth.exists() else 1}))',
709
+ ].join(";"),
710
+ ],
711
+ {
712
+ env: { ...runtimeEnv, HERMES_HOME: runtimeConfig.runtimeHome },
713
+ timeoutMs: 180_000,
714
+ }
715
+ ),
642
716
  ]);
717
+ let modelAuthReadback = null;
718
+ try {
719
+ modelAuthReadback = JSON.parse(modelAuth.stdout);
720
+ } catch {
721
+ modelAuthReadback = null;
722
+ }
643
723
  if (
644
724
  hermesVersion.status !== 0 ||
645
725
  mcpVersion.status !== 0 ||
726
+ modelAuth.status !== 0 ||
727
+ modelAuthReadback?.provider !== FLY_CUSTOMER_MODEL_PROVIDER ||
728
+ modelAuthReadback?.source !== "hermes-auth-store" ||
729
+ modelAuthReadback?.source_class !== "root_fallback" ||
730
+ modelAuthReadback?.auth_mode !== "chatgpt" ||
731
+ modelAuthReadback?.credential_present !== true ||
732
+ modelAuthReadback?.local_auth_entry_count !== 0 ||
646
733
  /Lazy-installing|pip install|npm install/i.test(
647
- `${hermesVersion.stdout}\n${hermesVersion.stderr}\n${mcpVersion.stdout}\n${mcpVersion.stderr}`
734
+ `${hermesVersion.stdout}\n${hermesVersion.stderr}\n${mcpVersion.stdout}\n${mcpVersion.stderr}\n${modelAuth.stdout}\n${modelAuth.stderr}`
648
735
  )
649
736
  ) {
650
- throw new Error("customer_runtime_binary_rejected");
737
+ throw new Error(
738
+ modelAuth.status !== 0 || !modelAuthReadback
739
+ ? "customer_model_auth_readback_rejected"
740
+ : "customer_runtime_binary_rejected"
741
+ );
651
742
  }
652
743
  return {
653
744
  profileDigest,
654
745
  hermesOutput: hermesVersion.stdout,
655
746
  mcpOutput: mcpVersion.stdout,
747
+ modelAuthOutput: modelAuth.stdout,
656
748
  };
657
749
  }
658
750
 
@@ -746,7 +838,9 @@ function completionReceipt({ claim, exchange, desired, proof }) {
746
838
  package: sha256(`${INSTALLER_PACKAGE}\n${MCP_PACKAGE}\n${HERMES_VERSION}`),
747
839
  config: proof.profileDigest,
748
840
  content: sha256(`${desired.soul}\n${desired.skill}`),
749
- process: sha256(`${proof.hermesOutput}\n${proof.mcpOutput}`),
841
+ process: sha256(
842
+ `${proof.hermesOutput}\n${proof.mcpOutput}\n${proof.modelAuthOutput}`
843
+ ),
750
844
  };
751
845
  return {
752
846
  ...identity,
@@ -773,6 +867,7 @@ function completionReceipt({ claim, exchange, desired, proof }) {
773
867
  { code: "NATIVE_SLACK_GATEWAY", status: "TRUE" },
774
868
  ]),
775
869
  { code: "MCP_PREBAKED", status: "TRUE" },
870
+ { code: "CODEX_OAUTH_READBACK", status: "TRUE" },
776
871
  { code: "NO_BOOT_INSTALL", status: "TRUE" },
777
872
  { code: "DEDICATED_SLACK_APP", status: "TRUE" },
778
873
  ],
@@ -1,11 +1,39 @@
1
- export const FLY_CUSTOMER_MODEL_PROVIDER = "openai-api";
1
+ export const FLY_CUSTOMER_MODEL_PROVIDER = "openai-codex";
2
2
  export const FLY_CUSTOMER_MODEL_ID = "gpt-5.6-sol";
3
3
  export const FLY_CUSTOMER_MODEL_REASONING_EFFORT = "high";
4
4
 
5
- const MODEL_CREDENTIAL_KEYS = ["OPENAI_API_KEY", "authMode"];
6
- const OPENAI_API_KEY = /^sk-[A-Za-z0-9_-]{16,512}$/;
5
+ const MODEL_CREDENTIAL_KEYS = [
6
+ "OPENAI_API_KEY",
7
+ "auth_mode",
8
+ "last_refresh",
9
+ "tokens",
10
+ ];
11
+ const MODEL_TOKEN_KEYS = [
12
+ "access_token",
13
+ "account_id",
14
+ "id_token",
15
+ "refresh_token",
16
+ ];
17
+ const exactKeys = (value, expected) =>
18
+ value &&
19
+ typeof value === "object" &&
20
+ !Array.isArray(value) &&
21
+ Object.keys(value).length === expected.length &&
22
+ Object.keys(value).every((key) => expected.includes(key));
23
+ const bounded = (value, minimum, maximum) =>
24
+ typeof value === "string" &&
25
+ value.length >= minimum &&
26
+ value.length <= maximum &&
27
+ !value.includes("\0");
7
28
 
8
29
  export function parseFlyCustomerModelCredential(secret) {
30
+ if (
31
+ typeof secret !== "string" ||
32
+ !secret ||
33
+ Buffer.byteLength(secret, "utf8") > 64 * 1024
34
+ ) {
35
+ throw new Error("customer_model_credential_rejected");
36
+ }
9
37
  let value;
10
38
  try {
11
39
  value = JSON.parse(secret);
@@ -13,21 +41,69 @@ export function parseFlyCustomerModelCredential(secret) {
13
41
  throw new Error("customer_model_credential_rejected");
14
42
  }
15
43
  if (
16
- !value ||
17
- typeof value !== "object" ||
18
- Array.isArray(value) ||
19
- Object.keys(value).sort().join("\0") !==
20
- [...MODEL_CREDENTIAL_KEYS].sort().join("\0") ||
21
- value.authMode !== "api-key" ||
22
- typeof value.OPENAI_API_KEY !== "string" ||
23
- !OPENAI_API_KEY.test(value.OPENAI_API_KEY)
44
+ !exactKeys(value, MODEL_CREDENTIAL_KEYS) ||
45
+ value.auth_mode !== "chatgpt" ||
46
+ value.OPENAI_API_KEY !== null ||
47
+ !bounded(value.last_refresh, 10, 128) ||
48
+ Number.isNaN(Date.parse(value.last_refresh)) ||
49
+ !exactKeys(value.tokens, MODEL_TOKEN_KEYS) ||
50
+ !bounded(value.tokens.id_token, 32, 32 * 1024) ||
51
+ !bounded(value.tokens.access_token, 32, 32 * 1024) ||
52
+ !bounded(value.tokens.refresh_token, 32, 16 * 1024) ||
53
+ !bounded(value.tokens.account_id, 1, 512)
24
54
  ) {
25
55
  throw new Error("customer_model_credential_rejected");
26
56
  }
57
+ const authJson = JSON.stringify({
58
+ auth_mode: "chatgpt",
59
+ OPENAI_API_KEY: null,
60
+ tokens: {
61
+ id_token: value.tokens.id_token,
62
+ access_token: value.tokens.access_token,
63
+ refresh_token: value.tokens.refresh_token,
64
+ account_id: value.tokens.account_id,
65
+ },
66
+ last_refresh: value.last_refresh,
67
+ });
68
+ const hermesRootAuthJson = JSON.stringify({
69
+ version: 1,
70
+ providers: {
71
+ "openai-codex": {
72
+ tokens: {
73
+ id_token: value.tokens.id_token,
74
+ access_token: value.tokens.access_token,
75
+ refresh_token: value.tokens.refresh_token,
76
+ account_id: value.tokens.account_id,
77
+ },
78
+ last_refresh: value.last_refresh,
79
+ auth_mode: "chatgpt",
80
+ },
81
+ },
82
+ credential_pool: {
83
+ "openai-codex": [
84
+ {
85
+ id: "sellable-control-plane",
86
+ label: "Sellable Codex OAuth",
87
+ auth_type: "oauth",
88
+ source: "device_code",
89
+ access_token: value.tokens.access_token,
90
+ refresh_token: value.tokens.refresh_token,
91
+ id_token: value.tokens.id_token,
92
+ account_id: value.tokens.account_id,
93
+ last_refresh: value.last_refresh,
94
+ priority: 0,
95
+ request_count: 0,
96
+ },
97
+ ],
98
+ },
99
+ active_provider: "openai-codex",
100
+ });
27
101
  return Object.freeze({
28
102
  provider: FLY_CUSTOMER_MODEL_PROVIDER,
29
103
  model: FLY_CUSTOMER_MODEL_ID,
30
104
  reasoningEffort: FLY_CUSTOMER_MODEL_REASONING_EFFORT,
31
- apiKey: value.OPENAI_API_KEY,
105
+ authJson,
106
+ hermesRootAuthJson,
107
+ testAccessToken: value.tokens.access_token,
32
108
  });
33
109
  }
@@ -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.411";
42
+ "@sellable/install@0.1.413";
43
43
  const MCP_PACKAGE = "@sellable/mcp@0.1.644";
44
44
  const HERMES_VERSION = "0.18.0";
45
45
  const SHA256 = /^[a-f0-9]{64}$/;
@@ -1514,7 +1514,7 @@ export function compileClaimToProfileDesired(activeClaim, config) {
1514
1514
  pinned.hermesCli !== "hermes" ||
1515
1515
  pinned.hermesVersion !== "0.18.0" ||
1516
1516
  pinned.installerPackage !==
1517
- "@sellable/install@0.1.411" ||
1517
+ "@sellable/install@0.1.413" ||
1518
1518
  pinned.mcpPackage !== "@sellable/mcp@0.1.644" ||
1519
1519
  !Array.isArray(policy.toolInclude) ||
1520
1520
  policy.toolInclude.length === 0 ||
@@ -193,7 +193,7 @@ function validateDesired(desired) {
193
193
  desired.hermesCli !== "hermes" ||
194
194
  desired.hermesVersion !== "0.18.0" ||
195
195
  desired.installerPackage !==
196
- "@sellable/install@0.1.411" ||
196
+ "@sellable/install@0.1.413" ||
197
197
  desired.mcpPackage !== "@sellable/mcp@0.1.644" ||
198
198
  !Array.isArray(desired.toolInclude) ||
199
199
  desired.toolInclude.length === 0 ||
@@ -2133,7 +2133,7 @@ function nativeSlackCustomerFiles(desired) {
2133
2133
  },
2134
2134
  slack: {
2135
2135
  require_mention: true,
2136
- strict_mention: true,
2136
+ strict_mention: false,
2137
2137
  allow_bots: false,
2138
2138
  allowed_channels: [...desired.selectedChannelIds],
2139
2139
  },
@@ -26,7 +26,7 @@ import { deriveContainedProfileId } from "./profile-materializer.mjs";
26
26
 
27
27
  export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
28
28
  export const PINNED_INSTALL_PACKAGE =
29
- "@sellable/install@0.1.411";
29
+ "@sellable/install@0.1.413";
30
30
  export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.644";
31
31
 
32
32
  export function deriveAgentProfileId(workspaceId, agentId, hostId) {
@@ -613,7 +613,7 @@ function inspectProvisionedProfile({
613
613
  manifest.serviceCredentialReference !== serviceCredentialReference ||
614
614
  manifest.installerPackage !== PINNED_INSTALL_PACKAGE ||
615
615
  manifest.installerVersion !==
616
- "0.1.411" ||
616
+ "0.1.413" ||
617
617
  manifest.mcpPackage !== PINNED_MCP_PACKAGE ||
618
618
  manifest.credentialKeyVersion !== credentialKeyVersion ||
619
619
  !/^[a-f0-9]{16}$/.test(manifest.credentialFingerprint) ||
@@ -827,7 +827,7 @@ function successReceipt({
827
827
  subject: `agent-profile:${observed.profileId}`,
828
828
  cli: {
829
829
  installPackage: PINNED_INSTALL_PACKAGE,
830
- installVersion: "0.1.411",
830
+ installVersion: "0.1.413",
831
831
  mcpPackage: PINNED_MCP_PACKAGE,
832
832
  command: "hermes profile bootstrap",
833
833
  },
@@ -875,7 +875,7 @@ function failure(code, stages, extra = {}, request = null) {
875
875
  : "agent-profile:unbound",
876
876
  cli: {
877
877
  installPackage: PINNED_INSTALL_PACKAGE,
878
- installVersion: "0.1.411",
878
+ installVersion: "0.1.413",
879
879
  mcpPackage: PINNED_MCP_PACKAGE,
880
880
  command: "hermes profile bootstrap",
881
881
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.411",
3
+ "version": "0.1.413",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {
@@ -74,10 +74,14 @@ get_subskill_prompt({ subskillName: "refill-sends-workflow" })
74
74
  get_subskill_asset({ subskillName: "refill-sends-workflow", assetPath: "core/flow.v1.json" })
75
75
  ```
76
76
 
77
- Continue chunks until `hasMore:false`; require
78
- exactly `workflow:"refill-sends-workflow"` and `version:"v1.8"`, then report
79
- that verified version before D1. Any other or unverified version is
80
- `workflow_version_mismatch`: stop before `get_refill_target_plan` or mutation.
77
+ Continue chunks until `hasMore:false`; require `workflow:"refill-sends-workflow"`
78
+ and a COMPATIBLE version: same major (`v1`) and minor at or above the minimum
79
+ `v1.8` so `v1.8`, `v1.9`, `v1.10` all pass and `v2.x` does not. Report the
80
+ verified version before D1. A missing, unparseable, wrong-major, or
81
+ below-minimum version is `workflow_version_mismatch`: stop before
82
+ `get_refill_target_plan` or mutation. Do NOT require exact string equality: the
83
+ flow asset ships in the npm package while this skill is installed separately,
84
+ so an exact pin makes every additive contract change a breaking one.
81
85
  Route intent is immutable:
82
86
  workspace-wide `--yolo` uses `intent:"auto"` from first plan through terminal.
83
87
 
@@ -484,9 +488,13 @@ flowchart TD
484
488
 
485
489
  One scheduler-loaded exact lane is not a workspace terminal: skip it and
486
490
  continue other planner-ranked lanes. Do not keep polling after the coordinator
487
- returns workspace `loaded_awaiting_scheduler`, which is valid only when every
488
- remaining exact target is complete or scheduler-owned and workspace
489
- `remainingReadyOrProjectedGap` is zero. It is not permission for a host-owned
491
+ returns workspace `loaded_awaiting_scheduler`. That terminal is admissible by
492
+ either path: every remaining exact target is complete or scheduler-owned and
493
+ workspace `remainingReadyOrProjectedGap` is zero, OR every gap-bearing sender
494
+ lane is already prepared and its residue is `ready_to_schedule` surplus, so the
495
+ remaining gap is scheduler/pacing-bound rather than unexposed supply — on that
496
+ second path `remainingReadyOrProjectedGap` is not necessarily zero. Neither path
497
+ means unexposed actionable supply, and neither is permission for a host-owned
490
498
  loop. The sole refill-ladder expansion
491
499
  that asks the user in `--yolo` is `new_campaign_required`; refill itself never
492
500
  creates that campaign. `new_campaign_required` is also the truthful terminal
@@ -497,7 +505,17 @@ remaining gap and ask the user; a future handler may automate the creation. A
497
505
  (approval/generate/enrich/rubric or unprocessed frontier rows) still exist but
498
506
  no executable exact edge surfaced: retry once with a fresh command, and if it
499
507
  repeats report it as a planner-exposure defect instead of treating the
500
- workspace as exhausted.
508
+ workspace as exhausted. That census counts CONVERTIBLE rows only, de-duplicated
509
+ per campaign/table lane — a frontier whose own preparation receipt carries a
510
+ non-retryable `icp_or_rubric_rejection` diagnosis is proven unable to convert,
511
+ so it never inflates the census. When such receipts are all that remain, the
512
+ terminal is `source_supply_exhausted_at_rubric`: a truthful source exhaustion,
513
+ not a defect and not retryable. Report the per-lane evidence from the note and
514
+ tell the user that widening the rubric or adding fresh source rows is the only
515
+ remaining fill path. Whenever a terminal reports a non-empty census, it also
516
+ names every exact edge the run refused (`Exact edges this run refused: ...`) —
517
+ surface those named blockers rather than describing the workspace as having no
518
+ available work.
501
519
 
502
520
  ## Non-negotiable authority and proof
503
521
 
@@ -33,7 +33,11 @@ You are a pipeline supply agent. People who engage with a sender's LinkedIn post
33
33
  <inputs>
34
34
  The invoking prompt names the senders ("refresh sender engagement for csreyes92 and thomas"). Resolve each via `list_senders` (match name/handle/LinkedIn URL). With no names given, inspect the active workspace and refresh every connected sender that has an active/paused sender-owned Post Engagers campaign backed by Signal Discovery.
35
35
 
36
- Optional: target sender names/ids, `tableId` when the user wants to force a specific campaign table, maximum posts per sender (default 5, hard cap 5 unless the user explicitly asks for more), and maximum engager pages per tracked post.
36
+ Optional: target sender names/ids, `campaignId` when the user wants to force a specific Post Engagers campaign, `tableId` when the user wants to force a specific campaign table, `maxPosts` to refresh a specific number of posts, and maximum engager pages per tracked post.
37
+
38
+ Leave `maxPosts` unset unless the user asked for a specific number. By default the command refreshes every tracked post published inside the 30-day lookback window, which is what keeps recent posts checked often enough to catch new engagers. A post whose engagement counts have not moved costs a single cheap count probe and is skipped before any engager fetch, so a wider sweep is not a proportionally more expensive one. Passing `maxPosts` narrows that sweep.
39
+
40
+ `campaignId` is optional. When it is omitted the command discovers the sender's Post Engagers campaign itself — it searches the workspace for campaigns the sender solely owns that are backed by a post-engager provider and named as a Post Engagers lane. A workspace can hold one such lane per sender, so discovery is always scoped to the sender you are refreshing; another sender's lane and shared multi-sender lanes are never selected. Do not pass a `campaignId` you guessed.
37
41
  </inputs>
38
42
 
39
43
  <entrypoint>
@@ -58,9 +62,12 @@ For each target sender/campaign:
58
62
  - Resolve each target sender with `list_senders`/`get_sender`. Use the sender id returned by the product, not a guessed handle.
59
63
  2. **Use the typed product command first**:
60
64
  - Call `refresh_sender_engagement` in `mode:"dry_run"` for each sender.
61
- - Always pass `workspaceId`, `senderId`, and any user-specified `tableId`, `maxPosts`, or `maxEngagerPages`.
65
+ - Always pass `workspaceId`, `senderId`, and any user-specified `campaignId`, `tableId`, `maxPosts`, or `maxEngagerPages`. Omitting `campaignId` is correct and normal — the command resolves the campaign itself.
66
+ - Read `discovery` on the response to see how the campaign was resolved (`managed_slot`, `explicit_campaign_id`, or `auto_discovered`) and which one was selected.
67
+ - If `discovery.additionalCandidates` is non-empty, that sender owns more than one Post Engagers lane. Refresh each one by repeating the dry-run/apply cycle with that candidate's `campaignId`. Every lane gets its own dry run and its own `dryRunFingerprint`; never reuse a fingerprint across campaigns.
62
68
  - Treat the dry-run response as the campaign/source boundary authority. It should identify the sender-owned Post Engagers slot, campaign, workflow table, source provider `campaign-tracked-post`, source table type `tracked_post_engager_source_list`, expected tracked posts, expected engager refresh/import work, and a `dryRunFingerprint`.
63
69
  - If the typed command reports no eligible sender-owned Post Engagers campaign, no tracked posts, no recent posts, or a workspace/sender mismatch, report that exact blocker and stop for that sender. Do not silently create campaigns or fall back to shared lanes.
70
+ - `sender_owned_post_engagers_slot_not_found` now means discovery genuinely found no eligible lane, not that a campaign id was missing. Report `discovery.campaignsInspected` alongside it, and describe it as missing campaign setup — never as "the sender has no posts", since no LinkedIn fetch happens before this point.
64
71
  3. **Gate writes with dry-run proof**:
65
72
  - Never call `refresh_sender_engagement` in `mode:"apply"` before a successful dry run from this same run.
66
73
  - For manual runs, show the target workspace, sender, campaign/table, source lead list, expected tracked posts, expected row/import impact, and `dryRunFingerprint`, then wait for explicit user approval before apply.
@@ -79,7 +86,7 @@ For each target sender/campaign:
79
86
  - LinkedIn operations here are read-only fetches plus a typed product import into a campaign table. **No messages are generated, approved, scheduled, or sent by this skill.**
80
87
  - Respect workspace boundaries: only add leads to campaigns in the active workspace, and only for senders that belong to it.
81
88
  - Respect campaign boundaries: only add engagers to the matched sender-owned Post Engagers campaign/table. Do not mix shared-lane engagers into sender-owned campaigns or sender-owned engagers into shared lanes.
82
- - Cap provider usage per run: at most 5 posts × `fetch_post_engagers` per sender. If the invoking automation wants more, it must say so explicitly.
89
+ - Provider usage per run is bounded by the 30-day lookback window plus the count-probe short-circuit: unchanged posts cost one probe and fetch no engagers. Do not widen `maxEngagerPages` beyond what the user asked for.
83
90
  - Never call `start_campaign`, `attach_sequence`, `queue_campaign_cells`, `start_campaign_message_preparation`, approval tools, send/schedule tools, or shared-lane mutation tools.
84
91
  - Never create campaigns, change campaign status, change sender ownership, approve campaign cells, or start message prep as part of this refresh.
85
92
  </safety>