@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.
- 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 +353 -0
- package/package.json +1 -1
- package/skill-templates/refill-sends-v2.md +6 -6
- package/skill-templates/refill-sends.md +91 -353
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
chmodSync,
|
|
4
|
+
closeSync,
|
|
5
|
+
cpSync,
|
|
6
|
+
existsSync,
|
|
7
|
+
lstatSync,
|
|
8
|
+
mkdirSync,
|
|
9
|
+
openSync,
|
|
10
|
+
readFileSync,
|
|
11
|
+
readlinkSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
renameSync,
|
|
14
|
+
rmSync,
|
|
15
|
+
symlinkSync,
|
|
16
|
+
writeFileSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
19
|
+
|
|
20
|
+
export const CODEX_PLUGIN_SELECTION_SCHEMA_VERSION = 1;
|
|
21
|
+
export const MAX_CODEX_CANARY_WINDOW_MS = 90 * 60 * 1000;
|
|
22
|
+
|
|
23
|
+
export function resolveCodexPluginSelectionPaths({
|
|
24
|
+
home = "",
|
|
25
|
+
marketplaceRoot = join(home, ".sellable", "codex-marketplace"),
|
|
26
|
+
cacheRoot = join(home, ".codex", "plugins", "cache", "sellable", "sellable"),
|
|
27
|
+
} = {}) {
|
|
28
|
+
if (!home) throw new Error("codex_selection_home_required");
|
|
29
|
+
return {
|
|
30
|
+
home: resolve(home),
|
|
31
|
+
configPath: join(home, ".codex", "config.toml"),
|
|
32
|
+
marketplaceRoot: resolve(marketplaceRoot),
|
|
33
|
+
marketplacePath: join(marketplaceRoot, ".agents", "plugins", "marketplace.json"),
|
|
34
|
+
pluginRoot: join(marketplaceRoot, "plugins", "sellable"),
|
|
35
|
+
cacheRoot: resolve(cacheRoot),
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function canonicalize(value) {
|
|
40
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
41
|
+
if (value && typeof value === "object") {
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.keys(value)
|
|
44
|
+
.sort()
|
|
45
|
+
.map((key) => [key, canonicalize(value[key])])
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function digest(value) {
|
|
52
|
+
return createHash("sha256").update(value).digest("hex");
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function canonicalDigest(value) {
|
|
56
|
+
return digest(JSON.stringify(canonicalize(value)));
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function treeRecords(root) {
|
|
60
|
+
if (!existsSync(root)) return [{ path: ".", type: "missing" }];
|
|
61
|
+
const records = [];
|
|
62
|
+
const visit = (path) => {
|
|
63
|
+
const stat = lstatSync(path);
|
|
64
|
+
const rel = relative(root, path) || ".";
|
|
65
|
+
if (stat.isSymbolicLink()) {
|
|
66
|
+
records.push({ path: rel, type: "symlink", target: readlinkSync(path) });
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
if (stat.isDirectory()) {
|
|
70
|
+
records.push({ path: rel, type: "directory", mode: stat.mode & 0o777 });
|
|
71
|
+
for (const name of readdirSync(path).sort()) visit(join(path, name));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
records.push({
|
|
75
|
+
path: rel,
|
|
76
|
+
type: "file",
|
|
77
|
+
mode: stat.mode & 0o777,
|
|
78
|
+
sha256: digest(readFileSync(path)),
|
|
79
|
+
});
|
|
80
|
+
};
|
|
81
|
+
visit(root);
|
|
82
|
+
return records;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function treeHash(root) {
|
|
86
|
+
return canonicalDigest(treeRecords(root));
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function pluginVersion(pluginRoot) {
|
|
90
|
+
const manifestPath = join(pluginRoot, ".codex-plugin", "plugin.json");
|
|
91
|
+
if (!existsSync(manifestPath)) throw new Error("codex_selection_plugin_manifest_missing");
|
|
92
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
93
|
+
if (manifest.name !== "sellable" || typeof manifest.version !== "string") {
|
|
94
|
+
throw new Error("codex_selection_plugin_manifest_invalid");
|
|
95
|
+
}
|
|
96
|
+
return manifest.version;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function ensureInside(parent, child, code) {
|
|
100
|
+
const root = resolve(parent);
|
|
101
|
+
const target = resolve(child);
|
|
102
|
+
if (!(target === root || target.startsWith(`${root}/`))) throw new Error(code);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function copyExact(source, target) {
|
|
106
|
+
rmSync(target, { recursive: true, force: true });
|
|
107
|
+
mkdirSync(dirname(target), { recursive: true });
|
|
108
|
+
cpSync(source, target, {
|
|
109
|
+
recursive: true,
|
|
110
|
+
dereference: false,
|
|
111
|
+
preserveTimestamps: true,
|
|
112
|
+
});
|
|
113
|
+
syncExactModes(source, target);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function syncExactModes(source, target) {
|
|
117
|
+
const stat = lstatSync(source);
|
|
118
|
+
if (stat.isSymbolicLink()) return;
|
|
119
|
+
chmodSync(target, stat.mode & 0o777);
|
|
120
|
+
if (!stat.isDirectory()) return;
|
|
121
|
+
for (const name of readdirSync(source)) {
|
|
122
|
+
syncExactModes(join(source, name), join(target, name));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function readCanonicalManifest(manifestPath, expectedSnapshotHash) {
|
|
127
|
+
const raw = readFileSync(manifestPath, "utf8");
|
|
128
|
+
const manifest = JSON.parse(raw);
|
|
129
|
+
if (raw !== `${JSON.stringify(manifest, null, 2)}\n`) {
|
|
130
|
+
throw new Error("codex_selection_snapshot_manifest_not_canonical");
|
|
131
|
+
}
|
|
132
|
+
const { snapshotHash, ...unsigned } = manifest;
|
|
133
|
+
if (snapshotHash !== canonicalDigest(unsigned) || snapshotHash !== expectedSnapshotHash) {
|
|
134
|
+
throw new Error("codex_selection_snapshot_hash_mismatch");
|
|
135
|
+
}
|
|
136
|
+
for (const item of manifest.items ?? []) {
|
|
137
|
+
if (treeHash(item.backupPath) !== item.hash) {
|
|
138
|
+
throw new Error(`codex_selection_snapshot_backup_mismatch:${item.name}`);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return manifest;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function stateItems(paths, stableVersion) {
|
|
145
|
+
return [
|
|
146
|
+
{ name: "config", sourcePath: paths.configPath },
|
|
147
|
+
{ name: "marketplace", sourcePath: paths.marketplacePath },
|
|
148
|
+
{ name: "pluginRoot", sourcePath: paths.pluginRoot },
|
|
149
|
+
{ name: "stableCache", sourcePath: join(paths.cacheRoot, stableVersion) },
|
|
150
|
+
];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function stateHash(paths, stableVersion) {
|
|
154
|
+
return canonicalDigest(
|
|
155
|
+
stateItems(paths, stableVersion).map((item) => ({
|
|
156
|
+
name: item.name,
|
|
157
|
+
sourcePath: item.sourcePath,
|
|
158
|
+
hash: treeHash(item.sourcePath),
|
|
159
|
+
}))
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function snapshotCodexPluginSelection({ paths, snapshotRoot }) {
|
|
164
|
+
if (!paths || !snapshotRoot) throw new Error("codex_selection_snapshot_arguments_required");
|
|
165
|
+
if (existsSync(snapshotRoot)) throw new Error("codex_selection_snapshot_root_exists");
|
|
166
|
+
const version = pluginVersion(paths.pluginRoot);
|
|
167
|
+
const items = stateItems(paths, version).map((item) => {
|
|
168
|
+
if (!existsSync(item.sourcePath)) {
|
|
169
|
+
throw new Error(`codex_selection_snapshot_source_missing:${item.name}`);
|
|
170
|
+
}
|
|
171
|
+
const backupPath = join(snapshotRoot, "data", item.name);
|
|
172
|
+
copyExact(item.sourcePath, backupPath);
|
|
173
|
+
return { ...item, backupPath, hash: treeHash(item.sourcePath) };
|
|
174
|
+
});
|
|
175
|
+
const unsigned = {
|
|
176
|
+
schemaVersion: CODEX_PLUGIN_SELECTION_SCHEMA_VERSION,
|
|
177
|
+
createdAt: new Date().toISOString(),
|
|
178
|
+
pluginVersion: version,
|
|
179
|
+
stateHash: stateHash(paths, version),
|
|
180
|
+
paths,
|
|
181
|
+
items,
|
|
182
|
+
};
|
|
183
|
+
const manifest = { ...unsigned, snapshotHash: canonicalDigest(unsigned) };
|
|
184
|
+
const manifestPath = join(snapshotRoot, "snapshot.json");
|
|
185
|
+
mkdirSync(dirname(manifestPath), { recursive: true });
|
|
186
|
+
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`, { mode: 0o600 });
|
|
187
|
+
return { manifestPath, snapshotHash: manifest.snapshotHash, pluginVersion: version };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function installCodexCandidateCache({ paths, sourcePluginRoot, version }) {
|
|
191
|
+
if (!paths || !sourcePluginRoot || !version) throw new Error("codex_candidate_cache_arguments_required");
|
|
192
|
+
if (pluginVersion(sourcePluginRoot) !== version) throw new Error("codex_candidate_version_mismatch");
|
|
193
|
+
const target = join(paths.cacheRoot, version);
|
|
194
|
+
ensureInside(paths.cacheRoot, target, "codex_candidate_cache_path_escape");
|
|
195
|
+
const sourceHash = treeHash(sourcePluginRoot);
|
|
196
|
+
if (existsSync(target)) {
|
|
197
|
+
if (treeHash(target) !== sourceHash) throw new Error("codex_candidate_cache_collision");
|
|
198
|
+
} else {
|
|
199
|
+
mkdirSync(paths.cacheRoot, { recursive: true });
|
|
200
|
+
const staging = join(paths.cacheRoot, `.phase98-${version}-${process.pid}`);
|
|
201
|
+
copyExact(sourcePluginRoot, staging);
|
|
202
|
+
if (treeHash(staging) !== sourceHash) throw new Error("codex_candidate_cache_copy_mismatch");
|
|
203
|
+
renameSync(staging, target);
|
|
204
|
+
}
|
|
205
|
+
return { version, cachePath: target, cacheHash: sourceHash };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function withSelectionLock(paths, fn) {
|
|
209
|
+
mkdirSync(paths.cacheRoot, { recursive: true });
|
|
210
|
+
const lockPath = join(paths.cacheRoot, ".phase98-selection.lock");
|
|
211
|
+
let handle;
|
|
212
|
+
try {
|
|
213
|
+
handle = openSync(lockPath, "wx", 0o600);
|
|
214
|
+
} catch {
|
|
215
|
+
throw new Error("codex_selection_concurrent_operation");
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
return fn();
|
|
219
|
+
} finally {
|
|
220
|
+
closeSync(handle);
|
|
221
|
+
rmSync(lockPath, { force: true });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function atomicReplaceTree(source, target) {
|
|
226
|
+
const parent = dirname(target);
|
|
227
|
+
mkdirSync(parent, { recursive: true });
|
|
228
|
+
const staging = join(parent, `.${basename(target)}.phase98-next-${process.pid}`);
|
|
229
|
+
const previous = join(parent, `.${basename(target)}.phase98-prev-${process.pid}`);
|
|
230
|
+
copyExact(source, staging);
|
|
231
|
+
if (existsSync(target)) renameSync(target, previous);
|
|
232
|
+
try {
|
|
233
|
+
renameSync(staging, target);
|
|
234
|
+
rmSync(previous, { recursive: true, force: true });
|
|
235
|
+
} catch (error) {
|
|
236
|
+
rmSync(target, { recursive: true, force: true });
|
|
237
|
+
if (existsSync(previous)) renameSync(previous, target);
|
|
238
|
+
rmSync(staging, { recursive: true, force: true });
|
|
239
|
+
throw error;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export function activateCodexCandidateSelection(input) {
|
|
244
|
+
const {
|
|
245
|
+
paths,
|
|
246
|
+
snapshotManifestPath,
|
|
247
|
+
expectedSnapshotHash,
|
|
248
|
+
candidateVersion,
|
|
249
|
+
expectedCandidateHash,
|
|
250
|
+
approvalId,
|
|
251
|
+
approvedAt,
|
|
252
|
+
restoreBy,
|
|
253
|
+
now,
|
|
254
|
+
} = input;
|
|
255
|
+
if (!approvalId) throw new Error("codex_selection_approval_required");
|
|
256
|
+
const approvedMs = Date.parse(approvedAt);
|
|
257
|
+
const restoreMs = Date.parse(restoreBy);
|
|
258
|
+
const nowMs = Date.parse(now);
|
|
259
|
+
if (![approvedMs, restoreMs, nowMs].every(Number.isFinite)) {
|
|
260
|
+
throw new Error("codex_selection_window_invalid");
|
|
261
|
+
}
|
|
262
|
+
if (nowMs < approvedMs || nowMs >= restoreMs || restoreMs - approvedMs > MAX_CODEX_CANARY_WINDOW_MS) {
|
|
263
|
+
throw new Error("codex_selection_window_exceeds_limit");
|
|
264
|
+
}
|
|
265
|
+
return withSelectionLock(paths, () => {
|
|
266
|
+
const snapshot = readCanonicalManifest(snapshotManifestPath, expectedSnapshotHash);
|
|
267
|
+
if (stateHash(paths, snapshot.pluginVersion) !== snapshot.stateHash) {
|
|
268
|
+
throw new Error("codex_selection_current_state_drift");
|
|
269
|
+
}
|
|
270
|
+
const candidatePath = join(paths.cacheRoot, candidateVersion);
|
|
271
|
+
if (!existsSync(candidatePath) || treeHash(candidatePath) !== expectedCandidateHash) {
|
|
272
|
+
throw new Error("codex_selection_candidate_cache_mismatch");
|
|
273
|
+
}
|
|
274
|
+
atomicReplaceTree(candidatePath, paths.pluginRoot);
|
|
275
|
+
if (pluginVersion(paths.pluginRoot) !== candidateVersion || treeHash(paths.pluginRoot) !== expectedCandidateHash) {
|
|
276
|
+
throw new Error("codex_selection_candidate_activation_mismatch");
|
|
277
|
+
}
|
|
278
|
+
const activation = {
|
|
279
|
+
schemaVersion: CODEX_PLUGIN_SELECTION_SCHEMA_VERSION,
|
|
280
|
+
approvalId,
|
|
281
|
+
approvedAt,
|
|
282
|
+
activatedAt: now,
|
|
283
|
+
restoreBy,
|
|
284
|
+
snapshotHash: expectedSnapshotHash,
|
|
285
|
+
stableVersion: snapshot.pluginVersion,
|
|
286
|
+
candidateVersion,
|
|
287
|
+
candidateHash: expectedCandidateHash,
|
|
288
|
+
changedPaths: [paths.pluginRoot],
|
|
289
|
+
};
|
|
290
|
+
const activationPath = join(dirname(snapshotManifestPath), "activation.json");
|
|
291
|
+
writeFileSync(activationPath, `${JSON.stringify(activation, null, 2)}\n`, { mode: 0o600 });
|
|
292
|
+
return { ...activation, activationPath };
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function restoreCodexPluginSelection(input) {
|
|
297
|
+
const {
|
|
298
|
+
paths,
|
|
299
|
+
snapshotManifestPath,
|
|
300
|
+
expectedSnapshotHash,
|
|
301
|
+
activationPath,
|
|
302
|
+
now,
|
|
303
|
+
taskEvidence,
|
|
304
|
+
recoveryReason,
|
|
305
|
+
} = input;
|
|
306
|
+
return withSelectionLock(paths, () => {
|
|
307
|
+
const snapshot = readCanonicalManifest(snapshotManifestPath, expectedSnapshotHash);
|
|
308
|
+
const activation = JSON.parse(readFileSync(activationPath, "utf8"));
|
|
309
|
+
if (
|
|
310
|
+
activation.snapshotHash !== expectedSnapshotHash ||
|
|
311
|
+
activation.candidateHash !== treeHash(paths.pluginRoot)
|
|
312
|
+
) throw new Error("codex_selection_active_candidate_drift");
|
|
313
|
+
if (!recoveryReason) {
|
|
314
|
+
if (!taskEvidence?.taskId || taskEvidence.resolvedCandidateHash !== activation.candidateHash) {
|
|
315
|
+
throw new Error("codex_selection_task_identity_mismatch");
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
for (const item of snapshot.items) copyExact(item.backupPath, item.sourcePath);
|
|
319
|
+
if (stateHash(paths, snapshot.pluginVersion) !== snapshot.stateHash) {
|
|
320
|
+
throw new Error("codex_selection_restore_hash_mismatch");
|
|
321
|
+
}
|
|
322
|
+
const deadlineExceeded = Date.parse(now) > Date.parse(activation.restoreBy);
|
|
323
|
+
const restoration = {
|
|
324
|
+
restored: true,
|
|
325
|
+
restoredAt: now,
|
|
326
|
+
deadlineExceeded,
|
|
327
|
+
recoveryReason: recoveryReason || null,
|
|
328
|
+
snapshotHash: expectedSnapshotHash,
|
|
329
|
+
stableVersion: snapshot.pluginVersion,
|
|
330
|
+
candidateVersion: activation.candidateVersion,
|
|
331
|
+
};
|
|
332
|
+
writeFileSync(
|
|
333
|
+
join(dirname(snapshotManifestPath), "restoration.json"),
|
|
334
|
+
`${JSON.stringify(restoration, null, 2)}\n`,
|
|
335
|
+
{ mode: 0o600 }
|
|
336
|
+
);
|
|
337
|
+
return restoration;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
export function readCodexPluginSelection({ paths, stableVersion } = {}) {
|
|
342
|
+
if (!paths) throw new Error("codex_selection_paths_required");
|
|
343
|
+
const version = pluginVersion(paths.pluginRoot);
|
|
344
|
+
const stable = stableVersion || version;
|
|
345
|
+
return {
|
|
346
|
+
pluginVersion: version,
|
|
347
|
+
pluginHash: treeHash(paths.pluginRoot),
|
|
348
|
+
configHash: treeHash(paths.configPath),
|
|
349
|
+
marketplaceHash: treeHash(paths.marketplacePath),
|
|
350
|
+
stableCacheHash: treeHash(join(paths.cacheRoot, stable)),
|
|
351
|
+
selectionHash: stateHash(paths, stable),
|
|
352
|
+
};
|
|
353
|
+
}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: refill-sends-v2
|
|
3
3
|
description: Execute refill sends v2 through the fenced evergreen refill loop, with dry-run and resume support.
|
|
4
|
-
visibility:
|
|
4
|
+
visibility: internal
|
|
5
5
|
allowed-tools:
|
|
6
|
-
-
|
|
6
|
+
- mcp__sellable__refill_sends
|
|
7
7
|
- mcp__sellable__get_refill_plan_v2
|
|
8
8
|
- mcp__sellable__get_subskill_prompt
|
|
9
9
|
- mcp__sellable__get_subskill_asset
|
|
@@ -24,7 +24,7 @@ Host command names:
|
|
|
24
24
|
- Claude Code: `/sellable:refill-sends-v2`
|
|
25
25
|
- Codex: `$sellable:refill-sends-v2`
|
|
26
26
|
|
|
27
|
-
`
|
|
27
|
+
`refill_sends` is the execution surface. In real-run mode it starts or
|
|
28
28
|
resumes a fenced refill run, reads a fresh packet, executes only the packet's
|
|
29
29
|
named bounded work, verifies the result, and returns either a terminal report, a
|
|
30
30
|
blocked report, or an in-progress resume handle. In dry-run mode it stays
|
|
@@ -40,19 +40,19 @@ with `WORKSPACE_REQUIRED`; do not switch the shared active workspace.
|
|
|
40
40
|
For a real refill run:
|
|
41
41
|
|
|
42
42
|
```text
|
|
43
|
-
|
|
43
|
+
refill_sends({ workspaceId, intent:"auto", senderIds?, approvalMode? })
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
For read-only inspection:
|
|
47
47
|
|
|
48
48
|
```text
|
|
49
|
-
|
|
49
|
+
refill_sends({ workspaceId, dryRun:true, intent:"auto", senderIds? })
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
To resume an in-progress run, pass the handle back exactly:
|
|
53
53
|
|
|
54
54
|
```text
|
|
55
|
-
|
|
55
|
+
refill_sends({ workspaceId, runId, fence })
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
If a stale handle loses the lease, the tool reports the holder status and the
|