@sellable/install 0.1.321 → 0.1.323

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.
@@ -1,67 +1,169 @@
1
1
  # Hermes Slack Profile Scoping
2
2
 
3
- Sellable customer profiles use one dedicated native Slack app/token pair, one
4
- workspace-locked plain Sellable MCP, and one native Hermes s6 gateway. There is
5
- no shared dispatcher, route registry, listener fallback, or shared customer app
3
+ Use this when creating or updating customer Hermes profiles that need Slack
4
+ access.
5
+
6
+ ## Default Rule
7
+
8
+ For Sellable-managed customer channels, use one dedicated Slack app/token pair
9
+ per active Hermes customer profile. Run native `hermes -p <profile> gateway run`
10
+ for that profile only. Do not run multiple active Hermes profile gateways
11
+ against the same `SLACK_APP_TOKEN`.
12
+
13
+ The customer's Slack app is bound to the exact Slack channel id and the
14
+ customer's profile-local Sellable MCP config. Customer profiles must not store
15
+ or read Sellable Admin MCP credentials, admin Slack app tokens, or sibling
16
+ profile config.
17
+
18
+ The shared dispatcher remains a fallback/research path only. Use it only when
19
+ Sellable Admin explicitly owns one shared listener and route registry.
20
+
21
+ The 2026-07-06 Hostinger POC proved the native Hermes listener for the
22
+ `sellable-admin` profile with the existing `Sellable Reply Bot` app in
23
+ `#team-chat`: a human mention produced a new Hermes session and a threaded bot
24
+ reply. Treat that as proof that the gateway/listener path works for one owning
25
+ profile, not as approval for multiple active customer profiles sharing one app
6
26
  token.
7
27
 
8
- The production Sellable Reply Bot remains separate HTTP-interactivity product
9
- infrastructure. Keep Socket Mode off and keep its request URL at
10
- `https://app.sellable.dev/api/v2/slack/interactions`.
28
+ The same day, the `acme` profile reused the same redacted Slack token
29
+ fingerprints for an outbound-only smoke to `#sellable-acme` via:
30
+
31
+ ```bash
32
+ hermes --profile acme send --to slack:C0BFERDV3N0 --subject '[Hermes acme smoke]' ...
33
+ ```
34
+
35
+ That succeeded and is useful as an internal smoke test. It does not change the
36
+ inbound listener rule: each active customer profile needs its own native Slack
37
+ app token pair unless Sellable Admin deliberately chooses the dispatcher
38
+ fallback.
39
+
40
+ ## Which Slack Values Are Enough
41
+
42
+ For outbound sends and ordinary Slack Web API calls, the important value is the
43
+ profile-local `SLACK_BOT_TOKEN`.
44
+
45
+ For native Hermes live listening through Slack Socket Mode, the profile also
46
+ needs `SLACK_APP_TOKEN`, an app-level `xapp-...` token with `connections:write`.
47
+ `SLACK_APP_ID`, `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`,
48
+ `SLACK_SIGNING_SECRET`, `SLACK_VERIFICATION_TOKEN`, `SLACK_TEAM_ID`, and
49
+ `SLACK_OPERATOR_EMAIL` do not replace `SLACK_APP_TOKEN`.
50
+
51
+ ## Required Slack App Shape
52
+
53
+ Outbound Slack sends can work with fewer permissions. Native Hermes inbound
54
+ listening needs the Slack app itself to be configured for Socket Mode and event
55
+ delivery:
11
56
 
12
- ## Public Boundary
57
+ - Enable Socket Mode.
58
+ - Create an app-level token with `connections:write`.
59
+ - Install the bot with `app_mentions:read`, `channels:history`,
60
+ `channels:read`, `chat:write`, `groups:history`, `groups:read`,
61
+ `im:history`, `im:read`, `mpim:history`, `mpim:read`, and `users:read`.
62
+ - Subscribe bot events for `app_mention`, `message.channels`,
63
+ `message.groups`, `message.im`, and `message.mpim`.
64
+ - Reinstall the app after changing scopes or event subscriptions.
13
65
 
14
- Customer onboarding is owned by the Sellable Admin native-app skill through one
15
- public command:
66
+ If the Slack app has `incoming-webhook`, Slack will ask for a webhook channel
67
+ during reinstall. Pick the profile home channel. That webhook selection does
68
+ not replace `SLACK_ALLOWED_CHANNELS`; keep the profile-local allowlist explicit.
69
+
70
+ ## Profile Bootstrap Command
71
+
72
+ Use the Sellable installer profile bootstrap:
16
73
 
