@hasna/skills 0.8.12 → 0.9.2
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/README.md +21 -5
- package/bin/index.js +271 -19
- package/bin/mcp.js +248 -154
- package/bin/migrate.js +100 -10
- package/bin/server.js +596 -51
- package/bin/worker.js +283 -20
- package/dist/cli/commands/registry.d.ts +1 -0
- package/dist/index.js +122 -13
- package/dist/lib/agent-integration.d.ts +26 -0
- package/dist/lib/cloud-executions.d.ts +20 -0
- package/dist/lib/registry-types.d.ts +3 -0
- package/dist/lib/remote-client.d.ts +11 -0
- package/dist/lib/revision.d.ts +3 -0
- package/dist/sdk/execution/admission.d.ts +2 -1
- package/dist/sdk/execution/types.d.ts +9 -0
- package/dist/sdk/index.d.ts +1 -0
- package/dist/sdk/index.js +1301 -502
- package/dist/server/runtime-policy.d.ts +4 -2
- package/dist/server/runtime-pure-contract.d.ts +51 -0
- package/dist/server/runtime-store.d.ts +4 -0
- package/dist/server/runtime-worker.d.ts +2 -1
- package/dist/server/selection-store.d.ts +9 -1
- package/dist/server/skills-api.d.ts +2 -1
- package/dist/server/sqlite-store.d.ts +4 -1
- package/dist/server/store.d.ts +6 -1
- package/dist/server/types.d.ts +27 -0
- package/migrations/postgres/0010_skill_archive_lifecycle.sql +5 -0
- package/migrations/sqlite/0010_skill_archive_lifecycle.sql +5 -0
- package/package.json +1 -1
package/bin/worker.js
CHANGED
|
@@ -24125,7 +24125,7 @@ import { randomUUID as randomUUID6 } from "crypto";
|
|
|
24125
24125
|
// package.json
|
|
24126
24126
|
var package_default = {
|
|
24127
24127
|
name: "@hasna/skills",
|
|
24128
|
-
version: "0.
|
|
24128
|
+
version: "0.9.2",
|
|
24129
24129
|
description: "Skills library for AI coding agents",
|
|
24130
24130
|
type: "module",
|
|
24131
24131
|
bin: {
|
|
@@ -34682,6 +34682,9 @@ class MemorySkillSelectionStore {
|
|
|
34682
34682
|
async getProfile(p2, id) {
|
|
34683
34683
|
return structuredClone(this.profiles.get(this.key(p2.orgId, id)) ?? null);
|
|
34684
34684
|
}
|
|
34685
|
+
async profilesReferencingSkill(p2, slug) {
|
|
34686
|
+
return [...this.profiles.values()].filter((profile2) => profile2.workspaceId === p2.orgId && profile2.selections.some((selection) => selection.slug === slug)).map((profile2) => profile2.id).sort();
|
|
34687
|
+
}
|
|
34685
34688
|
async saveProfile(p2, id, selected, expected) {
|
|
34686
34689
|
const key = this.key(p2.orgId, id), previous = this.profiles.get(key);
|
|
34687
34690
|
if ((previous?.revision ?? null) !== expected)
|
|
@@ -34719,14 +34722,25 @@ class SqliteSkillSelectionStore {
|
|
|
34719
34722
|
constructor(db) {
|
|
34720
34723
|
this.db = db;
|
|
34721
34724
|
}
|
|
34725
|
+
async profilesReferencingSkill(p2, slug) {
|
|
34726
|
+
const rows = this.db.query("SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=?").all(p2.orgId);
|
|
34727
|
+
return rows.filter((row) => selections(row.selections_json).some((selection) => selection.slug === slug)).map((row) => String(row.profile_id)).sort();
|
|
34728
|
+
}
|
|
34722
34729
|
async getProfile(p2, id) {
|
|
34723
34730
|
const row = this.db.query("SELECT * FROM skills_profiles WHERE org_id=? AND profile_id=?").get(p2.orgId, id);
|
|
34724
34731
|
return row ? profile(row) : null;
|
|
34725
34732
|
}
|
|
34726
34733
|
async saveProfile(p2, id, selected, expected) {
|
|
34727
|
-
|
|
34728
|
-
|
|
34729
|
-
|
|
34734
|
+
return this.db.transaction(() => {
|
|
34735
|
+
for (const selection of [...selected].sort((a3, b3) => a3.slug.localeCompare(b3.slug))) {
|
|
34736
|
+
const skill = this.db.query("SELECT lifecycle FROM skills_registry WHERE org_id=? AND slug=?").get(p2.orgId, selection.slug);
|
|
34737
|
+
if (skill?.lifecycle === "archived")
|
|
34738
|
+
return null;
|
|
34739
|
+
}
|
|
34740
|
+
const revision = randomUUID2(), timestamp = new Date().toISOString();
|
|
34741
|
+
const row = expected === null ? this.db.query("INSERT INTO skills_profiles(org_id,profile_id,revision,selections_json,updated_at) VALUES(?,?,?,?,?) ON CONFLICT(org_id,profile_id) DO NOTHING RETURNING *").get(p2.orgId, id, revision, JSON.stringify(selected), timestamp) : this.db.query("UPDATE skills_profiles SET revision=?,selections_json=?,updated_at=? WHERE org_id=? AND profile_id=? AND revision=? RETURNING *").get(revision, JSON.stringify(selected), timestamp, p2.orgId, id, expected);
|
|
34742
|
+
return row ? profile(row) : null;
|
|
34743
|
+
}).immediate();
|
|
34730
34744
|
}
|
|
34731
34745
|
async getStationState(p2, id) {
|
|
34732
34746
|
const row = this.db.query("SELECT * FROM skills_station_state WHERE org_id=? AND actor_id=? AND station_id=?").get(p2.orgId, p2.userId, id);
|
|
@@ -34745,14 +34759,25 @@ class PostgresSkillSelectionStore {
|
|
|
34745
34759
|
constructor(sql) {
|
|
34746
34760
|
this.sql = sql;
|
|
34747
34761
|
}
|
|
34762
|
+
async profilesReferencingSkill(p2, slug) {
|
|
34763
|
+
const rows = await this.sql`SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=${p2.orgId}`;
|
|
34764
|
+
return rows.filter((row) => selections(row.selections_json).some((selection) => selection.slug === slug)).map((row) => String(row.profile_id)).sort();
|
|
34765
|
+
}
|
|
34748
34766
|
async getProfile(p2, id) {
|
|
34749
34767
|
const rows = await this.sql`SELECT * FROM skills_profiles WHERE org_id=${p2.orgId} AND profile_id=${id}`;
|
|
34750
34768
|
return rows[0] ? profile(rows[0]) : null;
|
|
34751
34769
|
}
|
|
34752
34770
|
async saveProfile(p2, id, selected, expected) {
|
|
34753
|
-
|
|
34754
|
-
|
|
34755
|
-
|
|
34771
|
+
return this.sql.begin(async (tx) => {
|
|
34772
|
+
for (const selection of [...selected].sort((a3, b3) => a3.slug.localeCompare(b3.slug))) {
|
|
34773
|
+
const rows2 = await tx`SELECT lifecycle FROM skills_registry WHERE org_id=${p2.orgId} AND slug=${selection.slug} FOR UPDATE`;
|
|
34774
|
+
if (rows2[0]?.lifecycle === "archived")
|
|
34775
|
+
return null;
|
|
34776
|
+
}
|
|
34777
|
+
const revision = randomUUID2(), timestamp = new Date().toISOString();
|
|
34778
|
+
const rows = expected === null ? await tx`INSERT INTO skills_profiles(org_id,profile_id,revision,selections_json,updated_at) VALUES(${p2.orgId},${id},${revision},${JSON.stringify(selected)}::jsonb,${timestamp}) ON CONFLICT(org_id,profile_id) DO NOTHING RETURNING *` : await tx`UPDATE skills_profiles SET revision=${revision},selections_json=${JSON.stringify(selected)}::jsonb,updated_at=${timestamp} WHERE org_id=${p2.orgId} AND profile_id=${id} AND revision=${expected} RETURNING *`;
|
|
34779
|
+
return rows[0] ? profile(rows[0]) : null;
|
|
34780
|
+
});
|
|
34756
34781
|
}
|
|
34757
34782
|
async getStationState(p2, id) {
|
|
34758
34783
|
const rows = await this.sql`SELECT * FROM skills_station_state WHERE org_id=${p2.orgId} AND actor_id=${p2.userId} AND station_id=${id}`;
|
|
@@ -34909,7 +34934,11 @@ function rowToSkill(row) {
|
|
|
34909
34934
|
revisionId: String(row.revision_id ?? ""),
|
|
34910
34935
|
revisionNumber: Number(row.revision_number ?? 0),
|
|
34911
34936
|
...row.tombstoned_at ? { tombstonedAt: dateString(row.tombstoned_at) } : {},
|
|
34912
|
-
...row.tombstone_purge_after ? { tombstonePurgeAfter: dateString(row.tombstone_purge_after) } : {}
|
|
34937
|
+
...row.tombstone_purge_after ? { tombstonePurgeAfter: dateString(row.tombstone_purge_after) } : {},
|
|
34938
|
+
lifecycle: row.lifecycle === "archived" ? "archived" : "active",
|
|
34939
|
+
...row.archived_at ? { archivedAt: dateString(row.archived_at) } : {},
|
|
34940
|
+
...row.archive_reason ? { archiveReason: String(row.archive_reason) } : {},
|
|
34941
|
+
...row.replacement_slug ? { replacementSlug: String(row.replacement_slug) } : {}
|
|
34913
34942
|
};
|
|
34914
34943
|
}
|
|
34915
34944
|
function rowToPin(row) {
|
|
@@ -35028,10 +35057,21 @@ class SkillVersionExistsError extends Error {
|
|
|
35028
35057
|
}
|
|
35029
35058
|
}
|
|
35030
35059
|
|
|
35060
|
+
class SkillLifecycleConflictError extends Error {
|
|
35061
|
+
slug;
|
|
35062
|
+
profiles;
|
|
35063
|
+
constructor(slug, profiles) {
|
|
35064
|
+
super(`cannot archive '${slug}' while active profiles reference it: ${profiles.join(", ")}`);
|
|
35065
|
+
this.slug = slug;
|
|
35066
|
+
this.profiles = profiles;
|
|
35067
|
+
this.name = "SkillLifecycleConflictError";
|
|
35068
|
+
}
|
|
35069
|
+
}
|
|
35070
|
+
|
|
35031
35071
|
// src/lib/revision.ts
|
|
35032
35072
|
import { createHash as createHash4 } from "crypto";
|
|
35033
35073
|
function revisionIdOf(content) {
|
|
35034
|
-
const
|
|
35074
|
+
const canonicalContent = {
|
|
35035
35075
|
slug: content.slug,
|
|
35036
35076
|
displayName: content.displayName,
|
|
35037
35077
|
description: content.description,
|
|
@@ -35043,7 +35083,13 @@ function revisionIdOf(content) {
|
|
|
35043
35083
|
skillMd: content.skillMd ?? null,
|
|
35044
35084
|
bundleSha256: content.bundleSha256 ?? null,
|
|
35045
35085
|
bundleByteSize: content.bundleByteSize ?? null
|
|
35046
|
-
}
|
|
35086
|
+
};
|
|
35087
|
+
if (content.lifecycle === "archived" || content.archiveReason !== undefined || content.replacementSlug !== undefined) {
|
|
35088
|
+
canonicalContent.lifecycle = content.lifecycle ?? "active";
|
|
35089
|
+
canonicalContent.archiveReason = content.archiveReason ?? null;
|
|
35090
|
+
canonicalContent.replacementSlug = content.replacementSlug ?? null;
|
|
35091
|
+
}
|
|
35092
|
+
const canonical = JSON.stringify(canonicalContent);
|
|
35047
35093
|
return createHash4("sha256").update(canonical).digest("hex");
|
|
35048
35094
|
}
|
|
35049
35095
|
function revisionIdOfRecord(record) {
|
|
@@ -35146,6 +35192,28 @@ class SqliteSkillsStore {
|
|
|
35146
35192
|
scopes: parseScopes(row.scopes_json)
|
|
35147
35193
|
};
|
|
35148
35194
|
}
|
|
35195
|
+
async updateApiKeyScopes(actor, keyId, expectedScopes, addScopes) {
|
|
35196
|
+
const row = this.get("SELECT scopes_json FROM api_keys WHERE id = ? AND org_id = ? AND revoked_at IS NULL LIMIT 1", [keyId, actor.orgId]);
|
|
35197
|
+
if (!row)
|
|
35198
|
+
return { kind: "not_found" };
|
|
35199
|
+
const current = parseScopes(row.scopes_json);
|
|
35200
|
+
if (current.length !== expectedScopes.length || current.some((scope, index) => scope !== expectedScopes[index])) {
|
|
35201
|
+
return { kind: "stale", scopes: current };
|
|
35202
|
+
}
|
|
35203
|
+
const scopes = [...current, ...addScopes.filter((scope) => !current.includes(scope))];
|
|
35204
|
+
const tx = this.db.transaction(() => {
|
|
35205
|
+
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), keyId, actor.orgId, JSON.stringify(expectedScopes)]);
|
|
35206
|
+
if (changed.changes !== 1)
|
|
35207
|
+
return false;
|
|
35208
|
+
this.db.run("INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, metadata_json) VALUES (?, ?, ?, ?, ?, ?, ?)", [actor.orgId, actor.userId, actor.apiKeyId, "api_key_scopes_added", "api_key", keyId, JSON.stringify({ added: addScopes, scopes })]);
|
|
35209
|
+
return true;
|
|
35210
|
+
});
|
|
35211
|
+
if (!tx()) {
|
|
35212
|
+
const latest = this.get("SELECT scopes_json FROM api_keys WHERE id = ? AND org_id = ? AND revoked_at IS NULL LIMIT 1", [keyId, actor.orgId]);
|
|
35213
|
+
return { kind: "stale", scopes: latest ? parseScopes(latest.scopes_json) : current };
|
|
35214
|
+
}
|
|
35215
|
+
return { kind: "updated", scopes };
|
|
35216
|
+
}
|
|
35149
35217
|
async createRun(input) {
|
|
35150
35218
|
if (input.idempotencyKey) {
|
|
35151
35219
|
const existing = this.get("SELECT * FROM skills_runs WHERE org_id = ? AND idempotency_key = ? LIMIT 1", [input.principal.orgId, input.idempotencyKey]);
|
|
@@ -35467,6 +35535,39 @@ class SqliteSkillsStore {
|
|
|
35467
35535
|
const row = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
|
|
35468
35536
|
return row ? rowToSkill(row) : null;
|
|
35469
35537
|
}
|
|
35538
|
+
async setSkillLifecycle(principal, slug, patch, expectedRevisionId) {
|
|
35539
|
+
return this.db.transaction(() => {
|
|
35540
|
+
const rowCurrent = this.get("SELECT * FROM skills_registry WHERE org_id=? AND slug=? LIMIT 1", [principal.orgId, slug]);
|
|
35541
|
+
const current = rowCurrent ? rowToSkill(rowCurrent) : null;
|
|
35542
|
+
if (!current || current.tombstonedAt)
|
|
35543
|
+
return null;
|
|
35544
|
+
if (expectedRevisionId !== current.revisionId)
|
|
35545
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
35546
|
+
if (patch.lifecycle === "archived" && current.lifecycle !== "archived") {
|
|
35547
|
+
const profiles = this.db.query("SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=?").all(principal.orgId);
|
|
35548
|
+
const referencing = profiles.filter((profile2) => {
|
|
35549
|
+
const selections2 = typeof profile2.selections_json === "string" ? JSON.parse(profile2.selections_json) : profile2.selections_json;
|
|
35550
|
+
return Array.isArray(selections2) && selections2.some((selection) => selection?.slug === slug);
|
|
35551
|
+
}).map((profile2) => String(profile2.profile_id)).sort();
|
|
35552
|
+
if (referencing.length)
|
|
35553
|
+
throw new SkillLifecycleConflictError(slug, referencing);
|
|
35554
|
+
}
|
|
35555
|
+
const next2 = { ...current, lifecycle: patch.lifecycle, updatedAt: nowIso(), ...patch.lifecycle === "archived" ? { archivedAt: current.archivedAt ?? nowIso(), ...patch.reason ? { archiveReason: patch.reason } : {}, ...patch.replacementSlug ? { replacementSlug: patch.replacementSlug } : {} } : {} };
|
|
35556
|
+
if (patch.lifecycle === "active") {
|
|
35557
|
+
delete next2.archivedAt;
|
|
35558
|
+
delete next2.archiveReason;
|
|
35559
|
+
delete next2.replacementSlug;
|
|
35560
|
+
}
|
|
35561
|
+
const row = this.get("UPDATE skills_registry SET lifecycle=?, archived_at=?, archive_reason=?, replacement_slug=?, revision_id=?, revision_number=revision_number+1, updated_at=? WHERE org_id=? AND slug=? AND tombstoned_at IS NULL AND revision_id=? RETURNING *", [next2.lifecycle, next2.archivedAt ?? null, next2.archiveReason ?? null, next2.replacementSlug ?? null, revisionIdOfRecord(next2), next2.updatedAt, principal.orgId, slug, current.revisionId]);
|
|
35562
|
+
if (!row)
|
|
35563
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, this.getSkillSync(principal, slug)?.revisionId ?? null);
|
|
35564
|
+
return rowToSkill(row);
|
|
35565
|
+
}).immediate();
|
|
35566
|
+
}
|
|
35567
|
+
getSkillSync(principal, slug) {
|
|
35568
|
+
const row = this.get("SELECT * FROM skills_registry WHERE org_id=? AND slug=? LIMIT 1", [principal.orgId, slug]);
|
|
35569
|
+
return row ? rowToSkill(row) : null;
|
|
35570
|
+
}
|
|
35470
35571
|
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
35471
35572
|
const current = await this.getSkill(principal, slug);
|
|
35472
35573
|
if (!current || current.tombstonedAt)
|
|
@@ -35579,13 +35680,13 @@ class SqliteSkillsStore {
|
|
|
35579
35680
|
}
|
|
35580
35681
|
async listTags(principal) {
|
|
35581
35682
|
await this.purgeExpiredTombstones(principal);
|
|
35582
|
-
return this.all("SELECT DISTINCT tag FROM skills_tags WHERE org_id = ? ORDER BY tag ASC", [principal.orgId]).map((row) => String(row.tag));
|
|
35683
|
+
return this.all("SELECT DISTINCT t.tag FROM skills_tags t JOIN skills_registry s ON s.org_id=t.org_id AND s.slug=t.slug WHERE t.org_id = ? AND s.lifecycle='active' ORDER BY t.tag ASC", [principal.orgId]).map((row) => String(row.tag));
|
|
35583
35684
|
}
|
|
35584
35685
|
async listSkillsByTag(principal, tag) {
|
|
35585
35686
|
await this.purgeExpiredTombstones(principal);
|
|
35586
35687
|
return this.all(`SELECT s.* FROM skills_registry s
|
|
35587
35688
|
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
35588
|
-
WHERE t.org_id = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
35689
|
+
WHERE t.org_id = ? AND t.tag = ? AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
|
|
35589
35690
|
ORDER BY s.slug ASC`, [principal.orgId, tag]).map(rowToSkill);
|
|
35590
35691
|
}
|
|
35591
35692
|
async listPinsByTag(principal, tag) {
|
|
@@ -35593,7 +35694,7 @@ class SqliteSkillsStore {
|
|
|
35593
35694
|
return this.all(`SELECT p.* FROM skills_pins p
|
|
35594
35695
|
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
35595
35696
|
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
35596
|
-
WHERE p.org_id = ? AND p.principal = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
35697
|
+
WHERE p.org_id = ? AND p.principal = ? AND t.tag = ? AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
|
|
35597
35698
|
ORDER BY p.slug ASC`, [principal.orgId, principal.apiKeyId, tag]).map(rowToPin);
|
|
35598
35699
|
}
|
|
35599
35700
|
async listPublishedSlugs(principal) {
|
|
@@ -35962,6 +36063,27 @@ var MAX_MANIFEST_BYTES = MAX_SKILL_MD_BYTES + 64000;
|
|
|
35962
36063
|
var ALLOWED_PUBLISH_PARTS = new Set(["manifest", "bundle"]);
|
|
35963
36064
|
var MAX_VERSION_MANIFEST_BYTES = 512 * 1024;
|
|
35964
36065
|
|
|
36066
|
+
// src/sdk/execution/types.ts
|
|
36067
|
+
function canonicalJson(value) {
|
|
36068
|
+
return JSON.stringify(sortKeys(value));
|
|
36069
|
+
}
|
|
36070
|
+
function sortKeys(value) {
|
|
36071
|
+
if (Array.isArray(value))
|
|
36072
|
+
return value.map(sortKeys);
|
|
36073
|
+
if (value !== null && typeof value === "object") {
|
|
36074
|
+
const record = value;
|
|
36075
|
+
const sorted = Object.create(null);
|
|
36076
|
+
for (const key of Object.keys(record).sort()) {
|
|
36077
|
+
sorted[key] = sortKeys(record[key]);
|
|
36078
|
+
}
|
|
36079
|
+
return sorted;
|
|
36080
|
+
}
|
|
36081
|
+
return value;
|
|
36082
|
+
}
|
|
36083
|
+
|
|
36084
|
+
// src/sdk/execution/admission.ts
|
|
36085
|
+
import { createHash as createHash5 } from "crypto";
|
|
36086
|
+
|
|
35965
36087
|
// src/sdk/execution/storage.ts
|
|
35966
36088
|
class MemoryRunExecutionStore {
|
|
35967
36089
|
durable = false;
|
|
@@ -36142,10 +36264,38 @@ var DEFAULT_RUN_LIMITS = {
|
|
|
36142
36264
|
maxArtifactsBytes: 10 * 1024 * 1024,
|
|
36143
36265
|
maxConcurrency: 1
|
|
36144
36266
|
};
|
|
36267
|
+
function digestInput(input) {
|
|
36268
|
+
return createHash5("sha256").update(canonicalJson(input)).digest("hex");
|
|
36269
|
+
}
|
|
36145
36270
|
|
|
36146
36271
|
// src/sdk/execution/dispatchers/ecs.ts
|
|
36147
36272
|
var LIVE_TASK_STATUSES = new Set(["PROVISIONING", "PENDING", "ACTIVATING", "RUNNING", "DEACTIVATING", "STOPPING", "DEPROVISIONING"]);
|
|
36148
36273
|
|
|
36274
|
+
// src/server/runtime-pure-contract.ts
|
|
36275
|
+
var PURE_DESCRIPTOR = Object.freeze({
|
|
36276
|
+
id: "regex-test.v1",
|
|
36277
|
+
runtime: "bun",
|
|
36278
|
+
adapter: "skills-input-json.v1",
|
|
36279
|
+
input: Object.freeze({ patternBytes: 512, textBytes: 4096, totalBytes: 8192, flags: "dgimsuvy" }),
|
|
36280
|
+
output: "regex-matches.v1",
|
|
36281
|
+
secrets: "none",
|
|
36282
|
+
egress: "deny",
|
|
36283
|
+
maxDurationMs: 5000,
|
|
36284
|
+
maxMemoryMb: 512,
|
|
36285
|
+
maxCpuUnits: 256,
|
|
36286
|
+
stdoutBytes: 16384,
|
|
36287
|
+
stderrBytes: 8192,
|
|
36288
|
+
artifactsBytes: 0
|
|
36289
|
+
});
|
|
36290
|
+
var PURE_DESCRIPTOR_DIGEST = digestInput(PURE_DESCRIPTOR);
|
|
36291
|
+
var PURE_LIMITS = Object.freeze({
|
|
36292
|
+
maxDurationMs: 5000,
|
|
36293
|
+
maxMemoryMb: 512,
|
|
36294
|
+
maxCpuUnits: 256,
|
|
36295
|
+
maxArtifactsBytes: 0,
|
|
36296
|
+
maxConcurrency: 1
|
|
36297
|
+
});
|
|
36298
|
+
|
|
36149
36299
|
// src/server/config.ts
|
|
36150
36300
|
var DATABASE_URL_ENV = "HASNA_SKILLS_DATABASE_URL";
|
|
36151
36301
|
var SKILLS_ENV_NAMESPACE = "SKILLS";
|
|
@@ -36292,6 +36442,20 @@ class MemorySkillsStore {
|
|
|
36292
36442
|
async authenticateApiKeyHash(hash) {
|
|
36293
36443
|
return this.apiKeys.get(hash) ?? null;
|
|
36294
36444
|
}
|
|
36445
|
+
async updateApiKeyScopes(actor, keyId, expectedScopes, addScopes) {
|
|
36446
|
+
for (const [hash, target] of this.apiKeys) {
|
|
36447
|
+
if (target.apiKeyId !== keyId || target.orgId !== actor.orgId)
|
|
36448
|
+
continue;
|
|
36449
|
+
if (target.scopes.length !== expectedScopes.length || target.scopes.some((scope, index) => scope !== expectedScopes[index])) {
|
|
36450
|
+
return { kind: "stale", scopes: [...target.scopes] };
|
|
36451
|
+
}
|
|
36452
|
+
const scopes = [...target.scopes, ...addScopes.filter((scope) => !target.scopes.includes(scope))];
|
|
36453
|
+
const updated = { ...target, scopes };
|
|
36454
|
+
this.apiKeys.set(hash, updated);
|
|
36455
|
+
return { kind: "updated", scopes };
|
|
36456
|
+
}
|
|
36457
|
+
return { kind: "not_found" };
|
|
36458
|
+
}
|
|
36295
36459
|
async createRun(input) {
|
|
36296
36460
|
const idemKey = input.idempotencyKey?.trim();
|
|
36297
36461
|
const idemMapKey = idemKey ? `${input.principal.orgId}:${idemKey}` : undefined;
|
|
@@ -36421,7 +36585,11 @@ class MemorySkillsStore {
|
|
|
36421
36585
|
createdAt: previous?.createdAt ?? now,
|
|
36422
36586
|
updatedAt: now,
|
|
36423
36587
|
revisionId: revisionIdOfRecord(recordFieldsOf(input, carriedBundle, carriedSkillMd)),
|
|
36424
|
-
revisionNumber: (previous?.revisionNumber ?? 0) + 1
|
|
36588
|
+
revisionNumber: (previous?.revisionNumber ?? 0) + 1,
|
|
36589
|
+
lifecycle: previous?.lifecycle ?? "active",
|
|
36590
|
+
...previous?.archivedAt ? { archivedAt: previous.archivedAt } : {},
|
|
36591
|
+
...previous?.archiveReason ? { archiveReason: previous.archiveReason } : {},
|
|
36592
|
+
...previous?.replacementSlug ? { replacementSlug: previous.replacementSlug } : {}
|
|
36425
36593
|
};
|
|
36426
36594
|
this.skills.set(key, record);
|
|
36427
36595
|
if (input.version && versionSha && !this.versions.has(versionKey(input.principal.orgId, input.slug, input.version))) {
|
|
@@ -36457,6 +36625,33 @@ class MemorySkillsStore {
|
|
|
36457
36625
|
const skill = this.skills.get(skillKey(principal.orgId, slug));
|
|
36458
36626
|
return skill && skill.orgId === principal.orgId ? skill : null;
|
|
36459
36627
|
}
|
|
36628
|
+
async setSkillLifecycle(principal, slug, patch, expectedRevisionId) {
|
|
36629
|
+
const current = await this.getSkill(principal, slug);
|
|
36630
|
+
if (!current || current.tombstonedAt)
|
|
36631
|
+
return null;
|
|
36632
|
+
if (expectedRevisionId !== current.revisionId)
|
|
36633
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
36634
|
+
if (patch.lifecycle === "archived" && current.lifecycle !== "archived") {
|
|
36635
|
+
const profiles = await this.selectionStore.profilesReferencingSkill(principal, slug);
|
|
36636
|
+
if (profiles.length)
|
|
36637
|
+
throw new SkillLifecycleConflictError(slug, profiles);
|
|
36638
|
+
}
|
|
36639
|
+
const next2 = {
|
|
36640
|
+
...current,
|
|
36641
|
+
lifecycle: patch.lifecycle,
|
|
36642
|
+
updatedAt: nowIso(),
|
|
36643
|
+
revisionId: revisionIdOfRecord({ ...current, lifecycle: patch.lifecycle, archiveReason: patch.reason, replacementSlug: patch.replacementSlug }),
|
|
36644
|
+
revisionNumber: current.revisionNumber + 1,
|
|
36645
|
+
...patch.lifecycle === "archived" ? { archivedAt: current.archivedAt ?? nowIso(), ...patch.reason ? { archiveReason: patch.reason } : {}, ...patch.replacementSlug ? { replacementSlug: patch.replacementSlug } : {} } : {}
|
|
36646
|
+
};
|
|
36647
|
+
if (patch.lifecycle === "active") {
|
|
36648
|
+
delete next2.archivedAt;
|
|
36649
|
+
delete next2.archiveReason;
|
|
36650
|
+
delete next2.replacementSlug;
|
|
36651
|
+
}
|
|
36652
|
+
this.skills.set(skillKey(principal.orgId, slug), next2);
|
|
36653
|
+
return next2;
|
|
36654
|
+
}
|
|
36460
36655
|
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
36461
36656
|
const current = await this.getSkill(principal, slug);
|
|
36462
36657
|
if (!current || current.tombstonedAt)
|
|
@@ -36527,7 +36722,7 @@ class MemorySkillsStore {
|
|
|
36527
36722
|
await this.purgeExpiredTombstones(principal);
|
|
36528
36723
|
const tags = new Set;
|
|
36529
36724
|
for (const skill of this.skills.values()) {
|
|
36530
|
-
if (skill.orgId !== principal.orgId)
|
|
36725
|
+
if (skill.orgId !== principal.orgId || skill.lifecycle !== "active")
|
|
36531
36726
|
continue;
|
|
36532
36727
|
for (const tag of skill.tags) {
|
|
36533
36728
|
if (tag.trim())
|
|
@@ -36538,13 +36733,13 @@ class MemorySkillsStore {
|
|
|
36538
36733
|
}
|
|
36539
36734
|
async listSkillsByTag(principal, tag) {
|
|
36540
36735
|
await this.purgeExpiredTombstones(principal);
|
|
36541
|
-
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag)).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
36736
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt && skill.lifecycle === "active" && skill.tags.includes(tag)).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
36542
36737
|
}
|
|
36543
36738
|
async listPinsByTag(principal, tag) {
|
|
36544
36739
|
await this.purgeExpiredTombstones(principal);
|
|
36545
36740
|
const taggedSlugs = new Set;
|
|
36546
36741
|
for (const skill of this.skills.values()) {
|
|
36547
|
-
if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag))
|
|
36742
|
+
if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.lifecycle === "active" && skill.tags.includes(tag))
|
|
36548
36743
|
taggedSlugs.add(skill.slug);
|
|
36549
36744
|
}
|
|
36550
36745
|
return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId && taggedSlugs.has(pin.slug)).sort((a3, b3) => a3.slug.localeCompare(b3.slug));
|
|
@@ -36661,6 +36856,38 @@ class PostgresSkillsStore {
|
|
|
36661
36856
|
scopes: parseJsonArray(row.scopes_json)
|
|
36662
36857
|
};
|
|
36663
36858
|
}
|
|
36859
|
+
async updateApiKeyScopes(actor, keyId, expectedScopes, addScopes) {
|
|
36860
|
+
return this.sql.begin(async (tx) => {
|
|
36861
|
+
const rows = await tx`
|
|
36862
|
+
SELECT scopes_json FROM api_keys
|
|
36863
|
+
WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL
|
|
36864
|
+
LIMIT 1
|
|
36865
|
+
`;
|
|
36866
|
+
const row = rows[0];
|
|
36867
|
+
if (!row)
|
|
36868
|
+
return { kind: "not_found" };
|
|
36869
|
+
const current = parseJsonArray(row.scopes_json);
|
|
36870
|
+
if (current.length !== expectedScopes.length || current.some((scope, index) => scope !== expectedScopes[index])) {
|
|
36871
|
+
return { kind: "stale", scopes: current };
|
|
36872
|
+
}
|
|
36873
|
+
const scopes = [...current, ...addScopes.filter((scope) => !current.includes(scope))];
|
|
36874
|
+
const updated = await tx`
|
|
36875
|
+
UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
|
|
36876
|
+
WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL
|
|
36877
|
+
AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
|
|
36878
|
+
RETURNING scopes_json
|
|
36879
|
+
`;
|
|
36880
|
+
if (!updated[0]) {
|
|
36881
|
+
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`;
|
|
36882
|
+
return { kind: "stale", scopes: latest[0] ? parseJsonArray(latest[0].scopes_json) : current };
|
|
36883
|
+
}
|
|
36884
|
+
await tx`
|
|
36885
|
+
INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, metadata_json)
|
|
36886
|
+
VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes })}::jsonb)
|
|
36887
|
+
`;
|
|
36888
|
+
return { kind: "updated", scopes };
|
|
36889
|
+
});
|
|
36890
|
+
}
|
|
36664
36891
|
async withContext(orgId, worker, fn) {
|
|
36665
36892
|
return this.sql.begin(async (tx) => {
|
|
36666
36893
|
await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
|
|
@@ -36981,6 +37208,42 @@ class PostgresSkillsStore {
|
|
|
36981
37208
|
const rows = await this.sql`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
|
|
36982
37209
|
return rows[0] ? rowToSkill(rows[0]) : null;
|
|
36983
37210
|
}
|
|
37211
|
+
async setSkillLifecycle(principal, slug, patch, expectedRevisionId) {
|
|
37212
|
+
return this.sql.begin(async (tx) => {
|
|
37213
|
+
const currentRows = await tx`SELECT * FROM skills_registry WHERE org_id=${principal.orgId} AND slug=${slug} LIMIT 1 FOR UPDATE`;
|
|
37214
|
+
const current = currentRows[0] ? rowToSkill(currentRows[0]) : null;
|
|
37215
|
+
if (!current || current.tombstonedAt)
|
|
37216
|
+
return null;
|
|
37217
|
+
if (expectedRevisionId !== current.revisionId)
|
|
37218
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
37219
|
+
if (patch.lifecycle === "archived" && current.lifecycle !== "archived") {
|
|
37220
|
+
const profiles = await tx`SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=${principal.orgId}`;
|
|
37221
|
+
const referencing = profiles.filter((profile2) => {
|
|
37222
|
+
const selections2 = typeof profile2.selections_json === "string" ? JSON.parse(profile2.selections_json) : profile2.selections_json;
|
|
37223
|
+
return Array.isArray(selections2) && selections2.some((selection) => selection?.slug === slug);
|
|
37224
|
+
}).map((profile2) => String(profile2.profile_id)).sort();
|
|
37225
|
+
if (referencing.length)
|
|
37226
|
+
throw new SkillLifecycleConflictError(slug, referencing);
|
|
37227
|
+
}
|
|
37228
|
+
const next2 = {
|
|
37229
|
+
...current,
|
|
37230
|
+
lifecycle: patch.lifecycle,
|
|
37231
|
+
updatedAt: nowIso(),
|
|
37232
|
+
revisionId: revisionIdOfRecord({ ...current, lifecycle: patch.lifecycle, archiveReason: patch.reason, replacementSlug: patch.replacementSlug }),
|
|
37233
|
+
revisionNumber: current.revisionNumber + 1,
|
|
37234
|
+
...patch.lifecycle === "archived" ? { archivedAt: current.archivedAt ?? nowIso(), ...patch.reason ? { archiveReason: patch.reason } : {}, ...patch.replacementSlug ? { replacementSlug: patch.replacementSlug } : {} } : {}
|
|
37235
|
+
};
|
|
37236
|
+
if (patch.lifecycle === "active") {
|
|
37237
|
+
delete next2.archivedAt;
|
|
37238
|
+
delete next2.archiveReason;
|
|
37239
|
+
delete next2.replacementSlug;
|
|
37240
|
+
}
|
|
37241
|
+
const rows = await tx`UPDATE skills_registry SET lifecycle=${next2.lifecycle}, archived_at=${next2.archivedAt ?? null}, archive_reason=${next2.archiveReason ?? null}, replacement_slug=${next2.replacementSlug ?? null}, revision_id=${next2.revisionId}, revision_number=revision_number+1, updated_at=${next2.updatedAt} WHERE org_id=${principal.orgId} AND slug=${slug} AND tombstoned_at IS NULL AND revision_id=${current.revisionId} RETURNING *`;
|
|
37242
|
+
if (!rows[0])
|
|
37243
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, (await this.getSkill(principal, slug))?.revisionId ?? null);
|
|
37244
|
+
return rowToSkill(rows[0]);
|
|
37245
|
+
});
|
|
37246
|
+
}
|
|
36984
37247
|
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
36985
37248
|
const current = await this.getSkill(principal, slug);
|
|
36986
37249
|
if (!current || current.tombstonedAt)
|
|
@@ -37116,7 +37379,7 @@ class PostgresSkillsStore {
|
|
|
37116
37379
|
async listTags(principal) {
|
|
37117
37380
|
await this.purgeExpiredTombstones(principal);
|
|
37118
37381
|
const rows = await this.sql`
|
|
37119
|
-
SELECT DISTINCT tag FROM skills_tags WHERE org_id = ${principal.orgId} ORDER BY tag ASC
|
|
37382
|
+
SELECT DISTINCT t.tag FROM skills_tags t JOIN skills_registry s ON s.org_id = t.org_id AND s.slug = t.slug WHERE t.org_id = ${principal.orgId} AND s.lifecycle = 'active' ORDER BY t.tag ASC
|
|
37120
37383
|
`;
|
|
37121
37384
|
return rows.map((row) => String(row.tag));
|
|
37122
37385
|
}
|
|
@@ -37125,7 +37388,7 @@ class PostgresSkillsStore {
|
|
|
37125
37388
|
const rows = await this.sql`
|
|
37126
37389
|
SELECT s.* FROM skills_registry s
|
|
37127
37390
|
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
37128
|
-
WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
37391
|
+
WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
|
|
37129
37392
|
ORDER BY s.slug ASC
|
|
37130
37393
|
`;
|
|
37131
37394
|
return rows.map(rowToSkill);
|
|
@@ -37137,7 +37400,7 @@ class PostgresSkillsStore {
|
|
|
37137
37400
|
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
37138
37401
|
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
37139
37402
|
WHERE p.org_id = ${principal.orgId} AND p.principal = ${principal.apiKeyId}
|
|
37140
|
-
AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
37403
|
+
AND t.tag = ${tag} AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
|
|
37141
37404
|
ORDER BY p.slug ASC
|
|
37142
37405
|
`;
|
|
37143
37406
|
return rows.map(rowToPin);
|
|
@@ -13,3 +13,4 @@ export declare function registerPull(parent: Command): void;
|
|
|
13
13
|
* instance, newest first, with the one the instance currently serves marked (hasna/apps#1630).
|
|
14
14
|
*/
|
|
15
15
|
export declare function registerVersions(parent: Command): void;
|
|
16
|
+
export declare function registerLifecycle(parent: Command): void;
|