AI Dossier

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.

Parameters:

  • content — Raw dossier file content as a string

Returns: ParsedDossier — { frontmatter, body, raw }

Throws: Error if content is empty, not a string, or has no valid frontmatter delimiters.

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.md file

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

ConstantValue
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

statusMeaning
"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 { verifySignature } from '@ai-dossier/core';

const result = await verifySignature(body, frontmatter.signature);
if (result.valid) {
  console.log('Signature verified');
}

verifyWithEd25519(content: string, signature: string, publicKey: string): VerifyResult

Verify an Ed25519 signature directly.

Parameters:

  • content — The content that was signed
  • signature — Base64-encoded signature
  • publicKey — 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 signed
  • signature — Base64-encoded signature
  • keyId — AWS KMS key ARN or alias
  • region — 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

ExportPurpose
normalizePublicKey(key: string): stringReduce 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): booleanWhether 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): booleanCompare 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): booleanWhether 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 content
  • schema-valid — Frontmatter conforms to dossier schema
  • required-sections — Mandatory sections are present
  • semver-version — Version is valid semver
  • risk-level-consistency — Risk factors align with risk level
  • objective-quality — Objective meets quality standards
  • tools-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);

Formatting

formatDossierContent(content: string, options?: Partial<FormatOptions>): FormatResult

Format dossier content — sort frontmatter keys and update checksum.

Options (FormatOptions):

OptionTypeDefaultDescription
indentnumber2JSON indentation spaces
sortKeysbooleantrueSort frontmatter keys alphabetically
updateChecksumbooleantrueRecalculate and update checksum

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

ClassAlgorithmDescription
Ed25519Signered25519Sign with Ed25519 private key
Ed25519Verifiered25519Verify Ed25519 signatures
KmsSignerkms-ecdsa-sha256Sign with AWS KMS
KmsVerifierkms-ecdsa-sha256Verify 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;
  body: string;
  raw: string;
}

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
}

interface FormatResult {
  formatted: string;
  changed: boolean;
}

Utility Exports

FunctionDescription
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_EVENTThe 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.