@hasna/skills 0.8.11 → 0.9.1
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 -4
- package/bin/index.js +853 -446
- package/bin/mcp.js +309 -155
- 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.d.ts +1 -0
- package/dist/index.js +498 -184
- package/dist/lib/agent-discovery.d.ts +1 -1
- package/dist/lib/claude-settings-witness.d.ts +19 -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 +1755 -752
- 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/docs/plugin-admission.md +67 -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/server.js
CHANGED
|
@@ -24122,7 +24122,7 @@ var init_dist_es9 = __esm(() => {
|
|
|
24122
24122
|
// package.json
|
|
24123
24123
|
var package_default = {
|
|
24124
24124
|
name: "@hasna/skills",
|
|
24125
|
-
version: "0.
|
|
24125
|
+
version: "0.9.1",
|
|
24126
24126
|
description: "Skills library for AI coding agents",
|
|
24127
24127
|
type: "module",
|
|
24128
24128
|
bin: {
|
|
@@ -34897,6 +34897,9 @@ class MemorySkillSelectionStore {
|
|
|
34897
34897
|
async getProfile(p2, id) {
|
|
34898
34898
|
return structuredClone(this.profiles.get(this.key(p2.orgId, id)) ?? null);
|
|
34899
34899
|
}
|
|
34900
|
+
async profilesReferencingSkill(p2, slug) {
|
|
34901
|
+
return [...this.profiles.values()].filter((profile2) => profile2.workspaceId === p2.orgId && profile2.selections.some((selection) => selection.slug === slug)).map((profile2) => profile2.id).sort();
|
|
34902
|
+
}
|
|
34900
34903
|
async saveProfile(p2, id, selected, expected) {
|
|
34901
34904
|
const key = this.key(p2.orgId, id), previous = this.profiles.get(key);
|
|
34902
34905
|
if ((previous?.revision ?? null) !== expected)
|
|
@@ -34934,14 +34937,25 @@ class SqliteSkillSelectionStore {
|
|
|
34934
34937
|
constructor(db) {
|
|
34935
34938
|
this.db = db;
|
|
34936
34939
|
}
|
|
34940
|
+
async profilesReferencingSkill(p2, slug) {
|
|
34941
|
+
const rows = this.db.query("SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=?").all(p2.orgId);
|
|
34942
|
+
return rows.filter((row) => selections(row.selections_json).some((selection) => selection.slug === slug)).map((row) => String(row.profile_id)).sort();
|
|
34943
|
+
}
|
|
34937
34944
|
async getProfile(p2, id) {
|
|
34938
34945
|
const row = this.db.query("SELECT * FROM skills_profiles WHERE org_id=? AND profile_id=?").get(p2.orgId, id);
|
|
34939
34946
|
return row ? profile(row) : null;
|
|
34940
34947
|
}
|
|
34941
34948
|
async saveProfile(p2, id, selected, expected) {
|
|
34942
|
-
|
|
34943
|
-
|
|
34944
|
-
|
|
34949
|
+
return this.db.transaction(() => {
|
|
34950
|
+
for (const selection of [...selected].sort((a3, b3) => a3.slug.localeCompare(b3.slug))) {
|
|
34951
|
+
const skill = this.db.query("SELECT lifecycle FROM skills_registry WHERE org_id=? AND slug=?").get(p2.orgId, selection.slug);
|
|
34952
|
+
if (skill?.lifecycle === "archived")
|
|
34953
|
+
return null;
|
|
34954
|
+
}
|
|
34955
|
+
const revision = randomUUID2(), timestamp = new Date().toISOString();
|
|
34956
|
+
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);
|
|
34957
|
+
return row ? profile(row) : null;
|
|
34958
|
+
}).immediate();
|
|
34945
34959
|
}
|
|
34946
34960
|
async getStationState(p2, id) {
|
|
34947
34961
|
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);
|
|
@@ -34960,14 +34974,25 @@ class PostgresSkillSelectionStore {
|
|
|
34960
34974
|
constructor(sql) {
|
|
34961
34975
|
this.sql = sql;
|
|
34962
34976
|
}
|
|
34977
|
+
async profilesReferencingSkill(p2, slug) {
|
|
34978
|
+
const rows = await this.sql`SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=${p2.orgId}`;
|
|
34979
|
+
return rows.filter((row) => selections(row.selections_json).some((selection) => selection.slug === slug)).map((row) => String(row.profile_id)).sort();
|
|
34980
|
+
}
|
|
34963
34981
|
async getProfile(p2, id) {
|
|
34964
34982
|
const rows = await this.sql`SELECT * FROM skills_profiles WHERE org_id=${p2.orgId} AND profile_id=${id}`;
|
|
34965
34983
|
return rows[0] ? profile(rows[0]) : null;
|
|
34966
34984
|
}
|
|
34967
34985
|
async saveProfile(p2, id, selected, expected) {
|
|
34968
|
-
|
|
34969
|
-
|
|
34970
|
-
|
|
34986
|
+
return this.sql.begin(async (tx) => {
|
|
34987
|
+
for (const selection of [...selected].sort((a3, b3) => a3.slug.localeCompare(b3.slug))) {
|
|
34988
|
+
const rows2 = await tx`SELECT lifecycle FROM skills_registry WHERE org_id=${p2.orgId} AND slug=${selection.slug} FOR UPDATE`;
|
|
34989
|
+
if (rows2[0]?.lifecycle === "archived")
|
|
34990
|
+
return null;
|
|
34991
|
+
}
|
|
34992
|
+
const revision = randomUUID2(), timestamp = new Date().toISOString();
|
|
34993
|
+
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 *`;
|
|
34994
|
+
return rows[0] ? profile(rows[0]) : null;
|
|
34995
|
+
});
|
|
34971
34996
|
}
|
|
34972
34997
|
async getStationState(p2, id) {
|
|
34973
34998
|
const rows = await this.sql`SELECT * FROM skills_station_state WHERE org_id=${p2.orgId} AND actor_id=${p2.userId} AND station_id=${id}`;
|
|
@@ -35161,7 +35186,11 @@ function rowToSkill(row) {
|
|
|
35161
35186
|
revisionId: String(row.revision_id ?? ""),
|
|
35162
35187
|
revisionNumber: Number(row.revision_number ?? 0),
|
|
35163
35188
|
...row.tombstoned_at ? { tombstonedAt: dateString(row.tombstoned_at) } : {},
|
|
35164
|
-
...row.tombstone_purge_after ? { tombstonePurgeAfter: dateString(row.tombstone_purge_after) } : {}
|
|
35189
|
+
...row.tombstone_purge_after ? { tombstonePurgeAfter: dateString(row.tombstone_purge_after) } : {},
|
|
35190
|
+
lifecycle: row.lifecycle === "archived" ? "archived" : "active",
|
|
35191
|
+
...row.archived_at ? { archivedAt: dateString(row.archived_at) } : {},
|
|
35192
|
+
...row.archive_reason ? { archiveReason: String(row.archive_reason) } : {},
|
|
35193
|
+
...row.replacement_slug ? { replacementSlug: String(row.replacement_slug) } : {}
|
|
35165
35194
|
};
|
|
35166
35195
|
}
|
|
35167
35196
|
function rowToPin(row) {
|
|
@@ -35280,11 +35309,22 @@ class SkillVersionExistsError extends Error {
|
|
|
35280
35309
|
}
|
|
35281
35310
|
}
|
|
35282
35311
|
|
|
35312
|
+
class SkillLifecycleConflictError extends Error {
|
|
35313
|
+
slug;
|
|
35314
|
+
profiles;
|
|
35315
|
+
constructor(slug, profiles) {
|
|
35316
|
+
super(`cannot archive '${slug}' while active profiles reference it: ${profiles.join(", ")}`);
|
|
35317
|
+
this.slug = slug;
|
|
35318
|
+
this.profiles = profiles;
|
|
35319
|
+
this.name = "SkillLifecycleConflictError";
|
|
35320
|
+
}
|
|
35321
|
+
}
|
|
35322
|
+
|
|
35283
35323
|
// src/lib/revision.ts
|
|
35284
35324
|
import { createHash as createHash5 } from "crypto";
|
|
35285
35325
|
var REVISION_ID_PATTERN = /^[0-9a-f]{64}$/;
|
|
35286
35326
|
function revisionIdOf(content) {
|
|
35287
|
-
const
|
|
35327
|
+
const canonicalContent = {
|
|
35288
35328
|
slug: content.slug,
|
|
35289
35329
|
displayName: content.displayName,
|
|
35290
35330
|
description: content.description,
|
|
@@ -35296,7 +35336,13 @@ function revisionIdOf(content) {
|
|
|
35296
35336
|
skillMd: content.skillMd ?? null,
|
|
35297
35337
|
bundleSha256: content.bundleSha256 ?? null,
|
|
35298
35338
|
bundleByteSize: content.bundleByteSize ?? null
|
|
35299
|
-
}
|
|
35339
|
+
};
|
|
35340
|
+
if (content.lifecycle === "archived" || content.archiveReason !== undefined || content.replacementSlug !== undefined) {
|
|
35341
|
+
canonicalContent.lifecycle = content.lifecycle ?? "active";
|
|
35342
|
+
canonicalContent.archiveReason = content.archiveReason ?? null;
|
|
35343
|
+
canonicalContent.replacementSlug = content.replacementSlug ?? null;
|
|
35344
|
+
}
|
|
35345
|
+
const canonical = JSON.stringify(canonicalContent);
|
|
35300
35346
|
return createHash5("sha256").update(canonical).digest("hex");
|
|
35301
35347
|
}
|
|
35302
35348
|
function revisionIdOfRecord(record) {
|
|
@@ -35399,6 +35445,28 @@ class SqliteSkillsStore {
|
|
|
35399
35445
|
scopes: parseScopes(row.scopes_json)
|
|
35400
35446
|
};
|
|
35401
35447
|
}
|
|
35448
|
+
async updateApiKeyScopes(actor, keyId, expectedScopes, addScopes) {
|
|
35449
|
+
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]);
|
|
35450
|
+
if (!row)
|
|
35451
|
+
return { kind: "not_found" };
|
|
35452
|
+
const current = parseScopes(row.scopes_json);
|
|
35453
|
+
if (current.length !== expectedScopes.length || current.some((scope, index) => scope !== expectedScopes[index])) {
|
|
35454
|
+
return { kind: "stale", scopes: current };
|
|
35455
|
+
}
|
|
35456
|
+
const scopes = [...current, ...addScopes.filter((scope) => !current.includes(scope))];
|
|
35457
|
+
const tx = this.db.transaction(() => {
|
|
35458
|
+
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)]);
|
|
35459
|
+
if (changed.changes !== 1)
|
|
35460
|
+
return false;
|
|
35461
|
+
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 })]);
|
|
35462
|
+
return true;
|
|
35463
|
+
});
|
|
35464
|
+
if (!tx()) {
|
|
35465
|
+
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]);
|
|
35466
|
+
return { kind: "stale", scopes: latest ? parseScopes(latest.scopes_json) : current };
|
|
35467
|
+
}
|
|
35468
|
+
return { kind: "updated", scopes };
|
|
35469
|
+
}
|
|
35402
35470
|
async createRun(input) {
|
|
35403
35471
|
if (input.idempotencyKey) {
|
|
35404
35472
|
const existing = this.get("SELECT * FROM skills_runs WHERE org_id = ? AND idempotency_key = ? LIMIT 1", [input.principal.orgId, input.idempotencyKey]);
|
|
@@ -35720,6 +35788,39 @@ class SqliteSkillsStore {
|
|
|
35720
35788
|
const row = this.get("SELECT * FROM skills_registry WHERE org_id = ? AND slug = ? LIMIT 1", [principal.orgId, slug]);
|
|
35721
35789
|
return row ? rowToSkill(row) : null;
|
|
35722
35790
|
}
|
|
35791
|
+
async setSkillLifecycle(principal, slug, patch, expectedRevisionId) {
|
|
35792
|
+
return this.db.transaction(() => {
|
|
35793
|
+
const rowCurrent = this.get("SELECT * FROM skills_registry WHERE org_id=? AND slug=? LIMIT 1", [principal.orgId, slug]);
|
|
35794
|
+
const current = rowCurrent ? rowToSkill(rowCurrent) : null;
|
|
35795
|
+
if (!current || current.tombstonedAt)
|
|
35796
|
+
return null;
|
|
35797
|
+
if (expectedRevisionId !== current.revisionId)
|
|
35798
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
35799
|
+
if (patch.lifecycle === "archived" && current.lifecycle !== "archived") {
|
|
35800
|
+
const profiles = this.db.query("SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=?").all(principal.orgId);
|
|
35801
|
+
const referencing = profiles.filter((profile2) => {
|
|
35802
|
+
const selections2 = typeof profile2.selections_json === "string" ? JSON.parse(profile2.selections_json) : profile2.selections_json;
|
|
35803
|
+
return Array.isArray(selections2) && selections2.some((selection) => selection?.slug === slug);
|
|
35804
|
+
}).map((profile2) => String(profile2.profile_id)).sort();
|
|
35805
|
+
if (referencing.length)
|
|
35806
|
+
throw new SkillLifecycleConflictError(slug, referencing);
|
|
35807
|
+
}
|
|
35808
|
+
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 } : {} } : {} };
|
|
35809
|
+
if (patch.lifecycle === "active") {
|
|
35810
|
+
delete next2.archivedAt;
|
|
35811
|
+
delete next2.archiveReason;
|
|
35812
|
+
delete next2.replacementSlug;
|
|
35813
|
+
}
|
|
35814
|
+
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]);
|
|
35815
|
+
if (!row)
|
|
35816
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, this.getSkillSync(principal, slug)?.revisionId ?? null);
|
|
35817
|
+
return rowToSkill(row);
|
|
35818
|
+
}).immediate();
|
|
35819
|
+
}
|
|
35820
|
+
getSkillSync(principal, slug) {
|
|
35821
|
+
const row = this.get("SELECT * FROM skills_registry WHERE org_id=? AND slug=? LIMIT 1", [principal.orgId, slug]);
|
|
35822
|
+
return row ? rowToSkill(row) : null;
|
|
35823
|
+
}
|
|
35723
35824
|
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
35724
35825
|
const current = await this.getSkill(principal, slug);
|
|
35725
35826
|
if (!current || current.tombstonedAt)
|
|
@@ -35832,13 +35933,13 @@ class SqliteSkillsStore {
|
|
|
35832
35933
|
}
|
|
35833
35934
|
async listTags(principal) {
|
|
35834
35935
|
await this.purgeExpiredTombstones(principal);
|
|
35835
|
-
return this.all("SELECT DISTINCT tag FROM skills_tags WHERE org_id = ? ORDER BY tag ASC", [principal.orgId]).map((row) => String(row.tag));
|
|
35936
|
+
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));
|
|
35836
35937
|
}
|
|
35837
35938
|
async listSkillsByTag(principal, tag) {
|
|
35838
35939
|
await this.purgeExpiredTombstones(principal);
|
|
35839
35940
|
return this.all(`SELECT s.* FROM skills_registry s
|
|
35840
35941
|
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
35841
|
-
WHERE t.org_id = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
35942
|
+
WHERE t.org_id = ? AND t.tag = ? AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
|
|
35842
35943
|
ORDER BY s.slug ASC`, [principal.orgId, tag]).map(rowToSkill);
|
|
35843
35944
|
}
|
|
35844
35945
|
async listPinsByTag(principal, tag) {
|
|
@@ -35846,7 +35947,7 @@ class SqliteSkillsStore {
|
|
|
35846
35947
|
return this.all(`SELECT p.* FROM skills_pins p
|
|
35847
35948
|
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
35848
35949
|
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
35849
|
-
WHERE p.org_id = ? AND p.principal = ? AND t.tag = ? AND s.tombstoned_at IS NULL
|
|
35950
|
+
WHERE p.org_id = ? AND p.principal = ? AND t.tag = ? AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
|
|
35850
35951
|
ORDER BY p.slug ASC`, [principal.orgId, principal.apiKeyId, tag]).map(rowToPin);
|
|
35851
35952
|
}
|
|
35852
35953
|
async listPublishedSlugs(principal) {
|
|
@@ -36795,6 +36896,15 @@ class SkillRequestError extends Error {
|
|
|
36795
36896
|
this.code = code;
|
|
36796
36897
|
}
|
|
36797
36898
|
}
|
|
36899
|
+
function skillLifecyclePatch(body) {
|
|
36900
|
+
if (body.lifecycle !== "active" && body.lifecycle !== "archived")
|
|
36901
|
+
throw new SkillRequestError(400, "INVALID_LIFECYCLE", "lifecycle must be active or archived");
|
|
36902
|
+
if (body.reason !== undefined && (typeof body.reason !== "string" || body.reason.length > 512))
|
|
36903
|
+
throw new SkillRequestError(400, "INVALID_LIFECYCLE", "reason must be at most 512 characters");
|
|
36904
|
+
if (body.replacementSlug !== undefined && (typeof body.replacementSlug !== "string" || !SLUG_PATTERN.test(body.replacementSlug)))
|
|
36905
|
+
throw new SkillRequestError(400, "INVALID_LIFECYCLE", "replacementSlug must be a valid skill slug");
|
|
36906
|
+
return { lifecycle: body.lifecycle, ...body.reason ? { reason: body.reason } : {}, ...body.replacementSlug ? { replacementSlug: body.replacementSlug } : {} };
|
|
36907
|
+
}
|
|
36798
36908
|
function publishedSkillMeta(record) {
|
|
36799
36909
|
return {
|
|
36800
36910
|
name: record.slug,
|
|
@@ -36819,7 +36929,11 @@ function publishedPayload(record, opts = {}) {
|
|
|
36819
36929
|
publishedAt: record.createdAt,
|
|
36820
36930
|
updatedAt: record.updatedAt,
|
|
36821
36931
|
revisionId: record.revisionId,
|
|
36822
|
-
revisionNumber: record.revisionNumber
|
|
36932
|
+
revisionNumber: record.revisionNumber,
|
|
36933
|
+
lifecycle: record.lifecycle,
|
|
36934
|
+
...record.archivedAt ? { archivedAt: record.archivedAt } : {},
|
|
36935
|
+
...record.archiveReason ? { archiveReason: record.archiveReason } : {},
|
|
36936
|
+
...record.replacementSlug ? { replacementSlug: record.replacementSlug } : {}
|
|
36823
36937
|
};
|
|
36824
36938
|
}
|
|
36825
36939
|
function revisionEtag(revisionId) {
|
|
@@ -36859,7 +36973,7 @@ async function tombstoneStatus(store, artifactStorage, principal, record) {
|
|
|
36859
36973
|
};
|
|
36860
36974
|
}
|
|
36861
36975
|
async function listMergedSkills(store, principal) {
|
|
36862
|
-
return (await store.listSkills(principal)).sort((a3, b3) => a3.slug.localeCompare(b3.slug)).map((record) => publishedPayload(record));
|
|
36976
|
+
return (await store.listSkills(principal)).filter((record) => record.lifecycle === "active").sort((a3, b3) => a3.slug.localeCompare(b3.slug)).map((record) => publishedPayload(record));
|
|
36863
36977
|
}
|
|
36864
36978
|
async function resolvePublishedSkill(store, artifactStorage, principal, slug) {
|
|
36865
36979
|
const record = await store.getSkill(principal, slug);
|
|
@@ -36876,7 +36990,7 @@ async function listOrgTags(store, principal) {
|
|
|
36876
36990
|
return [...new Set((await store.listTags(principal)).filter((tag) => tag.trim()))].sort();
|
|
36877
36991
|
}
|
|
36878
36992
|
async function listMergedSkillsByTag(store, principal, tag) {
|
|
36879
|
-
return (await store.listSkillsByTag(principal, tag)).sort((a3, b3) => a3.slug.localeCompare(b3.slug)).map((record) => publishedPayload(record));
|
|
36993
|
+
return (await store.listSkillsByTag(principal, tag)).filter((record) => record.lifecycle === "active").sort((a3, b3) => a3.slug.localeCompare(b3.slug)).map((record) => publishedPayload(record));
|
|
36880
36994
|
}
|
|
36881
36995
|
function skillSummary(skill) {
|
|
36882
36996
|
return {
|
|
@@ -37438,6 +37552,8 @@ async function validatePublished(store, principal, selections2) {
|
|
|
37438
37552
|
const current = await store.getSkill(principal, selection2.slug);
|
|
37439
37553
|
if (!current || current.tombstonedAt)
|
|
37440
37554
|
throw new SkillRequestError(current?.tombstonedAt ? 410 : 404, "SELECTION_UNAVAILABLE", `Selected skill '${selection2.slug}' is unavailable`);
|
|
37555
|
+
if (current.lifecycle === "archived")
|
|
37556
|
+
throw new SkillRequestError(409, "SELECTION_ARCHIVED", `Selected skill '${selection2.slug}' is archived`);
|
|
37441
37557
|
const version2 = await store.getSkillVersion(principal, selection2.slug, selection2.version);
|
|
37442
37558
|
if (!version2 || `sha256:${version2.bundleSha256}` !== selection2.bundleDigest)
|
|
37443
37559
|
throw new SkillRequestError(409, "SELECTION_VERSION_MISMATCH", `Selected version of '${selection2.slug}' does not match its published digest`);
|
|
@@ -38165,6 +38281,7 @@ function createSubmitRunService(options) {
|
|
|
38165
38281
|
const policy = { ...DEFAULT_RUN_POLICY, ...input.policy };
|
|
38166
38282
|
input = {
|
|
38167
38283
|
...input,
|
|
38284
|
+
...input.executionContract ? { executionContract: { ...input.executionContract } } : {},
|
|
38168
38285
|
systemDeps: [...input.systemDeps ?? []],
|
|
38169
38286
|
policy: { ...policy, egressAllowlist: [...policy.egressAllowlist] },
|
|
38170
38287
|
limits: { ...DEFAULT_RUN_LIMITS, ...input.limits }
|
|
@@ -38173,7 +38290,7 @@ function createSubmitRunService(options) {
|
|
|
38173
38290
|
const byKey = await store.getRunByKey(input.tenantId, input.idempotencyKey);
|
|
38174
38291
|
if (byKey)
|
|
38175
38292
|
return { run: ownedAdmission(byKey.admission), created: false };
|
|
38176
|
-
const byDigests = await store.getRunByDigests({
|
|
38293
|
+
const byDigests = input.executionContract ? null : await store.getRunByDigests({
|
|
38177
38294
|
tenantId: input.tenantId,
|
|
38178
38295
|
skillId: input.skillId,
|
|
38179
38296
|
skillVersion: input.skillVersion,
|
|
@@ -38195,6 +38312,7 @@ function createSubmitRunService(options) {
|
|
|
38195
38312
|
bundleDigest: input.bundleDigest,
|
|
38196
38313
|
runtimeImageDigest: image.runtimeImageDigest,
|
|
38197
38314
|
dependencyLayerTag: image.dependencyLayerTag,
|
|
38315
|
+
...input.executionContract ? { executionContract: { ...input.executionContract } } : {},
|
|
38198
38316
|
inputDigest,
|
|
38199
38317
|
runtime: image.runtime,
|
|
38200
38318
|
policy: { ...DEFAULT_RUN_POLICY, ...input.policy },
|
|
@@ -38210,6 +38328,7 @@ function createSubmitRunService(options) {
|
|
|
38210
38328
|
function ownedAdmission(admission) {
|
|
38211
38329
|
return {
|
|
38212
38330
|
...admission,
|
|
38331
|
+
...admission.executionContract ? { executionContract: { ...admission.executionContract } } : {},
|
|
38213
38332
|
policy: { ...admission.policy, egressAllowlist: [...admission.policy.egressAllowlist] },
|
|
38214
38333
|
limits: { ...admission.limits }
|
|
38215
38334
|
};
|
|
@@ -38350,6 +38469,7 @@ function createReceiptService(store) {
|
|
|
38350
38469
|
runtimeImageDigest: input.admission.runtimeImageDigest,
|
|
38351
38470
|
bundleDigest: input.admission.bundleDigest,
|
|
38352
38471
|
dependencyLayerTag: input.admission.dependencyLayerTag,
|
|
38472
|
+
...input.admission.executionContract ? { executionContract: { ...input.admission.executionContract } } : {},
|
|
38353
38473
|
policy: input.admission.policy,
|
|
38354
38474
|
limits: input.admission.limits,
|
|
38355
38475
|
exitCode: null,
|
|
@@ -41823,7 +41943,7 @@ class EcsDispatcher {
|
|
|
41823
41943
|
}
|
|
41824
41944
|
cancellationReceipt(receipt, admission, attempt) {
|
|
41825
41945
|
try {
|
|
41826
|
-
return !!receipt && receipt.runId === admission.runId && receipt.attemptId === attempt.attemptId && !!attempt.clientToken && receipt.clientToken === attempt.clientToken && !!attempt.requestDigest && receipt.requestDigest === attempt.requestDigest && !!attempt.startedBy && receipt.startedBy === attempt.startedBy && receipt.bundleDigest === admission.bundleDigest && receipt.runtimeImageDigest === admission.runtimeImageDigest && receipt.dependencyLayerTag === admission.dependencyLayerTag && canonicalJson(receipt.policy) === canonicalJson(admission.policy) && canonicalJson(receipt.limits) === canonicalJson(admission.limits) && (receipt.taskId === null || receipt.taskId === attempt.taskId) && (receipt.status === null && receipt.completedAt === null || receipt.status === "cancelled" && typeof receipt.completedAt === "string" && Number.isFinite(Date.parse(receipt.completedAt)));
|
|
41946
|
+
return !!receipt && receipt.runId === admission.runId && receipt.attemptId === attempt.attemptId && !!attempt.clientToken && receipt.clientToken === attempt.clientToken && !!attempt.requestDigest && receipt.requestDigest === attempt.requestDigest && !!attempt.startedBy && receipt.startedBy === attempt.startedBy && receipt.bundleDigest === admission.bundleDigest && receipt.runtimeImageDigest === admission.runtimeImageDigest && receipt.dependencyLayerTag === admission.dependencyLayerTag && canonicalJson(receipt.executionContract ?? null) === canonicalJson(admission.executionContract ?? null) && canonicalJson(receipt.policy) === canonicalJson(admission.policy) && canonicalJson(receipt.limits) === canonicalJson(admission.limits) && (receipt.taskId === null || receipt.taskId === attempt.taskId) && (receipt.status === null && receipt.completedAt === null || receipt.status === "cancelled" && typeof receipt.completedAt === "string" && Number.isFinite(Date.parse(receipt.completedAt)));
|
|
41827
41947
|
} catch {
|
|
41828
41948
|
return false;
|
|
41829
41949
|
}
|
|
@@ -41920,7 +42040,174 @@ function validText(value) {
|
|
|
41920
42040
|
}
|
|
41921
42041
|
|
|
41922
42042
|
// src/server/runtime-policy.ts
|
|
41923
|
-
import { createHash as
|
|
42043
|
+
import { createHash as createHash11, createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
42044
|
+
|
|
42045
|
+
// src/server/runtime-pure-contract.ts
|
|
42046
|
+
import { createHash as createHash10 } from "crypto";
|
|
42047
|
+
var PURE_DESCRIPTOR = Object.freeze({
|
|
42048
|
+
id: "regex-test.v1",
|
|
42049
|
+
runtime: "bun",
|
|
42050
|
+
adapter: "skills-input-json.v1",
|
|
42051
|
+
input: Object.freeze({ patternBytes: 512, textBytes: 4096, totalBytes: 8192, flags: "dgimsuvy" }),
|
|
42052
|
+
output: "regex-matches.v1",
|
|
42053
|
+
secrets: "none",
|
|
42054
|
+
egress: "deny",
|
|
42055
|
+
maxDurationMs: 5000,
|
|
42056
|
+
maxMemoryMb: 512,
|
|
42057
|
+
maxCpuUnits: 256,
|
|
42058
|
+
stdoutBytes: 16384,
|
|
42059
|
+
stderrBytes: 8192,
|
|
42060
|
+
artifactsBytes: 0
|
|
42061
|
+
});
|
|
42062
|
+
var PURE_DESCRIPTOR_DIGEST = digestInput(PURE_DESCRIPTOR);
|
|
42063
|
+
var PURE_LIMITS = Object.freeze({
|
|
42064
|
+
maxDurationMs: 5000,
|
|
42065
|
+
maxMemoryMb: 512,
|
|
42066
|
+
maxCpuUnits: 256,
|
|
42067
|
+
maxArtifactsBytes: 0,
|
|
42068
|
+
maxConcurrency: 1
|
|
42069
|
+
});
|
|
42070
|
+
var hash = (bytes) => createHash10("sha256").update(bytes).digest("hex");
|
|
42071
|
+
var object2 = (v2) => {
|
|
42072
|
+
if (!v2 || typeof v2 !== "object" || Array.isArray(v2))
|
|
42073
|
+
throw Error("Pure contract requires an object");
|
|
42074
|
+
return v2;
|
|
42075
|
+
};
|
|
42076
|
+
function keys2(v2, allowed) {
|
|
42077
|
+
if (Object.keys(v2).some((k4) => !allowed.includes(k4)))
|
|
42078
|
+
throw Error("Unsupported pure contract field");
|
|
42079
|
+
}
|
|
42080
|
+
function uniqueJson(text) {
|
|
42081
|
+
const result = JSON.parse(text);
|
|
42082
|
+
let i4 = 0;
|
|
42083
|
+
const ws = () => {
|
|
42084
|
+
while (/\s/.test(text[i4] ?? "") && i4 < text.length)
|
|
42085
|
+
i4++;
|
|
42086
|
+
};
|
|
42087
|
+
const string = () => {
|
|
42088
|
+
const start = i4++;
|
|
42089
|
+
while (i4 < text.length) {
|
|
42090
|
+
const c4 = text[i4++];
|
|
42091
|
+
if (c4 === "\\")
|
|
42092
|
+
i4++;
|
|
42093
|
+
else if (c4 === '"')
|
|
42094
|
+
break;
|
|
42095
|
+
}
|
|
42096
|
+
return JSON.parse(text.slice(start, i4));
|
|
42097
|
+
};
|
|
42098
|
+
const value = (depth) => {
|
|
42099
|
+
if (depth > 32)
|
|
42100
|
+
throw Error("JSON nesting limit exceeded");
|
|
42101
|
+
ws();
|
|
42102
|
+
if (text[i4] === '"') {
|
|
42103
|
+
string();
|
|
42104
|
+
return;
|
|
42105
|
+
}
|
|
42106
|
+
if (text[i4] === "{") {
|
|
42107
|
+
i4++;
|
|
42108
|
+
ws();
|
|
42109
|
+
const seen = new Set;
|
|
42110
|
+
if (text[i4] === "}") {
|
|
42111
|
+
i4++;
|
|
42112
|
+
return;
|
|
42113
|
+
}
|
|
42114
|
+
for (;; ) {
|
|
42115
|
+
ws();
|
|
42116
|
+
const key = string();
|
|
42117
|
+
if (seen.has(key))
|
|
42118
|
+
throw Error("Duplicate JSON key");
|
|
42119
|
+
seen.add(key);
|
|
42120
|
+
ws();
|
|
42121
|
+
i4++;
|
|
42122
|
+
value(depth + 1);
|
|
42123
|
+
ws();
|
|
42124
|
+
if (text[i4++] === "}")
|
|
42125
|
+
return;
|
|
42126
|
+
}
|
|
42127
|
+
}
|
|
42128
|
+
if (text[i4] === "[") {
|
|
42129
|
+
i4++;
|
|
42130
|
+
ws();
|
|
42131
|
+
if (text[i4] === "]") {
|
|
42132
|
+
i4++;
|
|
42133
|
+
return;
|
|
42134
|
+
}
|
|
42135
|
+
for (;; ) {
|
|
42136
|
+
value(depth + 1);
|
|
42137
|
+
ws();
|
|
42138
|
+
if (text[i4++] === "]")
|
|
42139
|
+
return;
|
|
42140
|
+
}
|
|
42141
|
+
}
|
|
42142
|
+
while (i4 < text.length && !/[\s,\]}]/.test(text[i4]))
|
|
42143
|
+
i4++;
|
|
42144
|
+
};
|
|
42145
|
+
value(0);
|
|
42146
|
+
return result;
|
|
42147
|
+
}
|
|
42148
|
+
function pureInput(value) {
|
|
42149
|
+
const v2 = object2(value);
|
|
42150
|
+
keys2(v2, ["pattern", "text", "flags"]);
|
|
42151
|
+
if (typeof v2.pattern !== "string" || Buffer.byteLength(v2.pattern) > 512 || typeof v2.text !== "string" || Buffer.byteLength(v2.text) > 4096 || typeof v2.flags !== "string" || !/^[dgimsuvy]*$/.test(v2.flags) || new Set(v2.flags).size !== v2.flags.length || v2.flags.includes("u") && v2.flags.includes("v") || Buffer.byteLength(JSON.stringify(v2)) > 8192)
|
|
42152
|
+
throw Error("Invalid pure regex input");
|
|
42153
|
+
return { pattern: v2.pattern, text: v2.text, flags: v2.flags };
|
|
42154
|
+
}
|
|
42155
|
+
function assertPureContract(value) {
|
|
42156
|
+
const v2 = object2(value);
|
|
42157
|
+
keys2(v2, ["id", "descriptorDigest", "entrypoint", "entrypointDigest"]);
|
|
42158
|
+
if (v2.id !== PURE_DESCRIPTOR.id || v2.descriptorDigest !== PURE_DESCRIPTOR_DIGEST || typeof v2.entrypoint !== "string" || !/^src\/[a-zA-Z0-9_/-]+\.[cm]?[jt]s$/.test(v2.entrypoint) || v2.entrypoint.includes("//") || typeof v2.entrypointDigest !== "string" || !/^[a-f0-9]{64}$/.test(v2.entrypointDigest))
|
|
42159
|
+
throw Error("Unsupported pure execution contract");
|
|
42160
|
+
}
|
|
42161
|
+
function assertPureAdmission(a4) {
|
|
42162
|
+
assertPureContract(a4.executionContract);
|
|
42163
|
+
if (a4.runtime !== "bun" || a4.dependencyLayerTag !== null || a4.policy.egress !== "deny" || a4.policy.egressAllowlist.length !== 0 || a4.policy.networkByteCap !== 0 || Object.entries(PURE_LIMITS).some(([k4, v2]) => a4.limits[k4] !== v2))
|
|
42164
|
+
throw Error("Pure admission isolation policy mismatch");
|
|
42165
|
+
return a4.executionContract;
|
|
42166
|
+
}
|
|
42167
|
+
async function inspectPureBundle(bytes, contract) {
|
|
42168
|
+
assertPureContract(contract);
|
|
42169
|
+
const { entries } = await inspectSkillBundle(bytes, { limits: {
|
|
42170
|
+
compressedBytes: 1e6,
|
|
42171
|
+
decompressedBytes: 4000000,
|
|
42172
|
+
fileBytes: 1e6,
|
|
42173
|
+
entries: 100,
|
|
42174
|
+
timeoutMs: 5000
|
|
42175
|
+
} });
|
|
42176
|
+
const entry = entries.find((e4) => e4.path === contract.entrypoint);
|
|
42177
|
+
if (!entry || hash(entry.bytes) !== contract.entrypointDigest)
|
|
42178
|
+
throw Error("Pure entrypoint integrity failed");
|
|
42179
|
+
if (entries.some((e4) => e4.path.split("/").includes("node_modules")))
|
|
42180
|
+
throw Error("Pure bundle cannot supply dependencies");
|
|
42181
|
+
const read = (name) => {
|
|
42182
|
+
const entry2 = entries.find((e4) => e4.path === name);
|
|
42183
|
+
if (!entry2)
|
|
42184
|
+
throw Error("Pure bundle manifest missing");
|
|
42185
|
+
return object2(uniqueJson(Buffer.from(entry2.bytes).toString("utf8")));
|
|
42186
|
+
};
|
|
42187
|
+
const manifest = read("skill.json"), runtime = object2(manifest.runtime), pkg = read("package.json");
|
|
42188
|
+
if (manifest.kind !== "executable" || runtime.runtime !== "bun" || runtime.entrypoint !== contract.entrypoint || !Array.isArray(runtime.env) || runtime.env.length !== 0 || runtime.system_deps !== undefined && (!Array.isArray(runtime.system_deps) || runtime.system_deps.length !== 0) || runtime.needs_network === true || manifest.needs_network === true)
|
|
42189
|
+
throw Error("Pure bundle requires a credential-free self-contained Bun entrypoint");
|
|
42190
|
+
for (const key of ["dependencies", "optionalDependencies", "peerDependencies", "devDependencies", "scripts"])
|
|
42191
|
+
if (pkg[key] !== undefined && Object.keys(object2(pkg[key])).length)
|
|
42192
|
+
throw Error("Pure bundle cannot install dependencies or lifecycle scripts");
|
|
42193
|
+
return entries;
|
|
42194
|
+
}
|
|
42195
|
+
function validatePureOutput(stdout, input) {
|
|
42196
|
+
if (Buffer.byteLength(stdout) > PURE_DESCRIPTOR.stdoutBytes)
|
|
42197
|
+
throw Error("Pure output limit exceeded");
|
|
42198
|
+
const v2 = object2(uniqueJson(stdout));
|
|
42199
|
+
keys2(v2, ["pattern", "flags", "matches"]);
|
|
42200
|
+
if (v2.pattern !== input.pattern || v2.flags !== input.flags || !Array.isArray(v2.matches))
|
|
42201
|
+
throw Error("Pure output does not match admitted input");
|
|
42202
|
+
for (const value of v2.matches) {
|
|
42203
|
+
const m2 = object2(value);
|
|
42204
|
+
keys2(m2, ["match", "groups", "namedGroups", "index"]);
|
|
42205
|
+
if (typeof m2.match !== "string" || !Number.isInteger(m2.index) || m2.index < 0 || m2.index > input.text.length || input.text.slice(m2.index, m2.index + m2.match.length) !== m2.match || !Array.isArray(m2.groups) || m2.groups.some((x2) => x2 !== null && typeof x2 !== "string") || m2.namedGroups !== null && Object.values(object2(m2.namedGroups)).some((x2) => typeof x2 !== "string"))
|
|
42206
|
+
throw Error("Invalid pure match output");
|
|
42207
|
+
}
|
|
42208
|
+
}
|
|
42209
|
+
|
|
42210
|
+
// src/server/runtime-policy.ts
|
|
41924
42211
|
var RUNTIME_MAX_INPUT_BYTES = 1e5;
|
|
41925
42212
|
var RUNTIME_MAX_BUNDLE_BYTES = 1e6;
|
|
41926
42213
|
var RUNTIME_MAX_ARTIFACT_BYTES = 2000000;
|
|
@@ -41955,8 +42242,25 @@ function readRuntimeConfig(env = process.env) {
|
|
|
41955
42242
|
for (const key of ["subnets", "securityGroups"])
|
|
41956
42243
|
if (!Array.isArray(c4[key]) || !c4[key].length || c4[key].some((v2) => typeof v2 !== "string" || !v2))
|
|
41957
42244
|
throw Error(`Runtime configuration missing ${key}`);
|
|
41958
|
-
if (!Array.isArray(c4.reviewedBundles) || !c4.reviewedBundles.length
|
|
41959
|
-
throw Error("Runtime requires exact reviewed
|
|
42245
|
+
if (!Array.isArray(c4.reviewedBundles) || !c4.reviewedBundles.length)
|
|
42246
|
+
throw Error("Runtime requires exact reviewed versions and bundle digests");
|
|
42247
|
+
const seen = new Set;
|
|
42248
|
+
for (const b4 of c4.reviewedBundles) {
|
|
42249
|
+
if (!b4 || typeof b4.slug !== "string" || !/^[a-z0-9-]+$/.test(b4.slug) || typeof b4.version !== "string" || !/^\d+\.\d+\.\d+$/.test(b4.version) || !/^[a-f0-9]{64}$/.test(b4.sha256))
|
|
42250
|
+
throw Error("Runtime requires exact reviewed versions and bundle digests");
|
|
42251
|
+
if (b4.executionContract) {
|
|
42252
|
+
if (b4.slug === "pdf-generate")
|
|
42253
|
+
throw Error("pdf-generate is reserved for the legacy PDF contract");
|
|
42254
|
+
assertPureContract(b4.executionContract);
|
|
42255
|
+
if (typeof b4.tenantId !== "string" || !b4.tenantId.trim() || b4.imageDigest !== c4.imageDigest)
|
|
42256
|
+
throw Error("Pure runtime review must bind tenant and configured image");
|
|
42257
|
+
} else if (b4.slug !== "pdf-generate")
|
|
42258
|
+
throw Error("Missing reviewed execution contract");
|
|
42259
|
+
const key = JSON.stringify([b4.executionContract ? b4.tenantId : null, b4.slug, b4.version, b4.sha256]);
|
|
42260
|
+
if (seen.has(key))
|
|
42261
|
+
throw Error("Ambiguous runtime review");
|
|
42262
|
+
seen.add(key);
|
|
42263
|
+
}
|
|
41960
42264
|
return c4;
|
|
41961
42265
|
}
|
|
41962
42266
|
function runtimeInput(value) {
|
|
@@ -41992,7 +42296,7 @@ function verifyRuntimeToken(token, key, admission, attempt, now = Date.now()) {
|
|
|
41992
42296
|
const actual = Buffer.from(token);
|
|
41993
42297
|
return actual.length === expected.length && timingSafeEqual2(actual, expected);
|
|
41994
42298
|
}
|
|
41995
|
-
var hashBytes = (bytes) =>
|
|
42299
|
+
var hashBytes = (bytes) => createHash11("sha256").update(bytes).digest("hex");
|
|
41996
42300
|
|
|
41997
42301
|
// src/server/runtime-api.ts
|
|
41998
42302
|
async function createRuntimeService(options) {
|
|
@@ -42068,7 +42372,7 @@ async function body3(request, max) {
|
|
|
42068
42372
|
}
|
|
42069
42373
|
chunks.push(v2.value);
|
|
42070
42374
|
}
|
|
42071
|
-
return
|
|
42375
|
+
return uniqueJson(Buffer.concat(chunks).toString("utf8"));
|
|
42072
42376
|
}
|
|
42073
42377
|
var scope = (p2, action) => p2.scopes.includes("*") || p2.scopes.includes("runs:*") || p2.scopes.includes(action) || action === "runs:read" && p2.scopes.includes("skills:read");
|
|
42074
42378
|
function publicJob(job) {
|
|
@@ -42082,6 +42386,7 @@ function publicJob(job) {
|
|
|
42082
42386
|
bundleDigest: a4.bundleDigest,
|
|
42083
42387
|
inputDigest: a4.inputDigest,
|
|
42084
42388
|
runtimeImageDigest: a4.runtimeImageDigest,
|
|
42389
|
+
executionContract: a4.executionContract ?? { id: "pdf.v1" },
|
|
42085
42390
|
status: job.execution.status,
|
|
42086
42391
|
createdAt: a4.createdAt,
|
|
42087
42392
|
updatedAt: job.execution.updatedAt,
|
|
@@ -42092,6 +42397,33 @@ function publicJob(job) {
|
|
|
42092
42397
|
artifacts: (job.result?.artifacts ?? []).map(({ base64, ...meta }) => meta)
|
|
42093
42398
|
};
|
|
42094
42399
|
}
|
|
42400
|
+
function findReview(service, tenant, slug, version2, sha256) {
|
|
42401
|
+
return service.config.reviewedBundles.find((b4) => b4.slug === slug && b4.version === version2 && b4.sha256 === sha256 && (!b4.executionContract || b4.tenantId === tenant && b4.imageDigest === service.config.imageDigest));
|
|
42402
|
+
}
|
|
42403
|
+
function sameContract(admission, contract, image) {
|
|
42404
|
+
return digestInput(admission.executionContract ?? null) === digestInput(contract ?? null) && (!contract || admission.runtimeImageDigest === image);
|
|
42405
|
+
}
|
|
42406
|
+
|
|
42407
|
+
class RuntimeBundleError extends Error {
|
|
42408
|
+
code;
|
|
42409
|
+
status;
|
|
42410
|
+
constructor(code, status) {
|
|
42411
|
+
super(code);
|
|
42412
|
+
this.code = code;
|
|
42413
|
+
this.status = status;
|
|
42414
|
+
}
|
|
42415
|
+
}
|
|
42416
|
+
async function reviewedBytes(service, principal, digest2) {
|
|
42417
|
+
const bundle = await service.productStore.getSkillBundle(principal, digest2);
|
|
42418
|
+
if (!bundle)
|
|
42419
|
+
throw new RuntimeBundleError("BUNDLE_UNAVAILABLE", 503);
|
|
42420
|
+
if (bundle.byteSize > RUNTIME_MAX_BUNDLE_BYTES)
|
|
42421
|
+
throw new RuntimeBundleError("BUNDLE_LIMIT", 413);
|
|
42422
|
+
const bytes = await service.artifacts.readBundle(bundle);
|
|
42423
|
+
if (!bytes || bytes.length > RUNTIME_MAX_BUNDLE_BYTES || hashBytes(bytes) !== digest2)
|
|
42424
|
+
throw new RuntimeBundleError("BUNDLE_INTEGRITY_FAILED", 503);
|
|
42425
|
+
return bytes;
|
|
42426
|
+
}
|
|
42095
42427
|
async function handleRuntimeApiRequest(request, principal, service) {
|
|
42096
42428
|
const segments = path(request);
|
|
42097
42429
|
if (segments[0] !== "api" || segments[1] !== "v1" || segments[2] !== "executions")
|
|
@@ -42104,9 +42436,50 @@ async function handleRuntimeApiRequest(request, principal, service) {
|
|
|
42104
42436
|
if (!id || segments.length > 6 || !/^[a-z0-9_-]+$/.test(id))
|
|
42105
42437
|
return refusal("INVALID_EXECUTION_PATH", 400);
|
|
42106
42438
|
try {
|
|
42439
|
+
if (request.method === "GET" && sub === "eligibility" && segments.length === 5) {
|
|
42440
|
+
const query = new URL(request.url).searchParams;
|
|
42441
|
+
const versionName = query.get("version");
|
|
42442
|
+
if (!versionName || !/^\d+\.\d+\.\d+$/.test(versionName))
|
|
42443
|
+
return refusal("REVIEWED_VERSION_REQUIRED", 400);
|
|
42444
|
+
const skill = await service.productStore.getSkill(principal, id);
|
|
42445
|
+
if (skill?.lifecycle === "archived")
|
|
42446
|
+
return refusal("SKILL_ARCHIVED", 410);
|
|
42447
|
+
const version2 = skill && !skill.tombstonedAt && await service.productStore.getSkillVersion(principal, id, versionName);
|
|
42448
|
+
if (!version2)
|
|
42449
|
+
return refusal("SKILL_VERSION_NOT_FOUND", 404);
|
|
42450
|
+
const pinned = query.get("bundleDigest");
|
|
42451
|
+
if (pinned && pinned.replace(/^sha256:/, "") !== version2.bundleSha256)
|
|
42452
|
+
return refusal("SELECTION_DIGEST_MISMATCH", 409);
|
|
42453
|
+
const review = findReview(service, principal.orgId, id, versionName, version2.bundleSha256);
|
|
42454
|
+
if (!review)
|
|
42455
|
+
return json3({
|
|
42456
|
+
contractVersion: 1,
|
|
42457
|
+
eligible: false,
|
|
42458
|
+
reason: "BUNDLE_NOT_REVIEWED_FOR_CLOUD",
|
|
42459
|
+
skill: id,
|
|
42460
|
+
version: versionName,
|
|
42461
|
+
bundleDigest: version2.bundleSha256
|
|
42462
|
+
});
|
|
42463
|
+
if (review.executionContract) {
|
|
42464
|
+
const bytes = await reviewedBytes(service, principal, version2.bundleSha256);
|
|
42465
|
+
await inspectPureBundle(bytes, review.executionContract);
|
|
42466
|
+
}
|
|
42467
|
+
return json3({
|
|
42468
|
+
contractVersion: 1,
|
|
42469
|
+
eligible: true,
|
|
42470
|
+
skill: id,
|
|
42471
|
+
version: versionName,
|
|
42472
|
+
bundleDigest: version2.bundleSha256,
|
|
42473
|
+
runtimeImageDigest: service.config.imageDigest,
|
|
42474
|
+
executionContract: review.executionContract ?? { id: "pdf.v1" },
|
|
42475
|
+
secrets: "none",
|
|
42476
|
+
egress: "deny",
|
|
42477
|
+
...review.executionContract ? { descriptor: PURE_DESCRIPTOR, limits: PURE_LIMITS } : {}
|
|
42478
|
+
});
|
|
42479
|
+
}
|
|
42107
42480
|
if (request.method === "POST" && !sub) {
|
|
42108
42481
|
const raw = await body3(request, 150000);
|
|
42109
|
-
if (
|
|
42482
|
+
if (typeof raw.version !== "string" || !/^\d+\.\d+\.\d+$/.test(raw.version))
|
|
42110
42483
|
return refusal("REVIEWED_VERSION_REQUIRED", 400);
|
|
42111
42484
|
if (Object.keys(raw).some((k4) => ![
|
|
42112
42485
|
"version",
|
|
@@ -42118,31 +42491,32 @@ async function handleRuntimeApiRequest(request, principal, service) {
|
|
|
42118
42491
|
return refusal("UNSUPPORTED_EXECUTION_FIELD", 400);
|
|
42119
42492
|
if (raw.workspaceId !== undefined && raw.workspaceId !== principal.orgId)
|
|
42120
42493
|
return refusal("SELECTION_WORKSPACE_MISMATCH", 409);
|
|
42121
|
-
const input = runtimeInput(raw.input);
|
|
42122
42494
|
const key = request.headers.get("idempotency-key") ?? raw.idempotencyKey;
|
|
42123
42495
|
if (typeof key !== "string" || !/^[A-Za-z0-9._:-]{1,128}$/.test(key))
|
|
42124
42496
|
return refusal("IDEMPOTENCY_KEY_REQUIRED", 400);
|
|
42125
42497
|
const skill = await service.productStore.getSkill(principal, id);
|
|
42126
42498
|
if (!skill || skill.tombstonedAt)
|
|
42127
42499
|
return refusal("SKILL_NOT_FOUND", 404);
|
|
42500
|
+
if (skill.lifecycle === "archived")
|
|
42501
|
+
return refusal("SKILL_ARCHIVED", 410);
|
|
42128
42502
|
const version2 = await service.productStore.getSkillVersion(principal, id, raw.version);
|
|
42129
42503
|
if (!version2)
|
|
42130
42504
|
return refusal("SKILL_VERSION_NOT_FOUND", 404);
|
|
42131
42505
|
if (raw.bundleDigest !== undefined && (typeof raw.bundleDigest !== "string" || raw.bundleDigest.replace(/^sha256:/, "") !== version2.bundleSha256))
|
|
42132
42506
|
return refusal("SELECTION_DIGEST_MISMATCH", 409);
|
|
42133
|
-
|
|
42507
|
+
const review = findReview(service, principal.orgId, id, raw.version, version2.bundleSha256);
|
|
42508
|
+
if (!review)
|
|
42134
42509
|
return refusal("BUNDLE_NOT_REVIEWED_FOR_CLOUD", 403);
|
|
42510
|
+
const contract = review.executionContract;
|
|
42511
|
+
if (contract && (raw.workspaceId !== principal.orgId || typeof raw.bundleDigest !== "string"))
|
|
42512
|
+
return refusal("PURE_SELECTION_REQUIRED", 400);
|
|
42513
|
+
const input = contract ? pureInput(raw.input) : runtimeInput(raw.input);
|
|
42135
42514
|
const existing = await service.store.getRunByKey(principal.orgId, key);
|
|
42136
|
-
if (existing && (existing.admission.skillId !== id || existing.admission.skillVersion !== raw.version || existing.admission.bundleDigest !== version2.bundleSha256 || existing.admission.inputDigest !== digestInput(input)))
|
|
42515
|
+
if (existing && (existing.admission.skillId !== id || existing.admission.skillVersion !== raw.version || existing.admission.bundleDigest !== version2.bundleSha256 || existing.admission.inputDigest !== digestInput(input) || !sameContract(existing.admission, contract, service.config.imageDigest)))
|
|
42137
42516
|
return refusal("IDEMPOTENCY_CONFLICT", 409);
|
|
42138
|
-
const
|
|
42139
|
-
if (
|
|
42140
|
-
|
|
42141
|
-
if (bundle.byteSize > RUNTIME_MAX_BUNDLE_BYTES)
|
|
42142
|
-
return refusal("BUNDLE_LIMIT", 413);
|
|
42143
|
-
const bytes = await service.artifacts.readBundle(bundle);
|
|
42144
|
-
if (!bytes || hashBytes(bytes) !== version2.bundleSha256)
|
|
42145
|
-
return refusal("BUNDLE_INTEGRITY_FAILED", 503);
|
|
42517
|
+
const bytes = await reviewedBytes(service, principal, version2.bundleSha256);
|
|
42518
|
+
if (contract)
|
|
42519
|
+
await inspectPureBundle(bytes, contract);
|
|
42146
42520
|
const admission = createSubmitRunService({
|
|
42147
42521
|
store: service.store,
|
|
42148
42522
|
imageProfiles: createImageProfileRegistry({
|
|
@@ -42164,12 +42538,13 @@ async function handleRuntimeApiRequest(request, principal, service) {
|
|
|
42164
42538
|
input,
|
|
42165
42539
|
idempotencyKey: key,
|
|
42166
42540
|
runtime: "bun",
|
|
42167
|
-
|
|
42541
|
+
...contract ? { executionContract: contract } : {},
|
|
42542
|
+
limits: contract ? PURE_LIMITS : {
|
|
42168
42543
|
maxDurationMs: RUNTIME_TIMEOUT_MS,
|
|
42169
42544
|
maxArtifactsBytes: RUNTIME_MAX_ARTIFACT_BYTES
|
|
42170
42545
|
}
|
|
42171
42546
|
});
|
|
42172
|
-
if (run.tenantId !== principal.orgId || run.skillId !== id || run.skillVersion !== raw.version || run.bundleDigest !== version2.bundleSha256 || run.inputDigest !== digestInput(input))
|
|
42547
|
+
if (run.tenantId !== principal.orgId || run.skillId !== id || run.skillVersion !== raw.version || run.bundleDigest !== version2.bundleSha256 || run.inputDigest !== digestInput(input) || !sameContract(run, contract, service.config.imageDigest))
|
|
42173
42548
|
return refusal("IDEMPOTENCY_CONFLICT", 409);
|
|
42174
42549
|
await service.store.mutate(run.runId, (j4) => {
|
|
42175
42550
|
if (!j4.input) {
|
|
@@ -42261,6 +42636,8 @@ async function handleRuntimeApiRequest(request, principal, service) {
|
|
|
42261
42636
|
}
|
|
42262
42637
|
return refusal("EXECUTION_ROUTE_NOT_FOUND", 404);
|
|
42263
42638
|
} catch (error) {
|
|
42639
|
+
if (error instanceof RuntimeBundleError)
|
|
42640
|
+
return refusal(error.code, error.status);
|
|
42264
42641
|
if (error.message === "Runtime concurrency limit reached")
|
|
42265
42642
|
return refusal("RUNTIME_CONCURRENCY_LIMIT", 429);
|
|
42266
42643
|
return refusal("EXECUTION_REQUEST_FAILED", 400, error.message.startsWith("PDF ") ? error.message : "Execution request could not be completed");
|
|
@@ -42291,7 +42668,7 @@ async function handleRuntimeWorkerRequest(request, service) {
|
|
|
42291
42668
|
}
|
|
42292
42669
|
if (request.method === "POST" && s2[4] === "complete") {
|
|
42293
42670
|
try {
|
|
42294
|
-
const result = validatedResult(await body3(request, 3000000));
|
|
42671
|
+
const result = validatedResult(await body3(request, 3000000), job);
|
|
42295
42672
|
await service.store.mutate(s2[3], (j4) => {
|
|
42296
42673
|
if (j4.execution.status === "cancelled")
|
|
42297
42674
|
throw Error("Execution cancelled");
|
|
@@ -42328,12 +42705,28 @@ async function handleRuntimeWorkerRequest(request, service) {
|
|
|
42328
42705
|
}
|
|
42329
42706
|
return refusal("METHOD_NOT_ALLOWED", 405);
|
|
42330
42707
|
}
|
|
42331
|
-
function validatedResult(value) {
|
|
42708
|
+
function validatedResult(value, job) {
|
|
42332
42709
|
if (!value || typeof value !== "object")
|
|
42333
42710
|
throw Error("Invalid result");
|
|
42334
42711
|
const v2 = value;
|
|
42335
42712
|
if (!Number.isInteger(v2.exitCode) || v2.exitCode < 0 || v2.exitCode > 255 || typeof v2.stdout !== "string" || typeof v2.stderr !== "string" || Buffer.byteLength(v2.stdout) > RUNTIME_MAX_LOG_BYTES || Buffer.byteLength(v2.stderr) > RUNTIME_MAX_LOG_BYTES || !Array.isArray(v2.artifacts) || v2.artifacts.length > 2)
|
|
42336
42713
|
throw Error("Invalid result");
|
|
42714
|
+
if (job.execution.admission.executionContract) {
|
|
42715
|
+
assertPureAdmission(job.execution.admission);
|
|
42716
|
+
if (v2.artifacts.length || Buffer.byteLength(v2.stdout) > PURE_DESCRIPTOR.stdoutBytes || Buffer.byteLength(v2.stderr) > PURE_DESCRIPTOR.stderrBytes)
|
|
42717
|
+
throw Error("Invalid pure result limits");
|
|
42718
|
+
if (v2.exitCode === 0)
|
|
42719
|
+
validatePureOutput(v2.stdout, pureInput(job.input));
|
|
42720
|
+
return {
|
|
42721
|
+
exitCode: v2.exitCode,
|
|
42722
|
+
stdout: v2.stdout,
|
|
42723
|
+
stderr: v2.stderr,
|
|
42724
|
+
artifacts: [],
|
|
42725
|
+
...typeof v2.error === "string" ? { error: v2.error.slice(0, 500) } : {}
|
|
42726
|
+
};
|
|
42727
|
+
}
|
|
42728
|
+
if (job.execution.admission.skillId !== "pdf-generate")
|
|
42729
|
+
throw Error("Missing execution contract");
|
|
42337
42730
|
let total = 0;
|
|
42338
42731
|
const seen = new Set;
|
|
42339
42732
|
for (const a4 of v2.artifacts) {
|
|
@@ -42510,8 +42903,22 @@ class MemorySkillsStore {
|
|
|
42510
42903
|
async ensureBootstrapApiKey(token, principal) {
|
|
42511
42904
|
this.addApiKey(token, principal);
|
|
42512
42905
|
}
|
|
42513
|
-
async authenticateApiKeyHash(
|
|
42514
|
-
return this.apiKeys.get(
|
|
42906
|
+
async authenticateApiKeyHash(hash2) {
|
|
42907
|
+
return this.apiKeys.get(hash2) ?? null;
|
|
42908
|
+
}
|
|
42909
|
+
async updateApiKeyScopes(actor, keyId, expectedScopes, addScopes) {
|
|
42910
|
+
for (const [hash2, target] of this.apiKeys) {
|
|
42911
|
+
if (target.apiKeyId !== keyId || target.orgId !== actor.orgId)
|
|
42912
|
+
continue;
|
|
42913
|
+
if (target.scopes.length !== expectedScopes.length || target.scopes.some((scope2, index) => scope2 !== expectedScopes[index])) {
|
|
42914
|
+
return { kind: "stale", scopes: [...target.scopes] };
|
|
42915
|
+
}
|
|
42916
|
+
const scopes = [...target.scopes, ...addScopes.filter((scope2) => !target.scopes.includes(scope2))];
|
|
42917
|
+
const updated = { ...target, scopes };
|
|
42918
|
+
this.apiKeys.set(hash2, updated);
|
|
42919
|
+
return { kind: "updated", scopes };
|
|
42920
|
+
}
|
|
42921
|
+
return { kind: "not_found" };
|
|
42515
42922
|
}
|
|
42516
42923
|
async createRun(input) {
|
|
42517
42924
|
const idemKey = input.idempotencyKey?.trim();
|
|
@@ -42642,7 +43049,11 @@ class MemorySkillsStore {
|
|
|
42642
43049
|
createdAt: previous?.createdAt ?? now,
|
|
42643
43050
|
updatedAt: now,
|
|
42644
43051
|
revisionId: revisionIdOfRecord(recordFieldsOf(input, carriedBundle, carriedSkillMd)),
|
|
42645
|
-
revisionNumber: (previous?.revisionNumber ?? 0) + 1
|
|
43052
|
+
revisionNumber: (previous?.revisionNumber ?? 0) + 1,
|
|
43053
|
+
lifecycle: previous?.lifecycle ?? "active",
|
|
43054
|
+
...previous?.archivedAt ? { archivedAt: previous.archivedAt } : {},
|
|
43055
|
+
...previous?.archiveReason ? { archiveReason: previous.archiveReason } : {},
|
|
43056
|
+
...previous?.replacementSlug ? { replacementSlug: previous.replacementSlug } : {}
|
|
42646
43057
|
};
|
|
42647
43058
|
this.skills.set(key, record);
|
|
42648
43059
|
if (input.version && versionSha && !this.versions.has(versionKey(input.principal.orgId, input.slug, input.version))) {
|
|
@@ -42678,6 +43089,33 @@ class MemorySkillsStore {
|
|
|
42678
43089
|
const skill = this.skills.get(skillKey(principal.orgId, slug));
|
|
42679
43090
|
return skill && skill.orgId === principal.orgId ? skill : null;
|
|
42680
43091
|
}
|
|
43092
|
+
async setSkillLifecycle(principal, slug, patch, expectedRevisionId) {
|
|
43093
|
+
const current = await this.getSkill(principal, slug);
|
|
43094
|
+
if (!current || current.tombstonedAt)
|
|
43095
|
+
return null;
|
|
43096
|
+
if (expectedRevisionId !== current.revisionId)
|
|
43097
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
43098
|
+
if (patch.lifecycle === "archived" && current.lifecycle !== "archived") {
|
|
43099
|
+
const profiles = await this.selectionStore.profilesReferencingSkill(principal, slug);
|
|
43100
|
+
if (profiles.length)
|
|
43101
|
+
throw new SkillLifecycleConflictError(slug, profiles);
|
|
43102
|
+
}
|
|
43103
|
+
const next2 = {
|
|
43104
|
+
...current,
|
|
43105
|
+
lifecycle: patch.lifecycle,
|
|
43106
|
+
updatedAt: nowIso(),
|
|
43107
|
+
revisionId: revisionIdOfRecord({ ...current, lifecycle: patch.lifecycle, archiveReason: patch.reason, replacementSlug: patch.replacementSlug }),
|
|
43108
|
+
revisionNumber: current.revisionNumber + 1,
|
|
43109
|
+
...patch.lifecycle === "archived" ? { archivedAt: current.archivedAt ?? nowIso(), ...patch.reason ? { archiveReason: patch.reason } : {}, ...patch.replacementSlug ? { replacementSlug: patch.replacementSlug } : {} } : {}
|
|
43110
|
+
};
|
|
43111
|
+
if (patch.lifecycle === "active") {
|
|
43112
|
+
delete next2.archivedAt;
|
|
43113
|
+
delete next2.archiveReason;
|
|
43114
|
+
delete next2.replacementSlug;
|
|
43115
|
+
}
|
|
43116
|
+
this.skills.set(skillKey(principal.orgId, slug), next2);
|
|
43117
|
+
return next2;
|
|
43118
|
+
}
|
|
42681
43119
|
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
42682
43120
|
const current = await this.getSkill(principal, slug);
|
|
42683
43121
|
if (!current || current.tombstonedAt)
|
|
@@ -42748,7 +43186,7 @@ class MemorySkillsStore {
|
|
|
42748
43186
|
await this.purgeExpiredTombstones(principal);
|
|
42749
43187
|
const tags = new Set;
|
|
42750
43188
|
for (const skill of this.skills.values()) {
|
|
42751
|
-
if (skill.orgId !== principal.orgId)
|
|
43189
|
+
if (skill.orgId !== principal.orgId || skill.lifecycle !== "active")
|
|
42752
43190
|
continue;
|
|
42753
43191
|
for (const tag of skill.tags) {
|
|
42754
43192
|
if (tag.trim())
|
|
@@ -42759,13 +43197,13 @@ class MemorySkillsStore {
|
|
|
42759
43197
|
}
|
|
42760
43198
|
async listSkillsByTag(principal, tag) {
|
|
42761
43199
|
await this.purgeExpiredTombstones(principal);
|
|
42762
|
-
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag)).sort((a4, b4) => a4.slug.localeCompare(b4.slug));
|
|
43200
|
+
return Array.from(this.skills.values()).filter((skill) => skill.orgId === principal.orgId && !skill.tombstonedAt && skill.lifecycle === "active" && skill.tags.includes(tag)).sort((a4, b4) => a4.slug.localeCompare(b4.slug));
|
|
42763
43201
|
}
|
|
42764
43202
|
async listPinsByTag(principal, tag) {
|
|
42765
43203
|
await this.purgeExpiredTombstones(principal);
|
|
42766
43204
|
const taggedSlugs = new Set;
|
|
42767
43205
|
for (const skill of this.skills.values()) {
|
|
42768
|
-
if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.tags.includes(tag))
|
|
43206
|
+
if (skill.orgId === principal.orgId && !skill.tombstonedAt && skill.lifecycle === "active" && skill.tags.includes(tag))
|
|
42769
43207
|
taggedSlugs.add(skill.slug);
|
|
42770
43208
|
}
|
|
42771
43209
|
return Array.from(this.pins.values()).filter((pin) => pin.orgId === principal.orgId && pin.principal === principal.apiKeyId && taggedSlugs.has(pin.slug)).sort((a4, b4) => a4.slug.localeCompare(b4.slug));
|
|
@@ -42856,7 +43294,7 @@ class PostgresSkillsStore {
|
|
|
42856
43294
|
ON CONFLICT (key_hash) DO NOTHING
|
|
42857
43295
|
`;
|
|
42858
43296
|
}
|
|
42859
|
-
async authenticateApiKeyHash(
|
|
43297
|
+
async authenticateApiKeyHash(hash2) {
|
|
42860
43298
|
const rows = await this.sql`
|
|
42861
43299
|
SELECT k.id AS api_key_id, k.scopes_json, o.id AS org_id, o.slug AS org_slug, o.name AS org_name,
|
|
42862
43300
|
u.id AS user_id, u.email, m.role
|
|
@@ -42864,7 +43302,7 @@ class PostgresSkillsStore {
|
|
|
42864
43302
|
JOIN organizations o ON o.id = k.org_id
|
|
42865
43303
|
JOIN users u ON u.id = k.user_id
|
|
42866
43304
|
LEFT JOIN organization_members m ON m.org_id = k.org_id AND m.user_id = k.user_id
|
|
42867
|
-
WHERE k.key_hash = ${
|
|
43305
|
+
WHERE k.key_hash = ${hash2} AND k.revoked_at IS NULL
|
|
42868
43306
|
LIMIT 1
|
|
42869
43307
|
`;
|
|
42870
43308
|
const row = rows[0];
|
|
@@ -42882,6 +43320,38 @@ class PostgresSkillsStore {
|
|
|
42882
43320
|
scopes: parseJsonArray(row.scopes_json)
|
|
42883
43321
|
};
|
|
42884
43322
|
}
|
|
43323
|
+
async updateApiKeyScopes(actor, keyId, expectedScopes, addScopes) {
|
|
43324
|
+
return this.sql.begin(async (tx) => {
|
|
43325
|
+
const rows = await tx`
|
|
43326
|
+
SELECT scopes_json FROM api_keys
|
|
43327
|
+
WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL
|
|
43328
|
+
LIMIT 1
|
|
43329
|
+
`;
|
|
43330
|
+
const row = rows[0];
|
|
43331
|
+
if (!row)
|
|
43332
|
+
return { kind: "not_found" };
|
|
43333
|
+
const current = parseJsonArray(row.scopes_json);
|
|
43334
|
+
if (current.length !== expectedScopes.length || current.some((scope2, index) => scope2 !== expectedScopes[index])) {
|
|
43335
|
+
return { kind: "stale", scopes: current };
|
|
43336
|
+
}
|
|
43337
|
+
const scopes = [...current, ...addScopes.filter((scope2) => !current.includes(scope2))];
|
|
43338
|
+
const updated = await tx`
|
|
43339
|
+
UPDATE api_keys SET scopes_json = ${JSON.stringify(scopes)}::jsonb
|
|
43340
|
+
WHERE id = ${keyId} AND org_id = ${actor.orgId} AND revoked_at IS NULL
|
|
43341
|
+
AND scopes_json = ${JSON.stringify(expectedScopes)}::jsonb
|
|
43342
|
+
RETURNING scopes_json
|
|
43343
|
+
`;
|
|
43344
|
+
if (!updated[0]) {
|
|
43345
|
+
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`;
|
|
43346
|
+
return { kind: "stale", scopes: latest[0] ? parseJsonArray(latest[0].scopes_json) : current };
|
|
43347
|
+
}
|
|
43348
|
+
await tx`
|
|
43349
|
+
INSERT INTO skills_audit_events (org_id, user_id, api_key_id, action, target_type, target_id, metadata_json)
|
|
43350
|
+
VALUES (${actor.orgId}, ${actor.userId}, ${actor.apiKeyId}, ${"api_key_scopes_added"}, ${"api_key"}, ${keyId}, ${JSON.stringify({ added: addScopes, scopes })}::jsonb)
|
|
43351
|
+
`;
|
|
43352
|
+
return { kind: "updated", scopes };
|
|
43353
|
+
});
|
|
43354
|
+
}
|
|
42885
43355
|
async withContext(orgId, worker, fn) {
|
|
42886
43356
|
return this.sql.begin(async (tx) => {
|
|
42887
43357
|
await tx`SELECT set_config('app.skills_org_id', ${orgId ?? ""}, true)`;
|
|
@@ -43202,6 +43672,42 @@ class PostgresSkillsStore {
|
|
|
43202
43672
|
const rows = await this.sql`SELECT * FROM skills_registry WHERE org_id = ${principal.orgId} AND slug = ${slug} LIMIT 1`;
|
|
43203
43673
|
return rows[0] ? rowToSkill(rows[0]) : null;
|
|
43204
43674
|
}
|
|
43675
|
+
async setSkillLifecycle(principal, slug, patch, expectedRevisionId) {
|
|
43676
|
+
return this.sql.begin(async (tx) => {
|
|
43677
|
+
const currentRows = await tx`SELECT * FROM skills_registry WHERE org_id=${principal.orgId} AND slug=${slug} LIMIT 1 FOR UPDATE`;
|
|
43678
|
+
const current = currentRows[0] ? rowToSkill(currentRows[0]) : null;
|
|
43679
|
+
if (!current || current.tombstonedAt)
|
|
43680
|
+
return null;
|
|
43681
|
+
if (expectedRevisionId !== current.revisionId)
|
|
43682
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, current.revisionId);
|
|
43683
|
+
if (patch.lifecycle === "archived" && current.lifecycle !== "archived") {
|
|
43684
|
+
const profiles = await tx`SELECT profile_id,selections_json FROM skills_profiles WHERE org_id=${principal.orgId}`;
|
|
43685
|
+
const referencing = profiles.filter((profile2) => {
|
|
43686
|
+
const selections2 = typeof profile2.selections_json === "string" ? JSON.parse(profile2.selections_json) : profile2.selections_json;
|
|
43687
|
+
return Array.isArray(selections2) && selections2.some((selection2) => selection2?.slug === slug);
|
|
43688
|
+
}).map((profile2) => String(profile2.profile_id)).sort();
|
|
43689
|
+
if (referencing.length)
|
|
43690
|
+
throw new SkillLifecycleConflictError(slug, referencing);
|
|
43691
|
+
}
|
|
43692
|
+
const next2 = {
|
|
43693
|
+
...current,
|
|
43694
|
+
lifecycle: patch.lifecycle,
|
|
43695
|
+
updatedAt: nowIso(),
|
|
43696
|
+
revisionId: revisionIdOfRecord({ ...current, lifecycle: patch.lifecycle, archiveReason: patch.reason, replacementSlug: patch.replacementSlug }),
|
|
43697
|
+
revisionNumber: current.revisionNumber + 1,
|
|
43698
|
+
...patch.lifecycle === "archived" ? { archivedAt: current.archivedAt ?? nowIso(), ...patch.reason ? { archiveReason: patch.reason } : {}, ...patch.replacementSlug ? { replacementSlug: patch.replacementSlug } : {} } : {}
|
|
43699
|
+
};
|
|
43700
|
+
if (patch.lifecycle === "active") {
|
|
43701
|
+
delete next2.archivedAt;
|
|
43702
|
+
delete next2.archiveReason;
|
|
43703
|
+
delete next2.replacementSlug;
|
|
43704
|
+
}
|
|
43705
|
+
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 *`;
|
|
43706
|
+
if (!rows[0])
|
|
43707
|
+
throw new SkillRevisionConflictError(slug, expectedRevisionId, (await this.getSkill(principal, slug))?.revisionId ?? null);
|
|
43708
|
+
return rowToSkill(rows[0]);
|
|
43709
|
+
});
|
|
43710
|
+
}
|
|
43205
43711
|
async updateSkill(principal, slug, patch, expectedRevisionId) {
|
|
43206
43712
|
const current = await this.getSkill(principal, slug);
|
|
43207
43713
|
if (!current || current.tombstonedAt)
|
|
@@ -43337,7 +43843,7 @@ class PostgresSkillsStore {
|
|
|
43337
43843
|
async listTags(principal) {
|
|
43338
43844
|
await this.purgeExpiredTombstones(principal);
|
|
43339
43845
|
const rows = await this.sql`
|
|
43340
|
-
SELECT DISTINCT tag FROM skills_tags WHERE org_id = ${principal.orgId} ORDER BY tag ASC
|
|
43846
|
+
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
|
|
43341
43847
|
`;
|
|
43342
43848
|
return rows.map((row) => String(row.tag));
|
|
43343
43849
|
}
|
|
@@ -43346,7 +43852,7 @@ class PostgresSkillsStore {
|
|
|
43346
43852
|
const rows = await this.sql`
|
|
43347
43853
|
SELECT s.* FROM skills_registry s
|
|
43348
43854
|
JOIN skills_tags t ON t.org_id = s.org_id AND t.slug = s.slug
|
|
43349
|
-
WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
43855
|
+
WHERE t.org_id = ${principal.orgId} AND t.tag = ${tag} AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
|
|
43350
43856
|
ORDER BY s.slug ASC
|
|
43351
43857
|
`;
|
|
43352
43858
|
return rows.map(rowToSkill);
|
|
@@ -43358,7 +43864,7 @@ class PostgresSkillsStore {
|
|
|
43358
43864
|
JOIN skills_tags t ON t.org_id = p.org_id AND t.slug = p.slug
|
|
43359
43865
|
JOIN skills_registry s ON s.org_id = p.org_id AND s.slug = p.slug
|
|
43360
43866
|
WHERE p.org_id = ${principal.orgId} AND p.principal = ${principal.apiKeyId}
|
|
43361
|
-
AND t.tag = ${tag} AND s.tombstoned_at IS NULL
|
|
43867
|
+
AND t.tag = ${tag} AND s.tombstoned_at IS NULL AND s.lifecycle = 'active'
|
|
43362
43868
|
ORDER BY p.slug ASC
|
|
43363
43869
|
`;
|
|
43364
43870
|
return rows.map(rowToPin);
|
|
@@ -43455,7 +43961,7 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
43455
43961
|
if (request.method === "GET" && url.pathname === "/api/v1/capabilities") {
|
|
43456
43962
|
const response = await handleProfileApi(store, principal, request, ["capabilities"], config);
|
|
43457
43963
|
const payload = await response.json();
|
|
43458
|
-
return json4({ ...payload, cloudExecution: Boolean(runtime), capabilities: [...payload.capabilities, ...runtime ? ["skills.cloud-execution"] : []] });
|
|
43964
|
+
return json4({ ...payload, cloudExecution: Boolean(runtime), cloudExecutionContracts: runtime ? ["pdf.v1", "regex-test.v1"] : [], capabilities: [...payload.capabilities, ...runtime ? ["skills.cloud-execution"] : []] });
|
|
43459
43965
|
}
|
|
43460
43966
|
if (request.method === "GET" && url.pathname === "/api/auth/whoami") {
|
|
43461
43967
|
return json4(identityPayload(principal));
|
|
@@ -43479,6 +43985,9 @@ async function createSkillsFetchHandler(options = {}) {
|
|
|
43479
43985
|
attemptedBundleSha256: error.attemptedSha256
|
|
43480
43986
|
}, { status: 409 });
|
|
43481
43987
|
}
|
|
43988
|
+
if (error instanceof SkillLifecycleConflictError) {
|
|
43989
|
+
return json4({ error: error.message, code: "SKILL_ARCHIVE_PROFILE_CONFLICT", profiles: error.profiles }, { status: 409 });
|
|
43990
|
+
}
|
|
43482
43991
|
if (error instanceof SkillRevisionConflictError) {
|
|
43483
43992
|
return json4({
|
|
43484
43993
|
error: error.message,
|
|
@@ -43517,6 +44026,32 @@ async function handleApiV1(store, governanceStore, principal, request, parts, co
|
|
|
43517
44026
|
if (parts.some(segmentEscapesPath)) {
|
|
43518
44027
|
return json4({ error: "invalid path segment", code: "INVALID_PATH" }, { status: 400 });
|
|
43519
44028
|
}
|
|
44029
|
+
if (parts.length === 4 && resource === "admin" && id === "keys" && subresource && childId === "scopes" && request.method === "PATCH") {
|
|
44030
|
+
const adminScope = principal.scopes.includes("*") || principal.scopes.includes("skills:keys.admin");
|
|
44031
|
+
if (!adminScope || !["owner", "admin"].includes(principal.role)) {
|
|
44032
|
+
return json4({ error: "Key scope administration requires an owner/admin key with skills:keys.admin", code: "KEY_ADMIN_REQUIRED" }, { status: 403 });
|
|
44033
|
+
}
|
|
44034
|
+
if (!/^[A-Za-z0-9_-]+$/.test(subresource)) {
|
|
44035
|
+
return json4({ error: "Key id must be url-safe (letters, digits, '_' or '-')", code: "INVALID_KEY_ID" }, { status: 400 });
|
|
44036
|
+
}
|
|
44037
|
+
const body4 = await readJson(request, config.requestBodyLimitBytes);
|
|
44038
|
+
const expected = body4.expected_scopes;
|
|
44039
|
+
const additions = body4.add_scopes;
|
|
44040
|
+
if (!Array.isArray(expected) || !expected.every((scope2) => typeof scope2 === "string") || !Array.isArray(additions) || !additions.every((scope2) => typeof scope2 === "string")) {
|
|
44041
|
+
return json4({ error: "expected_scopes and add_scopes must be arrays of scope strings", code: "INVALID_KEY_SCOPE_UPDATE" }, { status: 400 });
|
|
44042
|
+
}
|
|
44043
|
+
if (additions.length !== 1 || additions[0] !== "skills:publish") {
|
|
44044
|
+
return json4({ error: "Only adding skills:publish to an existing Skills key is supported", code: "INVALID_KEY_SCOPE_UPDATE" }, { status: 400 });
|
|
44045
|
+
}
|
|
44046
|
+
const update = await store.updateApiKeyScopes?.(principal, subresource, expected, additions);
|
|
44047
|
+
if (!update)
|
|
44048
|
+
return json4({ error: "Key scope administration is unavailable", code: "KEY_SCOPE_UPDATE_UNAVAILABLE" }, { status: 501 });
|
|
44049
|
+
if (update.kind === "not_found")
|
|
44050
|
+
return json4({ error: "API key not found", code: "API_KEY_NOT_FOUND" }, { status: 404 });
|
|
44051
|
+
if (update.kind === "stale")
|
|
44052
|
+
return json4({ error: "API key scopes changed; re-read and retry with expected_scopes", code: "API_KEY_SCOPE_CONFLICT", keyId: subresource, scopes: update.scopes }, { status: 409 });
|
|
44053
|
+
return json4({ keyId: subresource, orgId: principal.orgId, scopes: update.scopes, updated: true });
|
|
44054
|
+
}
|
|
43520
44055
|
if (!permitsSkillsRoute(principal, request.method, resource ?? "")) {
|
|
43521
44056
|
return json4({ error: "The API key does not allow this operation", code: "INSUFFICIENT_SCOPE" }, { status: 403 });
|
|
43522
44057
|
}
|
|
@@ -43550,6 +44085,13 @@ async function handleApiV1(store, governanceStore, principal, request, parts, co
|
|
|
43550
44085
|
if (request.method === "GET" && id && subresource === "versions" && !childId) {
|
|
43551
44086
|
return json4(await listSkillVersionsPayload(store, principal, id));
|
|
43552
44087
|
}
|
|
44088
|
+
if (request.method === "PATCH" && id && subresource === "lifecycle" && !childId) {
|
|
44089
|
+
if (!["owner", "admin"].includes(principal.role))
|
|
44090
|
+
return json4({ error: "Only organization owners or admins may change skill lifecycle", code: "LIFECYCLE_ROLE_REQUIRED" }, { status: 403 });
|
|
44091
|
+
const expectedRevisionId = parseIfMatch(request.headers.get("if-match"));
|
|
44092
|
+
const updated = await store.setSkillLifecycle(principal, id, skillLifecyclePatch(await readJson(request, config.requestBodyLimitBytes)), expectedRevisionId);
|
|
44093
|
+
return updated ? json4(publishedPayload(updated), { headers: { ETag: revisionEtag(updated.revisionId) } }) : json4({ error: "published skill not found", code: "SKILL_NOT_FOUND" }, { status: 404 });
|
|
44094
|
+
}
|
|
43553
44095
|
if (request.method === "GET" && id && subresource === "versions" && childId && !parts[4]) {
|
|
43554
44096
|
const version2 = await readSkillVersion(store, principal, id, childId);
|
|
43555
44097
|
const current = (await store.getSkill(principal, id))?.bundleSha256;
|
|
@@ -43708,8 +44250,11 @@ async function handleApiV1(store, governanceStore, principal, request, parts, co
|
|
|
43708
44250
|
return json4({ error: "not found", code: "NOT_FOUND" }, { status: 404 });
|
|
43709
44251
|
}
|
|
43710
44252
|
function identityPayload(principal) {
|
|
44253
|
+
const safeKeyId = /^[A-Za-z0-9_-]{1,256}$/.test(principal.apiKeyId) ? principal.apiKeyId : undefined;
|
|
44254
|
+
const safeScopes = principal.scopes.length <= 32 && principal.scopes.every((scope2) => scope2.length <= 128 && /^(?:\*|[a-z][a-z0-9_-]*:(?:\*|[a-z][a-z0-9_-]*))$/.test(scope2)) ? [...new Set(principal.scopes)] : undefined;
|
|
43711
44255
|
return {
|
|
43712
44256
|
user: { id: principal.userId, email: principal.email, role: principal.role },
|
|
44257
|
+
...safeKeyId && safeScopes ? { apiKey: { id: safeKeyId, scopes: safeScopes } } : {},
|
|
43713
44258
|
organization: { id: principal.orgId, slug: principal.orgSlug, name: principal.orgName }
|
|
43714
44259
|
};
|
|
43715
44260
|
}
|