Core API Reference
Complete API reference for @ai-dossier/core — the programmatic library for parsing, verifying, linting, and formatting dossier files.
npm install @ai-dossier/core
Parsing
parseDossierContent(content: string): ParsedDossier
Parse a dossier content string into structured data. Supports both ---dossier (JSON/YAML) and standard --- (YAML) frontmatter delimiters, and both frontmatter shapes: legacy (every Dossier field flat at the top level) and spec (Agent Skills fields name, description, license, compatibility, allowed-tools at the top level, every other field under metadata as dossier.<field> strings). frontmatter is always the flat logical object.
Parameters:
content— Raw dossier file content as a string
Returns: ParsedDossier — { frontmatter, body, raw, rawFrontmatter, shape }; rawFrontmatter is the frontmatter as it sits on disk and shape is 'legacy' or 'spec'.
Throws: Error if content is empty, not a string, or has no valid frontmatter delimiters; SpecShapeError (message prefixed Invalid spec-shaped frontmatter:) when a spec-shaped frontmatter is ambiguous — a Dossier field at the top level, dossier.<agent-skills-field>, a non-string metadata value, or a YAML merge key.
import { parseDossierContent } from '@ai-dossier/core';
const raw = `---dossier
{
"dossier_schema_version": "1.0.0",
"title": "Deploy API",
"version": "1.0.0",
"status": "Stable",
"risk_level": "medium"
}
---
## Steps
1. Run migrations
2. Deploy containers
`;
const { frontmatter, body } = parseDossierContent(raw);
console.log(frontmatter.title); // "Deploy API"
console.log(frontmatter.risk_level); // "medium"
console.log(body); // "\n## Steps\n1. Run migrations\n..."
parseDossierFile(filePath: string): ParsedDossier
Read a dossier file from disk and parse it.
Parameters:
filePath— Path to the.ds.mdfile
Throws: Error if the file does not exist.
import { parseDossierFile } from '@ai-dossier/core';
const dossier = parseDossierFile('./deploy.ds.md');
validateFrontmatter(frontmatter: DossierFrontmatter): string[]
Validate required frontmatter fields and enum values.
Required fields: dossier_schema_version, title, version
Validated enums:
status—"Draft","Stable","Deprecated","Experimental"risk_level—"low","medium","high","critical"
Returns: Array of error message strings. Empty array means valid.
import { validateFrontmatter } from '@ai-dossier/core';
const errors = validateFrontmatter(dossier.frontmatter);
if (errors.length > 0) {
errors.forEach(e => console.error(e));
// "Missing required field: dossier_schema_version"
}
Constants
| Constant | Value |
|---|---|
REQUIRED_FIELDS | ['dossier_schema_version', 'title', 'version'] |
RECOMMENDED_FIELDS | ['objective', 'risk_level', 'status'] |
VALID_STATUSES | ['Draft', 'Stable', 'Deprecated', 'Experimental'] |
VALID_RISK_LEVELS | ['low', 'medium', 'high', 'critical'] |
Checksum Verification
calculateChecksum(body: string): string
Calculate the SHA-256 hash of the dossier body (everything after the closing --- delimiter).
Returns: Hex-encoded SHA-256 hash string.
import { calculateChecksum } from '@ai-dossier/core';
const hash = calculateChecksum(dossier.body);
// "a1b2c3d4..."
verifyIntegrity(body: string, expectedHash: string | undefined): IntegrityResult
Compare the computed body hash against the expected checksum.
Returns: IntegrityResult
status | Meaning |
|---|---|
"valid" | Hash matches — content is untampered |
"invalid" | Hash mismatch — content was modified |
"missing" | No checksum in frontmatter |
import { verifyIntegrity } from '@ai-dossier/core';
const result = verifyIntegrity(body, frontmatter.checksum?.hash);
if (result.status === 'invalid') {
console.error('Tampered!', result.expectedHash, '!=', result.actualHash);
}
Signature Verification
verifySignature(content: string, signature: SignatureResult): Promise<VerifyResult>
Verify a signature using the built-in verifier registry. Automatically selects the appropriate verifier based on signature.algorithm.
Returns: Promise<VerifyResult> — { valid: boolean, error?: string }
import { buildVerificationPayload, parseDossierContent, verifySignature } from '@ai-dossier/core';
const parsed = parseDossierContent(content);
const result = await verifySignature(buildVerificationPayload(parsed), parsed.frontmatter.signature);
if (result.valid) {
console.log('Signature verified');
}
buildVerificationPayload(parsed): string
The bytes a parsed dossier’s signature must verify against. Picks the scheme from signature.covers — absent: v1 (body only); frontmatter+body: v2; spec-frontmatter+body: v3 (on-disk spec-shaped frontmatter) — and binds it to the file’s shape: spec-shaped files verify only under v3, legacy files only under v1/v2. See the scheme table in PROTOCOL.md.
Throws: Error on an unsigned dossier, an unknown covers value, or a scheme that does not match the file’s shape. Treat a throw as a failed verification.
verifyWithEd25519(content: string, signature: string, publicKey: string): VerifyResult
Verify an Ed25519 signature directly.
Parameters:
content— The content that was signedsignature— Base64-encoded signaturepublicKey— Ed25519 public key as SPKI PEM, raw 32-byte base64, or base64 SPKI DER. Whatever form is passed, the key is rebuilt from a single parse before verification, so the key material verified against is the same material a trust check matches on.
verifyWithKms(content: string, signature: string, keyId: string, region?: string): Promise<VerifyResult>
Verify an ECDSA-SHA-256 signature using AWS KMS.
Parameters:
content— The content that was signedsignature— Base64-encoded signaturekeyId— AWS KMS key ARN or aliasregion— AWS region (default:"us-east-1")
loadTrustedKeys(filePath?: string): Map<string, string>
Load trusted public keys from a file.
Default path: ~/.dossier/trusted-keys.txt
File format:
# Comments start with #
<public-key> <identifier>
One entry per line. <public-key> is canonically the raw 32-byte base64 Ed25519 key that
ai-dossier keys add writes; a multi-line SPKI PEM block written by an older CLI is
rejoined and still honoured, as are legacy minisign RWT... keys.
Returns: Map<publicKey, identifier> — each key is indexed under both the form it was
written in and its normalized raw base64 form, so an entry recorded in one encoding still
matches a signature carrying another.
Lines that cannot be read as entries are reported to stderr rather than dropped silently: a
skipped entry is a key the user believes is trusted and is not, and the only other symptom
would be verify reporting “not trusted” with nothing pointing back at the file.
findTrustedIdentifier(trustedKeys: Map<string, string>, signature: { algorithm?: string; key_id?: string; public_key?: string }): string | undefined
Resolve the identifier a signature is trusted under, or undefined when none of its key
forms appear in the trust list. Every trust decision should go through this, so “is it
trusted” and “who is it” cannot disagree.
Only key material the verifier actually used is eligible, chosen by algorithm rather than
by which fields happen to be set. ECDSA-SHA-256 (KMS) verification asks KMS to check the
signature against key_id and never reads public_key, so only the ARN can confer trust;
every other scheme verifies against public_key, so only that may. Letting an ignored field
confer trust would hand it out for free — the trust list is keyed by public key and public
keys are public, so an Ed25519 dossier could name a trusted signer’s key in key_id while
verifying under an attacker’s public_key.
Key-format and trust-file helpers
| Export | Purpose |
|---|---|
normalizePublicKey(key: string): string | Reduce a key to its canonical raw 32-byte base64 form. Returns the input trimmed when it denotes no Ed25519 key, so exact-match comparison still works on the read path. |
isSupportedPublicKey(key: string): boolean | Whether a key is one this project can verify against — raw base64, SPKI PEM, base64 SPKI DER, or a legacy minisign RWT... key. Use this on write paths (keys add), where normalizePublicKey’s pass-through would otherwise store a typo or a file path as a trusted key. |
publicKeysMatch(a: string, b: string): boolean | Compare two keys after normalization. |
parseTrustedKeys(content: string): { entries: TrustedKeyEntry[]; problems: TrustedKeyProblem[] } | Split trusted-keys file content into { publicKey, keyId } entries, plus the lines it had to skip (each with a 1-based line and a message). The single definition of the file format — anything that lists, counts, or matches trusted keys goes through it, so keys list cannot disagree with what verification trusts. |
isKmsKeyIdentifier(value: string): boolean | Whether a string is an AWS KMS key ARN, and so a usable trust-list entry for a KMS-signed dossier. Paired with isSupportedPublicKey on write paths, since a KMS ARN is not a public key. |
reportTrustedKeyProblems(problems, source?) | Print unusable entries to stderr, capped at 3, with a re-add hint. Silent when there are none. |
trustedKeysFromContent(content, source?) | Build the trust map from file content without touching the filesystem. loadTrustedKeys is this plus a file read. Pass source to name the file in warnings; omit it to stay silent. |
Types: TrustedKeyEntry { publicKey, keyId }, TrustedKeyProblem { line, message }.
Only Ed25519 keys normalize. An AWS KMS signature carries an ECDSA SPKI DER in public_key,
which isSupportedPublicKey reports as unsupported — KMS trust is matched on that exact
base64 string as written in the trust file.
Dry-run analysis
import { analyzeDryRun } from '@ai-dossier/core';
const plan = analyzeDryRun(fs.readFileSync('x.ds.md', 'utf8')); // or { frontmatter, body }
plan.risk_score; // 0-100
plan.level; // 'low' | 'medium' | 'high' | 'critical'
plan.files; plan.commands; plan.network; plan.env;
analyzeDryRun is a static preview: it reads declared frontmatter and pattern-matches shell/script code fences.
Dossiers are executed by an LLM, so the result is not a guarantee of what will happen and the score is not a
safety verdict. See the --dry-run section of the CLI README for the score formula and JSON shape.
Linting
lintDossier(content: string, config?: LintConfig): LintResult
Lint a dossier content string against built-in rules.
Built-in rules:
checksum-valid— Checksum matches body contentschema-valid— Frontmatter conforms to dossier schemarequired-sections— Mandatory sections are presentsemver-version— Version is valid semverrisk-level-consistency— Risk factors align with risk levelobjective-quality— Objective meets quality standardstools-check-command— Tool commands reference valid executables
import { lintDossier } from '@ai-dossier/core';
const result = lintDossier(content);
console.log(`${result.errorCount} errors, ${result.warningCount} warnings`);
for (const d of result.diagnostics) {
console.log(`[${d.severity}] ${d.ruleId}: ${d.message}`);
}
lintDossierFile(filePath: string, config?: LintConfig): LintResult
Lint a dossier file from disk.
Lint Configuration
Override rule severities:
import { lintDossier } from '@ai-dossier/core';
import type { LintConfig } from '@ai-dossier/core';
const config: LintConfig = {
rules: {
'checksum-valid': 'error',
'objective-quality': 'off', // disable this rule
'semver-version': 'warning',
},
};
const result = lintDossier(content, config);
Custom Rules
Implement the LintRule interface and register it:
import { LintRuleRegistry } from '@ai-dossier/core';
import type { LintRule, LintRuleContext, LintDiagnostic } from '@ai-dossier/core';
const myRule: LintRule = {
id: 'my-custom-rule',
description: 'Ensure title is lowercase',
defaultSeverity: 'warning',
run(context: LintRuleContext): LintDiagnostic[] {
if (context.frontmatter.title !== context.frontmatter.title.toLowerCase()) {
return [{
ruleId: 'my-custom-rule',
severity: 'warning',
message: 'Title should be lowercase',
field: 'title',
}];
}
return [];
},
};
const registry = new LintRuleRegistry();
registry.register(myRule);
Spec Shape (Agent Skills layout)
Read and write the spec-shaped layout: Agent Skills fields at the top level, every other Dossier field under metadata as a dossier.<field> string. The format, value encoding and strict-YAML rules are specified in Spec-Shaped Dossiers and Signature v3.
| Export | Description |
|---|---|
toSpecFrontmatter(logical) | Flat logical frontmatter → spec-shaped object. Agent Skills fields must be strings; metadata is omitted when empty. Throws SpecShapeError for a value that cannot be encoded losslessly (NaN, Infinity, a Date) |
fromSpecFrontmatter(spec) | Spec-shaped object → flat logical frontmatter, rejecting a Dossier field at the top level, dossier.<agent-skills-field>, non-string values and duplicate fields. Non-dossier. metadata keys are skipped |
encodeSpecValue(value) / decodeSpecValue(text) | The lossless value encoding: a string stays as is unless it would parse as JSON (then it is JSON-quoted); anything else is canonical JSON. Decode is JSON.parse with a raw-string fallback |
isSpecShapedFrontmatter(fm) | Type guard: metadata is a map with at least one dossier.* key |
SPEC_TOP_LEVEL_FIELDS, DOSSIER_METADATA_PREFIX | name, description, license, compatibility, allowed-tools; "dossier." |
SpecShapeError | Thrown for an ambiguous spec-shaped frontmatter or a value that cannot be written portably |
renderSpecDossier(logical, body, original?) | File content in the spec layout for a logical frontmatter and body. Pass the parsed original to keep its non-dossier. metadata keys |
serializeSpecDossier(spec, body) | File content for an already spec-shaped object: block YAML, every value quoted, DEL/C1/NEL refused, unsafe keys quoted. Reparses the result and throws SpecShapeError if it does not round-trip |
buildSpecFrontmatter(logical, original?) | toSpecFrontmatter plus the original file’s non-dossier. metadata keys. Sign this exact object: buildSignedPayload(buildSpecFrontmatter(fm, parsed), body, 'spec-frontmatter+body') |
withSpecField(rawFrontmatter, field, value) | The on-disk spec object with one dossier.<field> replaced and every other string untouched, so only that field changes what a v3 signature covers |
withSkillIdentity(logical, nameSource?) | Fills name (from a registry or file path, else the title) and description (from objective) when absent; present values are kept |
deriveSkillName(source) | Agent Skills name from a path or title: last segment, extension stripped, lowercased, other characters collapsed to -, at most 64 characters |
foreignMetadata(rawFrontmatter) | The metadata entries outside dossier.* |
SPEC_SHAPE_WRITE_UNSUPPORTED | Deprecated; nothing refuses spec-shaped input any more |
Formatting
formatDossierContent(content: string, options?: Partial<FormatOptions>): FormatResult
Format dossier content — sort frontmatter keys and update checksum. Unsigned legacy input is converted to the spec layout by default (toSpec); signed legacy input stays legacy, because converting would orphan its signature. Spec-shaped input is re-serialized from its on-disk object, keeping every value’s exact string; the body is still whitespace-normalized, and when that changes it the checksum is updated and a v3 signature no longer verifies.
Options (FormatOptions):
| Option | Type | Default | Description |
|---|---|---|---|
indent | number | 2 | JSON indentation spaces (legacy output only) |
sortKeys | boolean | true | Sort frontmatter keys into the conventional order (unknown keys alphabetically, checksum/signature last); legacy input only |
updateChecksum | boolean | true | Recalculate and update checksum |
toSpec | boolean | true | Convert unsigned legacy input to the spec layout |
nameSource | string | — | Registry or file path to derive name from when absent |
Returns: FormatResult — { formatted: string, changed: boolean }
import { formatDossierContent } from '@ai-dossier/core';
const { formatted, changed } = formatDossierContent(rawContent);
if (changed) {
console.log('Content was reformatted');
}
formatDossierFile(filePath: string, options?: Partial<FormatOptions>): FormatResult
Format a dossier file in place. Only writes to disk if content changed.
Signer & Verifier Interfaces
Extensible interfaces for signing and verification.
Signer Interface
interface Signer {
readonly algorithm: string;
sign(content: string): Promise<SignatureResult>;
getPublicKey(): Promise<string>;
}
Verifier Interface
interface Verifier {
verify(content: string, signature: SignatureResult): Promise<VerifyResult>;
supports(algorithm: string): boolean;
}
Built-in Implementations
| Class | Algorithm | Description |
|---|---|---|
Ed25519Signer | ed25519 | Sign with Ed25519 private key |
Ed25519Verifier | ed25519 | Verify Ed25519 signatures |
KmsSigner | kms-ecdsa-sha256 | Sign with AWS KMS |
KmsVerifier | kms-ecdsa-sha256 | Verify with AWS KMS |
VerifierRegistry
Algorithm-based dispatch for verification:
import { getVerifierRegistry } from '@ai-dossier/core';
const registry = getVerifierRegistry();
const verifier = registry.get('ed25519');
const result = await verifier.verify(content, signatureResult);
Types
Core Types
interface DossierFrontmatter {
dossier_schema_version?: string;
name?: string;
title: string;
version: string;
status?: 'Draft' | 'Stable' | 'Deprecated' | 'Experimental';
risk_level?: 'low' | 'medium' | 'high' | 'critical';
objective?: string;
risk_factors?: string[];
destructive_operations?: string[];
requires_approval?: boolean;
checksum?: { algorithm: string; hash: string };
signature?: { algorithm: string; signature: string; public_key?: string; key_id?: string };
[key: string]: unknown;
}
interface ParsedDossier {
frontmatter: DossierFrontmatter; // flat logical view, whichever shape the file uses
body: string;
raw: string;
rawFrontmatter: Record<string, unknown>; // on-disk frontmatter (what a v3 signature covers)
shape: 'legacy' | 'spec';
}
type DossierStatus = 'Draft' | 'Stable' | 'Deprecated' | 'Experimental';
Verification Types
interface IntegrityResult {
status: 'valid' | 'invalid' | 'missing';
message: string;
expectedHash?: string;
actualHash?: string;
}
interface AuthenticityResult {
status: 'verified' | 'signed_unknown' | 'unsigned' | 'invalid' | 'error';
message: string;
signer?: string;
keyId?: string;
isTrusted: boolean;
}
interface RiskAssessment {
riskLevel: 'low' | 'medium' | 'high' | 'critical' | 'unknown';
riskFactors: string[];
destructiveOperations: string[];
requiresApproval: boolean;
}
interface VerificationResult {
dossierFile: string;
integrity: IntegrityResult;
authenticity: AuthenticityResult;
riskAssessment: RiskAssessment;
recommendation: 'ALLOW' | 'WARN' | 'BLOCK';
message: string;
errors: string[];
}
interface TrustedKey {
publicKey: string;
keyId: string;
}
interface DossierListItem {
name: string;
path: string;
version: string;
protocol: string;
status: string;
objective: string;
riskLevel: string;
}
Signing Types
interface SignatureResult {
algorithm: string;
signature: string;
public_key: string;
key_id?: string;
signed_by?: string;
signed_at: string;
}
interface VerifyResult {
valid: boolean;
error?: string;
}
Lint Types
type LintSeverity = 'error' | 'warning' | 'info';
interface LintDiagnostic {
ruleId: string;
severity: LintSeverity;
message: string;
field?: string;
}
interface LintResult {
file?: string;
diagnostics: LintDiagnostic[];
errorCount: number;
warningCount: number;
infoCount: number;
}
interface LintRule {
id: string;
description: string;
defaultSeverity: LintSeverity;
run(context: LintRuleContext): LintDiagnostic[];
}
interface LintConfig {
rules: Record<string, LintSeverity | 'off'>;
}
Format Types
interface FormatOptions {
indent: number; // default: 2
sortKeys: boolean; // default: true
updateChecksum: boolean; // default: true
toSpec: boolean; // default: true
nameSource?: string;
}
interface FormatResult {
formatted: string;
changed: boolean;
}
Utility Exports
| Function | Description |
|---|---|
sha256Hash(content) | SHA-256 as Buffer |
sha256Hex(content) | SHA-256 as hex string |
getErrorMessage(err) | Extract error message safely |
getErrorStack(err) | Extract error stack safely |
readFileIfExists(path) | Read file or return undefined |
createDefaultVerificationResult(file) | Create a default VerificationResult |
parseAgentUsage(stdout) | Token/cost/model from a claude headless result — one --output-format json object or a stream-json event stream. modelUsage is the source of record, never blended with the top-level usage block (#524); returns AgentRunUsage | null |
parseOpenCodeUsage(stdout) | The same, for an opencode run --format json JSONL event stream |
isOpenCodeUsageStream(stdout) | Identify the distinctive OpenCode step_finish / step-finish JSONL shape |
usageParserFor(cmd0) | Pick the parser for a spawned binary (opencode basename → parseOpenCodeUsage, else parseAgentUsage) |
runsLogPath(home?) | ~/.dossier/runs.jsonl — the run log both cli and sched append to |
SCHED_DISPATCH_EVENT | The type of the scheduler’s dispatch-log preamble line; every parser above skips it |
Types: AgentRunUsage (values are nullable/optional when unavailable — never fabricated; OpenCode exposes separate reasoning, step, provider, and cost-availability fields) and RunLogEntry (one runs.jsonl line).
Rendered from docs/reference/core-api.md in the repository. Edit it there.