@hasna/skills 0.9.2 → 0.9.3
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 +10 -7
- package/bin/maintenance.js +4913 -0
- package/bin/mcp.js +3 -2
- package/bin/migrate.js +55 -2
- package/bin/server.js +173 -10
- package/bin/worker.js +173 -10
- package/dist/index.js +8 -5
- package/dist/lib/agent-integration.d.ts +1 -0
- package/dist/lib/codex-hook-rpc.d.ts +1 -1
- package/dist/sdk/index.js +178 -13
- package/dist/server/maintenance-entry.d.ts +2 -0
- package/dist/server/maintenance.d.ts +1 -0
- package/dist/server/sqlite-store.d.ts +3 -1
- package/dist/server/store.d.ts +7 -1
- package/dist/server/types.d.ts +44 -0
- package/migrations/postgres/0005_operator_scope_receipts.sql +9 -0
- package/migrations/sqlite/0005_operator_scope_receipts.sql +7 -0
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -11448,7 +11448,7 @@ import { isAbsolute as isAbsolute10, join as join20, resolve as resolve10 } from
|
|
|
11448
11448
|
// package.json
|
|
11449
11449
|
var package_default = {
|
|
11450
11450
|
name: "@hasna/skills",
|
|
11451
|
-
version: "0.9.
|
|
11451
|
+
version: "0.9.3",
|
|
11452
11452
|
description: "Skills library for AI coding agents",
|
|
11453
11453
|
type: "module",
|
|
11454
11454
|
bin: {
|
|
@@ -11457,6 +11457,7 @@ var package_default = {
|
|
|
11457
11457
|
"skills-serve": "bin/server.js",
|
|
11458
11458
|
"skills-server": "bin/server.js",
|
|
11459
11459
|
"skills-worker": "bin/worker.js",
|
|
11460
|
+
"skills-maintenance": "bin/maintenance.js",
|
|
11460
11461
|
"skills-migrate": "bin/migrate.js"
|
|
11461
11462
|
},
|
|
11462
11463
|
exports: {
|
|
@@ -11497,7 +11498,7 @@ var package_default = {
|
|
|
11497
11498
|
types: "./dist/index.d.ts",
|
|
11498
11499
|
scripts: {
|
|
11499
11500
|
clean: "rm -rf bin/ dist/",
|
|
11500
|
-
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
11501
|
+
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/server/maintenance.ts --outfile ./bin/maintenance.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
11501
11502
|
"build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
11502
11503
|
test: "bun test --timeout 30000",
|
|
11503
11504
|
dev: "bun run ./src/cli/index.tsx",
|
|
@@ -15784,10 +15785,12 @@ function projectAncestorDirectories(projects) {
|
|
|
15784
15785
|
}
|
|
15785
15786
|
function inventoryNativeSkills(home = homedir3(), options = {}) {
|
|
15786
15787
|
const aliases = rootAliases(home, options.allowRootAliases);
|
|
15787
|
-
const
|
|
15788
|
+
const selectedAgents = options.agents ? new Set(options.agents) : undefined;
|
|
15789
|
+
const rootDefinitions = selectedAgents ? ROOTS.filter(([agent, path]) => selectedAgents.has(agent) || selectedAgents.has("hermes") && agent === "codex" && path === ".agents/skills") : ROOTS;
|
|
15790
|
+
const roots = rootDefinitions.map(([agent, path]) => [agent, canonicalAgentPath(join25(home, path), aliases)]);
|
|
15788
15791
|
const bridgePaths = Object.values(AGENT_ADAPTERS).map((adapter) => canonicalAgentPath(join25(home, adapter.root, CLI_BRIDGE_NAME), aliases));
|
|
15789
15792
|
for (const project of projectAncestorDirectories([...options.projectDirs ?? [], ...options.projectDir ? [options.projectDir] : []])) {
|
|
15790
|
-
for (const [agent, path] of
|
|
15793
|
+
for (const [agent, path] of rootDefinitions)
|
|
15791
15794
|
roots.push([agent, canonicalAgentPath(join25(project, path), aliases)]);
|
|
15792
15795
|
}
|
|
15793
15796
|
const entries = [], seen = new Set;
|
|
@@ -16043,7 +16046,7 @@ function planAgentIntegration(options) {
|
|
|
16043
16046
|
return [agent, { command, profileId: profileId2 }];
|
|
16044
16047
|
}));
|
|
16045
16048
|
const discoveries = [...new Set(options.agents)].map((agent) => resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, canonical: (path) => canonicalAgentPath(path, aliases) }));
|
|
16046
|
-
const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
|
|
16049
|
+
const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), agents: options.agents, projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
|
|
16047
16050
|
const changes = [];
|
|
16048
16051
|
for (const agent of [...new Set(options.agents)]) {
|
|
16049
16052
|
if (!INTEGRATION_AGENTS.includes(agent))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const SUPPORTED_CODEX_HOOK_VERSIONS: readonly ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0"];
|
|
1
|
+
export declare const SUPPORTED_CODEX_HOOK_VERSIONS: readonly ["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1"];
|
|
2
2
|
export interface CodexHookRpc {
|
|
3
3
|
version: string;
|
|
4
4
|
processId?: number;
|
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.3",
|
|
32377
32377
|
description: "Skills library for AI coding agents",
|
|
32378
32378
|
type: "module",
|
|
32379
32379
|
bin: {
|
|
@@ -32382,6 +32382,7 @@ var package_default = {
|
|
|
32382
32382
|
"skills-serve": "bin/server.js",
|
|
32383
32383
|
"skills-server": "bin/server.js",
|
|
32384
32384
|
"skills-worker": "bin/worker.js",
|
|
32385
|
+
"skills-maintenance": "bin/maintenance.js",
|
|
32385
32386
|
"skills-migrate": "bin/migrate.js"
|
|
32386
32387
|
},
|
|
32387
32388
|
exports: {
|
|
@@ -32422,7 +32423,7 @@ var package_default = {
|
|
|
32422
32423
|
types: "./dist/index.d.ts",
|
|
32423
32424
|
scripts: {
|
|
32424
32425
|
clean: "rm -rf bin/ dist/",
|
|
32425
|
-
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
32426
|
+
build: "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/server/maintenance.ts --outfile ./bin/maintenance.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
32426
32427
|
"build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
32427
32428
|
test: "bun test --timeout 30000",
|
|
32428
32429
|
dev: "bun run ./src/cli/index.tsx",
|
|
@@ -40707,10 +40708,12 @@ function projectAncestorDirectories(projects) {
|
|
|
40707
40708
|
}
|
|
40708
40709
|
function inventoryNativeSkills(home = homedir3(), options = {}) {
|
|
40709
40710
|
const aliases = rootAliases(home, options.allowRootAliases);
|
|
40710
|
-
const
|
|
40711
|
+
const selectedAgents = options.agents ? new Set(options.agents) : undefined;
|
|
40712
|
+
const rootDefinitions = selectedAgents ? ROOTS.filter(([agent, path]) => selectedAgents.has(agent) || selectedAgents.has("hermes") && agent === "codex" && path === ".agents/skills") : ROOTS;
|
|
40713
|
+
const roots = rootDefinitions.map(([agent, path]) => [agent, canonicalAgentPath(join21(home, path), aliases)]);
|
|
40711
40714
|
const bridgePaths = Object.values(AGENT_ADAPTERS).map((adapter) => canonicalAgentPath(join21(home, adapter.root, CLI_BRIDGE_NAME), aliases));
|
|
40712
40715
|
for (const project of projectAncestorDirectories([...options.projectDirs ?? [], ...options.projectDir ? [options.projectDir] : []])) {
|
|
40713
|
-
for (const [agent, path] of
|
|
40716
|
+
for (const [agent, path] of rootDefinitions)
|
|
40714
40717
|
roots.push([agent, canonicalAgentPath(join21(project, path), aliases)]);
|
|
40715
40718
|
}
|
|
40716
40719
|
const entries = [], seen = new Set;
|
|
@@ -40966,7 +40969,7 @@ function planAgentIntegration(options) {
|
|
|
40966
40969
|
return [agent, { command, profileId: profileId2 }];
|
|
40967
40970
|
}));
|
|
40968
40971
|
const discoveries = [...new Set(options.agents)].map((agent) => resolveAgentDiscovery({ home, agent, reviewed: options.discoveryInputs, canonical: (path) => canonicalAgentPath(path, aliases) }));
|
|
40969
|
-
const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
|
|
40972
|
+
const nativeSkills = inventoryNativeSkills(home, { includeVendor: true, guardHermes: options.agents.includes("hermes"), agents: options.agents, projectDir: options.projectDir, agentRoots: discoveries.flatMap((binding) => binding.roots.map((path) => ({ agent: binding.agent, path }))), allowRootAliases: options.allowRootAliases });
|
|
40970
40973
|
const changes = [];
|
|
40971
40974
|
for (const agent of [...new Set(options.agents)]) {
|
|
40972
40975
|
if (!INTEGRATION_AGENTS.includes(agent))
|
|
@@ -55225,6 +55228,10 @@ class SkillLifecycleConflictError extends Error {
|
|
|
55225
55228
|
this.name = "SkillLifecycleConflictError";
|
|
55226
55229
|
}
|
|
55227
55230
|
}
|
|
55231
|
+
function validOperatorScopeEnrollmentInput(input) {
|
|
55232
|
+
const bounded = (value, max = 256) => typeof value === "string" && value.length > 0 && value.length <= max && !/[\u0000-\u001f\u007f]/.test(value);
|
|
55233
|
+
return bounded(input.keyId) && bounded(input.stationId) && bounded(input.orgId) && bounded(input.operationId) && bounded(input.manifestDigest, 64) && /^[a-f0-9]{64}$/.test(input.manifestDigest) && bounded(input.operatorJobId) && bounded(input.operatorTaskArn, 512) && input.expectedScopes.length > 0 && input.expectedScopes.length <= 32 && input.expectedScopes.every((scope) => bounded(scope, 128) && /^[a-z][a-z0-9_-]*:[a-z][a-z0-9_-]*$/.test(scope));
|
|
55234
|
+
}
|
|
55228
55235
|
|
|
55229
55236
|
// src/lib/revision.ts
|
|
55230
55237
|
import { createHash as createHash20 } from "crypto";
|
|
@@ -55373,6 +55380,54 @@ class SqliteSkillsStore {
|
|
|
55373
55380
|
}
|
|
55374
55381
|
return { kind: "updated", scopes };
|
|
55375
55382
|
}
|
|
55383
|
+
async enrollPublishScopeByOperator(input) {
|
|
55384
|
+
if (!validOperatorScopeEnrollmentInput(input))
|
|
55385
|
+
return { kind: "invalid" };
|
|
55386
|
+
if (input.expectedScopes.includes("skills:publish"))
|
|
55387
|
+
return { kind: "stale", scopes: input.expectedScopes };
|
|
55388
|
+
const operation = this.get("SELECT org_id, target_id FROM skills_audit_events WHERE action = ? AND operator_operation_id = ? LIMIT 1", ["api_key_scopes_added", input.operationId]);
|
|
55389
|
+
if (operation && (String(operation.org_id) !== input.orgId || String(operation.target_id) !== input.keyId))
|
|
55390
|
+
return { kind: "target_mismatch" };
|
|
55391
|
+
const row = this.get("SELECT id, org_id, name, scopes_json FROM api_keys WHERE id = ? AND revoked_at IS NULL LIMIT 1", [input.keyId]);
|
|
55392
|
+
if (!row)
|
|
55393
|
+
return { kind: "not_found" };
|
|
55394
|
+
if (String(row.org_id) !== input.orgId)
|
|
55395
|
+
return { kind: "target_mismatch" };
|
|
55396
|
+
const current = parseScopes(row.scopes_json);
|
|
55397
|
+
const prior = this.get("SELECT metadata_json FROM skills_audit_events WHERE action = ? AND target_type = ? AND target_id = ? AND json_extract(metadata_json, '$.operator_operation_id') = ? LIMIT 1", ["api_key_scopes_added", "api_key", input.keyId, input.operationId]);
|
|
55398
|
+
if (prior) {
|
|
55399
|
+
try {
|
|
55400
|
+
if (JSON.parse(String(prior.metadata_json)).target_manifest_digest !== input.manifestDigest)
|
|
55401
|
+
return { kind: "target_mismatch" };
|
|
55402
|
+
} catch {
|
|
55403
|
+
return { kind: "target_mismatch" };
|
|
55404
|
+
}
|
|
55405
|
+
return current.includes("skills:publish") ? { kind: "already_applied", scopes: current } : { kind: "stale", scopes: current };
|
|
55406
|
+
}
|
|
55407
|
+
if (current.length !== input.expectedScopes.length || current.some((scope, index) => scope !== input.expectedScopes[index]))
|
|
55408
|
+
return { kind: "stale", scopes: current };
|
|
55409
|
+
const scopes = [...current, "skills:publish"];
|
|
55410
|
+
const tx = this.db.transaction(() => {
|
|
55411
|
+
const changed = this.db.run("UPDATE api_keys SET scopes_json = ? WHERE id = ? AND org_id = ? AND revoked_at IS NULL AND scopes_json = ?", [JSON.stringify(scopes), input.keyId, input.orgId, JSON.stringify(input.expectedScopes)]);
|
|
55412
|
+
if (changed.changes !== 1)
|
|
55413
|
+
return false;
|
|
55414
|
+
this.db.run("INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, operator_operation_id, metadata_json) VALUES (?, NULL, NULL, ?, ?, ?, ?, ?)", [input.orgId, "api_key_scopes_added", "api_key", input.keyId, input.operationId, JSON.stringify({ added: ["skills:publish"], scopes, operator_operation_id: input.operationId, operator_job_id: input.operatorJobId, operator_task_arn: input.operatorTaskArn, target_manifest_digest: input.manifestDigest, station_id: input.stationId })]);
|
|
55415
|
+
return true;
|
|
55416
|
+
});
|
|
55417
|
+
if (!tx()) {
|
|
55418
|
+
const latest = this.get("SELECT scopes_json FROM api_keys WHERE id = ? AND org_id = ? AND revoked_at IS NULL LIMIT 1", [input.keyId, input.orgId]);
|
|
55419
|
+
return { kind: "stale", scopes: latest ? parseScopes(latest.scopes_json) : current };
|
|
55420
|
+
}
|
|
55421
|
+
return { kind: "updated", scopes };
|
|
55422
|
+
}
|
|
55423
|
+
async inspectOperatorScopeTarget(keyId, orgId) {
|
|
55424
|
+
const row = this.get("SELECT org_id, scopes_json FROM api_keys WHERE id = ? AND revoked_at IS NULL LIMIT 1", [keyId]);
|
|
55425
|
+
if (!row)
|
|
55426
|
+
return { kind: "not_found" };
|
|
55427
|
+
if (String(row.org_id) !== orgId)
|
|
55428
|
+
return { kind: "target_mismatch" };
|
|
55429
|
+
return { kind: "found", scopes: parseScopes(row.scopes_json) };
|
|
55430
|
+
}
|
|
55376
55431
|
async createRun(input) {
|
|
55377
55432
|
if (input.idempotencyKey) {
|
|
55378
55433
|
const existing = this.get("SELECT * FROM skills_runs WHERE org_id = ? AND idempotency_key = ? LIMIT 1", [input.principal.orgId, input.idempotencyKey]);
|
|
@@ -62620,6 +62675,7 @@ class MemorySkillsStore {
|
|
|
62620
62675
|
bundles = new Map;
|
|
62621
62676
|
versions = new Map;
|
|
62622
62677
|
pins = new Map;
|
|
62678
|
+
operatorEnrollments = new Map;
|
|
62623
62679
|
constructor(apiKeys = []) {
|
|
62624
62680
|
for (const key of apiKeys)
|
|
62625
62681
|
this.addApiKey(key.token, key.principal);
|
|
@@ -62649,6 +62705,41 @@ class MemorySkillsStore {
|
|
|
62649
62705
|
}
|
|
62650
62706
|
return { kind: "not_found" };
|
|
62651
62707
|
}
|
|
62708
|
+
async enrollPublishScopeByOperator(input) {
|
|
62709
|
+
if (!validOperatorScopeEnrollmentInput(input))
|
|
62710
|
+
return { kind: "invalid" };
|
|
62711
|
+
const previous = this.operatorEnrollments.get(input.operationId);
|
|
62712
|
+
if (previous) {
|
|
62713
|
+
if (previous.keyId !== input.keyId || previous.manifestDigest !== input.manifestDigest)
|
|
62714
|
+
return { kind: "target_mismatch" };
|
|
62715
|
+
for (const target of this.apiKeys.values()) {
|
|
62716
|
+
if (target.apiKeyId === input.keyId && target.orgId === input.orgId && target.scopes.includes("skills:publish")) {
|
|
62717
|
+
return { kind: "already_applied", scopes: [...target.scopes] };
|
|
62718
|
+
}
|
|
62719
|
+
}
|
|
62720
|
+
return { kind: "stale", scopes: [] };
|
|
62721
|
+
}
|
|
62722
|
+
for (const [hash3, target] of this.apiKeys) {
|
|
62723
|
+
if (target.apiKeyId !== input.keyId || target.orgId !== input.orgId)
|
|
62724
|
+
continue;
|
|
62725
|
+
if (target.scopes.length !== input.expectedScopes.length || target.scopes.some((scope2, index) => scope2 !== input.expectedScopes[index])) {
|
|
62726
|
+
return { kind: "stale", scopes: [...target.scopes] };
|
|
62727
|
+
}
|
|
62728
|
+
const scopes = target.scopes.includes("skills:publish") ? [...target.scopes] : [...target.scopes, "skills:publish"];
|
|
62729
|
+
this.apiKeys.set(hash3, { ...target, scopes });
|
|
62730
|
+
this.operatorEnrollments.set(input.operationId, { keyId: input.keyId, manifestDigest: input.manifestDigest });
|
|
62731
|
+
return { kind: "updated", scopes };
|
|
62732
|
+
}
|
|
62733
|
+
return { kind: "not_found" };
|
|
62734
|
+
}
|
|
62735
|
+
async inspectOperatorScopeTarget(keyId, orgId) {
|
|
62736
|
+
for (const target of this.apiKeys.values()) {
|
|
62737
|
+
if (target.apiKeyId !== keyId)
|
|
62738
|
+
continue;
|
|
62739
|
+
return target.orgId === orgId ? { kind: "found", scopes: [...target.scopes] } : { kind: "target_mismatch" };
|
|
62740
|
+
}
|
|
62741
|
+
return { kind: "not_found" };
|
|
62742
|
+
}
|
|
62652
62743
|
async createRun(input) {
|
|
62653
62744
|
const idemKey = input.idempotencyKey?.trim();
|
|
62654
62745
|
const idemMapKey = idemKey ? `${input.principal.orgId}:${idemKey}` : undefined;
|
|
@@ -63063,24 +63154,98 @@ class PostgresSkillsStore {
|
|
|
63063
63154
|
if (current.length !== expectedScopes.length || current.some((scope2, index) => scope2 !== expectedScopes[index])) {
|
|
63064
63155
|
return { kind: "stale", scopes: current };
|
|
63065
63156
|
}
|
|
63066
|
-
const
|
|
63067
|
-
const updated = await tx`
|
|
63068
|
-
UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
|
|
63069
|
-
WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL
|
|
63070
|
-
AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
|
|
63071
|
-
RETURNING scopes_json
|
|
63072
|
-
`;
|
|
63157
|
+
const updated = await this.casUpdateApiKeyScopes(tx, keyId, actor.orgId, expectedScopes, addScopes);
|
|
63073
63158
|
if (!updated[0]) {
|
|
63074
63159
|
const latest = await tx`SELECT scopes_json FROM api_keys WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL LIMIT 1`;
|
|
63075
63160
|
return { kind: "stale", scopes: latest[0] ? parseJsonArray(latest[0].scopes_json) : current };
|
|
63076
63161
|
}
|
|
63077
63162
|
await tx`
|
|
63078
63163
|
INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, metadata_json)
|
|
63079
|
-
VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes })}::jsonb)
|
|
63164
|
+
VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes: parseJsonArray(updated[0].scopes_json) })}::jsonb)
|
|
63165
|
+
`;
|
|
63166
|
+
return { kind: "updated", scopes: parseJsonArray(updated[0].scopes_json) };
|
|
63167
|
+
});
|
|
63168
|
+
}
|
|
63169
|
+
async casUpdateApiKeyScopes(tx, keyId, orgId, expectedScopes, addScopes) {
|
|
63170
|
+
const current = expectedScopes;
|
|
63171
|
+
const scopes = [...current, ...addScopes.filter((scope2) => !current.includes(scope2))];
|
|
63172
|
+
return tx`
|
|
63173
|
+
UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
|
|
63174
|
+
WHERE id = ${keyId} AND org_id = ${orgId} AND revoked_at IS NULL
|
|
63175
|
+
AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
|
|
63176
|
+
RETURNING scopes_json
|
|
63177
|
+
`;
|
|
63178
|
+
}
|
|
63179
|
+
async enrollPublishScopeByOperator(input) {
|
|
63180
|
+
if (!validOperatorScopeEnrollmentInput(input))
|
|
63181
|
+
return { kind: "invalid" };
|
|
63182
|
+
if (input.expectedScopes.includes("skills:publish"))
|
|
63183
|
+
return { kind: "stale", scopes: input.expectedScopes };
|
|
63184
|
+
return this.sql.begin(async (tx) => {
|
|
63185
|
+
const operation = await tx`
|
|
63186
|
+
SELECT org_id, target_id FROM skills_audit_events
|
|
63187
|
+
WHERE action = ${"api_key_scopes_added"} AND operator_operation_id = ${input.operationId}
|
|
63188
|
+
LIMIT 1
|
|
63189
|
+
`;
|
|
63190
|
+
if (operation[0] && (String(operation[0].org_id) !== input.orgId || String(operation[0].target_id) !== input.keyId))
|
|
63191
|
+
return { kind: "target_mismatch" };
|
|
63192
|
+
const rows = await tx`
|
|
63193
|
+
SELECT id, org_id, name, scopes_json FROM api_keys
|
|
63194
|
+
WHERE id = ${input.keyId} AND revoked_at IS NULL
|
|
63195
|
+
FOR UPDATE
|
|
63196
|
+
`;
|
|
63197
|
+
const row = rows[0];
|
|
63198
|
+
if (!row)
|
|
63199
|
+
return { kind: "not_found" };
|
|
63200
|
+
if (String(row.org_id) !== input.orgId)
|
|
63201
|
+
return { kind: "target_mismatch" };
|
|
63202
|
+
const current = parseJsonArray(row.scopes_json);
|
|
63203
|
+
const prior = await tx`
|
|
63204
|
+
SELECT metadata_json, metadata_json->>'target_manifest_digest' AS manifest_digest FROM skills_audit_events
|
|
63205
|
+
WHERE action = ${"api_key_scopes_added"} AND target_type = ${"api_key"} AND target_id = ${input.keyId}
|
|
63206
|
+
AND operator_operation_id = ${input.operationId}
|
|
63207
|
+
LIMIT 1
|
|
63208
|
+
`;
|
|
63209
|
+
if (prior[0]) {
|
|
63210
|
+
const priorMetadata = typeof prior[0].metadata_json === "string" ? JSON.parse(prior[0].metadata_json) : prior[0].metadata_json;
|
|
63211
|
+
const priorDigest = prior[0].manifest_digest ?? priorMetadata?.target_manifest_digest;
|
|
63212
|
+
if (String(priorDigest) !== input.manifestDigest)
|
|
63213
|
+
return { kind: "target_mismatch" };
|
|
63214
|
+
if (!current.includes("skills:publish"))
|
|
63215
|
+
return { kind: "stale", scopes: current };
|
|
63216
|
+
return { kind: "already_applied", scopes: current };
|
|
63217
|
+
}
|
|
63218
|
+
if (current.length !== input.expectedScopes.length || current.some((scope2, index) => scope2 !== input.expectedScopes[index])) {
|
|
63219
|
+
return { kind: "stale", scopes: current };
|
|
63220
|
+
}
|
|
63221
|
+
const updated = await this.casUpdateApiKeyScopes(tx, input.keyId, input.orgId, input.expectedScopes, ["skills:publish"]);
|
|
63222
|
+
if (!updated[0])
|
|
63223
|
+
return { kind: "stale", scopes: current };
|
|
63224
|
+
const scopes = parseJsonArray(updated[0].scopes_json);
|
|
63225
|
+
await tx`
|
|
63226
|
+
INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, operator_operation_id, metadata_json)
|
|
63227
|
+
VALUES (${input.orgId}, NULL, NULL, ${"api_key_scopes_added"}, ${"api_key"}, ${input.keyId}, ${input.operationId}, ${JSON.stringify({
|
|
63228
|
+
added: ["skills:publish"],
|
|
63229
|
+
scopes,
|
|
63230
|
+
operator_operation_id: input.operationId,
|
|
63231
|
+
operator_job_id: input.operatorJobId,
|
|
63232
|
+
operator_task_arn: input.operatorTaskArn,
|
|
63233
|
+
target_manifest_digest: input.manifestDigest,
|
|
63234
|
+
station_id: input.stationId
|
|
63235
|
+
})}::jsonb)
|
|
63080
63236
|
`;
|
|
63081
63237
|
return { kind: "updated", scopes };
|
|
63082
63238
|
});
|
|
63083
63239
|
}
|
|
63240
|
+
async inspectOperatorScopeTarget(keyId, orgId) {
|
|
63241
|
+
const rows = await this.sql`SELECT org_id, scopes_json FROM api_keys WHERE id = ${keyId} AND revoked_at IS NULL LIMIT 1`;
|
|
63242
|
+
const row = rows[0];
|
|
63243
|
+
if (!row)
|
|
63244
|
+
return { kind: "not_found" };
|
|
63245
|
+
if (String(row.org_id) !== orgId)
|
|
63246
|
+
return { kind: "target_mismatch" };
|
|
63247
|
+
return { kind: "found", scopes: parseJsonArray(row.scopes_json) };
|
|
63248
|
+
}
|
|
63084
63249
|
async withContext(orgId, worker, fn) {
|
|
63085
63250
|
return this.sql.begin(async (tx) => {
|
|
63086
63251
|
await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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, ServerSkillRecord, SkillLifecyclePatch, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.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";
|
|
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;
|
|
@@ -53,6 +53,8 @@ export declare class SqliteSkillsStore implements SkillsProductStore {
|
|
|
53
53
|
ensureBootstrapApiKey(token: string, principal?: Partial<ApiPrincipal>): Promise<void>;
|
|
54
54
|
authenticateApiKeyHash(hash: string): Promise<ApiPrincipal | null>;
|
|
55
55
|
updateApiKeyScopes(actor: ApiPrincipal, keyId: string, expectedScopes: string[], addScopes: string[]): Promise<ApiKeyScopeUpdateResult>;
|
|
56
|
+
enrollPublishScopeByOperator(input: OperatorScopeEnrollmentInput): Promise<OperatorScopeEnrollmentResult>;
|
|
57
|
+
inspectOperatorScopeTarget(keyId: string, orgId: string): Promise<OperatorScopeTargetSnapshot>;
|
|
56
58
|
createRun(input: CreateRunInput): Promise<ServerRunRecord>;
|
|
57
59
|
listRuns(principal: ApiPrincipal, limit: number): Promise<ServerRunRecord[]>;
|
|
58
60
|
getRun(principal: ApiPrincipal, id: string): Promise<ServerRunRecord | null>;
|
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, SkillsProductStore, StoreBackendInfo, UpdateSkillPatch } from "./types.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";
|
|
4
4
|
import { type SkillLifecyclePatch } from "./types.js";
|
|
5
5
|
import { type SqliteStoreOptions } from "./sqlite-store.js";
|
|
6
6
|
export declare function createArtifactId(): string;
|
|
@@ -38,6 +38,7 @@ export declare class MemorySkillsStore implements SkillsProductStore {
|
|
|
38
38
|
private bundles;
|
|
39
39
|
private versions;
|
|
40
40
|
private pins;
|
|
41
|
+
private operatorEnrollments;
|
|
41
42
|
constructor(apiKeys?: Array<{
|
|
42
43
|
token: string;
|
|
43
44
|
principal?: Partial<ApiPrincipal>;
|
|
@@ -46,6 +47,8 @@ export declare class MemorySkillsStore implements SkillsProductStore {
|
|
|
46
47
|
ensureBootstrapApiKey(token: string, principal?: Partial<ApiPrincipal>): Promise<void>;
|
|
47
48
|
authenticateApiKeyHash(hash: string): Promise<ApiPrincipal | null>;
|
|
48
49
|
updateApiKeyScopes(actor: ApiPrincipal, keyId: string, expectedScopes: string[], addScopes: string[]): Promise<ApiKeyScopeUpdateResult>;
|
|
50
|
+
enrollPublishScopeByOperator(input: OperatorScopeEnrollmentInput): Promise<OperatorScopeEnrollmentResult>;
|
|
51
|
+
inspectOperatorScopeTarget(keyId: string, orgId: string): Promise<OperatorScopeTargetSnapshot>;
|
|
49
52
|
createRun(input: CreateRunInput): Promise<ServerRunRecord>;
|
|
50
53
|
listRuns(principal: ApiPrincipal, limit: number): Promise<ServerRunRecord[]>;
|
|
51
54
|
getRun(principal: ApiPrincipal, runId: string): Promise<ServerRunRecord | null>;
|
|
@@ -122,6 +125,9 @@ export declare class PostgresSkillsStore implements SkillsProductStore {
|
|
|
122
125
|
ensureBootstrapApiKey(token: string, principal?: Partial<ApiPrincipal>): Promise<void>;
|
|
123
126
|
authenticateApiKeyHash(hash: string): Promise<ApiPrincipal | null>;
|
|
124
127
|
updateApiKeyScopes(actor: ApiPrincipal, keyId: string, expectedScopes: string[], addScopes: string[]): Promise<ApiKeyScopeUpdateResult>;
|
|
128
|
+
private casUpdateApiKeyScopes;
|
|
129
|
+
enrollPublishScopeByOperator(input: OperatorScopeEnrollmentInput): Promise<OperatorScopeEnrollmentResult>;
|
|
130
|
+
inspectOperatorScopeTarget(keyId: string, orgId: string): Promise<OperatorScopeTargetSnapshot>;
|
|
125
131
|
/**
|
|
126
132
|
* Run a callback under an RLS tenant or worker context, on one pooled
|
|
127
133
|
* connection, for the duration of one transaction only.
|
package/dist/server/types.d.ts
CHANGED
|
@@ -69,6 +69,47 @@ export type ApiKeyScopeUpdateResult = {
|
|
|
69
69
|
kind: "stale";
|
|
70
70
|
scopes: string[];
|
|
71
71
|
};
|
|
72
|
+
/**
|
|
73
|
+
* A maintenance task's provenance is deliberately separate from ApiPrincipal.
|
|
74
|
+
* An ECS task is not a user and must never be represented by impersonating an
|
|
75
|
+
* API key, user id, or role in the audit row.
|
|
76
|
+
*/
|
|
77
|
+
export interface OperatorScopeEnrollmentInput {
|
|
78
|
+
keyId: string;
|
|
79
|
+
stationId: string;
|
|
80
|
+
orgId: string;
|
|
81
|
+
expectedScopes: string[];
|
|
82
|
+
operationId: string;
|
|
83
|
+
manifestDigest: string;
|
|
84
|
+
operatorJobId: string;
|
|
85
|
+
operatorTaskArn: string;
|
|
86
|
+
}
|
|
87
|
+
export declare function validOperatorScopeEnrollmentInput(input: OperatorScopeEnrollmentInput): boolean;
|
|
88
|
+
export declare function validOperatorScopeList(scopes: unknown): scopes is string[];
|
|
89
|
+
export type OperatorScopeEnrollmentResult = {
|
|
90
|
+
kind: "updated";
|
|
91
|
+
scopes: string[];
|
|
92
|
+
} | {
|
|
93
|
+
kind: "already_applied";
|
|
94
|
+
scopes: string[];
|
|
95
|
+
} | {
|
|
96
|
+
kind: "not_found";
|
|
97
|
+
} | {
|
|
98
|
+
kind: "target_mismatch";
|
|
99
|
+
} | {
|
|
100
|
+
kind: "invalid";
|
|
101
|
+
} | {
|
|
102
|
+
kind: "stale";
|
|
103
|
+
scopes: string[];
|
|
104
|
+
};
|
|
105
|
+
export type OperatorScopeTargetSnapshot = {
|
|
106
|
+
kind: "not_found";
|
|
107
|
+
} | {
|
|
108
|
+
kind: "target_mismatch";
|
|
109
|
+
} | {
|
|
110
|
+
kind: "found";
|
|
111
|
+
scopes: string[];
|
|
112
|
+
};
|
|
72
113
|
export interface ServerRunRecord {
|
|
73
114
|
id: string;
|
|
74
115
|
orgId: string;
|
|
@@ -339,6 +380,9 @@ export interface SkillsProductStore {
|
|
|
339
380
|
authenticateApiKeyHash(hash: string): Promise<ApiPrincipal | null>;
|
|
340
381
|
/** Guarded metadata-only scope admission for an existing key. */
|
|
341
382
|
updateApiKeyScopes?(actor: ApiPrincipal, keyId: string, expectedScopes: string[], addScopes: string[]): Promise<ApiKeyScopeUpdateResult>;
|
|
383
|
+
/** Add only skills:publish to an existing key from a protected maintenance task. */
|
|
384
|
+
enrollPublishScopeByOperator?(input: OperatorScopeEnrollmentInput): Promise<OperatorScopeEnrollmentResult>;
|
|
385
|
+
inspectOperatorScopeTarget?(keyId: string, orgId: string): Promise<OperatorScopeTargetSnapshot>;
|
|
342
386
|
ensureBootstrapApiKey?(token: string, principal?: Partial<ApiPrincipal>): Promise<void>;
|
|
343
387
|
createRun(input: CreateRunInput): Promise<ServerRunRecord>;
|
|
344
388
|
listRuns(principal: ApiPrincipal, limit: number): Promise<ServerRunRecord[]>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
ALTER TABLE skills_audit_events ADD COLUMN operator_operation_id text;
|
|
2
|
+
|
|
3
|
+
-- One operation may be retried, but it must produce one immutable receipt per
|
|
4
|
+
-- tenant/key. The partial expression index makes that property atomic under
|
|
5
|
+
-- concurrent maintenance tasks; application pre-reads are not the guard.
|
|
6
|
+
CREATE UNIQUE INDEX IF NOT EXISTS skills_operator_scope_receipt_idx
|
|
7
|
+
ON skills_audit_events (operator_operation_id)
|
|
8
|
+
WHERE action = 'api_key_scopes_added'
|
|
9
|
+
AND operator_operation_id IS NOT NULL;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
ALTER TABLE skills_audit_events ADD COLUMN operator_operation_id text;
|
|
2
|
+
|
|
3
|
+
-- SQLite equivalent of the PostgreSQL operation-receipt uniqueness fence.
|
|
4
|
+
CREATE UNIQUE INDEX IF NOT EXISTS skills_operator_scope_receipt_idx
|
|
5
|
+
ON skills_audit_events (operator_operation_id)
|
|
6
|
+
WHERE action = 'api_key_scopes_added'
|
|
7
|
+
AND operator_operation_id IS NOT NULL;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/skills",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"description": "Skills library for AI coding agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
"skills-serve": "bin/server.js",
|
|
10
10
|
"skills-server": "bin/server.js",
|
|
11
11
|
"skills-worker": "bin/worker.js",
|
|
12
|
+
"skills-maintenance": "bin/maintenance.js",
|
|
12
13
|
"skills-migrate": "bin/migrate.js"
|
|
13
14
|
},
|
|
14
15
|
"exports": {
|
|
@@ -49,7 +50,7 @@
|
|
|
49
50
|
"types": "./dist/index.d.ts",
|
|
50
51
|
"scripts": {
|
|
51
52
|
"clean": "rm -rf bin/ dist/",
|
|
52
|
-
"build": "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
53
|
+
"build": "bun run clean && bun build ./src/cli/index.tsx --outdir ./bin --target bun && bun build ./src/mcp/index.ts --outfile ./bin/mcp.js --target bun && bun build ./src/server/index.ts --outfile ./bin/server.js --target bun && bun build ./src/server/worker.ts --outfile ./bin/worker.js --target bun && bun build ./src/server/migrate.ts --outfile ./bin/migrate.js --target bun && bun build ./src/server/maintenance.ts --outfile ./bin/maintenance.js --target bun && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
53
54
|
"build:js": "rm -rf dist && bun build ./src/index.ts ./src/storage.ts ./src/sdk/index.ts ./src/admin-contract.ts --outdir ./dist --target bun && tsc --emitDeclarationOnly --declaration --outDir dist",
|
|
54
55
|
"test": "bun test --timeout 30000",
|
|
55
56
|
"dev": "bun run ./src/cli/index.tsx",
|