@sellable/install 0.1.362-phase111.20260720123904 → 0.1.362

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.
@@ -23,6 +23,7 @@ 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",
26
27
  "INSTALL_RUNTIME_SERVICE_ENV",
27
28
  "SET_RUNTIME_GATES",
28
29
  "OBSERVE_RUNTIME_GATES",
@@ -50,6 +51,7 @@ const PROFILE_ENTRIES = Object.freeze([
50
51
  ]);
51
52
  const ACTION_KEYS = Object.freeze({
52
53
  INSTALL_MCP_CREDENTIAL: ["action", "profileId", "operationId", "fence", "sourceRequestFile", "generation", "fingerprint"],
54
+ INSTALL_PROVIDER_CREDENTIAL: ["action", "profileId", "operationId", "fence", "fingerprint"],
53
55
  INSTALL_RUNTIME_SERVICE_ENV: ["action", "profileId", "operationId", "fence", "revisionId", "profileDigest"],
54
56
  SET_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
55
57
  OBSERVE_RUNTIME_GATES: ["action", "profileId", "operationId", "fence", "lifecycle"],
@@ -109,6 +111,79 @@ function atomicCredential(pathValue, secret, uid, gid) {
109
111
  fsyncDirectory(dirname(pathValue));
110
112
  }
111
113
 
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
+
112
187
  function applyRuntimeOwnership(pathValue, uid, gid) {
113
188
  const link = lstatSync(pathValue);
114
189
  if (link.isSymbolicLink()) throw new Error("profile symlink rejected");
@@ -509,7 +584,9 @@ function walkExternalRoot(root, ownerUid, ownerGid) {
509
584
  }
510
585
  };
511
586
  visit(root, "");
512
- return { directories: directories.sort((left, right) => left.path.localeCompare(right.path)), files };
587
+ const byPath = (left, right) => left.path < right.path ? -1 : left.path > right.path ? 1 : 0;
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) };
513
590
  }
514
591
 
515
592
  export function observeExternalRuntimeClosure(input = {}) {
@@ -569,18 +646,24 @@ export function observeExternalRuntimeClosure(input = {}) {
569
646
  if (
570
647
  JSON.stringify(rootRecord.directories.map((directory) => directory?.path)) !==
571
648
  JSON.stringify(rootRecord.directories.map((directory) => directory?.path).sort()) ||
572
- new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length ||
649
+ new Set(rootRecord.directories.map((directory) => directory?.path)).size !== rootRecord.directories.length
650
+ ) throw new Error("external runtime directory order rejected");
651
+ if (
573
652
  rootRecord.directories.some((directory) =>
574
653
  !exactKeys(directory, ["path", "mode"]) ||
575
- !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(directory.path ?? "") ||
654
+ !/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(directory.path ?? "") ||
576
655
  directory.mode !== 0o555
577
- ) ||
578
- JSON.stringify(walked.directories) !== JSON.stringify(rootRecord.directories) ||
656
+ )
657
+ ) throw new Error("external runtime directory schema rejected");
658
+ if (JSON.stringify(walked.directories) !== JSON.stringify(rootRecord.directories)) {
659
+ throw new Error("external runtime directory inventory rejected");
660
+ }
661
+ if (
579
662
  rootRecord.files.length === 0 ||
580
663
  JSON.stringify(rootRecord.files.map((file) => file?.path)) !==
581
664
  JSON.stringify(rootRecord.files.map((file) => file?.path).sort()) ||
582
665
  new Set(rootRecord.files.map((file) => file?.path)).size !== rootRecord.files.length
583
- ) throw new Error("external runtime inventory rejected");
666
+ ) throw new Error("external runtime file order rejected");
584
667
  const actualPaths = walked.files.map((file) => file.logicalPath);
