@sellable/install 0.1.412 → 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.
- package/container/Dockerfile +4 -4
- package/container/README.md +2 -2
- package/lib/sellable-agent/fly-customer-image/Dockerfile +3 -3
- package/lib/sellable-agent/fly-customer-image/customer-runtime.mjs +111 -16
- package/lib/sellable-agent/fly-customer-model.mjs +88 -12
- 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/skill-templates/refill-sends.md +8 -4
- package/skill-templates/refresh-sender-engagement.md +10 -3
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.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.
|
|
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.412" \
|
|
|
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.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.
|
|
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
|
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.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.
|
|
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.
|
|
7
|
-
ARG MCP_INTEGRITY=sha512-
|
|
8
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.
|
|
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.412";
|
|
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,7 +530,7 @@ 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:
|
|
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,
|
|
@@ -489,12 +542,13 @@ function applicationRuntimeEnv({ desired, runtimeConfig, secrets }) {
|
|
|
489
542
|
...(allowTestProviders()
|
|
490
543
|
? {
|
|
491
544
|
SELLABLE_AGENT_TEST_MODEL_KEY:
|
|
492
|
-
runtimeConfig.modelCredential.
|
|
545
|
+
runtimeConfig.modelCredential.testAccessToken,
|
|
493
546
|
}
|
|
494
|
-
: {
|
|
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(
|
|
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(
|
|
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-
|
|
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 = [
|
|
6
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
value.
|
|
22
|
-
|
|
23
|
-
!
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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 ||
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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.
|
|
878
|
+
installVersion: "0.1.413",
|
|
879
879
|
mcpPackage: PINNED_MCP_PACKAGE,
|
|
880
880
|
command: "hermes profile bootstrap",
|
|
881
881
|
},
|
package/package.json
CHANGED
|
@@ -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
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
|
|
@@ -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, `
|
|
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
|
-
-
|
|
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>
|