@sellable/install 0.1.719 → 0.1.720

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.719
6
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.720
7
7
  ARG MCP_PACKAGE=@sellable/mcp@0.1.915
8
8
  ARG INSTALLER_INTEGRITY
9
9
  ARG MCP_INTEGRITY=sha512-+WdyO3WxQ9U7fHMvxBojPRDIZun/INy9mKOz/A1IcBFEWXFkpPooT0yMUzY1qCYcewTM1AUxP3p2QOtsxRIYQA==
10
10
 
11
- RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.719" \
11
+ RUN test "${INSTALLER_PACKAGE}" = "@sellable/install@0.1.720" \
12
12
  && test "${MCP_PACKAGE}" = "@sellable/mcp@0.1.915" \
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.719" \
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.719' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.915') 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.720' || mcp.name !== '@sellable/mcp' || mcp.version !== '0.1.915') 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.719', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.915', 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.720', installerIntegrity: process.argv[1], mcpPackage: '@sellable/mcp@0.1.915', 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,7 +3,7 @@
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.719` is
6
+ `@sellable/install@0.1.720` is
7
7
  published:
8
8
 
9
9
  ```sh
@@ -284,6 +284,7 @@ RUN set -eux; \
284
284
 
285
285
  LABEL io.sellable.runtime.kind="admin" \
286
286
  io.sellable.runtime.listener="native-dedicated-slack" \
287
+ io.sellable.runtime.product-mcp-readiness="service-authenticated-safe-read/v1" \
287
288
  io.sellable.runtime.api-server="authenticated-loopback-127.0.0.1:8642" \
288
289
  io.sellable.runtime.installers="build-only" \
289
290
  io.sellable.runtime.default-profile-bundle-version="${DEFAULT_PROFILE_BUNDLE_VERSION}" \