17
74
  ```bash
18
- sellable-admin-install hermes customer bootstrap --mode <mode> [approved inputs]
75
+ sellable hermes profile bootstrap \
76
+ --profile acme \
77
+ --profiles-root /srv/hermes/profiles \
78
+ --workspace-id ws_acme \
79
+ --workspace-name Acme \
80
+ --token-file /run/secrets/sellable-acme-token \
81
+ --slack-bot-token "$SLACK_BOT_TOKEN" \
82
+ --slack-app-token "$SLACK_APP_TOKEN" \
83
+ --slack-home-channel C0ACME12345 \
84
+ --slack-home-channel-name sellable-acme \
85
+ --slack-allowed-users U0OPERATOR1 \
86
+ --slack-require-mention true \
87
+ --json
19
88
  ```
20
89
 
21
- The operator uses read-only `plan`, then approved `apply`, and later
22
- `resume|status|verify|rebuild|rollback` with the persisted `runId`. Do not ask
23
- the operator to coordinate the public Sellable installer, Slack API/CLI,
24
- cloud-browser, Gmail, profile, or gateway internals separately.
90
+ If `--slack-home-channel` is supplied and `--slack-allowed-channels` is omitted,
91
+ the installer writes `SLACK_ALLOWED_CHANNELS=<home-channel-id>` automatically.
92
+ That makes the profile fail closed to the customer channel by default.
25
93
 
26
- The approval packet binds exact team/workspace/channel/profile/user identity,
27
- customer Sellable token-file ref, `gpt-5.6-sol`, redacted evidence root, and the
28
- exact tested Admin install/MCP and Sellable install/MCP candidates plus all four
29
- SHA-256 values.
94
+ Use `--slack-allowed-channels C...,G...` only when a profile is intentionally
95
+ allowed to listen in more than one channel. Pass Slack IDs, not `#channel-name`
96
+ strings or wildcards.
30
97
 
31
- ## Public Sellable Installer Role
98
+ ## Resulting Profile Env
32
99
 
33
- The Admin factory calls the pinned `@sellable/install` candidate internally and
34
- passes the pinned `@sellable/mcp` candidate through `--mcp-package`. The public
35
- installer writes profile-local Sellable config and Hermes MCP env with:
100
+ For the dedicated customer Slack app model, the profile-local `.env` should
101
+ contain only scoped Slack keys for that profile:
102
+
103
+ ```env
104
+ SLACK_BOT_TOKEN=...
105
+ SLACK_APP_TOKEN=...
106
+ SLACK_HOME_CHANNEL=C0ACME12345
107
+ SLACK_HOME_CHANNEL_NAME=sellable-acme
108
+ SLACK_ALLOWED_CHANNELS=C0ACME12345
109
+ SLACK_ALLOWED_USERS=U0OPERATOR1
110
+ SLACK_REQUIRE_MENTION=true
111
+ ```
112
+
113
+ Keep the file mode at `0600`. Do not copy raw tokens into planning docs, chat,
114
+ or committed artifacts. Record token fingerprints only.
115
+
116
+ The profile's Hermes MCP env must also include the profile-local Sellable
117
+ config, customer workspace lock, and fail-closed guard:
36
118
 
37
119
  ```yaml
38
- SELLABLE_CONFIG_PATH: /opt/data/profiles/<customerSlug>/sellable/config.json
39
- SELLABLE_CONFIGS_DIR: /opt/data/profiles/<customerSlug>/sellable/configs
40
- SELLABLE_LOCK_WORKSPACE_ID: <approvedWorkspaceId>
120
+ SELLABLE_CONFIG_PATH: /srv/hermes/profiles/acme/sellable/config.json
121
+ SELLABLE_CONFIGS_DIR: /srv/hermes/profiles/acme/sellable/configs
122
+ SELLABLE_LOCK_WORKSPACE_ID: ws_acme
41
123
  SELLABLE_REQUIRE_WORKSPACE_LOCK: "1"
42
124
  ```
43
125
 
44
- It does not create Slack apps, handle Slack tokens, install Admin MCP, or choose
45
- the profile/workspace/channel from prompt text.
126
+ `SELLABLE_REQUIRE_WORKSPACE_LOCK=1` means an unbound customer profile fails
127
+ closed before making Sellable API calls instead of falling back to shared or
128
+ admin config state. Only the `sellable-admin` profile should provision,
129
+ inspect, or repair other profiles.
130
+
131
+ For the shared dispatcher model, do not copy the shared `SLACK_BOT_TOKEN` or
132
+ `SLACK_APP_TOKEN` into customer profile `.env` files. The dispatcher owns those
133
+ tokens and route metadata decides the customer profile.
46
134
 
