@sellable/install 0.1.321 → 0.1.322
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/HERMES-SLACK-PROFILE-SCOPING.md +146 -44
- package/README.md +103 -20
- package/bin/sellable-install.mjs +129 -19
- package/lib/codex-plugin-selection.mjs +341 -0
- package/package.json +1 -1
- package/skill-templates/refill-sends-v2.md +6 -6
- package/skill-templates/refill-sends.md +91 -353
|
@@ -1,67 +1,169 @@
|
|
|
1
1
|
# Hermes Slack Profile Scoping
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
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
|
-
|
|
15
|
-
|
|
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
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
the
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
##
|
|
98
|
+
## Resulting Profile Env
|
|
32
99
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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: /
|
|
39
|
-
SELLABLE_CONFIGS_DIR: /
|
|
40
|
-
SELLABLE_LOCK_WORKSPACE_ID:
|
|
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
|
-
|
|
45
|
-
|
|
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
|
-
##
|
|
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
|
-
|
|
154
|
+
Expected proof:
|
|
50
155
|
|
|
51
|
-
-
|
|
52
|
-
-
|
|
53
|
-
-
|
|
54
|
-
-
|
|
55
|
-
-
|
|
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
|
-
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
the
|
|
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
|
|
86
|
+
## Hermes Profile-Scoped Installs
|
|
87
87
|
|
|
88
|
-
|
|
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
|
-
|
|
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
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
101
|
-
workspace boundary:
|
|
103
|
+
For zero-shot local or Hostinger/Linux VPS bootstrap, use the profile command:
|
|
102
104
|
|
|
103
|
-
```
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
116
|
-
|
|
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/`
|
package/bin/sellable-install.mjs
CHANGED
|
@@ -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.
|
|
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
|
|
217
|
-
|
|
218
|
-
sellable
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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" &&
|