@@ -87,8 +87,8 @@ import {
87
87
  HERMES_SNAPSHOT_HOOKS,
88
88
  HERMES_SNAPSHOT_HOOK_ALLOWLIST_BYTES,
89
89
  applyHermesHostedBackgroundReviewPolicy,
90
- ensureHermesOpportunitySequencesCron,
91
90
  ensureHermesClientRelationshipsCrons,
91
+ ensureHermesOpportunitySequencesCron,
92
92
  ensureHermesSnapshotCron,
93
93
  markHermesSnapshotDirty,
94
94
  mergeHermesState,
@@ -98,6 +98,7 @@ import {
98
98
  replaceHermesState,
99
99
  restoreLatestHermesSnapshot,
100
100
  } from "../hermes-memory-snapshot.mjs";
101
+ import { createMcpAdminRuntimeBindingV3 } from "../mcp-runtime-binding.mjs";
101
102
  import {
102
103
  evaluateModelAuthReadback,
103
104
  modelAuthConfigOverlay,
@@ -107,7 +108,6 @@ import {
107
108
  reconcileHermesModelAuth,
108
109
  } from "../model-auth-reconciler.mjs";
109
110
  import { createModelHealthReporter } from "../model-health-reporter.mjs";
110
- import { createMcpAdminRuntimeBindingV3 } from "../mcp-runtime-binding.mjs";
111
111
  import { createAdminRuntimePhaseTelemetry } from "./admin-runtime-phase-telemetry.mjs";
112
112
  import {
113
113
  completeAdminStateReceiptRecovery,
@@ -134,13 +134,6 @@ const PROFILES_ROOT = join(DATA_ROOT, "profiles");
134
134
  const RUNTIME_ROOT = join(DATA_ROOT, ".sellable-agent", "admin-runtime");
135
135
  const DELIVERY_ROOT = join(RUNTIME_ROOT, "delivery");
136
136
  const SECRET_ROOT = "/run/sellable-agent/admin-runtime";
137
- // Keep the Product MCP's Agent credential outside its mutable JSON config.
138
- // The MCP re-reads that config on every request and ordinary login tooling can
139
- // update it, so the credential file is the durable service-authority source.
140
- const PRODUCT_MCP_CREDENTIAL_ROOT = join(
141
- SECRET_ROOT,
142
- "product-mcp-credentials"
143
- );
144
137
  // Turn descriptor + consumed roots for the Product MCP context proxy. They are
145
138
  // normally empty and receive one signed descriptor only while the fenced
146
139
  // endpoint relay owns a turn; the proxy fails closed if either root is absent.
@@ -597,6 +590,7 @@ async function inspectMcp({
597
590
  args,
598
591
  env,
599
592
  packageProof,
593
+ probeCall = null,
600
594
  }) {
601
595
  const child = spawn(command, args, {
602
596
  env: { ...process.env, ...env },
@@ -659,6 +653,9 @@ async function inspectMcp({
659
653
  : [];
660
654
  if (tools.length === 0)
661
655
  throw new Error(`admin_runtime_${alias}_inventory_empty`);
656
+ const safeRead = probeCall
657
+ ? await request(3, "tools/call", probeCall)
658
+ : null;
662
659
  return {
663
660
  alias,
664
661
  packageName: packageProof.name,
@@ -669,6 +666,7 @@ async function inspectMcp({
669
666
  toolCount: tools.length,
670
667
  toolInventoryDigest: sha256(JSON.stringify(tools)),
671
668
  tools,
669
+ ...(safeRead === null ? {} : { safeRead }),
672
670
  };
673
671
  } catch (error) {
674
672
  throw new Error(
@@ -680,6 +678,64 @@ async function inspectMcp({
680
678
  }
681
679
  }
682
680
 
681
+ async function proveAdminProductMcpReadiness({ profile, claim, installed }) {
682
+ const slack = claim?.runtimeContract?.slack;
683
+ const channelId = slack?.selectedBindings?.[0]?.channelId;
684
+ const requesterId = slack?.memberAllowlist?.[0];
685
+ const packageProof = profile?.stage?.packages?.product;
686
+ if (
687
+ !/^[A-Za-z0-9_-]{1,128}$/.test(channelId ?? "") ||
688
+ !/^[A-Za-z0-9_-]{1,128}$/.test(requesterId ?? "") ||
689
+ !packageProof ||
690
+ typeof installed?.productConfigPath !== "string"
691
+ ) {
692
+ throw new Error("admin_product_mcp_service_context_rejected");
693
+ }
694
+ try {
695
+ const layout = adminHomeLayout(profile.profileHome);
696
+ const proof = await inspectMcp({
697
+ alias: "sellable",
698
+ command: NODE,
699
+ args: [MCP_CONTEXT_PROXY],
700
+ env: {
701
+ HOME: profile.profileHome,
702
+ HERMES_HOME: DATA_ROOT,
703
+ ...adminAgentServiceEnvironment(claim),
704
+ ...productMcpContextProxyEnvironment(layout),
705
+ SELLABLE_CONFIG_PATH: installed.productConfigPath,
706
+ },
707
+ packageProof,
708
+ probeCall: {
709
+ name: "get_company_info",
710
+ arguments: {},
711
+ _meta: {
712
+ sellableAgent: {
713
+ channelId,
714
+ requesterId: claim.runtimeContract.slack.memberAllowlist[0],
715
+ providerRequestId: `admin-runtime-readiness-${claim.operation.id}`,
716
+ },
717
+ },
718
+ },
719
+ });
720
+ if (
721
+ !proof.safeRead ||
722
+ typeof proof.safeRead !== "object" ||
723
+ proof.safeRead.isError === true
724
+ ) {
725
+ throw new Error("admin_product_mcp_service_context_rejected");
726
+ }
727
+ return Object.freeze({
728
+ authenticated: true,
729
+ safeRead: true,
730
+ packageName: proof.packageName,
731
+ packageVersion: proof.packageVersion,
732
+ toolInventoryDigest: proof.toolInventoryDigest,
733
+ });
734
+ } catch {
735
+ throw new Error("admin_product_mcp_service_context_rejected");
736
+ }
737
+ }
738
+
683
739
  function exactSoul(claim) {
684
740
  const artifacts = claim?.revision?.artifacts?.filter(
685
741
  (artifact) =>
@@ -1326,41 +1382,40 @@ async function materializeAdminProfile({
1326
1382
  memoryCron,
1327
1383
  opportunitySequencesCron,
1328
1384
  clientRelationshipsCrons,
1329
- } = await runPhase(
1330
- "profile_crons",
1331
- async () => {
1332
- const adminHome = installAdminHome({ settingsRoot, layout });
1333
- const memoryCron = await ensureHermesSnapshotCron({
1385
+ } = await runPhase("profile_crons", async () => {
1386
+ const adminHome = installAdminHome({ settingsRoot, layout });
1387
+ const memoryCron = await ensureHermesSnapshotCron({
1388
+ profileId: profile,
1389
+ profileRoot: profileHome,
1390
+ hermesRoot: DATA_ROOT,
1391
+ hermesCli: HERMES,
1392
+ });
1393
+ if (!memoryCron.ok) throw new Error(memoryCron.code);
1394
+ const opportunitySequencesCron = await ensureHermesOpportunitySequencesCron(
1395
+ {
1334
1396
  profileId: profile,
1335
1397
  profileRoot: profileHome,
1336
1398
  hermesRoot: DATA_ROOT,
1337
- hermesCli: HERMES,
1338
- });
1339
- if (!memoryCron.ok) throw new Error(memoryCron.code);
1340
- const opportunitySequencesCron =
1341
- await ensureHermesOpportunitySequencesCron({
1342
- profileId: profile,
1343
- profileRoot: profileHome,
1344
- hermesRoot: DATA_ROOT,
1345
- statePath: layout.statePath,
1346
- });
1347
- // Ordered after its sibling and NON-FATAL: an Admin runtime must not fail
1348
- // to promote because one client's digest cron could not be written.
1349
- const clientRelationshipsCrons =
1350
- await ensureHermesClientRelationshipsCrons({
1351
- profileId: profile,
1352
- profileRoot: profileHome,
1353
- hermesRoot: DATA_ROOT,
1354
- statePath: layout.statePath,
1355
- });
1356
- return {
1357
- adminHome,
1358
- memoryCron,
1359
- opportunitySequencesCron,
1360
- clientRelationshipsCrons,
1361
- };
1362
- }
1363
- );
1399
+ statePath: layout.statePath,
1400
+ }
1401
+ );
1402
+ // Ordered after its sibling and NON-FATAL: an Admin runtime must not fail
1403
+ // to promote because one client's digest cron could not be written.
1404
+ const clientRelationshipsCrons = await ensureHermesClientRelationshipsCrons(
1405
+ {
1406
+ profileId: profile,
1407
+ profileRoot: profileHome,
1408
+ hermesRoot: DATA_ROOT,
1409
+ statePath: layout.statePath,
1410
+ }
1411
+ );
1412
+ return {
1413
+ adminHome,
1414
+ memoryCron,
1415
+ opportunitySequencesCron,
1416
+ clientRelationshipsCrons,
1417
+ };
1418
+ });
1364
1419
  return {
1365
1420
  stage,
1366
1421
  promotion: promoted.promotion,
@@ -1805,7 +1860,6 @@ function installRuntimeSecrets({ claim, secrets, desiredLifecycle }) {
1805
1860
  claim,
1806
1861
  secret: secrets.mcpService,
1807
1862
  });
1808
- const serviceAuthority = claim.runtimeContract.service;
1809
1863
  const modelAuthority = claim?.runtimeContract?.model;
1810
1864
  const modelEnvelope = claim?.secretEnvelopes?.modelCodex;
1811
1865
  const modelTuple = claim?.adminRuntimeGeneration?.tuple;
@@ -1896,29 +1950,18 @@ function installRuntimeSecrets({ claim, secrets, desiredLifecycle }) {
1896
1950
  slackSecretFile,
1897
1951
  `SLACK_BOT_TOKEN=${secrets.slackBot}\nSLACK_APP_TOKEN=${secrets.slackApp}\n`
1898
1952
  );
1899
- const productCredentialDirectory = join(
1900
- PRODUCT_MCP_CREDENTIAL_ROOT,
1901
- claim.operation.profileId
1902
- );
1903
- const productCredentialPath = join(
1904
- productCredentialDirectory,
1905
- "service-credential"
1906
- );
1907
- privateDirectory(PRODUCT_MCP_CREDENTIAL_ROOT);
1908
- privateDirectory(productCredentialDirectory);
1909
- atomicPrivateFile(productCredentialPath, `${secrets.mcpService}\n`);
1953
+ // Admin's service credential is runtime-owned and lives only in this 0600
1954
+ // tmpfs config. It is never copied into the durable profile or exposed to
1955
+ // login UX. Product MCP's managed-login guard still prevents a human token
1956
+ // from replacing this machine-minted Agent identity.
1910
1957
  const productConfigPath = join(SECRET_ROOT, "sellable-config.json");
1911
1958
  atomicPrivateFile(
1912
1959
  productConfigPath,
1913
1960
  `${JSON.stringify({
1914
1961
  apiUrl: ORIGIN,
1962
+ token: secrets.mcpService,
1915
1963
  activeWorkspaceId: claim.operation.workspaceId,
1916
1964
  workspaceId: claim.operation.workspaceId,
1917
- serviceCredential: {
1918
- reference: `secret://agent/service/g${serviceAuthority.credentialGeneration}`,
1919
- fingerprint: serviceAuthority.credentialFingerprint,
1920
- generation: serviceAuthority.credentialGeneration,
1921
- },
1922
1965
  })}\n`
1923
1966
  );
1924
1967
  return {
@@ -2118,7 +2161,6 @@ function clearRuntimeSecrets() {
2118
2161
  ]) {
2119
2162
  rmSync(join(SECRET_ROOT, name), { force: true });
2120
2163
  }
2121
- rmSync(PRODUCT_MCP_CREDENTIAL_ROOT, { recursive: true, force: true });
2122
2164
  }
2123
2165
 
2124
2166
  function adminAgentServiceEnvironment(claim) {
@@ -2176,13 +2218,6 @@ function adminAgentServiceEnvironment(claim) {
2176
2218
  SELLABLE_AGENT_SERVER_AUTHENTICATED_KIND: "ADMIN",
2177
2219
  SELLABLE_AGENT_RUNTIME: "1",
2178
2220
  SELLABLE_AGENT_RUNTIME_GENERATION: String(RUNTIME_GENERATION),
2179
- SELLABLE_AGENT_PROFILE_ID: operation.profileId,
2180
- SELLABLE_AGENT_RUNTIME_SECRET_ROOT: PRODUCT_MCP_CREDENTIAL_ROOT,
2181
- SELLABLE_AGENT_SERVICE_CREDENTIAL_FILE: join(
2182
- PRODUCT_MCP_CREDENTIAL_ROOT,
2183
- operation.profileId,
2184
- "service-credential"
2185
- ),
2186
2221
  SELLABLE_AGENT_ADMIN_GENERATION_ID: claim.adminRuntimeGeneration.id,
2187
2222
  SELLABLE_AGENT_ADMIN_GENERATION_FENCE: String(
2188
2223
  claim.adminRuntimeGeneration.fence
@@ -4019,6 +4054,9 @@ async function runAdminProduction() {
4019
4054
  agentServiceEnv: adminAgentServiceEnvironment(claim),
4020
4055
  runPhase: phaseTelemetry.run,
4021
4056
  });
4057
+ await phaseTelemetry.run("product_mcp_readiness", async () =>
4058
+ proveAdminProductMcpReadiness({ profile, claim, installed })
4059
+ );
4022
4060
  await operationHeartbeat.reattest();
4023
4061
  const materializerReceipt = adminMaterializerReceipt(
4024
4062
  profile.profile,
@@ -8,7 +8,7 @@ ENV PLAYWRIGHT_BROWSERS_PATH=/opt/hermes/.playwright
8
8
 
9
9
  ARG MCP_PACKAGE=@sellable/mcp@0.1.915
10
10
  ARG MCP_INTEGRITY=sha512-+WdyO3WxQ9U7fHMvxBojPRDIZun/INy9mKOz/A1IcBFEWXFkpPooT0yMUzY1qCYcewTM1AUxP3p2QOtsxRIYQA==
11
- ARG INSTALLER_PACKAGE=@sellable/install@0.1.719
11
+ ARG INSTALLER_PACKAGE=@sellable/install@0.1.720
12
12
  ARG ADMIN_MCP_PACKAGE=@sellable/admin-mcp@0.1.186-wip.phase158.20260802034727
13
13
  ARG ADMIN_MCP_INTEGRITY=sha512-qYNew5wd2IfgVcp1UYOG9QvpF/8Mx19YJ5HQtMKzvSfUf3evePjtaKsoXLf2C/GUJ5QlLlHJo84VWOI3V01+9A==
14
14
  ARG TUS_JS_CLIENT_VERSION=4.3.1
@@ -156,6 +156,7 @@ RUN set -eux; \
156
156
  /etc/s6-overlay/s6-rc.d/user/contents.d/customer-runtime
157
157
 
158
158
  LABEL io.sellable.runtime.kind="customer" \
159
+ io.sellable.runtime.product-mcp-readiness="service-authenticated-safe-read/v1" \
159
160
  io.sellable.runtime.listener="native-dedicated-slack" \
160
161
  io.sellable.runtime.api-server="authenticated-loopback-127.0.0.1:8642" \
161
162
  io.sellable.runtime.installers="build-only" \
@@ -107,7 +107,7 @@ const RUNTIME_GENERATION = Number(
107
107
  const BOOT_SESSION_ID = randomUUID();
108
108
  const HERMES_VERSION = process.env.SELLABLE_AGENT_HERMES_VERSION ?? "0.20.4";
109
109
  const INSTALLER_PACKAGE =
110
- process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.719";
110
+ process.env.SELLABLE_AGENT_INSTALLER_PACKAGE ?? "@sellable/install@0.1.720";
111
111
  const MCP_PACKAGE =
112
112
  process.env.SELLABLE_AGENT_MCP_PACKAGE ??
113
113
  "@sellable/mcp@0.1.915";
@@ -38,7 +38,7 @@ import { installAgentHostServices } from "./service-installer.mjs";
38
38
  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
- const INSTALLER_PACKAGE = "@sellable/install@0.1.719";
41
+ const INSTALLER_PACKAGE = "@sellable/install@0.1.720";
42
42
  const MCP_PACKAGE = "@sellable/mcp@0.1.915";
43
43
  const HERMES_VERSION = "0.18.0";
44
44
  const SHA256 = /^[a-f0-9]{64}$/;
@@ -1547,7 +1547,7 @@ export function compileClaimToProfileDesired(activeClaim, config) {
1547
1547
  pinned.hermesCli !== "hermes" ||
1548
1548
  pinned.hermesVersion !== "0.18.0" ||
1549
1549
  pinned.installerPackage !==
1550
- "@sellable/install@0.1.719" ||
1550
+ "@sellable/install@0.1.720" ||
1551
1551
  pinned.mcpPackage !== "@sellable/mcp@0.1.915" ||
1552
1552
  !Array.isArray(policy.toolInclude) ||
1553
1553
  policy.toolInclude.length === 0 ||
@@ -283,7 +283,7 @@ function validateDesired(desired) {
283
283
  desired.hermesCli !== "hermes" ||
284
284
  desired.hermesVersion !== "0.18.0" ||
285
285
  desired.installerPackage !==
286
- "@sellable/install@0.1.719" ||
286
+ "@sellable/install@0.1.720" ||
287
287
  desired.mcpPackage !== "@sellable/mcp@0.1.915" ||
288
288
  !Array.isArray(desired.toolInclude) ||
289
289
  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.719";
29
+ "@sellable/install@0.1.720";
30
30
  export const PINNED_MCP_PACKAGE = "@sellable/mcp@0.1.915";
31
31
  const PINNED_INSTALL_VERSION = PINNED_INSTALL_PACKAGE.slice(
32
32
  PINNED_INSTALL_PACKAGE.lastIndexOf("@") + 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.719",
3
+ "version": "0.1.720",
4
4
  "type": "module",
5
5
  "description": "One-command installer for Sellable MCP in Claude Code, Codex, and Hermes",
6
6
  "bin": {