47
- ## Customer Isolation
135
+ ## Shared Dispatcher Ownership
136
+
137
+ The shared Slack dispatcher and its route registry are fallback Sellable Admin
138
+ surfaces. This customer-facing installer must not create shared route bundles or
139
+ write shared listener tokens. Sellable Admin owns Slack app creation and profile
140
+ provisioning, then calls this installer for profile-local Sellable runtime
141
+ bootstrap inside the already-provisioned Hermes profile.
142
+
143
+ ## Verification
144
+
145
+ Run the installer and dispatcher UAT before publishing installer changes:
146
+
147
+ ```bash
148
+ npm run test:unit -- tests/install-package/agent-preferences.test.ts
149
+ scripts/run-phase92-hermes-profile-bootstrap-uat.sh --mode local-temp
150
+ scripts/run-phase92-hermes-profile-bootstrap-uat.sh --mode linux-shaped
151
+ scripts/run-phase031-hermes-slack-dispatcher-uat.sh --mode local-fixture
152
+ ```
48
153
 
49
- Customer profiles are blank-created. They contain:
154
+ Expected proof:
50
155
 
51
- - dedicated profile-local Slack runtime secrets in mode-`0600` storage
52
- - exact channel and allowed-human ID allowlists
53
- - customer-safe manifest with no slash/App Home/assistant/DM/MPIM surfaces
54
- - the current `openai-codex` `gpt-5.6-sol` model object
55
- - Hermes root OAuth fallback from `/opt/data/auth.json`
56
- - pinned plain Sellable MCP locked to one approved workspace
57
- - one profile-specific native s6 gateway
156
+ - profile-local Sellable config paths remain authoritative
157
+ - generated profile `.env` has `SLACK_HOME_CHANNEL`
158
+ - generated profile `.env` has `SLACK_ALLOWED_CHANNELS`
159
+ - `acme` allowlist points at the `sellable-acme` fixture channel id
160
+ - redaction scan passes
58
161
 
59
- They never contain customer-local `auth.json`, provider pools, OpenRouter/raw
60
- OpenAI keys, Sellable Admin MCP/admin skills, browser/Gmail/provisioning
61
- credentials, TOTP material, or sibling data.
162
+ ## Current Phase 03 Status
62
163
 
63
- Direct MCP proof before gateway start must show one intended active/visible
64
- workspace, `siblingInventoryFetched=false`, and no Admin MCP. Slack acceptance
65
- then requires real API/CLI, secret-file, s6 process, same-thread reply,
66
- negative-isolation, restart, rollback, rebuild, and redaction receipts tied to
67
- the immutable package hashes.
164
+ Phase 03 proved the profile-scoped installer path, outbound Hermes send path,
165
+ and live Slack listener for the `sellable-admin` profile. Phase 03.1 now targets
166
+ dedicated native Slack apps per customer profile, profile-local Sellable MCP
167
+ locks, and fail-closed customer profile behavior so managed customer channels
168
+ can be onboarded through the VPS without local-only browser state, manual SSH
169
+ edits, or untracked processes.
package/README.md CHANGED
@@ -83,37 +83,120 @@ Auth is stored once at:
83
83
  ~/.sellable/config.json
84
84
  ```
85
85
 
86
- ## Hermes Customer Profiles
86
+ ## Hermes Profile-Scoped Installs
87
87
 
88
- Customer onboarding is owned by the Sellable Admin native customer bootstrap:
88
+ For a single local Hermes profile, point Sellable auth and memory at the profile
89
+ instead of the global `~/.sellable/config.json`:
89
90
 
90
91
  ```bash
91
- sellable-admin-install hermes customer bootstrap --mode <mode> [approved inputs]
92
+ profile="$HOME/.hermes/profiles/acme"
93
+ sellable --host hermes \
94
+ --sellable-config-path "$profile/sellable/config.json" \
95
+ --sellable-configs-dir "$profile/sellable/configs"
92
96
  ```
93
97
 
94
- That command is the sole operator-facing interface. It calls the exact pinned
95
- `@sellable/install` tarball internally and passes the exact pinned
96
- `@sellable/mcp` tarball through `--mcp-package`. Do not ask an operator to run
97
- the public installer, supply Slack tokens in argv, edit profile files, or manage
98
- the gateway as separate onboarding steps.
98
+ That writes `SELLABLE_CONFIG_PATH` and `SELLABLE_CONFIGS_DIR` into
99
+ `mcp_servers.sellable.env`, so first-run login, `get_auth_status`, workspace
100
+ switches, and Sellable memory all use the same profile-local files. Hosted MCP
101
+ mode does not receive local filesystem paths.
99
102
 
100
- The internal profile stage writes profile-local Sellable paths and the exact
101
- workspace boundary:
103
+ For zero-shot local or Hostinger/Linux VPS bootstrap, use the profile command:
102
104
 
103
- ```yaml
104
- SELLABLE_CONFIG_PATH: /opt/data/profiles/<customerSlug>/sellable/config.json
105
- SELLABLE_CONFIGS_DIR: /opt/data/profiles/<customerSlug>/sellable/configs
106
- SELLABLE_LOCK_WORKSPACE_ID: <approvedWorkspaceId>
107
- SELLABLE_REQUIRE_WORKSPACE_LOCK: "1"
105
+ ```bash
106
+ sellable hermes profile bootstrap \
107
+ --profile acme \
108
+ --profiles-root /srv/hermes/profiles \
109
+ --workspace-id ws_acme \
110
+ --workspace-name Acme \
111
+ --token-file /run/secrets/sellable-acme-token \
112
+ --json
113
+ ```
114
+
115
+ It creates:
116
+
117
+ ```text
118
+ /srv/hermes/profiles/acme/config.yaml
119
+ /srv/hermes/profiles/acme/sellable/config.json
120
+ /srv/hermes/profiles/acme/sellable/configs/
121
+ /srv/hermes/profiles/acme/skills/sellable/
122
+ /srv/hermes/profiles/acme/.env # only when Slack token inputs are supplied
108
123
  ```
