@hasna/skills 0.9.12 → 0.9.13
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/bin/index.js +71 -18
- package/bin/maintenance.js +73 -0
- package/bin/mcp.js +63 -18
- package/bin/migrate.js +36 -1
- package/bin/server.js +96 -25
- package/bin/worker.js +74 -8
- package/dist/index.d.ts +2 -2
- package/dist/index.js +64 -17
- package/dist/lib/agent-discovery.d.ts +1 -1
- package/dist/lib/claude-marketplace-registry.d.ts +6 -0
- package/dist/lib/claude-settings-witness.d.ts +7 -0
- package/dist/sdk/index.js +138 -22
- package/dist/server/rows.d.ts +2 -1
- package/dist/server/sqlite-store.d.ts +2 -1
- package/dist/server/store.d.ts +3 -1
- package/dist/server/types.d.ts +18 -0
- package/docs/plugin-admission.md +36 -0
- package/package.json +1 -1
package/dist/sdk/index.js
CHANGED
|
@@ -32373,7 +32373,7 @@ var init_event_sink = __esm(() => {
|
|
|
32373
32373
|
// package.json
|
|
32374
32374
|
var package_default = {
|
|
32375
32375
|
name: "@hasna/skills",
|
|
32376
|
-
version: "0.9.
|
|
32376
|
+
version: "0.9.13",
|
|
32377
32377
|
description: "Skills library for AI coding agents",
|
|
32378
32378
|
type: "module",
|
|
32379
32379
|
bin: {
|
|
@@ -37545,10 +37545,10 @@ function assertAgentPolicyCollections(policy) {
|
|
|
37545
37545
|
text(source.path);
|
|
37546
37546
|
requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
|
|
37547
37547
|
if (source.hashMode !== undefined)
|
|
37548
|
-
requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
|
|
37549
|
-
if (source.hashMode === "claude-settings-v1")
|
|
37548
|
+
requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
|
|
37549
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
|
|
37550
37550
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "settings.json");
|
|
37551
|
-
if (source.hashMode === "claude-marketplace-registry")
|
|
37551
|
+
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
|
|
37552
37552
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute3(source.path) && resolve3(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
|
|
37553
37553
|
if (source.hashMode === "claude-plugin-registry") {
|
|
37554
37554
|
requireBound(agent === "claude");
|
|
@@ -39895,9 +39895,13 @@ function textField(value) {
|
|
|
39895
39895
|
need2(value?.kind === "string" && value.value.length > 0 && !/[\x00-\x1f\x7f]/.test(value.value), "contains an invalid recognized field");
|
|
39896
39896
|
return value.value;
|
|
39897
39897
|
}
|
|
39898
|
-
function normalizeRow(row) {
|
|
39899
|
-
if (!exactKeys2(row, ["source", "installLocation", "lastUpdated"]))
|
|
39900
|
-
|
|
39898
|
+
function normalizeRow(row, version = 1) {
|
|
39899
|
+
if (!exactKeys2(row, ["source", "installLocation", "lastUpdated"])) {
|
|
39900
|
+
if (version !== 2 || !exactKeys2(row, ["source", "installLocation", "lastUpdated", "autoUpdate"]))
|
|
39901
|
+
return;
|
|
39902
|
+
const autoUpdate = field(row, "autoUpdate");
|
|
39903
|
+
need2(autoUpdate?.kind === "literal" && (autoUpdate.value === "true" || autoUpdate.value === "false"), "contains an invalid automatic-update preference");
|
|
39904
|
+
}
|
|
39901
39905
|
const source = field(row, "source");
|
|
39902
39906
|
if (source?.kind !== "object")
|
|
39903
39907
|
return;
|
|
@@ -39924,6 +39928,13 @@ function captureClaudeMarketplaceRegistry(path, budget = { remaining: 256 * 1024
|
|
|
39924
39928
|
const sha2562 = createHash11("sha256").update("hasna.skills.claude-marketplace-registry.v1\x00").update(JSON.stringify(value)).digest("hex");
|
|
39925
39929
|
return { path, hashMode: "claude-marketplace-registry", sha256: sha2562 };
|
|
39926
39930
|
}
|
|
39931
|
+
function captureClaudeMarketplaceRegistryV2(path, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
39932
|
+
const value = parse5(readRegistry2(path, budget));
|
|
39933
|
+
for (const [, row] of value.entries)
|
|
39934
|
+
normalizeRow(row, 2);
|
|
39935
|
+
const sha2562 = createHash11("sha256").update("hasna.skills.claude-marketplace-registry.v2\x00").update(JSON.stringify(value)).digest("hex");
|
|
39936
|
+
return { path, hashMode: "claude-marketplace-registry-v2", sha256: sha2562 };
|
|
39937
|
+
}
|
|
39927
39938
|
|
|
39928
39939
|
// src/lib/claude-settings-witness.ts
|
|
39929
39940
|
import { createHash as createHash12 } from "crypto";
|
|
@@ -40136,9 +40147,33 @@ function assertClaudeStopHookReplacement(before, replacement) {
|
|
|
40136
40147
|
}
|
|
40137
40148
|
need3(JSON.stringify(withoutStop(original)) === JSON.stringify(withoutStop(next)), "Stop update changed settings outside hooks.Stop");
|
|
40138
40149
|
}
|
|
40139
|
-
|
|
40150
|
+
var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
|
|
40151
|
+
function inferencePreference(key, child) {
|
|
40152
|
+
if (key === "skipDangerousModePermissionPrompt") {
|
|
40153
|
+
need3(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
|
|
40154
|
+
return true;
|
|
40155
|
+
}
|
|
40156
|
+
if (key === "effortLevel") {
|
|
40157
|
+
need3(child.kind === "string" && PERSISTED_EFFORT_LEVELS.has(child.value), "contains an invalid persisted effort level");
|
|
40158
|
+
return true;
|
|
40159
|
+
}
|
|
40160
|
+
if (key !== "modelSettings")
|
|
40161
|
+
return false;
|
|
40162
|
+
need3(child.kind === "object", "requires an object of model preferences");
|
|
40163
|
+
child.entries = child.entries.filter(([model, preferences]) => {
|
|
40164
|
+
if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
|
|
40165
|
+
return true;
|
|
40166
|
+
const effort = preferences.entries[0][1];
|
|
40167
|
+
need3(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
|
|
40168
|
+
return false;
|
|
40169
|
+
});
|
|
40170
|
+
return child.entries.length === 0;
|
|
40171
|
+
}
|
|
40172
|
+
function settingsDigest(text2, version = 1) {
|
|
40140
40173
|
const value = parse6(text2);
|
|
40141
40174
|
value.entries = value.entries.filter(([key, child]) => {
|
|
40175
|
+
if (version === 2 && inferencePreference(key, child))
|
|
40176
|
+
return false;
|
|
40142
40177
|
if (key === "model") {
|
|
40143
40178
|
need3(child.kind === "string", "contains an invalid model selection");
|
|
40144
40179
|
return !BUILTIN_MODEL_SELECTIONS.has(child.value);
|
|
@@ -40153,7 +40188,7 @@ function settingsDigest(text2) {
|
|
|
40153
40188
|
}
|
|
40154
40189
|
return true;
|
|
40155
40190
|
});
|
|
40156
|
-
return createHash12("sha256").update(
|
|
40191
|
+
return createHash12("sha256").update(`hasna.skills.claude-settings.v${version}\x00`).update(JSON.stringify(canonical3(value))).digest("hex");
|
|
40157
40192
|
}
|
|
40158
40193
|
function hashClaudeSettingsReplacement(text2, budget) {
|
|
40159
40194
|
need3(typeof text2 === "string", "requires settings text");
|
|
@@ -40165,6 +40200,16 @@ function hashClaudeSettingsReplacement(text2, budget) {
|
|
|
40165
40200
|
function captureClaudeSettings(path, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
40166
40201
|
return { path, hashMode: "claude-settings-v1", sha256: settingsDigest(readSettings(path, budget)) };
|
|
40167
40202
|
}
|
|
40203
|
+
function captureClaudeSettingsV2(path, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
40204
|
+
return { path, hashMode: "claude-settings-v2", sha256: settingsDigest(readSettings(path, budget), 2) };
|
|
40205
|
+
}
|
|
40206
|
+
function hashClaudeSettingsReplacementV2(text2, budget) {
|
|
40207
|
+
need3(typeof text2 === "string", "requires settings text");
|
|
40208
|
+
const bytes = Buffer.byteLength(text2);
|
|
40209
|
+
need3(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
|
|
40210
|
+
budget.remaining -= bytes;
|
|
40211
|
+
return settingsDigest(text2, 2);
|
|
40212
|
+
}
|
|
40168
40213
|
|
|
40169
40214
|
// src/lib/codex-hook-discovery-recovery.ts
|
|
40170
40215
|
import { createHash as createHash14 } from "crypto";
|
|
@@ -40268,18 +40313,18 @@ function read2(path, changes) {
|
|
|
40268
40313
|
return readFileSync12(path, "utf8");
|
|
40269
40314
|
}
|
|
40270
40315
|
function projected(source, changes, budget = discoveryByteBudget()) {
|
|
40271
|
-
if (source.hashMode === "claude-settings-v1") {
|
|
40316
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
|
|
40272
40317
|
if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
|
|
40273
40318
|
throw new Error("Claude settings witnesses require exact typed metadata");
|
|
40274
|
-
const current = captureClaudeSettings(source.path, budget).sha256;
|
|
40319
|
+
const current = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
|
|
40275
40320
|
if (current !== source.sha256)
|
|
40276
40321
|
throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
|
|
40277
|
-
return changes?.has(source.path) ? hashClaudeSettingsReplacement(changes.get(source.path), budget) : current;
|
|
40322
|
+
return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current;
|
|
40278
40323
|
}
|
|
40279
|
-
if (source.hashMode === "claude-marketplace-registry") {
|
|
40324
|
+
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
|
|
40280
40325
|
if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
|
|
40281
40326
|
throw new Error("Claude marketplace witnesses require an exact reviewed registry without synthetic changes");
|
|
40282
|
-
const sha2562 = captureClaudeMarketplaceRegistry(source.path, budget).sha256;
|
|
40327
|
+
const sha2562 = (source.hashMode === "claude-marketplace-registry-v2" ? captureClaudeMarketplaceRegistryV2 : captureClaudeMarketplaceRegistry)(source.path, budget).sha256;
|
|
40283
40328
|
if (sha2562 !== source.sha256)
|
|
40284
40329
|
throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
|
|
40285
40330
|
return sha2562;
|
|
@@ -40336,9 +40381,9 @@ function verifyAgentDiscovery(binding, codexRecovery) {
|
|
|
40336
40381
|
safe4(root2);
|
|
40337
40382
|
}
|
|
40338
40383
|
function assertMarketplaceBinding(binding, source) {
|
|
40339
|
-
if (source.hashMode === "claude-settings-v1" && (binding.agent !== "claude" || binding.method !== "reviewed" || basename6(source.path) !== "settings.json"))
|
|
40384
|
+
if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename6(source.path) !== "settings.json"))
|
|
40340
40385
|
throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
|
|
40341
|
-
if (source.hashMode === "claude-marketplace-registry" && (binding.agent !== "claude" || binding.method !== "reviewed"))
|
|
40386
|
+
if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
|
|
40342
40387
|
throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
|
|
40343
40388
|
}
|
|
40344
40389
|
function rebindAgentDiscovery(binding, changes) {
|
|
@@ -40479,7 +40524,7 @@ function resolveAgentDiscovery(options) {
|
|
|
40479
40524
|
if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
|
|
40480
40525
|
throw new Error("Explicit discovery reviews require full source-file hashes");
|
|
40481
40526
|
verifyAgentDiscovery(supplied);
|
|
40482
|
-
if (review.sources.some((source) => source.hashMode === "claude-settings-v1" && source.path !== canonical4(configPath)))
|
|
40527
|
+
if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
|
|
40483
40528
|
throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
|
|
40484
40529
|
if (!review.sources.some((source) => source.path === canonical4(configPath)))
|
|
40485
40530
|
throw new Error("Discovery review must include the agent configuration source");
|
|
@@ -55561,6 +55606,18 @@ function rowToSkill(row) {
|
|
|
55561
55606
|
...row.replacement_slug ? { replacementSlug: String(row.replacement_slug) } : {}
|
|
55562
55607
|
};
|
|
55563
55608
|
}
|
|
55609
|
+
function rowToPublishedSelectionState(row) {
|
|
55610
|
+
return {
|
|
55611
|
+
slug: String(row.slug),
|
|
55612
|
+
version: String(row.version),
|
|
55613
|
+
current: row.skill_exists ? {
|
|
55614
|
+
lifecycle: row.lifecycle === "archived" ? "archived" : "active",
|
|
55615
|
+
...row.tombstoned_at ? { tombstonedAt: dateString(row.tombstoned_at) } : {}
|
|
55616
|
+
} : null,
|
|
55617
|
+
versionBundleSha256: typeof row.bundle_sha256 === "string" ? row.bundle_sha256 : null,
|
|
55618
|
+
bundleAvailable: Boolean(row.bundle_available)
|
|
55619
|
+
};
|
|
55620
|
+
}
|
|
55564
55621
|
function rowToPin(row) {
|
|
55565
55622
|
return {
|
|
55566
55623
|
orgId: String(row.org_id),
|
|
@@ -56335,6 +56392,22 @@ class SqliteSkillsStore {
|
|
|
56335
56392
|
const row = this.get("SELECT * FROM skills_versions WHERE org_id = ? AND slug = ? AND version = ? LIMIT 1", [principal.orgId, slug, version2]);
|
|
56336
56393
|
return row ? rowToSkillVersion(row) : null;
|
|
56337
56394
|
}
|
|
56395
|
+
async getPublishedSelectionStates(principal, selections2) {
|
|
56396
|
+
if (selections2.length > MAX_PROFILE_SELECTIONS)
|
|
56397
|
+
throw new RangeError("Published selection lookup exceeds the profile selection limit");
|
|
56398
|
+
if (!selections2.length)
|
|
56399
|
+
return [];
|
|
56400
|
+
const requested = JSON.stringify(selections2.map(({ slug, version: version2 }) => ({ slug, version: version2 })));
|
|
56401
|
+
return this.all(`SELECT json_extract(q.value, '$.slug') AS slug, json_extract(q.value, '$.version') AS version,
|
|
56402
|
+
r.slug IS NOT NULL AS skill_exists, r.lifecycle, r.tombstoned_at,
|
|
56403
|
+
v.bundle_sha256, b.sha256 IS NOT NULL AS bundle_available
|
|
56404
|
+
FROM json_each(?) q
|
|
56405
|
+
LEFT JOIN skills_registry r ON r.org_id = ? AND r.slug = json_extract(q.value, '$.slug')
|
|
56406
|
+
LEFT JOIN skills_versions v ON v.org_id = ?
|
|
56407
|
+
AND v.slug = json_extract(q.value, '$.slug') AND v.version = json_extract(q.value, '$.version')
|
|
56408
|
+
LEFT JOIN skills_bundles b ON b.org_id = ? AND b.sha256 = v.bundle_sha256
|
|
56409
|
+
ORDER BY CAST(q.key AS INTEGER)`, [requested, principal.orgId, principal.orgId, principal.orgId]).map(rowToPublishedSelectionState);
|
|
56410
|
+
}
|
|
56338
56411
|
async pinSkill(principal, slug, metadata = {}) {
|
|
56339
56412
|
const row = this.get(`INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
|
56340
56413
|
VALUES (?, ?, ?, ?, ?)
|
|
@@ -57463,16 +57536,21 @@ function selected(value) {
|
|
|
57463
57536
|
return selections2;
|
|
57464
57537
|
}
|
|
57465
57538
|
async function validatePublished(store, principal, selections2) {
|
|
57466
|
-
|
|
57467
|
-
|
|
57539
|
+
const states = await store.getPublishedSelectionStates(principal, selections2);
|
|
57540
|
+
if (states.length !== selections2.length)
|
|
57541
|
+
throw new SkillRequestError(503, "SELECTION_UNAVAILABLE", "Selected publication state is unavailable");
|
|
57542
|
+
for (const [index, selection2] of selections2.entries()) {
|
|
57543
|
+
const state2 = states[index];
|
|
57544
|
+
if (!state2 || state2.slug !== selection2.slug || state2.version !== selection2.version)
|
|
57545
|
+
throw new SkillRequestError(503, "SELECTION_UNAVAILABLE", "Selected publication state is unavailable");
|
|
57546
|
+
const current = state2.current;
|
|
57468
57547
|
if (!current || current.tombstonedAt)
|
|
57469
57548
|
throw new SkillRequestError(current?.tombstonedAt ? 410 : 404, "SELECTION_UNAVAILABLE", `Selected skill '${selection2.slug}' is unavailable`);
|
|
57470
57549
|
if (current.lifecycle === "archived")
|
|
57471
57550
|
throw new SkillRequestError(409, "SELECTION_ARCHIVED", `Selected skill '${selection2.slug}' is archived`);
|
|
57472
|
-
|
|
57473
|
-
if (!version2 || `sha256:${version2.bundleSha256}` !== selection2.bundleDigest)
|
|
57551
|
+
if (!state2.versionBundleSha256 || `sha256:${state2.versionBundleSha256}` !== selection2.bundleDigest)
|
|
57474
57552
|
throw new SkillRequestError(409, "SELECTION_VERSION_MISMATCH", `Selected version of '${selection2.slug}' does not match its published digest`);
|
|
57475
|
-
if (!
|
|
57553
|
+
if (!state2.bundleAvailable)
|
|
57476
57554
|
throw new SkillRequestError(503, "SELECTION_BUNDLE_UNAVAILABLE", "Selected bundle is unavailable");
|
|
57477
57555
|
}
|
|
57478
57556
|
}
|
|
@@ -63361,6 +63439,25 @@ class MemorySkillsStore {
|
|
|
63361
63439
|
const found = this.versions.get(versionKey(principal.orgId, slug, version2));
|
|
63362
63440
|
return found && found.orgId === principal.orgId ? found : null;
|
|
63363
63441
|
}
|
|
63442
|
+
async getPublishedSelectionStates(principal, selections2) {
|
|
63443
|
+
if (selections2.length > MAX_PROFILE_SELECTIONS)
|
|
63444
|
+
throw new RangeError("Published selection lookup exceeds the profile selection limit");
|
|
63445
|
+
return selections2.map(({ slug, version: version2 }) => {
|
|
63446
|
+
const current = this.skills.get(skillKey(principal.orgId, slug));
|
|
63447
|
+
const selectedVersion = this.versions.get(versionKey(principal.orgId, slug, version2));
|
|
63448
|
+
const bundle2 = selectedVersion && this.bundles.get(skillKey(principal.orgId, selectedVersion.bundleSha256));
|
|
63449
|
+
return {
|
|
63450
|
+
slug,
|
|
63451
|
+
version: version2,
|
|
63452
|
+
current: current?.orgId === principal.orgId ? {
|
|
63453
|
+
lifecycle: current.lifecycle,
|
|
63454
|
+
...current.tombstonedAt ? { tombstonedAt: current.tombstonedAt } : {}
|
|
63455
|
+
} : null,
|
|
63456
|
+
versionBundleSha256: selectedVersion?.orgId === principal.orgId ? selectedVersion.bundleSha256 : null,
|
|
63457
|
+
bundleAvailable: bundle2?.orgId === principal.orgId
|
|
63458
|
+
};
|
|
63459
|
+
});
|
|
63460
|
+
}
|
|
63364
63461
|
async listSkills(principal) {
|
|
63365
63462
|
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt).sort((a4, b4) => a4.slug.localeCompare(b4.slug));
|
|
63366
63463
|
}
|
|
@@ -64169,6 +64266,25 @@ class PostgresSkillsStore {
|
|
|
64169
64266
|
`;
|
|
64170
64267
|
return rows[0] ? rowToSkillVersion(rows[0]) : null;
|
|
64171
64268
|
}
|
|
64269
|
+
async getPublishedSelectionStates(principal, selections2) {
|
|
64270
|
+
if (selections2.length > MAX_PROFILE_SELECTIONS)
|
|
64271
|
+
throw new RangeError("Published selection lookup exceeds the profile selection limit");
|
|
64272
|
+
if (!selections2.length)
|
|
64273
|
+
return [];
|
|
64274
|
+
const requested = JSON.stringify(selections2.map(({ slug, version: version2 }) => ({ slug, version: version2 })));
|
|
64275
|
+
const rows = await this.sql`
|
|
64276
|
+
SELECT q.value->>'slug' AS slug, q.value->>'version' AS version,
|
|
64277
|
+
r.slug IS NOT NULL AS skill_exists, r.lifecycle, r.tombstoned_at,
|
|
64278
|
+
v.bundle_sha256, b.sha256 IS NOT NULL AS bundle_available
|
|
64279
|
+
FROM jsonb_array_elements(${requested}::text::jsonb) WITH ORDINALITY AS q(value, ordinal)
|
|
64280
|
+
LEFT JOIN skills_registry r ON r.org_id = ${principal.orgId} AND r.slug = q.value->>'slug'
|
|
64281
|
+
LEFT JOIN skills_versions v ON v.org_id = ${principal.orgId}
|
|
64282
|
+
AND v.slug = q.value->>'slug' AND v.version = q.value->>'version'
|
|
64283
|
+
LEFT JOIN skills_bundles b ON b.org_id = ${principal.orgId} AND b.sha256 = v.bundle_sha256
|
|
64284
|
+
ORDER BY q.ordinal
|
|
64285
|
+
`;
|
|
64286
|
+
return rows.map(rowToPublishedSelectionState);
|
|
64287
|
+
}
|
|
64172
64288
|
async pinSkill(principal, slug, metadata = {}) {
|
|
64173
64289
|
const rows = await this.sql`
|
|
64174
64290
|
INSERT INTO skills_pins (org_id, principal, slug, pinned_at, metadata_json)
|
package/dist/server/rows.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ServerSkillVersion, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord } from "./types.js";
|
|
1
|
+
import type { ServerSkillVersion, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, PublishedSkillSelectionState } from "./types.js";
|
|
2
2
|
export declare function nowIso(): string;
|
|
3
3
|
/**
|
|
4
4
|
* Coerce a caller-supplied row limit to a non-negative integer.
|
|
@@ -17,6 +17,7 @@ export declare function rowToRun(row: Record<string, unknown>): ServerRunRecord;
|
|
|
17
17
|
export declare function rowToLog(row: Record<string, unknown>): ServerRunLog;
|
|
18
18
|
export declare function rowToArtifact(row: Record<string, unknown>): ServerArtifact;
|
|
19
19
|
export declare function rowToSkill(row: Record<string, unknown>): ServerSkillRecord;
|
|
20
|
+
export declare function rowToPublishedSelectionState(row: Record<string, unknown>): PublishedSkillSelectionState;
|
|
20
21
|
export declare function rowToPin(row: Record<string, unknown>): ServerPin;
|
|
21
22
|
export declare function rowToSkillBundle(row: Record<string, unknown>): ServerSkillBundle;
|
|
22
23
|
export declare function parseJsonObject(value: unknown): Record<string, unknown>;
|
|
@@ -15,7 +15,7 @@ import { SqliteExecutionGrantStore } from "./execution-grant-store.js";
|
|
|
15
15
|
*/
|
|
16
16
|
import { Database } from "bun:sqlite";
|
|
17
17
|
import { SqliteSkillSelectionStore } from "./selection-store.js";
|
|
18
|
-
import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, ServerSkillRecord, SkillLifecyclePatch, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
|
|
18
|
+
import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, PublishedSkillSelection, PublishedSkillSelectionState, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, ServerSkillRecord, SkillLifecyclePatch, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
|
|
19
19
|
export interface SqliteStoreOptions {
|
|
20
20
|
/** Apply pending migrations on open. Default true - it is what makes zero-config work. */
|
|
21
21
|
migrate?: boolean;
|
|
@@ -113,6 +113,7 @@ export declare class SqliteSkillsStore implements SkillsProductStore {
|
|
|
113
113
|
getSkillBundle(principal: ApiPrincipal, sha256: string): Promise<ServerSkillBundle | null>;
|
|
114
114
|
listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
|
|
115
115
|
getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
|
|
116
|
+
getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
|
|
116
117
|
pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
|
|
117
118
|
unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
|
|
118
119
|
listPins(principal: ApiPrincipal): Promise<ServerPin[]>;
|
package/dist/server/store.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { MemoryExecutionGrantStore, PostgresExecutionGrantStore } from "./execution-grant-store.js";
|
|
2
2
|
import { MemorySkillSelectionStore, PostgresSkillSelectionStore } from "./selection-store.js";
|
|
3
|
-
import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
|
|
3
|
+
import type { ApiPrincipal, ClaimRunInput, CreateRunInput, PublishSkillInput, PublishedSkillSelection, PublishedSkillSelectionState, RunTransitionPatch, ServerArtifact, ServerPin, ServerRunLog, ServerRunRecord, ServerSkillBundle, ServerSkillRecord, ServerSkillVersion, ApiKeyScopeUpdateResult, OperatorScopeEnrollmentInput, OperatorScopeEnrollmentResult, OperatorScopeTargetSnapshot, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.js";
|
|
4
4
|
import { type SkillLifecyclePatch } from "./types.js";
|
|
5
5
|
import { type SqliteStoreOptions } from "./sqlite-store.js";
|
|
6
6
|
export declare function createArtifactId(): string;
|
|
@@ -79,6 +79,7 @@ export declare class MemorySkillsStore implements SkillsProductStore {
|
|
|
79
79
|
publishSkill(input: PublishSkillInput): Promise<ServerSkillRecord>;
|
|
80
80
|
listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
|
|
81
81
|
getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
|
|
82
|
+
getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
|
|
82
83
|
listSkills(principal: ApiPrincipal): Promise<ServerSkillRecord[]>;
|
|
83
84
|
getSkill(principal: ApiPrincipal, slug: string): Promise<ServerSkillRecord | null>;
|
|
84
85
|
setSkillLifecycle(principal: ApiPrincipal, slug: string, patch: SkillLifecyclePatch, expectedRevisionId?: string): Promise<ServerSkillRecord | null>;
|
|
@@ -180,6 +181,7 @@ export declare class PostgresSkillsStore implements SkillsProductStore {
|
|
|
180
181
|
getSkillBundle(principal: ApiPrincipal, sha256: string): Promise<ServerSkillBundle | null>;
|
|
181
182
|
listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
|
|
182
183
|
getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
|
|
184
|
+
getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
|
|
183
185
|
pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
|
|
184
186
|
unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
|
|
185
187
|
listPins(principal: ApiPrincipal): Promise<ServerPin[]>;
|
package/dist/server/types.d.ts
CHANGED
|
@@ -279,6 +279,17 @@ export interface ServerSkillVersion {
|
|
|
279
279
|
publishedByUserId?: string;
|
|
280
280
|
createdAt: string;
|
|
281
281
|
}
|
|
282
|
+
/** Exact version lookup, independent of the registry's mutable current version. */
|
|
283
|
+
export interface PublishedSkillSelection {
|
|
284
|
+
slug: string;
|
|
285
|
+
version: string;
|
|
286
|
+
}
|
|
287
|
+
/** Metadata only: bundle availability means its tenant-owned storage row exists. */
|
|
288
|
+
export interface PublishedSkillSelectionState extends PublishedSkillSelection {
|
|
289
|
+
current: Pick<ServerSkillRecord, "lifecycle" | "tombstonedAt"> | null;
|
|
290
|
+
versionBundleSha256: string | null;
|
|
291
|
+
bundleAvailable: boolean;
|
|
292
|
+
}
|
|
282
293
|
export interface PublishSkillInput {
|
|
283
294
|
/** Internal boot-seed precondition: never revive or replace non-bundled records. */
|
|
284
295
|
seedBundledOnly?: true;
|
|
@@ -441,6 +452,13 @@ export interface SkillsProductStore {
|
|
|
441
452
|
/** Every published version of a slug, newest first (hasna/apps#1630). */
|
|
442
453
|
listSkillVersions(principal: ApiPrincipal, slug: string): Promise<ServerSkillVersion[]>;
|
|
443
454
|
getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise<ServerSkillVersion | null>;
|
|
455
|
+
/**
|
|
456
|
+
* Read at most MAX_PROFILE_SELECTIONS exact selections in one database snapshot,
|
|
457
|
+
* scoped to the principal's org at every join. Returns one row per input in input
|
|
458
|
+
* order, including missing selections and duplicates. Never reads bundle bytes or
|
|
459
|
+
* substitutes the latest version; callers retain lifecycle/digest error policy.
|
|
460
|
+
*/
|
|
461
|
+
getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise<PublishedSkillSelectionState[]>;
|
|
444
462
|
pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record<string, unknown>): Promise<ServerPin>;
|
|
445
463
|
/** False when this principal has no pin by that slug. */
|
|
446
464
|
unpinSkill(principal: ApiPrincipal, slug: string): Promise<boolean>;
|
package/docs/plugin-admission.md
CHANGED
|
@@ -286,3 +286,39 @@ cache history is bounded to 128 entries and an aggregate 256 MiB per witness.
|
|
|
286
286
|
Primary runtime references: [command sources](https://code.claude.com/docs/en/plugin-marketplaces#command-sources),
|
|
287
287
|
[plugin components](https://code.claude.com/docs/en/plugins-reference), and
|
|
288
288
|
[exact command acceptance](https://github.com/anthropics/claude-code/blob/main/CHANGELOG.md#21271).
|
|
289
|
+
|
|
290
|
+
### Explicit v2 preference reviews
|
|
291
|
+
|
|
292
|
+
`captureClaudeSettingsV2(path)` creates a `claude-settings-v2` witness. In
|
|
293
|
+
addition to the v1 display/model selections, it tolerates the documented
|
|
294
|
+
persisted effort values `low`, `medium`, `high`, and `xhigh`, both at the top
|
|
295
|
+
level and in known built-in model entries whose only property is `effortLevel`.
|
|
296
|
+
Custom model entries and entries with any other property remain fully bound.
|
|
297
|
+
The boolean `skipDangerousModePermissionPrompt` is Claude's saved dialog
|
|
298
|
+
acknowledgment; it does not select a permission mode. Permission settings,
|
|
299
|
+
plugins, hooks, environment, ultracode and unknown configuration remain bound.
|
|
300
|
+
Invalid recognized preference types are refused.
|
|
301
|
+
|
|
302
|
+
`captureClaudeMarketplaceRegistryV2(path)` creates a
|
|
303
|
+
`claude-marketplace-registry-v2` witness. It also recognizes a registry row with
|
|
304
|
+
an optional boolean `autoUpdate`. That boolean remains bound; only a strictly
|
|
305
|
+
validated `lastUpdated` timestamp may vary. A new source or installation path,
|
|
306
|
+
a changed automatic-update policy, or an unknown field still requires review.
|
|
307
|
+
|
|
308
|
+
Capture either witness through the CLI:
|
|
309
|
+
|
|
310
|
+
```sh
|
|
311
|
+
skills hook witness --kind claude-settings-v2 --path /canonical/home/.claude/settings.json --json
|
|
312
|
+
skills hook witness --kind claude-marketplace-registry-v2 --path /canonical/home/.claude/plugins/known_marketplaces.json --json
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Capture does not install or authorize a review. Compare the current complete
|
|
316
|
+
configuration with its previous review, put the chosen witness in the reviewed
|
|
317
|
+
discovery inputs, and preview `skills hook install --discovery-inputs <file>`
|
|
318
|
+
before applying. Existing byte and v1 witnesses are never silently converted.
|
|
319
|
+
Runtime support for the selected witness version must be installed before
|
|
320
|
+
policy activation. A real hook installation is a configuration change, not a
|
|
321
|
+
preference: its owning installer must coordinate a guarded settings/policy
|
|
322
|
+
update, or it needs a fresh explicit review.
|
|
323
|
+
|
|
324
|
+
The effort schema follows [Claude's model configuration documentation](https://code.claude.com/docs/en/model-config#adjust-effort-level).
|