585
668
  if (JSON.stringify(actualPaths) !== JSON.stringify(rootRecord.files.map((file) => file.path))) {
586
669
  throw new Error("external runtime open inventory rejected");
@@ -588,7 +671,7 @@ export function observeExternalRuntimeClosure(input = {}) {
588
671
  const observedFiles = rootRecord.files.map((file, index) => {
589
672
  if (
590
673
  !exactKeys(file, ["path", "mode", "digest"]) ||
591
- !/^[A-Za-z0-9._-]+(?:\/[A-Za-z0-9._-]+)*$/.test(file.path ?? "") ||
674
+ !/^[-A-Za-z0-9@._+]+(?:\/[-A-Za-z0-9@._+]+)*$/.test(file.path ?? "") ||
592
675
  ![0o444, 0o555].includes(file.mode) || !SHA256.test(file.digest ?? "")
593
676
  ) throw new Error("external runtime file schema rejected");
594
677
  const actual = walked.files[index];
@@ -958,9 +1041,19 @@ async function observeRuntime(profile, request) {
958
1041
  const profileFiles = observeRuntimeProfileFiles(target);
959
1042
  const { runtimeConfig, runtimeMcp, tools, fileHashes: actualFileHashes } = profileFiles;
960
1043
  const actualProfileDigest = profileFiles.profileDigest;
961
- const observedProvider = environmentDrift
962
- ? sha256(`environment-drift\0${runtimeConfig?.provider?.fingerprint ?? "absent"}`)
963
- : runtimeConfig?.provider?.fingerprint;
1044
+ let observedProvider;
1045
+ try {
1046
+ observedProvider = observeProviderCredential(profile, request.profileId);
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
+ }
964
1057
  return {
965
1058
  profileId: manifest.profileId,
966
1059
  workspaceId: manifest.workspaceId,
@@ -1172,6 +1265,9 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
1172
1265
  generation: request.generation,
1173
1266
  };
1174
1267
  }
1268
+ if (request.action === "INSTALL_PROVIDER_CREDENTIAL") {
1269
+ return installProviderCredential(profile, request, config);
1270
+ }
1175
1271
  if (request.action === "INSTALL_RUNTIME_SERVICE_ENV") {
1176
1272
  return installRuntimeServiceEnvironment(profile, request);
1177
1273
  }
@@ -1187,6 +1283,7 @@ export async function runRuntimeHelperRequest({ requestPath, config }) {
1187
1283
  rmSync(target, { recursive: true, force: true });
1188
1284
  fsyncDirectory(root);
1189
1285
  }
1286
+ removeProviderCredential(profile, request.profileId);
1190
1287
  return { ok: true, profileId: request.profileId, disposition: "REMOVED" };
1191
1288
  }
1192
1289
  if (request.action === "CHOWN_PROFILE") {
@@ -247,6 +247,8 @@ 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 ?? "") ||
250
252
  profile.runtimeServicePath !== "/etc/services.d/sellable-agent-runtime" ||
251
253
  profile.runtimeServiceEnvRoot !== "/etc/services.d/sellable-agent-runtime/env" ||
252
254
  new Set([profile.runtimeUid, profile.proxyUid, profile.workerUid]).size !== 3
package/package.json CHANGED
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@sellable/install",
3
- "version": "0.1.362-phase111.20260720123904",
3
+ "version": "0.1.362",
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",
8
9
  "sellable-agent-egress-proxy": "bin/sellable-agent-egress-proxy.mjs",
9
10
  "sellable-agent-host-worker": "bin/sellable-agent-host-worker.mjs",
10
11
  "sellable-agent-runtime-helper": "bin/sellable-agent-runtime-helper.mjs",
@@ -20,6 +21,7 @@
20
21
  "bin",
21
22
  "lib",
22
23
  "services",
24
+ "container",
23
25
  "agents",
24
26
  "skill-templates",
25
27
  "README.md",
@@ -17,8 +17,3 @@ 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,35 +52,9 @@ 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, runHandle:{runId,fence}, targetConfig, reportingContext, messageTemplateRevision? })
55
+ refill_sends_v2({ workspaceId, runId, fence })
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
-
84
58
  If a stale handle loses the lease, the tool reports the holder status and the
85
59
  approximately 10 minute lockout window. Reinvoke with the current handle or wait
86
60
  for lease expiry; never guess a fence.
@@ -111,10 +85,6 @@ The run may execute only packet-named bounded work:
111
85
  when the campaign is in the packet's pinned lane chain;
112
86
  - refresh paid InMail credit facts during bootstrap and once per scheduler-wait
113
87
  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.
118
88
 
119
89
  The loop records planned -> did -> outcome before and after every foreign
120
90
  mutation. It refuses stale packets by fingerprint and validates packet
@@ -198,10 +168,5 @@ reason or resume handle, lane source, lane chain, chosen label with token
198
168
  secondary, plan revision, journal path, blocked continuation packets, and
199
169
  firstFailing checklist when present.
200
170
 
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
-
206
171
  Real-run journals live under `~/.sellable/refill/runs` by default and append an
207
172
  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", runHandle?: RefillRunHandleV1, targetConfig?: RefillTargetConfigV1, reportingContext?: RefillReportingContextV2, messageTemplateRevision?: MessageTemplateRevisionV1 })
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", runId?: string, fence?: number })
97
97
  ```
98
98
 
99
99
  That command helper normalizes arguments and returns the execution contract. In
@@ -104,33 +104,6 @@ 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
107
  Safe primitives are paid-credit refresh,
135
108
  existing-row message preparation, generated-message approval, receipt-proven
136
109
  same-source row copy, one bounded Signal Discovery search derived only from
@@ -594,11 +567,6 @@ scheduler wait loop. Do not finish the refill goal, mark it complete, or mark it
594
567
  blocked only because it is still `awaiting_scheduler_after_ready_buffer`; keep
595
568
  waiting unless Christian stops the run or the host cannot continue.
596
569
 
597
- Carry the canonical `refill_reporting.v2` DTO through progress, continuation,
598
- replay, and terminal reporting without recomputing counts or losing live epoch,
599
- snapshot sequence, reset reason, watermark, blockers, compatibility, or
600
- terminal outcome. Redact raw copy and prospect fields.
601
-
602
570
  Future scheduled coverage and already sent actions are distinct; only
603
571
  scheduler-owned future scheduled actions count as scheduled coverage. For
604
572
  multiple target dates, finish D1 execution and the full D1 reread before