109
124
 
110
- The locked MCP requests only the approved workspace, blocks workspace switching
111
- and creation, and reports `siblingInventoryFetched=false`. The customer profile
112
- must contain no Admin MCP, admin token, customer-local OAuth, or sibling config.
125
+ Bootstrap also writes `SELLABLE_LOCK_WORKSPACE_ID=<workspace-id>` and
126
+ `SELLABLE_REQUIRE_WORKSPACE_LOCK=1` into the profile's Hermes MCP env.
127
+ Customer profiles are therefore locked to their own Sellable workspace:
128
+ Sellable MCP requests are forced to that workspace, workspace switching/creation
129
+ is blocked, workspace listings are filtered, and a missing lock fails closed
130
+ instead of falling back to shared/admin config. Use the `sellable-admin` profile
131
+ for provisioning or cross-profile inspection.
113
132
 
114
133
  See [`HERMES-SLACK-PROFILE-SCOPING.md`](./HERMES-SLACK-PROFILE-SCOPING.md) for
115
- the dedicated native Slack app, root OAuth fallback, immutable package, s6
116
- gateway, rebuild, and live UAT contract.
134
+ the customer Slack isolation contract and Socket Mode token requirements.
135
+
136
+ If no token is supplied, bootstrap still creates a pending
137
+ `sellable/config.json` so the profile path is concrete from the first MCP
138
+ launch. Finish auth with:
139
+
140
+ ```bash
141
+ sellable auth set <token> --workspace-id <workspace_id> \
142
+ --sellable-config-path /srv/hermes/profiles/acme/sellable/config.json
143
+ ```
144
+
145
+ Slack Socket Mode tokens can be written profile-locally:
146
+
147
+ ```bash
148
+ sellable hermes profile bootstrap \
149
+ --profile acme \
150
+ --profiles-root /srv/hermes/profiles \
151
+ --workspace-id ws_acme \
152
+ --slack-bot-token "$SLACK_BOT_TOKEN" \
153
+ --slack-app-token "$SLACK_APP_TOKEN"
154
+ ```
155
+
156
+ The Slack app must also be configured for inbound Socket Mode delivery. Outbound
157
+ Slack Web API sends can work before this is complete, but Hermes will not hear
158
+ mentions until the app has Socket Mode enabled, an app-level token with
159
+ `connections:write`, bot scopes/events for `app_mention` plus channel/group/IM
160
+ message events, and a fresh workspace reinstall after scope or event changes.
161
+
162
+ Scope the profile to its customer channel at install time:
163
+
164
+ ```bash
165
+ sellable hermes profile bootstrap \
166
+ --profile acme \
167
+ --profiles-root /srv/hermes/profiles \
168
+ --workspace-id ws_acme \
169
+ --slack-bot-token "$SLACK_BOT_TOKEN" \
170
+ --slack-app-token "$SLACK_APP_TOKEN" \
171
+ --slack-home-channel C0ACME12345 \
172
+ --slack-home-channel-name sellable-acme \
173
+ --slack-allowed-users U0OPERATOR1 \
174
+ --slack-require-mention true
175
+ ```
176
+
177
+ When `--slack-home-channel` is supplied without
178
+ `--slack-allowed-channels`, bootstrap writes
179
+ `SLACK_ALLOWED_CHANNELS=<home-channel-id>` too. That fail-closes a customer
180
+ profile to the intended channel by default. Use
181
+ `--slack-allowed-channels C...,G...` only when a profile is deliberately allowed
182
+ to listen in multiple channels. Pass channel IDs, not `#channel-name`, and use
183
+ separate Slack bot/app tokens per customer profile when you need hard isolation
184
+ between concurrently listening Hermes profiles.
185
+
186
+ Customer profiles should use customer-scoped Sellable credentials. A shared
187
+ Sellable admin token can work for internal admin automation, but it is weaker
188
+ isolation and should not be the default for customer profiles. After changing
189
+ Hermes profile config, restart the gateway/session or run `/reload-mcp` and
190
+ `/reload-skills`.
191
+
192
+ Validate the active profile with the same path inputs:
193
+
194
+ ```bash
195
+ SELLABLE_CONFIG_PATH=/srv/hermes/profiles/acme/sellable/config.json \
196
+ SELLABLE_CONFIGS_DIR=/srv/hermes/profiles/acme/sellable/configs \
197
+ sellable --verify-only --host hermes --json \
198
+ --artifact /srv/hermes/profiles/acme/sellable/verify-runtime.json
199
+ ```
117
200
 
