@sellable/install 0.1.363 → 0.1.364-phase111.20260720135054
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/bin/sellable-install.mjs +2 -7
- package/lib/sellable-agent/hermes-bridge.mjs +4 -4
- package/lib/sellable-agent/host-worker.mjs +20 -16
- package/lib/sellable-agent/profile-materializer.mjs +2 -2
- package/lib/sellable-agent/provisioning-adapter.mjs +5 -5
- package/lib/sellable-agent/runtime-helper.mjs +10 -107
- package/lib/sellable-agent/service-installer.mjs +1 -3
- package/package.json +1 -3
- package/skill-templates/refill-sends-evergreen.md +5 -0
- package/skill-templates/refill-sends-v2.md +36 -1
- package/skill-templates/refill-sends.md +49 -1
- package/bin/sellable-agent-host-bootstrap.mjs +0 -26
- package/container/Dockerfile +0 -41
- package/container/README.md +0 -15
- package/container/compose.yaml +0 -22
- package/container/entrypoint.sh +0 -17
- package/lib/sellable-agent/external-runtime-builder.mjs +0 -272
- package/lib/sellable-agent/host-bootstrap.mjs +0 -458
package/bin/sellable-install.mjs
CHANGED
|
@@ -3712,8 +3712,7 @@ function upsertCodexMcpServerConfig(content, opts) {
|
|
|
3712
3712
|
content,
|
|
3713
3713
|
"mcp_servers.sellable",
|
|
3714
3714
|
`[mcp_servers.sellable]
|
|
3715
|
-
url = ${quoteToml(withHostedWatchModeDriver(opts.hostedUrl, "codex"))}
|
|
3716
|
-
default_tools_approval_mode = "approve"`
|
|
3715
|
+
url = ${quoteToml(withHostedWatchModeDriver(opts.hostedUrl, "codex"))}`
|
|
3717
3716
|
);
|
|
3718
3717
|
content = removeTomlSection(content, "mcp_servers.sellable.env");
|
|
3719
3718
|
return content;
|
|
@@ -3725,8 +3724,7 @@ default_tools_approval_mode = "approve"`
|
|
|
3725
3724
|
"mcp_servers.sellable",
|
|
3726
3725
|
`[mcp_servers.sellable]
|
|
3727
3726
|
command = ${quoteToml(command)}
|
|
3728
|
-
args = ${tomlArray(args)}
|
|
3729
|
-
default_tools_approval_mode = "approve"`
|
|
3727
|
+
args = ${tomlArray(args)}`
|
|
3730
3728
|
);
|
|
3731
3729
|
content = upsertTomlTable(
|
|
3732
3730
|
content,
|
|
@@ -3740,9 +3738,6 @@ ${tomlEnvTable("codex", opts)}`
|
|
|
3740
3738
|
function codexMcpServerMatches(content, opts) {
|
|
3741
3739
|
const server = readTomlTable(content, "mcp_servers.sellable");
|
|
3742
3740
|
if (!server) return false;
|
|
3743
|
-
if (!server.includes('default_tools_approval_mode = "approve"')) {
|
|
3744
|
-
return false;
|
|
3745
|
-
}
|
|
3746
3741
|
if (opts.server === "hosted") {
|
|
3747
3742
|
return (
|
|
3748
3743
|
server.includes(
|
|
@@ -285,14 +285,14 @@ export const HERMES_AGENT_BRIDGE_CONTRACT = normalizeContract({
|
|
|
285
285
|
{
|
|
286
286
|
path: "plugins/platforms/slack/adapter.py",
|
|
287
287
|
patchKind: "slack-adapter",
|
|
288
|
-
sourceSha256: "
|
|
289
|
-
patchedSha256: "
|
|
288
|
+
sourceSha256: "a84f1f15368d8d93341a1c2449c45ca13ebe92938049e92d80ce5cab0e1a6294",
|
|
289
|
+
patchedSha256: "4ba3b1f5b443abf970b8888de3c1b21ab7f043645aba6304432453c1dc643f55",
|
|
290
290
|
},
|
|
291
291
|
{
|
|
292
292
|
path: "tools/mcp_tool.py",
|
|
293
293
|
patchKind: "mcp-tool",
|
|
294
|
-
sourceSha256: "
|
|
295
|
-
patchedSha256: "
|
|
294
|
+
sourceSha256: "ee453ede1c72c90b28155e292a352bfba96eb1793c47eb8667d16fe79beb181e",
|
|
295
|
+
patchedSha256: "c0923d8c88d507a766121dab9f0f36e99963f85a9124cbef2908ee7176d56e07",
|
|
296
296
|
},
|
|
297
297
|
],
|
|
298
298
|
});
|
|
@@ -957,8 +957,8 @@ export function compileClaimToProfileDesired(activeClaim, config) {
|
|
|
957
957
|
]) ||
|
|
958
958
|
!exactObject(runtimeService, ["principalId", "policyHash", "credentialGeneration", "credentialFingerprint"]) ||
|
|
959
959
|
pinned.hermesCli !== "hermes" || pinned.hermesVersion !== "0.18.0" ||
|
|
960
|
-
pinned.installerPackage !== "@sellable/install@0.1.
|
|
961
|
-
pinned.mcpPackage !== "@sellable/mcp@0.1.
|
|
960
|
+
pinned.installerPackage !== "@sellable/install@0.1.357" ||
|
|
961
|
+
pinned.mcpPackage !== "@sellable/mcp@0.1.619" ||
|
|
962
962
|
!Array.isArray(policy.toolInclude) || policy.toolInclude.length === 0 ||
|
|
963
963
|
!Number.isSafeInteger(slack.generation) || slack.generation < 1 ||
|
|
964
964
|
!/^A[A-Z0-9]{8,20}$/.test(slack.appId ?? "") ||
|
|
@@ -1423,11 +1423,6 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
1423
1423
|
revisionId: request.evidence.revisionId,
|
|
1424
1424
|
profileDigest: request.evidence.profileDigest,
|
|
1425
1425
|
});
|
|
1426
|
-
await helper({
|
|
1427
|
-
action: "INSTALL_PROVIDER_CREDENTIAL",
|
|
1428
|
-
...helperIdentity(),
|
|
1429
|
-
fingerprint: context.desired.providerFingerprint,
|
|
1430
|
-
});
|
|
1431
1426
|
await helper({ action: "INSTALL_HERMES_BRIDGE", ...helperIdentity() });
|
|
1432
1427
|
await controlRuntime("UP");
|
|
1433
1428
|
},
|
|
@@ -1496,6 +1491,17 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
1496
1491
|
lifecycle: {},
|
|
1497
1492
|
};
|
|
1498
1493
|
const restart = () => controlRuntime("RESTART");
|
|
1494
|
+
const reload = async (path) => {
|
|
1495
|
+
const base = new URL(config.runtimeControlUrl);
|
|
1496
|
+
if (base.protocol !== "http:" || !["127.0.0.1", "localhost", "::1"].includes(base.hostname)) {
|
|
1497
|
+
throw new Error("runtime control endpoint rejected");
|
|
1498
|
+
}
|
|
1499
|
+
const response = await (dependencies.fetchImpl ?? fetch)(
|
|
1500
|
+
new URL(path, base),
|
|
1501
|
+
{ method: "POST", signal: AbortSignal.timeout(10_000) },
|
|
1502
|
+
);
|
|
1503
|
+
if (!response.ok) throw new Error("runtime reload rejected");
|
|
1504
|
+
};
|
|
1499
1505
|
const rawRuntimeObservation = async ({ drift = false } = {}) => {
|
|
1500
1506
|
const observed = (await helper({
|
|
1501
1507
|
action: drift ? "OBSERVE_RUNTIME_DRIFT" : "OBSERVE_RUNTIME",
|
|
@@ -1558,11 +1564,6 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
1558
1564
|
revisionId: result.receipt.revisionId,
|
|
1559
1565
|
profileDigest: result.receipt.profileDigest,
|
|
1560
1566
|
});
|
|
1561
|
-
await helper({
|
|
1562
|
-
action: "INSTALL_PROVIDER_CREDENTIAL",
|
|
1563
|
-
...helperIdentity(),
|
|
1564
|
-
fingerprint: context.desired.providerFingerprint,
|
|
1565
|
-
});
|
|
1566
1567
|
await helper({ action: "INSTALL_HERMES_BRIDGE", ...helperIdentity() });
|
|
1567
1568
|
};
|
|
1568
1569
|
const refreshThen = (effect) => async (input) => {
|
|
@@ -1570,8 +1571,8 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
1570
1571
|
return effect(input);
|
|
1571
1572
|
};
|
|
1572
1573
|
Object.assign(ports.refresh, {
|
|
1573
|
-
reloadMcp: refreshThen(
|
|
1574
|
-
reloadSkills: refreshThen(
|
|
1574
|
+
reloadMcp: refreshThen(() => reload("/reload-mcp")),
|
|
1575
|
+
reloadSkills: refreshThen(() => reload("/reload-skills")),
|
|
1575
1576
|
reloadEnvironmentOrRestart: refreshThen(restart),
|
|
1576
1577
|
restartGateway: refreshThen(restart),
|
|
1577
1578
|
restartProfileSession: refreshThen(restart),
|
|
@@ -1674,8 +1675,11 @@ export function createLocalPinnedActionPorts(config, dependencies = {}) {
|
|
|
1674
1675
|
async fenceDispatch() { await setRuntimeLifecycle("OFF"); },
|
|
1675
1676
|
async fenceRouteOutput() { await setRuntimeLifecycle("ARCHIVED"); },
|
|
1676
1677
|
async drain() {
|
|
1677
|
-
|
|
1678
|
-
const
|
|
1678
|
+
const base = new URL(config.runtimeControlUrl);
|
|
1679
|
+
const response = await fetch(new URL("/drain", base), { method: "POST", signal: AbortSignal.timeout(15_000) });
|
|
1680
|
+
if (!response.ok) throw new Error("drain rejected");
|
|
1681
|
+
const observed = await response.json();
|
|
1682
|
+
if (!observed?.drained || observed.remaining !== 0) throw new Error("drain readback rejected");
|
|
1679
1683
|
atomicPrivateJson(join(realpathSync(config.stateRoot), `drain-${context.activeClaim.operation.id}.json`), {
|
|
1680
1684
|
operationId: context.activeClaim.operation.id,
|
|
1681
1685
|
fence: context.activeClaim.operation.fence,
|
|
@@ -131,8 +131,8 @@ function validateDesired(desired) {
|
|
|
131
131
|
!Number.isSafeInteger(desired.slackGeneration) || desired.slackGeneration < 1 ||
|
|
132
132
|
!Number.isSafeInteger(desired.serviceCredentialGeneration) || desired.serviceCredentialGeneration < 1 ||
|
|
133
133
|
desired.hermesCli !== "hermes" || desired.hermesVersion !== "0.18.0" ||
|
|
134
|
-
desired.installerPackage !== "@sellable/install@0.1.
|
|
135
|
-
desired.mcpPackage !== "@sellable/mcp@0.1.
|
|
134
|
+
desired.installerPackage !== "@sellable/install@0.1.357" ||
|
|
135
|
+
desired.mcpPackage !== "@sellable/mcp@0.1.619" ||
|
|
136
136
|
!Array.isArray(desired.toolInclude) || desired.toolInclude.length === 0 ||
|
|
137
137
|
desired.toolInclude.some((tool) => !/^[a-z][a-z0-9_]{0,127}$/.test(tool)) ||
|
|
138
138
|
new Set(desired.toolInclude).size !== desired.toolInclude.length ||
|
|
@@ -19,8 +19,8 @@ import { fileURLToPath } from "node:url";
|
|
|
19
19
|
import { deriveContainedProfileId } from "./profile-materializer.mjs";
|
|
20
20
|
|
|
21
21
|
export const PROVISIONING_ACTION = "PROVISION_HERMES_PROFILE";
|
|
22
|
-
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.
|
|
23
|
-
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.
|
|
22
|
+
export const PINNED_INSTALL_PACKAGE = "@sellable/install@0.1.357";
|
|
23
|
+
export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.619";
|
|
24
24
|
|
|
25
25
|
export function deriveAgentProfileId(workspaceId, agentId, hostId) {
|
|
26
26
|
return deriveContainedProfileId(workspaceId, agentId, hostId);
|
|
@@ -576,7 +576,7 @@ function inspectProvisionedProfile({
|
|
|
576
576
|
!matchesReadback(manifest, request) ||
|
|
577
577
|
manifest.serviceCredentialReference !== serviceCredentialReference ||
|
|
578
578
|
manifest.installerPackage !== PINNED_INSTALL_PACKAGE ||
|
|
579
|
-
manifest.installerVersion !== "0.1.
|
|
579
|
+
manifest.installerVersion !== "0.1.357" ||
|
|
580
580
|
manifest.mcpPackage !== PINNED_MCP_PACKAGE ||
|
|
581
581
|
manifest.credentialKeyVersion !== credentialKeyVersion ||
|
|
582
582
|
!/^[a-f0-9]{16}$/.test(manifest.credentialFingerprint) ||
|
|
@@ -781,7 +781,7 @@ function successReceipt({
|
|
|
781
781
|
subject: `agent-profile:${observed.profileId}`,
|
|
782
782
|
cli: {
|
|
783
783
|
installPackage: PINNED_INSTALL_PACKAGE,
|
|
784
|
-
installVersion: "0.1.
|
|
784
|
+
installVersion: "0.1.357",
|
|
785
785
|
mcpPackage: PINNED_MCP_PACKAGE,
|
|
786
786
|
command: "hermes profile bootstrap",
|
|
787
787
|
},
|
|
@@ -821,7 +821,7 @@ function failure(code, stages, extra = {}, request = null) {
|
|
|
821
821
|
subject: safeIdentity ? `agent-profile:${safeIdentity}` : "agent-profile:unbound",
|
|
822
822
|
cli: {
|
|
823
823
|
installPackage: PINNED_INSTALL_PACKAGE,
|
|
824
|
-
installVersion: "0.1.
|
|
824
|
+
installVersion: "0.1.357",
|
|
825
825
|
mcpPackage: PINNED_MCP_PACKAGE,
|
|
826
826
|
command: "hermes profile bootstrap",
|
|
827
827
|
},
|
|
@@ -23,7 +23,6 @@ export const EXTERNAL_HERMES_ENTRYPOINT = `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/herm
|
|
|
23
23
|
const EXTERNAL_RUNTIME_MANIFEST_VERSION = "sellable-agent-external-runtime-closure/v1";
|
|
24
24
|
const ACTIONS = new Set([
|
|
25
25
|
"INSTALL_MCP_CREDENTIAL",
|
|
26
|
-
"INSTALL_PROVIDER_CREDENTIAL",
|
|
27
26
|
"INSTALL_RUNTIME_SERVICE_ENV",
|
|
28
27
|
"SET_RUNTIME_GATES",
|
|
29
28
|
"OBSERVE_RUNTIME_GATES",
|
|
@@ -51,7 +50,6 @@ const PROFILE_ENTRIES = Object.freeze([
|
|
|
51
50
|
]);
|
|
52
51
|
const ACTION_KEYS = Object.freeze({
|
|
53
52
|
INSTALL_MCP_CREDENTIAL: ["action", "profileId", "operationId", "fence", "sourceRequestFile", "generation", "fingerprint"],
|
|
54
|
-
INSTALL_PROVIDER_CREDENTIAL: ["action", "profileId", "operationId", "fence", "fingerprint"],
|
|
55
53
|
INSTALL_RUNTIME_SERVICE_ENV: ["action", "profileId", "operationId", "fence", "revisionId", "profileDigest"],
|
|
56
54
|
SET_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
|
|
57
55
|
OBSERVE_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
|
|
@@ -111,79 +109,6 @@ function atomicCredential(pathValue, secret, uid, gid) {
|
|
|
111
109
|
fsyncDirectory(dirname(pathValue));
|
|
112
110
|
}
|
|
113
111
|
|
|
114
|
-
function providerCredentialPath(profile, profileId) {
|
|
115
|
-
const runtimeStateRoot = safeRuntimeStateRoot(profile);
|
|
116
|
-
const profilesRoot = join(runtimeStateRoot, "profiles");
|
|
117
|
-
const profileRoot = join(profilesRoot, profileId);
|
|
118
|
-
const runtimeHome = join(profileRoot, "home");
|
|
119
|
-
for (const pathValue of [profilesRoot, profileRoot, runtimeHome]) {
|
|
120
|
-
const link = lstatSync(pathValue);
|
|
121
|
-
if (
|
|
122
|
-
link.isSymbolicLink() || !link.isDirectory() || (link.mode & 0o777) !== 0o700 ||
|
|
123
|
-
link.uid !== profile.runtimeUid || link.gid !== profile.runtimeGid
|
|
124
|
-
) throw new Error("provider runtime home rejected");
|
|
125
|
-
}
|
|
126
|
-
const codexRoot = join(runtimeHome, ".codex");
|
|
127
|
-
if (!existsSync(codexRoot)) {
|
|
128
|
-
mkdirSync(codexRoot, { mode: 0o700 });
|
|
129
|
-
chownSync(codexRoot, profile.runtimeUid, profile.runtimeGid);
|
|
130
|
-
}
|
|
131
|
-
const codex = lstatSync(codexRoot);
|
|
132
|
-
if (
|
|
133
|
-
codex.isSymbolicLink() || !codex.isDirectory() || (codex.mode & 0o777) !== 0o700 ||
|
|
134
|
-
codex.uid !== profile.runtimeUid || codex.gid !== profile.runtimeGid
|
|
135
|
-
) throw new Error("provider credential root rejected");
|
|
136
|
-
return join(codexRoot, "auth.json");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
function installProviderCredential(profile, request, config) {
|
|
140
|
-
if (
|
|
141
|
-
!SHA256.test(request.fingerprint ?? "") ||
|
|
142
|
-
request.fingerprint !== profile.providerCredentialFingerprint ||
|
|
143
|
-
(
|
|
144
|
-
profile.providerCredentialSourcePath !== "/var/lib/sellable-agent-bootstrap/provider-auth" &&
|
|
145
|
-
config.disposableFixture !== true
|
|
146
|
-
)
|
|
147
|
-
) throw new Error("provider credential identity rejected");
|
|
148
|
-
const sourceRoot = privateRoot(dirname(profile.providerCredentialSourcePath));
|
|
149
|
-
const source = readPrivateDirectFile(sourceRoot, profile.providerCredentialSourcePath).trim();
|
|
150
|
-
if (!source || source.length > 1024 * 1024 || sha256(source) !== request.fingerprint) {
|
|
151
|
-
throw new Error("provider credential source rejected");
|
|
152
|
-
}
|
|
153
|
-
const parsed = JSON.parse(source);
|
|
154
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
155
|
-
throw new Error("provider credential JSON rejected");
|
|
156
|
-
}
|
|
157
|
-
const target = providerCredentialPath(profile, request.profileId);
|
|
158
|
-
atomicCredential(target, source, profile.runtimeUid, profile.runtimeGid);
|
|
159
|
-
return {
|
|
160
|
-
ok: true,
|
|
161
|
-
profileId: request.profileId,
|
|
162
|
-
fingerprint: request.fingerprint,
|
|
163
|
-
path: target,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
function removeProviderCredential(profile, profileId) {
|
|
168
|
-
const target = providerCredentialPath(profile, profileId);
|
|
169
|
-
if (existsSync(target)) {
|
|
170
|
-
const link = lstatSync(target);
|
|
171
|
-
if (link.isSymbolicLink() || !link.isFile()) throw new Error("provider credential cleanup rejected");
|
|
172
|
-
rmSync(target, { force: true });
|
|
173
|
-
fsyncDirectory(dirname(target));
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function observeProviderCredential(profile, profileId) {
|
|
178
|
-
const target = providerCredentialPath(profile, profileId);
|
|
179
|
-
const link = lstatSync(target);
|
|
180
|
-
if (
|
|
181
|
-
link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 ||
|
|
182
|
-
link.uid !== profile.runtimeUid || link.gid !== profile.runtimeGid
|
|
183
|
-
) throw new Error("provider credential readback rejected");
|
|
184
|
-
return sha256(readFileSync(target, "utf8").trim());
|
|
185
|
-
}
|
|
186
|
-
|
|
187
112
|
function applyRuntimeOwnership(pathValue, uid, gid) {
|
|
188
113
|
const link = lstatSync(pathValue);
|
|
189
114
|
if (link.isSymbolicLink()) throw new Error("profile symlink rejected");
|
|
@@ -584,9 +509,7 @@ function walkExternalRoot(root, ownerUid, ownerGid) {
|
|
|
584
509
|
}
|
|
585
510
|
};
|
|
586
511
|
visit(root, "");
|
|
587
|
-
|
|
588
|
-
const byLogicalPath = (left, right) => left.logicalPath < right.logicalPath ? -1 : left.logicalPath > right.logicalPath ? 1 : 0;
|
|
589
|
-
return { directories: directories.sort(byPath), files: files.sort(byLogicalPath) };
|
|
512
|
+
return { directories: directories.sort((left, right) => left.path.localeCompare(right.path)), files };
|
|
590
513
|
}
|
|
591
514
|
|
|
592
515
|
export function observeExternalRuntimeClosure(input = {}) {
|
|
@@ -646,24 +569,18 @@ export function observeExternalRuntimeClosure(input = {}) {
|
|
|
646
569
|
if (
|
|
647
570
|
JSON.stringify(rootRecord.directories.map((directory) => directory?.path)) !==
|
|
648
571
|
JSON.stringify(rootRecord.directories.map((directory) => directory?.path).sort()) ||
|
|
649
|
-
new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length
|
|
650
|
-
) throw new Error("external runtime directory order rejected");
|
|
651
|
-
if (
|
|
572
|
+
new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length ||
|
|
652
573
|
rootRecord.directories.some((directory) =>
|
|
653
574
|
!exactKeys(directory, ["path", "mode"]) ||
|
|
654
|
-
!/^[
|
|
575
|
+
!/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(directory.path ?? "") ||
|
|
655
576
|
directory.mode !== 0o555
|
|
656
|
-
)
|
|
657
|
-
|
|
658
|
-
if (JSON.stringify(walked.directories) !== JSON.stringify(rootRecord.directories)) {
|
|
659
|
-
throw new Error("external runtime directory inventory rejected");
|
|
660
|
-
}
|
|
661
|
-
if (
|
|
577
|
+
) ||
|
|
578
|
+
JSON.stringify(walked.directories) !== JSON.stringify(rootRecord.directories) ||
|
|
662
579
|
rootRecord.files.length === 0 ||
|
|
663
580
|
JSON.stringify(rootRecord.files.map((file) => file?.path)) !==
|
|
664
581
|
JSON.stringify(rootRecord.files.map((file) => file?.path).sort()) ||
|
|
665
582
|
new Set(rootRecord.files.map((file) => file?.path)).size !== rootRecord.files.length
|
|
666
|
-
) throw new Error("external runtime
|
|
583
|
+
) throw new Error("external runtime inventory rejected");
|
|
667
584
|
const actualPaths = walked.files.map((file) => file.logicalPath);
|
|
668
585
|
if (JSON.stringify(actualPaths) !== JSON.stringify(rootRecord.files.map((file) => file.path))) {
|
|
669
586
|
throw new Error("external runtime open inventory rejected");
|
|
@@ -671,7 +588,7 @@ export function observeExternalRuntimeClosure(input = {}) {
|
|
|
671
588
|
const observedFiles = rootRecord.files.map((file, index) => {
|
|
672
589
|
if (
|
|
673
590
|
!exactKeys(file, ["path", "mode", "digest"]) ||
|
|
674
|
-
!/^[
|
|
591
|
+
!/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(file.path ?? "") ||
|
|
675
592
|
![0o444, 0o555].includes(file.mode) || !SHA256.test(file.digest ?? "")
|
|
676
593
|
) throw new Error("external runtime file schema rejected");
|
|
677
594
|
const actual = walked.files[index];
|
|
@@ -1041,19 +958,9 @@ async function observeRuntime(profile, request) {
|
|
|
1041
958
|
const profileFiles = observeRuntimeProfileFiles(target);
|
|
1042
959
|
const { runtimeConfig, runtimeMcp, tools, fileHashes: actualFileHashes } = profileFiles;
|
|
1043
960
|
const actualProfileDigest = profileFiles.profileDigest;
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
if (observedProvider !== runtimeConfig?.provider?.fingerprint) {
|
|
1048
|
-
throw new Error("provider credential fingerprint rejected");
|
|
1049
|
-
}
|
|
1050
|
-
} catch (error) {
|
|
1051
|
-
if (!driftMode) throw error;
|
|
1052
|
-
observedProvider = sha256(`provider-drift\0${runtimeConfig?.provider?.fingerprint ?? "absent"}`);
|
|
1053
|
-
}
|
|
1054
|
-
if (environmentDrift) {
|
|
1055
|
-
observedProvider = sha256(`environment-drift\0${observedProvider}`);
|
|
1056
|
-
}
|
|
961
|
+
const observedProvider = environmentDrift
|
|
962
|
+
? sha256(`environment-drift\0${runtimeConfig?.provider?.fingerprint ?? "absent"}`)
|
|
963
|
+
: runtimeConfig?.provider?.fingerprint;
|
|
1057
964
|
return {
|
|
1058
965
|
profileId: manifest.profileId,
|
|
1059
966
|
workspaceId: manifest.workspaceId,
|
|
@@ -1265,9 +1172,6 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
|
|
|
1265
1172
|
generation: request.generation,
|
|
1266
1173
|
};
|
|
1267
1174
|
}
|
|
1268
|
-
if (request.action === "INSTALL_PROVIDER_CREDENTIAL") {
|
|
1269
|
-
return installProviderCredential(profile, request, config);
|
|
1270
|
-
}
|
|
1271
1175
|
if (request.action === "INSTALL_RUNTIME_SERVICE_ENV") {
|
|
1272
1176
|
return installRuntimeServiceEnvironment(profile, request);
|
|
1273
1177
|
}
|
|
@@ -1283,7 +1187,6 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
|
|
|
1283
1187
|
rmSync(target, { recursive: true, force: true });
|
|
1284
1188
|
fsyncDirectory(root);
|
|
1285
1189
|
}
|
|
1286
|
-
removeProviderCredential(profile, request.profileId);
|
|
1287
1190
|
return { ok: true, profileId: request.profileId, disposition: "REMOVED" };
|
|
1288
1191
|
}
|
|
1289
1192
|
if (request.action === "CHOWN_PROFILE") {
|
|
@@ -247,8 +247,6 @@ function validateInstallInput(input) {
|
|
|
247
247
|
profile.hermesSourceRoot !== `${EXTERNAL_RUNTIME_CLOSURE_ROOT}/hermes` ||
|
|
248
248
|
profile.proxyConfigPath !== "/etc/sellable-agent/egress-proxy.json" ||
|
|
249
249
|
profile.runtimeBoundaryConfigPath !== "/etc/sellable-agent/runtime-boundary.json" ||
|
|
250
|
-
profile.providerCredentialSourcePath !== "/var/lib/sellable-agent-bootstrap/provider-auth" ||
|
|
251
|
-
!SHA256.test(profile.providerCredentialFingerprint ?? "") ||
|
|
252
250
|
profile.runtimeServicePath !== "/etc/services.d/sellable-agent-runtime" ||
|
|
253
251
|
profile.runtimeServiceEnvRoot !== "/etc/services.d/sellable-agent-runtime/env" ||
|
|
254
252
|
new Set([profile.runtimeUid, profile.proxyUid, profile.workerUid]).size !== 3
|
|
@@ -305,7 +303,7 @@ function configurationFiles(input, checked) {
|
|
|
305
303
|
].join("\n");
|
|
306
304
|
return [
|
|
307
305
|
["/etc/sellable-agent/egress-proxy.json", `${JSON.stringify(checked.proxyConfig, null, 2)}\n`, 0o444],
|
|
308
|
-
["/etc/sellable-agent/worker.json", `${JSON.stringify(input.workerConfig, null, 2)}\n`,
|
|
306
|
+
["/etc/sellable-agent/worker.json", `${JSON.stringify(input.workerConfig, null, 2)}\n`, 0o600],
|
|
309
307
|
["/etc/sellable-agent/runtime-helper.json", `${JSON.stringify(input.helperConfig, null, 2)}\n`, 0o600],
|
|
310
308
|
["/etc/sudoers.d/sellable-agent-runtime-helper", sudoers, 0o440],
|
|
311
309
|
].map(([logicalPath, bytes, mode]) => ({ logicalPath, bytes, mode }));
|
package/package.json
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sellable/install",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.364-phase111.20260720135054",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
|
|
6
6
|
"bin": {
|
|
7
7
|
"sellable": "bin/sellable-install.mjs",
|
|
8
|
-
"sellable-agent-host-bootstrap": "bin/sellable-agent-host-bootstrap.mjs",
|
|
9
8
|
"sellable-agent-egress-proxy": "bin/sellable-agent-egress-proxy.mjs",
|
|
10
9
|
"sellable-agent-host-worker": "bin/sellable-agent-host-worker.mjs",
|
|
11
10
|
"sellable-agent-runtime-helper": "bin/sellable-agent-runtime-helper.mjs",
|
|
@@ -21,7 +20,6 @@
|
|
|
21
20
|
"bin",
|
|
22
21
|
"lib",
|
|
23
22
|
"services",
|
|
24
|
-
"container",
|
|
25
23
|
"agents",
|
|
26
24
|
"skill-templates",
|
|
27
25
|
"README.md",
|
|
@@ -17,3 +17,8 @@ Use `refill-sends-v2` instead:
|
|
|
17
17
|
```text
|
|
18
18
|
get_subskill_prompt({ subskillName: "refill-sends-v2" })
|
|
19
19
|
```
|
|
20
|
+
|
|
21
|
+
The alias must not strip exact `targetConfig`, server `runHandle`,
|
|
22
|
+
`reportingContext`, canonical `refill_reporting.v2`, or an explicit
|
|
23
|
+
`messageTemplateRevision`. It never infers revision authority from
|
|
24
|
+
`forceRerun`.
|
|
@@ -52,9 +52,35 @@ refill_sends_v2({ workspaceId, dryRun:true, intent:"auto", senderIds? })
|
|
|
52
52
|
To resume an in-progress run, pass the handle back exactly:
|
|
53
53
|
|
|
54
54
|
```text
|
|
55
|
-
refill_sends_v2({ workspaceId, runId,
|
|
55
|
+
refill_sends_v2({ workspaceId, runHandle:{runId,fence}, targetConfig, reportingContext, messageTemplateRevision? })
|
|
56
56
|
```
|
|
57
57
|
|
|
58
|
+
`targetConfig` and the server-issued `runHandle` are separate authorities. Keep
|
|
59
|
+
both byte-for-byte through every continuation and takeover; never fold the
|
|
60
|
+
rotating fence into target configuration.
|
|
61
|
+
|
|
62
|
+
An approved copy change is reachable only through an explicit
|
|
63
|
+
`messageTemplateRevision` object with `version:1`, `source:"user_approved"`,
|
|
64
|
+
the approved `templateMarkdown`, `messageTemplateRevision` identity,
|
|
65
|
+
`priorTemplateAuthorityDigest`, `nextTemplateAuthorityDigest`, 1–500 exact
|
|
66
|
+
`cohortRowIds`, and `requestId`, `effectId`, and `revisionOperationId`. Pass it
|
|
67
|
+
with the exact `targetConfig` and `reportingContext`; the command obtains or
|
|
68
|
+
resumes the server run handle and lets the planner name
|
|
69
|
+
`revise_message_template_and_rerun`. Missing or partial input means no revision.
|
|
70
|
+
`forceRerun:true` on a queue operation never authorizes or implies a template
|
|
71
|
+
change.
|
|
72
|
+
|
|
73
|
+
After a committed and dispatched revision receipt, continue only with the
|
|
74
|
+
planner-provided revision-derived `approve_messages` action. It must preserve
|
|
75
|
+
the exact revision cohort as `rowSelector:{type:"rowIds",rowIds:[...]}` plus
|
|
76
|
+
`approvalMode:"approve"`, the compiled readiness identity, and the canonical
|
|
77
|
+
`reportingContext`, even when the ordinary sender lane is ineligible. Do not
|
|
78
|
+
replace it with `queue_campaign_cells` on Approved cells; that path cannot mint
|
|
79
|
+
the authoritative approval/lane-scope receipt. Require `laneScope` in the
|
|
80
|
+
bounded preparation receipt before the lane is considered approved. A
|
|
81
|
+
disconnected or zero-capacity sender still blocks scheduling and does not
|
|
82
|
+
authorize reconnecting, campaign start, or direct send.
|
|
83
|
+
|
|
58
84
|
If a stale handle loses the lease, the tool reports the holder status and the
|
|
59
85
|
approximately 10 minute lockout window. Reinvoke with the current handle or wait
|
|
60
86
|
for lease expiry; never guess a fence.
|
|
@@ -85,6 +111,10 @@ The run may execute only packet-named bounded work:
|
|
|
85
111
|
when the campaign is in the packet's pinned lane chain;
|
|
86
112
|
- refresh paid InMail credit facts during bootstrap and once per scheduler-wait
|
|
87
113
|
entry when the packet requires it.
|
|
114
|
+
- apply `revise_message_template_and_rerun` only when the packet contains the
|
|
115
|
+
complete explicit user-approved revision envelope and exact fenced target.
|
|
116
|
+
- after that revision commits, apply only its exact-row, readiness-bound
|
|
117
|
+
`approve_messages` continuation and require the returned lane-scope receipt.
|
|
88
118
|
|
|
89
119
|
The loop records planned -> did -> outcome before and after every foreign
|
|
90
120
|
mutation. It refuses stale packets by fingerprint and validates packet
|
|
@@ -168,5 +198,10 @@ reason or resume handle, lane source, lane chain, chosen label with token
|
|
|
168
198
|
secondary, plan revision, journal path, blocked continuation packets, and
|
|
169
199
|
firstFailing checklist when present.
|
|
170
200
|
|
|
201
|
+
Preserve `refill_reporting.v2` exactly in progress, continuation, replay, and
|
|
202
|
+
terminal results. Do not recompute its semantic counts or drop live-preparation
|
|
203
|
+
epoch, snapshot sequence, reset reason, watermark, blockers, compatibility, or
|
|
204
|
+
terminal fields. Never surface raw prospect or message-copy fields.
|
|
205
|
+
|
|
171
206
|
Real-run journals live under `~/.sellable/refill/runs` by default and append an
|
|
172
207
|
index line. Dry runs include the dry marker and do not create run records.
|
|
@@ -93,7 +93,7 @@ Accepted invocation flags in the same user request:
|
|
|
93
93
|
When the host can call typed MCP tools, start with:
|
|
94
94
|
|
|
95
95
|
```text
|
|
96
|
-
refill_sends({ yolo?: boolean, executionMode?: "manual" | "scheduled" | "yolo", requireWorkspace?: boolean, workspaceId?: string, senders?: string[], senderIds?: string[], senderNames?: string[], actionTypes?: ("send_invite" | "send_inmail_closed")[], horizonSendDays?: number, untilDate?: "YYYY-MM-DD", targetDate?: "YYYY-MM-DD",
|
|
96
|
+
refill_sends({ yolo?: boolean, executionMode?: "manual" | "scheduled" | "yolo", requireWorkspace?: boolean, workspaceId?: string, senders?: string[], senderIds?: string[], senderNames?: string[], actionTypes?: ("send_invite" | "send_inmail_closed")[], horizonSendDays?: number, untilDate?: "YYYY-MM-DD", targetDate?: "YYYY-MM-DD", runHandle?: RefillRunHandleV1, targetConfig?: RefillTargetConfigV1, reportingContext?: RefillReportingContextV2, messageTemplateRevision?: MessageTemplateRevisionV1 })
|
|
97
97
|
```
|
|
98
98
|
|
|
99
99
|
That command helper normalizes arguments and returns the execution contract. In
|
|
@@ -104,6 +104,49 @@ host must immediately call `refill_sends` again with the original full scope
|
|
|
104
104
|
plus the exact returned `runId` and `fence`. Do this automatically inside the
|
|
105
105
|
same user command until terminal projected coverage or a concrete blocker; do
|
|
106
106
|
not ask the user to type `continue` and do not start a second run.
|
|
107
|
+
|
|
108
|
+
`targetConfig` and the server-issued `runHandle` are separate values and must
|
|
109
|
+
survive each continuation unchanged except for a server-issued takeover fence.
|
|
110
|
+
Only a literal `messageTemplateRevision` with `source:"user_approved"`, the
|
|
111
|
+
approved `templateMarkdown`, prior/new authority digests, 1–500 exact cohort
|
|
112
|
+
row IDs, and request/effect/revision-operation IDs can authorize
|
|
113
|
+
`revise_message_template_and_rerun`. No envelope means no revision. A
|
|
114
|
+
`forceRerun:true` queue call cannot infer or authorize template mutation.
|
|
115
|
+
When the user supplied a complete revision except for the current prior digest,
|
|
116
|
+
acquire the exact run first and reread with its unchanged `targetConfig` and
|
|
117
|
+
`runHandle`. Use only `reportingContext.source.templateAuthorityDigest` from
|
|
118
|
+
that fenced reread as `priorTemplateAuthorityDigest`, then immediately continue
|
|
119
|
+
the same run with the complete literal revision—even when the ordinary refill
|
|
120
|
+
queue is empty or the sender is otherwise ineligible. An empty ordinary queue
|
|
121
|
+
must not suppress an explicit revision. If the fenced reread returns no digest,
|
|
122
|
+
stop with `template_authority_missing` or the returned
|
|
123
|
+
`templateAuthorityBlocker`; never guess the digest.
|
|
124
|
+
After the revision receipt is committed and dispatched, the next mutation must
|
|
125
|
+
be the planner-provided revision-derived `approve_messages` action. Preserve
|
|
126
|
+
its exact `rowSelector:{type:"rowIds",rowIds:[...]}` cohort,
|
|
127
|
+
`approvalMode:"approve"`, compiled readiness authority, and
|
|
128
|
+
`reportingContext`, even if the ordinary sender lane is ineligible. Never use
|
|
129
|
+
`queue_campaign_cells` to flip Approved cells: that path cannot produce the
|
|
130
|
+
authoritative approval/lane-scope receipt. Require the bounded preparation
|
|
131
|
+
receipt's `laneScope` before considering the selected first-touch lane
|
|
132
|
+
approved. Zero sender capacity remains a scheduler blocker and never authorizes
|
|
133
|
+
reconnection, campaign start, or direct send.
|
|
134
|
+
|
|
135
|
+
Before that approval mutation, perform an exact authoritative copy audit with
|
|
136
|
+
`select_campaign_cells({ columnRole:"generateMessage",
|
|
137
|
+
rowSelector:{type:"rowIds",rowIds:[...]},
|
|
138
|
+
includeGeneratedMessageReview:true })` over the complete revision cohort in
|
|
139
|
+
batches of at most 20. Require every
|
|
140
|
+
`authoritativeGeneratedMessageReview.complete:true`, every row status to be
|
|
141
|
+
completed-like, the exact user-approved rendered subject/body, the expected
|
|
142
|
+
`templateAuthorityDigest`, and `matchesCurrentTemplateAuthority:true`. This
|
|
143
|
+
projection is the only MCP copy-review source of truth. Never use formula
|
|
144
|
+
Message/Subject columns, campaign message previews, table previews, or CSV
|
|
145
|
+
exports to validate revised Generate Message results. If any row is missing,
|
|
146
|
+
non-current, or text/digest mismatched, stop before approval and report
|
|
147
|
+
`regenerated_copy_integrity_failed`; never regenerate again implicitly or
|
|
148
|
+
approve the cohort on counts/revision hashes alone.
|
|
149
|
+
|
|
107
150
|
Safe primitives are paid-credit refresh,
|
|
108
151
|
existing-row message preparation, generated-message approval, receipt-proven
|
|
109
152
|
same-source row copy, one bounded Signal Discovery search derived only from
|
|
@@ -567,6 +610,11 @@ scheduler wait loop. Do not finish the refill goal, mark it complete, or mark it
|
|
|
567
610
|
blocked only because it is still `awaiting_scheduler_after_ready_buffer`; keep
|
|
568
611
|
waiting unless Christian stops the run or the host cannot continue.
|
|
569
612
|
|
|
613
|
+
Carry the canonical `refill_reporting.v2` DTO through progress, continuation,
|
|
614
|
+
replay, and terminal reporting without recomputing counts or losing live epoch,
|
|
615
|
+
snapshot sequence, reset reason, watermark, blockers, compatibility, or
|
|
616
|
+
terminal outcome. Redact raw copy and prospect fields.
|
|
617
|
+
|
|
570
618
|
Future scheduled coverage and already sent actions are distinct; only
|
|
571
619
|
scheduler-owned future scheduled actions count as scheduled coverage. For
|
|
572
620
|
multiple target dates, finish D1 execution and the full D1 reread before
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import { lstatSync, readFileSync } from "node:fs";
|
|
4
|
-
import { isAbsolute } from "node:path";
|
|
5
|
-
import { bootstrapAgentHost } from "../lib/sellable-agent/host-bootstrap.mjs";
|
|
6
|
-
|
|
7
|
-
function configPath(argv) {
|
|
8
|
-
if (argv.length !== 2 || argv[0] !== "--config" || !isAbsolute(argv[1])) {
|
|
9
|
-
throw new Error("usage: --config /absolute/path");
|
|
10
|
-
}
|
|
11
|
-
const link = lstatSync(argv[1]);
|
|
12
|
-
if (link.isSymbolicLink() || !link.isFile() || (link.mode & 0o777) !== 0o600 || link.uid !== 0 || link.gid !== 0) {
|
|
13
|
-
throw new Error("host bootstrap config rejected");
|
|
14
|
-
}
|
|
15
|
-
return argv[1];
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
try {
|
|
19
|
-
if (process.getuid?.() !== 0 || process.getgid?.() !== 0) throw new Error("host bootstrap requires root");
|
|
20
|
-
const pathValue = configPath(process.argv.slice(2));
|
|
21
|
-
const result = bootstrapAgentHost({ config: JSON.parse(readFileSync(pathValue, "utf8")) });
|
|
22
|
-
process.stdout.write(`${JSON.stringify(result.receipt)}\n`);
|
|
23
|
-
} catch {
|
|
24
|
-
process.stderr.write("sellable-agent host bootstrap failed closed\n");
|
|
25
|
-
process.exitCode = 1;
|
|
26
|
-
}
|
package/container/Dockerfile
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
ARG HERMES_IMAGE=ghcr.io/hostinger/hvps-hermes-agent@sha256:f048663be9ba564de795df5f2a8184f32b92561a1f053c179dca3b45fa5bc8b9
|
|
2
|
-
FROM ${HERMES_IMAGE}
|
|
3
|
-
|
|
4
|
-
USER root
|
|
5
|
-
|
|
6
|
-
ARG INSTALLER_PACKAGE=@sellable/install@0.1.363
|
|
7
|
-
ARG MCP_PACKAGE=@sellable/mcp@0.1.620
|
|
8
|
-
ARG INSTALLER_INTEGRITY
|
|
9
|
-
ARG MCP_INTEGRITY=sha512-N+2clnLHWAlvoXCjX10HaeEUMPLqZQHIX+x/2e1CSJbqb56CU1SAtVdwmhXyha7+MQT0DZF7pm2+WnoZKFEzOw==
|
|
10
|
-
|
|
11
|
-
RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.363" \
|
|
12
|
-
&& test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.620" \
|
|
13
|
-
&& test -n "${INSTALLER_INTEGRITY}" \
|
|
14
|
-
&& test "$(npm view "${INSTALLER_PACKAGE}" dist.integrity)" = "${INSTALLER_INTEGRITY}" \
|
|
15
|
-
&& test "$(npm view "${MCP_PACKAGE}" dist.integrity)" = "${MCP_INTEGRITY}" \
|
|
16
|
-
&& apt-get update \
|
|
17
|
-
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
|
18
|
-
bubblewrap \
|
|
19
|
-
ca-certificates \
|
|
20
|
-
nftables \
|
|
21
|
-
sudo \
|
|
22
|
-
&& rm -rf /var/lib/apt/lists/* \
|
|
23
|
-
&& mkdir -p /opt/sellable-agent/install-root /opt/sellable-agent/mcp-root \
|
|
24
|
-
&& npm install --prefix /opt/sellable-agent/install-root --omit=dev --ignore-scripts --no-audit --no-fund "${INSTALLER_PACKAGE}" \
|
|
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.363' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.620') throw new Error('package identity rejected');" \
|
|
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.363', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.620', 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
|
-
&& npm cache clean --force
|
|
30
|
-
|
|
31
|
-
COPY entrypoint.sh /usr/local/bin/sellable-agent-container-entrypoint
|
|
32
|
-
RUN test -x /usr/local/bin/node \
|
|
33
|
-
&& test ! -e /usr/bin/node \
|
|
34
|
-
&& ln -s /usr/local/bin/node /usr/bin/node \
|
|
35
|
-
&& test "$(readlink -f /usr/bin/node)" = "/usr/local/bin/node" \
|
|
36
|
-
&& chmod 0555 /usr/local/bin/sellable-agent-container-entrypoint
|
|
37
|
-
|
|
38
|
-
HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 \
|
|
39
|
-
CMD /command/s6-svstat /etc/services.d/sellable-agent-host-worker | grep -q '^up' || exit 1
|
|
40
|
-
|
|
41
|
-
ENTRYPOINT ["/usr/local/bin/sellable-agent-container-entrypoint"]
|
package/container/README.md
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
# Sellable Agent host container
|
|
2
|
-
|
|
3
|
-
This image is a dedicated Agent worker/runtime boundary beside an existing Hermes dashboard. It never replaces or reconfigures the dashboard container.
|
|
4
|
-
|
|
5
|
-
Build from this directory after `@sellable/install@0.1.363` is published:
|
|
6
|
-
|
|
7
|
-
```sh
|
|
8
|
-
docker build --platform linux/amd64 \
|
|
9
|
-
--build-arg INSTALLER_INTEGRITY='sha512-…' \
|
|
10
|
-
--tag sellable-agent-host:0.1.363 .
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
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`.
|
|
14
|
-
|
|
15
|
-
Set `SELLABLE_AGENT_IMAGE` to the exact built image digest and start `compose.yaml`. The container bootstraps idempotently, writes `bootstrap/registration-receipt.json`, and starts the worker and isolated runtime under s6. Do not mount a Docker socket.
|