/** * Skill packs: declarative content a SaaS deployment distributes to a paired * device (plan `skill-pack-delivery-channel`, Phase 1). * * A pack is an `agentskills.io`-shaped `SKILL.md` plus its static companion * files. That is the WHOLE of it, and the narrowness is the design: * * 1. **No executable surface, by schema.** {@link SkillPackManifestSchema} is a * `strictObject`, so an unknown key is a rejection rather than a field * someone's tooling might later start honoring, and * {@link SKILL_PACK_FORBIDDEN_FIELDS} names the specific ones — `exec`, * `env`, `credential`, `hooks`, `allowed-tools` — that get their own * rejection message. The credential-isolation rule this SDK is built around * (§9.1) cannot be enforced downstream if the manifest can carry a command * to run or an environment to run it in, so it is enforced here, once. * 2. **Every declared limit has an evaluation point in this file.** A size cap * that nothing measures, a path rule that nothing checks, and a content hash * that nothing verifies are the same failure: a security claim with no * enforcement behind it. {@link checkSkillPackManifest} evaluates the * manifest-level limits and {@link checkSkillPackFileContent} evaluates the * byte-level ones, both returning a named rejection instead of a boolean, so * a caller can say which limit fired. * 3. **Hashing lives outside core.** Core has no `node:` import and no crypto * (the same rule `blob.ts` and `attestation.ts` follow), so this module owns * the canonical bytes a hash is taken over ({@link skillPackContentHashInput}) * and the comparison, never the digest itself. The installer supplies what it * observed; core decides whether that is acceptable. * * Distribution is hosted HTTP (`@byok-sdk/cloud`'s `GET /byok/skill-packs`), * declared through ADR-010 as `skills.pack`. Nothing here touches the frozen v1 * wire envelope, and there is deliberately no new message type: a pack is * content a device PULLS after reading a declaration, not a message a server * pushes into a task stream. */ import { z } from 'zod'; import { type ContentHash } from './blob'; import type { TenantId } from './tenant'; export declare const SKILL_PACK_MANIFEST_SCHEMA_ID = "byok-skill-pack-v1"; /** * Pack names: lowercase, starting alphanumeric — the same shape * `OBJECT_KEY_PREFIX_PATTERN` accepts per segment, for the same reason. A pack * name becomes a directory name on a real device filesystem, so the accepted * set is the one that reaches disk spelled exactly as declared: no case folding * to collide over, no separator, no leading dot, nothing needing escaping. */ export declare const SKILL_PACK_NAME_PATTERN: RegExp; export declare const SKILL_PACK_NAME_MAX_LENGTH = 64; export declare const SKILL_PACK_DESCRIPTION_MAX_LENGTH = 1024; /** `MAJOR.MINOR.PATCH`. A pack version orders two publications of one name; it is not a range language. */ export declare const SKILL_PACK_VERSION_PATTERN: RegExp; /** * Relative POSIX paths only: slash-joined segments of alphanumerics, `.`, `_` * and `-`, each STARTING with an alphanumeric. * * That single "starts with an alphanumeric" rule is what closes the traversal * class rather than a blocklist of spellings: `..` cannot match (a segment may * not begin with `.`), an absolute path cannot match (no leading slash), an * empty segment cannot match (`a//b`), a Windows drive or UNC path cannot match * (no `:`, no `\`), a home-relative path cannot match (no `~`), and a dotfile * cannot match either. A blocklist would have to anticipate every encoding of * the same idea; this allowlist has nothing to anticipate. */ export declare const SKILL_PACK_FILE_PATH_PATTERN: RegExp; export declare const SKILL_PACK_FILE_PATH_MAX_LENGTH = 200; /** The entry file every pack must carry, spelled as `agentskills.io` spells it. */ export declare const SKILL_PACK_ENTRY_PATH = "SKILL.md"; /** Per-file ceiling, measured in BYTES over the observed content — never in characters. */ export declare const SKILL_PACK_FILE_MAX_BYTES = 262144; /** Whole-pack ceiling, over the sum of the observed file sizes. */ export declare const SKILL_PACK_MAX_BYTES = 1048576; export declare const SKILL_PACK_MAX_FILES = 64; /** * Manifest keys that must never exist, listed so the prohibition is testable * rather than implied. * * `strictObject` already rejects every unknown key, so this list adds no new * authority — it adds a NAMED rejection for the subset that matters: an * operator who sees `unrecognized key: "env"` learns nothing about why, and a * reviewer reading the schema cannot tell whether the omission was a decision * or an oversight. Pinned from both directions by * `__tests__/skill-pack.test.ts`. */ export declare const SKILL_PACK_FORBIDDEN_FIELDS: readonly ['exec', 'command', 'entrypoint', 'run', 'script', 'shell', 'env', 'environment', 'credential', 'credentials', 'secret', 'secrets', 'token', 'apiKey', 'api_key', 'allowedTools', 'allowed-tools', 'hooks', 'preinstall', 'postinstall']; /** One file in a pack: where it goes, what it must hash to, and how big it is. */ export declare const SkillPackFileSchema: z.ZodObject<{ path: z.ZodString; contentHash: z.ZodPipe>; byteSize: z.ZodNumber; }, z.core.$strict>; export type SkillPackFile = z.infer; /** * The published description of one pack. * * `contentHash` addresses the manifest AS A WHOLE — see * {@link skillPackContentHashInput} — which is what makes an install * content-addressed: two publications that differ in any field a device can * observe land in two directories, and re-installing an unchanged pack is a * no-op rather than a partial overwrite. */ export declare const SkillPackManifestSchema: z.ZodObject<{ schema: z.ZodLiteral<"byok-skill-pack-v1">; name: z.ZodString; version: z.ZodString; description: z.ZodString; files: z.ZodArray>; byteSize: z.ZodNumber; }, z.core.$strict>>; contentHash: z.ZodPipe>; }, z.core.$strict>; export type SkillPackManifest = z.infer; /** * Parses a manifest fail-closed. * * @throws {ByokCoreError} code `skill_pack_manifest_invalid`. */ export declare function parseSkillPackManifest(input: unknown): SkillPackManifest; /** * The exact bytes `manifest.contentHash` is the sha256 of. * * Written out here, in core, because both ends have to agree on it and neither * end may derive its own version: the publisher hashes this string to mint the * address, the installer hashes it again to verify what it fetched, and a * disagreement between the two would make every install either falsely * accepted or unconditionally rejected. Newline-delimited and terminated so no * field's content can be shifted into the next field's position, and the file * rows are sorted by path so two publishers that enumerate a directory in * different orders still mint the same address. */ export declare function skillPackContentHashInput(manifest: { readonly name: string; readonly version: string; readonly description: string; readonly files: readonly SkillPackFile[]; }): string; /** Every way a pack can be refused. One name per declared limit — see {@link SkillPackCheck}. */ export declare const SKILL_PACK_REJECTIONS: readonly ['path-unsafe', 'duplicate-path', 'entry-missing', 'file-count-over-cap', 'file-over-cap', 'pack-over-cap', 'size-mismatch', 'hash-mismatch', 'name-mismatch']; export type SkillPackRejection = (typeof SKILL_PACK_REJECTIONS)[number]; /** * Outcome of a check. `bytes` is the measured total on the accept path so a * caller can record what it admitted; a rejection always names WHICH limit * fired, because "the pack was rejected" is not an operable message. */ export type SkillPackCheck = { readonly ok: true; readonly bytes: number; } | { readonly ok: false; readonly reason: SkillPackRejection; readonly detail: string; }; /** Non-throwing path-safety predicate — the one authority on what a pack may name. */ export declare function isSkillPackPathSafe(path: string): boolean; /** * The manifest-level limits, evaluated. * * Runs over a manifest that has already parsed, so the per-field caps zod owns * are not re-litigated here. What is left is everything zod cannot see: the * relationships BETWEEN files (a duplicate path, a missing entry file, a total * that clears every per-file cap and still blows the pack cap). */ export declare function checkSkillPackManifest(manifest: SkillPackManifest): SkillPackCheck; /** What an installer actually measured for one file, as opposed to what the manifest declared. */ export interface ObservedSkillPackFile { readonly byteSize: number; readonly contentHash: ContentHash; } /** * The byte-level limits, evaluated against what was actually fetched. * * The asymmetry with {@link checkSkillPackManifest} is the point: the declared * size is a claim by the publisher and the observed size is a fact about the * bytes on the device, so the cap is enforced on the OBSERVED value. A check * that only ever measured the declaration would pass a 4 KiB manifest row * attached to a 40 MiB response. */ export declare function checkSkillPackFileContent(declared: SkillPackFile, observed: ObservedSkillPackFile): SkillPackCheck; /** * `SKILL.md` frontmatter, in the `agentskills.io` shape: exactly `name` and * `description`, both required. * * The allowlist is the whole contract. `allowed-tools`, `hooks` and friends are * real fields in other skill dialects and every one of them widens what a pack * can ask a runtime to do; a parser that merely IGNORED them would still let a * pack ship them to a host that does not. */ export interface SkillFrontmatter { readonly name: string; readonly description: string; } /** The only frontmatter keys a pack may declare. */ export declare const SKILL_FRONTMATTER_FIELDS: readonly ['name', 'description']; /** * Parses and validates a `SKILL.md`'s frontmatter block. * * Deliberately not a YAML engine: this is a closed two-key grammar, and the * whole reason to hand-write it is that a general parser would happily accept * anchors, aliases, nested maps, multi-document streams and block scalars — * every one of which is a way to express something this format has decided not * to have. Anything outside the grammar is a rejection, never a skipped line. * * @throws {ByokCoreError} code `skill_pack_frontmatter_invalid`. */ export declare function parseSkillFrontmatter(text: string): SkillFrontmatter; /** * The entry file's own declaration must agree with the manifest's. * * Two authorities naming the same pack is not a redundancy to tolerate — a * device that installed under one name and handed the runtime a skill calling * itself another would be projecting content nobody asked for. */ export declare function checkSkillPackEntry(manifest: SkillPackManifest, entryText: string): SkillPackCheck; /** One file's bytes, as the distribution surface hands them over. */ export interface SkillPackFileContent { readonly path: string; readonly contentHash: ContentHash; readonly byteSize: number; /** UTF-8 text. A pack carries Markdown, YAML and static text assets — never binaries, never archives. */ readonly content: string; } /** What a publisher hands the store: the validated manifest plus the bytes it addresses. */ export interface SkillPackPublishInput { readonly manifest: SkillPackManifest; readonly files: readonly { readonly path: string; readonly content: string; }[]; } export interface SkillPackListQuery { readonly limit?: number; } /** * Skill pack port. Tenant-first, async, content plus manifest. * * Registered in `ports-contract.ts` as a core PORT, and deliberately not (yet) * a member of `CoreStores`: a composition contract that named it would oblige * every existing composition to implement it in the same slice, and the * Postgres implementation is Phase 2 of this plan. See `ports-contract.ts` for * the full note on that split. * * Raises: `skill_pack_manifest_invalid` (a publish whose bytes disagree with * the manifest it was handed). */ export interface SkillPackStore { /** Idempotent per (tenant, name, contentHash). Publishing a changed pack under the same name replaces it. */ publish(tenant: TenantId, input: SkillPackPublishInput): Promise; get(tenant: TenantId, name: string): Promise; list(tenant: TenantId, query: SkillPackListQuery): Promise; readFile(tenant: TenantId, name: string, path: string): Promise; }