118
201
  Claude Code, Codex, and Hermes are configured to launch the same packaged MCP server. The
119
202
  installer also writes Sellable agent definitions from the packaged `agents/`
@@ -22,6 +22,14 @@ import {
22
22
  REQUIRED_SELLABLE_MCP_TOOLS,
23
23
  verifySellableMcpRuntime,
24
24
  } from "../lib/runtime-verify.mjs";
25
+ import {
26
+ activateCodexCandidateSelection,
27
+ installCodexCandidateCache,
28
+ readCodexPluginSelection,
29
+ resolveCodexPluginSelectionPaths,
30
+ restoreCodexPluginSelection,
31
+ snapshotCodexPluginSelection,
32
+ } from "../lib/codex-plugin-selection.mjs";
25
33
 
26
34
  const DEFAULT_API_URL = "https://app.sellable.dev";
27
35
  const DEFAULT_SERVER_PACKAGE =
@@ -40,7 +48,7 @@ function getInstallVersion() {
40
48
  }
41
49
  }
42
50
 
43
- const CODEX_PLUGIN_VERSION = "0.1.54";
51
+ const CODEX_PLUGIN_VERSION = "0.1.56";
44
52
  const CODEX_PLUGIN_COMPAT_VERSIONS = [
45
53
  "0.1.8",
46
54
  "0.1.9",
@@ -79,6 +87,8 @@ const CODEX_PLUGIN_COMPAT_VERSIONS = [
79
87
  "0.1.51",
80
88
  "0.1.52",
81
89
  "0.1.53",
90
+ "0.1.54",
91
+ "0.1.55",
82
92
  ];
83
93
  const RAW_INSTALL_PACKAGE_SPEC =
84
94
  process.env.SELLABLE_INSTALL_PACKAGE_SPEC || "@sellable/install@latest";
@@ -170,6 +180,8 @@ Commands:
170
180
  Save the ask-first prompt-sharing preference.
171
181
  setup claude-permissions --allow-common-setup
172
182
  Add common Sellable Bash permissions to Claude settings.
183
+ codex plugin-selection snapshot|install-cache|activate|restore|status
184
+ Approval-gated Phase 98 exact-cache selection controls.
173
185
  hermes profile bootstrap Create a profile-local Hermes config, Sellable auth config,
174
186
  configs dir, skills, and optional Slack .env.
175
187
  uninstall Remove Sellable host config and installed artifacts.
@@ -213,19 +225,26 @@ Auth:
213
225
  also pass --sellable-config-path or set SELLABLE_CONFIG_PATH so auth and MCP
214
226
  runtime use the same profile-local config file.
215
227
 
216
- Hermes customer profile stage:
217
- Managed customer onboarding uses only:
218
- sellable-admin-install hermes customer bootstrap --mode <mode> [approved inputs]
219
-
220
- The Admin state machine invokes this package's profile bootstrap internally
221
- with exact token-file, workspace-lock, profile-root, and --mcp-package inputs.
222
- Operators must not use the low-level profile command to coordinate Slack,
223
- profile, or gateway onboarding manually. Customer profiles use root Codex
224
- OAuth fallback and contain no local auth.json, provider pool, or Admin MCP.
225
-
226
- Hermes customer Slack runtime:
227
- Each customer profile uses its own dedicated native Slack app and gateway.
228
- Sellable Admin owns the single resumable customer bootstrap interface.
228
+ Hermes profile bootstrap:
229
+ sellable hermes profile bootstrap --profile acme --profiles-root /srv/hermes/profiles --workspace-id ws_acme --token-file /run/secrets/sellable-token --json
230
+ sellable hermes profile bootstrap --profile-root "$HOME/.hermes/profiles/acme" --workspace-id ws_acme --json
231
+
232
+ Bootstrap writes <profileRoot>/config.yaml, <profileRoot>/sellable/config.json,
233
+ <profileRoot>/sellable/configs, Sellable Hermes skills, and optional
234
+ <profileRoot>/.env Slack token and channel-scope keys. For customer profiles,
235
+ pass --slack-home-channel; bootstrap defaults SLACK_ALLOWED_CHANNELS to that
236
+ exact channel id unless --slack-allowed-channels is supplied. Use
237
+ customer-scoped Sellable credentials and customer-scoped Slack credentials for
238
+ customer profiles; shared admin tokens are weaker isolation and should remain
239
+ internal-only. Bootstrap writes SELLABLE_REQUIRE_WORKSPACE_LOCK=1 whenever a
240
+ workspace id is supplied, so a customer profile fails closed if the workspace
241
+ lock is later removed or not yet provisioned.
242
+
243
+ Hermes shared Slack dispatcher:
244
+ Shared listener setup and route-registry validation are internal Sellable
245
+ Admin responsibilities. This customer-facing installer only bootstraps a
246
+ profile-local Sellable runtime after Sellable Admin provisions the profile
247
+ and dispatcher route.
229
248
  `;
230
249
  }
231
250
 
@@ -2031,6 +2050,20 @@ then retry \`get_subskill_prompt\`.
2031
2050
  `, host, "create-evergreen-campaigns");
