import type { OwnedBytes } from "../lib/skill-bundle.js"; import type { ExecutionGrantStore } from "./execution-grant-store.js"; import type { SkillSelectionStore } from "./selection-store.js"; export declare const SERVER_RUN_STATUSES: readonly ["queued", "waiting_for_approval", "running", "succeeded", "failed", "cancel_requested", "cancelled", "retrying", "expired", "refunded"]; export type ServerRunStatus = (typeof SERVER_RUN_STATUSES)[number]; /** * A fenced transition was refused because the run's lease_generation moved on. * * The worker that throws this is writing against a run it no longer owns: a * newer claim took it, or a cancellation fenced it. The run is not missing - * that is the null return - it has simply left the writer behind. */ export declare class StaleLeaseGenerationError extends Error { readonly runId: string; readonly expectedGeneration: number; readonly currentGeneration: number; readonly status: ServerRunStatus; constructor(runId: string, expectedGeneration: number, currentGeneration: number, status: ServerRunStatus); } /** * A publish or update was refused because it would silently overwrite a newer revision. * * The optimistic-concurrency guard (todos d061fcda): a write to an existing, live row * must carry If-Match naming the current revision_id (the ETag the server issued). A * missing guard, or one naming a different revision, is refused with this error instead * of being applied. `currentRevisionId` is null when the row is absent or tombstoned — * those are the two cases where the guard does not apply — and carries the live * revision otherwise, so the caller can name what the writer was racing against. */ export declare class SkillRevisionConflictError extends Error { readonly slug: string; readonly expectedRevisionId: string | null | undefined; readonly currentRevisionId: string | null; constructor(slug: string, expectedRevisionId: string | null | undefined, currentRevisionId: string | null); } /** * A publish was refused because (org, slug, version) already exists with a DIFFERENT bundle * digest. Versions are immutable (hasna/apps#1630): the same digest is idempotent, a new * digest needs a new version. Carries the stored digest so the client can say which. */ export declare class SkillVersionExistsError extends Error { readonly slug: string; readonly version: string; readonly existingSha256: string; readonly attemptedSha256: string; constructor(slug: string, version: string, existingSha256: string, attemptedSha256: string); } export declare class SkillLifecycleConflictError extends Error { readonly slug: string; readonly profiles: string[]; constructor(slug: string, profiles: string[]); } export interface ApiPrincipal { apiKeyId: string; orgId: string; orgSlug: string; orgName: string; userId: string; email: string; role: string; scopes: string[]; } export type ApiKeyScopeUpdateResult = { kind: "updated"; scopes: string[]; } | { kind: "not_found"; } | { kind: "stale"; scopes: string[]; }; /** * A maintenance task's provenance is deliberately separate from ApiPrincipal. * An ECS task is not a user and must never be represented by impersonating an * API key, user id, or role in the audit row. */ export interface OperatorScopeEnrollmentInput { keyId: string; stationId: string; orgId: string; expectedScopes: string[]; operationId: string; manifestDigest: string; operatorJobId: string; operatorTaskArn: string; } export declare function validOperatorScopeEnrollmentInput(input: OperatorScopeEnrollmentInput): boolean; export declare function validOperatorScopeList(scopes: unknown): scopes is string[]; export type OperatorScopeEnrollmentResult = { kind: "updated"; scopes: string[]; } | { kind: "already_applied"; scopes: string[]; } | { kind: "not_found"; } | { kind: "target_mismatch"; } | { kind: "invalid"; } | { kind: "stale"; scopes: string[]; }; export type OperatorScopeTargetSnapshot = { kind: "not_found"; } | { kind: "target_mismatch"; } | { kind: "found"; scopes: string[]; }; export interface ServerRunRecord { id: string; orgId: string; userId: string; skill: string; requestedSlug: string; status: ServerRunStatus; input: Record; args: string[]; idempotencyKey?: string; correlationId: string; costCents: number; /** * The cancellation fence. Bumped by every claim and by every cancellation; a * worker that transitions a run while carrying an older generation is writing * against a run it no longer owns and is rejected (see transitionRun). */ leaseGeneration: number; outputType?: string; outputPreview?: string; errorCode?: string; errorMessage?: string; createdAt: string; startedAt?: string; completedAt?: string; } export interface ServerRunLog { runId: string; sequence: number; level: "debug" | "info" | "warn" | "error"; message: string; createdAt: string; } export interface ServerArtifact { id: string; runId: string; orgId: string; fileName: string; relativePath: string; contentType: string; byteSize: number; sha256: string; storageKind: "db" | "s3"; storageKey?: string; bodyText?: string; /** * Output visibility. Runs' outputs are PRIVATE by default: the value is set at * write time from the governance default and persisted, never inferred at read * time. "public" is the explicit opt-in for artifacts the tenant intends to * share; it is not a default on any path. */ visibility: "private" | "public"; /** * Finite retention, set at write time as createdAt + configured TTL. Absent * only when the writing path opted out explicitly; the expiry sweep deletes * artifacts whose expiresAt is in the past and records a deletion receipt. */ expiresAt?: string; createdAt: string; } /** Where bytes actually live. Same vocabulary as ServerArtifact, same meaning. */ export type BlobStorageKind = "db" | "s3"; /** * A skill published to this instance by one organization. * * Distinct from `SkillMeta` (src/lib/registry-types.ts), which describes a skill the CLI * knows about from any source. This is the row: it always belongs to an org, and it * always carries the provenance of who put it there. */ export interface ServerSkillRecord { orgId: string; slug: string; displayName: string; description: string; category: string; tags: string[]; source: string; kind: "executable" | "instruction"; version?: string; /** The agent-facing document, served verbatim by GET /skills/:slug/skill.md. */ skillMd?: string; /** Digest of the stored bundle. Absent for a metadata-only publish. */ bundleSha256?: string; bundleByteSize?: number; publishedByUserId?: string; createdAt: string; updatedAt: string; /** * Immutable content-addressed revision identity (sha-256 over the published content, * computed in server/revision.ts). Same content -> same id; any content change mints * a new one. This is the ETag every read issues and every guarded write must match. */ revisionId: string; /** * Monotonic per-slug write counter. Every publish and every metadata update bumps it, * even when the content hash is unchanged, so "how many writes happened to this slug" * is a number, not a digest comparison. */ revisionNumber: number; /** * Present when the slug was deleted within the tombstone window (todos d061fcda). * Reads must answer 410 with the marker so a client's pull can reconcile; the row and * its bundle are purged once tombstonePurgeAfter passes. A re-publish clears both * fields and revives the slug as a fresh revision. */ tombstonedAt?: string; tombstonePurgeAfter?: string; /** Catalog lifecycle state. Archived rows retain versions and bundles for explicit reads. */ lifecycle: "active" | "archived"; archivedAt?: string; archiveReason?: string; replacementSlug?: string; } export interface SkillLifecyclePatch { lifecycle: "active" | "archived"; reason?: string; replacementSlug?: string; } /** * Bundle bytes, addressed by their own digest. * * `bytes` is present only when the bundle is stored in the database (`storageKind: "db"`). * For S3 the row is a pointer and SkillBundleStorage.read() fetches the object - the same * split ServerArtifact makes between `bodyText` and `storageKey`, except that a bundle is * a gzipped tar and so cannot be a string at any layer. */ /** * A skill the principal pinned on the hosted instance. * * The cloud-side twin of the local `.skills/project.json` pin: metadata-only, * never content. `principal` is the api_keys.id of the API key that pinned * (ApiPrincipal.apiKeyId) - a pin is a fact about a specific principal's * selection, and two API keys in one org each have their own pin set. */ export interface ServerPin { orgId: string; principal: string; slug: string; pinnedAt: string; metadata: Record; } export interface ServerSkillBundle { orgId: string; sha256: string; byteSize: number; contentType: string; storageKind: BlobStorageKind; storageKey?: string; bytes?: OwnedBytes; createdAt: string; } /** * One immutable published version of a skill (hasna/apps#1630). The registry row is the * mutable "current" pointer; these rows are the history: each (org, slug, version) exactly once, * pointing at a content-addressed bundle that orphan collection must keep alive. */ export interface ServerSkillVersion { orgId: string; slug: string; version: string; bundleSha256: string; bundleByteSize: number; storageKind: BlobStorageKind; storageKey?: string; /** Files (path -> sha256), byte counts and provenance the publisher sent; never secret. */ manifest: Record; publishedByUserId?: string; createdAt: string; } /** Exact version lookup, independent of the registry's mutable current version. */ export interface PublishedSkillSelection { slug: string; version: string; } /** Metadata only: bundle availability means its tenant-owned storage row exists. */ export interface PublishedSkillSelectionState extends PublishedSkillSelection { current: Pick | null; versionBundleSha256: string | null; bundleAvailable: boolean; } export interface PublishSkillInput { /** Internal boot-seed precondition: never revive or replace non-bundled records. */ seedBundledOnly?: true; principal: ApiPrincipal; slug: string; displayName: string; description: string; category: string; tags: string[]; source: string; kind: ServerSkillRecord["kind"]; version?: string; skillMd?: string; /** Omitted for a metadata-only publish or update. */ bundle?: Omit; /** * Publisher-supplied version manifest (file digests, provenance). Stored verbatim on the * version row when `version` is set; ignored otherwise. */ versionManifest?: Record; /** Version-addressed object key the API wrote (S3 mode); recorded on the version row. */ versionStorage?: { storageKind: BlobStorageKind; storageKey?: string; }; /** * Optimistic-concurrency guard (todos d061fcda): the revision_id (ETag) the writer * read. A publish against an existing, LIVE row requires the guard to name that row's * current revision_id; missing or mismatched is a SkillRevisionConflictError, never a * silent overwrite. The guard is not required for a first publish (no row exists) or * against a tombstoned row (nothing live to overwrite - the publish revives the slug * as a fresh revision). */ expectedRevisionId?: string; } /** Metadata-only patch. Never touches the bundle; republish to change bytes. */ export type UpdateSkillPatch = Partial>; export interface CreateRunInput { principal: ApiPrincipal; slug: string; input: Record; args: string[]; idempotencyKey?: string; } export interface ClaimRunInput { workerId: string; } /** * Fields a fenced transition may write, plus the lease_generation the fence * itself bumps on cancellation. Everything updateRun can write plus the * generation counter; the worker never patches the generation, the cancel * service does. */ export type RunTransitionPatch = Partial>; /** * What a store is, in the only two dimensions the server needs at boot: which backend * it is (for logs and diagnostics) and whether it survives a restart. * * `durable` exists because the server used to boot onto an in-process Map, report * `ok: true` at /health, and lose every run on restart with no warning at any point. A * store that knows it is non-durable can now say so, and startSkillsServer() refuses it * unless the operator explicitly opted in. */ export interface StoreBackendInfo { /** * The three bundled backends are named for autocomplete; the open `string` arm is * deliberate. This interface is a published seam, and a third-party store must be able * to declare what it is - "mysql", "dynamodb" - without either a type error or having * to misreport itself as one of ours. A closed union would make the seam implementable * only by us, which is the test R3 sets for it. */ kind: "postgres" | "sqlite" | "memory" | (string & {}); /** False when the data does not survive process exit. */ durable: boolean; /** Credential-free description, safe to put in logs and error messages. */ label: string; } export interface SkillsProductStore { /** * Optional so a third-party store implementing this seam keeps compiling. An * undeclared backend is treated as durable: we can refuse what a store tells us is * ephemeral, and cannot infer it about somebody else's implementation. The hazard * actually being closed is our own default, which is now SQLite on disk. */ readonly backend?: StoreBackendInfo; /** Optional for third-party stores; profile APIs fail closed when absent. */ readonly selectionStore?: SkillSelectionStore; readonly executionGrantStore?: ExecutionGrantStore; /** Release connections and file handles. Optional; not every backend holds any. */ close?(): Promise; /** * Prove the backend is actually reachable, throwing if it is not. * * Called once at startup. Bun's SQL client connects lazily, so without this a * Postgres URL pointing at a dead host produced a server that started happily and * then 500ed on the first request. */ verifyConnectivity?(): Promise; authenticateApiKeyHash(hash: string): Promise; /** Guarded metadata-only scope admission for an existing key. */ updateApiKeyScopes?(actor: ApiPrincipal, keyId: string, expectedScopes: string[], addScopes: string[]): Promise; /** Add only skills:publish to an existing key from a protected maintenance task. */ enrollPublishScopeByOperator?(input: OperatorScopeEnrollmentInput): Promise; inspectOperatorScopeTarget?(keyId: string, orgId: string): Promise; ensureBootstrapApiKey?(token: string, principal?: Partial): Promise; createRun(input: CreateRunInput): Promise; listRuns(principal: ApiPrincipal, limit: number): Promise; getRun(principal: ApiPrincipal, runId: string): Promise; claimNextRun(input: ClaimRunInput): Promise; updateRun(runId: string, patch: Partial>): Promise; /** * Generation-fenced transition, the cancellation gate. * * Optional so a third-party store implementing this seam keeps compiling; the * cancel service requires it and refuses to run without it (FENCING_UNSUPPORTED) * rather than cancelling unfenced. When present it is the ONLY transition the * worker paths use: the UPDATE re-asserts `lease_generation = ` and * reports zero rows when the generation moved on (a newer claim, or a * cancellation), so a late write from a stale worker never lands. * * Returns null when the run does not exist; throws StaleLeaseGenerationError * when the generation no longer matches. The distinction matters: "nothing * there" and "you lost the fence" are different facts. */ transitionRun?(runId: string, patch: RunTransitionPatch, expectedGeneration: number): Promise; appendLog(runId: string, orgId: string, level: ServerRunLog["level"], message: string): Promise; listLogs(principal: ApiPrincipal, runId: string): Promise; addArtifact(artifact: Omit): Promise; listArtifacts(principal: ApiPrincipal, runId: string): Promise; getArtifact(principal: ApiPrincipal, runId: string, artifactId: string): Promise; publishSkill(input: PublishSkillInput): Promise; listSkills(principal: ApiPrincipal): Promise; getSkill(principal: ApiPrincipal, slug: string): Promise; /** * Metadata-only patch, guarded by the same optimistic concurrency as publish: the * row's current revision_id must match `expectedRevisionId` or * SkillRevisionConflictError is thrown (409), never a silent overwrite. The revision * advances on every successful update (new content sha, number + 1). Returns null * when the org has no skill by that slug, or when the row is tombstoned. */ updateSkill(principal: ApiPrincipal, slug: string, patch: UpdateSkillPatch, expectedRevisionId?: string): Promise; setSkillLifecycle(principal: ApiPrincipal, slug: string, patch: SkillLifecyclePatch, expectedRevisionId?: string): Promise; /** * Tombstone a skill instead of hard-deleting it (todos d061fcda): the row is stamped * tombstoned_at + tombstone_purge_after (now + tombstoneWindowMs) and kept, so reads * can answer 410 with the marker and a pulling client can reconcile, and the bundle * survives until the purge. Returns the tombstoned record, or null when the org has * no row by that slug. A second delete of an already-tombstoned slug returns the * existing tombstone (idempotent; the window is not extended). */ deleteSkill(principal: ApiPrincipal, slug: string, tombstoneWindowMs: number): Promise; /** * Drop every tombstoned row whose window has expired, collecting the bundles nothing * (live or tombstoned) references. Called on the read paths and by listSkills so the * purge is lazy but does not wait for a slug to be read. Returns the purged records so * the caller can discard their stored objects (S3). */ purgeExpiredTombstones(principal: ApiPrincipal): Promise; getSkillBundle(principal: ApiPrincipal, sha256: string): Promise; /** Every published version of a slug, newest first (hasna/apps#1630). */ listSkillVersions(principal: ApiPrincipal, slug: string): Promise; getSkillVersion(principal: ApiPrincipal, slug: string, version: string): Promise; /** * Read at most MAX_PROFILE_SELECTIONS exact selections in one database snapshot, * scoped to the principal's org at every join. Returns one row per input in input * order, including missing selections and duplicates. Never reads bundle bytes or * substitutes the latest version; callers retain lifecycle/digest error policy. */ getPublishedSelectionStates(principal: ApiPrincipal, selections: readonly PublishedSkillSelection[]): Promise; pinSkill(principal: ApiPrincipal, slug: string, metadata?: Record): Promise; /** False when this principal has no pin by that slug. */ unpinSkill(principal: ApiPrincipal, slug: string): Promise; listPins(principal: ApiPrincipal): Promise; listTags(principal: ApiPrincipal): Promise; listSkillsByTag(principal: ApiPrincipal, tag: string): Promise; listPinsByTag(principal: ApiPrincipal, tag: string): Promise; /** * The slugs of the org's live (non-tombstoned) published skills. The tag * routes use this to resolve merged-view precedence: a bundled skill whose * slug a published row occupies must not resurface under a tag filter or in * the tag list. A single-column scan of the (org_id, slug) primary key. */ listPublishedSlugs(principal: ApiPrincipal): Promise; }