2032
2051
  }
2033
2052
 
2053
+ const REFILL_SENDS_FALLBACK_CONTRACT = `<!-- REFILL_CONTRACT_GENERATED:START -->
2054
+ contractVersion: 2.0.0
2055
+ cacheVersion: refill-contract-v1
2056
+ contractHash: e54ba0f80bc534fd70fd8c10c1caab109fe9a91c205cfc423f5f6735cd53fdc1
2057
+ scope: workspace_id, target_date, sender_selectors, campaign_selectors, approval_mode, contract_identity, installed_identity
2058
+ gates: approval_packet, one_primitive, fresh_reread, normalized_accounting, prep_circuit
2059
+ forbidden: direct_send, raw_scheduler_write, campaign_create, campaign_archive_delete, threshold_lowering, unselected_source_mutation, unapproved_campaign_start, sender_config_write
2060
+ terminal: projected_full, concrete_blocker, deadline_reached, iteration_limit, operator_stopped
2061
+ ready_buffer_exists_is_not_complete
2062
+ mcpPackageVersion: installed MCP package spec in host MCP config
2063
+ installPackageVersion: ${getInstallVersion()}
2064
+ codexPluginVersion: ${CODEX_PLUGIN_VERSION}
2065
+ <!-- REFILL_CONTRACT_GENERATED:END -->`;
2066
+
2034
2067
  function refillSendsSkillMd(host = "shared") {
2035
2068
  try {
2036
2069
  const here = dirname(fileURLToPath(import.meta.url));
@@ -2040,7 +2073,10 @@ function refillSendsSkillMd(host = "shared") {
2040
2073
  "skill-templates",
2041
2074
  "refill-sends.md"
2042
2075
  );
2043
- if (existsSync(templatePath)) {
2076
+ if (
2077
+ process.env.SELLABLE_FORCE_REFILL_TEMPLATE_MISSING !== "1" &&
2078
+ existsSync(templatePath)
2079
+ ) {
2044
2080
  return stampInstalledHost(
2045
2081
  addOrReplaceAllowedTools(
2046
2082
  readFileSync(templatePath, "utf8"),
@@ -2063,6 +2099,14 @@ ${allowedToolsYaml(REFILL_SENDS_ALLOWED_TOOLS)}
2063
2099
 
2064
2100
  # Sellable Refill Sends
2065
2101
 
2102
+ ${REFILL_SENDS_FALLBACK_CONTRACT}
2103
+
2104
+ fallbackSource: embedded
2105
+
2106
+ Load \`refill-sends-workflow\`, then \`core/flow.v1.json\`, then
2107
+ \`core/contract.v2.json\`, each through the Sellable MCP prompt tools until
2108
+ \`hasMore:false\`. Fail closed on unsupported major or stale contract hash.
2109
+
2066
2110
  Use this as the customer-facing entrypoint for the Sellable \`refill-sends\`
2067
2111
  workflow. It supports \`--yolo\` and optional repeated \`--sender <name-or-id>\`
2068
2112
  selectors. It also supports \`--target-date YYYY-MM-DD\` to fill one exact
@@ -2092,9 +2136,9 @@ Desktop, then start a new thread.
2092
2136
 
2093
2137
  1. Call \`mcp__sellable__get_auth_status({})\`.
2094
2138
  2. Resolve the target workspace id from the user request, automation config, or install-time workspace mapping. Pass \`workspaceId\` on every scheduled or \`--yolo\` refill tool call. Missing \`workspaceId\` in scheduled or \`--yolo\` mode is a blocker; return \`WORKSPACE_REQUIRED\` instead of running against shared config state. Do not solve automation workspace uncertainty by changing the shared active workspace.
2095
- 3. Normalize invocation arguments with \`mcp__sellable__refill_sends({ yolo, executionMode, requireWorkspace, workspaceId, senders, senderIds, senderNames, targetDate, untilDate })\` when the host exposes typed MCP tools. Type shape: \`refill_sends({ yolo?: boolean, executionMode?: "manual" | "scheduled" | "yolo", requireWorkspace?: boolean, workspaceId?: string, senders?: string[], senderIds?: string[], senderNames?: string[], horizonSendDays?: number, untilDate?: "YYYY-MM-DD", targetDate?: "YYYY-MM-DD" })\`.
2139
+ 3. Normalize invocation arguments with \`mcp__sellable__refill_sends({ yolo, executionMode, requireWorkspace, workspaceId, senders, senderIds, senderNames, actionTypes, targetDate, untilDate })\` when the host exposes typed MCP tools. Type shape: \`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" })\`.
2096
2140
  4. Clover scheduled automation example: \`mcp__sellable__refill_sends({ yolo:false, executionMode:"scheduled", requireWorkspace:true, workspaceId:"cmlq1v8ms0000jx04ang4hi7e" })\`, then \`mcp__sellable__get_refill_target_plan({ intent:"plain", approvalMode:"mark_ready", workspaceId:"cmlq1v8ms0000jx04ang4hi7e" })\`.
2097
- 5. Call \`mcp__sellable__get_refill_target_plan({ workspaceId, senders, senderIds, senderNames, targetDate, untilDate })\` before any mutation. Render \`target.eligibleSenderLedger\`, \`target.senderRefillPlans[]\`, and \`target.globalActionQueue\` as the structured packet. Preserve the labels \`Need to prepare\`, \`Goal\`, \`Already sent\`, \`Scheduled\`, \`Ready and waiting to be scheduled\`, and \`Still need\`. In \`--yolo\`, \`mcp__sellable__refill_sends\` maintains a run-local \`refreshedPaidInmailSenderIds\` set, refreshes stale/missing paid InMail facts for each selected paid-InMail sender at most once, reruns \`get_refill_target_plan\`, and returns \`autoPaidInmailRefresh\` plus the post-refresh \`targetPlan\` before choosing the next prep/source-copy/bounded-approval/read-only wait action. Refill ladder: approve generated rows only through the explicit bounded approval gate, process existing same-campaign unenriched/unprepared rows before source work, then same-source copy, then provider-aligned source-more. New source/provider switches change reply-rate baseline and are manual alternates, not \`--yolo\` side effects. Do not present paid-credit refresh as the next operator action after \`refill_sends\` returns that post-refresh plan. For exact-date proof, \`mcp__sellable__get_scheduler_fill_capacity\` is read-only and tells how many cells the scheduler will try to place for a sender/action/date. When ready rows cover the target but scheduled rows do not, \`mcp__sellable__run_scheduler_sweep({ workspaceId, action:"run" })\` may request immediate placement within existing gates and returns a receipt, but it never sends or bypasses limits. If the returned target is already complete, stop with the no-op receipt. If ready rows cover the target but scheduled rows do not, keep the run open in the read-only scheduler wait/re-read loop until projected coverage fills, a concrete non-scheduler blocker appears, or Christian explicitly stops/statuses the run.
2141
+ 5. Call \`mcp__sellable__get_refill_target_plan({ workspaceId, senders, senderIds, senderNames, actionTypes, targetDate, untilDate })\` before any mutation. Render \`target.eligibleSenderLedger\`, \`target.senderRefillPlans[]\`, and \`target.globalActionQueue\` as the structured packet. Preserve the labels \`Need to prepare\`, \`Goal\`, \`Already sent\`, \`Scheduled\`, \`Ready and waiting to be scheduled\`, and \`Still need\`. In \`--yolo\`, \`mcp__sellable__refill_sends\` maintains a run-local \`refreshedPaidInmailSenderIds\` set, refreshes stale/missing paid InMail facts for each selected paid-InMail sender at most once, reruns \`get_refill_target_plan\`, and returns \`autoPaidInmailRefresh\` plus the post-refresh \`targetPlan\` before choosing the next prep/source-copy/bounded-approval/read-only wait or exact-date sweep action. Refill ladder: approve generated rows only through the explicit bounded approval gate, process existing same-campaign unenriched/unprepared rows before source work, then same-source copy, then provider-aligned source-more. New source/provider switches change reply-rate baseline and are manual alternates, not \`--yolo\` side effects. Do not present paid-credit refresh as the next operator action after \`refill_sends\` returns that post-refresh plan. For exact-date proof, \`mcp__sellable__get_scheduler_fill_capacity\` is read-only and tells how many cells the scheduler will try to place for a sender/action/date. When the planner returns a request-scoped exact-date scheduler sweep, execute \`mcp__sellable__run_scheduler_sweep({ workspaceId, action:"run", targetDate, requestKey, targetShapeRevision })\` once, capture its receipt, and fully reread before another action. It may schedule eligible workspace cells through existing gates, but it never sends directly or bypasses limits. If the returned target is already complete, stop with the no-op receipt. Only non-exact or no-sweep waits use the read-only scheduler wait/re-read loop until projected coverage fills, a concrete non-scheduler blocker appears, or Christian explicitly stops/statuses the run.
2098
2142
  6. Load the canonical prompt and deterministic flow asset via
2099
2143
  \`mcp__sellable__get_subskill_prompt({ subskillName: "refill-sends" })\`,
2100
2144
  then load \`mcp__sellable__get_subskill_prompt({ subskillName: "refill-sends-workflow" })\`,
@@ -2105,8 +2149,7 @@ Desktop, then start a new thread.
2105
2149
  facts are present, refresh exact selected sender facts once, rerun
2106
2150
  \`get_refill_target_plan\`, and only then enter \`wait_for_scheduler\` if
2107
2151
  freshness is clean.
2108
- 7. Follow the canonical prompt exactly. Do not launch, start, archive, delete,
2109
- or write scheduler rows as part of refill sends.
2152
+ 7. Follow the canonical prompt exactly. Do not directly send, bypass scheduler gates, or raw-write scheduler fields. Only the exact rendered paused-campaign start and request-scoped scheduler sweep may use their existing product-gated paths; do not launch, archive, or delete unrelated work.
2110
2153
 
2111
2154
  ## MCP Prompt Fallback
2112
2155
 
@@ -5807,6 +5850,73 @@ async function main() {
5807
5850
  printCreateCommandHint();
5808
5851
  process.exit(0);
5809
5852
  }
5853
+ if (rawArgs[0] === "codex" && rawArgs[1] === "plugin-selection") {
5854
+ const command = rawArgs[2];
5855
+ const flags = new Map();
5856
+ for (let index = 3; index < rawArgs.length; index += 2) {
5857
+ const flag = rawArgs[index];
5858
+ const value = rawArgs[index + 1];
5859
+ if (!flag?.startsWith("--") || !value || value.startsWith("--")) {
5860
+ throw new Error(`Invalid Codex plugin-selection option: ${flag ?? "<missing>"}`);
5861
+ }
5862
+ flags.set(flag, value);
5863
+ }
5864
+ const required = (name) => {
5865
+ const value = flags.get(name);
5866
+ if (!value) throw new Error(`Missing value for ${name}`);
5867
+ return value;
5868
+ };
5869
+ const paths = resolveCodexPluginSelectionPaths({ home: homedir() });
5870
+ let result;
5871
+ if (command === "status") {
5872
+ result = readCodexPluginSelection({ paths, stableVersion: flags.get("--stable-version") });
5873
+ } else if (command === "snapshot") {
5874
+ result = snapshotCodexPluginSelection({
5875
+ paths,
5876
+ snapshotRoot: required("--snapshot-root"),
5877
+ });
5878
+ } else if (command === "install-cache") {
5879
+ result = installCodexCandidateCache({
5880
+ paths,
5881
+ sourcePluginRoot: required("--source"),
5882
+ version: required("--version"),
5883
+ });
5884
+ } else if (command === "activate") {
5885
+ result = activateCodexCandidateSelection({
5886
+ paths,
5887
+ snapshotManifestPath: required("--snapshot"),
5888
+ expectedSnapshotHash: required("--snapshot-hash"),
5889
+ candidateVersion: required("--candidate-version"),
5890
+ expectedCandidateHash: required("--candidate-hash"),
5891
+ approvalId: required("--approval-id"),
5892
+ approvedAt: required("--approved-at"),
5893
+ restoreBy: required("--restore-by"),
5894
+ now: required("--now"),
5895
+ });
5896
+ } else if (command === "restore") {
5897
+ result = restoreCodexPluginSelection({
5898
+ paths,
5899
+ snapshotManifestPath: required("--snapshot"),
5900
+ expectedSnapshotHash: required("--snapshot-hash"),
5901
+ activationPath: required("--activation"),
5902
+ now: required("--now"),
5903
+ recoveryReason: flags.get("--recovery-reason") || undefined,
5904
+ taskEvidence:
5905
+ flags.has("--task-id") && flags.has("--resolved-candidate-hash")
5906
+ ? {
5907
+ taskId: flags.get("--task-id"),
5908
+ resolvedCandidateHash: flags.get("--resolved-candidate-hash"),
5909
+ }
5910
+ : undefined,
5911
+ });
5912
+ } else {
5913
+ throw new Error(
5914
+ "Usage: sellable codex plugin-selection snapshot|install-cache|activate|restore|status"
5915
+ );
5916
+ }
5917
+ console.log(JSON.stringify(result, null, 2));
5918
+ process.exit(0);
5919
+ }
5810
5920
  if (
5811
5921
  rawArgs[0] === "hermes" &&
5812
5922
  rawArgs[1] === "profile" &&